feat: DatePicker (@internationalized/date + Base UI) + DataTable date filters#332
Draft
interacsean wants to merge 25 commits into
Draft
feat: DatePicker (@internationalized/date + Base UI) + DataTable date filters#332interacsean wants to merge 25 commits into
interacsean wants to merge 25 commits into
Conversation
Proposes the DatePicker / DateField / Calendar API for AppShell: react-aria-components + @internationalized/date wrapped in a thin app-shell layer, with passed-through vs masked props, locale and timezone wiring, alternatives analysis, and measured bundle costs. Tracked at tailor-inc/platform-planning#1093. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tting Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…r dep + re-export) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…zed/date + Base UI) Implements the proposal's §9 lighter-foundation option: DateField, DatePicker, and Calendar built on @internationalized/date (value layer) + Base UI (Popover), with the segmented spinbutton input and APG calendar grid hand-rolled. Same public API and a11y/DOM contract as the react-aria variant; net-new dependency is just @internationalized/date (~11 KB gz) since Base UI is already bundled. - Locale-driven segment ordering, first-day-of-week, localized month/weekday names - Full APG keyboard: arrows, Home/End, PageUp/PageDown, Shift+PageUp/Down, Enter/Space - Roving tabindex; popover auto-focuses the grid and contains Tab (prev → next → grid) - Month-change via nav buttons keeps focus on the button (one-shot focus signal) - Visible focus ring on nav buttons + day cells - AppShell timeZone prop + useResolvedLocale()/useTimeZone(); @internationalized/date re-exports - 25 tests (behaviour + DOM a11y contract), docs, changeset, example page, impl comparison Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire the app-shell DatePicker into the DataTable date-filter editor (single and between ranges), replacing the native date input, and add an example page demonstrating DataTable + DataTable.Filters driven by useCollectionVariables and a promise-based async data source (a stub for the GraphQL query). DatePicker robustness fixes surfaced by controlled filter usage: - useDateFieldState keeps internal segment state synced from a controlled value (no thrash/loss of in-progress entry on every keystroke) - typing only emits a complete & valid value; clearing emits null - first digit after a segment is focused replaces rather than accumulates - composeValue guards reject out-of-range segments (no invalid CalendarDate) - per-segment display formatting (never constructs a date from partial input) - DatePicker/DateField gain an `aria-label` prop for compact, label-less inputs Example page: dummy invoice dataset, async queryInvoices(variables) applying filter/order/cursor-pagination, date column with `type: "date"` filter. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Segment auto-advance now also fires when the typed-digit count fills the field's width, so typing "02" completes the day and advances — while "2" then "9" still builds 29 (a leading zero caps the digit count). - Anchor the calendar popover to the field group (not the calendar icon) with align="start", so its left edge aligns to the field's left edge and Base UI collision handling shifts/flips it inward near a viewport edge. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…r / before / between) Date columns now offer a slimmer, plainer operator set instead of the numeric comparators: - eq → "exact date" - gte → "after" (inclusive) - lte → "before" (inclusive) - between → "between" - dropped gt, lt, and ne (the inclusive after/before cover the intent) Scoped to `date` columns only — number/datetime/time keep the full numeric operator set and labels. Labels added for en + ja; chip labels and both the add-filter and edit-chip operator selects use them. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Date filter chips now render the value as a locale-appropriate medium date (e.g. "15 Jun 2026" / "Jun 15, 2026") via @internationalized/date's DateFormatter with the resolved AppShell locale, instead of the raw ISO "2026-06-15". Formatted in UTC so the calendar date never shifts across zones. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Align with the add-component skill: - Replace the inline JS focus-ring handlers (el.style.outline / chromeFocusProps) with the same `ring` utility Button/inputs use — focus-visible:ring on the nav buttons + trigger, and :focus:ring (with relative/z-10 so it sits above adjacent cells) on calendar day cells. No more CSS-in-JS. - Add structural snapshot tests for DateField, DatePicker (closed), and Calendar, per the skill's testing convention. The earlier "no visible focus" was a wrong-attribute bug (data-[focus-visible], a react-aria attr our cells never emit), not a failure of `ring`; the compiled utilities are present in dist and identical to Button's. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- data-table.md: date filters use the DatePicker input and the friendlier operator set (exact date / after / before / between); note locale-formatted chips and that datetime/time are unchanged. - date-picker.md: add the `aria-label` prop. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…onstraint The vite-app type-check (run by CI, not by esbuild) failed: an `interface` doesn't satisfy `Record<string, unknown>` — createColumnHelper / useDataTable's row constraint — because interfaces lack the implicit index signature that object-literal `type` aliases have. Switch `interface Invoice` to `type`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… on blur Two related date-validity fixes: - Day segment max is 31 until a valid month is entered (was tied to the anchor month, so "31" collapsed to "1" in a 30-day current month — and the snapshot was month-dependent). Snapshots updated to the now-stable max. - On blur, an impossible day for the entered month/year is clamped to that month's real length (e.g. 30/02/2026 → 28; 31/04 → 30). Leap years resolve correctly since the year is complete by blur — 29/02/2024 is kept, 29/02/2026 becomes 28. composeValue still never emits an invalid CalendarDate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Roving focus could land on a disabled or unavailable calendar day, but the arrow-key handler was only attached to selectable cells — so once focus reached such a day there was no way to navigate off it. Per APG, arrow keys traverse *through* disabled dates; they just can't be selected. Attach onKeyDown to every in-month cell (gated on `focusable`, not `interactive`) while keeping click + Enter/Space selection gated on `interactive`. selectDate already rejects out-of-range/unavailable dates. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… just blur An impossible day (e.g. 29 Feb in a non-leap year) could persist when the field was left in ways that didn't fire the group's blur clamp — including editing a previously-valid leap date's year (29/02/2024 → 2026). Clamp the day to the month's real length inside `commit`, the moment the date is fully specified (a 4-digit year). Partial years are left untouched so the day isn't shrunk mid-typing before the final year's leap-ness is known. The on-blur `clampDate` remains the backstop for the year-still-empty case. This makes the correction blur-independent: typing 29/02/2026 self-corrects to 28 as soon as the year lands, and re-typing the year of a valid leap date re-validates immediately. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an "In a form (submit validation)" section to the date-picker demo page: a standard Form + submit Button, with the DatePicker's required/past-date checks run on submit. The DatePicker isn't a Base UI Field control, so the error is surfaced through its own errorMessage/isInvalid props and clears as soon as a valid date is picked. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…over
Opening the popover moves focus into the grid (and roving nav moves it between
cells). Those programmatic `.focus()` calls let the browser scroll the focused
cell into view, jumping the page even when the field was already visible.
Pass `{ preventScroll: true }` to every programmatic focus in the calendar
(popover-open auto-focus, roving-focus effect, Tab containment).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "Week starts Sunday (default)" label was misleading: omitting `firstDayOfWeek` follows the active locale (Sunday for en-US, Monday for en-GB), so on a Monday-first locale it never showed Sunday. Replace with explicit "Forced Sunday" / "Forced Monday" examples plus a "Locale default" one, and a note explaining the behaviour. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…apse The group inherits `w-full min-w-0` from the shared input classes, so in a narrow or flex container it could shrink to nothing. Floor it at 142px — enough for "dd / mm / yyyy" + the trigger icon plus padding. Content can still exceed it, so wider locales (e.g. ja-JP) grow past the floor. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rcing to eq Date columns narrowed their operator set to eq/gte/lte/between (dropping gt/lt/ne). But a saved view or `useCollectionVariables` config can still hold a date filter on a dropped operator — the chip rendered fine, yet opening the editor preselected `eq` and omitted the original operator, so hitting Apply silently rewrote the filter's semantics (e.g. "after X" → "on X"). `resolveTemporalOperator` now keeps an incoming operator that's outside the standard set as a selectable, preselected option (for that one filter), so opening + re-applying never changes it. Applies to both the temporal and numeric editors; truly unknown operators still fall back to eq. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mobile gap The proposal still recorded react-aria-components as the v1 decision while this PR ships the §9 Base UI + @internationalized/date variant — a self-contradiction. Add a dated Revision note: the foundation swap is proposed for v1 and *pending sign-off*, with the original 2026-06-17 decision kept verbatim for the record. Also surface the mobile/touch-typing limitation (spinbutton segments, no hidden numeric input) and the not-yet-SR-audited caveat in the component docs and the changeset — previously only in the comparison proposal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ecision Foundation swap approved 2026-06-26. Update the proposal's Status and Revision note from "pending sign-off" to a finalised decision: v1 builds on the §9 @internationalized/date + Base UI variant, superseding the original 2026-06-17 react-aria-components decision (kept verbatim below for the record). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…not a hidden input) The comparison doc claimed react-aria enables mobile typing via a hidden `<input inputmode="numeric">`. That's wrong: react-aria's segments are `contentEditable` spans with `inputmode="numeric"` — the soft keyboard surfaces because the focused element is editable. Ours are non-editable spinbutton divs driven by keydown (which soft keyboards don't emit), hence no touch typing. Correct the comparison, component doc, and changeset, and note the gap is addressable by matching react-aria's contentEditable + beforeinput approach. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…the proposal Add a "Post-v1 fast-follows" section detailing the contentEditable + inputmode + beforeinput approach for touch typing (mirroring react-aria), plus a pointer to the comparison doc's full gap inventory — so the implementation thinking is recorded rather than living only in a PR thread. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Summary
Adds three accessible date components —
DateField,DatePicker, andCalendar— built on@internationalized/date(value layer) and Base UI (Popover), and wires theDatePickerinto the DataTable date-filter editor.The segmented input and the APG calendar grid are hand-rolled, so the only net-new dependency is
@internationalized/date(~11 KB gz) — Base UI is already in the bundle. This is the lighter-foundation option from the design proposal §9; docs/proposals/date-picker-impl-comparison.md records the measured bundle comparison against the react-aria variant (~11 KB vs ~74 KB) for the same public API and a11y contract.Video walkthrough
date-picker-walkthrough.mp4
What's included
Components —
DateField/DatePicker/CalendarDateFormatter)02advances;2→9builds29), Backspace to cleartimeZonesupport onAppShellwithuseResolvedLocale()/useTimeZone()hooks;@internationalized/datehelpers and types re-exported from the package (no separate install)DataTable date filters
date-type filter editor renders theDatePicker(single value andbetweenranges)dateonly; number/datetime/time operators unchanged. en + ja labels.15 Jun 2026) instead of raw ISOExamples & docs
/date-pickerinteractive demo page/data-tablepage: DataTable + filters driven byuseCollectionVariablesand a promise-based async data source (GraphQL-query stub)date-picker,data-table), the design proposal + implementation-comparison docs, and a changesetTesting
role="grid"cells withdata-*state,role="dialog"popover, roving-focus keyboard nav, and the date-filter commit/clear/between + operator-label pathsScope notes
contentEditablespinbutton<div>s read viakeydown, so on-screen keyboards don't type into them (on mobile the calendar popover is the touch path; desktop keyboard + calendar work fully). Addressable by mirroring react-aria —contentEditablesegments +inputmode="numeric"+ a nativebeforeinputhandler. The full approach is written up in the proposal (date-picker.md→ "Post-v1 fast-follows → Mobile / touch text entry") so the thinking isn't lost; it needs real-device QA before sign-off.🤖 Generated with Claude Code