空间导航(方向键焦点)

Web 功能: scrollIntoView()

定向焦点导航(使用箭头键或方向键)使仅使用键盘和替代输入的用户能够根据互动项的视觉 2D 布局进行导航。它对空间用户界面(例如媒体栏、信息中心面板或嵌套目录)非常有用,但必须谨慎实现,以免破坏原生浏览器行为(例如视口滚动)。

本指南介绍了如何在复合 widget 中构建高性能、无障碍的 2D 定向导航。


核心架构原则

实现自定义箭头键空间焦点时,请遵循以下基本规则:

  1. 仅限复合微件:请勿在预期使用默认依序 Tab 键导航的常规文档文本流或简单垂直列表布局中替换箭头键。如需了解一般文档流指南,请参阅无障碍编码指南。
  2. 保留原生边界滚动:如果按下方向键后没有可聚焦的元素,请勿阻止默认浏览器行为。让事件冒泡,以便浏览器可以原生滚动视口。无条件替换箭头键会破坏无障碍功能 (WCAG 1.4.10)。
  3. 确保焦点可见性和对齐:当焦点发生变化时,确保新聚焦的元素滚动到视图中(例如,使用 scrollIntoView({ block: 'nearest', inline: 'nearest' })),以便其保持完全可见。
  4. 与布局系统协调:在处理每次按键操作时,根据实时边界几何图形 (getBoundingClientRect()) 确定焦点候选位置。避免假设 DOM 顺序或固定网格结构代表视觉布局。如需了解现代布局建议,请参阅 CSS 布局和自适应设计。

无障碍焦点管理

空间容器必须向顺序页面标签页导航公开单个 Tab 键停靠位。在 widget 内,使用漫游 Tabindex 模式管理键盘焦点:

  • 活跃项具有 tabindex="0"。
  • 所有闲置标签页都具有 tabindex="-1"。
  • 导航时,通过切换 tabindex="0" 属性并调用 focus(),以编程方式移动焦点。

视觉候选对象选择

在非对称、换行或网格布局中,基于索引的映射会失败。而是根据当前按键的实时视觉几何图形推导候选词:

  • 候选:使用通过 checkVisibility() 的 widget 容器的可聚焦后代。隐藏元素在 (0, 0) 处可以具有大小为零的矩形,并且不得成为意外的 up 或 left 目标。
  • 几何图形:在处理按键时,读取焦点元素和每个候选元素的 getBoundingClientRect()。请勿在全局范围内缓存这些矩形:布局可能会因多种原因而发生变化,并且在处理用户节奏的按键事件时,少量几何图形读取操作的开销很低。
  • 方向过滤条件:只有当候选元素的近边缘位于或超出当前元素在该轴上的远边缘时,候选元素才位于按下的方向上,并允许对子像素舍入进行少量容差。例如,right 的候选人必须具备 candidate.left >= current.right - tolerance。请勿通过比较中心来确定候选对象是否在方向上。
  • 优先考虑对齐的候选元素:在通过方向过滤条件的候选元素中,优先考虑在垂直轴上的范围与当前元素的范围重叠的候选元素。选择导航轴上间距最小的元素,如果间距相同,则选择垂直中心偏移量最小的元素。这样,即使某个项异常宽,导航也会保持在相邻的视觉行或列中。
  • 后备评分:如果垂直轴上没有重叠的候选窗口,则将剩余候选窗口评分为 gap + w * perpendicularCentreOffset,并选择最小值。先使用权重 w >= 2,然后根据布局进行调整;权重越大,越有利于对齐更紧密的候选字词。
  • 无候选:不返回任何内容。事件处理脚本不得调用 preventDefault(),以便浏览器能够以原生方式滚动。

具体实现方式可能有所不同,但请确保方向测试基于边缘,并在应用加权后备之前应用重叠偏好。这是算法的核心;避免将缓存或固定权重作为普遍正确的答案。


按键集成和滚动协作

事件路由器必须仅在发生有效的空间焦点转换时阻止标准视口滚动:

  • 当辅助键处于按住状态、e.defaultPrevented 已为 true 或最内层事件目标本身会消耗箭头键时,请勿拦截事件。使用 e.composedPath()[0],而不仅仅是 e.target,因为 e.target 可以重新定位到影子宿主。实用的可修改/控件检查是 target.matches(':read-write, select, input[type="range"], [role="slider"]');无法检查封闭的影子根。
  • 将监听器限定在 widget 容器内,以便多个空间 widget 可以在一个页面上共存。
  • 仅当获得焦点的元素本身是候选元素时才执行操作。如果项包含其自己的可聚焦控件,请将这些控件也设为候选控件,或不作处理;请勿使用 closest() 解析父级。
  • 找到候选元素后,调用 preventDefault(),更新漫游 tabindex,调用 focus(),然后使用 scrollIntoView({ block: 'nearest', inline: 'nearest' })。
  • 如果未找到候选元素,则返回而不调用 preventDefault(),以便继续进行原生滚动。

避免添加单字符快捷键(例如 h/j/k/l 或 W/A/S/D)进行空间导航。箭头键不是字符键,在没有候选字词时保留原生滚动功能可避免不必要地阻止视口移动。