@astryxdesign/core 0.7.2-canary.caec7ca → 0.7.2-canary.cbb7fa3
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/dist/AspectRatio/AspectRatio.d.ts +6 -8
- package/dist/AspectRatio/AspectRatio.d.ts.map +1 -1
- package/dist/AspectRatio/AspectRatio.js +44 -3
- package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
- package/dist/Breadcrumbs/BreadcrumbItem.js +5 -2
- package/dist/CheckboxInput/CheckboxInput.d.ts +1 -1
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +3 -1
- package/dist/CommandPalette/CommandPaletteInput.d.ts +1 -1
- package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteInput.js +37 -5
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +5 -2
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +20 -8
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +46 -3
- package/dist/DropdownMenu/DropdownMenuContext.d.ts +17 -0
- package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuContext.js +21 -0
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.js +15 -4
- package/dist/EmptyState/EmptyState.d.ts.map +1 -1
- package/dist/EmptyState/EmptyState.js +4 -10
- package/dist/Field/Field.d.ts +1 -1
- package/dist/Field/Field.d.ts.map +1 -1
- package/dist/Field/Field.js +8 -4
- package/dist/Grid/GridSpan.d.ts.map +1 -1
- package/dist/Grid/GridSpan.js +1 -0
- package/dist/List/List.d.ts +46 -19
- package/dist/List/List.d.ts.map +1 -1
- package/dist/List/List.js +28 -10
- package/dist/List/ListContext.d.ts +8 -3
- package/dist/List/ListContext.d.ts.map +1 -1
- package/dist/List/ListContext.js +7 -2
- package/dist/List/ListItem.d.ts +4 -3
- package/dist/List/ListItem.d.ts.map +1 -1
- package/dist/List/ListItem.js +19 -8
- package/dist/List/index.d.ts +1 -1
- package/dist/List/index.d.ts.map +1 -1
- package/dist/Markdown/Markdown.d.ts.map +1 -1
- package/dist/Markdown/Markdown.js +8 -1
- package/dist/MetadataList/MetadataList.d.ts.map +1 -1
- package/dist/MetadataList/MetadataList.js +5 -1
- package/dist/MetadataList/MetadataListContext.d.ts +1 -1
- package/dist/MetadataList/MetadataListContext.d.ts.map +1 -1
- package/dist/RadioList/RadioList.d.ts +1 -1
- package/dist/RadioList/RadioList.d.ts.map +1 -1
- package/dist/RadioList/RadioList.js +6 -2
- package/dist/ToggleButton/ToggleButton.d.ts +3 -2
- package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
- package/dist/ToggleButton/ToggleButton.js +12 -1
- package/dist/TreeList/TreeList.d.ts.map +1 -1
- package/dist/TreeList/TreeList.js +1 -0
- package/dist/TreeList/TreeListBranches.d.ts.map +1 -1
- package/dist/TreeList/TreeListBranches.js +2 -2
- package/dist/TreeList/TreeListItem.d.ts.map +1 -1
- package/dist/TreeList/TreeListItem.js +57 -9
- package/dist/astryx.css +16 -3
- package/dist/hooks/useMenuHover.d.ts.map +1 -1
- package/dist/hooks/useMenuHover.js +18 -13
- package/dist/utils/plainDate.d.ts.map +1 -1
- package/dist/utils/plainDate.js +15 -9
- package/package.json +3 -3
- package/src/AspectRatio/AspectRatio.test.tsx +44 -43
- package/src/AspectRatio/AspectRatio.tsx +38 -18
- package/src/AspectRatio/__tests__/AspectRatioFitLayers.a11y.browser.spec.ts +157 -0
- package/src/Avatar/Avatar.doc.mjs +5 -3
- package/src/AvatarGroup/AvatarGroup.doc.mjs +3 -2
- package/src/Breadcrumbs/BreadcrumbItem.tsx +4 -1
- package/src/Chat/__tests__/ChatMessageMetadata.a11y.chromium.spec.ts +3 -1
- package/src/CheckboxInput/CheckboxInput.test.tsx +34 -0
- package/src/CheckboxInput/CheckboxInput.tsx +3 -1
- package/src/CommandPalette/CommandPaletteInput.test.tsx +53 -4
- package/src/CommandPalette/CommandPaletteInput.tsx +33 -3
- package/src/CommandPalette/__tests__/CommandPaletteInputAudit.a11y.chromium.spec.ts +335 -0
- package/src/ContextMenu/ContextMenu.tsx +4 -1
- package/src/Dialog/Dialog.test.tsx +46 -0
- package/src/Dialog/Dialog.tsx +20 -7
- package/src/Dialog/__tests__/Dialog.a11y.known-failures.ts +0 -14
- package/src/DropdownMenu/DropdownMenu.tsx +45 -2
- package/src/DropdownMenu/DropdownMenuContext.tsx +24 -0
- package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +185 -0
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +17 -3
- package/src/EmptyState/EmptyState.doc.mjs +3 -2
- package/src/EmptyState/EmptyState.spec.md +189 -0
- package/src/EmptyState/EmptyState.test.tsx +74 -3
- package/src/EmptyState/EmptyState.tsx +4 -8
- package/src/Field/Field.doc.mjs +14 -8
- package/src/Field/Field.spec.md +217 -0
- package/src/Field/Field.test.tsx +60 -2
- package/src/Field/Field.tsx +17 -8
- package/src/Grid/GridSpan.tsx +3 -0
- package/src/Item/Item.doc.mjs +1 -1
- package/src/Item/Item.tsx +1 -1
- package/src/List/List.doc.mjs +14 -1
- package/src/List/List.spec.md +129 -77
- package/src/List/List.test.tsx +144 -94
- package/src/List/List.tsx +74 -18
- package/src/List/ListContext.tsx +9 -3
- package/src/List/ListItem.tsx +46 -26
- package/src/List/index.ts +1 -1
- package/src/Markdown/Markdown.tsx +14 -1
- package/src/MetadataList/MetadataList.doc.mjs +2 -2
- package/src/MetadataList/MetadataList.test.tsx +38 -0
- package/src/MetadataList/MetadataList.tsx +8 -1
- package/src/MetadataList/MetadataListContext.tsx +1 -1
- package/src/RadioList/RadioList.fr10.test.tsx +45 -2
- package/src/RadioList/RadioList.tsx +4 -1
- package/src/Slider/Slider.spec.md +39 -18
- package/src/TabList/TabList.spec.md +57 -44
- package/src/TimeInput/TimeInput.fr10.test.tsx +15 -0
- package/src/ToggleButton/ToggleButton.test.tsx +95 -0
- package/src/ToggleButton/ToggleButton.tsx +12 -1
- package/src/TreeList/TreeList.doc.mjs +1 -0
- package/src/TreeList/TreeList.test.tsx +327 -6
- package/src/TreeList/TreeList.tsx +5 -1
- package/src/TreeList/TreeListBranches.tsx +6 -0
- package/src/TreeList/TreeListItem.tsx +88 -13
- package/src/docPropLiterals.test.ts +36 -10
- package/src/hooks/useMenuHover.ts +18 -13
- package/src/reset.css +0 -29
- package/src/theme/derivedVarRegistry.test.ts +7 -0
- package/src/utils/plainDate.test.ts +37 -1
- package/src/utils/plainDate.ts +15 -5
|
@@ -124,6 +124,30 @@ describe('MetadataList', () => {
|
|
|
124
124
|
expect(screen.getByText('B')).toBeInTheDocument();
|
|
125
125
|
});
|
|
126
126
|
|
|
127
|
+
it('uses top labels in horizontal orientation even when position is start', () => {
|
|
128
|
+
render(
|
|
129
|
+
<MetadataList orientation="horizontal" label={{position: 'start'}}>
|
|
130
|
+
<MetadataListItem label="A">1</MetadataListItem>
|
|
131
|
+
</MetadataList>,
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const item = screen.getByRole('term').closest('.astryx-metadata-list-item');
|
|
135
|
+
expect(item).toContainElement(screen.getByRole('definition'));
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('defaults to side labels and preserves width when position is omitted', () => {
|
|
139
|
+
const {container} = render(
|
|
140
|
+
<MetadataList label={{width: 120}}>
|
|
141
|
+
<MetadataListItem label="A">1</MetadataListItem>
|
|
142
|
+
</MetadataList>,
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const list = container.querySelector('dl');
|
|
146
|
+
expect(list?.getAttribute('style')).toContain('120px minmax(0, 1fr)');
|
|
147
|
+
expect(screen.getByRole('term').parentElement).toBe(list);
|
|
148
|
+
expect(screen.getByRole('definition').parentElement).toBe(list);
|
|
149
|
+
});
|
|
150
|
+
|
|
127
151
|
describe('numeric columns', () => {
|
|
128
152
|
// A fixed column count is a runtime value, so it arrives as a StyleX
|
|
129
153
|
// dynamic style: the template lands in the element's inline style (as the
|
|
@@ -163,6 +187,20 @@ describe('MetadataList', () => {
|
|
|
163
187
|
expect(gridTemplateOf(container)).not.toContain('repeat(');
|
|
164
188
|
});
|
|
165
189
|
|
|
190
|
+
it('defaults to top labels for columns={2} with a width-only label config', () => {
|
|
191
|
+
const {container} = render(
|
|
192
|
+
<MetadataList columns={2} label={{width: 260}}>
|
|
193
|
+
<MetadataListItem label="A">1</MetadataListItem>
|
|
194
|
+
</MetadataList>,
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
expect(gridTemplateOf(container)).toContain('repeat(2, 1fr)');
|
|
198
|
+
const item = screen
|
|
199
|
+
.getByRole('term')
|
|
200
|
+
.closest('.astryx-metadata-list-item');
|
|
201
|
+
expect(item).toContainElement(screen.getByRole('definition'));
|
|
202
|
+
});
|
|
203
|
+
|
|
166
204
|
it('ignores numeric columns in horizontal orientation', () => {
|
|
167
205
|
const {container} = render(
|
|
168
206
|
<MetadataList columns={3} orientation="horizontal">
|
|
@@ -206,7 +206,14 @@ export function MetadataList({
|
|
|
206
206
|
columns === 'multi' || (typeof columns === 'number' && columns > 1);
|
|
207
207
|
// Default to 'top' for multi-column and horizontal layouts since side labels
|
|
208
208
|
// don't work well when items are in separate grid cells.
|
|
209
|
-
const
|
|
209
|
+
const defaultLabelConfig = isMultiColumn ? LABEL_TOP : LABEL_START;
|
|
210
|
+
const labelConfig = useMemo(
|
|
211
|
+
() => ({
|
|
212
|
+
...label,
|
|
213
|
+
position: label?.position ?? defaultLabelConfig.position,
|
|
214
|
+
}),
|
|
215
|
+
[label, defaultLabelConfig],
|
|
216
|
+
);
|
|
210
217
|
const [isShowAll, setIsShowAll] = useState(false);
|
|
211
218
|
const contentId = useId();
|
|
212
219
|
const t = useTranslator();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
2
|
|
|
3
|
-
import {describe, it} from 'vitest';
|
|
4
|
-
import {screen} from '@testing-library/react';
|
|
3
|
+
import {describe, it, expect} from 'vitest';
|
|
4
|
+
import {render, screen} from '@testing-library/react';
|
|
5
5
|
import {expectFieldInputProps} from '../Field/fieldInputFamily.testing';
|
|
6
6
|
import {describeFieldInputParts} from '../Field/fieldInputFamily.render-testing';
|
|
7
7
|
import {RadioList, type RadioListProps} from './RadioList';
|
|
@@ -22,3 +22,46 @@ describeFieldInputParts('RadioList', {
|
|
|
22
22
|
getControl: () => screen.getByRole('radiogroup'),
|
|
23
23
|
inGroup: false,
|
|
24
24
|
});
|
|
25
|
+
|
|
26
|
+
describe('RadioList FR10 details', () => {
|
|
27
|
+
it("names the radiogroup by its label and then a caller's aria-labelledby", () => {
|
|
28
|
+
render(
|
|
29
|
+
<>
|
|
30
|
+
<span id="caller-name">Billing</span>
|
|
31
|
+
<RadioList
|
|
32
|
+
label="Plan"
|
|
33
|
+
value="a"
|
|
34
|
+
onChange={() => {}}
|
|
35
|
+
aria-labelledby="caller-name">
|
|
36
|
+
<RadioListItem label="A" value="a" />
|
|
37
|
+
</RadioList>
|
|
38
|
+
</>,
|
|
39
|
+
);
|
|
40
|
+
const group = screen.getByRole('radiogroup');
|
|
41
|
+
const ids = group.getAttribute('aria-labelledby')?.split(' ');
|
|
42
|
+
expect(ids).toHaveLength(2);
|
|
43
|
+
expect(document.getElementById(ids?.[0] ?? '')?.textContent).toContain(
|
|
44
|
+
'Plan',
|
|
45
|
+
);
|
|
46
|
+
expect(ids?.[1]).toBe('caller-name');
|
|
47
|
+
expect(screen.getByRole('radiogroup', {name: 'Plan Billing'})).toBe(group);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('puts hidden, inert, and dir on the radiogroup', () => {
|
|
51
|
+
render(
|
|
52
|
+
<RadioList
|
|
53
|
+
label="Plan"
|
|
54
|
+
value="a"
|
|
55
|
+
onChange={() => {}}
|
|
56
|
+
hidden
|
|
57
|
+
inert
|
|
58
|
+
dir="rtl">
|
|
59
|
+
<RadioListItem label="A" value="a" />
|
|
60
|
+
</RadioList>,
|
|
61
|
+
);
|
|
62
|
+
const group = document.querySelector<HTMLElement>('[role="radiogroup"]');
|
|
63
|
+
expect(group).toHaveAttribute('hidden');
|
|
64
|
+
expect(group).toHaveAttribute('inert');
|
|
65
|
+
expect(group).toHaveAttribute('dir', 'rtl');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -31,6 +31,7 @@ import type {InputStatus} from '../Field/types';
|
|
|
31
31
|
import {useTooltip} from '../Tooltip';
|
|
32
32
|
import {useResolvedRequired} from '../hooks/useResolvedRequired';
|
|
33
33
|
import {composeEventHandlers, mergeProps} from '../utils';
|
|
34
|
+
import {joinAriaIDs} from '../utils/inputAria';
|
|
34
35
|
import {
|
|
35
36
|
type FieldInputBaseProps,
|
|
36
37
|
useFieldInputStyling,
|
|
@@ -216,6 +217,7 @@ export function RadioList({
|
|
|
216
217
|
htmlName,
|
|
217
218
|
children,
|
|
218
219
|
onFocus,
|
|
220
|
+
'aria-labelledby': ariaLabelledByProp,
|
|
219
221
|
'aria-describedby': ariaDescribedByProp,
|
|
220
222
|
...restProps
|
|
221
223
|
}: RadioListProps) {
|
|
@@ -399,7 +401,8 @@ export function RadioList({
|
|
|
399
401
|
disabledMessageTooltip.ref(el);
|
|
400
402
|
}}
|
|
401
403
|
role="radiogroup"
|
|
402
|
-
aria-labelledby
|
|
404
|
+
// A caller aria-labelledby names the group together with its label.
|
|
405
|
+
aria-labelledby={joinAriaIDs(labelID, ariaLabelledByProp)}
|
|
403
406
|
onFocus={composeEventHandlers(handleFocus, onFocus)}
|
|
404
407
|
aria-describedby={
|
|
405
408
|
[
|
|
@@ -34,13 +34,20 @@ an additive target for the interactive control surface.
|
|
|
34
34
|
|
|
35
35
|
## Compatibility and migration
|
|
36
36
|
|
|
37
|
-
- Released default preserved: `
|
|
37
|
+
- Released default preserved: `no` — FR5 changes the released `onChangeEnd`
|
|
38
|
+
payload; every other released default is unchanged
|
|
38
39
|
- Compatibility class: additive target and state reflection; default styling,
|
|
39
40
|
DOM semantics, and public props remain unchanged, and the one keyboard change
|
|
40
41
|
AR1 names — the horizontal arrows follow the track's direction under RTL —
|
|
41
42
|
ships as a behavior fix
|
|
43
|
+
- `onChangeEnd` payload (FR5): a new contract decision (`spec:AST-017/FR33`),
|
|
44
|
+
not a correction. Released victim: an owner that moves `value` only from
|
|
45
|
+
`onChangeEnd`, with no `onChange`, moves by keyboard today and does not under
|
|
46
|
+
FR5, so the change is breaking (`spec:AST-017/FR1`) and ships as
|
|
47
|
+
`[breaking]` in a declared minor (`spec:AST-017/FR31`).
|
|
42
48
|
- Controlled/uncontrolled behavior: unchanged; Slider remains controlled
|
|
43
|
-
- Migration decision:
|
|
49
|
+
- Migration decision: an owner that moves `value` only from `onChangeEnd`
|
|
50
|
+
moves it from `onChange` instead (FR5)
|
|
44
51
|
|
|
45
52
|
Consumer migration instructions belong in consumer docs and release notes.
|
|
46
53
|
|
|
@@ -64,18 +71,20 @@ Consumer migration instructions belong in consumer docs and release notes.
|
|
|
64
71
|
|
|
65
72
|
## Public concepts
|
|
66
73
|
|
|
67
|
-
The record adds one public theming target
|
|
68
|
-
|
|
74
|
+
The record adds one public theming target, names the keyboard model (AR1), and
|
|
75
|
+
states which value `onChangeEnd` carries (FR5) without adding or changing a
|
|
76
|
+
component prop or value domain. Consumer props,
|
|
69
77
|
modes, states, and usage remain documented in `Slider.doc.mjs`.
|
|
70
78
|
|
|
71
79
|
## Behavioral and layout contract
|
|
72
80
|
|
|
73
|
-
| ID | Candidate invariant
|
|
74
|
-
| --- |
|
|
75
|
-
| FR1 | The current render places a filled range, one or two thumbs, and optional tick marks over the background track.
|
|
76
|
-
| FR2 | `Slider`, `Interactive control`, `Track`, and `Thumb` carry the `slider`, `slider-control`, `slider-track`, and `slider-thumb` targets respectively.
|
|
77
|
-
| FR3 | Filled range, tick marks, mark labels, and adjacent text value display are stable rendered parts without their own current Slider target.
|
|
78
|
-
| FR4 | Label and status presentation continue to use Field and FieldStatus; value tooltips continue to use Tooltip.
|
|
81
|
+
| ID | Candidate invariant | Basis | Review state |
|
|
82
|
+
| --- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | -------------------------------------------------- |
|
|
83
|
+
| FR1 | The current render places a filled range, one or two thumbs, and optional tick marks over the background track. | Current source, docs, and focused tests | Verified current behavior; no new behavior decided |
|
|
84
|
+
| FR2 | `Slider`, `Interactive control`, `Track`, and `Thumb` carry the `slider`, `slider-control`, `slider-track`, and `slider-thumb` targets respectively. | Current source, public docs, and DEC-1 | Approved additive target contract |
|
|
85
|
+
| FR3 | Filled range, tick marks, mark labels, and adjacent text value display are stable rendered parts without their own current Slider target. | Current source and public docs | Verified current asymmetry; not ratified as policy |
|
|
86
|
+
| FR4 | Label and status presentation continue to use Field and FieldStatus; value tooltips continue to use Tooltip. | Current source and focused tests | Verified composition boundary |
|
|
87
|
+
| FR5 | `onChangeEnd` fires once when an interaction ends — a pointer drag or track press is released or cancelled, or a keyboard step (arrow, Page, Home, End) completes — and carries the value the owner renders after its synchronous response to that interaction's `onChange` calls, including any clamp, snap, or rejection; it never carries a value Slider emitted that the owner did not render. A range reports both values the same way. | Maintainer decision (DEC-3) | Approved |
|
|
79
88
|
|
|
80
89
|
### Observed current target asymmetry
|
|
81
90
|
|
|
@@ -106,6 +115,8 @@ either component's remaining target shape.
|
|
|
106
115
|
|
|
107
116
|
- ORD1: an arrow or Page key result snaps to the nearest step, then clamps to
|
|
108
117
|
the bounds; Home and End go to `min` and `max` (AR1).
|
|
118
|
+
- ORD2: within one interaction, Slider calls `onChange`, the owner responds and
|
|
119
|
+
renders, then `onChangeEnd` reports the rendered value (FR5).
|
|
109
120
|
- The anatomy decision (DEC-1) introduces no value, snapping, geometry,
|
|
110
121
|
interaction, or styling precedence rule of its own.
|
|
111
122
|
|
|
@@ -214,14 +225,15 @@ separately listed value tooltip retains Tooltip's target.
|
|
|
214
225
|
|
|
215
226
|
## Verification map
|
|
216
227
|
|
|
217
|
-
| Contract | Verification | Representative states
|
|
218
|
-
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
219
|
-
| FR1 | `Slider.test.tsx` structure, range, and marks suites | Single, range, horizontal, vertical, marks
|
|
220
|
-
| AR1 | Slider accessibility contract (`internal/a11y-spec`, pending) bound to Slider, in jsdom and Chromium; `Slider.test.tsx` keyboard suites, including "follows the mirrored track under RTL: ArrowLeft increases, ArrowRight decreases, Up/Down unchanged" and "keeps ArrowLeft decreasing on a vertical track under RTL"; the Page, Home/End and Up/Down cases under the contract | Single and range; horizontal LTR and RTL; vertical; disabled; formatted value
|
|
221
|
-
| FR2 | `Slider.test.tsx`, `themingTargets.test.ts`, generated probe theme, and Chromium | Root, horizontal/vertical control, disabled control, track, and thumb
|
|
222
|
-
| FR3 | Source and consumer-doc review | Filled range, marks, labels, adjacent text value
|
|
223
|
-
| FR4 | Source inspection; focused tests cover label, status, and disabled-reason Tooltip only | Label, status, value tooltip, disabled reason
|
|
224
|
-
|
|
|
228
|
+
| Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
|
|
229
|
+
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- |
|
|
230
|
+
| FR1 | `Slider.test.tsx` structure, range, and marks suites | Single, range, horizontal, vertical, marks | Removing or misaligning stable parts breaks existing role, position, or mark assertions. | `audit:Slider/anatomy` |
|
|
231
|
+
| AR1 | Slider accessibility contract (`internal/a11y-spec`, pending) bound to Slider, in jsdom and Chromium; `Slider.test.tsx` keyboard suites, including "follows the mirrored track under RTL: ArrowLeft increases, ArrowRight decreases, Up/Down unchanged" and "keeps ArrowLeft decreasing on a vertical track under RTL"; the Page, Home/End and Up/Down cases under the contract | Single and range; horizontal LTR and RTL; vertical; disabled; formatted value | A thumb loses its role, name or value properties; an arrow, Page, Home or End key stops stepping or steps against the mirrored track; a range thumb crosses the other | `audit:Slider/accessibility` |
|
|
232
|
+
| FR2 | `Slider.test.tsx`, `themingTargets.test.ts`, generated probe theme, and Chromium | Root, horizontal/vertical control, disabled control, track, and thumb | A target class/state is missing, undocumented, or placed outside its owning anatomy. | `audit:Slider/theming` |
|
|
233
|
+
| FR3 | Source and consumer-doc review | Filled range, marks, labels, adjacent text value | A missing target is inaccurately documented as present or intentionally permanent. | `audit:Slider/theming` |
|
|
234
|
+
| FR4 | Source inspection; focused tests cover label, status, and disabled-reason Tooltip only | Label, status, value tooltip, disabled reason | Shared composition or its accessible association disappears; value-tooltip composition still lacks focused coverage. | `audit:Slider/anatomy` |
|
|
235
|
+
| FR5 | Pending: `Slider.test.tsx` change-end cases driven by an owner that renders, clamps, or rejects the emitted value (not yet in the suite) | Single and range; pointer release before and after the owner re-renders; each keyboard step | `onChangeEnd` reports a value the owner did not render, fires more than once per interaction, or skips a completed keyboard step | `audit:Slider/accessibility` |
|
|
236
|
+
| Theming anatomy map | `scripts/check-knowledge.mjs` | Canonical anatomy and four current local targets | Missing, extra, prefixed, stale, or unclaimed mappings fail repository validation. | `audit:Slider/theming` |
|
|
225
237
|
|
|
226
238
|
The focused Slider suite asserts the `slider-control` target's orientation and
|
|
227
239
|
disabled state. Source/metadata guards cover all four target declarations. The
|
|
@@ -250,6 +262,15 @@ A thumb already plays the slider role as one composite control, so it carries th
|
|
|
250
262
|
|
|
251
263
|
Rejected: keeping ArrowRight as "increase" under RTL; a Tab stop or arrow model per range thumb that differs from the single thumb.
|
|
252
264
|
|
|
265
|
+
### DEC-3 — `onChangeEnd` reports the value the owner renders
|
|
266
|
+
|
|
267
|
+
**Reference:** `component:Slider/DEC-3`
|
|
268
|
+
**Decider:** `cixzhang`, `2026-10-16`
|
|
269
|
+
|
|
270
|
+
`onChangeEnd` marks the end of an interaction, so it reports what the slider shows when the interaction ends: the controlled value after the owner has answered `onChange`. An owner that clamps, snaps, or rejects a value therefore receives the value it kept, and the commit agrees with the thumb (FR5).
|
|
271
|
+
|
|
272
|
+
Rejected: reporting the last value Slider emitted through `onChange`.
|
|
273
|
+
|
|
253
274
|
## Open questions
|
|
254
275
|
|
|
255
276
|
- **OQ1 — After a joint ProgressBar and Slider family audit, should any shared
|
|
@@ -31,10 +31,10 @@ system_specs: [spec:AST-002/DEC-1, spec:AST-002/DEC-2, spec:AST-017/DEC-1]
|
|
|
31
31
|
|
|
32
32
|
| Area | Contract |
|
|
33
33
|
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
34
|
-
| Public contract | Canonicalize the existing
|
|
34
|
+
| Public contract | Canonicalize the existing mode as `edge?: 'padded' \| 'bleed'` (default `'padded'`) from the shared `edge` vocabulary; deprecate `isFullBleed` as an alias of `edge="bleed"` and remove it in the next minor release. |
|
|
35
35
|
| Behavior | Move first and last visible tab content toward the host content line, reaching it when the host inset is at least the edge-tab padding, while preserving tab interaction padding, focus-ring room, overflow controls, selection-indicator geometry, and the optional divider rail. |
|
|
36
36
|
| End-user impact | Edge tabs lose unnecessary extra inset where host geometry permits and remain fully interactive without clipped focus, selection, overflow, or divider treatment. |
|
|
37
|
-
| Builder impact | Existing `isFullBleed` callsites keep working during the deprecation window and migrate mechanically to `
|
|
37
|
+
| Builder impact | Existing `isFullBleed` callsites keep working during the deprecation window and migrate mechanically to `edge="bleed"`. |
|
|
38
38
|
| Compatibility/readiness | Current behavior is shipped; the canonical alias is additive, while removing the released legacy prop is a separately implemented breaking change in the next minor release. |
|
|
39
39
|
| Review checks | Reject a behavior change disguised as a rename, directional values without overflow/indicator proof, or early removal of the legacy prop. |
|
|
40
40
|
| Governing rules | `architecture:container-padding/INV9–INV11`; `spec:AST-002/DEC-1–DEC-2`; `spec:AST-017/DEC-1`. |
|
|
@@ -43,12 +43,13 @@ This table is a review projection; the body below is authoritative.
|
|
|
43
43
|
|
|
44
44
|
## Intent
|
|
45
45
|
|
|
46
|
-
TabList's current opt-in is edge
|
|
47
|
-
host inset, moves visible edge-tab content toward the surrounding content line,
|
|
46
|
+
TabList's current opt-in is the shared `bleed` edge treatment: it consumes the
|
|
47
|
+
full inherited host inset, moves visible edge-tab content toward the surrounding content line,
|
|
48
48
|
and retains Tab-owned interaction padding plus TabList-owned divider, overflow,
|
|
49
49
|
and indicator geometry. Content reaches the host line only when that inset is at
|
|
50
50
|
least the edge tab's own padding. The public `isFullBleed` name is misleading
|
|
51
|
-
because the strip
|
|
51
|
+
because the strip keeps a content line: it restores uncovered content inset
|
|
52
|
+
after the outer box expands, which `architecture:container-padding` names `bleed`.
|
|
52
53
|
|
|
53
54
|
## Compatibility and migration
|
|
54
55
|
|
|
@@ -61,21 +62,21 @@ because the strip restores uncovered content inset after the outer box expands.
|
|
|
61
62
|
|
|
62
63
|
During the compatibility window:
|
|
63
64
|
|
|
64
|
-
- `
|
|
65
|
-
- omitted `
|
|
65
|
+
- `edge="bleed"` is the canonical spelling, and omitted `edge` means `padded`;
|
|
66
|
+
- omitted `edge` plus `isFullBleed={true}` selects `bleed`;
|
|
66
67
|
- `isFullBleed` is deprecated and maintained examples use the canonical prop;
|
|
67
|
-
- if both props are supplied, explicit `
|
|
68
|
-
|
|
68
|
+
- if both props are supplied, explicit `edge` is canonical and the legacy
|
|
69
|
+
boolean cannot override it.
|
|
69
70
|
|
|
70
71
|
The removal release MUST include a `[breaking]` Changeset for
|
|
71
72
|
`@astryxdesign/core`, consumer release notes, and an `astryx upgrade` codemod that rewrites
|
|
72
|
-
`isFullBleed` to `
|
|
73
|
+
`isFullBleed` to `edge="bleed"`.
|
|
73
74
|
|
|
74
75
|
## Ownership boundary
|
|
75
76
|
|
|
76
77
|
**Owns**
|
|
77
78
|
|
|
78
|
-
- The TabList-wide edge
|
|
79
|
+
- The TabList-wide `bleed` edge treatment and its compatibility alias.
|
|
79
80
|
- The relationship between the outer TabList box, inner strip padding, edge tabs,
|
|
80
81
|
focus-ring room, overflow controls, and selection indicator.
|
|
81
82
|
- Which logical edges are supported by the public mode.
|
|
@@ -84,33 +85,34 @@ The removal release MUST include a `[breaking]` Changeset for
|
|
|
84
85
|
|
|
85
86
|
- Host padding publication, nesting, or overlay resets — owned by
|
|
86
87
|
`architecture:container-padding`.
|
|
87
|
-
- Button or List edge
|
|
88
|
-
-
|
|
88
|
+
- Button or List edge projections.
|
|
89
|
+
- The `flush` or `outset` values, or per-edge application, without separate
|
|
90
|
+
proof and owner approval.
|
|
89
91
|
- A change to navigation/tab semantics, selection state, keyboard behavior, or
|
|
90
92
|
overflow ownership.
|
|
91
93
|
|
|
92
94
|
## Public concepts
|
|
93
95
|
|
|
94
|
-
| Concept
|
|
95
|
-
|
|
|
96
|
-
| Edge
|
|
97
|
-
| Legacy alias
|
|
96
|
+
| Concept | Closed values or states | Meaning | Availability by variant/orientation/state | Default | Owner | Stability | Invalid-value behavior |
|
|
97
|
+
| -------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | ------------------ | ------------------- | ------------------------------- | -------------------------------------------- |
|
|
98
|
+
| Edge treatment | omitted, `padded`, `bleed` | `bleed`: consume both inherited inline host insets and move edge-tab content toward the host lines while retaining local interaction and strip-owned geometry; `padded`: current default geometry | Navigation and tabs patterns across current layout/overflow states | omitted (`padded`) | `component:TabList` | accepted; canonical API pending | Other values are rejected by the public type |
|
|
99
|
+
| Legacy alias | omitted, `false`, `true` | Compatibility spelling for `padded` or `bleed` | Until the next minor release | omitted/`false` | `component:TabList` | deprecated | `true` maps to `bleed`; explicit `edge` wins |
|
|
98
100
|
|
|
99
101
|
## Behavioral and layout contract
|
|
100
102
|
|
|
101
|
-
| ID | Invariant
|
|
102
|
-
| --- |
|
|
103
|
-
| FR1 | The current opt-in MUST remain
|
|
104
|
-
| FR2 | Compensation MUST preserve Tab interaction padding, focus-ring room, overflow detection and controls, scroll access, selection-indicator geometry, and the optional divider rail's expanded inline span.
|
|
105
|
-
| FR3 | `
|
|
106
|
-
| FR4 | During the deprecation window, omitted `
|
|
107
|
-
| FR5 | `isFullBleed` MUST be removed in the next minor release only after maintained docs/examples use `
|
|
108
|
-
| FR6 | `
|
|
103
|
+
| ID | Invariant | Basis | Status |
|
|
104
|
+
| --- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | -------------------------------------------------------- |
|
|
105
|
+
| FR1 | The current opt-in MUST remain the `bleed` treatment: the outer TabList box consumes the full inherited inline host inset and any `hasDivider` rail spans that expanded width; the inner strip restores `max(host inset - edge-tab padding, 0)` at each logical edge. Visible edge-tab content reaches the host line only when the host inset is at least the tab padding; otherwise it moves toward that line without crossing the host boundary. | Current source and `component:TabList/DEC-1` | Shipped behavior |
|
|
106
|
+
| FR2 | Compensation MUST preserve Tab interaction padding, focus-ring room, overflow detection and controls, scroll access, selection-indicator geometry, and the optional divider rail's expanded inline span. | Current source plus owner direction | Shipped behavior; expanded evidence required with rename |
|
|
107
|
+
| FR3 | `edge?: 'padded' \| 'bleed'` MUST become the canonical API, with `bleed` selecting FR1–FR2 geometry and `padded` the omitted default, without changing either. | `architecture:container-padding/INV10`; `component:TabList/DEC-1` | Accepted contract; implementation pending |
|
|
108
|
+
| FR4 | During the deprecation window, omitted `edge` plus `isFullBleed={true}` MUST produce the canonical `bleed` behavior. Explicit `edge` MUST remain authoritative when both props are supplied. | Compatibility decision | Accepted contract; implementation pending |
|
|
109
|
+
| FR5 | `isFullBleed` MUST be removed in the next minor release only after maintained docs/examples use `edge`, both paths have compatibility evidence, and the removal includes a `[breaking]` Changeset plus an `astryx upgrade` codemod for mechanical consumer migration. | `component:TabList/DEC-1`; `spec:AST-017/DEC-1` | Accepted contract; implementation pending |
|
|
110
|
+
| FR6 | `flush`, `outset`, and per-edge application MUST remain rejected until overflow, clipping, scroll-control, focus-ring, indicator, direction, and theme-padding evidence receives owner approval. | Shared edge vocabulary and TabList boundary | Accepted boundary |
|
|
109
111
|
|
|
110
112
|
### Allowed variation
|
|
111
113
|
|
|
112
114
|
- Layout, size, role, selected value, labels, tab content, and overflow mode may
|
|
113
|
-
vary while the same
|
|
115
|
+
vary while the same `bleed` meaning is preserved.
|
|
114
116
|
- The implementation may retain the current outer-box/inner-strip technique or
|
|
115
117
|
replace it with equivalent geometry that satisfies FR1–FR2.
|
|
116
118
|
- A host may be painted or bounded only where TabList's component-specific
|
|
@@ -118,20 +120,20 @@ The removal release MUST include a `[breaking]` Changeset for
|
|
|
118
120
|
|
|
119
121
|
### Representative states
|
|
120
122
|
|
|
121
|
-
| State
|
|
122
|
-
|
|
|
123
|
-
| Default/omitted
|
|
124
|
-
| Canonical
|
|
125
|
-
| Divider
|
|
126
|
-
| Legacy alias
|
|
127
|
-
| Overflowing strip
|
|
128
|
-
| Focused edge tab
|
|
123
|
+
| State | Required invariant | Allowed variation |
|
|
124
|
+
| ----------------- | ------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- |
|
|
125
|
+
| Default/omitted | Current TabList box, strip, and tab geometry are unchanged. | Any current layout, role, or overflow mode. |
|
|
126
|
+
| Canonical `bleed` | Outer box consumes each host inset; edge content reaches the host line only when that inset is at least the tab padding. | Host inset smaller than, equal to, or larger than edge-tab padding; asymmetric insets; LTR/RTL. |
|
|
127
|
+
| Divider | `hasDivider` rail spans the expanded outer inline width. | Fitting or overflowing strips. |
|
|
128
|
+
| Legacy alias | `isFullBleed={true}` matches canonical `bleed` geometry during the compatibility window. | The canonical prop may also be present. |
|
|
129
|
+
| Overflowing strip | Scroll access, edge fades, controls, and selected-tab reveal remain correct. | Start/end overflow independently. |
|
|
130
|
+
| Focused edge tab | The focus ring remains visible and local to the tab. | Keyboard or pointer focus. |
|
|
129
131
|
|
|
130
132
|
### Transformation and precedence order
|
|
131
133
|
|
|
132
|
-
- **ORD1 — Public input.** Resolve explicit `
|
|
133
|
-
`isFullBleed={true}` to `
|
|
134
|
-
|
|
134
|
+
- **ORD1 — Public input.** Resolve explicit `edge`; otherwise map
|
|
135
|
+
`isFullBleed={true}` to `bleed` during the compatibility window; otherwise use
|
|
136
|
+
`padded`.
|
|
135
137
|
- **ORD2 — Host geometry.** Read the matching inherited logical host insets, with
|
|
136
138
|
missing values falling back to zero.
|
|
137
139
|
- **ORD3 — Component geometry.** Resolve edge-tab padding and focus/indicator
|
|
@@ -145,10 +147,10 @@ The removal release MUST include a `[breaking]` Changeset for
|
|
|
145
147
|
|
|
146
148
|
## Accessibility contract
|
|
147
149
|
|
|
148
|
-
- **AR1 — Interaction remains stable.**
|
|
150
|
+
- **AR1 — Interaction remains stable.** The `bleed` treatment MUST NOT change the
|
|
149
151
|
tab/nav pattern, accessible names, selected state, keyboard movement, tab stop,
|
|
150
152
|
focus target, or controlled value behavior.
|
|
151
|
-
- **AR2 — Overflow remains reachable.**
|
|
153
|
+
- **AR2 — Overflow remains reachable.** `bleed` geometry MUST NOT clip or
|
|
152
154
|
hide a focus ring, selected indicator, visible tab content, or an overflow
|
|
153
155
|
control required to reach offscreen tabs.
|
|
154
156
|
|
|
@@ -162,8 +164,8 @@ The removal release MUST include a `[breaking]` Changeset for
|
|
|
162
164
|
|
|
163
165
|
## Family and system relationships
|
|
164
166
|
|
|
165
|
-
- `architecture:container-padding` owns host publication, the shared
|
|
166
|
-
|
|
167
|
+
- `architecture:container-padding` owns host publication, the shared `edge`
|
|
168
|
+
vocabulary, logical-edge semantics, and overlay reset.
|
|
167
169
|
- `architecture:public-component-api` and `spec:AST-002` own public API
|
|
168
170
|
admission; `spec:AST-017/DEC-1` owns the breaking classification and migration
|
|
169
171
|
evidence required when the released alias is removed.
|
|
@@ -189,9 +191,20 @@ TabList's current mode consumes the full inherited inline host inset, keeps an
|
|
|
189
191
|
optional divider across that expanded width, and restores only the portion of
|
|
190
192
|
content inset not already supplied by edge-tab padding. The user-facing intent is
|
|
191
193
|
therefore edge compensation, not a promise that tab content itself reaches an
|
|
192
|
-
outer edge.
|
|
193
|
-
|
|
194
|
-
|
|
194
|
+
outer edge. DEC-2 sets the canonical spelling; `isFullBleed` remains a
|
|
195
|
+
compatibility alias until its `[breaking]` next-minor removal, with the required
|
|
196
|
+
codemod and release evidence.
|
|
197
|
+
|
|
198
|
+
### DEC-2 — TabList spells its mode with the shared edge vocabulary
|
|
199
|
+
|
|
200
|
+
**Reference:** `component:TabList/DEC-2`
|
|
201
|
+
|
|
202
|
+
**Decider:** cixzhang, 2026-10-16
|
|
203
|
+
|
|
204
|
+
`architecture:container-padding` names TabList's geometry `bleed`: paint reaches
|
|
205
|
+
the host's padding edge and content stays on the host content line, or at the
|
|
206
|
+
edge tab's own padding when the host inset is smaller. `edge="bleed"` is the
|
|
207
|
+
canonical spelling, so TabList and List share one prop name and one value set.
|
|
195
208
|
|
|
196
209
|
## Open questions
|
|
197
210
|
|
|
@@ -191,6 +191,21 @@ for (const [surface, presentation, selector] of SURFACES) {
|
|
|
191
191
|
expect(own?.textContent).toContain('Start');
|
|
192
192
|
});
|
|
193
193
|
|
|
194
|
+
it('puts hidden, inert, and dir on the control', () => {
|
|
195
|
+
render(
|
|
196
|
+
<TimeInput
|
|
197
|
+
label="Start"
|
|
198
|
+
presentation={presentation}
|
|
199
|
+
hidden
|
|
200
|
+
inert
|
|
201
|
+
dir="rtl"
|
|
202
|
+
/>,
|
|
203
|
+
);
|
|
204
|
+
expect(control(selector)).toHaveAttribute('hidden');
|
|
205
|
+
expect(control(selector)).toHaveAttribute('inert');
|
|
206
|
+
expect(control(selector)).toHaveAttribute('dir', 'rtl');
|
|
207
|
+
});
|
|
208
|
+
|
|
194
209
|
it('keeps control* on the control box in a horizontal-labels FormLayout', () => {
|
|
195
210
|
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
196
211
|
const {container} = render(
|
|
@@ -497,6 +497,78 @@ describe('ToggleButton', () => {
|
|
|
497
497
|
expect(button).not.toHaveAttribute('aria-busy', 'true');
|
|
498
498
|
await act(async () => {});
|
|
499
499
|
});
|
|
500
|
+
|
|
501
|
+
// family:buttons FR5: a caller onClick on a standalone ToggleButton runs
|
|
502
|
+
// before onPressedChange and pressedChangeAction, never replaces the toggle
|
|
503
|
+
// request, and cancels it only by calling preventDefault() itself.
|
|
504
|
+
describe('with a caller onClick', () => {
|
|
505
|
+
it('runs the caller onClick first, then requests the next state and runs the Action', async () => {
|
|
506
|
+
const calls: string[] = [];
|
|
507
|
+
render(
|
|
508
|
+
<ToggleButton
|
|
509
|
+
label="Compact"
|
|
510
|
+
isPressed={false}
|
|
511
|
+
onClick={() => calls.push('onClick')}
|
|
512
|
+
onPressedChange={next => calls.push(`onPressedChange:${next}`)}
|
|
513
|
+
pressedChangeAction={next => {
|
|
514
|
+
calls.push(`pressedChangeAction:${next}`);
|
|
515
|
+
}}
|
|
516
|
+
/>,
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
fireEvent.click(screen.getByRole('button', {name: 'Compact'}));
|
|
520
|
+
await act(async () => {});
|
|
521
|
+
|
|
522
|
+
expect(calls).toEqual([
|
|
523
|
+
'onClick',
|
|
524
|
+
'onPressedChange:true',
|
|
525
|
+
'pressedChangeAction:true',
|
|
526
|
+
]);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it('requests no next state when the caller onClick calls preventDefault', async () => {
|
|
530
|
+
const onPressedChange = vi.fn();
|
|
531
|
+
const pressedChangeAction = vi.fn();
|
|
532
|
+
render(
|
|
533
|
+
<ToggleButton
|
|
534
|
+
label="Compact"
|
|
535
|
+
isPressed={false}
|
|
536
|
+
onClick={event => event.preventDefault()}
|
|
537
|
+
onPressedChange={onPressedChange}
|
|
538
|
+
pressedChangeAction={pressedChangeAction}
|
|
539
|
+
/>,
|
|
540
|
+
);
|
|
541
|
+
|
|
542
|
+
const button = screen.getByRole('button', {name: 'Compact'});
|
|
543
|
+
fireEvent.click(button);
|
|
544
|
+
await act(async () => {});
|
|
545
|
+
|
|
546
|
+
expect(onPressedChange).not.toHaveBeenCalled();
|
|
547
|
+
expect(pressedChangeAction).not.toHaveBeenCalled();
|
|
548
|
+
expect(button).toHaveAttribute('aria-pressed', 'false');
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it('still requests the next state when the click was already prevented before the caller onClick ran', () => {
|
|
552
|
+
const onPressedChange = vi.fn();
|
|
553
|
+
render(
|
|
554
|
+
<div onClickCapture={event => event.preventDefault()}>
|
|
555
|
+
<ToggleButton
|
|
556
|
+
label="Compact"
|
|
557
|
+
isPressed={false}
|
|
558
|
+
onClick={() => {}}
|
|
559
|
+
onPressedChange={onPressedChange}
|
|
560
|
+
/>
|
|
561
|
+
</div>,
|
|
562
|
+
);
|
|
563
|
+
|
|
564
|
+
fireEvent.click(screen.getByRole('button', {name: 'Compact'}));
|
|
565
|
+
|
|
566
|
+
expect(onPressedChange).toHaveBeenCalledExactlyOnceWith(
|
|
567
|
+
true,
|
|
568
|
+
expect.anything(),
|
|
569
|
+
);
|
|
570
|
+
});
|
|
571
|
+
});
|
|
500
572
|
});
|
|
501
573
|
|
|
502
574
|
// =============================================================================
|
|
@@ -721,6 +793,29 @@ describe.each(['single', 'multiple'] as const)(
|
|
|
721
793
|
expect(onPressedChange).not.toHaveBeenCalled();
|
|
722
794
|
expect(pressedChangeAction).not.toHaveBeenCalled();
|
|
723
795
|
});
|
|
796
|
+
|
|
797
|
+
// family:buttons FR11: a member's selection request comes from the group,
|
|
798
|
+
// and a caller onClick on the member must not replace it.
|
|
799
|
+
it('still asks the group to toggle a member that receives an onClick', async () => {
|
|
800
|
+
const onChange = vi.fn();
|
|
801
|
+
const onClick = vi.fn();
|
|
802
|
+
const groupProps =
|
|
803
|
+
type === 'single'
|
|
804
|
+
? {type, value: null, onChange}
|
|
805
|
+
: {type, value: [], onChange};
|
|
806
|
+
render(
|
|
807
|
+
<ToggleButtonGroup {...groupProps} label="View">
|
|
808
|
+
<ToggleButton value="grid" label="Grid" onClick={onClick} />
|
|
809
|
+
</ToggleButtonGroup>,
|
|
810
|
+
);
|
|
811
|
+
|
|
812
|
+
await userEvent.setup().click(screen.getByRole('button', {name: 'Grid'}));
|
|
813
|
+
|
|
814
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
815
|
+
expect(onChange).toHaveBeenCalledExactlyOnceWith(
|
|
816
|
+
type === 'single' ? 'grid' : ['grid'],
|
|
817
|
+
);
|
|
818
|
+
});
|
|
724
819
|
},
|
|
725
820
|
);
|
|
726
821
|
|