feat(Calendar): pick a month or year from a list - #1317
Merged
tenphi merged 4 commits intoAug 14, 2026
Conversation
The calendar header's month and year are now buttons that open a month list and a year list, so reaching a distant date takes two clicks instead of paging with the arrows. `MonthPicker` and `QuarterPicker` gained the same year list behind the year in their header. The panels are shared: `Calendar`, `RangeCalendar` and `PeriodCalendar` now build on one header, one cell style set and one accessible `PeriodGrid` with full keyboard support. Also reviewed the surrounding implementation: `Calendar` never attached its ref to the DOM, `PeriodPicker` duplicated the field's label props onto its value text and ignored `isReadOnly`, and the period panels were a `role="grid"` with no rows. Closes CUB-3903 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
🦋 Changeset detectedLatest commit: f4b5992 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📦 NPM canary releaseDeployed canary version 0.0.0-canary-694a764. |
Contributor
🏋️ Size limit report
Click here if you want to find out what is changed in this build |
Contributor
🧪 Storybook is successfully deployed!
|
`Popover` called `useOverlay` from inside `PopoverWrapper`, which `Overlay` renders with an `isOpen` derived from the transition — false until the enter animation reaches `entered`. React Aria only registers an overlay in its visible-overlay stack while `isOpen`, and only dismisses the topmost registered one, so `Escape` was a no-op for the first frames after opening (and forever in a background tab, where rAF never advances the phase). `useOverlay` now runs in `Popover` against the trigger's open state, the way `Modal` and `Tray` already did. Also restore focus to the day grid when `Escape` steps back from the calendar's month or year panel: the grid the user was in unmounts, and without claiming focus it fell out of the popover, swallowing the next `Escape`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The month and year segments were `small` buttons with an `h6` preset and hand-rolled padding, so they stood 28px tall next to the 24px navigation arrows and their hover fill crowded the top of the popover. They are now plain `xsmall` clear buttons with no style overrides. The plain-title fallback (a decade range, or a calendar with navigation turned off) follows the same type scale, so the header doesn't switch weights depending on whether its title happens to be interactive. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
498.78 kB against a 497 kB limit. `main` rebuilds at 496.25 kB, so the month/year navigation costs +2.53 kB: the shared calendar panel, header and period grid, plus eleven strings across twelve locales. The Button budget is unchanged at 122.3 kB, so this reaches nobody who doesn't import a calendar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tenphi
deleted the
andrew/cub-3903-improve-calendar-component-in-ui-kit
branch
August 14, 2026 16:24
This was referenced Aug 14, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes CUB-3903.
Pick a month or a year from a list
The calendar header's title is now the navigation. The month and the year are separate buttons: pressing the month opens a month list, pressing the year opens a year list, and picking a year drops you into that year's months. Reaching a date two years out is two clicks instead of twenty-four.
MonthPickerandQuarterPickerget the same treatment — the year in their header opens a year list, so2026 → 2019no longer means seven clicks on the arrow.The month and year panels only navigate. They move the focused date and never select a value, so a range selection in progress survives a jump to another month.
Opt out with
hasMonthYearNavigation={false}.Shared internals
Calendar,RangeCalendarandPeriodCalendarhad three copies of the same header, the same shell element and two divergent copies of the cell styles. They now share:CalendarHeader— title segments + prev/next, one size of nav button (RangeCalendarusedsmallwhereCalendarusedxsmall)PeriodGrid— one accessible grid for months, quarters and yearsstyled.tsx— one set of cell statesCalendarPanelholds the day/month/year switching for bothCalendarandRangeCalendar.Review findings, fixed
Calendarnever attached its ref.domRefwas created, wrapped inuseImperativeHandle, and then dropped —createDOMRef(domRef)always resolved tonull.RangeCalendardid attach it.PeriodPickerspread the field'slabelPropsonto its value text, duplicating the label'sid(wrapWithFieldapplies the same props to the real<label>).PeriodPickerignoredisReadOnly— clicking the field opened the popover anyway.aria-describedbythe value text.role="grid"with no rows andaria-pressedon its cells. It is now a real grid (rows,gridcell,aria-selected) with a roving tab stop.PageUp/PageDownpage,Home/Endjump to the first/last selectable period, disabled cells are stepped over, andEscapesteps back one panel instead of closing the popover.<body>(the button you pressed unmounts). Focus now follows into the new grid.maxValue={2026-09-15}disabled all of September. A period is now disabled only when all of it falls outside the range.hiddenon out-of-range day cells did nothing (display: gridoverrides[hidden]), so they rendered anyway. Replaced with an explicitoutsidemodifier — same pixels, honest markup.gap: 1xon the calendar shell did nothing (display: block). The shell is now amax-contentgrid, which also stops a standalone calendar from stretching its cells across its container.currentmodifier.formatValuewas typed(date, picker)but called with(date, picker, locale).PeriodPicker's placeholders were hardcoded English; those and the new calendar labels are translated in all twelve locales.Not fixed (pre-existing, out of scope)
Docs, stories, tests
Calendarhad no stories, docs or tests. It now has all three (Other/Calendar), includingplay-driven stories for the month and year panels so Chromatic captures them.PeriodPicker.docs.mdxgained accessibility and style-props sections.Calendaris still not exported from the package root:export * from '@internationalized/date'already exports aCalendarinterface, and shadowing it would be a breaking change for consumers.Verification
pnpm test(90 files, 1841 tests),pnpm oxlint,tsc --noEmitandpnpm audit-defaultsare clean. Every panel was driven by hand in Storybook — light and dark, mouse and keyboard, acrossDatePicker,DateRangePicker,DateRangeSeparatedPickerand the four period pickers.Two existing
DatePickerassertions changed: the heading's text is now two buttons rather than one string, so they assert on the buttons.🤖 Generated with Claude Code
Update:
Escapenow closes the popoverThe earlier note claiming this was pre-existing
Dialogbehaviour was wrong on the cause, and the bug turned out to be fixable — it is fixed here.PopovercalleduseOverlayfrom insidePopoverWrapper. That component is rendered byOverlay, which clones its child with anisOpenderived from the transition (phase === 'entered'), not from the trigger. React Aria only pushes an overlay onto its visible-overlay stack whileisOpen, andonHidedismisses only the topmost entry — so for the first frames after opening, the popover was not in the stack andEscapedid nothing. In a background tab, whererequestAnimationFramenever advances the phase pastenter, it never worked at all, which is why it looked permanent.useOverlaynow runs inPopoveragainst the trigger's open state, exactly whereModalandTrayalready call it. This affects everyDialogTrigger type="popover", not just the date fields.One more fix fell out of it: when
Escapesteps back from the calendar's month or year panel, the grid the user was in unmounts. Without claiming focus for the day cell, focus fell out of the popover entirely and the nextEscapewas lost.CalendarPanelnow callsstate.setFocused(true)on the way back.Covered by three new tests in
DialogTrigger.test.tsx(dismiss duringenter, dismiss afterentered, and staying open when a child handlesEscape) plus two inDatePicker.test.tsxfor the step-back behaviour. RevertingPopover.tsxalone fails the "right after opening" test and passes the "once settled" one, which pins the diagnosis.