fix: harden the layering utils — back guards, containment, focus trap, scroll lock - #48
Merged
Conversation
… releases and remove guards by identity Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…="false", warn on a silent focus miss Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s, one stop per radio group Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, setProperty restore, null target locks nothing Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ivanbanov
added a commit
that referenced
this pull request
Aug 24, 2026
main's #46 renamed @dunky.dev/dom-navigation to @dunky.dev/browser-navigation and #48 hardened the same interceptor this branch extends. Two conflicts, both unions rather than choices: - detachWhenIdle: the listener now outlives armed guards, parked watchers, in-flight self-caused pops, AND undecided releases. Dropping the parked check would end a Forward watch the moment its layer closed. - The unwind loop: main removes the guard by identity (onBack may have released it, and a positional pop would evict the guard beneath). Parking now happens only when that removal actually found the guard — a layer that released itself inside onBack tore itself down rather than closing, so there is nothing to offer a reopen to. New test and SPEC clause for it. The changeset follows the rename. Full suite (240), typecheck, lint, format, and build pass; SPECs reconciled against the merged code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Review-driven fixes across the four layering DOM utils and their consumers. All behavior-first: every fix landed as a failing test before the source change.
browser-navigation
popstatelistener while the second release's self-causedhistory.back()was still in flight, leaving the next guard deaf to its first Back press. ApendingReleasescounter now keeps the listener alive until every deferred consumption has run.onPopStateremoved the answered guard positionally withpop(). When a guard'sonBackreleased that same guard synchronously (legal API use), the pop evicted the guard beneath it instead. Removal is now by identity.dom-overlay
hideExitingLayerwith a boundary that isn't an ancestor of the content walked to the document root and inerted<html>, taking the whole page out for the exit window. The walk now detects exhaustion and scopes the miss to the content itself.aria-hidden, butaria-hidden="false"asserts visible — the opposite of author-hidden — so such elements stayed exposed behind a modal. The skip now tests the value, and the undo restores the authored value instead of removing the attribute.dom-dialog
openDialogLayernow warns that the window needstabindex="-1".dom-focus-trap
bodyescaped to native tab order. It now binds to the document in the capture phase (bubble delivery can be cut off bystopPropagationin the subtree) and readsownerDocument.activeElementfor iframe safety.getFocusableswas selector-only, sohidden/display: none/visibility: hiddenelements entered the cycle and stalled the trap in a real browser. Candidates are now filtered withelement.checkVisibility(); jsdom's missing implementation is shimmed in a sharedvitest.setup.ts, keeping the package source browser-spec-only.dom-scroll-lock + use-scroll-lock (react, solid)
padding-inline-end/padding-block-end, erasing padding the target already had. It now adds the footprint to the computed padding.setPropertycalls — per CSSOM a saved''removes the declaration, so no branching.nullinto "the body", souseScrollLock(true, ref.current)locked the page on the first run and never corrected itself.nullnow means "no target yet" and locks nothing;undefinedstill means the body. The Dialog bindings, whose context usesnullfor "page dialog → body", map it toundefinedat the call site.Six changesets (all patch). Full suite: 219/219, typecheck, lint, and format clean.
🤖 Generated with Claude Code