Skip to content

键盘、焦点与交互状态 ​

键盘可用不是“每个元素都加 tabindex”。正常交互元素按文档顺序进入 Tab;tabindex="0" 只用于确实需要进入顺序的自定义控件,正数 tabindex 会制造难维护的焦点序列。弹窗打开时焦点应进入弹窗,关闭后回到触发按钮。

js
const trigger = document.querySelector('#open')
const dialog = document.querySelector('#dialog')
const close = document.querySelector('#close')
trigger.addEventListener('click', () => { dialog.hidden = false; close.focus() })
close.addEventListener('click', () => { dialog.hidden = true; trigger.focus() })
dialog.addEventListener('keydown', (event) => { if (event.key === 'Escape') close.click() })

这里只展示最小焦点回收;生产弹窗还要处理 Tab 循环、背景遮罩、滚动和嵌套弹窗。

练习 ​

为菜单按钮补 Enter/Space、Escape 和焦点回收;要求菜单关闭后焦点回到按钮,而不是丢到 body。

核心机制与实现 ​

无障碍实现是语义、键盘、焦点、名称、状态和动态反馈的组合;ARIA 只能补足缺失语义,不能替代原生元素。

围绕“键盘、焦点与交互状态”先写出输入 → 中间状态 → 可观察结果,再实现最小闭环。不要把事件监听、异步请求、DOM 读取或 store 写入混在一个函数里;每个副作用都要有创建、完成、取消和清理路径。

html
<button aria-expanded="false" aria-controls="menu">菜单</button>
<div id="menu" hidden>...</div>
<script>
button.addEventListener('click', () => {
  const open = button.getAttribute('aria-expanded') === 'true'
  button.setAttribute('aria-expanded', String(!open))
  menu.hidden = open
})
</script>

逐步运行:先只验证成功路径,再改变一个输入;记录时间顺序、对象身份、错误类型和最终可见状态。若环境不支持某个浏览器 API,要把静态协议验证和真实浏览器验证分开记录。

失败边界与变体 ​

  • 输入为空、重复、过期或非法时,不应静默覆盖有效状态。
  • 组件卸载、路由离开、标签页关闭或请求取消时,资源必须停止写入。
  • 跨文档、Shadow DOM、Worker、SSR 或多标签页会改变可见边界,不能沿用单页面直觉。
  • 需要持久化时记录 schema 版本和迁移失败策略;需要全局状态时先证明局部状态不足。

可运行实验与练习 ​

使用键盘完成打开、移动、关闭和返回焦点;用辅助技术或浏览器无障碍树核对名称、角色、状态、错误和动态更新。

练习要求:实现一个最小版本,补一个失败测试,再给出一个替代方案及其适用条件。答案应包括实际输入序列、观察结果、修复原因和未覆盖的浏览器差异。

查看本节答案

下一步 ​

完成练习后打开本章答案,再回到可访问性课程总览。

用心记录每一步成长