纯前端实现可访问焦点陷阱对话框:多层弹窗、滚动锁定与返回焦点
模态对话框打开后,用户只能先处理眼前的弹窗,再回到原页面。这不仅是视觉层级,更是键盘和读屏交互的约束:背景不能被误操作,Tab 不能在页面里游走,关闭后焦点还要回到触发按钮。一个可靠的焦点陷阱需要把浏览器原生能力、焦点管理和页面状态一起处理。
一、优先使用原生 dialog
现代浏览器已经提供 dialog 元素。以 showModal 打开时,浏览器会把它提升到顶层,并提供基础的模态隔离。相比手写遮罩加 role 属性,原生 dialog 更容易和浏览器、辅助技术保持一致的模态语义:
const dialog = document.querySelector('#edit-dialog');
dialog.showModal();
原生能力并不替代业务焦点管理。打开时要决定第一个焦点落在哪,关闭时要恢复触发元素,异步内容更新后还要重新检查当前焦点是否仍有效。
二、给对话框明确名称
dialog 要有可访问名称,通常用 aria-labelledby 指向标题。描述较长时,可以用 aria-describedby 关联说明,但不要把整段正文都作为描述,否则读屏打开时可能一次读出大量内容。关闭按钮要有“关闭”名称,不能只显示一个叉号。
<dialog aria-labelledby="dialog-title">
<h2 id="dialog-title">编辑项目</h2>
<button type="button" aria-label="关闭">×</button>
</dialog>
三、初始焦点要有明确规则
表单对话框通常把焦点放在第一个可编辑字段;确认型对话框优先聚焦最安全或最主要的操作;内容型对话框可以聚焦标题容器或关闭按钮。不要一律聚焦第一个元素,因为第一个元素可能是次要的关闭按钮。
打开前保存 document.activeElement,关闭后优先恢复到仍然存在的原触发元素。若触发元素已被删除,再退回页面主区域或其他稳定节点。只保存选择器并不可靠,元素重新渲染后可能指向同名但不同的控件。
四、焦点陷阱要处理边界
即使使用 showModal,也建议实现明确的 Tab 边界,尤其是自定义弹窗或需要兼容多种浏览器时。每次 Tab 前重新读取对话框内可见、可聚焦的元素,不能缓存一份永远不会变化的列表。表单禁用、条件字段显示和异步按钮都会改变候选集合。
function getFocusable(root) {
return [...root.querySelectorAll(
'a[href],button:not([disabled]),input:not([disabled]),' +
'select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])'
)].filter((node) => node.getClientRects().length > 0);
}
在最后一个元素按 Tab 时回到第一个,在第一个元素按 Shift+Tab 时回到最后一个。如果候选为空,让容器本身可聚焦并接住 Tab,不能让焦点掉回背景页面。
五、背景隔离与滚动锁定
原生 dialog 提供模态语义,但页面其他区域仍可能被脚本或旧的键盘处理误触。可以用 inert 属性隔离背景应用区域,避免背景控件进入焦点序列。不要把 inert 加在包含 dialog 的祖先上,否则对话框本身也会失效。
滚动锁定常被写成 body overflow:hidden,但这可能在桌面端引发布局跳动。打开前记录滚动条宽度,用 padding-right 补上差值;关闭时恢复原样式。移动端优先给对话框内容区独立滚动,而不是同时锁住页面和弹窗内容。
六、多层对话框
确认弹窗可能覆盖在表单弹窗上方。每层都要保存自己的触发元素和滚动锁计数,不能关闭上层时直接把所有状态清零。用栈记录打开顺序,只让最上层接收 Escape;关闭最上层后再把焦点还给它的触发控件,而不是最底层页面。
七、异步关闭与卸载
提交请求进行中,用户可能再次点击关闭。关闭流程要处理未完成请求:可以禁用重复提交、取消请求,或等结果返回后再关闭。若对话框已经被移除,异步回调不能再访问旧 DOM,也不能重新打开已关闭的弹窗。
八、Escape、点击遮罩和提示
Escape 适合取消普通对话框,但包含未保存内容时,应先确认而不是无声丢弃。点击遮罩关闭也要有明确策略,表单型弹窗通常不应因为轻微误触而丢失输入。错误提示应与对应字段关联,并通过 aria-live 播报整体结果,不能只改变边框颜色。
九、测试重点
覆盖首次打开焦点、Shift+Tab 回绕、背景不可聚焦、Escape、点击遮罩、异步内容出现、条件字段变化、表单提交成功和关闭后焦点恢复。还要检查多层弹窗连续关闭、触发元素被移除、移动端软键盘和滚动位置。用键盘完整走一遍流程,往往比只检查视觉截图更快发现焦点陷阱的漏洞。
十、小结
可访问对话框的核心不是一个 keydown 判断,而是打开前保存状态、打开后建立模态边界、动态计算可聚焦元素、关闭时恢复焦点并清理滚动锁。原生 dialog 提供了良好起点,业务层再把多层弹窗、异步关闭和错误反馈补齐,键盘用户才不会在弹窗与背景之间迷路。
- 点赞
- 收藏
- 关注作者
评论(0)