@astryxdesign/core 0.7.0-canary.fe57f54 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +186 -0
- package/dist/BottomSheet/BottomSheet.d.ts +5 -4
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetPanel.d.ts +1 -1
- package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetPanel.js +1 -1
- package/dist/Calendar/styles.d.ts +6 -6
- package/dist/Chat/ChatComposerInput.d.ts +0 -11
- package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
- package/dist/Chat/ChatComposerInput.js +0 -11
- package/dist/Chat/useTriggerMenu.d.ts.map +1 -1
- package/dist/Chat/useTriggerMenu.js +1 -1
- package/dist/CommandPalette/CommandPaletteList.d.ts +3 -2
- package/dist/CommandPalette/CommandPaletteList.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteList.js +3 -2
- package/dist/DateInput/TouchDateField.d.ts.map +1 -1
- package/dist/DateInput/TouchDateField.js +4 -1
- package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
- package/dist/DateTimeInput/TouchDateTimeField.js +29 -13
- package/dist/DropdownMenu/DropdownMenu.d.ts +11 -0
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +173 -11
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.js +13 -2
- package/dist/DropdownMenu/menuRegionsContext.d.ts +2 -0
- package/dist/DropdownMenu/menuRegionsContext.d.ts.map +1 -0
- package/dist/DropdownMenu/menuRegionsContext.js +17 -0
- package/dist/Field/inputStyles.stylex.d.ts +3 -3
- package/dist/Layout/container.stylex.d.ts +18 -20
- package/dist/Layout/container.stylex.d.ts.map +1 -1
- package/dist/Layout/container.stylex.js +15 -25
- package/dist/Layout/padding.stylex.d.ts +231 -231
- package/dist/Lightbox/Lightbox.d.ts.map +1 -1
- package/dist/Lightbox/Lightbox.js +15 -6
- package/dist/Markdown/plugins/protocol.d.ts.map +1 -1
- package/dist/Markdown/plugins/protocol.js +33 -8
- package/dist/NavItem/navItemStyles.stylex.d.ts +5 -5
- package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearch.js +21 -1
- package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearchEditPopover.js +30 -15
- package/dist/Stack/stack.stylex.d.ts +22 -22
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +89 -14
- package/dist/Table/table.stylex.d.ts +6 -6
- package/dist/TimeInput/TouchTimeField.d.ts.map +1 -1
- package/dist/TimeInput/TouchTimeField.js +4 -1
- package/dist/Tokenizer/Tokenizer.d.ts +1 -12
- package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
- package/dist/Tokenizer/Tokenizer.js +1 -13
- package/dist/Typeahead/BaseTypeahead.d.ts +0 -11
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +1 -9
- package/dist/Typeahead/Typeahead.d.ts +1 -12
- package/dist/Typeahead/Typeahead.d.ts.map +1 -1
- package/dist/Typeahead/Typeahead.js +1 -13
- package/dist/astryx.css +17 -12
- package/dist/hooks/useFocusTrap.d.ts +8 -0
- package/dist/hooks/useFocusTrap.d.ts.map +1 -1
- package/dist/hooks/useFocusTrap.js +5 -1
- package/dist/hooks/useRenamedProp.d.ts +6 -6
- package/dist/hooks/useRenamedProp.js +6 -6
- package/dist/reset.a11y.chromium.spec.d.ts +2 -0
- package/dist/reset.a11y.chromium.spec.d.ts.map +1 -0
- package/dist/reset.a11y.chromium.spec.js +176 -0
- package/dist/theme/tokens.stylex.d.ts +30 -30
- package/dist/theme/tokens.stylex.d.ts.map +1 -1
- package/dist/theme/tokens.stylex.js +18 -15
- package/dist/theme/useTheme.d.ts +1 -1
- package/package.json +4 -4
- package/src/BottomSheet/BottomSheet.doc.mjs +4 -4
- package/src/BottomSheet/BottomSheet.tsx +5 -4
- package/src/BottomSheet/BottomSheetPanel.test.tsx +6 -9
- package/src/BottomSheet/BottomSheetPanel.tsx +2 -3
- package/src/Chat/ChatComposerInput.doc.mjs +3 -3
- package/src/Chat/ChatComposerInput.test.tsx +7 -55
- package/src/Chat/ChatComposerInput.tsx +0 -36
- package/src/Chat/useTriggerMenu.tsx +1 -4
- package/src/CommandPalette/CommandPalette.spec.md +2 -0
- package/src/CommandPalette/CommandPaletteList.spec.md +189 -0
- package/src/CommandPalette/CommandPaletteList.test.tsx +47 -0
- package/src/CommandPalette/CommandPaletteList.tsx +3 -2
- package/src/DateInput/TouchDateField.tsx +3 -1
- package/src/DateTimeInput/DateTimeInputTouch.test.tsx +60 -1
- package/src/DateTimeInput/TouchDateTimeField.tsx +31 -14
- package/src/DropdownMenu/DropdownMenu.doc.mjs +12 -0
- package/src/DropdownMenu/DropdownMenu.spec.md +65 -40
- package/src/DropdownMenu/DropdownMenu.test.tsx +457 -0
- package/src/DropdownMenu/DropdownMenu.tsx +278 -49
- package/src/DropdownMenu/DropdownMenuCheckboxItem.doc.mjs +6 -3
- package/src/DropdownMenu/DropdownMenuRadioGroup.doc.mjs +2 -1
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +11 -1
- package/src/DropdownMenu/__tests__/MenuHeaderFooter.a11y.chromium.spec.ts +195 -0
- package/src/DropdownMenu/menuRegionsContext.ts +17 -0
- package/src/Layout/container.stylex.ts +11 -21
- package/src/Lightbox/Lightbox.test.tsx +58 -0
- package/src/Lightbox/Lightbox.tsx +12 -6
- package/src/Markdown/plugins/protocol.ts +45 -12
- package/src/Markdown/plugins/sourceLines.test.tsx +121 -0
- package/src/PowerSearch/PowerSearch.tsx +28 -1
- package/src/PowerSearch/PowerSearchEditPopover.test.tsx +284 -0
- package/src/PowerSearch/PowerSearchEditPopover.tsx +41 -13
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.test.tsx +33 -9
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +81 -18
- package/src/TimeInput/TouchTimeField.tsx +3 -1
- package/src/Tokenizer/Tokenizer.doc.mjs +0 -16
- package/src/Tokenizer/Tokenizer.test.tsx +12 -63
- package/src/Tokenizer/Tokenizer.tsx +1 -25
- package/src/Toolbar/Toolbar.test.tsx +29 -0
- package/src/Typeahead/BaseTypeahead.doc.mjs +0 -16
- package/src/Typeahead/BaseTypeahead.test.tsx +1 -1
- package/src/Typeahead/BaseTypeahead.tsx +1 -21
- package/src/Typeahead/Typeahead.doc.mjs +0 -7
- package/src/Typeahead/Typeahead.test.tsx +24 -42
- package/src/Typeahead/Typeahead.tsx +1 -25
- package/src/hooks/useFocusTrap.ts +5 -1
- package/src/hooks/useRenamedProp.ts +6 -6
- package/src/reset.a11y.chromium.spec.ts +178 -0
- package/src/reset.css +8 -8
- package/src/reset.test.ts +112 -0
- package/src/theme/__tests__/SpacingRootFontSize.a11y.chromium.spec.ts +198 -0
- package/src/theme/tokens.stylex.ts +18 -15
- package/src/theme/tokens.test.ts +1 -1
- package/src/theme/useTheme.test.tsx +4 -4
- package/src/theme/useTheme.ts +1 -1
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
---
|
|
2
|
+
schema_version: 3
|
|
3
|
+
template_version: 7
|
|
4
|
+
kind: component
|
|
5
|
+
id: component:CommandPaletteList
|
|
6
|
+
authority: draft
|
|
7
|
+
archive_reason: null
|
|
8
|
+
superseded_by: null
|
|
9
|
+
approved_by: null
|
|
10
|
+
approved_at: null
|
|
11
|
+
owners: [cixzhang]
|
|
12
|
+
review_triggers: [behavior, accessibility, public-api, theming]
|
|
13
|
+
verified_by:
|
|
14
|
+
[
|
|
15
|
+
packages/core/src/CommandPalette/CommandPaletteList.test.tsx,
|
|
16
|
+
packages/core/src/CommandPalette/CommandPalette.test.tsx,
|
|
17
|
+
packages/core/src/theme/themingTargets.test.ts,
|
|
18
|
+
apps/storybook/stories/CommandPaletteList.stories.tsx,
|
|
19
|
+
apps/storybook/rtl-audit/verified-not-applicable.json,
|
|
20
|
+
scripts/check-knowledge.mjs,
|
|
21
|
+
]
|
|
22
|
+
modules: []
|
|
23
|
+
families: []
|
|
24
|
+
design_specs: []
|
|
25
|
+
architecture:
|
|
26
|
+
[
|
|
27
|
+
architecture:component-test-sufficiency,
|
|
28
|
+
architecture:component-theming-surface,
|
|
29
|
+
architecture:public-component-api,
|
|
30
|
+
]
|
|
31
|
+
contributing: []
|
|
32
|
+
system_specs: []
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
# CommandPaletteList component contract
|
|
36
|
+
|
|
37
|
+
## Contract at a glance
|
|
38
|
+
|
|
39
|
+
| Area | Contract |
|
|
40
|
+
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
41
|
+
| Public contract | None changes. One named listbox container for result options, rendered by CommandPalette or directly for a standalone results list. |
|
|
42
|
+
| Behavior | The root is a `listbox` named by `label` or the translated default; inside CommandPalette it carries the context list id that the query field controls. |
|
|
43
|
+
| End-user impact | Screen-reader users reach a named results list from the query field; overflowing results scroll inside the list instead of growing the palette. |
|
|
44
|
+
| Builder impact | None. CommandPalette renders its own list; standalone compositions render items and groups directly inside this one. |
|
|
45
|
+
| Compatibility/readiness | Observational draft with no runtime change. A standalone caller `id` is overwritten (FR1), an empty list keeps the listbox role (AR4), and the standalone keyboard model remains OQ1. |
|
|
46
|
+
| Review checks | Reject a second list-id owner, a role or name that consumer props can replace, dropped native or styling inputs, or a list target moved off the scrolling root. |
|
|
47
|
+
| Governing rules | `architecture:public-component-api/INV5,INV6,INV8`; `architecture:component-theming-surface`; WAI-ARIA 1.2 `listbox`. |
|
|
48
|
+
|
|
49
|
+
This table is a review projection; the body below is authoritative.
|
|
50
|
+
|
|
51
|
+
## Intent
|
|
52
|
+
|
|
53
|
+
CommandPaletteList is the scrolling results container of the command palette.
|
|
54
|
+
Inside CommandPalette it is the listbox the query field controls. Standalone, it
|
|
55
|
+
presents a named results listbox around caller-provided option content.
|
|
56
|
+
|
|
57
|
+
## Compatibility and migration
|
|
58
|
+
|
|
59
|
+
- Released default preserved: `yes`
|
|
60
|
+
- Compatibility class: observational record only; runtime, DOM, styling, target,
|
|
61
|
+
and public API are unchanged
|
|
62
|
+
- Controlled/uncontrolled behavior: not applicable; the list holds no state
|
|
63
|
+
- Migration decision: none
|
|
64
|
+
|
|
65
|
+
Consumer migration instructions belong in consumer docs and release notes.
|
|
66
|
+
|
|
67
|
+
## Ownership boundary
|
|
68
|
+
|
|
69
|
+
**Owns**
|
|
70
|
+
|
|
71
|
+
- The listbox root and its accessible name from `label`, falling back to the
|
|
72
|
+
translated default.
|
|
73
|
+
- Applying the CommandPalette context list id to the root.
|
|
74
|
+
- Native passthrough, `ref`, and styling-input composition on the root.
|
|
75
|
+
- Block-axis scrolling of overflowing children, with spacing-scale padding and
|
|
76
|
+
gaps.
|
|
77
|
+
|
|
78
|
+
**Does not own / non-goals**
|
|
79
|
+
|
|
80
|
+
- List-id generation, query-field focus, active descendant, arrow-key
|
|
81
|
+
navigation, hover highlight, Enter selection, search, and the choice between
|
|
82
|
+
results and the empty state — owned by `component:CommandPalette`.
|
|
83
|
+
- The aggregate List anatomy entry and its `command-palette-list` target mapping
|
|
84
|
+
— owned by `component:CommandPalette`.
|
|
85
|
+
- Option semantics, state, and activation; group semantics and headings — owned
|
|
86
|
+
by CommandPaletteItem and CommandPaletteGroup.
|
|
87
|
+
- A standalone composite keyboard model; its direction remains OQ1.
|
|
88
|
+
|
|
89
|
+
## Public concepts
|
|
90
|
+
|
|
91
|
+
| Concept | Closed values or states | Meaning | Availability by variant/orientation/state | Default | Owner | Stability | Invalid-value behavior |
|
|
92
|
+
| --------------- | ---------------------------------- | ------------------------------------------------- | ----------------------------------------- | ----------------------------------------------------------- | ------------------------------ | --------- | ------------------------------------------------------------------------ |
|
|
93
|
+
| Accessible name | caller `label`; translated default | Name announced for the results listbox. | All uses | `@astryx.commandPalette.list.label` ("Commands" in English) | `component:CommandPaletteList` | Stable | An empty `label` leaves the listbox unnamed; callers supply a real name. |
|
|
94
|
+
| List identity | context list id; absent | DOM id the CommandPalette query field references. | Inside CommandPalette; standalone | Context id inside CommandPalette; absent standalone | `component:CommandPalette` | Stable | A standalone caller `id` is overwritten (FR1 gap). |
|
|
95
|
+
|
|
96
|
+
## Behavioral and layout contract
|
|
97
|
+
|
|
98
|
+
| ID | Candidate invariant | Basis | Draft review state |
|
|
99
|
+
| --- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
100
|
+
| FR1 | Native props and `ref` reach the listbox root, and consumer `className` and `style` combine with the list target and base styles. Inside CommandPalette the root id is the context list id; standalone, an absent id replaces a caller `id`. | Released source; `architecture:public-component-api/INV5,INV6,INV8` | Ref and styling verified by focused tests; the standalone `id` conflicts with INV5 |
|
|
101
|
+
| FR2 | The root exposes `role=listbox` with an accessible name from `label`, falling back to the translated default; consumer `role` and `aria-label` props do not replace them. | Released source and docs; WAI-ARIA 1.2 `listbox` | Role and name verified by focused tests; consumer-prop precedence is source-verified; the role also applies with no option (AR4) |
|
|
102
|
+
| FR3 | Inside CommandPalette, the query field's `aria-controls` names the listbox id. | Released source and component docs | Verified by a focused composition test |
|
|
103
|
+
| FR4 | The root lays out children in one column with spacing-scale padding and gaps, grows within a flex parent, caps its height at the parent's height, and scrolls overflowing children inside itself. | Released source and component docs | Source-verified; the scrolling fixture renders it in a browser, but scroll geometry is not yet measured |
|
|
104
|
+
|
|
105
|
+
### Allowed variation
|
|
106
|
+
|
|
107
|
+
- **AV1 — Children.** Items, groups, and empty states may vary in count, order,
|
|
108
|
+
and content without changing the listbox root.
|
|
109
|
+
- **AV2 — Layout.** Consumer `xstyle`, class, and style may adjust the root
|
|
110
|
+
without replacing its role, name, id, or target.
|
|
111
|
+
|
|
112
|
+
### Representative states
|
|
113
|
+
|
|
114
|
+
| State | Required invariant | Allowed variation |
|
|
115
|
+
| --------------------- | --------------------------------------------------------------------- | -------------------------- |
|
|
116
|
+
| Standalone results | The listbox is named by `label` or the translated default. | Item count and grouping. |
|
|
117
|
+
| Inside CommandPalette | The root id equals the query field's `aria-controls` value. | Results count and groups. |
|
|
118
|
+
| Overflowing results | Results scroll inside the list instead of growing past the container. | Container and item height. |
|
|
119
|
+
|
|
120
|
+
### Transformation and precedence order
|
|
121
|
+
|
|
122
|
+
- **ORD1 — Root attributes.** Consumer native props, then the component-owned
|
|
123
|
+
id, `listbox` role, and accessible name.
|
|
124
|
+
- **ORD2 — Styling.** List target, then base styles with `xstyle`, then
|
|
125
|
+
consumer class, then consumer style.
|
|
126
|
+
|
|
127
|
+
### Performance and resources
|
|
128
|
+
|
|
129
|
+
- **PR1 — No work beyond render.** The list adds no state, effect, listener, or
|
|
130
|
+
observer; it reads translation and CommandPalette context during render only.
|
|
131
|
+
|
|
132
|
+
## Accessibility contract
|
|
133
|
+
|
|
134
|
+
- **AR1 — Name.** The listbox takes `label` as its accessible name and falls back
|
|
135
|
+
to the translated default when `label` is omitted.
|
|
136
|
+
- **AR2 — Relationship.** Inside CommandPalette, the query field controls the
|
|
137
|
+
listbox through its id; CommandPalette owns focus and keyboard navigation under
|
|
138
|
+
its combobox model.
|
|
139
|
+
- **AR3 — Standalone gap.** A standalone list exposes a listbox of options with
|
|
140
|
+
no owning keyboard model. OQ1 must settle that contract before runtime
|
|
141
|
+
semantics change.
|
|
142
|
+
- **AR4 — Empty content.** The root keeps `role=listbox` when its children contain
|
|
143
|
+
no option, as in CommandPalette's empty states. WAI-ARIA 1.2 requires a listbox
|
|
144
|
+
to own options, so this is an open conflict, not a contract.
|
|
145
|
+
|
|
146
|
+
## Design relationships
|
|
147
|
+
|
|
148
|
+
| Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
|
|
149
|
+
| ---------------- | ----------------------------------------------------------------------------------- | ------------------------------ | -------------- | ------------------ |
|
|
150
|
+
| List | Presents result options in one scrolling column with spacing-scale padding and gap. | Current source and public docs | Supporting | FR1, FR4 |
|
|
151
|
+
|
|
152
|
+
## Family and system relationships
|
|
153
|
+
|
|
154
|
+
- `component:CommandPalette` owns list-id generation, keyboard navigation, hover
|
|
155
|
+
highlight, the aggregate anatomy, and the `command-palette-list` target
|
|
156
|
+
mapping; this component applies the id and paints the List.
|
|
157
|
+
- `architecture:public-component-api` owns native passthrough, styling
|
|
158
|
+
composition, and refs.
|
|
159
|
+
- `architecture:component-theming-surface` owns target qualification and the
|
|
160
|
+
aggregate List anatomy map.
|
|
161
|
+
- `architecture:component-test-sufficiency` owns the evidence rules behind the
|
|
162
|
+
verification map below.
|
|
163
|
+
|
|
164
|
+
## Verification map
|
|
165
|
+
|
|
166
|
+
| Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
|
|
167
|
+
| -------- | --------------------------------------------------------------------------------------- | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
|
|
168
|
+
| FR1 | `CommandPaletteList.test.tsx` | standalone root with ref, class, and style | Disconnecting `ref`, or dropping the consumer class or the list target, fails focused DOM assertions. | `audit:CommandPaletteList/api` |
|
|
169
|
+
| FR2, AR1 | `CommandPaletteList.test.tsx` | default and caller label | Removing the role or name fails the role and name assertions. | `audit:CommandPaletteList/a11y` |
|
|
170
|
+
| FR3, AR2 | `CommandPaletteList.test.tsx`, `CommandPalette.test.tsx` | inline CommandPalette | Dropping the context id breaks the query-field relationship assertion. | `audit:CommandPaletteList/a11y` |
|
|
171
|
+
| FR4 | Storybook fixtures, `themingTargets.test.ts`, exact-head accessibility and RTL evidence | standalone list; scrolling palette | Losing the target fails the target inventory; any new directional behavior requires a non-empty RTL dimension. | `audit:CommandPaletteList/theming` |
|
|
172
|
+
|
|
173
|
+
## Decision log
|
|
174
|
+
|
|
175
|
+
None. This draft records current facts and settles no component-local API,
|
|
176
|
+
keyboard, or visual decision.
|
|
177
|
+
|
|
178
|
+
## Open questions
|
|
179
|
+
|
|
180
|
+
- **OQ1 — What standalone keyboard contract should CommandPaletteList expose?**
|
|
181
|
+
(`human-api`) A standalone list exposes a listbox of options but no owning
|
|
182
|
+
keyboard model. Decide whether a public composite owner supplies that model,
|
|
183
|
+
the standalone composition takes a different role, or standalone use remains
|
|
184
|
+
pointer-only presentation.
|
|
185
|
+
|
|
186
|
+
## Content boundary
|
|
187
|
+
|
|
188
|
+
This file does not duplicate consumer prop tables/examples, current audit
|
|
189
|
+
results, implementation steps, or family/system rules. It links to their owners.
|
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import {describe, it, expect} from 'vitest';
|
|
10
|
+
import {createRef} from 'react';
|
|
10
11
|
import {render, screen} from '@testing-library/react';
|
|
12
|
+
import {createStaticSource} from '@astryxdesign/core/Typeahead';
|
|
13
|
+
import {CommandPalette} from './CommandPalette';
|
|
11
14
|
import {CommandPaletteList} from './CommandPaletteList';
|
|
12
15
|
|
|
13
16
|
describe('CommandPaletteList', () => {
|
|
@@ -54,4 +57,48 @@ describe('CommandPaletteList', () => {
|
|
|
54
57
|
'Search results',
|
|
55
58
|
);
|
|
56
59
|
});
|
|
60
|
+
|
|
61
|
+
it('is the listbox that the CommandPalette query field controls', async () => {
|
|
62
|
+
render(
|
|
63
|
+
<CommandPalette
|
|
64
|
+
isOpen
|
|
65
|
+
isInline
|
|
66
|
+
onOpenChange={() => {}}
|
|
67
|
+
searchSource={createStaticSource([{id: 'home', label: 'Home'}])}
|
|
68
|
+
/>,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const listbox = await screen.findByRole('listbox');
|
|
72
|
+
expect(listbox.id).not.toBe('');
|
|
73
|
+
expect(screen.getByRole('combobox')).toHaveAttribute(
|
|
74
|
+
'aria-controls',
|
|
75
|
+
listbox.id,
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('connects ref to the listbox element', () => {
|
|
80
|
+
const ref = createRef<HTMLDivElement>();
|
|
81
|
+
render(
|
|
82
|
+
<CommandPaletteList ref={ref}>
|
|
83
|
+
<div>Item</div>
|
|
84
|
+
</CommandPaletteList>,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
expect(ref.current).toBe(screen.getByRole('listbox'));
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('combines consumer className and style with the list theme target', () => {
|
|
91
|
+
render(
|
|
92
|
+
<CommandPaletteList className="product-results" style={{minHeight: 120}}>
|
|
93
|
+
<div>Item</div>
|
|
94
|
+
</CommandPaletteList>,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const listbox = screen.getByRole('listbox');
|
|
98
|
+
expect(listbox).toHaveClass(
|
|
99
|
+
'astryx-command-palette-list',
|
|
100
|
+
'product-results',
|
|
101
|
+
);
|
|
102
|
+
expect(listbox).toHaveStyle({minHeight: '120px'});
|
|
103
|
+
});
|
|
57
104
|
});
|
|
@@ -59,8 +59,9 @@ export interface CommandPaletteListProps extends BaseProps<HTMLDivElement> {
|
|
|
59
59
|
* When used inside CommandPalette, automatically gets the correct
|
|
60
60
|
* ID for aria-controls linking with the input.
|
|
61
61
|
*
|
|
62
|
-
* @compositionHint
|
|
63
|
-
*
|
|
62
|
+
* @compositionHint CommandPalette renders its own list after its input; render
|
|
63
|
+
* this list directly only for a standalone results list. Contains
|
|
64
|
+
* CommandPaletteItem and CommandPaletteGroup children.
|
|
64
65
|
*
|
|
65
66
|
* @example
|
|
66
67
|
* ```
|
|
@@ -1296,7 +1296,9 @@ export function TouchDateField({
|
|
|
1296
1296
|
label={t('@astryx.dateInput.dialogLabel')}
|
|
1297
1297
|
// The picker is a fixed height by construction, so the sheet should be
|
|
1298
1298
|
// exactly as tall as it is rather than claiming a viewport budget.
|
|
1299
|
-
height="hug"
|
|
1299
|
+
height="hug"
|
|
1300
|
+
// sheetBody owns the inset, including the band under the grab handle.
|
|
1301
|
+
padding={0}>
|
|
1300
1302
|
<div {...stylex.props(styles.sheetBody)}>{surface}</div>
|
|
1301
1303
|
</BottomSheet>
|
|
1302
1304
|
{showsDisabledMessage &&
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
beforeEach,
|
|
26
26
|
afterEach,
|
|
27
27
|
} from 'vitest';
|
|
28
|
-
import {useState} from 'react';
|
|
28
|
+
import {Profiler, useState} from 'react';
|
|
29
29
|
import * as stylex from '@stylexjs/stylex';
|
|
30
30
|
import {readFileSync} from 'node:fs';
|
|
31
31
|
import {render, screen, fireEvent, within} from '@testing-library/react';
|
|
@@ -986,3 +986,62 @@ describe('DateTimeInput touch surface', () => {
|
|
|
986
986
|
expect(field).toHaveAttribute('aria-expanded', 'false');
|
|
987
987
|
});
|
|
988
988
|
});
|
|
989
|
+
|
|
990
|
+
describe('DateTimeInput touch surface: no stale commit', () => {
|
|
991
|
+
// Records what each commit painted, so a value that is corrected one render
|
|
992
|
+
// later (by an effect) shows up as a stale intermediate frame.
|
|
993
|
+
function paintedFrames(read: () => string) {
|
|
994
|
+
const frames: string[] = [];
|
|
995
|
+
const onRender = () => frames.push(read());
|
|
996
|
+
return {frames, onRender};
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
it('clears the time display in the same commit as an external clear', () => {
|
|
1000
|
+
const {frames, onRender} = paintedFrames(
|
|
1001
|
+
() => (timeField() as HTMLInputElement).value,
|
|
1002
|
+
);
|
|
1003
|
+
const ui = (value: ISODateTimeString | undefined) => (
|
|
1004
|
+
<Profiler id="field" onRender={onRender}>
|
|
1005
|
+
<DateTimeInput label="Meeting" value={value} onChange={() => {}} />
|
|
1006
|
+
</Profiler>
|
|
1007
|
+
);
|
|
1008
|
+
const {rerender} = render(ui('2026-03-15T10:00' as ISODateTimeString));
|
|
1009
|
+
expect(timeField()).toHaveValue('10:00 AM');
|
|
1010
|
+
|
|
1011
|
+
frames.length = 0;
|
|
1012
|
+
rerender(ui(undefined));
|
|
1013
|
+
|
|
1014
|
+
expect(frames.length).toBeGreaterThan(0);
|
|
1015
|
+
expect(frames).not.toContain('10:00 AM');
|
|
1016
|
+
expect(timeField()).toHaveValue('');
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
it('shows an in-range month in the same commit that narrows max', () => {
|
|
1020
|
+
withMonthLayout(() => {
|
|
1021
|
+
const visibleMonth = () =>
|
|
1022
|
+
['March', 'April', 'May', 'June']
|
|
1023
|
+
.map(m => `${m} 2026`)
|
|
1024
|
+
.find(label => screen.queryByText(label) != null) ?? '';
|
|
1025
|
+
const {frames, onRender} = paintedFrames(visibleMonth);
|
|
1026
|
+
const ui = (max: ISODateTimeString) => (
|
|
1027
|
+
<Profiler id="field" onRender={onRender}>
|
|
1028
|
+
<DateTimeInput label="Meeting" max={max} onChange={() => {}} />
|
|
1029
|
+
</Profiler>
|
|
1030
|
+
);
|
|
1031
|
+
const {rerender} = render(ui('2026-12-31T23:59' as ISODateTimeString));
|
|
1032
|
+
openDateSheet();
|
|
1033
|
+
const next = screen.getByRole('button', {name: 'Next month'});
|
|
1034
|
+
fireEvent.click(next);
|
|
1035
|
+
fireEvent.click(next);
|
|
1036
|
+
fireEvent.click(next);
|
|
1037
|
+
expect(screen.getByText('June 2026')).toBeInTheDocument();
|
|
1038
|
+
|
|
1039
|
+
frames.length = 0;
|
|
1040
|
+
rerender(ui('2026-04-30T23:59' as ISODateTimeString));
|
|
1041
|
+
|
|
1042
|
+
expect(frames.length).toBeGreaterThan(0);
|
|
1043
|
+
expect(frames).not.toContain('June 2026');
|
|
1044
|
+
expect(screen.getByText('April 2026')).toBeInTheDocument();
|
|
1045
|
+
});
|
|
1046
|
+
});
|
|
1047
|
+
});
|
|
@@ -590,15 +590,25 @@ export function TouchDateTimeField({
|
|
|
590
590
|
);
|
|
591
591
|
const weekStartsOn = normalizeDayOfWeek(weekStartsOnProp);
|
|
592
592
|
|
|
593
|
-
|
|
593
|
+
// The draft follows the controlled time, and an external clear resets it.
|
|
594
|
+
// Adjusted during render when the value changes, so no commit paints the
|
|
595
|
+
// previous draft.
|
|
596
|
+
const [draftSource, setDraftSource] = useState({
|
|
597
|
+
time: normalizedValueTime,
|
|
598
|
+
value: optimisticValue,
|
|
599
|
+
});
|
|
600
|
+
// Object.is, matching the dependency comparison of the effect this replaces.
|
|
601
|
+
if (
|
|
602
|
+
!Object.is(draftSource.time, normalizedValueTime) ||
|
|
603
|
+
!Object.is(draftSource.value, optimisticValue)
|
|
604
|
+
) {
|
|
605
|
+
setDraftSource({time: normalizedValueTime, value: optimisticValue});
|
|
594
606
|
if (normalizedValueTime !== undefined) {
|
|
595
|
-
// eslint-disable-next-line @eslint-react/set-state-in-effect -- mirrors an externally controlled time into the sheet-local draft
|
|
596
607
|
setDraftTime(normalizedValueTime);
|
|
597
608
|
} else if (optimisticValue === undefined) {
|
|
598
|
-
// eslint-disable-next-line @eslint-react/set-state-in-effect -- external clear resets the sheet-local draft too
|
|
599
609
|
setDraftTime(undefined);
|
|
600
610
|
}
|
|
601
|
-
}
|
|
611
|
+
}
|
|
602
612
|
|
|
603
613
|
const [anchorMonthIndex] = useState(() =>
|
|
604
614
|
monthIndexOf(selectedDate ?? plainDateToday()),
|
|
@@ -614,19 +624,23 @@ export function TouchDateTimeField({
|
|
|
614
624
|
const [monthIndex, setMonthIndex] = useState(() =>
|
|
615
625
|
clampIndex(anchorMonthIndex, minMonthIndex, maxMonthIndex),
|
|
616
626
|
);
|
|
627
|
+
// min/max can narrow underneath the mounted sheet. Keep the visible month in
|
|
628
|
+
// range during render, so no commit shows an out-of-range month; the month
|
|
629
|
+
// scroller is keyed on the range and remounts at the clamped month.
|
|
630
|
+
const clampedMonthIndex = clampIndex(
|
|
631
|
+
monthIndex,
|
|
632
|
+
minMonthIndex,
|
|
633
|
+
maxMonthIndex,
|
|
634
|
+
);
|
|
635
|
+
// Object.is, matching the dependency comparison of the effect this replaces
|
|
636
|
+
// (a NaN index would otherwise never compare equal and re-render forever).
|
|
637
|
+
if (!Object.is(clampedMonthIndex, monthIndex)) {
|
|
638
|
+
setMonthIndex(clampedMonthIndex);
|
|
639
|
+
}
|
|
617
640
|
const {year, month} = fromMonthIndex(monthIndex);
|
|
618
641
|
const canStepBack = monthIndex > minMonthIndex;
|
|
619
642
|
const canStepForward = monthIndex < maxMonthIndex;
|
|
620
643
|
|
|
621
|
-
useEffect(() => {
|
|
622
|
-
const clamped = clampIndex(monthIndex, minMonthIndex, maxMonthIndex);
|
|
623
|
-
if (clamped !== monthIndex) {
|
|
624
|
-
// eslint-disable-next-line @eslint-react/set-state-in-effect -- min/max can change underneath the mounted sheet; keep the visible month in range
|
|
625
|
-
setMonthIndex(clamped);
|
|
626
|
-
scrollerHandleRef.current?.scrollToMonth(clamped, 'auto');
|
|
627
|
-
}
|
|
628
|
-
}, [monthIndex, minMonthIndex, maxMonthIndex]);
|
|
629
|
-
|
|
630
644
|
const dayNames = useMemo(
|
|
631
645
|
() =>
|
|
632
646
|
Array.from({length: 7}, (_, offset) =>
|
|
@@ -1464,7 +1478,10 @@ export function TouchDateTimeField({
|
|
|
1464
1478
|
isOpen={isSheetOpen}
|
|
1465
1479
|
onOpenChange={handleSheetOpenChange}
|
|
1466
1480
|
label={t('@astryx.dateTimeInput.dialogLabel')}
|
|
1467
|
-
height="hug"
|
|
1481
|
+
height="hug"
|
|
1482
|
+
// touchSheetBody owns the inset, including the band under the grab
|
|
1483
|
+
// handle.
|
|
1484
|
+
padding={0}>
|
|
1468
1485
|
<div {...stylex.props(styles.touchSheetBody)}>{surface}</div>
|
|
1469
1486
|
</BottomSheet>
|
|
1470
1487
|
{showsDisabledMessage &&
|
|
@@ -227,6 +227,18 @@ export const docs = {
|
|
|
227
227
|
description:
|
|
228
228
|
'Maximum height in pixels for the popover presentation, for a menu that must fit its rows. Lifts the default 300px cap; the viewport still bounds it.',
|
|
229
229
|
},
|
|
230
|
+
{
|
|
231
|
+
name: 'header',
|
|
232
|
+
type: 'ReactNode',
|
|
233
|
+
description:
|
|
234
|
+
'Content above the rows and outside the menu itself — a filter field, a toggle. A menu holds only its rows, so a control that is not a row goes here or in `footer`, never among the rows. It keeps its own keys and presses; Tab moves from it into the menu, and Shift+Tab before its first control closes the menu. The bottom sheet shows it above the actions.',
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: 'footer',
|
|
238
|
+
type: 'ReactNode',
|
|
239
|
+
description:
|
|
240
|
+
'Content below the rows and outside the menu itself — a "Create" button. Tab from the menu moves into it, and Tab past its last control closes the menu. The bottom sheet shows it below the actions.',
|
|
241
|
+
},
|
|
230
242
|
{
|
|
231
243
|
name: 'placement',
|
|
232
244
|
type: "'above' | 'below' | 'start' | 'end'",
|