@astryxdesign/core 0.7.3-canary.b74cf21 → 0.7.3-canary.ca48bb7
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/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/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 +2 -1
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- 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/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/astryx.css +7 -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 +3 -3
- package/src/Banner/Banner.spec.md +37 -32
- package/src/BottomSheet/BottomSheet.spec.md +26 -53
- package/src/BottomSheet/BottomSheet.test.tsx +29 -0
- package/src/BottomSheet/BottomSheet.tsx +8 -4
- package/src/BottomSheet/BottomSheetSwitcher.spec.md +45 -48
- package/src/BottomSheet/BottomSheetSwitcher.test.tsx +26 -0
- package/src/BottomSheet/BottomSheetSwitcher.tsx +6 -0
- package/src/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.test.tsx +82 -16
- package/src/DateRangeInput/DateRangeInput.tsx +40 -20
- 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/DropdownMenuSubMenu.doc.mjs +16 -1
- package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +623 -1
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +556 -99
- 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.test.tsx +48 -6
- package/src/Selector/Selector.tsx +51 -21
- package/src/SideNav/SideNav.test.tsx +35 -0
- 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/plugins/selection/useTableSelection.test.tsx +5 -3
- package/src/TimeInput/TimeInput.doc.mjs +5 -3
- package/src/TimeInput/TimeInput.test.tsx +20 -0
- package/src/TimeInput/TimeInput.tsx +2 -1
- 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/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/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/__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
|
@@ -271,6 +271,21 @@ const styles = stylex.create({
|
|
|
271
271
|
'[data-astryx-press="on"]': 'scale(0.98)',
|
|
272
272
|
},
|
|
273
273
|
},
|
|
274
|
+
// A disabled input-variant trigger keeps its tab stop (`spec:AST-085` FR1),
|
|
275
|
+
// and the shared disabled wrapper style drops the field focus ring, so the
|
|
276
|
+
// trigger shows keyboard focus here instead (FR6). It keys on the trigger
|
|
277
|
+
// itself, a direct child, so a focused status button keeps its own outline.
|
|
278
|
+
triggerDisabledFocus: {
|
|
279
|
+
borderColor: {
|
|
280
|
+
default: colorVars['--color-border-emphasized'],
|
|
281
|
+
':has(> [aria-disabled="true"]:focus-visible)':
|
|
282
|
+
colorVars['--color-accent'],
|
|
283
|
+
},
|
|
284
|
+
boxShadow: {
|
|
285
|
+
default: 'none',
|
|
286
|
+
':has(> [aria-disabled="true"]:focus-visible)': `inset 0px 0px 0px 2px ${colorVars['--color-accent-muted']}`,
|
|
287
|
+
},
|
|
288
|
+
},
|
|
274
289
|
triggerGhostDisabled: {
|
|
275
290
|
backgroundImage: 'none',
|
|
276
291
|
transform: {
|
|
@@ -688,8 +703,8 @@ export interface MultiSelectorProps<
|
|
|
688
703
|
* The selected values stay visible, focusable, and included in form
|
|
689
704
|
* submission, and retain their combobox identity with `aria-readonly`. The
|
|
690
705
|
* selection surface and editing affordances are removed.
|
|
691
|
-
* Unlike `isDisabled`, a read-only selector is not dimmed and
|
|
692
|
-
*
|
|
706
|
+
* Unlike `isDisabled`, a read-only selector is not dimmed and keeps its
|
|
707
|
+
* value in form submission. `isDisabled` takes precedence when both are set.
|
|
693
708
|
* @default false
|
|
694
709
|
*/
|
|
695
710
|
isReadOnly?: boolean;
|
|
@@ -701,8 +716,8 @@ export interface MultiSelectorProps<
|
|
|
701
716
|
* so the reason is discoverable by keyboard and assistive technology.
|
|
702
717
|
* Activation stays blocked.
|
|
703
718
|
*
|
|
704
|
-
* Use this instead of wrapping a disabled selector in `Tooltip
|
|
705
|
-
*
|
|
719
|
+
* Use this instead of wrapping a disabled selector in `Tooltip`, so the
|
|
720
|
+
* reason is attached to the trigger itself.
|
|
706
721
|
*
|
|
707
722
|
* @example
|
|
708
723
|
* ```
|
|
@@ -2409,8 +2424,18 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
|
|
|
2409
2424
|
});
|
|
2410
2425
|
}
|
|
2411
2426
|
|
|
2427
|
+
// A disabled trigger stays focusable but, like a natively disabled button,
|
|
2428
|
+
// runs no caller event handler other than focus and blur, and a press on it
|
|
2429
|
+
// reaches no handler at all (`spec:AST-085` FR2).
|
|
2430
|
+
const callerProps = isDisabled
|
|
2431
|
+
? Object.fromEntries(
|
|
2432
|
+
Object.entries(rest).filter(
|
|
2433
|
+
([key]) => !/^on[A-Z]/.test(key) || /^on(Focus|Blur)/.test(key),
|
|
2434
|
+
),
|
|
2435
|
+
)
|
|
2436
|
+
: rest;
|
|
2412
2437
|
const triggerSharedProps: React.HTMLAttributes<HTMLElement> = {
|
|
2413
|
-
...
|
|
2438
|
+
...callerProps,
|
|
2414
2439
|
id: triggerId,
|
|
2415
2440
|
// A caller's description is kept alongside the field's own.
|
|
2416
2441
|
'aria-describedby':
|
|
@@ -2423,13 +2448,20 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
|
|
|
2423
2448
|
// Newly forwarded handlers compose component first: the built-in toggle
|
|
2424
2449
|
// and keyboard model always run. `onFocus` keeps its existing caller-first
|
|
2425
2450
|
// order.
|
|
2426
|
-
onKeyDown:
|
|
2451
|
+
onKeyDown: isDisabled
|
|
2452
|
+
? onKeyDown
|
|
2453
|
+
: composeEventHandlers(onKeyDown, onKeyDownProp),
|
|
2427
2454
|
onFocus: event => {
|
|
2428
2455
|
onFocus?.(event);
|
|
2429
2456
|
surface.onTriggerFocus(event);
|
|
2430
2457
|
},
|
|
2431
|
-
onClick:
|
|
2432
|
-
|
|
2458
|
+
onClick: isDisabled
|
|
2459
|
+
? event => {
|
|
2460
|
+
event.preventDefault();
|
|
2461
|
+
event.stopPropagation();
|
|
2462
|
+
}
|
|
2463
|
+
: composeEventHandlers(handleTriggerButtonClick, onClick),
|
|
2464
|
+
tabIndex: 0,
|
|
2433
2465
|
...stylex.props(
|
|
2434
2466
|
styles.trigger,
|
|
2435
2467
|
isEffectivelyReadOnly && styles.triggerReadOnly,
|
|
@@ -2496,14 +2528,9 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
|
|
|
2496
2528
|
<>
|
|
2497
2529
|
{renderTrigger(triggerProps)}
|
|
2498
2530
|
{htmlName != null &&
|
|
2531
|
+
!isDisabled &&
|
|
2499
2532
|
value.map(v => (
|
|
2500
|
-
<input
|
|
2501
|
-
key={v}
|
|
2502
|
-
type="hidden"
|
|
2503
|
-
name={htmlName}
|
|
2504
|
-
value={v}
|
|
2505
|
-
disabled={isDisabled}
|
|
2506
|
-
/>
|
|
2533
|
+
<input key={v} type="hidden" name={htmlName} value={v} />
|
|
2507
2534
|
))}
|
|
2508
2535
|
{selectionSurface}
|
|
2509
2536
|
</>
|
|
@@ -2541,6 +2568,10 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
|
|
|
2541
2568
|
surface.isTriggerFocusRingSuppressed &&
|
|
2542
2569
|
selectorPresentationStyles.pointerRestoredFocus,
|
|
2543
2570
|
isDisabled && inputWrapperStyles.disabled,
|
|
2571
|
+
variant === 'input' &&
|
|
2572
|
+
isDisabled &&
|
|
2573
|
+
!surface.isTriggerFocusRingSuppressed &&
|
|
2574
|
+
styles.triggerDisabledFocus,
|
|
2544
2575
|
isEffectivelyReadOnly && styles.triggerReadOnly,
|
|
2545
2576
|
variant !== 'input' && isDisabled && styles.triggerGhostDisabled,
|
|
2546
2577
|
variant !== 'input' &&
|
|
@@ -2598,26 +2629,19 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
|
|
|
2598
2629
|
? activeDescendantId
|
|
2599
2630
|
: undefined
|
|
2600
2631
|
}
|
|
2601
|
-
//
|
|
2602
|
-
// aria-disabled
|
|
2603
|
-
//
|
|
2604
|
-
disabled={isDisabled
|
|
2605
|
-
aria-disabled={showsDisabledMessage ? 'true' : undefined}>
|
|
2632
|
+
// A disabled trigger keeps its tab stop and says so through
|
|
2633
|
+
// aria-disabled (`spec:AST-085` FR1); the isDisabled guards in
|
|
2634
|
+
// useMultiCombobox block every way of opening it.
|
|
2635
|
+
aria-disabled={isDisabled ? 'true' : undefined}>
|
|
2606
2636
|
<span {...stylex.props(styles.triggerContent)}>
|
|
2607
2637
|
{renderTriggerContent()}
|
|
2608
2638
|
</span>
|
|
2609
2639
|
</button>
|
|
2640
|
+
{/* A disabled selector submits no value (`spec:AST-085` FR3). */}
|
|
2610
2641
|
{htmlName != null &&
|
|
2642
|
+
!isDisabled &&
|
|
2611
2643
|
value.map(v => (
|
|
2612
|
-
<input
|
|
2613
|
-
key={v}
|
|
2614
|
-
type="hidden"
|
|
2615
|
-
name={htmlName}
|
|
2616
|
-
value={v}
|
|
2617
|
-
// Disabled native controls are excluded from form submission;
|
|
2618
|
-
// mirror that for the hidden carriers.
|
|
2619
|
-
disabled={isDisabled}
|
|
2620
|
-
/>
|
|
2644
|
+
<input key={v} type="hidden" name={htmlName} value={v} />
|
|
2621
2645
|
))}
|
|
2622
2646
|
{isBusy && <Spinner size="sm" />}
|
|
2623
2647
|
{hasClear &&
|
|
@@ -763,9 +763,13 @@ describe('Pagination', () => {
|
|
|
763
763
|
/>,
|
|
764
764
|
);
|
|
765
765
|
const dot = screen.getByRole('button', {name: 'Go to page 2'});
|
|
766
|
-
|
|
766
|
+
// spec:AST-085 FR1: the active dot keeps its tab stop, exposed as disabled.
|
|
767
|
+
expect(dot).not.toHaveAttribute('disabled');
|
|
768
|
+
expect(dot).toHaveAttribute('aria-disabled', 'true');
|
|
767
769
|
dot.focus();
|
|
770
|
+
expect(dot).toHaveFocus();
|
|
768
771
|
await user.keyboard('{ArrowRight}');
|
|
772
|
+
await user.keyboard('{Enter} ');
|
|
769
773
|
expect(onChange).not.toHaveBeenCalled();
|
|
770
774
|
});
|
|
771
775
|
});
|
|
@@ -706,7 +706,9 @@ export function Pagination({
|
|
|
706
706
|
// needs to focus the dot, which some browsers (Safari) skip for
|
|
707
707
|
// buttons, so focus it explicitly.
|
|
708
708
|
onClick={e => e.currentTarget.focus()}
|
|
709
|
-
|
|
709
|
+
// Disabled dots stay focusable (spec:AST-085 FR1); the page
|
|
710
|
+
// only follows focus while enabled (handleDotsFocus).
|
|
711
|
+
aria-disabled={isDisabled || undefined}
|
|
710
712
|
{...mergeProps(
|
|
711
713
|
themeProps('pagination-dot', {
|
|
712
714
|
active: isActive ? 'active' : null,
|
|
@@ -124,6 +124,21 @@ describe('Popover', () => {
|
|
|
124
124
|
expect(screen.getByRole('button', {name: 'Open'})).toBeInTheDocument();
|
|
125
125
|
});
|
|
126
126
|
|
|
127
|
+
it('does not open from an aria-disabled trigger', () => {
|
|
128
|
+
render(
|
|
129
|
+
<Popover content={<span>Content</span>} label="Test">
|
|
130
|
+
<button type="button" aria-disabled="true">
|
|
131
|
+
Trigger
|
|
132
|
+
</button>
|
|
133
|
+
</Popover>,
|
|
134
|
+
);
|
|
135
|
+
const trigger = screen.getByRole('button', {name: 'Trigger'});
|
|
136
|
+
// A click dispatched straight on the trigger, as element.click() or an
|
|
137
|
+
// assistive-technology activate command does (spec:AST-085 FR2).
|
|
138
|
+
act(() => trigger.click());
|
|
139
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
140
|
+
});
|
|
141
|
+
|
|
127
142
|
it('sets aria-haspopup on trigger', () => {
|
|
128
143
|
render(
|
|
129
144
|
<Popover content={<span>Content</span>} label="Test">
|
package/src/Popover/Popover.tsx
CHANGED
|
@@ -481,12 +481,22 @@ export function Popover({
|
|
|
481
481
|
}, [content, popover.isOpen, scheduleOverflowMeasurement]);
|
|
482
482
|
|
|
483
483
|
// Shared handler for click events on the trigger button.
|
|
484
|
-
const handleTriggerClick = useCallback(
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
484
|
+
const handleTriggerClick = useCallback(
|
|
485
|
+
(event?: {currentTarget: EventTarget | null}) => {
|
|
486
|
+
// An aria-disabled trigger stays focusable but opens nothing
|
|
487
|
+
// (spec:AST-085 FR2), even for a click dispatched straight on it.
|
|
488
|
+
const trigger = event?.currentTarget;
|
|
489
|
+
if (
|
|
490
|
+
!isEnabled ||
|
|
491
|
+
(trigger instanceof Element &&
|
|
492
|
+
trigger.getAttribute('aria-disabled') === 'true')
|
|
493
|
+
) {
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
popover.toggle();
|
|
497
|
+
},
|
|
498
|
+
[isEnabled, popover],
|
|
499
|
+
);
|
|
490
500
|
|
|
491
501
|
// Shared handler for keydown events on role="button" elements.
|
|
492
502
|
// Native <button> synthesizes click on Enter/Space, but role="button"
|
|
@@ -495,7 +505,7 @@ export function Popover({
|
|
|
495
505
|
(e: KeyboardEvent) => {
|
|
496
506
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
497
507
|
e.preventDefault();
|
|
498
|
-
handleTriggerClick();
|
|
508
|
+
handleTriggerClick(e);
|
|
499
509
|
}
|
|
500
510
|
},
|
|
501
511
|
[handleTriggerClick],
|
|
@@ -662,7 +672,8 @@ export function Popover({
|
|
|
662
672
|
if (isRenderProp) {
|
|
663
673
|
const triggerProps: PopoverTriggerRenderProps = {
|
|
664
674
|
ref: popover.triggerRef,
|
|
665
|
-
|
|
675
|
+
// The caller renders and owns this trigger element.
|
|
676
|
+
onClick: () => handleTriggerClick(),
|
|
666
677
|
'aria-haspopup': 'dialog',
|
|
667
678
|
'aria-expanded': popover.isOpen,
|
|
668
679
|
'aria-controls': popover.id,
|
|
@@ -370,7 +370,8 @@ describe('PowerSearchFilterEditor', () => {
|
|
|
370
370
|
|
|
371
371
|
expect(screen.queryByRole('button', {name: 'Delete'})).toBeNull();
|
|
372
372
|
const [fieldTrigger, operatorTrigger] = selectorTriggers();
|
|
373
|
-
|
|
374
|
-
expect(
|
|
373
|
+
// Disabled triggers stay focusable (spec:AST-085 FR1).
|
|
374
|
+
expect(fieldTrigger).toHaveAttribute('aria-disabled', 'true');
|
|
375
|
+
expect(operatorTrigger).toHaveAttribute('aria-disabled', 'true');
|
|
375
376
|
});
|
|
376
377
|
});
|
|
@@ -218,11 +218,13 @@ describe('PowerSearchToken', () => {
|
|
|
218
218
|
expect(onRemove).toHaveBeenCalledTimes(1);
|
|
219
219
|
});
|
|
220
220
|
|
|
221
|
-
it('
|
|
221
|
+
it('marks both the activation and remove buttons disabled when isDisabled is set', () => {
|
|
222
222
|
renderToken({onClick: vi.fn(), onRemove: vi.fn(), isDisabled: true});
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
screen.getByRole('button', {name
|
|
226
|
-
|
|
223
|
+
// spec:AST-085 FR1: focusable, exposed through aria-disabled.
|
|
224
|
+
for (const name of ['Status: is', 'Remove Status: is']) {
|
|
225
|
+
const button = screen.getByRole('button', {name});
|
|
226
|
+
expect(button).not.toHaveAttribute('disabled');
|
|
227
|
+
expect(button).toHaveAttribute('aria-disabled', 'true');
|
|
228
|
+
}
|
|
227
229
|
});
|
|
228
230
|
});
|
|
@@ -273,7 +273,7 @@ export const docs = {
|
|
|
273
273
|
name: 'disabledMessage',
|
|
274
274
|
type: 'string',
|
|
275
275
|
description:
|
|
276
|
-
'Explains why the selector is disabled. With isDisabled, shows a tooltip on hover
|
|
276
|
+
'Explains why the selector is disabled. With isDisabled, shows the reason as a tooltip on hover and keyboard focus of the trigger, which stays focusable (aria-disabled) like every disabled selector; activation stays blocked. Use this instead of wrapping a disabled Selector in Tooltip.',
|
|
277
277
|
},
|
|
278
278
|
{
|
|
279
279
|
name: 'isLabelHidden',
|
|
@@ -475,7 +475,7 @@ export const docs = {
|
|
|
475
475
|
{
|
|
476
476
|
guidance: false,
|
|
477
477
|
description:
|
|
478
|
-
'Wrap a disabled Selector in Tooltip to explain why it is disabled
|
|
478
|
+
'Wrap a disabled Selector in Tooltip to explain why it is disabled. Use the disabledMessage prop, which attaches the reason to the trigger itself.',
|
|
479
479
|
},
|
|
480
480
|
],
|
|
481
481
|
anatomy,
|
|
@@ -536,7 +536,7 @@ export const docsZh = {
|
|
|
536
536
|
{
|
|
537
537
|
guidance: false,
|
|
538
538
|
description:
|
|
539
|
-
'Wrap a disabled Selector in Tooltip to explain why it is disabled
|
|
539
|
+
'Wrap a disabled Selector in Tooltip to explain why it is disabled. Use the disabledMessage prop, which attaches the reason to the trigger itself.',
|
|
540
540
|
},
|
|
541
541
|
],
|
|
542
542
|
anatomy,
|
|
@@ -607,7 +607,7 @@ export const docsDense = {
|
|
|
607
607
|
{
|
|
608
608
|
guidance: false,
|
|
609
609
|
description:
|
|
610
|
-
'Wrap a disabled Selector in Tooltip to explain why it is disabled
|
|
610
|
+
'Wrap a disabled Selector in Tooltip to explain why it is disabled. Use the disabledMessage prop, which attaches the reason to the trigger itself.',
|
|
611
611
|
},
|
|
612
612
|
],
|
|
613
613
|
anatomy,
|
|
@@ -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
|
+
});
|
|
@@ -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', () => {
|
|
@@ -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 && (
|