showmodal() 不自动完成焦点围栏,仅启动模态视觉层;需手动用 requestanimationframe 获取并聚焦首个可聚焦元素,拦截 tab 循环,并在关闭后安全恢复触发源焦点。

dialog.showModal() 不自动完成焦点围栏,它只负责启动模态视觉层(遮罩、z-index、Esc 响应),不接管键盘焦点流。Safari 全版本、旧 Edge、部分 Chrome 场景下,document.activeElement 仍为 body,Tab 键直接穿透——这不是“没聚焦”,是焦点管理压根没启动。
为什么 showModal() 调用后焦点还是跑到了 body
浏览器只在 showModal() 启动时「尝试」聚焦第一个可聚焦子元素,但这个尝试非常脆弱:
- DOM 渲染未完成就查元素 →
querySelector返回null,聚焦失败 - 弹窗内容异步加载(如 fetch 后插入表单),
showModal()时内部还是空的 - 写了
tabindex="-1"的容器或纯div标签,不满足可聚焦条件,浏览器跳过 - Safari 对
autofocus属性不响应,且不会在showModal()时重新触发挂载逻辑
正确做法是:在 showModal() 后立刻用 requestAnimationFrame(() => firstFocusable.focus()),而不是等 CSS 动画结束或写 setTimeout(..., 0)。
怎么安全获取并聚焦第一个可聚焦子元素
不能依赖 querySelector(':focusable')(Safari 不支持),也不能只查 button 或 input——会漏掉带 tabindex="0" 的容器和 a[href],导致屏幕阅读器用户卡住。
必须用这个选择器:
button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])
聚焦前加两道保险:
- 用
requestAnimationFrame包裹,确保 DOM 已渲染、样式已计算 - 检查元素是否真实可挂载:
if (el.offsetParent !== null) el.focus() - 若列表为空,给
<dialog></dialog>自身设tabindex="-1"再.focus(),至少让它能接收键盘事件
Tab 键循环必须手动拦截,但不能全局监听 keydown
监听位置必须是 <dialog></dialog> 元素本身,不是 document。多层模态时,document 级监听会冲突,且无法判断当前焦点是否还在本层内。
关键不是“拦所有 Tab”,而是“只在焦点即将越界时干预”:
- 预生成可聚焦元素数组:
const focusables = Array.from(dialog.querySelectorAll(selector)) - 监听
dialog.addEventListener('keydown', e => { if (e.key !== 'Tab') return; ... }) - 仅当
activeElement === focusables[0] && e.shiftKey或activeElement === focusables.at(-1) && !e.shiftKey时才preventDefault()并手动跳转 - 务必在
dialog.close()前移除该监听器,否则多个弹窗叠加时互相干扰
关闭后焦点没回触发源?问题常出在缓存方式上
闭包里直接存 document.querySelector('#trigger-btn') 返回的节点,在 SPA 路由切换或组件重渲染后,该节点很可能已从文档中移除,.focus() 静默失败,且无任何报错。
更可靠的做法:
- 打开前,把触发源 ID 存到 dialog 上:
dialog.dataset.triggerId = triggerBtn.id - 关闭后,用
document.getElementById(dialog.dataset.triggerId)查找 - 查找成功后再检查
offsetParent !== null && matches(':enabled'),再调.focus() - 别用
document.body.focus()做兜底——这会让键盘用户回到页面顶部,破坏操作连续性
真正麻烦的从来不是“怎么聚焦”,而是“什么时候聚焦”和“聚焦失败了怎么降级”。Safari 的兼容性断层、动态内容的渲染时机、SPA 中 DOM 节点的生命周期,三者叠加,让任何“一次性写完就不管”的焦点逻辑都不可靠。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











