我已经基于 How to build a full-width mega-menu using Material-UI?
构建了一个 MegaMenu出于可访问性的原因,应该可以通过选项卡浏览菜单中的链接,并使用
ENTER
键选择其中一个链接。
但是,只要按下
TAB
键,MUI 菜单就会自动关闭。
这是我的菜单的最小版本:https://codesandbox.io/p/sandbox/dazzling-farrell-9sv3fy?file=%2Fsrc%2FMegaMenu.tsx
令我惊讶的是,按
TAB
键自动关闭菜单似乎是 MUI 菜单的默认行为。检查 https://mui.com/material-ui/react-menu/#basic-menu 上的默认示例,该示例也在 tab
上关闭。
所以我想知道创建可访问的 MegaMenu 的最佳方法是什么?也就是说,可以使用
Tab
键进行导航。
最后我不得不使用自己的实现来为各种 keyDown 事件添加键盘处理程序:
// MegaMenu.tsx implementation
import React, {
KeyboardEvent,
MouseEvent,
useEffect,
useRef,
useState,
} from "react";
import {
Box,
IconButton,
ListItem,
ListItemText,
Menu,
MenuItem,
MenuList,
Typography,
} from "@mui/material";
import MenuIcon from "@mui/icons-material/Menu";
import MuiLink from "@mui/material/Link";
import { useTheme } from "@mui/material/styles";
/**
* The menu as used for bigger (desktop) viewports
*/
const MegaMenu = ({ burgermenu }: OurNavigationData): JSX.Element => {
const theme = useTheme();
const [anchorElUser, setAnchorElUser] = useState<null | HTMLElement>(null);
const [selectedMenuIndex, setSelectedMenuIndex] = React.useState(0);
const [selectedLinkIndex, setSelectedLinkIndex] = React.useState(0);
const menuId = "our-mega-megamenu";
const menuItemClass = "our-mega-js-megamenu__menu-item";
const menuItemLinkClass = "our-mega-js-megamenu__menu-item-link";
const handleCloseMenu = (): void => {
setAnchorElUser(null);
setSelectedMenuIndex(0);
setSelectedLinkIndex(0);
};
const handleOpenMenu = (event: MouseEvent<HTMLElement>): void => {
setAnchorElUser(event.currentTarget);
const navArray = getNavArray();
const firstLink = navArray[0][0];
setTimeout(() => {
firstLink.focus();
}, 100);
};
const megaMenuRef = useRef<HTMLDivElement>(null);
const handleKeyDown = (event: KeyboardEvent<HTMLElement>): void => {
const key = event.key;
const targetKeys = [
"Home",
"End",
"left",
"ArrowLeft",
"Right",
"ArrowRight",
"Up",
"ArrowUp",
"Down",
"ArrowDown",
];
const navArray = getNavArray();
if (navArray && navArray.length > 0 && targetKeys.includes(key)) {
const firstMenuFirstLink = navArray[0][0];
const lastMenuIndex = navArray.length - 1;
const lastMenuFirstLink = navArray[lastMenuIndex][0];
// left arrow logic
const prevMenuIndex =
selectedMenuIndex > 0 ? selectedMenuIndex - 1 : navArray.length - 1;
const prevMenuLink = navArray[prevMenuIndex][0];
// right arrow logic
const nextMenuIndex =
selectedMenuIndex < navArray.length - 1 ? selectedMenuIndex + 1 : 0;
const nextMenuLink = navArray[nextMenuIndex][0];
// up arrow logic
const prevLinkIndex =
selectedLinkIndex > 0
? selectedLinkIndex - 1
: navArray[selectedMenuIndex].length - 1;
const prevLink = navArray[selectedMenuIndex][prevLinkIndex];
// down arrow logic
const nextLinkIndex =
selectedLinkIndex < navArray[selectedMenuIndex].length - 1
? selectedLinkIndex + 1
: 0;
const nextLink = navArray[selectedMenuIndex][nextLinkIndex];
switch (key) {
case "Home":
event.stopPropagation();
firstMenuFirstLink.focus();
setSelectedMenuIndex(0);
setSelectedLinkIndex(0);
break;
case "End":
event.stopPropagation();
lastMenuFirstLink.focus();
setSelectedMenuIndex(lastMenuIndex);
setSelectedLinkIndex(0);
break;
case "ArrowLeft":
case "Left":
event.stopPropagation();
prevMenuLink.focus();
setSelectedMenuIndex(prevMenuIndex);
setSelectedLinkIndex(0);
break;
case "ArrowRight":
case "Right":
event.stopPropagation();
nextMenuLink.focus();
setSelectedMenuIndex(nextMenuIndex);
setSelectedLinkIndex(0);
break;
case "ArrowUp":
case "Up":
event.stopPropagation();
prevLink.focus();
setSelectedLinkIndex(prevLinkIndex);
break;
case "ArrowDown":
case "Down":
event.stopPropagation();
nextLink.focus();
setSelectedLinkIndex(nextLinkIndex);
break;
}
}
};
const getNavArray = (): NodeListOf<HTMLAnchorElement>[] => {
const menuItems = megaMenuRef.current?.querySelectorAll(
`.${menuItemClass}`,
) as NodeListOf<HTMLLIElement> | undefined;
const menuItemsAndLinksArray = [] as NodeListOf<HTMLAnchorElement>[];
menuItems?.forEach((item) => {
const menuItemLinks: NodeListOf<HTMLAnchorElement> =
item.querySelectorAll(`.${menuItemLinkClass}`);
if (menuItemLinks.length > 0) {
menuItemsAndLinksArray.push(menuItemLinks);
}
});
return menuItemsAndLinksArray;
};
useEffect(() => {
window.addEventListener("resize", handleCloseMenu);
});
return (
<Box
sx={{
flexGrow: 0,
display: "inline-flex",
padding: {
xs: theme.spacing(0, 8, 0, 8),
lg: theme.spacing(1.5, 8, 1.5, 8),
},
}}
>
<IconButton
onClick={handleOpenMenu}
title={Boolean(anchorElUser) ? "Close menu" : "Open menu"}
aria-haspopup="true"
aria-expanded={Boolean(anchorElUser)}
aria-controls={menuId}
>
<MenuIcon
sx={{
transform: "scale(1.5)",
color: theme.palette.secondary.contrastText,
}}
/>
<Typography
component={"span"}
sx={{
borderLeft: "1.25rem solid transparent",
borderRight: "1.25rem solid transparent",
borderBottom: `2.0rem solid ${theme.palette.background.menu}`,
visibility: Boolean(anchorElUser) ? "visible" : "hidden",
opacity: Boolean(anchorElUser) ? "1" : "0",
position: "absolute",
top: "2.3rem",
transition: "opacity 864ms",
}}
></Typography>
</IconButton>
<Menu
anchorEl={anchorElUser}
keepMounted
MenuListProps={{
style: {
display: "flex",
alignItems: "flex-start",
},
}}
onClose={handleCloseMenu}
open={Boolean(anchorElUser)}
sx={{
position: "absolute",
}}
slotProps={{
paper: {
sx: {
backgroundColor: theme.palette.background.menu,
marginLeft: "0",
marginTop: {
xs: "0.6rem",
lg: "1.1rem",
},
maxWidth: "100%",
padding: {
xs: theme.spacing(8, 4),
lg: theme.spacing(8, 36),
},
width: "100%",
},
},
}}
role="menubar"
id={menuId}
ref={megaMenuRef}
>
{burgermenu &&
burgermenu.map((burgermenu) => (
<MenuItem
key={`megamenu-item${burgermenu.id}`}
className={menuItemClass}
onKeyDown={handleKeyDown}
>
<MenuList key={`burgermenu${burgermenu.id}`}>
<ListItem
sx={{
padding: theme.spacing(0, 8, 0, 0),
}}
role="none"
tabIndex={-1}
>
<ListItemText
id={`megamenu-menu-title-${burgermenu.id}`}
primaryTypographyProps={{
style: {
fontSize: "125%",
fontWeight: 700,
},
}}
>
{burgermenu.headline}
</ListItemText>
</ListItem>
{burgermenu.links.map((link) => (
<MenuItem
key={`link${link.id}`}
onClick={handleCloseMenu}
sx={{
display: "block",
padding: "unset",
fontSize: "85%",
}}
role="none"
tabIndex={-1}
>
<MuiLink
href={link.href}
sx={{
display: "block",
padding: theme.spacing(1.5, 8, 1.5, 0),
}}
target={link.target}
title={link.title}
className={menuItemLinkClass}
role="menuitem"
aria-label={`${burgermenu.headline} ${link.title}`}
>
{link.text}
</MuiLink>
</MenuItem>
))}
</MenuList>
</MenuItem>
))}
</Menu>
</Box>
);
};
export default MegaMenu;
```tsx