Skip to content

fix(date-picker): make outside-dismissal focus restore consistent, add restoreFocus prop - #3241

Open
davevanhoorn wants to merge 1 commit into
chakra-ui:mainfrom
davevanhoorn:fix/date-picker-restore-focus
Open

fix(date-picker): make outside-dismissal focus restore consistent, add restoreFocus prop#3241
davevanhoorn wants to merge 1 commit into
chakra-ui:mainfrom
davevanhoorn:fix/date-picker-restore-focus

Conversation

@davevanhoorn

Copy link
Copy Markdown

📝 Description

Fixes two related issues with focus restoration when the date picker is dismissed by interacting outside:

  1. Inconsistent focus restore across cycles. onInteractOutside records restoreFocus into a batched bindable, but the close transition's shouldRestoreFocus guard reads it synchronously during the same dismissal — one cycle stale, and never cleared. The first outside-click dismissal after mount never restores focus; every later dismissal applies the previous cycle's decision.

  2. No way to opt out. The restore decision lives in private context. The popover machine already exposes restoreFocus as a public prop; the date picker exposes nothing — so consumers whose forms validate on blur can't make an outside click actually blur the field (the restored focus keeps the widget focused, and blur-driven validation doesn't fire until a second click).

⛳️ Current behavior (updates)

  • Open the picker, click outside on a non-focusable target → focus is not restored to the trigger.
  • Reopen and dismiss the same way → focus is restored.

Every dismissal uses the previous dismissal's !event.detail.focusable value, and the very first uses the default. Reproduced in the new e2e test, which fails on main at cycle 1.

🚀 New behavior

  • restoreFocus moves from bindable context to refs — it drives no rendering, and refs read synchronously, so the guard sees the value written during the same dismissal. The flag also resets when the picker opens, so cycles can't leak into each other.
  • New public restoreFocus?: boolean prop (same name and shape as the popover machine's existing prop): unset preserves the current heuristic (restore only when the outside target is not focusable); false keeps focus where the user clicked; true always restores. Keyboard dismissal is unaffected — it still always restores focus via the separate setRestoreFocus action.

Prior art for the opt-out: the native popover's light dismiss never restores focus (the hide-popover algorithm runs with focusPreviousElement false), and Radix's non-modal popover skips the trigger refocus when an outside interaction caused the close. Form fields that validate on blur need the dismissal click to actually blur the field.

💣 Is this a breaking change (Yes/No):

No. The default heuristic and public API defaults are unchanged. One behavior note (it is the bug fix): the first dismissal after mount now restores focus like every later one — previously it never did. Called out in the changeset.

📝 Additional Information

  • New e2e coverage: a consistency test (two open → outside-click cycles must behave identically; fails on main where cycle 1 and cycle 2 diverge) and a restoreFocus={false} test against a new date-picker/restore-focus example page.
  • Full date-picker e2e suite: 38 passed, 3 flaky-on-retry — the same pre-existing typed-input timing flakes noted in fix(date-input): commit deferred entry on segment leave #3210's test report.
  • tsc --noEmit and eslint src clean for the package.
  • Happy to rename the prop (e.g. restoreFocusOnInteractOutside) if you'd rather the name carry the scoping instead of the JSDoc.

@changeset-bot

changeset-bot Bot commented Jul 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a1e5dce

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 86 packages
Name Type
@zag-js/date-picker Minor
@zag-js/anatomy-icons Minor
@zag-js/anatomy Minor
@zag-js/core Minor
@zag-js/docs Minor
@zag-js/preact Minor
@zag-js/react Minor
@zag-js/solid Minor
@zag-js/svelte Minor
@zag-js/vanilla Minor
@zag-js/vue Minor
@zag-js/accordion Minor
@zag-js/angle-slider Minor
@zag-js/async-list Minor
@zag-js/avatar Minor
@zag-js/carousel Minor
@zag-js/cascade-select Minor
@zag-js/checkbox Minor
@zag-js/clipboard Minor
@zag-js/collapsible Minor
@zag-js/color-picker Minor
@zag-js/combobox Minor
@zag-js/date-input Minor
@zag-js/dialog Minor
@zag-js/drawer Minor
@zag-js/editable Minor
@zag-js/file-upload Minor
@zag-js/floating-panel Minor
@zag-js/hover-card Minor
@zag-js/image-cropper Minor
@zag-js/listbox Minor
@zag-js/marquee Minor
@zag-js/menu Minor
@zag-js/navigation-menu Minor
@zag-js/number-input Minor
@zag-js/pagination Minor
@zag-js/password-input Minor
@zag-js/pin-input Minor
@zag-js/popover Minor
@zag-js/presence Minor
@zag-js/progress Minor
@zag-js/qr-code Minor
@zag-js/radio-group Minor
@zag-js/rating-group Minor
@zag-js/scroll-area Minor
@zag-js/select Minor
@zag-js/signature-pad Minor
@zag-js/slider Minor
@zag-js/splitter Minor
@zag-js/steps Minor
@zag-js/switch Minor
@zag-js/tabs Minor
@zag-js/tags-input Minor
@zag-js/timer Minor
@zag-js/toast Minor
@zag-js/toc Minor
@zag-js/toggle-group Minor
@zag-js/toggle Minor
@zag-js/tooltip Minor
@zag-js/tour Minor
@zag-js/tree-view Minor
@zag-js/store Minor
@zag-js/types Minor
@zag-js/aria-hidden Minor
@zag-js/auto-resize Minor
@zag-js/collection Minor
@zag-js/color-utils Minor
@zag-js/utils Minor
@zag-js/date-utils Minor
@zag-js/dismissable Minor
@zag-js/dom-query Minor
@zag-js/file-utils Minor
@zag-js/focus-trap Minor
@zag-js/focus-visible Minor
@zag-js/highlight-word Minor
@zag-js/hotkeys Minor
@zag-js/i18n-utils Minor
@zag-js/interact-outside Minor
@zag-js/json-tree-utils Minor
@zag-js/live-region Minor
@zag-js/popper Minor
@zag-js/rect-utils Minor
@zag-js/remove-scroll Minor
@zag-js/scroll-snap Minor
@zag-js/stringify-state Minor
svelte-kit-starter Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
zag-nextjs Ready Ready Preview Jul 30, 2026 10:57am
zag-solid Ready Ready Preview Jul 30, 2026 10:57am
zag-svelte Ready Ready Preview Jul 30, 2026 10:57am
zag-vue Ready Ready Preview Jul 30, 2026 10:57am
zag-website Ready Ready Preview Jul 30, 2026 10:57am

Request Review

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