Repository navigation
Replies: 2 comments 1 reply
|
已定位根因:Web UI 的弹层没有统一的焦点与 Escape 协调: |
0 replies
|
Runnable technical evidence is now available in my fork: yinren112@34e7d16 Manual verification on the rebuilt Web UI: Shift+Tab wrapped from the dialog Close button to the last enabled control inside the dialog; one Escape closed only the child copy-preset modal; focus returned to the Copy Standard opener; the Settings panel remained open. The patch is intentionally limited to the shared Modal primitive (one source file, no new dependency). |
1 reply
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Modal focus can escape to the page, and one Escape closes nested dialog layers
Summary
The shared Web UI
Modalexposes two related modal-stack problems:Shift+Tabcan move focus to controls behind it;keydownlistener, so the sameEscapeevent can close more than the topmost layer.I reproduced both behaviors in the current Web UI at commit
47f9438.Real reproduction
Environment: Windows 10, Chromium, local Web UI at
http://127.0.0.1:3080, source checkout at47f9438.Shift+Tabonce: focus moves to the copy modal's Close button.Shift+Tabagain: focus moves behind the modal to the Settings “close details” control.Escapeonce.Observed:
aria-modal="true"dialog;Escapecloses both the copy-preset modal and the Settings panel.Expected:
Tab/Shift+Tabstay within the topmost modal;Escapedismisses only the topmost modal;Call chain
The copy-preset flow mounts the shared
Modalinside the already-open Settings panel:Two independent document listeners then receive the same bubbling
keydownevent:Modal.tsx#L44-L51calls the child modal'sonClose()forEscape;SettingsRoot.tsx#L49-L55calls the Settings panel'sonClose()for that same event.The shared dialog declares
aria-modal="true", but has no focus trap or focus restoration:Modal.tsx#L55-L85.There is already an in-tree workaround documenting both assumptions in the directory picker: it manually makes the parent inert because “Modal has no focus trap” and guards the parent close because Escape reaches every mounted Modal listener:
DirectoryBrowser.tsx#L743-L760.Impact surface
This is a shared primitive rather than a copy-preset-only defect. The current tree has 14 production
<Modal>call sites, including workspace creation/browsing, agent presets, model/settings flows, and other nested UI paths. Any modal whose parent remains mounted can expose background controls to keyboard or assistive-technology users. Any nested layer with its own global Escape handler can also consume the same key event.The directory picker already carries local state and comments to compensate for this behavior, which is evidence that callers currently have to understand and defend against the primitive's modal-stack semantics.
Root cause
Modalvisually renders a modal overlay, but interaction ownership is global and unscoped:document, once per mounted instance;As a result, visual stacking and keyboard/event ownership diverge.
Minimum fix boundary
The smallest root-cause boundary is the shared
Modal, not each feature caller:Escapeon the dialog/overlay event path and stop propagation after the topmost modal claims it;Tab/Shift+Tabwithin that dialog while open;The Settings shell is a separate hand-built
role="dialog"; after the shared primitive is corrected, it should either adopt the same primitive or receive the same interaction semantics. The minimal patch should not add caller-specific guards to all 14 call sites—the directory picker workaround shows why that does not scale.I am preparing a runnable patch in a personal fork as technical evidence, since this repository currently routes external contributions through Discussions rather than pull requests.
All reactions