Jordan Lee
Jordan Lee

Reputation: 233

How to Make Material-UI Menu based on Hover, not Click

I am using Material-UI Menu. It should work as it was, but just using mouse hover, not click. Here is my code link: https://codesandbox.io/embed/vn3p5j40m0

Below is the code of what I tried. It opens correctly, but doesn't close when the mouse moves away.

import React from "react";
import Button from "@material-ui/core/Button";
import Menu from "@material-ui/core/Menu";
import MenuItem from "@material-ui/core/MenuItem";

function SimpleMenu() {
  const [anchorEl, setAnchorEl] = React.useState(null);

  function handleClick(event) {
    setAnchorEl(event.currentTarget);
  }

  function handleClose() {
    setAnchorEl(null);
  }

  return (
    <div>
      <Button
        aria-owns={anchorEl ? "simple-menu" : undefined}
        aria-haspopup="true"
        onClick={handleClick}
        onMouseEnter={handleClick}
      >
        Open Menu
      </Button>
      <Menu
        id="simple-menu"
        anchorEl={anchorEl}
        open={Boolean(anchorEl)}
        onClose={handleClose}
        onMouseLeave={handleClose}
      >
        <MenuItem onClick={handleClose}>Profile</MenuItem>
        <MenuItem onClick={handleClose}>My account</MenuItem>
        <MenuItem onClick={handleClose}>Logout</MenuItem>
      </Menu>
    </div>
  );
}

export default SimpleMenu;

Upvotes: 22

Views: 49728

Answers (10)

Just be sure to setTimeOut delay to at least 500 in case there is a gap between button and menu, otherwise it will keep closing menu before user can move over.

const handleCloseHover = ()=> {
  currentlyHovering = false;
  setTimeout(() => {
      if (!currentlyHovering) {
        handleClose();
      }
    }, 700);
}

Upvotes: 0

JAUGRY
JAUGRY

Reputation: 27

I found Jules Dupont's answer to a related question useful. The advantage of his solution is, that the menu closes upon moving the mouse out of the button. I adapted his approach to the current syntax.

export default function App(props) {
  const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
  const [open,setOpen] = useState<boolean>(false);
  const [mouseOverButton, setMouseOverButton] = useState<boolean>(false);
  const [mouseOverMenu, setMouseOverMenu] = useState<boolean>(false);

  const handleAdminClose = () => {
    setMouseOverButton(false)
    setMouseOverMenu(false)
  }
  const enterButton = (event: React.MouseEvent<HTMLElement>) => {
    setMouseOverButton(true)
    setAnchorEl(event.currentTarget)
  }

  const leaveButton = () => {
    setTimeout(() => {
     setMouseOverButton(false);
    }, 1000);
 }

  const enterMenu = () => {
    setMouseOverMenu(true);
  }

  const leaveMenu = () => {
     setTimeout(() => {
      setMouseOverMenu(false);
     }, 100);
  }
useEffect(() => setOpen(mouseOverButton || mouseOverMenu),[mouseOverButton,mouseOverMenu])


  return (
    <>
              <List>
                <ListItem disablePadding>
                  <ListItemButton>
                    <>
                    <ListItemText
                      primary='Button with menu on hover'
                      aria-controls={open ? 'basic-menu' : undefined}
                      aria-haspopup="true"
                      aria-expanded={open ? 'true' : undefined}
                      onMouseOver={enterButton}
                      onMouseLeave={leaveButton}
                    />
                    <Menu
                      id="basic-menu"
                      anchorEl={anchorEl}
                      open={open}
                      onClose={handleAdminClose}
                      MenuListProps={{
                        onMouseOver: enterMenu,
                        onMouseLeave: leaveMenu,
                        'aria-labelledby': 'basic-button',
                      }}
                    >
                      <MenuItem onClick={handleAdminClose}>Item1</MenuItem>
                      <MenuItem onClick={handleAdminClose}>Item2</MenuItem>
                      <MenuItem onClick={handleAdminClose}>Item3</MenuItem>
                    </Menu>
                    </>
                  </ListItemButton>

                  <ListItemButton>
                    <ListItemText primary='Another button' />
                  </ListItemButton>

                </ListItem>
              </List>
    </>
  );
}

Upvotes: 0

MkSM56852
MkSM56852

Reputation: 3

I think you should use the HoverMenu component provided by the "material-ui-popup-state" package. you can find an example here: https://jcoreio.github.io/material-ui-popup-state/ https://github.com/jcoreio/material-ui-popup-state

Upvotes: 0

peterhpchen
peterhpchen

Reputation: 96

As GaddMaster says, the answer of Ryan Cogswell has a flaw:

if we position the popup below the button, scroll over the button to trigger the popup, then scroll left/right, the popup wont close

This is because the Menu inherited from Popover which inherited from Modal. The z-index of Modal is 1300. onMouseLeave can be triggered only when the z-index of Button is bigger than Modal.

export default function App() {
  // ...

  return (
    <>
      <Button
        sx={{ zIndex: (theme) => theme.zIndex.modal + 1 }}
        onClick={handleOpen}
        onMouseEnter={handleOpen}
        onMouseLeave={handleClose}
      >
        Hover Me
      </Button>
      <Menu
        anchorEl={anchorEl}
        open={open}
        onClose={handleMenuClose}
        MenuListProps={{
          onMouseLeave: handleMenuClose,
          onMouseEnter: handleMenuEnter
        }}
      >
        <MenuItem>Hi</MenuItem>
        <MenuItem>Hello</MenuItem>
        <MenuItem>Bye</MenuItem>
      </Menu>
    </>
  );
}

And because we have double(Button and Menu) mouse enter and leave event to handle menu open or close, it needs to use setTimeout to prevent to trigger close event after enter to the other element.

  const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);

  const open = Boolean(anchorEl);

  const handleOpen = (event: React.MouseEvent<HTMLButtonElement>) => {
    setAnchorEl(event.currentTarget);
  };

  let timeoutId: NodeJS.Timeout | null = null;

  const handleClose = () => {
    if (!!timeoutId) {
      clearTimeout(timeoutId);
    }
    timeoutId = setTimeout(() => {
      setAnchorEl(null);
    }, 0);
  };

  const handleMenuClose = () => {
    setAnchorEl(null);
  };

  const handleMenuEnter = () => {
    if (!!timeoutId) {
      clearTimeout(timeoutId);
    }
  };

Edit objective-sound-qhwv5l

Upvotes: 4

Roozbeh
Roozbeh

Reputation: 732

As p8ul mentioned, BEST trick to get this done is via tooltip. Here is Mui Version 5 that I implemented

// components/AppBarMenu.tsx


import styled from '@emotion/styled';
import Tooltip, { TooltipProps, tooltipClasses } from '@mui/material/Tooltip';

const AppBarMenu = styled(({ className, ...props }: TooltipProps) => (
    <Tooltip {...props} classes={{ popper: className }} arrow placement="bottom-start" />
))(({ theme }: any) => ({
    [`& .${tooltipClasses.tooltip}`]: {
        backgroundColor: theme.palette.primary.light,
        fontSize: theme.typography.pxToRem(12),
        borderRadius: 12
    }
}));

export default AppBarMenu;

Usage

.
.
.
    const [productMenuOpen, setProductMenuOpen] = useState(false);
.
.
.


                            <AppBarMenu
                                open={productMenuOpen}
                                onOpen={() => setProductMenuOpen(true)}
                                onClose={() => setProductMenuOpen(false)}
                                title={
                                    <React.Fragment>
                                        <MenuItem component={RouteLink} to="/" onClick={() => setProductMenuOpen(false)}>
                                            <ListItemIcon>
                                                <Icon fontSize="small" />
                                            </ListItemIcon>
                                            Product
                                        </MenuItem>
                                        <MenuItem onClick={() => setProductMenuOpen(false)}>
                                            <ListItemIcon>
                                                <Icon fontSize="small" />
                                            </ListItemIcon>
                                            Product
                                        </MenuItem>
                                        <MenuItem onClick={() => setProductMenuOpen(false)}>
                                            <ListItemIcon>
                                                <AppRegistrationIcon fontSize="small" />
                                            </ListItemIcon>
                                            Product
                                        </MenuItem>
                                        <MenuItem onClick={() => setProductMenuOpen(false)}>
                                            <ListItemIcon>
                                                <TableChartIcon fontSize="small" />
                                            </ListItemIcon>
                                            Product
                                        </MenuItem>
                                        <Divider />
                                        <MenuItem disabled>
                                            <Typography variant="body2">About</Typography>
                                        </MenuItem>
                                        <MenuItem component={RouteLink} to="product/pricing" onClick={() => setProductMenuOpen(false)}>
                                            Pricing
                                        </MenuItem>
                                        <MenuItem onClick={() => setProductMenuOpen(false)}>Features</MenuItem>
                                        <MenuItem onClick={() => setProductMenuOpen(false)}>Data Coverage</MenuItem>
                                    </React.Fragment>
                                }
                            >
                                <MenuItemButton
                                    onClick={scrollToTop}
                                    sx={{ my: 2, color: 'white', display: 'block' }}
                                    aria-controls={productMenuOpen ? 'product-menu' : undefined}
                                    aria-haspopup="true"
                                    aria-expanded={productMenuOpen ? 'true' : undefined}
                                >
                                    Products
                                </MenuItemButton>
                            </AppBarMenu>

Upvotes: 2

Md Wahiduzzaman Emon
Md Wahiduzzaman Emon

Reputation: 1213

use **MenuListProps** in the Menu component and use your menu **closeFunction** -

MenuListProps={{ onMouseLeave: handleClose }}

example- 
 <Menu
      dense
      id="demo-positioned-menu"
      anchorEl={anchorEl}
      open={open}
      onClose={handleCloseMain}
      title={item?.title}
      anchorOrigin={{
            vertical: "bottom",
            horizontal: "right",
                    }}
      transformOrigin={{
            vertical: "top",
            horizontal: "center",
                    }}
      MenuListProps={{ onMouseLeave: handleClose }}
 />

I hope it will work perfectly.

Upvotes: 0

Winters
Winters

Reputation: 71

I gave up using Menu component because it implemented Popover. To solve the overlay problem I had to write too much code. So I tried to use the old CSS way:

CSS: relative parent element + absolute menu element

Component: Paper + MenuList

<ListItem>
  <Link href="#" >
    {user.name}
  </Link>
  <AccountPopover elevation={4}>
     <MenuList>
        <MenuItem>Profile</MenuItem>
        <MenuItem>Logout</MenuItem>
     </MenuList>
  </AccountPopover>
</ListItem>

styled components:

export const ListItem = styled(Stack)(() => ({
  position: 'relative',
  "&:hover .MuiPaper-root": {
    display: 'block'
  }
}))


export const AccountPopover = styled(Paper)(() => ({
  position: 'absolute',
  zIndex:2,
  right: 0,
  top: 30,
  width: 170,
  display: 'none'
}))


Upvotes: 0

braza
braza

Reputation: 4662

I've updated Ryan's original answer to fix the issue where it doesn't close when you move the mouse off the element to the side.

How it works is to disable the pointerEvents on the MUI backdrop so you can continue to detect the hover behind it (and re-enables it again inside the menu container). This means we can add a leave event listener to the button as well.

It then keeps track of if you've hovered over either the button or menu using currentlyHovering.

When you hover over the button it shows the menu, then when you leave it starts a 50ms timeout to close it, but if we hover over the button or menu again in that time it will reset currentlyHovering and keep it open.

I've also added these lines so the menu opens below the button:

getContentAnchorEl={null}
anchorOrigin={{ horizontal: "left", vertical: "bottom" }}
import React from "react";
import Button from "@material-ui/core/Button";
import Menu from "@material-ui/core/Menu";
import MenuItem from "@material-ui/core/MenuItem";
import makeStyles from "@material-ui/styles/makeStyles";

const useStyles = makeStyles({
  popOverRoot: {
    pointerEvents: "none"
  }
});

function SimpleMenu() {
  let currentlyHovering = false;
  const styles = useStyles();

  const [anchorEl, setAnchorEl] = React.useState(null);

  function handleClick(event) {
    if (anchorEl !== event.currentTarget) {
      setAnchorEl(event.currentTarget);
    }
  }

  function handleHover() {
    currentlyHovering = true;
  }

  function handleClose() {
    setAnchorEl(null);
  }

  function handleCloseHover() {
    currentlyHovering = false;
    setTimeout(() => {
      if (!currentlyHovering) {
        handleClose();
      }
    }, 50);
  }

  return (
    <div>
      <Button
        aria-owns={anchorEl ? "simple-menu" : undefined}
        aria-haspopup="true"
        onClick={handleClick}
        onMouseOver={handleClick}
        onMouseLeave={handleCloseHover}
      >
        Open Menu
      </Button>
      <Menu
        id="simple-menu"
        anchorEl={anchorEl}
        open={Boolean(anchorEl)}
        onClose={handleClose}
        MenuListProps={{
          onMouseEnter: handleHover,
          onMouseLeave: handleCloseHover,
          style: { pointerEvents: "auto" }
        }}
        getContentAnchorEl={null}
        anchorOrigin={{ horizontal: "left", vertical: "bottom" }}
        PopoverClasses={{
          root: styles.popOverRoot
        }}
      >
        <MenuItem onClick={handleClose}>Profile</MenuItem>
        <MenuItem onClick={handleClose}>My account</MenuItem>
        <MenuItem onClick={handleClose}>Logout</MenuItem>
      </Menu>
    </div>
  );
}

export default SimpleMenu;

Edit Material demo

Upvotes: 11

p8ul
p8ul

Reputation: 2320

Using an interactive HTML tooltip with menu items works perfectly, without requiring you to necessarily click to view menu items.

Here is an example for material UI v.4.

enter image description here

import React from 'react';
import { withStyles, Theme, makeStyles } from '@material-ui/core/styles';
import Tooltip from '@material-ui/core/Tooltip';
import { MenuItem, IconButton } from '@material-ui/core';
import MoreVertIcon from '@material-ui/icons/MoreVert';
import styles from 'assets/jss/material-dashboard-pro-react/components/tasksStyle.js';

// @ts-ignore
const useStyles = makeStyles(styles);

const LightTooltip = withStyles((theme: Theme) => ({
  tooltip: {
    backgroundColor: theme.palette.common.white,
    color: 'rgba(0, 0, 0, 0.87)',
    boxShadow: theme.shadows[1],
    fontSize: 11,
    padding: 0,
    margin: 4,
  },
}))(Tooltip);

interface IProps {
  menus: {
    action: () => void;
    name: string;
  }[];
}
const HoverDropdown: React.FC<IProps> = ({ menus }) => {
  const classes = useStyles();
  const [showTooltip, setShowTooltip] = useState(false);
  return (
    <div>
      <LightTooltip
        interactive
        open={showTooltip}
        onOpen={() => setShowTooltip(true)}
        onClose={() => setShowTooltip(false)}
        title={
          <React.Fragment>
            {menus.map((item) => {
              return <MenuItem onClick={item.action}>{item.name}</MenuItem>;
            })}
          </React.Fragment>
        }
      >
        <IconButton
          aria-label='more'
          aria-controls='long-menu'
          aria-haspopup='true'
          className={classes.tableActionButton}
        >
          <MoreVertIcon />
        </IconButton>
      </LightTooltip>
    </div>
  );
};

export default HoverDropdown;

Usage:

<HoverDropdown
        menus={[
          {
            name: 'Item 1',
            action: () => {
              history.push(
                codeGeneratorRoutes.getEditLink(row.values['node._id'])
              );
            },
          },{
            name: 'Item 2',
            action: () => {
              history.push(
                codeGeneratorRoutes.getEditLink(row.values['node._id'])
              );
            },
          },{
            name: 'Item 3',
            action: () => {
              history.push(
                codeGeneratorRoutes.getEditLink(row.values['node._id'])
              );
            },
          },{
            name: 'Item 4',
            action: () => {
              history.push(
                codeGeneratorRoutes.getEditLink(row.values['node._id'])
              );
            },
          },
        ]}
      />

Upvotes: 4

Ryan Cogswell
Ryan Cogswell

Reputation: 80966

The code below seems to work reasonably. The main changes compared to your sandbox are to use onMouseOver={handleClick} instead of onMouseEnter on the button. Without this change, it doesn't open reliably if the mouse isn't over where part of the menu will be. The other change is to use MenuListProps={{ onMouseLeave: handleClose }}. Using onMouseLeave directly on Menu doesn't work because the Menu includes an overlay as part of the Menu leveraging Modal and the mouse never "leaves" the overlay. MenuList is the portion of Menu that displays the menu items.

import React from "react";
import Button from "@material-ui/core/Button";
import Menu from "@material-ui/core/Menu";
import MenuItem from "@material-ui/core/MenuItem";

function SimpleMenu() {
  const [anchorEl, setAnchorEl] = React.useState(null);

  function handleClick(event) {
    if (anchorEl !== event.currentTarget) {
      setAnchorEl(event.currentTarget);
    }
  }

  function handleClose() {
    setAnchorEl(null);
  }

  return (
    <div>
      <Button
        aria-owns={anchorEl ? "simple-menu" : undefined}
        aria-haspopup="true"
        onClick={handleClick}
        onMouseOver={handleClick}
      >
        Open Menu
      </Button>
      <Menu
        id="simple-menu"
        anchorEl={anchorEl}
        open={Boolean(anchorEl)}
        onClose={handleClose}
        MenuListProps={{ onMouseLeave: handleClose }}
      >
        <MenuItem onClick={handleClose}>Profile</MenuItem>
        <MenuItem onClick={handleClose}>My account</MenuItem>
        <MenuItem onClick={handleClose}>Logout</MenuItem>
      </Menu>
    </div>
  );
}

export default SimpleMenu;

Edit Material demo

Upvotes: 52

Related Questions