UI: Add Calendar and RangeCalendar, moved from components private APIs - #81337
Conversation
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
5574710 to
8c559f2
Compare
|
Size Change: +62.2 kB (+0.81%) Total Size: 7.71 MB 📦 View Changed
|
I'll create an issue for that, thanks! It's a super weird coincidence because @mcsf pinged just a bit earlier than your comment noticing your UI issue, which seems to be for Safari. |
8c559f2 to
aee49c3
Compare
| @@ -0,0 +1,133 @@ | |||
| import { Meta } from '@storybook/addon-docs/blocks'; | |||
|
|
|||
| <Meta title="Design System/Components/Calendar/Best Practices" /> | |||
There was a problem hiding this comment.
It looks like wordpress/ui stories go with "Usage guidelines" instead of "Best practices".
I also found weird that the Calendar is displayed as a folder while the RangeCalendar is listed as a component. It'd be nice to have them either grouped (like Form is) or having them listed as separate components.
oandregal
left a comment
There was a problem hiding this comment.
This makes sense to me, it works in DataViews, and doesn't impact other DataViews consumers (e.g., they don't have to change anything about how styles are imported, etc.).
|
I'm currently taking a look at this PR and will post a review soon, please wait before merging. |
ciampo
left a comment
There was a problem hiding this comment.
I think the Calendar and RangeCalendar work fine, at least for now. If / when Base UI will release calendar-related components, we'll re-assess.
Noting that there's a new major version of react-day-picker, but it should be entirely compatible with the current shape of the components. Therefore I think we can perform the udpate in a follow-up PR, without making it a blocker here.
| import { getDate, getSettings } from '@wordpress/date'; | ||
| import { error as errorIcon } from '@wordpress/icons'; | ||
| import { Stack } from '@wordpress/ui'; | ||
| import { Calendar, RangeCalendar, Stack } from '@wordpress/ui'; |
There was a problem hiding this comment.
[Not a blocking comment, but more of a reflection for conversation and potential future improvements]
Moving from the components from @wordpress/components to @wordpress/ui means that, instead of being imported from the externalized bundle, these components will be bundled directly into Dataviews. Which means that every consume of dataviews (content-types, block-editor, edit-site, editor, media-utils) will get a ~20kb bundle size increase.
I'm sure this was discussed, but we should brainstorm on how to de-dupe these dependencies in the final bundle, at least in the context of Gutenberg where we fully control the building pipeline?
| &:has(.day-button:hover:not(:disabled)), | ||
| &:has(.day-button:focus-visible) { | ||
| color: var(--wpds-color-foreground-interactive-brand); | ||
| } |
There was a problem hiding this comment.
[Non-blocking, can be addressed in a follow-up]
Per WPDS token guidance, we shouldn't mix and match:
contentandinteractiveelements (currently, resting state inherits--wpds-color-foreground-content-neutral, while disabled and hover/focus use--wpds-color-foreground-interactive-*tokens)neutralandbrandtones for different states of the same UI (i.e., resting/disabled/hover/focus).
We should likely perform a component-wide assessment and align its styles with the rest of the @wordpress/ui componentry — for example, day buttons should likely look and behave like the minimal/neutral Button when unselected, and like its pressed variant when selected.
cc @WordPress/gutenberg-design
| --wp-ui-calendar-range-middle-background-color: color-mix(in srgb, var(--wpds-color-background-interactive-brand-strong) 4%, transparent); | ||
| --wp-ui-calendar-preview-border-color: color-mix(in srgb, var(--wpds-color-background-interactive-brand-strong) 16%, transparent); |
There was a problem hiding this comment.
( This goes together with the review/changes discussed in https://github.com/WordPress/gutenberg/pull/81337/changes#r3749764712 )
We should also consider an alternative to using color-mix here, ideally using WPDS tokens directly
|
@youknowriad and @oandregal , for the sake of speed of iteration, I'll go ahead and address all the points flagged in my review (except for the ones that can be done as follow-ups). |
Moves the private `DateCalendar` / `DateRangeCalendar` out of `@wordpress/components` and into `@wordpress/ui` as the public `Calendar` and `RangeCalendar`, rewritten to follow that package's conventions. DataViews was their only consumer, and it is a bundled package: unlocking a `@wordpress/components` private API from it crashes at module-eval time when two copies of `@wordpress/private-apis` exist in one runtime. The calendars were blocked on "still stabilizing in `@wordpress/components`", but `@wordpress/ui` is 0.x and documents itself as experimental, so it can absorb a component that is still settling. - Styles move from SCSS with `$components-color-*` to a CSS module on `--wpds-*` tokens, in the `wp-ui` cascade layers. The class map is passed to `react-day-picker`'s `classNames`, so the calendar can no longer be double-styled by a stale `wp-components` stylesheet. - Month navigation renders `Button`, chevrons render `Icon` with `@wordpress/icons`, via `react-day-picker`'s `components` overrides. - The root supports the `render` prop and forwards refs, like every other `@wordpress/ui` component. - `react-day-picker` moves from `@wordpress/components` to `@wordpress/ui`. - `TZDate` is no longer re-exported; `@wordpress/ui` exports components only. Consumers use `@date-fns/tz`. It had no production usage. Part of #81230: the `date` control no longer unlocks anything, and `datetime` keeps a single unlock for `ValidatedInputControl`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Fix the `render` re-render test, which asserted nothing: clicking a button with no handler never re-rendered the wrapper, so the identity check passed trivially. Use RTL's `rerender` with a fresh `render` element instead. - Mark both components `recommended` rather than `use-with-caution`; there are no problematic changes planned, which is the bar for that status. - Drop the Accessibility section from the Best Practices page. The `role="dialog"` advice is an oversimplification that is harmful if followed naively, and the remaining items are implementation notes already covered by the prop documentation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
d3d6506 to
fc1a9b8
Compare
| const [ calendarMonth, setCalendarMonth ] = useState< Date >( () => { | ||
| const parsedDate = parseDate( value ); | ||
| return parsedDate || new Date(); // Default to current month | ||
| } ); |
There was a problem hiding this comment.
Also non blocking, but I figured I'd flag it for a potential follow-up:
calendarMonth is initialized from the field value only on mount. If the parent later replaces data with a date in another month—for example after undo, reset, or switching the edited item—the Calendar's controlled value updates but its controlled month remains stale, so the selection can be off-screen. The same pattern exists in the range and datetime controls.
Potentially worth a follow-up fix?
| validationTimeoutRef.current = setTimeout( () => { | ||
| if ( inputControlRef.current ) { | ||
| inputControlRef.current.focus(); | ||
| inputControlRef.current.blur(); | ||
| onChangeCallback( dateTimeValue ); |
There was a problem hiding this comment.
Also non-blocking, but discovered while reviewing this PR.
One Calendar selection calls onChangeCallback immediately above, then calls it again in this timeout.
In default conditions, when the input ref exists, selecting or clearing a date sends two identical DataForm updates for one user action.
This may not be a trivial fix, since it may be related to the validation and focus workaround, but it should be looked into (and tested for).
|
@oandregal @ntsekouras can you give it another look / smoke test before merging? In the meantime, a recap of all the follow-up tasks that I collected:
|
|
Thanks for the collaboration, folks. I've removed myself from the task list then, and I'll focus on other aspects of #81230 |
|
I really appreciate all the collaboration here. This is looking good for a v1 I think. We can continue with the follow-ups separately. |
|
I opened draft PRs for all my assigned follow-ups, I will iterate on them depending on the priority of other incoming work. |

Part of #81230. Alternative to #81324.
What?
Moves the private
DateCalendarandDateRangeCalendarout of@wordpress/componentsand into@wordpress/uias the publicCalendarandRangeCalendar, rewritten to follow that package's conventions: design tokens, CSS modules,renderprop support,@wordpress/uiprimitives for the month navigation, and Storybook docs.The DataViews
dateanddatetimecontrols import them from@wordpress/ui, so thedatecontrol no longer unlocks anything at all anddatetimekeeps a single unlock forValidatedInputControl.Why?
#81324 proposes vendoring the calendars into
@wordpress/dataviewsinstead. That unblocks the private-API removal, but as noted in the issue it comes with a hard rule that DataViews may never export them — so the next consumer copies the code again, and the code never improves.The blocker was "still stabilizing in
@wordpress/components". That is a constraint of the components package's rules —window.wpexposure and long-term backward compatibility — not of@wordpress/ui, which is0.xand documents itself as experimental: "an early implementation subject to drastic and breaking changes". If@wordpress/uican't absorb a component that has shipped and been in use for a year, the 0.x posture isn't buying anything.The rewrite is also smaller than it looks. The only coupling to
@wordpress/componentswasuseControlledValue(52 lines) —react-day-pickerowns the entire DOM, so there were no components-package components to swap out.How?
$components-color-*andbase-stylesvariables are replaced with--wpds-*. Most mappings are value-exact ($grid-unit-40→--wpds-dimension-size-md= 32px,$radius-small→--wpds-border-radius-sm= 2px,$font-size-medium→--wpds-typography-font-size-md= 13px,gray-700→--wpds-color-foreground-content-neutral-weak=#707070).style.module.cssin thewp-uicascade layers, and the class map is handed toreact-day-picker'sclassNamesprop. This is less code than the global BEM strings it replaces, and it structurally removes the double-styling hazard that DataViews: VendorDateCalendarandDateRangeCalendarfrom components #81324 works around by renaming the class prefix.@wordpress/uiprimitives. The month navigation buttons renderButton(minimal/neutral/compact) and the chevrons renderIconwith@wordpress/icons, throughreact-day-picker'scomponentsoverrides. This deletes ~30 lines of CSS and picks up DS hover / active /data-disabledstyling.renderprop + ref forwarding. The root goes throughuseRender, wired via a context so thecomponentsobject stays referentially stable (a new component type would remount the calendar and drop focus on every render).react-day-pickermoves from@wordpress/componentsto@wordpress/ui, which also declaresdate-fnsdirectly.TZDateis no longer re-exported —@wordpress/uiexports components and nothing else. Consumers use@date-fns/tz. It had no production usage; only stories, tests and READMEs referenced it.Best PracticesMDX page; the prop tables are now JSDoc rendered by Storybook's ArgTypes.Intentional visual changes
Three deltas come from mapping to semantically correct tokens rather than preserving computed values:
#1e1e1e→#2d2d2d, hover#1e1e1e. On trunk the default and hover backgrounds were both#1e1e1e, so the hover state was a no-op;--wpds-color-background-interactive-neutral-strong{,-active}fixes that.#fcfcfctext on a#8d8d8dbackground; now uses the design system's disabled pairing,#8d8d8don#e6e6e6.font-weighton the weekday cells, so the browser default forth(bold) applied even though the calendar root already sets the default weight. Spotted by @mirka in review, who reads it as a bug fix — agreed.Points for reviewers
@wordpress/ui. Base UI 1.6 has no calendar or date-picker primitive, soreact-day-picker(which bringsdate-fns,@date-fns/tz,date-fns-jalali,@tabby_ai/hijri-converter) is the only option. This is the main decision to sign off on.use-recommended-componentsallowlist.CalendarandRangeCalendarare added, otherwise the DataViews import fails lint. That list is owned by the components team.Calendar/RangeCalendarfollows React Aria and matches theButton/IconButtonsibling-export pattern in this package. Easy to change ifDateCalendar/DateRangeCalendaris preferred.use-with-cautionin Storybook, with a note that the design and prop surface may still move — the "some components are more early than others" idea from the issue discussion, made explicit.Testing Instructions
Unit tests:
npm run test:unit packages/ui/src/calendar— 127 tests (115 ported, 12 new forrender/ ref forwarding).npm run test:unit packages/ui packages/dataviews packages/components— all green.npm run build,npm run lint:js,npm run lint:css.In the editor — the
datetimecontrol rendersCalendar:RangeCalendarhas no surface in the stock editor: every date field Gutenberg registers istype: 'datetime', and onlytype: 'date'offers thebetweenoperator. To exercise it, use Storybook → DataViews/FieldTypes → DateComponent → Add filter → Date → Between, which renders the range calendar with the hover preview.Also worth checking in Storybook → Design System/Components/Calendar and RangeCalendar: the RTL toggle (layout and chevrons should flip), and the WordPress global CSS toggle (appearance should be unchanged).
Screenshots or screencast
Use of AI Tools
This PR, including this description, was written by Claude Code (Claude Opus 5), based on the discussion in #81230, and human-reviewed before submission.
🤖 Generated with Claude Code