@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
@@ -49,14 +49,15 @@ export const docs = {
49
49
  {
50
50
  name: 'isDisabled',
51
51
  type: 'boolean',
52
- description: 'Disable the input and calendar.',
52
+ description:
53
+ 'Disable the input and calendar. The field stays focusable and exposes `aria-disabled`; typing and opening are blocked.',
53
54
  default: 'false',
54
55
  },
55
56
  {
56
57
  name: 'disabledMessage',
57
58
  type: 'string',
58
59
  description:
59
- '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 DateInput in Tooltip. Disabled controls swallow the hover events an external Tooltip needs.',
60
+ 'Explains why the input is disabled. With isDisabled, shows a tooltip on hover/keyboard focus (activation stays blocked). Use this instead of wrapping a disabled DateInput in Tooltip.',
60
61
  },
61
62
  {
62
63
  name: 'value',
@@ -275,7 +276,7 @@ export const docs = {
275
276
  {
276
277
  guidance: false,
277
278
  description:
278
- 'Wrap a disabled DateInput in Tooltip to explain why it is disabled; disabled triggers swallow the hover events the wrapper needs. Use the disabledMessage prop instead.',
279
+ 'Wrap a disabled DateInput in Tooltip to explain why it is disabled. Use the disabledMessage prop instead, which ties the reason to the field.',
279
280
  },
280
281
  ],
281
282
  anatomy: [
@@ -368,7 +369,7 @@ export const docsZh = {
368
369
  {
369
370
  guidance: false,
370
371
  description:
371
- 'Wrap a disabled DateInput in Tooltip to explain why it is disabled; disabled triggers swallow the hover events the wrapper needs. Use the disabledMessage prop instead.',
372
+ 'Wrap a disabled DateInput in Tooltip to explain why it is disabled. Use the disabledMessage prop instead, which ties the reason to the field.',
372
373
  },
373
374
  ],
374
375
  },
@@ -400,14 +401,14 @@ export const docsZh = {
400
401
  {
401
402
  name: 'isDisabled',
402
403
  type: 'boolean',
403
- description: '禁用输入框和日历。',
404
+ description: '禁用输入框和日历。输入框仍可聚焦并暴露 `aria-disabled`,但不能输入或打开日历。',
404
405
  default: 'false',
405
406
  },
406
407
  {
407
408
  name: 'disabledMessage',
408
409
  type: 'string',
409
410
  description:
410
- '说明输入框为何被禁用。与 isDisabled 一起使用时,在悬停/键盘聚焦时显示提示,并通过 aria-disabled 保持输入框可聚焦(仍阻止输入和激活)。请使用此属性,而不是用 Tooltip 包裹已禁用的 DateInput。',
411
+ '说明输入框为何被禁用。与 isDisabled 一起使用时,在悬停/键盘聚焦时显示提示(仍阻止输入和激活)。请使用此属性,而不是用 Tooltip 包裹已禁用的 DateInput。',
411
412
  },
412
413
  {
413
414
  name: 'value',
@@ -621,7 +622,7 @@ export const docsDense = {
621
622
  {
622
623
  guidance: false,
623
624
  description:
624
- 'Wrap a disabled DateInput in Tooltip to explain why it is disabled; disabled triggers swallow the hover events the wrapper needs. Use the disabledMessage prop instead.',
625
+ 'Wrap a disabled DateInput in Tooltip to explain why it is disabled. Use the disabledMessage prop instead, which ties the reason to the field.',
625
626
  },
626
627
  ],
627
628
  },
@@ -633,7 +634,7 @@ export const docsDense = {
633
634
  isRequired: 'mark field required',
634
635
  isDisabled: 'disable input+calendar',
635
636
  disabledMessage:
636
- 'reason shown in a tooltip on hover/focus when disabled; keeps field focusable via aria-disabled',
637
+ "reason shown in a tooltip on hover/focus when disabled; describes the field's focusable controls",
637
638
  value: 'selected date YYYY-MM-DD',
638
639
  onChange: 'callback on date change',
639
640
  changeAction: 'async action after onChange; drives optimistic UI',
@@ -84,9 +84,14 @@ describe('DateInput', () => {
84
84
  expect(screen.getByRole('combobox')).not.toHaveAttribute('aria-required');
85
85
  });
86
86
 
87
- it('sets disabled attribute when isDisabled is true', () => {
87
+ it('keeps a disabled input focusable without native disabled (AST-085 FR1)', () => {
88
88
  render(<DateInput label="Date" isDisabled onChange={() => {}} />);
89
- expect(screen.getByRole('combobox')).toBeDisabled();
89
+ const input = screen.getByRole('combobox');
90
+ expect(input).not.toHaveAttribute('disabled');
91
+ expect(input).toHaveAttribute('aria-disabled', 'true');
92
+ expect(input).toHaveAttribute('readonly');
93
+ input.focus();
94
+ expect(input).toHaveFocus();
90
95
  });
91
96
 
92
97
  it('is not disabled by default', () => {
@@ -115,10 +120,16 @@ describe('DateInput', () => {
115
120
  expect(button).not.toBeDisabled();
116
121
  });
117
122
 
118
- it('calendar button is disabled when isDisabled is true', () => {
123
+ it('calendar button is focusable-disabled when isDisabled is true', () => {
119
124
  render(<DateInput label="Date" isDisabled onChange={() => {}} />);
120
125
  const button = getButton('Open calendar');
121
- expect(button).toBeDisabled();
126
+ expect(button).not.toHaveAttribute('disabled');
127
+ expect(button).toHaveAttribute('aria-disabled', 'true');
128
+ fireEvent.click(button);
129
+ expect(screen.getByRole('combobox')).toHaveAttribute(
130
+ 'aria-expanded',
131
+ 'false',
132
+ );
122
133
  });
123
134
 
124
135
  it('does not call onChange while typing invalid input', async () => {
@@ -349,10 +360,15 @@ describe('DateInput', () => {
349
360
 
350
361
  // --- P1: Input disabled during isBusy (isLoading) ---
351
362
 
352
- it('disables input and button when isLoading is true', () => {
363
+ it('blocks input and button without native disabled when isLoading is true', () => {
353
364
  render(<DateInput label="Date" isLoading onChange={() => {}} />);
354
- expect(screen.getByRole('combobox')).toBeDisabled();
355
- expect(getButton('Open calendar')).toBeDisabled();
365
+ expect(screen.getByRole('combobox')).not.toHaveAttribute('disabled');
366
+ expect(screen.getByRole('combobox')).toHaveAttribute(
367
+ 'aria-disabled',
368
+ 'true',
369
+ );
370
+ expect(getButton('Open calendar')).not.toHaveAttribute('disabled');
371
+ expect(getButton('Open calendar')).toHaveAttribute('aria-disabled', 'true');
356
372
  });
357
373
 
358
374
  it('shows spinner when isLoading is true', () => {
@@ -526,7 +542,8 @@ describe('DateInput', () => {
526
542
  render(<DateInput label="Date" isDisabled onChange={() => {}} />);
527
543
 
528
544
  const input = screen.getByRole('combobox');
529
- expect(input).toBeDisabled();
545
+ expect(input).toHaveAttribute('aria-disabled', 'true');
546
+ expect(input).not.toHaveAttribute('disabled');
530
547
  });
531
548
 
532
549
  // --- P1: aria-busy on input ---
@@ -818,7 +835,10 @@ describe('DateInput', () => {
818
835
  expect(input).toHaveAttribute('aria-disabled', 'true');
819
836
  expect(input.getAttribute('aria-describedby')).toContain(tooltip.id);
820
837
  expect(tooltip).toHaveTextContent('Scheduling is locked');
821
- expect(getButton('Open calendar')).toBeDisabled();
838
+ expect(getButton('Open calendar')).toHaveAttribute(
839
+ 'aria-disabled',
840
+ 'true',
841
+ );
822
842
  });
823
843
  });
824
844
 
@@ -875,6 +895,10 @@ describe('DateInput', () => {
875
895
  );
876
896
 
877
897
  const tooltip = screen.getByRole('tooltip', h);
898
+ // The calendar button keeps its enabled tab stop (spec:AST-085 FR1),
899
+ // so the input is the second stop.
900
+ await user.tab();
901
+ expect(getButton('Open calendar')).toHaveFocus();
878
902
  await user.tab();
879
903
  expect(screen.getByRole('combobox')).toHaveFocus();
880
904
  await waitFor(() => {
@@ -908,6 +932,50 @@ describe('DateInput', () => {
908
932
  expect(input).toHaveAttribute('readonly');
909
933
  });
910
934
 
935
+ it('does not call a caller onClick while disabled (AST-085 FR2)', () => {
936
+ const onClick = vi.fn();
937
+ render(<DateInput label="Date" isDisabled onClick={onClick} />);
938
+ fireEvent.click(screen.getByRole('combobox'));
939
+ expect(onClick).not.toHaveBeenCalled();
940
+ });
941
+
942
+ it('links the reason tooltip from the calendar button too (AST-085 FR5)', () => {
943
+ render(
944
+ <DateInput
945
+ label="Date"
946
+ isDisabled
947
+ disabledMessage="You need the Editor role"
948
+ />,
949
+ );
950
+ const tooltip = screen.getByRole('tooltip', h);
951
+ expect(
952
+ getButton('Open calendar').getAttribute('aria-describedby'),
953
+ ).toContain(tooltip.id);
954
+ });
955
+
956
+ it('drops a draft instead of committing it when the field becomes disabled (AST-085 FR2)', async () => {
957
+ const user = userEvent.setup();
958
+ const onChange = vi.fn();
959
+ const {rerender} = render(
960
+ <DateInput label="Date" value="2026-03-10" onChange={onChange} />,
961
+ );
962
+ const input = screen.getByRole('combobox');
963
+ await user.click(input);
964
+ await user.clear(input);
965
+ rerender(
966
+ <DateInput
967
+ label="Date"
968
+ value="2026-03-10"
969
+ onChange={onChange}
970
+ isDisabled
971
+ />,
972
+ );
973
+ expect(input).toHaveFocus();
974
+ await user.keyboard('{Enter}');
975
+ await user.tab();
976
+ expect(onChange).not.toHaveBeenCalled();
977
+ });
978
+
911
979
  it('links the reason tooltip from the input via aria-describedby', () => {
912
980
  render(
913
981
  <DateInput
@@ -941,11 +1009,18 @@ describe('DateInput', () => {
941
1009
  expect(onChange).not.toHaveBeenCalled();
942
1010
  });
943
1011
 
944
- it('remains natively disabled when disabled without a reason', () => {
945
- render(<DateInput label="Date" isDisabled />);
1012
+ it('blocks value changes and opening when disabled without a reason (AST-085 FR1, FR2)', async () => {
1013
+ const user = userEvent.setup();
1014
+ const onChange = vi.fn();
1015
+ render(<DateInput label="Date" onChange={onChange} isDisabled />);
946
1016
  const input = screen.getByRole('combobox');
947
- expect(input).toBeDisabled();
948
- expect(input).not.toHaveAttribute('aria-disabled');
1017
+ expect(input).not.toHaveAttribute('disabled');
1018
+ await user.click(input);
1019
+ await user.type(input, '2026-03-15');
1020
+ await user.keyboard('{ArrowDown}');
1021
+ expect(input).toHaveValue('');
1022
+ expect(input).toHaveAttribute('aria-expanded', 'false');
1023
+ expect(onChange).not.toHaveBeenCalled();
949
1024
  });
950
1025
  });
951
1026
 
@@ -79,6 +79,18 @@ import {
79
79
  import {useDevWarning} from '../hooks/useDevWarning';
80
80
 
81
81
  const styles = stylex.create({
82
+ // The shared disabled style drops the focus ring; restore it while this
83
+ // field's own input holds focus (spec:AST-085 FR6). Defaults repeat it.
84
+ disabledFocusRing: {
85
+ borderColor: {
86
+ default: colorVars['--color-border-emphasized'],
87
+ ':has(input:focus)': colorVars['--color-accent'],
88
+ },
89
+ boxShadow: {
90
+ default: 'none',
91
+ ':has(input:focus)': `inset 0px 0px 0px 2px ${colorVars['--color-accent-muted']}`,
92
+ },
93
+ },
82
94
  iconButton: {
83
95
  display: 'flex',
84
96
  alignItems: 'center',
@@ -241,13 +253,13 @@ export interface DateInputProps extends Omit<
241
253
 
242
254
  /**
243
255
  * Explains why the input is disabled. When set together with
244
- * `isDisabled`, the input shows a tooltip with this text on hover and
245
- * keyboard focus, and the field stays focusable (via `aria-disabled`)
256
+ * `isDisabled`, the field shows a tooltip with this text on hover and
257
+ * keyboard focus, and the field's focusable controls are described by it,
246
258
  * so the reason is discoverable by keyboard and assistive technology.
247
259
  * Typing and calendar activation stay blocked.
248
260
  *
249
- * Use this instead of wrapping a disabled input in `Tooltip` — disabled
250
- * controls don't emit the pointer events an external tooltip needs.
261
+ * Use this instead of wrapping a disabled input in `Tooltip`, so the
262
+ * reason is tied to the field.
251
263
  *
252
264
  * @example
253
265
  * ```
@@ -537,11 +549,11 @@ function PointerDateField({
537
549
  const isBusy = isLoading || optimisticValue !== value;
538
550
  const isEffectivelyDisabled = isDisabled || isBusy;
539
551
 
540
- // Disabled-reason tooltip. Disabled controls swallow pointer events, so the
541
- // tooltip listeners attach to the input container (which already exists) and
542
- // the text input stays perceivable via aria-disabled instead of the disabled
543
- // attribute. Typing is blocked with readOnly and value mutation guards;
544
- // calendar activation is blocked by the isEffectivelyDisabled guards. Only
552
+ // A disabled or busy field never renders native `disabled` (spec:AST-085
553
+ // FR1): the input and calendar button stay focusable via aria-disabled.
554
+ // Typing is blocked with readOnly and value mutation guards; calendar
555
+ // activation is blocked by the isEffectivelyDisabled guards. The
556
+ // disabled-reason tooltip's listeners attach to the input container. Only
545
557
  // the persistent isDisabled state (not the transient busy state) surfaces a
546
558
  // reason.
547
559
  const showsDisabledMessage = isDisabled && !!disabledMessage;
@@ -703,8 +715,8 @@ function PointerDateField({
703
715
  // Handle input text change - update immediately if valid and allowed
704
716
  const handleInputChange = useCallback(
705
717
  (e: React.ChangeEvent<HTMLInputElement>) => {
706
- // With a disabledMessage the input drops `disabled` for focusability, so
707
- // guard value mutation explicitly (readOnly also blocks typing).
718
+ // The input is never natively disabled (spec:AST-085 FR1), so guard
719
+ // value mutation explicitly (readOnly also blocks typing).
708
720
  if (isEffectivelyDisabled) {
709
721
  return;
710
722
  }
@@ -733,6 +745,11 @@ function PointerDateField({
733
745
  if (pendingInput === null) {
734
746
  return;
735
747
  }
748
+ // Disabled or busy: keep the value, drop the draft (spec:AST-085 FR2).
749
+ if (isEffectivelyDisabled) {
750
+ setPendingInput(null);
751
+ return;
752
+ }
736
753
 
737
754
  if (!pendingInput.trim()) {
738
755
  if (value !== undefined) {
@@ -750,7 +767,14 @@ function PointerDateField({
750
767
  }
751
768
  }
752
769
  setPendingInput(null);
753
- }, [pendingInput, value, fireChange, isDateDisabled, locale]);
770
+ }, [
771
+ pendingInput,
772
+ isEffectivelyDisabled,
773
+ value,
774
+ fireChange,
775
+ isDateDisabled,
776
+ locale,
777
+ ]);
754
778
 
755
779
  // Handle blur - validate, check constraints, and clear pending input
756
780
  const handleBlur = useCallback(() => {
@@ -808,6 +832,7 @@ function PointerDateField({
808
832
  inputWrapperStyles.base,
809
833
  sizeStyles[size],
810
834
  isEffectivelyDisabled && inputWrapperStyles.disabled,
835
+ isEffectivelyDisabled && styles.disabledFocusRing,
811
836
  status && inputStatusBorderStyles[status.type],
812
837
  status &&
813
838
  !isEffectivelyDisabled &&
@@ -823,7 +848,11 @@ function PointerDateField({
823
848
  <button
824
849
  type="button"
825
850
  onClick={handleToggle}
826
- disabled={isEffectivelyDisabled}
851
+ aria-disabled={isEffectivelyDisabled || undefined}
852
+ // A tab stop while disabled too, so it carries the reason (FR5).
853
+ aria-describedby={
854
+ showsDisabledMessage ? disabledMessageTooltip.describedBy : undefined
855
+ }
827
856
  aria-label={
828
857
  popover.isOpen
829
858
  ? t('@astryx.dateInput.toggleCalendarClose')
@@ -858,16 +887,15 @@ function PointerDateField({
858
887
  value={displayValue}
859
888
  onChange={handleInputChange}
860
889
  onBlur={chainEventHandlers(handleBlur, onBlurProp)}
861
- onClick={chainEventHandlers(handleInputClick, onClickProp)}
890
+ onClick={
891
+ isEffectivelyDisabled
892
+ ? undefined
893
+ : chainEventHandlers(handleInputClick, onClickProp)
894
+ }
862
895
  onKeyDown={chainEventHandlers(handleInputKeyDown, onKeyDownProp)}
863
896
  placeholder={placeholder}
864
- // With a disabledMessage the input keeps focusability via
865
- // aria-disabled so the reason is focus-discoverable; typing is
866
- // blocked with readOnly and the mutation guards, and calendar
867
- // activation is blocked by the isEffectivelyDisabled guards.
868
- disabled={isEffectivelyDisabled && !showsDisabledMessage}
869
- aria-disabled={showsDisabledMessage ? 'true' : undefined}
870
- readOnly={showsDisabledMessage || undefined}
897
+ aria-disabled={isEffectivelyDisabled ? 'true' : undefined}
898
+ readOnly={isEffectivelyDisabled || undefined}
871
899
  aria-labelledby={
872
900
  ariaLabelledByProp != null
873
901
  ? joinAriaIDs(ariaLabelledBy ?? fieldLabelID, ariaLabelledByProp)
@@ -554,11 +554,32 @@ describe('DateInput — field parity', () => {
554
554
  onChange={() => {}}
555
555
  />,
556
556
  );
557
- expect(field()).toBeDisabled();
557
+ // Focusable, never natively disabled (AST-085 FR1), and still inert.
558
+ expect(field()).not.toHaveAttribute('disabled');
559
+ expect(field()).toHaveAttribute('aria-disabled', 'true');
560
+ field().focus();
561
+ expect(field()).toHaveFocus();
558
562
  fireEvent.click(field());
563
+ fireEvent.keyDown(field(), {key: 'Enter'});
564
+ expect(field()).toHaveAttribute('aria-expanded', 'false');
559
565
  expect(screen.queryByRole('grid')).not.toBeInTheDocument();
560
566
  });
561
567
 
568
+ it('does not call a caller onClick while disabled (AST-085 FR2)', () => {
569
+ const onClick = vi.fn();
570
+ render(
571
+ <DateInput
572
+ nativePicker="never"
573
+ label="Ship date"
574
+ isDisabled
575
+ onClick={onClick}
576
+ onChange={() => {}}
577
+ />,
578
+ );
579
+ fireEvent.click(field());
580
+ expect(onClick).not.toHaveBeenCalled();
581
+ });
582
+
562
583
  it('stays focusable and explains itself when disabled with a reason', () => {
563
584
  render(
564
585
  <DateInput
@@ -601,12 +601,38 @@ describe('DateInput nativePicker', () => {
601
601
  await act(async () => resolveAction());
602
602
  });
603
603
 
604
- it('disables the control and its toggle when isDisabled', () => {
604
+ it('keeps a disabled control focusable and its toggle inert (AST-085 FR1, FR2)', () => {
605
605
  stubPointer(true);
606
606
  render(<DateInput label="Date" isDisabled onChange={() => {}} />);
607
607
 
608
- expect(getInput()).toBeDisabled();
609
- expect(screen.getByLabelText('Open calendar')).toBeDisabled();
608
+ const input = getInput();
609
+ expect(input).not.toHaveAttribute('disabled');
610
+ expect(input).toHaveAttribute('aria-disabled', 'true');
611
+ expect(input).toHaveAttribute('readonly');
612
+ input.focus();
613
+ expect(input).toHaveFocus();
614
+ const toggle = screen.getByLabelText('Open calendar');
615
+ expect(toggle).not.toHaveAttribute('disabled');
616
+ expect(toggle).toHaveAttribute('aria-disabled', 'true');
617
+ const showPicker = vi.fn();
618
+ Object.defineProperty(input, 'showPicker', {value: showPicker});
619
+ fireEvent.click(toggle);
620
+ expect(showPicker).not.toHaveBeenCalled();
621
+ });
622
+
623
+ it('does not call a caller onClick while disabled (AST-085 FR2)', () => {
624
+ stubPointer(true);
625
+ const onClick = vi.fn();
626
+ render(
627
+ <DateInput
628
+ label="Date"
629
+ isDisabled
630
+ onClick={onClick}
631
+ onChange={() => {}}
632
+ />,
633
+ );
634
+ fireEvent.click(getInput());
635
+ expect(onClick).not.toHaveBeenCalled();
610
636
  });
611
637
 
612
638
  it('ignores a change while disabled', () => {
@@ -82,6 +82,17 @@ import {
82
82
  import {chainEventHandlers} from '../utils/chainEventHandlers';
83
83
 
84
84
  const styles = stylex.create({
85
+ // Same as DateInput's: the focus ring for a focused disabled field.
86
+ disabledFocusRing: {
87
+ borderColor: {
88
+ default: colorVars['--color-border-emphasized'],
89
+ ':has(input:focus)': colorVars['--color-accent'],
90
+ },
91
+ boxShadow: {
92
+ default: 'none',
93
+ ':has(input:focus)': `inset 0px 0px 0px 2px ${colorVars['--color-accent-muted']}`,
94
+ },
95
+ },
85
96
  wrapper: {
86
97
  gap: spacingVars['--spacing-2'],
87
98
  },
@@ -323,9 +334,10 @@ export function NativeDateField({
323
334
  // platform picker from its input.
324
335
  const isEffectivelyDisabled = isDisabled || isLoading;
325
336
 
326
- // Disabled-reason tooltip, same contract as the other two surfaces: a
327
- // disabled control swallows pointer events, so the listeners attach to the
328
- // wrapper and the input stays focusable via aria-disabled.
337
+ // Same contract as the other two surfaces: a disabled field never renders
338
+ // native `disabled` (spec:AST-085 FR1); the input stays focusable via
339
+ // aria-disabled and readOnly. The disabled-reason tooltip's listeners
340
+ // attach to the wrapper.
329
341
  const showsDisabledMessage = isDisabled && !!disabledMessage;
330
342
  const disabledMessageTooltip = useTooltip({
331
343
  placement: 'above',
@@ -582,6 +594,7 @@ export function NativeDateField({
582
594
  sizeStyles[size],
583
595
  styles.wrapper,
584
596
  isEffectivelyDisabled && inputWrapperStyles.disabled,
597
+ isEffectivelyDisabled && styles.disabledFocusRing,
585
598
  status && inputStatusBorderStyles[status.type],
586
599
  status &&
587
600
  !isEffectivelyDisabled &&
@@ -597,7 +610,7 @@ export function NativeDateField({
597
610
  <button
598
611
  type="button"
599
612
  onClick={openPicker}
600
- disabled={isEffectivelyDisabled}
613
+ aria-disabled={isEffectivelyDisabled || undefined}
601
614
  aria-label={t('@astryx.dateInput.openCalendar')}
602
615
  tabIndex={-1}
603
616
  {...stylex.props(
@@ -615,6 +628,7 @@ export function NativeDateField({
615
628
  <span {...stylex.props(styles.slot)}>
616
629
  <input
617
630
  {...controlProps}
631
+ onClick={isEffectivelyDisabled ? undefined : controlProps.onClick}
618
632
  ref={mergedInputRef}
619
633
  id={id}
620
634
  type="date"
@@ -626,9 +640,8 @@ export function NativeDateField({
626
640
  onBlur={chainEventHandlers(handleBlur, onBlurProp)}
627
641
  min={min}
628
642
  max={max}
629
- disabled={isEffectivelyDisabled && !showsDisabledMessage}
630
- aria-disabled={showsDisabledMessage ? 'true' : undefined}
631
- readOnly={showsDisabledMessage || undefined}
643
+ aria-disabled={isEffectivelyDisabled ? 'true' : undefined}
644
+ readOnly={isEffectivelyDisabled || undefined}
632
645
  aria-labelledby={
633
646
  ariaLabelledByProp != null
634
647
  ? joinAriaIDs(ariaLabelledBy ?? fieldLabelID, ariaLabelledByProp)
@@ -166,6 +166,17 @@ const sizeStyles = stylex.create({
166
166
  });
167
167
 
168
168
  const styles = stylex.create({
169
+ // Same as DateInput's: the focus ring for a focused disabled field.
170
+ disabledFocusRing: {
171
+ borderColor: {
172
+ default: colorVars['--color-border-emphasized'],
173
+ ':has(input:focus)': colorVars['--color-accent'],
174
+ },
175
+ boxShadow: {
176
+ default: 'none',
177
+ ':has(input:focus)': `inset 0px 0px 0px 2px ${colorVars['--color-accent-muted']}`,
178
+ },
179
+ },
169
180
  // ---- the closed field ----
170
181
  wrapper: {
171
182
  gap: spacingVars['--spacing-2'],
@@ -620,11 +631,11 @@ export function TouchDateField({
620
631
  const isBusy = isLoading || optimisticValue !== value;
621
632
  const isEffectivelyDisabled = isDisabled || isBusy;
622
633
 
623
- // Disabled-reason tooltip, same contract as DateInput: a disabled control
624
- // swallows pointer events, so the listeners attach to the wrapper and the
625
- // input stays focusable via aria-disabled rather than the disabled
626
- // attribute. Only the persistent disabled state surfaces a reason, never
627
- // the transient busy one.
634
+ // Same contract as DateInput: a disabled or busy field never renders native
635
+ // `disabled` (spec:AST-085 FR1); the input stays focusable via
636
+ // aria-disabled. The disabled-reason tooltip's listeners attach to the
637
+ // wrapper. Only the persistent disabled state surfaces a reason, never the
638
+ // transient busy one.
628
639
  const showsDisabledMessage = isDisabled && !!disabledMessage;
629
640
  const disabledMessageTooltip = useTooltip({
630
641
  placement: 'above',
@@ -1213,6 +1224,7 @@ export function TouchDateField({
1213
1224
  sizeStyles[size],
1214
1225
  styles.wrapper,
1215
1226
  isEffectivelyDisabled && inputWrapperStyles.disabled,
1227
+ isEffectivelyDisabled && styles.disabledFocusRing,
1216
1228
  status && inputStatusBorderStyles[status.type],
1217
1229
  status &&
1218
1230
  !isEffectivelyDisabled &&
@@ -1228,7 +1240,7 @@ export function TouchDateField({
1228
1240
  <button
1229
1241
  type="button"
1230
1242
  onClick={openSheet}
1231
- disabled={isEffectivelyDisabled}
1243
+ aria-disabled={isEffectivelyDisabled || undefined}
1232
1244
  aria-label={t('@astryx.dateInput.openCalendar')}
1233
1245
  tabIndex={-1}
1234
1246
  {...stylex.props(
@@ -1259,11 +1271,14 @@ export function TouchDateField({
1259
1271
  readOnly
1260
1272
  inputMode="none"
1261
1273
  onChange={() => {}}
1262
- onClick={chainEventHandlers(openSheet, onClickProp)}
1274
+ onClick={
1275
+ isEffectivelyDisabled
1276
+ ? undefined
1277
+ : chainEventHandlers(openSheet, onClickProp)
1278
+ }
1263
1279
  onKeyDown={chainEventHandlers(handleInputKeyDown, onKeyDownProp)}
1264
1280
  placeholder={placeholder}
1265
- disabled={isEffectivelyDisabled && !showsDisabledMessage}
1266
- aria-disabled={showsDisabledMessage ? 'true' : undefined}
1281
+ aria-disabled={isEffectivelyDisabled ? 'true' : undefined}
1267
1282
  aria-labelledby={
1268
1283
  ariaLabelledByProp != null
1269
1284
  ? joinAriaIDs(ariaLabelledBy ?? fieldLabelID, ariaLabelledByProp)
@@ -45,14 +45,15 @@ export const docs = {
45
45
  {
46
46
  name: 'isDisabled',
47
47
  type: 'boolean',
48
- description: 'Disable the trigger and picker.',
48
+ description:
49
+ 'Disable the trigger and picker. The trigger stays focusable and exposes `aria-disabled`; opening is blocked.',
49
50
  default: 'false',
50
51
  },
51
52
  {
52
53
  name: 'disabledMessage',
53
54
  type: 'string',
54
55
  description:
55
- '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 DateRangeInput in Tooltip. Disabled controls swallow the hover events an external Tooltip needs.',
56
+ 'Explains why the input is disabled. With isDisabled, shows a tooltip on hover/keyboard focus and describes the trigger (activation stays blocked). Use this instead of wrapping a disabled DateRangeInput in Tooltip.',
56
57
  },
57
58
  {
58
59
  name: 'value',
@@ -240,7 +241,7 @@ export const docs = {
240
241
  {
241
242
  guidance: false,
242
243
  description:
243
- 'Wrap a disabled DateRangeInput in Tooltip to explain why it is disabled; disabled triggers swallow the hover events the wrapper needs. Use the disabledMessage prop instead.',
244
+ 'Wrap a disabled DateRangeInput in Tooltip to explain why it is disabled. Use the disabledMessage prop instead, which ties the reason to the field.',
244
245
  },
245
246
  ],
246
247
  anatomy: [
@@ -344,7 +345,7 @@ export const docsDense = {
344
345
  {
345
346
  guidance: false,
346
347
  description:
347
- 'Wrap a disabled DateRangeInput in Tooltip to explain why it is disabled; disabled triggers swallow the hover events the wrapper needs. Use the disabledMessage prop instead.',
348
+ 'Wrap a disabled DateRangeInput in Tooltip to explain why it is disabled. Use the disabledMessage prop instead, which ties the reason to the field.',
348
349
  },
349
350
  ],
350
351
  },
@@ -356,7 +357,7 @@ export const docsDense = {
356
357
  isRequired: 'mark field required',
357
358
  isDisabled: 'disable trigger+picker',
358
359
  disabledMessage:
359
- 'reason shown in a tooltip on hover/focus when disabled; keeps trigger focusable via aria-disabled',
360
+ 'reason shown in a tooltip on hover/focus when disabled; describes the trigger',
360
361
  value:
361
362
  'selected range {start, end} or null; import DateRange type from @astryxdesign/core/DateRangeInput (do not redeclare)',
362
363
  onChange: 'callback on range change; null on clear',