@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
|
@@ -243,12 +243,35 @@ describe('FileInput', () => {
|
|
|
243
243
|
expect(trigger).not.toHaveAttribute('aria-required');
|
|
244
244
|
});
|
|
245
245
|
|
|
246
|
-
it('
|
|
246
|
+
it('keeps a disabled input focusable and out of form submission', async () => {
|
|
247
|
+
const user = userEvent.setup();
|
|
247
248
|
render(
|
|
248
|
-
<
|
|
249
|
+
<form>
|
|
250
|
+
<FileInput
|
|
251
|
+
label="Upload"
|
|
252
|
+
isRequired
|
|
253
|
+
isDisabled
|
|
254
|
+
value={null}
|
|
255
|
+
onChange={() => {}}
|
|
256
|
+
/>
|
|
257
|
+
</form>,
|
|
249
258
|
);
|
|
250
|
-
const input =
|
|
251
|
-
|
|
259
|
+
const input =
|
|
260
|
+
document.querySelector<HTMLInputElement>('input[type="file"]')!;
|
|
261
|
+
const trigger = screen.getByRole('button', {name: 'Upload'});
|
|
262
|
+
expect(input).not.toHaveAttribute('disabled');
|
|
263
|
+
expect(trigger).not.toHaveAttribute('disabled');
|
|
264
|
+
expect(trigger).toHaveAttribute('aria-disabled', 'true');
|
|
265
|
+
const openPicker = vi.spyOn(input, 'click');
|
|
266
|
+
await user.tab();
|
|
267
|
+
expect(trigger).toHaveFocus();
|
|
268
|
+
await user.keyboard('{Enter}');
|
|
269
|
+
expect(openPicker).not.toHaveBeenCalled();
|
|
270
|
+
expect(input.form).toBeNull();
|
|
271
|
+
expect(trigger.closest('form')!.checkValidity()).toBe(true);
|
|
272
|
+
// A label click activates the hidden input directly; it is canceled so
|
|
273
|
+
// the picker never opens (fireEvent returns false when prevented).
|
|
274
|
+
expect(fireEvent.click(input)).toBe(false);
|
|
252
275
|
});
|
|
253
276
|
|
|
254
277
|
it('sets aria-invalid when status type is error', () => {
|
|
@@ -1095,7 +1118,7 @@ describe('FileInput', () => {
|
|
|
1095
1118
|
expect(clickSpy).not.toHaveBeenCalled();
|
|
1096
1119
|
});
|
|
1097
1120
|
|
|
1098
|
-
it('keeps the trigger
|
|
1121
|
+
it('keeps the trigger focusable when disabled without a reason', () => {
|
|
1099
1122
|
render(
|
|
1100
1123
|
<FileInput
|
|
1101
1124
|
label="Resume"
|
|
@@ -1105,8 +1128,8 @@ describe('FileInput', () => {
|
|
|
1105
1128
|
/>,
|
|
1106
1129
|
);
|
|
1107
1130
|
const trigger = screen.getByRole('button');
|
|
1108
|
-
expect(trigger).toHaveAttribute('tabindex', '
|
|
1109
|
-
expect(trigger).
|
|
1131
|
+
expect(trigger).toHaveAttribute('tabindex', '0');
|
|
1132
|
+
expect(trigger).toHaveAttribute('aria-disabled', 'true');
|
|
1110
1133
|
});
|
|
1111
1134
|
});
|
|
1112
1135
|
});
|
|
@@ -179,7 +179,12 @@ const styles = stylex.create({
|
|
|
179
179
|
dropzoneDisabled: {
|
|
180
180
|
cursor: 'default',
|
|
181
181
|
opacity: 0.5,
|
|
182
|
-
|
|
182
|
+
// Keeps the :focus-within border: a focused disabled trigger still shows
|
|
183
|
+
// focus (spec:AST-085 FR6).
|
|
184
|
+
borderColor: {
|
|
185
|
+
default: colorVars['--color-border-emphasized'],
|
|
186
|
+
':focus-within': colorVars['--color-accent'],
|
|
187
|
+
},
|
|
183
188
|
},
|
|
184
189
|
compact: {
|
|
185
190
|
boxSizing: 'border-box',
|
|
@@ -221,7 +226,12 @@ const styles = stylex.create({
|
|
|
221
226
|
compactDisabled: {
|
|
222
227
|
cursor: 'default',
|
|
223
228
|
opacity: 0.5,
|
|
224
|
-
|
|
229
|
+
// Keeps the :focus-within border: a focused disabled trigger still shows
|
|
230
|
+
// focus (spec:AST-085 FR6).
|
|
231
|
+
borderColor: {
|
|
232
|
+
default: colorVars['--color-border-emphasized'],
|
|
233
|
+
':focus-within': colorVars['--color-accent'],
|
|
234
|
+
},
|
|
225
235
|
},
|
|
226
236
|
hiddenInput: {
|
|
227
237
|
position: 'absolute',
|
|
@@ -857,14 +867,9 @@ export function FileInput({
|
|
|
857
867
|
<VisuallyHidden>
|
|
858
868
|
<button
|
|
859
869
|
type="button"
|
|
860
|
-
//
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
// Otherwise the native disabled attribute removes it from the tab
|
|
864
|
-
// order.
|
|
865
|
-
disabled={isDisabled && !showsDisabledMessage}
|
|
866
|
-
tabIndex={isDisabled && !showsDisabledMessage ? -1 : 0}
|
|
867
|
-
aria-disabled={showsDisabledMessage ? 'true' : undefined}
|
|
870
|
+
// A disabled trigger keeps its Tab stop (spec:AST-085 FR1).
|
|
871
|
+
tabIndex={0}
|
|
872
|
+
aria-disabled={isDisabled ? 'true' : undefined}
|
|
868
873
|
onClick={handleClick}
|
|
869
874
|
onKeyDown={handleKeyDown}
|
|
870
875
|
// aria-label suppresses child content from the accessible name, so
|
|
@@ -888,7 +893,12 @@ export function FileInput({
|
|
|
888
893
|
type="file"
|
|
889
894
|
accept={accept}
|
|
890
895
|
multiple={isMultiple}
|
|
891
|
-
disabled
|
|
896
|
+
// Detached from its form while disabled, so selected files are not
|
|
897
|
+
// submitted and `required` does not block submission (FR3).
|
|
898
|
+
form={isDisabled ? '' : undefined}
|
|
899
|
+
// A label or description click activates this input directly; cancel
|
|
900
|
+
// it so a disabled input never opens the picker (FR2).
|
|
901
|
+
onClick={isDisabled ? event => event.preventDefault() : rest.onClick}
|
|
892
902
|
onChange={handleInputChange}
|
|
893
903
|
// The visually-hidden native input is never focused (tabIndex={-1});
|
|
894
904
|
// its describedby/required/invalid live on the trigger button.
|
package/src/Link/Link.test.tsx
CHANGED
|
@@ -58,10 +58,25 @@ describe('Link', () => {
|
|
|
58
58
|
expect(handleClick).toHaveBeenCalledTimes(1);
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
-
it('button fallback
|
|
62
|
-
|
|
61
|
+
it('button fallback stays focusable and inert when isDisabled', async () => {
|
|
62
|
+
const user = userEvent.setup();
|
|
63
|
+
const onClick = vi.fn();
|
|
64
|
+
render(
|
|
65
|
+
<Link isDisabled onClick={onClick}>
|
|
66
|
+
Disabled Action
|
|
67
|
+
</Link>,
|
|
68
|
+
);
|
|
63
69
|
const button = screen.getByRole('button');
|
|
64
|
-
expect(button).
|
|
70
|
+
expect(button).not.toHaveAttribute('disabled');
|
|
71
|
+
expect(button).toHaveAttribute('aria-disabled', 'true');
|
|
72
|
+
const nativeClick = vi.fn();
|
|
73
|
+
button.addEventListener('click', nativeClick);
|
|
74
|
+
await user.tab();
|
|
75
|
+
expect(button).toHaveFocus();
|
|
76
|
+
await user.keyboard('{Enter} ');
|
|
77
|
+
expect(nativeClick).not.toHaveBeenCalled();
|
|
78
|
+
fireEvent.click(button);
|
|
79
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
65
80
|
});
|
|
66
81
|
|
|
67
82
|
it('button fallback has type="button"', () => {
|
|
@@ -156,11 +171,10 @@ describe('Link', () => {
|
|
|
156
171
|
Disabled Link
|
|
157
172
|
</Link>,
|
|
158
173
|
);
|
|
159
|
-
//
|
|
160
|
-
const link = screen.
|
|
161
|
-
expect(link).not.toBeNull();
|
|
174
|
+
// The href-less anchor keeps a Tab stop and an explicit link role.
|
|
175
|
+
const link = screen.getByRole('link', {name: 'Disabled Link'});
|
|
162
176
|
expect(link).toHaveAttribute('aria-disabled', 'true');
|
|
163
|
-
expect(link).toHaveAttribute('tabIndex', '
|
|
177
|
+
expect(link).toHaveAttribute('tabIndex', '0');
|
|
164
178
|
// Visual classes are preserved in the disabled state.
|
|
165
179
|
expect(link?.className).toContain('astryx-link');
|
|
166
180
|
});
|
package/src/Link/Link.tsx
CHANGED
|
@@ -48,6 +48,7 @@ import {focusOutlineProps} from '../utils/focusOutline.stylex';
|
|
|
48
48
|
import {interactionOverlayStyles} from '../utils/interactionOverlay.stylex';
|
|
49
49
|
import {usePressFeedback} from '../hooks/usePressFeedback';
|
|
50
50
|
import {useTranslator} from '../i18n';
|
|
51
|
+
import {disabledInteractionProps} from '../utils/disabledInteraction';
|
|
51
52
|
|
|
52
53
|
/**
|
|
53
54
|
* Base link styles
|
|
@@ -174,9 +175,8 @@ export interface LinkProps extends BaseProps<
|
|
|
174
175
|
/**
|
|
175
176
|
* Whether the link is disabled.
|
|
176
177
|
* A disabled link renders as a plain anchor without an href (and without
|
|
177
|
-
* target/rel/onClick), so it cannot
|
|
178
|
-
*
|
|
179
|
-
* technology activation.
|
|
178
|
+
* target/rel/onClick), so it cannot navigate or run onClick. It keeps a tab
|
|
179
|
+
* stop and is announced as a disabled link.
|
|
180
180
|
* @default false
|
|
181
181
|
*/
|
|
182
182
|
isDisabled?: boolean;
|
|
@@ -264,15 +264,6 @@ export interface LinkProps extends BaseProps<
|
|
|
264
264
|
children: ReactNode;
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
-
/**
|
|
268
|
-
* Click handler for disabled links. The disabled anchor renders without an
|
|
269
|
-
* href, so there is no navigation to block in practice; preventDefault is a
|
|
270
|
-
* defensive guard against synthetic/programmatic clicks.
|
|
271
|
-
*/
|
|
272
|
-
function preventDefaultClick(event: React.MouseEvent<HTMLAnchorElement>): void {
|
|
273
|
-
event.preventDefault();
|
|
274
|
-
}
|
|
275
|
-
|
|
276
267
|
/**
|
|
277
268
|
* A styled anchor link component.
|
|
278
269
|
*
|
|
@@ -367,8 +358,6 @@ export function Link({
|
|
|
367
358
|
{...(isDisabled ? undefined : pressable)}
|
|
368
359
|
aria-label={label || undefined}
|
|
369
360
|
aria-disabled={isDisabled || undefined}
|
|
370
|
-
tabIndex={isDisabled ? -1 : undefined}
|
|
371
|
-
disabled={isDisabled}
|
|
372
361
|
{...mergeProps(
|
|
373
362
|
themeProps('link', {color}),
|
|
374
363
|
focusOutlineProps.focusVisible(
|
|
@@ -386,24 +375,26 @@ export function Link({
|
|
|
386
375
|
className,
|
|
387
376
|
style,
|
|
388
377
|
)}
|
|
389
|
-
{...props}
|
|
378
|
+
{...props}
|
|
379
|
+
// Focusable while disabled (spec:AST-085 FR1), but inert (FR2).
|
|
380
|
+
{...(isDisabled ? disabledInteractionProps(props.onKeyDown) : null)}>
|
|
390
381
|
{sharedContent}
|
|
391
382
|
</button>
|
|
392
383
|
);
|
|
393
384
|
} else if (isDisabled) {
|
|
394
|
-
// A disabled link renders as
|
|
395
|
-
//
|
|
396
|
-
//
|
|
397
|
-
//
|
|
398
|
-
//
|
|
399
|
-
//
|
|
385
|
+
// A disabled link renders as an <a> with no href, so nothing can navigate
|
|
386
|
+
// and the consumer onClick never fires. It keeps a Tab stop and the link
|
|
387
|
+
// role so keyboard and screen-reader users still find it (spec:AST-085
|
|
388
|
+
// FR1, FR5). The router LinkComponent is deliberately skipped — a disabled
|
|
389
|
+
// link performs no navigation, and custom router links may require a live
|
|
390
|
+
// href. target/rel are omitted with the href.
|
|
400
391
|
linkElement = (
|
|
401
392
|
<a
|
|
402
393
|
ref={ref as React.Ref<HTMLAnchorElement>}
|
|
403
|
-
|
|
394
|
+
role="link"
|
|
404
395
|
aria-label={label || undefined}
|
|
405
396
|
aria-disabled={true}
|
|
406
|
-
tabIndex={
|
|
397
|
+
tabIndex={0}
|
|
407
398
|
{...mergeProps(
|
|
408
399
|
themeProps('link', {color}),
|
|
409
400
|
focusOutlineProps.focusVisible(
|
|
@@ -417,7 +408,8 @@ export function Link({
|
|
|
417
408
|
className,
|
|
418
409
|
style,
|
|
419
410
|
)}
|
|
420
|
-
{...props}
|
|
411
|
+
{...props}
|
|
412
|
+
{...disabledInteractionProps(props.onKeyDown)}>
|
|
421
413
|
{sharedContent}
|
|
422
414
|
</a>
|
|
423
415
|
);
|
|
@@ -73,7 +73,7 @@ export const LINK_A11Y_EXCLUSIONS = [
|
|
|
73
73
|
{
|
|
74
74
|
owner: 'Link component tests and family:navigation-destinations FR7',
|
|
75
75
|
reason:
|
|
76
|
-
'A disabled Link renders
|
|
76
|
+
'A disabled Link renders a focusable, href-less anchor with an explicit link role and aria-disabled, so it cannot navigate. That disabled semantics is member-owned, so disabled states are not bound to this available-link contract.',
|
|
77
77
|
},
|
|
78
78
|
{
|
|
79
79
|
owner: 'Link component tests (button rendering)',
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Markdown.rerender.test.tsx
|
|
5
|
+
* @input Uses vitest, @testing-library/react, Markdown, a synthetic long document
|
|
6
|
+
* @output Tests that an unrelated caller re-render reuses the rendered document
|
|
7
|
+
* @position Performance regression evidence for long documents. Counting
|
|
8
|
+
* renders of a caller-supplied block renderer is deterministic, unlike a
|
|
9
|
+
* timing budget, and fails when Markdown rebuilds every block element on a
|
|
10
|
+
* re-render that changed nothing it reads.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {useState} from 'react';
|
|
14
|
+
import {act, cleanup, fireEvent, render} from '@testing-library/react';
|
|
15
|
+
import {afterEach, describe, expect, it, vi} from 'vitest';
|
|
16
|
+
import {Markdown} from './Markdown';
|
|
17
|
+
import type {MarkdownComponents} from './Markdown';
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
cleanup();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/** A long, review-style document: headings, prose, lists, a table, code. */
|
|
24
|
+
function longDocument(sections: number): string {
|
|
25
|
+
return Array.from({length: sections}, (_, i) =>
|
|
26
|
+
[
|
|
27
|
+
`## Section ${i}`,
|
|
28
|
+
'',
|
|
29
|
+
`Paragraph ${i} with **bold**, *emphasis*, \`code\` and a [link](/docs/${i}).`,
|
|
30
|
+
'',
|
|
31
|
+
`- Point ${i}`,
|
|
32
|
+
` - Nested point ${i}`,
|
|
33
|
+
`- [x] Done ${i}`,
|
|
34
|
+
'',
|
|
35
|
+
'| Name | Count |',
|
|
36
|
+
'| --- | ---: |',
|
|
37
|
+
`| row ${i} | ${i * 3} |`,
|
|
38
|
+
'',
|
|
39
|
+
'```ts',
|
|
40
|
+
`const value${i} = ${i};`,
|
|
41
|
+
'```',
|
|
42
|
+
].join('\n'),
|
|
43
|
+
).join('\n\n');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const SECTIONS = 40;
|
|
47
|
+
|
|
48
|
+
function createCountingComponents(): {
|
|
49
|
+
components: Partial<MarkdownComponents>;
|
|
50
|
+
renders: () => number;
|
|
51
|
+
} {
|
|
52
|
+
let count = 0;
|
|
53
|
+
function CountingCode({code}: {code: string; language?: string}) {
|
|
54
|
+
count++;
|
|
55
|
+
return <pre data-testid="code">{code}</pre>;
|
|
56
|
+
}
|
|
57
|
+
return {components: {code: CountingCode}, renders: () => count};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe('Markdown re-render reuse', () => {
|
|
61
|
+
it('does not re-render any block when a caller re-render changes nothing Markdown reads', () => {
|
|
62
|
+
const {components, renders} = createCountingComponents();
|
|
63
|
+
const source = longDocument(SECTIONS);
|
|
64
|
+
let bump: () => void = () => {};
|
|
65
|
+
function Page() {
|
|
66
|
+
const [tick, setTick] = useState(0);
|
|
67
|
+
bump = () => setTick(t => t + 1);
|
|
68
|
+
return (
|
|
69
|
+
<section data-tick={tick}>
|
|
70
|
+
{/* A new link handler each render, as inline callers write it. */}
|
|
71
|
+
<Markdown components={components} onLinkClick={() => {}}>
|
|
72
|
+
{source}
|
|
73
|
+
</Markdown>
|
|
74
|
+
</section>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
const {container} = render(<Page />);
|
|
78
|
+
const document = container.querySelector('[role="document"]');
|
|
79
|
+
const html = document?.innerHTML;
|
|
80
|
+
expect(renders()).toBe(SECTIONS);
|
|
81
|
+
|
|
82
|
+
act(() => bump());
|
|
83
|
+
act(() => bump());
|
|
84
|
+
|
|
85
|
+
expect(renders()).toBe(SECTIONS);
|
|
86
|
+
expect(container.querySelector('[role="document"]')).toBe(document);
|
|
87
|
+
expect(document?.innerHTML).toBe(html);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('re-renders when the text changes', () => {
|
|
91
|
+
const {components, renders} = createCountingComponents();
|
|
92
|
+
const {rerender, getAllByTestId} = render(
|
|
93
|
+
<Markdown components={components}>{longDocument(2)}</Markdown>,
|
|
94
|
+
);
|
|
95
|
+
expect(renders()).toBe(2);
|
|
96
|
+
rerender(
|
|
97
|
+
<Markdown components={components}>
|
|
98
|
+
{`${longDocument(2)}\n\n\`\`\`ts\nconst added = true;\n\`\`\``}
|
|
99
|
+
</Markdown>,
|
|
100
|
+
);
|
|
101
|
+
expect(getAllByTestId('code')).toHaveLength(3);
|
|
102
|
+
expect(getAllByTestId('code')[2].textContent).toBe('const added = true;');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('re-renders when a rendering prop changes', () => {
|
|
106
|
+
const {container, rerender} = render(
|
|
107
|
+
<Markdown headingLevelStart={1}>{'# Title'}</Markdown>,
|
|
108
|
+
);
|
|
109
|
+
expect(container.querySelector('h1')).not.toBeNull();
|
|
110
|
+
rerender(<Markdown headingLevelStart={3}>{'# Title'}</Markdown>);
|
|
111
|
+
expect(container.querySelector('h1')).toBeNull();
|
|
112
|
+
expect(container.querySelector('h3')?.textContent).toBe('Title');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('calls the latest link handler after a re-render that reused the document', () => {
|
|
116
|
+
const first = vi.fn();
|
|
117
|
+
const latest = vi.fn(() => false as const);
|
|
118
|
+
const {container, rerender} = render(
|
|
119
|
+
<Markdown onLinkClick={first}>{'[go](/next)'}</Markdown>,
|
|
120
|
+
);
|
|
121
|
+
rerender(<Markdown onLinkClick={latest}>{'[go](/next)'}</Markdown>);
|
|
122
|
+
const link = container.querySelector('a');
|
|
123
|
+
expect(link).not.toBeNull();
|
|
124
|
+
const event = new MouseEvent('click', {bubbles: true, cancelable: true});
|
|
125
|
+
act(() => {
|
|
126
|
+
link?.dispatchEvent(event);
|
|
127
|
+
});
|
|
128
|
+
expect(first).not.toHaveBeenCalled();
|
|
129
|
+
expect(latest).toHaveBeenCalledWith('/next', expect.anything());
|
|
130
|
+
expect(event.defaultPrevented).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('starts and stops handling link clicks when the handler is added or removed', () => {
|
|
134
|
+
const handler = vi.fn();
|
|
135
|
+
const {container, rerender} = render(<Markdown>{'[go](/next)'}</Markdown>);
|
|
136
|
+
rerender(<Markdown onLinkClick={handler}>{'[go](/next)'}</Markdown>);
|
|
137
|
+
fireEvent.click(container.querySelector('a') as HTMLAnchorElement);
|
|
138
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
139
|
+
rerender(<Markdown>{'[go](/next)'}</Markdown>);
|
|
140
|
+
fireEvent.click(container.querySelector('a') as HTMLAnchorElement);
|
|
141
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -41,6 +41,7 @@ import {TableHeaderCell} from '../Table/TableHeaderCell';
|
|
|
41
41
|
import {TableHeader} from '../Table/TableHeader';
|
|
42
42
|
import {TableBody} from '../Table/TableBody';
|
|
43
43
|
import {mergeProps} from '../utils';
|
|
44
|
+
import {useIsomorphicLayoutEffect} from '../hooks/useIsomorphicLayoutEffect';
|
|
44
45
|
import {useStreamingText} from '../hooks/useStreamingText';
|
|
45
46
|
import {computeBoundaries, computeSegments} from './streaming';
|
|
46
47
|
import type {BaseProps} from '../BaseProps';
|
|
@@ -2122,6 +2123,21 @@ export function Markdown<
|
|
|
2122
2123
|
}: MarkdownProps<Plugins>): React.ReactElement {
|
|
2123
2124
|
const t = useTranslator();
|
|
2124
2125
|
const LinkComponent = useLinkComponent();
|
|
2126
|
+
// Links read the caller's handler when clicked, so a handler recreated on
|
|
2127
|
+
// every caller render does not invalidate the memoized document below.
|
|
2128
|
+
// Only whether a handler exists changes the rendered links.
|
|
2129
|
+
const onLinkClickRef = useRef(onLinkClick);
|
|
2130
|
+
useIsomorphicLayoutEffect(() => {
|
|
2131
|
+
onLinkClickRef.current = onLinkClick;
|
|
2132
|
+
});
|
|
2133
|
+
const hasLinkClick = onLinkClick != null;
|
|
2134
|
+
const stableOnLinkClick = useMemo<MarkdownProps['onLinkClick']>(
|
|
2135
|
+
() =>
|
|
2136
|
+
hasLinkClick
|
|
2137
|
+
? (href, event) => onLinkClickRef.current?.(href, event)
|
|
2138
|
+
: undefined,
|
|
2139
|
+
[hasLinkClick],
|
|
2140
|
+
);
|
|
2125
2141
|
// Derive the set of source IDs for the parser (stable across renders when sources don't change)
|
|
2126
2142
|
const sourceIds = useMemo(
|
|
2127
2143
|
() => (sources ? new Set(Object.keys(sources)) : undefined),
|
|
@@ -2327,19 +2343,80 @@ export function Markdown<
|
|
|
2327
2343
|
return next;
|
|
2328
2344
|
}, [display, smoothedLen, maxSpans]);
|
|
2329
2345
|
|
|
2330
|
-
const
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
};
|
|
2335
|
-
|
|
2336
|
-
// Build citation context — numbers are assigned in encounter order during rendering.
|
|
2337
|
-
// This is recreated each render so numbering stays consistent with the AST.
|
|
2338
|
-
const citationCtx: CitationContext | null = sources
|
|
2339
|
-
? {sources, numberMap: new Map(), nextNumber: 1, style: citationStyle}
|
|
2346
|
+
const contentWidthValue = contentWidth
|
|
2347
|
+
? typeof contentWidth === 'number'
|
|
2348
|
+
? `${contentWidth}px`
|
|
2349
|
+
: contentWidth
|
|
2340
2350
|
: null;
|
|
2341
2351
|
|
|
2352
|
+
// The block document is the expensive part of a long Markdown render. It
|
|
2353
|
+
// depends only on these inputs, so a caller re-render that changes none of
|
|
2354
|
+
// them returns the same elements and React skips the whole subtree. The
|
|
2355
|
+
// cursor and citation numbering are rebuilt inside, from the start of the
|
|
2356
|
+
// document, exactly as a full render builds them.
|
|
2357
|
+
const renderedBlocks = useMemo(() => {
|
|
2358
|
+
if (display === 'inline') {
|
|
2359
|
+
return null;
|
|
2360
|
+
}
|
|
2361
|
+
const blockCursor: StreamingCursor = {
|
|
2362
|
+
offset: 0,
|
|
2363
|
+
boundaries,
|
|
2364
|
+
active: isStreaming,
|
|
2365
|
+
};
|
|
2366
|
+
// Numbers are assigned in encounter order during rendering.
|
|
2367
|
+
const blockCitationCtx: CitationContext | null = sources
|
|
2368
|
+
? {sources, numberMap: new Map(), nextNumber: 1, style: citationStyle}
|
|
2369
|
+
: null;
|
|
2370
|
+
return blocks.map((block, i) =>
|
|
2371
|
+
renderBlock(
|
|
2372
|
+
block,
|
|
2373
|
+
i,
|
|
2374
|
+
blocks.length,
|
|
2375
|
+
density,
|
|
2376
|
+
headingLevelStart,
|
|
2377
|
+
stableOnLinkClick,
|
|
2378
|
+
blockCursor,
|
|
2379
|
+
blockCitationCtx,
|
|
2380
|
+
contentWidthValue,
|
|
2381
|
+
contentAlign,
|
|
2382
|
+
LinkComponent,
|
|
2383
|
+
inlinePlugins,
|
|
2384
|
+
components,
|
|
2385
|
+
preparedPlugins,
|
|
2386
|
+
t,
|
|
2387
|
+
headingProjection,
|
|
2388
|
+
),
|
|
2389
|
+
);
|
|
2390
|
+
}, [
|
|
2391
|
+
display,
|
|
2392
|
+
blocks,
|
|
2393
|
+
boundaries,
|
|
2394
|
+
isStreaming,
|
|
2395
|
+
sources,
|
|
2396
|
+
citationStyle,
|
|
2397
|
+
density,
|
|
2398
|
+
headingLevelStart,
|
|
2399
|
+
stableOnLinkClick,
|
|
2400
|
+
contentWidthValue,
|
|
2401
|
+
contentAlign,
|
|
2402
|
+
LinkComponent,
|
|
2403
|
+
inlinePlugins,
|
|
2404
|
+
components,
|
|
2405
|
+
preparedPlugins,
|
|
2406
|
+
t,
|
|
2407
|
+
headingProjection,
|
|
2408
|
+
]);
|
|
2409
|
+
|
|
2342
2410
|
if (display === 'inline') {
|
|
2411
|
+
const cursor: StreamingCursor = {
|
|
2412
|
+
offset: 0,
|
|
2413
|
+
boundaries,
|
|
2414
|
+
active: isStreaming,
|
|
2415
|
+
};
|
|
2416
|
+
// Numbers are assigned in encounter order during rendering.
|
|
2417
|
+
const citationCtx: CitationContext | null = sources
|
|
2418
|
+
? {sources, numberMap: new Map(), nextNumber: 1, style: citationStyle}
|
|
2419
|
+
: null;
|
|
2343
2420
|
const renderedInline = (
|
|
2344
2421
|
<span
|
|
2345
2422
|
ref={ref}
|
|
@@ -2357,7 +2434,7 @@ export function Markdown<
|
|
|
2357
2434
|
renderInline(
|
|
2358
2435
|
node,
|
|
2359
2436
|
i,
|
|
2360
|
-
|
|
2437
|
+
stableOnLinkClick,
|
|
2361
2438
|
cursor,
|
|
2362
2439
|
citationCtx,
|
|
2363
2440
|
LinkComponent,
|
|
@@ -2391,30 +2468,7 @@ export function Markdown<
|
|
|
2391
2468
|
className,
|
|
2392
2469
|
style,
|
|
2393
2470
|
)}>
|
|
2394
|
-
{
|
|
2395
|
-
renderBlock(
|
|
2396
|
-
block,
|
|
2397
|
-
i,
|
|
2398
|
-
blocks.length,
|
|
2399
|
-
density,
|
|
2400
|
-
headingLevelStart,
|
|
2401
|
-
onLinkClick,
|
|
2402
|
-
cursor,
|
|
2403
|
-
citationCtx,
|
|
2404
|
-
contentWidth
|
|
2405
|
-
? typeof contentWidth === 'number'
|
|
2406
|
-
? `${contentWidth}px`
|
|
2407
|
-
: contentWidth
|
|
2408
|
-
: null,
|
|
2409
|
-
contentAlign,
|
|
2410
|
-
LinkComponent,
|
|
2411
|
-
inlinePlugins,
|
|
2412
|
-
components,
|
|
2413
|
-
preparedPlugins,
|
|
2414
|
-
t,
|
|
2415
|
-
headingProjection,
|
|
2416
|
-
),
|
|
2417
|
-
)}
|
|
2471
|
+
{renderedBlocks}
|
|
2418
2472
|
</div>
|
|
2419
2473
|
);
|
|
2420
2474
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file MultiSelector.disabledFocus.a11y.chromium.spec.ts
|
|
5
|
+
* @input The `Core/MultiSelector` Default story in a built Storybook.
|
|
6
|
+
* @output Real-engine proof of `spec:AST-085` FR4: a trigger disabled while it
|
|
7
|
+
* holds keyboard focus keeps focus and reports itself disabled.
|
|
8
|
+
* @position Run with `pnpm test:a11y-contract`; a DOM emulator does not move
|
|
9
|
+
* focus off an element that becomes disabled, so it cannot show the loss.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {expect, test} from '@playwright/test';
|
|
13
|
+
import {
|
|
14
|
+
DEFAULT_STORYBOOK_DIR,
|
|
15
|
+
serveStorybook,
|
|
16
|
+
type StaticServer,
|
|
17
|
+
} from '@astryxdesign/a11y-spec/storybook';
|
|
18
|
+
|
|
19
|
+
let storybook: StaticServer;
|
|
20
|
+
|
|
21
|
+
test.beforeAll(async () => {
|
|
22
|
+
storybook = await serveStorybook(
|
|
23
|
+
process.env.ASTRYX_STORYBOOK_DIR ?? DEFAULT_STORYBOOK_DIR,
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test.afterAll(async () => {
|
|
28
|
+
await storybook?.close();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('a focused trigger keeps focus when it becomes disabled', async ({
|
|
32
|
+
page,
|
|
33
|
+
}) => {
|
|
34
|
+
const storyId = 'core-multiselector--default';
|
|
35
|
+
await page.goto(
|
|
36
|
+
`${storybook.origin}/iframe.html?id=${storyId}&viewMode=story`,
|
|
37
|
+
);
|
|
38
|
+
const trigger = page.getByRole('combobox', {name: 'Columns'});
|
|
39
|
+
await trigger.focus();
|
|
40
|
+
await expect(trigger).toBeFocused();
|
|
41
|
+
|
|
42
|
+
await page.evaluate(id => {
|
|
43
|
+
const channel = (
|
|
44
|
+
window as unknown as {
|
|
45
|
+
__STORYBOOK_ADDONS_CHANNEL__?: {
|
|
46
|
+
emit: (event: string, payload: unknown) => void;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
).__STORYBOOK_ADDONS_CHANNEL__;
|
|
50
|
+
if (channel == null) {
|
|
51
|
+
throw new Error('Storybook channel is unavailable');
|
|
52
|
+
}
|
|
53
|
+
channel.emit('updateStoryArgs', {
|
|
54
|
+
storyId: id,
|
|
55
|
+
updatedArgs: {isDisabled: true},
|
|
56
|
+
});
|
|
57
|
+
}, storyId);
|
|
58
|
+
|
|
59
|
+
// Wait for the disabled re-render in either form, so focus is checked
|
|
60
|
+
// after it lands.
|
|
61
|
+
await expect
|
|
62
|
+
.poll(async () =>
|
|
63
|
+
trigger.evaluate(
|
|
64
|
+
el =>
|
|
65
|
+
el.hasAttribute('disabled') ||
|
|
66
|
+
el.getAttribute('aria-disabled') === 'true',
|
|
67
|
+
),
|
|
68
|
+
)
|
|
69
|
+
.toBe(true);
|
|
70
|
+
await expect(trigger).toBeFocused();
|
|
71
|
+
});
|