@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
@@ -0,0 +1,132 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file useFocusTrap.test.tsx
5
+ * @input Uses vitest, @testing-library/react, useFocusTrap hook
6
+ * @output Unit tests for useFocusTrap tabbable-element model + Escape/IME guard
7
+ * @position Testing; validates useFocusTrap.ts focusable detection + dismissal coordination
8
+ *
9
+ * SYNC: When useFocusTrap.ts changes, update tests to match new behavior
10
+ */
11
+
12
+ import {describe, it, expect, vi} from 'vitest';
13
+ import {render, screen, fireEvent} from '@testing-library/react';
14
+ import {useFocusTrap} from './useFocusTrap';
15
+
16
+ function Trap({children}: {children: React.ReactNode}) {
17
+ const {containerRef, focusFirst} = useFocusTrap<HTMLDivElement>({
18
+ isActive: true,
19
+ });
20
+ return (
21
+ <div>
22
+ <button type="button" data-testid="outside">
23
+ Outside
24
+ </button>
25
+ <div ref={containerRef} data-testid="trap">
26
+ {children}
27
+ </div>
28
+ <button type="button" onClick={focusFirst} data-testid="focus-first">
29
+ Focus first
30
+ </button>
31
+ </div>
32
+ );
33
+ }
34
+
35
+ function EscapeTrap({
36
+ isActive,
37
+ onEscape,
38
+ label,
39
+ }: {
40
+ isActive: boolean;
41
+ onEscape: () => void;
42
+ label: string;
43
+ }) {
44
+ const {containerRef} = useFocusTrap<HTMLDivElement>({isActive, onEscape});
45
+ return (
46
+ <div ref={containerRef} data-testid={label}>
47
+ <button type="button">{label}-btn</button>
48
+ </div>
49
+ );
50
+ }
51
+
52
+ describe('useFocusTrap tabbable model (infra-8)', () => {
53
+ it('treats a contenteditable as focusable (focusFirst lands on it)', () => {
54
+ render(
55
+ <Trap>
56
+ <div
57
+ contentEditable
58
+ data-testid="editor"
59
+ suppressContentEditableWarning>
60
+ Type here
61
+ </div>
62
+ </Trap>,
63
+ );
64
+ fireEvent.click(screen.getByTestId('focus-first'));
65
+ expect(screen.getByTestId('editor')).toHaveFocus();
66
+ });
67
+
68
+ it('ignores an inert subtree when finding focusables', () => {
69
+ render(
70
+ <Trap>
71
+ <div inert>
72
+ <button type="button" data-testid="inert-btn">
73
+ Inert
74
+ </button>
75
+ </div>
76
+ <button type="button" data-testid="real-btn">
77
+ Real
78
+ </button>
79
+ </Trap>,
80
+ );
81
+ fireEvent.click(screen.getByTestId('focus-first'));
82
+ // Focus skips the inert button and lands on the real one.
83
+ expect(screen.getByTestId('real-btn')).toHaveFocus();
84
+ });
85
+ });
86
+
87
+ describe('useFocusTrap Escape coordination', () => {
88
+ it('calls onEscape for a single active trap', () => {
89
+ const onEscape = vi.fn();
90
+ render(<EscapeTrap isActive onEscape={onEscape} label="only" />);
91
+ fireEvent.keyDown(document, {key: 'Escape'});
92
+ expect(onEscape).toHaveBeenCalledTimes(1);
93
+ });
94
+
95
+ it('only the top-most trap responds to Escape when nested', () => {
96
+ const outer = vi.fn();
97
+ const inner = vi.fn();
98
+ render(
99
+ <>
100
+ <EscapeTrap isActive onEscape={outer} label="outer" />
101
+ <EscapeTrap isActive onEscape={inner} label="inner" />
102
+ </>,
103
+ );
104
+ // The most recently activated trap (inner) is on top.
105
+ fireEvent.keyDown(document, {key: 'Escape'});
106
+ expect(inner).toHaveBeenCalledTimes(1);
107
+ expect(outer).not.toHaveBeenCalled();
108
+ });
109
+
110
+ it('ignores Escape during IME composition', () => {
111
+ const onEscape = vi.fn();
112
+ render(<EscapeTrap isActive onEscape={onEscape} label="ime" />);
113
+ fireEvent.keyDown(document, {key: 'Escape', isComposing: true});
114
+ expect(onEscape).not.toHaveBeenCalled();
115
+ // keyCode 229 (composition) is also ignored
116
+ fireEvent.keyDown(document, {key: 'Escape', keyCode: 229});
117
+ expect(onEscape).not.toHaveBeenCalled();
118
+ // a normal Escape still works
119
+ fireEvent.keyDown(document, {key: 'Escape'});
120
+ expect(onEscape).toHaveBeenCalledTimes(1);
121
+ });
122
+
123
+ it('does not respond after the trap is deactivated', () => {
124
+ const onEscape = vi.fn();
125
+ const {rerender} = render(
126
+ <EscapeTrap isActive onEscape={onEscape} label="toggle" />,
127
+ );
128
+ rerender(<EscapeTrap isActive={false} onEscape={onEscape} label="toggle" />);
129
+ fireEvent.keyDown(document, {key: 'Escape'});
130
+ expect(onEscape).not.toHaveBeenCalled();
131
+ });
132
+ });
@@ -17,11 +17,80 @@
17
17
 
18
18
  import {useCallback, useEffect, useRef} from 'react';
19
19
 
20
+ import {FOCUSABLE_SELECTOR} from './focusableSelector';
21
+
22
+ /**
23
+ * Module-level stack of active focus-trap Escape handlers.
24
+ *
25
+ * Every active `useFocusTrap` used to attach its own document-level `keydown`
26
+ * listener with no coordination, so a single Escape press closed *every* open
27
+ * layer at once (e.g. a popover nested inside a Dialog closed both). Tracking
28
+ * traps in a shared stack lets only the most recently activated (top-most)
29
+ * trap respond to Escape.
30
+ */
31
+ const escapeStack: (() => void)[] = [];
32
+
33
+ function pushEscapeHandler(handler: () => void): void {
34
+ escapeStack.push(handler);
35
+ }
36
+
37
+ function removeEscapeHandler(handler: () => void): void {
38
+ const index = escapeStack.lastIndexOf(handler);
39
+ if (index !== -1) {
40
+ escapeStack.splice(index, 1);
41
+ }
42
+ }
43
+
44
+ function isTopEscapeHandler(handler: () => void): boolean {
45
+ return escapeStack[escapeStack.length - 1] === handler;
46
+ }
47
+
20
48
  /**
21
- * Selector for commonly focusable elements.
49
+ * Whether any focus-trap Escape handler is currently active (i.e. a popover
50
+ * layer is open). Other overlay primitives that manage their own Escape (e.g.
51
+ * Dialog) can consult this to defer to a popover layered on top of them,
52
+ * giving topmost-only dismissal until a full layer stack exists.
22
53
  */
23
- const FOCUSABLE_SELECTOR =
24
- 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"]):not([disabled])';
54
+ export function hasActiveFocusTrapEscape(): boolean {
55
+ return escapeStack.length > 0;
56
+ }
57
+
58
+ /**
59
+ * Whether an Escape keydown should be ignored because it is cancelling an
60
+ * in-progress IME composition. CJK/IME users press Escape to cancel
61
+ * composition; that must not close the surrounding overlay. `keyCode === 229`
62
+ * covers browsers that fire keydown before `isComposing` is set. Exported so
63
+ * other overlays (Dialog, Drawer, CommandPalette) share one definition.
64
+ */
65
+ export function isImeKeyEvent(event: {
66
+ isComposing?: boolean;
67
+ keyCode?: number;
68
+ }): boolean {
69
+ return event.isComposing === true || event.keyCode === 229;
70
+ }
71
+
72
+ /**
73
+ * Whether an element is currently perceivable/focusable — excludes ones hidden
74
+ * via `display:none`/`visibility:hidden` or inside an `inert`/`hidden` subtree,
75
+ * which the browser skips for Tab.
76
+ */
77
+ function isVisiblyFocusable(el: HTMLElement): boolean {
78
+ if (el.hasAttribute('inert') || el.closest('[inert]')) {
79
+ return false;
80
+ }
81
+ if (el.hidden || el.closest('[hidden]')) {
82
+ return false;
83
+ }
84
+ // offsetParent is null for display:none (and fixed elements); pair with a
85
+ // visibility check via getComputedStyle when available.
86
+ if (typeof window !== 'undefined' && window.getComputedStyle) {
87
+ const style = window.getComputedStyle(el);
88
+ if (style.visibility === 'hidden' || style.display === 'none') {
89
+ return false;
90
+ }
91
+ }
92
+ return true;
93
+ }
25
94
 
26
95
  /**
27
96
  * Get all focusable elements within a container.
@@ -29,7 +98,7 @@ const FOCUSABLE_SELECTOR =
29
98
  function getFocusableElements(container: HTMLElement): HTMLElement[] {
30
99
  return Array.from(
31
100
  container.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR),
32
- );
101
+ ).filter(isVisiblyFocusable);
33
102
  }
34
103
 
35
104
  /**
@@ -203,6 +272,16 @@ export function useFocusTrap<T extends HTMLElement = HTMLElement>(
203
272
  return;
204
273
  }
205
274
 
275
+ // Register this trap on the shared Escape stack so only the top-most
276
+ // active trap responds to Escape. A stable identity per active period is
277
+ // enough — we push on activate and remove on cleanup.
278
+ const escapeHandler = () => {
279
+ onEscape?.();
280
+ };
281
+ if (onEscape) {
282
+ pushEscapeHandler(escapeHandler);
283
+ }
284
+
206
285
  const handleKeyDown = (event: KeyboardEvent) => {
207
286
  const container = containerRef.current;
208
287
  if (!container) {
@@ -210,7 +289,19 @@ export function useFocusTrap<T extends HTMLElement = HTMLElement>(
210
289
  }
211
290
 
212
291
  if (event.key === 'Escape' && onEscape) {
292
+ // Ignore Escape that is cancelling an IME composition, already handled
293
+ // by a nested handler, or not targeting the top-most trap.
294
+ if (
295
+ event.defaultPrevented ||
296
+ isImeKeyEvent(event) ||
297
+ !isTopEscapeHandler(escapeHandler)
298
+ ) {
299
+ return;
300
+ }
301
+ // Mark handled and stop propagation so an outer layer (e.g. a Dialog
302
+ // hosting this popover) does not also dismiss on the same press.
213
303
  event.preventDefault();
304
+ event.stopPropagation();
214
305
  onEscape();
215
306
  return;
216
307
  }
@@ -247,6 +338,9 @@ export function useFocusTrap<T extends HTMLElement = HTMLElement>(
247
338
 
248
339
  return () => {
249
340
  document.removeEventListener('keydown', handleKeyDown);
341
+ if (onEscape) {
342
+ removeEscapeHandler(escapeHandler);
343
+ }
250
344
  };
251
345
  }, [isActive, onEscape]);
252
346
 
@@ -21,10 +21,22 @@ export const docs = {
21
21
  {
22
22
  name: 'options.cellSelector',
23
23
  type: 'string',
24
- description: 'Selector for focusable cells within the grid.',
24
+ description: 'Selector for cells within the grid. Should match ALL cell positions in DOM order (including disabled/empty) so grid geometry is preserved.',
25
25
  default: "'button:not([disabled]), [tabindex]:not([tabindex=\"-1\"])'",
26
26
  required: false,
27
27
  },
28
+ {
29
+ name: 'options.isCellFocusable',
30
+ type: '(cell: HTMLElement) => boolean',
31
+ description: 'Predicate for whether a matched cell can receive focus. Omit to treat every matched cell as focusable.',
32
+ required: false,
33
+ },
34
+ {
35
+ name: 'options.getFocusTarget',
36
+ type: '(cell: HTMLElement) => HTMLElement | null',
37
+ description: 'Resolves the element to focus for a cell, e.g. a button inside a role="gridcell" wrapper. Omit to focus the cell itself.',
38
+ required: false,
39
+ },
28
40
  {
29
41
  name: 'options.onNavigateBefore',
30
42
  type: '(column: number, offset: number) => void',
@@ -49,6 +61,20 @@ export const docs = {
49
61
  description: 'Callback for Page Down key (e.g., navigate to next month in calendars).',
50
62
  required: false,
51
63
  },
64
+ {
65
+ name: 'options.isRtl',
66
+ type: 'boolean',
67
+ description: 'Swap ArrowLeft/ArrowRight so horizontal navigation follows visual direction in right-to-left contexts.',
68
+ default: 'false',
69
+ required: false,
70
+ },
71
+ {
72
+ name: 'options.hasRovingTabIndex',
73
+ type: 'boolean',
74
+ description: 'Own a single roving tab stop across the grid: one focusable cell (its resolved focus target) carries tabindex="0", the rest -1. Stamped/repaired on render and moved with arrow navigation. Attach the returned handleFocus to the container onFocus.',
75
+ default: 'false',
76
+ required: false,
77
+ },
52
78
  ],
53
79
  returns: [
54
80
  {
@@ -61,6 +87,11 @@ export const docs = {
61
87
  type: '(e: React.KeyboardEvent) => void',
62
88
  description: 'Key down handler to attach to the grid container.',
63
89
  },
90
+ {
91
+ name: 'handleFocus',
92
+ type: '(e: React.FocusEvent) => void',
93
+ description: 'Focus handler for the grid container. Keeps the roving tab stop in sync when hasRovingTabIndex is enabled; a no-op otherwise, so always safe to attach.',
94
+ },
64
95
  {
65
96
  name: 'focusCell',
66
97
  type: '(index: number) => void',
@@ -83,6 +114,7 @@ export const docs = {
83
114
  bestPractices: [
84
115
  { guidance: true, description: 'Use for calendar date grids: wire onPageUp/onPageDown to month navigation and onNavigateBefore/onNavigateAfter for cross-month arrow key navigation.' },
85
116
  { guidance: true, description: 'Attach both gridRef and handleKeyDown to the grid container element.' },
117
+ { guidance: true, description: 'For roving-tabindex grids (e.g. Calendar), set hasRovingTabIndex: true and attach handleFocus to the container onFocus; seed one focus target with tabindex=0 and the hook repairs and moves it.' },
86
118
  { guidance: false, description: 'Use for simple linear lists; prefer useListFocus for 1D navigation.' },
87
119
  ],
88
120
  },
@@ -99,15 +131,20 @@ export const docsDense = {
99
131
  paramDescriptions: {
100
132
  options: 'config for grid focus behavior.',
101
133
  'options.columns': '# columns in grid. Used for up/down navigation (moves by this many cells).',
102
- 'options.cellSelector': 'selector for focusable cells in grid.',
134
+ 'options.cellSelector': 'selector for grid cells in DOM order (incl. disabled/empty) so geometry is preserved.',
135
+ 'options.isCellFocusable': 'predicate for whether a matched cell can take focus. Omit = all focusable.',
136
+ 'options.getFocusTarget': 'resolve element to focus for a cell (e.g. button inside gridcell wrapper). Omit = focus the cell.',
103
137
  'options.onNavigateBefore': 'callback when navigation would go before first cell. Receives column index + offset (1 for horizontal, columns for vertical).',
104
138
  'options.onNavigateAfter': 'callback when navigation would go after last cell. Receives column index + offset.',
105
139
  'options.onPageUp': 'callback for Page Up key (e.g. navigate to previous month in calendars).',
106
140
  'options.onPageDown': 'callback for Page Down key (e.g. navigate to next month in calendars).',
141
+ 'options.isRtl': 'swap ArrowLeft/ArrowRight for RTL horizontal navigation. default false.',
142
+ 'options.hasRovingTabIndex': 'own a single roving tab stop across the grid (one focus target tabindex=0, rest -1); repaired on render + moved with arrows. Attach handleFocus to onFocus. default false.',
107
143
  },
108
144
  returnDescriptions: {
109
145
  gridRef: 'ref to attach to grid container element.',
110
146
  handleKeyDown: 'key down handler for grid container.',
147
+ handleFocus: 'focus handler for grid container; syncs roving tab stop when hasRovingTabIndex on, else no-op.',
111
148
  focusCell: 'focus specific cell by index (clamped to valid range).',
112
149
  focusFirst: 'focus first focusable cell in grid.',
113
150
  focusLast: 'focus last focusable cell in grid.',
@@ -118,6 +155,7 @@ export const docsDense = {
118
155
  bestPractices: [
119
156
  { guidance: true, description: 'Use for calendar date grids: wire onPageUp/onPageDown to month navigation + onNavigateBefore/onNavigateAfter for cross-month arrow key navigation.' },
120
157
  { guidance: true, description: 'Attach both gridRef + handleKeyDown to grid container element.' },
158
+ { guidance: true, description: 'For roving-tabindex grids (e.g. Calendar): hasRovingTabIndex: true + attach handleFocus to onFocus; seed one focus target tabindex=0, hook repairs + moves it.' },
121
159
  { guidance: false, description: 'Use for simple linear lists; prefer useListFocus for 1D navigation.' },
122
160
  ],
123
161
  },
@@ -0,0 +1,132 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file useGridFocus.test.tsx
5
+ * @input Uses vitest, @testing-library/react, useGridFocus hook
6
+ * @output Unit tests for useGridFocus roving tabindex + RTL navigation
7
+ * @position Testing; validates useGridFocus.ts roving-tabindex ownership
8
+ *
9
+ * SYNC: When useGridFocus.ts changes, update tests to match new behavior
10
+ */
11
+
12
+ import {describe, it, expect} from 'vitest';
13
+ import {render, screen, fireEvent} from '@testing-library/react';
14
+ import {useGridFocus} from './useGridFocus';
15
+
16
+ const NO_DISABLED: number[] = [];
17
+
18
+ /**
19
+ * A 3x3 grid of cells shaped like Calendar: each `role="gridcell"` div wraps a
20
+ * focusable `<button>` (the focus target). `seed` marks which button starts
21
+ * tabbable (mirrors Calendar seeding the selected/today/first-enabled day).
22
+ */
23
+ function Grid({
24
+ disabled = NO_DISABLED,
25
+ seed = 0,
26
+ ...opts
27
+ }: {
28
+ disabled?: number[];
29
+ seed?: number;
30
+ } & Partial<Parameters<typeof useGridFocus>[0]>) {
31
+ const {gridRef, handleKeyDown, handleFocus} = useGridFocus<HTMLDivElement>({
32
+ columns: 3,
33
+ cellSelector: '[role="gridcell"]',
34
+ isCellFocusable: cell =>
35
+ cell.querySelector('button:not([disabled])') !== null,
36
+ getFocusTarget: cell => cell.querySelector<HTMLElement>('button'),
37
+ hasRovingTabIndex: true,
38
+ ...opts,
39
+ });
40
+ return (
41
+ <div
42
+ ref={gridRef}
43
+ role="grid"
44
+ onKeyDown={handleKeyDown}
45
+ onFocus={handleFocus}>
46
+ {Array.from({length: 9}, (_, i) => (
47
+ <div role="gridcell" key={i}>
48
+ <button
49
+ type="button"
50
+ disabled={disabled.includes(i)}
51
+ tabIndex={i === seed ? 0 : -1}
52
+ data-testid={`cell-${i}`}>
53
+ {i}
54
+ </button>
55
+ </div>
56
+ ))}
57
+ </div>
58
+ );
59
+ }
60
+
61
+ describe('useGridFocus roving tabindex (hasRovingTabIndex)', () => {
62
+ it('honors the seeded tab stop and stamps -1 on the rest', () => {
63
+ render(<Grid seed={4} />);
64
+ expect(screen.getByTestId('cell-4')).toHaveAttribute('tabindex', '0');
65
+ expect(screen.getByTestId('cell-0')).toHaveAttribute('tabindex', '-1');
66
+ expect(screen.getByTestId('cell-8')).toHaveAttribute('tabindex', '-1');
67
+ });
68
+
69
+ it('repairs to the first focusable cell when no cell is seeded', () => {
70
+ render(<Grid seed={-1} />);
71
+ expect(screen.getByTestId('cell-0')).toHaveAttribute('tabindex', '0');
72
+ expect(screen.getByTestId('cell-1')).toHaveAttribute('tabindex', '-1');
73
+ });
74
+
75
+ it('promotes the first ENABLED cell when the seed is disabled', () => {
76
+ render(<Grid seed={-1} disabled={[0, 1]} />);
77
+ expect(screen.getByTestId('cell-2')).toHaveAttribute('tabindex', '0');
78
+ expect(screen.getByTestId('cell-0')).toHaveAttribute('tabindex', '-1');
79
+ });
80
+
81
+ it('ArrowRight moves the tab stop to the next cell', () => {
82
+ render(<Grid seed={0} />);
83
+ const grid = screen.getByRole('grid');
84
+ screen.getByTestId('cell-0').focus();
85
+ fireEvent.keyDown(grid, {key: 'ArrowRight'});
86
+ expect(screen.getByTestId('cell-1')).toHaveFocus();
87
+ expect(screen.getByTestId('cell-1')).toHaveAttribute('tabindex', '0');
88
+ expect(screen.getByTestId('cell-0')).toHaveAttribute('tabindex', '-1');
89
+ });
90
+
91
+ it('ArrowDown moves the tab stop one row down', () => {
92
+ render(<Grid seed={0} />);
93
+ const grid = screen.getByRole('grid');
94
+ screen.getByTestId('cell-0').focus();
95
+ fireEvent.keyDown(grid, {key: 'ArrowDown'});
96
+ expect(screen.getByTestId('cell-3')).toHaveFocus();
97
+ expect(screen.getByTestId('cell-3')).toHaveAttribute('tabindex', '0');
98
+ });
99
+
100
+ it('handleFocus repairs the stop when the tabbable cell became disabled', () => {
101
+ // Seed cell 0 as tabbable, then disable it and fire the container onFocus.
102
+ // syncTabStops should promote the first still-focusable cell (cell 1).
103
+ const {rerender} = render(<Grid seed={0} />);
104
+ const grid = screen.getByRole('grid');
105
+ expect(screen.getByTestId('cell-0')).toHaveAttribute('tabindex', '0');
106
+ rerender(<Grid seed={-1} disabled={[0]} />);
107
+ fireEvent.focus(grid);
108
+ expect(screen.getByTestId('cell-1')).toHaveAttribute('tabindex', '0');
109
+ expect(screen.getByTestId('cell-0')).toHaveAttribute('tabindex', '-1');
110
+ });
111
+
112
+ it('flips ArrowLeft/ArrowRight under RTL', () => {
113
+ render(<Grid seed={1} isRtl />);
114
+ const grid = screen.getByRole('grid');
115
+ screen.getByTestId('cell-1').focus();
116
+ // In RTL, ArrowLeft is "forward" (moves to the next cell in DOM order).
117
+ fireEvent.keyDown(grid, {key: 'ArrowLeft'});
118
+ expect(screen.getByTestId('cell-2')).toHaveFocus();
119
+ });
120
+
121
+ it('does not manage tabindex when hasRovingTabIndex is off', () => {
122
+ render(<Grid seed={0} hasRovingTabIndex={false} />);
123
+ // The seeded -1/0 values are left untouched (caller owns them), and
124
+ // navigation still works without stamping.
125
+ const grid = screen.getByRole('grid');
126
+ screen.getByTestId('cell-0').focus();
127
+ fireEvent.keyDown(grid, {key: 'ArrowRight'});
128
+ expect(screen.getByTestId('cell-1')).toHaveFocus();
129
+ // cell-1 kept its seeded -1; the hook did not promote it.
130
+ expect(screen.getByTestId('cell-1')).toHaveAttribute('tabindex', '-1');
131
+ });
132
+ });