@astryxdesign/core 0.4.6 → 0.4.7-canary.019ae5a

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 (486) hide show
  1. package/CHANGELOG.md +37 -1
  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/Avatar/Avatar.d.ts.map +1 -1
  8. package/dist/Avatar/Avatar.js +15 -10
  9. package/dist/AvatarGroup/AvatarGroup.d.ts.map +1 -1
  10. package/dist/AvatarGroup/AvatarGroup.js +3 -2
  11. package/dist/Banner/Banner.d.ts +40 -15
  12. package/dist/Banner/Banner.d.ts.map +1 -1
  13. package/dist/Banner/Banner.js +51 -18
  14. package/dist/BottomSheet/BottomSheet.d.ts +1 -0
  15. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
  16. package/dist/BottomSheet/BottomSheet.js +54 -15
  17. package/dist/BottomSheet/BottomSheetEdgeTint.d.ts +6 -0
  18. package/dist/BottomSheet/BottomSheetEdgeTint.d.ts.map +1 -0
  19. package/dist/BottomSheet/BottomSheetEdgeTint.js +62 -0
  20. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
  21. package/dist/BottomSheet/BottomSheetPanel.js +1 -0
  22. package/dist/BottomSheet/BottomSheetSwitcher.d.ts +1 -0
  23. package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
  24. package/dist/BottomSheet/BottomSheetSwitcher.js +13 -6
  25. package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -1
  26. package/dist/BottomSheet/useSheetGestures.js +23 -5
  27. package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
  28. package/dist/Breadcrumbs/BreadcrumbItem.js +11 -10
  29. package/dist/Button/Button.d.ts.map +1 -1
  30. package/dist/Button/Button.js +5 -4
  31. package/dist/ButtonGroup/ButtonGroup.d.ts +5 -1
  32. package/dist/ButtonGroup/ButtonGroup.d.ts.map +1 -1
  33. package/dist/ButtonGroup/ButtonGroup.js +26 -7
  34. package/dist/Calendar/Calendar.d.ts +3 -1
  35. package/dist/Calendar/Calendar.d.ts.map +1 -1
  36. package/dist/Calendar/Calendar.js +13 -14
  37. package/dist/Calendar/getInitialFocusDate.d.ts +46 -0
  38. package/dist/Calendar/getInitialFocusDate.d.ts.map +1 -0
  39. package/dist/Calendar/getInitialFocusDate.js +62 -0
  40. package/dist/Carousel/Carousel.d.ts.map +1 -1
  41. package/dist/Carousel/Carousel.js +3 -3
  42. package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
  43. package/dist/Chat/ChatComposerInput.js +2 -3
  44. package/dist/Chat/ChatLayout.d.ts.map +1 -1
  45. package/dist/Chat/ChatLayout.js +3 -3
  46. package/dist/Chat/ChatMessageList.d.ts +22 -3
  47. package/dist/Chat/ChatMessageList.d.ts.map +1 -1
  48. package/dist/Chat/ChatMessageList.js +6 -3
  49. package/dist/Chat/ChatTokenizedText.d.ts +2 -0
  50. package/dist/Chat/ChatTokenizedText.d.ts.map +1 -1
  51. package/dist/Chat/ChatTokenizedText.js +11 -6
  52. package/dist/Chat/useChatComposerTokens.js +1 -1
  53. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  54. package/dist/CheckboxInput/CheckboxInput.js +3 -2
  55. package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
  56. package/dist/ClickableCard/ClickableCard.js +3 -3
  57. package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
  58. package/dist/CommandPalette/CommandPaletteInput.js +3 -2
  59. package/dist/CommandPalette/CommandPaletteItem.d.ts.map +1 -1
  60. package/dist/CommandPalette/CommandPaletteItem.js +3 -2
  61. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  62. package/dist/ContextMenu/ContextMenu.js +4 -3
  63. package/dist/DateInput/DateInput.d.ts.map +1 -1
  64. package/dist/DateInput/DateInput.js +3 -2
  65. package/dist/DateInput/TouchDateField.d.ts.map +1 -1
  66. package/dist/DateInput/TouchDateField.js +38 -4
  67. package/dist/DateTimeInput/DateTimeInput.d.ts +21 -1
  68. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  69. package/dist/DateTimeInput/DateTimeInput.js +406 -12
  70. package/dist/DateTimeInput/index.d.ts +1 -1
  71. package/dist/DateTimeInput/index.d.ts.map +1 -1
  72. package/dist/Dialog/Dialog.d.ts +7 -1
  73. package/dist/Dialog/Dialog.d.ts.map +1 -1
  74. package/dist/Dialog/Dialog.js +89 -58
  75. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
  76. package/dist/DropdownMenu/DropdownMenuSubMenu.js +6 -5
  77. package/dist/FileInput/FileInput.d.ts.map +1 -1
  78. package/dist/FileInput/FileInput.js +3 -2
  79. package/dist/Heading/Heading.d.ts.map +1 -1
  80. package/dist/Heading/Heading.js +6 -2
  81. package/dist/HoverCard/HoverCard.d.ts +6 -0
  82. package/dist/HoverCard/HoverCard.d.ts.map +1 -1
  83. package/dist/HoverCard/useHoverCard.d.ts +5 -0
  84. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  85. package/dist/HoverCard/useHoverCard.js +67 -25
  86. package/dist/Item/Item.d.ts.map +1 -1
  87. package/dist/Item/Item.js +4 -2
  88. package/dist/Kbd/Kbd.d.ts.map +1 -1
  89. package/dist/Kbd/Kbd.js +10 -3
  90. package/dist/Layer/LayerDepthContext.d.ts +52 -0
  91. package/dist/Layer/LayerDepthContext.d.ts.map +1 -0
  92. package/dist/Layer/LayerDepthContext.js +66 -0
  93. package/dist/Layer/index.d.ts +3 -0
  94. package/dist/Layer/index.d.ts.map +1 -1
  95. package/dist/Layer/index.js +4 -0
  96. package/dist/Layer/layerStack.d.ts +98 -0
  97. package/dist/Layer/layerStack.d.ts.map +1 -0
  98. package/dist/Layer/layerStack.js +306 -0
  99. package/dist/Layer/useLayerDismissal.d.ts +93 -0
  100. package/dist/Layer/useLayerDismissal.d.ts.map +1 -0
  101. package/dist/Layer/useLayerDismissal.js +91 -0
  102. package/dist/Lightbox/Lightbox.d.ts +1 -0
  103. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  104. package/dist/Lightbox/Lightbox.js +103 -81
  105. package/dist/Markdown/index.d.ts +1 -1
  106. package/dist/Markdown/index.d.ts.map +1 -1
  107. package/dist/Markdown/parser.d.ts +38 -1
  108. package/dist/Markdown/parser.d.ts.map +1 -1
  109. package/dist/Markdown/parser.js +177 -29
  110. package/dist/MobileNav/MobileNav.d.ts +6 -1
  111. package/dist/MobileNav/MobileNav.d.ts.map +1 -1
  112. package/dist/MobileNav/MobileNav.js +61 -36
  113. package/dist/MultiSelector/MultiSelector.d.ts +15 -1
  114. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  115. package/dist/MultiSelector/MultiSelector.js +17 -6
  116. package/dist/MultiSelector/index.d.ts +1 -1
  117. package/dist/MultiSelector/index.d.ts.map +1 -1
  118. package/dist/NavMenu/NavHeadingMenu.d.ts.map +1 -1
  119. package/dist/NavMenu/NavHeadingMenu.js +3 -2
  120. package/dist/Outline/Outline.d.ts.map +1 -1
  121. package/dist/Outline/Outline.js +3 -2
  122. package/dist/OverflowList/OverflowList.d.ts.map +1 -1
  123. package/dist/OverflowList/OverflowList.js +3 -2
  124. package/dist/Overlay/Overlay.d.ts.map +1 -1
  125. package/dist/Overlay/Overlay.js +3 -2
  126. package/dist/Popover/usePopover.d.ts.map +1 -1
  127. package/dist/Popover/usePopover.js +20 -17
  128. package/dist/PowerSearch/PowerSearch.d.ts +8 -2
  129. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  130. package/dist/PowerSearch/PowerSearch.js +19 -6
  131. package/dist/PowerSearch/PowerSearchEditPopover.d.ts +3 -1
  132. package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
  133. package/dist/PowerSearch/PowerSearchEditPopover.js +28 -6
  134. package/dist/PowerSearch/PowerSearchValueEditor.d.ts +3 -1
  135. package/dist/PowerSearch/PowerSearchValueEditor.d.ts.map +1 -1
  136. package/dist/PowerSearch/PowerSearchValueEditor.js +19 -9
  137. package/dist/PowerSearch/types.d.ts +2 -0
  138. package/dist/PowerSearch/types.d.ts.map +1 -1
  139. package/dist/PowerSearch/usePowerSearchSource.d.ts +6 -1
  140. package/dist/PowerSearch/usePowerSearchSource.d.ts.map +1 -1
  141. package/dist/PowerSearch/usePowerSearchSource.js +14 -5
  142. package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
  143. package/dist/Resizable/ResizeHandle.js +3 -2
  144. package/dist/SegmentedControl/SegmentedControl.d.ts.map +1 -1
  145. package/dist/SegmentedControl/SegmentedControl.js +3 -2
  146. package/dist/SelectableCard/SelectableCard.d.ts.map +1 -1
  147. package/dist/SelectableCard/SelectableCard.js +3 -3
  148. package/dist/Selector/Selector.d.ts.map +1 -1
  149. package/dist/Selector/Selector.js +5 -0
  150. package/dist/SideNav/SideNav.d.ts.map +1 -1
  151. package/dist/SideNav/SideNav.js +4 -3
  152. package/dist/SideNav/SideNavHeading.d.ts.map +1 -1
  153. package/dist/SideNav/SideNavHeading.js +6 -5
  154. package/dist/SideNav/SideNavItem.d.ts.map +1 -1
  155. package/dist/SideNav/SideNavItem.js +4 -3
  156. package/dist/Slider/Slider.d.ts.map +1 -1
  157. package/dist/Slider/Slider.js +3 -3
  158. package/dist/Spinner/Spinner.d.ts +3 -3
  159. package/dist/Spinner/Spinner.d.ts.map +1 -1
  160. package/dist/Spinner/Spinner.js +130 -78
  161. package/dist/Stepper/Step.d.ts +140 -0
  162. package/dist/Stepper/Step.d.ts.map +1 -0
  163. package/dist/Stepper/Step.js +1045 -0
  164. package/dist/Stepper/StepStatus.d.ts +24 -0
  165. package/dist/Stepper/StepStatus.d.ts.map +1 -0
  166. package/dist/Stepper/StepStatus.js +1 -0
  167. package/dist/Stepper/Stepper.d.ts +93 -0
  168. package/dist/Stepper/Stepper.d.ts.map +1 -0
  169. package/dist/Stepper/Stepper.js +184 -0
  170. package/dist/Stepper/StepperContext.d.ts +41 -0
  171. package/dist/Stepper/StepperContext.d.ts.map +1 -0
  172. package/dist/Stepper/StepperContext.js +34 -0
  173. package/dist/Stepper/index.d.ts +8 -0
  174. package/dist/Stepper/index.d.ts.map +1 -0
  175. package/dist/Stepper/index.js +7 -0
  176. package/dist/Stepper/stepper.stylex.d.ts +15 -0
  177. package/dist/Stepper/stepper.stylex.d.ts.map +1 -0
  178. package/dist/Stepper/stepper.stylex.js +20 -0
  179. package/dist/Switch/Switch.d.ts.map +1 -1
  180. package/dist/Switch/Switch.js +3 -2
  181. package/dist/TabList/Tab.d.ts +15 -2
  182. package/dist/TabList/Tab.d.ts.map +1 -1
  183. package/dist/TabList/Tab.js +32 -3
  184. package/dist/TabList/TabList.d.ts +46 -7
  185. package/dist/TabList/TabList.d.ts.map +1 -1
  186. package/dist/TabList/TabList.js +353 -23
  187. package/dist/TabList/TabListContext.d.ts +11 -1
  188. package/dist/TabList/TabListContext.d.ts.map +1 -1
  189. package/dist/TabList/TabListContext.js +11 -2
  190. package/dist/TabList/TabMenu.d.ts.map +1 -1
  191. package/dist/TabList/TabMenu.js +3 -2
  192. package/dist/TabList/index.d.ts +2 -2
  193. package/dist/TabList/index.d.ts.map +1 -1
  194. package/dist/Table/BaseTable.d.ts.map +1 -1
  195. package/dist/Table/BaseTable.js +4 -1
  196. package/dist/Table/TableRow.d.ts +16 -5
  197. package/dist/Table/TableRow.d.ts.map +1 -1
  198. package/dist/Table/TableRow.js +16 -5
  199. package/dist/Table/plugins/groupedRows/useTableGroupedRows.d.ts.map +1 -1
  200. package/dist/Table/plugins/groupedRows/useTableGroupedRows.js +20 -8
  201. package/dist/Table/plugins/pagination/useTablePagination.d.ts +3 -0
  202. package/dist/Table/plugins/pagination/useTablePagination.d.ts.map +1 -1
  203. package/dist/Table/plugins/pagination/useTablePagination.js +21 -4
  204. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
  205. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +7 -2
  206. package/dist/Table/plugins/rowStatus/useTableRowStatus.d.ts.map +1 -1
  207. package/dist/Table/plugins/rowStatus/useTableRowStatus.js +10 -3
  208. package/dist/Table/plugins/selection/useTableSelection.d.ts +16 -0
  209. package/dist/Table/plugins/selection/useTableSelection.d.ts.map +1 -1
  210. package/dist/Table/plugins/selection/useTableSelection.js +19 -5
  211. package/dist/Table/types.d.ts +22 -4
  212. package/dist/Table/types.d.ts.map +1 -1
  213. package/dist/Table/useBaseTablePlugins.d.ts.map +1 -1
  214. package/dist/Table/useBaseTablePlugins.js +5 -0
  215. package/dist/Text/Text.d.ts.map +1 -1
  216. package/dist/Text/Text.js +6 -2
  217. package/dist/TextArea/TextArea.d.ts.map +1 -1
  218. package/dist/TextArea/TextArea.js +3 -2
  219. package/dist/TextInput/TextInput.d.ts.map +1 -1
  220. package/dist/TextInput/TextInput.js +3 -2
  221. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  222. package/dist/TimeInput/TimeInput.js +3 -2
  223. package/dist/Timestamp/Timestamp.d.ts.map +1 -1
  224. package/dist/Timestamp/Timestamp.js +5 -3
  225. package/dist/Token/TokenLink.d.ts.map +1 -1
  226. package/dist/Token/TokenLink.js +2 -2
  227. package/dist/Tokenizer/Tokenizer.d.ts +3 -1
  228. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  229. package/dist/Tokenizer/Tokenizer.js +2 -0
  230. package/dist/Tooltip/Tooltip.d.ts +5 -0
  231. package/dist/Tooltip/Tooltip.d.ts.map +1 -1
  232. package/dist/Tooltip/useTooltip.d.ts +5 -0
  233. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  234. package/dist/Tooltip/useTooltip.js +39 -24
  235. package/dist/TopNav/TopNavHeading.d.ts.map +1 -1
  236. package/dist/TopNav/TopNavHeading.js +3 -2
  237. package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
  238. package/dist/TopNav/TopNavMegaMenu.js +3 -3
  239. package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
  240. package/dist/TopNav/TopNavMenu.js +4 -3
  241. package/dist/Typeahead/BaseTypeahead.d.ts +2 -0
  242. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  243. package/dist/Typeahead/BaseTypeahead.js +68 -29
  244. package/dist/Typeahead/Typeahead.d.ts.map +1 -1
  245. package/dist/Typeahead/Typeahead.js +3 -2
  246. package/dist/astryx.css +72 -2
  247. package/dist/hooks/index.d.ts +1 -0
  248. package/dist/hooks/index.d.ts.map +1 -1
  249. package/dist/hooks/index.js +1 -0
  250. package/dist/hooks/useFocusTrap.d.ts +7 -4
  251. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  252. package/dist/hooks/useFocusTrap.js +60 -95
  253. package/dist/hooks/useHotkeys.d.ts.map +1 -1
  254. package/dist/hooks/useHotkeys.js +10 -3
  255. package/dist/hooks/useListFocus.d.ts +5 -2
  256. package/dist/hooks/useListFocus.d.ts.map +1 -1
  257. package/dist/hooks/useListFocus.js +12 -6
  258. package/dist/hooks/useMergedRefs.d.ts +18 -0
  259. package/dist/hooks/useMergedRefs.d.ts.map +1 -0
  260. package/dist/hooks/useMergedRefs.js +25 -0
  261. package/dist/index.d.ts +1 -0
  262. package/dist/index.d.ts.map +1 -1
  263. package/dist/index.js +1 -0
  264. package/dist/utils/groupItems.d.ts +5 -1
  265. package/dist/utils/groupItems.d.ts.map +1 -1
  266. package/dist/utils/groupItems.js +9 -9
  267. package/dist/utils/index.d.ts +1 -1
  268. package/dist/utils/index.d.ts.map +1 -1
  269. package/dist/utils/sharedResizeObserver.d.ts.map +1 -1
  270. package/dist/utils/sharedResizeObserver.js +10 -1
  271. package/dist/utils/timeParser.d.ts +1 -1
  272. package/dist/utils/timeParser.d.ts.map +1 -1
  273. package/dist/utils/timeParser.js +26 -12
  274. package/locales/en.json +31 -7
  275. package/locales/pseudo.json +18 -0
  276. package/package.json +8 -3
  277. package/src/AppShell/AppShell.tsx +3 -2
  278. package/src/AspectRatio/AspectRatio.doc.mjs +6 -3
  279. package/src/AspectRatio/AspectRatio.test.tsx +57 -10
  280. package/src/AspectRatio/AspectRatio.tsx +40 -1
  281. package/src/Avatar/Avatar.doc.mjs +2 -1
  282. package/src/Avatar/Avatar.test.tsx +17 -0
  283. package/src/Avatar/Avatar.tsx +61 -38
  284. package/src/AvatarGroup/AvatarGroup.tsx +3 -2
  285. package/src/Banner/Banner.doc.mjs +13 -11
  286. package/src/Banner/Banner.test.tsx +102 -15
  287. package/src/Banner/Banner.tsx +72 -25
  288. package/src/BottomSheet/BottomSheet.test.tsx +123 -0
  289. package/src/BottomSheet/BottomSheet.tsx +36 -4
  290. package/src/BottomSheet/BottomSheetEdgeTint.test.tsx +225 -0
  291. package/src/BottomSheet/BottomSheetEdgeTint.tsx +82 -0
  292. package/src/BottomSheet/BottomSheetPanel.test.tsx +66 -0
  293. package/src/BottomSheet/BottomSheetPanel.tsx +19 -0
  294. package/src/BottomSheet/BottomSheetSwitcher.tsx +16 -2
  295. package/src/BottomSheet/useSheetGestures.test.ts +27 -0
  296. package/src/BottomSheet/useSheetGestures.ts +25 -5
  297. package/src/Breadcrumbs/BreadcrumbItem.tsx +9 -5
  298. package/src/Button/Button.doc.mjs +22 -0
  299. package/src/Button/Button.test.tsx +13 -0
  300. package/src/Button/Button.tsx +5 -4
  301. package/src/ButtonGroup/ButtonGroup.doc.mjs +10 -1
  302. package/src/ButtonGroup/ButtonGroup.test.tsx +391 -1
  303. package/src/ButtonGroup/ButtonGroup.tsx +23 -5
  304. package/src/Calendar/Calendar.doc.mjs +4 -3
  305. package/src/Calendar/Calendar.test.tsx +52 -0
  306. package/src/Calendar/Calendar.tsx +18 -15
  307. package/src/Calendar/getInitialFocusDate.test.ts +94 -0
  308. package/src/Calendar/getInitialFocusDate.ts +93 -0
  309. package/src/Carousel/Carousel.tsx +3 -2
  310. package/src/Chat/ChatComposerInput.tsx +5 -1
  311. package/src/Chat/ChatLayout.tsx +3 -2
  312. package/src/Chat/ChatMessageList.doc.mjs +9 -1
  313. package/src/Chat/ChatMessageList.test.tsx +46 -0
  314. package/src/Chat/ChatMessageList.tsx +28 -4
  315. package/src/Chat/ChatTokenizedText.tsx +17 -10
  316. package/src/Chat/useChatComposerTokens.ts +1 -1
  317. package/src/CheckboxInput/CheckboxInput.tsx +3 -2
  318. package/src/ClickableCard/ClickableCard.tsx +3 -2
  319. package/src/CodeBlock/CodeBlock.doc.mjs +6 -0
  320. package/src/CommandPalette/CommandPaletteInput.tsx +3 -2
  321. package/src/CommandPalette/CommandPaletteItem.tsx +3 -2
  322. package/src/ContextMenu/ContextMenu.doc.mjs +5 -0
  323. package/src/ContextMenu/ContextMenu.tsx +4 -3
  324. package/src/DateInput/DateInput.tsx +3 -2
  325. package/src/DateInput/DateInputTouch.test.tsx +36 -0
  326. package/src/DateInput/TouchDateField.tsx +38 -4
  327. package/src/DateTimeInput/DateTimeInput.doc.mjs +18 -0
  328. package/src/DateTimeInput/DateTimeInput.test.tsx +1088 -0
  329. package/src/DateTimeInput/DateTimeInput.tsx +556 -24
  330. package/src/DateTimeInput/index.ts +1 -0
  331. package/src/Dialog/Dialog.doc.mjs +8 -3
  332. package/src/Dialog/Dialog.test.tsx +261 -1
  333. package/src/Dialog/Dialog.tsx +109 -58
  334. package/src/DropdownMenu/DropdownMenu.doc.mjs +6 -0
  335. package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +8 -5
  336. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +8 -11
  337. package/src/Field/Field.doc.mjs +13 -2
  338. package/src/FileInput/FileInput.tsx +3 -2
  339. package/src/Heading/Heading.test.tsx +23 -1
  340. package/src/Heading/Heading.tsx +6 -2
  341. package/src/HoverCard/HoverCard.doc.mjs +5 -0
  342. package/src/HoverCard/HoverCard.tsx +6 -0
  343. package/src/HoverCard/useHoverCard.tsx +81 -30
  344. package/src/Item/Item.tsx +5 -4
  345. package/src/Kbd/Kbd.test.tsx +17 -0
  346. package/src/Kbd/Kbd.tsx +10 -3
  347. package/src/Layer/LayerDepthContext.tsx +62 -0
  348. package/src/Layer/index.ts +9 -0
  349. package/src/Layer/layerDismissalFamilies.test.tsx +359 -0
  350. package/src/Layer/layerDismissalInvariants.test.tsx +417 -0
  351. package/src/Layer/layerStack.ts +362 -0
  352. package/src/Layer/useLayerDismissal.test.tsx +469 -0
  353. package/src/Layer/useLayerDismissal.ts +173 -0
  354. package/src/Layout/Layout.doc.mjs +2 -2
  355. package/src/Lightbox/Lightbox.doc.mjs +12 -0
  356. package/src/Lightbox/Lightbox.tsx +124 -102
  357. package/src/Link/Link.doc.mjs +39 -0
  358. package/src/Markdown/Markdown.doc.mjs +5 -0
  359. package/src/Markdown/incremental.test.ts +16 -2
  360. package/src/Markdown/index.ts +1 -0
  361. package/src/Markdown/parser.perf.test.ts +96 -1
  362. package/src/Markdown/parser.test.ts +145 -2
  363. package/src/Markdown/parser.ts +244 -29
  364. package/src/MobileNav/MobileNav.tsx +62 -36
  365. package/src/MoreMenu/MoreMenu.doc.mjs +7 -1
  366. package/src/MultiSelector/MultiSelector.doc.mjs +27 -0
  367. package/src/MultiSelector/MultiSelector.test.tsx +106 -0
  368. package/src/MultiSelector/MultiSelector.tsx +50 -6
  369. package/src/MultiSelector/index.ts +1 -0
  370. package/src/NavMenu/NavHeadingMenu.tsx +3 -2
  371. package/src/NumberInput/NumberInput.doc.mjs +5 -0
  372. package/src/Outline/Outline.tsx +3 -2
  373. package/src/OverflowList/OverflowList.tsx +3 -2
  374. package/src/Overlay/Overlay.tsx +3 -2
  375. package/src/Popover/Popover.test.tsx +27 -1
  376. package/src/Popover/usePopover.tsx +31 -28
  377. package/src/PowerSearch/PowerSearch.doc.mjs +160 -28
  378. package/src/PowerSearch/PowerSearch.test.tsx +240 -1
  379. package/src/PowerSearch/PowerSearch.tsx +34 -5
  380. package/src/PowerSearch/PowerSearchEditPopover.test.tsx +201 -0
  381. package/src/PowerSearch/PowerSearchEditPopover.tsx +29 -2
  382. package/src/PowerSearch/PowerSearchValueEditor.test.tsx +47 -1
  383. package/src/PowerSearch/PowerSearchValueEditor.tsx +15 -0
  384. package/src/PowerSearch/types.ts +2 -0
  385. package/src/PowerSearch/usePowerSearchSource.test.ts +82 -2
  386. package/src/PowerSearch/usePowerSearchSource.ts +13 -3
  387. package/src/Resizable/ResizeHandle.tsx +3 -2
  388. package/src/SegmentedControl/SegmentedControl.tsx +3 -2
  389. package/src/SelectableCard/SelectableCard.tsx +3 -2
  390. package/src/Selector/Selector.doc.mjs +12 -1
  391. package/src/Selector/Selector.test.tsx +45 -1
  392. package/src/Selector/Selector.tsx +11 -1
  393. package/src/SideNav/SideNav.doc.mjs +6 -6
  394. package/src/SideNav/SideNav.tsx +4 -2
  395. package/src/SideNav/SideNavHeading.tsx +10 -10
  396. package/src/SideNav/SideNavItem.tsx +4 -2
  397. package/src/Slider/Slider.tsx +3 -2
  398. package/src/Spinner/Spinner.test.tsx +147 -1
  399. package/src/Spinner/Spinner.tsx +120 -101
  400. package/src/Stepper/Step.doc.mjs +98 -0
  401. package/src/Stepper/Step.tsx +1638 -0
  402. package/src/Stepper/StepStatus.ts +26 -0
  403. package/src/Stepper/Stepper.doc.mjs +370 -0
  404. package/src/Stepper/Stepper.test.tsx +1024 -0
  405. package/src/Stepper/Stepper.tsx +258 -0
  406. package/src/Stepper/StepperContext.ts +71 -0
  407. package/src/Stepper/index.ts +18 -0
  408. package/src/Stepper/stepper.stylex.ts +19 -0
  409. package/src/Switch/Switch.tsx +3 -2
  410. package/src/TabList/Tab.doc.mjs +13 -1
  411. package/src/TabList/Tab.tsx +67 -3
  412. package/src/TabList/TabList.doc.mjs +22 -6
  413. package/src/TabList/TabList.test.tsx +580 -4
  414. package/src/TabList/TabList.tsx +490 -22
  415. package/src/TabList/TabListContext.ts +13 -2
  416. package/src/TabList/TabMenu.tsx +3 -2
  417. package/src/TabList/index.ts +6 -2
  418. package/src/Table/BaseTable.tsx +6 -3
  419. package/src/Table/Table.doc.mjs +6 -1
  420. package/src/Table/Table.test.tsx +132 -14
  421. package/src/Table/TableBody.doc.mjs +40 -0
  422. package/src/Table/TableFooter.doc.mjs +40 -0
  423. package/src/Table/TableHeader.doc.mjs +40 -0
  424. package/src/Table/TableRow.tsx +16 -5
  425. package/src/Table/plugins/groupedRows/useTableGroupedRows-perf.test.tsx +112 -0
  426. package/src/Table/plugins/groupedRows/useTableGroupedRows.test.tsx +100 -0
  427. package/src/Table/plugins/groupedRows/useTableGroupedRows.tsx +17 -8
  428. package/src/Table/plugins/pagination/useTablePagination.test.tsx +34 -4
  429. package/src/Table/plugins/pagination/useTablePagination.tsx +35 -3
  430. package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +9 -2
  431. package/src/Table/plugins/rowStatus/useTableRowStatus.test.tsx +13 -7
  432. package/src/Table/plugins/rowStatus/useTableRowStatus.tsx +12 -3
  433. package/src/Table/plugins/selection/useTableSelection.test.tsx +76 -0
  434. package/src/Table/plugins/selection/useTableSelection.tsx +40 -7
  435. package/src/Table/types.ts +22 -4
  436. package/src/Table/useBaseTablePlugins.ts +5 -0
  437. package/src/Table/useTableGroupedRows.doc.mjs +5 -4
  438. package/src/Table/useTablePagination.doc.mjs +7 -0
  439. package/src/Table/useTableRowStatus.doc.mjs +1 -1
  440. package/src/Table/useTableSelection.doc.mjs +31 -0
  441. package/src/Text/Text.test.tsx +23 -1
  442. package/src/Text/Text.tsx +6 -2
  443. package/src/TextArea/TextArea.tsx +3 -2
  444. package/src/TextInput/TextInput.doc.mjs +10 -0
  445. package/src/TextInput/TextInput.tsx +3 -2
  446. package/src/Thumbnail/Thumbnail.doc.mjs +1 -1
  447. package/src/TimeInput/TimeInput.tsx +2 -2
  448. package/src/Timestamp/Timestamp.tsx +4 -2
  449. package/src/Toast/Toast.doc.mjs +6 -0
  450. package/src/Token/TokenLink.tsx +2 -2
  451. package/src/Tokenizer/Tokenizer.doc.mjs +27 -0
  452. package/src/Tokenizer/Tokenizer.tsx +4 -0
  453. package/src/Toolbar/Toolbar.doc.mjs +5 -0
  454. package/src/Tooltip/Tooltip.doc.mjs +5 -0
  455. package/src/Tooltip/Tooltip.test.tsx +32 -0
  456. package/src/Tooltip/Tooltip.tsx +5 -0
  457. package/src/Tooltip/useTooltip.tsx +47 -24
  458. package/src/TopNav/TopNavHeading.tsx +3 -2
  459. package/src/TopNav/TopNavMegaMenu.tsx +3 -2
  460. package/src/TopNav/TopNavMenu.tsx +4 -3
  461. package/src/Typeahead/BaseTypeahead.doc.mjs +11 -0
  462. package/src/Typeahead/BaseTypeahead.tsx +85 -33
  463. package/src/Typeahead/Typeahead.doc.mjs +5 -0
  464. package/src/Typeahead/Typeahead.test.tsx +49 -0
  465. package/src/Typeahead/Typeahead.tsx +3 -2
  466. package/src/hooks/index.ts +2 -0
  467. package/src/hooks/useContainerReveal.doc.mjs +2 -2
  468. package/src/hooks/useFocusTrap.test.tsx +14 -0
  469. package/src/hooks/useFocusTrap.ts +62 -111
  470. package/src/hooks/useFocusTrapEscapeShim.test.tsx +165 -0
  471. package/src/hooks/useHotkeys.test.ts +18 -0
  472. package/src/hooks/useHotkeys.ts +10 -3
  473. package/src/hooks/useListFocus.doc.mjs +2 -2
  474. package/src/hooks/useListFocus.test.tsx +65 -3
  475. package/src/hooks/useListFocus.ts +15 -7
  476. package/src/hooks/useMergedRefs.doc.mjs +79 -0
  477. package/src/hooks/useMergedRefs.test.tsx +62 -0
  478. package/src/hooks/useMergedRefs.ts +36 -0
  479. package/src/index.ts +1 -0
  480. package/src/theme/MediaTheme.doc.mjs +5 -5
  481. package/src/utils/groupItems.ts +12 -6
  482. package/src/utils/index.ts +1 -1
  483. package/src/utils/sharedResizeObserver.ts +10 -2
  484. package/src/utils/timeParser.test.ts +6 -1
  485. package/src/utils/timeParser.ts +35 -13
  486. package/src/__tests__/inputWidthContract.test.tsx +0 -69
@@ -35,11 +35,12 @@ import {
35
35
  spacingVars,
36
36
  } from '../theme/tokens.stylex';
37
37
  import {focusOutlineProps} from '../utils/focusOutline.stylex';
38
- import {mergeProps, mergeRefs, rtlStyles} from '../utils';
38
+ import {mergeProps, rtlStyles} from '../utils';
39
39
  import type {ResizableProps} from './useResizable';
40
40
  import {themeProps} from '../utils/themeProps';
41
41
  import {useTranslator} from '../i18n';
42
42
 
43
+ import {useMergedRefs} from '../hooks/useMergedRefs';
43
44
  const KEYBOARD_STEP = 10;
44
45
  const KEYBOARD_LARGE_STEP = 50;
45
46
 
@@ -549,7 +550,7 @@ export function ResizeHandle({
549
550
 
550
551
  return (
551
552
  <div
552
- ref={mergeRefs(ref, handleRef)}
553
+ ref={useMergedRefs(ref, handleRef)}
553
554
  role="separator"
554
555
  aria-orientation={isHorizontal ? 'vertical' : 'horizontal'}
555
556
  aria-valuenow={ariaValueNow}
@@ -26,11 +26,12 @@ import type {
26
26
  SegmentedControlSize,
27
27
  SegmentedControlLayout,
28
28
  } from './SegmentedControlContext';
29
- import {mergeProps, mergeRefs, composeEventHandlers} from '../utils';
29
+ import {mergeProps, composeEventHandlers} from '../utils';
30
30
  import {useSize} from '../SizeContext/SizeContext';
31
31
  import type {BaseProps} from '../BaseProps';
32
32
  import {themeProps} from '../utils/themeProps';
33
33
 
34
+ import {useMergedRefs} from '../hooks/useMergedRefs';
34
35
  export interface SegmentedControlProps extends Omit<
35
36
  BaseProps<HTMLDivElement>,
36
37
  'onChange'
@@ -258,7 +259,7 @@ export function SegmentedControl({
258
259
  return (
259
260
  <SegmentedControlContext value={contextValue}>
260
261
  <div
261
- ref={mergeRefs(ref, listRef, disabledMessageTooltip.ref)}
262
+ ref={useMergedRefs(ref, listRef, disabledMessageTooltip.ref)}
262
263
  {...rest}
263
264
  role="radiogroup"
264
265
  aria-label={label}
@@ -40,7 +40,7 @@ import * as stylex from '@stylexjs/stylex';
40
40
  import type {StyleXStyles} from '@stylexjs/stylex';
41
41
  import {colorVars, durationVars, easeVars} from '../theme/tokens.stylex';
42
42
  import type {Elevation, SizeValue, SpacingStep} from '../utils/types';
43
- import {mergeProps, mergeRefs} from '../utils';
43
+ import {mergeProps} from '../utils';
44
44
  import {Card} from '../Card/Card';
45
45
  import type {CardVariant} from '../Card/Card';
46
46
  import {useClickableContainer} from '../hooks/useClickableContainer';
@@ -48,6 +48,7 @@ import type {BaseProps} from '../BaseProps';
48
48
  import {themeProps} from '../utils/themeProps';
49
49
  import {focusOutlineProps} from '../utils/focusOutline.stylex';
50
50
 
51
+ import {useMergedRefs} from '../hooks/useMergedRefs';
51
52
  // =============================================================================
52
53
  // Styles — selection + interaction; Card handles the rest
53
54
  // =============================================================================
@@ -346,7 +347,7 @@ export function SelectableCard({
346
347
 
347
348
  return (
348
349
  <Card
349
- ref={mergeRefs(ref, containerRef)}
350
+ ref={useMergedRefs(ref, containerRef)}
350
351
  width={width}
351
352
  height={height}
352
353
  maxWidth={maxWidth}
@@ -171,7 +171,7 @@ export const docs = {
171
171
  name: 'renderValue',
172
172
  type: '(option: SelectorOptionData) => ReactNode',
173
173
  description:
174
- 'Custom render function for the selected option inside the closed trigger. The trigger is sized by padding, so it is the size token for a one-line value (28/32/36) and exactly one text line taller for a two-line one (48/52/56) — always on the 4px rhythm, always aligned with the buttons and inputs beside it. Inside an InputGroup the group owns the row height: a SelectorOption folds onto one line and ellipsizes, and any taller node is cut off at the row.',
174
+ 'Custom render function for the selected option inside the closed trigger. The trigger is sized by padding, so it is the size token for a one-line value (28/32/36) and exactly one text line taller for a two-line one (48/52/56), always on the 4px rhythm, always aligned with the buttons and inputs beside it. Inside an InputGroup the group owns the row height: a SelectorOption folds onto one line and ellipsizes, and any taller node is cut off at the row.',
175
175
  },
176
176
  {
177
177
  name: 'indicatorPosition',
@@ -186,6 +186,17 @@ export const docs = {
186
186
  description:
187
187
  '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.',
188
188
  },
189
+ {
190
+ name: 'startIcon',
191
+ type: 'IconType | ReactNode',
192
+ description: 'Icon displayed at the start of the selector trigger.',
193
+ },
194
+ {
195
+ name: 'isLoading',
196
+ type: 'boolean',
197
+ description: 'Shows a loading spinner in the trigger.',
198
+ default: 'false',
199
+ },
189
200
  {
190
201
  name: 'xstyle',
191
202
  type: 'StyleXStyles',
@@ -961,7 +961,7 @@ describe('Selector', () => {
961
961
  expect(onChange).toHaveBeenCalledWith('Banana');
962
962
  });
963
963
 
964
- it('closes dropdown on Tab without preventing default focus movement', async () => {
964
+ it('closes the search dropdown on Tab without preventing default focus movement', async () => {
965
965
  const user = userEvent.setup();
966
966
  render(
967
967
  <>
@@ -1253,6 +1253,29 @@ describe('Selector', () => {
1253
1253
  });
1254
1254
 
1255
1255
  describe('keyboard accessibility', () => {
1256
+ it('Tab from the open listbox moves focus to the next control', async () => {
1257
+ const user = userEvent.setup();
1258
+ render(
1259
+ <>
1260
+ <Selector
1261
+ label="Fruit"
1262
+ options={OPTIONS}
1263
+ value="Apple"
1264
+ onChange={() => {}}
1265
+ />
1266
+ <button type="button">Next</button>
1267
+ </>,
1268
+ );
1269
+
1270
+ const trigger = screen.getByRole('combobox');
1271
+ await user.click(trigger);
1272
+ expect(trigger).toHaveAttribute('aria-expanded', 'true');
1273
+
1274
+ await user.keyboard('{Tab}');
1275
+ expect(trigger).toHaveAttribute('aria-expanded', 'false');
1276
+ expect(screen.getByRole('button', {name: 'Next'})).toHaveFocus();
1277
+ });
1278
+
1256
1279
  it('trigger is focusable via Tab when enabled', async () => {
1257
1280
  const user = userEvent.setup();
1258
1281
  render(<Selector label="Fruit" options={OPTIONS} />);
@@ -2484,6 +2507,27 @@ describe('Selector section headings', () => {
2484
2507
  // visible heading must not announce it a second time.
2485
2508
  expect(heading).toHaveAttribute('aria-hidden', 'true');
2486
2509
  });
2510
+
2511
+ it('hides a standalone divider from the accessibility tree (#4994)', async () => {
2512
+ const user = userEvent.setup();
2513
+ render(
2514
+ <Selector
2515
+ label="Fruit"
2516
+ options={['Apple', {type: 'divider'}, 'Banana']}
2517
+ value={undefined}
2518
+ onChange={() => {}}
2519
+ />,
2520
+ );
2521
+ await user.click(screen.getByRole('combobox'));
2522
+
2523
+ // role="listbox" only permits option/group children. The divider still
2524
+ // renders role="separator" (unchanged visual/DOM), but must be excluded
2525
+ // from the accessibility tree so it never reaches the listbox's exposed
2526
+ // children (axe aria-required-children).
2527
+ const divider = document.querySelector('[role="separator"]');
2528
+ expect(divider).toBeTruthy();
2529
+ expect(divider).toHaveAttribute('aria-hidden', 'true');
2530
+ });
2487
2531
  });
2488
2532
 
2489
2533
  describe('Selector search focus ring', () => {
@@ -1344,7 +1344,17 @@ export function Selector<T extends SelectorOptionType>(
1344
1344
  if (isSearching) {
1345
1345
  continue;
1346
1346
  }
1347
- elements.push(<Divider key={`divider-${i}`} xstyle={styles.divider} />);
1347
+ // role="listbox" only permits option/group children; the divider
1348
+ // carries no information the options don't, so it's hidden from the
1349
+ // accessibility tree entirely rather than exposing role="separator"
1350
+ // as a disallowed listbox child (axe aria-required-children).
1351
+ elements.push(
1352
+ <Divider
1353
+ key={`divider-${i}`}
1354
+ aria-hidden="true"
1355
+ xstyle={styles.divider}
1356
+ />,
1357
+ );
1348
1358
  } else if (isSection(option)) {
1349
1359
  const sectionItems: ReactNode[] = [];
1350
1360
  for (const opt of option.options) {
@@ -131,8 +131,8 @@ export const docs = {
131
131
  bestPractices: [
132
132
  {guidance: true, description: 'Use sections to group related navigation items and help users scan for their destination.'},
133
133
  {guidance: true, description: 'Pair outline and filled icon variants so the selected state is visually distinct.'},
134
- {guidance: true, description: 'Mark the current page with isSelected — it sets aria-current="page", so the current destination is announced rather than carried by color alone.'},
135
- {guidance: true, description: 'SideNav renders a navigation landmark, and a collapsible item follows the WAI-ARIA APG Disclosure pattern (https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/): the toggle carries aria-expanded and aria-controls, and the group it owns is inert while collapsed. Keep item labels short — they are the accessible name in both expanded and icon-only modes.'},
134
+ {guidance: true, description: 'Mark the current page with isSelected: it sets aria-current="page", so the current destination is announced rather than carried by color alone.'},
135
+ {guidance: true, description: 'SideNav renders a navigation landmark, and a collapsible item follows the WAI-ARIA APG Disclosure pattern (https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/): the toggle carries aria-expanded and aria-controls, and the group it owns is inert while collapsed. Keep item labels short; they are the accessible name in both expanded and icon-only modes.'},
136
136
  {guidance: true, description: 'While the nav is collapsed, an item with children shows them in a submenu flyout. On a device that can hover, pointing at the item opens it after a short delay and moving away closes it; a flyout opened by clicking stays open until it is dismissed. On touch, it opens on tap. Do not put an action in there that has no other route to it.'},
137
137
  {guidance: false, description: 'Include a SideNavHeading when a TopNav is already providing app identity; this duplicates branding.'},
138
138
  {guidance: false, description: 'Use for filtering content; use tabs or filter buttons instead.'},
@@ -153,8 +153,8 @@ export const docsZh = {
153
153
  bestPractices: [
154
154
  {guidance: true, description: 'Use sections to group related navigation items and help users scan for their destination.'},
155
155
  {guidance: true, description: 'Pair outline and filled icon variants so the selected state is visually distinct.'},
156
- {guidance: true, description: 'Mark the current page with isSelected — it sets aria-current="page", so the current destination is announced rather than carried by color alone.'},
157
- {guidance: true, description: 'SideNav renders a navigation landmark, and a collapsible item follows the WAI-ARIA APG Disclosure pattern (https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/): the toggle carries aria-expanded and aria-controls, and the group it owns is inert while collapsed. Keep item labels short — they are the accessible name in both expanded and icon-only modes.'},
156
+ {guidance: true, description: 'Mark the current page with isSelected: it sets aria-current="page", so the current destination is announced rather than carried by color alone.'},
157
+ {guidance: true, description: 'SideNav renders a navigation landmark, and a collapsible item follows the WAI-ARIA APG Disclosure pattern (https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/): the toggle carries aria-expanded and aria-controls, and the group it owns is inert while collapsed. Keep item labels short; they are the accessible name in both expanded and icon-only modes.'},
158
158
  {guidance: true, description: 'While the nav is collapsed, an item with children shows them in a submenu flyout. On a device that can hover, pointing at the item opens it after a short delay and moving away closes it; a flyout opened by clicking stays open until it is dismissed. On touch, it opens on tap. Do not put an action in there that has no other route to it.'},
159
159
  {guidance: false, description: 'Include a SideNavHeading when a TopNav is already providing app identity; this duplicates branding.'},
160
160
  {guidance: false, description: 'Use for filtering content; use tabs or filter buttons instead.'},
@@ -175,8 +175,8 @@ export const docsDense = {
175
175
  bestPractices: [
176
176
  {guidance: true, description: 'Use sections to group related navigation items and help users scan for their destination.'},
177
177
  {guidance: true, description: 'Pair outline and filled icon variants so the selected state is visually distinct.'},
178
- {guidance: true, description: 'Mark the current page with isSelected — it sets aria-current="page", so the current destination is announced rather than carried by color alone.'},
179
- {guidance: true, description: 'SideNav renders a navigation landmark, and a collapsible item follows the WAI-ARIA APG Disclosure pattern (https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/): the toggle carries aria-expanded and aria-controls, and the group it owns is inert while collapsed. Keep item labels short — they are the accessible name in both expanded and icon-only modes.'},
178
+ {guidance: true, description: 'Mark the current page with isSelected: it sets aria-current="page", so the current destination is announced rather than carried by color alone.'},
179
+ {guidance: true, description: 'SideNav renders a navigation landmark, and a collapsible item follows the WAI-ARIA APG Disclosure pattern (https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/): the toggle carries aria-expanded and aria-controls, and the group it owns is inert while collapsed. Keep item labels short; they are the accessible name in both expanded and icon-only modes.'},
180
180
  {guidance: true, description: 'While the nav is collapsed, an item with children shows them in a submenu flyout. On a device that can hover, pointing at the item opens it after a short delay and moving away closes it; a flyout opened by clicking stays open until it is dismissed. On touch, it opens on tap. Do not put an action in there that has no other route to it.'},
181
181
  {guidance: false, description: 'Include a SideNavHeading when a TopNav is already providing app identity; this duplicates branding.'},
182
182
  {guidance: false, description: 'Use for filtering content; use tabs or filter buttons instead.'},
@@ -32,7 +32,7 @@ import type {BaseProps} from '../BaseProps';
32
32
  import * as stylex from '@stylexjs/stylex';
33
33
  import type {StyleXStyles} from '@stylexjs/stylex';
34
34
  import {spacingVars} from '../theme/tokens.stylex';
35
- import {mergeProps, mergeRefs} from '../utils';
35
+ import {mergeProps} from '../utils';
36
36
  import {
37
37
  SideNavCollapseContext,
38
38
  type SideNavCollapseState,
@@ -49,6 +49,7 @@ import {themeProps} from '../utils/themeProps';
49
49
  import {SizeProvider} from '../SizeContext/SizeContext';
50
50
  import {useTranslator} from '../i18n';
51
51
 
52
+ import {useMergedRefs} from '../hooks/useMergedRefs';
52
53
  // =============================================================================
53
54
  // Constants
54
55
  // =============================================================================
@@ -349,6 +350,7 @@ export function SideNav({
349
350
  useState(defaultIsCollapsed);
350
351
  const collapsed = isControlled ? controlledCollapsed : uncontrolledCollapsed;
351
352
  const navRef = useRef<HTMLElement>(null);
353
+ const mergedNavRef = useMergedRefs(ref, navRef);
352
354
  const collapseStateRef = useRef<SideNavCollapseState>({
353
355
  isCollapsed: collapsed,
354
356
  toggle: () => {},
@@ -517,7 +519,7 @@ export function SideNav({
517
519
 
518
520
  const navElement = (
519
521
  <nav
520
- ref={mergeRefs(ref, navRef)}
522
+ ref={mergedNavRef}
521
523
  role="navigation"
522
524
  aria-label={t('@astryx.sideNav.label')}
523
525
  data-testid={testId}
@@ -40,7 +40,8 @@ import {focusOutlineProps} from '../utils/focusOutline.stylex';
40
40
  import {useSideNavCollapse} from './SideNavCollapseContext';
41
41
  import {useLinkComponent} from '../Link/useLinkComponent';
42
42
  import type {LinkComponentType} from '../Link/types';
43
- import {mergeProps, mergeRefs} from '../utils';
43
+ import {mergeProps} from '../utils';
44
+ import {useMergedRefs} from '../hooks/useMergedRefs';
44
45
  import type {BaseProps} from '../BaseProps';
45
46
  import {useMenuHover} from '../hooks/useMenuHover';
46
47
  import {NavHeadingCloseContext} from '../NavMenu/NavMenuContext';
@@ -399,11 +400,18 @@ export function SideNavHeading({
399
400
  // setTriggerEl belongs on the chevron button, not this root: it is the
400
401
  // focus-restore target and a <div> cannot take focus. triggerRef stays here
401
402
  // because the panel anchors to the whole heading.
402
- const setRef = mergeRefs<HTMLDivElement>(
403
+ const setRef = useMergedRefs<HTMLDivElement>(
403
404
  rootRef,
404
405
  ref,
405
406
  menu ? popover.triggerRef : undefined,
406
407
  );
408
+ const collapsedSetRef = useMergedRefs<HTMLElement>(
409
+ collapsedItemRef,
410
+ ref,
411
+ // Collapsed, this button is the trigger, so it takes both roles.
412
+ menu ? popover.triggerRef : undefined,
413
+ menu ? setTriggerEl : undefined,
414
+ );
407
415
 
408
416
  // In collapsed mode: hide if no icon, show icon-only if has icon
409
417
  if (isCollapsed && !icon) {
@@ -412,14 +420,6 @@ export function SideNavHeading({
412
420
  if (isCollapsed && icon) {
413
421
  const collapsedIcon = <span {...stylex.props(styles.icon)}>{icon}</span>;
414
422
 
415
- const collapsedSetRef = mergeRefs<HTMLElement>(
416
- collapsedItemRef,
417
- ref,
418
- // Collapsed, this button is the trigger, so it takes both roles.
419
- menu ? popover.triggerRef : undefined,
420
- menu ? setTriggerEl : undefined,
421
- );
422
-
423
423
  let collapsedElement: ReactNode;
424
424
 
425
425
  if (headingHref) {
@@ -47,7 +47,7 @@ import {useLinkComponent} from '../Link/useLinkComponent';
47
47
  import type {LinkComponentType} from '../Link/types';
48
48
  import {usePopover} from '../Popover/usePopover';
49
49
  import {useMenuHover} from '../hooks/useMenuHover';
50
- import {mergeProps, mergeRefs} from '../utils';
50
+ import {mergeProps} from '../utils';
51
51
  import type {BaseProps} from '../BaseProps';
52
52
  import {Tooltip} from '../Tooltip';
53
53
  import {navItemStyles, type NavItemSize} from '../NavItem/navItemStyles.stylex';
@@ -61,6 +61,7 @@ import {useAppShellMobile} from '../AppShell/AppShellMobileContext';
61
61
  import {themeProps} from '../utils/themeProps';
62
62
  import {useTranslator} from '../i18n';
63
63
 
64
+ import {useMergedRefs} from '../hooks/useMergedRefs';
64
65
  // =============================================================================
65
66
  // Styles
66
67
  // =============================================================================
@@ -408,6 +409,7 @@ export function SideNavItem({
408
409
  hasCloseButton: false,
409
410
  dialogLabel: t('@astryx.sideNavItem.submenuLabel', {label}),
410
411
  });
412
+ const mergedTriggerRef = useMergedRefs(ref, popover.triggerRef);
411
413
 
412
414
  // Collapse state for items with children
413
415
  const itemCollapsibleConfig = useMemo(
@@ -524,7 +526,7 @@ export function SideNavItem({
524
526
  return (
525
527
  <div {...stylex.props(styles.root, xstyle)}>
526
528
  <button
527
- ref={mergeRefs(ref, popover.triggerRef)}
529
+ ref={mergedTriggerRef}
528
530
  type="button"
529
531
  {...rest}
530
532
  {...hoverTriggerProps}
@@ -40,13 +40,14 @@ import {Tooltip} from '../Tooltip/Tooltip';
40
40
  import {useTooltip} from '../Tooltip';
41
41
  import {VisuallyHidden} from '../VisuallyHidden';
42
42
  import type {InputStatus} from '../Field/types';
43
- import {mergeProps, mergeRefs, rtlStyles} from '../utils';
43
+ import {mergeProps, rtlStyles} from '../utils';
44
44
  import {focusOutlineStyles} from '../utils/focusOutline.stylex';
45
45
  import {isRtlElement} from '../hooks/isRtlElement';
46
46
  import type {BaseProps} from '../BaseProps';
47
47
  import type {SizeValue} from '../utils/types';
48
48
  import {themeProps} from '../utils/themeProps';
49
49
 
50
+ import {useMergedRefs} from '../hooks/useMergedRefs';
50
51
  // =============================================================================
51
52
  // Types
52
53
  // =============================================================================
@@ -964,7 +965,7 @@ export function Slider({ref, ...props}: SliderProps) {
964
965
  />
965
966
  ))}
966
967
  <div
967
- ref={mergeRefs(ref, trackRef, disabledMessageTooltip.ref)}
968
+ ref={useMergedRefs(ref, trackRef, disabledMessageTooltip.ref)}
968
969
  {...(isRange
969
970
  ? {role: 'group', 'aria-labelledby': labelID}
970
971
  : undefined)}
@@ -9,10 +9,29 @@
9
9
  * SYNC: When Spinner.tsx changes, update tests to match new behavior
10
10
  */
11
11
 
12
- import {describe, it, expect} from 'vitest';
12
+ import {describe, it, expect, vi, afterEach} from 'vitest';
13
13
  import {render, screen} from '@testing-library/react';
14
14
  import {Spinner} from './Spinner';
15
15
 
16
+ /** sm/md/lg/xl, as Spinner.tsx defines them. */
17
+ const SIZES = {
18
+ sm: {diameter: 10, border: 2},
19
+ md: {diameter: 14, border: 3},
20
+ lg: {diameter: 18, border: 3},
21
+ xl: {diameter: 28, border: 4},
22
+ } as const;
23
+
24
+ const ring = (testId = 'spinner') =>
25
+ screen.getByTestId(testId).querySelector('svg') as SVGSVGElement;
26
+
27
+ const circles = (testId = 'spinner') => {
28
+ const [track, arc] = [...ring(testId).querySelectorAll('circle')];
29
+ return {track, arc};
30
+ };
31
+
32
+ const dashOf = (el: SVGCircleElement) =>
33
+ (el.getAttribute('stroke-dasharray') ?? '').split(/[\s,]+/).map(Number);
34
+
16
35
  describe('Spinner', () => {
17
36
  it('renders with default props', () => {
18
37
  render(<Spinner data-testid="spinner" />);
@@ -122,3 +141,130 @@ describe('Spinner', () => {
122
141
  expect(spinner.tagName.toLowerCase()).toBe('span');
123
142
  });
124
143
  });
144
+
145
+ describe('Spinner ring', () => {
146
+ afterEach(() => {
147
+ vi.restoreAllMocks();
148
+ });
149
+
150
+ it('draws the ring in SVG', () => {
151
+ render(<Spinner data-testid="spinner" />);
152
+ const svg = ring();
153
+ expect(svg).not.toBeNull();
154
+ expect(svg.querySelectorAll('circle')).toHaveLength(2);
155
+ expect(screen.getByTestId('spinner').querySelector('canvas')).toBeNull();
156
+ });
157
+
158
+ it.each(Object.entries(SIZES))(
159
+ 'sizes the %s ring from its size token',
160
+ (size, {diameter, border}) => {
161
+ render(
162
+ <Spinner size={size as keyof typeof SIZES} data-testid="spinner" />,
163
+ );
164
+ const frame = diameter + border * 2;
165
+ const svg = ring();
166
+ expect(svg.getAttribute('viewBox')).toBe(`0 0 ${frame} ${frame}`);
167
+ for (const c of [circles().track, circles().arc]) {
168
+ expect(Number(c.getAttribute('cx'))).toBe(frame / 2);
169
+ expect(Number(c.getAttribute('cy'))).toBe(frame / 2);
170
+ expect(Number(c.getAttribute('r'))).toBe(diameter / 2);
171
+ expect(Number(c.getAttribute('stroke-width'))).toBe(border);
172
+ }
173
+ },
174
+ );
175
+
176
+ // The canvas ring swept 135deg, not the 270deg its SPREAD comment claimed.
177
+ it.each(Object.entries(SIZES))(
178
+ 'sweeps 135 degrees at %s',
179
+ (size, {diameter}) => {
180
+ render(
181
+ <Spinner size={size as keyof typeof SIZES} data-testid="spinner" />,
182
+ );
183
+ const {track, arc} = circles();
184
+ const [on, off] = dashOf(arc);
185
+ expect(on + off).toBeCloseTo(Math.PI * diameter, 6);
186
+ expect((on / (on + off)) * 360).toBeCloseTo(135, 6);
187
+ expect(track.getAttribute('stroke-dasharray')).toBeNull();
188
+ },
189
+ );
190
+
191
+ it('starts the arc at twelve o’clock', () => {
192
+ render(<Spinner data-testid="spinner" />);
193
+ const frame = SIZES.md.diameter + SIZES.md.border * 2;
194
+ expect(circles().arc.getAttribute('transform')).toBe(
195
+ `rotate(-90 ${frame / 2} ${frame / 2})`,
196
+ );
197
+ });
198
+
199
+ // The whole point of the SVG ring: the colours come off the cascade, so
200
+ // nothing has to resolve them in JS. A read reaching the paint path again
201
+ // fails here.
202
+ it.each(['default', 'subtle', 'onMedia', 'inherit'] as const)(
203
+ 'reads no computed style to paint the %s shade',
204
+ shade => {
205
+ const spy = vi.spyOn(window, 'getComputedStyle');
206
+ render(<Spinner shade={shade} data-testid="spinner" />);
207
+ const spinner = screen.getByTestId('spinner');
208
+ const onSpinner = spy.mock.calls.filter(
209
+ ([el]) => el instanceof Element && spinner.contains(el),
210
+ );
211
+ expect(onSpinner).toEqual([]);
212
+ },
213
+ );
214
+
215
+ it('schedules no frame where the Web Animations API is absent', () => {
216
+ expect(SVGElement.prototype).not.toHaveProperty('getAnimations');
217
+ const raf = vi.spyOn(window, 'requestAnimationFrame');
218
+ render(<Spinner data-testid="spinner" />);
219
+ expect(ring()).not.toBeNull();
220
+ expect(raf).not.toHaveBeenCalled();
221
+ });
222
+
223
+ it('pins every ring to the timeline origin in a single frame', () => {
224
+ const animations: {startTime: number | null}[] = [];
225
+ const getAnimations = vi.fn(function (this: SVGElement) {
226
+ const a = {startTime: null as number | null};
227
+ animations.push(a);
228
+ return [a];
229
+ });
230
+ Object.defineProperty(SVGElement.prototype, 'getAnimations', {
231
+ value: getAnimations,
232
+ configurable: true,
233
+ writable: true,
234
+ });
235
+ const frames: FrameRequestCallback[] = [];
236
+ const raf = vi
237
+ .spyOn(window, 'requestAnimationFrame')
238
+ .mockImplementation(cb => {
239
+ frames.push(cb);
240
+ return frames.length;
241
+ });
242
+ const computed = vi.spyOn(window, 'getComputedStyle');
243
+
244
+ try {
245
+ const {container} = render(
246
+ <>
247
+ {Array.from({length: 5}, (_, i) => (
248
+ <Spinner key={i} />
249
+ ))}
250
+ </>,
251
+ );
252
+ // One frame for five spinners: the reads are batched, so no mount
253
+ // re-forces what the previous mount invalidated.
254
+ expect(raf).toHaveBeenCalledTimes(1);
255
+ frames.forEach(cb => cb(0));
256
+ expect(animations).toHaveLength(5);
257
+ expect(animations.every(a => a.startTime === 0)).toBe(true);
258
+ // The pin runs on the branch the other shade cases cannot reach, so the
259
+ // no-read assertion is made here too.
260
+ expect(
261
+ computed.mock.calls.filter(
262
+ ([el]) => el instanceof Element && container.contains(el),
263
+ ),
264
+ ).toEqual([]);
265
+ } finally {
266
+ // @ts-expect-error — removing the stub restores jsdom's real surface
267
+ delete SVGElement.prototype.getAnimations;
268
+ }
269
+ });
270
+ });