@astryxdesign/core 0.4.6 → 0.4.7-canary.20bae6c

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 (377) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/AppShell/AppShell.d.ts.map +1 -1
  3. package/dist/AppShell/AppShell.js +3 -2
  4. package/dist/AspectRatio/AspectRatio.d.ts +24 -0
  5. package/dist/AspectRatio/AspectRatio.d.ts.map +1 -1
  6. package/dist/AspectRatio/AspectRatio.js +23 -4
  7. package/dist/AvatarGroup/AvatarGroup.d.ts.map +1 -1
  8. package/dist/AvatarGroup/AvatarGroup.js +3 -2
  9. package/dist/BottomSheet/BottomSheet.d.ts +1 -0
  10. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
  11. package/dist/BottomSheet/BottomSheet.js +54 -15
  12. package/dist/BottomSheet/BottomSheetEdgeTint.d.ts +6 -0
  13. package/dist/BottomSheet/BottomSheetEdgeTint.d.ts.map +1 -0
  14. package/dist/BottomSheet/BottomSheetEdgeTint.js +62 -0
  15. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
  16. package/dist/BottomSheet/BottomSheetPanel.js +1 -0
  17. package/dist/BottomSheet/BottomSheetSwitcher.d.ts +1 -0
  18. package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
  19. package/dist/BottomSheet/BottomSheetSwitcher.js +13 -6
  20. package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -1
  21. package/dist/BottomSheet/useSheetGestures.js +23 -5
  22. package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
  23. package/dist/Breadcrumbs/BreadcrumbItem.js +11 -10
  24. package/dist/ButtonGroup/ButtonGroup.d.ts.map +1 -1
  25. package/dist/ButtonGroup/ButtonGroup.js +12 -4
  26. package/dist/Calendar/Calendar.d.ts +3 -1
  27. package/dist/Calendar/Calendar.d.ts.map +1 -1
  28. package/dist/Calendar/Calendar.js +13 -14
  29. package/dist/Calendar/getInitialFocusDate.d.ts +46 -0
  30. package/dist/Calendar/getInitialFocusDate.d.ts.map +1 -0
  31. package/dist/Calendar/getInitialFocusDate.js +62 -0
  32. package/dist/Carousel/Carousel.d.ts.map +1 -1
  33. package/dist/Carousel/Carousel.js +3 -3
  34. package/dist/Chat/ChatLayout.d.ts.map +1 -1
  35. package/dist/Chat/ChatLayout.js +3 -3
  36. package/dist/Chat/ChatMessageList.d.ts +22 -3
  37. package/dist/Chat/ChatMessageList.d.ts.map +1 -1
  38. package/dist/Chat/ChatMessageList.js +6 -3
  39. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  40. package/dist/CheckboxInput/CheckboxInput.js +3 -2
  41. package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
  42. package/dist/ClickableCard/ClickableCard.js +3 -3
  43. package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
  44. package/dist/CommandPalette/CommandPaletteInput.js +3 -2
  45. package/dist/CommandPalette/CommandPaletteItem.d.ts.map +1 -1
  46. package/dist/CommandPalette/CommandPaletteItem.js +3 -2
  47. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  48. package/dist/ContextMenu/ContextMenu.js +4 -3
  49. package/dist/DateInput/DateInput.d.ts.map +1 -1
  50. package/dist/DateInput/DateInput.js +3 -2
  51. package/dist/DateInput/TouchDateField.d.ts.map +1 -1
  52. package/dist/DateInput/TouchDateField.js +34 -1
  53. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  54. package/dist/DateTimeInput/DateTimeInput.js +3 -2
  55. package/dist/Dialog/Dialog.d.ts +7 -1
  56. package/dist/Dialog/Dialog.d.ts.map +1 -1
  57. package/dist/Dialog/Dialog.js +89 -58
  58. package/dist/FileInput/FileInput.d.ts.map +1 -1
  59. package/dist/FileInput/FileInput.js +3 -2
  60. package/dist/Heading/Heading.d.ts.map +1 -1
  61. package/dist/Heading/Heading.js +6 -2
  62. package/dist/HoverCard/HoverCard.d.ts +6 -0
  63. package/dist/HoverCard/HoverCard.d.ts.map +1 -1
  64. package/dist/HoverCard/useHoverCard.d.ts +5 -0
  65. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  66. package/dist/HoverCard/useHoverCard.js +67 -25
  67. package/dist/Item/Item.d.ts.map +1 -1
  68. package/dist/Item/Item.js +4 -2
  69. package/dist/Kbd/Kbd.d.ts.map +1 -1
  70. package/dist/Kbd/Kbd.js +10 -3
  71. package/dist/Layer/LayerDepthContext.d.ts +52 -0
  72. package/dist/Layer/LayerDepthContext.d.ts.map +1 -0
  73. package/dist/Layer/LayerDepthContext.js +66 -0
  74. package/dist/Layer/index.d.ts +3 -0
  75. package/dist/Layer/index.d.ts.map +1 -1
  76. package/dist/Layer/index.js +4 -0
  77. package/dist/Layer/layerStack.d.ts +98 -0
  78. package/dist/Layer/layerStack.d.ts.map +1 -0
  79. package/dist/Layer/layerStack.js +306 -0
  80. package/dist/Layer/useLayerDismissal.d.ts +93 -0
  81. package/dist/Layer/useLayerDismissal.d.ts.map +1 -0
  82. package/dist/Layer/useLayerDismissal.js +91 -0
  83. package/dist/Lightbox/Lightbox.d.ts +1 -0
  84. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  85. package/dist/Lightbox/Lightbox.js +103 -81
  86. package/dist/Markdown/index.d.ts +1 -1
  87. package/dist/Markdown/index.d.ts.map +1 -1
  88. package/dist/Markdown/parser.d.ts +38 -1
  89. package/dist/Markdown/parser.d.ts.map +1 -1
  90. package/dist/Markdown/parser.js +149 -23
  91. package/dist/MobileNav/MobileNav.d.ts +6 -1
  92. package/dist/MobileNav/MobileNav.d.ts.map +1 -1
  93. package/dist/MobileNav/MobileNav.js +61 -36
  94. package/dist/MultiSelector/MultiSelector.d.ts +15 -1
  95. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  96. package/dist/MultiSelector/MultiSelector.js +17 -6
  97. package/dist/MultiSelector/index.d.ts +1 -1
  98. package/dist/MultiSelector/index.d.ts.map +1 -1
  99. package/dist/NavMenu/NavHeadingMenu.d.ts.map +1 -1
  100. package/dist/NavMenu/NavHeadingMenu.js +3 -2
  101. package/dist/Outline/Outline.d.ts.map +1 -1
  102. package/dist/Outline/Outline.js +3 -2
  103. package/dist/OverflowList/OverflowList.d.ts.map +1 -1
  104. package/dist/OverflowList/OverflowList.js +3 -2
  105. package/dist/Overlay/Overlay.d.ts.map +1 -1
  106. package/dist/Overlay/Overlay.js +3 -2
  107. package/dist/Popover/usePopover.d.ts.map +1 -1
  108. package/dist/Popover/usePopover.js +20 -17
  109. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  110. package/dist/PowerSearch/PowerSearch.js +3 -4
  111. package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
  112. package/dist/Resizable/ResizeHandle.js +3 -2
  113. package/dist/SegmentedControl/SegmentedControl.d.ts.map +1 -1
  114. package/dist/SegmentedControl/SegmentedControl.js +3 -2
  115. package/dist/SelectableCard/SelectableCard.d.ts.map +1 -1
  116. package/dist/SelectableCard/SelectableCard.js +3 -3
  117. package/dist/Selector/Selector.d.ts.map +1 -1
  118. package/dist/Selector/Selector.js +5 -0
  119. package/dist/SideNav/SideNav.d.ts.map +1 -1
  120. package/dist/SideNav/SideNav.js +4 -3
  121. package/dist/SideNav/SideNavItem.d.ts.map +1 -1
  122. package/dist/SideNav/SideNavItem.js +4 -3
  123. package/dist/Slider/Slider.d.ts.map +1 -1
  124. package/dist/Slider/Slider.js +3 -3
  125. package/dist/Stepper/Step.d.ts +140 -0
  126. package/dist/Stepper/Step.d.ts.map +1 -0
  127. package/dist/Stepper/Step.js +1045 -0
  128. package/dist/Stepper/StepStatus.d.ts +24 -0
  129. package/dist/Stepper/StepStatus.d.ts.map +1 -0
  130. package/dist/Stepper/StepStatus.js +1 -0
  131. package/dist/Stepper/Stepper.d.ts +93 -0
  132. package/dist/Stepper/Stepper.d.ts.map +1 -0
  133. package/dist/Stepper/Stepper.js +184 -0
  134. package/dist/Stepper/StepperContext.d.ts +41 -0
  135. package/dist/Stepper/StepperContext.d.ts.map +1 -0
  136. package/dist/Stepper/StepperContext.js +34 -0
  137. package/dist/Stepper/index.d.ts +8 -0
  138. package/dist/Stepper/index.d.ts.map +1 -0
  139. package/dist/Stepper/index.js +7 -0
  140. package/dist/Stepper/stepper.stylex.d.ts +15 -0
  141. package/dist/Stepper/stepper.stylex.d.ts.map +1 -0
  142. package/dist/Stepper/stepper.stylex.js +20 -0
  143. package/dist/Switch/Switch.d.ts.map +1 -1
  144. package/dist/Switch/Switch.js +3 -2
  145. package/dist/TabList/Tab.d.ts.map +1 -1
  146. package/dist/TabList/Tab.js +5 -1
  147. package/dist/TabList/TabList.d.ts.map +1 -1
  148. package/dist/TabList/TabList.js +3 -3
  149. package/dist/Table/BaseTable.d.ts.map +1 -1
  150. package/dist/Table/BaseTable.js +4 -1
  151. package/dist/Table/plugins/groupedRows/useTableGroupedRows.d.ts.map +1 -1
  152. package/dist/Table/plugins/groupedRows/useTableGroupedRows.js +20 -8
  153. package/dist/Table/plugins/pagination/useTablePagination.d.ts +3 -0
  154. package/dist/Table/plugins/pagination/useTablePagination.d.ts.map +1 -1
  155. package/dist/Table/plugins/pagination/useTablePagination.js +21 -4
  156. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
  157. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +7 -2
  158. package/dist/Table/plugins/rowStatus/useTableRowStatus.d.ts.map +1 -1
  159. package/dist/Table/plugins/rowStatus/useTableRowStatus.js +10 -3
  160. package/dist/Table/plugins/selection/useTableSelection.d.ts +16 -0
  161. package/dist/Table/plugins/selection/useTableSelection.d.ts.map +1 -1
  162. package/dist/Table/plugins/selection/useTableSelection.js +19 -5
  163. package/dist/Table/types.d.ts +22 -4
  164. package/dist/Table/types.d.ts.map +1 -1
  165. package/dist/Table/useBaseTablePlugins.d.ts.map +1 -1
  166. package/dist/Table/useBaseTablePlugins.js +5 -0
  167. package/dist/Text/Text.d.ts.map +1 -1
  168. package/dist/Text/Text.js +6 -2
  169. package/dist/TextArea/TextArea.d.ts.map +1 -1
  170. package/dist/TextArea/TextArea.js +3 -2
  171. package/dist/TextInput/TextInput.d.ts.map +1 -1
  172. package/dist/TextInput/TextInput.js +3 -2
  173. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  174. package/dist/TimeInput/TimeInput.js +3 -2
  175. package/dist/Timestamp/Timestamp.d.ts.map +1 -1
  176. package/dist/Timestamp/Timestamp.js +5 -3
  177. package/dist/Token/TokenLink.d.ts.map +1 -1
  178. package/dist/Token/TokenLink.js +2 -2
  179. package/dist/Tooltip/Tooltip.d.ts +5 -0
  180. package/dist/Tooltip/Tooltip.d.ts.map +1 -1
  181. package/dist/Tooltip/useTooltip.d.ts +5 -0
  182. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  183. package/dist/Tooltip/useTooltip.js +39 -24
  184. package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
  185. package/dist/TopNav/TopNavMegaMenu.js +3 -3
  186. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  187. package/dist/Typeahead/BaseTypeahead.js +3 -3
  188. package/dist/Typeahead/Typeahead.d.ts.map +1 -1
  189. package/dist/Typeahead/Typeahead.js +3 -2
  190. package/dist/astryx.css +43 -1
  191. package/dist/hooks/index.d.ts +1 -0
  192. package/dist/hooks/index.d.ts.map +1 -1
  193. package/dist/hooks/index.js +1 -0
  194. package/dist/hooks/useFocusTrap.d.ts +7 -4
  195. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  196. package/dist/hooks/useFocusTrap.js +52 -91
  197. package/dist/hooks/useHotkeys.d.ts.map +1 -1
  198. package/dist/hooks/useHotkeys.js +10 -3
  199. package/dist/hooks/useListFocus.d.ts +5 -2
  200. package/dist/hooks/useListFocus.d.ts.map +1 -1
  201. package/dist/hooks/useListFocus.js +12 -6
  202. package/dist/hooks/useMergedRefs.d.ts +18 -0
  203. package/dist/hooks/useMergedRefs.d.ts.map +1 -0
  204. package/dist/hooks/useMergedRefs.js +25 -0
  205. package/dist/index.d.ts +1 -0
  206. package/dist/index.d.ts.map +1 -1
  207. package/dist/index.js +1 -0
  208. package/dist/utils/timeParser.d.ts +1 -1
  209. package/dist/utils/timeParser.d.ts.map +1 -1
  210. package/dist/utils/timeParser.js +26 -12
  211. package/locales/en.json +27 -7
  212. package/locales/pseudo.json +15 -0
  213. package/package.json +8 -3
  214. package/src/AppShell/AppShell.tsx +3 -2
  215. package/src/AspectRatio/AspectRatio.doc.mjs +6 -3
  216. package/src/AspectRatio/AspectRatio.test.tsx +57 -10
  217. package/src/AspectRatio/AspectRatio.tsx +40 -1
  218. package/src/Avatar/Avatar.doc.mjs +2 -1
  219. package/src/AvatarGroup/AvatarGroup.tsx +3 -2
  220. package/src/BottomSheet/BottomSheet.test.tsx +123 -0
  221. package/src/BottomSheet/BottomSheet.tsx +36 -4
  222. package/src/BottomSheet/BottomSheetEdgeTint.test.tsx +225 -0
  223. package/src/BottomSheet/BottomSheetEdgeTint.tsx +82 -0
  224. package/src/BottomSheet/BottomSheetPanel.test.tsx +66 -0
  225. package/src/BottomSheet/BottomSheetPanel.tsx +19 -0
  226. package/src/BottomSheet/BottomSheetSwitcher.tsx +16 -2
  227. package/src/BottomSheet/useSheetGestures.test.ts +27 -0
  228. package/src/BottomSheet/useSheetGestures.ts +25 -5
  229. package/src/Breadcrumbs/BreadcrumbItem.tsx +9 -5
  230. package/src/Button/Button.doc.mjs +22 -0
  231. package/src/ButtonGroup/ButtonGroup.doc.mjs +7 -1
  232. package/src/ButtonGroup/ButtonGroup.test.tsx +185 -1
  233. package/src/ButtonGroup/ButtonGroup.tsx +11 -3
  234. package/src/Calendar/Calendar.doc.mjs +4 -3
  235. package/src/Calendar/Calendar.test.tsx +52 -0
  236. package/src/Calendar/Calendar.tsx +18 -15
  237. package/src/Calendar/getInitialFocusDate.test.ts +94 -0
  238. package/src/Calendar/getInitialFocusDate.ts +93 -0
  239. package/src/Carousel/Carousel.tsx +3 -2
  240. package/src/Chat/ChatLayout.tsx +3 -2
  241. package/src/Chat/ChatMessageList.doc.mjs +9 -1
  242. package/src/Chat/ChatMessageList.test.tsx +46 -0
  243. package/src/Chat/ChatMessageList.tsx +28 -4
  244. package/src/CheckboxInput/CheckboxInput.tsx +3 -2
  245. package/src/ClickableCard/ClickableCard.tsx +3 -2
  246. package/src/CodeBlock/CodeBlock.doc.mjs +6 -0
  247. package/src/CommandPalette/CommandPaletteInput.tsx +3 -2
  248. package/src/CommandPalette/CommandPaletteItem.tsx +3 -2
  249. package/src/ContextMenu/ContextMenu.doc.mjs +5 -0
  250. package/src/ContextMenu/ContextMenu.tsx +4 -3
  251. package/src/DateInput/DateInput.tsx +3 -2
  252. package/src/DateInput/DateInputTouch.test.tsx +36 -0
  253. package/src/DateInput/TouchDateField.tsx +35 -1
  254. package/src/DateTimeInput/DateTimeInput.tsx +2 -2
  255. package/src/Dialog/Dialog.doc.mjs +8 -3
  256. package/src/Dialog/Dialog.test.tsx +261 -1
  257. package/src/Dialog/Dialog.tsx +109 -58
  258. package/src/DropdownMenu/DropdownMenu.doc.mjs +6 -0
  259. package/src/Field/Field.doc.mjs +13 -2
  260. package/src/FileInput/FileInput.tsx +3 -2
  261. package/src/Heading/Heading.test.tsx +23 -1
  262. package/src/Heading/Heading.tsx +6 -2
  263. package/src/HoverCard/HoverCard.doc.mjs +5 -0
  264. package/src/HoverCard/HoverCard.tsx +6 -0
  265. package/src/HoverCard/useHoverCard.tsx +81 -30
  266. package/src/Item/Item.tsx +5 -4
  267. package/src/Kbd/Kbd.test.tsx +17 -0
  268. package/src/Kbd/Kbd.tsx +10 -3
  269. package/src/Layer/LayerDepthContext.tsx +62 -0
  270. package/src/Layer/index.ts +9 -0
  271. package/src/Layer/layerDismissalFamilies.test.tsx +359 -0
  272. package/src/Layer/layerDismissalInvariants.test.tsx +417 -0
  273. package/src/Layer/layerStack.ts +362 -0
  274. package/src/Layer/useLayerDismissal.test.tsx +469 -0
  275. package/src/Layer/useLayerDismissal.ts +173 -0
  276. package/src/Layout/Layout.doc.mjs +2 -2
  277. package/src/Lightbox/Lightbox.doc.mjs +12 -0
  278. package/src/Lightbox/Lightbox.tsx +124 -102
  279. package/src/Link/Link.doc.mjs +39 -0
  280. package/src/Markdown/index.ts +1 -0
  281. package/src/Markdown/parser.perf.test.ts +71 -1
  282. package/src/Markdown/parser.test.ts +145 -2
  283. package/src/Markdown/parser.ts +208 -22
  284. package/src/MobileNav/MobileNav.tsx +62 -36
  285. package/src/MoreMenu/MoreMenu.doc.mjs +7 -1
  286. package/src/MultiSelector/MultiSelector.doc.mjs +27 -0
  287. package/src/MultiSelector/MultiSelector.test.tsx +105 -0
  288. package/src/MultiSelector/MultiSelector.tsx +50 -6
  289. package/src/MultiSelector/index.ts +1 -0
  290. package/src/NavMenu/NavHeadingMenu.tsx +3 -2
  291. package/src/NumberInput/NumberInput.doc.mjs +5 -0
  292. package/src/Outline/Outline.tsx +3 -2
  293. package/src/OverflowList/OverflowList.tsx +3 -2
  294. package/src/Overlay/Overlay.tsx +3 -2
  295. package/src/Popover/Popover.test.tsx +27 -1
  296. package/src/Popover/usePopover.tsx +31 -28
  297. package/src/PowerSearch/PowerSearch.doc.mjs +26 -0
  298. package/src/PowerSearch/PowerSearch.tsx +5 -2
  299. package/src/Resizable/ResizeHandle.tsx +3 -2
  300. package/src/SegmentedControl/SegmentedControl.tsx +3 -2
  301. package/src/SelectableCard/SelectableCard.tsx +3 -2
  302. package/src/Selector/Selector.doc.mjs +12 -1
  303. package/src/Selector/Selector.test.tsx +21 -0
  304. package/src/Selector/Selector.tsx +11 -1
  305. package/src/SideNav/SideNav.doc.mjs +6 -6
  306. package/src/SideNav/SideNav.tsx +4 -2
  307. package/src/SideNav/SideNavItem.tsx +4 -2
  308. package/src/Slider/Slider.tsx +3 -2
  309. package/src/Stepper/Step.doc.mjs +98 -0
  310. package/src/Stepper/Step.tsx +1638 -0
  311. package/src/Stepper/StepStatus.ts +26 -0
  312. package/src/Stepper/Stepper.doc.mjs +370 -0
  313. package/src/Stepper/Stepper.test.tsx +1024 -0
  314. package/src/Stepper/Stepper.tsx +258 -0
  315. package/src/Stepper/StepperContext.ts +71 -0
  316. package/src/Stepper/index.ts +18 -0
  317. package/src/Stepper/stepper.stylex.ts +19 -0
  318. package/src/Switch/Switch.tsx +3 -2
  319. package/src/TabList/Tab.tsx +5 -1
  320. package/src/TabList/TabList.test.tsx +21 -4
  321. package/src/TabList/TabList.tsx +3 -2
  322. package/src/Table/BaseTable.tsx +6 -3
  323. package/src/Table/Table.test.tsx +35 -0
  324. package/src/Table/plugins/groupedRows/useTableGroupedRows-perf.test.tsx +112 -0
  325. package/src/Table/plugins/groupedRows/useTableGroupedRows.test.tsx +100 -0
  326. package/src/Table/plugins/groupedRows/useTableGroupedRows.tsx +17 -8
  327. package/src/Table/plugins/pagination/useTablePagination.test.tsx +34 -4
  328. package/src/Table/plugins/pagination/useTablePagination.tsx +35 -3
  329. package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +9 -2
  330. package/src/Table/plugins/rowStatus/useTableRowStatus.test.tsx +13 -7
  331. package/src/Table/plugins/rowStatus/useTableRowStatus.tsx +12 -3
  332. package/src/Table/plugins/selection/useTableSelection.test.tsx +76 -0
  333. package/src/Table/plugins/selection/useTableSelection.tsx +40 -7
  334. package/src/Table/types.ts +22 -4
  335. package/src/Table/useBaseTablePlugins.ts +5 -0
  336. package/src/Table/useTableGroupedRows.doc.mjs +5 -4
  337. package/src/Table/useTablePagination.doc.mjs +7 -0
  338. package/src/Table/useTableRowStatus.doc.mjs +1 -1
  339. package/src/Table/useTableSelection.doc.mjs +31 -0
  340. package/src/Text/Text.test.tsx +23 -1
  341. package/src/Text/Text.tsx +6 -2
  342. package/src/TextArea/TextArea.tsx +3 -2
  343. package/src/TextInput/TextInput.doc.mjs +10 -0
  344. package/src/TextInput/TextInput.tsx +3 -2
  345. package/src/Thumbnail/Thumbnail.doc.mjs +1 -1
  346. package/src/TimeInput/TimeInput.tsx +2 -2
  347. package/src/Timestamp/Timestamp.tsx +4 -2
  348. package/src/Toast/Toast.doc.mjs +6 -0
  349. package/src/Token/TokenLink.tsx +2 -2
  350. package/src/Tokenizer/Tokenizer.doc.mjs +16 -0
  351. package/src/Toolbar/Toolbar.doc.mjs +5 -0
  352. package/src/Tooltip/Tooltip.doc.mjs +5 -0
  353. package/src/Tooltip/Tooltip.test.tsx +32 -0
  354. package/src/Tooltip/Tooltip.tsx +5 -0
  355. package/src/Tooltip/useTooltip.tsx +47 -24
  356. package/src/TopNav/TopNavMegaMenu.tsx +3 -2
  357. package/src/Typeahead/BaseTypeahead.tsx +3 -2
  358. package/src/Typeahead/Typeahead.doc.mjs +5 -0
  359. package/src/Typeahead/Typeahead.tsx +3 -2
  360. package/src/__tests__/complexComponentContract.test.tsx +81 -0
  361. package/src/hooks/index.ts +2 -0
  362. package/src/hooks/useContainerReveal.doc.mjs +2 -2
  363. package/src/hooks/useFocusTrap.ts +54 -107
  364. package/src/hooks/useFocusTrapEscapeShim.test.tsx +165 -0
  365. package/src/hooks/useHotkeys.test.ts +18 -0
  366. package/src/hooks/useHotkeys.ts +10 -3
  367. package/src/hooks/useListFocus.doc.mjs +2 -2
  368. package/src/hooks/useListFocus.test.tsx +65 -3
  369. package/src/hooks/useListFocus.ts +15 -7
  370. package/src/hooks/useMergedRefs.doc.mjs +79 -0
  371. package/src/hooks/useMergedRefs.test.tsx +62 -0
  372. package/src/hooks/useMergedRefs.ts +36 -0
  373. package/src/index.ts +1 -0
  374. package/src/theme/MediaTheme.doc.mjs +5 -5
  375. package/src/utils/timeParser.test.ts +6 -1
  376. package/src/utils/timeParser.ts +35 -13
  377. package/src/__tests__/inputWidthContract.test.tsx +0 -69
@@ -7,7 +7,7 @@ export const docs = {
7
7
  subComponentOf: 'Table',
8
8
  displayName: 'useTableGroupedRows',
9
9
  description:
10
- 'Hook that groups a flat data array into collapsible section rows. Each distinct groupBy value becomes a full-width section-header row with a chevron toggle, the group label, and a member count; collapsing hides that group\'s data rows while keeping the header visible. Mirrors useTableTreeState: the consumer owns the collapsedGroups set and the hook returns {data, plugin, idKey}: pass all three to Table (data, plugins, and idKey respectively).',
10
+ "Hook that groups a flat data array into collapsible section rows. Each distinct groupBy value becomes a full-width section-header row with a chevron toggle, the group label, and a member count; collapsing hides that group's data rows while keeping the header visible. Mirrors useTableTreeState: the consumer owns the collapsedGroups set and the hook returns {data, plugin, idKey}: pass them to Table as data, plugins, and idKey respectively.",
11
11
  props: [
12
12
  {
13
13
  name: 'data',
@@ -58,13 +58,14 @@ export const docs = {
58
58
  /** @type {import('@astryxdesign/cli/authoring').ComponentTranslationDoc} */
59
59
  export const docsDense = {
60
60
  description:
61
- 'Groups a flat data array into collapsible section rows. Each groupBy value becomes a full-width header (chevron + label + count); collapsing hides its rows. Returns {data, plugin, idKey}: pass to Table data / plugins / idKey. Consumer owns the collapsedGroups set.',
61
+ 'Groups a flat data array into collapsible section rows. Each groupBy value becomes a full-width header (chevron + label + count); collapsing hides its rows. Returns {data, plugin, idKey}: pass them to Table data / plugins / idKey. Consumer owns the collapsedGroups set.',
62
62
  propDescriptions: {
63
63
  data: 'The flat data to group.',
64
- groupBy: 'Derive a row\'s group key. Same key = same section.',
64
+ groupBy: "Derive a row's group key. Same key = same section.",
65
65
  collapsedGroups: 'Set of currently-collapsed group keys.',
66
66
  onToggleGroup: 'Called with the group key when a header is toggled.',
67
- renderGroupHeader: "Custom header content (right of chevron). Default '<key> (<count>)'.",
67
+ renderGroupHeader:
68
+ "Custom header content (right of chevron). Default '<key> (<count>)'.",
68
69
  getRowKey: 'Stable key for a real row; positional fallback when omitted.',
69
70
  groupOrder: 'Pin these group keys first; others keep first-seen order.',
70
71
  },
@@ -72,6 +72,12 @@ export const docs = {
72
72
  description: 'Horizontal alignment of the pagination controls.',
73
73
  default: "'center'",
74
74
  },
75
+ {
76
+ name: 'label',
77
+ type: 'string',
78
+ description: "Accessible name for the pagination nav landmark. With position='both' the two navs get distinct names — the label suffixed as '(top)' and '(bottom)' — so same-type landmarks stay unique (axe landmark-unique).",
79
+ default: "'Table pagination'",
80
+ },
75
81
  ],
76
82
  };
77
83
 
@@ -129,5 +135,6 @@ export const docsDense = {
129
135
  pageSize: 'Items per page. Default 10.',
130
136
  variant: "Pagination variant: 'pages'|'count'|'compact'|'dots'|'none'.",
131
137
  position: "Render position: 'below'|'above'|'both'|'none'.",
138
+ label: "Accessible name for the nav landmark; position='both' emits '<label> (top)' / '<label> (bottom)'.",
132
139
  },
133
140
  };
@@ -7,7 +7,7 @@ export const docs = {
7
7
  subComponentOf: 'Table',
8
8
  displayName: 'useTableRowStatus',
9
9
  description:
10
- 'Hook that returns a TablePlugin which prepends a narrow column signaling per-row status: a colored status dot by default, or an icon when provided (shape + color is more accessible than color alone). getStatus maps a row to a semantic color (mapped to a theme token) or raw CSS color, an optional icon, and a required accessible label (shown in a tooltip on hover and announced to assistive technology, so status is never color-only); return null for no indicator. Memoize getStatus with useCallback for a stable plugin identity.',
10
+ 'Hook that returns a TablePlugin which prepends a narrow column signaling per-row status: a colored status dot by default, or an icon when provided (shape + color is more accessible than color alone). getStatus maps a row to a semantic color (mapped to a theme token) or raw CSS color, an optional icon, and a required accessible label (shown in a tooltip on hover and announced to assistive technology, so status is never color-only); return null for no indicator. The column header is visually blank but carries a screen-reader-only localized name ("Row status", i18n key @astryx.table.rowStatus.columnHeader). Memoize getStatus with useCallback for a stable plugin identity.',
11
11
  props: [
12
12
  {
13
13
  name: 'getStatus',
@@ -61,6 +61,13 @@ export const docs = {
61
61
  description:
62
62
  'Derives a human-readable identity for a row; the row checkbox\'s hidden label becomes `Select ${getRowLabel(item)}` so screen readers announce which row each checkbox selects. Falls back to "Select row" when omitted.',
63
63
  },
64
+ {
65
+ name: 'hasRowHighlight',
66
+ type: 'boolean',
67
+ description:
68
+ 'Paints checked rows with the accent wash. Set false when the surrounding UI already uses row background to mean something else (a row open in a detail panel, say) — the wash is an inline style, so it cannot be overridden from userland. Only the background is dropped: aria-selected is still set on checked rows either way.',
69
+ default: 'true',
70
+ },
64
71
  ],
65
72
  examples: [
66
73
  {
@@ -82,6 +89,21 @@ const selectionPlugin = useTableSelection({
82
89
  idKey="id"
83
90
  plugins={{selection: selectionPlugin}}
84
91
  />;
92
+ `,
93
+ },
94
+ {
95
+ label: 'Opt out of the checked-row wash',
96
+ code: `
97
+ // This table already uses row background to mean "open in the detail
98
+ // panel". Turning the selection wash off keeps that meaning unambiguous;
99
+ // the checkbox and aria-selected still carry the selection.
100
+ const selectionPlugin = useTableSelection({
101
+ getIsItemSelected: item => selectedIds.has(item.id),
102
+ onSelectItem: ({item, isSelected}) => toggle(item.id, isSelected),
103
+ onSelectAll: ({isAllSelected}) => selectAll(isAllSelected),
104
+ getIsAllSelected: () => selectedIds.size === users.length,
105
+ hasRowHighlight: false,
106
+ });
85
107
  `,
86
108
  },
87
109
  ],
@@ -142,6 +164,13 @@ export const docsZh = {
142
164
  description:
143
165
  '为行派生人类可读的标识;行复选框的隐藏标签变为 `Select ${getRowLabel(item)}`,让屏幕阅读器播报每个复选框选择的是哪一行。省略时回退为 "Select row"。',
144
166
  },
167
+ {
168
+ name: 'hasRowHighlight',
169
+ type: 'boolean',
170
+ description:
171
+ '为选中的行绘制强调色背景。当周围的界面已用行背景表达其他含义(例如该行已在详情面板中打开)时设为 false —— 该背景是内联样式,无法从业务代码覆盖。仅去掉背景:无论如何选中的行仍会设置 aria-selected。',
172
+ default: 'true',
173
+ },
145
174
  ],
146
175
  };
147
176
 
@@ -164,5 +193,7 @@ export const docsDense = {
164
193
  'Returns whether row checkbox is interactive; disabled rows show disabled checkbox.',
165
194
  getRowLabel:
166
195
  'Derives row identity for the checkbox\'s hidden label: `Select ${getRowLabel(item)}`. Falls back to "Select row" when omitted.',
196
+ hasRowHighlight:
197
+ 'false => skip the accent wash on checked rows (inline style, not overridable from userland). aria-selected is unaffected. Defaults to true.',
167
198
  },
168
199
  };
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import {render, screen} from '@testing-library/react';
9
- import {describe, it, expect} from 'vitest';
9
+ import {describe, it, expect, vi} from 'vitest';
10
10
  import {Text} from './Text';
11
11
  import type {TextType} from './Text';
12
12
  import type {TextColor} from '../theme/types';
@@ -23,6 +23,28 @@ describe('Text', () => {
23
23
  expect(screen.getByText('Hello World')).toBeInTheDocument();
24
24
  });
25
25
 
26
+ it('keeps a forwarded ref attached across rerenders', () => {
27
+ const ref = vi.fn();
28
+ const {rerender} = render(
29
+ <Text ref={ref} type="body">
30
+ Before
31
+ </Text>,
32
+ );
33
+
34
+ const element = screen.getByText('Before');
35
+ expect(ref).toHaveBeenLastCalledWith(element);
36
+ ref.mockClear();
37
+
38
+ rerender(
39
+ <Text ref={ref} type="body">
40
+ After
41
+ </Text>,
42
+ );
43
+
44
+ expect(ref).not.toHaveBeenCalled();
45
+ expect(screen.getByText('After')).toBe(element);
46
+ });
47
+
26
48
  it('renders as span by default', () => {
27
49
  render(<Text type="body">Text</Text>);
28
50
  const element = screen.getByText('Text');
package/src/Text/Text.tsx CHANGED
@@ -48,7 +48,8 @@ import {
48
48
  } from './text.stylex';
49
49
  import {useTruncation} from './useTruncation';
50
50
  import type {LayerPlacement} from '../Layer';
51
- import {mergeProps, mergeRefs} from '../utils';
51
+ import {mergeProps} from '../utils';
52
+ import {useMergedRefs} from '../hooks/useMergedRefs';
52
53
  import type {BaseProps} from '../BaseProps';
53
54
  import {themeProps} from '../utils/themeProps';
54
55
 
@@ -271,13 +272,16 @@ export function Text({
271
272
  // Ref for the text element (used as tooltip anchor)
272
273
  const textRef = useRef<HTMLElement>(null);
273
274
 
275
+ // Keep the merged ref stable across rerenders.
276
+ const mergedRef = useMergedRefs(ref, truncation.ref, textRef);
277
+
274
278
  // Build inline style for -webkit-line-clamp (dynamic value)
275
279
  const inlineStyle = maxLines > 1 ? {WebkitLineClamp: maxLines} : undefined;
276
280
 
277
281
  return (
278
282
  <>
279
283
  <Component
280
- ref={mergeRefs(ref, truncation.ref, textRef)}
284
+ ref={mergedRef}
281
285
  {...mergeProps(
282
286
  themeProps('text', {type, size, color: resolvedColor}),
283
287
  stylex.props(
@@ -46,7 +46,7 @@ import {
46
46
  import {Icon, renderIconSlot, type IconType} from '../Icon';
47
47
  import {Spinner} from '../Spinner';
48
48
  import {useTooltip} from '../Tooltip';
49
- import {mergeProps, mergeRefs} from '../utils';
49
+ import {mergeProps} from '../utils';
50
50
  import type {BaseProps} from '../BaseProps';
51
51
  import type {SizeValue} from '../utils/types';
52
52
  import {useInputContainer} from '../hooks/useInputContainer';
@@ -58,6 +58,7 @@ import {themeProps} from '../utils/themeProps';
58
58
  import {characterCount} from '../utils/characters';
59
59
  import {useTranslator} from '../i18n';
60
60
 
61
+ import {useMergedRefs} from '../hooks/useMergedRefs';
61
62
  const COUNTER_WARNING_THRESHOLD = 0.8;
62
63
 
63
64
  const styles = stylex.create({
@@ -580,7 +581,7 @@ export function TextArea({
580
581
  )}
581
582
  <textarea
582
583
  {...rest}
583
- ref={mergeRefs(ref, textareaRef)}
584
+ ref={useMergedRefs(ref, textareaRef)}
584
585
  id={id}
585
586
  name={isDisabled ? undefined : htmlName}
586
587
  value={optimisticValue}
@@ -71,6 +71,16 @@ export const docs = {
71
71
  'Displays a "Required" indicator next to the label and sets aria-required. Mutually exclusive with isOptional.',
72
72
  default: 'false',
73
73
  },
74
+ {
75
+ name: 'onEnter',
76
+ type: '() => void',
77
+ description: 'Callback fired when the user presses the Enter key.',
78
+ },
79
+ {
80
+ name: 'onKeyDown',
81
+ type: '(e: KeyboardEvent<HTMLInputElement>) => void',
82
+ description: 'Callback fired on keydown events on the input.',
83
+ },
74
84
  {
75
85
  name: 'isDisabled',
76
86
  type: 'boolean',
@@ -97,7 +97,7 @@ export type {
97
97
  InputStatus as TextInputStatus,
98
98
  InputStatusType as TextInputStatusType,
99
99
  } from '../Field';
100
- import {mergeProps, mergeRefs} from '../utils';
100
+ import {mergeProps} from '../utils';
101
101
  import {useSize} from '../SizeContext/SizeContext';
102
102
  import {useInputContainer} from '../hooks/useInputContainer';
103
103
  import {useInputStatusIcon} from '../hooks/useInputStatusIcon';
@@ -108,6 +108,7 @@ import type {SizeValue} from '../utils/types';
108
108
  import {themeProps} from '../utils/themeProps';
109
109
  import {useTranslator} from '../i18n';
110
110
 
111
+ import {useMergedRefs} from '../hooks/useMergedRefs';
111
112
  export type TextInputType = 'text' | 'password' | 'email';
112
113
 
113
114
  export interface TextInputProps extends Omit<
@@ -421,7 +422,7 @@ export function TextInput({
421
422
  {inputGroup && <VisuallyHidden id={inputLabelID}>{label}</VisuallyHidden>}
422
423
  <input
423
424
  {...rest}
424
- ref={mergeRefs(ref, inputRef)}
425
+ ref={useMergedRefs(ref, inputRef)}
425
426
  id={id}
426
427
  name={isDisabled ? undefined : htmlName}
427
428
  type={type}
@@ -78,7 +78,7 @@ export const docs = {
78
78
  {className: 'astryx-thumbnail'},
79
79
  ],
80
80
  vars: [
81
- {name: '--_thumbnail-hit-inset', description: 'Outset of the remove button\u2019s invisible hit area, applied to a ::after overlay. 0 on a fine pointer; negative on a coarse one, which grows the 20px button to the 24px touch target without changing what is drawn.', default: '0px', private: true},
81
+ {name: '--_thumbnail-hit-inset', description: 'Outset of the remove button\'s invisible hit area, applied to a ::after overlay. 0 on a fine pointer; negative on a coarse one, which grows the 20px button to the 24px touch target without changing what is drawn.', default: '0px', private: true},
82
82
  ],
83
83
  },
84
84
  usage: {
@@ -58,7 +58,6 @@ import {
58
58
  isImeKeyEvent,
59
59
  isTimeInRange,
60
60
  mergeProps,
61
- mergeRefs,
62
61
  getInputARIA,
63
62
  } from '../utils';
64
63
  import type {BaseProps} from '../BaseProps';
@@ -74,6 +73,7 @@ import {useTooltip} from '../Tooltip';
74
73
  import {themeProps} from '../utils/themeProps';
75
74
  import {useTranslator} from '../i18n';
76
75
 
76
+ import {useMergedRefs} from '../hooks/useMergedRefs';
77
77
  const styles = stylex.create({
78
78
  icon: {
79
79
  display: 'flex',
@@ -648,7 +648,7 @@ export function TimeInput({
648
648
  </VisuallyHidden>
649
649
  )}
650
650
  <input
651
- ref={mergeRefs(ref, inputRef)}
651
+ ref={useMergedRefs(ref, inputRef)}
652
652
  id={id}
653
653
  type="text"
654
654
  value={displayValue}
@@ -20,7 +20,7 @@ import {lazy, Suspense, useEffect, useRef, useState} from 'react';
20
20
  import * as stylex from '@stylexjs/stylex';
21
21
  import {Text} from '../Text';
22
22
  import type {TextType, TextSize, TextColor, TextWeight} from '../theme/types';
23
- import {mergeProps, mergeRefs} from '../utils';
23
+ import {mergeProps} from '../utils';
24
24
  import {useDevWarning} from '../hooks/useDevWarning';
25
25
  import {useTranslator} from '../i18n';
26
26
  import {useLocale} from '../i18n/useLocale';
@@ -33,6 +33,7 @@ import type {
33
33
  TimestampTooltipLine,
34
34
  } from './tooltipEntries';
35
35
 
36
+ import {useMergedRefs} from '../hooks/useMergedRefs';
36
37
  // Load the overlay lazily so a card-less Timestamp — the default — never
37
38
  // bundles HoverCard or the copy affordance's Icon/IconButton. Mirrors the code
38
39
  // split the read-only Tooltip path used before it.
@@ -422,6 +423,7 @@ export function Timestamp({
422
423
  const t = useTranslator();
423
424
  const locale = useLocale();
424
425
  const timeRef = useRef<HTMLTimeElement>(null);
426
+ const mergedTimeRef = useMergedRefs(ref, timeRef);
425
427
  const [now, setNow] = useState(() => new Date());
426
428
 
427
429
  const date = parseValue(value);
@@ -526,7 +528,7 @@ export function Timestamp({
526
528
  xstyle={xstyle}
527
529
  {...timestampProps}>
528
530
  <time
529
- ref={mergeRefs(ref, timeRef)}
531
+ ref={mergedTimeRef}
530
532
  dateTime={isoString}
531
533
  // `ariaLabelText` is '' only for an invalid date, which bails out
532
534
  // before rendering — but keep the guard local: an empty aria-label
@@ -60,6 +60,12 @@ export const docs = {
60
60
  type: '(reason: "auto" | "manual") => void',
61
61
  description: 'Callback fired when the toast is removed.',
62
62
  },
63
+ {
64
+ name: 'onDismiss',
65
+ type: '(reason: "auto" | "manual") => void',
66
+ description: 'Callback fired when the toast is dismissed.',
67
+ required: true,
68
+ },
63
69
  ], theming: {
64
70
  targets: [
65
71
  {className: 'astryx-toast', visualProps: ['type']},
@@ -27,10 +27,10 @@
27
27
 
28
28
  import type {ReactNode, CSSProperties, Ref, HTMLAttributes} from 'react';
29
29
  import {useRef} from 'react';
30
- import {mergeRefs} from '../utils';
31
30
  import {useClickableContainer} from '../hooks/useClickableContainer';
32
31
  import type {LinkComponentType} from '../Link/types';
33
32
 
33
+ import {useMergedRefs} from '../hooks/useMergedRefs';
34
34
  export interface TokenLinkProps extends HTMLAttributes<HTMLElement> {
35
35
  /** Ref forwarded to the container element. */
36
36
  ref?: Ref<HTMLElement>;
@@ -86,7 +86,7 @@ export function TokenLink({
86
86
 
87
87
  return (
88
88
  <span
89
- ref={mergeRefs(ref, containerRef)}
89
+ ref={useMergedRefs(ref, containerRef)}
90
90
  onClick={isDisabled ? undefined : onClick}
91
91
  onMouseUp={isDisabled ? undefined : onMouseUp}
92
92
  {...containerProps}>
@@ -201,6 +201,22 @@ export const docs = {
201
201
  description:
202
202
  'Width of the field (number = pixels, string used as-is, e.g. "100%"). Sizes the whole field (label, control, and status) so they stay aligned.',
203
203
  },
204
+ {
205
+ name: 'tokenOverflowBehavior',
206
+ type: "'none' | 'unfocusedInline' | 'unfocusedLayer'",
207
+ description: 'Controls how tokens overflow when the container is too narrow.',
208
+ default: "'none'",
209
+ },
210
+ {
211
+ name: 'onFocus',
212
+ type: '(e: FocusEvent<HTMLInputElement>) => void',
213
+ description: 'Fires when focus enters the tokenizer input.',
214
+ },
215
+ {
216
+ name: 'onBlur',
217
+ type: '(e: FocusEvent<HTMLInputElement>) => void',
218
+ description: 'Fires when focus leaves the tokenizer input.',
219
+ },
204
220
  {
205
221
  name: 'xstyle',
206
222
  type: 'StyleXStyles',
@@ -133,6 +133,11 @@ export const docs = {
133
133
  description: 'Visual variant passed to Section.',
134
134
  default: "'transparent'",
135
135
  },
136
+ {
137
+ name: 'dividers',
138
+ type: "Array<'top' | 'bottom' | 'start' | 'end'>",
139
+ description: 'Borders/dividers rendered around the toolbar.',
140
+ },
136
141
  {
137
142
  name: 'xstyle',
138
143
  type: 'StyleXStyles',
@@ -96,6 +96,11 @@ export const docs = {
96
96
  type: 'boolean',
97
97
  description: 'Whether the tooltip should be shown on mount. Still dismissible.',
98
98
  },
99
+ {
100
+ name: 'isOpen',
101
+ type: 'boolean',
102
+ description: 'Controlled open state for the tooltip.',
103
+ },
99
104
  ],
100
105
  },
101
106
  ],
@@ -276,6 +276,38 @@ describe('Tooltip', () => {
276
276
  expect(onOpenChange).not.toHaveBeenCalledWith(false);
277
277
  });
278
278
  });
279
+
280
+ describe('controlled', () => {
281
+ // Escape asks a controlled tooltip to close by calling this same handler,
282
+ // so a consumer who complies sees the request and then this echo. Pinning
283
+ // the echo here keeps the two straight.
284
+ it('echoes the close through onOpenChange when the consumer flips isOpen', async () => {
285
+ const onOpenChange = vi.fn();
286
+ const {rerender} = render(
287
+ <Tooltip content="Pinned" isOpen onOpenChange={onOpenChange} delay={0}>
288
+ <button type="button">Trigger</button>
289
+ </Tooltip>,
290
+ );
291
+ await waitFor(() => {
292
+ expect(onOpenChange).toHaveBeenCalledWith(true);
293
+ });
294
+ onOpenChange.mockClear();
295
+
296
+ rerender(
297
+ <Tooltip
298
+ content="Pinned"
299
+ isOpen={false}
300
+ onOpenChange={onOpenChange}
301
+ delay={0}>
302
+ <button type="button">Trigger</button>
303
+ </Tooltip>,
304
+ );
305
+
306
+ await waitFor(() => {
307
+ expect(onOpenChange).toHaveBeenCalledWith(false);
308
+ });
309
+ });
310
+ });
279
311
  describe('touch', () => {
280
312
  // The modality is document-global; a tap in one case must not decide the
281
313
  // next one's answer.
@@ -146,6 +146,11 @@ export interface TooltipProps {
146
146
  * - `true`: force-show the tooltip (hover/focus hide is suppressed)
147
147
  * - `false`: force-hide the tooltip
148
148
  * - `undefined`: uncontrolled — hover/focus triggers manage visibility
149
+ *
150
+ * A controlled tooltip still takes Escape when it is the top-most layer, and
151
+ * answers by calling `onOpenChange(false)` without hiding itself — closing is
152
+ * your update's decision, exactly as for a controlled Dialog. Ignore the call
153
+ * and the tip stays, and so does the press: nothing underneath dismisses.
149
154
  */
150
155
  isOpen?: boolean;
151
156
 
@@ -31,8 +31,8 @@ import {
31
31
  type LayerTouchTrigger,
32
32
  } from '../Layer/useTouchTrigger';
33
33
  import {layerAnimations} from '../Layer/layerAnimations.stylex';
34
+ import {useLayerDismissal} from '../Layer/useLayerDismissal';
34
35
  import {themeProps} from '../utils/themeProps';
35
- import {isImeKeyEvent} from '../utils/ime';
36
36
  import {
37
37
  colorVars,
38
38
  radiusVars,
@@ -144,6 +144,11 @@ export interface TooltipOptions {
144
144
  * - `true`: force-show the tooltip (hover/focus hide is suppressed)
145
145
  * - `false`: force-hide the tooltip
146
146
  * - `undefined`: uncontrolled — hover/focus triggers manage visibility
147
+ *
148
+ * A controlled tooltip still takes Escape when it is the top-most layer, and
149
+ * answers by calling `onHide` without hiding itself — closing is your
150
+ * update's decision, exactly as for a controlled Dialog. Ignore the call and
151
+ * the tip stays, and so does the press: nothing underneath dismisses.
147
152
  */
148
153
  isOpen?: boolean;
149
154
 
@@ -518,34 +523,52 @@ export function useTooltip(options: TooltipOptions = {}): TooltipReturn {
518
523
  }
519
524
  }, [isOpen, clearTimeouts, layer]);
520
525
 
521
- // Dismiss on Escape (WCAG 1.4.13 — dismissible). Uncontrolled tooltips only;
522
- // a controlled tooltip's visibility is owned by the consumer. The listener is
523
- // mounted for the lifetime of an uncontrolled tooltip rather than gated on
524
- // `layer.isOpen` (React state, which can lag a frame behind the DOM) —
525
- // `layer.hide()` self-guards and no-ops when the layer is already closed.
526
- // Guarded against IME composition-cancel.
527
- useEffect(() => {
528
- if (isOpen !== undefined) {
529
- return;
530
- }
531
- const handleKeyDown = (e: KeyboardEvent) => {
532
- if (e.key !== 'Escape') {
533
- return;
526
+ // Dismiss on Escape (WCAG 1.4.13 — dismissible) through the shared layer
527
+ // stack. A visible tip is the top-most layer, so it takes the press and
528
+ // consumes it: Escape hides the tip and leaves the dialog underneath open.
529
+ // The user presses Escape again to close that. Consuming (rather than also
530
+ // dismissing what is beneath) keeps one rule with no per-component
531
+ // exceptions, and the failure mode is one extra keystroke instead of a
532
+ // dialog closing under someone who only wanted the tip gone.
533
+ //
534
+ // A controlled tooltip stays on the stack and takes the press like any other
535
+ // layer, but answers it by reporting instead of hiding: `isOpen` is the
536
+ // consumer's value, so only their update may change it. Same contract as a
537
+ // controlled Dialog.
538
+ useLayerDismissal({
539
+ // Registered for the hook's lifetime rather than gated on `layer.isOpen`:
540
+ // that state can lag a frame behind the DOM, so a press arriving right after
541
+ // the layer appears would find nothing registered. Because this layer
542
+ // CONSUMES the press, a stale registration would be worse than a missed one
543
+ // — it would silently eat Escapes meant for the dialog underneath — so
544
+ // presence is answered from the DOM at press time instead of from state.
545
+ isActive: true,
546
+ isPresent: () => {
547
+ const el =
548
+ typeof document === 'undefined'
549
+ ? null
550
+ : document.getElementById(layer.id);
551
+ if (el == null) {
552
+ return false;
534
553
  }
535
- if (isImeKeyEvent(e)) {
536
- // Ignore Escape that is committing/cancelling an IME composition;
537
- // see utils/ime.ts for why.
538
- return;
554
+ try {
555
+ return el.matches(':popover-open');
556
+ } catch {
557
+ // Browsers without the Popover API (and some test environments) cannot
558
+ // answer the selector; fall back to the hook's own state.
559
+ return layer.isOpen;
539
560
  }
561
+ },
562
+ onDismiss: () => {
540
563
  clearTimeouts();
541
564
  clearTapOpen();
565
+ if (isOpen !== undefined) {
566
+ onHide?.();
567
+ return;
568
+ }
542
569
  layer.hide();
543
- };
544
- document.addEventListener('keydown', handleKeyDown);
545
- return () => {
546
- document.removeEventListener('keydown', handleKeyDown);
547
- };
548
- }, [isOpen, layer, clearTimeouts, clearTapOpen]);
570
+ },
571
+ });
549
572
 
550
573
  // Render function that wraps layer.render with tooltip styling
551
574
  const renderTooltip = useCallback(
@@ -53,7 +53,7 @@ import {usePopover} from '../Popover/usePopover';
53
53
  import {useMenuHover} from '../hooks/useMenuHover';
54
54
  import {Grid} from '../Grid/Grid';
55
55
  import {Icon} from '../Icon';
56
- import {mergeProps, mergeRefs} from '../utils';
56
+ import {mergeProps} from '../utils';
57
57
  import type {BaseProps} from '../BaseProps';
58
58
  import {navItemStyles} from '../NavItem/navItemStyles.stylex';
59
59
  import {useTopNavSlot} from './TopNavContext';
@@ -61,6 +61,7 @@ import {useTopNavRenderMode} from './TopNavRenderContext';
61
61
  import {themeProps} from '../utils/themeProps';
62
62
  import {focusOutlineProps} from '../utils/focusOutline.stylex';
63
63
 
64
+ import {useMergedRefs} from '../hooks/useMergedRefs';
64
65
  // =============================================================================
65
66
  // Styles
66
67
  // =============================================================================
@@ -451,7 +452,7 @@ function DefaultMegaMenu({
451
452
  return (
452
453
  <>
453
454
  <button
454
- ref={mergeRefs(triggerButtonRef, setTriggerEl, ref)}
455
+ ref={useMergedRefs(triggerButtonRef, setTriggerEl, ref)}
455
456
  type="button"
456
457
  {...popover.triggerProps}
457
458
  {...hoverTriggerProps}
@@ -41,12 +41,13 @@ import {
41
41
  fontWeightVars,
42
42
  typeScaleVars,
43
43
  } from '../theme/tokens.stylex';
44
- import {getKey, mergeProps, mergeRefs} from '../utils';
44
+ import {getKey, mergeProps} from '../utils';
45
45
  import type {BaseProps} from '../BaseProps';
46
46
  import type {SearchableItem, SearchSource} from './types';
47
47
  import {themeProps} from '../utils/themeProps';
48
48
  import {useTranslator} from '../i18n';
49
49
 
50
+ import {useMergedRefs} from '../hooks/useMergedRefs';
50
51
  // =============================================================================
51
52
  // Types
52
53
  // =============================================================================
@@ -760,7 +761,7 @@ export const BaseTypeahead = function BaseTypeahead<T extends SearchableItem>({
760
761
  return (
761
762
  <>
762
763
  <input
763
- ref={mergeRefs(ref, inputRef, fallbackAnchorRef)}
764
+ ref={useMergedRefs(ref, inputRef, fallbackAnchorRef)}
764
765
  id={inputId}
765
766
  type="text"
766
767
  role="combobox"
@@ -171,6 +171,11 @@ export const docs = {
171
171
  description:
172
172
  'Width of the field (number = pixels, string used as-is, e.g. "100%"). Sizes the whole field (label, control, and status) so they stay aligned.',
173
173
  },
174
+ {
175
+ name: 'startIcon',
176
+ type: 'IconType | ReactNode',
177
+ description: 'SVG icon component displayed at the start of the input.',
178
+ },
174
179
  {
175
180
  name: 'xstyle',
176
181
  type: 'StyleXStyles',
@@ -45,13 +45,14 @@ import {VisuallyHidden} from '../VisuallyHidden';
45
45
  import {spacingVars, sizeVars} from '../theme/tokens.stylex';
46
46
  import {groupStyles} from '../InputGroup/groupStyles';
47
47
  import {useInputGroup} from '../InputGroup/InputGroupContext';
48
- import {getInputARIA, isImeKeyEvent, mergeProps, mergeRefs} from '../utils';
48
+ import {getInputARIA, isImeKeyEvent, mergeProps} from '../utils';
49
49
  import type {BaseProps} from '../BaseProps';
50
50
  import type {SizeValue} from '../utils/types';
51
51
  import type {SearchableItem, SearchSource} from './types';
52
52
  import {themeProps} from '../utils/themeProps';
53
53
  import {useTranslator} from '../i18n';
54
54
 
55
+ import {useMergedRefs} from '../hooks/useMergedRefs';
55
56
  export type {
56
57
  InputStatus as TypeaheadStatus,
57
58
  InputStatusType as TypeaheadStatusType,
@@ -418,7 +419,7 @@ export function Typeahead<T extends SearchableItem>({
418
419
  const typeaheadContent = (
419
420
  <>
420
421
  <div
421
- ref={mergeRefs(
422
+ ref={useMergedRefs(
422
423
  wrapperRef,
423
424
  disabledMessageTooltip.ref,
424
425
  inputGroup ? ref : undefined,