原生 不是无障碍模态框,需手动实现焦点捕获、循环与回归;showmodal() 在 safari 和旧 edge 中不触发真正焦点围栏,导致键盘用户可跳出对话框,屏幕阅读器无法感知内容。

原生 <dialog></dialog> 不等于无障碍模态框,必须手动补全焦点捕获、循环和回归逻辑,否则键盘用户在 Safari 和旧版 Edge 中会直接跳出对话框。
为什么 showModal() 不能替代焦点管理
它确实设置了 aria-modal="true"、生成了 ::backdrop、响应 Escape 键,但 Safari(全版本)和部分旧 Edge 中不触发真正的焦点围栏。实测现象包括:document.activeElement 仍指向 body,Tab 键可跳到页脚按钮——这不是体验问题,是功能失效。
- 焦点未被捕获:打开后
Tab仍从页面顶部开始遍历,屏幕阅读器无法感知弹窗内容 - 焦点未被循环:按
Tab到最后一个可聚焦元素后,焦点离开弹窗,而非回到第一个 - 焦点未被回归:关闭后焦点落到
document.body或随机节点,破坏导航连续性
如何正确实现首次聚焦与焦点循环
关键不是“等 DOM 渲染完就 focus”,而是确保聚焦时机可靠、目标元素真实可交互。
- 用
requestAnimationFrame(() => first.focus())替代setTimeout(..., 0),避免因样式未生效或异步内容未挂载导致聚焦失败 - 查找范围必须完整:
button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])—— 漏掉[href]会导致链接无法参与循环 - 监听事件绑定在
<dialog></dialog>元素上,而非document,防止多层模态时事件冲突或拦截错位 - 循环逻辑只在
event.key === 'Tab'且当前聚焦项是列表首尾时触发event.preventDefault(),不干扰屏幕阅读器的其他快捷键
关闭时如何安全恢复触发源焦点
不能依赖闭包变量存触发元素,也不能无条件调用 .focus()——元素可能已被移除、重渲染或不可见。
- 打开弹窗前,把触发元素 ID 存入
<dialog></dialog>的data-trigger-id属性中 - 关闭前通过
document.getElementById(triggerId)重新获取,再检查element.offsetParent !== null确认其仍在文档流中 - 若触发源已销毁,退回到最近的可聚焦祖先(如父级
button或表单容器),而不是document.body - 避免用
document.activeElement判断来源——关闭瞬间它常为body,不可靠
最易被忽略的点是:所有可聚焦容器(包括自定义下拉、手写菜单)都必须有明确的键盘行为闭环——能进、能操作、能出。光加 tabindex="0" 只解决了“能进”,没解决“按空格/回车是否触发”“按 Escape 是否关闭”“焦点离开时是否自动归位”。这些不是锦上添花,是键盘用户能完成任务的前提。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











