@astryxdesign/core 0.4.7 → 0.5.0-canary.54bb922

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 (467) 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.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.map +1 -1
  15. package/dist/BottomSheet/BottomSheet.js +17 -4
  16. package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
  17. package/dist/BottomSheet/BottomSheetSwitcher.js +3 -2
  18. package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
  19. package/dist/Breadcrumbs/BreadcrumbItem.js +11 -10
  20. package/dist/Button/Button.d.ts.map +1 -1
  21. package/dist/Button/Button.js +5 -4
  22. package/dist/ButtonGroup/ButtonGroup.d.ts +5 -1
  23. package/dist/ButtonGroup/ButtonGroup.d.ts.map +1 -1
  24. package/dist/ButtonGroup/ButtonGroup.js +26 -7
  25. package/dist/Calendar/Calendar.d.ts +3 -1
  26. package/dist/Calendar/Calendar.d.ts.map +1 -1
  27. package/dist/Calendar/Calendar.js +13 -14
  28. package/dist/Calendar/getInitialFocusDate.d.ts +46 -0
  29. package/dist/Calendar/getInitialFocusDate.d.ts.map +1 -0
  30. package/dist/Calendar/getInitialFocusDate.js +62 -0
  31. package/dist/Carousel/Carousel.d.ts.map +1 -1
  32. package/dist/Carousel/Carousel.js +3 -3
  33. package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
  34. package/dist/Chat/ChatComposerInput.js +2 -3
  35. package/dist/Chat/ChatLayout.d.ts.map +1 -1
  36. package/dist/Chat/ChatLayout.js +3 -3
  37. package/dist/Chat/ChatMessageList.d.ts +22 -3
  38. package/dist/Chat/ChatMessageList.d.ts.map +1 -1
  39. package/dist/Chat/ChatMessageList.js +6 -3
  40. package/dist/Chat/ChatTokenizedText.d.ts +2 -0
  41. package/dist/Chat/ChatTokenizedText.d.ts.map +1 -1
  42. package/dist/Chat/ChatTokenizedText.js +11 -6
  43. package/dist/Chat/useChatComposerTokens.js +1 -1
  44. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  45. package/dist/CheckboxInput/CheckboxInput.js +3 -2
  46. package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
  47. package/dist/ClickableCard/ClickableCard.js +3 -3
  48. package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
  49. package/dist/CommandPalette/CommandPaletteInput.js +3 -2
  50. package/dist/CommandPalette/CommandPaletteItem.d.ts.map +1 -1
  51. package/dist/CommandPalette/CommandPaletteItem.js +3 -2
  52. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  53. package/dist/ContextMenu/ContextMenu.js +4 -3
  54. package/dist/DateInput/DateInput.d.ts.map +1 -1
  55. package/dist/DateInput/DateInput.js +3 -2
  56. package/dist/DateInput/TouchDateField.d.ts.map +1 -1
  57. package/dist/DateInput/TouchDateField.js +38 -4
  58. package/dist/DateTimeInput/DateTimeInput.d.ts +21 -1
  59. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  60. package/dist/DateTimeInput/DateTimeInput.js +406 -12
  61. package/dist/DateTimeInput/index.d.ts +1 -1
  62. package/dist/DateTimeInput/index.d.ts.map +1 -1
  63. package/dist/Dialog/Dialog.d.ts +7 -1
  64. package/dist/Dialog/Dialog.d.ts.map +1 -1
  65. package/dist/Dialog/Dialog.js +89 -58
  66. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
  67. package/dist/DropdownMenu/DropdownMenuSubMenu.js +6 -5
  68. package/dist/FileInput/FileInput.d.ts.map +1 -1
  69. package/dist/FileInput/FileInput.js +3 -2
  70. package/dist/Heading/Heading.d.ts.map +1 -1
  71. package/dist/Heading/Heading.js +6 -2
  72. package/dist/HoverCard/HoverCard.d.ts +6 -0
  73. package/dist/HoverCard/HoverCard.d.ts.map +1 -1
  74. package/dist/HoverCard/useHoverCard.d.ts +5 -0
  75. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  76. package/dist/HoverCard/useHoverCard.js +67 -25
  77. package/dist/Item/Item.d.ts.map +1 -1
  78. package/dist/Item/Item.js +4 -2
  79. package/dist/Kbd/Kbd.d.ts.map +1 -1
  80. package/dist/Kbd/Kbd.js +2 -19
  81. package/dist/Layer/LayerDepthContext.d.ts +52 -0
  82. package/dist/Layer/LayerDepthContext.d.ts.map +1 -0
  83. package/dist/Layer/LayerDepthContext.js +66 -0
  84. package/dist/Layer/index.d.ts +3 -0
  85. package/dist/Layer/index.d.ts.map +1 -1
  86. package/dist/Layer/index.js +4 -0
  87. package/dist/Layer/layerStack.d.ts +98 -0
  88. package/dist/Layer/layerStack.d.ts.map +1 -0
  89. package/dist/Layer/layerStack.js +306 -0
  90. package/dist/Layer/useLayerDismissal.d.ts +93 -0
  91. package/dist/Layer/useLayerDismissal.d.ts.map +1 -0
  92. package/dist/Layer/useLayerDismissal.js +91 -0
  93. package/dist/Lightbox/Lightbox.d.ts +1 -0
  94. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  95. package/dist/Lightbox/Lightbox.js +103 -81
  96. package/dist/Markdown/index.d.ts +1 -1
  97. package/dist/Markdown/index.d.ts.map +1 -1
  98. package/dist/Markdown/parser.d.ts +38 -1
  99. package/dist/Markdown/parser.d.ts.map +1 -1
  100. package/dist/Markdown/parser.js +177 -29
  101. package/dist/MobileNav/MobileNav.d.ts +6 -1
  102. package/dist/MobileNav/MobileNav.d.ts.map +1 -1
  103. package/dist/MobileNav/MobileNav.js +61 -36
  104. package/dist/MultiSelector/MultiSelector.d.ts +15 -1
  105. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  106. package/dist/MultiSelector/MultiSelector.js +17 -6
  107. package/dist/MultiSelector/index.d.ts +1 -1
  108. package/dist/MultiSelector/index.d.ts.map +1 -1
  109. package/dist/NavMenu/NavHeadingMenu.d.ts.map +1 -1
  110. package/dist/NavMenu/NavHeadingMenu.js +3 -2
  111. package/dist/Outline/Outline.d.ts.map +1 -1
  112. package/dist/Outline/Outline.js +3 -2
  113. package/dist/OverflowList/OverflowList.d.ts.map +1 -1
  114. package/dist/OverflowList/OverflowList.js +3 -2
  115. package/dist/Overlay/Overlay.d.ts.map +1 -1
  116. package/dist/Overlay/Overlay.js +3 -2
  117. package/dist/Popover/usePopover.d.ts.map +1 -1
  118. package/dist/Popover/usePopover.js +20 -17
  119. package/dist/PowerSearch/PowerSearch.d.ts +8 -2
  120. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  121. package/dist/PowerSearch/PowerSearch.js +19 -6
  122. package/dist/PowerSearch/PowerSearchEditPopover.d.ts +3 -1
  123. package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
  124. package/dist/PowerSearch/PowerSearchEditPopover.js +28 -6
  125. package/dist/PowerSearch/PowerSearchValueEditor.d.ts +3 -1
  126. package/dist/PowerSearch/PowerSearchValueEditor.d.ts.map +1 -1
  127. package/dist/PowerSearch/PowerSearchValueEditor.js +19 -9
  128. package/dist/PowerSearch/types.d.ts +2 -0
  129. package/dist/PowerSearch/types.d.ts.map +1 -1
  130. package/dist/PowerSearch/usePowerSearchSource.d.ts +6 -1
  131. package/dist/PowerSearch/usePowerSearchSource.d.ts.map +1 -1
  132. package/dist/PowerSearch/usePowerSearchSource.js +14 -5
  133. package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
  134. package/dist/Resizable/ResizeHandle.js +3 -2
  135. package/dist/SegmentedControl/SegmentedControl.d.ts.map +1 -1
  136. package/dist/SegmentedControl/SegmentedControl.js +3 -2
  137. package/dist/SelectableCard/SelectableCard.d.ts.map +1 -1
  138. package/dist/SelectableCard/SelectableCard.js +3 -3
  139. package/dist/Selector/Selector.d.ts.map +1 -1
  140. package/dist/Selector/Selector.js +5 -0
  141. package/dist/SideNav/SideNav.d.ts.map +1 -1
  142. package/dist/SideNav/SideNav.js +4 -3
  143. package/dist/SideNav/SideNavHeading.d.ts.map +1 -1
  144. package/dist/SideNav/SideNavHeading.js +6 -5
  145. package/dist/SideNav/SideNavItem.d.ts.map +1 -1
  146. package/dist/SideNav/SideNavItem.js +4 -3
  147. package/dist/Slider/Slider.d.ts.map +1 -1
  148. package/dist/Slider/Slider.js +3 -3
  149. package/dist/Spinner/Spinner.d.ts +3 -3
  150. package/dist/Spinner/Spinner.d.ts.map +1 -1
  151. package/dist/Spinner/Spinner.js +130 -78
  152. package/dist/Stepper/Step.d.ts +140 -0
  153. package/dist/Stepper/Step.d.ts.map +1 -0
  154. package/dist/Stepper/Step.js +1045 -0
  155. package/dist/Stepper/StepStatus.d.ts +24 -0
  156. package/dist/Stepper/StepStatus.d.ts.map +1 -0
  157. package/dist/Stepper/StepStatus.js +1 -0
  158. package/dist/Stepper/Stepper.d.ts +93 -0
  159. package/dist/Stepper/Stepper.d.ts.map +1 -0
  160. package/dist/Stepper/Stepper.js +184 -0
  161. package/dist/Stepper/StepperContext.d.ts +41 -0
  162. package/dist/Stepper/StepperContext.d.ts.map +1 -0
  163. package/dist/Stepper/StepperContext.js +34 -0
  164. package/dist/Stepper/index.d.ts +8 -0
  165. package/dist/Stepper/index.d.ts.map +1 -0
  166. package/dist/Stepper/index.js +7 -0
  167. package/dist/Stepper/stepper.stylex.d.ts +15 -0
  168. package/dist/Stepper/stepper.stylex.d.ts.map +1 -0
  169. package/dist/Stepper/stepper.stylex.js +20 -0
  170. package/dist/Switch/Switch.d.ts.map +1 -1
  171. package/dist/Switch/Switch.js +3 -2
  172. package/dist/TabList/Tab.d.ts +15 -2
  173. package/dist/TabList/Tab.d.ts.map +1 -1
  174. package/dist/TabList/Tab.js +32 -7
  175. package/dist/TabList/TabList.d.ts +46 -7
  176. package/dist/TabList/TabList.d.ts.map +1 -1
  177. package/dist/TabList/TabList.js +353 -23
  178. package/dist/TabList/TabListContext.d.ts +11 -1
  179. package/dist/TabList/TabListContext.d.ts.map +1 -1
  180. package/dist/TabList/TabListContext.js +11 -2
  181. package/dist/TabList/TabMenu.d.ts.map +1 -1
  182. package/dist/TabList/TabMenu.js +3 -2
  183. package/dist/TabList/index.d.ts +2 -2
  184. package/dist/TabList/index.d.ts.map +1 -1
  185. package/dist/Table/BaseTable.d.ts.map +1 -1
  186. package/dist/Table/BaseTable.js +4 -1
  187. package/dist/Table/TableRow.d.ts +16 -5
  188. package/dist/Table/TableRow.d.ts.map +1 -1
  189. package/dist/Table/TableRow.js +16 -5
  190. package/dist/Table/plugins/groupedRows/useTableGroupedRows.d.ts.map +1 -1
  191. package/dist/Table/plugins/groupedRows/useTableGroupedRows.js +20 -8
  192. package/dist/Table/plugins/pagination/useTablePagination.d.ts +3 -0
  193. package/dist/Table/plugins/pagination/useTablePagination.d.ts.map +1 -1
  194. package/dist/Table/plugins/pagination/useTablePagination.js +21 -4
  195. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
  196. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +7 -2
  197. package/dist/Table/plugins/rowStatus/useTableRowStatus.d.ts.map +1 -1
  198. package/dist/Table/plugins/rowStatus/useTableRowStatus.js +10 -3
  199. package/dist/Table/plugins/selection/useTableSelection.d.ts +16 -0
  200. package/dist/Table/plugins/selection/useTableSelection.d.ts.map +1 -1
  201. package/dist/Table/plugins/selection/useTableSelection.js +19 -5
  202. package/dist/Table/types.d.ts +22 -4
  203. package/dist/Table/types.d.ts.map +1 -1
  204. package/dist/Table/useBaseTablePlugins.d.ts.map +1 -1
  205. package/dist/Table/useBaseTablePlugins.js +5 -0
  206. package/dist/Text/Text.d.ts.map +1 -1
  207. package/dist/Text/Text.js +6 -2
  208. package/dist/TextArea/TextArea.d.ts.map +1 -1
  209. package/dist/TextArea/TextArea.js +3 -2
  210. package/dist/TextInput/TextInput.d.ts.map +1 -1
  211. package/dist/TextInput/TextInput.js +3 -2
  212. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  213. package/dist/TimeInput/TimeInput.js +3 -2
  214. package/dist/Timestamp/Timestamp.d.ts.map +1 -1
  215. package/dist/Timestamp/Timestamp.js +5 -3
  216. package/dist/Token/TokenLink.d.ts.map +1 -1
  217. package/dist/Token/TokenLink.js +2 -2
  218. package/dist/Tokenizer/Tokenizer.d.ts +3 -1
  219. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  220. package/dist/Tokenizer/Tokenizer.js +2 -0
  221. package/dist/Tooltip/Tooltip.d.ts +5 -0
  222. package/dist/Tooltip/Tooltip.d.ts.map +1 -1
  223. package/dist/Tooltip/useTooltip.d.ts +5 -0
  224. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  225. package/dist/Tooltip/useTooltip.js +39 -24
  226. package/dist/TopNav/TopNavHeading.d.ts.map +1 -1
  227. package/dist/TopNav/TopNavHeading.js +3 -2
  228. package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
  229. package/dist/TopNav/TopNavMegaMenu.js +3 -3
  230. package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
  231. package/dist/TopNav/TopNavMenu.js +4 -3
  232. package/dist/Typeahead/BaseTypeahead.d.ts +2 -0
  233. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  234. package/dist/Typeahead/BaseTypeahead.js +68 -29
  235. package/dist/Typeahead/Typeahead.d.ts.map +1 -1
  236. package/dist/Typeahead/Typeahead.js +3 -2
  237. package/dist/astryx.css +68 -2
  238. package/dist/hooks/index.d.ts +1 -0
  239. package/dist/hooks/index.d.ts.map +1 -1
  240. package/dist/hooks/index.js +1 -0
  241. package/dist/hooks/useFocusTrap.d.ts +7 -4
  242. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  243. package/dist/hooks/useFocusTrap.js +60 -95
  244. package/dist/hooks/useHotkeys.d.ts.map +1 -1
  245. package/dist/hooks/useHotkeys.js +3 -18
  246. package/dist/hooks/useMergedRefs.d.ts +18 -0
  247. package/dist/hooks/useMergedRefs.d.ts.map +1 -0
  248. package/dist/hooks/useMergedRefs.js +25 -0
  249. package/dist/index.d.ts +1 -0
  250. package/dist/index.d.ts.map +1 -1
  251. package/dist/index.js +1 -0
  252. package/dist/utils/groupItems.d.ts +5 -1
  253. package/dist/utils/groupItems.d.ts.map +1 -1
  254. package/dist/utils/groupItems.js +9 -9
  255. package/dist/utils/index.d.ts +1 -1
  256. package/dist/utils/index.d.ts.map +1 -1
  257. package/dist/utils/isApplePlatform.d.ts +18 -0
  258. package/dist/utils/isApplePlatform.d.ts.map +1 -0
  259. package/dist/utils/isApplePlatform.js +37 -0
  260. package/dist/utils/sharedResizeObserver.d.ts.map +1 -1
  261. package/dist/utils/sharedResizeObserver.js +10 -1
  262. package/dist/utils/timeParser.d.ts +1 -1
  263. package/dist/utils/timeParser.d.ts.map +1 -1
  264. package/dist/utils/timeParser.js +26 -12
  265. package/locales/en.json +31 -7
  266. package/locales/pseudo.json +18 -0
  267. package/package.json +8 -3
  268. package/src/AppShell/AppShell.tsx +3 -2
  269. package/src/AspectRatio/AspectRatio.doc.mjs +6 -3
  270. package/src/AspectRatio/AspectRatio.test.tsx +57 -10
  271. package/src/AspectRatio/AspectRatio.tsx +40 -1
  272. package/src/Avatar/Avatar.doc.mjs +2 -1
  273. package/src/Avatar/Avatar.test.tsx +17 -0
  274. package/src/Avatar/Avatar.tsx +61 -38
  275. package/src/AvatarGroup/AvatarGroup.tsx +3 -2
  276. package/src/Banner/Banner.doc.mjs +13 -11
  277. package/src/Banner/Banner.test.tsx +102 -15
  278. package/src/Banner/Banner.tsx +72 -25
  279. package/src/BottomSheet/BottomSheet.test.tsx +123 -0
  280. package/src/BottomSheet/BottomSheet.tsx +17 -4
  281. package/src/BottomSheet/BottomSheetSwitcher.tsx +3 -2
  282. package/src/Breadcrumbs/BreadcrumbItem.tsx +9 -5
  283. package/src/Button/Button.doc.mjs +22 -0
  284. package/src/Button/Button.test.tsx +13 -0
  285. package/src/Button/Button.tsx +5 -4
  286. package/src/ButtonGroup/ButtonGroup.doc.mjs +10 -1
  287. package/src/ButtonGroup/ButtonGroup.test.tsx +391 -1
  288. package/src/ButtonGroup/ButtonGroup.tsx +23 -5
  289. package/src/Calendar/Calendar.doc.mjs +4 -3
  290. package/src/Calendar/Calendar.test.tsx +52 -0
  291. package/src/Calendar/Calendar.tsx +18 -15
  292. package/src/Calendar/getInitialFocusDate.test.ts +94 -0
  293. package/src/Calendar/getInitialFocusDate.ts +93 -0
  294. package/src/Carousel/Carousel.tsx +3 -2
  295. package/src/Chat/ChatComposerInput.tsx +5 -1
  296. package/src/Chat/ChatLayout.tsx +3 -2
  297. package/src/Chat/ChatMessageList.doc.mjs +9 -1
  298. package/src/Chat/ChatMessageList.test.tsx +46 -0
  299. package/src/Chat/ChatMessageList.tsx +28 -4
  300. package/src/Chat/ChatTokenizedText.tsx +17 -10
  301. package/src/Chat/useChatComposerTokens.ts +1 -1
  302. package/src/CheckboxInput/CheckboxInput.tsx +3 -2
  303. package/src/ClickableCard/ClickableCard.tsx +3 -2
  304. package/src/CodeBlock/CodeBlock.doc.mjs +6 -0
  305. package/src/CommandPalette/CommandPaletteInput.tsx +3 -2
  306. package/src/CommandPalette/CommandPaletteItem.tsx +3 -2
  307. package/src/ContextMenu/ContextMenu.doc.mjs +5 -0
  308. package/src/ContextMenu/ContextMenu.tsx +4 -3
  309. package/src/DateInput/DateInput.tsx +3 -2
  310. package/src/DateInput/DateInputTouch.test.tsx +36 -0
  311. package/src/DateInput/TouchDateField.tsx +38 -4
  312. package/src/DateTimeInput/DateTimeInput.doc.mjs +18 -0
  313. package/src/DateTimeInput/DateTimeInput.test.tsx +1088 -0
  314. package/src/DateTimeInput/DateTimeInput.tsx +556 -24
  315. package/src/DateTimeInput/index.ts +1 -0
  316. package/src/Dialog/Dialog.doc.mjs +8 -3
  317. package/src/Dialog/Dialog.test.tsx +261 -1
  318. package/src/Dialog/Dialog.tsx +109 -58
  319. package/src/DropdownMenu/DropdownMenu.doc.mjs +6 -0
  320. package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +8 -5
  321. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +8 -11
  322. package/src/Field/Field.doc.mjs +13 -2
  323. package/src/FileInput/FileInput.tsx +3 -2
  324. package/src/Heading/Heading.test.tsx +23 -1
  325. package/src/Heading/Heading.tsx +6 -2
  326. package/src/HoverCard/HoverCard.doc.mjs +5 -0
  327. package/src/HoverCard/HoverCard.tsx +6 -0
  328. package/src/HoverCard/useHoverCard.tsx +81 -30
  329. package/src/Item/Item.tsx +5 -4
  330. package/src/Kbd/Kbd.tsx +2 -19
  331. package/src/Layer/LayerDepthContext.tsx +62 -0
  332. package/src/Layer/index.ts +9 -0
  333. package/src/Layer/layerDismissalFamilies.test.tsx +359 -0
  334. package/src/Layer/layerDismissalInvariants.test.tsx +417 -0
  335. package/src/Layer/layerStack.ts +362 -0
  336. package/src/Layer/useLayerDismissal.test.tsx +469 -0
  337. package/src/Layer/useLayerDismissal.ts +173 -0
  338. package/src/Layout/Layout.doc.mjs +2 -2
  339. package/src/Lightbox/Lightbox.doc.mjs +12 -0
  340. package/src/Lightbox/Lightbox.tsx +124 -102
  341. package/src/Link/Link.doc.mjs +39 -0
  342. package/src/Markdown/Markdown.doc.mjs +5 -0
  343. package/src/Markdown/incremental.test.ts +16 -2
  344. package/src/Markdown/index.ts +1 -0
  345. package/src/Markdown/parser.perf.test.ts +96 -1
  346. package/src/Markdown/parser.test.ts +145 -2
  347. package/src/Markdown/parser.ts +244 -29
  348. package/src/MobileNav/MobileNav.tsx +62 -36
  349. package/src/MoreMenu/MoreMenu.doc.mjs +7 -1
  350. package/src/MultiSelector/MultiSelector.doc.mjs +27 -0
  351. package/src/MultiSelector/MultiSelector.test.tsx +106 -0
  352. package/src/MultiSelector/MultiSelector.tsx +50 -6
  353. package/src/MultiSelector/index.ts +1 -0
  354. package/src/NavMenu/NavHeadingMenu.tsx +3 -2
  355. package/src/NumberInput/NumberInput.doc.mjs +5 -0
  356. package/src/Outline/Outline.tsx +3 -2
  357. package/src/OverflowList/OverflowList.tsx +3 -2
  358. package/src/Overlay/Overlay.tsx +3 -2
  359. package/src/Popover/usePopover.tsx +31 -28
  360. package/src/PowerSearch/PowerSearch.doc.mjs +160 -28
  361. package/src/PowerSearch/PowerSearch.test.tsx +240 -1
  362. package/src/PowerSearch/PowerSearch.tsx +34 -5
  363. package/src/PowerSearch/PowerSearchEditPopover.test.tsx +201 -0
  364. package/src/PowerSearch/PowerSearchEditPopover.tsx +29 -2
  365. package/src/PowerSearch/PowerSearchValueEditor.test.tsx +47 -1
  366. package/src/PowerSearch/PowerSearchValueEditor.tsx +15 -0
  367. package/src/PowerSearch/types.ts +2 -0
  368. package/src/PowerSearch/usePowerSearchSource.test.ts +82 -2
  369. package/src/PowerSearch/usePowerSearchSource.ts +13 -3
  370. package/src/Resizable/ResizeHandle.tsx +3 -2
  371. package/src/SegmentedControl/SegmentedControl.tsx +3 -2
  372. package/src/SelectableCard/SelectableCard.tsx +3 -2
  373. package/src/Selector/Selector.doc.mjs +12 -1
  374. package/src/Selector/Selector.test.tsx +45 -1
  375. package/src/Selector/Selector.tsx +11 -1
  376. package/src/SideNav/SideNav.doc.mjs +6 -6
  377. package/src/SideNav/SideNav.tsx +4 -2
  378. package/src/SideNav/SideNavHeading.tsx +10 -10
  379. package/src/SideNav/SideNavItem.tsx +4 -2
  380. package/src/Slider/Slider.tsx +3 -2
  381. package/src/Spinner/Spinner.test.tsx +147 -1
  382. package/src/Spinner/Spinner.tsx +120 -101
  383. package/src/Stepper/Step.doc.mjs +98 -0
  384. package/src/Stepper/Step.tsx +1638 -0
  385. package/src/Stepper/StepStatus.ts +26 -0
  386. package/src/Stepper/Stepper.doc.mjs +370 -0
  387. package/src/Stepper/Stepper.test.tsx +1024 -0
  388. package/src/Stepper/Stepper.tsx +258 -0
  389. package/src/Stepper/StepperContext.ts +71 -0
  390. package/src/Stepper/index.ts +18 -0
  391. package/src/Stepper/stepper.stylex.ts +19 -0
  392. package/src/Switch/Switch.tsx +3 -2
  393. package/src/TabList/Tab.doc.mjs +13 -1
  394. package/src/TabList/Tab.tsx +67 -7
  395. package/src/TabList/TabList.doc.mjs +22 -6
  396. package/src/TabList/TabList.test.tsx +559 -0
  397. package/src/TabList/TabList.tsx +490 -22
  398. package/src/TabList/TabListContext.ts +13 -2
  399. package/src/TabList/TabMenu.tsx +3 -2
  400. package/src/TabList/index.ts +6 -2
  401. package/src/Table/BaseTable.tsx +6 -3
  402. package/src/Table/Table.doc.mjs +6 -1
  403. package/src/Table/Table.test.tsx +132 -14
  404. package/src/Table/TableBody.doc.mjs +40 -0
  405. package/src/Table/TableFooter.doc.mjs +40 -0
  406. package/src/Table/TableHeader.doc.mjs +40 -0
  407. package/src/Table/TableRow.tsx +16 -5
  408. package/src/Table/plugins/groupedRows/useTableGroupedRows-perf.test.tsx +112 -0
  409. package/src/Table/plugins/groupedRows/useTableGroupedRows.test.tsx +100 -0
  410. package/src/Table/plugins/groupedRows/useTableGroupedRows.tsx +17 -8
  411. package/src/Table/plugins/pagination/useTablePagination.test.tsx +34 -4
  412. package/src/Table/plugins/pagination/useTablePagination.tsx +35 -3
  413. package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +9 -2
  414. package/src/Table/plugins/rowStatus/useTableRowStatus.test.tsx +13 -7
  415. package/src/Table/plugins/rowStatus/useTableRowStatus.tsx +12 -3
  416. package/src/Table/plugins/selection/useTableSelection.test.tsx +76 -0
  417. package/src/Table/plugins/selection/useTableSelection.tsx +40 -7
  418. package/src/Table/types.ts +22 -4
  419. package/src/Table/useBaseTablePlugins.ts +5 -0
  420. package/src/Table/useTableGroupedRows.doc.mjs +5 -4
  421. package/src/Table/useTablePagination.doc.mjs +7 -0
  422. package/src/Table/useTableRowStatus.doc.mjs +1 -1
  423. package/src/Table/useTableSelection.doc.mjs +31 -0
  424. package/src/Text/Text.test.tsx +23 -1
  425. package/src/Text/Text.tsx +6 -2
  426. package/src/TextArea/TextArea.tsx +3 -2
  427. package/src/TextInput/TextInput.doc.mjs +10 -0
  428. package/src/TextInput/TextInput.tsx +3 -2
  429. package/src/Thumbnail/Thumbnail.doc.mjs +1 -1
  430. package/src/TimeInput/TimeInput.tsx +2 -2
  431. package/src/Timestamp/Timestamp.tsx +4 -2
  432. package/src/Toast/Toast.doc.mjs +6 -0
  433. package/src/Token/TokenLink.tsx +2 -2
  434. package/src/Tokenizer/Tokenizer.doc.mjs +27 -0
  435. package/src/Tokenizer/Tokenizer.tsx +4 -0
  436. package/src/Toolbar/Toolbar.doc.mjs +5 -0
  437. package/src/Tooltip/Tooltip.doc.mjs +5 -0
  438. package/src/Tooltip/Tooltip.test.tsx +32 -0
  439. package/src/Tooltip/Tooltip.tsx +5 -0
  440. package/src/Tooltip/useTooltip.tsx +47 -24
  441. package/src/TopNav/TopNavHeading.tsx +3 -2
  442. package/src/TopNav/TopNavMegaMenu.tsx +3 -2
  443. package/src/TopNav/TopNavMenu.tsx +4 -3
  444. package/src/Typeahead/BaseTypeahead.doc.mjs +11 -0
  445. package/src/Typeahead/BaseTypeahead.tsx +85 -33
  446. package/src/Typeahead/Typeahead.doc.mjs +5 -0
  447. package/src/Typeahead/Typeahead.test.tsx +49 -0
  448. package/src/Typeahead/Typeahead.tsx +3 -2
  449. package/src/hooks/index.ts +2 -0
  450. package/src/hooks/useContainerReveal.doc.mjs +2 -2
  451. package/src/hooks/useFocusTrap.test.tsx +14 -0
  452. package/src/hooks/useFocusTrap.ts +62 -111
  453. package/src/hooks/useFocusTrapEscapeShim.test.tsx +165 -0
  454. package/src/hooks/useHotkeys.ts +3 -18
  455. package/src/hooks/useMergedRefs.doc.mjs +79 -0
  456. package/src/hooks/useMergedRefs.test.tsx +62 -0
  457. package/src/hooks/useMergedRefs.ts +36 -0
  458. package/src/index.ts +1 -0
  459. package/src/theme/MediaTheme.doc.mjs +5 -5
  460. package/src/utils/groupItems.ts +12 -6
  461. package/src/utils/index.ts +1 -1
  462. package/src/utils/isApplePlatform.test.ts +61 -0
  463. package/src/utils/isApplePlatform.ts +37 -0
  464. package/src/utils/sharedResizeObserver.ts +10 -2
  465. package/src/utils/timeParser.test.ts +6 -1
  466. package/src/utils/timeParser.ts +35 -13
  467. package/src/__tests__/inputWidthContract.test.tsx +0 -69
@@ -13,6 +13,7 @@ import {describe, it, expect, vi, beforeAll, afterAll} from 'vitest';
13
13
  import {render, screen, fireEvent} from '@testing-library/react';
14
14
  import userEvent from '@testing-library/user-event';
15
15
  import {TabList} from './TabList';
16
+ import type {TabListProps} from './TabList';
16
17
  import {Tab} from './Tab';
17
18
  import {TabMenu} from './TabMenu';
18
19
  import {LinkProvider} from '../Link/LinkProvider';
@@ -960,3 +961,561 @@ describe('TabMenu keyboard navigation (roving tabindex)', () => {
960
961
  expect(hidePopover).toHaveBeenCalled();
961
962
  });
962
963
  });
964
+
965
+ describe('TabList overflow (scroll)', () => {
966
+ const STRIP = '.astryx-tab-strip';
967
+ const ARROW = '.astryx-tab-scroll-button';
968
+
969
+ /** jsdom has no layout, so the scroll box is described by hand. */
970
+ function fakeScrollBox(
971
+ strip: HTMLElement,
972
+ {
973
+ scrollWidth,
974
+ clientWidth,
975
+ scrollLeft = 0,
976
+ }: {scrollWidth: number; clientWidth: number; scrollLeft?: number},
977
+ ) {
978
+ Object.defineProperty(strip, 'scrollWidth', {
979
+ value: scrollWidth,
980
+ configurable: true,
981
+ });
982
+ Object.defineProperty(strip, 'clientWidth', {
983
+ value: clientWidth,
984
+ configurable: true,
985
+ });
986
+ Object.defineProperty(strip, 'scrollLeft', {
987
+ value: scrollLeft,
988
+ writable: true,
989
+ configurable: true,
990
+ });
991
+ }
992
+
993
+ /**
994
+ * Places the strip at 0..width and a tab at the given offsets, so the
995
+ * keep-selected-visible arithmetic has real numbers to work with.
996
+ */
997
+ function fakeGeometry(
998
+ strip: HTMLElement,
999
+ tabValue: string,
1000
+ {
1001
+ stripWidth,
1002
+ tabLeft,
1003
+ tabRight,
1004
+ }: {stripWidth: number; tabLeft: number; tabRight: number},
1005
+ ) {
1006
+ strip.getBoundingClientRect = () =>
1007
+ ({left: 0, right: stripWidth, width: stripWidth}) as DOMRect;
1008
+ const tab = strip.querySelector<HTMLElement>(
1009
+ `[data-tab-value="${tabValue}"]`,
1010
+ );
1011
+ if (tab) {
1012
+ tab.getBoundingClientRect = () =>
1013
+ ({
1014
+ left: tabLeft,
1015
+ right: tabRight,
1016
+ width: tabRight - tabLeft,
1017
+ }) as DOMRect;
1018
+ }
1019
+ }
1020
+
1021
+ function renderStrip(props: Partial<TabListProps> = {}) {
1022
+ const utils = render(
1023
+ <TabList value="a" onChange={() => {}} {...props}>
1024
+ <Tab value="a" label="Alpha" />
1025
+ <Tab value="b" label="Beta" />
1026
+ <Tab value="c" label="Gamma" />
1027
+ </TabList>,
1028
+ );
1029
+ const strip = utils.container.querySelector<HTMLElement>(STRIP);
1030
+ if (!strip) {
1031
+ throw new Error('no tab strip');
1032
+ }
1033
+ return {...utils, strip};
1034
+ }
1035
+
1036
+ it('renders the tabs inside a scroll strip by default', () => {
1037
+ const {strip} = renderStrip();
1038
+ expect(strip.querySelectorAll('[data-tab-value]')).toHaveLength(3);
1039
+ });
1040
+
1041
+ it('offers an end arrow while there are tabs past the end, and pressing it scrolls', () => {
1042
+ const {container, strip} = renderStrip();
1043
+ fakeScrollBox(strip, {scrollWidth: 600, clientWidth: 300});
1044
+ const scrollBy = vi.fn();
1045
+ strip.scrollBy = scrollBy;
1046
+ fireEvent.scroll(strip);
1047
+
1048
+ const arrows = container.querySelectorAll(ARROW);
1049
+ expect(arrows).toHaveLength(1);
1050
+
1051
+ fireEvent.click(arrows[0]);
1052
+ expect(scrollBy).toHaveBeenCalledWith({left: 240});
1053
+ });
1054
+
1055
+ it('offers both arrows once the strip is scrolled away from the start', () => {
1056
+ const {container, strip} = renderStrip();
1057
+ fakeScrollBox(strip, {scrollWidth: 600, clientWidth: 300, scrollLeft: 150});
1058
+ strip.scrollBy = vi.fn();
1059
+ fireEvent.scroll(strip);
1060
+
1061
+ expect(container.querySelectorAll(ARROW)).toHaveLength(2);
1062
+ });
1063
+
1064
+ it('keeps the arrows out of the tab order and out of the accessibility tree', () => {
1065
+ const {container, strip} = renderStrip();
1066
+ fakeScrollBox(strip, {scrollWidth: 600, clientWidth: 300});
1067
+ strip.scrollBy = vi.fn();
1068
+ fireEvent.scroll(strip);
1069
+
1070
+ const arrow = container.querySelector<HTMLElement>(ARROW);
1071
+ expect(arrow?.getAttribute('aria-hidden')).toBe('true');
1072
+ expect(arrow?.tabIndex).toBe(-1);
1073
+ });
1074
+
1075
+ it('scrolls a selected tab that starts out of view back into view on mount', () => {
1076
+ const scrollBy = vi.fn();
1077
+ // The strip mounts before the effect runs, so the geometry has to be in
1078
+ // place on the prototype rather than on an element we can reach first.
1079
+ const originalScrollBy = Element.prototype.scrollBy;
1080
+ const originalRect = Element.prototype.getBoundingClientRect;
1081
+ Element.prototype.scrollBy = scrollBy;
1082
+ Element.prototype.getBoundingClientRect = function (this: Element) {
1083
+ return (
1084
+ this.classList.contains('astryx-tab-strip')
1085
+ ? {left: 0, right: 300, width: 300}
1086
+ : {left: 420, right: 500, width: 80}
1087
+ ) as DOMRect;
1088
+ };
1089
+
1090
+ render(
1091
+ <TabList value="c" onChange={() => {}}>
1092
+ <Tab value="a" label="Alpha" />
1093
+ <Tab value="b" label="Beta" />
1094
+ <Tab value="c" label="Gamma" />
1095
+ </TabList>,
1096
+ );
1097
+
1098
+ expect(scrollBy).toHaveBeenCalledWith(
1099
+ expect.objectContaining({behavior: 'instant'}),
1100
+ );
1101
+ expect(scrollBy.mock.calls[0][0].left).toBeGreaterThan(0);
1102
+ Element.prototype.scrollBy = originalScrollBy;
1103
+ Element.prototype.getBoundingClientRect = originalRect;
1104
+ });
1105
+
1106
+ it('keeps the selected tab in view when the host changes value without focus', () => {
1107
+ const {strip, rerender} = renderStrip({value: 'a'});
1108
+ const scrollBy = vi.fn();
1109
+ strip.scrollBy = scrollBy;
1110
+ fakeGeometry(strip, 'c', {stripWidth: 300, tabLeft: 420, tabRight: 500});
1111
+
1112
+ rerender(
1113
+ <TabList value="c" onChange={() => {}}>
1114
+ <Tab value="a" label="Alpha" />
1115
+ <Tab value="b" label="Beta" />
1116
+ <Tab value="c" label="Gamma" />
1117
+ </TabList>,
1118
+ );
1119
+
1120
+ expect(scrollBy).toHaveBeenCalledTimes(1);
1121
+ expect(scrollBy.mock.calls[0][0].left).toBeGreaterThan(0);
1122
+ });
1123
+
1124
+ it('leaves a tab that is already in view alone', () => {
1125
+ const {strip, rerender} = renderStrip({value: 'a'});
1126
+ const scrollBy = vi.fn();
1127
+ strip.scrollBy = scrollBy;
1128
+ fakeGeometry(strip, 'b', {stripWidth: 300, tabLeft: 80, tabRight: 140});
1129
+
1130
+ rerender(
1131
+ <TabList value="b" onChange={() => {}}>
1132
+ <Tab value="a" label="Alpha" />
1133
+ <Tab value="b" label="Beta" />
1134
+ <Tab value="c" label="Gamma" />
1135
+ </TabList>,
1136
+ );
1137
+
1138
+ expect(scrollBy).not.toHaveBeenCalled();
1139
+ });
1140
+
1141
+ it('reveals on a selection change, not on an unrelated overflow change', () => {
1142
+ const {strip, rerender} = renderStrip({value: 'c', overflow: 'visible'});
1143
+ const scrollBy = vi.fn();
1144
+ strip.scrollBy = scrollBy;
1145
+ fakeGeometry(strip, 'c', {stripWidth: 300, tabLeft: 420, tabRight: 500});
1146
+
1147
+ // Turning scrolling on re-measures the strip, and that one reveal is the
1148
+ // resize path's. The selection has not moved, so nothing may add a second.
1149
+ rerender(
1150
+ <TabList value="c" onChange={() => {}} overflow="scroll">
1151
+ <Tab value="a" label="Alpha" />
1152
+ <Tab value="b" label="Beta" />
1153
+ <Tab value="c" label="Gamma" />
1154
+ </TabList>,
1155
+ );
1156
+
1157
+ expect(scrollBy).toHaveBeenCalledTimes(1);
1158
+
1159
+ fakeGeometry(strip, 'a', {stripWidth: 300, tabLeft: -200, tabRight: -120});
1160
+ rerender(
1161
+ <TabList value="a" onChange={() => {}} overflow="scroll">
1162
+ <Tab value="a" label="Alpha" />
1163
+ <Tab value="b" label="Beta" />
1164
+ <Tab value="c" label="Gamma" />
1165
+ </TabList>,
1166
+ );
1167
+
1168
+ expect(scrollBy).toHaveBeenCalledTimes(2);
1169
+ expect(scrollBy.mock.calls[1][0].left).toBeLessThan(0);
1170
+ });
1171
+
1172
+ it('finishes the job on focus: a tab only half in view is scrolled clear', () => {
1173
+ const {strip} = renderStrip({value: 'a'});
1174
+ const scrollBy = vi.fn();
1175
+ strip.scrollBy = scrollBy;
1176
+ // The browser scrolls a focused tab into view only when it is entirely
1177
+ // outside the box, so this one — straddling the end edge — stays cut off.
1178
+ fakeGeometry(strip, 'c', {stripWidth: 300, tabLeft: 260, tabRight: 340});
1179
+
1180
+ const tab = strip.querySelector<HTMLElement>('[data-tab-value="c"]');
1181
+ fireEvent.focus(tab!, {bubbles: true});
1182
+
1183
+ expect(scrollBy).toHaveBeenCalledTimes(1);
1184
+ expect(scrollBy.mock.calls[0][0].left).toBeCloseTo(40);
1185
+ });
1186
+
1187
+ it('reveals a half-visible overflow menu trigger too, not just tabs', () => {
1188
+ const {container} = render(
1189
+ <TabList value="a" onChange={() => {}}>
1190
+ <Tab value="a" label="Alpha" />
1191
+ <Tab value="b" label="Beta" />
1192
+ <TabMenu label="More" options={[{value: 'c', label: 'Gamma'}]} />
1193
+ </TabList>,
1194
+ );
1195
+ const strip = container.querySelector<HTMLElement>(STRIP);
1196
+ if (!strip) {
1197
+ throw new Error('no tab strip');
1198
+ }
1199
+ const scrollBy = vi.fn();
1200
+ strip.scrollBy = scrollBy;
1201
+ strip.getBoundingClientRect = () =>
1202
+ ({left: 0, right: 300, width: 300}) as DOMRect;
1203
+ const trigger = strip.querySelector<HTMLElement>('[data-tab-menu]');
1204
+ if (!trigger) {
1205
+ throw new Error('no menu trigger');
1206
+ }
1207
+ trigger.getBoundingClientRect = () =>
1208
+ ({left: 260, right: 340, width: 80}) as DOMRect;
1209
+
1210
+ fireEvent.focus(trigger, {bubbles: true});
1211
+
1212
+ expect(scrollBy).toHaveBeenCalledTimes(1);
1213
+ expect(scrollBy.mock.calls[0][0].left).toBeCloseTo(40);
1214
+ });
1215
+
1216
+ it('fades out at the strip’s own edge, not at the bleed edge', () => {
1217
+ // The scroll box is a ring-bleed wider than the TabList on each side while
1218
+ // it holds focus, so a fade that ran to the box edge would paint tabs
1219
+ // outside the component and past the arrow that caps that edge.
1220
+ const bleed = 'var(--_tab-strip-bleed)';
1221
+ const {strip} = renderStrip();
1222
+ strip.scrollBy = vi.fn();
1223
+
1224
+ fakeScrollBox(strip, {scrollWidth: 600, clientWidth: 300});
1225
+ fireEvent.scroll(strip);
1226
+ expect(getComputedStyle(strip).maskImage).toContain(`transparent ${bleed}`);
1227
+
1228
+ fakeScrollBox(strip, {scrollWidth: 600, clientWidth: 300, scrollLeft: 150});
1229
+ fireEvent.scroll(strip);
1230
+ expect(getComputedStyle(strip).maskImage).toContain(
1231
+ `transparent calc(100% - ${bleed})`,
1232
+ );
1233
+ });
1234
+
1235
+ it('is no wider than the TabList until the strip holds focus', () => {
1236
+ // The bleed is real geometry: while the strip carries it, its border box
1237
+ // sticks out of the TabList's and every ancestor that scrolls counts it.
1238
+ // jsdom has no layout, so the box is read off the declarations: padding
1239
+ // and its cancelling margin are both the bleed, and the bleed is zero
1240
+ // until a ring needs keeping whole. `:has(:focus-visible)` flips it --
1241
+ // covered in a browser, since jsdom does not recompute style on a focus
1242
+ // change.
1243
+ const {strip} = renderStrip();
1244
+ const cs = getComputedStyle(strip);
1245
+
1246
+ expect(cs.getPropertyValue('--_tab-strip-bleed')).toBe('0px');
1247
+ expect(cs.paddingInline).toBe('var(--_tab-strip-bleed)');
1248
+ expect(cs.marginInline).toBe('calc(-1 * (var(--_tab-strip-bleed)))');
1249
+ });
1250
+
1251
+ it('does not hand focus to an arrow, which is hidden from assistive tech', () => {
1252
+ const {container, strip} = renderStrip();
1253
+ fakeScrollBox(strip, {scrollWidth: 600, clientWidth: 300});
1254
+ strip.scrollBy = vi.fn();
1255
+ fireEvent.scroll(strip);
1256
+
1257
+ const arrow = container.querySelector<HTMLElement>(ARROW);
1258
+ const prevented = !fireEvent.mouseDown(arrow!);
1259
+ expect(prevented).toBe(true);
1260
+ });
1261
+
1262
+ it('overflow="visible" scrolls nothing and offers no arrows', () => {
1263
+ const {container, strip, rerender} = renderStrip({
1264
+ value: 'a',
1265
+ overflow: 'visible',
1266
+ });
1267
+ const scrollBy = vi.fn();
1268
+ strip.scrollBy = scrollBy;
1269
+ fakeScrollBox(strip, {scrollWidth: 600, clientWidth: 300});
1270
+ fakeGeometry(strip, 'c', {stripWidth: 300, tabLeft: 420, tabRight: 500});
1271
+ fireEvent.scroll(strip);
1272
+
1273
+ rerender(
1274
+ <TabList value="c" onChange={() => {}} overflow="visible">
1275
+ <Tab value="a" label="Alpha" />
1276
+ <Tab value="b" label="Beta" />
1277
+ <Tab value="c" label="Gamma" />
1278
+ </TabList>,
1279
+ );
1280
+
1281
+ expect(container.querySelectorAll(ARROW)).toHaveLength(0);
1282
+ expect(scrollBy).not.toHaveBeenCalled();
1283
+ });
1284
+
1285
+ it('shows the reading start of a stop too wide to fit', () => {
1286
+ const {strip} = renderStrip({value: 'a'});
1287
+ const scrollBy = vi.fn();
1288
+ strip.scrollBy = scrollBy;
1289
+ // Wider than the strip, so one end stays hidden either way. Aligning the
1290
+ // end would land a long label on its last words.
1291
+ fakeGeometry(strip, 'c', {stripWidth: 300, tabLeft: 420, tabRight: 800});
1292
+
1293
+ const tab = strip.querySelector<HTMLElement>('[data-tab-value="c"]');
1294
+ fireEvent.focus(tab!, {bubbles: true});
1295
+
1296
+ expect(scrollBy.mock.calls[0][0].left).toBeCloseTo(420);
1297
+ });
1298
+
1299
+ it('shows the reading start of an over-wide stop in RTL too', () => {
1300
+ const {strip} = renderStrip({value: 'a'});
1301
+ const scrollBy = vi.fn();
1302
+ strip.scrollBy = scrollBy;
1303
+ strip.style.direction = 'rtl';
1304
+ fakeGeometry(strip, 'c', {stripWidth: 300, tabLeft: -500, tabRight: -120});
1305
+
1306
+ const tab = strip.querySelector<HTMLElement>('[data-tab-value="c"]');
1307
+ fireEvent.focus(tab!, {bubbles: true});
1308
+
1309
+ // RTL reads from the right edge, so that is the end to bring flush.
1310
+ expect(scrollBy.mock.calls[0][0].left).toBeCloseTo(-420);
1311
+ });
1312
+ });
1313
+
1314
+ describe('TabList ARIA pattern — role="tablist"', () => {
1315
+ function warnSpy() {
1316
+ return vi.spyOn(console, 'warn').mockImplementation(() => {});
1317
+ }
1318
+
1319
+ it('speaks the tabs pattern: a labelled tablist of tabs with aria-selected', () => {
1320
+ render(
1321
+ <TabList value="b" onChange={() => {}} role="tablist" aria-label="Views">
1322
+ <Tab value="a" label="Alpha" panelId="panel-a" />
1323
+ <Tab value="b" label="Beta" panelId="panel-b" />
1324
+ </TabList>,
1325
+ );
1326
+
1327
+ const tablist = screen.getByRole('tablist', {name: 'Views'});
1328
+ const tabs = screen.getAllByRole('tab');
1329
+ expect(tabs).toHaveLength(2);
1330
+ expect(tabs.every(tab => tablist.contains(tab))).toBe(true);
1331
+ expect(tabs[0]).toHaveAttribute('aria-selected', 'false');
1332
+ expect(tabs[1]).toHaveAttribute('aria-selected', 'true');
1333
+ expect(tabs[1]).toHaveAttribute('aria-controls', 'panel-b');
1334
+ expect(tabs[1]).not.toHaveAttribute('aria-current');
1335
+ });
1336
+
1337
+ it('lets the consumer name the tablist from another element', () => {
1338
+ render(
1339
+ <>
1340
+ <h2 id="views-heading">Project views</h2>
1341
+ <TabList
1342
+ value="a"
1343
+ onChange={() => {}}
1344
+ role="tablist"
1345
+ aria-labelledby="views-heading">
1346
+ <Tab value="a" label="Alpha" panelId="panel-a" />
1347
+ </TabList>
1348
+ </>,
1349
+ );
1350
+
1351
+ expect(screen.getByRole('tablist', {name: 'Project views'})).not.toBeNull();
1352
+ });
1353
+
1354
+ it('renders no navigation landmark around the tabs', () => {
1355
+ const {container} = render(
1356
+ <TabList value="a" onChange={() => {}} role="tablist">
1357
+ <Tab value="a" label="Alpha" panelId="panel-a" />
1358
+ </TabList>,
1359
+ );
1360
+
1361
+ expect(container.querySelector('nav')).toBeNull();
1362
+ expect(screen.queryByRole('navigation')).toBeNull();
1363
+ });
1364
+
1365
+ it('omits aria-controls when a tab has no panel, and says so', () => {
1366
+ const warn = warnSpy();
1367
+ render(
1368
+ <TabList value="a" onChange={() => {}} role="tablist">
1369
+ <Tab value="a" label="Alpha" />
1370
+ </TabList>,
1371
+ );
1372
+
1373
+ expect(screen.getByRole('tab')).not.toHaveAttribute('aria-controls');
1374
+ expect(warn).toHaveBeenCalledWith(
1375
+ expect.stringContaining(
1376
+ 'Tab: a tab in a role="tablist" TabList controls nothing',
1377
+ ),
1378
+ );
1379
+ warn.mockRestore();
1380
+ });
1381
+
1382
+ it('leaves a hand-wired aria-controls alone, and does not ask for a panelId it already has', () => {
1383
+ const warn = warnSpy();
1384
+ render(
1385
+ <TabList value="a" onChange={() => {}} role="tablist">
1386
+ <Tab value="a" label="Alpha" aria-controls="panel-written-by-hand" />
1387
+ </TabList>,
1388
+ );
1389
+
1390
+ expect(screen.getByRole('tab')).toHaveAttribute(
1391
+ 'aria-controls',
1392
+ 'panel-written-by-hand',
1393
+ );
1394
+ expect(warn).not.toHaveBeenCalled();
1395
+ warn.mockRestore();
1396
+ });
1397
+
1398
+ it('ignores href: the tab is a button, and the caller is told', () => {
1399
+ const warn = warnSpy();
1400
+ render(
1401
+ <TabList value="a" onChange={() => {}} role="tablist">
1402
+ <Tab value="a" label="Alpha" panelId="panel-a" href="/alpha" />
1403
+ </TabList>,
1404
+ );
1405
+
1406
+ const tab = screen.getByRole('tab');
1407
+ expect(tab.tagName).toBe('BUTTON');
1408
+ expect(tab).not.toHaveAttribute('href');
1409
+ expect(warn).toHaveBeenCalledWith(
1410
+ expect.stringContaining('Tab: href is ignored in a role="tablist"'),
1411
+ );
1412
+ warn.mockRestore();
1413
+ });
1414
+
1415
+ it('warns about anything in the strip that is not a tab, however it got there', () => {
1416
+ const warn = warnSpy();
1417
+ const showMenu = true;
1418
+ render(
1419
+ <TabList value="a" onChange={() => {}} role="tablist">
1420
+ <Tab value="a" label="Alpha" panelId="panel-a" />
1421
+ {showMenu ? (
1422
+ <div>
1423
+ <TabMenu label="More" options={[{value: 'b', label: 'Beta'}]} />
1424
+ </div>
1425
+ ) : null}
1426
+ </TabList>,
1427
+ );
1428
+
1429
+ expect(warn).toHaveBeenCalledWith(
1430
+ expect.stringContaining('TabList: role="tablist" owns only tabs'),
1431
+ );
1432
+ warn.mockRestore();
1433
+ });
1434
+
1435
+ it('says nothing when the strip holds only tabs with panels', () => {
1436
+ const warn = warnSpy();
1437
+ render(
1438
+ <TabList value="a" onChange={() => {}} role="tablist">
1439
+ <Tab value="a" label="Alpha" panelId="panel-a" />
1440
+ <Tab value="b" label="Beta" panelId="panel-b" />
1441
+ </TabList>,
1442
+ );
1443
+
1444
+ expect(warn).not.toHaveBeenCalled();
1445
+ warn.mockRestore();
1446
+ });
1447
+
1448
+ it('leaves ArrowDown and ArrowUp to the page', async () => {
1449
+ // A tablist reports itself as horizontal, so the vertical arrows are the
1450
+ // page's to scroll with.
1451
+ const user = userEvent.setup();
1452
+ render(
1453
+ <TabList value="a" onChange={() => {}} role="tablist">
1454
+ <Tab value="a" label="Alpha" panelId="panel-a" />
1455
+ <Tab value="b" label="Beta" panelId="panel-b" />
1456
+ </TabList>,
1457
+ );
1458
+
1459
+ const alpha = screen.getByRole('tab', {name: 'Alpha'});
1460
+ alpha.focus();
1461
+ await user.keyboard('{ArrowDown}');
1462
+ expect(alpha).toHaveFocus();
1463
+ });
1464
+ });
1465
+
1466
+ describe('TabList ARIA pattern — no role', () => {
1467
+ it('is the navigation landmark it has always been', () => {
1468
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
1469
+ render(
1470
+ <TabList value="a" onChange={() => {}} aria-label="Views">
1471
+ <Tab value="a" label="Alpha" />
1472
+ <Tab value="b" label="Beta" />
1473
+ </TabList>,
1474
+ );
1475
+
1476
+ expect(screen.getByRole('navigation', {name: 'Views'})).toBeInTheDocument();
1477
+ expect(screen.queryByRole('tablist')).toBeNull();
1478
+ expect(screen.getByRole('button', {name: 'Alpha'})).toHaveAttribute(
1479
+ 'aria-current',
1480
+ 'true',
1481
+ );
1482
+ expect(warn).not.toHaveBeenCalled();
1483
+ warn.mockRestore();
1484
+ });
1485
+
1486
+ it('drops a panelId and says so: there is no panel relationship to state', () => {
1487
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
1488
+ render(
1489
+ <TabList value="a" onChange={() => {}}>
1490
+ <Tab value="a" label="Alpha" panelId="panel-a" />
1491
+ </TabList>,
1492
+ );
1493
+
1494
+ expect(screen.getByRole('button', {name: 'Alpha'})).not.toHaveAttribute(
1495
+ 'aria-controls',
1496
+ );
1497
+ expect(warn).toHaveBeenCalledWith(
1498
+ expect.stringContaining(
1499
+ 'Tab: panelId does nothing outside a role="tablist" TabList',
1500
+ ),
1501
+ );
1502
+ warn.mockRestore();
1503
+ });
1504
+ });
1505
+
1506
+ describe('TabList ARIA pattern — any other role', () => {
1507
+ it('passes the role through and leaves the tabs on the navigation pattern', () => {
1508
+ const {container} = render(
1509
+ <TabList value="a" onChange={() => {}} role="toolbar">
1510
+ <Tab value="a" label="Alpha" />
1511
+ </TabList>,
1512
+ );
1513
+
1514
+ expect(container.querySelector('nav')).toHaveAttribute('role', 'toolbar');
1515
+ expect(screen.queryByRole('tablist')).toBeNull();
1516
+ expect(screen.getByRole('button', {name: 'Alpha'})).toHaveAttribute(
1517
+ 'aria-current',
1518
+ 'true',
1519
+ );
1520
+ });
1521
+ });