@astryxdesign/core 0.7.2 → 0.7.3-canary.02f5fe4
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/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/Calendar/Calendar.js +3 -2
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +19 -24
- package/dist/ClickableCard/ClickableCard.d.ts +2 -2
- package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
- package/dist/ClickableCard/ClickableCard.js +11 -9
- package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
- package/dist/CodeBlock/CodeBlock.js +11 -1
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +23 -5
- package/dist/DateInput/DateInput.d.ts +4 -4
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +27 -20
- package/dist/DateInput/NativeDateField.d.ts.map +1 -1
- package/dist/DateInput/NativeDateField.js +14 -8
- package/dist/DateInput/TouchDateField.d.ts.map +1 -1
- package/dist/DateInput/TouchDateField.js +14 -10
- 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/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +52 -10
- package/dist/DropdownMenu/DropdownMenuContext.d.ts +34 -1
- package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuContext.js +15 -0
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts +21 -2
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.js +375 -42
- package/dist/DropdownMenu/menuRegionStops.d.ts +2 -2
- package/dist/DropdownMenu/menuRegionStops.js +2 -2
- package/dist/FileInput/FileInput.d.ts.map +1 -1
- package/dist/FileInput/FileInput.js +14 -12
- package/dist/Link/Link.d.ts +2 -3
- package/dist/Link/Link.d.ts.map +1 -1
- package/dist/Link/Link.js +11 -19
- 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/Popover/Popover.d.ts.map +1 -1
- package/dist/Popover/Popover.js +8 -4
- package/dist/Selector/Selector.d.ts +4 -4
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +23 -14
- package/dist/Selector/SelectorOption.d.ts.map +1 -1
- package/dist/Selector/SelectorOption.js +1 -0
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +7 -14
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +25 -17
- package/dist/Switch/switch.markers.stylex.d.ts +2 -0
- package/dist/Switch/switch.markers.stylex.d.ts.map +1 -1
- package/dist/Switch/switch.markers.stylex.js +6 -0
- package/dist/TimeInput/TimeInput.d.ts +7 -6
- 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/Token/Token.d.ts.map +1 -1
- package/dist/Token/Token.js +8 -3
- package/dist/Tooltip/Tooltip.d.ts.map +1 -1
- package/dist/Tooltip/Tooltip.js +36 -14
- package/dist/Tooltip/useTooltip.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.js +11 -2
- package/dist/TopNav/TopNav.d.ts.map +1 -1
- package/dist/TopNav/TopNav.js +2 -2
- package/dist/TopNav/TopNavHeading.d.ts.map +1 -1
- package/dist/TopNav/TopNavHeading.js +1 -0
- package/dist/TreeList/TreeListItem.d.ts.map +1 -1
- package/dist/TreeList/TreeListItem.js +8 -3
- package/dist/Typeahead/Typeahead.d.ts.map +1 -1
- package/dist/Typeahead/Typeahead.js +14 -2
- package/dist/astryx.css +8 -2
- package/dist/hooks/useGridFocus.d.ts.map +1 -1
- package/dist/hooks/useGridFocus.js +10 -1
- package/dist/hooks/useMenuPress.d.ts +11 -1
- package/dist/hooks/useMenuPress.d.ts.map +1 -1
- package/dist/hooks/useMenuPress.js +43 -5
- package/dist/utils/disabledInteraction.d.ts +21 -0
- package/dist/utils/disabledInteraction.d.ts.map +1 -0
- package/dist/utils/disabledInteraction.js +33 -0
- package/dist/utils/timeParser.d.ts +4 -0
- package/dist/utils/timeParser.d.ts.map +1 -1
- package/dist/utils/timeParser.js +9 -2
- package/package.json +4 -4
- package/scripts/agent-doc-state.mjs +58 -9
- package/src/Banner/Banner.spec.md +37 -32
- package/src/BottomSheet/BottomSheet.spec.md +34 -54
- package/src/BottomSheet/BottomSheet.test.tsx +29 -0
- package/src/BottomSheet/BottomSheet.tsx +8 -4
- package/src/BottomSheet/BottomSheetSwitcher.spec.md +125 -58
- package/src/BottomSheet/BottomSheetSwitcher.test.tsx +26 -0
- package/src/BottomSheet/BottomSheetSwitcher.tsx +6 -0
- package/src/Calendar/Calendar.test.tsx +68 -24
- package/src/Calendar/Calendar.tsx +2 -1
- package/src/CheckboxInput/CheckboxInput.test.tsx +44 -5
- package/src/CheckboxInput/CheckboxInput.tsx +16 -26
- package/src/CheckboxInput/__tests__/Checkbox.a11y.states.ts +3 -3
- package/src/CheckboxList/CheckboxList.test.tsx +9 -6
- package/src/ClickableCard/ClickableCard.doc.mjs +2 -2
- package/src/ClickableCard/ClickableCard.test.tsx +5 -4
- package/src/ClickableCard/ClickableCard.tsx +9 -9
- package/src/CodeBlock/CodeBlock.highlightStyles.test.tsx +87 -0
- package/src/CodeBlock/CodeBlock.tsx +11 -1
- package/src/ComplexSelector/ComplexSelector.focusableDisabled.test.tsx +76 -0
- package/src/ComplexSelector/ComplexSelector.tsx +21 -4
- package/src/ContextMenu/ContextMenu.test.tsx +41 -0
- package/src/DateInput/DateInput.doc.mjs +9 -8
- package/src/DateInput/DateInput.test.tsx +88 -13
- package/src/DateInput/DateInput.tsx +49 -21
- package/src/DateInput/DateInputTouch.test.tsx +22 -1
- package/src/DateInput/NativeDateField.test.tsx +29 -3
- package/src/DateInput/NativeDateField.tsx +20 -7
- package/src/DateInput/TouchDateField.tsx +24 -9
- package/src/DateRangeInput/DateRangeInput.doc.mjs +6 -5
- package/src/DateRangeInput/DateRangeInput.spec.md +13 -13
- 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/DropdownMenu.spec.md +33 -2
- package/src/DropdownMenu/DropdownMenu.tsx +79 -8
- package/src/DropdownMenu/DropdownMenuContext.public.test.ts +22 -0
- package/src/DropdownMenu/DropdownMenuContext.tsx +39 -1
- package/src/DropdownMenu/DropdownMenuItem.doc.mjs +66 -7
- package/src/DropdownMenu/DropdownMenuItem.spec.md +209 -0
- package/src/DropdownMenu/DropdownMenuSubMenu.doc.mjs +16 -1
- package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +623 -1
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +556 -99
- package/src/DropdownMenu/__tests__/DropdownMenuItemAudit.a11y.chromium.spec.ts +366 -0
- package/src/DropdownMenu/__tests__/SubMenuHeaderFooter.a11y.chromium.spec.ts +267 -0
- package/src/DropdownMenu/menuRegionStops.ts +2 -2
- package/src/DropdownMenu/modules/useMenuPress.spec.md +9 -0
- package/src/FieldStatus/FieldStatus.spec.md +3 -2
- package/src/FileInput/FileInput.test.tsx +30 -7
- package/src/FileInput/FileInput.tsx +21 -11
- package/src/Link/Link.test.tsx +21 -7
- package/src/Link/Link.tsx +16 -24
- package/src/Link/__tests__/Link.a11y.states.ts +1 -1
- 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/Popover/Popover.test.tsx +15 -0
- package/src/Popover/Popover.tsx +19 -8
- package/src/PowerSearch/PowerSearchFilterEditor.test.tsx +3 -2
- package/src/PowerSearch/PowerSearchToken.test.tsx +7 -5
- package/src/Selector/Selector.doc.mjs +4 -4
- package/src/Selector/Selector.focusableDisabled.test.tsx +127 -0
- package/src/Selector/Selector.spec.md +11 -11
- package/src/Selector/Selector.test.tsx +64 -6
- package/src/Selector/Selector.tsx +51 -21
- package/src/Selector/SelectorOption.tsx +4 -0
- package/src/SideNav/SideNav.test.tsx +35 -0
- package/src/Slider/Slider.spec.md +11 -11
- package/src/Slider/Slider.test.tsx +3 -3
- package/src/Slider/Slider.tsx +8 -15
- package/src/Switch/Switch.test.tsx +85 -8
- package/src/Switch/Switch.tsx +26 -16
- package/src/Switch/__tests__/Switch.a11y.states.ts +2 -2
- package/src/Switch/switch.markers.stylex.ts +4 -0
- package/src/Table/Table.spec.md +2 -0
- package/src/Table/plugins/infiniteScroll/useTableInfiniteScroll.spec.md +244 -0
- package/src/Table/plugins/selection/useTableSelection.test.tsx +5 -3
- package/src/Table/plugins/virtualization/useTableVirtualization.spec.md +266 -0
- package/src/TimeInput/NativeTimeInput.test.tsx +25 -1
- package/src/TimeInput/Presentation.test.tsx +23 -0
- package/src/TimeInput/TimeInput.doc.mjs +13 -10
- package/src/TimeInput/TimeInput.test.tsx +139 -7
- package/src/TimeInput/TimeInput.tsx +53 -24
- 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/Token/Token.test.tsx +26 -4
- package/src/Token/Token.tsx +6 -2
- package/src/Tokenizer/Tokenizer.spec.md +28 -28
- package/src/Tooltip/Tooltip.test.tsx +51 -6
- package/src/Tooltip/Tooltip.tsx +41 -18
- package/src/Tooltip/__tests__/Tooltip.a11y.chromium.spec.ts +129 -0
- package/src/Tooltip/__tests__/Tooltip.a11y.renders.tsx +86 -0
- package/src/Tooltip/__tests__/Tooltip.a11y.states.ts +132 -0
- package/src/Tooltip/__tests__/Tooltip.a11y.test.tsx +126 -0
- package/src/Tooltip/__tests__/TooltipHoverBridge.a11y.chromium.spec.ts +74 -0
- package/src/Tooltip/useTooltip.test.tsx +23 -0
- package/src/Tooltip/useTooltip.tsx +22 -9
- package/src/TopNav/TopNav.tsx +11 -1
- package/src/TopNav/TopNavHeading.tsx +1 -0
- package/src/TopNav/__tests__/TopNavMobileLongHeading.a11y.chromium.spec.ts +74 -0
- package/src/TreeList/TreeList.test.tsx +28 -0
- package/src/TreeList/TreeListItem.tsx +6 -2
- package/src/Typeahead/Typeahead.test.tsx +89 -0
- package/src/Typeahead/Typeahead.tsx +15 -2
- package/src/__tests__/DisabledTriggerFocus.a11y.chromium.spec.ts +86 -0
- package/src/hooks/useGridFocus.ts +16 -1
- package/src/hooks/useMenuPress.ts +45 -7
- package/src/utils/disabledInteraction.ts +40 -0
- package/src/utils/timeParser.test.ts +11 -0
- package/src/utils/timeParser.ts +9 -2
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Selector.focusableDisabled.test.tsx
|
|
5
|
+
* @input Uses vitest, @testing-library/react, StyleX, Selector
|
|
6
|
+
* @output Conformance tests for spec:AST-085 FR1–FR4 and FR6 on Selector
|
|
7
|
+
* @position Testing; validates the disabled trigger of Selector.tsx
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {describe, it, expect, vi} from 'vitest';
|
|
11
|
+
import {act, render, screen} from '@testing-library/react';
|
|
12
|
+
import userEvent from '@testing-library/user-event';
|
|
13
|
+
import * as stylex from '@stylexjs/stylex';
|
|
14
|
+
import {colorVars} from '../theme/tokens.stylex';
|
|
15
|
+
import {Selector} from './Selector';
|
|
16
|
+
|
|
17
|
+
type Handler = () => void;
|
|
18
|
+
function Harness(props: {
|
|
19
|
+
isDisabled: boolean;
|
|
20
|
+
variant?: 'input' | 'secondary' | 'ghost';
|
|
21
|
+
onClick?: Handler;
|
|
22
|
+
onKeyDown?: Handler;
|
|
23
|
+
onMouseDown?: Handler;
|
|
24
|
+
onDoubleClick?: Handler;
|
|
25
|
+
}) {
|
|
26
|
+
return (
|
|
27
|
+
<form aria-label="Form">
|
|
28
|
+
<button type="button">Before</button>
|
|
29
|
+
<Selector
|
|
30
|
+
label="Fruit"
|
|
31
|
+
htmlName="fruit"
|
|
32
|
+
options={['Apple', 'Banana']}
|
|
33
|
+
value="Banana"
|
|
34
|
+
onChange={() => {}}
|
|
35
|
+
{...props}
|
|
36
|
+
/>
|
|
37
|
+
<button type="button">After</button>
|
|
38
|
+
</form>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const trigger = () => screen.getByRole('combobox', {name: 'Fruit'});
|
|
43
|
+
|
|
44
|
+
describe('Selector disabled (spec:AST-085)', () => {
|
|
45
|
+
it('FR1: renders no native disabled and keeps its tab stop', async () => {
|
|
46
|
+
const user = userEvent.setup();
|
|
47
|
+
const {container} = render(<Harness isDisabled />);
|
|
48
|
+
expect(container.querySelector('[disabled]')).toBeNull();
|
|
49
|
+
expect(trigger()).toHaveAttribute('aria-disabled', 'true');
|
|
50
|
+
await user.click(screen.getByRole('button', {name: 'Before'}));
|
|
51
|
+
await user.tab();
|
|
52
|
+
expect(trigger()).toHaveFocus();
|
|
53
|
+
await user.tab();
|
|
54
|
+
expect(screen.getByRole('button', {name: 'After'})).toHaveFocus();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('FR2: no press or key opens it or reaches a caller or ancestor handler', async () => {
|
|
58
|
+
const user = userEvent.setup();
|
|
59
|
+
const handlers = {
|
|
60
|
+
onClick: vi.fn(),
|
|
61
|
+
onKeyDown: vi.fn(),
|
|
62
|
+
onMouseDown: vi.fn(),
|
|
63
|
+
onDoubleClick: vi.fn(),
|
|
64
|
+
};
|
|
65
|
+
const onAncestorClick = vi.fn();
|
|
66
|
+
render(
|
|
67
|
+
<div onClick={onAncestorClick}>
|
|
68
|
+
<Harness isDisabled {...handlers} />
|
|
69
|
+
</div>,
|
|
70
|
+
);
|
|
71
|
+
await user.dblClick(trigger());
|
|
72
|
+
trigger().focus();
|
|
73
|
+
for (const key of ['{Enter}', ' ', '{ArrowDown}', 'b']) {
|
|
74
|
+
await user.keyboard(key);
|
|
75
|
+
}
|
|
76
|
+
expect(trigger()).toHaveAttribute('aria-expanded', 'false');
|
|
77
|
+
expect(screen.queryByRole('listbox')).toBeNull();
|
|
78
|
+
for (const handler of [...Object.values(handlers), onAncestorClick]) {
|
|
79
|
+
expect(handler).not.toHaveBeenCalled();
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('FR3: submits no value while disabled', () => {
|
|
84
|
+
const {rerender} = render(<Harness isDisabled={false} />);
|
|
85
|
+
const form = () =>
|
|
86
|
+
screen.getByRole<HTMLFormElement>('form', {name: 'Form'});
|
|
87
|
+
expect(new FormData(form()).getAll('fruit')).toEqual(['Banana']);
|
|
88
|
+
rerender(<Harness isDisabled />);
|
|
89
|
+
expect(new FormData(form()).getAll('fruit')).toEqual([]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('FR4: keeps focus when it becomes disabled while focused', () => {
|
|
93
|
+
const {rerender} = render(<Harness isDisabled={false} />);
|
|
94
|
+
act(() => trigger().focus());
|
|
95
|
+
rerender(<Harness isDisabled />);
|
|
96
|
+
expect(trigger()).toHaveFocus();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// jsdom cannot match `:focus-visible`: compare the ring's StyleX class hash.
|
|
100
|
+
const RING = stylex
|
|
101
|
+
.props(
|
|
102
|
+
stylex.create({
|
|
103
|
+
ring: {
|
|
104
|
+
boxShadow: {
|
|
105
|
+
default: null,
|
|
106
|
+
':has(> [aria-disabled="true"]:focus-visible)': `inset 0px 0px 0px 2px ${colorVars['--color-accent-muted']}`,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
}).ring,
|
|
110
|
+
)
|
|
111
|
+
.className!.split(' ')
|
|
112
|
+
.pop()!;
|
|
113
|
+
|
|
114
|
+
it.each([
|
|
115
|
+
['input', true],
|
|
116
|
+
['secondary', false],
|
|
117
|
+
['ghost', false],
|
|
118
|
+
] as const)(
|
|
119
|
+
'FR6: a disabled %s trigger carries its own ring: %s',
|
|
120
|
+
(variant, ring) => {
|
|
121
|
+
render(<Harness isDisabled variant={variant} />);
|
|
122
|
+
const wrapper = trigger().parentElement!;
|
|
123
|
+
expect(wrapper.classList.contains(RING)).toBe(ring);
|
|
124
|
+
expect(getComputedStyle(wrapper).cursor).toBe('default');
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
});
|
|
@@ -163,17 +163,17 @@ These requirements describe shipped behavior on current `main`.
|
|
|
163
163
|
|
|
164
164
|
### Representative states
|
|
165
165
|
|
|
166
|
-
| State | Required invariant
|
|
167
|
-
| ------------------------ |
|
|
168
|
-
| closed with no value | Label and placeholder identify the field
|
|
169
|
-
| closed with a value | Selected option is represented in the trigger
|
|
170
|
-
| pointer / popover | Anchored surface exposes the listbox without modal-dialog semantics
|
|
171
|
-
| compact coarse pointer | BottomSheet exposes a modal dialog containing the listbox
|
|
172
|
-
| searching | Visible options, keyboard navigation, and announced result count use one filter
|
|
173
|
-
| loading | Trigger is busy; empty and no-results output is suppressed
|
|
174
|
-
| disabled with reason | Trigger remains focusable
|
|
175
|
-
| read-only | Value stays focusable and submittable; menu, clear, and disclosure affordances are absent
|
|
176
|
-
| selected/unselected rows | Empty marks consume no width; visible marks retain space at the logical edge
|
|
166
|
+
| State | Required invariant | Allowed variation |
|
|
167
|
+
| ------------------------ | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
|
|
168
|
+
| closed with no value | Label and placeholder identify the field | Consumer placeholder text |
|
|
169
|
+
| closed with a value | Selected option is represented in the trigger | Custom `renderValue` content |
|
|
170
|
+
| pointer / popover | Anchored surface exposes the listbox without modal-dialog semantics | Default or explicit placement |
|
|
171
|
+
| compact coarse pointer | BottomSheet exposes a modal dialog containing the listbox | Default or explicit `adaptive`, or explicit `bottom-sheet` |
|
|
172
|
+
| searching | Visible options, keyboard navigation, and announced result count use one filter | Consumer search and empty text |
|
|
173
|
+
| loading | Trigger is busy; empty and no-results output is suppressed | Consumer loading duration |
|
|
174
|
+
| disabled with reason | Trigger remains focusable (`spec:AST-085`) and exposes the reason; activation stays blocked | Consumer reason text |
|
|
175
|
+
| read-only | Value stays focusable and submittable; menu, clear, and disclosure affordances are absent | Value rendering, status, and busy presentation |
|
|
176
|
+
| selected/unselected rows | Empty marks consume no width; visible marks retain space at the logical edge | State-dependent label position/width and themed indicators |
|
|
177
177
|
|
|
178
178
|
### Transformation and precedence order
|
|
179
179
|
|
|
@@ -36,7 +36,7 @@ import {InternationalizationProvider} from '../i18n';
|
|
|
36
36
|
import {defineTheme} from '../theme/defineTheme';
|
|
37
37
|
import {Theme} from '../theme/Theme';
|
|
38
38
|
import {generateThemeCSS} from '../theme/generateThemeRules';
|
|
39
|
-
import {spacingVars} from '../theme/tokens.stylex';
|
|
39
|
+
import {colorVars, spacingVars} from '../theme/tokens.stylex';
|
|
40
40
|
import {selectorPresentationStyles} from './selectorPresentation.stylex';
|
|
41
41
|
import {groupStyles} from '../InputGroup/groupStyles';
|
|
42
42
|
|
|
@@ -303,6 +303,45 @@ describe('Selector', () => {
|
|
|
303
303
|
);
|
|
304
304
|
});
|
|
305
305
|
|
|
306
|
+
it('shows no disabled focus ring after a pointer pick (spec:AST-085 FR6)', async () => {
|
|
307
|
+
const field = (isDisabled: boolean) => (
|
|
308
|
+
<Selector
|
|
309
|
+
label="Fruit"
|
|
310
|
+
options={OPTIONS}
|
|
311
|
+
onChange={() => {}}
|
|
312
|
+
presentation="bottom-sheet"
|
|
313
|
+
isDisabled={isDisabled}
|
|
314
|
+
/>
|
|
315
|
+
);
|
|
316
|
+
const {rerender} = render(field(false));
|
|
317
|
+
const trigger = screen.getByRole('combobox');
|
|
318
|
+
fireEvent.pointerDown(trigger, {pointerType: 'touch'});
|
|
319
|
+
fireEvent.click(trigger, {detail: 1});
|
|
320
|
+
const option = await screen.findByRole('option', {name: /Banana/});
|
|
321
|
+
fireEvent.pointerDown(option, {pointerType: 'touch'});
|
|
322
|
+
fireEvent.click(option, {detail: 1});
|
|
323
|
+
rerender(field(true));
|
|
324
|
+
|
|
325
|
+
const ring = stylex
|
|
326
|
+
.props(
|
|
327
|
+
stylex.create({
|
|
328
|
+
ring: {
|
|
329
|
+
boxShadow: {
|
|
330
|
+
default: null,
|
|
331
|
+
':has(> [aria-disabled="true"]:focus-visible)': `inset 0px 0px 0px 2px ${colorVars['--color-accent-muted']}`,
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
}).ring,
|
|
335
|
+
)
|
|
336
|
+
.className!.split(' ')
|
|
337
|
+
.pop()!;
|
|
338
|
+
const wrapper = screen.getByRole('combobox').parentElement!;
|
|
339
|
+
expect(wrapper).toHaveClass(
|
|
340
|
+
stylex.props(selectorPresentationStyles.pointerRestoredFocus).className!,
|
|
341
|
+
);
|
|
342
|
+
expect(wrapper).not.toHaveClass(ring);
|
|
343
|
+
});
|
|
344
|
+
|
|
306
345
|
it('uses content-hugging height without extra bottom content padding', () => {
|
|
307
346
|
const source = readFileSync(
|
|
308
347
|
'packages/core/src/Selector/SelectorBottomSheet.tsx',
|
|
@@ -1821,9 +1860,11 @@ describe('Selector', () => {
|
|
|
1821
1860
|
expect(screen.getByRole('combobox')).toHaveFocus();
|
|
1822
1861
|
});
|
|
1823
1862
|
|
|
1824
|
-
it('trigger
|
|
1863
|
+
it('trigger keeps its tab stop when disabled (spec:AST-085 FR1)', () => {
|
|
1825
1864
|
render(<Selector label="Fruit" options={OPTIONS} isDisabled />);
|
|
1826
|
-
|
|
1865
|
+
const trigger = screen.getByRole('combobox');
|
|
1866
|
+
expect(trigger).toHaveAttribute('tabIndex', '0');
|
|
1867
|
+
expect(trigger).toHaveAttribute('aria-disabled', 'true');
|
|
1827
1868
|
});
|
|
1828
1869
|
|
|
1829
1870
|
it('opens the listbox with ArrowDown from a focused trigger', async () => {
|
|
@@ -2715,11 +2756,12 @@ describe('Selector', () => {
|
|
|
2715
2756
|
expect(onChange).not.toHaveBeenCalled();
|
|
2716
2757
|
});
|
|
2717
2758
|
|
|
2718
|
-
it('
|
|
2759
|
+
it('stays focusable when disabled without a reason (spec:AST-085 FR1)', () => {
|
|
2719
2760
|
render(<Selector label="Fruit" options={OPTIONS} isDisabled />);
|
|
2720
2761
|
const trigger = screen.getByRole('combobox');
|
|
2721
|
-
expect(trigger).
|
|
2722
|
-
expect(trigger).toHaveAttribute('
|
|
2762
|
+
expect(trigger).not.toHaveAttribute('disabled');
|
|
2763
|
+
expect(trigger).toHaveAttribute('aria-disabled', 'true');
|
|
2764
|
+
expect(trigger).toHaveAttribute('tabIndex', '0');
|
|
2723
2765
|
});
|
|
2724
2766
|
});
|
|
2725
2767
|
describe('isReadOnly', () => {
|
|
@@ -4809,6 +4851,22 @@ describe('Selector in a narrow row', () => {
|
|
|
4809
4851
|
expect(root.getAttribute('style')).toContain('80');
|
|
4810
4852
|
expect(getComputedStyle(root).minWidth).toBe('0');
|
|
4811
4853
|
});
|
|
4854
|
+
|
|
4855
|
+
it('lets a narrow list shrink an option so its label can ellipsize', () => {
|
|
4856
|
+
render(
|
|
4857
|
+
<Selector
|
|
4858
|
+
label="Project"
|
|
4859
|
+
options={['An option with a deliberately long readable label']}
|
|
4860
|
+
value={undefined}
|
|
4861
|
+
onChange={() => {}}
|
|
4862
|
+
isDefaultOpen
|
|
4863
|
+
/>,
|
|
4864
|
+
);
|
|
4865
|
+
const option = screen
|
|
4866
|
+
.getByRole('option', {hidden: true})
|
|
4867
|
+
.querySelector('.astryx-selector-option')!;
|
|
4868
|
+
expect(getComputedStyle(option).minWidth).toBe('0');
|
|
4869
|
+
});
|
|
4812
4870
|
});
|
|
4813
4871
|
|
|
4814
4872
|
describe('Selector and option actions (not adopted)', () => {
|
|
@@ -291,6 +291,21 @@ const styles = stylex.create({
|
|
|
291
291
|
'[data-astryx-press="on"]': 'scale(0.98)',
|
|
292
292
|
},
|
|
293
293
|
},
|
|
294
|
+
// A disabled input-variant trigger keeps its tab stop (`spec:AST-085` FR1),
|
|
295
|
+
// and the shared disabled wrapper style drops the field focus ring, so the
|
|
296
|
+
// trigger shows keyboard focus here instead (FR6). It keys on the trigger
|
|
297
|
+
// itself, a direct child, so a focused status button keeps its own outline.
|
|
298
|
+
triggerDisabledFocus: {
|
|
299
|
+
borderColor: {
|
|
300
|
+
default: colorVars['--color-border-emphasized'],
|
|
301
|
+
':has(> [aria-disabled="true"]:focus-visible)':
|
|
302
|
+
colorVars['--color-accent'],
|
|
303
|
+
},
|
|
304
|
+
boxShadow: {
|
|
305
|
+
default: 'none',
|
|
306
|
+
':has(> [aria-disabled="true"]:focus-visible)': `inset 0px 0px 0px 2px ${colorVars['--color-accent-muted']}`,
|
|
307
|
+
},
|
|
308
|
+
},
|
|
294
309
|
triggerGhostDisabled: {
|
|
295
310
|
backgroundImage: 'none',
|
|
296
311
|
transform: {
|
|
@@ -627,8 +642,8 @@ interface SelectorPropsBase<
|
|
|
627
642
|
* The selected value stays visible, focusable, and included in form
|
|
628
643
|
* submission, and retains its combobox identity with `aria-readonly`. The
|
|
629
644
|
* selection surface and editing affordances are removed.
|
|
630
|
-
* Unlike `isDisabled`, a read-only selector is not dimmed and
|
|
631
|
-
*
|
|
645
|
+
* Unlike `isDisabled`, a read-only selector is not dimmed and keeps its
|
|
646
|
+
* value in form submission. `isDisabled` takes precedence when both are set.
|
|
632
647
|
* @default false
|
|
633
648
|
*/
|
|
634
649
|
isReadOnly?: boolean;
|
|
@@ -640,8 +655,8 @@ interface SelectorPropsBase<
|
|
|
640
655
|
* so the reason is discoverable by keyboard and assistive technology.
|
|
641
656
|
* Activation stays blocked.
|
|
642
657
|
*
|
|
643
|
-
* Use this instead of wrapping a disabled selector in `Tooltip
|
|
644
|
-
*
|
|
658
|
+
* Use this instead of wrapping a disabled selector in `Tooltip`, so the
|
|
659
|
+
* reason is attached to the trigger itself.
|
|
645
660
|
*
|
|
646
661
|
* @example
|
|
647
662
|
* ```
|
|
@@ -1872,8 +1887,24 @@ export function Selector<T extends SelectorOptionType>(
|
|
|
1872
1887
|
});
|
|
1873
1888
|
}
|
|
1874
1889
|
|
|
1890
|
+
// A disabled trigger stays focusable but, like a natively disabled button,
|
|
1891
|
+
// runs no caller event handler other than focus and blur, and a press on it
|
|
1892
|
+
// reaches no handler at all (`spec:AST-085` FR2).
|
|
1893
|
+
const callerProps = isDisabled
|
|
1894
|
+
? Object.fromEntries(
|
|
1895
|
+
Object.entries(rest).filter(
|
|
1896
|
+
([key]) => !/^on[A-Z]/.test(key) || /^on(Focus|Blur)/.test(key),
|
|
1897
|
+
),
|
|
1898
|
+
)
|
|
1899
|
+
: rest;
|
|
1875
1900
|
const triggerSharedProps: React.HTMLAttributes<HTMLElement> = {
|
|
1876
|
-
...
|
|
1901
|
+
...callerProps,
|
|
1902
|
+
onClick: isDisabled
|
|
1903
|
+
? event => {
|
|
1904
|
+
event.preventDefault();
|
|
1905
|
+
event.stopPropagation();
|
|
1906
|
+
}
|
|
1907
|
+
: rest.onClick,
|
|
1877
1908
|
id: triggerId,
|
|
1878
1909
|
// A caller's description is kept alongside the field's own.
|
|
1879
1910
|
'aria-describedby':
|
|
@@ -1887,12 +1918,14 @@ export function Selector<T extends SelectorOptionType>(
|
|
|
1887
1918
|
// it leaves uncancelled. A caller `onClick` arrives through `rest` and
|
|
1888
1919
|
// runs on the trigger before the press bubbles to the control box's
|
|
1889
1920
|
// toggle, as it always has.
|
|
1890
|
-
onKeyDown:
|
|
1921
|
+
onKeyDown: isDisabled
|
|
1922
|
+
? handleTriggerKeyDown
|
|
1923
|
+
: composeEventHandlers(handleTriggerKeyDown, onKeyDownProp),
|
|
1891
1924
|
onFocus: event => {
|
|
1892
1925
|
onFocus?.(event);
|
|
1893
1926
|
surface.onTriggerFocus(event);
|
|
1894
1927
|
},
|
|
1895
|
-
tabIndex:
|
|
1928
|
+
tabIndex: 0,
|
|
1896
1929
|
...stylex.props(
|
|
1897
1930
|
styles.trigger,
|
|
1898
1931
|
isEffectivelyReadOnly && styles.triggerReadOnly,
|
|
@@ -1932,6 +1965,10 @@ export function Selector<T extends SelectorOptionType>(
|
|
|
1932
1965
|
surface.isTriggerFocusRingSuppressed &&
|
|
1933
1966
|
selectorPresentationStyles.pointerRestoredFocus,
|
|
1934
1967
|
isDisabled && inputWrapperStyles.disabled,
|
|
1968
|
+
variant === 'input' &&
|
|
1969
|
+
isDisabled &&
|
|
1970
|
+
!surface.isTriggerFocusRingSuppressed &&
|
|
1971
|
+
styles.triggerDisabledFocus,
|
|
1935
1972
|
isEffectivelyReadOnly && styles.triggerReadOnly,
|
|
1936
1973
|
variant !== 'input' && isDisabled && styles.triggerGhostDisabled,
|
|
1937
1974
|
variant !== 'input' &&
|
|
@@ -1991,22 +2028,15 @@ export function Selector<T extends SelectorOptionType>(
|
|
|
1991
2028
|
? getItemId(highlightedIndex)
|
|
1992
2029
|
: undefined
|
|
1993
2030
|
}
|
|
1994
|
-
//
|
|
1995
|
-
// aria-disabled
|
|
1996
|
-
//
|
|
1997
|
-
disabled={isDisabled
|
|
1998
|
-
aria-disabled={showsDisabledMessage ? 'true' : undefined}>
|
|
2031
|
+
// A disabled trigger keeps its tab stop and says so through
|
|
2032
|
+
// aria-disabled (`spec:AST-085` FR1); the isDisabled guards in
|
|
2033
|
+
// useCombobox block every way of opening it.
|
|
2034
|
+
aria-disabled={isDisabled ? 'true' : undefined}>
|
|
1999
2035
|
{valueContent}
|
|
2000
2036
|
</button>
|
|
2001
|
-
{
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
name={htmlName}
|
|
2005
|
-
value={value ?? ''}
|
|
2006
|
-
// Disabled native controls are excluded from form submission;
|
|
2007
|
-
// mirror that for the hidden carrier.
|
|
2008
|
-
disabled={isDisabled}
|
|
2009
|
-
/>
|
|
2037
|
+
{/* A disabled selector submits no value (`spec:AST-085` FR3). */}
|
|
2038
|
+
{htmlName != null && !isDisabled && (
|
|
2039
|
+
<input type="hidden" name={htmlName} value={value ?? ''} />
|
|
2010
2040
|
)}
|
|
2011
2041
|
{isBusy && <Spinner size="sm" />}
|
|
2012
2042
|
{hasClear && value != null && !isDisabled && !isEffectivelyReadOnly && (
|
|
@@ -22,6 +22,10 @@ const embeddedStyles = stylex.create({
|
|
|
22
22
|
paddingBlock: 0,
|
|
23
23
|
paddingInline: 0,
|
|
24
24
|
borderRadius: 0,
|
|
25
|
+
// The host row lays this out as a flex item, whose automatic minimum
|
|
26
|
+
// size is the full label width. Without the reset a long label runs
|
|
27
|
+
// past a narrow popover and is clipped instead of ellipsizing.
|
|
28
|
+
minWidth: 0,
|
|
25
29
|
},
|
|
26
30
|
});
|
|
27
31
|
|
|
@@ -3691,3 +3691,38 @@ describe('SideNav collapse conflict warning', () => {
|
|
|
3691
3691
|
expect(warn).not.toHaveBeenCalled();
|
|
3692
3692
|
});
|
|
3693
3693
|
});
|
|
3694
|
+
|
|
3695
|
+
// =============================================================================
|
|
3696
|
+
// SideNavItem — collapsed-from-mount rail tooltip
|
|
3697
|
+
// =============================================================================
|
|
3698
|
+
|
|
3699
|
+
describe('SideNavItem collapsed-from-mount rail tooltip', () => {
|
|
3700
|
+
// #7692: a childless item that mounts inside an already-collapsed SideNav
|
|
3701
|
+
// renders its Tooltip inside the very element anchorRef points at. React
|
|
3702
|
+
// attaches that ref only after the tooltip's layout effect has run, so the
|
|
3703
|
+
// tooltip never attached and a persisted collapsed rail lost all tooltips
|
|
3704
|
+
// on page load.
|
|
3705
|
+
it('anchors the rail tooltip when the nav is collapsed at mount', async () => {
|
|
3706
|
+
render(
|
|
3707
|
+
<SideNav
|
|
3708
|
+
aria-label="Main"
|
|
3709
|
+
collapsible={{isCollapsed: true, hasButton: false}}>
|
|
3710
|
+
<SideNavItem label="Dashboard" icon={StubIcon} href="/dashboard" />
|
|
3711
|
+
</SideNav>,
|
|
3712
|
+
);
|
|
3713
|
+
const link = screen.getByRole('link', {name: 'Dashboard'});
|
|
3714
|
+
const anchor = link.parentElement as HTMLElement;
|
|
3715
|
+
|
|
3716
|
+
// The tooltip attaches despite mounting inside its own anchor: the item
|
|
3717
|
+
// row is described by the tooltip layer.
|
|
3718
|
+
await waitFor(() => {
|
|
3719
|
+
expect(anchor.getAttribute('aria-describedby')).not.toBe('');
|
|
3720
|
+
});
|
|
3721
|
+
const layer = document.getElementById(
|
|
3722
|
+
anchor.getAttribute('aria-describedby') as string,
|
|
3723
|
+
);
|
|
3724
|
+
expect(layer).not.toBeNull();
|
|
3725
|
+
expect(layer).toHaveAttribute('role', 'tooltip');
|
|
3726
|
+
expect(layer).toHaveTextContent('Dashboard');
|
|
3727
|
+
});
|
|
3728
|
+
});
|
|
@@ -134,8 +134,8 @@ value-tooltip behavior; AR1 records the keyboard model and its one correction.
|
|
|
134
134
|
thumb as one composite control: every thumb is `role="slider"` with an
|
|
135
135
|
accessible name (a range names each thumb), `aria-valuemin`, `aria-valuemax`,
|
|
136
136
|
`aria-valuenow`, `aria-valuetext` when a formatter changes the spoken value,
|
|
137
|
-
`aria-orientation`, and `aria-disabled` when disabled;
|
|
138
|
-
|
|
137
|
+
`aria-orientation`, and `aria-disabled` when disabled; every thumb is a Tab
|
|
138
|
+
stop, enabled or disabled (`spec:AST-085`).
|
|
139
139
|
ArrowUp and ArrowDown step by `step`; on a horizontal track the horizontal
|
|
140
140
|
arrows follow the track's direction — ArrowRight increases and ArrowLeft
|
|
141
141
|
decreases under LTR, and under RTL ArrowLeft increases and ArrowRight
|
|
@@ -225,15 +225,15 @@ separately listed value tooltip retains Tooltip's target.
|
|
|
225
225
|
|
|
226
226
|
## Verification map
|
|
227
227
|
|
|
228
|
-
| Contract | Verification
|
|
229
|
-
| ------------------- |
|
|
230
|
-
| FR1 | `Slider.test.tsx` structure, range, and marks suites
|
|
231
|
-
| AR1 | Slider accessibility contract (`internal/a11y-spec`, pending) bound to Slider
|
|
232
|
-
| FR2 | `Slider.test.tsx`, `themingTargets.test.ts`, generated probe theme, and Chromium
|
|
233
|
-
| FR3 | Source and consumer-doc review
|
|
234
|
-
| FR4 | Source inspection; focused tests cover label, status, and disabled-reason Tooltip only
|
|
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)
|
|
236
|
-
| Theming anatomy map | `scripts/check-knowledge.mjs`
|
|
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; disabled-thumb Tab stop pending conformance; 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` |
|
|
237
237
|
|
|
238
238
|
The focused Slider suite asserts the `slider-control` target's orientation and
|
|
239
239
|
disabled state. Source/metadata guards cover all four target declarations. The
|
|
@@ -385,7 +385,7 @@ describe('Slider', () => {
|
|
|
385
385
|
render(<Slider label="Volume" value={50} isDisabled />);
|
|
386
386
|
const slider = screen.getByRole('slider');
|
|
387
387
|
expect(slider).toHaveAttribute('aria-disabled', 'true');
|
|
388
|
-
expect(slider).toHaveAttribute('tabIndex', '
|
|
388
|
+
expect(slider).toHaveAttribute('tabIndex', '0');
|
|
389
389
|
});
|
|
390
390
|
|
|
391
391
|
it('does not fire onChange on pointer down when disabled', () => {
|
|
@@ -1111,11 +1111,11 @@ describe('Slider', () => {
|
|
|
1111
1111
|
expect(onChange).not.toHaveBeenCalled();
|
|
1112
1112
|
});
|
|
1113
1113
|
|
|
1114
|
-
it('
|
|
1114
|
+
it('stays focusable when disabled without a reason', () => {
|
|
1115
1115
|
render(
|
|
1116
1116
|
<Slider label="Volume" value={50} valueDisplay="none" isDisabled />,
|
|
1117
1117
|
);
|
|
1118
|
-
expect(screen.getByRole('slider')).toHaveAttribute('tabindex', '
|
|
1118
|
+
expect(screen.getByRole('slider')).toHaveAttribute('tabindex', '0');
|
|
1119
1119
|
});
|
|
1120
1120
|
});
|
|
1121
1121
|
describe('form participation', () => {
|
package/src/Slider/Slider.tsx
CHANGED
|
@@ -558,15 +558,12 @@ export function Slider({ref, ...props}: SliderProps) {
|
|
|
558
558
|
|
|
559
559
|
const handleThumbFocus = useCallback(
|
|
560
560
|
(thumbIndex: number, _e: FocusEvent<HTMLDivElement>) => {
|
|
561
|
-
// A disabled thumb
|
|
562
|
-
// reason tooltip, so there is nothing to track for one.
|
|
561
|
+
// A focused disabled thumb still shows the ring (spec:AST-085 FR6).
|
|
563
562
|
setKeyboardFocusThumb(
|
|
564
|
-
|
|
565
|
-
? thumbIndex
|
|
566
|
-
: null,
|
|
563
|
+
getInteractionModality() === 'keyboard' ? thumbIndex : null,
|
|
567
564
|
);
|
|
568
565
|
},
|
|
569
|
-
[
|
|
566
|
+
[],
|
|
570
567
|
);
|
|
571
568
|
|
|
572
569
|
const handleThumbBlur = useCallback((_e: FocusEvent<HTMLDivElement>) => {
|
|
@@ -960,10 +957,8 @@ export function Slider({ref, ...props}: SliderProps) {
|
|
|
960
957
|
ref={!isRange ? ref : undefined}
|
|
961
958
|
id={!isRange ? id : undefined}
|
|
962
959
|
role="slider"
|
|
963
|
-
//
|
|
964
|
-
|
|
965
|
-
// guards in the pointer/keyboard handlers.
|
|
966
|
-
tabIndex={isDisabled && !showsDisabledMessage ? -1 : 0}
|
|
960
|
+
// A disabled thumb keeps its Tab stop (spec:AST-085 FR1).
|
|
961
|
+
tabIndex={0}
|
|
967
962
|
aria-valuemin={ariaValueMin}
|
|
968
963
|
aria-valuemax={ariaValueMax}
|
|
969
964
|
aria-valuenow={val}
|
|
@@ -999,8 +994,7 @@ export function Slider({ref, ...props}: SliderProps) {
|
|
|
999
994
|
: rtlStyles.centerInline('50%'),
|
|
1000
995
|
!isDisabled && styles.thumbHover,
|
|
1001
996
|
!isDisabled && draggingThumb === thumbIndex && styles.thumbPressed,
|
|
1002
|
-
|
|
1003
|
-
keyboardFocusThumb === thumbIndex &&
|
|
997
|
+
keyboardFocusThumb === thumbIndex &&
|
|
1004
998
|
focusOutlineStyles.focusVisible,
|
|
1005
999
|
isDisabled && styles.thumbDisabled,
|
|
1006
1000
|
),
|
|
@@ -1095,7 +1089,9 @@ export function Slider({ref, ...props}: SliderProps) {
|
|
|
1095
1089
|
}),
|
|
1096
1090
|
stylex.props(styles.sliderRow),
|
|
1097
1091
|
)}>
|
|
1092
|
+
{/* A disabled slider submits no value (spec:AST-085 FR3). */}
|
|
1098
1093
|
{htmlName != null &&
|
|
1094
|
+
!isDisabled &&
|
|
1099
1095
|
values.map((v, i) => (
|
|
1100
1096
|
<input
|
|
1101
1097
|
// Positional identity: index 0 is the start thumb, 1 the end.
|
|
@@ -1103,9 +1099,6 @@ export function Slider({ref, ...props}: SliderProps) {
|
|
|
1103
1099
|
type="hidden"
|
|
1104
1100
|
name={htmlName}
|
|
1105
1101
|
value={String(v)}
|
|
1106
|
-
// Disabled native controls are excluded from form submission;
|
|
1107
|
-
// mirror that for the hidden carrier.
|
|
1108
|
-
disabled={isDisabled}
|
|
1109
1102
|
/>
|
|
1110
1103
|
))}
|
|
1111
1104
|
<div
|