feat: CalendarPreview root and inline calendar - #895
Conversation
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>
|
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: |
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>
The header sat on a different grid from the days beneath it, in three compounding ways. There is no global `box-sizing: border-box` here, so the cells were sizing content-box: a day cell carried its 1px border and the user agent's 1px cell padding on top of the 40px column and came out 44px, while a weekday heading came out 42px. Two pixels per column, and by Saturday the heading stood 13px clear of the days under it. Cells now size to the border box with the agent's padding dropped, so both are the same 40px column and every column measures zero drift. The table's default 2px border-spacing ringed the grid, leaving the header wider than the columns it captions. It is now zero. Aligning the header to the column box was still optically wrong, because a weekday label is centred inside its cell rather than flush to it: the caption read as sitting left of "Sun". Both headers take 8px of inline padding, which is the bearing that label leaves. A week-number column is a gutter rather than a date column, so the single-month header adds its width on top — asked of the rendered grid through `:has()`, since `showWeekNumber` is a `.Grid` prop and the header is its sibling. Also, against the standing rules: the two spacer spans in the two-month header existed only to carry a class, so the header is three fixed grid tracks and the empty one reserves itself. `monthNames()` went dead when the caption scroller moved to abbreviations, and is deleted with its test. Comments across the module are cut back to the constraints that are not obvious from the code — file-header essays and JSDoc restating a signature are gone, and `date-adapter.ts` loses 89 lines without losing a fact. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Covers what this PR ships: composition and children overriding a part's computed content, `.Reset` keyed off `defaultDate`, selection bounds that never clamp navigation, grid layout with outside days off by default, `dateInfo` and `tooltipMessages` as functions, and the caption's own month and year scroller. Slot and cell-state tables are included, since both are semver-covered. `useCalendar()` is documented as a fenced example rather than a live one: `noInline` is not set on the docs' react-live provider, so a demo that declares a component would render a runtime error on the page. The footer demos wrap their two parts in a column. The root renders no element of its own, so `.Days` and `.Footer` stack on normal block flow but sit side by side inside the flex row the preview centres with — the `.Footer` section says so. Also corrects the icon count in the demo scope comment, which the undo glyph made stale. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The root was a bare provider, so `.Days` and `.Footer` were in-flow siblings with no box of their own and inherited whatever the surrounding layout did — side by side inside a flex row, which is what the docs preview centres with. It now renders a column that hugs its content, so the documented composition stacks anywhere and needs no wrapper. `data-slot="calendar-preview"`, plus `render`, `className` and `ref` and `...props` last, which is what every other part already takes. The exact slot-set assertion filtered on the `calendar-preview-` prefix and so could never have seen the root's own slot; it filters on the bare name now. `CalendarPreviewProps` extends the div props with `defaultValue` omitted: `HTMLAttributes` declares it as a form value, which is not what it means here. This is orthogonal to the scale contract in the RFC review — the value shape, `trailingValue`, the draft and the conversion rule are all state, and none of them says anything about what the root renders. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Summary
PR 2 of 7 in the RFC 005 stack, on top of #894.
Ships the inline calendar — a root that owns the state, plus the parts that draw a day grid.
components/calendar/is untouched —Calendar,DatePickerandRangePickerkeep working.useCalendar(), docs page.Trigger/.Content/.Input(PR 3), range (PR 4), scale switcher + period views (PR 5)Changes
Parts — each takes
render,className,ref, adata-slot, and spreads...propslast..DaysnumberOfMonths. Hugs its content.Header.Captiondropdownopens our own two-column month/year scroller.ResetdefaultDate. Value reset, not a view reset.Grid.Day/.Weekday.PrevMonth/.NextMonth.FooterBehaviour
.Resetrenders only whendefaultDateis set and the value differs from it; it leaves the visible month alone..Caption dropdownmounts zeroSelects — plain buttons in a popover we own. Picking moves the view, never selects a value.minDate/maxDate/isDateUnavailable) disable cells but never stop navigation.dateInfoandtooltipMessagesare now functions, not records keyed by a formatted string.useCalendar()exported from the barrel: value, scale, view month, setters, availability predicate./docs/components/calendar-preview.One default changed
showOutsideDaystruefalseTechnical Details
.GridonlyhideNavigation+captionLayout='label'so it never mounts aSelect. Selection props come from context, notCalendarPreviewGridProps— so...propsreally is lastdefaultDateseparate fromdefaultValueuseControlledignoresdefaultValueoncevalueis passed, so a controlled consumer would never see.Reset.Daysand.Footerare in-flow siblings; without a box they inherit the parent's layout and sit side by side in a flex rowminDatecarrying a time of day still leaves its own day selectablemonthNames()Header alignment — the header sat on a different grid from the days. No global
box-sizing: border-boxhere, so:td)th)Also removed the table's default 2px
border-spacing, and gave both headers 8px inline padding so the caption aligns with the label rather than the cell edge. A week-number column is a gutter, so the header adds its width on top — read from the rendered grid with:has(), sinceshowWeekNumberis a.Gridprop and the header is its sibling.Muted days in the frames — April 1-16 render muted with today on the 17th. A
minDatedemo and a built-in "past dates disabled" draw identically. Built as a demo, no default bound: a past bound isn't expressible as an opt-out (you'd needminDate={new Date(0)}) and would break date-of-birth and "filter since" fields.Test Plan
calendar-preview/testsbiome checktsc --noEmitcalendar*/build:apsara/ docs buildCovered — exact
data-slotset and every state attribute; all four.Resetvisibility cases plus reset under a controlledvalue; caption dropdown mounts zeroSelects; bounds disable cells without stopping navigation;.Footerwith a string and a node.Manual — rendered single-month, two-month and caption-dropdown layouts in a real browser against the Figma frames; measured column alignment directly (0.0px drift across all seven columns); stepped through every demo tab on the new docs page.
SQL Safety (if your PR touches
*_repository.goorgoqu.*)Not applicable — TypeScript and CSS only. No Go files, no database access.