Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
a7976dc
Add accessible filter dialog behavior and search focus
woksin Sep 26, 2026
4a1a20a
Document filter dialog accessibility and group search focus
woksin Sep 26, 2026
f92fba0
Add pre-execution confirmation to command dialogs and steppers
woksin Sep 26, 2026
d16dc4d
Show Chat query loading and failure states
woksin Sep 26, 2026
9937757
Document Chat query display states
woksin Sep 26, 2026
f60dddf
Document pre-execution confirmation for command dialogs and steppers
woksin Sep 26, 2026
6a5cee5
Hide stale Chat content when query access is denied
woksin Sep 26, 2026
2d3cdc3
Clarify Chat authorization state behavior
woksin Sep 26, 2026
a72e5f5
Document DatePickerInput and turn Common and Dialogs landings into se…
woksin Sep 26, 2026
486a8ee
Support non-interactive React nodes in Tooltip content
woksin Sep 26, 2026
8f63c69
Add configurable PivotViewer toolbar labels
woksin Sep 26, 2026
6feba32
Extract query status resolution from DataTables for Chat
woksin Sep 26, 2026
43fa308
Merge branch 'pi-agent-04e5a975-1318-4ed' into feature/release-4160
woksin Sep 26, 2026
4679e9b
Merge branch 'pi-agent-040c4a09-6486-47c' into feature/release-4160
woksin Sep 26, 2026
6095533
Point the Common landing at the Tooltip docs and correct two landing …
woksin Sep 26, 2026
c5f1363
Focus the filter dialog when it opens without a focused group search
woksin Sep 26, 2026
5af5051
Respect handled and composing Escape in filter controls
woksin Sep 26, 2026
f765802
Dismiss filter before enclosing modal on anchor Escape
woksin Sep 26, 2026
10538a3
Focus checkbox search only once per expansion
woksin Sep 26, 2026
1e69d2d
Separate filter spec actions and dispatch Escape on focused controls
woksin Sep 26, 2026
bed1123
Clarify filter dialog role and focus on open
woksin Sep 26, 2026
8d0ca9b
Assert the stepper dialog stays open immediately after decline
woksin Sep 26, 2026
781b1ac
Split guard outcomes into focused dialog specifications
woksin Sep 26, 2026
d4e7a25
Release command busy state before result callbacks
woksin Sep 26, 2026
e94cab7
Deliver command results after unmount without updating state
woksin Sep 26, 2026
afab89f
Require explicit true to approve command execution
woksin Sep 26, 2026
5027b7a
Share single-flight command execution across dialogs and steppers
woksin Sep 26, 2026
c72cdf7
Show chat failure alerts alongside existing content
woksin Sep 26, 2026
1d7ea8b
Fix read-only DatePickerInput calendar and actions
woksin Sep 26, 2026
e9c90b8
Clarify access-denied chat label documentation
woksin Sep 26, 2026
907beeb
Merge branch 'fix/filter-panel-accessibility' into feature/release-4160
woksin Sep 26, 2026
e187f98
Remount chat and table announcements when query status changes
woksin Sep 26, 2026
9e3d1c9
Disable chat creation and composing when access is denied
woksin Sep 26, 2026
5a8a6cd
Cover idle observable chat and direct sidebar statuses
woksin Sep 26, 2026
bb533e5
Cover rich Tooltip content and a disabled DatePicker Clear in conform…
woksin Sep 26, 2026
227c3f3
Merge branch 'feat/chat-query-status-327' into feature/release-4160
woksin Sep 26, 2026
3fde9ae
Preserve success close callbacks after dialog unmount
woksin Sep 26, 2026
dcb2e17
Continue unguarded execution after async transform and unmount
woksin Sep 26, 2026
ae20ad4
Release busy before reporting rejected guard after unmount
woksin Sep 26, 2026
bc3803f
Exercise real submission flight in guard specs
woksin Sep 26, 2026
af4f7c6
Specify unguarded execution timing across dialog steppers
woksin Sep 26, 2026
293fb60
Apply transformed values before executing after unmount
woksin Sep 26, 2026
aad27b2
Merge branch 'feature/command-dialog-confirm-before-execute' into fea…
woksin Sep 26, 2026
b1ddbe3
Fix FilterPanel Escape propagation to React ancestors
woksin Sep 26, 2026
7dd5c39
Focus initially open FilterPanel after hydration
woksin Sep 26, 2026
7756c90
Focus CheckboxListFilter search when options arrive
woksin Sep 26, 2026
6e69061
Disable empty boolean Tooltip content
woksin Sep 26, 2026
7f496aa
Localize PivotViewer filter panel labels
woksin Sep 26, 2026
7a22770
Prevent execution after guarded command values change
woksin Sep 26, 2026
ebc78e9
Report an uncloneable guarded snapshot through onException
woksin Sep 26, 2026
8154469
Fix FilterPanel dialog semantics and settled story checks
woksin Sep 26, 2026
04ae405
Move the pinned Storybook counts for the 4.16.0 stories
woksin Sep 26, 2026
7bbf37e
Size the PivotViewer labels story like the other PivotViewer stories
woksin Sep 26, 2026
ca1add7
Fix command confirmation against serialized property values
woksin Sep 26, 2026
4c0c49c
Mention pt.clear.disabled in the DatePickerInput Clear action row
woksin Sep 26, 2026
29ada2f
Close Chat emoji picker when composer is disabled
woksin Sep 26, 2026
7c2a1c9
Fix ChatComposer auto focus after reenabling
woksin Sep 26, 2026
d5f8692
Give the FilterPanel story settle waits a load-tolerant timeout
woksin Sep 26, 2026
76cc31f
Name PivotViewer option-group searches from their filter labels
woksin Sep 26, 2026
9145f7e
Fix stale Chat query state across topic selections
woksin Sep 26, 2026
b4ed0a6
Correct PivotViewer filter Escape documentation
woksin Sep 26, 2026
bc41174
Keep transient not-ready query results loading
woksin Sep 26, 2026
2c4c525
Clarify observable chat content after query failure
woksin Sep 26, 2026
229c898
Fix stale chat topics across query argument changes
woksin Sep 26, 2026
75a5ac0
Fix cached chat topics flashing loading before paint
woksin Sep 27, 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
12 changes: 11 additions & 1 deletion Conformance/src/internal/slotProfiles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand All @@ -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,
}),
},
{
Expand Down Expand Up @@ -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"]');
Expand Down
20 changes: 19 additions & 1 deletion Conformance/src/runConformance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Record<string, unknown>>;
if (!variantPt || typeof variantPt !== 'object') return markers;
const merged: Record<string, Record<string, unknown>> = { ...markers };
for (const [part, attributes] of Object.entries(variantPt as Record<string, unknown>)) {
merged[part] = {
...(attributes as Record<string, unknown>),
...(markers[part] ?? {}),
};
}
return merged;
};

const addCheck = (
checks: ConformanceCheck[],
family: ConformanceFamily,
Expand Down Expand Up @@ -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,
);
Expand Down
16 changes: 16 additions & 0 deletions Documentation/Chat/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand All @@ -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.
Expand Down
12 changes: 10 additions & 2 deletions Documentation/Chat/observable-queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
46 changes: 43 additions & 3 deletions Documentation/CommandDialog/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'`)
Expand All @@ -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

Expand All @@ -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 (
<CommandDialog<UpdateProject>
command={UpdateProject}
title='Update project'
initialValues={{ name: 'Example Project' }}
confirmBeforeExecute={async (_values) =>
(await showConfirmation()) === DialogResult.Yes}
/>
);
}

export function ProjectDialogs() {
return (
<DialogComponents confirmation={ConfirmationDialog}>
<UpdateProjectDialog />
</DialogComponents>
);
}
```

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`:
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion Documentation/CommandForm/calendar-field.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading
Loading