可键盘导航的 JavaScript 组件

Web 应用程序通常使用 JavaScript 来模仿桌面部件,例如菜单、树状视图、富文本字段和选项卡面板。这些部件通常由 <div><span> 元素组成,这些元素本质上不提供与其桌面对应物相同的键盘功能。本文档描述了使 JavaScript 部件可键盘访问的技术。

使用 tabindex

默认情况下,当人们使用 Tab 键浏览网页时,只有交互式元素(如链接、表单控件)才能获得焦点。通过 tabindex 全局属性,作者也可以使其他元素获得焦点。当设置为 0 时,元素可以通过键盘和脚本获得焦点。当设置为 -1 时,元素可以通过脚本获得焦点,但它不会成为键盘焦点顺序的一部分。

使用键盘时元素获得焦点的顺序,默认是源顺序。在特殊情况下,作者可能希望重新定义顺序。为此,作者可以将 tabindex 设置为任何正数。

警告: 避免为 tabindex 使用正值。具有正 tabindex 的元素会出现在页面上默认交互式元素之前,这意味着页面作者在使用一个或多个正 tabindex 值时,必须为页面上所有可聚焦的元素设置(并维护)tabindex 值。

下表描述了现代浏览器中 tabindex 的行为

tabindex 属性 可通过鼠标或 JavaScript (element.focus()) 聚焦 Tab 可导航
不存在 遵循元素的平台约定(表单控件、链接等为“是”)。 遵循元素的平台约定。
负值(例如 tabindex="-1" 否;作者必须通过 focus() 方法响应箭头键或其他按键来聚焦该元素。
零(例如 tabindex="0" 在文档中相对于元素位置的 Tab 顺序中(请注意,<a> 等交互式元素默认具有此行为,它们不需要该属性)。
正值(例如 tabindex="33" tabindex 值决定了该元素在 Tab 顺序中的位置:较小的值将比较大的值更早地将元素放置在 Tab 顺序中(例如,tabindex="7" 将出现在 tabindex="11" 之前)。

非原生控件

<a><input><select> 这样的原生 HTML 交互式元素已经是键盘可访问的,因此使用它们是使组件支持键盘的最快途径。

通过添加 tabindex0 值,作者也可以使 <div><span> 元素键盘可访问。这对于使用 HTML 中不存在的交互式元素的组件特别有用。

分组控件

对于菜单、选项卡列表、网格或树状视图等组件的父元素分组,父元素应在 Tab 顺序中(tabindex="0"),并且每个后代选项/选项卡/单元格/行都应从 Tab 顺序中移除(tabindex="-1")。用户应能够使用箭头键导航后代元素。(有关典型小部件通常预期的键盘支持的完整描述,请参阅 WAI-ARIA Authoring Practices。)

下面的示例展示了这种技术在嵌套菜单控件中的使用。一旦键盘焦点落在包含的 <ul> 元素上,JavaScript 开发人员就必须以编程方式管理焦点并响应箭头键。有关在组件内管理焦点的方法,请参阅下面的“管理组内焦点”。

html
<ul id="mb1" tabindex="0">
  <li id="mb1_menu1" tabindex="-1">
    Font
    <ul id="fontMenu" title="Font" tabindex="-1">
      <li id="sans-serif" tabindex="-1">Sans-serif</li>
      <li id="serif" tabindex="-1">Serif</li>
      <li id="monospace" tabindex="-1">Monospace</li>
      <li id="fantasy" tabindex="-1">Fantasy</li>
    </ul>
  </li>
  <li id="mb1_menu2" tabindex="-1">
    Style
    <ul id="styleMenu" title="Style" tabindex="-1">
      <li id="italic" tabindex="-1">Italics</li>
      <li id="bold" tabindex="-1">Bold</li>
      <li id="underline" tabindex="-1">Underlined</li>
    </ul>
  </li>
  <li id="mb1_menu3" tabindex="-1">
    Justification
    <ul id="justificationMenu" title="Justification" tabindex="-1">
      <li id="left" tabindex="-1">Left</li>
      <li id="center" tabindex="-1">Centered</li>
      <li id="right" tabindex="-1">Right</li>
      <li id="justify" tabindex="-1">Justify</li>
    </ul>
  </li>
</ul>

禁用控件

当自定义控件变为禁用状态时,通过将 tabindex="-1" 设置为 -1 来将其从 Tab 顺序中移除。请注意,分组控件(如菜单中的菜单项)中的禁用项应仍可通过箭头键导航。

管理组内焦点

当用户通过 Tab 键离开某个组件并返回时,焦点应返回到之前具有焦点的特定元素,例如树状项或网格单元格。有两种方法可以实现此目的:

  1. 漫游 tabindex:以编程方式移动焦点
  2. aria-activedescendant:管理“虚拟”焦点

技术 1:漫游 tabindex

将当前焦点的元素的 tabindex 设置为“0”可确保,如果用户通过 Tab 键离开组件然后返回,组内选定的项目将保留焦点。请注意,将 tabindex 更新为“0”还需要将先前选定的项目更新为 tabindex="-1"。此技术涉及响应按键事件以编程方式移动焦点,并更新 tabindex 以反映当前具有焦点的项目。为此:

为组中的每个元素绑定一个按键处理程序,当使用箭头键移动到另一个元素时:

  1. 以编程方式将焦点应用于新元素,
  2. 将当前焦点元素的 tabindex 更新为“0”,以及
  3. 将先前焦点元素的 tabindex 更新为“-1”。

技术 2:aria-activedescendant

此技术涉及将单个事件处理程序绑定到容器组件,并使用 aria-activedescendant 来跟踪“虚拟”焦点。(有关 ARIA 的更多信息,请参阅此 可访问 Web 应用程序和组件概述。)

aria-activedescendant 属性标识当前具有虚拟焦点的后代元素的 ID。容器上的事件处理程序必须通过更新 aria-activedescendant 的值来响应按键和鼠标事件,并确保当前项目被适当地设置样式(例如,通过边框或背景颜色)。

一般准则

焦点事件的使用

  • 请勿分派 focus 事件来将焦点发送到某个元素。DOM 焦点事件仅用于信息:它们在某个元素获得焦点后由系统生成,但实际上不用于设置焦点。请使用 element.focus() 代替。
  • 请监听 focusblur 事件来跟踪焦点变化。不要假设所有焦点变化都来自按键和鼠标事件:辅助技术(如屏幕阅读器)可以将焦点设置到任何可聚焦的元素。如果您想跟踪整个文档的焦点状态,可以使用 document.activeElement 来获取活动元素,或者使用 document.hasFocus 来确保当前文档是否具有焦点。

确保键盘和鼠标产生相同的体验

为了确保用户体验无论输入设备如何都保持一致,键盘和鼠标事件处理程序应在适当的地方共享代码。例如,当用户使用箭头键导航时更新 tabindex 或样式的代码也应被鼠标点击处理程序使用,以产生相同的更改。

确保键盘可用于激活元素

为了确保键盘可用于激活元素,绑定到鼠标事件的所有处理程序也应绑定到键盘事件。例如,为了确保 Enter 键可以激活元素,如果您有一个 onclick="doSomething()",您也应该将 doSomething() 绑定到按键事件:onkeydown="event.code === "Enter" && doSomething();"

始终为 tabindex="-1" 的项目和以编程方式获得焦点的元素绘制焦点

确保获得焦点的元素具有焦点环。这可以通过 CSS outline 属性来完成,该属性不应被无条件设置为 none — 如果您想阻止显示不必要的焦点环,请使用 :focus-visible 伪类。

阻止使用的按键事件执行浏览器功能

如果您的组件处理某个按键事件,请通过使用事件处理程序的返回值来阻止浏览器也处理它(例如,响应箭头键的滚动)。如果您的事件处理程序返回 false,事件将不会传播到您的处理程序之外。

例如

html
<span tabindex="-1">…</span>
js
span.onkeydown = handleKeyDown;

如果 handleKeyDown() 返回 false,事件将被捕获,阻止浏览器根据按键执行任何操作。

此时,不要依赖按键重复的一致行为

不幸的是,onkeydown 是否重复取决于您运行的浏览器和操作系统。