You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
摘要
Web UI 的弹层没有统一的焦点与 Escape 协调:
primitives Modal与 Settings 面板(SettingsPanel)是两套独立实现,各自向 document 注册自己的 keydown 监听器(一个 Escape 事件会触发所有弹层的onClose),且两者都没有任何焦点圈禁(无 Tab 循环、无背景 inert)——Shift+Tab可以把焦点移出aria-modal="true"对话框到背后控件,关闭后焦点也不会恢复到触发元素。复现步骤
Shift+Tab两次:焦点依次移到 copy 弹窗的 Close、再移到 Settings 面板背后的"关闭详情"控件 —— 焦点逃逸出对话框。Escape:copy-preset 弹窗 和 Settings 面板同时关闭 —— 一个 Escape 事件关闭了两层。根因(源码定位,rc.6)
primitives
Modal:独立 document 监听 + 无焦点管理 —@deepseek-ai/dsh-client-ui-primitives/lib/index.js:useEffect中document.addEventListener("keydown", onKeyDown),if (e.key === "Escape") onClose()—— 每个 Modal 实例挂载期间都占用 document 级监听;role="dialog"/aria-modal="true"(L2003-2004),没有焦点圈禁逻辑、没有对背景层的 inert/aria-hidden 处理、没有关闭后的焦点恢复。Settings 面板(
SettingsPanel):另一套同样的独立实现 —@deepseek-ai/dsh-client-ui-settings-general/lib/client.js:document.addEventListener("keydown", onKeyDown)、Escape →onClose(L94-95 注释明示 "document-level Escape (mounted only while open)");role="dialog"+aria-modal="true"面板,同样无 Tab 循环(仅在打开时聚焦一次 close 按钮,L110-112)。两个监听器同时收到同一个 Escape 事件:Settings 面板与 copy-preset Modal 各自持有 document keydown 监听,事件冒泡到 document 后两处
onClose都被调用 → 一层 Escape 关闭两层(与复现完全吻合);两个弹层都没有 Tab 焦点圈禁 → 焦点自由移出对话框(与复现完全吻合)。建议修复
方案 A(推荐)· 引入全局弹层栈协调:一个共享的 modal stack 服务/context —— 每个弹层注册进栈;
Escape只派发到栈顶弹层;焦点圈禁(Tab/Shift+Tab 在栈顶 dialog 内循环)、打开时聚焦首个可聚焦元素、关闭后把焦点还给触发元素。SettingsPanel与primitives Modal都接入该栈,消除两套各自为政的监听。方案 B · 分别补齐两处实现:primitives Modal 增加焦点陷阱 + 打开聚焦/关闭恢复;SettingsPanel 同步改造。改动局部但两处仍需保持一致(协调层缺失问题依旧,后续新弹层还会重蹈覆辙)。
方案 C · 背景 inert + 顶层 Escape 拦截:弹层打开时给背景层设
inert(阻止焦点进入,缓解逃逸);Escape 监听改为"仅当自己是栈顶时响应"。可作为最小补丁,但焦点恢复与栈管理仍需 A 的方案才能完整。影响
aria-modal="true"语义被破坏——屏幕阅读器/键盘用户焦点可进入背后控件,与无障碍契约不符;环境
验证材料
dsh-client-ui-primitives/lib/index.jsL1983-2034 与dsh-client-ui-settings-general/lib/client.jsL97-124;First analysis of discussion #938. Happy to open a PR with fix option A.
All reactions