fix(dialog): non-modal coexistence, drag-safe outside presses, accessible-name warning - #54
Open
ivanbanov wants to merge 2 commits into
Open
fix(dialog): non-modal coexistence, drag-safe outside presses, accessible-name warning#54ivanbanov wants to merge 2 commits into
ivanbanov wants to merge 2 commits into
Conversation
…arning Three dialog fixes, decided once in dom-dialog and inherited by both DOM substrates: - A non-modal Viewport no longer swallows presses aimed at the page: it renders pointer-events: none (the window stays auto), and outside presses move to a document-level watch — topmost-only, window and trigger excepted — since a transparent Viewport can't detect them itself. - A text-selection drag no longer dismisses: trackPressOrigin captures where a press began at pointerdown, before the browser collapses the click target to the common ancestor, and the press gates refuse a gesture that started inside the window. - A dialog that resolves no accessible name warns on open — the core contract (Title, or aria-label/aria-labelledby, never neither) was documented but unenforced. Deferred a macrotask so a rendered Title's registration commits first; cancelled by close. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ss parts The drag guard and the document-level outside-press watch were born in dom-dialog but nothing about them is dialog's: every light-dismissable layer needs both. Pre-positioned where the next primitive will look — - trackPressOrigin -> @dunky.dev/dom-press-origin, a new util. It imports nothing; the origin answer feeds any outside-press decision. - watchOutsidePress -> @dunky.dev/dom-overlay, next to the stack it queries (topmost-only is its gating), with layer-generic vocabulary (element, not content). viewportPointerEvents / contentPointerEvents stay in dom-dialog: they are values for dialog parts. The substrates now import the utils directly, per the boundary; dom-dialog re-exports nothing it doesn't own. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Member
Author
|
Follow-up commit: the reusable parts moved to their long-term homes before review — |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three dialog fixes from a behavior audit against the SPECs. Each decision lives once in
@dunky.dev/dom-dialog; the React and Solid bindings only plumb refs and lifecycles.1 · A non-modal dialog no longer blocks the page
Dialog.Viewportis a full-coverage layer, so even with no backdrop rendered it swallowed every press aimed at the page beneath — the page element never received it, and the dialog dismissed instead, against the core contract that a non-modal dialog coexists with the page. Whilemodal={false}the Viewport now renderspointer-events: noneand the windowpointer-events: auto(the consumer's ownstylewins over both). Detection moves with it: a transparent Viewport never receives the press it used to detect, sowatchOutsidePressobserves at the document level — the only vantage point that sees both the portaled dialog and the page — with the same refusals as the element path: topmost dialog only, the window excepted, and the trigger excepted so its own press stays a plain toggle instead of a close-and-reopen.2 · A text-selection drag no longer dismisses
A mousedown inside the window with a mouseup outside it makes the browser fire
clickon their common ancestor — the viewport or backdrop — indistinguishable from a genuine outside press by the click's target alone. Selecting text across the window's edge closed the dialog and lost the form state with it. Where a press began now decides:trackPressOrigincaptures each press's origin atpointerdown, before the collapse, andacceptsBackdropPress/acceptsViewportPresstake astartedInsideargument and refuse a gesture that started inside the window.3 · A nameless dialog warns
The core contract — a rendered Title, or
aria-label/aria-labelledbyon Content, never neither (APG: dialogs require an accessible name) — was documented but unenforced.openDialogLayernow checks the window's own attributes on open and warns with the fix, the same loud treatment the stranded-focus miss already gets. The check reads the DOM rather than the machine's Title-presence flag (a label may arrive through a prop spread), defers a macrotask so a rendered Title's registration commits first, and is cancelled by the close.Cross-scope
closeOnInteractOutsidedescriptions updated to match the new mechanism.trackPressOrigin/watchOutsidePressunits, and the name-warning cases. Two test-infra bugs fixed along the way: the name-check timer leaked past close (now cleared in the disposer), and both substrate test files accumulatedconsole.warnspy history across tests (nowrestoreAllMocksper test, matching the dom file's convention).Pressable's press must start and end on it) and solved the fall-through withpointerEvents="box-none".Gate: 308 tests across 16 files, typecheck (base + solid), oxlint, oxfmt — all clean.
🤖 Generated with Claude Code