Skip to content

feat: CalendarPreview root and inline calendar - #895

Open
Shreyag02 wants to merge 2 commits into
chore/calendar-deps-upgradefrom
feat/calendar-preview-base
Open

feat: CalendarPreview root and inline calendar#895
Shreyag02 wants to merge 2 commits into
chore/calendar-deps-upgradefrom
feat/calendar-preview-base

Conversation

@Shreyag02

Copy link
Copy Markdown
Contributor

PR 2 of 7 in the RFC 005 stack — RFC 005: Calendar Rewrite CalendarPreview. Stacked on #894.

Delivers the inline calendar: a root that owns the state, and eight parts that render it.

<CalendarPreview>
  <CalendarPreview.Days />
</CalendarPreview>

No popover, no input. .Trigger, .Content and .Input are PR 3; selection='range' is PR 4; the scale switcher and the four period views are PR 5. A test asserts the exported part list exactly, so one of them appearing early fails the build rather than shipping as public API by accident.

The two things worth reading the diff for

.Reset is a value reset, keyed off defaultDate

It renders only when there is something to restore — defaultDate is set and the current value differs from it — and clicking it commits the default day and leaves the visible month exactly where the user left it. It is not a view reset.

defaultDate is a separate prop from defaultValue for a specific reason: useControlled ignores defaultValue the moment value is passed, so a controlled consumer would never see .Reset at all. There is a test for precisely that — reset renders and works under a controlled value — alongside the four visibility cases and one asserting onMonthChange never fires.

The caption dropdown is our own scroller, with no Select mounted

.Caption dropdown opens a two-column month + year scroller built from plain buttons in a popup we own. It is not react-day-picker's dropdown and not Apsara Select.

That is the whole point. A Select portal looking "outside" to a naive dismiss handler is the first of the three reasons use-picker-popover.ts exists, and 185 lines of suppression is what re-introducing one costs. The test does not take my word for it:

expect(document.body.querySelectorAll('select')).toHaveLength(0);
expect(screen.queryAllByRole('combobox')).toHaveLength(0);
expect(screen.queryAllByRole('listbox')).toHaveLength(0);
expect(document.body.querySelectorAll('[data-slot^="select"]')).toHaveLength(0);

Picking from either column moves the view; it never selects a value (asserted — onValueChange is not called).

The rest

  • Bounds limit selection, never navigation. minDate / maxDate / isDateUnavailable disable cells; the nav buttons and the scroller still go wherever the user wants. This is the behavioural break the RFC flags, and it is tested from both edges. Bounds compare as day-keys, so a minDate carrying a time of day still makes its own day selectable — the current family compares instants and silently disables it.
  • dateInfo and tooltipMessages are functions now. The record form keyed cells by a formatted string and silently missed every day once a timeZone shifted the key. Info still renders above the date number (asserted on child order, not just presence).
  • .Grid is the only file importing react-day-picker (grep-asserted). It runs with hideNavigation and captionLayout='label', and mode, selected, onSelect, required, month, onMonthChange and timeZone come from context — none is in CalendarPreviewGridProps, so nothing is force-overridden after the consumer's spread and spread-last genuinely holds for the first time in this family. RDP's required union is discriminated on a literal, which a boolean cannot narrow, so the two arms are written out rather than cast away; the union reaches no consumer.
  • Cell state beside the slot: data-selected, data-draft, data-unavailable, data-today, data-outside, data-scale. At day scale the draft is the roving-focus cell — arrowed to, not yet entered — which is the same field PR 5's scale-switch draft will write to.
  • useCalendar() ships from the barrel beside useTour and the other six: value, scale, view month, their setters, the availability predicate. Nothing else, because what it returns is semver-covered.
  • Parity with today's Calendar: tooltips, disabled days, controlled month navigation, multiple months, loading state. Loading still disables navigation — .Header and .Grid are siblings now, so that state lives on their common parent .Days rather than the root, and two day views in one tree cannot disable each other.

Two additions beyond the prop list

  • yearRange on the root. .Caption dropdown cannot offer a year column without one. It is in the RFC's Root Props; the default is ten years either side of today, stretched to cover minDate/maxDate so a picker bounded at 2050 can actually scroll there — a year the user can never reach would be a trap.
  • monthNames, formatDayLabel, formatMonthLabel, formatCaptionLabel, shiftMonths, monthStart added to date-adapter.ts. Every one is a date-library call, and the adapter is the only module allowed to make them. No dayjs.extend() anywhere.

formatValue is plumbed into context but has no visible consumer until .Trigger and .Input land in PR 3. Its default is tested directly as a pure function.

Rules

Zero slotProps. Zero biome-ignore. No forwardRef — plain function components with ref as a prop. <Ctx value> throughout, never <Ctx.Provider>. Every part takes render, className, ref and a data-slot, spreads ...props last, and has a displayName — all four asserted per part rather than described. CSS uses --rs-* tokens only, with zero Todo: var does not exist; the only literals left are 1px hairlines, which the space scale starts above.

components/calendar/ is untouched.

Testing

  • data-slots.test.tsx asserts the slot set is exactly the documented one — extras and typos fail as loudly as renames — plus every state data-* on every rendered element.
  • 349 tests in the directory. Full package suite green: 3056 passed, 1 skipped (pre-existing).
  • 100% statement, function and line coverage on components/calendar-preview/; 99.6% of branches. The single uncovered branch is a defensive guard in the root's reset, unreachable while .Reset is its only caller.
  • pnpm --filter @raystack/apsara build clean; pnpm --filter www build green; biome check clean; tsc --noEmit reports only the 6 errors that predate this stack, none in calendar/ or calendar-preview/.

Note

The two screenshots on the request did not reach me as images, so the caption scroller and header layout follow the RFC's description and existing library precedent rather than a pixel target. Happy to adjust the visual details if they differ.

🤖 Generated with Claude Code

Phase 1 of RFC 005. Delivers the inline day view — a root that owns the
state and eight parts that render it. No popover, no input: `.Trigger`,
`.Content` and `.Input` land in PR 3.

    <CalendarPreview>
      <CalendarPreview.Days />
    </CalendarPreview>

The root renders no DOM of its own. It holds value, view month and scale
through `useControlled`, and hands them to the parts through a part-aware
context hook whose generic value is stored as `unknown` and cast once, at
the hook boundary. A part used outside the root throws a message naming
the part the author actually wrote, not this file.

Two things the RFC singles out:

`.Reset` is a **value** reset, keyed off `defaultDate`. It renders only
when there is something to restore — `defaultDate` is set and the value
differs — and it leaves the visible month alone. `defaultDate` is a
separate prop from `defaultValue` precisely because `useControlled`
ignores `defaultValue` once `value` is passed, so a controlled consumer
would otherwise never see the part at all.

`.Caption`'s dropdown is **our own scroller**: two columns of plain
buttons in a popup we own. No `Select` is mounted anywhere in this
component — asserted, not asserted-to — which is what keeps the
popover-dismissal loop `use-picker-popover.ts` spends 185 lines
suppressing from coming back. Picking from it moves the view; it never
selects a value.

Everything else worth naming:

- Bounds limit **selection**, never navigation. `minDate`/`maxDate` and
  `isDateUnavailable` disable cells; the nav buttons and the caption
  scroller still move the view wherever the user wants. Bounds compare as
  day-keys, so a `minDate` carrying a time of day still makes its own day
  selectable — the current family compares instants and silently disables
  it
- `dateInfo` and `tooltipMessages` are now functions. The record form
  keyed cells by a formatted string and silently missed every day once a
  `timeZone` shifted the key. Info still renders above the date number
- `.Grid` is the only file importing react-day-picker. It runs with
  `hideNavigation` and `captionLayout='label'`, and `mode`, `selected`,
  `onSelect`, `required`, `month`, `onMonthChange` and `timeZone` come
  from context rather than props — none is in `CalendarPreviewGridProps`,
  so nothing is force-overridden after the consumer's spread and
  spread-last holds for the first time in this family
- Cells carry `data-selected`, `data-draft`, `data-unavailable`,
  `data-today`, `data-outside` and `data-scale` beside their slot. At day
  scale the draft is the roving-focus cell — arrowed to, not yet entered
- `useCalendar()` ships from the barrel beside `useTour` and the other
  six, returning value, scale, view month, their setters and the
  availability predicate. Nothing more: what it returns is semver-covered

Zero `slotProps`, zero `biome-ignore`, no `forwardRef`, `<Ctx value>`
throughout, every part spreads `...props` last, and the CSS carries no
`Todo: var does not exist`.

`components/calendar/` is untouched.

100% statement, function and line coverage on the new directory, 99.6% of
branches — the one uncovered branch is a defensive guard in the root's
`reset`, unreachable while `.Reset` is the only caller.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 4, 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 4, 2026 5:34am UTC

@coderabbitai

coderabbitai Bot commented Sep 4, 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: c815924a-d168-4fb3-85aa-0f93826baec7

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 4, 2026

Copy link
Copy Markdown

Open in StackBlitz

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

commit: d14913d

The parts landed in the previous commit with placeholder chrome. This
matches them to reference A, and finishes the UndoIcon that commit left
imported but unmapped.

Single month: the caption moves to the left and the reset joins the two
nav buttons on the right, drawn as the undo glyph. Source order is the
visual order, so nothing reorders in CSS and tab order follows the row.

Several months: there is no single header to hold one caption, so each
month captions itself. `.Days` drops the header above the grid and binds
react-day-picker's MonthCaption slot instead — previous on the first
month, next on the last, a spacer holding the absent button's place so
every caption centres on its own grid. No reset in this layout; the
Calendar Header component in the file has three variants and none of the
two-month ones carries it.

Also from the frames: weekday headings go to three letters, the caption
abbreviates to `Apr 2024`, the scroller lists `Jan`/`Feb`/`Mar`, its
chip and selected row take neutral grey rather than accent, and the
popover anchors to the caption's start edge over the grid.

`showOutsideDays` now defaults to false. Every new frame ends its grid on
the last day of the month with the leading cells blank, and no frame
shows an outside day. This diverges from today's DatePicker, which is
priced in — the rewrite ships no shim.

One frame detail is deliberately not encoded: April 1-16 render muted
with today on the 17th, which a `minDate` demonstration and a built-in
past bound draw identically. Read as a demonstration, so no default
bound is applied — a past bound is not expressible as an opt-out and
would break date-of-birth and "filter since" fields. Flagged for design.

Co-Authored-By: Claude Opus 5 (1M context) <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