feat: CalendarPreview date picker composition - #896
Conversation
PR 3 of 7. Adds `.Trigger`, `.Content` and `.Input`, and the root's `open` / `defaultOpen` / `onOpenChange` forwarding Base UI's own typed details. The picker is not an export — it is these parts composed, and the composition lives in the docs. Focus-to-open could not arrive the way the RFC assumed. Base UI 1.7.0 has no `openOnFocus`: `Popover.Trigger` wires only `useClick` and hover, and `useFocus` is unexported floating-ui internals. So focus-to-open is a handler — but a single one, on `.Trigger`, reporting through Base UI's own `trigger-focus` reason. `.Input` never touches open state. Driving real Chrome over CDP with trusted input showed that handler alone reproducing the exact race the rewrite exists to kill: a click gave `trigger-focus` then `trigger-press` closing it then `trigger-focus` again, and Escape closed and instantly reopened because Base UI hands focus back to the trigger. Synthetic DOM events had reported all of this as passing, which is the jsdom-shaped false negative the RFC warns about. Two guards fix it, both taken from floating-ui's own `useFocus`: skip the focus-open while a pointer press is in flight, since `useClick` is already going to open it; and skip the one focus that follows a close caused by Escape or a press on the trigger. The first tracks the pointer, the second the last close reason — neither mirrors open state, and neither touches dismissal, which stays entirely Base UI's. No file in `calendar-preview/` listens on the document. `.Input` parses with `parseScaleInput` and renders through the root's `formatValue`. Typing emits nothing; Enter, blur and the blur an outside click causes all commit. Coarser scales parse but are refused until the scale views land, rather than committing a day the user never typed. Validity is reported through `onValidityChange`, which needs to tell a bound from a consumer rejection, so the root now carries `minDate` and `maxDate` on its context alongside the predicate that folds them. Verified with real browser input: 1 click opens: true, single trigger-press, no flicker 2 escape closes: true, no reopen 3 Tab focus opens: true, single trigger-focus 4 outside press closes: true, via focus-out 5 Enter commits: Thu May 20 2027 6 outside-click commits: Tue Feb 01 2028 7 selects mounted: 0 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Team Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
commit: |
Summary
PR 3 of 7 in the RFC 005 stack, on top of #895.
Adds the date picker — which is not an export. It's three parts composed, and the composition lives in the docs.
.Trigger,.Content,.Input, rootopen/defaultOpen/onOpenChangeuse-picker-popover.tsgoes when the old family doesChanges
.Triggerbutton— it wraps a control.ContentPopover.Content. Takes its props; flips on collision.InputInput. Parses viaparseScaleInput, displays viaformatValue, reportsonValidityChangeopen/defaultOpen/onOpenChange, forwarding Base UI's typed details — not a re-declared{ reason?: string }.commitprop.May 2027,Q4 2026) parse but are refused until PR 5, rather than committing a day nobody typed.minDate/maxDatejoin the context so.Inputcan separateout-of-boundsfromunavailable.trailingIcon={null}— composition, not a prop.Technical Details
The RFC's mechanism doesn't exist. It assumed focus-to-open would arrive as a Base UI trigger option:
openOn*in Base UI 1.7.0openOnHover,openOnInputClick,openOnArrowKeyDown— noopenOnFocusPopover.TriggerwiringuseClick+ hover onlyuseFocusfloating-ui-reactinternalsSo it's a handler — but one, on
.Trigger, reporting Base UI's owntrigger-focus..Inputnever touches open state.jsdom passed; real input didn't. Driving Chrome over CDP with trusted events (no new dependency — Node 24 ships a global
WebSocket):trigger-focus→trigger-presscloses →trigger-focusreopensThat's the race this rewrite exists to kill, and the jsdom-shaped false negative the RFC warns about.
Two guards, both floating-ui's own rules from
useFocus:useClickis already going to open itescapeKey/triggerPresscloseNeither mirrors open state. Neither touches dismissal.
Nothing here dismisses anything.
Outside press, escape and focus-out are all
Popover.Root's — no document listener, no swallowed trigger-press close, no open-state mirror. Those three are what madeuse-picker-popover.ts185 lines.Test Plan
Real browser, trusted input — the EXIT criterion:
trigger-press, no flickertrigger-focusfocus-outThu May 20 2027Tue Feb 01 2028picker.test.tsxcalendar-preview/totalbiome check/tsc --noEmitbuild:apsara/ docs buildCovered — focus opens with one event and no re-close; commit on Enter / blur / outside click, each asserted separately; nothing emitted while typing; partial input stays visible; all three day formats; coarser scale refused; clear on empty;
formatValuedisplay; everyonValidityChangereason plus no re-fire on consecutive invalid keystrokes; disabled and readOnly; controlledopen; trigger renders nobutton; zero Selects open or closed.Review notes
minDate/maxDate.Inputmust distinguish the two invalid reasons; the existing predicate folds themas neveron the trigger refRef<HTMLButtonElement> & Ref<HTMLElement>— an intersection no single ref satisfies. Ours is always adivSQL Safety (if your PR touches
*_repository.goorgoqu.*)Not applicable — TypeScript and CSS only. No Go files, no database access.