@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
@@ -4,7 +4,7 @@
4
4
 
5
5
  /**
6
6
  * @file Switch.tsx
7
- * @input Uses React, useId, ChangeEvent, FieldLabel, FieldStatus, IconType, InputStatus
7
+ * @input Uses React, useId, ChangeEvent, FieldLabel, FieldStatus, IconType, InputStatus, useTooltip
8
8
  * @output Exports Switch component, SwitchProps, SwitchLabelPosition, SwitchLabelSpacing
9
9
  * @position Core implementation; consumed by index.ts, tested by Switch.test.tsx
10
10
  *
@@ -39,11 +39,13 @@ import {FieldStatus} from '../FieldStatus/FieldStatus';
39
39
  import type {IconType} from '../Icon';
40
40
  import type {InputStatus} from '../Field/types';
41
41
  import {Spinner} from '../Spinner';
42
- import {mergeProps} from '../utils';
42
+ import {useTooltip} from '../Tooltip';
43
+ import {mergeProps, mergeRefs} from '../utils';
43
44
  import {switchScope} from './switch.markers.stylex';
44
45
  import type {BaseProps} from '../BaseProps';
45
46
  import type {SizeValue} from '../utils/types';
46
47
  import {themeProps} from '../utils/themeProps';
48
+ import {VisuallyHidden} from '../VisuallyHidden';
47
49
 
48
50
  // Fixed dimensions: 40px width, 24px height, 16px thumb (off), 20px thumb (on)
49
51
  const SWITCH_WIDTH = 40;
@@ -179,17 +181,6 @@ const styles = stylex.create({
179
181
  fontSize: typeScaleVars['--text-supporting-size'],
180
182
  color: colorVars['--color-text-secondary'],
181
183
  },
182
- srOnly: {
183
- position: 'absolute',
184
- width: 1,
185
- height: 1,
186
- padding: 0,
187
- margin: -1,
188
- overflow: 'hidden',
189
- clip: 'rect(0, 0, 0, 0)',
190
- whiteSpace: 'nowrap',
191
- borderWidth: 0,
192
- },
193
184
  });
194
185
 
195
186
  export type SwitchLabelPosition = 'start' | 'end';
@@ -237,6 +228,27 @@ export interface SwitchProps extends Omit<BaseProps, 'onChange'> {
237
228
  * @default false
238
229
  */
239
230
  isDisabled?: boolean;
231
+ /**
232
+ * Explains why the switch is disabled. When set together with `isDisabled`,
233
+ * the switch shows a tooltip with this text on hover and keyboard focus, and
234
+ * the control stays focusable (via `aria-disabled`) so the reason is
235
+ * discoverable by keyboard and assistive technology. Activation stays
236
+ * blocked.
237
+ *
238
+ * Use this instead of wrapping a disabled switch in `Tooltip` — disabled
239
+ * controls don't emit the pointer events an external tooltip needs.
240
+ *
241
+ * @example
242
+ * ```
243
+ * <Switch
244
+ * label="Enable notifications"
245
+ * value={enabled}
246
+ * isDisabled
247
+ * disabledMessage="Notifications are turned off org-wide"
248
+ * />
249
+ * ```
250
+ */
251
+ disabledMessage?: string;
240
252
  /**
241
253
  * Whether the field is optional. Mutually exclusive with isRequired.
242
254
  * @default false
@@ -322,6 +334,7 @@ export function Switch({
322
334
  isLoading = false,
323
335
  value,
324
336
  isDisabled = false,
337
+ disabledMessage,
325
338
  isOptional = false,
326
339
  isRequired = false,
327
340
  onFocus,
@@ -347,6 +360,19 @@ export function Switch({
347
360
 
348
361
  const isOn = optimisticValue === true;
349
362
 
363
+ // Disabled-reason tooltip. Disabled controls swallow pointer events, so the
364
+ // tooltip listeners attach to the switch row (which already exists) and the
365
+ // native checkbox stays perceivable via aria-disabled instead of the disabled
366
+ // attribute. Toggling is blocked by the isDisabled guard in onChange.
367
+ const showsDisabledMessage = isDisabled && !!disabledMessage;
368
+ const disabledMessageTooltip = useTooltip({
369
+ placement: 'above',
370
+ // The container row is not naturally focusable; focusin bubbles up from the
371
+ // native input, so always attach focus listeners.
372
+ focusTrigger: 'always',
373
+ isEnabled: showsDisabledMessage,
374
+ });
375
+
350
376
  // Build aria-describedby from description and status message
351
377
  // Only include descriptionID when the element actually renders
352
378
  const describedByParts: string[] = [];
@@ -356,21 +382,28 @@ export function Switch({
356
382
  if (status?.message) {
357
383
  describedByParts.push(statusMessageID);
358
384
  }
385
+ if (showsDisabledMessage) {
386
+ describedByParts.push(disabledMessageTooltip.describedBy);
387
+ }
359
388
  const ariaDescribedBy =
360
389
  describedByParts.length > 0 ? describedByParts.join(' ') : undefined;
361
390
 
362
391
  const switchElement = (
363
392
  <div {...stylex.props(styles.switchWrapper)}>
364
393
  <input
365
- ref={ref}
394
+ ref={mergeRefs(ref, disabledMessageTooltip.positionRef)}
366
395
  id={id}
367
396
  type="checkbox"
368
397
  role="switch"
369
398
  checked={isOn}
370
- disabled={isDisabled}
399
+ // With a disabledMessage the switch keeps focusability via aria-disabled
400
+ // so the reason is focus-discoverable; toggling is still blocked by the
401
+ // isDisabled guard in onChange below.
402
+ disabled={isDisabled && !showsDisabledMessage}
403
+ aria-disabled={showsDisabledMessage ? 'true' : undefined}
371
404
  required={isRequired}
372
405
  onChange={e => {
373
- if (isBusy) {
406
+ if (isDisabled || isBusy) {
374
407
  return;
375
408
  }
376
409
  const checked = e.target.checked;
@@ -416,11 +449,7 @@ export function Switch({
416
449
  {isBusy && <Spinner size="sm" />}
417
450
  </div>
418
451
  </div>
419
- {isBusy && (
420
- <span {...stylex.props(styles.srOnly)} role="status">
421
- Loading
422
- </span>
423
- )}
452
+ {isBusy && <VisuallyHidden role="status">Loading</VisuallyHidden>}
424
453
  </div>
425
454
  );
426
455
 
@@ -453,6 +482,15 @@ export function Switch({
453
482
  style,
454
483
  )}>
455
484
  <div
485
+ ref={el => {
486
+ // Interaction (hover/focus) listeners for the disabled-message
487
+ // tooltip attach to the whole row for a larger trigger target;
488
+ // positioning anchors on the switch itself (above) so the tooltip
489
+ // appears next to the control, not the far edge of the row.
490
+ // Handlers are gated internally by isEnabled, so attaching
491
+ // unconditionally is safe.
492
+ disabledMessageTooltip.interactionRef(el);
493
+ }}
456
494
  {...stylex.props(
457
495
  styles.container,
458
496
  labelSpacing === 'spread' && styles.containerSpread,
@@ -481,6 +519,8 @@ export function Switch({
481
519
  />
482
520
  </div>
483
521
  )}
522
+ {showsDisabledMessage &&
523
+ disabledMessageTooltip.renderTooltip(disabledMessage)}
484
524
  </div>
485
525
  );
486
526
  }
@@ -259,7 +259,13 @@ export function Tab({
259
259
  ...restProps,
260
260
  ...(isLabelHidden ? {'aria-label': label} : {}),
261
261
  [EDGE_COMP_ATTR]: '',
262
+ 'data-tab-value': value,
262
263
  'aria-current': isSelected ? ('page' as const) : undefined,
264
+ // Roving tabindex: the tab strip is a single Tab stop. The selected tab is
265
+ // the tabbable one; the rest are reachable via arrow keys (handled by
266
+ // TabList's onKeyDown). When no tab is selected, TabList's repair effect
267
+ // makes the first stop tabbable.
268
+ tabIndex: isSelected ? 0 : -1,
263
269
  ...mergeProps(
264
270
  themeProps('tab', {
265
271
  selected: isSelected ? 'selected' : null,
@@ -55,6 +55,12 @@ export const docs = {
55
55
  description: 'Whether to show a bottom border divider under the tab list.',
56
56
  default: 'false',
57
57
  },
58
+ {
59
+ name: 'orientation',
60
+ type: "'horizontal' | 'vertical'",
61
+ description: "Orientation of the tab strip, controlling which arrow keys move focus between tabs and the reported aria-orientation. 'horizontal': ArrowLeft/ArrowRight. 'vertical': ArrowUp/ArrowDown. Both axes' arrows are accepted regardless.",
62
+ default: "'horizontal'",
63
+ },
58
64
  {
59
65
  name: 'children',
60
66
  type: 'ReactNode',
@@ -75,6 +75,49 @@ describe('TabList', () => {
75
75
  expect(screen.getByRole('button', {name: 'Settings'})).toBeInTheDocument();
76
76
  });
77
77
 
78
+ it('does not set aria-orientation on the nav (invalid for role navigation)', () => {
79
+ // Regression: aria-orientation is not an allowed attribute on the
80
+ // navigation role and produces an axe aria-allowed-attr violation. The
81
+ // `orientation` prop must drive keyboard/hint behavior without emitting
82
+ // this attribute, regardless of the value passed.
83
+ const {rerender} = render(
84
+ <TabList value="home" onChange={() => {}}>
85
+ <Tab value="home" label="Home" />
86
+ <Tab value="settings" label="Settings" />
87
+ </TabList>,
88
+ );
89
+
90
+ expect(screen.getByRole('navigation')).not.toHaveAttribute(
91
+ 'aria-orientation',
92
+ );
93
+
94
+ rerender(
95
+ <TabList value="home" onChange={() => {}} orientation="vertical">
96
+ <Tab value="home" label="Home" />
97
+ <Tab value="settings" label="Settings" />
98
+ </TabList>,
99
+ );
100
+
101
+ expect(screen.getByRole('navigation')).not.toHaveAttribute(
102
+ 'aria-orientation',
103
+ );
104
+ });
105
+
106
+ it('ignores a consumer-supplied aria-orientation on the nav', () => {
107
+ // A caller passing aria-orientation should not reintroduce the invalid
108
+ // attribute onto the nav.
109
+ render(
110
+ <TabList value="home" onChange={() => {}} aria-orientation="vertical">
111
+ <Tab value="home" label="Home" />
112
+ <Tab value="settings" label="Settings" />
113
+ </TabList>,
114
+ );
115
+
116
+ expect(screen.getByRole('navigation')).not.toHaveAttribute(
117
+ 'aria-orientation',
118
+ );
119
+ });
120
+
78
121
  it('marks selected tab with aria-current', () => {
79
122
  render(
80
123
  <TabList value="home" onChange={() => {}}>
@@ -280,6 +323,214 @@ describe('TabList', () => {
280
323
  });
281
324
  });
282
325
 
326
+ describe('TabList keyboard navigation (roving tabindex)', () => {
327
+ it('exposes the tab strip as a single Tab stop (only selected tab is tabbable)', () => {
328
+ render(
329
+ <TabList value="settings" onChange={() => {}}>
330
+ <Tab value="home" label="Home" />
331
+ <Tab value="settings" label="Settings" />
332
+ <Tab value="profile" label="Profile" />
333
+ </TabList>,
334
+ );
335
+
336
+ expect(screen.getByRole('button', {name: 'Home'})).toHaveAttribute(
337
+ 'tabindex',
338
+ '-1',
339
+ );
340
+ expect(screen.getByRole('button', {name: 'Settings'})).toHaveAttribute(
341
+ 'tabindex',
342
+ '0',
343
+ );
344
+ expect(screen.getByRole('button', {name: 'Profile'})).toHaveAttribute(
345
+ 'tabindex',
346
+ '-1',
347
+ );
348
+ });
349
+
350
+ it('makes the first tab tabbable when the selected value matches no tab', () => {
351
+ render(
352
+ <TabList value="__none__" onChange={() => {}}>
353
+ <Tab value="home" label="Home" />
354
+ <Tab value="settings" label="Settings" />
355
+ </TabList>,
356
+ );
357
+
358
+ expect(screen.getByRole('button', {name: 'Home'})).toHaveAttribute(
359
+ 'tabindex',
360
+ '0',
361
+ );
362
+ expect(screen.getByRole('button', {name: 'Settings'})).toHaveAttribute(
363
+ 'tabindex',
364
+ '-1',
365
+ );
366
+ });
367
+
368
+ it('moves focus with ArrowRight and ArrowLeft', async () => {
369
+ const user = userEvent.setup();
370
+ render(
371
+ <TabList value="home" onChange={() => {}}>
372
+ <Tab value="home" label="Home" />
373
+ <Tab value="settings" label="Settings" />
374
+ <Tab value="profile" label="Profile" />
375
+ </TabList>,
376
+ );
377
+
378
+ const home = screen.getByRole('button', {name: 'Home'});
379
+ const settings = screen.getByRole('button', {name: 'Settings'});
380
+ const profile = screen.getByRole('button', {name: 'Profile'});
381
+
382
+ home.focus();
383
+ expect(home).toHaveFocus();
384
+
385
+ await user.keyboard('{ArrowRight}');
386
+ expect(settings).toHaveFocus();
387
+
388
+ await user.keyboard('{ArrowRight}');
389
+ expect(profile).toHaveFocus();
390
+
391
+ await user.keyboard('{ArrowLeft}');
392
+ expect(settings).toHaveFocus();
393
+ });
394
+
395
+ it('supports ArrowDown and ArrowUp as forward/backward as well', async () => {
396
+ const user = userEvent.setup();
397
+ render(
398
+ <TabList value="home" onChange={() => {}}>
399
+ <Tab value="home" label="Home" />
400
+ <Tab value="settings" label="Settings" />
401
+ </TabList>,
402
+ );
403
+
404
+ const home = screen.getByRole('button', {name: 'Home'});
405
+ const settings = screen.getByRole('button', {name: 'Settings'});
406
+
407
+ home.focus();
408
+ await user.keyboard('{ArrowDown}');
409
+ expect(settings).toHaveFocus();
410
+
411
+ await user.keyboard('{ArrowUp}');
412
+ expect(home).toHaveFocus();
413
+ });
414
+
415
+ it('jumps to first and last tab with Home and End', async () => {
416
+ const user = userEvent.setup();
417
+ render(
418
+ <TabList value="settings" onChange={() => {}}>
419
+ <Tab value="home" label="Home" />
420
+ <Tab value="settings" label="Settings" />
421
+ <Tab value="profile" label="Profile" />
422
+ </TabList>,
423
+ );
424
+
425
+ const home = screen.getByRole('button', {name: 'Home'});
426
+ const settings = screen.getByRole('button', {name: 'Settings'});
427
+ const profile = screen.getByRole('button', {name: 'Profile'});
428
+
429
+ settings.focus();
430
+
431
+ await user.keyboard('{End}');
432
+ expect(profile).toHaveFocus();
433
+
434
+ await user.keyboard('{Home}');
435
+ expect(home).toHaveFocus();
436
+ });
437
+
438
+ it('wraps around at the ends', async () => {
439
+ const user = userEvent.setup();
440
+ render(
441
+ <TabList value="home" onChange={() => {}}>
442
+ <Tab value="home" label="Home" />
443
+ <Tab value="settings" label="Settings" />
444
+ <Tab value="profile" label="Profile" />
445
+ </TabList>,
446
+ );
447
+
448
+ const home = screen.getByRole('button', {name: 'Home'});
449
+ const profile = screen.getByRole('button', {name: 'Profile'});
450
+
451
+ home.focus();
452
+ await user.keyboard('{ArrowLeft}');
453
+ expect(profile).toHaveFocus();
454
+
455
+ await user.keyboard('{ArrowRight}');
456
+ expect(home).toHaveFocus();
457
+ });
458
+
459
+ it('skips disabled tabs during arrow navigation', async () => {
460
+ const user = userEvent.setup();
461
+ render(
462
+ <TabList value="home" onChange={() => {}}>
463
+ <Tab value="home" label="Home" />
464
+ <Tab value="settings" label="Settings" aria-disabled="true" />
465
+ <Tab value="profile" label="Profile" />
466
+ </TabList>,
467
+ );
468
+
469
+ const home = screen.getByRole('button', {name: 'Home'});
470
+ const profile = screen.getByRole('button', {name: 'Profile'});
471
+
472
+ home.focus();
473
+ await user.keyboard('{ArrowRight}');
474
+ expect(profile).toHaveFocus();
475
+ });
476
+
477
+ it('does not intercept unrelated keys', async () => {
478
+ const user = userEvent.setup();
479
+ render(
480
+ <TabList value="home" onChange={() => {}}>
481
+ <Tab value="home" label="Home" />
482
+ <Tab value="settings" label="Settings" />
483
+ </TabList>,
484
+ );
485
+
486
+ const home = screen.getByRole('button', {name: 'Home'});
487
+ home.focus();
488
+ await user.keyboard('a');
489
+ expect(home).toHaveFocus();
490
+ });
491
+
492
+ it('composes consumer onKeyDown with internal arrow navigation', async () => {
493
+ const user = userEvent.setup();
494
+ const onKeyDown = vi.fn();
495
+
496
+ render(
497
+ <TabList value="home" onChange={() => {}} onKeyDown={onKeyDown}>
498
+ <Tab value="home" label="Home" />
499
+ <Tab value="settings" label="Settings" />
500
+ </TabList>,
501
+ );
502
+
503
+ const home = screen.getByRole('button', {name: 'Home'});
504
+ const settings = screen.getByRole('button', {name: 'Settings'});
505
+
506
+ home.focus();
507
+ await user.keyboard('{ArrowRight}');
508
+
509
+ expect(onKeyDown).toHaveBeenCalled();
510
+ expect(settings).toHaveFocus();
511
+ });
512
+
513
+ it('respects preventDefault from consumer onKeyDown', async () => {
514
+ const user = userEvent.setup();
515
+
516
+ render(
517
+ <TabList
518
+ value="home"
519
+ onChange={() => {}}
520
+ onKeyDown={e => e.preventDefault()}>
521
+ <Tab value="home" label="Home" />
522
+ <Tab value="settings" label="Settings" />
523
+ </TabList>,
524
+ );
525
+
526
+ const home = screen.getByRole('button', {name: 'Home'});
527
+ home.focus();
528
+ await user.keyboard('{ArrowRight}');
529
+
530
+ expect(home).toHaveFocus();
531
+ });
532
+ });
533
+
283
534
  describe('Tab polymorphic link', () => {
284
535
  it('renders custom component when href and as are provided', () => {
285
536
  render(
@@ -4,9 +4,11 @@
4
4
 
5
5
  /**
6
6
  * @file TabList.tsx
7
- * @input Uses React, StyleX, TabListContext
7
+ * @input Uses React, StyleX, TabListContext, useListFocus, useKeyboardHint
8
8
  * @output Exports TabList component and TabListProps type
9
- * @position Nav wrapper; provides TabListContext to Tab and TabMenu children
9
+ * @position Nav wrapper; provides TabListContext to Tab and TabMenu children.
10
+ * Owns roving-tabindex keyboard navigation (Arrow/Home/End) across the tab
11
+ * strip via the shared useListFocus hook so it is a single Tab stop.
10
12
  *
11
13
  * SYNC: When modified, update:
12
14
  * - /packages/core/src/TabList/TabList.doc.mjs
@@ -15,21 +17,27 @@
15
17
  * - /packages/cli/templates/blocks/components/TabList/ (showcase blocks)
16
18
  */
17
19
 
18
- import React, {useMemo, type ReactNode} from 'react';
20
+ import React, {useCallback, useMemo, type ReactNode} from 'react';
19
21
  import * as stylex from '@stylexjs/stylex';
20
22
  import {borderVars, colorVars, spacingVars} from '../theme/tokens.stylex';
21
23
  import type {BaseProps} from '../BaseProps';
22
24
  import {TabListContext} from './TabListContext';
23
- import type {TabListSize} from './TabListContext';
25
+ import type {TabListOrientation, TabListSize} from './TabListContext';
24
26
  import {useSize} from '../SizeContext/SizeContext';
25
- import {mergeProps} from '../utils';
27
+ import {mergeProps, mergeRefs} from '../utils';
28
+ import {useListFocus} from '../hooks/useListFocus';
29
+ import {useKeyboardHint} from '../hooks/useKeyboardHint';
26
30
  import {EDGE_COMP_ATTR} from '../Layout/edgeCompensation.stylex';
27
31
  import {themeProps} from '../utils/themeProps';
28
32
 
29
- export interface TabListProps extends Omit<
30
- BaseProps<HTMLElement>,
31
- 'onChange'
32
- > {
33
+ /**
34
+ * Selector matching the focusable stops in the tab strip: every Tab
35
+ * (`[data-tab-value]`) and every TabMenu trigger (`[data-tab-menu]`),
36
+ * in DOM order. Disabled stops are filtered out by the handler.
37
+ */
38
+ const TAB_STOP_SELECTOR = '[data-tab-value],[data-tab-menu]';
39
+
40
+ export interface TabListProps extends Omit<BaseProps<HTMLElement>, 'onChange'> {
33
41
  ref?: React.Ref<HTMLElement>;
34
42
  /**
35
43
  * The currently selected tab value.
@@ -57,6 +65,15 @@ export interface TabListProps extends Omit<
57
65
  * @default false
58
66
  */
59
67
  hasDivider?: boolean;
68
+ /**
69
+ * Orientation of the tab strip, controlling which arrow keys move
70
+ * focus between tabs.
71
+ * - `'horizontal'` (default): ArrowLeft / ArrowRight (ArrowUp / ArrowDown
72
+ * also work per the WAI-ARIA APG).
73
+ * - `'vertical'`: ArrowUp / ArrowDown (ArrowLeft / ArrowRight also work).
74
+ * @default 'horizontal'
75
+ */
76
+ orientation?: TabListOrientation;
60
77
  /**
61
78
  * Tab and TabMenu children.
62
79
  */
@@ -104,26 +121,99 @@ export function TabList({
104
121
  size: sizeProp,
105
122
  layout = 'hug',
106
123
  hasDivider = false,
124
+ orientation = 'horizontal',
107
125
  xstyle,
108
126
  className,
109
127
  style,
110
128
  children,
129
+ onKeyDown: onKeyDownProp,
130
+ onFocus: onFocusProp,
131
+ onBlur: onBlurProp,
132
+ 'aria-label': ariaLabel = 'Tabs',
133
+ 'aria-orientation': _ariaOrientation,
134
+ [EDGE_COMP_ATTR]: _edgeCompAttr,
111
135
  ...restProps
112
136
  }: TabListProps) {
113
137
  const size = useSize(sizeProp, 'md');
114
138
 
139
+ // Roving-tabindex keyboard navigation across the tab strip via the shared
140
+ // hook. `orientation: 'both'` accepts both arrow axes per the WAI-ARIA APG
141
+ // allowance for tab strips (ArrowRight/ArrowDown advance, ArrowLeft/ArrowUp
142
+ // retreat) regardless of the component's `orientation` prop, which only
143
+ // drives the keyboard hint badge (see useKeyboardHint below). We do not set
144
+ // `aria-orientation` on the `<nav>`: that attribute is invalid on the
145
+ // navigation role and triggers an axe `aria-allowed-attr` violation.
146
+ //
147
+ // `hasRovingTabIndex` makes the hook own the single tab stop: it stamps
148
+ // tabindex 0/-1, repairs the stop on mount and as stops mount/unmount or
149
+ // toggle disabled, and — via `handleFocus` on the nav — keeps the stop in
150
+ // sync after clicks or programmatic focus. Individual Tabs still render
151
+ // `tabIndex={isSelected ? 0 : -1}` (see Tab.tsx) as the initial source of
152
+ // truth; the hook's repair preserves an existing tab stop and only promotes
153
+ // the first enabled stop when none is tabbable.
154
+ const {listRef, handleKeyDown, handleFocus} = useListFocus<HTMLElement>({
155
+ itemSelector: TAB_STOP_SELECTOR,
156
+ orientation: 'both',
157
+ hasRovingTabIndex: true,
158
+ });
159
+
160
+ const {
161
+ hintElement,
162
+ onKeyDown: onHintKeyDown,
163
+ onFocus: onHintFocus,
164
+ onBlur: onHintBlur,
165
+ } = useKeyboardHint({orientation});
166
+
115
167
  const contextValue = useMemo(
116
168
  () => ({value, onChange, size, layout}),
117
169
  [value, onChange, size, layout],
118
170
  );
119
171
 
172
+ const handleRootKeyDown = useCallback(
173
+ (e: React.KeyboardEvent<HTMLElement>) => {
174
+ onKeyDownProp?.(e);
175
+ if (e.defaultPrevented) {
176
+ return;
177
+ }
178
+ onHintKeyDown(e);
179
+ handleKeyDown(e);
180
+ },
181
+ [onKeyDownProp, onHintKeyDown, handleKeyDown],
182
+ );
183
+
184
+ const handleRootFocus = useCallback(
185
+ (e: React.FocusEvent<HTMLElement>) => {
186
+ onFocusProp?.(e);
187
+ if (e.defaultPrevented) {
188
+ return;
189
+ }
190
+ onHintFocus(e);
191
+ handleFocus(e);
192
+ },
193
+ [onFocusProp, onHintFocus, handleFocus],
194
+ );
195
+
196
+ const handleRootBlur = useCallback(
197
+ (e: React.FocusEvent<HTMLElement>) => {
198
+ onBlurProp?.(e);
199
+ if (e.defaultPrevented) {
200
+ return;
201
+ }
202
+ onHintBlur(e);
203
+ },
204
+ [onBlurProp, onHintBlur],
205
+ );
206
+
120
207
  return (
121
208
  <TabListContext value={contextValue}>
122
209
  <nav
123
- ref={ref}
124
- aria-label="Tabs"
125
- {...{[EDGE_COMP_ATTR]: ''}}
210
+ ref={mergeRefs(ref, listRef)}
126
211
  {...restProps}
212
+ aria-label={ariaLabel}
213
+ onKeyDown={handleRootKeyDown}
214
+ onFocus={handleRootFocus}
215
+ onBlur={handleRootBlur}
216
+ {...{[EDGE_COMP_ATTR]: ''}}
127
217
  {...mergeProps(
128
218
  themeProps('tab-list', {size}),
129
219
  stylex.props(
@@ -136,6 +226,7 @@ export function TabList({
136
226
  style,
137
227
  )}>
138
228
  {children}
229
+ {hintElement}
139
230
  </nav>
140
231
  </TabListContext>
141
232
  );
@@ -26,6 +26,12 @@ export type TabListSize = 'sm' | 'md' | 'lg';
26
26
  */
27
27
  export type TabListLayout = 'hug' | 'fill';
28
28
 
29
+ /**
30
+ * Orientation of the tab strip. Controls which arrow keys move focus
31
+ * between tabs and the reported `aria-orientation`.
32
+ */
33
+ export type TabListOrientation = 'horizontal' | 'vertical';
34
+
29
35
  /**
30
36
  * Context for communicating value/onChange/size/layout from TabList to children.
31
37
  */
@@ -36,9 +42,7 @@ export interface TabListContextValue {
36
42
  layout: TabListLayout;
37
43
  }
38
44
 
39
- export const TabListContext = createContext<TabListContextValue | null>(
40
- null,
41
- );
45
+ export const TabListContext = createContext<TabListContextValue | null>(null);
42
46
  TabListContext.displayName = 'TabListContext';
43
47
 
44
48
  /**
@@ -267,6 +267,9 @@ export function TabMenu({
267
267
  hasLightDismiss: true,
268
268
  hasCloseButton: false,
269
269
  hasAutoFocus: false,
270
+ // The popup's own role="menu" is the exposed semantics; a modal dialog
271
+ // wrapper would announce an unnamed dialog around the menu.
272
+ role: 'none',
270
273
  });
271
274
 
272
275
  const {listRef, handleKeyDown: handleListKeyDown} =
@@ -306,6 +309,8 @@ export function TabMenu({
306
309
  aria-haspopup="menu"
307
310
  aria-expanded={popover.isOpen}
308
311
  aria-controls={menuId}
312
+ data-tab-menu=""
313
+ tabIndex={hasSelectedOption ? 0 : -1}
309
314
  onClick={handleToggle}
310
315
  {...mergeProps(
311
316
  themeProps('tab-menu'),
@@ -390,9 +395,7 @@ export function TabMenu({
390
395
  })}
391
396
  {option.label}
392
397
  </span>
393
- {isSelected && (
394
- <Icon icon="check" size="sm" color="accent" />
395
- )}
398
+ {isSelected && <Icon icon="check" size="sm" color="accent" />}
396
399
  </div>
397
400
  );
398
401
  })}