2

I am building a reactjs application -- and I am trying to make this popup component modular -so that I could have the button look like a badge/icon combination -- activate the popup menu on hover instead of clicks.

here is a sandbox -- but I need to create popover menus for each -- at the moment its displacing the buttons. https://codesandbox.io/s/material-demo-forked-wrn2g?file=/demo.js enter image description here enter image description here

Here is the component as it is currently http://jsfiddle.net/4benm6wo/


import React from 'react';
import Button from '@material-ui/core/Button';
import ClickAwayListener from '@material-ui/core/ClickAwayListener';
import Grow from '@material-ui/core/Grow';
import Paper from '@material-ui/core/Paper';
import Popper from '@material-ui/core/Popper';
import MenuItem from '@material-ui/core/MenuItem';
import MenuList from '@material-ui/core/MenuList';

import Badge from '@material-ui/core/Badge';
import PersonIcon from '@material-ui/icons/Person';

import './PopOverMenu.scss';


export default function MenuListComposition() {
  const [open, setOpen] = React.useState(false);
  const anchorRef = React.useRef(null);

  const handleToggle = () => {
    setOpen((prevOpen) => !prevOpen);
  };

  const handleClose = (event) => {
    if (anchorRef.current && anchorRef.current.contains(event.target)) {
      return;
    }

    setOpen(false);
  };

  function handleListKeyDown(event) {
    if (event.key === 'Tab') {
      event.preventDefault();
      setOpen(false);
    }
  }

  // return focus to the button when we transitioned from !open -> open
  const prevOpen = React.useRef(open);
  React.useEffect(() => {
    if (prevOpen.current === true && open === false) {
      anchorRef.current.focus();
    }

    prevOpen.current = open;
  }, [open]);

  return (
    <div className="popover-menu">
      <div>
        <Button
          ref={anchorRef}
          aria-controls={open ? 'menu-list-grow' : undefined}
          aria-haspopup="true"
          onClick={handleToggle}
        >
          <Badge badgeContent={11} color="primary">
            <PersonIcon />
          </Badge>
        </Button>

        <Popper open={open} anchorEl={anchorRef.current} role={undefined} transition disablePortal>
          {({ TransitionProps, placement }) => (
            <Grow
              {...TransitionProps}
              style={{ transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom' }}
            >
              <Paper>
                <ClickAwayListener onClickAway={handleClose}>
                  <MenuList autoFocusItem={open} id="menu-list-grow" onKeyDown={handleListKeyDown}>
                    <MenuItem onClick={handleClose}>Profile</MenuItem>
                    <MenuItem onClick={handleClose}>My account</MenuItem>
                    <MenuItem onClick={handleClose}>Logout</MenuItem>
                  </MenuList>
                </ClickAwayListener>
              </Paper>
            </Grow>
          )}
        </Popper>
      </div>
    </div>
  );
}

but I want to create a popperMenu component -- where I push the icon, badge count into the poppup -- so I need help implement it with props and states.

this is my current attempt http://jsfiddle.net/4benm6wo/1/

class MenuListComposition extends Component {
  constructor(props, context) {
    super(props, context);
    this.state = { open: false };
  }

  render() {
    return (
      <div className="popover-menu">
        <div>
          <Button
            ref={anchorRef}
            aria-controls={open ? 'menu-list-grow' : undefined}
            aria-haspopup="true"
            onClick={handleToggle}
          >
            <Badge badgeContent={11} color="primary">
              <PersonIcon />
            </Badge>
          </Button>

          <Popper open={open} anchorEl={anchorRef.current} role={undefined} transition disablePortal>
            {({ TransitionProps, placement }) => (
              <Grow
                {...TransitionProps}
                style={{ transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom' }}
              >
                <Paper>
                  <ClickAwayListener onClickAway={handleClose}>
                    <MenuList autoFocusItem={open} id="menu-list-grow" onKeyDown={handleListKeyDown}>
                      <MenuItem onClick={handleClose}>Profile</MenuItem>
                      <MenuItem onClick={handleClose}>My account</MenuItem>
                      <MenuItem onClick={handleClose}>Logout</MenuItem>
                    </MenuList>
                  </ClickAwayListener>
                </Paper>
              </Grow>
            )}
          </Popper>
        </div>
      </div>
    )
  }
}

export default MenuListComposition;

so I would create a button/badge popup kind of like this from the shell

<MenuListComposition badgeCount={10} icon={<PersonIcon />} menu={[{ "label": "Profile", "link": "user/1" }, { "label": "Logout", "link": "logout" }]} />

latest code


LoginButton.js http://jsfiddle.net/z3L89x2e/

import React, { Component } from 'react'
import { withRouter } from 'react-router-dom';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';

import Button from '@material-ui/core/Button';
import PopOverMenu from '../_SharedGlobalComponents/PopOverMenu/PopOverMenu';
import MailIcon from '@material-ui/icons/Mail';
import NotificationsIcon from '@material-ui/icons/Notifications';
import PersonIcon from '@material-ui/icons/Person';

class LoggedInButtons extends Component {
  
  /*
  constructor(props, context) {
    super(props, context);
  }
  */

  render() {

    return (
      <div className="login-badges">
        <PopOverMenu
          icon={<NotificationsIcon />}
          badgecount="3"
          menu={[
              { "label": "xxx", "value": "/" },
              { "label": "xxxx", "value": "/" },
              { "label": "xxxxx", "value": "/" },
          ]}
        />

        <PopOverMenu
          icon={<MailIcon />}
          badgecount="5"
          menu={[
              { "label": "xxx", "value": "/" },
              { "label": "xxxx", "value": "/" },
              { "label": "xxxxx", "value": "/" },
          ]}
        />

        <PopOverMenu
          icon={<PersonIcon />}
          badgecount="2"
          menu={[
              { "label": "Profile", "value": "/profile" },
              { "label": "My account", "value": "/my-account" },
              { "label": "Logout", "value": "/logout" },
          ]}
        />

        <Button 
           variant="text"
           color="default"
           startIcon={<PersonIcon />}
           href="/user/view/2"
        >
          User 2
        </Button>


        <Button variant="contained" color="secondary" href="/logout">
          log out
        </Button>


      </div>
    )
  }
}

function mapStateToProps(state) {
  return {
  };
}

function mapDispatchToProps(dispatch) {
 return bindActionCreators({  }, dispatch);
}

export default withRouter(connect(mapStateToProps, mapDispatchToProps)(LoggedInButtons))

PopOverMenu.js http://jsfiddle.net/z3L89x2e/2/

import React, { Component } from 'react';
import Button from '@material-ui/core/Button';
import ClickAwayListener from '@material-ui/core/ClickAwayListener';
import Grow from '@material-ui/core/Grow';
import Paper from '@material-ui/core/Paper';
import Popper from '@material-ui/core/Popper';
import MenuItem from '@material-ui/core/MenuItem';
import MenuList from '@material-ui/core/MenuList';
import Badge from '@material-ui/core/Badge';

//import './PopOverMenu.scss';

class PopOverMenu extends Component {

    constructor(props, context) {
        super(props, context);
        this.state = { open: false };
        this.anchorRef = React.createRef(null);
    }


    handleToggle = () => {
        this.setState({open: !this.state.open});
    };

    handleClose = () => {        
        this.setState({open: false});
    };

    handleListKeyDown = (event) => {
        if (event.key === 'Tab') {
            event.preventDefault();
            this.setState({open: false});
        }
    }

showMenuItems = () => (
    this.props.menu.map((item, i) => (
        <MenuItem onClick={this.handleClose}>{item.label}</MenuItem>
    ))
)

render() {
    return (
        <div style={{display:'inline', float:'left', marginLeft:'20px', marginTop:'10px'}} className="popover-menu">
                <Button
                    ref={this.anchorRef}
                    aria-controls={this.state.open ? 'menu-list-grow' : null}
                    aria-haspopup="true"
                    onClick={this.handleToggle}
                >
                    <Badge badgeContent={this.props.badgecount} color="primary">
                        {this.props.icon}
                    </Badge>
                </Button>

                <Popper style={{position: 'relative'}} open={this.state.open} role={undefined} transition disablePortal>
                    {({ TransitionProps, placement }) => (
                        <Grow
                            {...TransitionProps}
                            style={{ transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom' }}
                        >
                            <Paper>
                                <ClickAwayListener onClickAway={this.handleClose}>
                                    <MenuList autoFocusItem={this.state.open} id="menu-list-grow" onKeyDown={this.handleListKeyDown}>
                                        {this.showMenuItems()}
                                    </MenuList>
                                </ClickAwayListener>
                            </Paper>
                        </Grow>
                    )}
                </Popper>
        </div>
    );
}

}

export default PopOverMenu;

this is the latest attempt - it nearly worked - but then I got errors in the ancor el

I am using material ui and the icon/badge modules. I started to get an error on hovering over the menu.

I've tried to follow something like this - of putting the ancor el into the state - but its not working.

How can I convert popover MATERIAL-UI functional component to class based component?

import React, { Component } from 'react';
import { NavLink } from 'react-router-dom';
import Button from '@material-ui/core/Button';
import ClickAwayListener from '@material-ui/core/ClickAwayListener';
import Grow from '@material-ui/core/Grow';
import Paper from '@material-ui/core/Paper';
import Popper from '@material-ui/core/Popper';
import MenuItem from '@material-ui/core/MenuItem';
import MenuList from '@material-ui/core/MenuList';
import Badge from '@material-ui/core/Badge';

import './PopOverMenu.scss';

class PopOverMenu extends Component {

    constructor(props, context) {
        super(props, context);

//        this.state = { open: false};
//        this.anchorRef = React.createRef(null);

        this.state = { anchorEl: null, open: false };
    }

    handleToggle = (event) => {
        this.setState({open: !this.state.open});
//        this.state.ancherEl ? this.setState({ anchorEl: null }) : this.setState({ anchorEl: event.currentTarget });
    };

    handleOpen = (event) => {        
        this.setState({open: true});

        console.log("event.currentTarget", event.currentTarget);

        this.state.ancherEl ? this.setState({ anchorEl: null }) : this.setState({ anchorEl: event.currentTarget });
    };

    handleClose = () => {        
        this.setState({open: false});
        //this.setState({ anchorEl: null })
    };

    handleListKeyDown = (event) => {
        if (event.key === 'Tab') {
            event.preventDefault();
            this.setState({open: false});
        }
    }

    showMenuItems = () => (
        this.props.menu.map((item, i) => (
            <MenuItem key={i} onClick={this.handleClose}>
                <NavLink to={item.value}>{item.label}</NavLink>
            </MenuItem>
        ))
    )

render() {

    //console.log("this.anchorRef", this.anchorRef)

    return (
        <div className="popover-menu">
                <Button
                    //ref={this.anchorRef}
                    aria-controls={this.state.open ? 'menu-list-grow' : null}
                    aria-haspopup="true"
                    onClick={this.handleToggle}
                    onMouseOver={this.handleOpen}
                    onMouseLeave={this.handleClose}
                >
                    <Badge badgeContent={this.props.badgecount} color="primary">
                        {this.props.icon}
                    </Badge>
                </Button>

                <Popper 
                    className="popper-list" 
                    //anchorEl={this.anchorRef}
                    open={this.state.open} 
                    anchorEl={this.state.anchorEl}
                    //role={undefined} 
                    transition 
                    disablePortal
                    onMouseOver={this.handleOpen}
                    onMouseLeave={this.handleClose}
                >
                    {({ TransitionProps, placement }) => (
                        <Grow
                            {...TransitionProps}
                            style={{ transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom' }}
                        >
                            <Paper>
                                <ClickAwayListener onClickAway={this.handleClose}>
                                    <MenuList autoFocusItem={this.state.open} id="menu-list-grow" onKeyDown={this.handleListKeyDown}>
                                        {/*this.showMenuItems()*/}
                                    </MenuList>
                                </ClickAwayListener>
                            </Paper>
                        </Grow>
                    )}
                </Popper>
        </div>
    );
}

}

export default PopOverMenu;

.popover-menu{
    width: 40px;
    display: inline;
    float: left;
    //border: 1px solid red;
    //background: blue;
    padding: 5px;

    .popper-list{
        width: 160px; 
        position: relative!important;
        //border: 1px solid blue;
    }
}
The Old County
  • 89
  • 13
  • 59
  • 129
  • do I need to render the buttons separately -- and when a button is clicked - generate 1 popup to take that menu and ancor it? but this will still displace the buttons no? – The Old County Sep 15 '20 at 13:21
  • I took a look at your CodeSandBox. It does not really show the displacement issue. Are you able to edit it so it shows the issues you're encountering? – 95faf8e76605e973 Sep 16 '20 at 03:20
  • that sandbox - doesn't ancor link the menu to the right button – The Old County Sep 16 '20 at 07:13
  • please find my answer [here](https://stackoverflow.com/questions/63766916/how-do-i-implement-a-dropdown-header-in-material-ui-in-react/63769452#63769452) I hope it will helpful. – Prasad Phule Sep 16 '20 at 08:13
  • that didn't solve the problem – The Old County Sep 16 '20 at 15:48
  • Can you please update question with only latest code (single file) if possible provide sandbox link, so I will check that code only and try to resolved. I am confusing with multiple code files. – Prasad Phule Sep 16 '20 at 17:19
  • that is the latest code -- – The Old County Sep 16 '20 at 17:39
  • I think you are close to your objective, the only reason I'm seeing for the content to shift while the Popper is open is because of the `style={{position: 'relative'}} `. Remove that position override and it probably won't shift the buttons anymore. – Chris Sep 17 '20 at 07:31
  • you want to create a reactjs sandbox for it -- I don't see why the framework would require special styling - – The Old County Sep 17 '20 at 10:10

1 Answers1

1

Here is a working CodeSandbox

  1. You are using the wrong ref.
    You need to assign the ref to the Popper on click (see line 89).
    To be able to do this, you need to set the ref using setState and not createRef.

  2. for hover activation: You need to toggle it using onMouseEnter and onMouseLeave on parent div

  3. Add onClick on button to toggle the popper

Itamar
  • 1,601
  • 1
  • 10
  • 23