Making Material UIs Drawer component work with onMouseOver

Making Material UIs Drawer component work with onMouseOver

As standard the Drawer component uses List and ListItem components to make a menu. And the default mode is opening the submenu by clicking on the ListItem.

But I wanted to make the submenus open when I hover the mouse over them.

Red is anchorEl (mainmenu item) Yellow is menu (submenu) ref1.png

My starter code:

return (
    <div>
      <Drawer
        variant="permanent"
        className="menu-list"
       >
        <List
          component="nav"
          aria-labelledby="nested-list-subheader"
          subheader={
            <div className={classes.subHeader}>
              <img src={OMSLogo} className={classes.logo} alt="site logo"/>
            </div>
          }
          className={classes.root}
        >
          {modules.map((module, k) => (
            <ManagerMenuModule module={module} key={k} classes={{ nested: classes.nested }} />
          ))}
        </List>
      </Drawer>
    </div>
  );

And the ManagerMenuModule:

  const [smallMenuOpen, setSmallMenuOpen] = React.useState(false);
  const [anchorEl, setAnchorEl] = React.useState<any>(null);

return (
        <div id="simple-menu">
          <Popper
            open={smallMenuOpen}
            anchorEl={anchorEl}
            placement={"right-start"}
            transition
            onMouseLeave={(e) => handleSmallMenuClose()}
            className={classes.popper}
          >
            {({ TransitionProps }) => (
              <ClickAwayListener onClickAway={handleSmallMenuClose}>
                <Zoom {...TransitionProps}>
                  <Paper elevation={3}>{smallSubMenu(props.module.module_name, true)}</Paper>
                </Zoom>
              </ClickAwayListener>
            )}
          </Popper>
          <Tooltip placement="right" arrow title={props.module.module_name} enterDelay={1000}>
          <ListItem className={listItemClasses()} button key={props.module.module_id} onClick={handleSmallMenuOpen}
                      onMouseOver={handleSingleModuleSmallMenu}
                      onMouseLeave={(e) => handleClose(e)}

                      >
            <ListItemIcon className={listItemIconClasses()}>{renderIcon(props.module.icon)}</ListItemIcon>
          </ListItem>
          </Tooltip>
        </div >
      );
 const handleSingleModuleSmallMenu = (event) => {
    setAnchorEl(event.currentTarget);
    setSmallMenuOpen(true);
  };
  const handleClose = (e) => {
    if (e.currentTarget.localName !== "ul") {
      const menu = document.getElementById("simple-menu");
      const menuBoundary = {
        left: (anchorEl?.offsetLeft  ?? 0) + (anchorEl?.offsetWidth ?? 0),
        top: anchorEl?.offsetTop ?? 0,
        right: ((anchorEl?.offsetLeft  ?? 0) + (anchorEl?.offsetWidth ?? 0)) + (menu?.offsetWidth  ?? 0),
        bottom: (menu?.offsetHeight ?? 0) + (anchorEl.offsetTop ?? 0)
      };
      // console.log("coords", menuBoundary.left, menuBoundary.top, menuBoundary.right, menuBoundary.bottom, e.clientX, e.clientY)
      if (
        e.clientX >= menuBoundary.left &&
        e.clientX <= menuBoundary.right &&
        e.clientY <= menuBoundary.bottom &&
        e.clientY >= menuBoundary.top
      ) {
        return;
      }
    }

    setSmallMenuOpen(false);
  }

So just by setting onMouseOver={handleSingleModuleSmallMenu} on the ListItem the menu item shows, but if you dont tell it when to close you end up with all menu items open.

If you just do onMouseLeave={() => setSmallMenuOpen(false)} you cant move the mouse to the submenu, because it closes the menu as soon as your mouse leaves the main menu icon/text.

So we make the handleClose method to decide when to close the submenu.

First we save the main menu item, so we know where on the page we are. setAnchorEl(event.currentTarget)

Next we have given the

surrounding each ListItem an id of "simple-menu", we use that to select the submenu element with its coordinates. const menu = document.getElementById("simple-menu");

We then define the divs outer limits, so we know when we move the mouse outside of these bounds - we can close the menu.

const menuBoundary = {
        left: anchorEl.offsetLeft + anchorEl.offsetWidth,
        top: anchorEl.offsetTop,
        right: anchorEl.offsetLeft + anchorEl.offsetWidth + menu.offsetWidth,
        bottom: menu.offsetHeight + anchorEl.offsetTop
      };

ref2.png

*Left is: where does anchorEl start + anchorEl width. Marked pink on picture.

Right is: where does anchorEl start + anchorEl width + submenu width. Marked red+green on image.

Top is: where does anchorEl start. Marked red on image.

Bottom is: where does anchorEl start + submenu height. Marked blue on image.*

The submenu shows up on the right side of the mainmenu. So we use the anchorEl ref to have a startingpoint. Everything is in relative position to this ref.

Last we just have to check if the coordinates of the onMouseLeave event is inside the submenu or not. If it is inside submenu, we dont close.

This only handles the onMouseLeave on the ListItem, which is the mainmenu.

But we also have to listen for onMouseLeave on the shown submenu, this is being handled by setting onMouseLeave={() => handleSmallMenuClose()} on the parent element, which in this case is the Popper .