Skip to content

[Toolkit][Shadcn] Add calendar recipe - #3855

Open
ker0x wants to merge 5 commits into
symfony:3.xfrom
ker0x:feat/toolkit-shadcn-calendar
Open

ker0x wants to merge 5 commits into
symfony:3.xfrom
ker0x:feat/toolkit-shadcn-calendar

Conversation

@ker0x

@ker0x ker0x commented Sep 9, 2026

Copy link
Copy Markdown
Contributor
Q A
Bug fix? no
New feature? yes
Deprecations? no
Documentation? yes
Issues Part of #3233
License MIT

Adds a calendar recipe to the Shadcn kit, ported from registry/bases/radix/ui/calendar.tsx.

Upstream is a thin wrapper around react-day-picker, which has no Twig equivalent, so the month grid is reimplemented rather than wrapped: <twig:Calendar> renders it server-side and one Stimulus controller progressively enhances it.

Supports single / multiple / range selection, month navigation, label or dropdown caption, outside days, ISO week numbers, fixed weeks, disabled dates, min/max and navigation bounds, arbitrary modifiers, and form submission through hidden inputs (name, name[], name[from]/name[to]).

Every day state is a data-* attribute

The grid is always a fixed six-by-seven table, and each day's state is a literal data-* attribute with an explicit value, with the matching styles expressed as Tailwind data-variants inside the static base class string:

<td data-slot="calendar-day" data-day="2026-03-01" data-today="false" data-range-start="false"class="… data-[today=true]:bg-muted data-[range-start=true]:rounded-s-(--cell-radius) data-[range-start=true]:bg-muted …">

So the controller never touches a class name — navigating a month only flips attributes and rewrites day labels. No Tailwind string is duplicated into JavaScript, the no-JS render is complete, and the snapshots are meaningful.

RTL uses the logical-property forms from upstream's own RTL changelog (rounded-s-/rounded-e-, after:start-0/after:end-0) rather than ltr:/rtl: pairs, since upstream dropped its ui-rtl/ directory in favour of exactly that.

Localization

Month, weekday and day labels go through twig/intl-extra (|format_date(pattern: …, locale: …)), mirrored client-side by Intl.DateTimeFormat so server and client agree after navigation. This is the first kit recipe to render dates, so it is also the first to need twig/intl-extra.

Two changes outside the recipe follow from that:

  • twig/intl-extra added to src/Toolkit/composer.json require-dev, otherwise |format_date is undefined when the snapshot test renders the README examples.
  • format_date added to ComposerSymbolChecker's curated symbol map, so the linter can tell a recipe using it to declare twig/extra-bundle + twig/intl-extra. Purely additive: no other kit uses format_date.

Deliberate deviations from upstream

  • Persian / Hijri / Jalali is not ported. Upstream swaps react-day-picker for its /persian build; a faithful port needs a non-Gregorian calendar engine in both PHP and JavaScript, and the grid arithmetic is not Gregorian.
  • Custom Cell Size drops the per-day price labels. The section documents --cell-size, which is ported in full; custom per-day content cannot survive the controller re-rendering cells on month change.
  • Selected Date (With TimeZone) is omitted. It documents a React SSR hydration pitfall that does not exist server-side.

Every other upstream example is present as a live preview in the recipe README.md.

Deterministic snapshots

A calendar that renders "today" produces snapshots that break on an unrelated day when the month rolls over. Every README example therefore pins today and month explicitly — today is a real prop, as it is in react-day-picker.

Testing

  • 11 new snapshots; full Toolkit suite green; bin/ux-toolkit-kit-lint --fail-on-warning clean on all four kits; php-cs-fixer, twig-cs-fixer, oxfmt, oxlint clean.
  • Behaviour was driven in a headless browser (68 assertions): month navigation, dropdown caption, all three selection modes, range hover preview, disabled/min/max/modifiers, keyboard navigation (arrows, Home/End, PageUp/PageDown, RTL-aware) with roving focus, week numbers, fixedWeeks row hiding, showOutsideDays, navigation bounds, hidden-input serialization, and presets driving the calendar from outside the grid.
  • Rendered on a local ux.symfony.com against this branch, in light and dark, with no console or request errors.

No companion PR on symfony/ux.symfony.com is needed: since symfony/ux.symfony.com@c8f4c8e kit controllers are auto-registered by globbing the vendored kits, the Tailwind build already @sources the kit directory (and its output is not committed), and twig/intl-extra is already required there.

Three portability fixes it turned out to need

Rendering dates in a snapshot test exposed three latent environment dependencies. Each is a separate commit.

  • [Toolkit] Make HTML snapshots stable across libxml versions — when an attribute value contains a double quote, libxml < 2.14 switches the whole attribute to single quotes while >= 2.14 keeps double quotes and escapes the inner ones as &quot;. The calendar's JSON-valued Stimulus attributes hit this on 9 of 11 examples. Normalized on the escaped form, right next to the entity normalization already in ComponentsRenderingTest for the same 2.14 boundary. This also fixes the common/post-link snapshot, which until now only matched on libxml < 2.14 — it fails on any newer machine, and its snapshot is regenerated here.

  • [Toolkit][Shadcn] Pin the calendar RTL example numbering system — a bare ar locale resolves to Arabic-Indic or Latin digits depending on the ICU version, so day numbers differed between my runtime and CI's. Pinned with the -u-nu- Unicode extension, which both IntlDateFormatter and Intl.DateTimeFormat honour. Not just a snapshot concern: the browser's ICU is not necessarily the server's, so without pinning the digits could flip when navigating a month.

  • [Toolkit] Enable the intl extension on the Windows test job — the Windows job passes setup-php an explicit extension list, which replaces the defaults rather than adding to them, so intl was absent. With twig/intl-extra now in the Toolkit's dev dependencies that failed the whole suite (779 errors, not only the calendar) with Class "IntlDateFormatter" not found. Added intl to that list and declared ext-intl in require-dev so the requirement surfaces at install time instead of at runtime.

@ker0x
ker0x requested a review from Kocal as a code owner September 9, 2026 15:25
@carsonbot carsonbot added Documentation Improvements or additions to documentation Feature New Feature Toolkit Status: Needs Review Needs to be reviewed labels Sep 9, 2026
When an attribute value contains a double quote, libxml < 2.14 switches the
whole attribute to single quotes, while >= 2.14 keeps double quotes and escapes
the inner ones as &quot;. The rendering snapshots therefore depend on the libxml
the suite happens to run against.

Normalize on the escaped form, next to the entity normalization already there
for the same 2.14 boundary. This fixes the `calendar` recipe, whose JSON-valued
Stimulus attributes hit the case on every example, and the `post-link` snapshot,
which until now only matched on libxml < 2.14.

Claude-Session: https://claude.ai/code/session_01UnZ3JkQ2pyVz2fYvA83p3D
A bare `ar` locale resolves to Arabic-Indic or Latin digits depending on the ICU
version, so the rendered day numbers -- and the snapshot -- differ between the
runtime the suite is developed on and the one CI runs. The browser's ICU is not
necessarily the server's either, so the digits could also flip on navigation.

Pin it with the `-u-nu-` Unicode extension, which both IntlDateFormatter and
Intl.DateTimeFormat honour, and document the reason in the RTL section.

Claude-Session: https://claude.ai/code/session_01UnZ3JkQ2pyVz2fYvA83p3D
The Windows job passes setup-php an explicit extension list, which replaces the
defaults rather than adding to it, so `intl` was absent. Now that the Toolkit
test suite renders localized dates through twig/intl-extra, its absence fails
the whole suite on Windows with `Class "IntlDateFormatter" not found`.

Add `intl` to that list, and declare `ext-intl` in the Toolkit's require-dev so
the requirement fails at install time rather than at runtime.

Claude-Session: https://claude.ai/code/session_01UnZ3JkQ2pyVz2fYvA83p3D
@Kocal

Kocal commented Sep 10, 2026

Copy link
Copy Markdown
Member

Make HTML snapshots stable across libxml versions

Ah you had the issue as well :D

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

Labels

Documentation Improvements or additions to documentation Feature New Feature Status: Needs Review Needs to be reviewed Toolkit

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants