Skip to content

feat: CalendarPreview date picker composition - #896

Open
Shreyag02 wants to merge 1 commit into
feat/calendar-preview-basefrom
feat/calendar-preview-datepicker
Open

feat: CalendarPreview date picker composition#896
Shreyag02 wants to merge 1 commit into
feat/calendar-preview-basefrom
feat/calendar-preview-datepicker

Conversation

@Shreyag02

@Shreyag02 Shreyag02 commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

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.

<CalendarPreview value={date} onValueChange={setDate}>
  <CalendarPreview.Trigger>
    <CalendarPreview.Input />
  </CalendarPreview.Trigger>
  <CalendarPreview.Content>
    <CalendarPreview.Days />
  </CalendarPreview.Content>
</CalendarPreview>
In .Trigger, .Content, .Input, root open / defaultOpen / onOpenChange
Not in Range (PR 4), scale switcher + period views (PR 5)
Deleted Nothing — use-picker-popover.ts goes when the old family does

Changes

Part Notes
.Trigger Anchors the popover, owns opening. Renders the value or placeholder when childless. Never a button — it wraps a control
.Content Apsara Popover.Content. Takes its props; flips on collision
.Input Apsara Input. Parses via parseScaleInput, displays via formatValue, reports onValidityChange
  • Root gains open / defaultOpen / onOpenChange, forwarding Base UI's typed details — not a re-declared { reason?: string }.
  • Typing emits nothing. Enter, blur, and the blur an outside click causes commit. No Apply button, no commit prop.
  • Coarser scales (May 2027, Q4 2026) parse but are refused until PR 5, rather than committing a day nobody typed.
  • minDate / maxDate join the context so .Input can separate out-of-bounds from unavailable.
  • Docs: Basic, Disabled, Disabled dates, Without calendar icon, With Field, Custom trigger. No-icon is 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:

Checked Result
openOn* in Base UI 1.7.0 openOnHover, openOnInputClick, openOnArrowKeyDownno openOnFocus
Popover.Trigger wiring useClick + hover only
useFocus exists, unexported floating-ui-react internals

So it's a handler — but one, on .Trigger, reporting Base UI's own trigger-focus. .Input never 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):

Case Synthetic Real trusted input
Click input pass trigger-focustrigger-press closestrigger-focus reopens
Escape pass closes, then instantly reopens — Base UI returns focus to the trigger

That'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:

Guard Tracks Why
Skip focus-open during a pointer press the pointer useClick is already going to open it
Skip the focus after an escapeKey / triggerPress close last close reason that focus is Base UI handing the trigger back, not the user asking again

Neither mirrors open state. Neither touches dismissal.

Nothing here dismisses anything.

$ grep -rn "addEventListener|mouseup|mousedown|outsideClick" components/calendar-preview/
NONE

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 made use-picker-popover.ts 185 lines.

Test Plan

  • Manual testing completed
  • Build and type checking passes

Real browser, trusted input — the EXIT criterion:

# Case Result
1 Click opens single trigger-press, no flicker
2 Escape closes no reopen
3 Tab focus opens single trigger-focus
4 Outside press closes via focus-out
5 Enter commits Thu May 20 2027
6 Outside-click commits Tue Feb 01 2028
7 Selects mounted 0
Check Result
New picker.test.tsx 29 passed
calendar-preview/ total 385 passed
Full package suite 3092 passed, 1 skipped (skip predates this stack)
biome check / tsc --noEmit clean
build:apsara / docs build both green

Covered — 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; formatValue display; every onValidityChange reason plus no re-fire on consecutive invalid keystrokes; disabled and readOnly; controlled open; trigger renders no button; zero Selects open or closed.

Review notes

Thing Why
Context grew by minDate / maxDate .Input must distinguish the two invalid reasons; the existing predicate folds them
One as never on the trigger ref Base UI types it Ref<HTMLButtonElement> & Ref<HTMLElement> — an intersection no single ref satisfies. Ours is always a div

SQL Safety (if your PR touches *_repository.go or goqu.*)

Not applicable — TypeScript and CSS only. No Go files, no database access.

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>
@vercel

vercel Bot commented Sep 5, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated
apsara Ready Ready Preview Sep 5, 2026 9:20am UTC

@coderabbitai

coderabbitai Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: 6d6b604a-e1c3-4c44-afb0-0a1e3d0bf4da

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pkg-pr-new

pkg-pr-new Bot commented Sep 5, 2026

Copy link
Copy Markdown

Open in StackBlitz

pnpm add https://pkg.pr.new/@raystack/apsara@896

commit: bcc77ec

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