@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,15 +4,25 @@
4
4
 
5
5
  /**
6
6
  * @file useListFocus.ts
7
- * @input Uses React useCallback, useRef
7
+ * @input Uses React useCallback, useRef, useIsomorphicLayoutEffect
8
8
  * @output Exports useListFocus hook for linear list keyboard navigation
9
- * @position Core hook; used by TabMenu for dropdown menu navigation, Toolbar for roving tabindex
9
+ * @position Core hook; used by TabMenu for dropdown menu navigation, Toolbar
10
+ * for roving tabindex, ButtonGroup, ContextMenu, NavHeadingMenu, and more.
10
11
  *
11
12
  * SYNC: When modified, update:
12
13
  * - /packages/core/src/hooks/index.ts
13
14
  */
14
15
 
15
16
  import {useCallback, useRef} from 'react';
17
+ import {useIsomorphicLayoutEffect} from './useIsomorphicLayoutEffect';
18
+
19
+ /**
20
+ * Navigation orientation for a linear list.
21
+ * - `'horizontal'`: ArrowLeft/ArrowRight move between items.
22
+ * - `'vertical'`: ArrowUp/ArrowDown move between items.
23
+ * - `'both'`: all four arrows move between items (in linear DOM order).
24
+ */
25
+ export type ListFocusOrientation = 'horizontal' | 'vertical' | 'both';
16
26
 
17
27
  /**
18
28
  * Configuration for list focus behavior
@@ -36,11 +46,51 @@ export interface UseListFocusOptions {
36
46
  onEscape?: () => void;
37
47
 
38
48
  /**
39
- * Navigation orientation. 'horizontal' uses ArrowLeft/ArrowRight,
40
- * 'vertical' uses ArrowUp/ArrowDown.
49
+ * Navigation orientation. `'horizontal'` uses ArrowLeft/ArrowRight,
50
+ * `'vertical'` uses ArrowUp/ArrowDown, `'both'` accepts all four arrows.
41
51
  * @default 'vertical'
42
52
  */
43
- orientation?: 'horizontal' | 'vertical';
53
+ orientation?: ListFocusOrientation;
54
+
55
+ /**
56
+ * Whether Home/End jump to the first/last enabled item.
57
+ * @default true
58
+ */
59
+ hasHomeEnd?: boolean;
60
+
61
+ /**
62
+ * Whether the list is in a right-to-left context. When true, ArrowLeft and
63
+ * ArrowRight are swapped so horizontal navigation follows visual direction.
64
+ * Only affects horizontal (`'horizontal'`/`'both'`) navigation.
65
+ * @default false
66
+ */
67
+ isRtl?: boolean;
68
+
69
+ /**
70
+ * Roving-tabindex ownership. When true, the hook manages a single tab stop
71
+ * across the items: exactly one enabled item carries `tabindex="0"` and the
72
+ * rest `tabindex="-1"`. The tab stop is stamped on mount and repaired
73
+ * whenever items mount/unmount or toggle disabled, and moves with arrow
74
+ * navigation. Attach the returned {@link UseListFocusReturn.handleFocus} to
75
+ * the container's `onFocus` to keep the stop in sync after clicks or
76
+ * programmatic focus.
77
+ *
78
+ * When false (the default), the hook only *moves* focus (`.focus()`) and
79
+ * never touches `tabindex` — the caller owns tab-stop management.
80
+ * @default false
81
+ */
82
+ hasRovingTabIndex?: boolean;
83
+
84
+ /**
85
+ * When true, arrow keys are not stolen from a nested text input/textarea
86
+ * whose caret is not at the boundary in the direction of travel (or that has
87
+ * a non-collapsed selection), and are never stolen from a nested
88
+ * `contenteditable` (rich-text editor / chat composer). This preserves
89
+ * normal caret movement while the user is editing inline within the list
90
+ * (e.g. a toolbar search field or composer).
91
+ * @default false
92
+ */
93
+ hasCaretGuard?: boolean;
44
94
  }
45
95
 
46
96
  /**
@@ -57,20 +107,120 @@ export interface UseListFocusReturn<T extends HTMLElement = HTMLElement> {
57
107
  */
58
108
  handleKeyDown: (e: React.KeyboardEvent) => void;
59
109
 
110
+ /**
111
+ * Focus handler to attach to the container's `onFocus`. Keeps the roving tab
112
+ * stop in sync when `hasRovingTabIndex` is enabled; a no-op otherwise, so it is
113
+ * always safe to attach.
114
+ */
115
+ handleFocus: (e: React.FocusEvent) => void;
116
+
60
117
  /**
61
118
  * Focus a specific item by index.
62
119
  */
63
120
  focusItem: (index: number) => void;
64
121
 
65
122
  /**
66
- * Focus the first focusable item.
123
+ * Focus the first enabled item. Returns true when an item was focused.
67
124
  */
68
- focusFirst: () => void;
125
+ focusFirst: () => boolean;
69
126
 
70
127
  /**
71
- * Focus the last focusable item.
128
+ * Focus the last enabled item. Returns true when an item was focused.
72
129
  */
73
- focusLast: () => void;
130
+ focusLast: () => boolean;
131
+ }
132
+
133
+ // Text-editing inputs whose caret must not be hijacked by arrow navigation.
134
+ const TEXT_INPUT_TYPES = new Set([
135
+ 'text',
136
+ 'search',
137
+ 'url',
138
+ 'tel',
139
+ 'email',
140
+ 'password',
141
+ 'number',
142
+ ]);
143
+
144
+ /**
145
+ * The nearest `contenteditable` root for `el`, or null when `el` is not inside
146
+ * an editable region. Prefers the browser's `isContentEditable` property and
147
+ * falls back to the closest `[contenteditable]` ancestor whose value is not
148
+ * `"false"` (so environments without `isContentEditable` still work).
149
+ */
150
+ function getContentEditableRoot(el: HTMLElement): HTMLElement | null {
151
+ if (el.isContentEditable) {
152
+ return el;
153
+ }
154
+ const candidate = el.closest<HTMLElement>('[contenteditable]');
155
+ if (candidate && candidate.getAttribute('contenteditable') !== 'false') {
156
+ return candidate;
157
+ }
158
+ return null;
159
+ }
160
+
161
+ /**
162
+ * Whether an arrow/Home/End key should be left to the browser because the
163
+ * event target is a text-editing element whose caret is not yet at the boundary
164
+ * in the direction of travel (or a selection is present). Returns true when the
165
+ * list should NOT steal the key.
166
+ *
167
+ * Covers three editing surfaces:
168
+ * - `<textarea>` and text-type `<input>` — use `selectionStart`/`selectionEnd`
169
+ * to steal only at the boundary in the travel direction.
170
+ * - `contenteditable` (rich-text editor / chat composer) — precise caret
171
+ * boundary detection in an arbitrary editable subtree is fragile, so we err
172
+ * on the side of never hijacking an active editor: defer on every caret key
173
+ * whenever focus is inside a non-empty contenteditable. (An empty editable
174
+ * has nothing to caret through, so navigation may proceed.)
175
+ */
176
+ function shouldDeferToCaret(target: EventTarget | null, key: string): boolean {
177
+ if (!(target instanceof HTMLElement)) {
178
+ return false;
179
+ }
180
+
181
+ // contenteditable (covers an element with contenteditable="" / "true" and
182
+ // descendants that inherit it). `isContentEditable` is the reliable property
183
+ // in the browser; fall back to the nearest `contenteditable` ancestor for
184
+ // environments (e.g. jsdom) that don't implement the property.
185
+ const editableRoot = getContentEditableRoot(target);
186
+ if (editableRoot) {
187
+ const selection =
188
+ typeof window !== 'undefined' ? window.getSelection() : null;
189
+ // A non-collapsed selection means the user is selecting text — never steal.
190
+ if (selection && selection.rangeCount > 0 && !selection.isCollapsed) {
191
+ return true;
192
+ }
193
+ // Collapsed caret (or no selection API): defer whenever the editor has
194
+ // content, so arrow keys stay with the active rich-text editor.
195
+ return (editableRoot.textContent ?? '').length > 0;
196
+ }
197
+
198
+ const isTextarea = target.tagName === 'TEXTAREA';
199
+ const isTextInput =
200
+ target.tagName === 'INPUT' &&
201
+ TEXT_INPUT_TYPES.has((target as HTMLInputElement).type);
202
+ if (!isTextarea && !isTextInput) {
203
+ return false;
204
+ }
205
+ const input = target as HTMLInputElement | HTMLTextAreaElement;
206
+ const {selectionStart, selectionEnd, value} = input;
207
+ // A non-collapsed selection means the user is selecting text — never steal.
208
+ if (selectionStart !== selectionEnd) {
209
+ return true;
210
+ }
211
+ // number inputs report null selection; treat as "always defer" for caret keys.
212
+ if (selectionStart == null) {
213
+ return true;
214
+ }
215
+ if (key === 'ArrowLeft' || key === 'ArrowUp' || key === 'Home') {
216
+ // Defer unless the caret is at the very start.
217
+ return selectionStart > 0;
218
+ }
219
+ if (key === 'ArrowRight' || key === 'ArrowDown' || key === 'End') {
220
+ // Defer unless the caret is at the very end.
221
+ return selectionStart < value.length;
222
+ }
223
+ return false;
74
224
  }
75
225
 
76
226
  /**
@@ -83,6 +233,12 @@ export interface UseListFocusReturn<T extends HTMLElement = HTMLElement> {
83
233
  * - End: Move to last item
84
234
  * - Escape: Custom callback (e.g., close menu)
85
235
  *
236
+ * By default the hook only *moves* focus and leaves `tabindex` management to
237
+ * the caller. Opt into {@link UseListFocusOptions.hasRovingTabIndex} for a hook
238
+ * that owns a single tab stop (roving tabindex) across the items — stamping and
239
+ * repairing it as items mount/unmount or toggle disabled — for toolbars,
240
+ * segmented controls, tab strips, and similar composite widgets.
241
+ *
86
242
  * @example
87
243
  * ```
88
244
  * const {listRef, handleKeyDown} = useListFocus({
@@ -93,6 +249,22 @@ export interface UseListFocusReturn<T extends HTMLElement = HTMLElement> {
93
249
  * {items.map(item => <div role="menuitem" tabIndex={0}>{item}</div>)}
94
250
  * </div>
95
251
  * ```
252
+ *
253
+ * Roving-tabindex composite (e.g. a toolbar):
254
+ *
255
+ * @example
256
+ * ```
257
+ * const {listRef, handleKeyDown, handleFocus} = useListFocus<HTMLDivElement>({
258
+ * itemSelector: 'button, input, [tabindex]',
259
+ * orientation: 'horizontal',
260
+ * hasRovingTabIndex: true,
261
+ * hasCaretGuard: true,
262
+ * });
263
+ *
264
+ * <div ref={listRef} role="toolbar" onKeyDown={handleKeyDown} onFocus={handleFocus}>
265
+ * {children}
266
+ * </div>
267
+ * ```
96
268
  */
97
269
  export function useListFocus<T extends HTMLElement = HTMLElement>(
98
270
  options: UseListFocusOptions = {},
@@ -102,10 +274,27 @@ export function useListFocus<T extends HTMLElement = HTMLElement>(
102
274
  wrap = true,
103
275
  onEscape,
104
276
  orientation = 'vertical',
277
+ hasHomeEnd = true,
278
+ isRtl = false,
279
+ hasRovingTabIndex = false,
280
+ hasCaretGuard = false,
105
281
  } = options;
106
282
 
107
283
  const listRef = useRef<T>(null);
108
284
 
285
+ /**
286
+ * Whether an item is disabled and therefore cannot receive DOM focus.
287
+ * A `.focus()` call on such an element silently no-ops, so navigation must
288
+ * skip these to avoid freezing on a disabled item (menus-4, navigation-5).
289
+ */
290
+ const isItemDisabled = useCallback((el: HTMLElement): boolean => {
291
+ return (
292
+ el.getAttribute('aria-disabled') === 'true' ||
293
+ (el as HTMLButtonElement).disabled === true ||
294
+ el.hasAttribute('disabled')
295
+ );
296
+ }, []);
297
+
109
298
  /**
110
299
  * Get all focusable items in the list.
111
300
  */
@@ -118,6 +307,42 @@ export function useListFocus<T extends HTMLElement = HTMLElement>(
118
307
  );
119
308
  }, [itemSelector]);
120
309
 
310
+ /**
311
+ * Find the next enabled item index from `start`, moving by `step`, optionally
312
+ * wrapping. Returns -1 when no enabled item exists in range. Skipping
313
+ * disabled items here (rather than relying on the selector) keeps navigation
314
+ * from stalling on an item whose `.focus()` silently no-ops.
315
+ */
316
+ const findEnabledIndex = useCallback(
317
+ (
318
+ items: HTMLElement[],
319
+ start: number,
320
+ step: 1 | -1,
321
+ shouldWrap: boolean,
322
+ ): number => {
323
+ const count = items.length;
324
+ if (count === 0) {
325
+ return -1;
326
+ }
327
+ let index = start;
328
+ for (let i = 0; i < count; i++) {
329
+ if (index < 0 || index >= count) {
330
+ if (!shouldWrap) {
331
+ return -1;
332
+ }
333
+ index = (index + count) % count;
334
+ }
335
+ const item = items[index];
336
+ if (item && !isItemDisabled(item)) {
337
+ return index;
338
+ }
339
+ index += step;
340
+ }
341
+ return -1;
342
+ },
343
+ [isItemDisabled],
344
+ );
345
+
121
346
  /**
122
347
  * Get the currently focused item index.
123
348
  */
@@ -127,6 +352,65 @@ export function useListFocus<T extends HTMLElement = HTMLElement>(
127
352
  return items.findIndex(item => item === active || item.contains(active));
128
353
  }, [getItems]);
129
354
 
355
+ // --- Roving tabindex ownership (opt-in via `hasRovingTabIndex`) -------------
356
+
357
+ /**
358
+ * Set `tabindex` on an item, but only when it differs (avoids redundant DOM
359
+ * writes). Uses setAttribute so the value reflects even for elements (like
360
+ * `<button>`) whose default tabIndex is already 0.
361
+ */
362
+ const setTabIndex = useCallback((el: HTMLElement, value: 0 | -1) => {
363
+ if (el.getAttribute('tabindex') !== String(value)) {
364
+ el.setAttribute('tabindex', String(value));
365
+ }
366
+ }, []);
367
+
368
+ /**
369
+ * Stamp the roving tab stop: exactly one enabled item is tabbable (0), the
370
+ * rest are -1. Prefer keeping the currently-tabbable item if it is still
371
+ * enabled; otherwise promote the first enabled item (tab-stop repair).
372
+ */
373
+ const syncTabStops = useCallback(() => {
374
+ const items = getItems();
375
+ const enabled = items.filter(el => !isItemDisabled(el));
376
+ if (enabled.length === 0) {
377
+ return;
378
+ }
379
+ const current = enabled.find(el => el.getAttribute('tabindex') === '0');
380
+ const tabbable = current ?? enabled[0];
381
+ for (const el of items) {
382
+ setTabIndex(el, el === tabbable ? 0 : -1);
383
+ }
384
+ }, [getItems, isItemDisabled, setTabIndex]);
385
+
386
+ // Keep the tab stop valid across renders (items added/removed, disabled
387
+ // toggled). Runs after every commit but only when roving tabindex is on.
388
+ useIsomorphicLayoutEffect(() => {
389
+ if (hasRovingTabIndex) {
390
+ syncTabStops();
391
+ }
392
+ });
393
+
394
+ /**
395
+ * Move focus to `items[index]`. When roving tabindex is enabled, also move
396
+ * the tab stop so `index` becomes the sole tabbable item.
397
+ */
398
+ const focusIndex = useCallback(
399
+ (items: HTMLElement[], index: number) => {
400
+ const target = items[index];
401
+ if (!target) {
402
+ return;
403
+ }
404
+ if (hasRovingTabIndex) {
405
+ for (const el of items) {
406
+ setTabIndex(el, el === target ? 0 : -1);
407
+ }
408
+ }
409
+ target.focus();
410
+ },
411
+ [hasRovingTabIndex, setTabIndex],
412
+ );
413
+
130
414
  /**
131
415
  * Focus an item by index, clamping to valid range.
132
416
  */
@@ -137,82 +421,135 @@ export function useListFocus<T extends HTMLElement = HTMLElement>(
137
421
  return;
138
422
  }
139
423
  const clampedIndex = Math.max(0, Math.min(index, items.length - 1));
140
- items[clampedIndex]?.focus();
424
+ focusIndex(items, clampedIndex);
141
425
  },
142
- [getItems],
426
+ [getItems, focusIndex],
143
427
  );
144
428
 
145
429
  /**
146
- * Focus the first item.
430
+ * Focus the first enabled item. Returns true when an item was focused.
147
431
  */
148
- const focusFirst = useCallback(() => {
432
+ const focusFirst = useCallback((): boolean => {
149
433
  const items = getItems();
150
- items[0]?.focus();
151
- }, [getItems]);
434
+ const index = findEnabledIndex(items, 0, 1, false);
435
+ if (index !== -1) {
436
+ focusIndex(items, index);
437
+ return true;
438
+ }
439
+ return false;
440
+ }, [getItems, findEnabledIndex, focusIndex]);
152
441
 
153
442
  /**
154
- * Focus the last item.
443
+ * Focus the last enabled item. Returns true when an item was focused.
155
444
  */
156
- const focusLast = useCallback(() => {
445
+ const focusLast = useCallback((): boolean => {
157
446
  const items = getItems();
158
- items[items.length - 1]?.focus();
159
- }, [getItems]);
447
+ const index = findEnabledIndex(items, items.length - 1, -1, false);
448
+ if (index !== -1) {
449
+ focusIndex(items, index);
450
+ return true;
451
+ }
452
+ return false;
453
+ }, [getItems, findEnabledIndex, focusIndex]);
454
+
455
+ /**
456
+ * Keep the roving stop pointing at whatever ended up focused (e.g. a click
457
+ * or programmatic focus) so the next Tab behaves correctly. No-op unless
458
+ * roving tabindex is enabled.
459
+ */
460
+ const handleFocus = useCallback(() => {
461
+ if (hasRovingTabIndex) {
462
+ syncTabStops();
463
+ }
464
+ }, [hasRovingTabIndex, syncTabStops]);
160
465
 
161
466
  /**
162
467
  * Handle keyboard navigation.
163
468
  */
164
469
  const handleKeyDown = useCallback(
165
470
  (e: React.KeyboardEvent) => {
471
+ // Let browser/OS shortcut chords (Ctrl/Cmd/Alt + key) through untouched.
472
+ if (e.ctrlKey || e.metaKey || e.altKey) {
473
+ return;
474
+ }
475
+
476
+ // Escape is handled regardless of orientation. Preserve the historical
477
+ // behavior of always consuming Escape here (preventDefault) so consumers
478
+ // that relied on it are unaffected.
479
+ if (e.key === 'Escape') {
480
+ e.preventDefault();
481
+ onEscape?.();
482
+ return;
483
+ }
484
+
485
+ const horizontal = orientation === 'horizontal' || orientation === 'both';
486
+ const vertical = orientation === 'vertical' || orientation === 'both';
487
+
488
+ // Resolve which keys advance vs retreat, honoring RTL for horizontal.
489
+ const nextKeys: string[] = [];
490
+ const prevKeys: string[] = [];
491
+ if (horizontal) {
492
+ nextKeys.push(isRtl ? 'ArrowLeft' : 'ArrowRight');
493
+ prevKeys.push(isRtl ? 'ArrowRight' : 'ArrowLeft');
494
+ }
495
+ if (vertical) {
496
+ nextKeys.push('ArrowDown');
497
+ prevKeys.push('ArrowUp');
498
+ }
499
+
500
+ const isNext = nextKeys.includes(e.key);
501
+ const isPrev = prevKeys.includes(e.key);
502
+ const isHome = hasHomeEnd && e.key === 'Home';
503
+ const isEnd = hasHomeEnd && e.key === 'End';
504
+
505
+ if (!isNext && !isPrev && !isHome && !isEnd) {
506
+ return;
507
+ }
508
+
509
+ // Never hijack a caret key from a nested text input mid-line. Check both
510
+ // the React event target and the actual focused element (events may
511
+ // bubble up to the container).
512
+ if (
513
+ hasCaretGuard &&
514
+ (shouldDeferToCaret(e.target, e.key) ||
515
+ shouldDeferToCaret(document.activeElement, e.key))
516
+ ) {
517
+ return;
518
+ }
519
+
166
520
  const currentIndex = getCurrentIndex();
167
521
  const items = getItems();
168
- let handled = true;
169
-
170
- const nextKey = orientation === 'horizontal' ? 'ArrowRight' : 'ArrowDown';
171
- const prevKey = orientation === 'horizontal' ? 'ArrowLeft' : 'ArrowUp';
172
-
173
- switch (e.key) {
174
- case nextKey: {
175
- if (currentIndex === -1) {
176
- items[0]?.focus();
177
- } else if (currentIndex < items.length - 1) {
178
- items[currentIndex + 1]?.focus();
179
- } else if (wrap) {
180
- items[0]?.focus();
181
- }
182
- break;
522
+
523
+ if (isNext) {
524
+ const from = currentIndex === -1 ? 0 : currentIndex + 1;
525
+ const next = findEnabledIndex(items, from, 1, wrap);
526
+ if (next !== -1) {
527
+ focusIndex(items, next);
183
528
  }
184
- case prevKey: {
185
- if (currentIndex === -1) {
186
- items[items.length - 1]?.focus();
187
- } else if (currentIndex > 0) {
188
- items[currentIndex - 1]?.focus();
189
- } else if (wrap) {
190
- items[items.length - 1]?.focus();
191
- }
192
- break;
529
+ } else if (isPrev) {
530
+ const from = currentIndex === -1 ? items.length - 1 : currentIndex - 1;
531
+ const prev = findEnabledIndex(items, from, -1, wrap);
532
+ if (prev !== -1) {
533
+ focusIndex(items, prev);
193
534
  }
194
- case 'Home':
195
- focusFirst();
196
- break;
197
- case 'End':
198
- focusLast();
199
- break;
200
- case 'Escape':
201
- onEscape?.();
202
- break;
203
- default:
204
- handled = false;
535
+ } else if (isHome) {
536
+ focusFirst();
537
+ } else if (isEnd) {
538
+ focusLast();
205
539
  }
206
540
 
207
- if (handled) {
208
- e.preventDefault();
209
- }
541
+ e.preventDefault();
210
542
  },
211
543
  [
212
544
  getCurrentIndex,
213
545
  getItems,
214
546
  wrap,
215
547
  orientation,
548
+ isRtl,
549
+ hasHomeEnd,
550
+ hasCaretGuard,
551
+ findEnabledIndex,
552
+ focusIndex,
216
553
  focusFirst,
217
554
  focusLast,
218
555
  onEscape,
@@ -222,6 +559,7 @@ export function useListFocus<T extends HTMLElement = HTMLElement>(
222
559
  return {
223
560
  listRef,
224
561
  handleKeyDown,
562
+ handleFocus,
225
563
  focusItem,
226
564
  focusFirst,
227
565
  focusLast,
@@ -0,0 +1,134 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest';
4
+ import {renderHook, act} from '@testing-library/react';
5
+ import {useLongPress} from './useLongPress';
6
+
7
+ // Minimal React.TouchEvent stand-in carrying only what the hook reads.
8
+ function touchEvent(touches: {clientX: number; clientY: number}[]) {
9
+ return {touches} as unknown as React.TouchEvent;
10
+ }
11
+
12
+ describe('useLongPress', () => {
13
+ beforeEach(() => {
14
+ vi.useFakeTimers();
15
+ });
16
+
17
+ afterEach(() => {
18
+ vi.useRealTimers();
19
+ });
20
+
21
+ it('fires onLongPress with the start point after the delay', () => {
22
+ const onLongPress = vi.fn();
23
+ const {result} = renderHook(() => useLongPress({onLongPress}));
24
+
25
+ act(() => {
26
+ result.current.onTouchStart(touchEvent([{clientX: 10, clientY: 20}]));
27
+ });
28
+ expect(onLongPress).not.toHaveBeenCalled();
29
+
30
+ act(() => {
31
+ vi.advanceTimersByTime(500);
32
+ });
33
+ expect(onLongPress).toHaveBeenCalledTimes(1);
34
+ expect(onLongPress).toHaveBeenCalledWith({x: 10, y: 20});
35
+ });
36
+
37
+ it('respects a custom delayMs', () => {
38
+ const onLongPress = vi.fn();
39
+ const {result} = renderHook(() =>
40
+ useLongPress({onLongPress, delayMs: 1000}),
41
+ );
42
+
43
+ act(() => {
44
+ result.current.onTouchStart(touchEvent([{clientX: 1, clientY: 2}]));
45
+ vi.advanceTimersByTime(999);
46
+ });
47
+ expect(onLongPress).not.toHaveBeenCalled();
48
+
49
+ act(() => {
50
+ vi.advanceTimersByTime(1);
51
+ });
52
+ expect(onLongPress).toHaveBeenCalledTimes(1);
53
+ });
54
+
55
+ it('does nothing when disabled', () => {
56
+ const onLongPress = vi.fn();
57
+ const {result} = renderHook(() =>
58
+ useLongPress({onLongPress, disabled: true}),
59
+ );
60
+
61
+ act(() => {
62
+ result.current.onTouchStart(touchEvent([{clientX: 0, clientY: 0}]));
63
+ vi.advanceTimersByTime(500);
64
+ });
65
+ expect(onLongPress).not.toHaveBeenCalled();
66
+ });
67
+
68
+ it('ignores multi-touch starts', () => {
69
+ const onLongPress = vi.fn();
70
+ const {result} = renderHook(() => useLongPress({onLongPress}));
71
+
72
+ act(() => {
73
+ result.current.onTouchStart(
74
+ touchEvent([
75
+ {clientX: 0, clientY: 0},
76
+ {clientX: 5, clientY: 5},
77
+ ]),
78
+ );
79
+ vi.advanceTimersByTime(500);
80
+ });
81
+ expect(onLongPress).not.toHaveBeenCalled();
82
+ });
83
+
84
+ it('cancels when the finger moves past the threshold', () => {
85
+ const onLongPress = vi.fn();
86
+ const {result} = renderHook(() => useLongPress({onLongPress}));
87
+
88
+ act(() => {
89
+ result.current.onTouchStart(touchEvent([{clientX: 0, clientY: 0}]));
90
+ // Move past the default 10px threshold.
91
+ result.current.onTouchMove(touchEvent([{clientX: 15, clientY: 0}]));
92
+ vi.advanceTimersByTime(500);
93
+ });
94
+ expect(onLongPress).not.toHaveBeenCalled();
95
+ });
96
+
97
+ it('does not cancel for movement within the threshold', () => {
98
+ const onLongPress = vi.fn();
99
+ const {result} = renderHook(() => useLongPress({onLongPress}));
100
+
101
+ act(() => {
102
+ result.current.onTouchStart(touchEvent([{clientX: 0, clientY: 0}]));
103
+ result.current.onTouchMove(touchEvent([{clientX: 5, clientY: 5}]));
104
+ vi.advanceTimersByTime(500);
105
+ });
106
+ expect(onLongPress).toHaveBeenCalledTimes(1);
107
+ });
108
+
109
+ it('cancels on touch end', () => {
110
+ const onLongPress = vi.fn();
111
+ const {result} = renderHook(() => useLongPress({onLongPress}));
112
+
113
+ act(() => {
114
+ result.current.onTouchStart(touchEvent([{clientX: 0, clientY: 0}]));
115
+ result.current.onTouchEnd();
116
+ vi.advanceTimersByTime(500);
117
+ });
118
+ expect(onLongPress).not.toHaveBeenCalled();
119
+ });
120
+
121
+ it('cancels the pending timer on unmount', () => {
122
+ const onLongPress = vi.fn();
123
+ const {result, unmount} = renderHook(() => useLongPress({onLongPress}));
124
+
125
+ act(() => {
126
+ result.current.onTouchStart(touchEvent([{clientX: 0, clientY: 0}]));
127
+ });
128
+ unmount();
129
+ act(() => {
130
+ vi.advanceTimersByTime(500);
131
+ });
132
+ expect(onLongPress).not.toHaveBeenCalled();
133
+ });
134
+ });