@astryxdesign/core 0.4.7 → 0.5.0-canary.1f8525f

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 (566) hide show
  1. package/CHANGELOG.md +161 -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 +25 -1
  8. package/dist/Avatar/Avatar.d.ts.map +1 -1
  9. package/dist/Avatar/Avatar.js +58 -19
  10. package/dist/Avatar/index.d.ts +2 -2
  11. package/dist/Avatar/index.d.ts.map +1 -1
  12. package/dist/Avatar/index.js +1 -1
  13. package/dist/AvatarGroup/AvatarGroup.d.ts +9 -2
  14. package/dist/AvatarGroup/AvatarGroup.d.ts.map +1 -1
  15. package/dist/AvatarGroup/AvatarGroup.js +8 -4
  16. package/dist/AvatarGroup/AvatarGroupContext.d.ts +2 -1
  17. package/dist/AvatarGroup/AvatarGroupContext.d.ts.map +1 -1
  18. package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
  19. package/dist/AvatarGroup/AvatarGroupOverflow.js +10 -6
  20. package/dist/Banner/Banner.d.ts +40 -15
  21. package/dist/Banner/Banner.d.ts.map +1 -1
  22. package/dist/Banner/Banner.js +51 -18
  23. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
  24. package/dist/BottomSheet/BottomSheet.js +17 -4
  25. package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
  26. package/dist/BottomSheet/BottomSheetSwitcher.js +3 -2
  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 +14 -15
  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/Calendar/styles.d.ts +5 -14
  41. package/dist/Calendar/styles.d.ts.map +1 -1
  42. package/dist/Calendar/styles.js +15 -15
  43. package/dist/Carousel/Carousel.d.ts.map +1 -1
  44. package/dist/Carousel/Carousel.js +3 -3
  45. package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
  46. package/dist/Chat/ChatComposerInput.js +2 -3
  47. package/dist/Chat/ChatLayout.d.ts.map +1 -1
  48. package/dist/Chat/ChatLayout.js +3 -3
  49. package/dist/Chat/ChatMessageList.d.ts +22 -3
  50. package/dist/Chat/ChatMessageList.d.ts.map +1 -1
  51. package/dist/Chat/ChatMessageList.js +6 -3
  52. package/dist/Chat/ChatTokenizedText.d.ts +2 -0
  53. package/dist/Chat/ChatTokenizedText.d.ts.map +1 -1
  54. package/dist/Chat/ChatTokenizedText.js +11 -6
  55. package/dist/Chat/useChatComposerTokens.js +1 -1
  56. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  57. package/dist/CheckboxInput/CheckboxInput.js +10 -3
  58. package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
  59. package/dist/ClickableCard/ClickableCard.js +3 -3
  60. package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
  61. package/dist/CodeBlock/CodeBlock.js +15 -4
  62. package/dist/CodeBlock/highlightRanges.js +1 -1
  63. package/dist/CodeBlock/highlightStyles.js +13 -13
  64. package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
  65. package/dist/CommandPalette/CommandPaletteInput.js +3 -2
  66. package/dist/CommandPalette/CommandPaletteItem.d.ts.map +1 -1
  67. package/dist/CommandPalette/CommandPaletteItem.js +3 -2
  68. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  69. package/dist/ContextMenu/ContextMenu.js +4 -3
  70. package/dist/DateInput/DateInput.d.ts +43 -0
  71. package/dist/DateInput/DateInput.d.ts.map +1 -1
  72. package/dist/DateInput/DateInput.js +22 -2
  73. package/dist/DateInput/NativeDateField.d.ts +10 -0
  74. package/dist/DateInput/NativeDateField.d.ts.map +1 -0
  75. package/dist/DateInput/NativeDateField.js +494 -0
  76. package/dist/DateInput/TouchDateField.d.ts.map +1 -1
  77. package/dist/DateInput/TouchDateField.js +38 -4
  78. package/dist/DateInput/index.d.ts +1 -1
  79. package/dist/DateInput/index.d.ts.map +1 -1
  80. package/dist/DateInput/nativeDateSegments.d.ts +22 -0
  81. package/dist/DateInput/nativeDateSegments.d.ts.map +1 -0
  82. package/dist/DateInput/nativeDateSegments.js +111 -0
  83. package/dist/DateTimeInput/DateTimeInput.d.ts +21 -1
  84. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  85. package/dist/DateTimeInput/DateTimeInput.js +406 -12
  86. package/dist/DateTimeInput/index.d.ts +1 -1
  87. package/dist/DateTimeInput/index.d.ts.map +1 -1
  88. package/dist/Dialog/Dialog.d.ts +7 -1
  89. package/dist/Dialog/Dialog.d.ts.map +1 -1
  90. package/dist/Dialog/Dialog.js +89 -58
  91. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
  92. package/dist/DropdownMenu/DropdownMenuSubMenu.js +6 -5
  93. package/dist/Field/FieldLabel.d.ts.map +1 -1
  94. package/dist/Field/FieldLabel.js +9 -1
  95. package/dist/FileInput/FileInput.d.ts.map +1 -1
  96. package/dist/FileInput/FileInput.js +3 -2
  97. package/dist/Heading/Heading.d.ts.map +1 -1
  98. package/dist/Heading/Heading.js +6 -2
  99. package/dist/HoverCard/HoverCard.d.ts +6 -0
  100. package/dist/HoverCard/HoverCard.d.ts.map +1 -1
  101. package/dist/HoverCard/HoverCard.js +52 -3
  102. package/dist/HoverCard/useHoverCard.d.ts +16 -2
  103. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  104. package/dist/HoverCard/useHoverCard.js +74 -26
  105. package/dist/Indicator/CheckboxIndicator.d.ts.map +1 -1
  106. package/dist/Indicator/CheckboxIndicator.js +6 -2
  107. package/dist/Item/Item.d.ts.map +1 -1
  108. package/dist/Item/Item.js +4 -2
  109. package/dist/Kbd/Kbd.d.ts.map +1 -1
  110. package/dist/Kbd/Kbd.js +2 -19
  111. package/dist/Layer/LayerDepthContext.d.ts +52 -0
  112. package/dist/Layer/LayerDepthContext.d.ts.map +1 -0
  113. package/dist/Layer/LayerDepthContext.js +66 -0
  114. package/dist/Layer/index.d.ts +3 -0
  115. package/dist/Layer/index.d.ts.map +1 -1
  116. package/dist/Layer/index.js +4 -0
  117. package/dist/Layer/layerStack.d.ts +98 -0
  118. package/dist/Layer/layerStack.d.ts.map +1 -0
  119. package/dist/Layer/layerStack.js +306 -0
  120. package/dist/Layer/useLayerDismissal.d.ts +93 -0
  121. package/dist/Layer/useLayerDismissal.d.ts.map +1 -0
  122. package/dist/Layer/useLayerDismissal.js +91 -0
  123. package/dist/Lightbox/Lightbox.d.ts +1 -0
  124. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  125. package/dist/Lightbox/Lightbox.js +103 -81
  126. package/dist/Markdown/index.d.ts +1 -1
  127. package/dist/Markdown/index.d.ts.map +1 -1
  128. package/dist/Markdown/parser.d.ts +38 -1
  129. package/dist/Markdown/parser.d.ts.map +1 -1
  130. package/dist/Markdown/parser.js +177 -29
  131. package/dist/MobileNav/MobileNav.d.ts +6 -1
  132. package/dist/MobileNav/MobileNav.d.ts.map +1 -1
  133. package/dist/MobileNav/MobileNav.js +61 -36
  134. package/dist/MultiSelector/MultiSelector.d.ts +15 -1
  135. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  136. package/dist/MultiSelector/MultiSelector.js +17 -6
  137. package/dist/MultiSelector/index.d.ts +1 -1
  138. package/dist/MultiSelector/index.d.ts.map +1 -1
  139. package/dist/NavIcon/NavIcon.d.ts.map +1 -1
  140. package/dist/NavIcon/NavIcon.js +5 -1
  141. package/dist/NavMenu/NavHeadingMenu.d.ts.map +1 -1
  142. package/dist/NavMenu/NavHeadingMenu.js +3 -2
  143. package/dist/NumberInput/NumberInput.d.ts +1 -0
  144. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  145. package/dist/NumberInput/NumberInput.js +27 -37
  146. package/dist/NumberInput/numberParser.d.ts +51 -0
  147. package/dist/NumberInput/numberParser.d.ts.map +1 -0
  148. package/dist/NumberInput/numberParser.js +333 -0
  149. package/dist/Outline/Outline.d.ts.map +1 -1
  150. package/dist/Outline/Outline.js +3 -2
  151. package/dist/OverflowList/OverflowList.d.ts.map +1 -1
  152. package/dist/OverflowList/OverflowList.js +3 -2
  153. package/dist/Overlay/Overlay.d.ts.map +1 -1
  154. package/dist/Overlay/Overlay.js +3 -2
  155. package/dist/Popover/usePopover.d.ts.map +1 -1
  156. package/dist/Popover/usePopover.js +20 -17
  157. package/dist/PowerSearch/PowerSearch.d.ts +8 -2
  158. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  159. package/dist/PowerSearch/PowerSearch.js +19 -6
  160. package/dist/PowerSearch/PowerSearchEditPopover.d.ts +3 -1
  161. package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
  162. package/dist/PowerSearch/PowerSearchEditPopover.js +28 -6
  163. package/dist/PowerSearch/PowerSearchValueEditor.d.ts +3 -1
  164. package/dist/PowerSearch/PowerSearchValueEditor.d.ts.map +1 -1
  165. package/dist/PowerSearch/PowerSearchValueEditor.js +19 -9
  166. package/dist/PowerSearch/types.d.ts +2 -0
  167. package/dist/PowerSearch/types.d.ts.map +1 -1
  168. package/dist/PowerSearch/usePowerSearchSource.d.ts +6 -1
  169. package/dist/PowerSearch/usePowerSearchSource.d.ts.map +1 -1
  170. package/dist/PowerSearch/usePowerSearchSource.js +14 -5
  171. package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
  172. package/dist/ProgressBar/ProgressBar.js +18 -5
  173. package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
  174. package/dist/Resizable/ResizeHandle.js +3 -2
  175. package/dist/SegmentedControl/SegmentedControl.d.ts.map +1 -1
  176. package/dist/SegmentedControl/SegmentedControl.js +3 -2
  177. package/dist/SelectableCard/SelectableCard.d.ts.map +1 -1
  178. package/dist/SelectableCard/SelectableCard.js +3 -3
  179. package/dist/Selector/Selector.d.ts.map +1 -1
  180. package/dist/Selector/Selector.js +5 -0
  181. package/dist/SideNav/SideNav.d.ts.map +1 -1
  182. package/dist/SideNav/SideNav.js +4 -3
  183. package/dist/SideNav/SideNavHeading.d.ts.map +1 -1
  184. package/dist/SideNav/SideNavHeading.js +8 -7
  185. package/dist/SideNav/SideNavItem.d.ts.map +1 -1
  186. package/dist/SideNav/SideNavItem.js +4 -3
  187. package/dist/Slider/Slider.d.ts.map +1 -1
  188. package/dist/Slider/Slider.js +37 -5
  189. package/dist/Spinner/Spinner.d.ts +3 -3
  190. package/dist/Spinner/Spinner.d.ts.map +1 -1
  191. package/dist/Spinner/Spinner.js +130 -78
  192. package/dist/StatusDot/StatusDot.d.ts.map +1 -1
  193. package/dist/StatusDot/StatusDot.js +6 -1
  194. package/dist/Stepper/Step.d.ts +140 -0
  195. package/dist/Stepper/Step.d.ts.map +1 -0
  196. package/dist/Stepper/Step.js +1045 -0
  197. package/dist/Stepper/StepStatus.d.ts +24 -0
  198. package/dist/Stepper/StepStatus.d.ts.map +1 -0
  199. package/dist/Stepper/StepStatus.js +1 -0
  200. package/dist/Stepper/Stepper.d.ts +93 -0
  201. package/dist/Stepper/Stepper.d.ts.map +1 -0
  202. package/dist/Stepper/Stepper.js +184 -0
  203. package/dist/Stepper/StepperContext.d.ts +41 -0
  204. package/dist/Stepper/StepperContext.d.ts.map +1 -0
  205. package/dist/Stepper/StepperContext.js +34 -0
  206. package/dist/Stepper/index.d.ts +8 -0
  207. package/dist/Stepper/index.d.ts.map +1 -0
  208. package/dist/Stepper/index.js +7 -0
  209. package/dist/Stepper/stepper.stylex.d.ts +15 -0
  210. package/dist/Stepper/stepper.stylex.d.ts.map +1 -0
  211. package/dist/Stepper/stepper.stylex.js +20 -0
  212. package/dist/Switch/Switch.d.ts.map +1 -1
  213. package/dist/Switch/Switch.js +8 -3
  214. package/dist/TabList/Tab.d.ts +15 -2
  215. package/dist/TabList/Tab.d.ts.map +1 -1
  216. package/dist/TabList/Tab.js +32 -7
  217. package/dist/TabList/TabList.d.ts +46 -7
  218. package/dist/TabList/TabList.d.ts.map +1 -1
  219. package/dist/TabList/TabList.js +353 -23
  220. package/dist/TabList/TabListContext.d.ts +11 -1
  221. package/dist/TabList/TabListContext.d.ts.map +1 -1
  222. package/dist/TabList/TabListContext.js +11 -2
  223. package/dist/TabList/TabMenu.d.ts.map +1 -1
  224. package/dist/TabList/TabMenu.js +3 -2
  225. package/dist/TabList/index.d.ts +2 -2
  226. package/dist/TabList/index.d.ts.map +1 -1
  227. package/dist/Table/BaseTable.d.ts.map +1 -1
  228. package/dist/Table/BaseTable.js +10 -2
  229. package/dist/Table/Table.d.ts.map +1 -1
  230. package/dist/Table/Table.js +3 -6
  231. package/dist/Table/TableRow.d.ts +16 -5
  232. package/dist/Table/TableRow.d.ts.map +1 -1
  233. package/dist/Table/TableRow.js +16 -5
  234. package/dist/Table/plugins/groupedRows/useTableGroupedRows.d.ts.map +1 -1
  235. package/dist/Table/plugins/groupedRows/useTableGroupedRows.js +20 -8
  236. package/dist/Table/plugins/pagination/useTablePagination.d.ts +3 -0
  237. package/dist/Table/plugins/pagination/useTablePagination.d.ts.map +1 -1
  238. package/dist/Table/plugins/pagination/useTablePagination.js +21 -4
  239. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
  240. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +7 -2
  241. package/dist/Table/plugins/rowStatus/useTableRowStatus.d.ts.map +1 -1
  242. package/dist/Table/plugins/rowStatus/useTableRowStatus.js +10 -3
  243. package/dist/Table/plugins/selection/useTableSelection.d.ts +16 -0
  244. package/dist/Table/plugins/selection/useTableSelection.d.ts.map +1 -1
  245. package/dist/Table/plugins/selection/useTableSelection.js +19 -5
  246. package/dist/Table/plugins/stickyColumns/useTableStickyColumns.d.ts.map +1 -1
  247. package/dist/Table/plugins/stickyColumns/useTableStickyColumns.js +3 -4
  248. package/dist/Table/types.d.ts +22 -4
  249. package/dist/Table/types.d.ts.map +1 -1
  250. package/dist/Table/useBaseTablePlugins.d.ts.map +1 -1
  251. package/dist/Table/useBaseTablePlugins.js +5 -0
  252. package/dist/Text/Text.d.ts.map +1 -1
  253. package/dist/Text/Text.js +6 -2
  254. package/dist/TextArea/TextArea.d.ts.map +1 -1
  255. package/dist/TextArea/TextArea.js +9 -3
  256. package/dist/TextInput/TextInput.d.ts.map +1 -1
  257. package/dist/TextInput/TextInput.js +3 -2
  258. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  259. package/dist/TimeInput/TimeInput.js +3 -2
  260. package/dist/Timestamp/Timestamp.d.ts.map +1 -1
  261. package/dist/Timestamp/Timestamp.js +5 -3
  262. package/dist/Toast/ToastViewport.d.ts.map +1 -1
  263. package/dist/Toast/ToastViewport.js +27 -8
  264. package/dist/Token/TokenLink.d.ts.map +1 -1
  265. package/dist/Token/TokenLink.js +2 -2
  266. package/dist/Tokenizer/Tokenizer.d.ts +3 -1
  267. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  268. package/dist/Tokenizer/Tokenizer.js +2 -0
  269. package/dist/Tooltip/Tooltip.d.ts +5 -0
  270. package/dist/Tooltip/Tooltip.d.ts.map +1 -1
  271. package/dist/Tooltip/useTooltip.d.ts +5 -0
  272. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  273. package/dist/Tooltip/useTooltip.js +39 -24
  274. package/dist/TopNav/TopNavHeading.d.ts.map +1 -1
  275. package/dist/TopNav/TopNavHeading.js +5 -4
  276. package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
  277. package/dist/TopNav/TopNavMegaMenu.js +3 -3
  278. package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
  279. package/dist/TopNav/TopNavMenu.js +4 -3
  280. package/dist/Typeahead/BaseTypeahead.d.ts +2 -0
  281. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  282. package/dist/Typeahead/BaseTypeahead.js +68 -29
  283. package/dist/Typeahead/Typeahead.d.ts.map +1 -1
  284. package/dist/Typeahead/Typeahead.js +3 -2
  285. package/dist/astryx.css +96 -7
  286. package/dist/hooks/index.d.ts +1 -0
  287. package/dist/hooks/index.d.ts.map +1 -1
  288. package/dist/hooks/index.js +1 -0
  289. package/dist/hooks/useFocusTrap.d.ts +7 -4
  290. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  291. package/dist/hooks/useFocusTrap.js +60 -95
  292. package/dist/hooks/useHotkeys.d.ts.map +1 -1
  293. package/dist/hooks/useHotkeys.js +3 -18
  294. package/dist/hooks/useMergedRefs.d.ts +18 -0
  295. package/dist/hooks/useMergedRefs.d.ts.map +1 -0
  296. package/dist/hooks/useMergedRefs.js +25 -0
  297. package/dist/index.d.ts +1 -0
  298. package/dist/index.d.ts.map +1 -1
  299. package/dist/index.js +1 -0
  300. package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
  301. package/dist/theme/derivedVarRegistry.js +24 -4
  302. package/dist/theme/syntax/tokens.d.ts +1 -1
  303. package/dist/theme/syntax/tokens.d.ts.map +1 -1
  304. package/dist/theme/syntax/tokens.js +5 -2
  305. package/dist/utils/groupItems.d.ts +5 -1
  306. package/dist/utils/groupItems.d.ts.map +1 -1
  307. package/dist/utils/groupItems.js +9 -9
  308. package/dist/utils/index.d.ts +1 -1
  309. package/dist/utils/index.d.ts.map +1 -1
  310. package/dist/utils/isApplePlatform.d.ts +18 -0
  311. package/dist/utils/isApplePlatform.d.ts.map +1 -0
  312. package/dist/utils/isApplePlatform.js +37 -0
  313. package/dist/utils/sharedResizeObserver.d.ts.map +1 -1
  314. package/dist/utils/sharedResizeObserver.js +10 -1
  315. package/dist/utils/timeParser.d.ts +1 -1
  316. package/dist/utils/timeParser.d.ts.map +1 -1
  317. package/dist/utils/timeParser.js +26 -12
  318. package/locales/en.json +31 -7
  319. package/locales/pseudo.json +18 -0
  320. package/package.json +8 -3
  321. package/src/AppShell/AppShell.tsx +3 -2
  322. package/src/AspectRatio/AspectRatio.doc.mjs +6 -3
  323. package/src/AspectRatio/AspectRatio.test.tsx +57 -10
  324. package/src/AspectRatio/AspectRatio.tsx +40 -1
  325. package/src/Avatar/Avatar.doc.mjs +14 -3
  326. package/src/Avatar/Avatar.test.tsx +97 -0
  327. package/src/Avatar/Avatar.tsx +125 -52
  328. package/src/Avatar/index.ts +2 -2
  329. package/src/AvatarGroup/AvatarGroup.doc.mjs +8 -2
  330. package/src/AvatarGroup/AvatarGroup.test.tsx +24 -0
  331. package/src/AvatarGroup/AvatarGroup.tsx +15 -6
  332. package/src/AvatarGroup/AvatarGroupContext.ts +5 -3
  333. package/src/AvatarGroup/AvatarGroupOverflow.tsx +10 -4
  334. package/src/Banner/Banner.doc.mjs +13 -11
  335. package/src/Banner/Banner.test.tsx +102 -15
  336. package/src/Banner/Banner.tsx +72 -25
  337. package/src/BottomSheet/BottomSheet.test.tsx +123 -0
  338. package/src/BottomSheet/BottomSheet.tsx +17 -4
  339. package/src/BottomSheet/BottomSheetSwitcher.tsx +3 -2
  340. package/src/Breadcrumbs/BreadcrumbItem.tsx +9 -5
  341. package/src/Button/Button.doc.mjs +22 -0
  342. package/src/Button/Button.test.tsx +13 -0
  343. package/src/Button/Button.tsx +5 -4
  344. package/src/ButtonGroup/ButtonGroup.doc.mjs +10 -1
  345. package/src/ButtonGroup/ButtonGroup.test.tsx +391 -1
  346. package/src/ButtonGroup/ButtonGroup.tsx +23 -5
  347. package/src/Calendar/Calendar.doc.mjs +4 -3
  348. package/src/Calendar/Calendar.test.tsx +103 -0
  349. package/src/Calendar/Calendar.tsx +19 -16
  350. package/src/Calendar/getInitialFocusDate.test.ts +94 -0
  351. package/src/Calendar/getInitialFocusDate.ts +93 -0
  352. package/src/Calendar/styles.ts +49 -31
  353. package/src/Carousel/Carousel.tsx +3 -2
  354. package/src/Chat/ChatComposerInput.tsx +5 -1
  355. package/src/Chat/ChatLayout.tsx +3 -2
  356. package/src/Chat/ChatMessageList.doc.mjs +9 -1
  357. package/src/Chat/ChatMessageList.test.tsx +46 -0
  358. package/src/Chat/ChatMessageList.tsx +28 -4
  359. package/src/Chat/ChatTokenizedText.tsx +17 -10
  360. package/src/Chat/useChatComposerTokens.ts +1 -1
  361. package/src/CheckboxInput/CheckboxInput.doc.mjs +2 -0
  362. package/src/CheckboxInput/CheckboxInput.test.tsx +15 -0
  363. package/src/CheckboxInput/CheckboxInput.tsx +8 -2
  364. package/src/ClickableCard/ClickableCard.tsx +3 -2
  365. package/src/CodeBlock/CodeBlock.doc.mjs +16 -4
  366. package/src/CodeBlock/CodeBlock.test.tsx +37 -0
  367. package/src/CodeBlock/CodeBlock.tsx +20 -4
  368. package/src/CodeBlock/highlightRanges.ts +1 -1
  369. package/src/CodeBlock/highlightStyles.ts +13 -13
  370. package/src/CommandPalette/CommandPaletteInput.tsx +3 -2
  371. package/src/CommandPalette/CommandPaletteItem.tsx +3 -2
  372. package/src/ContextMenu/ContextMenu.doc.mjs +5 -0
  373. package/src/ContextMenu/ContextMenu.tsx +4 -3
  374. package/src/DateInput/DateInput.doc.mjs +16 -0
  375. package/src/DateInput/DateInput.tsx +55 -2
  376. package/src/DateInput/DateInputTouch.test.tsx +68 -9
  377. package/src/DateInput/NativeDateField.test.tsx +635 -0
  378. package/src/DateInput/NativeDateField.tsx +641 -0
  379. package/src/DateInput/TouchDateField.tsx +38 -4
  380. package/src/DateInput/index.ts +1 -0
  381. package/src/DateInput/nativeDateSegments.ts +127 -0
  382. package/src/DateTimeInput/DateTimeInput.doc.mjs +18 -0
  383. package/src/DateTimeInput/DateTimeInput.test.tsx +1088 -0
  384. package/src/DateTimeInput/DateTimeInput.tsx +556 -24
  385. package/src/DateTimeInput/index.ts +1 -0
  386. package/src/Dialog/Dialog.doc.mjs +8 -3
  387. package/src/Dialog/Dialog.test.tsx +261 -1
  388. package/src/Dialog/Dialog.tsx +109 -58
  389. package/src/DropdownMenu/DropdownMenu.doc.mjs +6 -0
  390. package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +8 -5
  391. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +8 -11
  392. package/src/Field/Field.doc.mjs +13 -2
  393. package/src/Field/FieldLabel.test.tsx +29 -0
  394. package/src/Field/FieldLabel.tsx +7 -0
  395. package/src/FileInput/FileInput.tsx +3 -2
  396. package/src/Heading/Heading.test.tsx +23 -1
  397. package/src/Heading/Heading.tsx +6 -2
  398. package/src/HoverCard/HoverCard.doc.mjs +13 -2
  399. package/src/HoverCard/HoverCard.test.tsx +101 -4
  400. package/src/HoverCard/HoverCard.tsx +71 -3
  401. package/src/HoverCard/useHoverCard.tsx +101 -33
  402. package/src/Indicator/CheckboxIndicator.tsx +14 -8
  403. package/src/Indicator/Indicator.doc.mjs +2 -0
  404. package/src/Item/Item.tsx +5 -4
  405. package/src/Kbd/Kbd.tsx +2 -19
  406. package/src/Layer/LayerDepthContext.tsx +62 -0
  407. package/src/Layer/index.ts +9 -0
  408. package/src/Layer/layerDismissalFamilies.test.tsx +359 -0
  409. package/src/Layer/layerDismissalInvariants.test.tsx +417 -0
  410. package/src/Layer/layerStack.ts +362 -0
  411. package/src/Layer/useLayerDismissal.test.tsx +469 -0
  412. package/src/Layer/useLayerDismissal.ts +173 -0
  413. package/src/Layout/Layout.doc.mjs +2 -2
  414. package/src/Lightbox/Lightbox.doc.mjs +12 -0
  415. package/src/Lightbox/Lightbox.tsx +124 -102
  416. package/src/Link/Link.doc.mjs +39 -0
  417. package/src/Markdown/Markdown.doc.mjs +5 -0
  418. package/src/Markdown/incremental.test.ts +16 -2
  419. package/src/Markdown/index.ts +1 -0
  420. package/src/Markdown/parser.perf.test.ts +96 -1
  421. package/src/Markdown/parser.test.ts +145 -2
  422. package/src/Markdown/parser.ts +244 -29
  423. package/src/MobileNav/MobileNav.tsx +62 -36
  424. package/src/MoreMenu/MoreMenu.doc.mjs +7 -1
  425. package/src/MultiSelector/MultiSelector.doc.mjs +27 -0
  426. package/src/MultiSelector/MultiSelector.test.tsx +106 -0
  427. package/src/MultiSelector/MultiSelector.tsx +50 -6
  428. package/src/MultiSelector/index.ts +1 -0
  429. package/src/NavIcon/NavIcon.doc.mjs +8 -2
  430. package/src/NavIcon/NavIcon.test.tsx +9 -0
  431. package/src/NavIcon/NavIcon.tsx +5 -1
  432. package/src/NavMenu/NavHeadingMenu.tsx +3 -2
  433. package/src/NumberInput/NumberInput.doc.mjs +20 -0
  434. package/src/NumberInput/NumberInput.tsx +28 -29
  435. package/src/NumberInput/numberParser.test.ts +210 -0
  436. package/src/NumberInput/numberParser.ts +386 -0
  437. package/src/Outline/Outline.tsx +3 -2
  438. package/src/OverflowList/OverflowList.tsx +3 -2
  439. package/src/Overlay/Overlay.tsx +3 -2
  440. package/src/Popover/usePopover.tsx +31 -28
  441. package/src/PowerSearch/PowerSearch.doc.mjs +160 -28
  442. package/src/PowerSearch/PowerSearch.test.tsx +240 -1
  443. package/src/PowerSearch/PowerSearch.tsx +34 -5
  444. package/src/PowerSearch/PowerSearchEditPopover.test.tsx +201 -0
  445. package/src/PowerSearch/PowerSearchEditPopover.tsx +29 -2
  446. package/src/PowerSearch/PowerSearchValueEditor.test.tsx +47 -1
  447. package/src/PowerSearch/PowerSearchValueEditor.tsx +15 -0
  448. package/src/PowerSearch/types.ts +2 -0
  449. package/src/PowerSearch/usePowerSearchSource.test.ts +82 -2
  450. package/src/PowerSearch/usePowerSearchSource.ts +13 -3
  451. package/src/ProgressBar/ProgressBar.doc.mjs +42 -6
  452. package/src/ProgressBar/ProgressBar.test.tsx +24 -0
  453. package/src/ProgressBar/ProgressBar.tsx +28 -8
  454. package/src/Resizable/ResizeHandle.tsx +3 -2
  455. package/src/SegmentedControl/SegmentedControl.tsx +3 -2
  456. package/src/SelectableCard/SelectableCard.tsx +3 -2
  457. package/src/Selector/Selector.doc.mjs +12 -1
  458. package/src/Selector/Selector.test.tsx +45 -1
  459. package/src/Selector/Selector.tsx +11 -1
  460. package/src/SideNav/SideNav.doc.mjs +6 -6
  461. package/src/SideNav/SideNav.tsx +4 -2
  462. package/src/SideNav/SideNavHeading.tsx +12 -12
  463. package/src/SideNav/SideNavItem.tsx +4 -2
  464. package/src/Slider/Slider.test.tsx +106 -1
  465. package/src/Slider/Slider.tsx +55 -3
  466. package/src/Spinner/Spinner.test.tsx +147 -1
  467. package/src/Spinner/Spinner.tsx +120 -101
  468. package/src/StatusDot/StatusDot.doc.mjs +8 -2
  469. package/src/StatusDot/StatusDot.test.tsx +9 -0
  470. package/src/StatusDot/StatusDot.tsx +7 -1
  471. package/src/Stepper/Step.doc.mjs +98 -0
  472. package/src/Stepper/Step.tsx +1638 -0
  473. package/src/Stepper/StepStatus.ts +26 -0
  474. package/src/Stepper/Stepper.doc.mjs +370 -0
  475. package/src/Stepper/Stepper.test.tsx +1024 -0
  476. package/src/Stepper/Stepper.tsx +258 -0
  477. package/src/Stepper/StepperContext.ts +71 -0
  478. package/src/Stepper/index.ts +18 -0
  479. package/src/Stepper/stepper.stylex.ts +19 -0
  480. package/src/Switch/Switch.doc.mjs +2 -0
  481. package/src/Switch/Switch.test.tsx +10 -0
  482. package/src/Switch/Switch.tsx +6 -2
  483. package/src/TabList/Tab.doc.mjs +13 -1
  484. package/src/TabList/Tab.tsx +67 -7
  485. package/src/TabList/TabList.doc.mjs +22 -6
  486. package/src/TabList/TabList.test.tsx +559 -0
  487. package/src/TabList/TabList.tsx +490 -22
  488. package/src/TabList/TabListContext.ts +13 -2
  489. package/src/TabList/TabMenu.tsx +3 -2
  490. package/src/TabList/index.ts +6 -2
  491. package/src/Table/BaseTable.tsx +12 -4
  492. package/src/Table/Table.doc.mjs +9 -2
  493. package/src/Table/Table.test.tsx +132 -14
  494. package/src/Table/Table.tsx +3 -8
  495. package/src/Table/TableBody.doc.mjs +40 -0
  496. package/src/Table/TableFooter.doc.mjs +40 -0
  497. package/src/Table/TableHeader.doc.mjs +40 -0
  498. package/src/Table/TableRow.tsx +16 -5
  499. package/src/Table/plugins/groupedRows/useTableGroupedRows-perf.test.tsx +112 -0
  500. package/src/Table/plugins/groupedRows/useTableGroupedRows.test.tsx +100 -0
  501. package/src/Table/plugins/groupedRows/useTableGroupedRows.tsx +17 -8
  502. package/src/Table/plugins/pagination/useTablePagination.test.tsx +34 -4
  503. package/src/Table/plugins/pagination/useTablePagination.tsx +35 -3
  504. package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +9 -2
  505. package/src/Table/plugins/rowStatus/useTableRowStatus.test.tsx +13 -7
  506. package/src/Table/plugins/rowStatus/useTableRowStatus.tsx +12 -3
  507. package/src/Table/plugins/selection/useTableSelection.test.tsx +76 -0
  508. package/src/Table/plugins/selection/useTableSelection.tsx +40 -7
  509. package/src/Table/plugins/stickyColumns/useTableStickyColumns.tsx +1 -2
  510. package/src/Table/types.ts +22 -4
  511. package/src/Table/useBaseTablePlugins.ts +5 -0
  512. package/src/Table/useTableGroupedRows.doc.mjs +5 -4
  513. package/src/Table/useTablePagination.doc.mjs +7 -0
  514. package/src/Table/useTableRowStatus.doc.mjs +1 -1
  515. package/src/Table/useTableSelection.doc.mjs +31 -0
  516. package/src/Text/Text.test.tsx +23 -1
  517. package/src/Text/Text.tsx +6 -2
  518. package/src/TextArea/TextArea.doc.mjs +8 -2
  519. package/src/TextArea/TextArea.test.tsx +11 -0
  520. package/src/TextArea/TextArea.tsx +15 -8
  521. package/src/TextInput/TextInput.doc.mjs +10 -0
  522. package/src/TextInput/TextInput.tsx +3 -2
  523. package/src/Thumbnail/Thumbnail.doc.mjs +1 -1
  524. package/src/TimeInput/TimeInput.tsx +2 -2
  525. package/src/Timestamp/Timestamp.tsx +4 -2
  526. package/src/Toast/Toast.doc.mjs +6 -0
  527. package/src/Toast/ToastViewport.tsx +17 -1
  528. package/src/Token/TokenLink.tsx +2 -2
  529. package/src/Tokenizer/Tokenizer.doc.mjs +27 -0
  530. package/src/Tokenizer/Tokenizer.tsx +4 -0
  531. package/src/Toolbar/Toolbar.doc.mjs +5 -0
  532. package/src/Tooltip/Tooltip.doc.mjs +5 -0
  533. package/src/Tooltip/Tooltip.test.tsx +32 -0
  534. package/src/Tooltip/Tooltip.tsx +5 -0
  535. package/src/Tooltip/useTooltip.tsx +47 -24
  536. package/src/TopNav/TopNavHeading.tsx +5 -4
  537. package/src/TopNav/TopNavMegaMenu.tsx +3 -2
  538. package/src/TopNav/TopNavMenu.tsx +4 -3
  539. package/src/Typeahead/BaseTypeahead.doc.mjs +11 -0
  540. package/src/Typeahead/BaseTypeahead.tsx +85 -33
  541. package/src/Typeahead/Typeahead.doc.mjs +5 -0
  542. package/src/Typeahead/Typeahead.test.tsx +49 -0
  543. package/src/Typeahead/Typeahead.tsx +3 -2
  544. package/src/hooks/index.ts +2 -0
  545. package/src/hooks/useContainerReveal.doc.mjs +2 -2
  546. package/src/hooks/useFocusTrap.test.tsx +14 -0
  547. package/src/hooks/useFocusTrap.ts +62 -111
  548. package/src/hooks/useFocusTrapEscapeShim.test.tsx +165 -0
  549. package/src/hooks/useHotkeys.ts +3 -18
  550. package/src/hooks/useMergedRefs.doc.mjs +79 -0
  551. package/src/hooks/useMergedRefs.test.tsx +62 -0
  552. package/src/hooks/useMergedRefs.ts +36 -0
  553. package/src/index.ts +1 -0
  554. package/src/theme/MediaTheme.doc.mjs +5 -5
  555. package/src/theme/derivedVarRegistry.test.ts +19 -3
  556. package/src/theme/derivedVarRegistry.ts +23 -4
  557. package/src/theme/generateThemeRules.test.ts +37 -0
  558. package/src/theme/syntax/tokens.ts +5 -2
  559. package/src/utils/groupItems.ts +12 -6
  560. package/src/utils/index.ts +1 -1
  561. package/src/utils/isApplePlatform.test.ts +61 -0
  562. package/src/utils/isApplePlatform.ts +37 -0
  563. package/src/utils/sharedResizeObserver.ts +10 -2
  564. package/src/utils/timeParser.test.ts +6 -1
  565. package/src/utils/timeParser.ts +35 -13
  566. package/src/__tests__/inputWidthContract.test.tsx +0 -69
@@ -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}
@@ -13,7 +13,10 @@ import {useState} from 'react';
13
13
  import {describe, it, expect, vi, beforeEach} from 'vitest';
14
14
  import {render, screen, act, fireEvent, waitFor} from '@testing-library/react';
15
15
  import userEvent from '@testing-library/user-event';
16
+ import * as stylex from '@stylexjs/stylex';
16
17
  import {Slider} from './Slider';
18
+ import {focusOutlineStyles} from '../utils/focusOutline.stylex';
19
+ import {__resetInteractionModalityForTest} from '../utils/interactionModality';
17
20
 
18
21
  // Mock showPopover/hidePopover (not implemented in jsdom) so the tooltip layer
19
22
  // reflects its open state via a `popover-open` attribute the tests can assert.
@@ -854,7 +857,7 @@ describe('Slider', () => {
854
857
  />,
855
858
  );
856
859
  const thumb = screen.getByRole('slider');
857
- thumb.focus();
860
+ act(() => thumb.focus());
858
861
  await user.keyboard('{ArrowRight}');
859
862
  expect(onChange).not.toHaveBeenCalled();
860
863
  });
@@ -902,4 +905,106 @@ describe('Slider', () => {
902
905
  ]).toEqual([]);
903
906
  });
904
907
  });
908
+
909
+ describe('focus ring modality', () => {
910
+ beforeEach(() => {
911
+ __resetInteractionModalityForTest();
912
+ });
913
+
914
+ // The ring is a stylex class, so derive it from the same source the
915
+ // component applies rather than hardcoding a hash.
916
+ const RING = stylex
917
+ .props(focusOutlineStyles.focusVisible)
918
+ .className!.split(' ');
919
+ const isRinged = (el: HTMLElement) =>
920
+ RING.every(c => el.classList.contains(c));
921
+
922
+ // The track has no layout in jsdom; pointer maths needs a real rect.
923
+ const grabTrack = (thumb: HTMLElement) => {
924
+ const track = thumb.parentElement!;
925
+ track.getBoundingClientRect = () => ({
926
+ left: 0,
927
+ top: 0,
928
+ right: 200,
929
+ bottom: 20,
930
+ width: 200,
931
+ height: 20,
932
+ x: 0,
933
+ y: 0,
934
+ toJSON: () => {},
935
+ });
936
+ fireEvent.pointerDown(track, {clientX: 100, clientY: 10, pointerId: 1});
937
+ };
938
+
939
+ it('rings when the thumb is reached with the keyboard', async () => {
940
+ const user = userEvent.setup();
941
+ render(<Slider label="Volume" value={50} onChange={vi.fn()} />);
942
+ await user.tab();
943
+ expect(isRinged(screen.getByRole('slider'))).toBe(true);
944
+ });
945
+
946
+ it('does not ring when the thumb is grabbed with the mouse', () => {
947
+ render(<Slider label="Volume" value={50} onChange={vi.fn()} />);
948
+ const thumb = screen.getByRole('slider');
949
+ grabTrack(thumb);
950
+ expect(thumb).toHaveFocus();
951
+ expect(isRinged(thumb)).toBe(false);
952
+ });
953
+
954
+ it('drops the ring when the mouse grabs a thumb that already had it', async () => {
955
+ const user = userEvent.setup();
956
+ render(<Slider label="Volume" value={50} onChange={vi.fn()} />);
957
+ await user.tab();
958
+ const thumb = screen.getByRole('slider');
959
+ expect(isRinged(thumb)).toBe(true);
960
+ // Already focused, so focus() fires no focus event.
961
+ grabTrack(thumb);
962
+ expect(isRinged(thumb)).toBe(false);
963
+ });
964
+
965
+ it('brings the ring back when an arrow key follows a mouse drag', () => {
966
+ render(<Slider label="Volume" value={50} onChange={vi.fn()} />);
967
+ const thumb = screen.getByRole('slider');
968
+ grabTrack(thumb);
969
+ expect(isRinged(thumb)).toBe(false);
970
+ fireEvent.keyDown(thumb, {key: 'ArrowRight'});
971
+ expect(isRinged(thumb)).toBe(true);
972
+ });
973
+
974
+ it('leaves the ring off for a modifier chord after a mouse drag', () => {
975
+ render(<Slider label="Volume" value={50} onChange={vi.fn()} />);
976
+ const thumb = screen.getByRole('slider');
977
+ grabTrack(thumb);
978
+ // Copying or reloading is not navigation, and the mouse is still on it.
979
+ fireEvent.keyDown(thumb, {key: 'c', metaKey: true});
980
+ expect(isRinged(thumb)).toBe(false);
981
+ });
982
+
983
+ it('drops the ring on blur', async () => {
984
+ const user = userEvent.setup();
985
+ render(<Slider label="Volume" value={50} onChange={vi.fn()} />);
986
+ await user.tab();
987
+ const thumb = screen.getByRole('slider');
988
+ expect(isRinged(thumb)).toBe(true);
989
+ act(() => thumb.blur());
990
+ expect(isRinged(thumb)).toBe(false);
991
+ });
992
+
993
+ it('rings the range thumb the keyboard reached, not its sibling', async () => {
994
+ const user = userEvent.setup();
995
+ render(
996
+ <Slider
997
+ label="Price"
998
+ value={[20, 80] as [number, number]}
999
+ onChange={vi.fn()}
1000
+ />,
1001
+ );
1002
+ await user.tab();
1003
+ await user.tab();
1004
+ const thumbs = screen.getAllByRole('slider');
1005
+ expect(thumbs[1]).toHaveFocus();
1006
+ expect(isRinged(thumbs[1])).toBe(true);
1007
+ expect(isRinged(thumbs[0])).toBe(false);
1008
+ });
1009
+ });
905
1010
  });
@@ -17,11 +17,13 @@
17
17
  */
18
18
 
19
19
  import {
20
+ useEffect,
20
21
  useId,
21
22
  useMemo,
22
23
  useRef,
23
24
  useState,
24
25
  useCallback,
26
+ type FocusEvent,
25
27
  type KeyboardEvent,
26
28
  type PointerEvent,
27
29
  } from 'react';
@@ -40,13 +42,18 @@ import {Tooltip} from '../Tooltip/Tooltip';
40
42
  import {useTooltip} from '../Tooltip';
41
43
  import {VisuallyHidden} from '../VisuallyHidden';
42
44
  import type {InputStatus} from '../Field/types';
43
- import {mergeProps, mergeRefs, rtlStyles} from '../utils';
45
+ import {mergeProps, rtlStyles} from '../utils';
44
46
  import {focusOutlineStyles} from '../utils/focusOutline.stylex';
47
+ import {
48
+ getInteractionModality,
49
+ trackInteractionModality,
50
+ } from '../utils/interactionModality';
45
51
  import {isRtlElement} from '../hooks/isRtlElement';
46
52
  import type {BaseProps} from '../BaseProps';
47
53
  import type {SizeValue} from '../utils/types';
48
54
  import {themeProps} from '../utils/themeProps';
49
55
 
56
+ import {useMergedRefs} from '../hooks/useMergedRefs';
50
57
  // =============================================================================
51
58
  // Types
52
59
  // =============================================================================
@@ -492,6 +499,36 @@ export function Slider({ref, ...props}: SliderProps) {
492
499
  const draggingThumbRef = useRef<number | null>(null);
493
500
  const [draggingThumb, setDraggingThumb] = useState<number | null>(null);
494
501
 
502
+ // A thumb is a div[role="slider"], and `handlePointerDown` focuses it from
503
+ // script after preventDefault — which Chromium treats as focus-visible, so
504
+ // dragging with a mouse drew the keyboard ring (measured: `:focus-visible`
505
+ // true on pointerdown). Gate the ring on how the user last interacted; the
506
+ // CSS condition stays `:focus-visible`, this only narrows it.
507
+ const [keyboardFocusThumb, setKeyboardFocusThumb] = useState<number | null>(
508
+ null,
509
+ );
510
+
511
+ useEffect(() => {
512
+ trackInteractionModality();
513
+ }, []);
514
+
515
+ const handleThumbFocus = useCallback(
516
+ (thumbIndex: number, _e: FocusEvent<HTMLDivElement>) => {
517
+ // A disabled thumb draws no ring even when it stays focusable for its
518
+ // reason tooltip, so there is nothing to track for one.
519
+ setKeyboardFocusThumb(
520
+ !isDisabled && getInteractionModality() === 'keyboard'
521
+ ? thumbIndex
522
+ : null,
523
+ );
524
+ },
525
+ [isDisabled],
526
+ );
527
+
528
+ const handleThumbBlur = useCallback((_e: FocusEvent<HTMLDivElement>) => {
529
+ setKeyboardFocusThumb(null);
530
+ }, []);
531
+
495
532
  // Disabled-reason tooltip. This is a *separate* useTooltip instance from the
496
533
  // per-thumb value bubble (the `<Tooltip>` component below): it anchors to the
497
534
  // track container and fires on hover/focus of the whole control. Disabled
@@ -678,6 +715,10 @@ export function Slider({ref, ...props}: SliderProps) {
678
715
  const thumbs = track.querySelectorAll<HTMLElement>('[role="slider"]');
679
716
  thumbs[thumbIndex]?.focus();
680
717
  }
718
+ // Also clear it explicitly: focusing an already-focused thumb fires no
719
+ // focus event, so a thumb the user had tabbed to would keep its ring
720
+ // through the drag.
721
+ setKeyboardFocusThumb(null);
681
722
 
682
723
  if (
683
724
  typeof (e.currentTarget as HTMLElement).setPointerCapture === 'function'
@@ -716,6 +757,13 @@ export function Slider({ref, ...props}: SliderProps) {
716
757
  if (isDisabled) {
717
758
  return;
718
759
  }
760
+ // Unlike a text field, a thumb has no caret to show where input is
761
+ // going, so a keypress after a mouse drag must bring the ring back.
762
+ // Ask the utility rather than assuming: a modifier chord (⌘R, ⌃C) is
763
+ // not navigation and must not re-ring a thumb the mouse is holding.
764
+ if (getInteractionModality() === 'keyboard') {
765
+ setKeyboardFocusThumb(thumbIndex);
766
+ }
719
767
  const currentVal = values[thumbIndex];
720
768
  let newVal: number;
721
769
 
@@ -842,6 +890,8 @@ export function Slider({ref, ...props}: SliderProps) {
842
890
  aria-labelledby={!isRange ? labelID : undefined}
843
891
  aria-describedby={ariaDescribedBy}
844
892
  onKeyDown={e => handleKeyDown(thumbIndex, e)}
893
+ onFocus={e => handleThumbFocus(thumbIndex, e)}
894
+ onBlur={handleThumbBlur}
845
895
  {...mergeProps(
846
896
  themeProps('slider-thumb', {
847
897
  orientation,
@@ -853,7 +903,9 @@ export function Slider({ref, ...props}: SliderProps) {
853
903
  ? styles.thumbHorizontal
854
904
  : rtlStyles.centerInline('50%'),
855
905
  !isDisabled && styles.thumbHover,
856
- !isDisabled && focusOutlineStyles.focusVisible,
906
+ !isDisabled &&
907
+ keyboardFocusThumb === thumbIndex &&
908
+ focusOutlineStyles.focusVisible,
857
909
  isDisabled && styles.thumbDisabled,
858
910
  ),
859
911
  undefined,
@@ -964,7 +1016,7 @@ export function Slider({ref, ...props}: SliderProps) {
964
1016
  />
965
1017
  ))}
966
1018
  <div
967
- ref={mergeRefs(ref, trackRef, disabledMessageTooltip.ref)}
1019
+ ref={useMergedRefs(ref, trackRef, disabledMessageTooltip.ref)}
968
1020
  {...(isRange
969
1021
  ? {role: 'group', 'aria-labelledby': labelID}
970
1022
  : 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
+ });