@astryxdesign/core 0.1.2 → 0.1.3

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,16 +4,17 @@
4
4
 
5
5
  /**
6
6
  * @file useGridFocus.ts
7
- * @input Uses React useCallback, useRef
7
+ * @input Uses React useCallback, useRef, useIsomorphicLayoutEffect
8
8
  * @output Exports useGridFocus hook for grid keyboard navigation
9
9
  * @position Core hook; used by Calendar for date grid navigation
10
10
  *
11
11
  * SYNC: When modified, update:
12
12
  * - /packages/core/src/hooks/index.ts
13
- * - /packages/core/src/hooks/useGridFocus.test.ts
13
+ * - /packages/core/src/hooks/useGridFocus.test.tsx
14
14
  */
15
15
 
16
16
  import {useCallback, useRef} from 'react';
17
+ import {useIsomorphicLayoutEffect} from './useIsomorphicLayoutEffect';
17
18
 
18
19
  /**
19
20
  * Configuration for grid focus behavior
@@ -26,11 +27,38 @@ export interface UseGridFocusOptions {
26
27
  columns: number;
27
28
 
28
29
  /**
29
- * Selector for focusable cells within the grid.
30
+ * Selector for cells within the grid. This should match ALL grid cell
31
+ * positions in DOM order (including disabled/empty ones) so the hook can
32
+ * preserve the true grid geometry when computing rows and columns.
33
+ *
34
+ * Use {@link UseGridFocusOptions.isCellFocusable} to distinguish cells that
35
+ * can receive focus from those that cannot (disabled or empty). Navigation
36
+ * moves to a target row/column and, if that cell is not focusable, continues
37
+ * in the same direction to the next focusable cell — the geometry is never
38
+ * collapsed to only-focusable cells.
39
+ *
30
40
  * @default 'button:not([disabled]), [tabindex]:not([tabindex="-1"])'
31
41
  */
32
42
  cellSelector?: string;
33
43
 
44
+ /**
45
+ * Predicate determining whether a cell matched by {@link cellSelector} can
46
+ * receive focus. When omitted, every matched cell is considered focusable
47
+ * (backwards-compatible behavior).
48
+ *
49
+ * @param cell A cell element matched by `cellSelector`.
50
+ */
51
+ isCellFocusable?: (cell: HTMLElement) => boolean;
52
+
53
+ /**
54
+ * Resolves the element to focus for a given cell. Useful when the cell is a
55
+ * wrapper element (e.g. a `role="gridcell"` div) whose focusable content is a
56
+ * descendant (e.g. a `<button>`). When omitted, the cell itself is focused.
57
+ *
58
+ * @param cell A cell element matched by `cellSelector`.
59
+ */
60
+ getFocusTarget?: (cell: HTMLElement) => HTMLElement | null;
61
+
34
62
  /**
35
63
  * Callback when navigation would go before the first cell.
36
64
  * Useful for navigating to previous month in calendars.
@@ -56,6 +84,34 @@ export interface UseGridFocusOptions {
56
84
  * Callback for Page Down key (e.g., next month).
57
85
  */
58
86
  onPageDown?: () => void;
87
+
88
+ /**
89
+ * Whether the grid is in a right-to-left context. When true, ArrowLeft and
90
+ * ArrowRight are swapped so horizontal navigation follows visual direction.
91
+ * @default false
92
+ */
93
+ isRtl?: boolean;
94
+
95
+ /**
96
+ * Roving-tabindex ownership. When true, the hook manages a single tab stop
97
+ * across the grid: exactly one focusable cell carries `tabindex="0"` and the
98
+ * rest `tabindex="-1"`. The tab stop is stamped on mount and repaired
99
+ * whenever cells mount/unmount or toggle focusable, and moves with arrow
100
+ * navigation. Attach the returned {@link UseGridFocusReturn.handleFocus} to
101
+ * the container's `onFocus` to keep the stop in sync after clicks or
102
+ * programmatic focus.
103
+ *
104
+ * The tab stop is stamped on the resolved focus target (see
105
+ * {@link UseGridFocusOptions.getFocusTarget}), not the cell wrapper, so it
106
+ * works when the focusable element is a descendant of the cell. An existing
107
+ * `tabindex="0"` on a focus target is honored, letting the caller seed which
108
+ * cell is initially tabbable.
109
+ *
110
+ * When false (the default), the hook only *moves* focus (`.focus()`) and
111
+ * never touches `tabindex` — the caller owns tab-stop management.
112
+ * @default false
113
+ */
114
+ hasRovingTabIndex?: boolean;
59
115
  }
60
116
 
61
117
  /**
@@ -72,6 +128,13 @@ export interface UseGridFocusReturn<T extends HTMLElement = HTMLElement> {
72
128
  */
73
129
  handleKeyDown: (e: React.KeyboardEvent) => void;
74
130
 
131
+ /**
132
+ * Focus handler to attach to the container's `onFocus`. Keeps the roving tab
133
+ * stop in sync when `hasRovingTabIndex` is enabled; a no-op otherwise, so it
134
+ * is always safe to attach.
135
+ */
136
+ handleFocus: (e: React.FocusEvent) => void;
137
+
75
138
  /**
76
139
  * Focus a specific cell by index.
77
140
  */
@@ -99,6 +162,18 @@ export interface UseGridFocusReturn<T extends HTMLElement = HTMLElement> {
99
162
  * - Ctrl+End: Move to last cell in grid
100
163
  * - Page Up/Down: Custom callbacks (e.g., month navigation)
101
164
  *
165
+ * The hook enumerates ALL cells matched by `cellSelector` in DOM order and
166
+ * computes row/column over that full set, preserving the true grid geometry
167
+ * even when some cells are disabled or empty. When a move lands on a
168
+ * non-focusable cell (per `isCellFocusable`), it continues in the same
169
+ * direction to the next focusable cell.
170
+ *
171
+ * By default the hook only *moves* focus and leaves `tabindex` management to
172
+ * the caller. Opt into {@link UseGridFocusOptions.hasRovingTabIndex} for a hook
173
+ * that owns a single tab stop (roving tabindex) across the grid — stamping and
174
+ * repairing it as cells mount/unmount or toggle focusable, and moving it with
175
+ * arrow navigation.
176
+ *
102
177
  * @example
103
178
  * ```
104
179
  * const {gridRef, handleKeyDown} = useGridFocus({
@@ -111,6 +186,24 @@ export interface UseGridFocusReturn<T extends HTMLElement = HTMLElement> {
111
186
  * {cells.map(cell => <button role="gridcell">{cell}</button>)}
112
187
  * </div>
113
188
  * ```
189
+ *
190
+ * Roving-tabindex grid (e.g. a date picker where the cell is a wrapper around
191
+ * a focusable button):
192
+ *
193
+ * @example
194
+ * ```
195
+ * const {gridRef, handleKeyDown, handleFocus} = useGridFocus({
196
+ * columns: 7,
197
+ * cellSelector: '[role="gridcell"]',
198
+ * isCellFocusable: cell => cell.querySelector('button:not([disabled])') != null,
199
+ * getFocusTarget: cell => cell.querySelector('button'),
200
+ * hasRovingTabIndex: true,
201
+ * });
202
+ *
203
+ * <div ref={gridRef} role="grid" onKeyDown={handleKeyDown} onFocus={handleFocus}>
204
+ * {cells}
205
+ * </div>
206
+ * ```
114
207
  */
115
208
  export function useGridFocus<T extends HTMLElement = HTMLElement>(
116
209
  options: UseGridFocusOptions,
@@ -118,16 +211,21 @@ export function useGridFocus<T extends HTMLElement = HTMLElement>(
118
211
  const {
119
212
  columns,
120
213
  cellSelector = 'button:not([disabled]), [tabindex]:not([tabindex="-1"])',
214
+ isCellFocusable,
215
+ getFocusTarget,
121
216
  onNavigateBefore,
122
217
  onNavigateAfter,
123
218
  onPageUp,
124
219
  onPageDown,
220
+ isRtl = false,
221
+ hasRovingTabIndex = false,
125
222
  } = options;
126
223
 
127
224
  const gridRef = useRef<T>(null);
128
225
 
129
226
  /**
130
- * Get all focusable cells in the grid.
227
+ * Get all cells in the grid, in DOM order. This includes disabled and empty
228
+ * cells so the true grid geometry is preserved.
131
229
  */
132
230
  const getCells = useCallback((): HTMLElement[] => {
133
231
  if (!gridRef.current) {
@@ -139,41 +237,159 @@ export function useGridFocus<T extends HTMLElement = HTMLElement>(
139
237
  }, [cellSelector]);
140
238
 
141
239
  /**
142
- * Get the currently focused cell index.
240
+ * Whether a cell can receive focus.
143
241
  */
144
- const getCurrentIndex = useCallback((): number => {
145
- const cells = getCells();
146
- const active = document.activeElement;
147
- return cells.findIndex(cell => cell === active || cell.contains(active));
148
- }, [getCells]);
242
+ const cellFocusable = useCallback(
243
+ (cell: HTMLElement | undefined): boolean => {
244
+ if (!cell) {
245
+ return false;
246
+ }
247
+ return isCellFocusable ? isCellFocusable(cell) : true;
248
+ },
249
+ [isCellFocusable],
250
+ );
149
251
 
150
252
  /**
151
- * Focus a cell by index, clamping to valid range.
152
- * Returns true if focus was successful, false if navigation callback was triggered.
253
+ * Resolve the element that should actually receive focus for a cell.
153
254
  */
154
- const focusCellInternal = useCallback(
155
- (index: number, currentColumn: number, offset: number) => {
156
- const cells = getCells();
157
- if (cells.length === 0) {
158
- return;
255
+ const resolveFocusTarget = useCallback(
256
+ (cell: HTMLElement | undefined): HTMLElement | null => {
257
+ if (!cell) {
258
+ return null;
159
259
  }
260
+ return getFocusTarget ? getFocusTarget(cell) : cell;
261
+ },
262
+ [getFocusTarget],
263
+ );
264
+
265
+ // --- Roving tabindex ownership (opt-in via `hasRovingTabIndex`) -------------
266
+
267
+ /**
268
+ * Set `tabindex` on an element, but only when it differs (avoids redundant
269
+ * DOM writes). Uses setAttribute so the value reflects even for elements
270
+ * (like `<button>`) whose default tabIndex is already 0.
271
+ */
272
+ const setTabIndex = useCallback((el: HTMLElement, value: 0 | -1) => {
273
+ if (el.getAttribute('tabindex') !== String(value)) {
274
+ el.setAttribute('tabindex', String(value));
275
+ }
276
+ }, []);
277
+
278
+ /**
279
+ * The resolved focus targets of all focusable cells, in DOM order. These are
280
+ * the elements that carry the roving tab stop (a descendant of each cell when
281
+ * `getFocusTarget` is provided, otherwise the cell itself).
282
+ */
283
+ const getFocusTargets = useCallback((): HTMLElement[] => {
284
+ return getCells()
285
+ .filter(cell => cellFocusable(cell))
286
+ .map(cell => resolveFocusTarget(cell))
287
+ .filter((el): el is HTMLElement => el !== null);
288
+ }, [getCells, cellFocusable, resolveFocusTarget]);
289
+
290
+ /**
291
+ * Stamp the roving tab stop: exactly one focus target is tabbable (0), the
292
+ * rest are -1. Prefer keeping the currently-tabbable target if it is still
293
+ * focusable; otherwise promote the first focusable one (tab-stop repair).
294
+ */
295
+ const syncTabStops = useCallback(() => {
296
+ const targets = getFocusTargets();
297
+ if (targets.length === 0) {
298
+ return;
299
+ }
300
+ const current = targets.find(el => el.getAttribute('tabindex') === '0');
301
+ const tabbable = current ?? targets[0];
302
+ for (const el of targets) {
303
+ setTabIndex(el, el === tabbable ? 0 : -1);
304
+ }
305
+ }, [getFocusTargets, setTabIndex]);
306
+
307
+ // Keep the tab stop valid across renders (cells added/removed, focusability
308
+ // toggled). Runs after every commit but only when roving tabindex is on.
309
+ useIsomorphicLayoutEffect(() => {
310
+ if (hasRovingTabIndex) {
311
+ syncTabStops();
312
+ }
313
+ });
160
314
 
161
- if (index < 0) {
162
- onNavigateBefore?.(currentColumn, offset);
315
+ /**
316
+ * Move the roving tab stop so `target` becomes the sole tabbable focus
317
+ * target, then focus it. No-op on tab stops unless roving tabindex is on.
318
+ */
319
+ const focusTarget = useCallback(
320
+ (target: HTMLElement | null) => {
321
+ if (!target) {
163
322
  return;
164
323
  }
165
- if (index >= cells.length) {
166
- onNavigateAfter?.(currentColumn, offset);
167
- return;
324
+ if (hasRovingTabIndex) {
325
+ for (const el of getFocusTargets()) {
326
+ setTabIndex(el, el === target ? 0 : -1);
327
+ }
168
328
  }
329
+ target.focus();
330
+ },
331
+ [hasRovingTabIndex, getFocusTargets, setTabIndex],
332
+ );
169
333
 
170
- cells[index]?.focus();
334
+ /**
335
+ * Focus a cell by resolving and focusing its target, moving the roving tab
336
+ * stop when enabled.
337
+ */
338
+ const focusCellWithStop = useCallback(
339
+ (cell: HTMLElement | undefined): boolean => {
340
+ const target = resolveFocusTarget(cell);
341
+ if (!target) {
342
+ return false;
343
+ }
344
+ focusTarget(target);
345
+ return true;
171
346
  },
172
- [getCells, onNavigateBefore, onNavigateAfter],
347
+ [resolveFocusTarget, focusTarget],
173
348
  );
174
349
 
175
350
  /**
176
- * Public focusCell that doesn't trigger navigation callbacks
351
+ * Keep the roving stop pointing at whatever ended up focused (e.g. a click
352
+ * or programmatic focus) so the next Tab behaves correctly. No-op unless
353
+ * roving tabindex is enabled.
354
+ */
355
+ const handleFocus = useCallback(() => {
356
+ if (hasRovingTabIndex) {
357
+ syncTabStops();
358
+ }
359
+ }, [hasRovingTabIndex, syncTabStops]);
360
+
361
+ /**
362
+ * Get the currently focused cell index within the full cell set.
363
+ */
364
+ const getCurrentIndex = useCallback((): number => {
365
+ const cells = getCells();
366
+ const active = document.activeElement;
367
+ return cells.findIndex(cell => cell === active || cell.contains(active));
368
+ }, [getCells]);
369
+
370
+ /**
371
+ * Find the next focusable cell starting at `startIndex`, moving by `step`.
372
+ * Returns the index of the focusable cell, or -1 if none exists in range
373
+ * (i.e. we ran off the start/end of the grid).
374
+ */
375
+ const findFocusableInDirection = useCallback(
376
+ (cells: HTMLElement[], startIndex: number, step: number): number => {
377
+ let index = startIndex;
378
+ while (index >= 0 && index < cells.length) {
379
+ if (cellFocusable(cells[index])) {
380
+ return index;
381
+ }
382
+ index += step;
383
+ }
384
+ return -1;
385
+ },
386
+ [cellFocusable],
387
+ );
388
+
389
+ /**
390
+ * Focus a cell by index, clamping to valid range. Skips non-focusable cells
391
+ * toward the nearest edge. Used by public focus helpers (no navigation
392
+ * callbacks).
177
393
  */
178
394
  const focusCell = useCallback(
179
395
  (index: number) => {
@@ -182,104 +398,162 @@ export function useGridFocus<T extends HTMLElement = HTMLElement>(
182
398
  return;
183
399
  }
184
400
  const clampedIndex = Math.max(0, Math.min(index, cells.length - 1));
185
- cells[clampedIndex]?.focus();
401
+ // Prefer the requested cell; otherwise search forward, then backward.
402
+ let target = findFocusableInDirection(cells, clampedIndex, 1);
403
+ if (target === -1) {
404
+ target = findFocusableInDirection(cells, clampedIndex, -1);
405
+ }
406
+ if (target !== -1) {
407
+ focusCellWithStop(cells[target]);
408
+ }
186
409
  },
187
- [getCells],
410
+ [getCells, findFocusableInDirection, focusCellWithStop],
188
411
  );
189
412
 
190
413
  /**
191
- * Focus the first cell.
414
+ * Focus the first focusable cell.
192
415
  */
193
416
  const focusFirst = useCallback(() => {
194
417
  const cells = getCells();
195
- cells[0]?.focus();
196
- }, [getCells]);
418
+ const index = findFocusableInDirection(cells, 0, 1);
419
+ if (index !== -1) {
420
+ focusCellWithStop(cells[index]);
421
+ }
422
+ }, [getCells, findFocusableInDirection, focusCellWithStop]);
197
423
 
198
424
  /**
199
- * Focus the last cell.
425
+ * Focus the last focusable cell.
200
426
  */
201
427
  const focusLast = useCallback(() => {
202
428
  const cells = getCells();
203
- cells[cells.length - 1]?.focus();
204
- }, [getCells]);
429
+ const index = findFocusableInDirection(cells, cells.length - 1, -1);
430
+ if (index !== -1) {
431
+ focusCellWithStop(cells[index]);
432
+ }
433
+ }, [getCells, findFocusableInDirection, focusCellWithStop]);
205
434
 
206
435
  /**
207
436
  * Handle keyboard navigation.
208
437
  */
209
438
  const handleKeyDown = useCallback(
210
439
  (e: React.KeyboardEvent) => {
440
+ const cells = getCells();
441
+ if (cells.length === 0) {
442
+ return;
443
+ }
444
+
211
445
  const currentIndex = getCurrentIndex();
212
446
  if (currentIndex === -1) {
213
447
  return;
214
448
  }
215
449
 
216
- const cells = getCells();
217
450
  const currentRow = Math.floor(currentIndex / columns);
218
451
  const currentCol = currentIndex % columns;
219
452
  const totalRows = Math.ceil(cells.length / columns);
220
453
 
221
454
  let handled = true;
222
455
 
223
- switch (e.key) {
224
- case 'ArrowRight':
225
- // Horizontal navigation: offset = 1
226
- focusCellInternal(currentIndex + 1, (currentCol + 1) % columns, 1);
456
+ // In RTL, ArrowLeft/ArrowRight are swapped so horizontal navigation
457
+ // follows visual direction. Vertical keys (Up/Down) are unaffected.
458
+ let key = e.key;
459
+ if (isRtl) {
460
+ if (key === 'ArrowLeft') {
461
+ key = 'ArrowRight';
462
+ } else if (key === 'ArrowRight') {
463
+ key = 'ArrowLeft';
464
+ }
465
+ }
466
+
467
+ switch (key) {
468
+ case 'ArrowRight': {
469
+ // Move right, skipping non-focusable cells in the same direction.
470
+ const target = findFocusableInDirection(cells, currentIndex + 1, 1);
471
+ if (target !== -1) {
472
+ focusCellWithStop(cells[target]);
473
+ } else {
474
+ onNavigateAfter?.((currentCol + 1) % columns, 1);
475
+ }
227
476
  break;
477
+ }
228
478
 
229
- case 'ArrowLeft':
230
- // Horizontal navigation: offset = 1
231
- focusCellInternal(
232
- currentIndex - 1,
233
- currentCol === 0 ? columns - 1 : currentCol - 1,
234
- 1,
235
- );
479
+ case 'ArrowLeft': {
480
+ const target = findFocusableInDirection(cells, currentIndex - 1, -1);
481
+ if (target !== -1) {
482
+ focusCellWithStop(cells[target]);
483
+ } else {
484
+ onNavigateBefore?.(
485
+ currentCol === 0 ? columns - 1 : currentCol - 1,
486
+ 1,
487
+ );
488
+ }
236
489
  break;
490
+ }
237
491
 
238
- case 'ArrowDown':
239
- // Vertical navigation: offset = columns (7 for calendar)
492
+ case 'ArrowDown': {
240
493
  if (currentRow < totalRows - 1) {
241
- const nextIndex = currentIndex + columns;
242
- if (nextIndex < cells.length) {
243
- focusCellInternal(nextIndex, currentCol, columns);
494
+ // Move to the same column one row down, then continue downward
495
+ // (by whole rows) to the next focusable cell in that column.
496
+ const target = findFocusableInDirection(
497
+ cells,
498
+ currentIndex + columns,
499
+ columns,
500
+ );
501
+ if (target !== -1) {
502
+ focusCellWithStop(cells[target]);
244
503
  } else {
245
- // Last row might be partial, focus last cell
246
- focusCellInternal(cells.length - 1, currentCol, columns);
504
+ onNavigateAfter?.(currentCol, columns);
247
505
  }
248
506
  } else {
249
507
  onNavigateAfter?.(currentCol, columns);
250
508
  }
251
509
  break;
510
+ }
252
511
 
253
- case 'ArrowUp':
254
- // Vertical navigation: offset = columns (7 for calendar)
512
+ case 'ArrowUp': {
255
513
  if (currentRow > 0) {
256
- focusCellInternal(currentIndex - columns, currentCol, columns);
514
+ const target = findFocusableInDirection(
515
+ cells,
516
+ currentIndex - columns,
517
+ -columns,
518
+ );
519
+ if (target !== -1) {
520
+ focusCellWithStop(cells[target]);
521
+ } else {
522
+ onNavigateBefore?.(currentCol, columns);
523
+ }
257
524
  } else {
258
525
  onNavigateBefore?.(currentCol, columns);
259
526
  }
260
527
  break;
528
+ }
261
529
 
262
530
  case 'Home':
263
531
  if (e.ctrlKey || e.metaKey) {
264
- // Ctrl+Home: first cell in grid
532
+ // Ctrl+Home: first focusable cell in grid
265
533
  focusFirst();
266
534
  } else {
267
- // Home: first cell in current row
268
- focusCell(currentRow * columns);
535
+ // Home: first focusable cell in current row (searching rightward)
536
+ const rowStart = currentRow * columns;
537
+ const rowEnd = Math.min(rowStart + columns - 1, cells.length - 1);
538
+ const target = findFocusableInDirection(cells, rowStart, 1);
539
+ if (target !== -1 && target <= rowEnd) {
540
+ focusCellWithStop(cells[target]);
541
+ }
269
542
  }
270
543
  break;
271
544
 
272
545
  case 'End':
273
546
  if (e.ctrlKey || e.metaKey) {
274
- // Ctrl+End: last cell in grid
547
+ // Ctrl+End: last focusable cell in grid
275
548
  focusLast();
276
549
  } else {
277
- // End: last cell in current row
278
- const rowEnd = Math.min(
279
- (currentRow + 1) * columns - 1,
280
- cells.length - 1,
281
- );
282
- focusCell(rowEnd);
550
+ // End: last focusable cell in current row (searching leftward)
551
+ const rowStart = currentRow * columns;
552
+ const rowEnd = Math.min(rowStart + columns - 1, cells.length - 1);
553
+ const target = findFocusableInDirection(cells, rowEnd, -1);
554
+ if (target !== -1 && target >= rowStart) {
555
+ focusCellWithStop(cells[target]);
556
+ }
283
557
  }
284
558
  break;
285
559
 
@@ -302,12 +576,13 @@ export function useGridFocus<T extends HTMLElement = HTMLElement>(
302
576
  },
303
577
  [
304
578
  columns,
305
- focusCell,
306
- focusCellInternal,
579
+ findFocusableInDirection,
580
+ focusCellWithStop,
307
581
  focusFirst,
308
582
  focusLast,
309
583
  getCells,
310
584
  getCurrentIndex,
585
+ isRtl,
311
586
  onNavigateAfter,
312
587
  onNavigateBefore,
313
588
  onPageDown,
@@ -318,6 +593,7 @@ export function useGridFocus<T extends HTMLElement = HTMLElement>(
318
593
  return {
319
594
  gridRef,
320
595
  handleKeyDown,
596
+ handleFocus,
321
597
  focusCell,
322
598
  focusFirst,
323
599
  focusLast,
@@ -0,0 +1,101 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /** @type {import('../docs-types').HookDoc} */
4
+ export const docs = {
5
+ name: 'useKeyboardHint',
6
+ displayName: 'useKeyboardHint',
7
+ keywords: ['keyboard', 'hint', 'arrow', 'navigation', 'roving', 'tabindex', 'focus', 'discoverability', 'toolbar', 'tabs', 'segmented', 'affordance', 'a11y'],
8
+ params: [
9
+ {
10
+ name: 'options',
11
+ type: 'UseKeyboardHintOptions',
12
+ description: 'Configuration object for the keyboard hint.',
13
+ required: false,
14
+ },
15
+ {
16
+ name: 'options.orientation',
17
+ type: "'horizontal' | 'vertical' | 'both'",
18
+ description: 'Which arrow-key axis the composite navigates, controlling which arrow icons the hint shows (← → for horizontal, ↑ ↓ for vertical, all four for both).',
19
+ default: "'horizontal'",
20
+ required: false,
21
+ },
22
+ {
23
+ name: 'options.dismissAfterMs',
24
+ type: 'number',
25
+ description: 'Milliseconds before the hint auto-dismisses after appearing.',
26
+ default: '3000',
27
+ required: false,
28
+ },
29
+ {
30
+ name: 'options.isEnabled',
31
+ type: 'boolean',
32
+ description: 'Whether the hint is enabled. Set to false to suppress it for a specific instance (e.g. a disabled or read-only widget).',
33
+ default: 'true',
34
+ required: false,
35
+ },
36
+ ],
37
+ returns: [
38
+ {
39
+ name: 'hintElement',
40
+ type: 'ReactNode',
41
+ description: 'The popover hint element to render inside the composite container (as the last child). Portals to the top layer via popover="manual", renders arrow keys with Kbd, and manages its own visibility — render it unconditionally.',
42
+ },
43
+ {
44
+ name: 'onFocus',
45
+ type: '(e: React.FocusEvent) => void',
46
+ description: 'Attach to the container onFocus. Shows the hint on the first keyboard-focus (:focus-visible) entry from outside the composite.',
47
+ },
48
+ {
49
+ name: 'onBlur',
50
+ type: '(e: React.FocusEvent) => void',
51
+ description: 'Attach to the container onBlur. Hides the hint when focus leaves the composite entirely, and re-anchors when focus moves within.',
52
+ },
53
+ {
54
+ name: 'onKeyDown',
55
+ type: '(e: React.KeyboardEvent) => void',
56
+ description: 'Attach to the container onKeyDown. Dismisses the hint on the first arrow press (the user has discovered the interaction). Never prevents default or stops propagation.',
57
+ },
58
+ ],
59
+ usage: {
60
+ description:
61
+ 'Shows an ephemeral "← → to navigate" hint anchored to the focused item the first time a roving-tabindex composite (Toolbar, TabList, SegmentedControl, etc.) receives keyboard focus. It teaches sighted keyboard users that arrow keys move within the group. The hint renders arrow keys with Kbd in the top layer (popover="manual") and is CSS-anchor-positioned to the focused element, so overflow containers never clip it. It auto-dismisses on the first arrow press, on timeout, or on blur, and does not re-show for that instance. Toolbar, TabList, and SegmentedControl wire this in automatically — reach for the hook directly only when building a custom roving-tabindex widget.',
62
+ bestPractices: [
63
+ { guidance: true, description: 'Compose the returned onFocus/onKeyDown with your existing focus handlers rather than replacing them — call onKeyDown first (it only dismisses, never prevents), then your navigation handler.' },
64
+ { guidance: true, description: 'Render hintElement as the last child of the composite container; it is position:fixed in the top layer and aria-hidden, so it never affects layout or the accessibility tree.' },
65
+ { guidance: true, description: 'Match orientation to the arrow keys your widget actually responds to so the hint shows the correct icons.' },
66
+ { guidance: false, description: 'Use for single controls or widgets without roving-tabindex navigation — the hint only makes sense where arrows move focus within a group.' },
67
+ ],
68
+ },
69
+ relatedComponents: ['Toolbar', 'TabList', 'SegmentedControl'],
70
+ relatedHooks: ['useListFocus', 'useGridFocus', 'useTreeFocus'],
71
+ importPath: '@astryxdesign/core/hooks',
72
+ category: 'focus',
73
+ };
74
+
75
+ /** @type {import('../docs-types').HookTranslationDoc} */
76
+ export const docsDense = {
77
+ description:
78
+ 'Ephemeral "← → to navigate" hint anchored to the focused item on first keyboard focus of a roving-tabindex composite (Toolbar/TabList/SegmentedControl). Teaches sighted keyboard users that arrows move within the group. Kbd-rendered arrows in a top-layer popover="manual", CSS-anchor-positioned (never clipped by overflow), aria-hidden. Auto-dismisses on first arrow press, timeout, or blur; never re-shows for that instance.',
79
+ paramDescriptions: {
80
+ options: 'config for the keyboard hint.',
81
+ 'options.orientation': "arrow axis: 'horizontal' (← →), 'vertical' (↑ ↓), 'both' (all four). Controls shown icons.",
82
+ 'options.dismissAfterMs': 'ms before auto-dismiss after appearing.',
83
+ 'options.isEnabled': 'whether the hint is enabled; false suppresses it (e.g. disabled/read-only widget).',
84
+ },
85
+ returnDescriptions: {
86
+ hintElement: 'popover hint to render as last child of the container. Kbd-rendered arrows, top-layer, self-managing — render unconditionally.',
87
+ onFocus: 'container onFocus: shows hint on first :focus-visible entry from outside.',
88
+ onBlur: 'container onBlur: hides on leaving the composite; re-anchors on internal moves.',
89
+ onKeyDown: 'container onKeyDown: dismisses on first arrow press. Never prevents default.',
90
+ },
91
+ usage: {
92
+ description:
93
+ 'Ephemeral "← → to navigate" hint on first keyboard focus of a roving-tabindex composite. Top-layer anchored popover, aria-hidden. Auto-dismisses on first arrow press, timeout, or blur; no re-show. Toolbar/TabList/SegmentedControl wire it in automatically.',
94
+ bestPractices: [
95
+ { guidance: true, description: 'Compose returned onFocus/onKeyDown with existing handlers (call onKeyDown first — it only dismisses).' },
96
+ { guidance: true, description: 'Render hintElement as last child; top-layer + aria-hidden, no layout/a11y impact.' },
97
+ { guidance: true, description: 'Match orientation to the arrows the widget responds to.' },
98
+ { guidance: false, description: 'Use for single controls / non-roving widgets.' },
99
+ ],
100
+ },
101
+ };