diff --git a/Conformance/src/internal/slotProfiles.tsx b/Conformance/src/internal/slotProfiles.tsx index 75fc9ac3..41eeab9f 100644 --- a/Conformance/src/internal/slotProfiles.tsx +++ b/Conformance/src/internal/slotProfiles.tsx @@ -357,7 +357,9 @@ export const createSlotProfiles = ( ownershipSelectors: ['[data-cratis-part="trigger"]'], createProps: () => recordProps({ - content: 'Save changes', + // A React node, not a string: Tooltip content accepts any node since 4.16.0, and an + // adapter must render it as-is rather than coerce it to text. + content: createElement('span', null, 'Save ', createElement('strong', null, 'changes')), children: createElement( 'button', { type: 'button', className: 'sample-trigger' }, @@ -374,6 +376,7 @@ export const createSlotProfiles = ( }, exercise: async (document) => ({ popupCount: document.querySelectorAll('[data-cratis-part="popup"]').length, + popupEmphasis: document.querySelector('[data-cratis-part="popup"] strong')?.textContent, }), }, { @@ -628,6 +631,13 @@ export const createSlotProfiles = ( 'aria-label': 'Bounded delivery date', onChange: () => undefined, } satisfies DatePickerInputProps), + recordProps({ + value: new Date(2024, 5, 15), + showButtonBar: true, + pt: { clear: { disabled: true } }, + 'aria-label': 'Required delivery date', + onChange: () => undefined, + } satisfies DatePickerInputProps), ], activate: async (_document, container) => { click(container, '[data-cratis-part="trigger"]'); diff --git a/Conformance/src/runConformance.tsx b/Conformance/src/runConformance.tsx index e58dabaa..68b193bf 100644 --- a/Conformance/src/runConformance.tsx +++ b/Conformance/src/runConformance.tsx @@ -146,6 +146,24 @@ const passThrough = (profile: SlotProfile) => }), ); +/** + * Combines a part variant's own `pt` attributes with the profile's pass-through markers, part by + * part, so a variant can reach a state through `pt` (for example a disabled action) while every + * part still carries its conformance marker. + */ +const variantPassThrough = (profile: SlotProfile, variantPt: unknown) => { + const markers = passThrough(profile) as Record>; + if (!variantPt || typeof variantPt !== 'object') return markers; + const merged: Record> = { ...markers }; + for (const [part, attributes] of Object.entries(variantPt as Record)) { + merged[part] = { + ...(attributes as Record), + ...(markers[part] ?? {}), + }; + } + return merged; +}; + const addCheck = ( checks: ConformanceCheck[], family: ConformanceFamily, @@ -409,7 +427,7 @@ const checkSlot = async ( const variantFixture = await mount( document, typedDeclaration, - { ...variant, pt: passThrough(profile) }, + { ...variant, pt: variantPassThrough(profile, variant.pt) }, profile.refCapable, options.wrapper, ); diff --git a/Documentation/Chat/index.md b/Documentation/Chat/index.md index 1c284f27..91e1d614 100644 --- a/Documentation/Chat/index.md +++ b/Documentation/Chat/index.md @@ -91,6 +91,7 @@ export const Workspace = () => { | ---------------------------------------- | --------------------------------------------- | --------- | ---------------------------------------------------------------------------------------------------- | | `open` / `onClose` | `boolean`, `() => void` | Required | The host owns the open state; the close button calls `onClose`. | | `topics` / `messages` | `TTopic[]`, `TMessage[]` | Required | The data, in any order. See [Topics and naming](./topics-and-naming.md). | +| `topicsStatus` / `messagesStatus` | `ChatStatus` | `Ready` | Optional independent states for the list and conversation. See [Query display states](#query-display-states). | | `onSendMessage` | `(topicId, body, mentions) => void` | Required | Receives the trimmed body and who it mentions. | | `onStartTopic` | `() => ChatIdentifier \| undefined \| Promise` | — | Enables the new-topic affordance; answer the new topic's id to open it. | | `onRequestTopicName` / `isTopicUnnamed` | callbacks | — | The host-side naming contract. | @@ -104,6 +105,21 @@ export const Workspace = () => { | `modal` | `boolean` | `false` | Adds a blocking backdrop with Escape and outside-click dismissal. | | `labels`, `className`, `pt` | `ChatSidebarLabels`, `string`, `ChatSidebarParts` | — | English label overrides, panel class, and stable parts. | +## Query display states + +If your application owns the queries, import `ChatStatus` from `@cratis/components/Chat` and pass `topicsStatus` and `messagesStatus` to `ChatSidebar`. Both default to `ChatStatus.Ready`, so omitting them preserves the existing empty states. The standalone `ChatTopicList` and `ChatConversation` accept the same optional enum through their `status` prop. + +| Status | Without topics or messages | With existing topics or messages | +| ------ | -------------------------- | -------------------------------- | +| `ChatStatus.Ready` | Ordinary empty-state text | Existing content | +| `ChatStatus.Loading` | Loading text with a status announcement | Existing content remains visible during a refetch | +| `ChatStatus.Failed` | Failure text with an alert | Failure alert above the existing content; topics or messages remain visible | +| `ChatStatus.Unauthorized` | Access-denied text with an alert | Access-denied alert replaces the content | + +While access is denied, the new-topic button and conversation composer are disabled, even if a draft was already started. The close and back buttons remain available. + +Override these messages with `labels.topicList.loading`, `.failed`, or `.unauthorized` for topics, and `labels.conversation.loading`, `.failed`, or `.unauthorized` for messages. Unset fields use English defaults; the existing `empty` labels still apply to successful empty results. Chat labels do not come from `CratisComponentsProvider.messages`. The [observable-query wrapper](./observable-queries.md#loading-and-failed-queries) resolves statuses for you. + ## Focus and dismissal The default, non-modal sidebar is a plain panel with an `h2` title. It portals to `document.body` after mount, or to the container supplied by the provider's `overlayEnvironment`. If that container is unavailable, the portal waits and checks again on the next render while open; server rendering produces no panel markup, so hydration does not mismatch. The panel slides in and out on open and close, except when reduced motion is requested. diff --git a/Documentation/Chat/observable-queries.md b/Documentation/Chat/observable-queries.md index 5bf1cf73..24c7433d 100644 --- a/Documentation/Chat/observable-queries.md +++ b/Documentation/Chat/observable-queries.md @@ -43,10 +43,18 @@ export const LiveChat = () => { - `topicsQuery` subscribes as soon as the component mounts, whether or not `open` is `true` (pass `topicsArguments` when the query takes any). Mount the wrapper only where the subscription should be live. - `messagesQuery` subscribes only while a topic is open. `messagesArguments` derives the query arguments from the open topic's id; answering `undefined` holds the subscription. - The wrapper owns the topic selection so it can re-target the messages subscription; everything else — every callback, every render hook — is the same contract as [`ChatSidebar`](./index.md). It does not accept `selectedTopicId`; observe changes through `onTopicSelected`. -- Only each query's `data` reaches the sidebar. While a query is loading or after it fails, the sidebar shows an empty topic list or an empty conversation. The wrapper surfaces no loading or error state. +- The wrapper passes each query's data and display status to the sidebar. With no topics or messages, a pending query shows a loading announcement; a failed, invalid, or unauthorized query shows an alert rather than the empty-state text. It never displays server exception details. + +## Loading and failed queries + +The topics and messages queries resolve their statuses independently. Authorization denial takes precedence over failure or validation errors; failure takes precedence over loading. When a query is performing with no data, its list or conversation shows a loading message. When it completes successfully with an empty result, the ordinary empty-state text returns. Topics and messages stay visible during a refetch of the same arguments only while the latest query result contains them. Changing `topicsArguments` or opening another topic shows loading without the previous query's status or data until the new result arrives; revisiting earlier arguments can restore a cached result. After a failure, the alert appears above any topics or messages that the latest result still contains; the wrapper does not retain the last successful data if the failed result omits it. An unauthorized result replaces previously loaded content with an access-denied alert and disables starting a new topic or composing a message. + +Set `labels.topicList.loading`, `labels.topicList.failed`, and `labels.topicList.unauthorized` for the topic list; set the corresponding `labels.conversation` keys for messages. The English defaults are “Loading topics…”, “Could not load topics.”, and “You are not authorized to view these topics.” for the list, and “Loading messages…”, “Could not load messages.”, and “You are not authorized to view these messages.” for the conversation. Chat uses `labels`, not the provider's `messages.dataTable` settings. + +If you own the queries yourself, use [`ChatSidebar`](./index.md#query-display-states) with `topicsStatus` and `messagesStatus` instead of this wrapper. ## When not to use it If the application already manages its own subscriptions (a shared cache, a view model layer, data arriving over something other than Arc queries), use `ChatSidebar` directly and hand it the arrays — the wrapper adds nothing but the two `useObservableQuery` calls. -Use `ChatSidebar` directly as well when you need to show a loading or failure state, to own the selection (deep links, restoring the open topic), or to map read models whose shape differs from `ChatTopic` and `ChatMessage`. Call `useObservableQuery` (or the proxies' `use()` methods) yourself and pass `result.data` along with your own status UI. +Use `ChatSidebar` directly as well when you need to own the selection (deep links, restoring the open topic), map read models whose shape differs from `ChatTopic` and `ChatMessage`, or render query states differently from the built-in status messages. Call `useObservableQuery` (or the proxies' `use()` methods) yourself and pass the data, optional statuses, and any custom UI. diff --git a/Documentation/CommandDialog/index.md b/Documentation/CommandDialog/index.md index 670439c4..113d4b8c 100644 --- a/Documentation/CommandDialog/index.md +++ b/Documentation/CommandDialog/index.md @@ -14,7 +14,7 @@ CommandDialog simplifies the process of presenting a command form to users withi - Binds `CommandForm` field children to one command instance - Keeps the confirm button disabled until the command passes client validation - Field-level change tracking and custom field validation -- Pre-execution transformation of values +- Pre-execution transformation of values and optional approval before execution - Success and cancellation handling through the Arc dialog context - Busy state management during command execution (buttons and fields disabled, spinner shown) - Integration with Cratis Arc command system @@ -194,6 +194,7 @@ The form is treated as invalid until its first validation finishes, so confirm s - `onFieldValidate`: Custom validation function for fields - `onFieldChange`: Callback when field values change - `onBeforeExecute`: Transform command values before execution. It must return the values to run with and may be async +- `confirmBeforeExecute(values)`: Optional synchronous or async guard called with the transformed values after validation. Return `true` to execute or `false` to stay open without executing - `style`: Custom CSS styles - `contentStyle`: Custom CSS styles for the dialog content area - `width`: Dialog width (default: `'450px'`) @@ -216,7 +217,7 @@ The form is treated as invalid until its first validation finishes, so confirm s - `onUnauthorized()`: Invoked when authorization fails. - `onValidationFailure(validationResults: ValidationResult[])`: Invoked on validation errors. -Multiple callbacks may fire for the same execution. For example, both `onFailed` and `onValidationFailure` will be invoked for validation errors. If `onBeforeExecute` throws, the command does not execute, no result callback runs, and the dialog stays open. +Multiple callbacks may fire for the same execution. For example, both `onFailed` and `onValidationFailure` will be invoked for validation errors. If `onBeforeExecute` throws, the command does not execute, no result callback runs, and the dialog stays open. A rejection from `confirmBeforeExecute` also keeps the dialog open and skips execution; it calls `onException` with the error message and stack trace (or logs the error if no callback is supplied), not `onFailed`. ### Dialog Callbacks @@ -225,11 +226,50 @@ Multiple callbacks may fire for the same execution. For example, both `onFailed` | User action | `onConfirm` / `onCancel` present | Only `onClose` present | No callback | | ----------- | -------------------------------- | ---------------------- | ----------- | | Confirm, after the command succeeds | `onConfirm()` runs; closes only if it returns `true` | `onClose(DialogResult.Ok)` runs; closes unless it returns `false` | Closes with `DialogResult.Ok` | +| Confirm, `confirmBeforeExecute` returns `false` | Not called; dialog stays open | Not called; dialog stays open | Dialog stays open | | Confirm, command fails | Not called; dialog stays open | Not called; dialog stays open | Dialog stays open | | Cancel, X, `Escape`, backdrop | `onCancel()` runs; closes only if it returns `true` | `onClose(DialogResult.Cancelled)` runs; closes unless it returns `false` | Closes with `DialogResult.Cancelled` | A callback that calls `closeDialog(...)` itself closes the dialog regardless of its return value. Use `closeDialog(DialogResult.Ok, value)` when the caller needs a value; the automatic close never passes one. +## Confirm before executing + +Use `confirmBeforeExecute` for a second, explicit decision. Unlike `onConfirm`, which runs **after** successful execution to decide whether to close, the guard runs **before** execution. A `false` result leaves the command dialog open, does not execute, and calls neither `onSuccess` nor `onFailed`. If you also provide `onBeforeExecute`, the guard receives its returned values, not the original form values. Client validation still happens first. If the values change while confirmation is pending (for example, through `currentValues`), the command is not executed and no result callback runs; confirm again with the new values. + +Register `ConfirmationDialog` in `DialogComponents` above the component using the hook. The hook must run in a child of that provider, not in the component that creates the provider. The confirmation opens on a higher dialog z-index tier and its No result leaves the command form ready to edit or retry: + +```tsx +import { DialogButtons, DialogComponents, DialogResult, useConfirmationDialog } from '@cratis/arc.react/dialogs'; +import { CommandDialog } from '@cratis/components/CommandDialog'; +import { ConfirmationDialog } from '@cratis/components/Dialogs'; +import { UpdateProject } from './UpdateProject'; + +function UpdateProjectDialog() { + const [showConfirmation] = useConfirmationDialog( + 'Save changes?', 'Apply these changes?', DialogButtons.YesNo, + ); + return ( + + command={UpdateProject} + title='Update project' + initialValues={{ name: 'Example Project' }} + confirmBeforeExecute={async (_values) => + (await showConfirmation()) === DialogResult.Yes} + /> + ); +} + +export function ProjectDialogs() { + return ( + + + + ); +} +``` + +The equivalent runnable, type-checked example is the `WithPreExecutionConfirmation` story in `Source/CommandDialog/CommandDialog.stories.tsx`. In an app using `useDialog`, render the command dialog's wrapper inside `DialogComponents` instead of rendering it permanently as above. + ## Controlled visibility Without `useDialog` there is no dialog context, so nothing closes automatically. Close the dialog yourself from `onSuccess` and `onCancel`: @@ -304,7 +344,7 @@ Pressing `Enter` inside a text field does not submit the command. The footer but ## Busy State -`CommandDialog` automatically manages a busy state from the start of `onBeforeExecute` until command execution settles: +`CommandDialog` automatically manages a busy state from the start of `onBeforeExecute`, through `confirmBeforeExecute`, until command execution settles: - All buttons, including header close, are disabled and the primary button shows a loading spinner. - The form fields are disabled and inert, so values cannot change while the command runs. diff --git a/Documentation/CommandForm/calendar-field.md b/Documentation/CommandForm/calendar-field.md index 04ff6c12..d76a2f53 100644 --- a/Documentation/CommandForm/calendar-field.md +++ b/Documentation/CommandForm/calendar-field.md @@ -3,7 +3,7 @@ title: CalendarField description: Bind a Date property on an Arc command to the locale-aware Cratis date picker. --- -`CalendarField` wraps the internationalized Cratis `DatePickerInput` while preserving a `Date | null` command value. +`CalendarField` wraps the internationalized Cratis [`DatePickerInput`](../Common/date-picker-input.md) while preserving a `Date | null` command value. ## Usage diff --git a/Documentation/CommandStepper/index.md b/Documentation/CommandStepper/index.md index 63a8a259..8f6b6996 100644 --- a/Documentation/CommandStepper/index.md +++ b/Documentation/CommandStepper/index.md @@ -62,6 +62,7 @@ export const ProjectWizard = () => ( - `onException`: Callback invoked with the exception messages and stack trace when the result has exceptions - `onUnauthorized`: Callback invoked when the user is not authorized to execute the command - `onBeforeExecute`: Transform command values before execution — it must **return** the values to run with, and it runs only on submit, so it can never satisfy required-field validation (seed those through `initialValues`). Submit turns busy before the transform runs, so an async transform cannot be submitted twice +- `confirmBeforeExecute(values)`: Optional sync or async approval after validation and `onBeforeExecute`. Receives the transformed values; return `false` to skip execution and remain on the last step. A rejected guard calls `onException`, not `onFailed`, and leaves Submit available to retry - `linear` (default `true`), `orientation` (`'horizontal'` default / `'vertical'`), `headerPosition` (`'top'` default / `'bottom'`), `start`, `end`, and `pt`: the active `StepperCustomizationProps` surface. It maps onto stable `root`, `list`, `step`, `header`, `number`, `title`, `separator`, `panels`, and `panel` parts. - `onChangeStep`: Called once after each successful move to a different step through Previous, Next, or a clickable header. Receives `{ index }`, with a zero-based index. Blocked moves and clicks on the current header do not call it. - `ptOptions` and `unstyled`: retained temporarily for source compatibility; ignored because Cratis part attributes always merge and styling is CSS-owned. @@ -77,10 +78,40 @@ Conditional steps written as `{condition && }` are counted correc - `linear` (the default) makes other step headers unclickable; use Previous and Next to navigate. In non-linear mode, clickable headers also cannot advance past a current step showing an error. Neither mode requires a step to be complete before Next when no errors are shown. - The headers form an ordered list of buttons, not ARIA tabs. Each step panel is labelled by its header unless you pass `pt.header.id`. In that case, the headers keep your id and the panels keep their text labels instead of referencing a shared header id. The current header has `aria-current="step"`. - On the last step Submit is always rendered, but it is disabled until the command passes client validation and no step shows an error. (`StepperCommandDialog` hides its Submit button instead.) -- While the command runs, Next and Submit are disabled and Submit shows a spinner. `isBusy` disables Previous, Next, and Submit for your own long-running work. +- While the transform, confirmation, or command runs, Next and Submit are disabled and Submit shows a spinner. `isBusy` disables Previous, Next, and Submit for your own long-running work. - On failure the stepper stays on the last step, and server validation messages appear on their fields and turn their steps red. - On success nothing else happens: the stepper keeps its values and stays on the last step. Navigate away or reset the surrounding view in `onSuccess`. +## Confirm before submitting + +The same `confirmBeforeExecute` callback used by [`CommandDialog`](../CommandDialog/index.md#confirm-before-executing) works on the inline stepper. Fields are disabled from the moment you submit until the guarded submission settles. If the values change during that time (for example, through `currentValues`), the command is not executed and no result callback runs; confirm again with the new values. Wrap the page in `DialogComponents` and call `useConfirmationDialog` from a child of that provider: + +```tsx +import { DialogButtons, DialogComponents, DialogResult, useConfirmationDialog } from '@cratis/arc.react/dialogs'; +import { CommandStepper, StepperPanel } from '@cratis/components/CommandDialog'; +import { InputTextField } from '@cratis/components/CommandForm/fields'; +import { ConfirmationDialog } from '@cratis/components/Dialogs'; +import { CreateProject } from './CreateProject'; + +function ProjectSteps() { + const [confirm] = useConfirmationDialog('Create project?', 'Continue?', DialogButtons.YesNo); + return ( + command={CreateProject} + confirmBeforeExecute={async (_values) => (await confirm()) === DialogResult.Yes}> + + value={(command) => command.name} title='Project name' /> + + + ); +} + +export function ProjectWizardWithConfirmation() { + return ; +} +``` + +No keeps the stepper on the last step with its values intact. The type-checked dialog example is `WithPreExecutionConfirmation` in `Source/CommandDialog/CommandDialog.stories.tsx`. + ## Validation Indicators `CommandStepper` identifies `CommandFormField` children inside each `StepperPanel` and extracts the field names from their `value` accessors. diff --git a/Documentation/Common/basic-controls.md b/Documentation/Common/basic-controls.md index faff4ed5..86da10a1 100644 --- a/Documentation/Common/basic-controls.md +++ b/Documentation/Common/basic-controls.md @@ -57,6 +57,20 @@ import { `Radio` represents exactly one native option. Give related options the same `name`; the browser owns grouping. Components does not add radio-group state or keyboard orchestration. +## Tooltips + +Wrap one focusable control with `Tooltip` to show supplementary, non-interactive content on hover or keyboard focus: + +```tsx +import { Tooltip } from '@cratis/components/Common'; + +Save the current changes} position='right'> + + +``` + +`content` accepts a React node, including text, elements, and the number `0`. `undefined`, `null`, either boolean (`true` or `false`), and an empty string disable the tooltip. Keep tooltip content non-interactive: an ARIA tooltip is not focusable, so links and buttons inside it cannot be used reliably. Put interactive content in a [`Dialog`](../Dialogs/dialog.md) or an inline region instead. `Button` and `IconButton` retain their text-only `tooltip` prop. + ## Change metadata Each value control uses `ChangeHandler`: diff --git a/Documentation/Common/date-picker-input.md b/Documentation/Common/date-picker-input.md new file mode 100644 index 00000000..91ad270a --- /dev/null +++ b/Documentation/Common/date-picker-input.md @@ -0,0 +1,226 @@ +--- +title: DatePickerInput +description: Enter a controlled Date | null through locale-aware segments and a calendar popover, with bounds, Today and Clear actions, and stable parts. +--- + +`DatePickerInput` is the standalone date and date-time picker. The user types into locale-ordered segments (day, month, year and, optionally, hour and minute) or picks a day from a calendar popover. Your code only sees a plain JavaScript `Date | null`; the React Aria calendar values the control uses internally never cross its props. + +Use it for date entry in ordinary React state. To bind a `Date` property on an Arc command, use [`CalendarField`](../CommandForm/calendar-field.md), which wraps this control. + +```tsx +import { DatePickerInput } from '@cratis/components/Common'; +``` + +## Controlled usage + +```tsx +import { useState } from 'react'; +import { DatePickerInput } from '@cratis/components/Common'; + +export const SampleDeliveryDate = () => { + const [deliveryDate, setDeliveryDate] = useState(null); + const earliest = new Date(2030, 0, 1); + const latest = new Date(2030, 11, 31); + const outOfRange = + deliveryDate !== null && (deliveryDate < earliest || deliveryDate > latest); + + return ( +
+ +

+ {outOfRange ? 'Pick a date in 2030.' : 'Deliveries run throughout 2030.'} +

+
+ ); +}; +``` + +Render it under [`CratisComponentsProvider`](cratis-components-provider.md) so the locale and labels come from your configuration. Without the provider, the labels use the English defaults and React Aria falls back to the browser's locale. `value` and `onChange` are both required: the control is controlled, so pass the new date back through `value`. + +`minDate`/`maxDate` disable out-of-range calendar cells and the Today action, but typing or stepping a segment can still produce an out-of-range date, and `onChange` receives it. The example therefore computes `invalid` from the value itself. See [Validation and states](#validation-and-states). + +## Value and change contract + +| Aspect | Behavior | +| --- | --- | +| Value type | `Date \| null`. `null` means no date. Conversion to and from `@internationalized/date` happens inside the component. | +| Time zone | Conversions use the browser's local time zone. | +| Date-only mode (default) | The time portion of an incoming `value` is ignored. Emitted dates are local midnight. | +| Date-time mode (`showTime`) | Hour and minute segments are added. `minDate`/`maxDate` are compared including their time. | +| `onChange` signature | `(value: Date \| null, meta?: ChangeMeta) => void`. A React state setter can be passed directly. | +| `meta.source` | `'user'` for every change the component emits. | +| `meta.nativeEvent` | Present for the Today and Clear actions; absent for segment and calendar changes. | +| Segment editing | `onChange` fires when the segments form a complete date. Clearing one segment leaves the last complete value in place without a callback; clearing every segment emits `null`. | +| Today action | Emits today's date (local midnight, also in date-time mode). Disabled and inert when today falls outside `minDate`/`maxDate`, or the picker is disabled or read-only. | +| Clear action | Emits `null`. Disabled and inert when the picker is disabled or read-only, or when `pt.clear.disabled` is set. | +| Programmatic changes | Changing `value` from outside never calls `onChange`. | +| Disabled or read-only | Segments cannot change the value. The trigger and Today/Clear actions are disabled; read-only segments remain focusable, but cannot open the calendar with `Alt+ArrowDown`/`Alt+ArrowUp`. | + +The Today and Clear buttons call `onChange` directly; they do not close the popover themselves. + +## Props + +| Prop | Type | Default | Behavior | +| --- | --- | --- | --- | +| `value` | `Date \| null` | Required | Controlled value. | +| `onChange` | `ChangeHandler` | Required | Receives the next date or `null` and optional change metadata. | +| `onBlur` | `FocusEventHandler` | — | Attached to the root wrapper. React focus events bubble, so it also fires when focus moves between segments or into the calendar, not only when focus leaves the picker. | +| `invalid` | `boolean` | `false` | Marks the picker invalid. See [Validation and states](#validation-and-states). | +| `disabled` | `boolean` | `false` | Disables the segments, trigger, calendar and Today/Clear actions. | +| `readOnly` | `boolean` | `false` | Keeps segments focusable but prevents editing; disables the trigger and Today/Clear actions and prevents opening the calendar with `Alt+ArrowDown`/`Alt+ArrowUp`. | +| `id` | `string` | — | DOM id of the segmented-input group. | +| `placeholder` | `string` | — | Text shown while the value is `null` and the field is not focused. Also used as the accessible name when `aria-label` is absent. | +| `showIcon` | `boolean` | `true` | Renders the calendar trigger button. Without it, the calendar is still reachable with `Alt+ArrowDown`. | +| `showButtonBar` | `boolean` | `false` | Adds Today and Clear actions below the calendar. | +| `showTime` | `boolean` | `false` | Adds hour and minute segments. | +| `hourFormat` | `'12' \| '24'` | Locale | Hour cycle for the time segments. Omit it to use the locale's hour cycle. | +| `minDate` / `maxDate` | `Date` | Unbounded | Earliest and latest selectable date for the calendar and the Today action. | +| `todayLabel` / `clearLabel` | `string` | Provider message | Per-instance labels for the Today and Clear actions. | +| `aria-label` | `string` | See [Labels](#locale-and-labels) | Accessible name of the segmented-input group. | +| `aria-labelledby` | `string` | — | Id of an element that labels the picker. | +| `aria-describedby` | `string` | — | Id of an element that describes the picker. | +| `className` | `string` | — | Added to the root element, after `pt.root.className`. | +| `style` | `CSSProperties` | — | Inline style on the root element; merged over `pt.root.style`. | +| `pt` | `DatePickerInputPassThrough` | — | Per-part attributes. See [Stable parts](#stable-parts). | +| `dateFormat` | `string` | — | Accepted for source compatibility and ignored. The locale controls formatting. | +| `ptOptions` | `object` | — | Deprecated and ignored. Parts always merge. | +| `unstyled` | `boolean` | — | Deprecated and ignored. Style through `pt` and CSS. | + +`DatePickerInputProps` and `DatePickerInputPassThrough` are exported types. + +Top-level props take precedence over their `pt.input` equivalents. When a top-level prop is omitted, the component falls back to `pt.input.id`, `disabled`, `readOnly`, `placeholder`, `aria-invalid`, `aria-label`, `aria-labelledby` and `aria-describedby`; `pt.input.id` is applied to the group, like `id`. Other `pt.input` attributes, such as `data-*`, are forwarded to the segmented input. + +## Locale and labels + +The locale comes from the nearest `CratisComponentsProvider` (`value.locale`, default `en-US`; an invalid locale also falls back to `en-US`). It decides segment order, separators, the calendar system and the default hour cycle. There is no per-instance locale prop. + +Components-owned text comes from `messages.datePicker`: + +| Message key | Used for | Default | +| --- | --- | --- | +| `label` | Accessible name fallback for the segmented input | `Date` | +| `today` | Today action | `Today` | +| `clear` | Clear action | `Clear` | +| `openCalendar` | Calendar trigger name | `Open calendar` | +| `previousMonth` | Previous-month button name | `Previous month` | +| `nextMonth` | Next-month button name | `Next month` | + +```tsx +import { CratisComponentsProvider } from '@cratis/components'; + + + +; +``` + +Resolution order for each text: + +- **Accessible name:** `aria-label` → `pt.input['aria-label']` → `placeholder` → `messages.datePicker.label` → `Date`. +- **Today / Clear:** `todayLabel` / `clearLabel` → `messages.datePicker.today` / `.clear` → English default. +- **Trigger and month buttons:** `pt.trigger`, `pt.previous` or `pt.next` `aria-label` → provider message → English default. + +The previous- and next-month glyphs follow the provider's `icons.previous` and `icons.next`. See [Localize owned labels](cratis-components-provider.md#localize-owned-labels) and [Register an icon set](cratis-components-provider.md#register-an-icon-set). + +## Keyboard and screen readers + +The segmented input, calendar and popover are React Aria's `DatePicker` parts. The behavior below is what React Aria provides in the installed version; verify it with the assistive technologies your application supports. + +| Key | Where | Effect | +| --- | --- | --- | +| `Tab` / `Shift+Tab` | Segments, trigger | Each editable segment is its own tab stop, followed by the trigger. | +| `ArrowLeft` / `ArrowRight` | Segment | Move to the previous or next segment (visual order in right-to-left locales). | +| `ArrowUp` / `ArrowDown` | Segment | Increment or decrement the segment, wrapping at its limits. | +| `PageUp` / `PageDown` | Segment | Step by a larger amount (for example 7 days, 2 months, 5 years, 15 minutes). | +| `Home` / `End` | Segment | Set the segment to its minimum or maximum. | +| Digits | Segment | Type the segment value. | +| `Backspace` / `Delete` | Segment | Remove the last digit, then clear the segment. | +| `Alt+ArrowDown` / `Alt+ArrowUp` | Segmented input | Open the calendar popover unless the picker is read-only. | + +Inside the popover, the calendar grid follows React Aria's calendar keyboard model; see the [React Aria DatePicker documentation](https://react-spectrum.adobe.com/react-aria/DatePicker.html). + +What assistive technology receives: + +- The segmented input is a `group` carrying the resolved accessible name, `aria-labelledby`, `aria-describedby` and, when `invalid` is set, `aria-invalid`. +- Each editable segment is a `spinbutton` named with its localized segment type followed by the group's name, for example "month, Delivery date". On iOS, where VoiceOver cannot focus spinbuttons, React Aria renders segments as `textbox` instead. +- Separator segments (such as `/` or `.`) are hidden from assistive technology. +- `aria-describedby` is applied to the first editable segment only, unless the field is invalid, so the description is not repeated on every segment. +- The visible placeholder is `aria-hidden`; its text reaches screen readers as the accessible name fallback instead. +- The trigger, previous-month and next-month buttons have the localized names listed above; their glyphs are `aria-hidden`. + +:::caution[Naming with aria-labelledby] +The group always carries a resolved `aria-label` (falling back to `Date`). React Aria then adds the group itself to `aria-labelledby`, so a picker labelled only by `aria-labelledby` is announced as "Date" followed by the referenced text. When that prefix is unwanted, pass `aria-label` with the visible label text instead of `aria-labelledby`. A `