Search code examples
javascriptmaterial-uireact-ref

MUI: The `anchorEl` prop provided to the component is invalid - adding objects into empty MUI <Menu/>


I have weird problem with MUI Menu component and anchorEl.

There are two lists on the page:

  • The first list is a simple row-aligned list of <Button>Icon<Button/> components.
  • Next to it, there's a <Button startIcon=<MenuIcon/>> component that contains <Menu/>. Inside that menu are<Button>Icon<Button/> items.

When a button is clicked, it moves icon from one list to the other. If the <Menu/> has no items, <Button startIcon<MenuIcon>> doesn't appear on the page.

The issue: When the menu button is hidden, clicking a button from the first list moves icon to the <Menu/> list. However, instead of the <Button startIcon<MenuIcon>> appearing normally, a <Menu/> incorrectly shows up in the top left corner, and an error is logged in the console. The expected behavior is for the menu button to appear normally when the first icon is added to it's list without showing <Menu> yet.

Shown error:

MUI: The anchorEl prop provided to the component is invalid. The anchor element should be part of the document layout. Make sure the element is present in the document or that it's not display none.

Codesandbox with reproduced bug

index.tsx

import { React, useState } from "react";
import { Box, Button, Menu } from "@material-ui/core";
import AccessibilityIcon from "@mui/icons-material/Accessibility";
import AnchorIcon from "@mui/icons-material/Anchor";
import ApprovalIcon from "@mui/icons-material/Approval";
import MenuIcon from "@mui/icons-material/Menu";
import * as ReactDOM from "react-dom";
import { EmoticonsState } from "./App.models";
import EmoticonsContainer from "./EmoticonsContainer";

function App() {
  const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);

  const [visibleEmoticons, setVisibleEmoticons] = useState<
    EmoticonsState[] | []
  >([
    { icon: AccessibilityIcon, name: "accesibility" },
    { icon: AnchorIcon, name: "anchor" },
  ]);
  const [invisibleEmoticons, setInvisibleEmoticons] = useState<
    EmoticonsState[] | []
  >([{ icon: ApprovalIcon, name: "approval" }]);

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

  const handleClose = (event: React.MouseEvent<HTMLButtonElement>) => {
    event.stopPropagation();
    setAnchorEl(null);
  };

  const handleClickOnVisibleList = (emoticon: EmoticonsState) => {
    const filteredList = visibleEmoticons.filter(
      (e: { name: string }) => e.name !== emoticon.name
    );

    setVisibleEmoticons(filteredList);
    setInvisibleEmoticons([...invisibleEmoticons, emoticon]);
  };

  const handleClickOnInvisibleList = (emoticon: EmoticonsState) => {
    const filteredList = invisibleEmoticons.filter(
      (e: { name: string }) => e.name !== emoticon.name
    );

    setInvisibleEmoticons(filteredList);
    setVisibleEmoticons([...visibleEmoticons, emoticon]);
  };

  return (
    <Box sx={{ display: "flex" }}>
      {visibleEmoticons.length > 0 && (
        <EmoticonsContainer
          emoticons={visibleEmoticons}
          handleClickOnList={handleClickOnVisibleList}
        />
      )}

      {invisibleEmoticons.length > 0 && (
        <Box>
          <Button startIcon={<MenuIcon />} onClick={handleOpenMenu} />

          {anchorEl && (
            <Menu
              anchorEl={anchorEl}
              open={Boolean(anchorEl)}
              onClose={handleClose}
              disableRestoreFocus
            >
              <EmoticonsContainer
                emoticons={invisibleEmoticons}
                handleClickOnList={handleClickOnInvisibleList}
              />
            </Menu>
          )}
        </Box>
      )}
    </Box>
  );
}

ReactDOM.render(<App />, document.querySelector("#app"));

EmoticonsContainer.tsx

import { Button } from "@material-ui/core";
import { React, memo } from "react";
import { EmoticonsState } from "./App.models";

interface Props {
  emoticons: EmoticonsState[];
  handleClickOnList: (emoticon: EmoticonsState) => void;
}

function EmoticonsContainer(props: Props) {
  const { emoticons, handleClickOnList } = props;

  return (
    <>
      {emoticons.map((emoticon, key) => (
        <Button
          startIcon={<emoticon.icon />}
          key={key}
          onClick={() => handleClickOnList(emoticon)}
        >
          key
        </Button>
      ))}
    </>
  );
}

export default memo(EmoticonsContainer);

App.models.ts

import { OverridableComponent } from "@material-ui/core/OverridableComponent";
import { SvgIconTypeMap } from "@material-ui/core";

export type IconType = OverridableComponent<SvgIconTypeMap<{}, "svg">> & {
  muiName: string;
};

export interface EmoticonsState {
  icon: IconType;
  name: string;
}

Solution

  • The anchorEl was not reseted when the menu got hidden.

    const handleClickOnInvisibleList = (emoticon: EmoticonsState) => {
      const filteredList = invisibleEmoticons.filter(
        (e: { name: string }) => e.name !== emoticon.name
      );
    
      setInvisibleEmoticons(filteredList);
      setVisibleEmoticons([...visibleEmoticons, emoticon]);
      
     if (filteredList.length <= 0) setAnchorEl(null);
    };