配了五年工具栏,每次加个按钮都要重写一整套键盘导航——今天一个 HTML 属性把这件事彻底原生化了

做过工具栏、Tab 列表、菜单的前端工程师,大概都写过或者抄过这套代码:监听方向键,find 当前焦点元素,update 下一个元素的 tabindex,call focus()。如果你的项目里没有这段,说明你们用了某个 UI 库把它封装掉了——但封装掉的代码依然是这段逻辑,只是你不用自己写。

这套模式在 ARIA Authoring Practices Guide 里叫 roving tabindex,是 WAI-ARIA 给复合组件的标准键盘交互方案。标准答案所有人都知道,但实现起来有几十个细节要处理:RTL 语言的方向键映射、disabled 和 hidden 元素要跳过、动态增删项目要维护索引、用户切走再切回来要恢复上次焦点……一个生产级的 roving tabindex 至少 40 行。

Chrome 150 和 Edge 150 把这件事彻底原生化了。一个 HTML 属性,全部替代。

focusgroup=”toolbar” 就能用

在容器上加 focusgroup=”toolbar”,里面所有可聚焦子元素就自动获得箭头键导航、单一 Tab 停靠、以及焦点记忆。不用写一行 JS:

<div focusgroup="toolbar wrap">
  <button>Bold</button>
  <button>Italic</button>
  <button>Underline</button>
</div>

用户按 Tab 进入工具栏后,Left/Right 方向键在三个按钮之间循环切换(因为有 wrap),切走再 Tab 回来焦点会回到上次离开时的按钮。整个过程浏览器自动处理,不需要任何事件监听。

behavior token:toolbar / tablist / radiogroup / listbox / menu / menubar

focusgroup 的值是一个 behavior token,声明这个组件的类型。不同类型会获得不同的默认 ARIA 角色和键盘行为:

  • toolbar:左右箭头键循环导航
  • tablist:tabs 模式,当前 tab 独占 Tab 停靠,非活跃 tab 从无障碍树隐藏
  • radiogroup:单选组,箭头键切换选中项
  • listbox:列表框,上下左右导航
  • menu / menubar:菜单,完整菜单键盘交互

modifier:inline / block / wrap / nowrap / nomemory

在 behavior token 后面可以加修饰符:

  • inline / block:限制导航轴向。inline 只能用左右键,block 只能用上下键
  • wrap / nowrap:wrap 导航到边缘后循环,nowrap 则停在边界
  • nomemory:关闭焦点记忆,每次进组都从头开始
<!-- 垂直工具栏,不循环,不记忆 -->
<div focusgroup="toolbar block nowrap nomemory">
  <button>Up</button>
  <button>Down</button>
</div>

自动 ARIA 推断,但有限制

focusgroup 会自动给没有显式角色的子元素推断 ARIA role。focusgroup=”tablist” 里的 button 会自动获得 role=”tab”,父元素获得 role=”tablist”。这很省事,但有个前提:只有在元素没有自己语义的时候才会覆盖。

如果你用的是 <section> 而非 <div>,role 推断就不会发生,因为 section 有自己的语义。这时候你还是要手动补 role=”tablist”。

嵌套 focusgroup:菜单里装菜单

可以在一个 focusgroup 里面再声明一个 focusgroup。内层会创建独立的导航上下文,自动从外层的方向键导航中退出:

<div focusgroup="menubar">
  <div focusgroup="menu">
    <!-- 子菜单,独立的键盘导航 -->
  </div>
</div>

这正是真实菜单系统的结构——菜单栏里套下拉菜单,focusgroup 把这个嵌套模式也原生支持了。

浏览器支持与 polyfill

Chrome 150+ / Edge 150+ 已稳定支持,约 45% 全球覆盖。Firefox 持积极态度(pending 实现),Safari 暂无信号。如果要在生产环境使用,可以用 @microsoft/focusgroup-polyfill 做渐进增强:

if (!(focusgroup in HTMLElement.prototype)) {
  // 加载 polyfill
}

有 polyfill 的情况下,用户体验和原生支持完全一致——只是 polyfill 是 JS 模拟,效率不如浏览器原生,但功能完整。

一个属性的价值

focusgroup 替换掉的是一整类工程问题:每个 UI 库都在重复实现同一套键盘导航逻辑,每个团队都在维护同一份样板,每个无障碍审计都会挑出各种细节遗漏。浏览器把它收进去之后,这个问题从「你们自己处理」变成了「平台负责」,框架和业务代码都不需要再管。

这就是平台级 API 的意义——不是给你加一个新能力,是把所有人重复建设的那个东西消灭掉。

评论区

0 条评论

登录后可评论。

阿柯·前端架构 13 阅读