J3.02.7nested focus trap设计研究

多层嵌套的浮层各自捕获焦点时可能相互冲突形成死锁

别名: 嵌套对话框 · 焦点死锁 · stacked modal

概念解释

一层浮层捕获焦点并且留了门,通常是对的。两层以上同时宣称「焦点归我」时,门会打到空处或打到错的一层:嵌套捕获死锁。典型现场是对话框里再打开日期选择器或确认框,Escape 关掉了底下那层、留下无主的顶层;或者两层都在监听 Escape,一次按键把整个栈拆光;或者顶层关掉后焦点还给了已经不存在的触发者,键盘悬空。

这不是「要不要捕获」,是所有权栈乱了:每一层都以为自己是唯一的捕获者。

机制

单层捕获的实现往往是全局的:在 document 上拦截 Tab,把焦点拽回「当前对话框」的首尾。第二层打开时,若再注册一份同样的拦截而不暂停第一份,两份逻辑会抢同一个按键。Escape 更糟——两层都绑了「关闭自己」,事件冒泡会连关两层,或内层 stopPropagation 之后外层再也收不到关闭。

归还焦点依赖一张「谁打开了我」的指针。嵌套时这张指针必须是栈:关顶层,还给打开顶层的控件(它还在下一层里);关下一层,再还给页面上的原触发者。用单个全局变量覆盖,关顶层就会把焦点送回已经藏在遮罩下、甚至已经卸载的页面按钮,人被留在既不在顶层也不在底层的空档里。

怎么研究

构造最小栈:页面按钮 → 对话框 → 对话框内的选择器或二次确认。对每一层记录:打开时焦点进了哪一层、Escape 关掉了哪一层、关闭后焦点在哪。死锁的操作定义:存在一种按键序列,之后焦点不在任何一层的可操作控件上,且无法只靠键盘恢复。

第三方小组件在第一方对话框里打开自己的层(颜色选择、支付、OAuth)是高发组合,必须纳入抽样,不要只测自家对话框套自家下拉。

边界

菜单打开子菜单通常不应各自做模态捕获,而应共享一次漫游:方向键进出子菜单,Escape 先关子菜单再关父菜单。那是约定好的栈,不是死锁。真正需要两层模态的场景(未保存确认盖在编辑对话框上)可以把栈做对,而不是禁止嵌套。系统级提示(操作系统权限框、浏览器文件对话框)不在页面的捕获栈里,页面必须在它们关闭后还能认领焦点,不能假定自己仍是最顶层。

怎么落地

  • 把打开的浮层做成栈:只有栈顶捕获;Escape 只关栈顶;关闭后焦点还给栈里的下一层触发者。
  • 能合并的不要拆成两层模态。必须嵌套时,内层关闭不得把外层一起拆掉。
  • 验证:在对话框里打开第二层,按 Escape 一次。若两层一起没了、或焦点落到遮罩后面的页面上,所有权栈已经死锁。

延伸

  • 同组J3.02.1 焦点顺序需与视觉顺序一致 · J3.02.2 浮层需捕获焦点并可退出 · J3.02.3 无法退出的焦点陷阱是阻断性缺陷 · J3.02.4 动态插入的元素若不显式管理焦点,会默认停留在原处造成脱节 · J3.02.5 焦点顺序应随可见的动态变化(如展开、隐藏)实时更新 · J3.02.6 检测焦点顺序问题需要实际用键盘遍历而非仅检查代码顺序
  • 相邻E4.10 模态对话框 · E4.12 弹出层 · J3.01 键盘可达
  • 站内检索nested modal · focus stack · stacked dialog

同组卡片

快捷操作

分享

分享当前页面

ios_share

https://hci.top/zh/handbook/J3.02.7