diff --git a/.changeset/odd-maps-glow.md b/.changeset/odd-maps-glow.md new file mode 100644 index 00000000..5f3d0201 --- /dev/null +++ b/.changeset/odd-maps-glow.md @@ -0,0 +1,36 @@ +--- +"@tailor-platform/app-shell": major +--- + +Refactor `DateField` / `DatePicker` to follow the same composition model as `Field`, `Select`, `Combobox`, and `Autocomplete`. + +The date controls are now **control-first**: field chrome moved out of the control props and into `Field.Root` composition. This is a breaking change for the field chrome API (`label`, `description`, `errorMessage`, `isInvalid`), while the semantic date props (`isRequired`, `isDisabled`, `isReadOnly`, `minValue`, `maxValue`, `isDateUnavailable`) remain top-level and aligned with `Calendar`. + +Before: + +```tsx + +``` + +After: + +```tsx + + Delivery date + + When should we ship your order? + {error} + +``` + +Standalone usage still works with accessible naming: + +```tsx + +``` diff --git a/docs/components/date-picker.md b/docs/components/date-picker.md index 20df4f31..f88b934b 100644 --- a/docs/components/date-picker.md +++ b/docs/components/date-picker.md @@ -5,9 +5,7 @@ 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. +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/) and Base UI. [Live preview in the UI Catalogue →](https://ui.tailor.tech/components/date-picker) @@ -21,36 +19,48 @@ import { // Date value helpers (re-exported from @internationalized/date) parseDate, getLocalTimeZone, + today, 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`. +## API shape + +`DateField` and `DatePicker` are standalone composite controls. + +- They own date entry, keyboard behavior, constraints, locale/timezone handling, and form value serialization. +- They expose standard labeling hooks: `id`, `aria-label`, `aria-labelledby`, `aria-describedby`, and `isInvalid`. +- They do **not** auto-wire into Base UI `Field.Root`; use standard HTML labels / descriptions / errors instead. ## 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. +Standalone usage with an accessible name: ```tsx - + ``` -### With description and error +With a visible label + description: ```tsx + +

Format follows your locale

``` -### Controlled +Controlled: ```tsx const [date, setDate] = useState(null); -; + +; ``` ## DatePicker @@ -58,14 +68,18 @@ const [date, setDate] = useState(null); A `DateField` with a calendar popover. ```tsx - + ``` -### Constrained + unavailable dates +Constrained + unavailable dates: ```tsx + { const dow = date.toDate(getLocalTimeZone()).getDay(); @@ -74,15 +88,34 @@ A `DateField` with a calendar popover. /> ``` -### Week start +Week start: ```tsx - + +``` + +## External errors + +Use standard HTML + ARIA for external errors: + +```tsx + + +{error &&

{error}

} ``` ## Calendar -A standalone calendar grid for custom date-selection UIs (e.g. reporting filters). +A standalone calendar grid for custom date-selection UIs. ```tsx console.log(date)} /> @@ -93,95 +126,54 @@ A standalone calendar grid for custom date-selection UIs (e.g. reporting filters Locale and timezone come from AppShell automatically. Override per field with `locale` / `timeZone`: ```tsx - + ``` -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, `/` commits the current segment and advances (so a single `1` means January, not the start of `1x`). -- **Whole-date shortcuts** (QuickBooks Online-style, case-insensitive): `t` today · `m`/`h` start/end of the entered month (current month when empty) · `y`/`r` start/end of the year · `w`/`k` start/end of the week (locale-aware) · `-` previous day · `=`/`+` next day (both step across month **and** year boundaries; `+` needs no Shift). A 1–2 digit year expands to the 2000s on blur (`26` → `2026`). These work **from a focused date segment** (they set the field value, clamped to `minValue`/`maxValue`) **and while the calendar popover is open** (they move the highlighted day like the arrow keys — press `Enter` to confirm; `minValue`/`maxValue` clamp and unavailable days can't be confirmed). -- **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. `Alt`+`↓` opens the calendar from the field (`DatePicker`). - -## 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 `
` 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. +- **Segments:** `↑`/`↓` increment/decrement, digits type-to-fill (auto-advance), `←`/`→` move between segments, `Backspace` clears, `/` commits the current segment and advances. +- **Whole-date shortcuts:** `t` today · `m`/`h` start/end of the entered month · `y`/`r` start/end of the year · `w`/`k` start/end of the week · `-` previous day · `=`/`+` next day. +- **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. `Alt`+`↓` opens the calendar from the field. ## 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 `` with the ISO value for form submission | -| `firstDayOfWeek` | `"sun" \| "mon" \| "tue" \| "wed" \| "thu" \| "fri" \| "sat"` | Override the locale's week start for the `w`/`k` keyboard shortcuts; omit to follow the locale | -| `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. +| Prop | Type | Description | +| -------------------------------- | ------------------------------------------------------------- | ---------------------------------------------------------- | +| `value` / `defaultValue` | `DateValue \| null` | Controlled / uncontrolled value | +| `onChange` | `(v: DateValue \| null) => void` | Fires when the value changes | +| `onBlur` | `() => void` | Fires when focus leaves the whole segmented control | +| `minValue` / `maxValue` | `DateValue` | Inclusive date range bounds | +| `isDateUnavailable` | `(date: DateValue) => boolean` | Marks specific dates unavailable | +| `isDisabled` | `boolean` | Disables interaction and form submission | +| `isReadOnly` | `boolean` | Allows focus/navigation without editing | +| `isRequired` | `boolean` | Marks the control required | +| `isInvalid` | `boolean` | Adds invalid styling / `aria-invalid` to the segmented UI | +| `placeholderValue` | `DateValue` | Seeds unset segments | +| `autoFocus` | `boolean` | Focus the first segment on mount | +| `locale` | `string` | BCP-47 locale override | +| `name` | `string` | Emits a form value through the proxy input | +| `id` | `string` | Proxy input id (use with external `