@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
@@ -90,12 +90,12 @@ preset-list anatomy and its additive theming surface.
90
90
 
91
91
  ### Representative states
92
92
 
93
- | State | Required invariant | Allowed variation |
94
- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- |
95
- | No presets | No preset group or preset-button target renders. | Calendar configuration |
96
- | Presets, no match | Every button carries the preset target with no selected state. | Preset count and labels |
97
- | Applied preset | The matching button carries `aria-current="true"` and the target's selected state. | Selected range |
98
- | Disabled preset | A preset that violates an endpoint date constraint or a range-span constraint is natively disabled and carries the target's disabled state. | Constraint source |
93
+ | State | Required invariant | Allowed variation |
94
+ | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- |
95
+ | No presets | No preset group or preset-button target renders. | Calendar configuration |
96
+ | Presets, no match | Every button carries the preset target with no selected state. | Preset count and labels |
97
+ | Applied preset | The matching button carries `aria-current="true"` and the target's selected state. | Selected range |
98
+ | Disabled preset | A preset that violates an endpoint date constraint or a range-span constraint is disabled per `spec:AST-085` and carries the target's disabled state. | Constraint source |
99
99
 
100
100
  ### Transformation and precedence order
101
101
 
@@ -120,8 +120,8 @@ No new performance or resource constraint is introduced.
120
120
  - **AR2 — Current state.** The applied preset remains exposed with
121
121
  `aria-current="true"`; theme state reflection is additive.
122
122
  - **AR3 — Disabled state.** A preset that violates an endpoint date constraint or
123
- a range-span constraint remains natively disabled; theme state reflection does
124
- not replace that behavior.
123
+ a range-span constraint is disabled and stays focusable (`spec:AST-085`); theme
124
+ state reflection does not replace that behavior.
125
125
 
126
126
  ## Design relationships
127
127
 
@@ -172,11 +172,11 @@ No new performance or resource constraint is introduced.
172
172
 
173
173
  ## Verification map
174
174
 
175
- | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
176
- | ------------------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------- | ------------------------------ |
177
- | FR1, FR2, AR1–AR3 | `DateRangeInput.test.tsx` | no match, applied preset, endpoint-invalid preset, span-invalid preset | Semantics or state attributes disappear | `audit:DateRangeInput/presets` |
178
- | FR3, FR4 | `DateRangeInput.test.tsx`, `themingTargets.test.ts`, generated probe theme | group target, selected button, disabled button | Target class/state is missing, undocumented, or placed on the wrong element | `audit:DateRangeInput/theming` |
179
- | Theming anatomy map | `scripts/check-knowledge.mjs` | all documented anatomy | Target ownership or anatomy names drift | `audit:DateRangeInput/theming` |
175
+ | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
176
+ | ------------------- | --------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------- | ------------------------------ |
177
+ | FR1, FR2, AR1–AR3 | `DateRangeInput.test.tsx`; disabled-preset focusability (AR3) pending conformance | no match, applied preset, endpoint-invalid preset, span-invalid preset | Semantics or state attributes disappear | `audit:DateRangeInput/presets` |
178
+ | FR3, FR4 | `DateRangeInput.test.tsx`, `themingTargets.test.ts`, generated probe theme | group target, selected button, disabled button | Target class/state is missing, undocumented, or placed on the wrong element | `audit:DateRangeInput/theming` |
179
+ | Theming anatomy map | `scripts/check-knowledge.mjs` | all documented anatomy | Target ownership or anatomy names drift | `audit:DateRangeInput/theming` |
180
180
 
181
181
  ## Decision log
182
182
 
@@ -23,6 +23,8 @@ function generateThemeTestCSS(theme: Parameters<typeof generateThemeCSS>[0]) {
23
23
  // made every role+name query compute ~85 accessible names through jsdom's
24
24
  // slow getComputedStyle (~450ms per query). See fastRoleQueries.ts.
25
25
  import {getButton, queryButton} from '../__tests__/fastRoleQueries';
26
+ import * as stylex from '@stylexjs/stylex';
27
+ import {colorVars} from '../theme/tokens.stylex';
26
28
  import {DateRangeInput} from './DateRangeInput';
27
29
  import type {DateRange} from './DateRangeInput';
28
30
  import {Icon} from '../Icon';
@@ -155,7 +157,46 @@ describe('DateRangeInput', () => {
155
157
  />,
156
158
  );
157
159
  const trigger = getButton(/Range/);
158
- expect(trigger).toBeDisabled();
160
+ expect(trigger).not.toHaveAttribute('disabled');
161
+ expect(trigger).toHaveAttribute('aria-disabled', 'true');
162
+ trigger.focus();
163
+ expect(trigger).toHaveFocus();
164
+ });
165
+
166
+ it('restores the focus ring on a focused disabled field (AST-085 FR6)', () => {
167
+ // The same declarations as the field's ring style compile to the same
168
+ // atomic classes.
169
+ const ringClasses = stylex
170
+ .props(
171
+ stylex.create({
172
+ ring: {
173
+ borderColor: {
174
+ default: colorVars['--color-border-emphasized'],
175
+ ':focus-within': colorVars['--color-accent'],
176
+ },
177
+ boxShadow: {
178
+ default: 'none',
179
+ ':focus-within': `inset 0px 0px 0px 2px ${colorVars['--color-accent-muted']}`,
180
+ },
181
+ },
182
+ }).ring,
183
+ )
184
+ .className!.split(' ')
185
+ // Keep the atomic classes, not the dev-only debug name.
186
+ .filter(name => !name.includes('__'));
187
+ render(
188
+ <DateRangeInput
189
+ label="Range"
190
+ isDisabled
191
+ value={null}
192
+ onChange={() => {}}
193
+ />,
194
+ );
195
+ // The shared disabled style alone drops these :focus-within declarations.
196
+ const wrapper = getButton(/Range/).parentElement!;
197
+ for (const name of ringClasses) {
198
+ expect(wrapper).toHaveClass(name);
199
+ }
159
200
  });
160
201
 
161
202
  it('is not disabled by default', () => {
@@ -272,7 +313,8 @@ describe('DateRangeInput', () => {
272
313
  onChange={() => {}}
273
314
  />,
274
315
  );
275
- expect(getButton('Open calendar')).toBeDisabled();
316
+ expect(getButton('Open calendar')).not.toHaveAttribute('disabled');
317
+ expect(getButton('Open calendar')).toHaveAttribute('aria-disabled', 'true');
276
318
  });
277
319
 
278
320
  it('renders with size="lg"', () => {
@@ -526,7 +568,8 @@ describe('DateRangeInput', () => {
526
568
  );
527
569
 
528
570
  const preset = getButton('Last 7 days');
529
- expect(preset).toBeDisabled();
571
+ expect(preset).not.toHaveAttribute('disabled');
572
+ expect(preset).toHaveAttribute('aria-disabled', 'true');
530
573
  fireEvent.click(preset);
531
574
  expect(handleChange).not.toHaveBeenCalled();
532
575
  });
@@ -544,7 +587,8 @@ describe('DateRangeInput', () => {
544
587
  );
545
588
 
546
589
  const preset = getButton('Last 7 days');
547
- expect(preset).toBeDisabled();
590
+ expect(preset).not.toHaveAttribute('disabled');
591
+ expect(preset).toHaveAttribute('aria-disabled', 'true');
548
592
  fireEvent.click(preset);
549
593
  expect(handleChange).not.toHaveBeenCalled();
550
594
  });
@@ -562,7 +606,8 @@ describe('DateRangeInput', () => {
562
606
  );
563
607
 
564
608
  const preset = getButton('Last 7 days');
565
- expect(preset).toBeDisabled();
609
+ expect(preset).not.toHaveAttribute('disabled');
610
+ expect(preset).toHaveAttribute('aria-disabled', 'true');
566
611
  fireEvent.click(preset);
567
612
  expect(handleChange).not.toHaveBeenCalled();
568
613
  });
@@ -590,7 +635,7 @@ describe('DateRangeInput', () => {
590
635
 
591
636
  const getRangeCallsBeforeClick = getRange.mock.calls.length;
592
637
  const preset = getButton('Allowed range');
593
- expect(preset).not.toBeDisabled();
638
+ expect(preset).not.toHaveAttribute('aria-disabled');
594
639
  fireEvent.click(preset);
595
640
  expect(handleChange).toHaveBeenCalledWith(range);
596
641
  expect(getRangeCallsAtChange).toBe(getRangeCallsBeforeClick);
@@ -613,7 +658,10 @@ describe('DateRangeInput', () => {
613
658
  // accessibility tree; use hidden: true to find it.
614
659
  const h = {hidden: true} as const;
615
660
 
616
- function renderDisabled(props?: {disabledMessage?: string}) {
661
+ function renderDisabled(props?: {
662
+ disabledMessage?: string;
663
+ onClick?: () => void;
664
+ }) {
617
665
  return render(
618
666
  <DateRangeInput
619
667
  label="Range"
@@ -699,11 +747,22 @@ describe('DateRangeInput', () => {
699
747
  expect(trigger).toHaveAttribute('aria-expanded', 'false');
700
748
  });
701
749
 
702
- it('remains natively disabled when disabled without a reason', () => {
703
- renderDisabled();
750
+ it('blocks activation and a caller onClick when disabled without a reason (AST-085 FR1, FR2)', async () => {
751
+ const user = userEvent.setup();
752
+ const onClick = vi.fn();
753
+ renderDisabled({onClick});
704
754
  const trigger = getButton(/Range:/);
705
- expect(trigger).toBeDisabled();
706
- expect(trigger).not.toHaveAttribute('aria-disabled');
755
+ expect(trigger).not.toHaveAttribute('disabled');
756
+ await user.click(trigger);
757
+ await user.keyboard('{Enter}');
758
+ expect(trigger).toHaveAttribute('aria-expanded', 'false');
759
+ expect(onClick).not.toHaveBeenCalled();
760
+ });
761
+
762
+ it('blocks the calendar button while disabled (AST-085 FR2)', () => {
763
+ renderDisabled();
764
+ fireEvent.click(getButton('Open calendar'));
765
+ expect(getButton(/Range:/)).toHaveAttribute('aria-expanded', 'false');
707
766
  });
708
767
  });
709
768
  });
@@ -988,13 +1047,19 @@ describe('DateRangeInput range-span forwarding', () => {
988
1047
  fireEvent.click(getButton('Open calendar'));
989
1048
 
990
1049
  // Before a start is picked, a far-off day is selectable.
991
- expect(dayButton('2026-01-20')).not.toBeDisabled();
1050
+ expect(dayButton('2026-01-20')).not.toHaveAttribute(
1051
+ 'aria-disabled',
1052
+ 'true',
1053
+ );
992
1054
 
993
1055
  fireEvent.click(dayButton('2026-01-10') as HTMLButtonElement);
994
1056
 
995
1057
  // A 7-day window spans start ± 6 days: Jan 16 is the edge, Jan 17 is out.
996
- expect(dayButton('2026-01-16')).not.toBeDisabled();
997
- expect(dayButton('2026-01-17')).toBeDisabled();
1058
+ expect(dayButton('2026-01-16')).not.toHaveAttribute(
1059
+ 'aria-disabled',
1060
+ 'true',
1061
+ );
1062
+ expect(dayButton('2026-01-17')).toHaveAttribute('aria-disabled', 'true');
998
1063
  });
999
1064
 
1000
1065
  it('disables a preset whose range violates the span cap', () => {
@@ -1030,9 +1095,10 @@ describe('DateRangeInput range-span forwarding', () => {
1030
1095
  // The 3-day preset fits the 7-day cap; the 30-day preset can't be committed.
1031
1096
  const withinCap = getButton('Last 3 days');
1032
1097
  const overCap = getButton('Last 30 days');
1033
- expect(withinCap).not.toBeDisabled();
1098
+ expect(withinCap).not.toHaveAttribute('aria-disabled');
1034
1099
  expect(withinCap).not.toHaveAttribute('data-disabled');
1035
- expect(overCap).toBeDisabled();
1100
+ expect(overCap).not.toHaveAttribute('disabled');
1101
+ expect(overCap).toHaveAttribute('aria-disabled', 'true');
1036
1102
  expect(overCap).toHaveClass('astryx-date-range-input-preset');
1037
1103
  expect(overCap).toHaveAttribute('data-disabled', 'disabled');
1038
1104
 
@@ -97,6 +97,18 @@ export type {
97
97
  } from '../Field';
98
98
 
99
99
  const styles = stylex.create({
100
+ // The shared disabled style drops the focus ring; restore it while a
101
+ // control inside the field holds focus (spec:AST-085 FR6). Defaults repeat it.
102
+ disabledFocusRing: {
103
+ borderColor: {
104
+ default: colorVars['--color-border-emphasized'],
105
+ ':focus-within': colorVars['--color-accent'],
106
+ },
107
+ boxShadow: {
108
+ default: 'none',
109
+ ':focus-within': `inset 0px 0px 0px 2px ${colorVars['--color-accent-muted']}`,
110
+ },
111
+ },
100
112
  trigger: {
101
113
  display: 'block',
102
114
  flex: 1,
@@ -309,12 +321,12 @@ export interface DateRangeInputProps extends Omit<
309
321
  /**
310
322
  * Explains why the input is disabled. When set together with
311
323
  * `isDisabled`, the input shows a tooltip with this text on hover and
312
- * keyboard focus, and the trigger stays focusable (via `aria-disabled`)
313
- * so the reason is discoverable by keyboard and assistive technology.
314
- * Activation stays blocked.
324
+ * keyboard focus, and the trigger is described by it, so the reason is
325
+ * discoverable by keyboard and assistive technology. Activation stays
326
+ * blocked.
315
327
  *
316
- * Use this instead of wrapping a disabled input in `Tooltip` — disabled
317
- * controls don't emit the pointer events an external tooltip needs.
328
+ * Use this instead of wrapping a disabled input in `Tooltip`, so the
329
+ * reason is tied to the field.
318
330
  *
319
331
  * @example
320
332
  * ```
@@ -546,12 +558,12 @@ export function DateRangeInput({
546
558
  const isBusy = isLoading || optimisticValue !== value;
547
559
  const isEffectivelyDisabled = isDisabled || isBusy;
548
560
 
549
- // Disabled-reason tooltip. Disabled controls swallow pointer events, so the
550
- // tooltip listeners attach to the trigger container (which already exists)
551
- // and the trigger button stays perceivable via aria-disabled instead of the
552
- // disabled attribute. Activation is blocked by the isEffectivelyDisabled
553
- // guard in handleToggle. Only the persistent isDisabled state (not the
554
- // transient busy state) surfaces a reason.
561
+ // A disabled or busy field never renders native `disabled` (spec:AST-085
562
+ // FR1): its buttons stay focusable via aria-disabled, and activation is
563
+ // blocked by the isEffectivelyDisabled guard in handleToggle. The
564
+ // disabled-reason tooltip's listeners attach to the trigger container. Only
565
+ // the persistent isDisabled state (not the transient busy state) surfaces a
566
+ // reason.
555
567
  const showsDisabledMessage = isDisabled && !!disabledMessage;
556
568
  const disabledMessageTooltip = useTooltip({
557
569
  placement: 'above',
@@ -695,6 +707,7 @@ export function DateRangeInput({
695
707
  inputWrapperStyles.base,
696
708
  sizeStyles[size],
697
709
  isEffectivelyDisabled && inputWrapperStyles.disabled,
710
+ isEffectivelyDisabled && styles.disabledFocusRing,
698
711
  status && inputStatusBorderStyles[status.type],
699
712
  status &&
700
713
  !isEffectivelyDisabled &&
@@ -708,7 +721,7 @@ export function DateRangeInput({
708
721
  <button
709
722
  type="button"
710
723
  onClick={handleToggle}
711
- disabled={isEffectivelyDisabled}
724
+ aria-disabled={isEffectivelyDisabled || undefined}
712
725
  aria-label={
713
726
  popover.isOpen
714
727
  ? t('@astryx.dateInput.toggleCalendarClose')
@@ -734,12 +747,14 @@ export function DateRangeInput({
734
747
  ref={triggerButtonRef}
735
748
  id={id}
736
749
  type="button"
737
- onClick={chainEventHandlers(handleToggle, onClickProp)}
738
- // With a disabledMessage the trigger keeps focusability via
739
- // aria-disabled so the reason is focus-discoverable; activation is
740
- // still blocked by the isEffectivelyDisabled guard in handleToggle.
741
- disabled={isEffectivelyDisabled && !showsDisabledMessage}
742
- aria-disabled={showsDisabledMessage ? 'true' : undefined}
750
+ // The browser still dispatches a click on an aria-disabled button,
751
+ // so a caller's onClick is skipped too (spec:AST-085 FR2).
752
+ onClick={
753
+ isEffectivelyDisabled
754
+ ? undefined
755
+ : chainEventHandlers(handleToggle, onClickProp)
756
+ }
757
+ aria-disabled={isEffectivelyDisabled ? 'true' : undefined}
743
758
  // A caller aria-label replaces the trigger's name; a caller
744
759
  // aria-labelledby is read after it (label and selected range).
745
760
  aria-label={ariaLabelProp ?? triggerAriaLabel}
@@ -807,8 +822,13 @@ export function DateRangeInput({
807
822
  // marked with aria-current (not aria-selected, a listbox
808
823
  // concept that contradicted the Tab interaction) (forms-5).
809
824
  aria-current={isActive ? 'true' : undefined}
810
- disabled={isPresetDisabled}
811
- onClick={() => handleRangeSelect(presetRange)}
825
+ // Focusable while disabled (AR3, spec:AST-085 FR1).
826
+ aria-disabled={isPresetDisabled || undefined}
827
+ onClick={() => {
828
+ if (!isPresetDisabled) {
829
+ handleRangeSelect(presetRange);
830
+ }
831
+ }}
812
832
  {...mergeProps(
813
833
  themeProps('date-range-input-preset', {
814
834
  selected: isActive ? 'selected' : null,
@@ -51,14 +51,15 @@ export const docs = {
51
51
  {
52
52
  name: 'isDisabled',
53
53
  type: 'boolean',
54
- description: 'Disable the input and picker.',
54
+ description:
55
+ 'Disable the input and picker. The fields stay focusable and expose `aria-disabled`; typing and opening are blocked.',
55
56
  default: 'false',
56
57
  },
57
58
  {
58
59
  name: 'disabledMessage',
59
60
  type: 'string',
60
61
  description:
61
- 'Explains why the input is disabled. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the field focusable via aria-disabled (activation stays blocked). Use this instead of wrapping a disabled DateTimeInput in Tooltip. Disabled controls swallow the hover events an external Tooltip needs.',
62
+ 'Explains why the input is disabled. With isDisabled, shows a tooltip on hover/keyboard focus and describes the fields (activation stays blocked). Use this instead of wrapping a disabled DateTimeInput in Tooltip.',
62
63
  },
63
64
  {
64
65
  name: 'value',
@@ -289,7 +290,7 @@ export const docs = {
289
290
  {
290
291
  guidance: false,
291
292
  description:
292
- 'Wrap a disabled DateTimeInput in Tooltip to explain why it is disabled; disabled triggers swallow the hover events the wrapper needs. Use the disabledMessage prop instead.',
293
+ 'Wrap a disabled DateTimeInput in Tooltip to explain why it is disabled. Use the disabledMessage prop instead, which ties the reason to the field.',
293
294
  },
294
295
  ],
295
296
  anatomy: [
@@ -412,7 +413,7 @@ export const docsZh = {
412
413
  {
413
414
  guidance: false,
414
415
  description:
415
- 'Wrap a disabled DateTimeInput in Tooltip to explain why it is disabled; disabled triggers swallow the hover events the wrapper needs. Use the disabledMessage prop instead.',
416
+ 'Wrap a disabled DateTimeInput in Tooltip to explain why it is disabled. Use the disabledMessage prop instead, which ties the reason to the field.',
416
417
  },
417
418
  ],
418
419
  },
@@ -444,14 +445,14 @@ export const docsZh = {
444
445
  {
445
446
  name: 'isDisabled',
446
447
  type: 'boolean',
447
- description: '禁用输入框和选择器。',
448
+ description: '禁用输入框和选择器。字段仍可聚焦并暴露 `aria-disabled`,但不能输入或打开选择器。',
448
449
  default: 'false',
449
450
  },
450
451
  {
451
452
  name: 'disabledMessage',
452
453
  type: 'string',
453
454
  description:
454
- '说明输入框为何被禁用。与 isDisabled 一起使用时,在悬停/键盘聚焦时显示提示,并通过 aria-disabled 保持日期和时间字段可聚焦(仍阻止输入和激活)。请使用此属性,而不是用 Tooltip 包裹已禁用的 DateTimeInput。',
455
+ '说明输入框为何被禁用。与 isDisabled 一起使用时,在悬停/键盘聚焦时显示提示,并描述日期和时间字段(仍阻止输入和激活)。请使用此属性,而不是用 Tooltip 包裹已禁用的 DateTimeInput。',
455
456
  },
456
457
  {
457
458
  name: 'value',
@@ -667,7 +668,7 @@ export const docsDense = {
667
668
  {
668
669
  guidance: false,
669
670
  description:
670
- 'Wrap a disabled DateTimeInput in Tooltip to explain why it is disabled; disabled triggers swallow the hover events the wrapper needs. Use the disabledMessage prop instead.',
671
+ 'Wrap a disabled DateTimeInput in Tooltip to explain why it is disabled. Use the disabledMessage prop instead, which ties the reason to the field.',
671
672
  },
672
673
  ],
673
674
  },
@@ -679,7 +680,7 @@ export const docsDense = {
679
680
  isRequired: 'mark field required',
680
681
  isDisabled: 'disable input+picker',
681
682
  disabledMessage:
682
- 'reason shown in a tooltip on hover/focus when disabled; keeps fields focusable via aria-disabled',
683
+ 'reason shown in a tooltip on hover/focus when disabled; describes the fields',
683
684
  value: 'selected datetime ISO 8601',
684
685
  onChange: 'callback on datetime change',
685
686
  changeAction: 'async action after onChange; drives optimistic UI',
@@ -14,6 +14,7 @@ import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest';
14
14
  import {render, screen, fireEvent, waitFor} from '@testing-library/react';
15
15
  import userEvent from '@testing-library/user-event';
16
16
  import * as stylex from '@stylexjs/stylex';
17
+ import {colorVars} from '../theme/tokens.stylex';
17
18
  import {getButton, queryButton} from '../__tests__/fastRoleQueries';
18
19
  import {DateTimeInput} from './DateTimeInput';
19
20
  import type {ISODateTimeString} from './DateTimeInput';
@@ -288,10 +289,52 @@ describe('DateTimeInput', () => {
288
289
  expect(screen.getByRole('combobox')).not.toHaveAttribute('aria-required');
289
290
  });
290
291
 
291
- it('sets disabled on both inputs when isDisabled is true', () => {
292
+ it('keeps both disabled inputs focusable without native disabled (AST-085 FR1)', () => {
292
293
  render(<DateTimeInput label="Meeting" isDisabled onChange={() => {}} />);
293
- expect(screen.getByRole('combobox')).toBeDisabled();
294
- expect(screen.getByLabelText('Meeting time')).toBeDisabled();
294
+ for (const input of [
295
+ screen.getByRole('combobox'),
296
+ screen.getByLabelText('Meeting time'),
297
+ ]) {
298
+ expect(input).not.toHaveAttribute('disabled');
299
+ expect(input).toHaveAttribute('aria-disabled', 'true');
300
+ expect(input).toHaveAttribute('readonly');
301
+ input.focus();
302
+ expect(input).toHaveFocus();
303
+ }
304
+ });
305
+
306
+ it('restores the focus ring on a focused disabled field (AST-085 FR6)', () => {
307
+ // The same declarations as the field's ring style compile to the same
308
+ // atomic classes; the shared disabled style alone drops them.
309
+ const ringClasses = stylex
310
+ .props(
311
+ stylex.create({
312
+ ring: {
313
+ borderColor: {
314
+ default: colorVars['--color-border-emphasized'],
315
+ ':focus-within': colorVars['--color-accent'],
316
+ },
317
+ boxShadow: {
318
+ default: 'none',
319
+ ':focus-within': `inset 0px 0px 0px 2px ${colorVars['--color-accent-muted']}`,
320
+ },
321
+ },
322
+ }).ring,
323
+ )
324
+ .className!.split(' ')
325
+ .filter(name => !name.includes('__'));
326
+ render(<DateTimeInput label="Meeting" isDisabled onChange={() => {}} />);
327
+ for (const input of [
328
+ screen.getByRole('combobox'),
329
+ screen.getByLabelText('Meeting time'),
330
+ ]) {
331
+ const wrapper = input.closest(
332
+ '.astryx-date-time-input-date-segment, .astryx-date-time-input-time-segment',
333
+ )!;
334
+ for (const name of ringClasses) {
335
+ expect(wrapper).toHaveClass(name);
336
+ }
337
+ }
295
338
  });
296
339
 
297
340
  it('is not disabled by default', () => {
@@ -366,14 +409,25 @@ describe('DateTimeInput', () => {
366
409
  it('calendar button is disabled when isDisabled is true', () => {
367
410
  render(<DateTimeInput label="Meeting" isDisabled onChange={() => {}} />);
368
411
  const button = getButton('Open calendar');
369
- expect(button).toBeDisabled();
412
+ expect(button).not.toHaveAttribute('disabled');
413
+ expect(button).toHaveAttribute('aria-disabled', 'true');
414
+ fireEvent.click(button);
415
+ expect(screen.getByRole('combobox')).toHaveAttribute(
416
+ 'aria-expanded',
417
+ 'false',
418
+ );
370
419
  });
371
420
 
372
421
  it('disables inputs and button when isLoading is true', () => {
373
422
  render(<DateTimeInput label="Meeting" isLoading onChange={() => {}} />);
374
- expect(screen.getByRole('combobox')).toBeDisabled();
375
- expect(screen.getByLabelText('Meeting time')).toBeDisabled();
376
- expect(getButton('Open calendar')).toBeDisabled();
423
+ for (const el of [
424
+ screen.getByRole('combobox'),
425
+ screen.getByLabelText('Meeting time'),
426
+ getButton('Open calendar'),
427
+ ]) {
428
+ expect(el).not.toHaveAttribute('disabled');
429
+ expect(el).toHaveAttribute('aria-disabled', 'true');
430
+ }
377
431
  });
378
432
 
379
433
  it('sets aria-busy when isLoading is true', () => {
@@ -872,6 +926,12 @@ describe('DateTimeInput', () => {
872
926
  );
873
927
 
874
928
  const tooltip = screen.getByRole('tooltip', h);
929
+ // The calendar button keeps its enabled tab stop (spec:AST-085 FR1).
930
+ await user.tab();
931
+ expect(getButton('Open calendar')).toHaveFocus();
932
+ expect(
933
+ getButton('Open calendar').getAttribute('aria-describedby'),
934
+ ).toContain(tooltip.id);
875
935
  await user.tab();
876
936
  expect(screen.getByRole('combobox')).toHaveFocus();
877
937
  await waitFor(() => {
@@ -948,11 +1008,48 @@ describe('DateTimeInput', () => {
948
1008
  expect(onChange).not.toHaveBeenCalled();
949
1009
  });
950
1010
 
951
- it('remains natively disabled when disabled without a reason', () => {
952
- render(<DateTimeInput label="When" onChange={() => {}} isDisabled />);
1011
+ it('blocks typing, opening and a caller onClick when disabled without a reason (AST-085 FR2)', async () => {
1012
+ const user = userEvent.setup();
1013
+ const onChange = vi.fn();
1014
+ const onClick = vi.fn();
1015
+ render(
1016
+ <DateTimeInput
1017
+ label="When"
1018
+ onChange={onChange}
1019
+ onClick={onClick}
1020
+ isDisabled
1021
+ />,
1022
+ );
1023
+ const dateInput = screen.getByRole('combobox');
1024
+ await user.click(dateInput);
1025
+ await user.type(dateInput, '2026-03-15');
1026
+ await user.keyboard('{ArrowDown}');
1027
+ expect(dateInput).toHaveValue('');
1028
+ expect(dateInput).toHaveAttribute('aria-expanded', 'false');
1029
+ expect(onChange).not.toHaveBeenCalled();
1030
+ expect(onClick).not.toHaveBeenCalled();
1031
+ });
1032
+
1033
+ it('drops a date draft instead of committing it when the field becomes disabled (AST-085 FR2)', async () => {
1034
+ const user = userEvent.setup();
1035
+ const onChange = vi.fn();
1036
+ const value = '2026-03-10T09:00' as ISODateTimeString;
1037
+ const {rerender} = render(
1038
+ <DateTimeInput label="When" value={value} onChange={onChange} />,
1039
+ );
953
1040
  const dateInput = screen.getByRole('combobox');
954
- expect(dateInput).toBeDisabled();
955
- expect(dateInput).not.toHaveAttribute('aria-disabled');
1041
+ await user.click(dateInput);
1042
+ await user.clear(dateInput);
1043
+ rerender(
1044
+ <DateTimeInput
1045
+ label="When"
1046
+ value={value}
1047
+ onChange={onChange}
1048
+ isDisabled
1049
+ />,
1050
+ );
1051
+ await user.tab();
1052
+ expect(onChange).not.toHaveBeenCalled();
956
1053
  });
957
1054
 
958
1055
  it('does not swap in the time format-hint placeholder on focus while disabled', () => {
@@ -1030,6 +1127,24 @@ describe('DateTimeInput', () => {
1030
1127
  });
1031
1128
  });
1032
1129
 
1130
+ it('announces no step from a disabled time field (AST-085 FR2)', async () => {
1131
+ render(
1132
+ <DateTimeInput
1133
+ label="Meeting"
1134
+ value={'2026-03-15T14:30' as ISODateTimeString}
1135
+ onChange={() => {}}
1136
+ isDisabled
1137
+ />,
1138
+ );
1139
+
1140
+ fireEvent.keyDown(screen.getByLabelText('Meeting time'), {
1141
+ key: 'ArrowUp',
1142
+ });
1143
+
1144
+ await new Promise(resolve => setTimeout(resolve, 50));
1145
+ expect(politeRegion()?.textContent ?? '').not.toContain('2:31 PM');
1146
+ });
1147
+
1033
1148
  it('politely announces the new time after ArrowDown stepping', async () => {
1034
1149
  render(
1035
1150
  <DateTimeInput