React递归菜单:实现点击关闭其他子菜单及外部点击关闭功能
Material-UI递归菜单交互优化需求
已基于Material-UI在React中实现了支持子菜单的递归菜单,现需补充两个交互功能:
- 点击带子菜单的菜单项时,无论其他子菜单当前状态如何,关闭所有其他子菜单,仅展开当前点击的菜单
- 点击递归菜单(包括主菜单和所有子菜单)外部区域时,关闭所有已打开的子菜单及主菜单
原实现代码
import React, { useState } from 'react'; import Menu from '@mui/material/Menu'; import MenuItem from '@mui/material/MenuItem'; import IconButton from '@mui/material/IconButton'; import MenuIcon from '@mui/icons-material/Menu'; import Checkbox from '@mui/material/Checkbox'; const RecursiveMenu = ({ menuItems }) => { const [anchorEl, setAnchorEl] = useState(null); const handleClick = (event) => { setAnchorEl(event.currentTarget); }; const handleClose = () => { setAnchorEl(null); }; return ( <div> <IconButton aria-controls="simple-menu" aria-haspopup="true" onClick={handleClick} > <MenuIcon /> </IconButton> <Menu id="simple-menu" anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleClose} > {Object.values(menuItems) .filter((menuItem) => menuItem.showing !== false) .map((menuItem) => ( <RecursiveMenuItem key={menuItem.id} menuItem={menuItem} /> ))} </Menu> </div> ); }; const RecursiveMenuItem = ({ menuItem }) => { const [subMenuAnchorEl, setSubMenuAnchorEl] = useState(null); const [showingState, setShowingState] = useState(menuItem.showing); const handleClick = (event) => { if (menuItem.options) { setSubMenuAnchorEl(event.currentTarget); } else { // Toggle between 0, 1, and 2 for checked, indeterminate, and unchecked setShowingState((prevState) => (prevState + 1) % 3); } }; const handleClose = () => { setSubMenuAnchorEl(null); }; return ( <> <MenuItem style={{ display: 'flex', justifyContent: 'space-between' }} onClick={handleClick}> <span>{menuItem.name}</span> {!menuItem.options && ( <Checkbox checked={showingState === 0} indeterminate={showingState === 1} onChange={handleClick} inputProps={{ 'aria-label': 'indeterminate-checkbox' }} /> )} </MenuItem> {menuItem.options && ( <Menu anchorEl={subMenuAnchorEl} open={Boolean(subMenuAnchorEl)} onClose={handleClose} > {Object.values(menuItem.options) .filter((subMenuItem) => subMenuItem.showing !== false) .map((subMenuItem) => ( <RecursiveMenuItem key={subMenuItem.id} menuItem={subMenuItem} /> ))} </Menu> )} </> ); }; const App = () => { const menuItems = { A: { id: "A", name: "A", enabled: false, showing: 0 }, B: { id: "B", name: "B", enabled: false, showing: 2 }, C: { id: "c", name: "C", options: { C1: { id: "C1", name: "C1", enabled: false, showing: 1 }, C2: { id: "C2", name: "C2", options: { C21: { id: "C21", name: "C2.1", enabled: false, showing: 0 }, C22: { id: "C22", name: "C2.2", enabled: false, showing: 2 }, } }, }, }, Shaun: { id: "D", name: "D", enabled: false, showing: 2 }, }; return <RecursiveMenu menuItems={menuItems} />; }; export default App;
解决方案
核心思路是将所有子菜单的展开状态提升到顶层组件统一管理,这样才能实现全局的状态控制:
1. 顶层菜单状态改造
在RecursiveMenu中新增状态记录当前展开的子菜单ID,以及控制所有菜单关闭的函数:
openSubMenus:用对象记录每个子菜单的展开状态(键为菜单ID,值为对应的anchor元素)handleSubMenuOpen:点击子菜单时,先清空其他所有子菜单的状态,再设置当前菜单为展开handleSubMenuClose:关闭指定ID的子菜单handleCloseAll:关闭主菜单及所有子菜单
2. 递归菜单项接收控制函数
将顶层的状态控制函数传递给RecursiveMenuItem,替换原有的局部状态管理逻辑,让顶层统一控制所有子菜单的展开/关闭。
3. 外部点击处理
利用Material-UI Menu组件的onClose事件,当点击外部时触发handleCloseAll,关闭所有菜单。
修改后的完整代码
import React, { useState } from 'react'; import Menu from '@mui/material/Menu'; import MenuItem from '@mui/material/MenuItem'; import IconButton from '@mui/material/IconButton'; import MenuIcon from '@mui/icons-material/Menu'; import Checkbox from '@mui/material/Checkbox'; const RecursiveMenu = ({ menuItems }) => { const [anchorEl, setAnchorEl] = useState(null); // 统一管理所有子菜单的展开状态:key为菜单ID,value为对应的anchor元素 const [openSubMenus, setOpenSubMenus] = useState({}); const handleClick = (event) => { setAnchorEl(event.currentTarget); }; // 关闭主菜单及所有子菜单 const handleCloseAll = () => { setAnchorEl(null); setOpenSubMenus({}); }; // 打开指定ID的子菜单,同时关闭其他所有子菜单 const handleSubMenuOpen = (menuId, anchorEl) => { setOpenSubMenus({ [menuId]: anchorEl, }); }; // 关闭指定ID的子菜单 const handleSubMenuClose = (menuId) => { setOpenSubMenus(prev => { const newState = {...prev}; delete newState[menuId]; return newState; }); }; return ( <div> <IconButton aria-controls="simple-menu" aria-haspopup="true" onClick={handleClick} > <MenuIcon /> </IconButton> <Menu id="simple-menu" anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleCloseAll} // 点击主菜单外部时关闭所有 > {Object.values(menuItems) .filter((menuItem) => menuItem.showing !== false) .map((menuItem) => ( <RecursiveMenuItem key={menuItem.id} menuItem={menuItem} openSubMenus={openSubMenus} onSubMenuOpen={handleSubMenuOpen} onSubMenuClose={handleSubMenuClose} onCloseAll={handleCloseAll} /> ))} </Menu> </div> ); }; const RecursiveMenuItem = ({ menuItem, openSubMenus, onSubMenuOpen, onSubMenuClose, onCloseAll }) => { const [showingState, setShowingState] = useState(menuItem.showing); const handleClick = (event) => { if (menuItem.options) { // 点击带子菜单的项:关闭其他所有子菜单,展开当前 onSubMenuOpen(menuItem.id, event.currentTarget); } else { // 切换复选框状态逻辑保持不变 setShowingState((prevState) => (prevState + 1) % 3); } }; return ( <> <MenuItem style={{ display: 'flex', justifyContent: 'space-between' }} onClick={handleClick}> <span>{menuItem.name}</span> {!menuItem.options && ( <Checkbox checked={showingState === 0} indeterminate={showingState === 1} onChange={handleClick} inputProps={{ 'aria-label': 'indeterminate-checkbox' }} /> )} </MenuItem> {menuItem.options && ( <Menu anchorEl={openSubMenus[menuItem.id]} open={Boolean(openSubMenus[menuItem.id])} onClose={() => onSubMenuClose(menuItem.id)} // 点击子菜单外部时关闭所有菜单 onClickOutside={onCloseAll} > {Object.values(menuItem.options) .filter((subMenuItem) => subMenuItem.showing !== false) .map((subMenuItem) => ( <RecursiveMenuItem key={subMenuItem.id} menuItem={subMenuItem} openSubMenus={openSubMenus} onSubMenuOpen={onSubMenuOpen} onSubMenuClose={onSubMenuClose} onCloseAll={onCloseAll} /> ))} </Menu> )} </> ); }; const App = () => { const menuItems = { A: { id: "A", name: "A", enabled: false, showing: 0 }, B: { id: "B", name: "B", enabled: false, showing: 2 }, C: { id: "c", name: "C", options: { C1: { id: "C1", name: "C1", enabled: false, showing: 1 }, C2: { id: "C2", name: "C2", options: { C21: { id: "C21", name: "C2.1", enabled: false, showing: 0 }, C22: { id: "C22", name: "C2.2", enabled: false, showing: 2 }, } }, }, }, Shaun: { id: "D", name: "D", enabled: false, showing: 2 }, }; return <RecursiveMenu menuItems={menuItems} />; }; export default App;
关键修改说明
- 状态提升:所有子菜单的展开状态由顶层
RecursiveMenu的openSubMenus统一管理,避免了局部状态无法同步的问题 - 排他展开:点击子菜单时,
handleSubMenuOpen会重置openSubMenus只保留当前菜单的状态,实现关闭其他子菜单的效果 - 外部点击处理:主菜单的
onClose和子菜单的onClickOutside都绑定handleCloseAll,点击外部时直接清空所有菜单状态
内容的提问来源于stack exchange,提问作者OmriHalifa
相关产品推荐
相关产品推荐

