Nested React topmenu not closing with mouse click

Viewed 49

I developed the following code inspired by several attempts on the net to create a nested top menu in React. The code is behaving as expecting except for when selecting the last item in the recursive menu, it only closes one level of the menu and leaves the rest of the menu still showing. Pressing outside the menu several times would eventually close all menus. Is there a way to close all the submenus once the user selects the last menu item in the tree?

How the current menu looks like

// menuItems.js
export const menuItems = [
  {
    title: "Home",
  },
  {
    title: "Services",
    submenu: [
      {
        title: "web design",
      },
      {
        title: "web development",
        submenu: [
          {
            title: "Frontend",
          },
          {
            title: "Backend",
            submenu: [
              {
                title: "NodeJS",
              },
              {
                title: "PHP",
              },
            ],
          },
        ],
      },
      {
        title: "SEO",
      },
    ],
  },
  {
    title: "About",
    submenu: [
      {
        title: "Who we are",
      },
      {
        title: "Our values",
      },
    ],
  },
];
// App.js
import { Box } from "@mui/material";
import * as React from "react";
import { useState } from "react";
import MainMenu from "./MainMenu";

function App() {
  const [selectedMenuItem, setselectedMenuItem] = useState("");
  return (
    <>
      <MainMenu setselectedMenuItem={setselectedMenuItem} />
      <Box sx={{ bgcolor: "yellow", mt: "300px" }}>
        <p>
          Item Selected from Menu is{" "}
          <span style={{ color: "blue", fontSize: 32, fontWeight: "bold" }}>
            {selectedMenuItem}
          </span>
        </p>
      </Box>
    </>
  );
}

export default App;
// MainMenu.js

import { Box } from "@mui/material";
import * as React from "react";
import MenuDropDown from "./MenuDropDown";
import { menuItems } from "./menuItems"; // Holds the structure of the menu

export default function MainMenu({ setselectedMenuItem }) {
  const depthLevel = 0;

  return (
    <Box sx={{ display: "flex", flexDirection: "row"}}>
      {menuItems.map((menu, index) => {
        return (
          <MenuDropDown
            key={index}
            menuDropDown={menuItems[index]}
            setselectedMenuItem={setselectedMenuItem}
            depthLevel = {depthLevel}
          />
        );
      })}
    </Box>
  );
}
// Holds one top level menu option as a button and its underneath menu items
// MenuDropDown.js
import React from "react";
import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import ListItemText from "@mui/material/ListItemText";
import ListItemIcon from "@mui/material/ListItemIcon";
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
import ArrowRightIcon from "@mui/icons-material/ArrowRight";

const MenuDropDown = ({ menuDropDown, setselectedMenuItem, depthLevel }) => {
  const [anchorEl, setAnchorEl] = React.useState(null);
  const open = Boolean(anchorEl);

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

  const handleClose = (event) => {
    setselectedMenuItem(event.currentTarget.innerText);
    setAnchorEl(null);
  };

  let currentDepthLevel = depthLevel;

  return (
    <div>
      {/* use an immediately-invoked function expression (IIFE). */}
      {(() => {
        // Create an arrow function and call it. That was the only way
        if ("submenu" in menuDropDown) {
          currentDepthLevel = depthLevel + 1;
          return (
            <div>
              {/* Add an arrow down to indicate submenu presence */}
              <MenuItem sx={{ mr: 0, pr: 0 }} onClick={handleClick}>
                <ListItemText>{menuDropDown.title}</ListItemText>
                <ListItemIcon>
                  {currentDepthLevel === 1 ? (
                    <ArrowDropDownIcon />
                  ) : (
                    <ArrowRightIcon />
                  )}
                </ListItemIcon>
              </MenuItem>

              {/* if there is a submenu array, then display it */}
              <Menu
                id="submenu"
                anchorEl={anchorEl}
                open={open}
                onClose={handleClose}
                anchorOrigin={
                  currentDepthLevel === 1
                    ? {
                        vertical: "bottom",
                        horizontal: "left",
                      }
                    : {
                        vertical: "bottom",
                        horizontal: "center",
                      }
                }
              >
                {menuDropDown.submenu.map((submenu, index) => {
                  if ("submenu" in submenu) {
                    return (
                      <MenuDropDown
                        key={index}
                        menuDropDown={submenu}
                        setselectedMenuItem={setselectedMenuItem}
                        depthLevel={currentDepthLevel}
                      />
                    );
                  }
                  return (
                    <MenuItem key={index} onClick={handleClose}>
                      {submenu.title}
                    </MenuItem>
                  );
                })}
              </Menu>
            </div>
          );
        } else {
          return (
            <MenuItem onClick={handleClick}>{menuDropDown.title}</MenuItem>
          );
        }
      })()}
    </div>
  );
};

export default MenuDropDown;
// Package.json
{
  "name": "mongo-form",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@emotion/react": "^11.10.4",
    "@emotion/styled": "^11.10.4",
    "@testing-library/jest-dom": "^5.16.5",
    "@testing-library/react": "^13.3.0",
    "@testing-library/user-event": "^13.5.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-router-dom": "^6.3.0",
    "react-scripts": "5.0.1",
    "web-vitals": "^2.1.4"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

1 Answers

In the way you did the closing, the only way I could think of closing all at once, was to store all setState of each menu, and a function to set all of them to null.

In the MainMenu I created a state to store all the MenuDropDown's setState and the function to loop the list and set all to null.

And pass the closeAll function and the state and setState functions to every call of MenuDropDown:

https://codesandbox.io/s/multilevel-react-menu-forked-wrtmfp

Related