@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.
Files changed (237) hide show
  1. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
  2. package/dist/BottomSheet/BottomSheet.js +8 -4
  3. package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
  4. package/dist/BottomSheet/BottomSheetSwitcher.js +7 -0
  5. package/dist/Calendar/Calendar.js +3 -2
  6. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  7. package/dist/CheckboxInput/CheckboxInput.js +19 -24
  8. package/dist/ClickableCard/ClickableCard.d.ts +2 -2
  9. package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
  10. package/dist/ClickableCard/ClickableCard.js +11 -9
  11. package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
  12. package/dist/CodeBlock/CodeBlock.js +11 -1
  13. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  14. package/dist/ComplexSelector/ComplexSelector.js +23 -5
  15. package/dist/DateInput/DateInput.d.ts +4 -4
  16. package/dist/DateInput/DateInput.d.ts.map +1 -1
  17. package/dist/DateInput/DateInput.js +27 -20
  18. package/dist/DateInput/NativeDateField.d.ts.map +1 -1
  19. package/dist/DateInput/NativeDateField.js +14 -8
  20. package/dist/DateInput/TouchDateField.d.ts.map +1 -1
  21. package/dist/DateInput/TouchDateField.js +14 -10
  22. package/dist/DateRangeInput/DateRangeInput.d.ts +5 -5
  23. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  24. package/dist/DateRangeInput/DateRangeInput.js +27 -18
  25. package/dist/DateTimeInput/DateTimeInput.d.ts +5 -5
  26. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  27. package/dist/DateTimeInput/DateTimeInput.js +25 -27
  28. package/dist/DateTimeInput/NativeDateSegment.d.ts +3 -2
  29. package/dist/DateTimeInput/NativeDateSegment.d.ts.map +1 -1
  30. package/dist/DateTimeInput/NativeDateSegment.js +5 -5
  31. package/dist/DateTimeInput/NativeTimeSegment.d.ts +3 -2
  32. package/dist/DateTimeInput/NativeTimeSegment.d.ts.map +1 -1
  33. package/dist/DateTimeInput/NativeTimeSegment.js +5 -5
  34. package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
  35. package/dist/DateTimeInput/TouchDateTimeField.js +12 -9
  36. package/dist/Dialog/Dialog.d.ts.map +1 -1
  37. package/dist/Dialog/Dialog.js +9 -4
  38. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  39. package/dist/DropdownMenu/DropdownMenu.js +52 -10
  40. package/dist/DropdownMenu/DropdownMenuContext.d.ts +34 -1
  41. package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
  42. package/dist/DropdownMenu/DropdownMenuContext.js +15 -0
  43. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts +21 -2
  44. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
  45. package/dist/DropdownMenu/DropdownMenuSubMenu.js +375 -42
  46. package/dist/DropdownMenu/menuRegionStops.d.ts +2 -2
  47. package/dist/DropdownMenu/menuRegionStops.js +2 -2
  48. package/dist/FileInput/FileInput.d.ts.map +1 -1
  49. package/dist/FileInput/FileInput.js +14 -12
  50. package/dist/Link/Link.d.ts +2 -3
  51. package/dist/Link/Link.d.ts.map +1 -1
  52. package/dist/Link/Link.js +11 -19
  53. package/dist/Markdown/Markdown.d.ts.map +1 -1
  54. package/dist/Markdown/Markdown.js +48 -15
  55. package/dist/MultiSelector/MultiSelector.d.ts +4 -4
  56. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  57. package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.d.ts +2 -0
  58. package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.d.ts.map +1 -0
  59. package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.js +48 -0
  60. package/dist/MultiSelector/MultiSelector.js +25 -18
  61. package/dist/Pagination/Pagination.d.ts.map +1 -1
  62. package/dist/Pagination/Pagination.js +5 -2
  63. package/dist/Popover/Popover.d.ts.map +1 -1
  64. package/dist/Popover/Popover.js +8 -4
  65. package/dist/Selector/Selector.d.ts +4 -4
  66. package/dist/Selector/Selector.d.ts.map +1 -1
  67. package/dist/Selector/Selector.js +23 -14
  68. package/dist/Selector/SelectorOption.d.ts.map +1 -1
  69. package/dist/Selector/SelectorOption.js +1 -0
  70. package/dist/Slider/Slider.d.ts.map +1 -1
  71. package/dist/Slider/Slider.js +7 -14
  72. package/dist/Switch/Switch.d.ts.map +1 -1
  73. package/dist/Switch/Switch.js +25 -17
  74. package/dist/Switch/switch.markers.stylex.d.ts +2 -0
  75. package/dist/Switch/switch.markers.stylex.d.ts.map +1 -1
  76. package/dist/Switch/switch.markers.stylex.js +6 -0
  77. package/dist/TimeInput/TimeInput.d.ts +7 -6
  78. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  79. package/dist/TimeInput/TimeInput.js +37 -20
  80. package/dist/TimeInput/TouchTimeField.d.ts.map +1 -1
  81. package/dist/TimeInput/TouchTimeField.js +17 -6
  82. package/dist/Timestamp/Timestamp.d.ts.map +1 -1
  83. package/dist/Timestamp/Timestamp.js +41 -8
  84. package/dist/Token/Token.d.ts.map +1 -1
  85. package/dist/Token/Token.js +8 -3
  86. package/dist/Tooltip/Tooltip.d.ts.map +1 -1
  87. package/dist/Tooltip/Tooltip.js +36 -14
  88. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  89. package/dist/Tooltip/useTooltip.js +11 -2
  90. package/dist/TopNav/TopNav.d.ts.map +1 -1
  91. package/dist/TopNav/TopNav.js +2 -2
  92. package/dist/TopNav/TopNavHeading.d.ts.map +1 -1
  93. package/dist/TopNav/TopNavHeading.js +1 -0
  94. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  95. package/dist/TreeList/TreeListItem.js +8 -3
  96. package/dist/Typeahead/Typeahead.d.ts.map +1 -1
  97. package/dist/Typeahead/Typeahead.js +14 -2
  98. package/dist/astryx.css +8 -2
  99. package/dist/hooks/useGridFocus.d.ts.map +1 -1
  100. package/dist/hooks/useGridFocus.js +10 -1
  101. package/dist/hooks/useMenuPress.d.ts +11 -1
  102. package/dist/hooks/useMenuPress.d.ts.map +1 -1
  103. package/dist/hooks/useMenuPress.js +43 -5
  104. package/dist/utils/disabledInteraction.d.ts +21 -0
  105. package/dist/utils/disabledInteraction.d.ts.map +1 -0
  106. package/dist/utils/disabledInteraction.js +33 -0
  107. package/dist/utils/timeParser.d.ts +4 -0
  108. package/dist/utils/timeParser.d.ts.map +1 -1
  109. package/dist/utils/timeParser.js +9 -2
  110. package/package.json +4 -4
  111. package/scripts/agent-doc-state.mjs +58 -9
  112. package/src/Banner/Banner.spec.md +37 -32
  113. package/src/BottomSheet/BottomSheet.spec.md +34 -54
  114. package/src/BottomSheet/BottomSheet.test.tsx +29 -0
  115. package/src/BottomSheet/BottomSheet.tsx +8 -4
  116. package/src/BottomSheet/BottomSheetSwitcher.spec.md +125 -58
  117. package/src/BottomSheet/BottomSheetSwitcher.test.tsx +26 -0
  118. package/src/BottomSheet/BottomSheetSwitcher.tsx +6 -0
  119. package/src/Calendar/Calendar.test.tsx +68 -24
  120. package/src/Calendar/Calendar.tsx +2 -1
  121. package/src/CheckboxInput/CheckboxInput.test.tsx +44 -5
  122. package/src/CheckboxInput/CheckboxInput.tsx +16 -26
  123. package/src/CheckboxInput/__tests__/Checkbox.a11y.states.ts +3 -3
  124. package/src/CheckboxList/CheckboxList.test.tsx +9 -6
  125. package/src/ClickableCard/ClickableCard.doc.mjs +2 -2
  126. package/src/ClickableCard/ClickableCard.test.tsx +5 -4
  127. package/src/ClickableCard/ClickableCard.tsx +9 -9
  128. package/src/CodeBlock/CodeBlock.highlightStyles.test.tsx +87 -0
  129. package/src/CodeBlock/CodeBlock.tsx +11 -1
  130. package/src/ComplexSelector/ComplexSelector.focusableDisabled.test.tsx +76 -0
  131. package/src/ComplexSelector/ComplexSelector.tsx +21 -4
  132. package/src/ContextMenu/ContextMenu.test.tsx +41 -0
  133. package/src/DateInput/DateInput.doc.mjs +9 -8
  134. package/src/DateInput/DateInput.test.tsx +88 -13
  135. package/src/DateInput/DateInput.tsx +49 -21
  136. package/src/DateInput/DateInputTouch.test.tsx +22 -1
  137. package/src/DateInput/NativeDateField.test.tsx +29 -3
  138. package/src/DateInput/NativeDateField.tsx +20 -7
  139. package/src/DateInput/TouchDateField.tsx +24 -9
  140. package/src/DateRangeInput/DateRangeInput.doc.mjs +6 -5
  141. package/src/DateRangeInput/DateRangeInput.spec.md +13 -13
  142. package/src/DateRangeInput/DateRangeInput.test.tsx +82 -16
  143. package/src/DateRangeInput/DateRangeInput.tsx +40 -20
  144. package/src/DateTimeInput/DateTimeInput.doc.mjs +9 -8
  145. package/src/DateTimeInput/DateTimeInput.test.tsx +126 -11
  146. package/src/DateTimeInput/DateTimeInput.tsx +42 -26
  147. package/src/DateTimeInput/DateTimeInputTouch.test.tsx +24 -0
  148. package/src/DateTimeInput/NativeDateSegment.tsx +9 -6
  149. package/src/DateTimeInput/NativePickerSegments.test.tsx +31 -0
  150. package/src/DateTimeInput/NativeTimeSegment.tsx +9 -6
  151. package/src/DateTimeInput/TouchDateTimeField.tsx +22 -7
  152. package/src/Dialog/Dialog.test.tsx +39 -0
  153. package/src/Dialog/Dialog.tsx +9 -4
  154. package/src/DropdownMenu/DropdownMenu.spec.md +33 -2
  155. package/src/DropdownMenu/DropdownMenu.tsx +79 -8
  156. package/src/DropdownMenu/DropdownMenuContext.public.test.ts +22 -0
  157. package/src/DropdownMenu/DropdownMenuContext.tsx +39 -1
  158. package/src/DropdownMenu/DropdownMenuItem.doc.mjs +66 -7
  159. package/src/DropdownMenu/DropdownMenuItem.spec.md +209 -0
  160. package/src/DropdownMenu/DropdownMenuSubMenu.doc.mjs +16 -1
  161. package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +623 -1
  162. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +556 -99
  163. package/src/DropdownMenu/__tests__/DropdownMenuItemAudit.a11y.chromium.spec.ts +366 -0
  164. package/src/DropdownMenu/__tests__/SubMenuHeaderFooter.a11y.chromium.spec.ts +267 -0
  165. package/src/DropdownMenu/menuRegionStops.ts +2 -2
  166. package/src/DropdownMenu/modules/useMenuPress.spec.md +9 -0
  167. package/src/FieldStatus/FieldStatus.spec.md +3 -2
  168. package/src/FileInput/FileInput.test.tsx +30 -7
  169. package/src/FileInput/FileInput.tsx +21 -11
  170. package/src/Link/Link.test.tsx +21 -7
  171. package/src/Link/Link.tsx +16 -24
  172. package/src/Link/__tests__/Link.a11y.states.ts +1 -1
  173. package/src/Markdown/Markdown.rerender.test.tsx +143 -0
  174. package/src/Markdown/Markdown.tsx +89 -35
  175. package/src/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.ts +71 -0
  176. package/src/MultiSelector/MultiSelector.doc.mjs +5 -5
  177. package/src/MultiSelector/MultiSelector.focusableDisabled.test.tsx +121 -0
  178. package/src/MultiSelector/MultiSelector.test.tsx +49 -7
  179. package/src/MultiSelector/MultiSelector.tsx +53 -29
  180. package/src/Pagination/Pagination.test.tsx +5 -1
  181. package/src/Pagination/Pagination.tsx +3 -1
  182. package/src/Popover/Popover.test.tsx +15 -0
  183. package/src/Popover/Popover.tsx +19 -8
  184. package/src/PowerSearch/PowerSearchFilterEditor.test.tsx +3 -2
  185. package/src/PowerSearch/PowerSearchToken.test.tsx +7 -5
  186. package/src/Selector/Selector.doc.mjs +4 -4
  187. package/src/Selector/Selector.focusableDisabled.test.tsx +127 -0
  188. package/src/Selector/Selector.spec.md +11 -11
  189. package/src/Selector/Selector.test.tsx +64 -6
  190. package/src/Selector/Selector.tsx +51 -21
  191. package/src/Selector/SelectorOption.tsx +4 -0
  192. package/src/SideNav/SideNav.test.tsx +35 -0
  193. package/src/Slider/Slider.spec.md +11 -11
  194. package/src/Slider/Slider.test.tsx +3 -3
  195. package/src/Slider/Slider.tsx +8 -15
  196. package/src/Switch/Switch.test.tsx +85 -8
  197. package/src/Switch/Switch.tsx +26 -16
  198. package/src/Switch/__tests__/Switch.a11y.states.ts +2 -2
  199. package/src/Switch/switch.markers.stylex.ts +4 -0
  200. package/src/Table/Table.spec.md +2 -0
  201. package/src/Table/plugins/infiniteScroll/useTableInfiniteScroll.spec.md +244 -0
  202. package/src/Table/plugins/selection/useTableSelection.test.tsx +5 -3
  203. package/src/Table/plugins/virtualization/useTableVirtualization.spec.md +266 -0
  204. package/src/TimeInput/NativeTimeInput.test.tsx +25 -1
  205. package/src/TimeInput/Presentation.test.tsx +23 -0
  206. package/src/TimeInput/TimeInput.doc.mjs +13 -10
  207. package/src/TimeInput/TimeInput.test.tsx +139 -7
  208. package/src/TimeInput/TimeInput.tsx +53 -24
  209. package/src/TimeInput/TouchTimeField.tsx +27 -5
  210. package/src/Timestamp/Timestamp.doc.mjs +1 -1
  211. package/src/Timestamp/Timestamp.test.tsx +168 -0
  212. package/src/Timestamp/Timestamp.tsx +62 -11
  213. package/src/Token/Token.test.tsx +26 -4
  214. package/src/Token/Token.tsx +6 -2
  215. package/src/Tokenizer/Tokenizer.spec.md +28 -28
  216. package/src/Tooltip/Tooltip.test.tsx +51 -6
  217. package/src/Tooltip/Tooltip.tsx +41 -18
  218. package/src/Tooltip/__tests__/Tooltip.a11y.chromium.spec.ts +129 -0
  219. package/src/Tooltip/__tests__/Tooltip.a11y.renders.tsx +86 -0
  220. package/src/Tooltip/__tests__/Tooltip.a11y.states.ts +132 -0
  221. package/src/Tooltip/__tests__/Tooltip.a11y.test.tsx +126 -0
  222. package/src/Tooltip/__tests__/TooltipHoverBridge.a11y.chromium.spec.ts +74 -0
  223. package/src/Tooltip/useTooltip.test.tsx +23 -0
  224. package/src/Tooltip/useTooltip.tsx +22 -9
  225. package/src/TopNav/TopNav.tsx +11 -1
  226. package/src/TopNav/TopNavHeading.tsx +1 -0
  227. package/src/TopNav/__tests__/TopNavMobileLongHeading.a11y.chromium.spec.ts +74 -0
  228. package/src/TreeList/TreeList.test.tsx +28 -0
  229. package/src/TreeList/TreeListItem.tsx +6 -2
  230. package/src/Typeahead/Typeahead.test.tsx +89 -0
  231. package/src/Typeahead/Typeahead.tsx +15 -2
  232. package/src/__tests__/DisabledTriggerFocus.a11y.chromium.spec.ts +86 -0
  233. package/src/hooks/useGridFocus.ts +16 -1
  234. package/src/hooks/useMenuPress.ts +45 -7
  235. package/src/utils/disabledInteraction.ts +40 -0
  236. package/src/utils/timeParser.test.ts +11 -0
  237. package/src/utils/timeParser.ts +9 -2
@@ -304,7 +304,7 @@ export const docs = {
304
304
  name: 'disabledMessage',
305
305
  type: 'string',
306
306
  description:
307
- 'Explains why the selector is disabled. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the trigger focusable via aria-disabled (activation stays blocked). Use this instead of wrapping a disabled MultiSelector in Tooltip. Disabled controls swallow the hover events an external Tooltip needs.',
307
+ '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 MultiSelector in Tooltip.',
308
308
  },
309
309
  {
310
310
  name: 'isLabelHidden',
@@ -511,7 +511,7 @@ export const docs = {
511
511
  {
512
512
  guidance: false,
513
513
  description:
514
- 'Wrap a disabled MultiSelector in Tooltip to explain why it is disabled; disabled triggers swallow the hover events the wrapper needs. Use the disabledMessage prop instead.',
514
+ 'Wrap a disabled MultiSelector in Tooltip to explain why it is disabled. Use the disabledMessage prop, which attaches the reason to the trigger itself.',
515
515
  },
516
516
  ],
517
517
  },
@@ -552,7 +552,7 @@ export const docsZh = {
552
552
  htmlName:
553
553
  '用于表单提交的 HTML name 属性。为每个已选值渲染一个隐藏输入,类似原生多选。',
554
554
  disabledMessage:
555
- '解释选择器被禁用的原因。与 isDisabled 一起使用时,悬停/键盘聚焦时显示工具提示,并通过 aria-disabled 保持触发器可聚焦(仍无法激活)。请使用此属性,而不是用 Tooltip 包裹被禁用的选择器。',
555
+ '解释选择器被禁用的原因。与 isDisabled 一起使用时,悬停/键盘聚焦时显示工具提示,触发器与所有被禁用的选择器一样通过 aria-disabled 保持可聚焦(仍无法激活)。请使用此属性,而不是用 Tooltip 包裹被禁用的选择器。',
556
556
  isLabelHidden: '视觉上隐藏标签同时保持其可访问性。',
557
557
  description: '标签下方显示的辅助文本。',
558
558
  isOptional: '将字段标记为可选。',
@@ -619,7 +619,7 @@ export const docsZh = {
619
619
  {
620
620
  guidance: false,
621
621
  description:
622
- 'Wrap a disabled MultiSelector in Tooltip to explain why it is disabled; disabled triggers swallow the hover events the wrapper needs. Use the disabledMessage prop instead.',
622
+ 'Wrap a disabled MultiSelector in Tooltip to explain why it is disabled. Use the disabledMessage prop, which attaches the reason to the trigger itself.',
623
623
  },
624
624
  ],
625
625
  },
@@ -674,7 +674,7 @@ export const docsDense = {
674
674
  {
675
675
  guidance: false,
676
676
  description:
677
- 'Wrap a disabled MultiSelector in Tooltip to explain why it is disabled; disabled triggers swallow the hover events the wrapper needs. Use the disabledMessage prop instead.',
677
+ 'Wrap a disabled MultiSelector in Tooltip to explain why it is disabled. Use the disabledMessage prop, which attaches the reason to the trigger itself.',
678
678
  },
679
679
  ],
680
680
  },
@@ -0,0 +1,121 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file MultiSelector.focusableDisabled.test.tsx
5
+ * @input Uses vitest, @testing-library/react, StyleX, MultiSelector
6
+ * @output Conformance tests for spec:AST-085 FR1–FR3 and FR6 on MultiSelector;
7
+ * FR4 needs a real engine (MultiSelector.disabledFocus.a11y.chromium.spec.ts)
8
+ * @position Testing; validates the disabled trigger of MultiSelector.tsx
9
+ */
10
+
11
+ import {describe, it, expect, vi} from 'vitest';
12
+ import {render, screen} from '@testing-library/react';
13
+ import userEvent from '@testing-library/user-event';
14
+ import * as stylex from '@stylexjs/stylex';
15
+ import {colorVars} from '../theme/tokens.stylex';
16
+ import {MultiSelector} from './MultiSelector';
17
+
18
+ type Handler = () => void;
19
+ function Harness(props: {
20
+ isDisabled: boolean;
21
+ variant?: 'input' | 'secondary' | 'ghost';
22
+ onClick?: Handler;
23
+ onKeyDown?: Handler;
24
+ onMouseDown?: Handler;
25
+ onDoubleClick?: Handler;
26
+ }) {
27
+ return (
28
+ <form aria-label="Form">
29
+ <button type="button">Before</button>
30
+ <MultiSelector
31
+ label="Fruit"
32
+ htmlName="fruit"
33
+ options={['Apple', 'Banana']}
34
+ value={['Banana']}
35
+ onChange={() => {}}
36
+ {...props}
37
+ />
38
+ <button type="button">After</button>
39
+ </form>
40
+ );
41
+ }
42
+
43
+ const trigger = () => screen.getByRole('combobox', {name: 'Fruit'});
44
+
45
+ describe('MultiSelector disabled (spec:AST-085)', () => {
46
+ it('FR1: renders no native disabled and keeps its tab stop', async () => {
47
+ const user = userEvent.setup();
48
+ const {container} = render(<Harness isDisabled />);
49
+ expect(container.querySelector('[disabled]')).toBeNull();
50
+ expect(trigger()).toHaveAttribute('aria-disabled', 'true');
51
+ await user.click(screen.getByRole('button', {name: 'Before'}));
52
+ await user.tab();
53
+ expect(trigger()).toHaveFocus();
54
+ await user.tab();
55
+ expect(screen.getByRole('button', {name: 'After'})).toHaveFocus();
56
+ });
57
+
58
+ it('FR2: no press or key opens it or reaches a caller or ancestor handler', async () => {
59
+ const user = userEvent.setup();
60
+ const handlers = {
61
+ onClick: vi.fn(),
62
+ onKeyDown: vi.fn(),
63
+ onMouseDown: vi.fn(),
64
+ onDoubleClick: vi.fn(),
65
+ };
66
+ const onAncestorClick = vi.fn();
67
+ render(
68
+ <div onClick={onAncestorClick}>
69
+ <Harness isDisabled {...handlers} />
70
+ </div>,
71
+ );
72
+ await user.dblClick(trigger());
73
+ trigger().focus();
74
+ for (const key of ['{Enter}', ' ', '{ArrowDown}', 'b']) {
75
+ await user.keyboard(key);
76
+ }
77
+ expect(trigger()).toHaveAttribute('aria-expanded', 'false');
78
+ expect(screen.queryByRole('listbox')).toBeNull();
79
+ for (const handler of [...Object.values(handlers), onAncestorClick]) {
80
+ expect(handler).not.toHaveBeenCalled();
81
+ }
82
+ });
83
+
84
+ it('FR3: submits no value while disabled', () => {
85
+ const {rerender} = render(<Harness isDisabled={false} />);
86
+ const form = () =>
87
+ screen.getByRole<HTMLFormElement>('form', {name: 'Form'});
88
+ expect(new FormData(form()).getAll('fruit')).toEqual(['Banana']);
89
+ rerender(<Harness isDisabled />);
90
+ expect(new FormData(form()).getAll('fruit')).toEqual([]);
91
+ });
92
+
93
+ // jsdom cannot match `:focus-visible`: compare the ring's StyleX class hash.
94
+ const RING = stylex
95
+ .props(
96
+ stylex.create({
97
+ ring: {
98
+ boxShadow: {
99
+ default: null,
100
+ ':has(> [aria-disabled="true"]:focus-visible)': `inset 0px 0px 0px 2px ${colorVars['--color-accent-muted']}`,
101
+ },
102
+ },
103
+ }).ring,
104
+ )
105
+ .className!.split(' ')
106
+ .pop()!;
107
+
108
+ it.each([
109
+ ['input', true],
110
+ ['secondary', false],
111
+ ['ghost', false],
112
+ ] as const)(
113
+ 'FR6: a disabled %s trigger carries its own ring: %s',
114
+ (variant, ring) => {
115
+ render(<Harness isDisabled variant={variant} />);
116
+ const wrapper = trigger().parentElement!;
117
+ expect(wrapper.classList.contains(RING)).toBe(ring);
118
+ expect(getComputedStyle(wrapper).cursor).toBe('default');
119
+ },
120
+ );
121
+ });
@@ -35,7 +35,7 @@ import {generateThemeCSS} from '../theme/generateThemeRules';
35
35
  import {selectorPresentationStyles} from '../Selector/selectorPresentation.stylex';
36
36
  import {groupStyles} from '../InputGroup/groupStyles';
37
37
  import {InputGroup, InputGroupText} from '../InputGroup';
38
- import {focusVars} from '../theme/tokens.stylex';
38
+ import {colorVars, focusVars} from '../theme/tokens.stylex';
39
39
 
40
40
  function generateThemeTestCSS(theme: Parameters<typeof generateThemeCSS>[0]) {
41
41
  const {prose, component} = generateThemeCSS(theme);
@@ -210,6 +210,45 @@ describe('MultiSelector', () => {
210
210
  );
211
211
  });
212
212
 
213
+ it('shows no disabled focus ring after a pointer pick (spec:AST-085 FR6)', async () => {
214
+ const field = (isDisabled: boolean) => (
215
+ <MultiSelector
216
+ label="Fruit"
217
+ options={defaultOptions}
218
+ value={[]}
219
+ onChange={() => {}}
220
+ presentation="bottom-sheet"
221
+ isDisabled={isDisabled}
222
+ />
223
+ );
224
+ const {rerender} = render(field(false));
225
+ const trigger = screen.getByRole('combobox');
226
+ fireEvent.pointerDown(trigger, {pointerType: 'touch'});
227
+ fireEvent.click(trigger, {detail: 1});
228
+ const dialog = await screen.findByRole('dialog', {name: 'Fruit'});
229
+ fireEvent.pointerDown(dialog, {pointerType: 'touch'});
230
+ fireEvent.click(dialog, {detail: 1});
231
+ rerender(field(true));
232
+ const ring = stylex
233
+ .props(
234
+ stylex.create({
235
+ ring: {
236
+ boxShadow: {
237
+ default: null,
238
+ ':has(> [aria-disabled="true"]:focus-visible)': `inset 0px 0px 0px 2px ${colorVars['--color-accent-muted']}`,
239
+ },
240
+ },
241
+ }).ring,
242
+ )
243
+ .className!.split(' ')
244
+ .pop()!;
245
+ const wrapper = screen.getByRole('combobox').parentElement!;
246
+ expect(wrapper).toHaveClass(
247
+ stylex.props(selectorPresentationStyles.pointerRestoredFocus).className!,
248
+ );
249
+ expect(wrapper).not.toHaveClass(ring);
250
+ });
251
+
213
252
  it('renders with label', () => {
214
253
  render(
215
254
  <MultiSelector
@@ -515,7 +554,9 @@ describe('MultiSelector', () => {
515
554
  isDisabled
516
555
  />,
517
556
  );
518
- expect(screen.getByRole('combobox')).toBeDisabled();
557
+ const trigger = screen.getByRole('combobox');
558
+ expect(trigger).not.toHaveAttribute('disabled');
559
+ expect(trigger).toHaveAttribute('aria-disabled', 'true');
519
560
  });
520
561
 
521
562
  it('has correct ARIA attributes', () => {
@@ -1602,7 +1643,7 @@ describe('MultiSelector', () => {
1602
1643
  expect(screen.getByRole('combobox')).toHaveFocus();
1603
1644
  });
1604
1645
 
1605
- it('trigger is not focusable when disabled', () => {
1646
+ it('trigger keeps its tab stop when disabled (spec:AST-085 FR1)', () => {
1606
1647
  render(
1607
1648
  <MultiSelector
1608
1649
  label="Fruit"
@@ -1612,7 +1653,7 @@ describe('MultiSelector', () => {
1612
1653
  isDisabled
1613
1654
  />,
1614
1655
  );
1615
- expect(screen.getByRole('combobox')).toHaveAttribute('tabIndex', '-1');
1656
+ expect(screen.getByRole('combobox')).toHaveAttribute('tabIndex', '0');
1616
1657
  });
1617
1658
 
1618
1659
  it('opens the listbox with ArrowDown from a focused trigger', async () => {
@@ -2076,7 +2117,7 @@ describe('MultiSelector', () => {
2076
2117
  expect(onChange).not.toHaveBeenCalled();
2077
2118
  });
2078
2119
 
2079
- it('remains non-focusable when disabled without a reason', () => {
2120
+ it('stays focusable when disabled without a reason (spec:AST-085 FR1)', () => {
2080
2121
  render(
2081
2122
  <MultiSelector
2082
2123
  label="Fruit"
@@ -2087,8 +2128,9 @@ describe('MultiSelector', () => {
2087
2128
  />,
2088
2129
  );
2089
2130
  const trigger = screen.getByRole('combobox');
2090
- expect(trigger).toBeDisabled();
2091
- expect(trigger).toHaveAttribute('tabIndex', '-1');
2131
+ expect(trigger).not.toHaveAttribute('disabled');
2132
+ expect(trigger).toHaveAttribute('aria-disabled', 'true');
2133
+ expect(trigger).toHaveAttribute('tabIndex', '0');
2092
2134
  });
2093
2135
  });
2094
2136
  describe('isReadOnly', () => {
@@ -271,6 +271,21 @@ const styles = stylex.create({
271
271
  '[data-astryx-press="on"]': 'scale(0.98)',
272
272
  },
273
273
  },
274
+ // A disabled input-variant trigger keeps its tab stop (`spec:AST-085` FR1),
275
+ // and the shared disabled wrapper style drops the field focus ring, so the
276
+ // trigger shows keyboard focus here instead (FR6). It keys on the trigger
277
+ // itself, a direct child, so a focused status button keeps its own outline.
278
+ triggerDisabledFocus: {
279
+ borderColor: {
280
+ default: colorVars['--color-border-emphasized'],
281
+ ':has(> [aria-disabled="true"]:focus-visible)':
282
+ colorVars['--color-accent'],
283
+ },
284
+ boxShadow: {
285
+ default: 'none',
286
+ ':has(> [aria-disabled="true"]:focus-visible)': `inset 0px 0px 0px 2px ${colorVars['--color-accent-muted']}`,
287
+ },
288
+ },
274
289
  triggerGhostDisabled: {
275
290
  backgroundImage: 'none',
276
291
  transform: {
@@ -688,8 +703,8 @@ export interface MultiSelectorProps<
688
703
  * The selected values stay visible, focusable, and included in form
689
704
  * submission, and retain their combobox identity with `aria-readonly`. The
690
705
  * selection surface and editing affordances are removed.
691
- * Unlike `isDisabled`, a read-only selector is not dimmed and stays in the
692
- * tab order. `isDisabled` takes precedence when both are set.
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` — disabled
705
- * controls don't emit the pointer events an external tooltip needs.
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
- ...rest,
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: composeEventHandlers(onKeyDown, onKeyDownProp),
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: composeEventHandlers(handleTriggerButtonClick, onClick),
2432
- tabIndex: isDisabled && !showsDisabledMessage ? -1 : 0,
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
- // With a disabledMessage the trigger keeps focusability via
2602
- // aria-disabled so the reason is focus-discoverable; activation is
2603
- // still blocked by the isDisabled guards in useMultiCombobox.
2604
- disabled={isDisabled && !showsDisabledMessage}
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
- expect(dot).toBeDisabled();
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
- disabled={isDisabled}
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">
@@ -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
- if (!isEnabled) {
486
- return;
487
- }
488
- popover.toggle();
489
- }, [isEnabled, popover]);
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
- onClick: handleTriggerClick,
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
- expect(fieldTrigger).toBeDisabled();
374
- expect(operatorTrigger).toBeDisabled();
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('disables both the activation and remove buttons when isDisabled is set', () => {
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
- expect(screen.getByRole('button', {name: 'Status: is'})).toBeDisabled();
224
- expect(
225
- screen.getByRole('button', {name: 'Remove Status: is'}),
226
- ).toBeDisabled();
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/keyboard focus and keeps the trigger focusable via aria-disabled (activation stays blocked). Use this instead of wrapping a disabled Selector in Tooltip. Disabled controls swallow the hover events an external Tooltip needs.',
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; disabled triggers swallow the hover events the wrapper needs. Use the disabledMessage prop instead.',
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; disabled triggers swallow the hover events the wrapper needs. Use the disabledMessage prop instead.',
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; disabled triggers swallow the hover events the wrapper needs. Use the disabledMessage prop instead.',
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,