纯前端实现可访问焦点陷阱对话框:多层弹窗、滚动锁定与返回焦点

举报
yd_213843956 发表于 2026/11/04 07:46:50 2026/11/04
【摘要】 模态对话框不只是遮罩加内容。这篇讲清焦点陷阱、初始焦点、返回焦点、背景 inert、滚动锁定和多层弹窗的边界。

模态对话框打开后,用户只能先处理眼前的弹窗,再回到原页面。这不仅是视觉层级,更是键盘和读屏交互的约束:背景不能被误操作,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 提供了良好起点,业务层再把多层弹窗、异步关闭和错误反馈补齐,键盘用户才不会在弹窗与背景之间迷路。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。