React MUI菜单按TAB键自动关闭,如何实现可TAB导航的无障碍巨菜单?
MUI Menu组件默认会在按下TAB键时触发关闭逻辑,这不符合巨菜单的无障碍需求——我们需要让用户能在菜单内部通过TAB遍历所有链接,同时保留ENTER键选中链接、ESC键关闭菜单的交互。以下是具体的实现方案:
核心思路
- 拦截Menu组件的TAB键事件,阻止默认的关闭行为;
- 手动管理菜单内可聚焦元素的焦点移动,支持正向/反向TAB遍历;
- 补充无障碍细节,比如菜单打开时自动聚焦第一个可交互元素。
实现代码示例
import React, { useRef, useEffect } from 'react'; import { Menu, MenuItem, Button } from '@mui/material'; const MegaMenu = () => { const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null); const menuRef = useRef<HTMLDivElement>(null); const focusableElementsRef = useRef<HTMLElement[]>([]); const open = Boolean(anchorEl); const handleClick = (event: React.MouseEvent<HTMLElement>) => { setAnchorEl(event.currentTarget); }; const handleClose = () => { setAnchorEl(null); }; // 收集菜单内所有可聚焦元素 useEffect(() => { if (menuRef.current) { focusableElementsRef.current = Array.from( menuRef.current.querySelectorAll( 'a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])' ) ) as HTMLElement[]; } }, [open]); // 菜单打开时自动聚焦第一个可交互元素 useEffect(() => { if (open && focusableElementsRef.current.length > 0) { focusableElementsRef.current[0].focus(); } }, [open]); const handleMenuKeyDown = (event: React.KeyboardEvent) => { const { key, shiftKey } = event; const currentFocusIndex = focusableElementsRef.current.findIndex( el => el === document.activeElement ); if (key === 'Tab') { event.preventDefault(); // 阻止MUI默认的TAB关闭行为 if (shiftKey) { // SHIFT+TAB:切换到上一个可聚焦元素,循环到末尾 const targetIndex = currentFocusIndex > 0 ? currentFocusIndex - 1 : focusableElementsRef.current.length - 1; focusableElementsRef.current[targetIndex].focus(); } else { // TAB:切换到下一个可聚焦元素,循环到开头 const targetIndex = currentFocusIndex < focusableElementsRef.current.length - 1 ? currentFocusIndex + 1 : 0; focusableElementsRef.current[targetIndex].focus(); } } else if (key === 'Escape') { // 保留ESC键关闭菜单的默认交互 handleClose(); } }; return ( <div> <Button id="mega-menu-trigger" aria-controls={open ? 'full-width-mega-menu' : undefined} aria-haspopup="true" aria-expanded={open ? 'true' : undefined} onClick={handleClick} > 打开全宽巨菜单 </Button> <Menu id="full-width-mega-menu" anchorEl={anchorEl} open={open} onClose={handleClose} onKeyDown={handleMenuKeyDown} ref={menuRef} PaperProps={{ sx: { width: '100%', maxWidth: 'none', padding: '16px' } }} > <div style={{ display: 'flex', gap: '24px' }}> <div> <h3 style={{ margin: '0 0 8px', fontSize: '16px' }}>导航组1</h3> <a href="/home" tabIndex="0" style={{ display: 'block', margin: '4px 0' }}>首页</a> <a href="/about" tabIndex="0" style={{ display: 'block', margin: '4px 0' }}>关于我们</a> <a href="/contact" tabIndex="0" style={{ display: 'block', margin: '4px 0' }}>联系我们</a> </div> <div> <h3 style={{ margin: '0 0 8px', fontSize: '16px' }}>导航组2</h3> <a href="/products" tabIndex="0" style={{ display: 'block', margin: '4px 0' }}>产品列表</a> <a href="/services" tabIndex="0" style={{ display: 'block', margin: '4px 0' }}>服务介绍</a> </div> </div> </Menu> </div> ); }; export default MegaMenu;
关键细节说明
- 阻止默认TAB行为:通过
onKeyDown事件监听TAB键,调用event.preventDefault()避免菜单关闭; - 焦点管理:收集菜单内所有可聚焦元素,手动处理TAB/Shift+TAB的焦点切换,支持循环遍历;
- 无障碍优化:菜单打开时自动聚焦第一个元素,同时保留ESC键关闭菜单的标准交互;
- 全宽样式:通过
PaperProps.sx设置菜单宽度为100%,实现全宽巨菜单效果。
额外无障碍建议
- 给所有可交互元素添加清晰的
aria-label或关联aria-labelledby,方便屏幕阅读器识别; - 确保菜单触发按钮有正确的
aria属性(如示例中的aria-controls、aria-expanded); - 链接被选中时(ENTER键),完成跳转后自动关闭菜单,保持交互一致性。
内容的提问来源于stack exchange,提问作者nachtigall
相关产品推荐
相关产品推荐

