”工欲善其事,必先利其器。“—孔子《论语.录灵公》
首页 > 编程 > 使用 React Hooks 构建可访问的导航菜单栏

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

发布于2024-08-25
浏览:117

Building an Accessible Navigation Menubar with React Hooks

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

介绍

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

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

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

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

我们正在建设什么?

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

HTML 标记

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

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


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

成分

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

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

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

我最终编写了这些组件:

  • NavMenu 用于菜单栏的最外层。
  • MenuItem 用于单个菜单项。
    • 菜单项链接
    • MenuItemWithSubMenu
  • 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 (
    
  );
}

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 (
    
  • {item.label}
  • ); }

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

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

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

    调整#1

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

      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]);
    

    逻辑重用和自定义 Hook

    到目前为止,我们已经有了功能性的 NavMenu 和 MenuItemLink 组件。让我们继续讨论带有子菜单的菜单项。

    当我快速构建它时,我意识到这个菜单项将共享大部分逻辑

    版本声明 本文转载于:https://dev.to/godsamit/building-an-accessible-navigation-menubar-with-react-hooks-blh?1如有侵犯,请联系[email protected]删除
    最新教程 更多>
    • 如何使用FormData()处理多个文件上传?
      如何使用FormData()处理多个文件上传?
      )处理多个文件输入时,通常需要处理多个文件上传时,通常是必要的。 The fd.append("fileToUpload[]", files[x]); method can be used for this purpose, allowing you to send multi...
      编程 发布于2025-05-17
    • 如何克服PHP的功能重新定义限制?
      如何克服PHP的功能重新定义限制?
      克服PHP的函数重新定义限制在PHP中,多次定义一个相同名称的函数是一个no-no。尝试这样做,如提供的代码段所示,将导致可怕的“不能重新列出”错误。 但是,PHP工具腰带中有一个隐藏的宝石:runkit扩展。它使您能够灵活地重新定义函数。 runkit_function_renction_re...
      编程 发布于2025-05-17
    • Go web应用何时关闭数据库连接?
      Go web应用何时关闭数据库连接?
      在GO Web Applications中管理数据库连接很少,考虑以下简化的web应用程序代码:出现的问题:何时应在DB连接上调用Close()方法?,该特定方案将自动关闭程序时,该程序将在EXITS EXITS EXITS出现时自动关闭。但是,其他考虑因素可能保证手动处理。选项1:隐式关闭终止数...
      编程 发布于2025-05-17
    • Java开发者如何保护数据库凭证免受反编译?
      Java开发者如何保护数据库凭证免受反编译?
      在java 在单独的配置文件保护数据库凭证的最有效方法中存储凭据是将它们存储在单独的配置文件中。该文件可以在运行时加载,从而使登录数据从编译的二进制文件中远离。使用prevereness class import java.util.prefs.preferences; 公共类示例{ 首选项...
      编程 发布于2025-05-17
    • 我可以将加密从McRypt迁移到OpenSSL,并使用OpenSSL迁移MCRYPT加密数据?
      我可以将加密从McRypt迁移到OpenSSL,并使用OpenSSL迁移MCRYPT加密数据?
      将我的加密库从mcrypt升级到openssl 问题:是否可以将我的加密库从McRypt升级到OpenSSL?如果是这样,如何?答案:是的,可以将您的Encryption库从McRypt升级到OpenSSL。可以使用openssl。附加说明: [openssl_decrypt()函数要求iv参...
      编程 发布于2025-05-17
    • 人脸检测失败原因及解决方案:Error -215
      人脸检测失败原因及解决方案:Error -215
      错误处理:解决“ error:((-215)!empty()in Function Multultiscale中的“ openCV 要解决此问题,必须确保提供给HAAR CASCADE XML文件的路径有效。在提供的代码片段中,级联分类器装有硬编码路径,这可能对您的系统不准确。相反,OPENCV提...
      编程 发布于2025-05-17
    • 如何在php中使用卷发发送原始帖子请求?
      如何在php中使用卷发发送原始帖子请求?
      如何使用php 创建请求来发送原始帖子请求,开始使用curl_init()开始初始化curl session。然后,配置以下选项: curlopt_url:请求 [要发送的原始数据指定内容类型,为原始的帖子请求指定身体的内容类型很重要。在这种情况下,它是文本/平原。要执行此操作,请使用包含以下标头...
      编程 发布于2025-05-17
    • 在JavaScript中如何并发运行异步操作并正确处理错误?
      在JavaScript中如何并发运行异步操作并正确处理错误?
      同意操作execution 在执行asynchronous操作时,相关的代码段落会遇到一个问题,当执行asynchronous操作:此实现在启动下一个操作之前依次等待每个操作的完成。要启用并发执行,需要进行修改的方法。 第一个解决方案试图通过获得每个操作的承诺来解决此问题,然后单独等待它们: co...
      编程 发布于2025-05-17
    • 为什么我的CSS背景图像出现?
      为什么我的CSS背景图像出现?
      故障排除:CSS背景图像未出现 ,您的背景图像尽管遵循教程说明,但您的背景图像仍未加载。图像和样式表位于相同的目录中,但背景仍然是空白的白色帆布。而不是不弃用的,您已经使用了CSS样式: bockent {背景:封闭图像文件名:背景图:url(nickcage.jpg); 如果您的html,css...
      编程 发布于2025-05-17
    • 解决Spring Security 4.1及以上版本CORS问题指南
      解决Spring Security 4.1及以上版本CORS问题指南
      弹簧安全性cors filter:故障排除常见问题 在将Spring Security集成到现有项目中时,您可能会遇到与CORS相关的错误,如果像“访问Control-allo-allow-Origin”之类的标头,则无法设置在响应中。为了解决此问题,您可以实现自定义过滤器,例如代码段中的MyFi...
      编程 发布于2025-05-17
    • 在细胞编辑后,如何维护自定义的JTable细胞渲染?
      在细胞编辑后,如何维护自定义的JTable细胞渲染?
      在JTable中维护jtable单元格渲染后,在JTable中,在JTable中实现自定义单元格渲染和编辑功能可以增强用户体验。但是,至关重要的是要确保即使在编辑操作后也保留所需的格式。在设置用于格式化“价格”列的“价格”列,用户遇到的数字格式丢失的“价格”列的“价格”之后,问题在设置自定义单元格...
      编程 发布于2025-05-17
    • 为什么尽管有效代码,为什么在PHP中捕获输入?
      为什么尽管有效代码,为什么在PHP中捕获输入?
      在php ;?>" method="post">The intention is to capture the input from the text box and display it when the submit button is clicked.但是,输出...
      编程 发布于2025-05-17
    • 在Oracle SQL中如何提取下划线前的子字符串?
      在Oracle SQL中如何提取下划线前的子字符串?
      [ 在oracle sql 解决方案: Explanation:SUBSTR function extracts a substring starting from the specified position (0) and continuing for a specified length.IN...
      编程 发布于2025-05-17
    • 如何使用Java.net.urlConnection和Multipart/form-data编码使用其他参数上传文件?
      如何使用Java.net.urlConnection和Multipart/form-data编码使用其他参数上传文件?
      使用http request 上传文件上传到http server,同时也提交其他参数,java.net.net.urlconnection and Multipart/form-data Encoding是普遍的。 Here's a breakdown of the process:Mu...
      编程 发布于2025-05-17
    • 为什么在我的Linux服务器上安装Archive_Zip后,我找不到“ class \” class \'ziparchive \'错误?
      为什么在我的Linux服务器上安装Archive_Zip后,我找不到“ class \” class \'ziparchive \'错误?
      Class 'ZipArchive' Not Found Error While Installing Archive_Zip on Linux ServerSymptom:When attempting to run a script that utilizes the ZipAr...
      编程 发布于2025-05-17

    免责声明: 提供的所有资源部分来自互联网,如果有侵犯您的版权或其他权益,请说明详细缘由并提供版权或权益证明然后发到邮箱:[email protected] 我们会第一时间内为您处理。

    Copyright© 2022 湘ICP备2022001581号-3