Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
f9f5569
docs(date-picker): add design proposal
interacsean Jun 17, 2026
2aed518
docs(date-picker): drop Radix references from proposal
interacsean Jun 17, 2026
8fc9cb9
docs(date-picker): clarify value layer (representation) vs Intl forma…
interacsean Jun 18, 2026
a62647a
docs(date-picker): document @internationalized/date packaging (regula…
interacsean Jun 18, 2026
0b690e2
feat(date-picker): add DateField/DatePicker/Calendar (@internationali…
interacsean Jun 23, 2026
7835500
feat(data-table): use DatePicker for date filters + example page
interacsean Jun 23, 2026
77ea85e
fix(date-picker): advance on leading-zero entry + align popover to field
interacsean Jun 24, 2026
86213e1
feat(data-table): friendlier date filter operators (exact date / afte…
interacsean Jun 24, 2026
dc0f4bb
feat(data-table): locale-format the date value in filter chips
interacsean Jun 24, 2026
5b1b7a1
refactor(date-picker): idiomatic Tailwind focus ring + snapshot tests
interacsean Jun 24, 2026
8a1adb0
docs: document DatePicker-backed date filters + aria-label prop
interacsean Jun 24, 2026
57c816c
fix(example): make Invoice a type so it satisfies the DataTable row c…
interacsean Jun 24, 2026
03df912
Merge remote-tracking branch 'origin/main' into feat/date-picker-base…
interacsean Jun 24, 2026
b44fe85
fix(date-picker): allow day 31 before a month, clamp impossible dates…
interacsean Jun 24, 2026
1f2b11b
fix(date-picker): let keyboard focus traverse disabled/unavailable days
interacsean Jun 25, 2026
a375999
fix(date-picker): self-correct impossible day on date completion, not…
interacsean Jun 25, 2026
8f125b4
docs(example): demonstrate DatePicker validation in a Form
interacsean Jun 26, 2026
b63134c
fix(date-picker): don't scroll the page when opening the calendar pop…
interacsean Jun 26, 2026
e0ac877
docs(example): clarify locale-dependent week start in date-picker demo
interacsean Jun 26, 2026
36863ae
fix(date-picker): give the field a default min-width so it can't coll…
interacsean Jun 26, 2026
b8c3062
fix(data-table): preserve legacy date-filter operators instead of coe…
interacsean Jun 26, 2026
da0ac67
docs(date-picker): reconcile proposal with shipped variant; document …
interacsean Jun 26, 2026
65336cd
docs(date-picker): record the Base UI foundation as the approved v1 d…
interacsean Jun 26, 2026
16a4934
docs(date-picker): correct mobile-typing mechanism (contentEditable, …
interacsean Jun 26, 2026
e78c05b
docs(date-picker): capture the mobile-typing fast-follow approach in …
interacsean Jun 26, 2026
76d4069
docs(date-picker): split implemented vs proposed props; proper tables…
interacsean Jul 1, 2026
4686bfb
refactor(date-picker): rename date-picker-standalone.tsx to date-fiel…
interacsean Jul 1, 2026
e694a00
refactor(date-picker): split into date-field/ and calendar/ component…
interacsean Jul 1, 2026
b7d4aa9
Merge remote-tracking branch 'origin/main' into feat/date-picker-base…
interacsean Jul 1, 2026
4ac225a
feat(date-field): backfill current month/year from a partial entry on…
interacsean Jul 2, 2026
40a570a
chore(vite-app): normalize routes.generated.ts ordering
interacsean Jul 2, 2026
51aa166
fix(date-field): use the resolved timezone for DatePicker's field state
interacsean Jul 2, 2026
7483bfa
fix(appshell): normalize a full-tag locale to its language subtag for…
interacsean Jul 2, 2026
b0f05e1
perf(date): memoize Intl formatters out of the per-keystroke/per-cell…
interacsean Jul 2, 2026
cff2ab9
fix(data-table): date-filter picker uses the column's visible label
interacsean Jul 2, 2026
e941583
fix(date-field): controlled null, aria-required, and localized aria s…
interacsean Jul 2, 2026
be04e5e
feat(date-field): localize the remaining built-in aria strings
interacsean Jul 2, 2026
f0edb3a
Merge remote-tracking branch 'origin/main' into feat/date-picker-base…
interacsean Jul 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .changeset/date-picker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
"@tailor-platform/app-shell": minor
---

Add DateField, DatePicker, and Calendar components (@internationalized/date + Base UI implementation)

Introduces three accessible date-input components, implemented on `@internationalized/date` (value layer) and Base UI (`Popover`) with hand-rolled segmented-input and calendar-grid behaviour:

- `DateField` — segmented date/time input with label, description, and error message
- `DatePicker` — date field with a popover calendar
- `Calendar` — standalone calendar grid

All three accept `LocalizedString` labels/descriptions and resolve locale + timezone from the AppShell context. The `@internationalized/date` value types (`CalendarDate`, `CalendarDateTime`, `ZonedDateTime`, …) and helpers (`today`, `parseDate`, `getLocalTimeZone`, …) are re-exported from `@tailor-platform/app-shell`.

New AppShell context hooks:

- `useResolvedLocale()` — full BCP-47 locale (e.g. `"en-GB"`) plus the language code
- `useTimeZone()` — the configured IANA timezone, falling back to the user's local timezone

AppShell now accepts an optional `timeZone` prop.

> This is the **`@internationalized/date` + Base UI** variant — the lighter foundation tracked in the design proposal (§9). Net-new dependency is just `@internationalized/date` (~11 KB gz); Base UI is already in the bundle. Public API and accessibility contract are identical to the react-aria variant.
>
> **Known limitation:** the segmented input is built from `role="spinbutton"` elements that aren't `contentEditable`, so on-screen-keyboard typing on touch devices is limited — the calendar popover is the touch-friendly path (desktop keyboard entry works fully). The APG behaviour is unit-tested but not yet screen-reader-audited.
15 changes: 14 additions & 1 deletion docs/components/data-table.md
Original file line number Diff line number Diff line change
Expand Up @@ -492,14 +492,27 @@ The `filter` property on a column accepts a `FilterConfig` object. When set, the
| `string` | Text | `eq`, `ne`, `contains`, `notContains`, `hasPrefix`, `hasSuffix`, `notHasPrefix`, `notHasSuffix`, `in`, `nin` |
| `number` | Number | `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, **`between`**, `in`, `nin` |
| `datetime` | Datetime-local | `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, **`between`**, `in`, `nin` |
| `date` | Date | `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, **`between`**, `in`, `nin` |
| `date` | **DatePicker** | `eq` (_exact date_), `gte` (_after_), `lte` (_before_), **`between`** |
| `time` | Time | `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, **`between`**, `in`, `nin` |
| `enum` | Dropdown | `eq`, `ne`, `in`, `nin` |
| `boolean` | Toggle | `eq`, `ne` |
| `uuid` | Text | `eq`, `ne`, `in`, `nin` |

When the user selects the `between` operator on a `number`, `datetime`, `date`, or `time` column, the filter chip renders a range input with **min** and **max** bounds.

### Date Filters

`date` columns use the app-shell [`DatePicker`](./date-picker.md) as the filter input (single value and `between` ranges) and present a friendlier, slimmer operator set:

| Operator | Label | Meaning |
| --------- | ------------ | -------------------------- |
| `eq` | _exact date_ | matches that calendar date |
| `gte` | _after_ | on or after (inclusive) |
| `lte` | _before_ | on or before (inclusive) |
| `between` | _between_ | inclusive min–max range |

`gt` / `lt` / `ne` are intentionally dropped — the inclusive _after_ / _before_ cover the intent. The filter chip shows the value as a locale-formatted date (e.g. `15 Jun 2026`), and the picker resolves its locale/timezone from the AppShell context. (Only `date` is remapped this way; `datetime` and `time` keep the full numeric operator set and native inputs.)

### String Filter Case Sensitivity

String filters are **case-insensitive by default** — they use the Tailor Platform `regex` operator with an `(?i)` prefix. The filter chip renders a **"Case sensitive"** checkbox that lets users opt into exact-case matching.
Expand Down
182 changes: 182 additions & 0 deletions docs/components/date-picker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
---
title: DatePicker
description: Accessible date input components (@internationalized/date + Base UI)
---

# DatePicker

Three related components for date input — a segmented field, a field with a calendar popover, and a standalone calendar grid. Built on [`@internationalized/date`](https://react-spectrum.adobe.com/internationalized/date/) (the value layer) and Base UI (`Popover`), with the segmented input and calendar grid implemented to the [ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/patterns/) date-picker/grid patterns. They integrate automatically with AppShell's locale and timezone context.

> **Implementation note.** This is the `@internationalized/date` + Base UI variant. The public API and accessibility contract are identical to the react-aria variant; only the internals differ. See `docs/proposals/date-picker-impl-comparison.md`.

## Import

```tsx
import {
DateField,
DatePicker,
Calendar,
// Date value helpers (re-exported from @internationalized/date)
today,
parseDate,
getLocalTimeZone,
type CalendarDate,
type DateValue,
} from "@tailor-platform/app-shell";
```

No separate `@internationalized/date` install needed — the value types and helpers are re-exported from `@tailor-platform/app-shell`.

## DateField

A segmented input that lets users type dates digit-by-digit, with per-segment Up/Down, type-to-fill auto-advance, and full keyboard support.

```tsx
<DateField label="Invoice date" />
```

### With description and error

```tsx
<DateField
label="Start date"
description="Format follows your locale"
errorMessage="A start date is required"
/>
```

### Controlled

```tsx
const [date, setDate] = useState<CalendarDate | null>(null);
<DateField label="Invoice date" value={date} onChange={setDate} />;
```

## DatePicker

A `DateField` with a calendar popover.

```tsx
<DatePicker label="Ship date" />
```

### Constrained + unavailable dates

```tsx
<DatePicker
label="Delivery date"
minValue={today(getLocalTimeZone())}
isDateUnavailable={(date) => {
const dow = date.toDate(getLocalTimeZone()).getDay();
return dow === 0 || dow === 6; // weekends
}}
/>
```

### Week start

```tsx
<DatePicker label="Date" firstDayOfWeek="mon" />
```

## Calendar

A standalone calendar grid for custom date-selection UIs (e.g. reporting filters).

```tsx
<Calendar aria-label="Select date" onChange={(date) => console.log(date)} />
```

## Localization

Locale and timezone come from AppShell automatically. Override per field with `locale` / `timeZone`:

```tsx
<DatePicker label="Date" locale="ja-JP" />
```

Segment order, first-day-of-week, and month/weekday names all follow the resolved locale.

## Keyboard

- **Segments:** `↑`/`↓` increment/decrement, digits type-to-fill (auto-advance), `←`/`→` move between segments, `Backspace` clears.
- **Calendar grid:** arrows move by day/week, `Home`/`End` to week start/end, `PageUp`/`PageDown` by month, `Shift`+`PageUp`/`PageDown` by year, `Enter`/`Space` selects.

## Accessibility

- The segmented field is a labelled `role="group"` of `role="spinbutton"` segments with `aria-valuemin`/`max`/`now`/`text`.
- The calendar is a `role="grid"`; each day is a button with a full-date `aria-label`; disabled/unavailable days are announced via `aria-disabled`.
- The popover is a labelled `role="dialog"`.

> **Known limitations (this variant).** The segments are `<div role="spinbutton">` that aren't `contentEditable`, so a touch device's on-screen keyboard doesn't open for typing — on mobile, use the calendar popover to pick a date (desktop keyboard entry and the calendar both work fully). The APG patterns are implemented and unit-tested but **not yet screen-reader-audited**, and RTL arrow-key flipping isn't handled. See [the implementation comparison](../proposals/date-picker-impl-comparison.md) ("Known gaps vs react-aria") for the full list.

## Props

The tables below list props this variant **actually implements** for v1 (date granularity). A few props are part of the type surface — kept identical to the react-aria variant so a later swap is source-compatible — but aren't acted on yet; those are called out under [Proposed / not yet implemented](#proposed--not-yet-implemented).

### DateFieldProps

| Prop | Type | Description |
| ----------------------------------------- | -------------------------------- | ----------------------------------------------------------------------- |
| `label` | `LocalizedString` | Field label |
| `description` | `LocalizedString` | Helper text below the field |
| `errorMessage` | `LocalizedString` | Error text; also sets the invalid state |
| `value` / `defaultValue` | `DateValue \| null` | Controlled / uncontrolled value (`CalendarDate` at date granularity) |
| `onChange` | `(v: DateValue \| null) => void` | Fires on a complete, valid value; `null` when cleared |
| `isDisabled` / `isReadOnly` / `isInvalid` | `boolean` | State flags |
| `isRequired` | `boolean` | Sets `aria-required` on the segments (no visual required indicator yet) |
| `placeholderValue` | `DateValue` | Seeds unset segments (increment start + segment order) |
| `autoFocus` | `boolean` | Focus the first segment on mount |
| `locale` | `string` | BCP-47 locale override (defaults to the AppShell formatting locale) |
| `name` | `string` | Emits a hidden `<input>` with the ISO value for form submission |
| `aria-label` | `string` | Accessible name when there's no visible `label` (e.g. compact filters) |
| `className` | `string` | Root element class |

> `DateField` has no calendar, so `minValue` / `maxValue` / `isDateUnavailable` don't apply to it — they're honoured by `DatePicker` and `Calendar` below.

### DatePickerProps

All `DateFieldProps`, plus:

| Prop | Type | Description |
| ----------------------- | ------------------------------------------------------------- | -------------------------------------------------------------------- |
| `minValue` / `maxValue` | `DateValue` | Earliest / latest selectable date in the calendar |
| `isDateUnavailable` | `(date: DateValue) => boolean` | Mark individual dates unselectable (still keyboard-navigable) |
| `firstDayOfWeek` | `"sun" \| "mon" \| "tue" \| "wed" \| "thu" \| "fri" \| "sat"` | Force the calendar's first column; omit to follow the locale |
| `timeZone` | `string` | IANA timezone for resolving "today"; defaults to AppShell `timeZone` |

### CalendarProps

The standalone calendar grid. It has no segmented input, so its surface is listed in full:

| Prop | Type | Description |
| -------------------------------------- | ------------------------------ | ------------------------------------------------------------- |
| `value` / `defaultValue` | `DateValue \| null` | Controlled / uncontrolled selected date |
| `onChange` | `(v: DateValue) => void` | Fires when a date is selected |
| `minValue` / `maxValue` | `DateValue` | Earliest / latest selectable date |
| `isDateUnavailable` | `(date: DateValue) => boolean` | Mark individual dates unselectable (still keyboard-navigable) |
| `focusedValue` / `defaultFocusedValue` | `DateValue` | Controlled / initial focused (visible) date |
| `onFocusChange` | `(date: CalendarDate) => void` | Fires when the focused date changes (arrows, month paging) |
| `firstDayOfWeek` | `"sun" \| "mon" \| …` | Force the first column; omit to follow the locale |
| `isDisabled` / `isReadOnly` | `boolean` | Disable the grid / prevent selection changes |
| `timeZone` | `string` | IANA timezone for "today"; defaults to AppShell `timeZone` |
| `locale` | `string` | BCP-47 locale override |
| `aria-label` / `aria-labelledby` | `string` | Accessible name for the grid |
| `className` | `string` | Root element class |

### Proposed / not yet implemented

Accepted by the prop types (for parity with the react-aria variant) but **not acted on** in this variant yet:

| Prop | Type | Status |
| -------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `granularity` | `"day" \| "hour" \| "minute" \| "second"` | Only `"day"` is supported (the default). Time granularities — and the `CalendarDateTime` / `ZonedDateTime` values they produce — are the tracked **DateTime fast-follow**; the calendar has no time selection yet. |
| `hourCycle` | `12 \| 24` | No effect until time granularity lands (12h/24h only matters with an hour segment). |
| `hideTimeZone` | `boolean` | Unused; only relevant to `ZonedDateTime` display (time granularity). |

See the proposal's [Post-v1 fast-follows](../proposals/date-picker.md) for the DateTime plan.

## Related

- [Form](./form.md) — wrap date fields with validation
- [Input](./input.md) — plain text input
Loading
Loading