@astryxdesign/core 0.1.2 → 0.1.3-canary.6d57c8d

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 (567) hide show
  1. package/CHANGELOG.md +165 -0
  2. package/README.md +12 -12
  3. package/dist/Avatar/Avatar.d.ts.map +1 -1
  4. package/dist/Avatar/Avatar.js +11 -5
  5. package/dist/Breadcrumbs/BreadcrumbItem.d.ts +1 -1
  6. package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
  7. package/dist/Breadcrumbs/BreadcrumbItem.js +24 -5
  8. package/dist/Button/Button.d.ts.map +1 -1
  9. package/dist/Button/Button.js +2 -4
  10. package/dist/ButtonGroup/ButtonGroup.d.ts.map +1 -1
  11. package/dist/ButtonGroup/ButtonGroup.js +0 -1
  12. package/dist/Calendar/Calendar.d.ts +5 -4
  13. package/dist/Calendar/Calendar.d.ts.map +1 -1
  14. package/dist/Calendar/Calendar.js +82 -43
  15. package/dist/Calendar/hooks/index.d.ts +0 -2
  16. package/dist/Calendar/hooks/index.d.ts.map +1 -1
  17. package/dist/Calendar/hooks/index.js +1 -2
  18. package/dist/Calendar/index.d.ts +3 -3
  19. package/dist/Calendar/index.d.ts.map +1 -1
  20. package/dist/Calendar/index.js +1 -1
  21. package/dist/Calendar/styles.d.ts +3 -9
  22. package/dist/Calendar/styles.d.ts.map +1 -1
  23. package/dist/Calendar/styles.js +3 -11
  24. package/dist/Carousel/Carousel.d.ts.map +1 -1
  25. package/dist/Carousel/Carousel.js +17 -3
  26. package/dist/Center/Center.d.ts +11 -1
  27. package/dist/Center/Center.d.ts.map +1 -1
  28. package/dist/Center/Center.js +9 -3
  29. package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
  30. package/dist/Chat/ChatComposerInput.js +0 -1
  31. package/dist/Chat/ChatMessageList.d.ts +14 -1
  32. package/dist/Chat/ChatMessageList.d.ts.map +1 -1
  33. package/dist/Chat/ChatMessageList.js +2 -0
  34. package/dist/Chat/useTriggerMenu.d.ts +8 -1
  35. package/dist/Chat/useTriggerMenu.d.ts.map +1 -1
  36. package/dist/Chat/useTriggerMenu.js +15 -3
  37. package/dist/CheckboxInput/CheckboxInput.d.ts +23 -2
  38. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  39. package/dist/CheckboxInput/CheckboxInput.js +51 -9
  40. package/dist/CheckboxList/CheckboxList.d.ts +13 -1
  41. package/dist/CheckboxList/CheckboxList.d.ts.map +1 -1
  42. package/dist/CheckboxList/CheckboxList.js +39 -9
  43. package/dist/CheckboxList/CheckboxListContext.d.ts +7 -0
  44. package/dist/CheckboxList/CheckboxListContext.d.ts.map +1 -1
  45. package/dist/CheckboxList/CheckboxListItem.d.ts.map +1 -1
  46. package/dist/CheckboxList/CheckboxListItem.js +0 -1
  47. package/dist/Citation/Citation.d.ts.map +1 -1
  48. package/dist/Citation/Citation.js +9 -5
  49. package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
  50. package/dist/CodeBlock/CodeBlock.js +51 -37
  51. package/dist/CommandPalette/CommandPalette.d.ts.map +1 -1
  52. package/dist/CommandPalette/CommandPalette.js +5 -5
  53. package/dist/ContextMenu/ContextMenu.d.ts +4 -5
  54. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  55. package/dist/ContextMenu/ContextMenu.js +93 -11
  56. package/dist/DateInput/DateInput.d.ts +23 -1
  57. package/dist/DateInput/DateInput.d.ts.map +1 -1
  58. package/dist/DateInput/DateInput.js +167 -99
  59. package/dist/DateRangeInput/DateRangeInput.d.ts +23 -1
  60. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  61. package/dist/DateRangeInput/DateRangeInput.js +42 -9
  62. package/dist/DateTimeInput/DateTimeInput.d.ts +35 -2
  63. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  64. package/dist/DateTimeInput/DateTimeInput.js +78 -15
  65. package/dist/Dialog/Dialog.d.ts.map +1 -1
  66. package/dist/Dialog/Dialog.js +12 -0
  67. package/dist/DropdownMenu/DropdownMenu.d.ts +1 -8
  68. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  69. package/dist/DropdownMenu/DropdownMenu.js +42 -14
  70. package/dist/Field/Field.d.ts +19 -2
  71. package/dist/Field/Field.d.ts.map +1 -1
  72. package/dist/Field/Field.js +4 -0
  73. package/dist/Field/FieldLabel.d.ts +21 -2
  74. package/dist/Field/FieldLabel.d.ts.map +1 -1
  75. package/dist/Field/FieldLabel.js +41 -26
  76. package/dist/FileInput/FileInput.d.ts +22 -1
  77. package/dist/FileInput/FileInput.d.ts.map +1 -1
  78. package/dist/FileInput/FileInput.js +56 -10
  79. package/dist/Grid/Grid.d.ts +11 -1
  80. package/dist/Grid/Grid.d.ts.map +1 -1
  81. package/dist/Grid/Grid.js +32 -6
  82. package/dist/InputGroup/InputGroup.d.ts +4 -3
  83. package/dist/InputGroup/InputGroup.d.ts.map +1 -1
  84. package/dist/InputGroup/InputGroup.js +16 -6
  85. package/dist/InputGroup/InputGroupContext.d.ts +4 -0
  86. package/dist/InputGroup/InputGroupContext.d.ts.map +1 -1
  87. package/dist/InputGroup/InputGroupContext.js +2 -2
  88. package/dist/InputGroup/groupStyles.d.ts +1 -1
  89. package/dist/InputGroup/groupStyles.d.ts.map +1 -1
  90. package/dist/InputGroup/groupStyles.js +3 -3
  91. package/dist/InputGroup/index.d.ts +1 -0
  92. package/dist/InputGroup/index.d.ts.map +1 -1
  93. package/dist/Kbd/Kbd.d.ts.map +1 -1
  94. package/dist/Kbd/Kbd.js +33 -1
  95. package/dist/Layer/layerAnimations.stylex.d.ts +4 -0
  96. package/dist/Layer/layerAnimations.stylex.d.ts.map +1 -1
  97. package/dist/Layer/layerAnimations.stylex.js +8 -4
  98. package/dist/Layer/useLayer.d.ts +10 -0
  99. package/dist/Layer/useLayer.d.ts.map +1 -1
  100. package/dist/Layer/useLayer.js +64 -9
  101. package/dist/Layout/Layout.d.ts +4 -11
  102. package/dist/Layout/Layout.d.ts.map +1 -1
  103. package/dist/Layout/Layout.js +10 -11
  104. package/dist/Layout/LayoutFooter.d.ts +2 -2
  105. package/dist/Layout/LayoutFooter.d.ts.map +1 -1
  106. package/dist/Layout/LayoutHeader.d.ts +2 -2
  107. package/dist/Layout/LayoutHeader.d.ts.map +1 -1
  108. package/dist/Layout/LayoutPanel.d.ts +2 -2
  109. package/dist/Layout/LayoutPanel.d.ts.map +1 -1
  110. package/dist/Layout/padding.stylex.d.ts +51 -0
  111. package/dist/Layout/padding.stylex.d.ts.map +1 -1
  112. package/dist/Layout/padding.stylex.js +63 -0
  113. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  114. package/dist/Lightbox/Lightbox.js +4 -2
  115. package/dist/Link/Link.d.ts +16 -1
  116. package/dist/Link/Link.d.ts.map +1 -1
  117. package/dist/Link/Link.js +18 -7
  118. package/dist/Markdown/Markdown.d.ts.map +1 -1
  119. package/dist/Markdown/Markdown.js +7 -0
  120. package/dist/MoreMenu/MoreMenu.d.ts +1 -7
  121. package/dist/MoreMenu/MoreMenu.d.ts.map +1 -1
  122. package/dist/MoreMenu/MoreMenu.js +0 -2
  123. package/dist/MultiSelector/MultiSelector.d.ts +27 -2
  124. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  125. package/dist/MultiSelector/MultiSelector.js +141 -42
  126. package/dist/MultiSelector/hooks.d.ts +12 -1
  127. package/dist/MultiSelector/hooks.d.ts.map +1 -1
  128. package/dist/MultiSelector/hooks.js +14 -1
  129. package/dist/NavMenu/NavHeadingMenu.d.ts.map +1 -1
  130. package/dist/NavMenu/NavHeadingMenu.js +34 -3
  131. package/dist/NumberInput/NumberInput.d.ts +23 -2
  132. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  133. package/dist/NumberInput/NumberInput.js +62 -12
  134. package/dist/Pagination/Pagination.d.ts +1 -1
  135. package/dist/Pagination/Pagination.d.ts.map +1 -1
  136. package/dist/Pagination/Pagination.js +15 -1
  137. package/dist/Popover/Popover.d.ts +18 -2
  138. package/dist/Popover/Popover.d.ts.map +1 -1
  139. package/dist/Popover/Popover.js +5 -2
  140. package/dist/Popover/usePopover.d.ts +38 -3
  141. package/dist/Popover/usePopover.d.ts.map +1 -1
  142. package/dist/Popover/usePopover.js +24 -9
  143. package/dist/PowerSearch/PowerSearch.d.ts +11 -1
  144. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  145. package/dist/PowerSearch/PowerSearch.js +6 -1
  146. package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
  147. package/dist/ProgressBar/ProgressBar.js +3 -5
  148. package/dist/RadioList/RadioList.d.ts +20 -2
  149. package/dist/RadioList/RadioList.d.ts.map +1 -1
  150. package/dist/RadioList/RadioList.js +105 -9
  151. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  152. package/dist/RadioList/RadioListItem.js +13 -2
  153. package/dist/SegmentedControl/SegmentedControl.d.ts +13 -2
  154. package/dist/SegmentedControl/SegmentedControl.d.ts.map +1 -1
  155. package/dist/SegmentedControl/SegmentedControl.js +78 -44
  156. package/dist/SegmentedControl/SegmentedControlContext.d.ts +7 -0
  157. package/dist/SegmentedControl/SegmentedControlContext.d.ts.map +1 -1
  158. package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
  159. package/dist/SegmentedControl/SegmentedControlItem.js +13 -2
  160. package/dist/Selector/Selector.d.ts +24 -1
  161. package/dist/Selector/Selector.d.ts.map +1 -1
  162. package/dist/Selector/Selector.js +122 -43
  163. package/dist/Selector/hooks.d.ts +8 -1
  164. package/dist/Selector/hooks.d.ts.map +1 -1
  165. package/dist/Selector/hooks.js +14 -1
  166. package/dist/SideNav/SideNavHeading.d.ts.map +1 -1
  167. package/dist/SideNav/SideNavHeading.js +2 -0
  168. package/dist/SideNav/SideNavItem.d.ts.map +1 -1
  169. package/dist/SideNav/SideNavItem.js +1 -1
  170. package/dist/Skeleton/Skeleton.d.ts.map +1 -1
  171. package/dist/Skeleton/Skeleton.js +8 -2
  172. package/dist/Slider/Slider.d.ts +21 -0
  173. package/dist/Slider/Slider.d.ts.map +1 -1
  174. package/dist/Slider/Slider.js +36 -9
  175. package/dist/Spinner/Spinner.d.ts.map +1 -1
  176. package/dist/Spinner/Spinner.js +1 -1
  177. package/dist/Stack/Stack.d.ts +38 -23
  178. package/dist/Stack/Stack.d.ts.map +1 -1
  179. package/dist/Stack/Stack.js +25 -1
  180. package/dist/Stack/StackItem.d.ts +12 -23
  181. package/dist/Stack/StackItem.d.ts.map +1 -1
  182. package/dist/Stack/StackItem.js +8 -1
  183. package/dist/Switch/Switch.d.ts +23 -2
  184. package/dist/Switch/Switch.d.ts.map +1 -1
  185. package/dist/Switch/Switch.js +41 -11
  186. package/dist/TabList/Tab.d.ts.map +1 -1
  187. package/dist/TabList/Tab.js +6 -0
  188. package/dist/TabList/TabList.d.ts +16 -4
  189. package/dist/TabList/TabList.d.ts.map +1 -1
  190. package/dist/TabList/TabList.js +87 -10
  191. package/dist/TabList/TabListContext.d.ts +5 -0
  192. package/dist/TabList/TabListContext.d.ts.map +1 -1
  193. package/dist/TabList/TabListContext.js +5 -0
  194. package/dist/TabList/TabMenu.d.ts.map +1 -1
  195. package/dist/TabList/TabMenu.js +6 -1
  196. package/dist/Table/BaseTable.d.ts.map +1 -1
  197. package/dist/Table/BaseTable.js +11 -8
  198. package/dist/Table/Table.d.ts.map +1 -1
  199. package/dist/Table/Table.js +10 -1
  200. package/dist/Table/TableCell.d.ts +8 -1
  201. package/dist/Table/TableCell.d.ts.map +1 -1
  202. package/dist/Table/TableCell.js +10 -10
  203. package/dist/Table/TableHeaderCell.d.ts +8 -1
  204. package/dist/Table/TableHeaderCell.d.ts.map +1 -1
  205. package/dist/Table/TableHeaderCell.js +16 -16
  206. package/dist/Table/index.d.ts +2 -1
  207. package/dist/Table/index.d.ts.map +1 -1
  208. package/dist/Table/index.js +2 -2
  209. package/dist/Table/plugins/pagination/paginateData.d.ts +2 -1
  210. package/dist/Table/plugins/pagination/paginateData.d.ts.map +1 -1
  211. package/dist/Table/plugins/pagination/paginateData.js +8 -3
  212. package/dist/Table/plugins/pagination/useTablePagination.d.ts.map +1 -1
  213. package/dist/Table/plugins/pagination/useTablePagination.js +6 -1
  214. package/dist/Table/plugins/sortable/useTableSortable.d.ts.map +1 -1
  215. package/dist/Table/plugins/sortable/useTableSortable.js +59 -3
  216. package/dist/Table/tableContextMenu.d.ts +33 -0
  217. package/dist/Table/tableContextMenu.d.ts.map +1 -0
  218. package/dist/Table/tableContextMenu.js +133 -0
  219. package/dist/Table/types.d.ts +66 -0
  220. package/dist/Table/types.d.ts.map +1 -1
  221. package/dist/TextArea/TextArea.d.ts +22 -1
  222. package/dist/TextArea/TextArea.d.ts.map +1 -1
  223. package/dist/TextArea/TextArea.js +41 -9
  224. package/dist/TextInput/TextInput.d.ts +23 -2
  225. package/dist/TextInput/TextInput.d.ts.map +1 -1
  226. package/dist/TextInput/TextInput.js +55 -10
  227. package/dist/TimeInput/TimeInput.d.ts +23 -1
  228. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  229. package/dist/TimeInput/TimeInput.js +147 -76
  230. package/dist/Toast/Toast.d.ts.map +1 -1
  231. package/dist/Toast/Toast.js +14 -0
  232. package/dist/Toast/ToastViewport.d.ts.map +1 -1
  233. package/dist/Toast/ToastViewport.js +131 -3
  234. package/dist/Tokenizer/Tokenizer.d.ts +12 -1
  235. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  236. package/dist/Tokenizer/Tokenizer.js +26 -4
  237. package/dist/Toolbar/Toolbar.d.ts +3 -3
  238. package/dist/Toolbar/Toolbar.d.ts.map +1 -1
  239. package/dist/Toolbar/Toolbar.js +51 -7
  240. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  241. package/dist/Tooltip/useTooltip.js +56 -9
  242. package/dist/TopNav/TopNavHeading.d.ts +7 -1
  243. package/dist/TopNav/TopNavHeading.d.ts.map +1 -1
  244. package/dist/TopNav/TopNavHeading.js +8 -0
  245. package/dist/TreeList/TreeList.d.ts +1 -1
  246. package/dist/TreeList/TreeList.d.ts.map +1 -1
  247. package/dist/TreeList/TreeList.js +72 -2
  248. package/dist/TreeList/TreeListItem.d.ts +11 -1
  249. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  250. package/dist/TreeList/TreeListItem.js +51 -12
  251. package/dist/TreeList/treeListItem.markers.stylex.d.ts +16 -0
  252. package/dist/TreeList/treeListItem.markers.stylex.d.ts.map +1 -0
  253. package/dist/TreeList/treeListItem.markers.stylex.js +22 -0
  254. package/dist/Typeahead/BaseTypeahead.d.ts +13 -0
  255. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  256. package/dist/Typeahead/BaseTypeahead.js +69 -6
  257. package/dist/Typeahead/Typeahead.d.ts +26 -3
  258. package/dist/Typeahead/Typeahead.d.ts.map +1 -1
  259. package/dist/Typeahead/Typeahead.js +65 -29
  260. package/dist/VisuallyHidden/VisuallyHidden.d.ts +61 -0
  261. package/dist/VisuallyHidden/VisuallyHidden.d.ts.map +1 -0
  262. package/dist/VisuallyHidden/VisuallyHidden.js +65 -0
  263. package/dist/VisuallyHidden/index.d.ts +11 -0
  264. package/dist/VisuallyHidden/index.d.ts.map +1 -0
  265. package/dist/VisuallyHidden/index.js +13 -0
  266. package/dist/astryx.css +18 -0
  267. package/dist/astryx.umd.js +55 -55
  268. package/dist/astryx.umd.js.map +4 -4
  269. package/dist/hooks/focusableSelector.d.ts +22 -0
  270. package/dist/hooks/focusableSelector.d.ts.map +1 -0
  271. package/dist/hooks/focusableSelector.js +24 -0
  272. package/dist/hooks/index.d.ts +12 -2
  273. package/dist/hooks/index.d.ts.map +1 -1
  274. package/dist/hooks/index.js +7 -2
  275. package/dist/hooks/useAnnounce.d.ts +40 -0
  276. package/dist/hooks/useAnnounce.d.ts.map +1 -0
  277. package/dist/hooks/useAnnounce.js +142 -0
  278. package/dist/hooks/useClickableContainer.d.ts +16 -1
  279. package/dist/hooks/useClickableContainer.d.ts.map +1 -1
  280. package/dist/hooks/useClickableContainer.js +16 -4
  281. package/dist/hooks/useEntryAnimation.d.ts.map +1 -1
  282. package/dist/hooks/useEntryAnimation.js +4 -4
  283. package/dist/hooks/useFocusTrap.d.ts +18 -0
  284. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  285. package/dist/hooks/useFocusTrap.js +87 -3
  286. package/dist/hooks/useGridFocus.d.ts +88 -1
  287. package/dist/hooks/useGridFocus.d.ts.map +1 -1
  288. package/dist/hooks/useGridFocus.js +261 -62
  289. package/dist/hooks/useKeyboardHint.d.ts +82 -0
  290. package/dist/hooks/useKeyboardHint.d.ts.map +1 -0
  291. package/dist/hooks/useKeyboardHint.js +220 -0
  292. package/dist/hooks/useListFocus.d.ts +78 -7
  293. package/dist/hooks/useListFocus.d.ts.map +1 -1
  294. package/dist/hooks/useListFocus.js +305 -52
  295. package/dist/hooks/useLongPress.d.ts +21 -0
  296. package/dist/hooks/useLongPress.d.ts.map +1 -0
  297. package/dist/hooks/useLongPress.js +89 -0
  298. package/dist/hooks/useTreeFocus.d.ts +157 -0
  299. package/dist/hooks/useTreeFocus.d.ts.map +1 -0
  300. package/dist/hooks/useTreeFocus.js +337 -0
  301. package/dist/hooks/useTypeahead.d.ts +63 -0
  302. package/dist/hooks/useTypeahead.d.ts.map +1 -0
  303. package/dist/hooks/useTypeahead.js +117 -0
  304. package/dist/index.d.ts +1 -0
  305. package/dist/index.d.ts.map +1 -1
  306. package/dist/index.js +1 -0
  307. package/dist/utils/dateTypes.d.ts +12 -0
  308. package/dist/utils/dateTypes.d.ts.map +1 -1
  309. package/dist/utils/dateTypes.js +41 -1
  310. package/dist/utils/index.d.ts +4 -0
  311. package/dist/utils/index.d.ts.map +1 -1
  312. package/dist/utils/index.js +3 -1
  313. package/dist/utils/inputAria.d.ts +29 -0
  314. package/dist/utils/inputAria.d.ts.map +1 -0
  315. package/dist/utils/inputAria.js +30 -0
  316. package/dist/utils/timeParser.d.ts.map +1 -1
  317. package/dist/utils/timeParser.js +5 -2
  318. package/docs.mjs +2 -2
  319. package/package.json +12 -8
  320. package/src/Avatar/Avatar.test.tsx +37 -0
  321. package/src/Avatar/Avatar.tsx +10 -5
  322. package/src/Breadcrumbs/BreadcrumbItem.tsx +25 -7
  323. package/src/Breadcrumbs/Breadcrumbs.test.tsx +31 -10
  324. package/src/Button/Button.tsx +3 -16
  325. package/src/ButtonGroup/ButtonGroup.test.tsx +9 -9
  326. package/src/ButtonGroup/ButtonGroup.tsx +0 -1
  327. package/src/Calendar/Calendar.doc.mjs +4 -4
  328. package/src/Calendar/Calendar.test.tsx +168 -30
  329. package/src/Calendar/Calendar.tsx +121 -66
  330. package/src/Calendar/hooks/index.ts +0 -6
  331. package/src/Calendar/index.ts +1 -3
  332. package/src/Calendar/styles.ts +4 -9
  333. package/src/Carousel/Carousel.doc.mjs +7 -1
  334. package/src/Carousel/Carousel.test.tsx +20 -0
  335. package/src/Carousel/Carousel.tsx +16 -1
  336. package/src/Center/Center.doc.mjs +12 -2
  337. package/src/Center/Center.tsx +28 -3
  338. package/src/Chat/ChatComposerInput.test.tsx +26 -7
  339. package/src/Chat/ChatComposerInput.tsx +0 -1
  340. package/src/Chat/ChatDictationButton.doc.mjs +1 -1
  341. package/src/Chat/ChatMessageList.doc.mjs +8 -0
  342. package/src/Chat/ChatMessageList.test.tsx +18 -0
  343. package/src/Chat/ChatMessageList.tsx +16 -0
  344. package/src/Chat/useTriggerMenu.tsx +22 -4
  345. package/src/CheckboxInput/CheckboxInput.doc.mjs +10 -0
  346. package/src/CheckboxInput/CheckboxInput.test.tsx +172 -27
  347. package/src/CheckboxInput/CheckboxInput.tsx +78 -6
  348. package/src/CheckboxList/CheckboxList.doc.mjs +9 -0
  349. package/src/CheckboxList/CheckboxList.test.tsx +192 -26
  350. package/src/CheckboxList/CheckboxList.tsx +56 -3
  351. package/src/CheckboxList/CheckboxListContext.tsx +7 -0
  352. package/src/CheckboxList/CheckboxListItem.tsx +0 -3
  353. package/src/Citation/Citation.doc.mjs +10 -0
  354. package/src/Citation/Citation.test.tsx +121 -0
  355. package/src/Citation/Citation.tsx +15 -3
  356. package/src/CodeBlock/CodeBlock.test.tsx +115 -0
  357. package/src/CodeBlock/CodeBlock.tsx +63 -33
  358. package/src/CommandPalette/CommandPalette.test.tsx +47 -1
  359. package/src/CommandPalette/CommandPalette.tsx +9 -21
  360. package/src/ContextMenu/ContextMenu.doc.mjs +4 -4
  361. package/src/ContextMenu/ContextMenu.test.tsx +155 -1
  362. package/src/ContextMenu/ContextMenu.tsx +113 -16
  363. package/src/DateInput/DateInput.doc.mjs +286 -56
  364. package/src/DateInput/DateInput.test.tsx +248 -2
  365. package/src/DateInput/DateInput.tsx +196 -100
  366. package/src/DateRangeInput/DateRangeInput.doc.mjs +251 -46
  367. package/src/DateRangeInput/DateRangeInput.test.tsx +182 -20
  368. package/src/DateRangeInput/DateRangeInput.tsx +63 -5
  369. package/src/DateTimeInput/DateTimeInput.doc.mjs +368 -67
  370. package/src/DateTimeInput/DateTimeInput.test.tsx +279 -11
  371. package/src/DateTimeInput/DateTimeInput.tsx +125 -17
  372. package/src/Dialog/Dialog.doc.mjs +2 -2
  373. package/src/Dialog/Dialog.tsx +12 -0
  374. package/src/DropdownMenu/DropdownMenu.doc.mjs +1 -8
  375. package/src/DropdownMenu/DropdownMenu.test.tsx +56 -42
  376. package/src/DropdownMenu/DropdownMenu.tsx +52 -18
  377. package/src/Field/Field.doc.mjs +59 -18
  378. package/src/Field/Field.test.tsx +32 -0
  379. package/src/Field/Field.tsx +23 -5
  380. package/src/Field/FieldLabel.tsx +55 -17
  381. package/src/FileInput/FileInput.doc.mjs +17 -0
  382. package/src/FileInput/FileInput.test.tsx +283 -49
  383. package/src/FileInput/FileInput.tsx +76 -4
  384. package/src/Grid/Grid.doc.mjs +17 -4
  385. package/src/Grid/Grid.test.tsx +43 -30
  386. package/src/Grid/Grid.tsx +38 -3
  387. package/src/Icon/Icon.doc.mjs +3 -3
  388. package/src/InputGroup/InputGroup.doc.mjs +111 -23
  389. package/src/InputGroup/InputGroup.test.tsx +257 -60
  390. package/src/InputGroup/InputGroup.tsx +23 -5
  391. package/src/InputGroup/InputGroupContext.ts +9 -4
  392. package/src/InputGroup/groupStyles.ts +7 -1
  393. package/src/InputGroup/index.ts +1 -0
  394. package/src/Kbd/Kbd.test.tsx +22 -4
  395. package/src/Kbd/Kbd.tsx +35 -3
  396. package/src/Layer/layerAnimations.stylex.ts +20 -4
  397. package/src/Layer/useLayer.doc.mjs +5 -0
  398. package/src/Layer/useLayer.test.tsx +111 -0
  399. package/src/Layer/useLayer.tsx +90 -9
  400. package/src/Layout/Layout.tsx +8 -16
  401. package/src/Layout/LayoutFooter.doc.mjs +4 -4
  402. package/src/Layout/LayoutFooter.tsx +3 -3
  403. package/src/Layout/LayoutHeader.doc.mjs +4 -4
  404. package/src/Layout/LayoutHeader.tsx +3 -3
  405. package/src/Layout/LayoutPanel.tsx +3 -3
  406. package/src/Layout/padding.stylex.ts +52 -0
  407. package/src/Lightbox/Lightbox.doc.mjs +9 -0
  408. package/src/Lightbox/Lightbox.test.tsx +53 -5
  409. package/src/Lightbox/Lightbox.tsx +9 -4
  410. package/src/Link/Link.doc.mjs +14 -0
  411. package/src/Link/Link.test.tsx +50 -0
  412. package/src/Link/Link.tsx +39 -4
  413. package/src/Markdown/Markdown.test.tsx +11 -0
  414. package/src/Markdown/Markdown.tsx +7 -0
  415. package/src/MetadataList/MetadataListItem.doc.mjs +4 -0
  416. package/src/MoreMenu/MoreMenu.doc.mjs +2 -17
  417. package/src/MoreMenu/MoreMenu.tsx +0 -9
  418. package/src/MultiSelector/MultiSelector.doc.mjs +30 -0
  419. package/src/MultiSelector/MultiSelector.test.tsx +375 -8
  420. package/src/MultiSelector/MultiSelector.tsx +194 -48
  421. package/src/MultiSelector/hooks.ts +27 -0
  422. package/src/NavMenu/NavHeadingMenu.test.tsx +77 -1
  423. package/src/NavMenu/NavHeadingMenu.tsx +50 -3
  424. package/src/NumberInput/NumberInput.doc.mjs +17 -0
  425. package/src/NumberInput/NumberInput.test.tsx +242 -40
  426. package/src/NumberInput/NumberInput.tsx +89 -11
  427. package/src/Pagination/Pagination.doc.mjs +3 -3
  428. package/src/Pagination/Pagination.test.tsx +117 -5
  429. package/src/Pagination/Pagination.tsx +21 -1
  430. package/src/Popover/Popover.doc.mjs +26 -0
  431. package/src/Popover/Popover.test.tsx +121 -0
  432. package/src/Popover/Popover.tsx +23 -2
  433. package/src/Popover/usePopover.doc.mjs +9 -3
  434. package/src/Popover/usePopover.tsx +70 -12
  435. package/src/PowerSearch/PowerSearch.doc.mjs +15 -0
  436. package/src/PowerSearch/PowerSearch.test.tsx +97 -1
  437. package/src/PowerSearch/PowerSearch.tsx +16 -3
  438. package/src/ProgressBar/ProgressBar.test.tsx +34 -26
  439. package/src/ProgressBar/ProgressBar.tsx +3 -4
  440. package/src/RadioList/RadioList.doc.mjs +9 -0
  441. package/src/RadioList/RadioList.test.tsx +252 -16
  442. package/src/RadioList/RadioList.tsx +166 -10
  443. package/src/RadioList/RadioListItem.tsx +14 -2
  444. package/src/SegmentedControl/SegmentedControl.doc.mjs +9 -0
  445. package/src/SegmentedControl/SegmentedControl.test.tsx +214 -42
  446. package/src/SegmentedControl/SegmentedControl.tsx +102 -54
  447. package/src/SegmentedControl/SegmentedControlContext.ts +7 -0
  448. package/src/SegmentedControl/SegmentedControlItem.tsx +14 -1
  449. package/src/SelectableCard/SelectableCard.doc.mjs +1 -1
  450. package/src/Selector/Selector.doc.mjs +26 -0
  451. package/src/Selector/Selector.test.tsx +387 -15
  452. package/src/Selector/Selector.tsx +175 -67
  453. package/src/Selector/hooks.ts +23 -0
  454. package/src/SideNav/SideNav.test.tsx +48 -0
  455. package/src/SideNav/SideNavHeading.tsx +8 -2
  456. package/src/SideNav/SideNavItem.tsx +2 -4
  457. package/src/Skeleton/Skeleton.test.tsx +25 -0
  458. package/src/Skeleton/Skeleton.tsx +11 -1
  459. package/src/Slider/Slider.doc.mjs +15 -0
  460. package/src/Slider/Slider.test.tsx +155 -2
  461. package/src/Slider/Slider.tsx +53 -5
  462. package/src/Spinner/Spinner.tsx +7 -1
  463. package/src/Stack/Stack.doc.mjs +77 -0
  464. package/src/Stack/Stack.test.tsx +55 -5
  465. package/src/Stack/Stack.tsx +73 -24
  466. package/src/Stack/StackItem.test.tsx +23 -0
  467. package/src/Stack/StackItem.tsx +20 -23
  468. package/src/Switch/Switch.doc.mjs +15 -0
  469. package/src/Switch/Switch.test.tsx +162 -27
  470. package/src/Switch/Switch.tsx +61 -21
  471. package/src/TabList/Tab.tsx +6 -0
  472. package/src/TabList/TabList.doc.mjs +6 -0
  473. package/src/TabList/TabList.test.tsx +251 -0
  474. package/src/TabList/TabList.tsx +103 -12
  475. package/src/TabList/TabListContext.ts +7 -3
  476. package/src/TabList/TabMenu.tsx +6 -3
  477. package/src/Table/BaseTable.tsx +21 -17
  478. package/src/Table/Table.doc.mjs +2 -0
  479. package/src/Table/Table.test.tsx +10 -0
  480. package/src/Table/Table.tsx +8 -0
  481. package/src/Table/TableCell.tsx +25 -23
  482. package/src/Table/TableHeaderCell.tsx +28 -29
  483. package/src/Table/index.ts +3 -2
  484. package/src/Table/plugins/pagination/paginateData.ts +10 -3
  485. package/src/Table/plugins/pagination/useTablePagination.test.tsx +16 -0
  486. package/src/Table/plugins/pagination/useTablePagination.tsx +8 -1
  487. package/src/Table/plugins/sortable/useTableSortable.test.tsx +91 -2
  488. package/src/Table/plugins/sortable/useTableSortable.tsx +53 -1
  489. package/src/Table/tableContextMenu.test.tsx +157 -0
  490. package/src/Table/tableContextMenu.tsx +142 -0
  491. package/src/Table/types.ts +74 -0
  492. package/src/Table/useTableColumnResize.doc.mjs +3 -3
  493. package/src/Table/useTableStickyColumns.doc.mjs +2 -2
  494. package/src/Text/Text.doc.mjs +18 -18
  495. package/src/TextArea/TextArea.doc.mjs +17 -0
  496. package/src/TextArea/TextArea.test.tsx +167 -35
  497. package/src/TextArea/TextArea.tsx +62 -15
  498. package/src/TextInput/TextInput.doc.mjs +17 -0
  499. package/src/TextInput/TextInput.test.tsx +168 -34
  500. package/src/TextInput/TextInput.tsx +77 -9
  501. package/src/Thumbnail/Thumbnail.doc.mjs +4 -4
  502. package/src/TimeInput/TimeInput.doc.mjs +340 -60
  503. package/src/TimeInput/TimeInput.test.tsx +255 -7
  504. package/src/TimeInput/TimeInput.tsx +176 -70
  505. package/src/Timestamp/Timestamp.doc.mjs +19 -19
  506. package/src/Toast/Toast.tsx +14 -0
  507. package/src/Toast/ToastViewport.test.tsx +214 -0
  508. package/src/Toast/ToastViewport.tsx +167 -21
  509. package/src/Tokenizer/Tokenizer.doc.mjs +29 -14
  510. package/src/Tokenizer/Tokenizer.test.tsx +105 -1
  511. package/src/Tokenizer/Tokenizer.tsx +37 -1
  512. package/src/Toolbar/Toolbar.test.tsx +109 -6
  513. package/src/Toolbar/Toolbar.tsx +59 -5
  514. package/src/Tooltip/Tooltip.test.tsx +68 -0
  515. package/src/Tooltip/useTooltip.tsx +55 -8
  516. package/src/TopNav/TopNav.test.tsx +50 -0
  517. package/src/TopNav/TopNavHeading.tsx +17 -1
  518. package/src/TreeList/TreeList.test.tsx +295 -6
  519. package/src/TreeList/TreeList.tsx +73 -1
  520. package/src/TreeList/TreeListItem.tsx +51 -5
  521. package/src/TreeList/treeListItem.markers.stylex.ts +20 -0
  522. package/src/Typeahead/BaseTypeahead.tsx +93 -9
  523. package/src/Typeahead/Typeahead.doc.mjs +141 -30
  524. package/src/Typeahead/Typeahead.test.tsx +331 -1
  525. package/src/Typeahead/Typeahead.tsx +105 -33
  526. package/src/VisuallyHidden/VisuallyHidden.doc.mjs +80 -0
  527. package/src/VisuallyHidden/VisuallyHidden.test.tsx +76 -0
  528. package/src/VisuallyHidden/VisuallyHidden.tsx +113 -0
  529. package/src/VisuallyHidden/index.ts +15 -0
  530. package/src/docs-script.test.mjs +50 -0
  531. package/src/hooks/focusableSelector.ts +25 -0
  532. package/src/hooks/index.ts +28 -2
  533. package/src/hooks/useAnnounce.test.tsx +101 -0
  534. package/src/hooks/useAnnounce.ts +168 -0
  535. package/src/hooks/useClickableContainer.ts +16 -4
  536. package/src/hooks/useEntryAnimation.ts +16 -4
  537. package/src/hooks/useFocusTrap.test.tsx +132 -0
  538. package/src/hooks/useFocusTrap.ts +98 -4
  539. package/src/hooks/useGridFocus.doc.mjs +40 -2
  540. package/src/hooks/useGridFocus.test.tsx +132 -0
  541. package/src/hooks/useGridFocus.ts +343 -67
  542. package/src/hooks/useKeyboardHint.doc.mjs +101 -0
  543. package/src/hooks/useKeyboardHint.test.tsx +70 -0
  544. package/src/hooks/useKeyboardHint.tsx +332 -0
  545. package/src/hooks/useListFocus.doc.mjs +50 -10
  546. package/src/hooks/useListFocus.test.tsx +375 -0
  547. package/src/hooks/useListFocus.ts +395 -57
  548. package/src/hooks/useLongPress.test.tsx +134 -0
  549. package/src/hooks/useLongPress.ts +119 -0
  550. package/src/hooks/useTreeFocus.doc.mjs +140 -0
  551. package/src/hooks/useTreeFocus.test.tsx +214 -0
  552. package/src/hooks/useTreeFocus.ts +559 -0
  553. package/src/hooks/useTypeahead.test.tsx +109 -0
  554. package/src/hooks/useTypeahead.ts +183 -0
  555. package/src/index.ts +1 -0
  556. package/src/theme/derivedVarRegistry.test.ts +10 -5
  557. package/src/utils/dateTypes.test.ts +40 -0
  558. package/src/utils/dateTypes.ts +40 -0
  559. package/src/utils/index.ts +9 -0
  560. package/src/utils/inputAria.test.ts +39 -0
  561. package/src/utils/inputAria.ts +58 -0
  562. package/src/utils/timeParser.test.ts +10 -0
  563. package/src/utils/timeParser.ts +5 -2
  564. package/dist/Calendar/hooks/useCalendarRovingTabindex.d.ts +0 -57
  565. package/dist/Calendar/hooks/useCalendarRovingTabindex.d.ts.map +0 -1
  566. package/dist/Calendar/hooks/useCalendarRovingTabindex.js +0 -96
  567. package/src/Calendar/hooks/useCalendarRovingTabindex.ts +0 -118
@@ -88,6 +88,12 @@ export const docs = {
88
88
  'Disables the input, preventing interaction and dimming the element.',
89
89
  default: 'false',
90
90
  },
91
+ {
92
+ name: 'disabledMessage',
93
+ type: 'string',
94
+ description:
95
+ 'Explains why the input is disabled. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the trigger focusable via aria-disabled (opening the file picker stays blocked). Use this instead of wrapping a disabled FileInput in Tooltip — disabled controls swallow the hover events an external Tooltip needs.',
96
+ },
91
97
  {
92
98
  name: 'isLoading',
93
99
  type: 'boolean',
@@ -135,6 +141,7 @@ export const docs = {
135
141
  {guidance: true, description: 'Use changeAction for immediate upload workflows that benefit from optimistic UI.'},
136
142
  {guidance: false, description: "Don't use FileInput for directory or folder uploads; that is not supported in v1."},
137
143
  {guidance: false, description: "Don't avoid dropzone mode unless space is constrained; drag-and-drop is the expected interaction for file uploads."},
144
+ {guidance: false, description: "Don't wrap a disabled FileInput in Tooltip to explain why it's disabled; disabled controls swallow the hover events the wrapper needs. Use the disabledMessage prop instead."},
138
145
  ],
139
146
  anatomy: [
140
147
  {name: 'Label', required: true, description: 'Text that identifies the field. Always rendered for accessibility even when visually hidden.'},
@@ -211,6 +218,12 @@ export const docsZh = {
211
218
  description: '禁用输入框,阻止交互并使元素变暗。',
212
219
  default: 'false',
213
220
  },
221
+ {
222
+ name: 'disabledMessage',
223
+ type: 'string',
224
+ description:
225
+ '说明输入框被禁用的原因。与 isDisabled 一起使用时,在悬停/键盘聚焦时显示工具提示,并通过 aria-disabled 保持触发器可聚焦(打开文件选择器仍被阻止)。请使用此属性,而不是用 Tooltip 包裹已禁用的 FileInput。',
226
+ },
214
227
  {
215
228
  name: 'isLoading',
216
229
  type: 'boolean',
@@ -248,6 +261,7 @@ export const docsZh = {
248
261
  {guidance: true, description: 'Add a description to communicate constraints.'},
249
262
  {guidance: false, description: "Don't use FileInput for directory uploads."},
250
263
  {guidance: false, description: "Don't use mode='input' unless space is constrained; dropzone mode provides a better experience."},
264
+ {guidance: false, description: "Don't wrap a disabled FileInput in Tooltip to explain the disabled state; use the disabledMessage prop instead."},
251
265
  ],
252
266
  anatomy: [
253
267
  {name: 'Label', required: true, description: 'Text identifying the field.'},
@@ -272,6 +286,7 @@ export const docsDense = {
272
286
  {guidance: true, description: 'Use changeAction for immediate upload workflows that benefit from optimistic UI.'},
273
287
  {guidance: false, description: "Don't use FileInput for directory uploads."},
274
288
  {guidance: false, description: "Don't use mode='input' unless space is constrained; dropzone mode provides a better experience."},
289
+ {guidance: false, description: "Don't wrap a disabled FileInput in Tooltip to explain the disabled state; use the disabledMessage prop instead."},
275
290
  ],
276
291
  anatomy: [
277
292
  {name: 'Label', required: true, description: 'Text identifying the field.'},
@@ -295,6 +310,8 @@ export const docsDense = {
295
310
  isOptional: 'Shows "Optional" indicator.',
296
311
  isRequired: 'Shows "Required" indicator+sets aria-required.',
297
312
  isDisabled: 'Disables input, prevents interaction.',
313
+ disabledMessage:
314
+ 'Explains why input is disabled. With isDisabled, shows tooltip on hover/focus + keeps trigger focusable via aria-disabled (opening picker stays blocked). Use instead of wrapping a disabled FileInput in Tooltip.',
298
315
  isLoading: 'Loading state w/ spinner+aria-busy.',
299
316
  placeholder: 'Placeholder when no files selected.',
300
317
  mode: "Visual mode: 'input' (compact) or 'dropzone' (drag-and-drop).",
@@ -9,10 +9,64 @@
9
9
  * SYNC: When FileInput.tsx changes, update tests to match new behavior
10
10
  */
11
11
 
12
- import {describe, it, expect, vi} from 'vitest';
13
- import {render, screen, fireEvent} from '@testing-library/react';
12
+ import {describe, it, expect, vi, afterEach, beforeEach} from 'vitest';
13
+ import {render, screen, fireEvent, waitFor} from '@testing-library/react';
14
14
  import userEvent from '@testing-library/user-event';
15
15
  import {FileInput} from './FileInput';
16
+ import {__resetLiveRegionsForTest} from '../hooks/useAnnounce';
17
+
18
+ afterEach(() => {
19
+ __resetLiveRegionsForTest();
20
+ });
21
+
22
+ // Mock showPopover/hidePopover since jsdom does not implement them. Used by the
23
+ // disabledMessage tooltip.
24
+ beforeEach(() => {
25
+ HTMLElement.prototype.showPopover = vi.fn(function (this: HTMLElement) {
26
+ this.setAttribute('popover-open', '');
27
+ const event = new Event('toggle', {bubbles: false});
28
+ Object.defineProperty(event, 'newState', {value: 'open'});
29
+ this.dispatchEvent(event);
30
+ });
31
+ HTMLElement.prototype.hidePopover = vi.fn(function (this: HTMLElement) {
32
+ this.removeAttribute('popover-open');
33
+ const event = new Event('toggle', {bubbles: false});
34
+ Object.defineProperty(event, 'newState', {value: 'closed'});
35
+ this.dispatchEvent(event);
36
+ });
37
+ const originalMatches = HTMLElement.prototype.matches;
38
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
39
+ (HTMLElement.prototype as any).matches = function (
40
+ selector: string,
41
+ ): boolean {
42
+ if (selector === ':popover-open') {
43
+ return this.hasAttribute('popover-open');
44
+ }
45
+ // jsdom does not resolve :focus-visible for role="button" divs the way a
46
+ // browser does after keyboard focus; treat the active element as
47
+ // focus-visible so the tooltip's keyboard-focus path is exercised.
48
+ if (selector === ':focus-visible') {
49
+ return this === document.activeElement;
50
+ }
51
+ return originalMatches.call(this, selector);
52
+ };
53
+ });
54
+
55
+ // jsdom popover content is in the DOM but may not be "visible" in the
56
+ // accessibility tree. Use hidden: true to find it.
57
+ const h = {hidden: true} as const;
58
+
59
+ function politeRegion(): HTMLElement | null {
60
+ return document.querySelector('[data-astryx-live-region="polite"]');
61
+ }
62
+
63
+ function fileInputEl(): HTMLInputElement {
64
+ const el = document.querySelector('input[type="file"]');
65
+ if (!(el instanceof HTMLInputElement)) {
66
+ throw new Error('file input not found');
67
+ }
68
+ return el;
69
+ }
16
70
 
17
71
  function createFile(
18
72
  name: string,
@@ -36,12 +90,7 @@ describe('FileInput', () => {
36
90
 
37
91
  it('renders default placeholder for multiple files', () => {
38
92
  render(
39
- <FileInput
40
- label="Files"
41
- value={null}
42
- onChange={() => {}}
43
- isMultiple
44
- />,
93
+ <FileInput label="Files" value={null} onChange={() => {}} isMultiple />,
45
94
  );
46
95
  expect(screen.getByText('Choose files')).toBeInTheDocument();
47
96
  });
@@ -67,12 +116,7 @@ describe('FileInput', () => {
67
116
  it('displays multiple file names', () => {
68
117
  const files = [createFile('a.txt', 100), createFile('b.txt', 200)];
69
118
  render(
70
- <FileInput
71
- label="Files"
72
- value={files}
73
- onChange={() => {}}
74
- isMultiple
75
- />,
119
+ <FileInput label="Files" value={files} onChange={() => {}} isMultiple />,
76
120
  );
77
121
  expect(screen.getByText('a.txt, b.txt')).toBeInTheDocument();
78
122
  });
@@ -80,12 +124,7 @@ describe('FileInput', () => {
80
124
  it('forwards ref to the native input', () => {
81
125
  const ref = vi.fn();
82
126
  render(
83
- <FileInput
84
- ref={ref}
85
- label="Upload"
86
- value={null}
87
- onChange={() => {}}
88
- />,
127
+ <FileInput ref={ref} label="Upload" value={null} onChange={() => {}} />,
89
128
  );
90
129
  expect(ref).toHaveBeenCalledWith(expect.any(HTMLInputElement));
91
130
  });
@@ -103,32 +142,44 @@ describe('FileInput', () => {
103
142
  });
104
143
 
105
144
  it('sets aria-required when isRequired is true', () => {
145
+ render(
146
+ <FileInput label="Resume" isRequired value={null} onChange={() => {}} />,
147
+ );
148
+ // aria-required lives on the focusable role="button" wrapper, not the
149
+ // hidden file input (forms-6).
150
+ expect(screen.getByRole('button', {name: 'Resume'})).toHaveAttribute(
151
+ 'aria-required',
152
+ 'true',
153
+ );
154
+ });
155
+
156
+ it('places aria-describedby on the focusable button, not the hidden input (forms-6)', () => {
106
157
  render(
107
158
  <FileInput
108
159
  label="Resume"
109
- isRequired
160
+ description="PDF only"
110
161
  value={null}
111
162
  onChange={() => {}}
112
163
  />,
113
164
  );
165
+ const button = screen.getByRole('button', {name: 'Resume'});
166
+ expect(button).toHaveAttribute('aria-describedby');
167
+ // The hidden file input no longer carries the describedby/required/invalid.
114
168
  const input = document.querySelector('input[type="file"]')!;
115
- expect(input).toHaveAttribute('aria-required', 'true');
169
+ expect(input).not.toHaveAttribute('aria-describedby');
170
+ expect(input).toHaveAttribute('aria-hidden', 'true');
116
171
  });
117
172
 
118
173
  it('does not set aria-required by default', () => {
119
174
  render(<FileInput label="Resume" value={null} onChange={() => {}} />);
120
- const input = document.querySelector('input[type="file"]')!;
121
- expect(input).not.toHaveAttribute('aria-required');
175
+ expect(screen.getByRole('button', {name: 'Resume'})).not.toHaveAttribute(
176
+ 'aria-required',
177
+ );
122
178
  });
123
179
 
124
180
  it('sets disabled attribute when isDisabled is true', () => {
125
181
  render(
126
- <FileInput
127
- label="Upload"
128
- isDisabled
129
- value={null}
130
- onChange={() => {}}
131
- />,
182
+ <FileInput label="Upload" isDisabled value={null} onChange={() => {}} />,
132
183
  );
133
184
  const input = document.querySelector('input[type="file"]')!;
134
185
  expect(input).toBeDisabled();
@@ -143,8 +194,10 @@ describe('FileInput', () => {
143
194
  status={{type: 'error', message: 'Something went wrong'}}
144
195
  />,
145
196
  );
146
- const input = document.querySelector('input[type="file"]')!;
147
- expect(input).toHaveAttribute('aria-invalid', 'true');
197
+ expect(screen.getByRole('button', {name: 'Upload'})).toHaveAttribute(
198
+ 'aria-invalid',
199
+ 'true',
200
+ );
148
201
  });
149
202
 
150
203
  it('does not set aria-invalid for warning status', () => {
@@ -156,8 +209,9 @@ describe('FileInput', () => {
156
209
  status={{type: 'warning'}}
157
210
  />,
158
211
  );
159
- const input = document.querySelector('input[type="file"]')!;
160
- expect(input).not.toHaveAttribute('aria-invalid');
212
+ expect(screen.getByRole('button', {name: 'Upload'})).not.toHaveAttribute(
213
+ 'aria-invalid',
214
+ );
161
215
  });
162
216
 
163
217
  it('renders status message when provided', () => {
@@ -187,9 +241,7 @@ describe('FileInput', () => {
187
241
  describe('file selection via native input', () => {
188
242
  it('calls onChange when a file is selected', () => {
189
243
  const handleChange = vi.fn();
190
- render(
191
- <FileInput label="Upload" value={null} onChange={handleChange} />,
192
- );
244
+ render(<FileInput label="Upload" value={null} onChange={handleChange} />);
193
245
  const input = document.querySelector(
194
246
  'input[type="file"]',
195
247
  ) as HTMLInputElement;
@@ -247,6 +299,55 @@ describe('FileInput', () => {
247
299
  });
248
300
  });
249
301
 
302
+ describe('announcements', () => {
303
+ it('announces a single file selection politely', async () => {
304
+ render(<FileInput label="Upload" value={null} onChange={() => {}} />);
305
+ fireEvent.change(fileInputEl(), {
306
+ target: {files: [createFile('report.pdf', 100)]},
307
+ });
308
+ await waitFor(() => {
309
+ expect(politeRegion()).toHaveTextContent('1 file selected: report.pdf');
310
+ });
311
+ });
312
+
313
+ it('announces a multi-file count politely', async () => {
314
+ render(
315
+ <FileInput
316
+ label="Upload"
317
+ value={null}
318
+ onChange={() => {}}
319
+ isMultiple
320
+ />,
321
+ );
322
+ const files = [
323
+ createFile('a.txt', 100),
324
+ createFile('b.txt', 200),
325
+ createFile('c.txt', 300),
326
+ ];
327
+ fireEvent.change(fileInputEl(), {target: {files}});
328
+ await waitFor(() => {
329
+ expect(politeRegion()).toHaveTextContent('3 files selected');
330
+ });
331
+ });
332
+
333
+ it('does not announce a selection when validation rejects all files', async () => {
334
+ render(
335
+ <FileInput
336
+ label="Upload"
337
+ value={null}
338
+ onChange={() => {}}
339
+ accept=".pdf"
340
+ />,
341
+ );
342
+ fireEvent.change(fileInputEl(), {
343
+ target: {files: [createFile('note.txt', 100)]},
344
+ });
345
+ // A rejected selection creates no polite region (only the error goes to
346
+ // the existing role="status" region).
347
+ expect(politeRegion()).toBeNull();
348
+ });
349
+ });
350
+
250
351
  describe('validation', () => {
251
352
  it('rejects files exceeding maxSize', () => {
252
353
  const handleChange = vi.fn();
@@ -382,9 +483,7 @@ describe('FileInput', () => {
382
483
  const user = userEvent.setup();
383
484
  const handleChange = vi.fn();
384
485
  const file = createFile('test.txt', 100);
385
- render(
386
- <FileInput label="Upload" value={file} onChange={handleChange} />,
387
- );
486
+ render(<FileInput label="Upload" value={file} onChange={handleChange} />);
388
487
  await user.click(screen.getByRole('button', {name: 'Clear Upload'}));
389
488
  expect(handleChange).toHaveBeenCalledWith(null);
390
489
  });
@@ -392,12 +491,7 @@ describe('FileInput', () => {
392
491
  it('does not show clear button during loading', () => {
393
492
  const file = createFile('test.txt', 100);
394
493
  render(
395
- <FileInput
396
- label="Upload"
397
- value={file}
398
- onChange={() => {}}
399
- isLoading
400
- />,
494
+ <FileInput label="Upload" value={file} onChange={() => {}} isLoading />,
401
495
  );
402
496
  expect(
403
497
  screen.queryByRole('button', {name: 'Clear Upload'}),
@@ -426,9 +520,7 @@ describe('FileInput', () => {
426
520
 
427
521
  it('does not handle drop in input mode', () => {
428
522
  const handleChange = vi.fn();
429
- render(
430
- <FileInput label="Upload" value={null} onChange={handleChange} />,
431
- );
523
+ render(<FileInput label="Upload" value={null} onChange={handleChange} />);
432
524
  const dropzone = screen.getByRole('button', {name: 'Upload'});
433
525
  const file = createFile('dropped.txt', 100);
434
526
  fireEvent.drop(dropzone, {
@@ -526,4 +618,146 @@ describe('FileInput', () => {
526
618
  expect(input).toHaveAttribute('data-testid', 'file-upload');
527
619
  });
528
620
  });
621
+
622
+ describe('disabledMessage', () => {
623
+ it('shows the reason tooltip on hover when disabled with a reason', async () => {
624
+ render(
625
+ <FileInput
626
+ label="Resume"
627
+ value={null}
628
+ onChange={() => {}}
629
+ isDisabled
630
+ disabledMessage="You need the Editor role"
631
+ />,
632
+ );
633
+
634
+ const trigger = screen.getByRole('button');
635
+ const tooltip = screen.getByRole('tooltip', h);
636
+ expect(tooltip).toHaveTextContent('You need the Editor role');
637
+
638
+ fireEvent.mouseEnter(trigger);
639
+ await waitFor(() => {
640
+ expect(tooltip).toHaveAttribute('popover-open');
641
+ });
642
+
643
+ fireEvent.mouseLeave(trigger);
644
+ await waitFor(() => {
645
+ expect(tooltip).not.toHaveAttribute('popover-open');
646
+ });
647
+ });
648
+
649
+ it('shows the reason tooltip on keyboard focus', async () => {
650
+ const user = userEvent.setup();
651
+ render(
652
+ <FileInput
653
+ label="Resume"
654
+ value={null}
655
+ onChange={() => {}}
656
+ isDisabled
657
+ disabledMessage="You need the Editor role"
658
+ />,
659
+ );
660
+
661
+ const tooltip = screen.getByRole('tooltip', h);
662
+ await user.tab();
663
+ expect(screen.getByRole('button')).toHaveFocus();
664
+ await waitFor(() => {
665
+ expect(tooltip).toHaveAttribute('popover-open');
666
+ });
667
+ });
668
+
669
+ it('does not render a tooltip when not disabled', () => {
670
+ render(
671
+ <FileInput
672
+ label="Resume"
673
+ value={null}
674
+ onChange={() => {}}
675
+ disabledMessage="You need the Editor role"
676
+ />,
677
+ );
678
+ expect(screen.queryByRole('tooltip', h)).not.toBeInTheDocument();
679
+ });
680
+
681
+ it('does not render a tooltip when disabled without a reason', () => {
682
+ render(
683
+ <FileInput
684
+ label="Resume"
685
+ value={null}
686
+ onChange={() => {}}
687
+ isDisabled
688
+ />,
689
+ );
690
+ expect(screen.queryByRole('tooltip', h)).not.toBeInTheDocument();
691
+ });
692
+
693
+ it('keeps the trigger focusable via aria-disabled when a reason is provided', () => {
694
+ render(
695
+ <FileInput
696
+ label="Resume"
697
+ value={null}
698
+ onChange={() => {}}
699
+ isDisabled
700
+ disabledMessage="You need the Editor role"
701
+ />,
702
+ );
703
+ const trigger = screen.getByRole('button');
704
+ expect(trigger).toHaveAttribute('aria-disabled', 'true');
705
+ expect(trigger).toHaveAttribute('tabindex', '0');
706
+ });
707
+
708
+ it('links the reason tooltip from the trigger via aria-describedby', () => {
709
+ render(
710
+ <FileInput
711
+ label="Resume"
712
+ value={null}
713
+ onChange={() => {}}
714
+ isDisabled
715
+ disabledMessage="You need the Editor role"
716
+ />,
717
+ );
718
+ const trigger = screen.getByRole('button');
719
+ const tooltip = screen.getByRole('tooltip', h);
720
+ expect(trigger.getAttribute('aria-describedby')).toContain(tooltip.id);
721
+ });
722
+
723
+ it('blocks opening the file picker while focusable-disabled', async () => {
724
+ const user = userEvent.setup();
725
+ render(
726
+ <FileInput
727
+ label="Resume"
728
+ value={null}
729
+ onChange={() => {}}
730
+ isDisabled
731
+ disabledMessage="You need the Editor role"
732
+ />,
733
+ );
734
+
735
+ const trigger = screen.getByRole('button');
736
+ const input = document.querySelector(
737
+ 'input[type="file"]',
738
+ ) as HTMLInputElement;
739
+ const clickSpy = vi.spyOn(input, 'click');
740
+
741
+ await user.click(trigger);
742
+ trigger.focus();
743
+ await user.keyboard('{Enter}');
744
+ await user.keyboard(' ');
745
+
746
+ expect(clickSpy).not.toHaveBeenCalled();
747
+ });
748
+
749
+ it('keeps the trigger non-focusable when disabled without a reason', () => {
750
+ render(
751
+ <FileInput
752
+ label="Resume"
753
+ value={null}
754
+ onChange={() => {}}
755
+ isDisabled
756
+ />,
757
+ );
758
+ const trigger = screen.getByRole('button');
759
+ expect(trigger).toHaveAttribute('tabindex', '-1');
760
+ expect(trigger).not.toHaveAttribute('aria-disabled');
761
+ });
762
+ });
529
763
  });
@@ -44,6 +44,8 @@ import {
44
44
  } from '../Field';
45
45
  import {Icon, type IconName} from '../Icon';
46
46
  import {Spinner} from '../Spinner';
47
+ import {useAnnounce} from '../hooks/useAnnounce';
48
+ import {useTooltip} from '../Tooltip';
47
49
 
48
50
  export type {
49
51
  InputStatus as FileInputStatus,
@@ -320,6 +322,27 @@ export interface FileInputProps extends Omit<
320
322
  * @default false
321
323
  */
322
324
  isDisabled?: boolean;
325
+ /**
326
+ * Explains why the input is disabled. When set together with `isDisabled`,
327
+ * the file input shows a tooltip with this text on hover and keyboard focus,
328
+ * and its trigger stays focusable (via `aria-disabled`) so the reason is
329
+ * discoverable by keyboard and assistive technology. Opening the file picker
330
+ * stays blocked.
331
+ *
332
+ * Use this instead of wrapping a disabled input in `Tooltip` — disabled
333
+ * controls don't emit the pointer events an external tooltip needs.
334
+ *
335
+ * @example
336
+ * ```
337
+ * <FileInput
338
+ * label="Resume"
339
+ * value={file}
340
+ * isDisabled
341
+ * disabledMessage="Uploads are locked until your profile is verified"
342
+ * />
343
+ * ```
344
+ */
345
+ disabledMessage?: string;
323
346
  /**
324
347
  * Whether the input is required.
325
348
  * @default false
@@ -386,6 +409,7 @@ export function FileInput({
386
409
  maxSize,
387
410
  maxFiles,
388
411
  isDisabled = false,
412
+ disabledMessage,
389
413
  isRequired = false,
390
414
  isLoading = false,
391
415
  status: statusProp,
@@ -410,6 +434,23 @@ export function FileInput({
410
434
  const [validationError, setValidationError] = useState<string | null>(null);
411
435
  const [, startTransition] = useTransition();
412
436
 
437
+ // Announce successful file selection to screen readers via a persistent
438
+ // live region (forms-17). The component's own role="status" region only
439
+ // carries validation errors, so a successful attach was previously silent.
440
+ const announce = useAnnounce();
441
+
442
+ // Disabled-reason tooltip. Disabled controls swallow pointer events, so the
443
+ // tooltip listeners attach to the role="button" trigger (which already
444
+ // exists) and the trigger stays perceivable via aria-disabled instead of the
445
+ // native disabled attribute. Opening the picker is blocked by the isDisabled
446
+ // guards in handleClick / handleKeyDown / handleFiles.
447
+ const showsDisabledMessage = isDisabled && !!disabledMessage;
448
+ const disabledMessageTooltip = useTooltip({
449
+ placement: 'above',
450
+ focusTrigger: 'always',
451
+ isEnabled: showsDisabledMessage,
452
+ });
453
+
413
454
  const status =
414
455
  statusProp ??
415
456
  (validationError
@@ -435,6 +476,7 @@ export function FileInput({
435
476
  [
436
477
  description ? descriptionID : null,
437
478
  status?.message ? statusMessageID : null,
479
+ showsDisabledMessage ? disabledMessageTooltip.describedBy : null,
438
480
  ]
439
481
  .filter(Boolean)
440
482
  .join(' ') || undefined;
@@ -470,6 +512,17 @@ export function FileInput({
470
512
  const result = isMultiple ? valid : valid[0];
471
513
  onChange(result);
472
514
 
515
+ // Announce the successful selection politely. Validation errors are
516
+ // handled by the role="status" region below, so only announce the
517
+ // attach here (do not double-announce errors).
518
+ if (errors.length === 0) {
519
+ announce(
520
+ valid.length === 1
521
+ ? `1 file selected: ${valid[0].name}`
522
+ : `${valid.length} files selected`,
523
+ );
524
+ }
525
+
473
526
  if (changeAction) {
474
527
  startTransition(async () => {
475
528
  await changeAction(result);
@@ -485,6 +538,7 @@ export function FileInput({
485
538
  onChange,
486
539
  changeAction,
487
540
  startTransition,
541
+ announce,
488
542
  ],
489
543
  );
490
544
 
@@ -666,12 +720,28 @@ export function FileInput({
666
720
  labelTooltip={labelTooltip}
667
721
  width={width}>
668
722
  <div
723
+ ref={el => {
724
+ // Anchor + hover/focus listeners for the disabled-message tooltip.
725
+ // Handlers are gated internally by isEnabled, and anchor names
726
+ // compose, so attaching unconditionally is safe.
727
+ disabledMessageTooltip.ref(el);
728
+ }}
669
729
  role="button"
670
- tabIndex={isDisabled ? -1 : 0}
730
+ // With a disabledMessage the trigger keeps focusability via
731
+ // aria-disabled so the reason is focus-discoverable; opening the picker
732
+ // is still blocked by the isDisabled guards in the handlers.
733
+ tabIndex={isDisabled && !showsDisabledMessage ? -1 : 0}
734
+ aria-disabled={showsDisabledMessage ? 'true' : undefined}
671
735
  onClick={handleClick}
672
736
  onKeyDown={handleKeyDown}
673
737
  aria-label={label}
674
738
  aria-busy={isLoading || undefined}
739
+ // These describe the operable control, so they belong on the focusable
740
+ // role="button" wrapper — not the hidden tabIndex={-1} file input the
741
+ // user never focuses (forms-6).
742
+ aria-describedby={ariaDescribedBy}
743
+ aria-required={isRequired ? 'true' : undefined}
744
+ aria-invalid={status?.type === 'error' ? 'true' : undefined}
675
745
  {...dragDropProps}
676
746
  {...mergeProps(
677
747
  themeProps('file-input', {mode, status: status?.type ?? null}),
@@ -696,9 +766,9 @@ export function FileInput({
696
766
  multiple={isMultiple}
697
767
  disabled={isDisabled}
698
768
  onChange={handleInputChange}
699
- aria-describedby={ariaDescribedBy}
700
- aria-required={isRequired ? 'true' : undefined}
701
- aria-invalid={status?.type === 'error' ? 'true' : undefined}
769
+ // The visually-hidden native input is never focused (tabIndex={-1});
770
+ // its describedby/required/invalid live on the role="button" wrapper.
771
+ aria-hidden="true"
702
772
  tabIndex={-1}
703
773
  {...stylex.props(styles.hiddenInput)}
704
774
  />
@@ -714,6 +784,8 @@ export function FileInput({
714
784
  {...stylex.props(styles.liveRegion)}>
715
785
  {validationError}
716
786
  </div>
787
+ {showsDisabledMessage &&
788
+ disabledMessageTooltip.renderTooltip(disabledMessage)}
717
789
  </Field>
718
790
  );
719
791
  }