C6.22.3Roving tabindex设计研究

漫游 tabindex 让复合控件内只有一个成员处于Tab序列,其余靠方向键切换

别名: roving tabindex · 活动 tabindex · 单停点复合控件

概念解释

漫游 tabindex(roving tabindex)是实现“Tab 进组、方向键内移”的 DOM 手法:同一时刻只有当前子项的 tabindex 为 0(或保持自然可焦),其余子项设为 -1。方向键把 0 从这个子项挪到下一个,于是 Tab 序列里这个复合控件永远只占一个停点。没有漫游时,每个按钮都是 0,Tab 会把工具栏拆成一串停点——惯例在交互层写对了,实现层仍会漏。

机制

浏览器的 Tab 遍历只看可聚焦性,不理解“这些按钮属于同一工具栏”。把非当前子项设为 -1,等于从链表里摘掉它们,方向键的脚本再调用 focus() 并交换 tabindex。焦点离开组件再回来时,应回到上次活动的子项(记住哪一颗是 0),否则用户会丢失位置。失败常见于动态增删子项后 0 落在已删除节点上,或 React 重渲染把 -1 全部打回 0。漫游解决的是停点数量,不是焦点可见性,也不是阅读顺序是否与画面一致。

怎么研究

用无障碍树或 Tab 探测脚本统计复合控件在静止和移动内部焦点之后的停点个数,期望恒为 1。再测离开再进入是否恢复上次子项。把虚拟列表(只渲染可见行)单独做:滚出视口的节点被销毁时,tabindex 状态机容易丢。自动化可以抓停点,仍要用键盘走一遍确认 focus() 没有被 preventScroll 搞到看不见的项上。

边界

原生 <select>、某些系统菜单自己实现了单停点,不必再套漫游。内容可编辑区域内的 Tab 有插入制表符的语义,不能把编辑器内部格子都做成 roving。触屏点击不经过 tabindex。若复合控件只有一项,漫游没有收益。iframe 或 shadow DOM 边界会切断脚本对子项 tabindex 的控制,需要在边界内各自实现一份。

怎么落地

  • 工具栏、选项卡、网格:挂载时只有活动子项为 0,其余 -1;方向键更新这对值并 focus 新项。
  • 组件失焦再获焦时恢复上次活动子项,不要每次回到第一项。
  • 子项增删后重建活动指针,避免 0 停在已卸载节点。
  • 验证:打开无障碍检查器,在工具栏上按方向键若干次,每次停下来只应有一个子项的 tabindex 为 0,Tab 一次应整组离开。

延伸

  • 同组C6.22.1 复合控件内部常用方向键导航,Tab只负责进出该控件 · C6.22.2 跳转链接允许键盘用户跳过重复的导航区块直达主要内容 · C6.22.4 键盘导航路径与鼠标可达路径必须功能等价,不能只做视觉呈现
  • 相邻C6.05 焦点顺序 · C6.03 快捷键体系
  • 站内检索roving tabindex · tabindex · composite widget

同组卡片

快捷操作

分享

分享当前页面

ios_share

https://hci.top/zh/handbook/C6.22.3