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;
    }
}
9
  • 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? Commented Sep 15, 2020 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? Commented Sep 16, 2020 at 3:20
  • that sandbox - doesn't ancor link the menu to the right button Commented Sep 16, 2020 at 7:13
  • please find my answer here I hope it will helpful. Commented Sep 16, 2020 at 8:13
  • that didn't solve the problem Commented Sep 16, 2020 at 15:48

1 Answer 1

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

Sign up to request clarification or add additional context in comments.

11 Comments

I still get the error - "Failed prop type: Material-UI: The anchorEl prop provided to the component is invalid. It should be an HTML element instance or a referenceObject "
Will fix it too
updated Codesandbox. now the error is not shown on click
no errors appear - but it also doesn't show the menu
its like my menu is being cropped - css issue?
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.