」工欲善其事,必先利其器。「—孔子《論語.錄靈公》
首頁 > 程式設計 > 使用 React Hooks 建立可存取的導覽功能表欄

使用 React Hooks 建立可存取的導覽功能表欄

發佈於2024-08-25
瀏覽:140

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]刪除
    最新教學 更多>
    • 如何使用Regex在PHP中有效地提取括號內的文本
      如何使用Regex在PHP中有效地提取括號內的文本
      php:在括號內提取文本在處理括號內的文本時,找到最有效的解決方案是必不可少的。一種方法是利用PHP的字符串操作函數,如下所示: 作為替代 $ text ='忽略除此之外的一切(text)'; preg_match('#((。 &&& [Regex使用模式來搜索特...
      程式設計 發佈於2025-04-06
    • HTML格式標籤
      HTML格式標籤
      HTML 格式化元素 **HTML Formatting is a process of formatting text for better look and feel. HTML provides us ability to format text without us...
      程式設計 發佈於2025-04-06
    • 哪種在JavaScript中聲明多個變量的方法更可維護?
      哪種在JavaScript中聲明多個變量的方法更可維護?
      在JavaScript中聲明多個變量:探索兩個方法在JavaScript中,開發人員經常遇到需要聲明多個變量的需要。對此的兩種常見方法是:在單獨的行上聲明每個變量: 當涉及性能時,這兩種方法本質上都是等效的。但是,可維護性可能會有所不同。 第一個方法被認為更易於維護。每個聲明都是其自己的語句,使...
      程式設計 發佈於2025-04-06
    • 如何將MySQL數據庫添加到Visual Studio 2012中的數據源對話框中?
      如何將MySQL數據庫添加到Visual Studio 2012中的數據源對話框中?
      在Visual Studio 2012 儘管已安裝了MySQL Connector v.6.5.4,但無法將MySQL數據庫添加到實體框架的“ DataSource對話框”中。為了解決這一問題,至關重要的是要了解MySQL連接器v.6.5.5及以後的6.6.x版本將提供MySQL的官方Visual...
      程式設計 發佈於2025-04-06
    • 找到最大計數時,如何解決mySQL中的“組函數\”錯誤的“無效使用”?
      找到最大計數時,如何解決mySQL中的“組函數\”錯誤的“無效使用”?
      如何在mySQL中使用mySql 檢索最大計數,您可能會遇到一個問題,您可能會在嘗試使用以下命令:理解錯誤正確找到由名稱列分組的值的最大計數,請使用以下修改後的查詢: 計數(*)為c 來自EMP1 按名稱組 c desc訂購 限制1 查詢說明 select語句提取名稱列和每個名稱...
      程式設計 發佈於2025-04-06
    • 如何在Java字符串中有效替換多個子字符串?
      如何在Java字符串中有效替換多個子字符串?
      在java 中有效地替換多個substring,需要在需要替換一個字符串中的多個substring的情況下,很容易求助於重複應用字符串的刺激力量。 However, this can be inefficient for large strings or when working with nu...
      程式設計 發佈於2025-04-06
    • 如何克服PHP的功能重新定義限制?
      如何克服PHP的功能重新定義限制?
      克服PHP的函數重新定義限制在PHP中,多次定義一個相同名稱的函數是一個no-no。嘗試這樣做,如提供的代碼段所示,將導致可怕的“不能重新列出”錯誤。 但是,PHP工具腰帶中有一個隱藏的寶石:runkit擴展。它使您能夠靈活地重新定義函數。 runkit_function_renction_...
      程式設計 發佈於2025-04-06
    • PHP陣列鍵值異常:了解07和08的好奇情況
      PHP陣列鍵值異常:了解07和08的好奇情況
      PHP數組鍵值問題,使用07&08 在給定數月的數組中,鍵值07和08呈現令人困惑的行為時,就會出現一個不尋常的問題。運行print_r($月)返回意外結果:鍵“ 07”丟失,而鍵“ 08”分配給了9月的值。 此問題源於PHP對領先零的解釋。當一個數字帶有0(例如07或08)的前綴時,PHP將...
      程式設計 發佈於2025-04-06
    • 為什麼在我的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-04-06
    • 我可以將加密從McRypt遷移到OpenSSL,並使用OpenSSL遷移MCRYPT加密數據?
      我可以將加密從McRypt遷移到OpenSSL,並使用OpenSSL遷移MCRYPT加密數據?
      將我的加密庫從mcrypt升級到openssl 問題:是否可以將我的加密庫從McRypt升級到OpenSSL?如果是這樣,如何? 答案:是的,可以將您的Encryption庫從McRypt升級到OpenSSL。 可以使用openssl。 附加說明: [openssl_decrypt()函數要求...
      程式設計 發佈於2025-04-06
    • Java是否允許多種返回類型:仔細研究通用方法?
      Java是否允許多種返回類型:仔細研究通用方法?
      在Java中的多個返回類型:一種誤解類型:在Java編程中揭示,在Java編程中,Peculiar方法簽名可能會出現,可能會出現,使開發人員陷入困境,使開發人員陷入困境。 getResult(string s); ,其中foo是自定義類。該方法聲明似乎擁有兩種返回類型:列表和E。但這確實是如此嗎...
      程式設計 發佈於2025-04-06
    • 如何在Java中執行命令提示命令,包括目錄更改,包括目錄更改?
      如何在Java中執行命令提示命令,包括目錄更改,包括目錄更改?
      在java 通過Java通過Java運行命令命令可能很具有挑戰性。儘管您可能會找到打開命令提示符的代碼段,但他們通常缺乏更改目錄並執行其他命令的能力。 solution:使用Java使用Java,使用processBuilder。這種方法允許您:啟動一個過程,然後將其標準錯誤重定向到其標準輸出...
      程式設計 發佈於2025-04-06
    • 為什麼PYTZ最初顯示出意外的時區偏移?
      為什麼PYTZ最初顯示出意外的時區偏移?
      與pytz 最初從pytz獲得特定的偏移。例如,亞洲/hong_kong最初顯示一個七個小時37分鐘的偏移: 差異源利用本地化將時區分配給日期,使用了適當的時區名稱和偏移量。但是,直接使用DateTime構造器分配時區不允許進行正確的調整。 example pytz.timezone(&#...
      程式設計 發佈於2025-04-06
    • 如何使用不同數量列的聯合數據庫表?
      如何使用不同數量列的聯合數據庫表?
      合併列數不同的表 當嘗試合併列數不同的數據庫表時,可能會遇到挑戰。一種直接的方法是在列數較少的表中,為缺失的列追加空值。 例如,考慮兩個表,表 A 和表 B,其中表 A 的列數多於表 B。為了合併這些表,同時處理表 B 中缺失的列,請按照以下步驟操作: 確定表 B 中缺失的列,並將它們添加到表的...
      程式設計 發佈於2025-04-06
    • 哪種方法更有效地用於點 - 填點檢測:射線跟踪或matplotlib \的路徑contains_points?
      哪種方法更有效地用於點 - 填點檢測:射線跟踪或matplotlib \的路徑contains_points?
      在Python Matplotlib's path.contains_points FunctionMatplotlib's path.contains_points function employs a path object to represent the polygon.它...
      程式設計 發佈於2025-04-06

    免責聲明: 提供的所有資源部分來自互聯網,如果有侵犯您的版權或其他權益,請說明詳細緣由並提供版權或權益證明然後發到郵箱:[email protected] 我們會在第一時間內為您處理。

    Copyright© 2022 湘ICP备2022001581号-3