Skip to content

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

Open
youknowriad wants to merge 2 commits into
trunkfrom
try/ui-calendar-components
Open

UI: Add Calendar and RangeCalendar, moved from components private APIs#81337
youknowriad wants to merge 2 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: mirka <0mirka00@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: +81.2 kB (+1.04%)

Total Size: 7.9 MB

📦 View Changed
Filename Size Change
build/modules/boot/index.min.js 49.4 kB +1 B (0%)
build/modules/content-types/index.min.js 191 kB +19.5 kB (+11.37%) ⚠️
build/scripts/block-editor/index.min.js 457 kB +23.1 kB (+5.32%) 🔍
build/scripts/components/index.min.js 256 kB -22.1 kB (-7.95%)
build/scripts/customize-widgets/index.min.js 15 kB -1 B (-0.01%)
build/scripts/edit-site/index.min.js 333 kB +22.5 kB (+7.23%) 🔍
build/scripts/editor/index.min.js 529 kB +19.8 kB (+3.88%)
build/scripts/format-library/index.min.js 29.5 kB +1 B (0%)
build/scripts/media-utils/index.min.js 153 kB +23.7 kB (+18.36%) ⚠️
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.

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>
@youknowriad
youknowriad force-pushed the try/ui-calendar-components branch from 8c559f2 to aee49c3 Compare August 7, 2026 15:35

@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
## Accessibility

- The calendar renders with `role="application"` by default. Set `role="dialog"` when it is presented inside a dialog.
- Use `autoFocus` to move focus to the selected day (or today) after a user action, such as opening a popover that contains the calendar.

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 line might have been sufficient for a prop description, but reads a bit too broad ("after a user action") for an accessibility best practice section. @ciampo I feel like the only time autofocus is allowed is when a popover first opens where the main purpose is to show the calendar? Anything else?

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

3 participants