@astryxdesign/core 0.6.2-canary.bfee8a4 → 0.6.2-canary.c8d5849
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/Chat/ChatComposerInput.d.ts.map +1 -1
- package/dist/Chat/ChatComposerInput.js +2 -2
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +12 -2
- package/dist/Collapsible/Collapsible.d.ts.map +1 -1
- package/dist/Collapsible/Collapsible.js +6 -1
- package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteInput.js +1 -1
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +1 -1
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +4 -4
- package/dist/DateInput/NativeDateField.d.ts.map +1 -1
- package/dist/DateInput/NativeDateField.js +16 -16
- package/dist/DateInput/TouchDateField.d.ts.map +1 -1
- package/dist/DateInput/TouchDateField.js +2 -2
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +4 -4
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +8 -8
- package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
- package/dist/DateTimeInput/TouchDateTimeField.js +4 -4
- package/dist/DateTimeInput/nativePickerSegmentStyles.d.ts +9 -3
- package/dist/DateTimeInput/nativePickerSegmentStyles.d.ts.map +1 -1
- package/dist/DateTimeInput/nativePickerSegmentStyles.js +3 -3
- package/dist/Field/PanelSearchInput.d.ts.map +1 -1
- package/dist/Field/PanelSearchInput.js +1 -1
- package/dist/FileInput/FileInput.d.ts.map +1 -1
- package/dist/FileInput/FileInput.js +7 -7
- package/dist/Kbd/Kbd.d.ts +5 -3
- package/dist/Kbd/Kbd.d.ts.map +1 -1
- package/dist/Kbd/Kbd.js +36 -42
- package/dist/Link/Link.d.ts.map +1 -1
- package/dist/Link/Link.js +6 -2
- package/dist/Markdown/Markdown.d.ts +8 -2
- package/dist/Markdown/Markdown.d.ts.map +1 -1
- package/dist/Markdown/Markdown.fr23.bench.d.ts +2 -0
- package/dist/Markdown/Markdown.fr23.bench.d.ts.map +1 -0
- package/dist/Markdown/Markdown.fr23.bench.js +150 -0
- package/dist/Markdown/Markdown.fr23.sampling.d.ts +2 -0
- package/dist/Markdown/Markdown.fr23.sampling.d.ts.map +1 -0
- package/dist/Markdown/Markdown.fr23.sampling.js +43 -0
- package/dist/Markdown/Markdown.js +262 -109
- package/dist/Markdown/ast.d.ts +162 -0
- package/dist/Markdown/ast.d.ts.map +1 -0
- package/dist/Markdown/ast.js +74 -0
- package/dist/Markdown/index.d.ts +5 -3
- package/dist/Markdown/index.d.ts.map +1 -1
- package/dist/Markdown/index.js +3 -2
- package/dist/Markdown/parser/index.d.ts +10 -0
- package/dist/Markdown/parser/index.d.ts.map +1 -0
- package/dist/Markdown/parser/index.js +10 -0
- package/dist/Markdown/parser.d.ts +126 -58
- package/dist/Markdown/parser.d.ts.map +1 -1
- package/dist/Markdown/parser.js +716 -167
- package/dist/Markdown/plugins/frontmatter.d.ts +32 -0
- package/dist/Markdown/plugins/frontmatter.d.ts.map +1 -0
- package/dist/Markdown/plugins/frontmatter.js +132 -0
- package/dist/Markdown/plugins/index.d.ts +16 -0
- package/dist/Markdown/plugins/index.d.ts.map +1 -0
- package/dist/Markdown/plugins/index.js +13 -0
- package/dist/Markdown/plugins/protocol.d.ts +191 -0
- package/dist/Markdown/plugins/protocol.d.ts.map +1 -0
- package/dist/Markdown/plugins/protocol.js +957 -0
- package/dist/Markdown/plugins/semanticFence.d.ts +36 -0
- package/dist/Markdown/plugins/semanticFence.d.ts.map +1 -0
- package/dist/Markdown/plugins/semanticFence.js +213 -0
- package/dist/Markdown/plugins/sourceDecoration.d.ts +76 -0
- package/dist/Markdown/plugins/sourceDecoration.d.ts.map +1 -0
- package/dist/Markdown/plugins/sourceDecoration.js +393 -0
- package/dist/Markdown/plugins/textTransform.d.ts +30 -0
- package/dist/Markdown/plugins/textTransform.d.ts.map +1 -0
- package/dist/Markdown/plugins/textTransform.js +374 -0
- package/dist/Markdown/remark.d.ts +196 -0
- package/dist/Markdown/remark.d.ts.map +1 -0
- package/dist/Markdown/remark.js +1062 -0
- package/dist/Markdown/url.d.ts +4 -0
- package/dist/Markdown/url.d.ts.map +1 -0
- package/dist/Markdown/url.js +25 -0
- package/dist/Markdown/utils.d.ts +4 -2
- package/dist/Markdown/utils.d.ts.map +1 -1
- package/dist/Markdown/utils.js +2 -1
- package/dist/NumberInput/NumberInput.d.ts.map +1 -1
- package/dist/NumberInput/NumberInput.js +4 -4
- package/dist/Outline/index.d.ts +1 -0
- package/dist/Outline/index.d.ts.map +1 -1
- package/dist/Outline/parseOutlineFromMarkdown.d.ts +7 -1
- package/dist/Outline/parseOutlineFromMarkdown.d.ts.map +1 -1
- package/dist/Outline/parseOutlineFromMarkdown.js +14 -8
- package/dist/Outline/useOutlineFromMarkdown.d.ts +3 -1
- package/dist/Outline/useOutlineFromMarkdown.d.ts.map +1 -1
- package/dist/Outline/useOutlineFromMarkdown.js +8 -3
- package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearchEditPopover.js +46 -30
- package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
- package/dist/ProgressBar/ProgressBar.js +36 -31
- package/dist/RadioList/RadioListItem.d.ts.map +1 -1
- package/dist/RadioList/RadioListItem.js +13 -1
- package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
- package/dist/SegmentedControl/SegmentedControlItem.js +5 -5
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +1 -1
- package/dist/SideNav/SideNav.d.ts +2 -1
- package/dist/SideNav/SideNav.d.ts.map +1 -1
- package/dist/SideNav/SideNav.js +9 -3
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +14 -8
- package/dist/Spinner/Spinner.d.ts.map +1 -1
- package/dist/Spinner/Spinner.js +23 -7
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +11 -0
- package/dist/TabList/Tab.d.ts +1 -1
- package/dist/TabList/Tab.d.ts.map +1 -1
- package/dist/TabList/Tab.js +20 -7
- package/dist/Table/Table.d.ts +1 -0
- package/dist/Table/Table.d.ts.map +1 -1
- package/dist/Table/Table.js +29 -13
- package/dist/TextArea/TextArea.d.ts.map +1 -1
- package/dist/TextArea/TextArea.js +1 -1
- package/dist/TextInput/TextInput.d.ts.map +1 -1
- package/dist/TextInput/TextInput.js +2 -2
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +4 -4
- package/dist/ToggleButton/ToggleButton.d.ts +2 -1
- package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
- package/dist/ToggleButton/ToggleButton.js +7 -1
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +16 -7
- package/dist/astryx.css +14 -6
- package/dist/utils/interactionOverlay.stylex.d.ts +8 -0
- package/dist/utils/interactionOverlay.stylex.d.ts.map +1 -1
- package/dist/utils/interactionOverlay.stylex.js +9 -0
- package/package.json +18 -3
- package/src/Button/__tests__/Button.a11y.chromium.spec.ts +17 -1
- package/src/Button/__tests__/Button.a11y.known-failures.ts +0 -29
- package/src/Button/__tests__/Button.a11y.renders.tsx +9 -2
- package/src/Button/__tests__/Button.a11y.states.ts +9 -0
- package/src/Chat/ChatComposerInput.doc.mjs +3 -3
- package/src/Chat/ChatComposerInput.tsx +10 -2
- package/src/CheckboxInput/CheckboxInput.doc.mjs +11 -0
- package/src/CheckboxInput/CheckboxInput.test.tsx +34 -0
- package/src/CheckboxInput/CheckboxInput.tsx +21 -1
- package/src/ClickableCard/ClickableCard.test.tsx +102 -5
- package/src/Collapsible/Collapsible.doc.mjs +11 -0
- package/src/Collapsible/Collapsible.test.tsx +21 -0
- package/src/Collapsible/Collapsible.tsx +5 -0
- package/src/CommandPalette/CommandPaletteInput.tsx +6 -1
- package/src/ComplexSelector/ComplexSelector.tsx +6 -1
- package/src/DateInput/DateInput.tsx +6 -1
- package/src/DateInput/NativeDateField.tsx +24 -8
- package/src/DateInput/TouchDateField.tsx +6 -2
- package/src/DateRangeInput/DateRangeInput.tsx +6 -1
- package/src/DateTimeInput/DateTimeInput.tsx +6 -1
- package/src/DateTimeInput/TouchDateTimeField.tsx +6 -1
- package/src/DateTimeInput/nativePickerSegmentStyles.ts +24 -8
- package/src/Field/PanelSearchInput.tsx +5 -2
- package/src/FieldStatus/__tests__/StatusMessage.a11y.states.ts +1 -1
- package/src/FileInput/FileInput.tsx +11 -2
- package/src/Kbd/Kbd.doc.mjs +3 -3
- package/src/Kbd/Kbd.test.tsx +57 -1
- package/src/Kbd/Kbd.tsx +57 -37
- package/src/Layout/LayoutFooter.doc.mjs +17 -0
- package/src/Link/Link.doc.mjs +11 -0
- package/src/Link/Link.test.tsx +24 -0
- package/src/Link/Link.tsx +5 -0
- package/src/Markdown/Markdown.doc.mjs +272 -0
- package/src/Markdown/Markdown.fr23.bench.ts +174 -0
- package/src/Markdown/Markdown.fr23.perf.test.ts +249 -0
- package/src/Markdown/Markdown.fr23.sampling.ts +49 -0
- package/src/Markdown/Markdown.helpers.perf.test.ts +489 -0
- package/src/Markdown/Markdown.public.test.ts +126 -3
- package/src/Markdown/Markdown.spec.md +197 -44
- package/src/Markdown/Markdown.test.tsx +11 -0
- package/src/Markdown/Markdown.tsx +414 -135
- package/src/Markdown/ast.test.ts +307 -0
- package/src/Markdown/ast.ts +337 -0
- package/src/Markdown/incremental.test.ts +12 -0
- package/src/Markdown/index.ts +37 -1
- package/src/Markdown/modules/remark.spec.md +269 -0
- package/src/Markdown/parser/index.ts +64 -0
- package/src/Markdown/parser.public.test.ts +103 -0
- package/src/Markdown/parser.ts +1180 -235
- package/src/Markdown/plugins/frontmatter.test.tsx +125 -0
- package/src/Markdown/plugins/frontmatter.ts +198 -0
- package/src/Markdown/plugins/index.ts +53 -0
- package/src/Markdown/plugins/protocol.test.tsx +1022 -0
- package/src/Markdown/plugins/protocol.ts +1618 -0
- package/src/Markdown/plugins/semanticFence.test.tsx +546 -0
- package/src/Markdown/plugins/semanticFence.ts +424 -0
- package/src/Markdown/plugins/sourceDecoration.test.tsx +799 -0
- package/src/Markdown/plugins/sourceDecoration.ts +539 -0
- package/src/Markdown/plugins/textTransform.test.ts +467 -0
- package/src/Markdown/plugins/textTransform.ts +554 -0
- package/src/Markdown/remark.test.tsx +1877 -0
- package/src/Markdown/remark.ts +1574 -0
- package/src/Markdown/url.ts +30 -0
- package/src/Markdown/utils.ts +13 -1
- package/src/MultiSelector/MultiSelector.test.tsx +1 -35
- package/src/MultiSelector/__tests__/MultiSelector.listbox.a11y.test.tsx +128 -0
- package/src/NumberInput/NumberInput.tsx +6 -1
- package/src/Outline/Outline.doc.mjs +7 -5
- package/src/Outline/Outline.spec.md +1 -1
- package/src/Outline/index.ts +1 -0
- package/src/Outline/modules/parseOutlineFromMarkdown.spec.md +142 -0
- package/src/Outline/parseOutlineFromMarkdown.ts +40 -10
- package/src/Outline/useOutlineFromMarkdown.ts +15 -3
- package/src/PowerSearch/PowerSearch.doc.mjs +2 -2
- package/src/PowerSearch/PowerSearchEditPopover.test.tsx +150 -1
- package/src/PowerSearch/PowerSearchEditPopover.tsx +51 -28
- package/src/ProgressBar/ProgressBar.test.tsx +29 -11
- package/src/ProgressBar/ProgressBar.tsx +59 -72
- package/src/RadioList/RadioList.doc.mjs +11 -0
- package/src/RadioList/RadioList.test.tsx +32 -0
- package/src/RadioList/RadioListItem.tsx +25 -1
- package/src/SegmentedControl/SegmentedControl.doc.mjs +2 -2
- package/src/SegmentedControl/SegmentedControl.test.tsx +31 -0
- package/src/SegmentedControl/SegmentedControlItem.tsx +6 -9
- package/src/Selector/Selector.spec.md +12 -9
- package/src/Selector/Selector.tsx +6 -1
- package/src/Selector/__tests__/Listbox.a11y.chromium.spec.ts +130 -0
- package/src/Selector/__tests__/Listbox.a11y.dom.ts +38 -0
- package/src/Selector/__tests__/Listbox.a11y.inventory.test.ts +63 -0
- package/src/Selector/__tests__/Listbox.a11y.known-failures.ts +37 -0
- package/src/Selector/__tests__/Listbox.a11y.states.ts +232 -0
- package/src/Selector/__tests__/Selector.listbox.a11y.test.tsx +121 -0
- package/src/SideNav/SideNav.doc.mjs +1 -1
- package/src/SideNav/SideNav.test.tsx +10 -0
- package/src/SideNav/SideNav.tsx +14 -2
- package/src/Slider/Slider.doc.mjs +11 -0
- package/src/Slider/Slider.test.tsx +128 -0
- package/src/Slider/Slider.tsx +25 -7
- package/src/Spinner/Spinner.test.tsx +4 -0
- package/src/Spinner/Spinner.tsx +31 -18
- package/src/Switch/Switch.doc.mjs +11 -0
- package/src/Switch/Switch.test.tsx +16 -0
- package/src/Switch/Switch.tsx +28 -0
- package/src/TabList/Tab.tsx +35 -7
- package/src/TabList/TabList.doc.mjs +11 -0
- package/src/TabList/TabList.test.tsx +66 -0
- package/src/TabList/__tests__/Tabs.a11y.known-failures.ts +1 -34
- package/src/Table/Table.doc.mjs +1 -1
- package/src/Table/Table.spec.md +54 -40
- package/src/Table/Table.test.tsx +83 -10
- package/src/Table/Table.tsx +30 -22
- package/src/Table/__tests__/TableScroll.a11y.chromium.spec.ts +137 -0
- package/src/TextArea/TextArea.tsx +6 -1
- package/src/TextInput/TextInput.test.tsx +20 -1
- package/src/TextInput/TextInput.tsx +6 -1
- package/src/TimeInput/TimeInput.tsx +6 -1
- package/src/ToggleButton/ToggleButton.test.tsx +133 -0
- package/src/ToggleButton/ToggleButton.tsx +9 -2
- package/src/ToggleButton/__tests__/ToggleButton.a11y.chromium.spec.ts +209 -0
- package/src/Tokenizer/Tokenizer.spec.md +142 -75
- package/src/TreeList/TreeList.spec.md +123 -75
- package/src/Typeahead/BaseTypeahead.spec.md +4 -3
- package/src/Typeahead/BaseTypeahead.tsx +21 -7
- package/src/Typeahead/Typeahead.test.tsx +53 -0
- package/src/__tests__/PressedState.a11y.chromium.spec.ts +813 -0
- package/src/__tests__/pressState.ts +93 -0
- package/src/inputFontFloor.test.ts +106 -0
- package/src/utils/interactionOverlay.stylex.ts +20 -0
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import {describe, it, expect, vi, beforeEach} from 'vitest';
|
|
16
16
|
import {render, screen, fireEvent, waitFor} from '@testing-library/react';
|
|
17
17
|
import userEvent from '@testing-library/user-event';
|
|
18
|
+
import {hasPressedArm} from '../__tests__/pressState';
|
|
18
19
|
import {SegmentedControl} from './SegmentedControl';
|
|
19
20
|
import {SegmentedControlItem} from './SegmentedControlItem';
|
|
20
21
|
import {
|
|
@@ -672,3 +673,33 @@ describe('forced colors (WCAG 1.4.11)', () => {
|
|
|
672
673
|
expect(getAllInjectedCss()).toContain('forced-color-adjust: none;');
|
|
673
674
|
});
|
|
674
675
|
});
|
|
676
|
+
|
|
677
|
+
describe('pressed state', () => {
|
|
678
|
+
it('paints the pressed overlay on a segment while it is pressed', () => {
|
|
679
|
+
render(
|
|
680
|
+
<SegmentedControl value="grid" onChange={() => {}} label="View mode">
|
|
681
|
+
<SegmentedControlItem value="grid" label="Grid" />
|
|
682
|
+
<SegmentedControlItem value="list" label="List" />
|
|
683
|
+
</SegmentedControl>,
|
|
684
|
+
);
|
|
685
|
+
// The unselected segment is the one a press can change; it carries the
|
|
686
|
+
// system's hover and pressed overlay.
|
|
687
|
+
expect(hasPressedArm(screen.getByRole('radio', {name: 'List'}))).toBe(true);
|
|
688
|
+
// The selected segment keeps its raised surface as it is.
|
|
689
|
+
expect(hasPressedArm(screen.getByRole('radio', {name: 'Grid'}))).toBe(
|
|
690
|
+
false,
|
|
691
|
+
);
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
it('does not press a disabled segment', () => {
|
|
695
|
+
render(
|
|
696
|
+
<SegmentedControl value="grid" onChange={() => {}} label="View mode">
|
|
697
|
+
<SegmentedControlItem value="grid" label="Grid" />
|
|
698
|
+
<SegmentedControlItem value="list" label="List" isDisabled />
|
|
699
|
+
</SegmentedControl>,
|
|
700
|
+
);
|
|
701
|
+
expect(hasPressedArm(screen.getByRole('radio', {name: 'List'}))).toBe(
|
|
702
|
+
false,
|
|
703
|
+
);
|
|
704
|
+
});
|
|
705
|
+
});
|
|
@@ -33,6 +33,7 @@ import {mergeProps, composeEventHandlers} from '../utils';
|
|
|
33
33
|
import type {BaseProps} from '../BaseProps';
|
|
34
34
|
import {themeProps} from '../utils/themeProps';
|
|
35
35
|
import {focusOutlineProps} from '../utils/focusOutline.stylex';
|
|
36
|
+
import {interactionOverlayStyles} from '../utils/interactionOverlay.stylex';
|
|
36
37
|
|
|
37
38
|
export interface SegmentedControlItemProps extends BaseProps<HTMLButtonElement> {
|
|
38
39
|
ref?: React.Ref<HTMLButtonElement>;
|
|
@@ -91,14 +92,6 @@ const styles = stylex.create({
|
|
|
91
92
|
transitionDuration: durationVars['--duration-fast'],
|
|
92
93
|
transitionTimingFunction: easeVars['--ease-standard'],
|
|
93
94
|
},
|
|
94
|
-
hover: {
|
|
95
|
-
backgroundColor: {
|
|
96
|
-
default: null,
|
|
97
|
-
':hover:where(:not(:disabled,[aria-disabled="true"]))': {
|
|
98
|
-
'@media (hover: hover)': colorVars['--color-overlay-hover'],
|
|
99
|
-
},
|
|
100
|
-
},
|
|
101
|
-
},
|
|
102
95
|
selected: {
|
|
103
96
|
// Forced colors (Windows High Contrast) strips the painted surface fill
|
|
104
97
|
// and box shadow, which would leave the selected segment with no state
|
|
@@ -248,7 +241,11 @@ export function SegmentedControlItem({
|
|
|
248
241
|
sizeStyles[size],
|
|
249
242
|
isFill && styles.fill,
|
|
250
243
|
isSelected && styles.selected,
|
|
251
|
-
|
|
244
|
+
// The shared hover and pressed overlay, on the segments a press can
|
|
245
|
+
// change: the selected segment keeps its raised surface as it is.
|
|
246
|
+
!isSelected &&
|
|
247
|
+
!isItemDisabled &&
|
|
248
|
+
interactionOverlayStyles.backgroundColor,
|
|
252
249
|
isItemDisabled && styles.disabled,
|
|
253
250
|
xstyle,
|
|
254
251
|
),
|
|
@@ -14,6 +14,8 @@ verified_by:
|
|
|
14
14
|
[
|
|
15
15
|
packages/core/src/Selector/Selector.test.tsx,
|
|
16
16
|
packages/core/src/Selector/Selector.source-build.test.mjs,
|
|
17
|
+
packages/core/src/Selector/__tests__/Selector.listbox.a11y.test.tsx,
|
|
18
|
+
packages/core/src/Selector/__tests__/Listbox.a11y.chromium.spec.ts,
|
|
17
19
|
]
|
|
18
20
|
modules: []
|
|
19
21
|
families: [family:input-fields]
|
|
@@ -274,15 +276,16 @@ in `spec:AST-004` as current runtime behavior.
|
|
|
274
276
|
|
|
275
277
|
## Verification map
|
|
276
278
|
|
|
277
|
-
| Contract | Verification
|
|
278
|
-
| --------------------- |
|
|
279
|
-
| FR1, AR2, AR3 | `Selector.test.tsx` selection, focus, and keyboard suites
|
|
280
|
-
| FR2 | placement and selected-item geometry tests
|
|
281
|
-
| FR3, AV1 | `itemMarkColumn` source inspection plus indicator-position and replacement-indicator tests
|
|
282
|
-
| FR4, AR1, AR2 | presentation tests plus `aria-haspopup` source review
|
|
283
|
-
|
|
|
284
|
-
|
|
|
285
|
-
|
|
|
279
|
+
| Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
|
|
280
|
+
| --------------------- | -------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- |
|
|
281
|
+
| FR1, AR2, AR3 | `Selector.test.tsx` selection, focus, and keyboard suites | closed/open, search/non-search, disabled option | Removing selection wiring, focus movement, or keyboard behavior fails the named interaction tests | `audit:Selector/behavior` |
|
|
282
|
+
| FR2 | placement and selected-item geometry tests | default, explicit, RTL, transformed entry | Using the wrong positioning model fails the expected position-area or margin | `audit:Selector/behavior` |
|
|
283
|
+
| FR3, AV1 | `itemMarkColumn` source inspection plus indicator-position and replacement-indicator tests | start/end, selected/unselected, check/radio | Removing the wrapper fails row-structure tests; changing its reserved width requires source/layout review | `audit:Selector/design-rendered` |
|
|
284
|
+
| FR4, AR1, AR2 | presentation tests plus `aria-haspopup` source review | pointer, compact coarse pointer, search/non-search | Wrong Popover/dialog roles or focus destinations fail tests; `aria-haspopup` values require source/a11y review | `audit:Selector/accessibility` |
|
|
285
|
+
| AR1, AV2 | `Selector.listbox.a11y.test.tsx` and `Listbox.a11y.chromium.spec.ts` against the shared Listbox contract | popover/sheet, flat/grouped, selected/unselected, disabled, search, loading, custom RTL | A new or wider loss of role, listbox/option name, state, or ownership gates; the exact unnamed no-search sheet state remains known debt | `audit:Selector/accessibility` |
|
|
286
|
+
| FR5 | loading, empty-state, and live-region tests | empty options, unmatched search, loading | Empty/no-results output appears or is announced while loading | `audit:Selector/behavior` |
|
|
287
|
+
| FR6, AR4 | read-only interaction, form, ARIA, and theme-state tests | search/non-search, clearable, open→read-only, disabled precedence | A value changes, popup or edit affordance remains, form value disappears, or read-only semantics are absent | `audit:Selector/accessibility` |
|
|
288
|
+
| source-build contract | `Selector.source-build.test.mjs` | package source compiled by consumer Babel | Moving evaluated StyleX values outside the supported source form fails compilation | `audit:Selector/code-health` |
|
|
286
289
|
|
|
287
290
|
## Decision log
|
|
288
291
|
|
|
@@ -102,9 +102,14 @@ const styles = stylex.create({
|
|
|
102
102
|
paddingBlock: spacingVars['--spacing-2'],
|
|
103
103
|
paddingInline: spacingVars['--spacing-3'],
|
|
104
104
|
fontFamily: typographyVars['--font-family-body'],
|
|
105
|
+
// The 16px floor is iOS-only: iOS Safari zooms the page when a focused
|
|
106
|
+
// control sits under 16px, and only iOS WebKit implements
|
|
107
|
+
// -webkit-touch-callout to key the coarse-pointer floor to it.
|
|
105
108
|
fontSize: {
|
|
106
109
|
default: typeScaleVars['--text-label-size'],
|
|
107
|
-
'@media (pointer: coarse)':
|
|
110
|
+
'@media (pointer: coarse)': {
|
|
111
|
+
'@supports (-webkit-touch-callout: none)': `max(1rem, ${typeScaleVars['--text-label-size']})`,
|
|
112
|
+
},
|
|
108
113
|
},
|
|
109
114
|
// A FIXED line box, not the ratio: the trigger's padding is derived from
|
|
110
115
|
// one line being `--spacing-5` tall, and a ratio makes the line box track
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Listbox.a11y.chromium.spec.ts
|
|
5
|
+
* @input Uses the Listbox contract, browser harness, state inventory, and checked-in stories
|
|
6
|
+
* @output DOM/accessibility-tree evidence for each real Listbox binding part
|
|
7
|
+
* @position Browser binding; keyboard policy, pixels, and real AT remain separate owners.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {expect, test} from '@playwright/test';
|
|
11
|
+
import {
|
|
12
|
+
LISTBOX_PATTERN,
|
|
13
|
+
checkAccessibilitySpec,
|
|
14
|
+
blockingResults,
|
|
15
|
+
formatFailures,
|
|
16
|
+
formatReport,
|
|
17
|
+
summarize,
|
|
18
|
+
neverExercised,
|
|
19
|
+
unmatchedKnownFailures,
|
|
20
|
+
type BindingResult,
|
|
21
|
+
} from '@astryxdesign/a11y-spec';
|
|
22
|
+
import {
|
|
23
|
+
createChromiumHarness,
|
|
24
|
+
holdMotionStill,
|
|
25
|
+
} from '@astryxdesign/a11y-spec/chromium';
|
|
26
|
+
import {
|
|
27
|
+
serveStorybook,
|
|
28
|
+
DEFAULT_STORYBOOK_DIR,
|
|
29
|
+
type StaticServer,
|
|
30
|
+
} from '@astryxdesign/a11y-spec/storybook';
|
|
31
|
+
import {LISTBOX_KNOWN_FAILURES} from './Listbox.a11y.known-failures';
|
|
32
|
+
import {LISTBOX_SCENARIOS, listboxParts} from './Listbox.a11y.states';
|
|
33
|
+
|
|
34
|
+
let storybook: StaticServer;
|
|
35
|
+
const observedResults: BindingResult[] = [];
|
|
36
|
+
test.beforeAll(async () => {
|
|
37
|
+
storybook = await serveStorybook(
|
|
38
|
+
process.env.ASTRYX_STORYBOOK_DIR ?? DEFAULT_STORYBOOK_DIR,
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
test.afterAll(async () => {
|
|
42
|
+
await storybook?.close();
|
|
43
|
+
expect(
|
|
44
|
+
unmatchedKnownFailures(LISTBOX_KNOWN_FAILURES, observedResults),
|
|
45
|
+
).toEqual([]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
for (const scenario of LISTBOX_SCENARIOS) {
|
|
49
|
+
test(`${scenario.component} — ${scenario.id}`, async ({page}) => {
|
|
50
|
+
test.setTimeout(2 * 60 * 1000);
|
|
51
|
+
const cdp = await page.context().newCDPSession(page);
|
|
52
|
+
const results: BindingResult[] = [];
|
|
53
|
+
const mount = async () => {
|
|
54
|
+
await page.goto(
|
|
55
|
+
`${storybook.origin}/iframe.html?id=a11y-listbox-pattern--${scenario.id}&viewMode=story&globals=direction:${scenario.direction ?? 'ltr'}`,
|
|
56
|
+
{waitUntil: 'load'},
|
|
57
|
+
);
|
|
58
|
+
await holdMotionStill(page);
|
|
59
|
+
await expect(
|
|
60
|
+
page.locator(`[data-listbox-scenario="${scenario.id}"]`),
|
|
61
|
+
).toBeAttached();
|
|
62
|
+
await page.getByRole('listbox').waitFor({state: 'visible'});
|
|
63
|
+
if (scenario.query != null) {
|
|
64
|
+
await page.getByRole('combobox').fill(scenario.query);
|
|
65
|
+
}
|
|
66
|
+
await expect(page.getByRole('listbox').getByRole('option')).toHaveCount(
|
|
67
|
+
scenario.expectedOptions.length,
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
for (const part of listboxParts(scenario)) {
|
|
72
|
+
results.push(
|
|
73
|
+
await checkAccessibilitySpec({
|
|
74
|
+
spec: LISTBOX_PATTERN,
|
|
75
|
+
binding: `${scenario.component}.${part.role}`,
|
|
76
|
+
state: part.state,
|
|
77
|
+
facts: part.facts,
|
|
78
|
+
knownFailures: LISTBOX_KNOWN_FAILURES,
|
|
79
|
+
mount: async () => {
|
|
80
|
+
await mount();
|
|
81
|
+
const listbox = page.getByRole('listbox');
|
|
82
|
+
const subject =
|
|
83
|
+
part.role === 'listbox'
|
|
84
|
+
? listbox
|
|
85
|
+
: part.role === 'group'
|
|
86
|
+
? listbox
|
|
87
|
+
.getByRole('group')
|
|
88
|
+
.nth(scenario.groups.indexOf(part.name ?? ''))
|
|
89
|
+
: listbox.getByRole('option', {
|
|
90
|
+
name: part.name,
|
|
91
|
+
exact: true,
|
|
92
|
+
});
|
|
93
|
+
return createChromiumHarness({
|
|
94
|
+
page,
|
|
95
|
+
cdp,
|
|
96
|
+
subject,
|
|
97
|
+
related: {
|
|
98
|
+
listbox,
|
|
99
|
+
...Object.fromEntries(
|
|
100
|
+
scenario.expectedOptions.map(option => [
|
|
101
|
+
`option:${option.value}`,
|
|
102
|
+
listbox.getByRole('option', {
|
|
103
|
+
name: option.name,
|
|
104
|
+
exact: true,
|
|
105
|
+
}),
|
|
106
|
+
]),
|
|
107
|
+
),
|
|
108
|
+
...Object.fromEntries(
|
|
109
|
+
scenario.groups.map((name, index) => [
|
|
110
|
+
`group:${name}`,
|
|
111
|
+
listbox.getByRole('group').nth(index),
|
|
112
|
+
]),
|
|
113
|
+
),
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
observedResults.push(...results);
|
|
122
|
+
const report = summarize(LISTBOX_PATTERN, results);
|
|
123
|
+
console.log(formatReport(report));
|
|
124
|
+
expect(formatFailures(blockingResults(results))).toBe('');
|
|
125
|
+
expect(report.counts.unrun).toBe(0);
|
|
126
|
+
if (scenario.component === 'Selector' && scenario.groups.length > 0) {
|
|
127
|
+
expect(neverExercised(results)).toEqual([]);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Listbox.a11y.dom.ts
|
|
5
|
+
* @input Uses Vitest lifecycle hooks and the DOM Dialog prototype
|
|
6
|
+
* @output Scoped stubs for Dialog methods missing from jsdom
|
|
7
|
+
* @position DOM-only binding setup; these stubs prove no native modal behavior.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {afterAll, beforeAll} from 'vitest';
|
|
11
|
+
|
|
12
|
+
export function installListboxDialogStubs(): void {
|
|
13
|
+
const originals = new Map<string, PropertyDescriptor | undefined>();
|
|
14
|
+
beforeAll(() => {
|
|
15
|
+
for (const name of ['show', 'showModal', 'close']) {
|
|
16
|
+
originals.set(
|
|
17
|
+
name,
|
|
18
|
+
Object.getOwnPropertyDescriptor(HTMLDialogElement.prototype, name),
|
|
19
|
+
);
|
|
20
|
+
Object.defineProperty(HTMLDialogElement.prototype, name, {
|
|
21
|
+
configurable: true,
|
|
22
|
+
writable: true,
|
|
23
|
+
value: function (this: HTMLDialogElement) {
|
|
24
|
+
this.open = name !== 'close';
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
afterAll(() => {
|
|
30
|
+
for (const [name, descriptor] of originals) {
|
|
31
|
+
if (descriptor == null) {
|
|
32
|
+
Reflect.deleteProperty(HTMLDialogElement.prototype, name);
|
|
33
|
+
} else {
|
|
34
|
+
Object.defineProperty(HTMLDialogElement.prototype, name, descriptor);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Listbox.a11y.inventory.test.ts
|
|
5
|
+
* @input Uses the locked Listbox migration matrix and checklist
|
|
6
|
+
* @output Coverage-boundary and part-inventory regression checks
|
|
7
|
+
* @position Binding metadata proof, separate from runtime conformance results.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {describe, expect, it} from 'vitest';
|
|
11
|
+
import {LISTBOX_PATTERN, unansweredDimensions} from '@astryxdesign/a11y-spec';
|
|
12
|
+
import {LISTBOX_KNOWN_FAILURES} from './Listbox.a11y.known-failures';
|
|
13
|
+
import {
|
|
14
|
+
LISTBOX_SCENARIOS,
|
|
15
|
+
LISTBOX_EXCLUSIONS,
|
|
16
|
+
listboxParts,
|
|
17
|
+
} from './Listbox.a11y.states';
|
|
18
|
+
|
|
19
|
+
describe('Listbox migration inventory', () => {
|
|
20
|
+
it('accounts for every checklist dimension without claiming other evidence layers', () => {
|
|
21
|
+
expect(unansweredDimensions(LISTBOX_PATTERN)).toEqual([]);
|
|
22
|
+
expect(LISTBOX_EXCLUSIONS).toHaveLength(5);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('binds 21 scenarios and all six real semantic parts', () => {
|
|
26
|
+
expect(LISTBOX_SCENARIOS).toHaveLength(21);
|
|
27
|
+
const parts = LISTBOX_SCENARIOS.flatMap(scenario =>
|
|
28
|
+
listboxParts(scenario).map(part => ({
|
|
29
|
+
binding: `${scenario.component}.${part.role}`,
|
|
30
|
+
state: part.state,
|
|
31
|
+
})),
|
|
32
|
+
);
|
|
33
|
+
expect(parts).toHaveLength(89);
|
|
34
|
+
expect(new Set(parts.map(part => part.state)).size).toBe(89);
|
|
35
|
+
expect([...new Set(parts.map(part => part.binding))].sort()).toEqual([
|
|
36
|
+
'MultiSelector.group',
|
|
37
|
+
'MultiSelector.listbox',
|
|
38
|
+
'MultiSelector.option',
|
|
39
|
+
'Selector.group',
|
|
40
|
+
'Selector.listbox',
|
|
41
|
+
'Selector.option',
|
|
42
|
+
]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('pins each known failure to one exact inventory part', () => {
|
|
46
|
+
const parts = LISTBOX_SCENARIOS.flatMap(scenario =>
|
|
47
|
+
listboxParts(scenario).map(part => ({
|
|
48
|
+
binding: `${scenario.component}.${part.role}`,
|
|
49
|
+
state: part.state,
|
|
50
|
+
})),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
expect(LISTBOX_KNOWN_FAILURES).toHaveLength(2);
|
|
54
|
+
for (const record of LISTBOX_KNOWN_FAILURES) {
|
|
55
|
+
expect(
|
|
56
|
+
parts.filter(
|
|
57
|
+
part =>
|
|
58
|
+
part.binding === record.binding && part.state === record.state,
|
|
59
|
+
),
|
|
60
|
+
).toHaveLength(1);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Listbox.a11y.known-failures.ts
|
|
5
|
+
* @input Uses KnownFailure from @astryxdesign/a11y-spec
|
|
6
|
+
* @output Exact public-safe Selector and MultiSelector listbox migration failures
|
|
7
|
+
* @position AST-021 debt records; operational ownership remains outside public source.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {KnownFailure} from '@astryxdesign/a11y-spec';
|
|
11
|
+
|
|
12
|
+
export const LISTBOX_KNOWN_FAILURES: ReadonlyArray<KnownFailure> = [
|
|
13
|
+
{
|
|
14
|
+
expectation: 'listbox.exposure.identity',
|
|
15
|
+
binding: 'Selector.listbox',
|
|
16
|
+
state: 'single-sheet:listbox',
|
|
17
|
+
evidenceLayer: 'accessibility-tree',
|
|
18
|
+
failureEquals: 'the browser exposes the listbox without an accessible name',
|
|
19
|
+
standardsReference: 'WCAG 2.2 4.1.2 Name, Role, Value (Level A)',
|
|
20
|
+
userImpact:
|
|
21
|
+
'An accessibility consumer encounters an unnamed listbox in the bottom-sheet presentation and cannot identify what set of choices it contains.',
|
|
22
|
+
reason:
|
|
23
|
+
'The no-search bottom-sheet presentation renders a visible sheet heading without assigning that heading, or another label, as the listbox accessible name. This migration records the existing behavior without changing the component.',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
expectation: 'listbox.exposure.identity',
|
|
27
|
+
binding: 'MultiSelector.listbox',
|
|
28
|
+
state: 'multiple-sheet:listbox',
|
|
29
|
+
evidenceLayer: 'accessibility-tree',
|
|
30
|
+
failureEquals: 'the browser exposes the listbox without an accessible name',
|
|
31
|
+
standardsReference: 'WCAG 2.2 4.1.2 Name, Role, Value (Level A)',
|
|
32
|
+
userImpact:
|
|
33
|
+
'An accessibility consumer encounters an unnamed multi-select listbox in the bottom-sheet presentation and cannot identify what set of choices it contains.',
|
|
34
|
+
reason:
|
|
35
|
+
'The no-search bottom-sheet presentation renders a visible sheet heading without assigning that heading, or another label, as the listbox accessible name. This migration records the existing behavior without changing the component.',
|
|
36
|
+
},
|
|
37
|
+
];
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Listbox.a11y.states.ts
|
|
5
|
+
* @input Uses public Selector option types and the shared Listbox facts
|
|
6
|
+
* @output A bounded, explicit inventory of existing listbox/group/option states
|
|
7
|
+
* @position Test-only migration ledger shared by component tests and browser stories.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {ListboxStateFacts} from '@astryxdesign/a11y-spec';
|
|
11
|
+
import type {SelectorOptionType} from '../types';
|
|
12
|
+
|
|
13
|
+
export interface ListboxScenario {
|
|
14
|
+
readonly id: string;
|
|
15
|
+
readonly component: 'Selector' | 'MultiSelector';
|
|
16
|
+
readonly options: SelectorOptionType[];
|
|
17
|
+
readonly values: string[];
|
|
18
|
+
readonly expectedOptions: ReadonlyArray<{
|
|
19
|
+
value: string;
|
|
20
|
+
name: string;
|
|
21
|
+
disabled: boolean;
|
|
22
|
+
selected?: boolean;
|
|
23
|
+
group?: string;
|
|
24
|
+
}>;
|
|
25
|
+
readonly groups: ReadonlyArray<string>;
|
|
26
|
+
readonly hasSearch?: boolean;
|
|
27
|
+
readonly query?: string;
|
|
28
|
+
readonly presentation?: 'popover' | 'bottom-sheet';
|
|
29
|
+
readonly customContent?: boolean;
|
|
30
|
+
readonly hiddenLabel?: boolean;
|
|
31
|
+
readonly direction?: 'ltr' | 'rtl';
|
|
32
|
+
readonly isLoading?: boolean;
|
|
33
|
+
readonly hasSelectAll?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const flatOptions: SelectorOptionType[] = [
|
|
37
|
+
{value: 'apple', label: 'Apple'},
|
|
38
|
+
{value: 'banana', label: 'Banana'},
|
|
39
|
+
{value: 'cherry', label: 'Cherry', disabled: true},
|
|
40
|
+
];
|
|
41
|
+
const flatExpected = [
|
|
42
|
+
{value: 'apple', name: 'Apple', disabled: false},
|
|
43
|
+
{value: 'banana', name: 'Banana', disabled: false},
|
|
44
|
+
{value: 'cherry', name: 'Cherry', disabled: true},
|
|
45
|
+
];
|
|
46
|
+
const groupedOptions: SelectorOptionType[] = [
|
|
47
|
+
{
|
|
48
|
+
type: 'section',
|
|
49
|
+
title: 'Citrus',
|
|
50
|
+
options: [
|
|
51
|
+
{value: 'orange', label: 'Orange'},
|
|
52
|
+
{value: 'lemon', label: 'Lemon'},
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
{type: 'divider'},
|
|
56
|
+
{
|
|
57
|
+
type: 'section',
|
|
58
|
+
title: 'Berries',
|
|
59
|
+
options: [{value: 'blueberry', label: 'Blueberry', disabled: true}],
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
const groupedExpected = [
|
|
63
|
+
{value: 'orange', name: 'Orange', disabled: false, group: 'Citrus'},
|
|
64
|
+
{value: 'lemon', name: 'Lemon', disabled: false, group: 'Citrus'},
|
|
65
|
+
{value: 'blueberry', name: 'Blueberry', disabled: true, group: 'Berries'},
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
function scenarios(component: ListboxScenario['component']): ListboxScenario[] {
|
|
69
|
+
const prefix = component === 'Selector' ? 'single' : 'multiple';
|
|
70
|
+
const base = {
|
|
71
|
+
component,
|
|
72
|
+
options: flatOptions,
|
|
73
|
+
expectedOptions: flatExpected,
|
|
74
|
+
groups: [],
|
|
75
|
+
};
|
|
76
|
+
return [
|
|
77
|
+
{...base, id: `${prefix}-unset`, values: []},
|
|
78
|
+
{
|
|
79
|
+
...base,
|
|
80
|
+
id: `${prefix}-selected`,
|
|
81
|
+
values: component === 'Selector' ? ['apple'] : ['apple', 'banana'],
|
|
82
|
+
},
|
|
83
|
+
{...base, id: `${prefix}-disabled-selected`, values: ['cherry']},
|
|
84
|
+
{
|
|
85
|
+
...base,
|
|
86
|
+
id: `${prefix}-grouped`,
|
|
87
|
+
options: groupedOptions,
|
|
88
|
+
values: ['orange'],
|
|
89
|
+
expectedOptions: groupedExpected,
|
|
90
|
+
groups: ['Citrus', 'Berries'],
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
...base,
|
|
94
|
+
id: `${prefix}-filtered`,
|
|
95
|
+
options: groupedOptions,
|
|
96
|
+
values: ['orange'],
|
|
97
|
+
expectedOptions: [
|
|
98
|
+
{value: 'lemon', name: 'Lemon', disabled: false, group: 'Citrus'},
|
|
99
|
+
],
|
|
100
|
+
groups: ['Citrus'],
|
|
101
|
+
hasSearch: true,
|
|
102
|
+
query: 'lem',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
...base,
|
|
106
|
+
id: `${prefix}-custom-rtl`,
|
|
107
|
+
values: ['banana'],
|
|
108
|
+
customContent: true,
|
|
109
|
+
hiddenLabel: true,
|
|
110
|
+
direction: 'rtl',
|
|
111
|
+
expectedOptions: [
|
|
112
|
+
{value: 'apple', name: 'Apple details', disabled: false},
|
|
113
|
+
{value: 'banana', name: 'Banana details', disabled: false},
|
|
114
|
+
{value: 'cherry', name: 'Cherry details', disabled: true},
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
...base,
|
|
119
|
+
id: `${prefix}-sheet`,
|
|
120
|
+
values: ['apple'],
|
|
121
|
+
presentation: 'bottom-sheet',
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
...base,
|
|
125
|
+
id: `${prefix}-sheet-search`,
|
|
126
|
+
values: ['banana'],
|
|
127
|
+
presentation: 'bottom-sheet',
|
|
128
|
+
hasSearch: true,
|
|
129
|
+
},
|
|
130
|
+
{...base, id: `${prefix}-loading`, values: ['apple'], isLoading: true},
|
|
131
|
+
];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export const LISTBOX_SCENARIOS: ReadonlyArray<ListboxScenario> = [
|
|
135
|
+
...scenarios('Selector'),
|
|
136
|
+
...scenarios('MultiSelector'),
|
|
137
|
+
...(['none', 'partial', 'all'] as const).map(state => ({
|
|
138
|
+
id: `multiple-select-all-${state}`,
|
|
139
|
+
component: 'MultiSelector' as const,
|
|
140
|
+
options: flatOptions,
|
|
141
|
+
values:
|
|
142
|
+
state === 'none'
|
|
143
|
+
? []
|
|
144
|
+
: state === 'partial'
|
|
145
|
+
? ['apple']
|
|
146
|
+
: ['apple', 'banana'],
|
|
147
|
+
groups: [],
|
|
148
|
+
hasSelectAll: true,
|
|
149
|
+
expectedOptions: [
|
|
150
|
+
{
|
|
151
|
+
value: 'select-all',
|
|
152
|
+
name:
|
|
153
|
+
state === 'partial' ? 'Select all, partially selected' : 'Select all',
|
|
154
|
+
disabled: false,
|
|
155
|
+
selected: state === 'all',
|
|
156
|
+
},
|
|
157
|
+
...flatExpected,
|
|
158
|
+
],
|
|
159
|
+
})),
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
export interface ListboxBindingPart {
|
|
163
|
+
readonly role: 'listbox' | 'group' | 'option';
|
|
164
|
+
readonly name?: string;
|
|
165
|
+
readonly state: string;
|
|
166
|
+
readonly facts: ListboxStateFacts;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function listboxParts(
|
|
170
|
+
scenario: ListboxScenario,
|
|
171
|
+
): ReadonlyArray<ListboxBindingPart> {
|
|
172
|
+
const multiple = scenario.component === 'MultiSelector';
|
|
173
|
+
return [
|
|
174
|
+
{
|
|
175
|
+
role: 'listbox',
|
|
176
|
+
state: `${scenario.id}:listbox`,
|
|
177
|
+
facts: {
|
|
178
|
+
part: 'listbox',
|
|
179
|
+
multiple,
|
|
180
|
+
optionRelations: scenario.expectedOptions.map(
|
|
181
|
+
option => `option:${option.value}`,
|
|
182
|
+
),
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
...scenario.groups.map(name => ({
|
|
186
|
+
role: 'group' as const,
|
|
187
|
+
name,
|
|
188
|
+
state: `${scenario.id}:group:${name}`,
|
|
189
|
+
facts: {part: 'group' as const, multiple},
|
|
190
|
+
})),
|
|
191
|
+
...scenario.expectedOptions.map(option => ({
|
|
192
|
+
role: 'option' as const,
|
|
193
|
+
name: option.name,
|
|
194
|
+
state: `${scenario.id}:option:${option.value}`,
|
|
195
|
+
facts: {
|
|
196
|
+
part: 'option' as const,
|
|
197
|
+
multiple,
|
|
198
|
+
selected: option.selected ?? scenario.values.includes(option.value),
|
|
199
|
+
disabled: option.disabled,
|
|
200
|
+
ownerGroup: option.group == null ? undefined : `group:${option.group}`,
|
|
201
|
+
},
|
|
202
|
+
})),
|
|
203
|
+
];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export const LISTBOX_EXCLUSIONS = [
|
|
207
|
+
{
|
|
208
|
+
owner: 'Selector/MultiSelector trigger and search input',
|
|
209
|
+
reason:
|
|
210
|
+
'Combobox, field validation, required/read-only/disabled trigger state, and opening/closing are separate contracts. This slice observes active popup parts.',
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
owner: 'Selector/MultiSelector existing interaction suites',
|
|
214
|
+
reason:
|
|
215
|
+
'Keyboard selection, selection-follows-focus, disabled-option discovery, Home/End, typeahead, and callback policy are not newly adopted here.',
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
owner: 'Selector/MultiSelector empty-state and live-region owners',
|
|
219
|
+
reason:
|
|
220
|
+
'Zero-result representation and announcement timing require their own authority/evidence. No required option is invented for an empty listbox.',
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
owner: 'Typeahead, Tokenizer, PowerSearch, ComplexSelector, CommandPalette',
|
|
224
|
+
reason:
|
|
225
|
+
'Later bounded adopters; this first migration is limited to Selector and MultiSelector.',
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
owner: 'Component-specific suites',
|
|
229
|
+
reason:
|
|
230
|
+
'Public ID forwarding, callback payloads, forms, value formatting, sorting, required/error status, and styling stay local. Selector has no independent uncontrolled selected-value owner; an omitted value is the unset state.',
|
|
231
|
+
},
|
|
232
|
+
] as const;
|