Skip to content

feat(dialog): restoreFocus β€” a fallback focus home for the close - #53

Open
ivanbanov wants to merge 1 commit into
mainfrom
feat/dialog-restore-focus
Open

feat(dialog): restoreFocus β€” a fallback focus home for the close#53
ivanbanov wants to merge 1 commit into
mainfrom
feat/dialog-restore-focus

Conversation

@ivanbanov

Copy link
Copy Markdown
Member

What

Dialog.Content gains restoreFocus, the close-side counterpart to initialFocus. Closing still restores focus to whatever held it before the dialog opened β€” but only when that element can meaningfully take focus back. When focus sat on the body (a pointer press can leave it there) or on an element removed from the document since, the restore falls back to the consumer-designated target, typically the trigger. With neither, focus stays where it is.

Where

  • @dunky.dev/dom-dialog β€” the decision lives once in openDialogLayer: a new restoreFocus?: () => HTMLElement | null option, resolved at close, plus the meaningful-previous check (instanceof HTMLElement, not the body, still connected) in the disposer.
  • @dunky.dev/react-dialog β€” restoreFocus?: RefObject<HTMLElement | null> on Dialog.Content, read through a ref at close time.
  • @dunky.dev/solid-dialog β€” restoreFocus?: HTMLElement | (() => HTMLElement | null | undefined) on Dialog.Content, same shape as initialFocus, resolved at close time.

The bindings add no behavior β€” they only plumb their host idiom into the DOM option, per the one-way boundary.

Why

Both fallback cases previously dropped focus silently: restoring to the body goes nowhere, and focus() on 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.

Coverage

  • dom-dialog: priority of the meaningful previous over the designated target; body fallback; removed-holder fallback; no-target no-op.
  • react/solid: the prop plumbed end to end (body-focused open, close restores to the designated element).
  • SPECs updated: the open-edge restore contract in dom-dialog, prop tables in both substrates. Changeset: minor Γ— 3.

Full suite green: 16 files, 281 tests. Typecheck (base + solid), oxlint, oxfmt clean.

πŸ€– Generated with Claude Code

Closing restores focus to whatever held it before opening; when that
holder can't meaningfully take focus back β€” the body (a pointer press
leaves focus there), or an element since removed from the document β€” the
restore now falls back to a consumer-designated target, typically the
trigger. Before, both cases silently dropped focus.

The decision lives once in openDialogLayer (restoreFocus?: () =>
HTMLElement | null, resolved at close); the React and Solid bindings only
plumb their idioms β€” a ref, an element-or-accessor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant