@astryxdesign/core 0.7.3-canary.f94e962 → 0.7.3
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 +88 -0
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheet.js +8 -4
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetSwitcher.js +7 -0
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +15 -2
- package/dist/Calendar/Calendar.js +3 -2
- package/dist/Chat/ChatComposer.d.ts.map +1 -1
- package/dist/Chat/ChatComposer.js +25 -2
- package/dist/Chat/ChatSystemMessage.d.ts +4 -0
- package/dist/Chat/ChatSystemMessage.d.ts.map +1 -1
- package/dist/Chat/ChatSystemMessage.js +4 -2
- package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
- package/dist/CodeBlock/CodeBlock.js +11 -1
- package/dist/DateRangeInput/DateRangeInput.d.ts +5 -5
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +27 -18
- package/dist/DateTimeInput/DateTimeInput.d.ts +5 -5
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +25 -27
- package/dist/DateTimeInput/NativeDateSegment.d.ts +3 -2
- package/dist/DateTimeInput/NativeDateSegment.d.ts.map +1 -1
- package/dist/DateTimeInput/NativeDateSegment.js +5 -5
- package/dist/DateTimeInput/NativeTimeSegment.d.ts +3 -2
- package/dist/DateTimeInput/NativeTimeSegment.d.ts.map +1 -1
- package/dist/DateTimeInput/NativeTimeSegment.js +5 -5
- package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
- package/dist/DateTimeInput/TouchDateTimeField.js +12 -9
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +9 -4
- package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts +9 -5
- package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuRadioGroup.js +2 -0
- package/dist/Markdown/Markdown.d.ts.map +1 -1
- package/dist/Markdown/Markdown.js +48 -15
- package/dist/MultiSelector/MultiSelector.d.ts +4 -4
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.d.ts +2 -0
- package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.d.ts.map +1 -0
- package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.js +48 -0
- package/dist/MultiSelector/MultiSelector.js +25 -18
- package/dist/Pagination/Pagination.d.ts.map +1 -1
- package/dist/Pagination/Pagination.js +5 -2
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +13 -3
- package/dist/TimeInput/TimeInput.d.ts +5 -5
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +37 -20
- package/dist/TimeInput/TouchTimeField.d.ts.map +1 -1
- package/dist/TimeInput/TouchTimeField.js +17 -6
- package/dist/Timestamp/Timestamp.d.ts.map +1 -1
- package/dist/Timestamp/Timestamp.js +41 -8
- package/dist/TreeList/TreeListItem.d.ts.map +1 -1
- package/dist/TreeList/TreeListItem.js +8 -3
- package/dist/hooks/useGridFocus.d.ts.map +1 -1
- package/dist/hooks/useGridFocus.js +10 -1
- package/package.json +4 -4
- package/scripts/agent-doc-state.mjs +24 -2
- package/src/BottomSheet/BottomSheet.spec.md +26 -53
- package/src/BottomSheet/BottomSheet.test.tsx +29 -0
- package/src/BottomSheet/BottomSheet.tsx +8 -4
- package/src/BottomSheet/BottomSheetSwitcher.spec.md +45 -48
- package/src/BottomSheet/BottomSheetSwitcher.test.tsx +26 -0
- package/src/BottomSheet/BottomSheetSwitcher.tsx +6 -0
- package/src/Button/Button.test.tsx +13 -2
- package/src/Button/Button.tsx +23 -2
- package/src/Button/__tests__/Button.a11y.states.ts +4 -3
- package/src/Calendar/Calendar.test.tsx +68 -24
- package/src/Calendar/Calendar.tsx +2 -1
- package/src/Carousel/Carousel.spec.md +146 -0
- package/src/Chat/ChatComposer.spec.md +26 -26
- package/src/Chat/ChatComposer.test.tsx +58 -2
- package/src/Chat/ChatComposer.tsx +25 -1
- package/src/Chat/ChatSystemMessage.doc.mjs +1 -1
- package/src/Chat/ChatSystemMessage.test.tsx +25 -3
- package/src/Chat/ChatSystemMessage.tsx +6 -4
- package/src/Chat/__tests__/ChatSystemMessage.a11y.chromium.spec.ts +8 -9
- package/src/CheckboxList/__tests__/CheckboxList.a11y.chromium.spec.ts +7 -6
- package/src/CodeBlock/CodeBlock.highlightStyles.test.tsx +87 -0
- package/src/CodeBlock/CodeBlock.tsx +11 -1
- package/src/DateRangeInput/DateRangeInput.doc.mjs +6 -5
- package/src/DateRangeInput/DateRangeInput.test.tsx +82 -16
- package/src/DateRangeInput/DateRangeInput.tsx +40 -20
- package/src/DateTimeInput/DateTimeInput.doc.mjs +9 -8
- package/src/DateTimeInput/DateTimeInput.test.tsx +126 -11
- package/src/DateTimeInput/DateTimeInput.tsx +42 -26
- package/src/DateTimeInput/DateTimeInputTouch.test.tsx +24 -0
- package/src/DateTimeInput/NativeDateSegment.tsx +9 -6
- package/src/DateTimeInput/NativePickerSegments.test.tsx +31 -0
- package/src/DateTimeInput/NativeTimeSegment.tsx +9 -6
- package/src/DateTimeInput/TouchDateTimeField.tsx +22 -7
- package/src/Dialog/Dialog.test.tsx +39 -0
- package/src/Dialog/Dialog.tsx +9 -4
- package/src/DropdownMenu/DropdownMenuItem.doc.mjs +66 -7
- package/src/DropdownMenu/DropdownMenuItem.spec.md +209 -0
- package/src/DropdownMenu/DropdownMenuRadioGroup.doc.mjs +62 -7
- package/src/DropdownMenu/DropdownMenuRadioGroup.spec.md +190 -0
- package/src/DropdownMenu/DropdownMenuRadioGroup.tsx +10 -4
- package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +93 -0
- package/src/DropdownMenu/__tests__/DropdownMenuItemAudit.a11y.chromium.spec.ts +366 -0
- package/src/DropdownMenu/__tests__/DropdownMenuRadioGroupAudit.a11y.chromium.spec.ts +327 -0
- package/src/FieldStatus/FieldStatus.spec.md +4 -4
- package/src/FieldStatus/__tests__/StatusMessage.a11y.renders.tsx +0 -18
- package/src/FieldStatus/__tests__/StatusMessage.a11y.states.ts +3 -18
- package/src/FieldStatus/__tests__/StatusMessage.a11y.test.tsx +1 -2
- package/src/Markdown/Markdown.helpers.perf.test.ts +25 -19
- package/src/Markdown/Markdown.rerender.test.tsx +143 -0
- package/src/Markdown/Markdown.tsx +89 -35
- package/src/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.ts +71 -0
- package/src/MultiSelector/MultiSelector.doc.mjs +5 -5
- package/src/MultiSelector/MultiSelector.focusableDisabled.test.tsx +121 -0
- package/src/MultiSelector/MultiSelector.test.tsx +49 -7
- package/src/MultiSelector/MultiSelector.tsx +53 -29
- package/src/Pagination/Pagination.test.tsx +5 -1
- package/src/Pagination/Pagination.tsx +3 -1
- package/src/Switch/Switch.test.tsx +56 -0
- package/src/Switch/Switch.tsx +13 -2
- package/src/Table/plugins/columnResize/useTableColumnResize.spec.md +11 -11
- package/src/Table/plugins/rowStatus/useTableRowStatus.spec.md +4 -3
- package/src/TimeInput/NativeTimeInput.test.tsx +25 -1
- package/src/TimeInput/Presentation.test.tsx +23 -0
- package/src/TimeInput/TimeInput.doc.mjs +8 -7
- package/src/TimeInput/TimeInput.test.tsx +119 -7
- package/src/TimeInput/TimeInput.tsx +51 -23
- package/src/TimeInput/TouchTimeField.tsx +27 -5
- package/src/Timestamp/Timestamp.doc.mjs +1 -1
- package/src/Timestamp/Timestamp.test.tsx +168 -0
- package/src/Timestamp/Timestamp.tsx +62 -11
- package/src/TreeList/TreeList.test.tsx +28 -0
- package/src/TreeList/TreeListItem.tsx +6 -2
- package/src/__tests__/DisabledTriggerFocus.a11y.chromium.spec.ts +86 -0
- package/src/hooks/useGridFocus.ts +16 -1
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file MultiSelector.focusableDisabled.test.tsx
|
|
5
|
+
* @input Uses vitest, @testing-library/react, StyleX, MultiSelector
|
|
6
|
+
* @output Conformance tests for spec:AST-085 FR1–FR3 and FR6 on MultiSelector;
|
|
7
|
+
* FR4 needs a real engine (MultiSelector.disabledFocus.a11y.chromium.spec.ts)
|
|
8
|
+
* @position Testing; validates the disabled trigger of MultiSelector.tsx
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {describe, it, expect, vi} from 'vitest';
|
|
12
|
+
import {render, screen} from '@testing-library/react';
|
|
13
|
+
import userEvent from '@testing-library/user-event';
|
|
14
|
+
import * as stylex from '@stylexjs/stylex';
|
|
15
|
+
import {colorVars} from '../theme/tokens.stylex';
|
|
16
|
+
import {MultiSelector} from './MultiSelector';
|
|
17
|
+
|
|
18
|
+
type Handler = () => void;
|
|
19
|
+
function Harness(props: {
|
|
20
|
+
isDisabled: boolean;
|
|
21
|
+
variant?: 'input' | 'secondary' | 'ghost';
|
|
22
|
+
onClick?: Handler;
|
|
23
|
+
onKeyDown?: Handler;
|
|
24
|
+
onMouseDown?: Handler;
|
|
25
|
+
onDoubleClick?: Handler;
|
|
26
|
+
}) {
|
|
27
|
+
return (
|
|
28
|
+
<form aria-label="Form">
|
|
29
|
+
<button type="button">Before</button>
|
|
30
|
+
<MultiSelector
|
|
31
|
+
label="Fruit"
|
|
32
|
+
htmlName="fruit"
|
|
33
|
+
options={['Apple', 'Banana']}
|
|
34
|
+
value={['Banana']}
|
|
35
|
+
onChange={() => {}}
|
|
36
|
+
{...props}
|
|
37
|
+
/>
|
|
38
|
+
<button type="button">After</button>
|
|
39
|
+
</form>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const trigger = () => screen.getByRole('combobox', {name: 'Fruit'});
|
|
44
|
+
|
|
45
|
+
describe('MultiSelector disabled (spec:AST-085)', () => {
|
|
46
|
+
it('FR1: renders no native disabled and keeps its tab stop', async () => {
|
|
47
|
+
const user = userEvent.setup();
|
|
48
|
+
const {container} = render(<Harness isDisabled />);
|
|
49
|
+
expect(container.querySelector('[disabled]')).toBeNull();
|
|
50
|
+
expect(trigger()).toHaveAttribute('aria-disabled', 'true');
|
|
51
|
+
await user.click(screen.getByRole('button', {name: 'Before'}));
|
|
52
|
+
await user.tab();
|
|
53
|
+
expect(trigger()).toHaveFocus();
|
|
54
|
+
await user.tab();
|
|
55
|
+
expect(screen.getByRole('button', {name: 'After'})).toHaveFocus();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('FR2: no press or key opens it or reaches a caller or ancestor handler', async () => {
|
|
59
|
+
const user = userEvent.setup();
|
|
60
|
+
const handlers = {
|
|
61
|
+
onClick: vi.fn(),
|
|
62
|
+
onKeyDown: vi.fn(),
|
|
63
|
+
onMouseDown: vi.fn(),
|
|
64
|
+
onDoubleClick: vi.fn(),
|
|
65
|
+
};
|
|
66
|
+
const onAncestorClick = vi.fn();
|
|
67
|
+
render(
|
|
68
|
+
<div onClick={onAncestorClick}>
|
|
69
|
+
<Harness isDisabled {...handlers} />
|
|
70
|
+
</div>,
|
|
71
|
+
);
|
|
72
|
+
await user.dblClick(trigger());
|
|
73
|
+
trigger().focus();
|
|
74
|
+
for (const key of ['{Enter}', ' ', '{ArrowDown}', 'b']) {
|
|
75
|
+
await user.keyboard(key);
|
|
76
|
+
}
|
|
77
|
+
expect(trigger()).toHaveAttribute('aria-expanded', 'false');
|
|
78
|
+
expect(screen.queryByRole('listbox')).toBeNull();
|
|
79
|
+
for (const handler of [...Object.values(handlers), onAncestorClick]) {
|
|
80
|
+
expect(handler).not.toHaveBeenCalled();
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('FR3: submits no value while disabled', () => {
|
|
85
|
+
const {rerender} = render(<Harness isDisabled={false} />);
|
|
86
|
+
const form = () =>
|
|
87
|
+
screen.getByRole<HTMLFormElement>('form', {name: 'Form'});
|
|
88
|
+
expect(new FormData(form()).getAll('fruit')).toEqual(['Banana']);
|
|
89
|
+
rerender(<Harness isDisabled />);
|
|
90
|
+
expect(new FormData(form()).getAll('fruit')).toEqual([]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// jsdom cannot match `:focus-visible`: compare the ring's StyleX class hash.
|
|
94
|
+
const RING = stylex
|
|
95
|
+
.props(
|
|
96
|
+
stylex.create({
|
|
97
|
+
ring: {
|
|
98
|
+
boxShadow: {
|
|
99
|
+
default: null,
|
|
100
|
+
':has(> [aria-disabled="true"]:focus-visible)': `inset 0px 0px 0px 2px ${colorVars['--color-accent-muted']}`,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
}).ring,
|
|
104
|
+
)
|
|
105
|
+
.className!.split(' ')
|
|
106
|
+
.pop()!;
|
|
107
|
+
|
|
108
|
+
it.each([
|
|
109
|
+
['input', true],
|
|
110
|
+
['secondary', false],
|
|
111
|
+
['ghost', false],
|
|
112
|
+
] as const)(
|
|
113
|
+
'FR6: a disabled %s trigger carries its own ring: %s',
|
|
114
|
+
(variant, ring) => {
|
|
115
|
+
render(<Harness isDisabled variant={variant} />);
|
|
116
|
+
const wrapper = trigger().parentElement!;
|
|
117
|
+
expect(wrapper.classList.contains(RING)).toBe(ring);
|
|
118
|
+
expect(getComputedStyle(wrapper).cursor).toBe('default');
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
});
|
|
@@ -35,7 +35,7 @@ import {generateThemeCSS} from '../theme/generateThemeRules';
|
|
|
35
35
|
import {selectorPresentationStyles} from '../Selector/selectorPresentation.stylex';
|
|
36
36
|
import {groupStyles} from '../InputGroup/groupStyles';
|
|
37
37
|
import {InputGroup, InputGroupText} from '../InputGroup';
|
|
38
|
-
import {focusVars} from '../theme/tokens.stylex';
|
|
38
|
+
import {colorVars, focusVars} from '../theme/tokens.stylex';
|
|
39
39
|
|
|
40
40
|
function generateThemeTestCSS(theme: Parameters<typeof generateThemeCSS>[0]) {
|
|
41
41
|
const {prose, component} = generateThemeCSS(theme);
|
|
@@ -210,6 +210,45 @@ describe('MultiSelector', () => {
|
|
|
210
210
|
);
|
|
211
211
|
});
|
|
212
212
|
|
|
213
|
+
it('shows no disabled focus ring after a pointer pick (spec:AST-085 FR6)', async () => {
|
|
214
|
+
const field = (isDisabled: boolean) => (
|
|
215
|
+
<MultiSelector
|
|
216
|
+
label="Fruit"
|
|
217
|
+
options={defaultOptions}
|
|
218
|
+
value={[]}
|
|
219
|
+
onChange={() => {}}
|
|
220
|
+
presentation="bottom-sheet"
|
|
221
|
+
isDisabled={isDisabled}
|
|
222
|
+
/>
|
|
223
|
+
);
|
|
224
|
+
const {rerender} = render(field(false));
|
|
225
|
+
const trigger = screen.getByRole('combobox');
|
|
226
|
+
fireEvent.pointerDown(trigger, {pointerType: 'touch'});
|
|
227
|
+
fireEvent.click(trigger, {detail: 1});
|
|
228
|
+
const dialog = await screen.findByRole('dialog', {name: 'Fruit'});
|
|
229
|
+
fireEvent.pointerDown(dialog, {pointerType: 'touch'});
|
|
230
|
+
fireEvent.click(dialog, {detail: 1});
|
|
231
|
+
rerender(field(true));
|
|
232
|
+
const ring = stylex
|
|
233
|
+
.props(
|
|
234
|
+
stylex.create({
|
|
235
|
+
ring: {
|
|
236
|
+
boxShadow: {
|
|
237
|
+
default: null,
|
|
238
|
+
':has(> [aria-disabled="true"]:focus-visible)': `inset 0px 0px 0px 2px ${colorVars['--color-accent-muted']}`,
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
}).ring,
|
|
242
|
+
)
|
|
243
|
+
.className!.split(' ')
|
|
244
|
+
.pop()!;
|
|
245
|
+
const wrapper = screen.getByRole('combobox').parentElement!;
|
|
246
|
+
expect(wrapper).toHaveClass(
|
|
247
|
+
stylex.props(selectorPresentationStyles.pointerRestoredFocus).className!,
|
|
248
|
+
);
|
|
249
|
+
expect(wrapper).not.toHaveClass(ring);
|
|
250
|
+
});
|
|
251
|
+
|
|
213
252
|
it('renders with label', () => {
|
|
214
253
|
render(
|
|
215
254
|
<MultiSelector
|
|
@@ -515,7 +554,9 @@ describe('MultiSelector', () => {
|
|
|
515
554
|
isDisabled
|
|
516
555
|
/>,
|
|
517
556
|
);
|
|
518
|
-
|
|
557
|
+
const trigger = screen.getByRole('combobox');
|
|
558
|
+
expect(trigger).not.toHaveAttribute('disabled');
|
|
559
|
+
expect(trigger).toHaveAttribute('aria-disabled', 'true');
|
|
519
560
|
});
|
|
520
561
|
|
|
521
562
|
it('has correct ARIA attributes', () => {
|
|
@@ -1602,7 +1643,7 @@ describe('MultiSelector', () => {
|
|
|
1602
1643
|
expect(screen.getByRole('combobox')).toHaveFocus();
|
|
1603
1644
|
});
|
|
1604
1645
|
|
|
1605
|
-
it('trigger
|
|
1646
|
+
it('trigger keeps its tab stop when disabled (spec:AST-085 FR1)', () => {
|
|
1606
1647
|
render(
|
|
1607
1648
|
<MultiSelector
|
|
1608
1649
|
label="Fruit"
|
|
@@ -1612,7 +1653,7 @@ describe('MultiSelector', () => {
|
|
|
1612
1653
|
isDisabled
|
|
1613
1654
|
/>,
|
|
1614
1655
|
);
|
|
1615
|
-
expect(screen.getByRole('combobox')).toHaveAttribute('tabIndex', '
|
|
1656
|
+
expect(screen.getByRole('combobox')).toHaveAttribute('tabIndex', '0');
|
|
1616
1657
|
});
|
|
1617
1658
|
|
|
1618
1659
|
it('opens the listbox with ArrowDown from a focused trigger', async () => {
|
|
@@ -2076,7 +2117,7 @@ describe('MultiSelector', () => {
|
|
|
2076
2117
|
expect(onChange).not.toHaveBeenCalled();
|
|
2077
2118
|
});
|
|
2078
2119
|
|
|
2079
|
-
it('
|
|
2120
|
+
it('stays focusable when disabled without a reason (spec:AST-085 FR1)', () => {
|
|
2080
2121
|
render(
|
|
2081
2122
|
<MultiSelector
|
|
2082
2123
|
label="Fruit"
|
|
@@ -2087,8 +2128,9 @@ describe('MultiSelector', () => {
|
|
|
2087
2128
|
/>,
|
|
2088
2129
|
);
|
|
2089
2130
|
const trigger = screen.getByRole('combobox');
|
|
2090
|
-
expect(trigger).
|
|
2091
|
-
expect(trigger).toHaveAttribute('
|
|
2131
|
+
expect(trigger).not.toHaveAttribute('disabled');
|
|
2132
|
+
expect(trigger).toHaveAttribute('aria-disabled', 'true');
|
|
2133
|
+
expect(trigger).toHaveAttribute('tabIndex', '0');
|
|
2092
2134
|
});
|
|
2093
2135
|
});
|
|
2094
2136
|
describe('isReadOnly', () => {
|
|
@@ -271,6 +271,21 @@ const styles = stylex.create({
|
|
|
271
271
|
'[data-astryx-press="on"]': 'scale(0.98)',
|
|
272
272
|
},
|
|
273
273
|
},
|
|
274
|
+
// A disabled input-variant trigger keeps its tab stop (`spec:AST-085` FR1),
|
|
275
|
+
// and the shared disabled wrapper style drops the field focus ring, so the
|
|
276
|
+
// trigger shows keyboard focus here instead (FR6). It keys on the trigger
|
|
277
|
+
// itself, a direct child, so a focused status button keeps its own outline.
|
|
278
|
+
triggerDisabledFocus: {
|
|
279
|
+
borderColor: {
|
|
280
|
+
default: colorVars['--color-border-emphasized'],
|
|
281
|
+
':has(> [aria-disabled="true"]:focus-visible)':
|
|
282
|
+
colorVars['--color-accent'],
|
|
283
|
+
},
|
|
284
|
+
boxShadow: {
|
|
285
|
+
default: 'none',
|
|
286
|
+
':has(> [aria-disabled="true"]:focus-visible)': `inset 0px 0px 0px 2px ${colorVars['--color-accent-muted']}`,
|
|
287
|
+
},
|
|
288
|
+
},
|
|
274
289
|
triggerGhostDisabled: {
|
|
275
290
|
backgroundImage: 'none',
|
|
276
291
|
transform: {
|
|
@@ -688,8 +703,8 @@ export interface MultiSelectorProps<
|
|
|
688
703
|
* The selected values stay visible, focusable, and included in form
|
|
689
704
|
* submission, and retain their combobox identity with `aria-readonly`. The
|
|
690
705
|
* selection surface and editing affordances are removed.
|
|
691
|
-
* Unlike `isDisabled`, a read-only selector is not dimmed and
|
|
692
|
-
*
|
|
706
|
+
* Unlike `isDisabled`, a read-only selector is not dimmed and keeps its
|
|
707
|
+
* value in form submission. `isDisabled` takes precedence when both are set.
|
|
693
708
|
* @default false
|
|
694
709
|
*/
|
|
695
710
|
isReadOnly?: boolean;
|
|
@@ -701,8 +716,8 @@ export interface MultiSelectorProps<
|
|
|
701
716
|
* so the reason is discoverable by keyboard and assistive technology.
|
|
702
717
|
* Activation stays blocked.
|
|
703
718
|
*
|
|
704
|
-
* Use this instead of wrapping a disabled selector in `Tooltip
|
|
705
|
-
*
|
|
719
|
+
* Use this instead of wrapping a disabled selector in `Tooltip`, so the
|
|
720
|
+
* reason is attached to the trigger itself.
|
|
706
721
|
*
|
|
707
722
|
* @example
|
|
708
723
|
* ```
|
|
@@ -2409,8 +2424,18 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
|
|
|
2409
2424
|
});
|
|
2410
2425
|
}
|
|
2411
2426
|
|
|
2427
|
+
// A disabled trigger stays focusable but, like a natively disabled button,
|
|
2428
|
+
// runs no caller event handler other than focus and blur, and a press on it
|
|
2429
|
+
// reaches no handler at all (`spec:AST-085` FR2).
|
|
2430
|
+
const callerProps = isDisabled
|
|
2431
|
+
? Object.fromEntries(
|
|
2432
|
+
Object.entries(rest).filter(
|
|
2433
|
+
([key]) => !/^on[A-Z]/.test(key) || /^on(Focus|Blur)/.test(key),
|
|
2434
|
+
),
|
|
2435
|
+
)
|
|
2436
|
+
: rest;
|
|
2412
2437
|
const triggerSharedProps: React.HTMLAttributes<HTMLElement> = {
|
|
2413
|
-
...
|
|
2438
|
+
...callerProps,
|
|
2414
2439
|
id: triggerId,
|
|
2415
2440
|
// A caller's description is kept alongside the field's own.
|
|
2416
2441
|
'aria-describedby':
|
|
@@ -2423,13 +2448,20 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
|
|
|
2423
2448
|
// Newly forwarded handlers compose component first: the built-in toggle
|
|
2424
2449
|
// and keyboard model always run. `onFocus` keeps its existing caller-first
|
|
2425
2450
|
// order.
|
|
2426
|
-
onKeyDown:
|
|
2451
|
+
onKeyDown: isDisabled
|
|
2452
|
+
? onKeyDown
|
|
2453
|
+
: composeEventHandlers(onKeyDown, onKeyDownProp),
|
|
2427
2454
|
onFocus: event => {
|
|
2428
2455
|
onFocus?.(event);
|
|
2429
2456
|
surface.onTriggerFocus(event);
|
|
2430
2457
|
},
|
|
2431
|
-
onClick:
|
|
2432
|
-
|
|
2458
|
+
onClick: isDisabled
|
|
2459
|
+
? event => {
|
|
2460
|
+
event.preventDefault();
|
|
2461
|
+
event.stopPropagation();
|
|
2462
|
+
}
|
|
2463
|
+
: composeEventHandlers(handleTriggerButtonClick, onClick),
|
|
2464
|
+
tabIndex: 0,
|
|
2433
2465
|
...stylex.props(
|
|
2434
2466
|
styles.trigger,
|
|
2435
2467
|
isEffectivelyReadOnly && styles.triggerReadOnly,
|
|
@@ -2496,14 +2528,9 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
|
|
|
2496
2528
|
<>
|
|
2497
2529
|
{renderTrigger(triggerProps)}
|
|
2498
2530
|
{htmlName != null &&
|
|
2531
|
+
!isDisabled &&
|
|
2499
2532
|
value.map(v => (
|
|
2500
|
-
<input
|
|
2501
|
-
key={v}
|
|
2502
|
-
type="hidden"
|
|
2503
|
-
name={htmlName}
|
|
2504
|
-
value={v}
|
|
2505
|
-
disabled={isDisabled}
|
|
2506
|
-
/>
|
|
2533
|
+
<input key={v} type="hidden" name={htmlName} value={v} />
|
|
2507
2534
|
))}
|
|
2508
2535
|
{selectionSurface}
|
|
2509
2536
|
</>
|
|
@@ -2541,6 +2568,10 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
|
|
|
2541
2568
|
surface.isTriggerFocusRingSuppressed &&
|
|
2542
2569
|
selectorPresentationStyles.pointerRestoredFocus,
|
|
2543
2570
|
isDisabled && inputWrapperStyles.disabled,
|
|
2571
|
+
variant === 'input' &&
|
|
2572
|
+
isDisabled &&
|
|
2573
|
+
!surface.isTriggerFocusRingSuppressed &&
|
|
2574
|
+
styles.triggerDisabledFocus,
|
|
2544
2575
|
isEffectivelyReadOnly && styles.triggerReadOnly,
|
|
2545
2576
|
variant !== 'input' && isDisabled && styles.triggerGhostDisabled,
|
|
2546
2577
|
variant !== 'input' &&
|
|
@@ -2598,26 +2629,19 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
|
|
|
2598
2629
|
? activeDescendantId
|
|
2599
2630
|
: undefined
|
|
2600
2631
|
}
|
|
2601
|
-
//
|
|
2602
|
-
// aria-disabled
|
|
2603
|
-
//
|
|
2604
|
-
disabled={isDisabled
|
|
2605
|
-
aria-disabled={showsDisabledMessage ? 'true' : undefined}>
|
|
2632
|
+
// A disabled trigger keeps its tab stop and says so through
|
|
2633
|
+
// aria-disabled (`spec:AST-085` FR1); the isDisabled guards in
|
|
2634
|
+
// useMultiCombobox block every way of opening it.
|
|
2635
|
+
aria-disabled={isDisabled ? 'true' : undefined}>
|
|
2606
2636
|
<span {...stylex.props(styles.triggerContent)}>
|
|
2607
2637
|
{renderTriggerContent()}
|
|
2608
2638
|
</span>
|
|
2609
2639
|
</button>
|
|
2640
|
+
{/* A disabled selector submits no value (`spec:AST-085` FR3). */}
|
|
2610
2641
|
{htmlName != null &&
|
|
2642
|
+
!isDisabled &&
|
|
2611
2643
|
value.map(v => (
|
|
2612
|
-
<input
|
|
2613
|
-
key={v}
|
|
2614
|
-
type="hidden"
|
|
2615
|
-
name={htmlName}
|
|
2616
|
-
value={v}
|
|
2617
|
-
// Disabled native controls are excluded from form submission;
|
|
2618
|
-
// mirror that for the hidden carriers.
|
|
2619
|
-
disabled={isDisabled}
|
|
2620
|
-
/>
|
|
2644
|
+
<input key={v} type="hidden" name={htmlName} value={v} />
|
|
2621
2645
|
))}
|
|
2622
2646
|
{isBusy && <Spinner size="sm" />}
|
|
2623
2647
|
{hasClear &&
|
|
@@ -763,9 +763,13 @@ describe('Pagination', () => {
|
|
|
763
763
|
/>,
|
|
764
764
|
);
|
|
765
765
|
const dot = screen.getByRole('button', {name: 'Go to page 2'});
|
|
766
|
-
|
|
766
|
+
// spec:AST-085 FR1: the active dot keeps its tab stop, exposed as disabled.
|
|
767
|
+
expect(dot).not.toHaveAttribute('disabled');
|
|
768
|
+
expect(dot).toHaveAttribute('aria-disabled', 'true');
|
|
767
769
|
dot.focus();
|
|
770
|
+
expect(dot).toHaveFocus();
|
|
768
771
|
await user.keyboard('{ArrowRight}');
|
|
772
|
+
await user.keyboard('{Enter} ');
|
|
769
773
|
expect(onChange).not.toHaveBeenCalled();
|
|
770
774
|
});
|
|
771
775
|
});
|
|
@@ -706,7 +706,9 @@ export function Pagination({
|
|
|
706
706
|
// needs to focus the dot, which some browsers (Safari) skip for
|
|
707
707
|
// buttons, so focus it explicitly.
|
|
708
708
|
onClick={e => e.currentTarget.focus()}
|
|
709
|
-
|
|
709
|
+
// Disabled dots stay focusable (spec:AST-085 FR1); the page
|
|
710
|
+
// only follows focus while enabled (handleDotsFocus).
|
|
711
|
+
aria-disabled={isDisabled || undefined}
|
|
710
712
|
{...mergeProps(
|
|
711
713
|
themeProps('pagination-dot', {
|
|
712
714
|
active: isActive ? 'active' : null,
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* SYNC: When Switch.tsx changes, update tests to match new behavior
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
+
import {StrictMode} from 'react';
|
|
25
26
|
import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest';
|
|
26
27
|
import {render, screen, fireEvent, waitFor} from '@testing-library/react';
|
|
27
28
|
import userEvent from '@testing-library/user-event';
|
|
@@ -417,6 +418,61 @@ describe('Switch', () => {
|
|
|
417
418
|
});
|
|
418
419
|
});
|
|
419
420
|
|
|
421
|
+
it('announces one busy start once under StrictMode, from the switch alone', async () => {
|
|
422
|
+
// Count every message the shared polite region is given. Each announce
|
|
423
|
+
// writes its text in a later frame, so two announces add it twice.
|
|
424
|
+
const written: string[] = [];
|
|
425
|
+
const observer = new MutationObserver(records => {
|
|
426
|
+
for (const record of records) {
|
|
427
|
+
const target = record.target as Element;
|
|
428
|
+
if (target.getAttribute?.('data-astryx-live-region') !== 'polite') {
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
for (const node of record.addedNodes) {
|
|
432
|
+
if (node.textContent) {written.push(node.textContent);}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
observer.observe(document.body, {childList: true, subtree: true});
|
|
437
|
+
|
|
438
|
+
const {container} = render(
|
|
439
|
+
<StrictMode>
|
|
440
|
+
<Switch
|
|
441
|
+
label="Sync photos"
|
|
442
|
+
value={false}
|
|
443
|
+
onChange={() => {}}
|
|
444
|
+
isLoading
|
|
445
|
+
/>
|
|
446
|
+
</StrictMode>,
|
|
447
|
+
);
|
|
448
|
+
await waitFor(() => {
|
|
449
|
+
expect(written).toContain('Loading');
|
|
450
|
+
});
|
|
451
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
452
|
+
observer.disconnect();
|
|
453
|
+
|
|
454
|
+
expect(written.filter(text => text === 'Loading')).toHaveLength(1);
|
|
455
|
+
// The thumb's spinner is a silent indicator: no live role reaches
|
|
456
|
+
// assistive technology from inside the switch.
|
|
457
|
+
for (const status of container.querySelectorAll('[role="status"]')) {
|
|
458
|
+
expect(status.closest('[aria-hidden="true"]')).not.toBeNull();
|
|
459
|
+
}
|
|
460
|
+
expect(container.querySelector('[aria-live]')).toBeNull();
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it('announces again when the switch becomes busy again after settling', async () => {
|
|
464
|
+
const polite = () =>
|
|
465
|
+
document.querySelector('[data-astryx-live-region="polite"]');
|
|
466
|
+
const props = {label: 'Sync photos', value: false, onChange: () => {}};
|
|
467
|
+
const {rerender} = render(<Switch {...props} isLoading />);
|
|
468
|
+
await waitFor(() => expect(polite()).toHaveTextContent('Loading'));
|
|
469
|
+
|
|
470
|
+
rerender(<Switch {...props} />);
|
|
471
|
+
polite()!.textContent = '';
|
|
472
|
+
rerender(<Switch {...props} isLoading />);
|
|
473
|
+
await waitFor(() => expect(polite()).toHaveTextContent('Loading'));
|
|
474
|
+
});
|
|
475
|
+
|
|
420
476
|
it('localizes the loading announcement through the i18n catalog', async () => {
|
|
421
477
|
render(
|
|
422
478
|
<InternationalizationProvider
|
package/src/Switch/Switch.tsx
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
useOptimistic,
|
|
22
22
|
useTransition,
|
|
23
23
|
useEffect,
|
|
24
|
+
useRef,
|
|
24
25
|
type ChangeEvent,
|
|
25
26
|
type FocusEvent,
|
|
26
27
|
type ReactNode,
|
|
@@ -587,10 +588,20 @@ export function Switch({
|
|
|
587
588
|
const isBusy = isLoading || optimisticValue !== value;
|
|
588
589
|
|
|
589
590
|
const announce = useAnnounce();
|
|
591
|
+
// One announcement per busy start (spec:AST-086 FR6). The ref outlives
|
|
592
|
+
// StrictMode's effect replay and rerenders, so only a new busy start (busy
|
|
593
|
+
// again after settling) speaks again.
|
|
594
|
+
const announcedBusyRef = useRef(false);
|
|
590
595
|
useEffect(() => {
|
|
591
|
-
if (isBusy) {
|
|
592
|
-
|
|
596
|
+
if (!isBusy) {
|
|
597
|
+
announcedBusyRef.current = false;
|
|
598
|
+
return;
|
|
593
599
|
}
|
|
600
|
+
if (announcedBusyRef.current) {
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
announcedBusyRef.current = true;
|
|
604
|
+
announce(t('@astryx.switch.loading'));
|
|
594
605
|
}, [announce, isBusy, t]);
|
|
595
606
|
|
|
596
607
|
const isOn = optimisticValue === true;
|
|
@@ -227,17 +227,17 @@ without a target of its own.
|
|
|
227
227
|
|
|
228
228
|
## Verification map
|
|
229
229
|
|
|
230
|
-
| Contract | Verification
|
|
231
|
-
| ------------------ |
|
|
232
|
-
| FR1, FR6 | `useTableColumnResize.test.tsx` boundary-presence cases plus hover-capability and coarse-primary cases
|
|
233
|
-
| FR2 | Browser measurement of scroll-region vertical overflow with the plugin enabled
|
|
234
|
-
| FR3-FR5, FR9, ORD1 | New focused tests for resting, header-hovered, pointer-on-boundary, focused, and dragging states
|
|
235
|
-
| FR10, ORD0 | Scroll-gate test driving the scroll region with a stationary pointer over the header
|
|
236
|
-
| FR7 | Snapshot or assertion that header hover changes no other header or row treatment
|
|
237
|
-
| FR8, AR1-AR3, AR6 | Existing keyboard and ARIA suites
|
|
238
|
-
| PR1, PR2 | Render-count assertion around header pointer enter/leave and during a scroll; listener and observer inspection
|
|
239
|
-
| Plugin composition | Test mounting the resize and sticky-columns plugins together in both insertion orders
|
|
240
|
-
| Record shape | `scripts/check-knowledge.mjs`
|
|
230
|
+
| Contract | Verification | Representative states | Mutation or failure expectation |
|
|
231
|
+
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
232
|
+
| FR1, FR6 | `useTableColumnResize.test.tsx` boundary-presence cases plus hover-capability and coarse-primary cases | Resizable, opted-out, and generated columns; hover-capable, coarse-primary with keyboard, and coarse-only inputs | A boundary appears on a non-resizable column, disappears from a resizable one, the hover reveal fires where no pointer can hover, or the control is absent where an input can operate it. |
|
|
233
|
+
| FR2 | Browser measurement of scroll-region vertical overflow with the plugin enabled | Short and tall tables, wrapped and single-line headers, dense and default | The indicator ends below the last row or the scroll region gains vertical scroll that the same table lacks without resize. |
|
|
234
|
+
| FR3-FR5, FR9, ORD1 | New focused tests for resting, header-hovered, pointer-on-boundary, focused, and dragging states | Rest, header hover, hover plus pointer on one boundary, keyboard focus with no pointer in the row, drag | A resting header draws boundaries, header hover misses a boundary, the pointed/focused boundary fails to outrank the hint, or focus alone draws a neighbor. |
|
|
235
|
+
| FR10, ORD0 | Scroll-gate test driving the scroll region with a stationary pointer over the header | Pointer resting over a header while the region scrolls and after it settles | Boundaries reveal mid-scroll, an existing hint survives the scroll, or disclosure fails to resume once scrolling stops. |
|
|
236
|
+
| FR7 | Snapshot or assertion that header hover changes no other header or row treatment | Header hover over a sortable and a selectable header | Header hover changes background, label, cursor, width, or another control's state. |
|
|
237
|
+
| FR8, AR1-AR3, AR6 | Existing keyboard and ARIA suites and a coarse-primary keyboard case; an assistive-technology spot check is recommended follow-up | Tab to boundary, arrow resize, screen-reader navigation of the header, coarse-primary device with a keyboard | The boundary leaves tab order on any input that can operate it, loses its name or values, or the hint reaches the accessibility tree. |
|
|
238
|
+
| PR1, PR2 | Render-count assertion around header pointer enter/leave and during a scroll; listener and observer inspection | Pointer enters and leaves the header repeatedly; the region scrolls under a resting pointer | Disclosure rerenders the Table, the scroll gate sets React state, more than one scroll listener or any timer is added, or the extent measurement observes what it resizes. |
|
|
239
|
+
| Plugin composition | Test mounting the resize and sticky-columns plugins together in both insertion orders | Pinned start column with resize enabled, before and after a drag | A pinned column loses `position: sticky` in either order, or the composition depends on caller insertion order. |
|
|
240
|
+
| Record shape | `scripts/check-knowledge.mjs` | Colocated module record, canonical id and filename, parent backlink | A missing, misnamed, mis-parented, or unlinked record passes validation. |
|
|
241
241
|
|
|
242
242
|
Browser, render-count, and assistive-technology evidence are pending. Passing this
|
|
243
243
|
specification pull request proves record integrity only.
|
|
@@ -214,8 +214,9 @@ add semantic/custom cases to representative Table render and no-op update budget
|
|
|
214
214
|
- **AR5 — Supplemental tooltip.** Tooltip may repeat `label` as pointer help, but
|
|
215
215
|
the indicator's accessible name MUST remain sufficient without opening it. The
|
|
216
216
|
current indicator is not focusable, so tooltip presence is not keyboard evidence.
|
|
217
|
-
- **AR6 — Row context.**
|
|
218
|
-
|
|
217
|
+
- **AR6 — Row context.** The named status is exposed in its row context without a
|
|
218
|
+
duplicate name, proved at the accessibility tree. Assistive-technology
|
|
219
|
+
observation of the announcement is recommended follow-up under `spec:AST-009`.
|
|
219
220
|
|
|
220
221
|
## Design relationships
|
|
221
222
|
|
|
@@ -259,7 +260,7 @@ own AST-002 review and synchronized consumer anatomy.
|
|
|
259
260
|
| FR3, FR4, FR6 | Add public-subpath compile fixtures for the unchanged interface, declaration merging and extension, semantic interface, callback union, missing labels, unknown statuses, and forbidden mixtures. | Stable custom annotations/extensions; all semantic statuses. | `TableRowStatus` changes or stops merging, semantic usage is inaccessible, or supported status mixes with custom fields. |
|
|
260
261
|
| FR3-FR5 | Extend row-status runtime tests with semantic-looking custom colors, raw/palette dots, explicit custom icons, all semantic statuses, and active-theme substitution. | Custom dot, custom icon, and three semantic outcomes. | Custom color selects an implicit glyph, semantic status bypasses shared resolution, or variant becomes caller/theme input. |
|
|
261
262
|
| FR7, ORD2-ORD4, PR4 | Runtime tests under development and production modes with repeated mixed objects, rows, and renders. | `status + color`, `status + icon`, and all three fields. | Custom input overrides supported status, forbidden fields affect output, production warns, or development warns more than once per loaded module instance. |
|
|
262
|
-
| AR1-AR6 | Existing role/name tests plus browser axe, forced-colors, tooltip modality, and VoiceOver table
|
|
263
|
+
| AR1-AR6 | Existing role/name tests plus browser axe, forced-colors, tooltip modality, and accessibility-tree row-context evidence; VoiceOver table navigation is recommended follow-up. | Semantic glyphs, custom icon, custom dot, `null`, light/dark, and every shipped theme. | Header or indicator loses its name, nested glyph is announced twice, glyph distinctions disappear, tooltip becomes sole meaning, or row context duplicates status. |
|
|
263
264
|
| PR1-PR3 | Extend `Table.perf.test.tsx` with semantic/custom initial render and no-op update cases. | Stable rerender, mixed rows, and theme substitution. | Resolution adds measurement/resources, creates a parallel registry, or exceeds existing representative Table budgets. |
|
|
264
265
|
| Structural ownership | `scripts/check-knowledge.mjs`. | Colocated module, canonical filename/id, and parent backlink. | A missing, duplicate, orphaned, mis-parented, misnamed, or undiscovered record passes validation. |
|
|
265
266
|
| Public hygiene and scope | Prettier, `check:knowledge`, checker tests, `check:repo`, changed-file scan, and public-content scan. | Parent backlink plus one module specification; no Changeset. | Runtime/docs files, private context, invalid record shape, or unrelated changes enter this contract proposal. |
|
|
@@ -231,14 +231,38 @@ describe('TimeInput nativePicker', () => {
|
|
|
231
231
|
expect(input).not.toBeDisabled();
|
|
232
232
|
expect(input).toHaveAttribute('aria-disabled', 'true');
|
|
233
233
|
expect(input).toHaveAttribute('readonly');
|
|
234
|
+
// The shared native time segment's picker button is focusable-disabled
|
|
235
|
+
// (spec:AST-085 FR1) and still blocks opening.
|
|
234
236
|
expect(
|
|
235
237
|
screen.getByRole('button', {name: 'Open Start time'}),
|
|
236
|
-
).
|
|
238
|
+
).toHaveAttribute('aria-disabled', 'true');
|
|
237
239
|
const tooltip = document.querySelector<HTMLElement>('[role="tooltip"]');
|
|
238
240
|
expect(tooltip).not.toBeNull();
|
|
239
241
|
expect(input.getAttribute('aria-describedby')).toContain(tooltip?.id);
|
|
240
242
|
});
|
|
241
243
|
|
|
244
|
+
it('keeps a disabled native field focusable without native disabled (AST-085 FR1, FR2)', () => {
|
|
245
|
+
stubPointer(true);
|
|
246
|
+
const onClick = vi.fn();
|
|
247
|
+
render(
|
|
248
|
+
<TimeInput
|
|
249
|
+
label="Start time"
|
|
250
|
+
nativePicker="always"
|
|
251
|
+
isDisabled
|
|
252
|
+
onClick={onClick}
|
|
253
|
+
onChange={() => {}}
|
|
254
|
+
/>,
|
|
255
|
+
);
|
|
256
|
+
const input = getNativeTimeInput();
|
|
257
|
+
expect(input).not.toHaveAttribute('disabled');
|
|
258
|
+
expect(input).toHaveAttribute('aria-disabled', 'true');
|
|
259
|
+
expect(input).toHaveAttribute('readonly');
|
|
260
|
+
input.focus();
|
|
261
|
+
expect(input).toHaveFocus();
|
|
262
|
+
fireEvent.click(input);
|
|
263
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
264
|
+
});
|
|
265
|
+
|
|
242
266
|
it('forwards refs and autofocus to the native input', () => {
|
|
243
267
|
stubPointer(true);
|
|
244
268
|
const ref = vi.fn();
|
|
@@ -96,6 +96,29 @@ describe('TimeInput presentation', () => {
|
|
|
96
96
|
expect(screen.getByRole('listbox', {name: 'Minute'})).toBeInTheDocument();
|
|
97
97
|
});
|
|
98
98
|
|
|
99
|
+
it('bottom-sheet: a disabled field stays focusable and inert (AST-085 FR1, FR2)', () => {
|
|
100
|
+
stubPointer(false);
|
|
101
|
+
const onClick = vi.fn();
|
|
102
|
+
render(
|
|
103
|
+
<TimeInput
|
|
104
|
+
label="Start"
|
|
105
|
+
presentation="bottom-sheet"
|
|
106
|
+
isDisabled
|
|
107
|
+
onClick={onClick}
|
|
108
|
+
onChange={() => {}}
|
|
109
|
+
/>,
|
|
110
|
+
);
|
|
111
|
+
const input = screen.getByRole('combobox', {name: 'Start'});
|
|
112
|
+
expect(input).not.toHaveAttribute('disabled');
|
|
113
|
+
expect(input).toHaveAttribute('aria-disabled', 'true');
|
|
114
|
+
input.focus();
|
|
115
|
+
expect(input).toHaveFocus();
|
|
116
|
+
fireEvent.click(input);
|
|
117
|
+
expect(input).toHaveAttribute('aria-expanded', 'false');
|
|
118
|
+
expect(screen.queryByRole('listbox', {name: 'Hour'})).toBeNull();
|
|
119
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
120
|
+
});
|
|
121
|
+
|
|
99
122
|
it('adaptive-bottom-sheet: typed on fine, sheet on coarse', () => {
|
|
100
123
|
stubPointer(false);
|
|
101
124
|
const {unmount} = render(
|