chore(release): version packages - #51
Open
github-actions[bot] wants to merge 1 commit into
Open
Conversation
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
August 26, 2026 16:43
181e716 to
d6f33a0
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
August 27, 2026 19:21
d6f33a0 to
31a2af2
Compare
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.
This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.
Releases
@dunky.dev/dom-dialog@0.3.0
Minor Changes
#53
2cc4a1bThanks @ivanbanov! -Dialog.ContentgainsrestoreFocus— the close-side counterpart toinitialFocus. Closing still returns focus to whatever held it before thedialog opened;
restoreFocusnames where it goes when that holder can'tmeaningfully take focus back: focus sat on the body (a pointer press can
leave it there), or on an element removed from the document since.
Typically the dialog's trigger.
Before, those two cases silently dropped focus: restoring to the body goes
nowhere, and focusing a disconnected element is a no-op, leaving focus
stranded on the closing layer. The element focused before opening still
always wins when it is meaningful — the fallback never overrides it. At the
DOM layer,
openDialogLayertakesrestoreFocus?: () => HTMLElement | null,resolved at close.
Patch Changes
#50
bbb04daThanks @ivanbanov! - Focus candidates barred by an ancestor are excluded: controls disabled throughfieldset[disabled]and anything inside[inert].FOCUSABLE_SELECTORand the form-field selector gate on an element's ownattributes (
input:not([disabled])), but both bars also arrive fromancestors, so a barred control satisfied the selector while a browser refuses
to focus it — silently.
In the focus trap that was a hard dead end: the Tab keydown is already
preventDefault()-ed when focus is stepped by hand, so every press recomputedthe same refused target and focus never moved again. The cycle now only holds
what a browser would actually focus, keeping the native exception that
controls in a disabled fieldset's first
legendstay enabled.In the initial-focus chain it was the quieter failure mode: the barred field
won the draw,
focus()no-opped, and focus fell to the overlay window evenwhen a viable field came later. Every candidate — designated element and form
fields alike — is now also filtered for these bars.
Both use the new
isFocusablefrom@dunky.dev/dom-element, beside theisRenderedfilter they already shared.#50
bfbe863Thanks @ivanbanov! - Initial focus now skips a candidate that didn't render.getInitialFocusfiltered[disabled]and[type="hidden"]but never askedwhether the element actually rendered. A field inside a collapsed section
satisfied the selector and won the draw;
focus()on it did nothing — and saidnothing — so focus fell back to the dialog window, with the fallback's warning
unable to fire, because from its point of view the fallback had succeeded. The
overlay opened on its window instead of the field: degraded, not broken, and
silent.
A designated
initialFocusthat hadn't rendered was worse. It went straight tothe window and skipped the form-field step entirely, contradicting the
documented "when one is set and can take focus". So
getInitialFocusnowtakes the designated element as a second argument and resolves the whole chain
in one call, filtering every step rather than just the last:
Callers that were writing
initialFocus ?? getInitialFocus(content)shouldpass the designated element in instead — the
??is what spent it on acandidate that couldn't take focus.
@dunky.dev/dom-dialogdoes this for everyDOM substrate already, so a dialog's
initialFocusinherits the fix without achange on the consumer's side.
The predicate is
isRenderedfrom@dunky.dev/dom-element, shared with thefocus trap so the two can't disagree on what counts as rendered.
Updated dependencies [
bbb04da,bfbe863,f5becd4,bfbe863,772a7df]:@dunky.dev/dom-element@0.1.0
Minor Changes
#50
6c249f9Thanks @ivanbanov! - New package:@dunky.dev/dom-element, framework-free predicates about a singleelement.
isRendered(element)answers whether an element actually rendered, and so cantake focus, be pressed, or be read out. Presence in the DOM is not enough: an
element inside a collapsed section still answers
querySelector, butfocus()on it does nothing and reports nothing.
Checked: the
hiddenattribute (hidden="until-found"included),display: noneon the element or any ancestor —displaydoesn't inherit, soancestors are walked —
visibility: hidden | collapse, and being detached.Not checked:
opacity: 0andcontent-visibility, which do render, andrendering is what decides focusability.
isFocusable(element)is the sibling facet: whether anything bars the elementfrom taking focus. Disabling and inertness also arrive from ancestors — a
control inside a
fieldset[disabled]subtree (with the native exception forits first
legend) or anything inside[inert]refusesfocus()— which aselector's own-attribute checks (
:not([disabled])) can't see. The facets aredeliberately narrow and compose; the tab order stays the caller's question.
It's a package of its own because two utils have to agree on the answers:
@dunky.dev/dom-focus-trapfilters its Tab cycle with them and@dunky.dev/dom-overlayfilters its initial-focus candidates, both guardingagainst the same silent
focus()no-op.@dunky.dev/react-dialog@0.5.0
Minor Changes
#53
2cc4a1bThanks @ivanbanov! -Dialog.ContentgainsrestoreFocus— the close-side counterpart toinitialFocus. Closing still returns focus to whatever held it before thedialog opened;
restoreFocusnames where it goes when that holder can'tmeaningfully take focus back: focus sat on the body (a pointer press can
leave it there), or on an element removed from the document since.
Typically the dialog's trigger.
Before, those two cases silently dropped focus: restoring to the body goes
nowhere, and focusing a disconnected element is a no-op, leaving focus
stranded on the closing layer. The element focused before opening still
always wins when it is meaningful — the fallback never overrides it. At the
DOM layer,
openDialogLayertakesrestoreFocus?: () => HTMLElement | null,resolved at close.
Patch Changes
2cc4a1b,bbb04da,bfbe863]:@dunky.dev/solid-dialog@0.3.0
Minor Changes
#53
2cc4a1bThanks @ivanbanov! -Dialog.ContentgainsrestoreFocus— the close-side counterpart toinitialFocus. Closing still returns focus to whatever held it before thedialog opened;
restoreFocusnames where it goes when that holder can'tmeaningfully take focus back: focus sat on the body (a pointer press can
leave it there), or on an element removed from the document since.
Typically the dialog's trigger.
Before, those two cases silently dropped focus: restoring to the body goes
nowhere, and focusing a disconnected element is a no-op, leaving focus
stranded on the closing layer. The element focused before opening still
always wins when it is meaningful — the fallback never overrides it. At the
DOM layer,
openDialogLayertakesrestoreFocus?: () => HTMLElement | null,resolved at close.
Patch Changes
2cc4a1b,bbb04da,bfbe863]:@dunky.dev/dom-focus-trap@0.1.3
Patch Changes
#50
bbb04daThanks @ivanbanov! - Focus candidates barred by an ancestor are excluded: controls disabled throughfieldset[disabled]and anything inside[inert].FOCUSABLE_SELECTORand the form-field selector gate on an element's ownattributes (
input:not([disabled])), but both bars also arrive fromancestors, so a barred control satisfied the selector while a browser refuses
to focus it — silently.
In the focus trap that was a hard dead end: the Tab keydown is already
preventDefault()-ed when focus is stepped by hand, so every press recomputedthe same refused target and focus never moved again. The cycle now only holds
what a browser would actually focus, keeping the native exception that
controls in a disabled fieldset's first
legendstay enabled.In the initial-focus chain it was the quieter failure mode: the barred field
won the draw,
focus()no-opped, and focus fell to the overlay window evenwhen a viable field came later. Every candidate — designated element and form
fields alike — is now also filtered for these bars.
Both use the new
isFocusablefrom@dunky.dev/dom-element, beside theisRenderedfilter they already shared.#50
bfbe863Thanks @ivanbanov! - The Tab cycle's rendered check now comes from@dunky.dev/dom-elementinsteadof a private copy.
@dunky.dev/dom-overlayneeds the same predicate to filter its initial-focuscandidates, and two packages answering the question separately would drift.
The check itself is unchanged in intent — a non-rendered element is a no-op to
focus, so keeping one in the cycle would stall the trap on it — but sharing it
tightens two cases:
style on one reports the property defaults rather than
none, so the displaywalk alone let it through.
display: noneancestor above the container now excludes thefocusables under it. The private copy stopped its walk at the container.
Nothing inside a hidden container can take focus either way, so this lands on
the trap's documented behavior for an empty cycle: Tab is a no-op.
Updated dependencies [
6c249f9]:@dunky.dev/browser-navigation@0.2.1
Patch Changes
#52
f5becd4Thanks @ivanbanov! -SPEC.md(shipped with the package) gains a Scenarios section: 26 compacttraces of
interceptBackNavigationandwatchSpentEntrybehavior, groupedby one layer, release and consumption, forward/claims/reload, stacked
layers, and timing edges.
The behavior contract was already fully stated, but as one mechanism per
prose bullet — nothing let a reader replay a concrete flow end to end.
Each trace is a replayable episode in the module's own vocabulary (
arm,release,Back,Forward), e.g.:Two outcomes the prose previously left implicit are now stated outright: a
deliberately released entry absorbs one Forward press (it can't be deleted,
only left to soak the traversal), and any re-plant — a new layer or a
veto's re-arm — truncates every parked Forward watch above it.
@dunky.dev/dom-overlay@0.2.1
Patch Changes
#50
bbb04daThanks @ivanbanov! - Focus candidates barred by an ancestor are excluded: controls disabled throughfieldset[disabled]and anything inside[inert].FOCUSABLE_SELECTORand the form-field selector gate on an element's ownattributes (
input:not([disabled])), but both bars also arrive fromancestors, so a barred control satisfied the selector while a browser refuses
to focus it — silently.
In the focus trap that was a hard dead end: the Tab keydown is already
preventDefault()-ed when focus is stepped by hand, so every press recomputedthe same refused target and focus never moved again. The cycle now only holds
what a browser would actually focus, keeping the native exception that
controls in a disabled fieldset's first
legendstay enabled.In the initial-focus chain it was the quieter failure mode: the barred field
won the draw,
focus()no-opped, and focus fell to the overlay window evenwhen a viable field came later. Every candidate — designated element and form
fields alike — is now also filtered for these bars.
Both use the new
isFocusablefrom@dunky.dev/dom-element, beside theisRenderedfilter they already shared.#50
bfbe863Thanks @ivanbanov! - Initial focus now skips a candidate that didn't render.getInitialFocusfiltered[disabled]and[type="hidden"]but never askedwhether the element actually rendered. A field inside a collapsed section
satisfied the selector and won the draw;
focus()on it did nothing — and saidnothing — so focus fell back to the dialog window, with the fallback's warning
unable to fire, because from its point of view the fallback had succeeded. The
overlay opened on its window instead of the field: degraded, not broken, and
silent.
A designated
initialFocusthat hadn't rendered was worse. It went straight tothe window and skipped the form-field step entirely, contradicting the
documented "when one is set and can take focus". So
getInitialFocusnowtakes the designated element as a second argument and resolves the whole chain
in one call, filtering every step rather than just the last:
Callers that were writing
initialFocus ?? getInitialFocus(content)shouldpass the designated element in instead — the
??is what spent it on acandidate that couldn't take focus.
@dunky.dev/dom-dialogdoes this for everyDOM substrate already, so a dialog's
initialFocusinherits the fix without achange on the consumer's side.
The predicate is
isRenderedfrom@dunky.dev/dom-element, shared with thefocus trap so the two can't disagree on what counts as rendered.
#49
772a7dfThanks @ivanbanov! - Page content beside a layer portalled into an app branch is now hidden by amodal layer's containment.
Containment holds a few elements out of the hiding — the topmost modal layer,
its backdrop, and the layers stacked above it — and it matched them by
ancestry, so a branch that contained one was skipped whole. Where a layer
sits is the consumer's choice:
containeron the Portal part lets it landanywhere, and when that branch also held page content, the entire branch went
unhidden — the page reachable by pointer, keyboard, and screen reader for as
long as the layer was open.
Hiding now descends from the body instead of walking up from the layer. A
branch that holds one of those retained elements is descended into rather than
spared, so the content beside it is hidden individually while the layer itself
stays reachable. A layer at or above the body is a no-op — nothing sits
outside it.
Updated dependencies [
6c249f9]:@dunky.dev/react-use-focus-trap@0.1.3
Patch Changes
bbb04da,bfbe863]:@dunky.dev/solid-use-focus-trap@0.1.2
Patch Changes
bbb04da,bfbe863]: