Skip to content

UI: Add Calendar and RangeCalendar, moved from components private APIs - #81337

Merged
youknowriad merged 5 commits into
trunkfrom
try/ui-calendar-components
Aug 11, 2026
Merged

UI: Add Calendar and RangeCalendar, moved from components private APIs#81337
youknowriad merged 5 commits into
trunkfrom
try/ui-calendar-components

Conversation

@youknowriad

@youknowriad youknowriad commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Part of #81230. Alternative to #81324.

What?

Moves the private DateCalendar and DateRangeCalendar out of @wordpress/components and into @wordpress/ui as the public Calendar and RangeCalendar, rewritten to follow that package's conventions: design tokens, CSS modules, render prop support, @wordpress/ui primitives for the month navigation, and Storybook docs.

The DataViews date and datetime controls import them from @wordpress/ui, so the date control no longer unlocks anything at all and datetime keeps a single unlock for ValidatedInputControl.

Why?

#81324 proposes vendoring the calendars into @wordpress/dataviews instead. 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.wp exposure and long-term backward compatibility — not of @wordpress/ui, which is 0.x and documents itself as experimental: "an early implementation subject to drastic and breaking changes". If @wordpress/ui can'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/components was useControlledValue (52 lines) — react-day-picker owns the entire DOM, so there were no components-package components to swap out.

How?

  • Tokens. $components-color-* and base-styles variables 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).
  • CSS modules. The stylesheet becomes style.module.css in the wp-ui cascade layers, and the class map is handed to react-day-picker's classNames prop. This is less code than the global BEM strings it replaces, and it structurally removes the double-styling hazard that DataViews: Vendor DateCalendar and DateRangeCalendar from components #81324 works around by renaming the class prefix.
  • @wordpress/ui primitives. The month navigation buttons render Button (minimal / neutral / compact) and the chevrons render Icon with @wordpress/icons, through react-day-picker's components overrides. This deletes ~30 lines of CSS and picks up DS hover / active / data-disabled styling.
  • render prop + ref forwarding. The root goes through useRender, wired via a context so the components object stays referentially stable (a new component type would remount the calendar and drop focus on every render).
  • Dependency. react-day-picker moves from @wordpress/components to @wordpress/ui, which also declares date-fns directly.
  • TZDate is no longer re-exported — @wordpress/ui exports components and nothing else. Consumers use @date-fns/tz. It had no production usage; only stories, tests and READMEs referenced it.
  • The two READMEs become a Storybook Best Practices MDX 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:

  • Selected day: background #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.
  • Disabled + selected day: was #fcfcfc text on a #8d8d8d background; now uses the design system's disabled pairing, #8d8d8d on #e6e6e6.
  • Weekday headers: regular weight instead of bold. The old stylesheet set no font-weight on the weekday cells, so the browser default for th (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

  • New dependency in @wordpress/ui. Base UI 1.6 has no calendar or date-picker primitive, so react-day-picker (which brings date-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-components allowlist. Calendar and RangeCalendar are added, otherwise the DataViews import fails lint. That list is owned by the components team.
  • Naming. Calendar / RangeCalendar follows React Aria and matches the Button / IconButton sibling-export pattern in this package. Easy to change if DateCalendar / DateRangeCalendar is preferred.
  • Both are marked use-with-caution in 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:

  1. npm run test:unit packages/ui/src/calendar — 127 tests (115 ported, 12 new for render / ref forwarding).
  2. npm run test:unit packages/ui packages/dataviews packages/components — all green.
  3. npm run build, npm run lint:js, npm run lint:css.

In the editor — the datetime control renders Calendar:

  1. Open any post in the post editor → Summary panel in the sidebar → the Publish / Date field. The calendar should behave exactly as on trunk: month navigation, today indicator, selected day, keyboard navigation.
  2. Schedule a post for a future date, then reopen the Summary panel — the Scheduled Date field renders the same control in its compact form.
  3. Site Editor → Pages → row actions → Quick Edit → the date field.
  4. Site Editor → PagesAdd filterDateBefore / After — the filter popover renders the calendar.

RangeCalendar has no surface in the stock editor: every date field Gutenberg registers is type: 'datetime', and only type: 'date' offers the between operator. To exercise it, use Storybook → DataViews/FieldTypes → DateComponentAdd filterDateBetween, 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

@youknowriad youknowriad added [Type] Enhancement A suggestion for improvement. [Package] UI /packages/ui [Package] DataViews /packages/dataviews [Package] Components /packages/components labels Aug 7, 2026
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

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 props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: youknowriad <youknowriad@git.wordpress.org>
Co-authored-by: ciampo <mciampini@git.wordpress.org>
Co-authored-by: mirka <0mirka00@git.wordpress.org>
Co-authored-by: oandregal <oandregal@git.wordpress.org>
Co-authored-by: ntsekouras <ntsekouras@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@youknowriad
youknowriad force-pushed the try/ui-calendar-components branch from 5574710 to 8c559f2 Compare August 7, 2026 12:04
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

Size Change: +62.2 kB (+0.81%)

Total Size: 7.71 MB

📦 View Changed
Filename Size Change
build/scripts/block-editor/index.min.js 458 kB +23.3 kB (+5.36%) 🔍
build/scripts/components/index.min.js 256 kB -22.1 kB (-7.94%)
build/scripts/customize-widgets/index.min.js 15 kB -1 B (-0.01%)
build/scripts/edit-post/index.min.js 52.9 kB +1 B (0%)
build/scripts/edit-site/index.min.js 333 kB +22.5 kB (+7.23%) 🔍
build/scripts/editor/index.min.js 530 kB +19.9 kB (+3.91%)
build/scripts/media-utils/index.min.js 153 kB +23.8 kB (+18.42%) ⚠️
build/styles/components/style-rtl.css 16.5 kB -1.57 kB (-8.69%)
build/styles/components/style-rtl.min.css 13.9 kB -1.07 kB (-7.13%)
build/styles/components/style.css 16.5 kB -1.57 kB (-8.65%)
build/styles/components/style.min.css 13.9 kB -1.06 kB (-7.09%)

compressed-size-action

@youknowriad

Copy link
Copy Markdown
Contributor Author

@oandregal @ntsekouras

Screenshot 2026-08-07 at 14 45 20

The quick edit calendar is broken in this PR but in trunk as well in the exact same way. Alignment issues, but also if I pick a date the "day - 1" is picked for me. It's super weird. I'm not going to touch this here, I consider it out of scope, but we might want to look at fixing this.

@ntsekouras

Copy link
Copy Markdown
Contributor

The quick edit calendar is broken in this PR but in trunk as well in the exact same way.

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.

@mirka mirka left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only had minor things, but probably the important thing we should agree on before merge is the name change. I think I'm fine with it, but I'll wait for @ciampo's reasoning since he decided on the name.

Comment thread packages/ui/src/calendar/test/render-prop.test.tsx Outdated
Comment thread packages/ui/src/calendar/stories/index.story.tsx Outdated
Comment thread packages/ui/src/calendar/stories/best-practices.mdx Outdated
Comment thread packages/ui/src/calendar/stories/best-practices.mdx Outdated
Comment thread packages/ui/src/calendar/stories/best-practices.mdx Outdated
Comment thread packages/ui/src/calendar/stories/best-practices.mdx Outdated
Comment thread packages/ui/src/calendar/style.module.css
Comment thread packages/ui/src/calendar/style.module.css
@@ -0,0 +1,133 @@
import { Meta } from '@storybook/addon-docs/blocks';

<Meta title="Design System/Components/Calendar/Best Practices" />

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 oandregal left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.).

@ciampo

ciampo commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

I'm currently taking a look at this PR and will post a review soon, please wait before merging.

@ciampo ciampo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread packages/ui/src/calendar/calendar.tsx
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';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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?

Comment thread packages/ui/CHANGELOG.md Outdated
&:has(.day-button:hover:not(:disabled)),
&:has(.day-button:focus-visible) {
color: var(--wpds-color-foreground-interactive-brand);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Non-blocking, can be addressed in a follow-up]

Per WPDS token guidance, we shouldn't mix and match:

  • content and interactive elements (currently, resting state inherits --wpds-color-foreground-content-neutral, while disabled and hover/focus use --wpds-color-foreground-interactive-* tokens)
  • neutral and brand tones 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

Comment thread packages/ui/src/calendar/calendar.tsx
Comment thread packages/ui/src/calendar/utils/use-localization-props.ts
Comment thread packages/ui/src/calendar/utils/use-localization-props.ts
Comment on lines +8 to +9
--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);

@ciampo ciampo Aug 10, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

( 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

Comment thread packages/ui/src/calendar/types.ts Outdated
Comment thread packages/ui/src/calendar/types.ts Outdated
@ciampo

ciampo commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

@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).

youknowriad and others added 3 commits August 10, 2026 15:51
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>
@ciampo
ciampo force-pushed the try/ui-calendar-components branch from d3d6506 to fc1a9b8 Compare August 10, 2026 14:08
const [ calendarMonth, setCalendarMonth ] = useState< Date >( () => {
const parsedDate = parseDate( value );
return parsedDate || new Date(); // Default to current month
} );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Comment on lines 104 to 108
validationTimeoutRef.current = setTimeout( () => {
if ( inputControlRef.current ) {
inputControlRef.current.focus();
inputControlRef.current.blur();
onChangeCallback( dateTimeValue );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

@ciampo

ciampo commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

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

Task Original comment Assignee Status
Fix Quick Edit calendar alignment and off-by-one date selection. Comment @ntsekouras #81344 #81345
Synchronize the displayed month with external DataViews value changes. Comment @ntsekouras todo
Prevent duplicate DataForm updates from datetime calendar selections. Comment @ntsekouras #81440
Pass the WordPress locale to DataViews calendars. Comment @ntsekouras todo
Verify the Calendar root role with browsers and assistive technologies. Comment @ciampo #81443
Align Calendar styling with WPDS tokens and remove color-mix(). Comment @ciampo #81438
Investigate and reduce the DataViews bundle-size increase. Comment ? to discuss
Upgrade react-day-picker to version 10. Comment @ciampo #81439

@ntsekouras

ntsekouras commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Fix Quick Edit calendar alignment and off-by-one date selection.

There are issues for those 1, 2 and a PRs to move things forward. I'll take a look tomorrow for the rest of them.

@oandregal

Copy link
Copy Markdown
Member

Thanks for the collaboration, folks. I've removed myself from the task list then, and I'll focus on other aspects of #81230

@youknowriad

Copy link
Copy Markdown
Contributor Author

I really appreciate all the collaboration here. This is looking good for a v1 I think. We can continue with the follow-ups separately.

@youknowriad
youknowriad merged commit 637513a into trunk Aug 11, 2026
72 of 83 checks passed
@youknowriad
youknowriad deleted the try/ui-calendar-components branch August 11, 2026 11:50
@github-actions github-actions Bot added this to the Gutenberg 23.8 milestone Aug 11, 2026
@ciampo

ciampo commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

I opened draft PRs for all my assigned follow-ups, I will iterate on them depending on the priority of other incoming work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Package] Components /packages/components [Package] DataViews /packages/dataviews [Package] UI /packages/ui [Type] Enhancement A suggestion for improvement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants