首页  >  文章  >  web前端  >  使用 React Hooks 构建可访问的导航菜单栏

使用 React Hooks 构建可访问的导航菜单栏

WBOY
WBOY原创
2024-08-08 13:25:29964浏览

Building an Accessible Navigation Menubar with React Hooks

不小心发布了!请稍后回来了解更多!

介绍

创建可访问的 Web 应用程序不仅是一种好的做法,而且现在是一种必要。最近,我有机会构建一个专注于 a11y 的导航菜单栏。当我进行研究时,我意识到大多数菜单栏都不符合 ARIA 模式。例如,您是否知道应该使用箭头键导航菜单栏并管理其自己的焦点,而不是通过选项卡浏览菜单项?

虽然我确实找到了一些教程,但我最终没有完全遵循它们。我写这篇文章是因为我认为我最终构建的内容值得分享 - 如果您也对小型组件和自定义挂钩有兴趣。

虽然我将通过一些开发步骤来构建这个博客,但我的目标不是编写分步指南。我相信您了解 React 基础知识以及自定义钩子的工作原理。

我现在只分享关键的实现细节,但我计划将来当我有更多时间时用代码沙箱示例更新本文。

我们正在建设什么?

对于这个博客,我们正在构建一个导航菜单栏,就像您在许多网络应用程序的顶部或侧面看到的那样。在此菜单栏中,某些菜单项可能有子菜单,这些子菜单将在鼠标进入/离开时打开/关闭。

HTML 标记

首先,语义 HTML 和适当的角色以及 ARIA 属性对于可访问性至关重要。对于菜单栏模式,您可以在此处阅读官方文档的更多内容。

以下是适当 HTML 标记的示例:

<nav aria-label="Accessible Menubar">
  <menu role="menubar">
    <li role="none">
      <a role="menuitem" href="/">Home</a>
    </li>
    <li role="none">
      <a role="menuitem" href="/about">About</a>
    </li>
    <li role="none">
      <button 
        role="menuitem" 
        aria-haspopup="true"
        aria-expanded="false"
      >
        Expand Me!
      </button>
      <menu role="menu">
        <li role="none">
          <a role="menuitem" href="/sub-item-1">Sub Menu Item 1</a>
        </li>
        <li role="none">
          <a role="menuitem" href="/sub-item-2">Sub Menu Item 2</a>
        </li>
      </menu>
    </li>
  </menu>
</nav>

请注意,我们正在使用语义 HTML 的按钮标签。该按钮还应该有 aria-haspopup 来提醒屏幕阅读器。最后,应根据菜单状态分配适当的 aria-expanded 属性。

成分

让我们看看我们需要的组件。显然,我们需要一个整体菜单组件,以及一个菜单项组件。

有些菜单项有子菜单,有些则没有。带有子菜单的菜单项需要管理其状态,以便在悬停和键盘事件时打开/关闭子菜单。所以它需要有自己的组件。

子菜单也需要是它自己的组件。尽管子菜单也只是菜单项的容器,但它们不管理其状态或处理键盘事件。这将它们与顶级导航菜单区分开来。

我最终编写了这些组件:

  • NavMenu 用于菜单栏的最外层。
  • MenuItem 用于单个菜单项。
    • 菜单项链接
    • 菜单项与子菜单
  • SubMenu 用于展开的子菜单。 MenuItem 可以递归嵌套在子菜单中。

焦点管理

简单来说,“焦点管理”只是意味着组件需要知道哪个子组件拥有焦点。因此,当用户的焦点离开并返回时,先前聚焦的子级将重新聚焦。

焦点管理的常用技术是“Roving Tab Index”,其中组中焦点元素的 Tab 索引为 0,其他元素的 Tab 索引为 -1。这样,当用户返回焦点组时,选项卡索引为 0 的元素将自动获得焦点。

NavMenu 的第一个实现可能如下所示:

export function NavMenu ({ menuItems }) {
  // state for the currently focused index
  const [focusedIndex, setFocusedIndex] = useState(0);

  // functions to update focused index
  const goToStart = () => setCurrentIndex(0);
  const goToEnd = () => setCurrentIndex(menuItems.length - 1);
  const goToPrev = () => {
    const index = currentIndex === 0 ? menuItems.length - 1 : currentIndex - 1;
    setCurrentIndex(index);
  };
  const goToNext = () => {
    const index = currentIndex === menuItems.length - 1 ? 0 : currentIndex + 1;
    setCurrentIndex(index);
  };

  // key down handler according to aria specification
  const handleKeyDown = (e) => {
    e.stopPropagation();
    switch (e.code) {
      case "ArrowLeft":
      case "ArrowUp":
        e.preventDefault();
        goToPrev();
        break;
      case "ArrowRight":
      case "ArrowDown": 
        e.preventDefault();
        goToNext();
        break;
      case "End":
        e.preventDefault();
        goToEnd();
        break;
      case "Home":
        e.preventDefault();
        goToStart();
        break;
      default:
        break;
    }
  }

  return (
    <nav>
      <menu role="menubar" onKeyDown={handleKeyDown}>
        {menuItems.map((item, index) => 
          <MenuItem
            key={item.label}
            item={item}
            index={index}
            focusedIndex={focusedIndex}
            setFocusedIndex={setFocusedIndex}
          />
        )}
      </menu>
    </nav>
  );
}

e.preventDefault() 的作用是防止 ArrowDown 滚动页面之类的事情。

这是 MenuItem 组件。让我们暂时忽略带有子菜单的项目。当 focusIndex 发生变化时,我们使用 useEffect、usePrevious 和 element.focus() 来聚焦于元素:

export function MenuItem ({ item, index, focusedIndex, setFocusedIndex }) {
  const linkRef = useRef(null);
  const prevFocusedIndex = usePrevious(focusedIndex);
  const isFocused = index === focusedIndex;

  useEffect(() => {
    if (linkRef.current 
      && prevFocusedIndex !== currentIndex 
      && isFocused) {
      linkRef.current.focus()
    }
  }, [isFocused, prevFocusedIndex, focusedIndex]);

  const handleFocus = () => {
    if (focusedIndex !== index) {
      setFocusedIndex(index);
    }
  };

  return (
    <li role="none">
      <a 
        ref={linkRef} 
        role="menuitem"
        tabIndex={isFocused ? 0 : -1}
        onFocus={handleFocus}
      >
        {item.label}
      </a>
    </li>
  );
}

请注意,a 标签应该具有 ref (带有子菜单的菜单项的按钮),因此当它们被聚焦时,默认键盘行为将按预期启动,例如 Enter 上的导航。此外,根据焦点元素正确分配选项卡索引。

我们正在为焦点事件添加一个事件处理程序,以防焦点事件不是来自键/鼠标事件。以下是网络文档中的引用:

不要假设所有焦点更改都将通过按键和鼠标事件实现:屏幕阅读器等辅助技术可以将焦点设置到任何可聚焦元素。

调整#1

如果您遵循上述 useEffect,您会发现即使用户没有使用键盘进行导航,第一个元素也会获得焦点。为了解决这个问题,我们可以检查活动元素,并且仅在用户启动某些键盘事件时调用 focus() ,这会将焦点从主体上移开。

  useEffect(() => {
    if (linkRef.current 
      && document.activeElement !== document.body // only call focus when user uses keyboard navigation
      && prevFocusedIndex !== focusedIndex
      && isCurrent) {
      linkRef.current.focus();
    }
  }, [isCurrent, focusedIndex, prevFocusedIndex]);

Logic Reuse and Custom Hook

So far, we have functional NavMenu and MenuItemLink components. Let's move on to menu item with sub menus.

As I was quickly building it out, I realized that this menu item will share the majority of the logic

以上是使用 React Hooks 构建可访问的导航菜单栏的详细内容。更多信息请关注PHP中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn