@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
|
@@ -278,7 +278,7 @@ describe('CheckboxInput', () => {
|
|
|
278
278
|
);
|
|
279
279
|
const checkbox = screen.getByRole('checkbox');
|
|
280
280
|
expect(checkbox).toBeChecked();
|
|
281
|
-
expect(checkbox).
|
|
281
|
+
expect(checkbox).toHaveAttribute('aria-disabled', 'true');
|
|
282
282
|
expect(checkbox).toHaveAttribute('type', 'checkbox');
|
|
283
283
|
});
|
|
284
284
|
|
|
@@ -588,16 +588,55 @@ describe('CheckboxInput', () => {
|
|
|
588
588
|
expect(checkbox).not.toBeChecked();
|
|
589
589
|
});
|
|
590
590
|
|
|
591
|
-
it('
|
|
591
|
+
it('stays focusable via aria-disabled when disabled without a reason', async () => {
|
|
592
|
+
const user = userEvent.setup();
|
|
593
|
+
const onChange = vi.fn();
|
|
594
|
+
const onClick = vi.fn();
|
|
595
|
+
render(
|
|
596
|
+
<form>
|
|
597
|
+
<CheckboxInput
|
|
598
|
+
label="Accept terms"
|
|
599
|
+
value={false}
|
|
600
|
+
onChange={onChange}
|
|
601
|
+
onClick={onClick}
|
|
602
|
+
htmlName="terms"
|
|
603
|
+
isRequired
|
|
604
|
+
isDisabled
|
|
605
|
+
/>
|
|
606
|
+
</form>,
|
|
607
|
+
);
|
|
608
|
+
const checkbox = screen.getByRole('checkbox');
|
|
609
|
+
expect(checkbox).not.toHaveAttribute('disabled');
|
|
610
|
+
expect(checkbox).toHaveAttribute('aria-disabled', 'true');
|
|
611
|
+
await user.tab();
|
|
612
|
+
expect(checkbox).toHaveFocus();
|
|
613
|
+
await user.keyboard(' ');
|
|
614
|
+
await user.click(checkbox);
|
|
615
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
616
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
617
|
+
expect(checkbox).not.toBeChecked();
|
|
618
|
+
const form = checkbox.closest('form')!;
|
|
619
|
+
expect(form.checkValidity()).toBe(true);
|
|
620
|
+
expect([...new FormData(form).keys()]).toEqual([]);
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
it('keeps a disabled indeterminate checkbox mixed after Space or a click', async () => {
|
|
624
|
+
const user = userEvent.setup();
|
|
592
625
|
render(
|
|
593
626
|
<CheckboxInput
|
|
594
|
-
label="
|
|
595
|
-
value=
|
|
627
|
+
label="Select all"
|
|
628
|
+
value="indeterminate"
|
|
596
629
|
onChange={() => {}}
|
|
597
630
|
isDisabled
|
|
598
631
|
/>,
|
|
599
632
|
);
|
|
600
|
-
|
|
633
|
+
const checkbox = screen.getByRole<HTMLInputElement>('checkbox');
|
|
634
|
+
expect(checkbox.indeterminate).toBe(true);
|
|
635
|
+
await user.tab();
|
|
636
|
+
await user.keyboard(' ');
|
|
637
|
+
fireEvent.click(checkbox);
|
|
638
|
+
expect(checkbox.indeterminate).toBe(true);
|
|
639
|
+
expect(checkbox).not.toBeChecked();
|
|
601
640
|
});
|
|
602
641
|
});
|
|
603
642
|
describe('form participation', () => {
|
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
import {
|
|
20
20
|
useId,
|
|
21
21
|
useCallback,
|
|
22
|
-
use,
|
|
23
22
|
useOptimistic,
|
|
24
23
|
useRef,
|
|
25
24
|
useTransition,
|
|
@@ -52,7 +51,6 @@ import {usePressFeedback} from '../hooks/usePressFeedback';
|
|
|
52
51
|
import {useResolvedRequired} from '../hooks/useResolvedRequired';
|
|
53
52
|
import {useIndicator} from '../Indicator';
|
|
54
53
|
import {themeProps} from '../utils/themeProps';
|
|
55
|
-
import {CheckboxListContext} from '../CheckboxList/CheckboxListContext';
|
|
56
54
|
|
|
57
55
|
import {useMergedRefs} from '../hooks/useMergedRefs';
|
|
58
56
|
|
|
@@ -354,21 +352,10 @@ export function CheckboxInput({
|
|
|
354
352
|
const [optimisticValue, setOptimisticValue] = useOptimistic(value);
|
|
355
353
|
const isBusy = isLoading || optimisticValue !== value;
|
|
356
354
|
|
|
357
|
-
//
|
|
358
|
-
//
|
|
359
|
-
//
|
|
360
|
-
// attribute. Value mutation is blocked by the isDisabled guard in onChange.
|
|
355
|
+
// A disabled checkbox stays focusable via aria-disabled instead of the
|
|
356
|
+
// native disabled attribute (spec:AST-085 FR1). Value mutation is blocked by
|
|
357
|
+
// the click and onChange guards on the input.
|
|
361
358
|
const showsDisabledMessage = isDisabled && !!disabledMessage;
|
|
362
|
-
// Keep the native checkbox focusable via aria-disabled either when it renders
|
|
363
|
-
// its own reason tooltip, or when it sits in a CheckboxList whose whole-group
|
|
364
|
-
// `disabledMessage` (shown on the group container) needs each checkbox to
|
|
365
|
-
// stay keyboard-perceivable. The group signals this through context rather
|
|
366
|
-
// than a public prop.
|
|
367
|
-
const checkboxListContext = use(CheckboxListContext);
|
|
368
|
-
const isFocusableDisabled =
|
|
369
|
-
isDisabled &&
|
|
370
|
-
(showsDisabledMessage ||
|
|
371
|
-
(checkboxListContext?.hasDisabledMessage ?? false));
|
|
372
359
|
const disabledMessageTooltip = useTooltip({
|
|
373
360
|
placement: 'above',
|
|
374
361
|
// The container row is not naturally focusable; focusin bubbles up from the
|
|
@@ -384,7 +371,7 @@ export function CheckboxInput({
|
|
|
384
371
|
// `opacity: 0`, and only the indicator's own element can shape the outline
|
|
385
372
|
// to match it. See useIndicatorFocusRing.
|
|
386
373
|
const indicatorRef = useRef<HTMLSpanElement>(null);
|
|
387
|
-
const {focusProps} = useIndicatorFocusRing(indicatorRef
|
|
374
|
+
const {focusProps} = useIndicatorFocusRing(indicatorRef);
|
|
388
375
|
|
|
389
376
|
const isIndeterminate = optimisticValue === 'indeterminate';
|
|
390
377
|
const isChecked = optimisticValue === true;
|
|
@@ -471,22 +458,25 @@ export function CheckboxInput({
|
|
|
471
458
|
)}
|
|
472
459
|
id={id}
|
|
473
460
|
type="checkbox"
|
|
474
|
-
// Withhold the name while disabled:
|
|
475
|
-
//
|
|
476
|
-
// control must not submit.
|
|
461
|
+
// Withhold the name while disabled: the input stays focusable (not
|
|
462
|
+
// natively disabled), and a disabled control must not submit.
|
|
477
463
|
name={isDisabled ? undefined : htmlName}
|
|
478
464
|
checked={isChecked}
|
|
479
|
-
|
|
480
|
-
//
|
|
481
|
-
//
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
465
|
+
aria-disabled={isDisabled ? 'true' : undefined}
|
|
466
|
+
// Detached from its form while disabled, so it neither submits nor
|
|
467
|
+
// blocks submission through `required` (spec:AST-085 FR3).
|
|
468
|
+
form={isDisabled ? '' : undefined}
|
|
469
|
+
// Native `disabled` suppressed onClick (FR2); preventDefault here
|
|
470
|
+
// would desync React's controlled checked state.
|
|
471
|
+
onClick={isDisabled ? undefined : rest.onClick}
|
|
485
472
|
readOnly={isReadOnly}
|
|
486
473
|
required={isRequired}
|
|
487
474
|
aria-required={isEffectivelyRequired ? 'true' : undefined}
|
|
488
475
|
onChange={e => {
|
|
489
476
|
if (isDisabled || isBusy || isReadOnly) {
|
|
477
|
+
// Activation clears the native mixed state; React restores
|
|
478
|
+
// only `checked`, so restore `indeterminate` too.
|
|
479
|
+
e.target.indeterminate = isIndeterminate;
|
|
490
480
|
return;
|
|
491
481
|
}
|
|
492
482
|
const checked = e.target.checked;
|
|
@@ -125,8 +125,8 @@ export const CHECKBOX_BINDING_STATES = [
|
|
|
125
125
|
{
|
|
126
126
|
id: 'input-disabled',
|
|
127
127
|
binding: 'CheckboxInput',
|
|
128
|
-
summary: 'a
|
|
129
|
-
facts: facts({operable: false, focusable:
|
|
128
|
+
summary: 'a disabled checkbox that keeps its tab stop',
|
|
129
|
+
facts: facts({operable: false, focusable: true, disabled: true}),
|
|
130
130
|
visibleLabel: 'Managed setting',
|
|
131
131
|
storyId: 'a11y-checkbox-pattern--input-disabled',
|
|
132
132
|
},
|
|
@@ -269,7 +269,7 @@ export const CHECKBOX_BINDING_STATES = [
|
|
|
269
269
|
id: 'list-item-disabled',
|
|
270
270
|
binding: 'CheckboxListItem',
|
|
271
271
|
summary: 'an individually disabled list item',
|
|
272
|
-
facts: facts({operable: false, focusable:
|
|
272
|
+
facts: facts({operable: false, focusable: true, disabled: true}),
|
|
273
273
|
visibleLabel: 'SMS',
|
|
274
274
|
visibleLabelSelector: '[data-a11y-visible-label]',
|
|
275
275
|
storyId: 'a11y-checkbox-pattern--list-item-disabled',
|
|
@@ -288,8 +288,9 @@ describe('CheckboxList', () => {
|
|
|
288
288
|
</CheckboxList>,
|
|
289
289
|
);
|
|
290
290
|
const checkboxes = screen.getAllByRole('checkbox');
|
|
291
|
-
expect(checkboxes[0]).
|
|
292
|
-
expect(checkboxes[1]).
|
|
291
|
+
expect(checkboxes[0]).toHaveAttribute('aria-disabled', 'true');
|
|
292
|
+
expect(checkboxes[1]).toHaveAttribute('aria-disabled', 'true');
|
|
293
|
+
expect(checkboxes[0]).not.toHaveAttribute('disabled');
|
|
293
294
|
});
|
|
294
295
|
|
|
295
296
|
it('does not call onChange when group is disabled', () => {
|
|
@@ -317,8 +318,9 @@ describe('CheckboxList', () => {
|
|
|
317
318
|
</CheckboxList>,
|
|
318
319
|
);
|
|
319
320
|
const checkboxes = screen.getAllByRole('checkbox');
|
|
320
|
-
expect(checkboxes[0]).not.
|
|
321
|
-
expect(checkboxes[1]).
|
|
321
|
+
expect(checkboxes[0]).not.toHaveAttribute('aria-disabled');
|
|
322
|
+
expect(checkboxes[1]).toHaveAttribute('aria-disabled', 'true');
|
|
323
|
+
expect(checkboxes[1]).not.toHaveAttribute('disabled');
|
|
322
324
|
});
|
|
323
325
|
|
|
324
326
|
it('throws when item has no value prop inside collection-mode CheckboxList', () => {
|
|
@@ -1137,7 +1139,7 @@ describe('CheckboxListItem ARIA props', () => {
|
|
|
1137
1139
|
expect(onChange).not.toHaveBeenCalled();
|
|
1138
1140
|
});
|
|
1139
1141
|
|
|
1140
|
-
it('keeps checkboxes
|
|
1142
|
+
it('keeps checkboxes focusable via aria-disabled when disabled without a reason', () => {
|
|
1141
1143
|
render(
|
|
1142
1144
|
<CheckboxList
|
|
1143
1145
|
label="Notifications"
|
|
@@ -1149,7 +1151,8 @@ describe('CheckboxListItem ARIA props', () => {
|
|
|
1149
1151
|
</CheckboxList>,
|
|
1150
1152
|
);
|
|
1151
1153
|
for (const checkbox of screen.getAllByRole('checkbox', h)) {
|
|
1152
|
-
expect(checkbox).
|
|
1154
|
+
expect(checkbox).not.toHaveAttribute('disabled');
|
|
1155
|
+
expect(checkbox).toHaveAttribute('aria-disabled', 'true');
|
|
1153
1156
|
}
|
|
1154
1157
|
});
|
|
1155
1158
|
});
|
|
@@ -24,7 +24,7 @@ export const docs = {
|
|
|
24
24
|
{name: 'onClick', type: '(event: MouseEvent) => void', description: 'Click handler: fires on card surface only.'},
|
|
25
25
|
{name: 'href', type: 'string', description: 'Navigation URL. Plain, new-tab, Cmd/Ctrl-click, and middle-click activation all follow the shared navigation rule described on the Link `href` prop.'},
|
|
26
26
|
{name: 'target', type: 'string', description: 'Link target.', default: "'_self'"},
|
|
27
|
-
{name: 'isDisabled', type: 'boolean', description: 'Disables the card
|
|
27
|
+
{name: 'isDisabled', type: 'boolean', description: 'Disables the card and removes a link destination; the card keeps its tab stop and is announced as disabled.', default: 'false'},
|
|
28
28
|
{name: 'children', type: 'ReactNode', description: 'Card content.'},
|
|
29
29
|
{name: 'padding', type: '0 | 0.5 | 1 | 1.5 | 2 | 3 | 4 | 5 | 6 | 8 | 10', description: 'Inner padding.', default: '4'},
|
|
30
30
|
{name: 'variant', type: "'default' | 'transparent' | 'muted' | 'blue' | 'cyan' | 'gray' | 'green' | 'orange' | 'pink' | 'purple' | 'red' | 'teal' | 'yellow'", description: 'Background color variant.', default: "'default'"},
|
|
@@ -75,7 +75,7 @@ export const docsDense = {
|
|
|
75
75
|
onClick: 'click handler: fires on card surface only',
|
|
76
76
|
href: 'navigation URL; every activation follows the shared navigation rule (see Link href)',
|
|
77
77
|
target: 'link target',
|
|
78
|
-
isDisabled: 'disables card, removes link destination,
|
|
78
|
+
isDisabled: 'disables card, removes link destination, keeps tab stop',
|
|
79
79
|
padding: 'inner padding',
|
|
80
80
|
variant: 'background color variant',
|
|
81
81
|
elevation: 'resting shadow depth: none|low|med|high; often raised to signal clickability',
|
|
@@ -129,7 +129,7 @@ describe('ClickableCard', () => {
|
|
|
129
129
|
expect(link).toHaveAttribute('target', '_blank');
|
|
130
130
|
});
|
|
131
131
|
|
|
132
|
-
it('disabled button
|
|
132
|
+
it('disabled button stays focusable and the surface runs nothing', () => {
|
|
133
133
|
const handleClick = vi.fn();
|
|
134
134
|
render(
|
|
135
135
|
<ClickableCard label="Disabled" onClick={handleClick} isDisabled>
|
|
@@ -137,13 +137,14 @@ describe('ClickableCard', () => {
|
|
|
137
137
|
</ClickableCard>,
|
|
138
138
|
);
|
|
139
139
|
const button = screen.getByRole('button', {name: 'Disabled'});
|
|
140
|
-
expect(button).
|
|
140
|
+
expect(button).not.toHaveAttribute('disabled');
|
|
141
|
+
expect(button).toHaveAttribute('aria-disabled', 'true');
|
|
141
142
|
fireEvent.click(screen.getByText('Content'));
|
|
142
143
|
fireEvent.click(button);
|
|
143
144
|
expect(handleClick).not.toHaveBeenCalled();
|
|
144
145
|
});
|
|
145
146
|
|
|
146
|
-
it('disabled link has no live destination,
|
|
147
|
+
it('disabled link has no live destination, keeps its tab stop, and runs nothing', () => {
|
|
147
148
|
const handleClick = vi.fn();
|
|
148
149
|
render(
|
|
149
150
|
<ClickableCard
|
|
@@ -157,7 +158,7 @@ describe('ClickableCard', () => {
|
|
|
157
158
|
);
|
|
158
159
|
const link = screen.getByRole('link', {name: 'Disabled link'});
|
|
159
160
|
expect(link).toHaveAttribute('aria-disabled', 'true');
|
|
160
|
-
expect(link).toHaveAttribute('tabindex', '
|
|
161
|
+
expect(link).toHaveAttribute('tabindex', '0');
|
|
161
162
|
expect(link).not.toHaveAttribute('href');
|
|
162
163
|
expect(link).not.toHaveAttribute('target');
|
|
163
164
|
expect(fireEvent.click(link)).toBe(false);
|
|
@@ -51,6 +51,7 @@ import {interactionOverlayStyles} from '../utils/interactionOverlay.stylex';
|
|
|
51
51
|
import {usePressFeedback} from '../hooks/usePressFeedback';
|
|
52
52
|
|
|
53
53
|
import {useMergedRefs} from '../hooks/useMergedRefs';
|
|
54
|
+
import {disabledInteractionProps} from '../utils/disabledInteraction';
|
|
54
55
|
// =============================================================================
|
|
55
56
|
// Styles — only the interactive layer, Card handles everything else
|
|
56
57
|
// =============================================================================
|
|
@@ -210,8 +211,8 @@ export interface ClickableCardProps extends BaseProps {
|
|
|
210
211
|
|
|
211
212
|
/**
|
|
212
213
|
* Set to true to disable the card.
|
|
213
|
-
* Disabled
|
|
214
|
-
*
|
|
214
|
+
* Disabled cards keep their tab stop and expose aria-disabled; activation is
|
|
215
|
+
* blocked, and disabled link cards render no live destination.
|
|
215
216
|
*/
|
|
216
217
|
isDisabled?: boolean;
|
|
217
218
|
|
|
@@ -255,10 +256,6 @@ export interface ClickableCardProps extends BaseProps {
|
|
|
255
256
|
// Component
|
|
256
257
|
// =============================================================================
|
|
257
258
|
|
|
258
|
-
function preventDisabledLinkClick(event: MouseEvent<HTMLAnchorElement>): void {
|
|
259
|
-
event.preventDefault();
|
|
260
|
-
}
|
|
261
|
-
|
|
262
259
|
/**
|
|
263
260
|
* An interactive card that acts as a single navigation or action target.
|
|
264
261
|
*
|
|
@@ -376,11 +373,12 @@ export function ClickableCard({
|
|
|
376
373
|
ref={interactiveRef as React.Ref<HTMLAnchorElement>}
|
|
377
374
|
href={isDisabled ? undefined : href}
|
|
378
375
|
target={isDisabled ? undefined : target}
|
|
379
|
-
onClick={isDisabled ? preventDisabledLinkClick : undefined}
|
|
380
376
|
role={isDisabled ? 'link' : undefined}
|
|
377
|
+
{...(isDisabled ? disabledInteractionProps() : null)}
|
|
381
378
|
aria-label={label}
|
|
382
379
|
aria-disabled={isDisabled || undefined}
|
|
383
|
-
|
|
380
|
+
// A disabled card keeps its Tab stop (spec:AST-085 FR1).
|
|
381
|
+
tabIndex={0}
|
|
384
382
|
{...stylex.props(styles.srOnly)}
|
|
385
383
|
/>
|
|
386
384
|
) : (
|
|
@@ -388,8 +386,10 @@ export function ClickableCard({
|
|
|
388
386
|
ref={interactiveRef as React.Ref<HTMLButtonElement>}
|
|
389
387
|
type="button"
|
|
390
388
|
aria-label={label}
|
|
391
|
-
disabled={isDisabled}
|
|
389
|
+
aria-disabled={isDisabled || undefined}
|
|
392
390
|
onClick={onClickProp}
|
|
391
|
+
// Focusable while disabled (FR1), but inert (FR2).
|
|
392
|
+
{...(isDisabled ? disabledInteractionProps() : null)}
|
|
393
393
|
{...stylex.props(styles.srOnly)}
|
|
394
394
|
/>
|
|
395
395
|
)}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file CodeBlock.highlightStyles.test.tsx
|
|
5
|
+
* @input Uses vitest, @testing-library/react, CodeBlock, a mocked CSS Custom Highlight API
|
|
6
|
+
* @output Tests that the ::highlight() rules exist before a commit's layout effects run
|
|
7
|
+
* @position Performance regression evidence. Rules inserted after layout
|
|
8
|
+
* effects make the browser restyle the whole document a second time once
|
|
9
|
+
* any layout effect has read style, so their insertion point is pinned here.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {useLayoutEffect} from 'react';
|
|
13
|
+
import {cleanup, render} from '@testing-library/react';
|
|
14
|
+
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
|
15
|
+
|
|
16
|
+
class MockHighlight extends Set<Range> {}
|
|
17
|
+
const realCss = globalThis.CSS;
|
|
18
|
+
|
|
19
|
+
const STYLE_SELECTOR = 'style[data-astryx-highlight-styles]';
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
// A fresh module instance, so the once-per-document insertion flag starts
|
|
23
|
+
// unset in every test.
|
|
24
|
+
vi.resetModules();
|
|
25
|
+
document.head.querySelectorAll(STYLE_SELECTOR).forEach(el => el.remove());
|
|
26
|
+
const highlights = new Map<string, MockHighlight>();
|
|
27
|
+
globalThis.CSS = {
|
|
28
|
+
escape: realCss.escape,
|
|
29
|
+
highlights: {
|
|
30
|
+
get: (name: string) => highlights.get(name),
|
|
31
|
+
set: (name: string, h: MockHighlight) => highlights.set(name, h),
|
|
32
|
+
} as unknown as HighlightRegistry,
|
|
33
|
+
} as typeof CSS;
|
|
34
|
+
vi.stubGlobal('Highlight', MockHighlight);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
cleanup();
|
|
39
|
+
globalThis.CSS = realCss;
|
|
40
|
+
vi.unstubAllGlobals();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('CodeBlock ::highlight() rules', () => {
|
|
44
|
+
it('are in the document before layout effects in the same commit run', async () => {
|
|
45
|
+
const {CodeBlock} = await import('./CodeBlock');
|
|
46
|
+
const seen: boolean[] = [];
|
|
47
|
+
function LayoutReader() {
|
|
48
|
+
useLayoutEffect(() => {
|
|
49
|
+
seen.push(document.head.querySelector(STYLE_SELECTOR) != null);
|
|
50
|
+
}, []);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
render(
|
|
54
|
+
<>
|
|
55
|
+
<CodeBlock code="const a = 1;" language="ts" highlightMode="ranges" />
|
|
56
|
+
<LayoutReader />
|
|
57
|
+
</>,
|
|
58
|
+
);
|
|
59
|
+
expect(seen).toEqual([true]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('are inserted once for many code blocks', async () => {
|
|
63
|
+
const {CodeBlock} = await import('./CodeBlock');
|
|
64
|
+
render(
|
|
65
|
+
<>
|
|
66
|
+
{Array.from({length: 20}, (_, i) => (
|
|
67
|
+
<CodeBlock
|
|
68
|
+
key={i}
|
|
69
|
+
code={`const v${i} = ${i};`}
|
|
70
|
+
language="ts"
|
|
71
|
+
highlightMode="ranges"
|
|
72
|
+
/>
|
|
73
|
+
))}
|
|
74
|
+
</>,
|
|
75
|
+
);
|
|
76
|
+
expect(document.head.querySelectorAll(STYLE_SELECTOR)).toHaveLength(1);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('are not inserted by range highlighting without the Highlight API', async () => {
|
|
80
|
+
vi.stubGlobal('Highlight', undefined);
|
|
81
|
+
const {CodeBlock} = await import('./CodeBlock');
|
|
82
|
+
render(
|
|
83
|
+
<CodeBlock code="const a = 1;" language="ts" highlightMode="ranges" />,
|
|
84
|
+
);
|
|
85
|
+
expect(document.head.querySelector(STYLE_SELECTOR)).toBeNull();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -803,11 +803,21 @@ function RangeCodeContent({
|
|
|
803
803
|
}) {
|
|
804
804
|
const codeRef = useRef<HTMLElement>(null);
|
|
805
805
|
|
|
806
|
+
// Insert the ::highlight() rules before the commit's layout reads, as the
|
|
807
|
+
// span path does. Inserted later, the new rules restyle the whole document
|
|
808
|
+
// a second time after any layout read in the same commit has already
|
|
809
|
+
// styled it, which on a long page full of code blocks costs a full style
|
|
810
|
+
// recalculation.
|
|
811
|
+
useInsertionEffect(() => {
|
|
812
|
+
if (hasHighlightAPI()) {
|
|
813
|
+
ensureHighlightStyles();
|
|
814
|
+
}
|
|
815
|
+
}, []);
|
|
816
|
+
|
|
806
817
|
useEffect(() => {
|
|
807
818
|
if (!hasHighlightAPI()) {
|
|
808
819
|
return;
|
|
809
820
|
}
|
|
810
|
-
ensureHighlightStyles();
|
|
811
821
|
|
|
812
822
|
const codeEl = codeRef.current;
|
|
813
823
|
if (!codeEl || tokenLines.length === 0) {
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file ComplexSelector.focusableDisabled.test.tsx
|
|
5
|
+
* @input Uses vitest, @testing-library/react, ComplexSelector
|
|
6
|
+
* @output Conformance tests for spec:AST-085 FR1, FR2 and FR4 on ComplexSelector
|
|
7
|
+
* @position Testing; validates the disabled trigger of ComplexSelector.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 {ComplexSelector} from './ComplexSelector';
|
|
14
|
+
|
|
15
|
+
function Harness(props: {
|
|
16
|
+
isDisabled: boolean;
|
|
17
|
+
onClick?: () => void;
|
|
18
|
+
onKeyDown?: () => void;
|
|
19
|
+
}) {
|
|
20
|
+
return (
|
|
21
|
+
<>
|
|
22
|
+
<button type="button">Before</button>
|
|
23
|
+
<ComplexSelector label="View options" value={[]} {...props}>
|
|
24
|
+
{() => <button type="button">Apply</button>}
|
|
25
|
+
</ComplexSelector>
|
|
26
|
+
<button type="button">After</button>
|
|
27
|
+
</>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const trigger = () => screen.getByRole('button', {name: 'View options'});
|
|
32
|
+
|
|
33
|
+
describe('ComplexSelector disabled (spec:AST-085)', () => {
|
|
34
|
+
it('FR1: renders no native disabled and keeps its tab stop', async () => {
|
|
35
|
+
const user = userEvent.setup();
|
|
36
|
+
const {container} = render(<Harness isDisabled />);
|
|
37
|
+
expect(container.querySelector('[disabled]')).toBeNull();
|
|
38
|
+
expect(trigger()).toHaveAttribute('aria-disabled', 'true');
|
|
39
|
+
await user.click(screen.getByRole('button', {name: 'Before'}));
|
|
40
|
+
await user.tab();
|
|
41
|
+
expect(trigger()).toHaveFocus();
|
|
42
|
+
await user.tab();
|
|
43
|
+
expect(screen.getByRole('button', {name: 'After'})).toHaveFocus();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('FR2: no press or key opens it or reaches a caller or ancestor handler', async () => {
|
|
47
|
+
const user = userEvent.setup();
|
|
48
|
+
const onClick = vi.fn();
|
|
49
|
+
const onKeyDown = vi.fn();
|
|
50
|
+
const onAncestorClick = vi.fn();
|
|
51
|
+
render(
|
|
52
|
+
<div onClick={onAncestorClick}>
|
|
53
|
+
<Harness isDisabled onClick={onClick} onKeyDown={onKeyDown} />
|
|
54
|
+
</div>,
|
|
55
|
+
);
|
|
56
|
+
await user.click(trigger());
|
|
57
|
+
await user.click(trigger().parentElement!);
|
|
58
|
+
trigger().focus();
|
|
59
|
+
for (const key of ['{Enter}', ' ', '{ArrowDown}']) {
|
|
60
|
+
await user.keyboard(key);
|
|
61
|
+
}
|
|
62
|
+
expect(trigger()).toHaveAttribute('aria-expanded', 'false');
|
|
63
|
+
expect(screen.queryByRole('dialog')).toBeNull();
|
|
64
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
65
|
+
expect(onKeyDown).not.toHaveBeenCalled();
|
|
66
|
+
expect(onAncestorClick).not.toHaveBeenCalled();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('FR4: keeps focus when it becomes disabled while focused', () => {
|
|
70
|
+
const {rerender} = render(<Harness isDisabled={false} />);
|
|
71
|
+
act(() => trigger().focus());
|
|
72
|
+
rerender(<Harness isDisabled />);
|
|
73
|
+
expect(trigger()).toHaveFocus();
|
|
74
|
+
expect(trigger()).toHaveAttribute('aria-disabled', 'true');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -609,10 +609,24 @@ export function ComplexSelector<Value>({
|
|
|
609
609
|
event: React.KeyboardEvent<HTMLElement>,
|
|
610
610
|
) => {
|
|
611
611
|
handleTriggerKeyDown(event);
|
|
612
|
-
|
|
612
|
+
// A disabled trigger keeps its tab stop but invokes no callback
|
|
613
|
+
// (`spec:AST-085` FR2).
|
|
614
|
+
if (!isDisabled) {
|
|
615
|
+
onKeyDownProp?.(event);
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
// A press on a disabled control reaches no handler, the component's or an
|
|
619
|
+
// ancestor's, as with a natively disabled button (`spec:AST-085` FR2).
|
|
620
|
+
const swallowDisabledPress = (event: React.MouseEvent<HTMLElement>) => {
|
|
621
|
+
event.preventDefault();
|
|
622
|
+
event.stopPropagation();
|
|
613
623
|
};
|
|
614
624
|
const handleControlBoxClick = useCallback(
|
|
615
625
|
(event: React.MouseEvent<HTMLDivElement>) => {
|
|
626
|
+
if (isDisabled) {
|
|
627
|
+
event.stopPropagation();
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
616
630
|
if (triggerRef.current?.contains(event.target as Node)) {
|
|
617
631
|
return;
|
|
618
632
|
}
|
|
@@ -620,7 +634,7 @@ export function ComplexSelector<Value>({
|
|
|
620
634
|
// press on the trigger itself, caller `onClick` included.
|
|
621
635
|
triggerRef.current?.click();
|
|
622
636
|
},
|
|
623
|
-
[],
|
|
637
|
+
[isDisabled],
|
|
624
638
|
);
|
|
625
639
|
|
|
626
640
|
useImperativeHandle(
|
|
@@ -767,8 +781,11 @@ export function ComplexSelector<Value>({
|
|
|
767
781
|
aria-required={isEffectivelyRequired ? 'true' : undefined}
|
|
768
782
|
aria-invalid={status?.type === 'error' ? 'true' : undefined}
|
|
769
783
|
aria-busy={isBusy || undefined}
|
|
770
|
-
disabled
|
|
771
|
-
|
|
784
|
+
// A disabled trigger keeps its tab stop and says so through
|
|
785
|
+
// aria-disabled (`spec:AST-085` FR1). Its press runs neither the
|
|
786
|
+
// caller's handler nor the toggle (FR2).
|
|
787
|
+
aria-disabled={isDisabled ? 'true' : undefined}
|
|
788
|
+
onClick={isDisabled ? swallowDisabledPress : handleTriggerButtonClick}
|
|
772
789
|
onKeyDown={handleTriggerButtonKeyDown}
|
|
773
790
|
{...stylex.props(styles.trigger)}>
|
|
774
791
|
<span {...stylex.props(styles.triggerText)}>{triggerContent}</span>
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
ContextMenuRadioItem,
|
|
25
25
|
} from './index';
|
|
26
26
|
import {DropdownMenuItem} from '../DropdownMenu/DropdownMenuItem';
|
|
27
|
+
import {DropdownMenuSubMenu} from '../DropdownMenu/DropdownMenuSubMenu';
|
|
27
28
|
import {Divider} from '../Divider';
|
|
28
29
|
import {useLayerDismissal} from '../Layer/useLayerDismissal';
|
|
29
30
|
import {warmMenuBottomSheet} from '../DropdownMenu/__tests__/warmMenuBottomSheet';
|
|
@@ -706,6 +707,46 @@ describe('ContextMenu dividers', () => {
|
|
|
706
707
|
});
|
|
707
708
|
|
|
708
709
|
describe('ContextMenu compound mode', () => {
|
|
710
|
+
it('shows a sub-menu with a header or footer as its rows alone, and says why in development', async () => {
|
|
711
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
712
|
+
try {
|
|
713
|
+
render(
|
|
714
|
+
<ContextMenu
|
|
715
|
+
menuContent={
|
|
716
|
+
<>
|
|
717
|
+
<DropdownMenuItem label="Rename" onClick={() => {}} />
|
|
718
|
+
<DropdownMenuSubMenu
|
|
719
|
+
label="Labels"
|
|
720
|
+
header={<input aria-label="Filter labels" />}
|
|
721
|
+
footer={<button type="button">Create label</button>}>
|
|
722
|
+
<DropdownMenuItem label="Bug" onClick={() => {}} />
|
|
723
|
+
</DropdownMenuSubMenu>
|
|
724
|
+
</>
|
|
725
|
+
}>
|
|
726
|
+
<div>Right-click me</div>
|
|
727
|
+
</ContextMenu>,
|
|
728
|
+
);
|
|
729
|
+
fireEvent.contextMenu(screen.getByText('Right-click me'));
|
|
730
|
+
const row = screen.getByRole('menuitem', {name: /Labels/, hidden: true});
|
|
731
|
+
// No region can sit outside this menu, so none renders inside it.
|
|
732
|
+
expect(row).toHaveAttribute('aria-haspopup', 'menu');
|
|
733
|
+
fireEvent.click(row, {detail: 1});
|
|
734
|
+
await waitFor(() => expect(row).toHaveAttribute('aria-expanded', 'true'));
|
|
735
|
+
expect(screen.queryByRole('dialog', {hidden: true})).toBeNull();
|
|
736
|
+
expect(
|
|
737
|
+
screen.queryByRole('textbox', {name: 'Filter labels', hidden: true}),
|
|
738
|
+
).toBeNull();
|
|
739
|
+
expect(
|
|
740
|
+
screen.getByRole('menuitem', {name: 'Bug', hidden: true}),
|
|
741
|
+
).toBeInTheDocument();
|
|
742
|
+
expect(warn).toHaveBeenCalledWith(
|
|
743
|
+
expect.stringContaining('DropdownMenuSubMenu: header and footer'),
|
|
744
|
+
);
|
|
745
|
+
} finally {
|
|
746
|
+
warn.mockRestore();
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
|
|
709
750
|
it('renders menuContent as menu items', () => {
|
|
710
751
|
render(
|
|
711
752
|
<ContextMenu
|