使用“TAB”键导航 React MUI 菜单会关闭菜单

问题描述 投票:0回答:1

我已经基于 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
键进行导航。

reactjs material-ui accessibility
1个回答
0
投票

最后我不得不使用自己的实现来为各种 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
最新问题
© www.soinside.com 2019 - 2025. All rights reserved.