Can I include <Hidden> menu items in a Material UI <Menu> component?

Viewed 3086

I have a menu in my AppBar and I'd like to have to have certain items show up as buttons on the AppBar when there's space and as menu items in an existing menu when there isn't. That is, the menu is always present and always has items in it. I'd just like some functions to be AppBar buttons or menu items depending on the screen size.

Here's a simplified example:

<Menu
  id="settings-menu"
  anchorEl={anchorEl}
  open={Boolean(anchorEl)}
  onClose={handleClose}
>
  <Hidden lgUp>
    <MenuItem onClick={logout}>
      <ListItemIcon><ExitToAppIcon /></ListItemIcon>
      <ListItemText primary="Hidden log out" />
    </MenuItem>
  </Hidden>
  <MenuItem onClick={logout}>
    <ListItemIcon><ExitToAppIcon /></ListItemIcon>
    <ListItemText primary="Log out" />
  </MenuItem>
</Menu>

When I open the menu, I get an error message in the console:

Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

Check the render method of `ForwardRef(Menu)`.
    in Hidden (at NavBar.js:76)
    in ul (created by ForwardRef(List))
    in ForwardRef(List) (created by WithStyles(ForwardRef(List)))
    in WithStyles(ForwardRef(List)) (created by ForwardRef(MenuList))
    in ForwardRef(MenuList) (created by ForwardRef(Menu))
    in div (created by ForwardRef(Paper))
    in ForwardRef(Paper) (created by WithStyles(ForwardRef(Paper)))
    in WithStyles(ForwardRef(Paper)) (created by Transition)
    in Transition (created by ForwardRef(Grow))
    in ForwardRef(Grow) (created by TrapFocus)
    in TrapFocus (created by ForwardRef(Modal))
    in div (created by ForwardRef(Modal))
    in ForwardRef(Portal) (created by ForwardRef(Modal))
    in ForwardRef(Modal) (created by ForwardRef(Popover))
    in ForwardRef(Popover) (created by WithStyles(ForwardRef(Popover)))
    in WithStyles(ForwardRef(Popover)) (created by ForwardRef(Menu))
    in ForwardRef(Menu) (created by WithStyles(ForwardRef(Menu)))
    in WithStyles(ForwardRef(Menu)) (at NavBar.js:70)
    in div (at NavBar.js:50)
    in div (created by ForwardRef(Toolbar))
    in ForwardRef(Toolbar) (created by WithStyles(ForwardRef(Toolbar)))
    in WithStyles(ForwardRef(Toolbar)) (at NavBar.js:48)
    in header (created by ForwardRef(Paper))
    in ForwardRef(Paper) (created by WithStyles(ForwardRef(Paper)))
    in WithStyles(ForwardRef(Paper)) (created by ForwardRef(AppBar))
    in ForwardRef(AppBar) (created by WithStyles(ForwardRef(AppBar)))
    in WithStyles(ForwardRef(AppBar)) (at NavBar.js:47)
    in Header (at Layout.js:32)
    in div (at Layout.js:31)
    in div (at Layout.js:29)
    in Layout (at App.js:18)
    in Unknown (at src/index.js:39)
    in Router (at src/index.js:38)
    in ThemeProvider (at src/index.js:37)
    in Provider (at src/index.js:36)

Are <Hidden> components allowed within a <Menu>? If not, what's a good way to allow menu items to appear conditionally based on the screen size?

1 Answers

As the error indicates Hidden doesn't support receiving a ref and that is required for menu items.

You can, however, achieve the same thing using Box:

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 Box from "@material-ui/core/Box";

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

  const handleClick = event => {
    setAnchorEl(event.currentTarget);
  };

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

  return (
    <div>
      <Button
        aria-controls="simple-menu"
        aria-haspopup="true"
        onClick={handleClick}
      >
        Open Menu
      </Button>
      <Menu
        id="simple-menu"
        anchorEl={anchorEl}
        keepMounted
        open={Boolean(anchorEl)}
        onClose={handleClose}
      >
        <MenuItem onClick={handleClose}>Always Displayed</MenuItem>
        <Box clone display={{ sm: "none" }}>
          <MenuItem onClick={handleClose}>Profile</MenuItem>
        </Box>
        <Box clone display={{ lg: "none" }}>
          <MenuItem onClick={handleClose}>My account</MenuItem>
        </Box>
        <Box clone display={{ md: "none" }}>
          <MenuItem onClick={handleClose}>Logout</MenuItem>
        </Box>
      </Menu>
    </div>
  );
}

Edit Hidden menu items

Unfortunately, using Box can be brittle in a case like this where it is overriding a style set by the wrapped component, since import order affects which one wins (see further discussion here: Box vs className vs style for vertical spacing in Material UI).

Another option is using withStyles to create versions of MenuItem that hide at a particular breakpoint:

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 { withStyles } from "@material-ui/core/styles";

const MenuItemHiddenLgUp = withStyles(theme => ({
  root: {
    [theme.breakpoints.up("lg")]: {
      display: "none"
    }
  }
}))(MenuItem);

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

  const handleClick = event => {
    setAnchorEl(event.currentTarget);
  };

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

  return (
    <div>
      <Button
        aria-controls="simple-menu"
        aria-haspopup="true"
        onClick={handleClick}
      >
        Open Menu
      </Button>
      <Menu
        id="simple-menu"
        anchorEl={anchorEl}
        keepMounted
        open={Boolean(anchorEl)}
        onClose={handleClose}
      >
        <MenuItem onClick={handleClose}>Always Displayed</MenuItem>
        <MenuItemHiddenLgUp onClick={handleClose}>Profile</MenuItemHiddenLgUp>
        <MenuItemHiddenLgUp onClick={handleClose}>
          My account
        </MenuItemHiddenLgUp>
        <MenuItem onClick={handleClose}>Logout</MenuItem>
      </Menu>
    </div>
  );
}

Edit Hidden menu items

Documentation:

Related