@astryxdesign/core 0.7.5 → 0.7.6-canary.0bd3b37

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 (1575) hide show
  1. package/dist/AlertDialog/AlertDialog.d.ts +1 -1
  2. package/dist/AlertDialog/AlertDialog.d.ts.map +1 -1
  3. package/dist/AlertDialog/AlertDialog.js +5 -5
  4. package/dist/AppShell/AppShell.d.ts.map +1 -1
  5. package/dist/AppShell/AppShell.js +183 -5
  6. package/dist/AppShell/observeMainOverflow.d.ts +9 -0
  7. package/dist/AppShell/observeMainOverflow.d.ts.map +1 -0
  8. package/dist/AppShell/observeMainOverflow.js +125 -0
  9. package/dist/Avatar/Avatar.d.ts.map +1 -1
  10. package/dist/Avatar/Avatar.js +12 -3
  11. package/dist/Badge/Badge.d.ts.map +1 -1
  12. package/dist/Badge/Badge.js +8 -1
  13. package/dist/BottomSheet/BottomSheet.js +1 -1
  14. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
  15. package/dist/BottomSheet/BottomSheetPanel.js +2 -1
  16. package/dist/BottomSheet/useSheetGestures.d.ts +12 -0
  17. package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -1
  18. package/dist/BottomSheet/useSheetGestures.js +30 -3
  19. package/dist/Button/Button.js +4 -4
  20. package/dist/Carousel/Carousel.d.ts +3 -1
  21. package/dist/Carousel/Carousel.d.ts.map +1 -1
  22. package/dist/Carousel/Carousel.js +12 -3
  23. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  24. package/dist/CheckboxInput/CheckboxInput.js +6 -5
  25. package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
  26. package/dist/ClickableCard/ClickableCard.js +37 -4
  27. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  28. package/dist/ComplexSelector/ComplexSelector.js +49 -6
  29. package/dist/DateInput/DateInput.d.ts.map +1 -1
  30. package/dist/DateInput/DateInput.js +12 -4
  31. package/dist/DateInput/MonthScroller.d.ts.map +1 -1
  32. package/dist/DateInput/MonthScroller.js +108 -23
  33. package/dist/DateInput/TouchDateField.d.ts.map +1 -1
  34. package/dist/DateInput/TouchDateField.js +8 -3
  35. package/dist/DateInput/Wheel.d.ts.map +1 -1
  36. package/dist/DateInput/Wheel.js +11 -7
  37. package/dist/DateInput/usePointerDragScroll.d.ts +10 -1
  38. package/dist/DateInput/usePointerDragScroll.d.ts.map +1 -1
  39. package/dist/DateInput/usePointerDragScroll.js +25 -9
  40. package/dist/DateInput/useScrollSettle.d.ts +3 -1
  41. package/dist/DateInput/useScrollSettle.d.ts.map +1 -1
  42. package/dist/DateInput/useScrollSettle.js +37 -8
  43. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  44. package/dist/DateRangeInput/DateRangeInput.js +9 -5
  45. package/dist/DateTimeInput/DateTimeInput.d.ts +5 -2
  46. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  47. package/dist/DateTimeInput/DateTimeInput.js +23 -12
  48. package/dist/DateTimeInput/NativeTimeSegment.d.ts.map +1 -1
  49. package/dist/DateTimeInput/NativeTimeSegment.js +5 -4
  50. package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
  51. package/dist/DateTimeInput/TouchDateTimeField.js +4 -3
  52. package/dist/Dialog/Dialog.d.ts.map +1 -1
  53. package/dist/Dialog/Dialog.js +34 -9
  54. package/dist/Dialog/DialogHeader.d.ts.map +1 -1
  55. package/dist/Dialog/DialogHeader.js +5 -4
  56. package/dist/Divider/Divider.d.ts.map +1 -1
  57. package/dist/Divider/Divider.js +2 -1
  58. package/dist/Drawer/Drawer.d.ts +1 -0
  59. package/dist/Drawer/Drawer.d.ts.map +1 -1
  60. package/dist/Drawer/Drawer.js +7 -2
  61. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  62. package/dist/DropdownMenu/DropdownMenu.js +35 -22
  63. package/dist/DropdownMenu/DropdownMenuGroup.d.ts.map +1 -1
  64. package/dist/DropdownMenu/DropdownMenuGroup.js +2 -1
  65. package/dist/DropdownMenu/MenuBottomSheetActionList.d.ts.map +1 -1
  66. package/dist/DropdownMenu/MenuBottomSheetActionList.js +6 -5
  67. package/dist/DropdownMenu/lazyMenuBottomSheet.d.ts +20 -2
  68. package/dist/DropdownMenu/lazyMenuBottomSheet.d.ts.map +1 -1
  69. package/dist/DropdownMenu/lazyMenuBottomSheet.js +31 -3
  70. package/dist/DropdownMenu/renderDropdownItems.d.ts.map +1 -1
  71. package/dist/DropdownMenu/renderDropdownItems.js +21 -16
  72. package/dist/EmptyState/EmptyState.d.ts.map +1 -1
  73. package/dist/EmptyState/EmptyState.js +2 -1
  74. package/dist/Field/FieldLabel.d.ts +1 -1
  75. package/dist/Field/FieldLabel.d.ts.map +1 -1
  76. package/dist/Field/FieldLabel.js +31 -6
  77. package/dist/Field/InputClearButton.js +3 -3
  78. package/dist/Field/PanelSearchInput.js +2 -2
  79. package/dist/FieldStatus/FieldStatus.d.ts +3 -0
  80. package/dist/FieldStatus/FieldStatus.d.ts.map +1 -1
  81. package/dist/FieldStatus/FieldStatus.js +3 -2
  82. package/dist/FileInput/FileInput.d.ts +3 -0
  83. package/dist/FileInput/FileInput.d.ts.map +1 -1
  84. package/dist/FileInput/FileInput.js +66 -34
  85. package/dist/Heading/Heading.js +1 -1
  86. package/dist/Icon/Icon.js +1 -1
  87. package/dist/Indicator/CheckIndicator.d.ts.map +1 -1
  88. package/dist/Indicator/CheckIndicator.js +2 -1
  89. package/dist/Indicator/CheckboxIndicator.d.ts.map +1 -1
  90. package/dist/Indicator/CheckboxIndicator.js +3 -1
  91. package/dist/Indicator/RadioIndicator.d.ts.map +1 -1
  92. package/dist/Indicator/RadioIndicator.js +3 -1
  93. package/dist/Item/Item.d.ts.map +1 -1
  94. package/dist/Item/Item.js +21 -7
  95. package/dist/Item/ItemSwipeLayer.js +1 -1
  96. package/dist/Layer/popoverOperations.d.ts +27 -0
  97. package/dist/Layer/popoverOperations.d.ts.map +1 -0
  98. package/dist/Layer/popoverOperations.js +159 -0
  99. package/dist/Layer/useLayer.d.ts +1 -1
  100. package/dist/Layer/useLayer.d.ts.map +1 -1
  101. package/dist/Layer/useLayer.js +120 -65
  102. package/dist/Layer/useTouchTrigger.d.ts +9 -0
  103. package/dist/Layer/useTouchTrigger.d.ts.map +1 -1
  104. package/dist/Layer/useTouchTrigger.js +1 -1
  105. package/dist/Layout/Layout.js +1 -1
  106. package/dist/Layout/LayoutContent.js +1 -1
  107. package/dist/Layout/LayoutFooter.js +1 -1
  108. package/dist/Layout/LayoutHeader.js +1 -1
  109. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  110. package/dist/Lightbox/Lightbox.js +5 -0
  111. package/dist/Link/Link.d.ts.map +1 -1
  112. package/dist/Link/Link.js +18 -4
  113. package/dist/List/List.js +1 -1
  114. package/dist/List/ListItem.js +3 -3
  115. package/dist/MobileNav/MobileNav.d.ts.map +1 -1
  116. package/dist/MobileNav/MobileNav.js +7 -2
  117. package/dist/MultiSelector/MultiSelector.d.ts +1 -1
  118. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  119. package/dist/MultiSelector/MultiSelector.js +45 -19
  120. package/dist/NavMenu/NavHeadingMenuItem.d.ts.map +1 -1
  121. package/dist/NavMenu/NavHeadingMenuItem.js +5 -29
  122. package/dist/NumberInput/NumberInput.d.ts +3 -1
  123. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  124. package/dist/NumberInput/NumberInput.js +21 -4
  125. package/dist/NumberInput/numberParser.d.ts +3 -0
  126. package/dist/NumberInput/numberParser.d.ts.map +1 -1
  127. package/dist/NumberInput/numberParser.js +7 -44
  128. package/dist/Outline/Outline.d.ts.map +1 -1
  129. package/dist/Outline/Outline.js +27 -0
  130. package/dist/Popover/Popover.d.ts.map +1 -1
  131. package/dist/Popover/Popover.js +32 -12
  132. package/dist/Popover/usePopover.d.ts.map +1 -1
  133. package/dist/Popover/usePopover.js +10 -3
  134. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  135. package/dist/PowerSearch/PowerSearch.js +37 -30
  136. package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
  137. package/dist/ProgressBar/ProgressBar.js +16 -12
  138. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  139. package/dist/RadioList/RadioListItem.js +11 -3
  140. package/dist/Resizable/useResizable.d.ts.map +1 -1
  141. package/dist/Resizable/useResizable.js +81 -12
  142. package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
  143. package/dist/SegmentedControl/SegmentedControlItem.js +4 -1
  144. package/dist/Selector/Selector.d.ts.map +1 -1
  145. package/dist/Selector/Selector.js +9 -6
  146. package/dist/Selector/hooks.js +2 -2
  147. package/dist/Selector/useSelectorPresentation.d.ts +8 -1
  148. package/dist/Selector/useSelectorPresentation.d.ts.map +1 -1
  149. package/dist/Selector/useSelectorPresentation.js +20 -4
  150. package/dist/SideNav/SideNavItem.d.ts +2 -2
  151. package/dist/SideNav/SideNavItem.d.ts.map +1 -1
  152. package/dist/SideNav/SideNavItem.js +18 -7
  153. package/dist/Skeleton/Skeleton.d.ts.map +1 -1
  154. package/dist/Skeleton/Skeleton.js +1 -1
  155. package/dist/Slider/Slider.d.ts.map +1 -1
  156. package/dist/Slider/Slider.js +100 -12
  157. package/dist/Spinner/Spinner.d.ts.map +1 -1
  158. package/dist/Spinner/Spinner.js +3 -3
  159. package/dist/Stack/Stack.js +1 -1
  160. package/dist/Stack/StackItem.js +1 -1
  161. package/dist/Stepper/Step.d.ts.map +1 -1
  162. package/dist/Stepper/Step.js +26 -6
  163. package/dist/Switch/Switch.d.ts.map +1 -1
  164. package/dist/Switch/Switch.js +10 -3
  165. package/dist/TabList/TabList.d.ts.map +1 -1
  166. package/dist/TabList/TabList.js +16 -1
  167. package/dist/Table/Table.d.ts.map +1 -1
  168. package/dist/Table/Table.js +36 -10
  169. package/dist/Table/plugins/columnResize/useTableColumnResize.d.ts.map +1 -1
  170. package/dist/Table/plugins/columnResize/useTableColumnResize.js +9 -1
  171. package/dist/Table/plugins/sortable/useTableSortable.d.ts.map +1 -1
  172. package/dist/Table/plugins/sortable/useTableSortable.js +46 -58
  173. package/dist/Text/Text.js +1 -1
  174. package/dist/Text/useTruncation.d.ts.map +1 -1
  175. package/dist/Text/useTruncation.js +23 -5
  176. package/dist/TextArea/TextArea.d.ts +1 -1
  177. package/dist/TextArea/TextArea.d.ts.map +1 -1
  178. package/dist/TextArea/TextArea.js +13 -2
  179. package/dist/TextInput/TextInput.d.ts.map +1 -1
  180. package/dist/TextInput/TextInput.js +3 -3
  181. package/dist/TimeInput/TimeInput.d.ts +6 -2
  182. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  183. package/dist/TimeInput/TimeInput.js +21 -14
  184. package/dist/TimeInput/TouchTimeField.d.ts.map +1 -1
  185. package/dist/TimeInput/TouchTimeField.js +5 -4
  186. package/dist/Timestamp/Timestamp.d.ts.map +1 -1
  187. package/dist/Timestamp/Timestamp.js +2 -2
  188. package/dist/Timestamp/TimestampHoverCard.js +4 -4
  189. package/dist/Toast/Toast.d.ts.map +1 -1
  190. package/dist/Toast/Toast.js +60 -20
  191. package/dist/Toast/ToastViewport.d.ts.map +1 -1
  192. package/dist/Toast/ToastViewport.js +101 -54
  193. package/dist/ToggleButton/ToggleButtonGroup.d.ts +7 -1
  194. package/dist/ToggleButton/ToggleButtonGroup.d.ts.map +1 -1
  195. package/dist/ToggleButton/ToggleButtonGroup.js +16 -2
  196. package/dist/Token/Token.d.ts +1 -1
  197. package/dist/Token/Token.d.ts.map +1 -1
  198. package/dist/Token/Token.js +52 -5
  199. package/dist/Token/TokenLink.d.ts.map +1 -1
  200. package/dist/Token/TokenLink.js +14 -2
  201. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  202. package/dist/Tokenizer/Tokenizer.js +95 -54
  203. package/dist/Tokenizer/TokenizerBottomSheet.d.ts.map +1 -1
  204. package/dist/Tokenizer/TokenizerBottomSheet.js +11 -17
  205. package/dist/Tokenizer/TokenizerClearAllMessageContext.d.ts +2 -0
  206. package/dist/Tokenizer/TokenizerClearAllMessageContext.d.ts.map +1 -0
  207. package/dist/Tokenizer/TokenizerClearAllMessageContext.js +17 -0
  208. package/dist/Toolbar/Toolbar.d.ts.map +1 -1
  209. package/dist/Toolbar/Toolbar.js +2 -1
  210. package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
  211. package/dist/TopNav/TopNavMegaMenu.js +3 -0
  212. package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
  213. package/dist/TopNav/TopNavMenu.js +1 -0
  214. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  215. package/dist/TreeList/TreeListItem.js +27 -4
  216. package/dist/Typeahead/BaseTypeahead.d.ts +4 -4
  217. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  218. package/dist/Typeahead/BaseTypeahead.js +52 -9
  219. package/dist/Typeahead/Typeahead.d.ts +1 -1
  220. package/dist/Typeahead/Typeahead.d.ts.map +1 -1
  221. package/dist/Typeahead/Typeahead.js +43 -9
  222. package/dist/astryx.css +26 -5
  223. package/dist/gestures/GestureArena.d.ts +32 -1
  224. package/dist/gestures/GestureArena.d.ts.map +1 -1
  225. package/dist/gestures/GestureArena.js +110 -9
  226. package/dist/gestures/GestureHost.d.ts +351 -0
  227. package/dist/gestures/GestureHost.d.ts.map +1 -0
  228. package/dist/gestures/GestureHost.js +1773 -0
  229. package/dist/gestures/GestureRecognizer.d.ts +13 -1
  230. package/dist/gestures/GestureRecognizer.d.ts.map +1 -1
  231. package/dist/gestures/GestureRecognizer.js +19 -1
  232. package/dist/gestures/LongPressGestureRecognizer.d.ts +1 -0
  233. package/dist/gestures/LongPressGestureRecognizer.d.ts.map +1 -1
  234. package/dist/gestures/LongPressGestureRecognizer.js +3 -0
  235. package/dist/gestures/PanGestureRecognizer.d.ts +1 -0
  236. package/dist/gestures/PanGestureRecognizer.d.ts.map +1 -1
  237. package/dist/gestures/PanGestureRecognizer.js +3 -0
  238. package/dist/gestures/PinchGestureRecognizer.d.ts +31 -0
  239. package/dist/gestures/PinchGestureRecognizer.d.ts.map +1 -0
  240. package/dist/gestures/PinchGestureRecognizer.js +128 -0
  241. package/dist/gestures/PressTracker.d.ts +112 -0
  242. package/dist/gestures/PressTracker.d.ts.map +1 -0
  243. package/dist/gestures/PressTracker.js +267 -0
  244. package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts +71 -0
  245. package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts.map +1 -0
  246. package/dist/gestures/ScreenEdgePanGestureRecognizer.js +223 -0
  247. package/dist/gestures/SwipeGestureRecognizer.d.ts +37 -0
  248. package/dist/gestures/SwipeGestureRecognizer.d.ts.map +1 -0
  249. package/dist/gestures/SwipeGestureRecognizer.js +148 -0
  250. package/dist/gestures/TapGestureRecognizer.d.ts +1 -0
  251. package/dist/gestures/TapGestureRecognizer.d.ts.map +1 -1
  252. package/dist/gestures/TapGestureRecognizer.js +3 -0
  253. package/dist/gestures/VelocityTracker.d.ts +6 -2
  254. package/dist/gestures/VelocityTracker.d.ts.map +1 -1
  255. package/dist/gestures/VelocityTracker.js +7 -3
  256. package/dist/gestures/haptics.d.ts +65 -0
  257. package/dist/gestures/haptics.d.ts.map +1 -0
  258. package/dist/gestures/haptics.js +182 -0
  259. package/dist/gestures/index.d.ts +37 -0
  260. package/dist/gestures/index.d.ts.map +1 -0
  261. package/dist/gestures/index.js +25 -0
  262. package/dist/gestures/nativeScroll.d.ts +42 -0
  263. package/dist/gestures/nativeScroll.d.ts.map +1 -0
  264. package/dist/gestures/nativeScroll.js +228 -0
  265. package/dist/gestures/touchAction.d.ts +60 -0
  266. package/dist/gestures/touchAction.d.ts.map +1 -0
  267. package/dist/gestures/touchAction.js +157 -0
  268. package/dist/gestures/touchActionRule.d.ts +34 -0
  269. package/dist/gestures/touchActionRule.d.ts.map +1 -0
  270. package/dist/gestures/touchActionRule.js +140 -0
  271. package/dist/gestures/types.d.ts +18 -7
  272. package/dist/gestures/types.d.ts.map +1 -1
  273. package/dist/gestures/types.js +22 -2
  274. package/dist/gestures/useGestureRecognizer.d.ts +56 -0
  275. package/dist/gestures/useGestureRecognizer.d.ts.map +1 -0
  276. package/dist/gestures/useGestureRecognizer.js +226 -0
  277. package/dist/hooks/index.d.ts +1 -1
  278. package/dist/hooks/index.d.ts.map +1 -1
  279. package/dist/hooks/modalAnnounce.d.ts.map +1 -1
  280. package/dist/hooks/modalAnnounce.js +107 -61
  281. package/dist/hooks/scrollKeyboardDelegation.d.ts.map +1 -1
  282. package/dist/hooks/scrollKeyboardDelegation.js +7 -3
  283. package/dist/hooks/useAdaptivePresentation.d.ts.map +1 -1
  284. package/dist/hooks/useAdaptivePresentation.js +4 -1
  285. package/dist/hooks/useAnnounce.d.ts +101 -42
  286. package/dist/hooks/useAnnounce.d.ts.map +1 -1
  287. package/dist/hooks/useAnnounce.js +161 -75
  288. package/dist/hooks/useAnnounceRenderedText.d.ts +11 -3
  289. package/dist/hooks/useAnnounceRenderedText.d.ts.map +1 -1
  290. package/dist/hooks/useAnnounceRenderedText.js +5 -3
  291. package/dist/hooks/useBusyAnnouncement.d.ts.map +1 -1
  292. package/dist/hooks/useBusyAnnouncement.js +6 -12
  293. package/dist/hooks/useMenuHover.d.ts.map +1 -1
  294. package/dist/hooks/useMenuHover.js +12 -1
  295. package/dist/hooks/useScrollOverflow.d.ts +2 -1
  296. package/dist/hooks/useScrollOverflow.d.ts.map +1 -1
  297. package/dist/hooks/useScrollOverflow.js +56 -10
  298. package/dist/i18n/generated-locales/af-ZA.generated.d.ts.map +1 -1
  299. package/dist/i18n/generated-locales/af-ZA.generated.js +0 -5
  300. package/dist/i18n/generated-locales/ar-SA.generated.d.ts.map +1 -1
  301. package/dist/i18n/generated-locales/ar-SA.generated.js +0 -5
  302. package/dist/i18n/generated-locales/ca-ES.generated.d.ts.map +1 -1
  303. package/dist/i18n/generated-locales/ca-ES.generated.js +0 -5
  304. package/dist/i18n/generated-locales/cs-CZ.generated.d.ts.map +1 -1
  305. package/dist/i18n/generated-locales/cs-CZ.generated.js +0 -5
  306. package/dist/i18n/generated-locales/da-DK.generated.d.ts.map +1 -1
  307. package/dist/i18n/generated-locales/da-DK.generated.js +0 -5
  308. package/dist/i18n/generated-locales/de-DE.generated.d.ts.map +1 -1
  309. package/dist/i18n/generated-locales/de-DE.generated.js +0 -5
  310. package/dist/i18n/generated-locales/el-GR.generated.d.ts.map +1 -1
  311. package/dist/i18n/generated-locales/el-GR.generated.js +0 -5
  312. package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
  313. package/dist/i18n/generated-locales/en.generated.js +8 -5
  314. package/dist/i18n/generated-locales/es-ES.generated.d.ts.map +1 -1
  315. package/dist/i18n/generated-locales/es-ES.generated.js +0 -5
  316. package/dist/i18n/generated-locales/fi-FI.generated.d.ts.map +1 -1
  317. package/dist/i18n/generated-locales/fi-FI.generated.js +0 -5
  318. package/dist/i18n/generated-locales/fr-FR.generated.d.ts.map +1 -1
  319. package/dist/i18n/generated-locales/fr-FR.generated.js +0 -5
  320. package/dist/i18n/generated-locales/he-IL.generated.d.ts.map +1 -1
  321. package/dist/i18n/generated-locales/he-IL.generated.js +0 -5
  322. package/dist/i18n/generated-locales/hu-HU.generated.d.ts.map +1 -1
  323. package/dist/i18n/generated-locales/hu-HU.generated.js +0 -5
  324. package/dist/i18n/generated-locales/it-IT.generated.d.ts.map +1 -1
  325. package/dist/i18n/generated-locales/it-IT.generated.js +0 -5
  326. package/dist/i18n/generated-locales/ja-JP.generated.d.ts.map +1 -1
  327. package/dist/i18n/generated-locales/ja-JP.generated.js +0 -5
  328. package/dist/i18n/generated-locales/ko-KR.generated.d.ts.map +1 -1
  329. package/dist/i18n/generated-locales/ko-KR.generated.js +0 -5
  330. package/dist/i18n/generated-locales/nl-NL.generated.d.ts.map +1 -1
  331. package/dist/i18n/generated-locales/nl-NL.generated.js +0 -5
  332. package/dist/i18n/generated-locales/no-NO.generated.d.ts.map +1 -1
  333. package/dist/i18n/generated-locales/no-NO.generated.js +0 -5
  334. package/dist/i18n/generated-locales/pl-PL.generated.d.ts.map +1 -1
  335. package/dist/i18n/generated-locales/pl-PL.generated.js +0 -5
  336. package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
  337. package/dist/i18n/generated-locales/pseudo.generated.js +8 -5
  338. package/dist/i18n/generated-locales/pt-BR.generated.d.ts.map +1 -1
  339. package/dist/i18n/generated-locales/pt-BR.generated.js +0 -5
  340. package/dist/i18n/generated-locales/pt-PT.generated.d.ts.map +1 -1
  341. package/dist/i18n/generated-locales/pt-PT.generated.js +0 -5
  342. package/dist/i18n/generated-locales/ro-RO.generated.d.ts.map +1 -1
  343. package/dist/i18n/generated-locales/ro-RO.generated.js +0 -5
  344. package/dist/i18n/generated-locales/ru-RU.generated.d.ts.map +1 -1
  345. package/dist/i18n/generated-locales/ru-RU.generated.js +0 -5
  346. package/dist/i18n/generated-locales/sr-SP.generated.d.ts.map +1 -1
  347. package/dist/i18n/generated-locales/sr-SP.generated.js +0 -5
  348. package/dist/i18n/generated-locales/sv-SE.generated.d.ts.map +1 -1
  349. package/dist/i18n/generated-locales/sv-SE.generated.js +0 -5
  350. package/dist/i18n/generated-locales/tr-TR.generated.d.ts.map +1 -1
  351. package/dist/i18n/generated-locales/tr-TR.generated.js +0 -5
  352. package/dist/i18n/generated-locales/uk-UA.generated.d.ts.map +1 -1
  353. package/dist/i18n/generated-locales/uk-UA.generated.js +0 -5
  354. package/dist/i18n/generated-locales/vi-VN.generated.d.ts.map +1 -1
  355. package/dist/i18n/generated-locales/vi-VN.generated.js +0 -5
  356. package/dist/i18n/generated-locales/zh-CN.generated.d.ts.map +1 -1
  357. package/dist/i18n/generated-locales/zh-CN.generated.js +0 -5
  358. package/dist/i18n/generated-locales/zh-TW.generated.d.ts.map +1 -1
  359. package/dist/i18n/generated-locales/zh-TW.generated.js +0 -5
  360. package/dist/i18n/useListFormatter.d.ts +9 -0
  361. package/dist/i18n/useListFormatter.d.ts.map +1 -0
  362. package/dist/i18n/useListFormatter.js +41 -0
  363. package/dist/motion/animator.d.ts +83 -8
  364. package/dist/motion/animator.d.ts.map +1 -1
  365. package/dist/motion/animator.js +701 -119
  366. package/dist/motion/coast.d.ts +4 -1
  367. package/dist/motion/coast.d.ts.map +1 -1
  368. package/dist/motion/coast.js +4 -2
  369. package/dist/motion/reducedMotion.d.ts.map +1 -1
  370. package/dist/motion/reducedMotion.js +6 -2
  371. package/dist/motion/reducedMotionPlan.d.ts +7 -0
  372. package/dist/motion/reducedMotionPlan.d.ts.map +1 -1
  373. package/dist/motion/reducedMotionPlan.js +14 -1
  374. package/dist/motion/resolveMotion.d.ts +42 -0
  375. package/dist/motion/resolveMotion.d.ts.map +1 -0
  376. package/dist/motion/resolveMotion.js +43 -0
  377. package/dist/motion/roleValue.d.ts +2 -1
  378. package/dist/motion/roleValue.d.ts.map +1 -1
  379. package/dist/motion/roleValue.js +2 -1
  380. package/dist/motion/uikit/timing.d.ts +7 -1
  381. package/dist/motion/uikit/timing.d.ts.map +1 -1
  382. package/dist/motion/uikit/timing.js +19 -1
  383. package/dist/theme/defineTheme.d.ts +19 -5
  384. package/dist/theme/defineTheme.d.ts.map +1 -1
  385. package/dist/theme/defineTheme.js +64 -4
  386. package/dist/theme/domainTokens/domainTokenDefaults.d.ts +3 -0
  387. package/dist/theme/domainTokens/domainTokenDefaults.d.ts.map +1 -0
  388. package/dist/theme/domainTokens/domainTokenDefaults.js +19 -0
  389. package/dist/theme/domainTokens/index.d.ts +5 -4
  390. package/dist/theme/domainTokens/index.d.ts.map +1 -1
  391. package/dist/theme/domainTokens/index.js +5 -9
  392. package/dist/theme/index.d.ts +1 -0
  393. package/dist/theme/index.d.ts.map +1 -1
  394. package/dist/theme/motionKnobs.d.ts +56 -0
  395. package/dist/theme/motionKnobs.d.ts.map +1 -0
  396. package/dist/theme/motionKnobs.js +48 -0
  397. package/dist/theme/resolveThemeValues.d.ts +11 -4
  398. package/dist/theme/resolveThemeValues.d.ts.map +1 -1
  399. package/dist/theme/resolveThemeValues.js +40 -4
  400. package/dist/theme/themeAdaptations.d.ts +7 -3
  401. package/dist/theme/themeAdaptations.d.ts.map +1 -1
  402. package/dist/theme/themeAdaptations.js +36 -7
  403. package/dist/theme/themeMotion.d.ts +96 -0
  404. package/dist/theme/themeMotion.d.ts.map +1 -0
  405. package/dist/theme/themeMotion.js +177 -0
  406. package/dist/theme/tokenReferences.d.ts +31 -0
  407. package/dist/theme/tokenReferences.d.ts.map +1 -0
  408. package/dist/theme/tokenReferences.js +132 -0
  409. package/dist/theme/tokens.d.ts +1 -0
  410. package/dist/theme/tokens.d.ts.map +1 -1
  411. package/dist/theme/tokens.js +9 -129
  412. package/dist/theme/tokens.stylex.d.ts +12 -0
  413. package/dist/theme/tokens.stylex.d.ts.map +1 -1
  414. package/dist/theme/tokens.stylex.js +12 -0
  415. package/dist/utils/focusOutline.stylex.d.ts +6 -0
  416. package/dist/utils/focusOutline.stylex.d.ts.map +1 -1
  417. package/dist/utils/focusOutline.stylex.js +7 -0
  418. package/dist/utils/focusReturn.d.ts +17 -1
  419. package/dist/utils/focusReturn.d.ts.map +1 -1
  420. package/dist/utils/focusReturn.js +11 -4
  421. package/dist/utils/getNodeText.d.ts +8 -0
  422. package/dist/utils/getNodeText.d.ts.map +1 -0
  423. package/dist/utils/getNodeText.js +37 -0
  424. package/dist/utils/interactionModality.d.ts +11 -2
  425. package/dist/utils/interactionModality.d.ts.map +1 -1
  426. package/dist/utils/interactionModality.js +58 -5
  427. package/dist/utils/interactionOverlay.stylex.d.ts.map +1 -1
  428. package/dist/utils/interactionOverlay.stylex.js +17 -8
  429. package/dist/utils/plainDate.d.ts +2 -0
  430. package/dist/utils/plainDate.d.ts.map +1 -1
  431. package/dist/utils/plainDate.js +46 -3
  432. package/dist/utils/presentation.d.ts +50 -0
  433. package/dist/utils/presentation.d.ts.map +1 -0
  434. package/dist/utils/presentation.js +125 -0
  435. package/dist/utils/pressFeedback.d.ts.map +1 -1
  436. package/dist/utils/pressFeedback.js +20 -1
  437. package/dist/utils/primaryClick.d.ts +11 -0
  438. package/dist/utils/primaryClick.d.ts.map +1 -0
  439. package/dist/utils/primaryClick.js +25 -0
  440. package/dist/utils/sharedMutationObserver.d.ts +19 -11
  441. package/dist/utils/sharedMutationObserver.d.ts.map +1 -1
  442. package/dist/utils/sharedMutationObserver.js +66 -26
  443. package/dist/utils/timeFormat.d.ts +47 -0
  444. package/dist/utils/timeFormat.d.ts.map +1 -0
  445. package/dist/utils/timeFormat.js +240 -0
  446. package/dist/utils/touchPresentation.d.ts +2 -2
  447. package/dist/utils/touchPresentation.d.ts.map +1 -1
  448. package/dist/utils/touchPresentation.js +13 -8
  449. package/dist/utils/touchPresentation.stylex.d.ts +15 -0
  450. package/dist/utils/touchPresentation.stylex.d.ts.map +1 -0
  451. package/dist/utils/touchPresentation.stylex.js +33 -0
  452. package/dist/utils/touchQueries.d.ts +13 -0
  453. package/dist/utils/touchQueries.d.ts.map +1 -0
  454. package/dist/utils/touchQueries.js +15 -0
  455. package/dist/utils/unicodeDigits.d.ts +20 -0
  456. package/dist/utils/unicodeDigits.d.ts.map +1 -0
  457. package/dist/utils/unicodeDigits.js +70 -0
  458. package/dist/utils/useFocusAcrossSwap.d.ts +19 -0
  459. package/dist/utils/useFocusAcrossSwap.d.ts.map +1 -0
  460. package/dist/utils/useFocusAcrossSwap.js +41 -0
  461. package/locales/af-ZA.json +0 -21
  462. package/locales/ar-SA.json +0 -21
  463. package/locales/ca-ES.json +0 -21
  464. package/locales/cs-CZ.json +0 -21
  465. package/locales/da-DK.json +0 -21
  466. package/locales/de-DE.json +0 -21
  467. package/locales/el-GR.json +0 -21
  468. package/locales/en.json +35 -23
  469. package/locales/es-ES.json +0 -21
  470. package/locales/fi-FI.json +0 -21
  471. package/locales/fr-FR.json +0 -21
  472. package/locales/he-IL.json +0 -21
  473. package/locales/hu-HU.json +0 -21
  474. package/locales/it-IT.json +0 -21
  475. package/locales/ja-JP.json +0 -21
  476. package/locales/ko-KR.json +0 -21
  477. package/locales/nl-NL.json +0 -21
  478. package/locales/no-NO.json +0 -21
  479. package/locales/pl-PL.json +0 -21
  480. package/locales/pseudo.json +23 -14
  481. package/locales/pt-BR.json +0 -21
  482. package/locales/pt-PT.json +0 -21
  483. package/locales/ro-RO.json +0 -21
  484. package/locales/ru-RU.json +0 -21
  485. package/locales/sr-SP.json +0 -21
  486. package/locales/sv-SE.json +0 -21
  487. package/locales/tr-TR.json +0 -21
  488. package/locales/uk-UA.json +0 -21
  489. package/locales/vi-VN.json +0 -21
  490. package/locales/zh-CN.json +0 -21
  491. package/locales/zh-TW.json +0 -21
  492. package/package.json +19 -5
  493. package/src/AlertDialog/AlertDialog.tsx +6 -5
  494. package/src/AppShell/AppShell.doc.mjs +1 -1
  495. package/src/AppShell/AppShell.spec.md +9 -12
  496. package/src/AppShell/AppShell.tsx +225 -5
  497. package/src/AppShell/observeMainOverflow.ts +143 -0
  498. package/src/Avatar/Avatar.spec.md +22 -22
  499. package/src/Avatar/Avatar.tsx +18 -1
  500. package/src/Badge/Badge.doc.mjs +1 -1
  501. package/src/Badge/Badge.tsx +20 -2
  502. package/src/BottomSheet/BottomSheet.tsx +1 -1
  503. package/src/BottomSheet/BottomSheetPanel.tsx +2 -1
  504. package/src/BottomSheet/useSheetGestures.ts +37 -6
  505. package/src/Button/Button.tsx +4 -4
  506. package/src/Calendar/Calendar.spec.md +87 -48
  507. package/src/Carousel/Carousel.doc.mjs +1 -1
  508. package/src/Carousel/Carousel.spec.md +9 -9
  509. package/src/Carousel/Carousel.tsx +10 -2
  510. package/src/Chat/ChatComposer.spec.md +75 -59
  511. package/src/Chat/ChatComposerTokenElement.spec.md +45 -33
  512. package/src/Chat/ChatDictationButton.spec.md +126 -81
  513. package/src/Chat/ChatLayout.spec.md +41 -36
  514. package/src/Chat/ChatLayoutScrollButton.spec.md +87 -103
  515. package/src/Chat/ChatMessage.spec.md +64 -65
  516. package/src/Chat/ChatSendButton.spec.md +78 -67
  517. package/src/CheckboxInput/CheckboxInput.tsx +6 -5
  518. package/src/Citation/Citation.spec.md +60 -47
  519. package/src/ClickableCard/ClickableCard.tsx +48 -4
  520. package/src/Collapsible/Collapsible.spec.md +2 -0
  521. package/src/Collapsible/CollapsibleGroup.spec.md +57 -29
  522. package/src/ComplexSelector/ComplexSelector.tsx +60 -6
  523. package/src/ContextMenu/ContextMenu.spec.md +84 -56
  524. package/src/ContextMenu/ContextMenuItem.spec.md +132 -0
  525. package/src/DateInput/DateInput.spec.md +74 -57
  526. package/src/DateInput/DateInput.tsx +29 -14
  527. package/src/DateInput/MonthScroller.tsx +108 -24
  528. package/src/DateInput/TouchDateField.tsx +14 -3
  529. package/src/DateInput/Wheel.tsx +9 -6
  530. package/src/DateInput/usePointerDragScroll.ts +34 -10
  531. package/src/DateInput/useScrollSettle.ts +38 -7
  532. package/src/DateRangeInput/DateRangeInput.tsx +34 -15
  533. package/src/DateTimeInput/DateTimeInput.doc.mjs +10 -8
  534. package/src/DateTimeInput/DateTimeInput.spec.md +75 -51
  535. package/src/DateTimeInput/DateTimeInput.tsx +46 -24
  536. package/src/DateTimeInput/NativeTimeSegment.tsx +4 -6
  537. package/src/DateTimeInput/TouchDateTimeField.tsx +3 -7
  538. package/src/Dialog/Dialog.tsx +42 -9
  539. package/src/Dialog/DialogHeader.tsx +5 -4
  540. package/src/Divider/Divider.tsx +2 -1
  541. package/src/Drawer/Drawer.spec.md +111 -80
  542. package/src/Drawer/Drawer.tsx +13 -2
  543. package/src/Drawer/DrawerHeader.spec.md +181 -0
  544. package/src/DropdownMenu/DropdownMenu.spec.md +198 -218
  545. package/src/DropdownMenu/DropdownMenu.tsx +43 -21
  546. package/src/DropdownMenu/DropdownMenuCheckboxItem.spec.md +192 -0
  547. package/src/DropdownMenu/DropdownMenuDivider.spec.md +25 -31
  548. package/src/DropdownMenu/DropdownMenuGroup.spec.md +145 -0
  549. package/src/DropdownMenu/DropdownMenuGroup.tsx +2 -1
  550. package/src/DropdownMenu/DropdownMenuItem.spec.md +48 -42
  551. package/src/DropdownMenu/DropdownMenuRadioGroup.spec.md +34 -37
  552. package/src/DropdownMenu/DropdownMenuRadioItem.spec.md +54 -53
  553. package/src/DropdownMenu/DropdownMenuSubMenu.spec.md +47 -41
  554. package/src/DropdownMenu/MenuBottomSheetActionList.tsx +6 -5
  555. package/src/DropdownMenu/lazyMenuBottomSheet.ts +36 -7
  556. package/src/DropdownMenu/renderDropdownItems.tsx +24 -19
  557. package/src/EmptyState/EmptyState.spec.md +35 -29
  558. package/src/EmptyState/EmptyState.tsx +2 -1
  559. package/src/Field/Field.doc.mjs +2 -1
  560. package/src/Field/FieldLabel.doc.mjs +4 -2
  561. package/src/Field/FieldLabel.spec.md +30 -21
  562. package/src/Field/FieldLabel.tsx +42 -7
  563. package/src/Field/InputClearButton.tsx +3 -3
  564. package/src/Field/PanelSearchInput.tsx +2 -2
  565. package/src/FieldStatus/FieldStatus.tsx +9 -2
  566. package/src/FileInput/FileInput.tsx +73 -29
  567. package/src/FormLayout/FormLayout.spec.md +11 -20
  568. package/src/Heading/Heading.tsx +1 -1
  569. package/src/Icon/Icon.tsx +1 -1
  570. package/src/Indicator/CheckIndicator.spec.md +47 -42
  571. package/src/Indicator/CheckIndicator.tsx +2 -1
  572. package/src/Indicator/CheckboxIndicator.spec.md +40 -37
  573. package/src/Indicator/CheckboxIndicator.tsx +3 -1
  574. package/src/Indicator/Indicator.spec.md +188 -0
  575. package/src/Indicator/RadioIndicator.tsx +3 -1
  576. package/src/Item/Item.tsx +16 -8
  577. package/src/Item/ItemSwipeLayer.tsx +1 -1
  578. package/src/Kbd/Kbd.spec.md +81 -38
  579. package/src/Layer/popoverOperations.ts +172 -0
  580. package/src/Layer/useLayer.tsx +145 -77
  581. package/src/Layer/useTouchTrigger.ts +1 -1
  582. package/src/Layout/Layout.spec.md +127 -131
  583. package/src/Layout/Layout.tsx +1 -1
  584. package/src/Layout/LayoutContent.spec.md +205 -0
  585. package/src/Layout/LayoutContent.tsx +1 -1
  586. package/src/Layout/LayoutFooter.spec.md +190 -0
  587. package/src/Layout/LayoutFooter.tsx +1 -1
  588. package/src/Layout/LayoutHeader.spec.md +193 -0
  589. package/src/Layout/LayoutHeader.tsx +1 -1
  590. package/src/Layout/LayoutPanel.spec.md +194 -0
  591. package/src/Lightbox/Lightbox.spec.md +249 -61
  592. package/src/Lightbox/Lightbox.tsx +5 -0
  593. package/src/Link/Link.spec.md +3 -2
  594. package/src/Link/Link.tsx +20 -2
  595. package/src/List/List.tsx +1 -1
  596. package/src/List/ListItem.spec.md +205 -0
  597. package/src/List/ListItem.tsx +3 -3
  598. package/src/MetadataList/MetadataList.spec.md +187 -0
  599. package/src/MetadataList/MetadataListItem.spec.md +165 -0
  600. package/src/MobileNav/MobileNav.tsx +9 -1
  601. package/src/MultiSelector/MultiSelector.spec.md +124 -114
  602. package/src/MultiSelector/MultiSelector.tsx +44 -15
  603. package/src/NavMenu/NavHeadingMenu.spec.md +1 -5
  604. package/src/NavMenu/NavHeadingMenuItem.tsx +5 -39
  605. package/src/NumberInput/NumberInput.doc.mjs +13 -7
  606. package/src/NumberInput/NumberInput.spec.md +59 -37
  607. package/src/NumberInput/NumberInput.tsx +37 -4
  608. package/src/NumberInput/numberParser.ts +7 -45
  609. package/src/Outline/Outline.spec.md +165 -69
  610. package/src/Outline/Outline.tsx +23 -0
  611. package/src/Outline/modules/parseOutlineFromMarkdown.spec.md +3 -4
  612. package/src/OverflowList/OverflowList.spec.md +102 -55
  613. package/src/Pagination/Pagination.spec.md +158 -84
  614. package/src/Popover/Popover.tsx +34 -14
  615. package/src/Popover/usePopover.tsx +10 -3
  616. package/src/PowerSearch/PowerSearch.tsx +42 -31
  617. package/src/ProgressBar/ProgressBar.spec.md +143 -119
  618. package/src/ProgressBar/ProgressBar.tsx +58 -10
  619. package/src/RadioList/RadioList.spec.md +1 -1
  620. package/src/RadioList/RadioListItem.tsx +11 -2
  621. package/src/Resizable/Resizable.spec.md +144 -124
  622. package/src/Resizable/useResizable.ts +82 -16
  623. package/src/SegmentedControl/SegmentedControlItem.tsx +11 -5
  624. package/src/Selector/Selector.tsx +16 -6
  625. package/src/Selector/hooks.ts +2 -2
  626. package/src/Selector/useSelectorPresentation.ts +31 -3
  627. package/src/SideNav/SideNavItem.spec.md +14 -24
  628. package/src/SideNav/SideNavItem.tsx +27 -10
  629. package/src/Skeleton/Skeleton.tsx +3 -1
  630. package/src/Slider/Slider.spec.md +18 -16
  631. package/src/Slider/Slider.tsx +117 -12
  632. package/src/Spinner/Spinner.tsx +5 -3
  633. package/src/Stack/Stack.tsx +1 -1
  634. package/src/Stack/StackItem.tsx +1 -1
  635. package/src/Stepper/Step.spec.md +36 -42
  636. package/src/Stepper/Step.tsx +33 -6
  637. package/src/Stepper/Stepper.doc.mjs +1 -1
  638. package/src/Switch/Switch.spec.md +1 -1
  639. package/src/Switch/Switch.tsx +11 -3
  640. package/src/TabList/TabList.tsx +16 -1
  641. package/src/Table/Table.spec.md +1 -1
  642. package/src/Table/Table.tsx +43 -11
  643. package/src/Table/plugins/columnResize/useTableColumnResize.tsx +8 -4
  644. package/src/Table/plugins/sortable/useTableSortable.tsx +44 -73
  645. package/src/Text/Text.tsx +1 -1
  646. package/src/Text/useTruncation.ts +23 -5
  647. package/src/TextArea/TextArea.spec.md +10 -13
  648. package/src/TextArea/TextArea.tsx +15 -1
  649. package/src/TextInput/TextInput.tsx +3 -1
  650. package/src/TimeInput/TimeInput.doc.mjs +8 -8
  651. package/src/TimeInput/TimeInput.spec.md +58 -61
  652. package/src/TimeInput/TimeInput.tsx +36 -18
  653. package/src/TimeInput/TouchTimeField.tsx +4 -6
  654. package/src/Timestamp/Timestamp.spec.md +6 -12
  655. package/src/Timestamp/Timestamp.tsx +6 -2
  656. package/src/Timestamp/TimestampHoverCard.tsx +4 -4
  657. package/src/Toast/Toast.spec.md +15 -24
  658. package/src/Toast/Toast.tsx +104 -36
  659. package/src/Toast/ToastViewport.tsx +121 -53
  660. package/src/ToggleButton/ToggleButtonGroup.tsx +20 -1
  661. package/src/Token/Token.doc.mjs +2 -2
  662. package/src/Token/Token.spec.md +52 -61
  663. package/src/Token/Token.tsx +75 -5
  664. package/src/Token/TokenLink.tsx +21 -7
  665. package/src/Tokenizer/Tokenizer.spec.md +18 -4
  666. package/src/Tokenizer/Tokenizer.tsx +102 -60
  667. package/src/Tokenizer/TokenizerBottomSheet.tsx +11 -17
  668. package/src/Tokenizer/TokenizerClearAllMessageContext.ts +21 -0
  669. package/src/Toolbar/Toolbar.spec.md +5 -6
  670. package/src/Toolbar/Toolbar.tsx +2 -1
  671. package/src/TopNav/TopNavMegaMenu.tsx +4 -1
  672. package/src/TopNav/TopNavMenu.tsx +1 -0
  673. package/src/TreeList/TreeListItem.tsx +26 -4
  674. package/src/Typeahead/BaseTypeahead.doc.mjs +3 -3
  675. package/src/Typeahead/BaseTypeahead.spec.md +45 -45
  676. package/src/Typeahead/BaseTypeahead.tsx +66 -11
  677. package/src/Typeahead/Typeahead.tsx +51 -9
  678. package/src/gestures/GestureArena.ts +111 -11
  679. package/src/gestures/GestureHost.ts +2064 -0
  680. package/src/gestures/GestureRecognizer.ts +21 -1
  681. package/src/gestures/LongPressGestureRecognizer.ts +4 -0
  682. package/src/gestures/PanGestureRecognizer.ts +4 -0
  683. package/src/gestures/PinchGestureRecognizer.ts +148 -0
  684. package/src/gestures/PressTracker.ts +386 -0
  685. package/src/gestures/README.md +420 -0
  686. package/src/gestures/ScreenEdgePanGestureRecognizer.ts +256 -0
  687. package/src/gestures/SwipeGestureRecognizer.ts +197 -0
  688. package/src/gestures/TapGestureRecognizer.ts +4 -0
  689. package/src/gestures/VelocityTracker.ts +10 -3
  690. package/src/gestures/haptics.ts +203 -0
  691. package/src/gestures/index.ts +79 -0
  692. package/src/gestures/nativeScroll.ts +277 -0
  693. package/src/gestures/touchAction.ts +191 -0
  694. package/src/gestures/touchActionRule.ts +157 -0
  695. package/src/gestures/types.ts +30 -9
  696. package/src/gestures/useGestureRecognizer.ts +305 -0
  697. package/src/hooks/index.ts +5 -1
  698. package/src/hooks/modalAnnounce.ts +129 -72
  699. package/src/hooks/scrollKeyboardDelegation.ts +9 -3
  700. package/src/hooks/useAdaptivePresentation.ts +4 -1
  701. package/src/hooks/useAnnounce.doc.mjs +35 -3
  702. package/src/hooks/useAnnounce.ts +209 -102
  703. package/src/hooks/useAnnounceRenderedText.ts +17 -3
  704. package/src/hooks/useBusyAnnouncement.ts +7 -10
  705. package/src/hooks/useMenuHover.ts +15 -1
  706. package/src/hooks/useScrollOverflow.doc.mjs +1 -1
  707. package/src/hooks/useScrollOverflow.ts +59 -10
  708. package/src/i18n/generated-locales/af-ZA.generated.ts +0 -7
  709. package/src/i18n/generated-locales/ar-SA.generated.ts +0 -6
  710. package/src/i18n/generated-locales/ca-ES.generated.ts +0 -7
  711. package/src/i18n/generated-locales/cs-CZ.generated.ts +0 -6
  712. package/src/i18n/generated-locales/da-DK.generated.ts +0 -6
  713. package/src/i18n/generated-locales/de-DE.generated.ts +0 -6
  714. package/src/i18n/generated-locales/el-GR.generated.ts +0 -7
  715. package/src/i18n/generated-locales/en.generated.ts +9 -6
  716. package/src/i18n/generated-locales/es-ES.generated.ts +0 -7
  717. package/src/i18n/generated-locales/fi-FI.generated.ts +0 -6
  718. package/src/i18n/generated-locales/fr-FR.generated.ts +0 -7
  719. package/src/i18n/generated-locales/he-IL.generated.ts +0 -6
  720. package/src/i18n/generated-locales/hu-HU.generated.ts +0 -6
  721. package/src/i18n/generated-locales/it-IT.generated.ts +0 -7
  722. package/src/i18n/generated-locales/ja-JP.generated.ts +0 -6
  723. package/src/i18n/generated-locales/ko-KR.generated.ts +0 -6
  724. package/src/i18n/generated-locales/nl-NL.generated.ts +0 -6
  725. package/src/i18n/generated-locales/no-NO.generated.ts +0 -6
  726. package/src/i18n/generated-locales/pl-PL.generated.ts +0 -7
  727. package/src/i18n/generated-locales/pseudo.generated.ts +9 -6
  728. package/src/i18n/generated-locales/pt-BR.generated.ts +0 -7
  729. package/src/i18n/generated-locales/pt-PT.generated.ts +0 -7
  730. package/src/i18n/generated-locales/ro-RO.generated.ts +0 -6
  731. package/src/i18n/generated-locales/ru-RU.generated.ts +0 -7
  732. package/src/i18n/generated-locales/sr-SP.generated.ts +0 -6
  733. package/src/i18n/generated-locales/sv-SE.generated.ts +0 -6
  734. package/src/i18n/generated-locales/tr-TR.generated.ts +0 -7
  735. package/src/i18n/generated-locales/uk-UA.generated.ts +0 -7
  736. package/src/i18n/generated-locales/vi-VN.generated.ts +0 -6
  737. package/src/i18n/generated-locales/zh-CN.generated.ts +0 -6
  738. package/src/i18n/generated-locales/zh-TW.generated.ts +0 -6
  739. package/src/i18n/useListFormatter.ts +42 -0
  740. package/src/motion/MEASUREMENTS.md +2 -1
  741. package/src/motion/README.md +113 -33
  742. package/src/motion/animator.ts +932 -130
  743. package/src/motion/coast.ts +10 -3
  744. package/src/motion/reducedMotion.ts +12 -2
  745. package/src/motion/reducedMotionPlan.ts +20 -1
  746. package/src/motion/resolveMotion.ts +68 -0
  747. package/src/motion/roleValue.ts +2 -1
  748. package/src/motion/uikit/timing.ts +15 -1
  749. package/src/theme/Theme.doc.mjs +1 -1
  750. package/src/theme/defineTheme.ts +117 -8
  751. package/src/theme/domainTokens/domainTokenDefaults.ts +19 -0
  752. package/src/theme/domainTokens/index.ts +6 -10
  753. package/src/theme/index.ts +5 -0
  754. package/src/theme/motionKnobs.ts +82 -0
  755. package/src/theme/resolveThemeValues.ts +68 -6
  756. package/src/theme/themeAdaptations.ts +52 -12
  757. package/src/theme/themeMotion.ts +221 -0
  758. package/src/theme/tokenReferences.ts +149 -0
  759. package/src/theme/tokens.stylex.ts +10 -0
  760. package/src/theme/tokens.ts +15 -158
  761. package/src/utils/focusOutline.stylex.ts +28 -0
  762. package/src/utils/focusReturn.ts +17 -6
  763. package/src/utils/getNodeText.ts +37 -0
  764. package/src/utils/interactionModality.ts +64 -5
  765. package/src/utils/interactionOverlay.stylex.ts +12 -3
  766. package/src/utils/plainDate.ts +58 -3
  767. package/src/utils/presentation.ts +180 -0
  768. package/src/utils/pressFeedback.ts +34 -11
  769. package/src/utils/primaryClick.ts +28 -0
  770. package/src/utils/sharedMutationObserver.ts +87 -29
  771. package/src/utils/timeFormat.ts +314 -0
  772. package/src/utils/touchPresentation.stylex.ts +43 -0
  773. package/src/utils/touchPresentation.ts +13 -8
  774. package/src/utils/touchQueries.ts +15 -0
  775. package/src/utils/unicodeDigits.ts +71 -0
  776. package/src/utils/useFocusAcrossSwap.ts +55 -0
  777. package/dist/Item/ItemMarkerColumns.a11y.chromium.spec.d.ts +0 -2
  778. package/dist/Item/ItemMarkerColumns.a11y.chromium.spec.js +0 -132
  779. package/dist/Lightbox/LightboxKeyboard.a11y.browser.spec.d.ts +0 -2
  780. package/dist/Lightbox/LightboxKeyboard.a11y.browser.spec.js +0 -78
  781. package/dist/Lightbox/LightboxViewport.a11y.browser.spec.d.ts +0 -2
  782. package/dist/Lightbox/LightboxViewport.a11y.browser.spec.js +0 -65
  783. package/dist/List/ListMarkers.a11y.chromium.spec.d.ts +0 -2
  784. package/dist/List/ListMarkers.a11y.chromium.spec.js +0 -677
  785. package/dist/Markdown/Markdown.fr23.bench.d.ts +0 -2
  786. package/dist/Markdown/Markdown.fr23.bench.d.ts.map +0 -1
  787. package/dist/Markdown/Markdown.fr23.bench.js +0 -152
  788. package/dist/Markdown/MarkdownTable.a11y.chromium.spec.d.ts +0 -2
  789. package/dist/Markdown/MarkdownTable.a11y.chromium.spec.js +0 -590
  790. package/dist/Markdown/plugins/headingLinks.a11y.chromium.spec.d.ts +0 -2
  791. package/dist/Markdown/plugins/headingLinks.a11y.chromium.spec.js +0 -657
  792. package/dist/MobileNav/MobileNavPinchZoom.a11y.chromium.spec.d.ts +0 -2
  793. package/dist/MobileNav/MobileNavPinchZoom.a11y.chromium.spec.js +0 -153
  794. package/dist/MultiSelector/CreateRow.a11y.chromium.spec.d.ts +0 -2
  795. package/dist/MultiSelector/CreateRow.a11y.chromium.spec.js +0 -116
  796. package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.d.ts +0 -2
  797. package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.js +0 -49
  798. package/dist/MultiSelector/OptionActions.a11y.chromium.spec.d.ts +0 -2
  799. package/dist/MultiSelector/OptionActions.a11y.chromium.spec.js +0 -273
  800. package/dist/Outline/OutlineFocus.a11y.chromium.spec.d.ts +0 -2
  801. package/dist/Outline/OutlineFocus.a11y.chromium.spec.js +0 -421
  802. package/dist/Typeahead/BaseTypeahead.emptyState.a11y.chromium.spec.d.ts +0 -2
  803. package/dist/Typeahead/BaseTypeahead.emptyState.a11y.chromium.spec.js +0 -118
  804. package/dist/reset.a11y.chromium.spec.d.ts +0 -2
  805. package/dist/reset.a11y.chromium.spec.js +0 -231
  806. package/dist/reset.until-found.a11y.browser.spec.d.ts +0 -2
  807. package/dist/reset.until-found.a11y.browser.spec.js +0 -72
  808. package/src/AlertDialog/AlertDialog.test.tsx +0 -519
  809. package/src/AlertDialog/__tests__/AlertDialog.a11y.chromium.spec.ts +0 -116
  810. package/src/AlertDialog/__tests__/AlertDialog.a11y.renders.tsx +0 -52
  811. package/src/AlertDialog/__tests__/AlertDialog.a11y.states.ts +0 -68
  812. package/src/AlertDialog/__tests__/AlertDialog.a11y.test.tsx +0 -144
  813. package/src/AlertDialog/useImperativeAlertDialog.test.tsx +0 -163
  814. package/src/AppShell/AppShell.test.tsx +0 -1148
  815. package/src/AspectRatio/AspectRatio.test.tsx +0 -398
  816. package/src/AspectRatio/__tests__/AspectRatioFitLayers.a11y.browser.spec.ts +0 -158
  817. package/src/Avatar/Avatar.test.tsx +0 -867
  818. package/src/Avatar/AvatarStatusDot.test.tsx +0 -306
  819. package/src/AvatarGroup/AvatarGroup.test.tsx +0 -313
  820. package/src/AvatarGroup/AvatarGroupOverflow.test.tsx +0 -263
  821. package/src/Badge/Badge.test.tsx +0 -125
  822. package/src/Banner/Banner.test.tsx +0 -879
  823. package/src/Blockquote/Blockquote.test.tsx +0 -121
  824. package/src/BottomSheet/BottomSheet.test.tsx +0 -3108
  825. package/src/BottomSheet/BottomSheetEdgeTint.test.tsx +0 -248
  826. package/src/BottomSheet/BottomSheetKeyboard.test.tsx +0 -195
  827. package/src/BottomSheet/BottomSheetPanel.test.tsx +0 -549
  828. package/src/BottomSheet/BottomSheetReplace.test.tsx +0 -1638
  829. package/src/BottomSheet/BottomSheetStacking.announce.test.tsx +0 -246
  830. package/src/BottomSheet/BottomSheetStacking.test.tsx +0 -3184
  831. package/src/BottomSheet/BottomSheetSwitcher.test.tsx +0 -2010
  832. package/src/BottomSheet/__tests__/BottomSheetDrag.a11y.chromium.spec.ts +0 -129
  833. package/src/BottomSheet/__tests__/BottomSheetFocusReturn.a11y.browser.spec.ts +0 -97
  834. package/src/BottomSheet/__tests__/BottomSheetKeyboard.a11y.browser.spec.ts +0 -415
  835. package/src/BottomSheet/__tests__/BottomSheetNativeModal.a11y.browser.spec.ts +0 -462
  836. package/src/BottomSheet/__tests__/BottomSheetPinchZoom.a11y.chromium.spec.ts +0 -247
  837. package/src/BottomSheet/__tests__/BottomSheetToastOutlet.a11y.chromium.spec.ts +0 -83
  838. package/src/BottomSheet/snapOffsets.test.ts +0 -254
  839. package/src/BottomSheet/useSheetGestures.test.ts +0 -1638
  840. package/src/Breadcrumbs/Breadcrumbs.test.tsx +0 -921
  841. package/src/Breadcrumbs/__tests__/Breadcrumbs.a11y.chromium.spec.ts +0 -120
  842. package/src/Breadcrumbs/__tests__/Breadcrumbs.a11y.renders.tsx +0 -45
  843. package/src/Breadcrumbs/__tests__/Breadcrumbs.a11y.states.ts +0 -81
  844. package/src/Breadcrumbs/__tests__/Breadcrumbs.a11y.test.tsx +0 -125
  845. package/src/Button/Button.test.tsx +0 -864
  846. package/src/Button/__tests__/Button.a11y.chromium.spec.ts +0 -355
  847. package/src/Button/__tests__/Button.a11y.harnesses.tsx +0 -47
  848. package/src/Button/__tests__/Button.a11y.known-failures.ts +0 -25
  849. package/src/Button/__tests__/Button.a11y.renders.tsx +0 -139
  850. package/src/Button/__tests__/Button.a11y.states.ts +0 -317
  851. package/src/Button/__tests__/Button.a11y.test.tsx +0 -187
  852. package/src/ButtonGroup/ButtonGroup.test.tsx +0 -1025
  853. package/src/Calendar/Calendar.test.tsx +0 -2186
  854. package/src/Calendar/__tests__/Calendar.a11y.chromium.spec.ts +0 -127
  855. package/src/Calendar/__tests__/Calendar.a11y.known-failures.ts +0 -12
  856. package/src/Calendar/__tests__/Calendar.a11y.renders.tsx +0 -57
  857. package/src/Calendar/__tests__/Calendar.a11y.states.ts +0 -139
  858. package/src/Calendar/__tests__/Calendar.a11y.test.tsx +0 -137
  859. package/src/Calendar/dayCellUtils.test.ts +0 -520
  860. package/src/Calendar/getInitialFocusDate.test.ts +0 -94
  861. package/src/Calendar/getStandaloneShortWeekdayNames.test.ts +0 -54
  862. package/src/Calendar/hooks/useCalendarConstraints.test.ts +0 -153
  863. package/src/Calendar/hooks/useCalendarDays.test.ts +0 -265
  864. package/src/Calendar/hooks/useCalendarNavigation.test.ts +0 -252
  865. package/src/Card/Card.test.tsx +0 -202
  866. package/src/Carousel/Carousel.test.tsx +0 -915
  867. package/src/Carousel/__tests__/Carousel.a11y.chromium.spec.ts +0 -193
  868. package/src/Carousel/__tests__/Carousel.a11y.known-failures.ts +0 -27
  869. package/src/Carousel/__tests__/Carousel.a11y.renders.tsx +0 -39
  870. package/src/Carousel/__tests__/Carousel.a11y.states.ts +0 -139
  871. package/src/Carousel/__tests__/Carousel.a11y.test.tsx +0 -127
  872. package/src/Center/Center.test.tsx +0 -406
  873. package/src/Chat/ChatComposer.test.tsx +0 -364
  874. package/src/Chat/ChatComposerDrawer.test.tsx +0 -130
  875. package/src/Chat/ChatComposerInput.test.tsx +0 -2136
  876. package/src/Chat/ChatComposerTokenElement.test.tsx +0 -85
  877. package/src/Chat/ChatDictationButton.test.tsx +0 -108
  878. package/src/Chat/ChatLayout.test.tsx +0 -311
  879. package/src/Chat/ChatLayoutScrollButton.test.tsx +0 -99
  880. package/src/Chat/ChatMessage.test.tsx +0 -344
  881. package/src/Chat/ChatMessageBubble.test.tsx +0 -294
  882. package/src/Chat/ChatMessageList.test.tsx +0 -198
  883. package/src/Chat/ChatMessageMetadata.test.tsx +0 -66
  884. package/src/Chat/ChatPastedTextToken.test.tsx +0 -71
  885. package/src/Chat/ChatSendButton.test.tsx +0 -309
  886. package/src/Chat/ChatSystemMessage.test.tsx +0 -189
  887. package/src/Chat/ChatTokenizedText.test.tsx +0 -163
  888. package/src/Chat/ChatToolCalls.test.tsx +0 -413
  889. package/src/Chat/__tests__/ChatLayoutScrollButton.a11y.chromium.spec.ts +0 -577
  890. package/src/Chat/__tests__/ChatLayoutScrollButtonSurface.a11y.chromium.spec.ts +0 -740
  891. package/src/Chat/__tests__/ChatMessageBubble.a11y.chromium.spec.ts +0 -457
  892. package/src/Chat/__tests__/ChatMessageBubble.auditProvenance.test.ts +0 -91
  893. package/src/Chat/__tests__/ChatMessageBubble.auditProvenance.ts +0 -91
  894. package/src/Chat/__tests__/ChatMessageList.a11y.chromium.spec.ts +0 -465
  895. package/src/Chat/__tests__/ChatMessageMetadata.a11y.chromium.spec.ts +0 -416
  896. package/src/Chat/__tests__/ChatSendButton.a11y.chromium.spec.ts +0 -1523
  897. package/src/Chat/__tests__/ChatSystemMessage.a11y.chromium.spec.ts +0 -1004
  898. package/src/Chat/__tests__/ChatTokenizedText.a11y.chromium.spec.ts +0 -574
  899. package/src/Chat/__tests__/useChatStreamScroll.a11y.chromium.spec.ts +0 -263
  900. package/src/Chat/__tests__/useChatStreamScroll.fixture.tsx +0 -61
  901. package/src/Chat/useChatDictation.test.tsx +0 -369
  902. package/src/Chat/useChatNewMessages.test.tsx +0 -209
  903. package/src/Chat/useChatPasteAsToken.test.ts +0 -191
  904. package/src/Chat/useChatStreamScroll.test.tsx +0 -381
  905. package/src/Chat/useTriggerMenu.test.tsx +0 -994
  906. package/src/CheckboxInput/CheckboxInput.test.tsx +0 -926
  907. package/src/CheckboxInput/__tests__/Checkbox.a11y.chromium.spec.ts +0 -354
  908. package/src/CheckboxInput/__tests__/Checkbox.a11y.known-failures.ts +0 -54
  909. package/src/CheckboxInput/__tests__/Checkbox.a11y.renders.tsx +0 -310
  910. package/src/CheckboxInput/__tests__/Checkbox.a11y.states.ts +0 -404
  911. package/src/CheckboxInput/__tests__/Checkbox.a11y.test.tsx +0 -128
  912. package/src/CheckboxList/CheckboxList.fr10.test.tsx +0 -51
  913. package/src/CheckboxList/CheckboxList.test.tsx +0 -1159
  914. package/src/CheckboxList/__tests__/CheckboxList.a11y.chromium.spec.ts +0 -1998
  915. package/src/Citation/Citation.test.tsx +0 -361
  916. package/src/ClickableCard/ClickableCard.test.tsx +0 -286
  917. package/src/Code/Code.test.tsx +0 -75
  918. package/src/CodeBlock/CodeBlock.highlightStyles.test.tsx +0 -87
  919. package/src/CodeBlock/CodeBlock.test.tsx +0 -752
  920. package/src/CodeBlock/CodeLanguage.test.tsx +0 -173
  921. package/src/CodeBlock/__tests__/CodeBlockCopyClearance.a11y.chromium.spec.ts +0 -290
  922. package/src/CodeBlock/__tests__/CodeBlockLanguages.a11y.chromium.spec.ts +0 -64
  923. package/src/CodeBlock/__tests__/CodeBlockOverflowStop.a11y.chromium.spec.ts +0 -126
  924. package/src/CodeBlock/__tests__/DiffRowPaint.a11y.chromium.spec.ts +0 -94
  925. package/src/CodeBlock/highlightRanges.test.ts +0 -253
  926. package/src/CodeBlock/languages.test.ts +0 -402
  927. package/src/CodeBlock/tokenizer.test.ts +0 -280
  928. package/src/CodeBlock/tokenizerBudget.test.ts +0 -112
  929. package/src/Collapsible/Collapsible.test.tsx +0 -620
  930. package/src/Collapsible/CollapsibleGroup.test.tsx +0 -656
  931. package/src/Collapsible/__tests__/Collapsible.a11y.chromium.spec.ts +0 -183
  932. package/src/Collapsible/__tests__/Collapsible.a11y.states.ts +0 -117
  933. package/src/Collapsible/__tests__/Collapsible.a11y.test.tsx +0 -140
  934. package/src/Collapsible/__tests__/CollapsibleGroup.a11y.chromium.spec.ts +0 -559
  935. package/src/Collapsible/__tests__/CollapsibleGroup.a11y.renders.tsx +0 -45
  936. package/src/Collapsible/__tests__/CollapsibleGroup.a11y.states.ts +0 -139
  937. package/src/Collapsible/__tests__/CollapsibleGroup.a11y.test.tsx +0 -135
  938. package/src/Collapsible/__tests__/CollapsibleGroupAccordion.a11y.chromium.spec.ts +0 -129
  939. package/src/Collapsible/useCollapsible.test.tsx +0 -236
  940. package/src/CommandPalette/CommandPalette.perf.test.ts +0 -100
  941. package/src/CommandPalette/CommandPalette.test.tsx +0 -750
  942. package/src/CommandPalette/CommandPaletteEmpty.test.tsx +0 -101
  943. package/src/CommandPalette/CommandPaletteFooter.test.tsx +0 -122
  944. package/src/CommandPalette/CommandPaletteGroup.test.tsx +0 -129
  945. package/src/CommandPalette/CommandPaletteInput.test.tsx +0 -539
  946. package/src/CommandPalette/CommandPaletteItem.test.tsx +0 -417
  947. package/src/CommandPalette/CommandPaletteList.test.tsx +0 -184
  948. package/src/CommandPalette/__tests__/CommandPalette.a11y.chromium.spec.ts +0 -600
  949. package/src/CommandPalette/__tests__/CommandPaletteEmptyAudit.a11y.chromium.spec.ts +0 -715
  950. package/src/CommandPalette/__tests__/CommandPaletteFooter.a11y.chromium.spec.ts +0 -582
  951. package/src/CommandPalette/__tests__/CommandPaletteInputAudit.a11y.chromium.spec.ts +0 -336
  952. package/src/CommandPalette/__tests__/CommandPaletteItemAudit.a11y.chromium.spec.ts +0 -523
  953. package/src/ComplexSelector/ComplexSelector.focusableDisabled.test.tsx +0 -76
  954. package/src/ComplexSelector/ComplexSelector.fr10.test.tsx +0 -262
  955. package/src/ComplexSelector/ComplexSelector.test.tsx +0 -1023
  956. package/src/ComplexSelector/__tests__/ComplexSelector.a11y.chromium.spec.ts +0 -965
  957. package/src/ContextMenu/ContextMenu.test.tsx +0 -1582
  958. package/src/ContextMenu/ContextMenu.triggerProps.test.tsx +0 -54
  959. package/src/ContextMenu/__tests__/ContextMenuAudit.a11y.chromium.spec.ts +0 -439
  960. package/src/ContextMenu/__tests__/ContextMenuCursorAnchor.a11y.browser.spec.ts +0 -154
  961. package/src/ContextMenu/__tests__/ContextMenuLongPress.a11y.chromium.spec.ts +0 -117
  962. package/src/DateInput/DateInput.fr10.test.tsx +0 -283
  963. package/src/DateInput/DateInput.test.tsx +0 -1459
  964. package/src/DateInput/DateInputTouch.test.tsx +0 -2828
  965. package/src/DateInput/NativeDateField.test.tsx +0 -741
  966. package/src/DateInput/Presentation.test.tsx +0 -121
  967. package/src/DateInput/__tests__/DateInputAudit.a11y.chromium.spec.ts +0 -577
  968. package/src/DateInput/__tests__/WheelPinchZoom.a11y.chromium.spec.ts +0 -171
  969. package/src/DateRangeInput/DateRangeInput.fr10.test.tsx +0 -165
  970. package/src/DateRangeInput/DateRangeInput.test.tsx +0 -1108
  971. package/src/DateTimeInput/DateTimeInput.fr10.test.tsx +0 -319
  972. package/src/DateTimeInput/DateTimeInput.test.tsx +0 -2575
  973. package/src/DateTimeInput/DateTimeInputTouch.test.tsx +0 -1071
  974. package/src/DateTimeInput/NativePickerSegments.test.tsx +0 -657
  975. package/src/DateTimeInput/Presentation.test.tsx +0 -89
  976. package/src/Dialog/Dialog.source-build.test.mjs +0 -43
  977. package/src/Dialog/Dialog.test.tsx +0 -1385
  978. package/src/Dialog/DialogHeader.test.tsx +0 -477
  979. package/src/Dialog/__tests__/Dialog.a11y.browser.spec.ts +0 -111
  980. package/src/Dialog/__tests__/Dialog.a11y.chromium.spec.ts +0 -218
  981. package/src/Dialog/__tests__/Dialog.a11y.known-failures.ts +0 -28
  982. package/src/Dialog/__tests__/Dialog.a11y.renders.tsx +0 -107
  983. package/src/Dialog/__tests__/Dialog.a11y.states.ts +0 -104
  984. package/src/Dialog/__tests__/Dialog.a11y.test.tsx +0 -102
  985. package/src/Dialog/__tests__/Dialog.reduced-motion.a11y.browser.spec.ts +0 -252
  986. package/src/Dialog/useImperativeDialog.test.tsx +0 -78
  987. package/src/Divider/Divider.test.tsx +0 -155
  988. package/src/Drawer/Drawer.test.tsx +0 -1300
  989. package/src/Drawer/DrawerDirection.test.tsx +0 -127
  990. package/src/Drawer/DrawerHeader.test.tsx +0 -102
  991. package/src/Drawer/__tests__/DrawerAudit.a11y.chromium.spec.ts +0 -402
  992. package/src/Drawer/__tests__/DrawerHeaderAudit.a11y.chromium.spec.ts +0 -298
  993. package/src/DropdownMenu/DropdownMenu.ssr.test.tsx +0 -93
  994. package/src/DropdownMenu/DropdownMenu.test.tsx +0 -4340
  995. package/src/DropdownMenu/DropdownMenuContext.public.test.ts +0 -22
  996. package/src/DropdownMenu/DropdownMenuLazySheet.test.tsx +0 -126
  997. package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +0 -635
  998. package/src/DropdownMenu/DropdownMenuSheetFirstOpen.test.tsx +0 -72
  999. package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +0 -2408
  1000. package/src/DropdownMenu/__tests__/DropdownMenuDividerAudit.a11y.chromium.spec.ts +0 -281
  1001. package/src/DropdownMenu/__tests__/DropdownMenuDrillIn.a11y.chromium.spec.ts +0 -175
  1002. package/src/DropdownMenu/__tests__/DropdownMenuFocusReturn.a11y.browser.spec.ts +0 -74
  1003. package/src/DropdownMenu/__tests__/DropdownMenuGroupAudit.a11y.chromium.spec.ts +0 -335
  1004. package/src/DropdownMenu/__tests__/DropdownMenuItemAudit.a11y.chromium.spec.ts +0 -367
  1005. package/src/DropdownMenu/__tests__/DropdownMenuRadioGroupAudit.a11y.chromium.spec.ts +0 -328
  1006. package/src/DropdownMenu/__tests__/DropdownMenuRadioItemAudit.a11y.chromium.spec.ts +0 -540
  1007. package/src/DropdownMenu/__tests__/DropdownMenuSubMenuAudit.a11y.chromium.spec.ts +0 -671
  1008. package/src/DropdownMenu/__tests__/Menu.a11y.chromium.spec.ts +0 -146
  1009. package/src/DropdownMenu/__tests__/Menu.a11y.known-failures.ts +0 -26
  1010. package/src/DropdownMenu/__tests__/Menu.a11y.renders.tsx +0 -65
  1011. package/src/DropdownMenu/__tests__/Menu.a11y.states.ts +0 -138
  1012. package/src/DropdownMenu/__tests__/Menu.a11y.test.tsx +0 -136
  1013. package/src/DropdownMenu/__tests__/MenuButton.a11y.chromium.spec.ts +0 -137
  1014. package/src/DropdownMenu/__tests__/MenuButton.a11y.known-failures.ts +0 -12
  1015. package/src/DropdownMenu/__tests__/MenuButton.a11y.renders.tsx +0 -67
  1016. package/src/DropdownMenu/__tests__/MenuButton.a11y.states.ts +0 -141
  1017. package/src/DropdownMenu/__tests__/MenuButton.a11y.test.tsx +0 -138
  1018. package/src/DropdownMenu/__tests__/MenuHeaderFooter.a11y.chromium.spec.ts +0 -231
  1019. package/src/DropdownMenu/__tests__/MenuHoverFocus.a11y.chromium.spec.ts +0 -238
  1020. package/src/DropdownMenu/__tests__/MenuPinchZoom.a11y.chromium.spec.ts +0 -173
  1021. package/src/DropdownMenu/__tests__/MenuPress.a11y.chromium.spec.ts +0 -503
  1022. package/src/DropdownMenu/__tests__/SubMenuHeaderFooter.a11y.chromium.spec.ts +0 -268
  1023. package/src/DropdownMenu/__tests__/warmMenuBottomSheet.tsx +0 -43
  1024. package/src/EmptyState/EmptyState.announce.test.tsx +0 -227
  1025. package/src/EmptyState/EmptyState.test.tsx +0 -284
  1026. package/src/Field/Field.test.tsx +0 -800
  1027. package/src/Field/FieldLabel.test.tsx +0 -244
  1028. package/src/Field/InputClearButton.test.tsx +0 -395
  1029. package/src/Field/__tests__/FieldLabelAudit.a11y.chromium.spec.ts +0 -297
  1030. package/src/Field/fieldInputFamily.test.tsx +0 -160
  1031. package/src/FieldStatus/FieldStatus.test.tsx +0 -482
  1032. package/src/FieldStatus/__tests__/StatusMessage.a11y.chromium.spec.ts +0 -206
  1033. package/src/FieldStatus/__tests__/StatusMessage.a11y.known-failures.ts +0 -13
  1034. package/src/FieldStatus/__tests__/StatusMessage.a11y.renders.tsx +0 -287
  1035. package/src/FieldStatus/__tests__/StatusMessage.a11y.states.ts +0 -302
  1036. package/src/FieldStatus/__tests__/StatusMessage.a11y.test.tsx +0 -154
  1037. package/src/FileInput/FileInput.fr10.test.tsx +0 -21
  1038. package/src/FileInput/FileInput.test.tsx +0 -1270
  1039. package/src/FormLayout/FormLayout.test.tsx +0 -444
  1040. package/src/FormLayout/__snapshots__/FormLayout.test.tsx.snap +0 -46
  1041. package/src/Grid/Grid.test.tsx +0 -509
  1042. package/src/HStack/HStack.test.tsx +0 -129
  1043. package/src/Heading/Heading.test.tsx +0 -328
  1044. package/src/HoverCard/HoverCard.test.tsx +0 -1339
  1045. package/src/Icon/Icon.test.tsx +0 -990
  1046. package/src/Icon/globalIconRegistry.test.tsx +0 -613
  1047. package/src/IconButton/IconButton.test.tsx +0 -156
  1048. package/src/Indicator/Indicator.test.tsx +0 -389
  1049. package/src/Indicator/indicatorRegistry.test.tsx +0 -106
  1050. package/src/InputGroup/InputGroup.test.tsx +0 -502
  1051. package/src/InteractiveRoleContext/InteractiveRoleContext.test.tsx +0 -185
  1052. package/src/Item/Item.source-build.test.mjs +0 -88
  1053. package/src/Item/Item.swipeLayer.test.tsx +0 -74
  1054. package/src/Item/Item.test.tsx +0 -1455
  1055. package/src/Item/ItemMarkerColumns.a11y.chromium.spec.ts +0 -211
  1056. package/src/Item/__tests__/ItemSwipeActions.a11y.chromium.spec.ts +0 -337
  1057. package/src/Item/__tests__/ItemSwipePinchZoom.a11y.chromium.spec.ts +0 -186
  1058. package/src/Kbd/Kbd.test.tsx +0 -299
  1059. package/src/Layer/LayerProvider.test.tsx +0 -143
  1060. package/src/Layer/__tests__/LayerReducedMotion.a11y.chromium.spec.ts +0 -176
  1061. package/src/Layer/__tests__/TouchTrigger.a11y.chromium.spec.ts +0 -200
  1062. package/src/Layer/anchorName.test.ts +0 -146
  1063. package/src/Layer/layerDismissalFamilies.test.tsx +0 -487
  1064. package/src/Layer/layerDismissalInvariants.test.tsx +0 -501
  1065. package/src/Layer/layerHost.test.ts +0 -117
  1066. package/src/Layer/layerScopedContext.test.tsx +0 -499
  1067. package/src/Layer/layerViewportInset.source-build.test.mjs +0 -90
  1068. package/src/Layer/layerViewportInset.test.ts +0 -120
  1069. package/src/Layer/useLayer.test.tsx +0 -2423
  1070. package/src/Layer/useLayerDismissal.test.tsx +0 -580
  1071. package/src/Layer/useTouchTrigger.test.ts +0 -70
  1072. package/src/Layout/Layout.test.tsx +0 -375
  1073. package/src/Layout/LayoutSlots.test.tsx +0 -376
  1074. package/src/Layout/__tests__/Layout.a11y.chromium.spec.ts +0 -122
  1075. package/src/Layout/__tests__/Layout.a11y.renders.tsx +0 -110
  1076. package/src/Layout/__tests__/Layout.a11y.states.ts +0 -93
  1077. package/src/Layout/__tests__/Layout.a11y.test.tsx +0 -142
  1078. package/src/Layout/__tests__/childrenAsContent.test.tsx +0 -59
  1079. package/src/Layout/__tests__/contentWidth.test.tsx +0 -303
  1080. package/src/Layout/__tests__/edgeCompensation.test.tsx +0 -191
  1081. package/src/Layout/__tests__/nestedPadding.test.tsx +0 -264
  1082. package/src/Layout/overlayPaddingReset.test.tsx +0 -224
  1083. package/src/Lightbox/Lightbox.test.tsx +0 -819
  1084. package/src/Lightbox/LightboxKeyboard.a11y.browser.spec.ts +0 -78
  1085. package/src/Lightbox/LightboxViewport.a11y.browser.spec.ts +0 -70
  1086. package/src/Lightbox/useLightbox.test.tsx +0 -263
  1087. package/src/Link/Link.test.tsx +0 -457
  1088. package/src/Link/__tests__/Link.a11y.chromium.spec.ts +0 -143
  1089. package/src/Link/__tests__/Link.a11y.renders.tsx +0 -97
  1090. package/src/Link/__tests__/Link.a11y.states.ts +0 -104
  1091. package/src/Link/__tests__/Link.a11y.test.tsx +0 -127
  1092. package/src/Link/__tests__/Link.navigation.a11y.chromium.spec.ts +0 -262
  1093. package/src/Link/useLinkComponent.test.tsx +0 -572
  1094. package/src/Link/useLinkify.test.tsx +0 -146
  1095. package/src/List/List.test.tsx +0 -1052
  1096. package/src/List/ListItem.source-build.test.mjs +0 -41
  1097. package/src/List/ListMarkers.a11y.chromium.spec.ts +0 -904
  1098. package/src/Markdown/Markdown.codeLanguages.test.tsx +0 -137
  1099. package/src/Markdown/Markdown.fr23.bench.ts +0 -176
  1100. package/src/Markdown/Markdown.fr23.perf.test.ts +0 -484
  1101. package/src/Markdown/Markdown.helpers.perf.test.ts +0 -495
  1102. package/src/Markdown/Markdown.public.test.ts +0 -309
  1103. package/src/Markdown/Markdown.renderBoundary.test.tsx +0 -72
  1104. package/src/Markdown/Markdown.rerender.test.tsx +0 -143
  1105. package/src/Markdown/Markdown.test.tsx +0 -1383
  1106. package/src/Markdown/MarkdownTable.a11y.chromium.spec.ts +0 -727
  1107. package/src/Markdown/__tests__/MarkdownNesting.a11y.chromium.spec.ts +0 -102
  1108. package/src/Markdown/angleDestinations.test.tsx +0 -104
  1109. package/src/Markdown/ast.test.ts +0 -374
  1110. package/src/Markdown/atxHeadingIndentation.test.tsx +0 -53
  1111. package/src/Markdown/blockNesting.test.tsx +0 -124
  1112. package/src/Markdown/blockquoteMarkers.test.tsx +0 -69
  1113. package/src/Markdown/characterReferences.test.ts +0 -47
  1114. package/src/Markdown/codeSpans.test.tsx +0 -65
  1115. package/src/Markdown/emphasis.test.tsx +0 -214
  1116. package/src/Markdown/fenceInfo.test.tsx +0 -47
  1117. package/src/Markdown/imageAlt.test.tsx +0 -84
  1118. package/src/Markdown/incremental.test.ts +0 -1198
  1119. package/src/Markdown/incrementalIndentation.test.tsx +0 -36
  1120. package/src/Markdown/indentedFences.test.tsx +0 -374
  1121. package/src/Markdown/indentedTables.test.tsx +0 -53
  1122. package/src/Markdown/linkBrackets.test.tsx +0 -116
  1123. package/src/Markdown/linkDestinationLinear.test.tsx +0 -54
  1124. package/src/Markdown/linkDestinations.test.tsx +0 -127
  1125. package/src/Markdown/linkTextCode.test.tsx +0 -65
  1126. package/src/Markdown/listBullets.test.tsx +0 -55
  1127. package/src/Markdown/listContinuation.test.tsx +0 -94
  1128. package/src/Markdown/listThematicBreak.test.tsx +0 -49
  1129. package/src/Markdown/longLinkLabels.test.tsx +0 -49
  1130. package/src/Markdown/openFenceFinalNewline.test.tsx +0 -85
  1131. package/src/Markdown/parser.perf.test.ts +0 -398
  1132. package/src/Markdown/parser.public.test.ts +0 -144
  1133. package/src/Markdown/parser.test.ts +0 -1925
  1134. package/src/Markdown/plugin-renderer/MarkdownPluginNodeRenderer.test.tsx +0 -300
  1135. package/src/Markdown/plugins/frontmatter.test.tsx +0 -150
  1136. package/src/Markdown/plugins/headingLinks.a11y.chromium.spec.ts +0 -856
  1137. package/src/Markdown/plugins/headingLinks.ssr.test.tsx +0 -143
  1138. package/src/Markdown/plugins/headingLinks.test.tsx +0 -637
  1139. package/src/Markdown/plugins/protocol.test.tsx +0 -1083
  1140. package/src/Markdown/plugins/semanticFence.test.tsx +0 -655
  1141. package/src/Markdown/plugins/softBreaks.test.tsx +0 -172
  1142. package/src/Markdown/plugins/sourceDecoration.test.tsx +0 -799
  1143. package/src/Markdown/plugins/sourceLines.test.tsx +0 -715
  1144. package/src/Markdown/plugins/textTransform.test.ts +0 -467
  1145. package/src/Markdown/remark.test.tsx +0 -1877
  1146. package/src/Markdown/streaming.test.ts +0 -148
  1147. package/src/Markdown/streamingListShapes.test.tsx +0 -93
  1148. package/src/Markdown/trailingSpaces.test.tsx +0 -45
  1149. package/src/MetadataList/MetadataList.test.tsx +0 -383
  1150. package/src/MobileNav/MobileNav.test.tsx +0 -281
  1151. package/src/MobileNav/MobileNavCloseEdgeCases.test.tsx +0 -584
  1152. package/src/MobileNav/MobileNavCloseTiming.test.ts +0 -58
  1153. package/src/MobileNav/MobileNavCloseVisibility.test.tsx +0 -172
  1154. package/src/MobileNav/MobileNavEntryAnimation.test.tsx +0 -212
  1155. package/src/MobileNav/MobileNavFocus.test.tsx +0 -553
  1156. package/src/MobileNav/MobileNavPinchZoom.a11y.chromium.spec.ts +0 -147
  1157. package/src/MobileNav/MobileNavReopen.test.tsx +0 -118
  1158. package/src/MobileNav/MobileNavScrollbarGutter.test.tsx +0 -109
  1159. package/src/MobileNav/MobileNavToggle.test.tsx +0 -179
  1160. package/src/MobileNav/__tests__/MobileNavWidth.a11y.chromium.spec.ts +0 -103
  1161. package/src/MoreMenu/MoreMenu.test.tsx +0 -356
  1162. package/src/MultiSelector/CreateRow.a11y.chromium.spec.ts +0 -149
  1163. package/src/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.ts +0 -72
  1164. package/src/MultiSelector/MultiSelector.focusableDisabled.test.tsx +0 -121
  1165. package/src/MultiSelector/MultiSelector.fr10.test.tsx +0 -221
  1166. package/src/MultiSelector/MultiSelector.test.tsx +0 -4702
  1167. package/src/MultiSelector/OptionActions.a11y.chromium.spec.ts +0 -310
  1168. package/src/MultiSelector/__tests__/MultiSelector.a11y.chromium.spec.ts +0 -46
  1169. package/src/MultiSelector/__tests__/MultiSelector.listbox.a11y.test.tsx +0 -128
  1170. package/src/NavIcon/NavIcon.test.tsx +0 -41
  1171. package/src/NavMenu/NavHeadingMenu.test.tsx +0 -714
  1172. package/src/NavMenu/__tests__/NavMenusSlideSelection.a11y.chromium.spec.ts +0 -137
  1173. package/src/NumberInput/NumberInput.fr10.test.tsx +0 -21
  1174. package/src/NumberInput/NumberInput.test.tsx +0 -2636
  1175. package/src/NumberInput/__tests__/NumberInput.a11y.chromium.spec.ts +0 -124
  1176. package/src/NumberInput/__tests__/NumberInput.a11y.known-failures.ts +0 -26
  1177. package/src/NumberInput/__tests__/NumberInput.a11y.renders.tsx +0 -77
  1178. package/src/NumberInput/__tests__/NumberInput.a11y.states.ts +0 -116
  1179. package/src/NumberInput/__tests__/NumberInput.a11y.test.tsx +0 -154
  1180. package/src/NumberInput/numberInputCommit.test.ts +0 -72
  1181. package/src/NumberInput/numberParser.docblock.test.ts +0 -234
  1182. package/src/NumberInput/numberParser.test.ts +0 -222
  1183. package/src/Outline/Outline.test.tsx +0 -1069
  1184. package/src/Outline/OutlineFocus.a11y.chromium.spec.ts +0 -551
  1185. package/src/Outline/parseOutlineFromMarkdown.test.ts +0 -104
  1186. package/src/OverflowList/OverflowList.test.tsx +0 -1073
  1187. package/src/Overlay/Overlay.test.tsx +0 -424
  1188. package/src/Pagination/Pagination.test.tsx +0 -1754
  1189. package/src/Popover/Popover.test.tsx +0 -1424
  1190. package/src/Popover/PopoverPresentation.test.tsx +0 -952
  1191. package/src/Popover/__tests__/PopoverSecondDocument.a11y.chromium.spec.ts +0 -205
  1192. package/src/PowerSearch/PowerSearch.test.tsx +0 -1003
  1193. package/src/PowerSearch/PowerSearchEditPopover.test.tsx +0 -1190
  1194. package/src/PowerSearch/PowerSearchFilterEditor.test.tsx +0 -377
  1195. package/src/PowerSearch/PowerSearchToken.test.tsx +0 -230
  1196. package/src/PowerSearch/PowerSearchValueEditor.test.tsx +0 -754
  1197. package/src/PowerSearch/formatFilterValue.test.ts +0 -339
  1198. package/src/PowerSearch/resolveDateTimeRangePart.test.ts +0 -40
  1199. package/src/PowerSearch/usePowerSearchConfig.test.ts +0 -832
  1200. package/src/PowerSearch/usePowerSearchSource.test.ts +0 -621
  1201. package/src/ProgressBar/ProgressBar.test.tsx +0 -852
  1202. package/src/ProgressBar/__tests__/ProgressBar.a11y.chromium.spec.ts +0 -115
  1203. package/src/ProgressBar/__tests__/ProgressBar.a11y.known-failures.ts +0 -12
  1204. package/src/ProgressBar/__tests__/ProgressBar.a11y.renders.tsx +0 -38
  1205. package/src/ProgressBar/__tests__/ProgressBar.a11y.states.ts +0 -123
  1206. package/src/ProgressBar/__tests__/ProgressBar.a11y.test.tsx +0 -107
  1207. package/src/RadioList/RadioList.fr10.test.tsx +0 -67
  1208. package/src/RadioList/RadioList.test.tsx +0 -967
  1209. package/src/RadioList/RadioListItem.press.source-build.test.mjs +0 -80
  1210. package/src/RadioList/__tests__/RadioGroup.a11y.chromium.spec.ts +0 -274
  1211. package/src/RadioList/__tests__/RadioGroup.a11y.known-failures.ts +0 -12
  1212. package/src/RadioList/__tests__/RadioGroup.a11y.renders.tsx +0 -232
  1213. package/src/RadioList/__tests__/RadioGroup.a11y.states.ts +0 -503
  1214. package/src/RadioList/__tests__/RadioGroup.a11y.test.tsx +0 -217
  1215. package/src/Resizable/ResizeHandle.test.tsx +0 -1067
  1216. package/src/Resizable/__tests__/ResizeHandle.a11y.chromium.spec.ts +0 -120
  1217. package/src/Resizable/__tests__/ResizeHandle.a11y.known-failures.ts +0 -39
  1218. package/src/Resizable/__tests__/ResizeHandle.a11y.renders.tsx +0 -80
  1219. package/src/Resizable/__tests__/ResizeHandle.a11y.states.ts +0 -197
  1220. package/src/Resizable/__tests__/ResizeHandle.a11y.test.tsx +0 -123
  1221. package/src/Resizable/shippedDirections.test.ts +0 -462
  1222. package/src/Resizable/useResizable.test.ts +0 -1682
  1223. package/src/Resizable/utils.test.ts +0 -74
  1224. package/src/ScrollableArea/ScrollableArea.test.tsx +0 -304
  1225. package/src/Section/Section.test.tsx +0 -376
  1226. package/src/SegmentedControl/SegmentedControl.test.tsx +0 -897
  1227. package/src/SelectableCard/SelectableCard.test.tsx +0 -216
  1228. package/src/Selector/Selector.focusableDisabled.test.tsx +0 -127
  1229. package/src/Selector/Selector.fr10.test.tsx +0 -149
  1230. package/src/Selector/Selector.sheetActiveDescendant.test.tsx +0 -95
  1231. package/src/Selector/Selector.source-build.test.mjs +0 -79
  1232. package/src/Selector/Selector.test.tsx +0 -5028
  1233. package/src/Selector/__tests__/Combobox.a11y.known-failures.ts +0 -27
  1234. package/src/Selector/__tests__/Combobox.a11y.states.ts +0 -145
  1235. package/src/Selector/__tests__/ComboboxField.a11y.chromium.spec.ts +0 -458
  1236. package/src/Selector/__tests__/EmptyStateAnnouncement.a11y.chromium.spec.ts +0 -91
  1237. package/src/Selector/__tests__/Listbox.a11y.chromium.spec.ts +0 -131
  1238. package/src/Selector/__tests__/Listbox.a11y.dom.ts +0 -38
  1239. package/src/Selector/__tests__/Listbox.a11y.inventory.test.ts +0 -63
  1240. package/src/Selector/__tests__/Listbox.a11y.known-failures.ts +0 -12
  1241. package/src/Selector/__tests__/Listbox.a11y.states.ts +0 -232
  1242. package/src/Selector/__tests__/Selector.a11y.chromium.spec.ts +0 -46
  1243. package/src/Selector/__tests__/Selector.combobox.a11y.chromium.spec.ts +0 -109
  1244. package/src/Selector/__tests__/Selector.combobox.a11y.test.tsx +0 -126
  1245. package/src/Selector/__tests__/Selector.listbox.a11y.test.tsx +0 -121
  1246. package/src/Selector/useCombobox.public.test.tsx +0 -89
  1247. package/src/Selector/useCombobox.test.tsx +0 -444
  1248. package/src/SideNav/SideNav.test.tsx +0 -4128
  1249. package/src/SideNav/SideNavRailMotion.test.tsx +0 -620
  1250. package/src/SideNav/__tests__/SideNavRailMotion.interaction.browser.spec.ts +0 -417
  1251. package/src/SizeContext/SizeContext.test.tsx +0 -226
  1252. package/src/Skeleton/Skeleton.test.tsx +0 -148
  1253. package/src/Slider/Slider.fr10.test.tsx +0 -93
  1254. package/src/Slider/Slider.test.tsx +0 -1630
  1255. package/src/Slider/__tests__/Slider.a11y.chromium.spec.ts +0 -113
  1256. package/src/Slider/__tests__/Slider.a11y.known-failures.ts +0 -12
  1257. package/src/Slider/__tests__/Slider.a11y.renders.tsx +0 -42
  1258. package/src/Slider/__tests__/Slider.a11y.states.ts +0 -106
  1259. package/src/Slider/__tests__/Slider.a11y.test.tsx +0 -105
  1260. package/src/Spinner/Spinner.test.tsx +0 -484
  1261. package/src/Stack/Stack.test.tsx +0 -473
  1262. package/src/Stack/StackItem.test.tsx +0 -105
  1263. package/src/StatusDot/StatusDot.test.tsx +0 -213
  1264. package/src/Stepper/Stepper.public.test.ts +0 -91
  1265. package/src/Stepper/Stepper.test.tsx +0 -2068
  1266. package/src/Switch/Switch.focusRing.source-build.test.mjs +0 -81
  1267. package/src/Switch/Switch.test.tsx +0 -1071
  1268. package/src/Switch/__tests__/Switch.a11y.chromium.spec.ts +0 -211
  1269. package/src/Switch/__tests__/Switch.a11y.known-failures.ts +0 -28
  1270. package/src/Switch/__tests__/Switch.a11y.states.ts +0 -275
  1271. package/src/Switch/__tests__/Switch.a11y.test.tsx +0 -144
  1272. package/src/TabList/TabList.test.tsx +0 -1701
  1273. package/src/TabList/__tests__/TabListOverflow.interaction.browser.spec.ts +0 -99
  1274. package/src/TabList/__tests__/Tabs.a11y.chromium.spec.ts +0 -199
  1275. package/src/TabList/__tests__/Tabs.a11y.known-failures.ts +0 -12
  1276. package/src/TabList/__tests__/Tabs.a11y.renders.tsx +0 -92
  1277. package/src/TabList/__tests__/Tabs.a11y.states.ts +0 -247
  1278. package/src/TabList/__tests__/Tabs.a11y.test.tsx +0 -153
  1279. package/src/Table/Table.perf.test.tsx +0 -569
  1280. package/src/Table/Table.test.tsx +0 -1851
  1281. package/src/Table/TableHeaderCell.test.tsx +0 -291
  1282. package/src/Table/TableSelectionToolbar.test.tsx +0 -109
  1283. package/src/Table/__tests__/Table.a11y.chromium.spec.ts +0 -129
  1284. package/src/Table/__tests__/Table.a11y.contracts.ts +0 -35
  1285. package/src/Table/__tests__/Table.a11y.renders.tsx +0 -112
  1286. package/src/Table/__tests__/Table.a11y.states.ts +0 -140
  1287. package/src/Table/__tests__/Table.a11y.test.tsx +0 -198
  1288. package/src/Table/__tests__/TableMinWidth.a11y.chromium.spec.ts +0 -75
  1289. package/src/Table/__tests__/TableRowGlide.a11y.browser.spec.ts +0 -382
  1290. package/src/Table/__tests__/TableScroll.a11y.chromium.spec.ts +0 -138
  1291. package/src/Table/__tests__/TableStickyDividers.a11y.browser.spec.ts +0 -560
  1292. package/src/Table/plugins/columnResize/useTableColumnResize.test.tsx +0 -919
  1293. package/src/Table/plugins/columnSettings/useTableColumnSettings.test.tsx +0 -175
  1294. package/src/Table/plugins/columnSettings/useTableColumnSettingsState.test.tsx +0 -296
  1295. package/src/Table/plugins/filtering/useTableFilterState.test.tsx +0 -143
  1296. package/src/Table/plugins/filtering/useTableFiltering.test.tsx +0 -357
  1297. package/src/Table/plugins/groupedRows/useTableGroupedRows-perf.test.tsx +0 -112
  1298. package/src/Table/plugins/groupedRows/useTableGroupedRows.test.tsx +0 -409
  1299. package/src/Table/plugins/pagination/useTablePagination-perf.test.tsx +0 -232
  1300. package/src/Table/plugins/pagination/useTablePagination.test.tsx +0 -666
  1301. package/src/Table/plugins/rowExpansion/useTableRowExpansion.glide.test.tsx +0 -280
  1302. package/src/Table/plugins/rowExpansion/useTableRowExpansion.test.tsx +0 -681
  1303. package/src/Table/plugins/rowIndex/useTableRowIndex.test.tsx +0 -132
  1304. package/src/Table/plugins/rowStatus/useTableRowStatus.public.test.ts +0 -92
  1305. package/src/Table/plugins/rowStatus/useTableRowStatus.test.tsx +0 -519
  1306. package/src/Table/plugins/selection/useTableSelection-perf.test.tsx +0 -258
  1307. package/src/Table/plugins/selection/useTableSelection.test.tsx +0 -424
  1308. package/src/Table/plugins/selection/useTableSelectionState.test.tsx +0 -451
  1309. package/src/Table/plugins/sortable/__tests__/useTableSortable.a11y.test.tsx +0 -174
  1310. package/src/Table/plugins/sortable/__tests__/useTableSortable.a11y.ts +0 -86
  1311. package/src/Table/plugins/sortable/useTableSortable.test.tsx +0 -857
  1312. package/src/Table/plugins/sortable/useTableSortableState.test.tsx +0 -592
  1313. package/src/Table/plugins/stickyColumns/useTableStickyColumns.test.tsx +0 -334
  1314. package/src/Table/plugins/tree/useTableTreeData-perf.test.tsx +0 -163
  1315. package/src/Table/plugins/tree/useTableTreeData.test.tsx +0 -992
  1316. package/src/Table/plugins/tree/useTableTreeState.test.tsx +0 -740
  1317. package/src/Table/tableContextMenu.test.tsx +0 -208
  1318. package/src/Text/Text.test.tsx +0 -405
  1319. package/src/Text/useTruncation.test.tsx +0 -69
  1320. package/src/TextArea/TextArea.fr10.test.tsx +0 -21
  1321. package/src/TextArea/TextArea.test.tsx +0 -1257
  1322. package/src/TextInput/TextInput.fr10.test.tsx +0 -21
  1323. package/src/TextInput/TextInput.test.tsx +0 -1259
  1324. package/src/TextInput/__tests__/TextInput.a11y.chromium.spec.ts +0 -214
  1325. package/src/TextInput/__tests__/TextInput.a11y.known-failures.ts +0 -40
  1326. package/src/TextInput/__tests__/TextInput.a11y.renders.tsx +0 -202
  1327. package/src/TextInput/__tests__/TextInput.a11y.states.ts +0 -384
  1328. package/src/TextInput/__tests__/TextInput.a11y.test.tsx +0 -130
  1329. package/src/Thumbnail/Thumbnail.test.tsx +0 -384
  1330. package/src/TimeInput/NativeTimeInput.test.tsx +0 -326
  1331. package/src/TimeInput/Presentation.test.tsx +0 -171
  1332. package/src/TimeInput/TimeInput.fr10.test.tsx +0 -312
  1333. package/src/TimeInput/TimeInput.test.tsx +0 -831
  1334. package/src/Timer/Timer.test.tsx +0 -302
  1335. package/src/Timestamp/Timestamp.test.tsx +0 -1636
  1336. package/src/Timestamp/tooltipEntries.test.ts +0 -566
  1337. package/src/Toast/ToastViewport.modalHost.test.tsx +0 -891
  1338. package/src/Toast/ToastViewport.test.tsx +0 -2333
  1339. package/src/Toast/useToast.test.tsx +0 -258
  1340. package/src/ToggleButton/ToggleButton.test.tsx +0 -973
  1341. package/src/ToggleButton/__tests__/ToggleButton.a11y.chromium.spec.ts +0 -177
  1342. package/src/ToggleButton/__tests__/ToggleButton.a11y.renders.tsx +0 -133
  1343. package/src/ToggleButton/__tests__/ToggleButton.a11y.states.ts +0 -167
  1344. package/src/ToggleButton/__tests__/ToggleButton.a11y.test.tsx +0 -103
  1345. package/src/Token/Token.test.tsx +0 -640
  1346. package/src/Token/__tests__/TokenRemoveTarget.a11y.chromium.spec.ts +0 -87
  1347. package/src/Tokenizer/Tokenizer.fr10.test.tsx +0 -28
  1348. package/src/Tokenizer/Tokenizer.props-type.test.tsx +0 -126
  1349. package/src/Tokenizer/Tokenizer.ssr.test.tsx +0 -196
  1350. package/src/Tokenizer/Tokenizer.test.tsx +0 -2121
  1351. package/src/Tokenizer/TokenizerBottomSheet.test.tsx +0 -2719
  1352. package/src/Tokenizer/__tests__/TokenizerEndLane.a11y.chromium.spec.ts +0 -505
  1353. package/src/Toolbar/Toolbar.test.tsx +0 -570
  1354. package/src/Toolbar/__tests__/Toolbar.a11y.chromium.spec.ts +0 -164
  1355. package/src/Toolbar/__tests__/Toolbar.a11y.renders.tsx +0 -63
  1356. package/src/Toolbar/__tests__/Toolbar.a11y.states.ts +0 -142
  1357. package/src/Toolbar/__tests__/Toolbar.a11y.test.tsx +0 -126
  1358. package/src/Tooltip/Tooltip.test.tsx +0 -819
  1359. package/src/Tooltip/__tests__/Tooltip.a11y.chromium.spec.ts +0 -130
  1360. package/src/Tooltip/__tests__/Tooltip.a11y.renders.tsx +0 -86
  1361. package/src/Tooltip/__tests__/Tooltip.a11y.states.ts +0 -132
  1362. package/src/Tooltip/__tests__/Tooltip.a11y.test.tsx +0 -126
  1363. package/src/Tooltip/__tests__/TooltipFocusReturn.a11y.chromium.spec.ts +0 -130
  1364. package/src/Tooltip/__tests__/TooltipHoverBridge.a11y.chromium.spec.ts +0 -75
  1365. package/src/Tooltip/__tests__/TooltipTallContent.a11y.browser.spec.ts +0 -673
  1366. package/src/Tooltip/__tests__/TooltipTallContentTracking.a11y.browser.spec.ts +0 -1162
  1367. package/src/Tooltip/useTooltip.test.tsx +0 -483
  1368. package/src/TopNav/TopNav.test.tsx +0 -836
  1369. package/src/TopNav/TopNavMegaMenu.reduced-motion.source-build.test.mjs +0 -83
  1370. package/src/TopNav/TopNavMegaMenu.test.tsx +0 -1132
  1371. package/src/TopNav/TopNavMenu.test.tsx +0 -485
  1372. package/src/TopNav/__tests__/TopNavMobileLongHeading.a11y.chromium.spec.ts +0 -75
  1373. package/src/TreeList/TreeList.test.tsx +0 -1711
  1374. package/src/TreeList/__tests__/TreeList.a11y.chromium.spec.ts +0 -138
  1375. package/src/TreeList/__tests__/TreeList.a11y.renders.tsx +0 -44
  1376. package/src/TreeList/__tests__/TreeList.a11y.states.ts +0 -164
  1377. package/src/TreeList/__tests__/TreeList.a11y.test.tsx +0 -138
  1378. package/src/TreeList/__tests__/TreeListGuideAlignment.interaction.browser.spec.ts +0 -127
  1379. package/src/TreeList/__tests__/TreeListRowNames.a11y.chromium.spec.ts +0 -83
  1380. package/src/Typeahead/BaseTypeahead.emptyState.a11y.chromium.spec.ts +0 -138
  1381. package/src/Typeahead/BaseTypeahead.test.tsx +0 -553
  1382. package/src/Typeahead/Typeahead.fr10.test.tsx +0 -28
  1383. package/src/Typeahead/Typeahead.test.tsx +0 -2824
  1384. package/src/Typeahead/TypeaheadItem.test.tsx +0 -134
  1385. package/src/VStack/VStack.test.tsx +0 -128
  1386. package/src/VisuallyHidden/VisuallyHidden.test.tsx +0 -115
  1387. package/src/__tests__/BusyOwnerAnnouncement.test.tsx +0 -853
  1388. package/src/__tests__/CardPress.a11y.chromium.spec.ts +0 -353
  1389. package/src/__tests__/DisabledTriggerFocus.a11y.chromium.spec.ts +0 -87
  1390. package/src/__tests__/PressModality.a11y.chromium.spec.ts +0 -392
  1391. package/src/__tests__/PressedState.a11y.chromium.spec.ts +0 -814
  1392. package/src/__tests__/TestIcon.tsx +0 -26
  1393. package/src/__tests__/babelPluginAddExtensions.test.ts +0 -190
  1394. package/src/__tests__/fastRoleQueries.ts +0 -66
  1395. package/src/__tests__/forcedColors.ts +0 -60
  1396. package/src/__tests__/pressState.ts +0 -212
  1397. package/src/__tests__/stubMatchMedia.test.ts +0 -79
  1398. package/src/__tests__/stubMatchMedia.ts +0 -95
  1399. package/src/__tests__/stylexDeclarations.ts +0 -51
  1400. package/src/__tests__/unthemedLength.ts +0 -34
  1401. package/src/docPropLiterals.test.ts +0 -964
  1402. package/src/docPropReferences.test.ts +0 -178
  1403. package/src/docs-script.test.mjs +0 -55
  1404. package/src/gestures/__tests__/LongPressGestureRecognizer.test.ts +0 -141
  1405. package/src/gestures/__tests__/PanGestureRecognizer.test.ts +0 -327
  1406. package/src/gestures/__tests__/TapGestureRecognizer.test.ts +0 -154
  1407. package/src/gestures/__tests__/VelocityTracker.streams.test.ts +0 -243
  1408. package/src/gestures/__tests__/VelocityTracker.test.ts +0 -261
  1409. package/src/gestures/__tests__/arena.test.ts +0 -824
  1410. package/src/gestures/__tests__/arenaContinuous.test.ts +0 -207
  1411. package/src/gestures/__tests__/arenaTapLongPress.test.ts +0 -211
  1412. package/src/gestures/__tests__/fixtures/pointer-streams/README.md +0 -82
  1413. package/src/gestures/__tests__/fixtures/pointer-streams/android-chromium-fling.json +0 -130
  1414. package/src/gestures/__tests__/fixtures/pointer-streams/android-chromium-slow.json +0 -860
  1415. package/src/gestures/__tests__/fixtures/pointer-streams/android-chromium-stop-release.json +0 -357
  1416. package/src/gestures/__tests__/fixtures/pointer-streams/android-firefox-fling.json +0 -66
  1417. package/src/gestures/__tests__/fixtures/pointer-streams/android-firefox-slow.json +0 -268
  1418. package/src/gestures/__tests__/fixtures/pointer-streams/android-firefox-stop-release.json +0 -128
  1419. package/src/gestures/__tests__/fixtures/pointer-streams/capture.html +0 -123
  1420. package/src/gestures/__tests__/fixtures/pointer-streams/desktop-chromium-mouse-fling.json +0 -64
  1421. package/src/gestures/__tests__/fixtures/pointer-streams/desktop-chromium-mouse-slow.json +0 -268
  1422. package/src/gestures/__tests__/fixtures/pointer-streams/desktop-chromium-mouse-stop-release.json +0 -124
  1423. package/src/gestures/__tests__/fixtures/pointer-streams/desktop-chromium-pen-fling.json +0 -64
  1424. package/src/gestures/__tests__/fixtures/pointer-streams/desktop-chromium-pen-slow.json +0 -268
  1425. package/src/gestures/__tests__/fixtures/pointer-streams/desktop-chromium-pen-stop-release.json +0 -124
  1426. package/src/gestures/__tests__/fixtures/pointer-streams/desktop-chromium-touch-fling.json +0 -84
  1427. package/src/gestures/__tests__/fixtures/pointer-streams/desktop-chromium-touch-slow.json +0 -381
  1428. package/src/gestures/__tests__/fixtures/pointer-streams/desktop-chromium-touch-stop-release.json +0 -171
  1429. package/src/gestures/__tests__/fixtures/pointer-streams/ios-safari-fling.json +0 -45
  1430. package/src/gestures/__tests__/fixtures/pointer-streams/ios-safari-slow.json +0 -101
  1431. package/src/gestures/__tests__/fixtures/pointer-streams/ios-safari-stop-release.json +0 -130
  1432. package/src/gestures/__tests__/harness.ts +0 -204
  1433. package/src/hooks/__tests__/MenuPressShadowRoot.a11y.chromium.spec.ts +0 -144
  1434. package/src/hooks/__tests__/menuLinkClicks.ts +0 -71
  1435. package/src/hooks/actionGroupTracker.exports.test.ts +0 -28
  1436. package/src/hooks/actionGroupTracker.test.ts +0 -510
  1437. package/src/hooks/computeOverflow.test.ts +0 -422
  1438. package/src/hooks/deprecatedApis.public.test.ts +0 -33
  1439. package/src/hooks/menuPressGesture.test.ts +0 -253
  1440. package/src/hooks/scrollKeyboardDelegation.test.ts +0 -155
  1441. package/src/hooks/scrollbarGutter.test.ts +0 -167
  1442. package/src/hooks/useAnnounce.modal.test.tsx +0 -1291
  1443. package/src/hooks/useAnnounce.test.tsx +0 -293
  1444. package/src/hooks/useAnnounceRenderedText.adopters.test.tsx +0 -226
  1445. package/src/hooks/useAutoMediaMode.test.ts +0 -109
  1446. package/src/hooks/useBusyAnnouncement.adopters.test.tsx +0 -69
  1447. package/src/hooks/useBusyAnnouncement.test.tsx +0 -112
  1448. package/src/hooks/useClickableContainer.test.tsx +0 -236
  1449. package/src/hooks/useClipboard.test.tsx +0 -151
  1450. package/src/hooks/useContainerReveal.test.tsx +0 -163
  1451. package/src/hooks/useDevWarning.test.tsx +0 -45
  1452. package/src/hooks/useEntryAnimation.ssr.test.tsx +0 -143
  1453. package/src/hooks/useFocusTrap.test.tsx +0 -635
  1454. package/src/hooks/useFocusTrapEscapeShim.test.tsx +0 -166
  1455. package/src/hooks/useGridFocus.test.tsx +0 -165
  1456. package/src/hooks/useHotkeys.test.ts +0 -265
  1457. package/src/hooks/useImageMode.test.ts +0 -133
  1458. package/src/hooks/useInputContainer.test.tsx +0 -180
  1459. package/src/hooks/useInputStatusIcon.test.tsx +0 -240
  1460. package/src/hooks/useIsHydrating.test.tsx +0 -70
  1461. package/src/hooks/useKeyboardHint.test.tsx +0 -92
  1462. package/src/hooks/useListFocus.test.tsx +0 -664
  1463. package/src/hooks/useLongPress.test.tsx +0 -152
  1464. package/src/hooks/useMediaQuery.test.ts +0 -165
  1465. package/src/hooks/useMenuHover.test.tsx +0 -536
  1466. package/src/hooks/useMenuPress.test.tsx +0 -565
  1467. package/src/hooks/useMergedRefs.test.tsx +0 -62
  1468. package/src/hooks/useOverflow.test.ts +0 -504
  1469. package/src/hooks/useRenamedProp.test.tsx +0 -95
  1470. package/src/hooks/useScrollLock.test.ts +0 -248
  1471. package/src/hooks/useScrollOverflow.test.tsx +0 -222
  1472. package/src/hooks/useScrollableArea.test.tsx +0 -836
  1473. package/src/hooks/useStreamingText.test.ts +0 -237
  1474. package/src/hooks/useTreeFocus.test.tsx +0 -351
  1475. package/src/hooks/useTypeahead.test.tsx +0 -351
  1476. package/src/i18n/__tests__/InternationalizationProvider.test.tsx +0 -128
  1477. package/src/i18n/__tests__/e2e-pagination.test.tsx +0 -172
  1478. package/src/i18n/__tests__/e2e-powersearch.test.tsx +0 -166
  1479. package/src/i18n/__tests__/getLocaleDirection.test.ts +0 -40
  1480. package/src/i18n/__tests__/resolve.test.ts +0 -325
  1481. package/src/i18n/__tests__/useCollator.test.tsx +0 -115
  1482. package/src/i18n/__tests__/useDirection.test.tsx +0 -66
  1483. package/src/i18n/__tests__/useLocale.test.tsx +0 -52
  1484. package/src/i18n/useTranslator.test.tsx +0 -267
  1485. package/src/inputFontFloor.test.ts +0 -106
  1486. package/src/internal/tokenizer-presentation.test.ts +0 -29
  1487. package/src/internal/touch.test.ts +0 -44
  1488. package/src/motion/__tests__/animatedLayout.interaction.browser.spec.ts +0 -1613
  1489. package/src/motion/__tests__/animatedLayoutAdopters.interaction.browser.spec.ts +0 -217
  1490. package/src/motion/__tests__/animatedLayoutBundle.ts +0 -17
  1491. package/src/motion/animatedLayout.boundary.test.ts +0 -155
  1492. package/src/motion/animatedLayout.swap.test.ts +0 -827
  1493. package/src/motion/animatedLayout.test.ts +0 -1154
  1494. package/src/motion/animatedLayoutAdopters.test.tsx +0 -500
  1495. package/src/motion/animator.test.ts +0 -2653
  1496. package/src/motion/bezier.test.ts +0 -52
  1497. package/src/motion/coast.test.ts +0 -99
  1498. package/src/motion/decay.test.ts +0 -120
  1499. package/src/motion/projection.test.ts +0 -107
  1500. package/src/motion/reducedMotion.test.ts +0 -129
  1501. package/src/motion/reducedMotionPlan.test.ts +0 -182
  1502. package/src/motion/resolveSnapPoint.test.ts +0 -189
  1503. package/src/motion/roles.test.ts +0 -256
  1504. package/src/motion/rubberBand.test.ts +0 -88
  1505. package/src/motion/selectPage.test.ts +0 -31
  1506. package/src/motion/spring.test.ts +0 -650
  1507. package/src/motion/uikit/gestures.test.ts +0 -89
  1508. package/src/motion/uikit/springRows.test.ts +0 -71
  1509. package/src/motion/uikit/uikit.test.ts +0 -364
  1510. package/src/motion/uikit/values.test.ts +0 -110
  1511. package/src/naming.test.ts +0 -44
  1512. package/src/postinstall.test.mjs +0 -216
  1513. package/src/reset.a11y.chromium.spec.ts +0 -238
  1514. package/src/reset.test.ts +0 -205
  1515. package/src/reset.until-found.a11y.browser.spec.ts +0 -76
  1516. package/src/reset.until-found.test.ts +0 -32
  1517. package/src/serverSafeComponents.test.ts +0 -675
  1518. package/src/stylexValidation.source-build.test.mjs +0 -42
  1519. package/src/tailwind-theme.test.ts +0 -153
  1520. package/src/theme/MediaTheme.dom.test.tsx +0 -97
  1521. package/src/theme/Theme.test.tsx +0 -337
  1522. package/src/theme/__tests__/SpacingRootFontSize.a11y.chromium.spec.ts +0 -199
  1523. package/src/theme/contrast.test.ts +0 -94
  1524. package/src/theme/declarationBoundary.test.ts +0 -126
  1525. package/src/theme/defineTheme.test.ts +0 -2028
  1526. package/src/theme/derivedVarRegistry.test.ts +0 -588
  1527. package/src/theme/expandColorScale.test.ts +0 -651
  1528. package/src/theme/expandMotionScale.test.ts +0 -145
  1529. package/src/theme/expandRadiusScale.test.ts +0 -59
  1530. package/src/theme/expandTypeScale.test.ts +0 -351
  1531. package/src/theme/extensibleAxes.test.ts +0 -432
  1532. package/src/theme/generateThemeRules.test.ts +0 -1870
  1533. package/src/theme/hct.test.ts +0 -93
  1534. package/src/theme/onMediaTokens.test.ts +0 -431
  1535. package/src/theme/publicThemeHelperContract.test.ts +0 -67
  1536. package/src/theme/syntax/defineSyntaxTheme.diff.test.ts +0 -91
  1537. package/src/theme/syntax/serverSafeSyntax.test.ts +0 -68
  1538. package/src/theme/syntax/useSyntaxTheme.diff.test.tsx +0 -40
  1539. package/src/theme/themeAdaptations.test.ts +0 -1795
  1540. package/src/theme/themeRegistry.test.ts +0 -59
  1541. package/src/theme/themingTargets.test.ts +0 -589
  1542. package/src/theme/tokenValueCompat.test.ts +0 -204
  1543. package/src/theme/tokens.test.ts +0 -441
  1544. package/src/theme/useTheme.test.tsx +0 -322
  1545. package/src/unicodePropertyEscapes.source-build.test.mjs +0 -69
  1546. package/src/utils/chainEventHandlers.test.ts +0 -39
  1547. package/src/utils/characters.test.ts +0 -141
  1548. package/src/utils/color.test.ts +0 -233
  1549. package/src/utils/composeEventHandlers.test.ts +0 -71
  1550. package/src/utils/customPropertyArmLayering.test.ts +0 -131
  1551. package/src/utils/dateParser.test.ts +0 -402
  1552. package/src/utils/dateTypes.test.ts +0 -40
  1553. package/src/utils/devWarning.test.ts +0 -56
  1554. package/src/utils/edgeAutoscroll.test.ts +0 -192
  1555. package/src/utils/focusReturn.test.ts +0 -119
  1556. package/src/utils/getKey.test.ts +0 -25
  1557. package/src/utils/hitArea.test.ts +0 -628
  1558. package/src/utils/ime.test.ts +0 -32
  1559. package/src/utils/inputAria.test.ts +0 -39
  1560. package/src/utils/interactionModality.test.ts +0 -100
  1561. package/src/utils/isApplePlatform.test.ts +0 -61
  1562. package/src/utils/mergeProps.test.ts +0 -166
  1563. package/src/utils/mergeRefs.test.ts +0 -48
  1564. package/src/utils/parseStyleKey.test.ts +0 -56
  1565. package/src/utils/plainDate.test.ts +0 -848
  1566. package/src/utils/pressFeedback.test.ts +0 -874
  1567. package/src/utils/pressGesture.test.ts +0 -323
  1568. package/src/utils/pressPaintSource.test.ts +0 -65
  1569. package/src/utils/pressableCoverage.test.ts +0 -192
  1570. package/src/utils/safeUrl.test.ts +0 -101
  1571. package/src/utils/sharedMutationObserver.test.ts +0 -53
  1572. package/src/utils/sharedResizeObserver.test.ts +0 -283
  1573. package/src/utils/themeProps.test.ts +0 -62
  1574. package/src/utils/timeParser.test.ts +0 -295
  1575. package/src/utils/touchPresentation.test.ts +0 -323
@@ -1,4128 +0,0 @@
1
- // Copyright (c) Meta Platforms, Inc. and affiliates.
2
-
3
- /**
4
- * @file SideNav.test.tsx
5
- * @input Uses vitest, @testing-library/react, SideNav components
6
- * @output Unit tests for SideNav component suite
7
- * @position Testing; validates SideNav implementations
8
- *
9
- * SYNC: When SideNav components change, update tests to match new behavior
10
- */
11
-
12
- import React from 'react';
13
- import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest';
14
- import {
15
- render,
16
- screen,
17
- act,
18
- fireEvent,
19
- waitFor,
20
- within,
21
- } from '@testing-library/react';
22
- import userEvent from '@testing-library/user-event';
23
- import {useRef, useState, type ReactNode} from 'react';
24
- import * as stylex from '@stylexjs/stylex';
25
- import {focusOutlineStyles} from '../utils/focusOutline.stylex';
26
- import {getForcedColorsRules} from '../__tests__/forcedColors';
27
- import {Button} from '../Button';
28
- import {SideNav} from './SideNav';
29
- import {__resetMenuPressForTest} from '../hooks/useMenuPress';
30
- import {expectNativeLinkClicks} from '../hooks/__tests__/menuLinkClicks';
31
- import {NavHeadingMenu, NavHeadingMenuItem} from '../NavMenu';
32
- import {SideNavCollapseButton} from './SideNavCollapseButton';
33
- import {SideNavHeading} from './SideNavHeading';
34
- import {SideNavItem} from './SideNavItem';
35
- import {SideNavSection} from './SideNavSection';
36
- import {LinkProvider} from '../Link/LinkProvider';
37
- import {InternationalizationProvider} from '../i18n/InternationalizationProvider';
38
- import pseudoCatalog from '../../locales/pseudo.json';
39
- import {
40
- SideNavCollapseContext,
41
- type SideNavImperativeCollapseHandle,
42
- } from './SideNavCollapseContext';
43
-
44
- function CustomLink({
45
- children,
46
- ref,
47
- ...props
48
- }: React.ComponentPropsWithRef<'a'>) {
49
- return (
50
- <a ref={ref} data-custom-link {...props}>
51
- {children}
52
- </a>
53
- );
54
- }
55
-
56
- // =============================================================================
57
- // SideNav
58
- // =============================================================================
59
-
60
- describe('SideNav', () => {
61
- it('renders with navigation role', () => {
62
- render(<SideNav>Content</SideNav>);
63
- expect(screen.getByRole('navigation')).toBeInTheDocument();
64
- });
65
-
66
- it('renders aria-label for page navigation', () => {
67
- render(<SideNav>Content</SideNav>);
68
- expect(screen.getByRole('navigation')).toHaveAttribute(
69
- 'aria-label',
70
- 'Side navigation',
71
- );
72
- });
73
-
74
- it('renders children in scrollable area', () => {
75
- render(
76
- <SideNav>
77
- <span data-testid="nav-content">Nav items</span>
78
- </SideNav>,
79
- );
80
- expect(screen.getByTestId('nav-content')).toBeInTheDocument();
81
- });
82
-
83
- it('renders header slot', () => {
84
- render(
85
- <SideNav header={<span data-testid="header">Header</span>}>
86
- Content
87
- </SideNav>,
88
- );
89
- expect(screen.getByTestId('header')).toBeInTheDocument();
90
- });
91
-
92
- it('renders topContent slot', () => {
93
- render(
94
- <SideNav topContent={<span data-testid="sticky">Sticky</span>}>
95
- Content
96
- </SideNav>,
97
- );
98
- expect(screen.getByTestId('sticky')).toBeInTheDocument();
99
- });
100
-
101
- it('renders footer slot', () => {
102
- render(
103
- <SideNav footer={<span data-testid="footer">Footer</span>}>
104
- Content
105
- </SideNav>,
106
- );
107
- expect(screen.getByTestId('footer')).toBeInTheDocument();
108
- });
109
-
110
- it('renders footerIcons slot', () => {
111
- render(
112
- <SideNav footerIcons={<span data-testid="footer-icons">Icons</span>}>
113
- Content
114
- </SideNav>,
115
- );
116
- expect(screen.getByTestId('footer-icons')).toBeInTheDocument();
117
- });
118
-
119
- it('renders all slots together', () => {
120
- render(
121
- <SideNav
122
- header={<span data-testid="header">Header</span>}
123
- topContent={<span data-testid="sticky">Sticky</span>}
124
- footer={<span data-testid="footer">Footer</span>}
125
- footerIcons={<span data-testid="icons">Icons</span>}>
126
- <span data-testid="content">Content</span>
127
- </SideNav>,
128
- );
129
- expect(screen.getByTestId('header')).toBeInTheDocument();
130
- expect(screen.getByTestId('sticky')).toBeInTheDocument();
131
- expect(screen.getByTestId('content')).toBeInTheDocument();
132
- expect(screen.getByTestId('footer')).toBeInTheDocument();
133
- expect(screen.getByTestId('icons')).toBeInTheDocument();
134
- });
135
-
136
- it('forwards ref correctly', () => {
137
- const ref = vi.fn();
138
- render(<SideNav ref={ref}>Content</SideNav>);
139
- expect(ref).toHaveBeenCalledWith(expect.any(HTMLElement));
140
- });
141
-
142
- it('passes data-testid to root', () => {
143
- render(<SideNav data-testid="page-nav">Content</SideNav>);
144
- expect(screen.getByTestId('page-nav')).toBeInTheDocument();
145
- });
146
-
147
- it('renders and toggles from outside SideNav when handleRef is provided', async () => {
148
- const user = userEvent.setup();
149
-
150
- function Example() {
151
- const [isCollapsed, setIsCollapsed] = useState(false);
152
- const handleRef = useRef<SideNavImperativeCollapseHandle>(null);
153
-
154
- return (
155
- <>
156
- <SideNavCollapseButton handleRef={handleRef} />
157
- <SideNav
158
- handleRef={handleRef}
159
- collapsible={{
160
- isCollapsed,
161
- onCollapsedChange: setIsCollapsed,
162
- hasButton: false,
163
- }}>
164
- <SideNavSection title="Main">
165
- <SideNavItem label="Dashboard" icon={StubIcon} />
166
- </SideNavSection>
167
- </SideNav>
168
- </>
169
- );
170
- }
171
-
172
- render(<Example />);
173
-
174
- const button = screen.getByRole('button', {name: 'Collapse sidebar'});
175
- await user.click(button);
176
-
177
- expect(
178
- screen.getByRole('button', {name: 'Expand sidebar'}),
179
- ).toBeInTheDocument();
180
- });
181
-
182
- it('does not render an empty footer container when collapsible.hasButton is false', () => {
183
- render(
184
- <SideNav data-testid="nav" collapsible={{hasButton: false}}>
185
- Content
186
- </SideNav>,
187
- );
188
-
189
- // The built-in collapse button is opted out (consumers render their own
190
- // SideNavCollapseButton in the header), so it must not appear...
191
- expect(
192
- screen.queryByRole('button', {name: 'Collapse sidebar'}),
193
- ).not.toBeInTheDocument();
194
-
195
- // ...and no empty sticky-bottom container should be left behind. With no
196
- // footer/footerIcons and no built-in button, the scrollable content region
197
- // is the nav's only child.
198
- const nav = screen.getByTestId('nav');
199
- expect(nav.children).toHaveLength(1);
200
- });
201
-
202
- it('centers footer content when collapsed, matching children alignment', () => {
203
- render(
204
- <SideNav
205
- data-testid="nav"
206
- collapsible={{isCollapsed: true, hasButton: false}}
207
- footer={<span data-testid="footer-content">F</span>}>
208
- <span data-testid="children-content">C</span>
209
- </SideNav>,
210
- );
211
-
212
- // scrollableCollapsed (wraps children) and stickyBottomCollapsed (wraps
213
- // footer) are structurally parallel collapsed-rail containers; both
214
- // must center their content the same way, or full-width footer content
215
- // stretches to the collapsed rail's width instead of centering.
216
- const childrenContainer =
217
- screen.getByTestId('children-content').parentElement;
218
- const footerContainer = screen.getByTestId('footer-content').parentElement;
219
- expect(getComputedStyle(childrenContainer!).alignItems).toBe('center');
220
- expect(getComputedStyle(footerContainer!).alignItems).toBe('center');
221
- });
222
-
223
- it('fires a consumer onClick on the collapse button in addition to toggling', async () => {
224
- const user = userEvent.setup();
225
- const onClick = vi.fn();
226
-
227
- function Example() {
228
- const [isCollapsed, setIsCollapsed] = useState(false);
229
- const handleRef = useRef<SideNavImperativeCollapseHandle>(null);
230
- return (
231
- <>
232
- <SideNavCollapseButton handleRef={handleRef} onClick={onClick} />
233
- <SideNav
234
- handleRef={handleRef}
235
- collapsible={{
236
- isCollapsed,
237
- onCollapsedChange: setIsCollapsed,
238
- hasButton: false,
239
- }}>
240
- <SideNavSection title="Main">
241
- <SideNavItem label="Dashboard" icon={StubIcon} />
242
- </SideNavSection>
243
- </SideNav>
244
- </>
245
- );
246
- }
247
-
248
- render(<Example />);
249
- await user.click(screen.getByRole('button', {name: 'Collapse sidebar'}));
250
-
251
- expect(onClick).toHaveBeenCalledTimes(1);
252
- // Toggle still ran: the label flipped to "Expand sidebar".
253
- expect(
254
- screen.getByRole('button', {name: 'Expand sidebar'}),
255
- ).toBeInTheDocument();
256
- });
257
- });
258
-
259
- // =============================================================================
260
- // SideNavHeading
261
- // =============================================================================
262
-
263
- describe('SideNavHeading', () => {
264
- it('renders heading text', () => {
265
- render(<SideNavHeading heading="My App" />);
266
- expect(screen.getByText('My App')).toBeInTheDocument();
267
- });
268
-
269
- it('renders icon', () => {
270
- render(
271
- <SideNavHeading
272
- heading="My App"
273
- icon={<span data-testid="app-icon">🏠</span>}
274
- />,
275
- );
276
- expect(screen.getByTestId('app-icon')).toBeInTheDocument();
277
- });
278
-
279
- it('renders superheading', () => {
280
- render(<SideNavHeading heading="Product" superheading="Suite Name" />);
281
- expect(screen.getByText('Suite Name')).toBeInTheDocument();
282
- });
283
-
284
- it('renders subheading', () => {
285
- render(<SideNavHeading heading="Product" subheading="Account" />);
286
- expect(screen.getByText('Account')).toBeInTheDocument();
287
- });
288
-
289
- it('renders as link when headingHref is provided without menu', () => {
290
- render(<SideNavHeading heading="My App" headingHref="/home" />);
291
- const link = screen.getByRole('link');
292
- expect(link).toHaveAttribute('href', '/home');
293
- expect(link).toHaveTextContent('My App');
294
- });
295
-
296
- it('uses custom link component from as prop', () => {
297
- render(
298
- <SideNavHeading heading="My App" headingHref="/home" as={CustomLink} />,
299
- );
300
- const link = screen.getByRole('link');
301
- expect(link).toHaveAttribute('data-custom-link');
302
- });
303
-
304
- it('uses custom link component from LinkProvider', () => {
305
- render(
306
- <LinkProvider component={CustomLink}>
307
- <SideNavHeading heading="My App" headingHref="/home" />
308
- </LinkProvider>,
309
- );
310
- const link = screen.getByRole('link');
311
- expect(link).toHaveAttribute('data-custom-link');
312
- });
313
-
314
- it('renders independent links when headingHref and superheadingHref are provided', () => {
315
- render(
316
- <SideNavHeading
317
- heading="Product"
318
- headingHref="/product"
319
- superheading="Suite"
320
- superheadingHref="/suite"
321
- />,
322
- );
323
- const links = screen.getAllByRole('link');
324
- expect(links).toHaveLength(2);
325
- expect(links[0]).toHaveAttribute('href', '/suite');
326
- expect(links[1]).toHaveAttribute('href', '/product');
327
- });
328
-
329
- it('gives every link an accessible name with superheadingHref, headingHref, and menu', () => {
330
- render(
331
- <SideNavHeading
332
- icon={<span>Icon</span>}
333
- superheading="Suite Name"
334
- superheadingHref="/suite"
335
- heading="Product Name"
336
- headingHref="/product"
337
- menu={<div>Analytics</div>}
338
- />,
339
- );
340
- // The icon link to /product previously rendered with no text and no
341
- // aria-label, producing an empty accessible name (axe rule: link-name).
342
- // Every link pointing at /product must now expose "Product Name".
343
- const productLinks = screen
344
- .getAllByRole('link', {name: 'Product Name'})
345
- .filter(link => link.getAttribute('href') === '/product');
346
- expect(productLinks.length).toBeGreaterThan(0);
347
- for (const link of productLinks) {
348
- expect(link).toHaveAccessibleName('Product Name');
349
- }
350
- // The independent superheading link is unaffected.
351
- expect(screen.getByRole('link', {name: 'Suite Name'})).toHaveAttribute(
352
- 'href',
353
- '/suite',
354
- );
355
- // No link should be missing an accessible name.
356
- for (const link of screen.getAllByRole('link')) {
357
- expect(link).toHaveAccessibleName();
358
- }
359
- });
360
-
361
- it('shows chevron when menu is provided', () => {
362
- render(<SideNavHeading heading="My App" menu={<div>Menu content</div>} />);
363
- // The chevron SVG should be rendered
364
- const button = screen.getByRole('button');
365
- expect(button).toBeInTheDocument();
366
- });
367
-
368
- it('does not show chevron without menu', () => {
369
- const {container} = render(<SideNavHeading heading="My App" />);
370
- const svg = container.querySelector('svg');
371
- expect(svg).not.toBeInTheDocument();
372
- });
373
-
374
- it('whole heading is popover trigger when menu provided without hrefs', () => {
375
- render(<SideNavHeading heading="My App" menu={<div>Menu</div>} />);
376
- const button = screen.getByRole('button');
377
- // The popup is no longer a dialog (role: 'none' on usePopover), so the
378
- // trigger advertises a generic popup rather than aria-haspopup="dialog".
379
- expect(button).toHaveAttribute('aria-haspopup', 'true');
380
- expect(button).toHaveAttribute('aria-expanded', 'false');
381
- });
382
-
383
- it('has popoverTarget on trigger button when menu is provided', () => {
384
- render(
385
- <SideNavHeading
386
- heading="My App"
387
- menu={<div data-testid="menu-content">Menu</div>}
388
- />,
389
- );
390
- const button = screen.getByRole('button');
391
- // The trigger button uses aria attributes from usePopover and
392
- // an onClick handler from useMenuHover for click-to-lock toggle.
393
- expect(button).toHaveAttribute('aria-haspopup', 'true');
394
- expect(button).toHaveAttribute('aria-expanded');
395
- });
396
-
397
- it('renders chevron as separate trigger when menu and hrefs are provided', () => {
398
- render(
399
- <SideNavHeading
400
- heading="Product"
401
- headingHref="/product"
402
- menu={<div>Menu</div>}
403
- />,
404
- );
405
- const button = screen.getByRole('button', {name: 'Open menu'});
406
- expect(button).toHaveAttribute('aria-haspopup', 'true');
407
- });
408
-
409
- it('passes data-testid', () => {
410
- render(<SideNavHeading heading="My App" data-testid="nav-header" />);
411
- expect(screen.getByTestId('nav-header')).toBeInTheDocument();
412
- });
413
-
414
- // ===========================================================================
415
- // Menu popover semantics — the popup must not be announced as a modal
416
- // dialog, and role="menu" must be scoped to the actual menu items so the
417
- // heading button is not an invalid child of the menu.
418
- // ===========================================================================
419
-
420
- // The popover layer keeps `popover` content display:none in jsdom even
421
- // when open, hiding it from role queries — so these tests assert the
422
- // popup's ARIA semantics at the DOM level instead.
423
- describe('menu popover semantics', () => {
424
- const menuItems = (
425
- <>
426
- <div role="menuitem">Alpha</div>
427
- <div role="menuitem">Beta</div>
428
- </>
429
- );
430
-
431
- it('leaves a press on caller content that is not an item to that content: a button click and a link click run (interaction-modality INV4)', async () => {
432
- __resetMenuPressForTest();
433
- const user = userEvent.setup();
434
- const onButton = vi.fn();
435
- const onLink = vi.fn((event: React.MouseEvent) => event.preventDefault());
436
- render(
437
- <SideNavHeading
438
- heading="My App"
439
- menu={
440
- <>
441
- <button type="button" onClick={onButton}>
442
- Switch account
443
- </button>
444
- <a href="#docs" onClick={onLink}>
445
- Docs
446
- </a>
447
- </>
448
- }
449
- />,
450
- );
451
- await user.click(screen.getByRole('button', {name: 'Open menu'}));
452
- for (const pointer of [
453
- {pointerType: 'mouse', pointerId: 1, button: 0},
454
- {pointerType: 'touch', pointerId: 2},
455
- ]) {
456
- onButton.mockClear();
457
- onLink.mockClear();
458
- const button = screen.getByRole('button', {
459
- name: 'Switch account',
460
- hidden: true,
461
- });
462
- fireEvent.pointerDown(button, pointer);
463
- fireEvent.pointerUp(button, pointer);
464
- // A pointer's click reports detail 1 (a keyboard's reports 0).
465
- fireEvent.click(button, {detail: 1});
466
- expect(onButton).toHaveBeenCalledTimes(1);
467
- const link = screen.getByRole('link', {name: 'Docs', hidden: true});
468
- fireEvent.pointerDown(link, pointer);
469
- fireEvent.pointerUp(link, pointer);
470
- fireEvent.click(link, {detail: 1});
471
- expect(onLink).toHaveBeenCalledTimes(1);
472
- }
473
- });
474
-
475
- it('leaves a Ctrl-, Cmd-, Shift- or middle click on a link item in its menu to the browser', async () => {
476
- __resetMenuPressForTest();
477
- const user = userEvent.setup();
478
- render(
479
- <SideNavHeading
480
- heading="My App"
481
- menu={
482
- <NavHeadingMenu>
483
- <NavHeadingMenuItem label="Docs" href="/docs" />
484
- <NavHeadingMenuItem label="Status" href="/status" />
485
- </NavHeadingMenu>
486
- }
487
- />,
488
- );
489
- await user.click(screen.getByRole('button', {name: 'Open menu'}));
490
- expectNativeLinkClicks(
491
- screen.getByRole('menuitem', {name: 'Docs', hidden: true}),
492
- );
493
- });
494
-
495
- it('closes when a mouse pressed on a NavHeadingMenu item is released outside the menu (useMenuPress FR3)', async () => {
496
- __resetMenuPressForTest();
497
- const user = userEvent.setup();
498
- render(
499
- <>
500
- <SideNavHeading
501
- heading="My App"
502
- menu={
503
- <NavHeadingMenu>
504
- <NavHeadingMenuItem label="Alpha" onClick={() => {}} />
505
- <NavHeadingMenuItem label="Beta" onClick={() => {}} />
506
- </NavHeadingMenu>
507
- }
508
- />
509
- <button type="button">Outside</button>
510
- </>,
511
- );
512
- const trigger = screen.getByRole('button', {name: 'Open menu'});
513
- await user.click(trigger);
514
- expect(trigger).toHaveAttribute('aria-expanded', 'true');
515
- const mouse = {pointerType: 'mouse', pointerId: 3, button: 0};
516
- const outside = screen.getByRole('button', {name: 'Outside'});
517
- fireEvent.pointerDown(
518
- screen.getByRole('menuitem', {name: 'Alpha', hidden: true}),
519
- mouse,
520
- );
521
- fireEvent.pointerMove(outside, mouse);
522
- fireEvent.pointerUp(outside, mouse);
523
- expect(trigger).toHaveAttribute('aria-expanded', 'false');
524
- });
525
-
526
- it('acts on the item a press slid across the menu lifts on, once (family:action-groups FR1, FR2, FR5)', async () => {
527
- __resetMenuPressForTest();
528
- const user = userEvent.setup();
529
- const onAlpha = vi.fn();
530
- const onBeta = vi.fn();
531
- render(
532
- <SideNavHeading
533
- heading="My App"
534
- menu={
535
- <>
536
- <div role="menuitem" tabIndex={-1} onClick={onAlpha}>
537
- Alpha
538
- </div>
539
- <div role="menuitem" tabIndex={-1} onClick={onBeta}>
540
- Beta
541
- </div>
542
- </>
543
- }
544
- />,
545
- );
546
- await user.click(screen.getByRole('button', {name: 'Open menu'}));
547
- const item = (name: string) =>
548
- screen.getByRole('menuitem', {name, hidden: true});
549
- for (const pointer of [
550
- {pointerType: 'touch', pointerId: 1},
551
- {pointerType: 'mouse', pointerId: 2, button: 0},
552
- ]) {
553
- onAlpha.mockClear();
554
- onBeta.mockClear();
555
- fireEvent.pointerDown(item('Alpha'), pointer);
556
- fireEvent.pointerMove(item('Beta'), pointer);
557
- fireEvent.pointerUp(item('Beta'), pointer);
558
- expect(onBeta).toHaveBeenCalledTimes(1);
559
- expect(onAlpha).not.toHaveBeenCalled();
560
- }
561
- });
562
-
563
- it('does not wrap the heading menu popup in a modal dialog', async () => {
564
- const user = userEvent.setup();
565
- render(<SideNavHeading heading="My App" menu={menuItems} />);
566
- await user.click(screen.getByRole('button', {name: 'Open menu'}));
567
- expect(document.querySelector('[role="dialog"]')).toBeNull();
568
- expect(document.querySelector('[aria-modal="true"]')).toBeNull();
569
- });
570
-
571
- it('scopes role="menu" to only menuitem children with an accessible name', async () => {
572
- const user = userEvent.setup();
573
- render(<SideNavHeading heading="My App" menu={menuItems} />);
574
- await user.click(screen.getByRole('button', {name: 'Open menu'}));
575
- const menu = document.querySelector('[role="menu"]');
576
- expect(menu).not.toBeNull();
577
- expect(menu).toHaveAttribute('aria-label', 'My App');
578
- const children = Array.from(menu!.children);
579
- expect(children.length).toBeGreaterThan(0);
580
- for (const child of children) {
581
- expect(child).toHaveAttribute('role', 'menuitem');
582
- }
583
- });
584
-
585
- it('keeps the popup heading button outside the menu and closes on click', async () => {
586
- const user = userEvent.setup();
587
- render(<SideNavHeading heading="My App" menu={menuItems} />);
588
- const trigger = screen.getByRole('button', {name: 'Open menu'});
589
- await user.click(trigger);
590
- expect(trigger).toHaveAttribute('aria-expanded', 'true');
591
- const menu = document.querySelector('[role="menu"]');
592
- expect(menu).not.toBeNull();
593
- // The heading replica button in the popup is a sibling of the menu,
594
- // not an invalid menu child.
595
- const headingButton = Array.from(
596
- document.querySelectorAll('button'),
597
- ).find(b => b !== trigger && b.textContent?.includes('My App'));
598
- expect(headingButton).toBeDefined();
599
- expect(menu!.contains(headingButton!)).toBe(false);
600
- // Clicking it still closes the popup.
601
- fireEvent.click(headingButton!);
602
- expect(trigger).toHaveAttribute('aria-expanded', 'false');
603
- });
604
-
605
- it('applies the same semantics in mixed mode (menu + hrefs)', async () => {
606
- const user = userEvent.setup();
607
- render(
608
- <SideNavHeading
609
- heading="Product"
610
- headingHref="/product"
611
- menu={menuItems}
612
- />,
613
- );
614
- await user.click(screen.getByRole('button', {name: 'Open menu'}));
615
- expect(document.querySelector('[role="dialog"]')).toBeNull();
616
- const menu = document.querySelector('[role="menu"]');
617
- expect(menu).not.toBeNull();
618
- expect(menu).toHaveAttribute('aria-label', 'Product');
619
- for (const child of Array.from(menu!.children)) {
620
- expect(child).toHaveAttribute('role', 'menuitem');
621
- }
622
- });
623
- });
624
- });
625
-
626
- // =============================================================================
627
- // SideNavHeading — collapsed mode
628
- // =============================================================================
629
-
630
- const COLLAPSED_CONTEXT = {
631
- isCollapsed: true,
632
- toggle: () => {},
633
- isCollapsible: true,
634
- };
635
-
636
- function CollapsedWrapper({children}: {children: ReactNode}) {
637
- return (
638
- <SideNavCollapseContext value={COLLAPSED_CONTEXT}>
639
- {children}
640
- </SideNavCollapseContext>
641
- );
642
- }
643
-
644
- describe('SideNavHeading collapsed', () => {
645
- it('returns null when collapsed without icon', () => {
646
- const {container} = render(
647
- <CollapsedWrapper>
648
- <SideNavHeading heading="My App" />
649
- </CollapsedWrapper>,
650
- );
651
- expect(container.innerHTML).toBe('');
652
- });
653
-
654
- it('renders icon when collapsed with icon', () => {
655
- render(
656
- <CollapsedWrapper>
657
- <SideNavHeading
658
- heading="My App"
659
- icon={<span data-testid="app-icon">🏠</span>}
660
- />
661
- </CollapsedWrapper>,
662
- );
663
- expect(screen.getByTestId('app-icon')).toBeInTheDocument();
664
- });
665
-
666
- it('does not show heading text inline when collapsed (only in tooltip)', () => {
667
- const {container} = render(
668
- <CollapsedWrapper>
669
- <SideNavHeading
670
- heading="My App"
671
- icon={<span data-testid="app-icon">🏠</span>}
672
- />
673
- </CollapsedWrapper>,
674
- );
675
- // The heading text should not appear as a visible inline element
676
- // (it exists only in the tooltip for accessibility)
677
- const headingSpans = container.querySelectorAll('span');
678
- const inlineHeadingText = Array.from(headingSpans).find(
679
- el =>
680
- el.textContent === 'My App' &&
681
- !el.closest('[role="tooltip"]') &&
682
- !el.hasAttribute('data-tooltip'),
683
- );
684
- expect(inlineHeadingText).toBeUndefined();
685
- });
686
-
687
- it('renders as link when collapsed with headingHref', () => {
688
- render(
689
- <CollapsedWrapper>
690
- <SideNavHeading
691
- heading="My App"
692
- headingHref="/home"
693
- icon={<span data-testid="app-icon">🏠</span>}
694
- />
695
- </CollapsedWrapper>,
696
- );
697
- const link = screen.getByRole('link');
698
- expect(link).toHaveAttribute('href', '/home');
699
- expect(link).toHaveAttribute('aria-label', 'My App');
700
- });
701
-
702
- it('does not show chevron when collapsed', () => {
703
- const {container} = render(
704
- <CollapsedWrapper>
705
- <SideNavHeading
706
- heading="My App"
707
- headingHref="/home"
708
- icon={<span data-testid="app-icon">🏠</span>}
709
- menu={<div>Menu</div>}
710
- />
711
- </CollapsedWrapper>,
712
- );
713
- const svg = container.querySelector('svg');
714
- expect(svg).not.toBeInTheDocument();
715
- });
716
-
717
- it('passes data-testid when collapsed', () => {
718
- render(
719
- <CollapsedWrapper>
720
- <SideNavHeading
721
- heading="My App"
722
- icon={<span>🏠</span>}
723
- data-testid="nav-header"
724
- />
725
- </CollapsedWrapper>,
726
- );
727
- expect(screen.getByTestId('nav-header')).toBeInTheDocument();
728
- });
729
-
730
- it('collapsed menu popup is not a dialog and scopes role="menu" to menu items', async () => {
731
- const user = userEvent.setup();
732
- render(
733
- <CollapsedWrapper>
734
- <SideNavHeading
735
- heading="My App"
736
- icon={<span data-testid="app-icon">🏠</span>}
737
- menu={
738
- <>
739
- <div role="menuitem">Alpha</div>
740
- <div role="menuitem">Beta</div>
741
- </>
742
- }
743
- />
744
- </CollapsedWrapper>,
745
- );
746
- await user.click(screen.getByRole('button', {name: 'My App'}));
747
- // No modal dialog wrapper around the menu popup.
748
- expect(document.querySelector('[role="dialog"]')).toBeNull();
749
- expect(document.querySelector('[aria-modal="true"]')).toBeNull();
750
- // role="menu" is scoped to the menu items and has a direct name.
751
- const menu = document.querySelector('[role="menu"]');
752
- expect(menu).not.toBeNull();
753
- expect(menu).toHaveAttribute('aria-label', 'My App');
754
- const children = Array.from(menu!.children);
755
- expect(children.length).toBeGreaterThan(0);
756
- for (const child of children) {
757
- expect(child).toHaveAttribute('role', 'menuitem');
758
- }
759
- // No button (trigger or heading replica) lives inside the menu.
760
- for (const button of Array.from(document.querySelectorAll('button'))) {
761
- expect(menu!.contains(button)).toBe(false);
762
- }
763
- });
764
-
765
- it('anchors the collapsed icon-only trigger so the popover positions against it', () => {
766
- render(
767
- <CollapsedWrapper>
768
- <SideNavHeading
769
- heading="My App"
770
- icon={<span data-testid="app-icon">🏠</span>}
771
- menu={<div role="menuitem">Alpha</div>}
772
- />
773
- </CollapsedWrapper>,
774
- );
775
- const trigger = screen.getByRole('button', {name: 'My App'});
776
- // The same element also carries the collapsed-item Tooltip's own
777
- // anchor-name (via anchorRef={collapsedItemRef}), so a plain
778
- // non-empty check would pass even when the menu popover itself isn't
779
- // anchored. Assert the popover's own position-anchor id specifically
780
- // appears in the trigger's (possibly multi-value) anchor-name list.
781
- const popoverEl = document.querySelector('[popover]') as HTMLElement;
782
- const anchorId = popoverEl.style.positionAnchor;
783
- expect(anchorId).not.toBe('');
784
- const triggerAnchorNames = trigger.style.anchorName
785
- .split(',')
786
- .map(s => s.trim());
787
- expect(triggerAnchorNames).toContain(anchorId);
788
- });
789
- });
790
-
791
- // =============================================================================
792
- // SideNavHeading — headerEndContent
793
- // =============================================================================
794
-
795
- describe('SideNavHeading headerEndContent', () => {
796
- it('renders headerEndContent in the default static path', () => {
797
- render(
798
- <SideNavHeading
799
- heading="My App"
800
- headerEndContent={<span data-testid="end-badge">3</span>}
801
- />,
802
- );
803
- expect(screen.getByTestId('end-badge')).toBeInTheDocument();
804
- });
805
-
806
- it('renders headerEndContent beside the link, not inside it, in isWholeHeadingLink path', () => {
807
- const onPress = vi.fn();
808
- render(
809
- <SideNavHeading
810
- heading="My App"
811
- headingHref="/home"
812
- headerEndContent={
813
- <button type="button" onClick={onPress}>
814
- Create
815
- </button>
816
- }
817
- />,
818
- );
819
- const link = screen.getByRole('link', {name: 'My App'});
820
- expect(link).toHaveAttribute('href', '/home');
821
- // A control inside a link is invalid and its press would follow the link.
822
- const button = screen.getByRole('button', {name: 'Create'});
823
- expect(button.closest('a')).toBeNull();
824
- expect(link.parentElement).toBe(button.parentElement?.parentElement);
825
- fireEvent.click(button);
826
- expect(onPress).toHaveBeenCalledTimes(1);
827
- });
828
-
829
- it('renders headerEndContent in isWholeHeadingTrigger path', () => {
830
- render(
831
- <SideNavHeading
832
- heading="My App"
833
- menu={<div>Menu</div>}
834
- headerEndContent={<span data-testid="end-badge">3</span>}
835
- />,
836
- );
837
- const badge = screen.getByTestId('end-badge');
838
- expect(badge).toBeInTheDocument();
839
- // Badge renders inside the heading container (div), alongside the chevron button
840
- expect(badge.closest('[class]')).not.toBeNull();
841
- });
842
-
843
- it('renders headerEndContent in mixed mode (menu + href)', () => {
844
- render(
845
- <SideNavHeading
846
- heading="My App"
847
- headingHref="/home"
848
- menu={<div>Menu</div>}
849
- headerEndContent={<span data-testid="end-badge">3</span>}
850
- />,
851
- );
852
- expect(screen.getByTestId('end-badge')).toBeInTheDocument();
853
- });
854
-
855
- it('renders headerEndContent with independent links (no menu)', () => {
856
- render(
857
- <SideNavHeading
858
- heading="Product"
859
- headingHref="/product"
860
- superheading="Suite"
861
- superheadingHref="/suite"
862
- headerEndContent={<span data-testid="end-badge">3</span>}
863
- />,
864
- );
865
- expect(screen.getByTestId('end-badge')).toBeInTheDocument();
866
- });
867
-
868
- it('hides headerEndContent when collapsed', () => {
869
- render(
870
- <CollapsedWrapper>
871
- <SideNavHeading
872
- heading="My App"
873
- icon={<span>🏠</span>}
874
- headerEndContent={<span data-testid="end-badge">3</span>}
875
- />
876
- </CollapsedWrapper>,
877
- );
878
- expect(screen.queryByTestId('end-badge')).not.toBeInTheDocument();
879
- });
880
- });
881
-
882
- // =============================================================================
883
- // SideNavHeading — truncation tooltips
884
- // =============================================================================
885
-
886
- describe('SideNavHeading truncation tooltips', () => {
887
- it('attaches truncation refs to heading text spans', () => {
888
- render(
889
- <SideNavHeading
890
- heading="My App"
891
- superheading="Acme Corp"
892
- subheading="admin@acme.com"
893
- />,
894
- );
895
- // All three text spans should be present
896
- expect(screen.getByText('My App')).toBeInTheDocument();
897
- expect(screen.getByText('Acme Corp')).toBeInTheDocument();
898
- expect(screen.getByText('admin@acme.com')).toBeInTheDocument();
899
- });
900
-
901
- it('does not crash with truncation hooks when only heading is provided', () => {
902
- render(<SideNavHeading heading="My App" />);
903
- expect(screen.getByText('My App')).toBeInTheDocument();
904
- });
905
- });
906
-
907
- // =============================================================================
908
- // SideNavItem
909
- // =============================================================================
910
-
911
- describe('SideNavItem', () => {
912
- it('renders label text', () => {
913
- render(<SideNavItem label="Dashboard" />);
914
- expect(screen.getByText('Dashboard')).toBeInTheDocument();
915
- });
916
-
917
- it('renders as link when href is provided', () => {
918
- render(<SideNavItem label="Dashboard" href="/dashboard" />);
919
- const link = screen.getByRole('link');
920
- expect(link).toHaveAttribute('href', '/dashboard');
921
- });
922
-
923
- it('renders as button when no href', () => {
924
- render(<SideNavItem label="Dashboard" />);
925
- expect(screen.getByRole('button')).toBeInTheDocument();
926
- });
927
-
928
- it('sets aria-current="page" when selected', () => {
929
- render(<SideNavItem label="Dashboard" isSelected />);
930
- const button = screen.getByRole('button');
931
- expect(button).toHaveAttribute('aria-current', 'page');
932
- });
933
-
934
- it('does not set aria-current when not selected', () => {
935
- render(<SideNavItem label="Dashboard" />);
936
- const button = screen.getByRole('button');
937
- expect(button).not.toHaveAttribute('aria-current');
938
- });
939
-
940
- it('disables the button when isDisabled', () => {
941
- render(<SideNavItem label="Dashboard" isDisabled />);
942
- const button = screen.getByRole('button');
943
- expect(button).toBeDisabled();
944
- });
945
-
946
- it('calls onClick handler', async () => {
947
- const user = userEvent.setup();
948
- const handleClick = vi.fn();
949
- render(<SideNavItem label="Dashboard" onClick={handleClick} />);
950
- await user.click(screen.getByRole('button'));
951
- expect(handleClick).toHaveBeenCalledTimes(1);
952
- });
953
-
954
- it('renders endContent', () => {
955
- render(
956
- <SideNavItem
957
- label="Projects"
958
- endContent={<span data-testid="badge">3</span>}
959
- />,
960
- );
961
- expect(screen.getByTestId('badge')).toBeInTheDocument();
962
- });
963
-
964
- it('renders nested children', () => {
965
- render(
966
- <SideNavItem label="Settings">
967
- <SideNavItem label="General" />
968
- <SideNavItem label="Security" />
969
- </SideNavItem>,
970
- );
971
- expect(screen.getByText('General')).toBeInTheDocument();
972
- expect(screen.getByText('Security')).toBeInTheDocument();
973
- });
974
-
975
- it('keeps nested rows full width and indents their content inside the row', () => {
976
- render(
977
- <SideNavItem label="Settings">
978
- <SideNavItem label="General" href="/settings/general">
979
- <SideNavItem label="Advanced" href="/settings/general/advanced" />
980
- </SideNavItem>
981
- </SideNavItem>,
982
- );
983
-
984
- // The children group adds no inset of its own, so a nested row's hover
985
- // and selected fill spans the nav list like a top-level row's.
986
- const groups = screen.getAllByRole('group', {hidden: true});
987
- for (const group of groups) {
988
- expect(declaredOn(group, 'padding-inline-start')).toEqual([]);
989
- expect(
990
- declaredOn(group.firstElementChild!, 'padding-inline-start'),
991
- ).toEqual([]);
992
- }
993
-
994
- // The indent lives inside each row instead, one step per nesting level.
995
- // An ordinary row is the link itself: its own padding plus the steps.
996
- const top = screen.getByRole('button', {name: 'Settings'});
997
- const general = screen.getByRole('link', {name: 'General'});
998
- const advanced = screen.getByRole('link', {name: 'Advanced'});
999
- expect(indentOf(top)).toBeNull();
1000
- expect(indentOf(advanced)).toBe(
1001
- 'calc(var(--spacing-2) + 2 * var(--spacing-6))',
1002
- );
1003
-
1004
- // A split row (own href plus children) keeps the wrapper's base padding
1005
- // and puts the steps on the link, so the whole indent is clickable
1006
- // instead of dead space that still shows hover and press feedback.
1007
- expect(indentOf(general.parentElement!)).toBeNull();
1008
- expect(indentOf(general)).toBe('calc(1 * var(--spacing-6))');
1009
- });
1010
-
1011
- it('clips the children group only while it is collapsed', async () => {
1012
- // Nested rows sit flush with the group, so a permanent clip would cut off
1013
- // their focus ring. The group clips for the collapse animation only.
1014
- const user = userEvent.setup();
1015
- render(
1016
- <SideNavItem label="Settings" collapsible>
1017
- <SideNavItem label="General" href="/settings/general" />
1018
- </SideNavItem>,
1019
- );
1020
- const group = () => screen.getByRole('group', {hidden: true});
1021
- expect(declaredOn(group(), 'overflow')).toEqual(['visible']);
1022
- expect(declaredOn(group().firstElementChild!, 'overflow')).toEqual([]);
1023
-
1024
- await user.click(screen.getByRole('button', {name: 'Settings'}));
1025
- expect(declaredOn(group(), 'overflow')).toEqual(['hidden']);
1026
- });
1027
-
1028
- it('puts the indent on the primary link of a nested actions row', () => {
1029
- render(
1030
- <SideNavItem label="Projects">
1031
- <SideNavItem
1032
- label="Alpha"
1033
- href="/projects/alpha"
1034
- actions={
1035
- <button type="button" aria-label="Alpha actions">
1036
- More
1037
- </button>
1038
- }
1039
- />
1040
- </SideNavItem>,
1041
- );
1042
- const alpha = screen.getByRole('link', {name: 'Alpha'});
1043
- expect(indentOf(alpha.parentElement!)).toBeNull();
1044
- expect(indentOf(alpha)).toBe('calc(1 * var(--spacing-6))');
1045
- });
1046
-
1047
- it("composes a caller's style with the indent on a nested row", () => {
1048
- render(
1049
- <SideNavItem label="Settings">
1050
- <SideNavItem
1051
- label="General"
1052
- href="/settings/general"
1053
- style={{color: 'rebeccapurple'}}
1054
- />
1055
- <SideNavItem
1056
- label="Security"
1057
- href="/settings/security"
1058
- style={{color: 'rebeccapurple'}}>
1059
- <SideNavItem label="Keys" href="/settings/security/keys" />
1060
- </SideNavItem>
1061
- </SideNavItem>,
1062
- );
1063
- // Ordinary row: the link is the row.
1064
- const general = screen.getByRole('link', {name: 'General'});
1065
- expect(general.style.color).toBe('rebeccapurple');
1066
- expect(indentOf(general)).toBe(
1067
- 'calc(var(--spacing-2) + 1 * var(--spacing-6))',
1068
- );
1069
- // Split row: the caller's style still lands on the primary link.
1070
- const security = screen.getByRole('link', {name: 'Security'});
1071
- expect(security.style.color).toBe('rebeccapurple');
1072
- expect(indentOf(security)).toBe('calc(1 * var(--spacing-6))');
1073
- });
1074
-
1075
- it("keeps a caller's className on the row element", () => {
1076
- render(
1077
- <SideNavItem label="Settings">
1078
- <SideNavItem
1079
- label="General"
1080
- href="/settings/general"
1081
- className="caller-row"
1082
- />
1083
- <SideNavItem
1084
- label="Security"
1085
- href="/settings/security"
1086
- className="caller-row">
1087
- <SideNavItem label="Keys" href="/settings/security/keys" />
1088
- </SideNavItem>
1089
- </SideNavItem>,
1090
- );
1091
- // Ordinary row: the link is the row, and the caller's class sits beside
1092
- // the row's own classes instead of being replaced by them.
1093
- const general = screen.getByRole('link', {name: 'General'});
1094
- expect(general).toHaveClass('caller-row');
1095
- expect(general).toHaveClass('astryx-side-nav-item');
1096
- // Split row: the class lands on the primary link, as `style` does.
1097
- expect(screen.getByRole('link', {name: 'Security'})).toHaveClass(
1098
- 'caller-row',
1099
- );
1100
- });
1101
-
1102
- it("keeps a caller's className on a collapsed rail item", () => {
1103
- render(
1104
- <SideNav collapsible={{isCollapsed: true, hasButton: false}}>
1105
- <SideNavItem
1106
- label="Home"
1107
- href="/"
1108
- icon="calendar"
1109
- className="caller-row"
1110
- />
1111
- </SideNav>,
1112
- );
1113
- expect(screen.getByRole('link', {name: 'Home'})).toHaveClass('caller-row');
1114
- });
1115
-
1116
- it("lets a caller's inline-start padding replace the indent", () => {
1117
- // The caller's `style` composes after the indent: a caller-set
1118
- // inline-start padding is an inline declaration, so it wins over the
1119
- // indent's class rule on the element that carries the indent, on both
1120
- // the ordinary and the split row path.
1121
- render(
1122
- <SideNavItem label="Settings">
1123
- <SideNavItem
1124
- label="General"
1125
- href="/settings/general"
1126
- style={{paddingInlineStart: '20px'}}
1127
- />
1128
- <SideNavItem
1129
- label="Security"
1130
- href="/settings/security"
1131
- style={{paddingInlineStart: '20px'}}>
1132
- <SideNavItem label="Keys" href="/settings/security/keys" />
1133
- </SideNavItem>
1134
- </SideNavItem>,
1135
- );
1136
- const general = screen.getByRole('link', {name: 'General'});
1137
- expect(general.style.paddingInlineStart).toBe('20px');
1138
- const security = screen.getByRole('link', {name: 'Security'});
1139
- expect(security.style.paddingInlineStart).toBe('20px');
1140
- // A row whose caller sets no padding keeps its indent.
1141
- expect(indentOf(screen.getByRole('link', {name: 'Keys'}))).toBe(
1142
- 'calc(var(--spacing-2) + 2 * var(--spacing-6))',
1143
- );
1144
- });
1145
-
1146
- it('indents a small nested row from the small row padding', () => {
1147
- render(
1148
- <SideNavItem label="Settings">
1149
- <SideNavItem label="General" href="/settings/general" size="sm" />
1150
- </SideNavItem>,
1151
- );
1152
- expect(indentOf(screen.getByRole('link', {name: 'General'}))).toBe(
1153
- 'calc(var(--spacing-1) + 1 * var(--spacing-6))',
1154
- );
1155
- });
1156
-
1157
- it('does not indent the first level of a collapsed rail flyout', async () => {
1158
- const user = userEvent.setup();
1159
- renderCollapsed(
1160
- <SideNavItem label="Settings" icon={StubIcon}>
1161
- <SideNavItem label="General" href="/settings/general">
1162
- <SideNavItem label="Advanced" href="/settings/general/advanced" />
1163
- </SideNavItem>
1164
- </SideNavItem>,
1165
- );
1166
- await user.click(screen.getByRole('button', {name: 'Settings'}));
1167
-
1168
- // jsdom leaves the native popover `display: none`, hence `hidden: true`.
1169
- // The flyout lists the parent's children as its own top level.
1170
- const general = screen.getByRole('link', {name: 'General', hidden: true});
1171
- const advanced = screen.getByRole('link', {name: 'Advanced', hidden: true});
1172
- expect(indentOf(general)).toBeNull();
1173
- expect(indentOf(general.parentElement!)).toBeNull();
1174
- expect(indentOf(advanced)).toBe(
1175
- 'calc(var(--spacing-2) + 1 * var(--spacing-6))',
1176
- );
1177
- });
1178
-
1179
- it('passes data-testid', () => {
1180
- render(<SideNavItem label="Dashboard" data-testid="nav-item" />);
1181
- expect(screen.getByTestId('nav-item')).toBeInTheDocument();
1182
- });
1183
-
1184
- it('renders with selected link', () => {
1185
- render(<SideNavItem label="Dashboard" href="/dashboard" isSelected />);
1186
- const link = screen.getByRole('link');
1187
- expect(link).toHaveAttribute('aria-current', 'page');
1188
- });
1189
-
1190
- it('places aria-current on the link, not the wrapper, for split-action items', () => {
1191
- // A collapsible item (has children) WITH a primary href renders the
1192
- // split-action path: the link and the expand toggle are siblings inside a
1193
- // wrapper div. aria-current="page" must sit on the focusable link so it is
1194
- // announced as the current page (navigation-8).
1195
- render(
1196
- <SideNavItem label="Reports" href="/reports" isSelected>
1197
- <SideNavItem label="Weekly" href="/reports/weekly" />
1198
- </SideNavItem>,
1199
- );
1200
- const link = screen.getByRole('link', {name: /Reports/});
1201
- expect(link).toHaveAttribute('aria-current', 'page');
1202
- // The wrapper div must NOT carry aria-current.
1203
- expect(link.closest('[aria-current="page"]')).toBe(link);
1204
- });
1205
-
1206
- it('renders custom component when as and href are provided', () => {
1207
- render(<SideNavItem label="Dashboard" href="/dashboard" as={CustomLink} />);
1208
- const link = screen.getByRole('link');
1209
- expect(link).toHaveAttribute('data-custom-link');
1210
- expect(link).toHaveAttribute('href', '/dashboard');
1211
- });
1212
-
1213
- it('still renders button when no href even with as prop', () => {
1214
- render(<SideNavItem label="Dashboard" as={CustomLink} />);
1215
- const button = screen.getByRole('button');
1216
- expect(button).toBeInTheDocument();
1217
- expect(button).not.toHaveAttribute('data-custom-link');
1218
- });
1219
-
1220
- it('renders custom component from LinkProvider when href is provided', () => {
1221
- render(
1222
- <LinkProvider component={CustomLink}>
1223
- <SideNavItem label="Dashboard" href="/dashboard" />
1224
- </LinkProvider>,
1225
- );
1226
- const link = screen.getByRole('link');
1227
- expect(link).toHaveAttribute('data-custom-link');
1228
- });
1229
- });
1230
-
1231
- // =============================================================================
1232
- // SideNavSection
1233
- // =============================================================================
1234
-
1235
- describe('SideNavSection', () => {
1236
- it('renders with group role', () => {
1237
- render(
1238
- <SideNavSection title="Main">
1239
- <SideNavItem label="Dashboard" />
1240
- </SideNavSection>,
1241
- );
1242
- expect(screen.getByRole('group')).toBeInTheDocument();
1243
- });
1244
-
1245
- it('renders heading text', () => {
1246
- render(
1247
- <SideNavSection title="Main">
1248
- <SideNavItem label="Dashboard" />
1249
- </SideNavSection>,
1250
- );
1251
- expect(screen.getByText('Main')).toBeInTheDocument();
1252
- });
1253
-
1254
- it('uses aria-labelledby to link title to group', () => {
1255
- render(
1256
- <SideNavSection title="Main">
1257
- <SideNavItem label="Dashboard" />
1258
- </SideNavSection>,
1259
- );
1260
- const group = screen.getByRole('group');
1261
- const labelId = group.getAttribute('aria-labelledby');
1262
- expect(labelId).toBeTruthy();
1263
- const label = document.getElementById(labelId!);
1264
- expect(label).toHaveTextContent('Main');
1265
- });
1266
-
1267
- it('renders subheading', () => {
1268
- render(
1269
- <SideNavSection title="Main" subtitle="Primary navigation">
1270
- <SideNavItem label="Dashboard" />
1271
- </SideNavSection>,
1272
- );
1273
- expect(screen.getByText('Primary navigation')).toBeInTheDocument();
1274
- });
1275
-
1276
- it('renders endContent', () => {
1277
- render(
1278
- <SideNavSection
1279
- title="Main"
1280
- endContent={<span data-testid="section-action">+</span>}>
1281
- <SideNavItem label="Dashboard" />
1282
- </SideNavSection>,
1283
- );
1284
- expect(screen.getByTestId('section-action')).toBeInTheDocument();
1285
- });
1286
-
1287
- it('passes data-testid', () => {
1288
- render(
1289
- <SideNavSection title="Main" data-testid="nav-section">
1290
- <SideNavItem label="Dashboard" />
1291
- </SideNavSection>,
1292
- );
1293
- expect(screen.getByTestId('nav-section')).toBeInTheDocument();
1294
- });
1295
-
1296
- it('forwards className to root element', () => {
1297
- render(
1298
- <SideNavSection title="Main" className="custom-section">
1299
- <SideNavItem label="Dashboard" />
1300
- </SideNavSection>,
1301
- );
1302
- const group = screen.getByRole('group');
1303
- expect(group.className).toContain('custom-section');
1304
- });
1305
-
1306
- it('forwards style to root element', () => {
1307
- render(
1308
- <SideNavSection title="Main" style={{marginTop: 16}}>
1309
- <SideNavItem label="Dashboard" />
1310
- </SideNavSection>,
1311
- );
1312
- const group = screen.getByRole('group');
1313
- expect(group.style.marginTop).toBe('16px');
1314
- });
1315
-
1316
- it('forwards arbitrary pass-through attributes (id, aria-*) to root element', () => {
1317
- render(
1318
- <SideNavSection title="Main" id="section-1" aria-describedby="hint">
1319
- <SideNavItem label="Dashboard" />
1320
- </SideNavSection>,
1321
- );
1322
- const group = screen.getByRole('group');
1323
- expect(group).toHaveAttribute('id', 'section-1');
1324
- expect(group).toHaveAttribute('aria-describedby', 'hint');
1325
- });
1326
- });
1327
-
1328
- // =============================================================================
1329
- // Resizable
1330
- // =============================================================================
1331
-
1332
- describe('SideNav resizable', () => {
1333
- it('renders drag handle when resizable', () => {
1334
- render(<SideNav resizable>Content</SideNav>);
1335
- expect(
1336
- screen.getByTestId('astryx-sidenav-resize-handle'),
1337
- ).toBeInTheDocument();
1338
- });
1339
-
1340
- it('names the nav it resizes with aria-controls (APG window splitter)', () => {
1341
- render(<SideNav resizable>Content</SideNav>);
1342
- const nav = screen.getByRole('navigation');
1343
- const handle = screen.getByRole('separator');
1344
- expect(nav.id).not.toBe('');
1345
- expect(handle).toHaveAttribute('aria-controls', nav.id);
1346
- });
1347
-
1348
- it("keeps a consumer's nav id and points the handle at it", () => {
1349
- render(
1350
- <SideNav resizable id="primary-nav">
1351
- Content
1352
- </SideNav>,
1353
- );
1354
- expect(screen.getByRole('navigation')).toHaveAttribute('id', 'primary-nav');
1355
- expect(screen.getByRole('separator')).toHaveAttribute(
1356
- 'aria-controls',
1357
- 'primary-nav',
1358
- );
1359
- });
1360
-
1361
- it('keeps the generated nav id when a wrapper forwards id={undefined}', () => {
1362
- render(
1363
- <SideNav resizable id={undefined}>
1364
- Content
1365
- </SideNav>,
1366
- );
1367
- const nav = screen.getByRole('navigation');
1368
- expect(nav.id).not.toBe('');
1369
- expect(screen.getByRole('separator')).toHaveAttribute(
1370
- 'aria-controls',
1371
- nav.id,
1372
- );
1373
- });
1374
-
1375
- it('clips the overlay drag handle within the resizable nav bounds', () => {
1376
- render(<SideNav resizable>Content</SideNav>);
1377
- const nav = screen.getByRole('navigation');
1378
- const container = nav.parentElement;
1379
- const handle = screen.getByTestId('astryx-sidenav-resize-handle');
1380
-
1381
- expect(container).toContainElement(handle);
1382
- expect(getComputedStyle(container!).overflow).toBe('clip');
1383
- });
1384
-
1385
- it('does not render drag handle without resizable', () => {
1386
- render(<SideNav>Content</SideNav>);
1387
- expect(
1388
- screen.queryByTestId('astryx-sidenav-resize-handle'),
1389
- ).not.toBeInTheDocument();
1390
- });
1391
-
1392
- it('does not render drag handle when collapsed', () => {
1393
- render(
1394
- <SideNav
1395
- resizable
1396
- collapsible={{isCollapsed: true, onCollapsedChange: () => {}}}>
1397
- Content
1398
- </SideNav>,
1399
- );
1400
- expect(
1401
- screen.queryByTestId('astryx-sidenav-resize-handle'),
1402
- ).not.toBeInTheDocument();
1403
- });
1404
-
1405
- it('calls onWidthChange after drag', () => {
1406
- const handleWidthChange = vi.fn();
1407
- render(
1408
- <SideNav resizable={{onWidthChange: handleWidthChange}}>Content</SideNav>,
1409
- );
1410
- const handle = screen.getByTestId('astryx-sidenav-resize-handle');
1411
- // The pointer event handler is on the hit area child inside the handle.
1412
- const hitArea = handle.firstElementChild as HTMLElement;
1413
-
1414
- act(() => {
1415
- fireEvent.pointerDown(hitArea, {pointerId: 1, clientX: 260});
1416
- fireEvent.pointerMove(hitArea, {pointerId: 1, clientX: 310});
1417
- fireEvent.pointerUp(hitArea, {pointerId: 1, clientX: 310});
1418
- });
1419
-
1420
- expect(handleWidthChange).toHaveBeenCalledTimes(1);
1421
- expect(handleWidthChange).toHaveBeenCalledWith(expect.any(Number));
1422
- });
1423
-
1424
- it('respects defaultWidth', () => {
1425
- render(<SideNav resizable={{defaultWidth: 300}}>Content</SideNav>);
1426
- const nav = screen.getByRole('navigation');
1427
- expect(nav.style.width).toBe('300px');
1428
- });
1429
-
1430
- it('drag handle has separator role', () => {
1431
- render(<SideNav resizable>Content</SideNav>);
1432
- expect(screen.getByRole('separator')).toBeInTheDocument();
1433
- });
1434
- });
1435
-
1436
- // =============================================================================
1437
- // Integration
1438
- // =============================================================================
1439
-
1440
- describe('SideNav integration', () => {
1441
- it('renders a complete page nav', () => {
1442
- render(
1443
- <SideNav
1444
- header={<SideNavHeading heading="My App" />}
1445
- topContent={<button type="button">Create</button>}
1446
- footer={<div data-testid="promo">Promo</div>}
1447
- footerIcons={<button type="button">Help</button>}>
1448
- <SideNavSection title="Main">
1449
- <SideNavItem label="Dashboard" isSelected />
1450
- <SideNavItem label="Projects" />
1451
- </SideNavSection>
1452
- <SideNavSection title="Settings">
1453
- <SideNavItem label="General" />
1454
- </SideNavSection>
1455
- </SideNav>,
1456
- );
1457
-
1458
- expect(screen.getByRole('navigation')).toBeInTheDocument();
1459
- expect(screen.getByText('My App')).toBeInTheDocument();
1460
- expect(screen.getByText('Dashboard')).toBeInTheDocument();
1461
- expect(screen.getByText('Projects')).toBeInTheDocument();
1462
- expect(screen.getByText('General')).toBeInTheDocument();
1463
- expect(screen.getByTestId('promo')).toBeInTheDocument();
1464
- });
1465
- });
1466
-
1467
- // Stub icon for testing
1468
- const StubIcon = () => <svg data-testid="stub-icon" />;
1469
-
1470
- /** Helper to render inside a collapsed SideNav context */
1471
- function renderCollapsed(ui: React.ReactElement) {
1472
- return render(
1473
- <SideNavCollapseContext
1474
- value={{isCollapsed: true, toggle: () => {}, isCollapsible: true}}>
1475
- {ui}
1476
- </SideNavCollapseContext>,
1477
- );
1478
- }
1479
-
1480
- /** Helper to render inside an expanded SideNav context */
1481
- function renderExpanded(ui: React.ReactElement) {
1482
- return render(
1483
- <SideNavCollapseContext
1484
- value={{isCollapsed: false, toggle: () => {}, isCollapsible: true}}>
1485
- {ui}
1486
- </SideNavCollapseContext>,
1487
- );
1488
- }
1489
-
1490
- // =============================================================================
1491
- // SideNavItem — Collapsed mode
1492
- // =============================================================================
1493
-
1494
- describe('SideNavItem (collapsed)', () => {
1495
- it('hides items without icons when collapsed', () => {
1496
- const {container} = renderCollapsed(<SideNavItem label="No Icon Item" />);
1497
- expect(screen.queryByText('No Icon Item')).not.toBeInTheDocument();
1498
- expect(container.querySelector('[data-xds="side-nav-item"]')).toBeNull();
1499
- });
1500
-
1501
- it('renders icon-only button when collapsed with icon and no children', () => {
1502
- renderCollapsed(
1503
- <SideNavItem label="Dashboard" icon={StubIcon} data-testid="item" />,
1504
- );
1505
- // Should have an element with aria-label (icon-only)
1506
- const item = screen.getByLabelText('Dashboard');
1507
- expect(item).toBeInTheDocument();
1508
- // Icon should be rendered
1509
- expect(screen.getByTestId('stub-icon')).toBeInTheDocument();
1510
- });
1511
-
1512
- it('renders collapsed link when href is provided', () => {
1513
- renderCollapsed(
1514
- <SideNavItem label="Dashboard" icon={StubIcon} href="/dashboard" />,
1515
- );
1516
- const link = screen.getByRole('link');
1517
- expect(link).toHaveAttribute('href', '/dashboard');
1518
- expect(link).toHaveAttribute('aria-label', 'Dashboard');
1519
- });
1520
-
1521
- it('renders popover trigger when collapsed with icon and children', () => {
1522
- renderCollapsed(
1523
- <SideNavItem label="Settings" icon={StubIcon} data-testid="parent">
1524
- <SideNavItem label="General" />
1525
- <SideNavItem label="Security" />
1526
- </SideNavItem>,
1527
- );
1528
- const trigger = screen.getByTestId('parent');
1529
- expect(trigger).toHaveAttribute('aria-haspopup', 'dialog');
1530
- expect(trigger).toHaveAttribute('aria-expanded', 'false');
1531
- expect(trigger).toHaveAttribute('aria-label', 'Settings');
1532
- });
1533
-
1534
- describe('consumer aria-label (#5641)', () => {
1535
- it('keeps a consumer aria-label on the collapsed link, instead of falling back to label', () => {
1536
- renderCollapsed(
1537
- <SideNavItem
1538
- label="Home"
1539
- icon={StubIcon}
1540
- href="/home"
1541
- aria-label="Open Home, 3 items need attention"
1542
- />,
1543
- );
1544
- expect(screen.getByRole('link')).toHaveAttribute(
1545
- 'aria-label',
1546
- 'Open Home, 3 items need attention',
1547
- );
1548
- });
1549
-
1550
- it('keeps a consumer aria-label on the collapsed button, instead of falling back to label', () => {
1551
- renderCollapsed(
1552
- <SideNavItem
1553
- label="Home"
1554
- icon={StubIcon}
1555
- aria-label="Open Home, 3 items need attention"
1556
- />,
1557
- );
1558
- expect(screen.getByRole('button')).toHaveAttribute(
1559
- 'aria-label',
1560
- 'Open Home, 3 items need attention',
1561
- );
1562
- });
1563
-
1564
- it('keeps a consumer aria-label on the collapsed popover trigger, instead of falling back to label', () => {
1565
- renderCollapsed(
1566
- <SideNavItem
1567
- label="Settings"
1568
- icon={StubIcon}
1569
- data-testid="parent"
1570
- aria-label="Settings, 2 updates available">
1571
- <SideNavItem label="General" />
1572
- </SideNavItem>,
1573
- );
1574
- expect(screen.getByTestId('parent')).toHaveAttribute(
1575
- 'aria-label',
1576
- 'Settings, 2 updates available',
1577
- );
1578
- });
1579
-
1580
- it('falls back to label when a consumer aria-label is empty or whitespace', () => {
1581
- const {rerender} = renderCollapsed(
1582
- <SideNavItem
1583
- label="Dashboard"
1584
- icon={StubIcon}
1585
- href="/dashboard"
1586
- aria-label=""
1587
- />,
1588
- );
1589
- expect(screen.getByRole('link')).toHaveAttribute(
1590
- 'aria-label',
1591
- 'Dashboard',
1592
- );
1593
-
1594
- rerender(
1595
- <SideNavCollapseContext value={COLLAPSED_CONTEXT}>
1596
- <SideNavItem label="Dashboard" icon={StubIcon} aria-label=" " />
1597
- </SideNavCollapseContext>,
1598
- );
1599
- expect(screen.getByRole('button')).toHaveAttribute(
1600
- 'aria-label',
1601
- 'Dashboard',
1602
- );
1603
- });
1604
-
1605
- it('still falls back to label when no consumer aria-label is given', () => {
1606
- renderCollapsed(
1607
- <SideNavItem label="Dashboard" icon={StubIcon} href="/dashboard" />,
1608
- );
1609
- expect(screen.getByRole('link')).toHaveAttribute(
1610
- 'aria-label',
1611
- 'Dashboard',
1612
- );
1613
- });
1614
- });
1615
-
1616
- it('opens popover on click showing children in expanded form', async () => {
1617
- const user = userEvent.setup();
1618
- renderCollapsed(
1619
- <SideNavItem label="Settings" icon={StubIcon} data-testid="parent">
1620
- <SideNavItem label="General" data-testid="child-general" />
1621
- <SideNavItem label="Security" data-testid="child-security" />
1622
- </SideNavItem>,
1623
- );
1624
- await user.click(screen.getByTestId('parent'));
1625
-
1626
- // Children should be visible in expanded form (label text visible)
1627
- expect(screen.getByText('General')).toBeInTheDocument();
1628
- expect(screen.getByText('Security')).toBeInTheDocument();
1629
- });
1630
-
1631
- it('shows parent label as header in the popover', async () => {
1632
- const user = userEvent.setup();
1633
- renderCollapsed(
1634
- <SideNavItem label="Settings" icon={StubIcon} data-testid="parent">
1635
- <SideNavItem label="General" />
1636
- </SideNavItem>,
1637
- );
1638
- await user.click(screen.getByTestId('parent'));
1639
- expect(screen.getByText('Settings')).toBeInTheDocument();
1640
- });
1641
-
1642
- it('does not render children without icon when collapsed', () => {
1643
- renderCollapsed(
1644
- <SideNavItem label="Settings">
1645
- <SideNavItem label="General" />
1646
- <SideNavItem label="Security" />
1647
- </SideNavItem>,
1648
- );
1649
- expect(screen.queryByText('Settings')).not.toBeInTheDocument();
1650
- expect(screen.queryByText('General')).not.toBeInTheDocument();
1651
- });
1652
-
1653
- it('renders normally when not collapsed', () => {
1654
- renderExpanded(
1655
- <SideNavItem label="Dashboard" icon={StubIcon}>
1656
- <SideNavItem label="General" />
1657
- </SideNavItem>,
1658
- );
1659
- expect(screen.getAllByText('Dashboard').length).toBeGreaterThanOrEqual(1);
1660
- expect(screen.getByText('General')).toBeInTheDocument();
1661
- expect(screen.getByRole('group')).toBeInTheDocument();
1662
- });
1663
- });
1664
-
1665
- // =============================================================================
1666
- // SideNavItem — collapsed submenu hover intent (useMenuHover adoption, C12)
1667
- // =============================================================================
1668
-
1669
- /**
1670
- * Pin the ambient pointer modality: the global `matchMedia` stub never
1671
- * matches, which reads as a coarse pointer, so hover assertions must opt in.
1672
- */
1673
- function stubPointerModality(modality: 'fine' | 'coarse') {
1674
- vi.stubGlobal('matchMedia', (query: string) => ({
1675
- matches: modality === 'fine' && query.includes('hover: hover'),
1676
- media: query,
1677
- onchange: null,
1678
- addListener: () => {},
1679
- removeListener: () => {},
1680
- addEventListener: () => {},
1681
- removeEventListener: () => {},
1682
- dispatchEvent: () => false,
1683
- }));
1684
- }
1685
-
1686
- /** The hook's hover-intent delays: 150 ms to show, 200 ms to hide. */
1687
- const SHOW_DELAY = 150;
1688
- const HIDE_DELAY = 200;
1689
-
1690
- describe('SideNavItem — collapsed submenu hover intent', () => {
1691
- afterEach(() => {
1692
- vi.useRealTimers();
1693
- vi.unstubAllGlobals();
1694
- });
1695
-
1696
- function renderFlyout() {
1697
- renderCollapsed(
1698
- <SideNavItem label="Settings" icon={StubIcon} data-testid="parent">
1699
- <SideNavItem label="General" href="/settings/general" />
1700
- <SideNavItem label="Security" href="/settings/security" />
1701
- </SideNavItem>,
1702
- );
1703
- return screen.getByTestId('parent');
1704
- }
1705
-
1706
- const advance = (ms: number) =>
1707
- act(() => {
1708
- vi.advanceTimersByTime(ms);
1709
- });
1710
-
1711
- it('opens the flyout only after the hover-intent delay', () => {
1712
- stubPointerModality('fine');
1713
- vi.useFakeTimers();
1714
- const trigger = renderFlyout();
1715
-
1716
- fireEvent.mouseEnter(trigger);
1717
- // A pointer merely passing over the icon must not open anything.
1718
- advance(SHOW_DELAY - 1);
1719
- expect(trigger).toHaveAttribute('aria-expanded', 'false');
1720
-
1721
- advance(1);
1722
- expect(trigger).toHaveAttribute('aria-expanded', 'true');
1723
- });
1724
-
1725
- it('closes a hover-opened flyout when the pointer leaves', () => {
1726
- stubPointerModality('fine');
1727
- vi.useFakeTimers();
1728
- const trigger = renderFlyout();
1729
-
1730
- fireEvent.mouseEnter(trigger);
1731
- advance(SHOW_DELAY);
1732
- expect(trigger).toHaveAttribute('aria-expanded', 'true');
1733
-
1734
- fireEvent.mouseLeave(trigger);
1735
- advance(HIDE_DELAY);
1736
- expect(trigger).toHaveAttribute('aria-expanded', 'false');
1737
- });
1738
-
1739
- it('keeps a click-opened flyout open when the pointer leaves', () => {
1740
- stubPointerModality('fine');
1741
- vi.useFakeTimers();
1742
- const trigger = renderFlyout();
1743
-
1744
- // `detail: 1` marks a pointer click: the hook reads a detail-0 click as
1745
- // Enter/Space, which always opens rather than toggling.
1746
- fireEvent.click(trigger, {detail: 1});
1747
- expect(trigger).toHaveAttribute('aria-expanded', 'true');
1748
-
1749
- // Clicking is a commitment: the flyout outlives the pointer.
1750
- fireEvent.mouseLeave(trigger);
1751
- advance(HIDE_DELAY * 2);
1752
- expect(trigger).toHaveAttribute('aria-expanded', 'true');
1753
- });
1754
-
1755
- it('does not spring back open when a click dismisses it under a stationary pointer', () => {
1756
- stubPointerModality('fine');
1757
- vi.useFakeTimers();
1758
- const trigger = renderFlyout();
1759
-
1760
- fireEvent.mouseEnter(trigger);
1761
- advance(SHOW_DELAY);
1762
- expect(trigger).toHaveAttribute('aria-expanded', 'true');
1763
-
1764
- // The pointer never left, so dismissing fires a fresh mouseenter as the
1765
- // flyout unmounts — which used to reopen what the user just closed.
1766
- fireEvent.click(trigger, {detail: 1});
1767
- expect(trigger).toHaveAttribute('aria-expanded', 'false');
1768
-
1769
- fireEvent.mouseEnter(trigger);
1770
- advance(SHOW_DELAY);
1771
- expect(trigger).toHaveAttribute('aria-expanded', 'false');
1772
-
1773
- // One-shot, not a latch: a deliberate leave-and-return opens it again.
1774
- fireEvent.mouseLeave(trigger);
1775
- fireEvent.mouseEnter(trigger);
1776
- advance(SHOW_DELAY);
1777
- expect(trigger).toHaveAttribute('aria-expanded', 'true');
1778
- });
1779
-
1780
- it('schedules no hover timers on a coarse pointer', () => {
1781
- stubPointerModality('coarse');
1782
- vi.useFakeTimers();
1783
- const trigger = renderFlyout();
1784
-
1785
- // A touch tap synthesises mouseenter. On a device that cannot hover there
1786
- // is no hover intent to read, so nothing may be scheduled from it.
1787
- fireEvent.mouseEnter(trigger);
1788
- expect(vi.getTimerCount()).toBe(0);
1789
-
1790
- advance(SHOW_DELAY * 2);
1791
- expect(trigger).toHaveAttribute('aria-expanded', 'false');
1792
-
1793
- // Tapping still opens it — the touch path goes through click, untouched.
1794
- fireEvent.click(trigger, {detail: 1});
1795
- expect(trigger).toHaveAttribute('aria-expanded', 'true');
1796
- });
1797
-
1798
- it('does not open the hover popover after the item unmounts', () => {
1799
- stubPointerModality('fine');
1800
- vi.useFakeTimers();
1801
- const {unmount} = render(
1802
- <SideNavCollapseContext
1803
- value={{isCollapsed: true, toggle: () => {}, isCollapsible: true}}>
1804
- <SideNavItem label="Settings" icon={StubIcon} data-testid="parent">
1805
- <SideNavItem label="General" href="/settings/general" />
1806
- </SideNavItem>
1807
- </SideNavCollapseContext>,
1808
- );
1809
-
1810
- fireEvent.mouseEnter(screen.getByTestId('parent'));
1811
- expect(vi.getTimerCount()).toBeGreaterThan(0);
1812
-
1813
- unmount();
1814
-
1815
- // The hook owns the timer cleanup now; it still has to hold.
1816
- expect(vi.getTimerCount()).toBe(0);
1817
- });
1818
-
1819
- it('leaves arrow keys in the flyout to the browser', async () => {
1820
- stubPointerModality('fine');
1821
- const user = userEvent.setup();
1822
- renderCollapsed(
1823
- <SideNavItem label="Settings" icon={StubIcon} data-testid="parent">
1824
- <SideNavItem label="General" href="/settings/general" />
1825
- <SideNavItem label="Security" href="/settings/security" />
1826
- </SideNavItem>,
1827
- );
1828
-
1829
- const trigger = screen.getByTestId('parent');
1830
- await user.click(trigger);
1831
-
1832
- // The flyout renders as a native popover, which jsdom leaves
1833
- // `display: none`, so its contents are outside the default a11y-tree
1834
- // queries.
1835
- const first = screen.getByRole('link', {name: 'General', hidden: true});
1836
- first.focus();
1837
-
1838
- // A dialog of links, not a menu of menuitems: wiring the hook's
1839
- // list-focus half would preventDefault every arrow key and find no
1840
- // `[role="menuitem"]` to move to.
1841
- const notPrevented = fireEvent.keyDown(first, {key: 'ArrowDown'});
1842
- expect(notPrevented).toBe(true);
1843
- expect(first).toHaveFocus();
1844
- expect(trigger).toHaveAttribute('aria-expanded', 'true');
1845
- });
1846
-
1847
- it('opens from the keyboard, moves focus into the flyout, and restores it on Escape', async () => {
1848
- stubPointerModality('fine');
1849
- const user = userEvent.setup();
1850
- renderCollapsed(
1851
- <SideNavItem label="Settings" icon={StubIcon} data-testid="parent">
1852
- <SideNavItem label="General" href="/settings/general" />
1853
- <SideNavItem label="Security" href="/settings/security" />
1854
- </SideNavItem>,
1855
- );
1856
-
1857
- const trigger = screen.getByTestId('parent');
1858
- trigger.focus();
1859
- await user.keyboard('{Enter}');
1860
- expect(trigger).toHaveAttribute('aria-expanded', 'true');
1861
-
1862
- // Keyboard opens move focus in; hover opens leave it on the trigger.
1863
- await waitFor(() =>
1864
- expect(
1865
- screen.getByRole('link', {name: 'General', hidden: true}),
1866
- ).toHaveFocus(),
1867
- );
1868
-
1869
- await user.keyboard('{Escape}');
1870
- expect(trigger).toHaveAttribute('aria-expanded', 'false');
1871
- await waitFor(() => expect(trigger).toHaveFocus());
1872
- });
1873
-
1874
- it('leaves focus on the trigger when hover opens the flyout', () => {
1875
- stubPointerModality('fine');
1876
- vi.useFakeTimers();
1877
- const trigger = renderFlyout();
1878
- trigger.focus();
1879
-
1880
- fireEvent.mouseEnter(trigger);
1881
- advance(SHOW_DELAY);
1882
- act(() => {
1883
- vi.advanceTimersToNextFrame();
1884
- });
1885
-
1886
- // `skipAutoFocus`: a mid-hover pointer user keeps their caret.
1887
- expect(trigger).toHaveAttribute('aria-expanded', 'true');
1888
- expect(trigger).toHaveFocus();
1889
- });
1890
- });
1891
-
1892
- // =============================================================================
1893
- // Mobile nav close-on-activate
1894
- // =============================================================================
1895
-
1896
- // =============================================================================
1897
- // SideNavItem — collapsible + href (independent toggle)
1898
- // =============================================================================
1899
-
1900
- describe('SideNavItem — collapsible + href', () => {
1901
- it('renders a link that navigates when both collapsible and href are set', () => {
1902
- render(
1903
- <SideNavItem
1904
- label="Settings"
1905
- href="/settings"
1906
- collapsible
1907
- data-testid="parent">
1908
- <SideNavItem label="General" href="/settings/general" />
1909
- </SideNavItem>,
1910
- );
1911
- const link = screen.getByRole('link', {name: 'Settings'});
1912
- expect(link).toHaveAttribute('href', '/settings');
1913
- });
1914
-
1915
- it('renders a separate toggle button for the chevron', () => {
1916
- render(
1917
- <SideNavItem label="Settings" href="/settings" collapsible>
1918
- <SideNavItem label="General" href="/settings/general" />
1919
- </SideNavItem>,
1920
- );
1921
- const toggle = screen.getByRole('button', {name: /collapse settings/i});
1922
- expect(toggle).toBeInTheDocument();
1923
- expect(toggle).toHaveAttribute('aria-expanded', 'true');
1924
- });
1925
-
1926
- it('toggle button collapses children without navigating', async () => {
1927
- const user = userEvent.setup();
1928
- render(
1929
- <SideNavItem label="Settings" href="/settings" collapsible>
1930
- <SideNavItem label="General" href="/settings/general" />
1931
- </SideNavItem>,
1932
- );
1933
- const toggle = screen.getByRole('button', {name: /collapse settings/i});
1934
- await user.click(toggle);
1935
- // After collapsing, aria-hidden on children container
1936
- expect(toggle).toHaveAttribute('aria-expanded', 'false');
1937
- expect(toggle).toHaveAccessibleName('Expand Settings');
1938
- });
1939
-
1940
- it('link does not toggle collapse when clicked', async () => {
1941
- const user = userEvent.setup();
1942
- const onClick = vi.fn();
1943
- render(
1944
- <SideNavItem
1945
- label="Settings"
1946
- href="/settings"
1947
- collapsible
1948
- onClick={onClick}>
1949
- <SideNavItem label="General" href="/settings/general" />
1950
- </SideNavItem>,
1951
- );
1952
- const link = screen.getByRole('link', {name: 'Settings'});
1953
- await user.click(link);
1954
- expect(onClick).toHaveBeenCalledTimes(1);
1955
- // Children should still be visible (not collapsed)
1956
- const toggle = screen.getByRole('button', {name: /collapse settings/i});
1957
- expect(toggle).toHaveAttribute('aria-expanded', 'true');
1958
- });
1959
-
1960
- it('link does not have aria-expanded (toggle button owns it)', () => {
1961
- render(
1962
- <SideNavItem label="Settings" href="/settings" collapsible>
1963
- <SideNavItem label="General" href="/settings/general" />
1964
- </SideNavItem>,
1965
- );
1966
- const link = screen.getByRole('link', {name: 'Settings'});
1967
- expect(link).not.toHaveAttribute('aria-expanded');
1968
- });
1969
-
1970
- it('without href or onClick, clicking the item toggles collapse', async () => {
1971
- const user = userEvent.setup();
1972
- render(
1973
- <SideNavItem label="Settings" collapsible>
1974
- <SideNavItem label="General" />
1975
- </SideNavItem>,
1976
- );
1977
- const button = screen.getByRole('button', {name: 'Settings'});
1978
- expect(button).toHaveAttribute('aria-expanded', 'true');
1979
- await user.click(button);
1980
- expect(button).toHaveAttribute('aria-expanded', 'false');
1981
- });
1982
-
1983
- it('with onClick (no href), clicking the label fires onClick', async () => {
1984
- const user = userEvent.setup();
1985
- const onClick = vi.fn();
1986
- render(
1987
- <SideNavItem label="Settings" onClick={onClick} collapsible>
1988
- <SideNavItem label="General" />
1989
- </SideNavItem>,
1990
- );
1991
- const primaryButton = screen.getByRole('button', {name: 'Settings'});
1992
- await user.click(primaryButton);
1993
- expect(onClick).toHaveBeenCalledTimes(1);
1994
- // Children should still be visible
1995
- const toggle = screen.getByRole('button', {name: /collapse settings/i});
1996
- expect(toggle).toHaveAttribute('aria-expanded', 'true');
1997
- });
1998
-
1999
- it('with onClick (no href), toggle collapses without firing onClick', async () => {
2000
- const user = userEvent.setup();
2001
- const onClick = vi.fn();
2002
- render(
2003
- <SideNavItem label="Settings" onClick={onClick} collapsible>
2004
- <SideNavItem label="General" />
2005
- </SideNavItem>,
2006
- );
2007
- const toggle = screen.getByRole('button', {name: /collapse settings/i});
2008
- await user.click(toggle);
2009
- expect(onClick).not.toHaveBeenCalled();
2010
- expect(toggle).toHaveAttribute('aria-expanded', 'false');
2011
- });
2012
-
2013
- it('collapsed children are inert (not focusable)', async () => {
2014
- const user = userEvent.setup();
2015
- render(
2016
- <SideNavItem label="Settings" collapsible>
2017
- <SideNavItem label="General" href="/settings/general" />
2018
- </SideNavItem>,
2019
- );
2020
- // Collapse the item
2021
- const button = screen.getByRole('button', {name: 'Settings'});
2022
- await user.click(button);
2023
- // The children container should have inert attribute
2024
- const childrenContainer = document.getElementById(
2025
- button.getAttribute('aria-controls')!,
2026
- );
2027
- expect(childrenContainer).toHaveAttribute('inert');
2028
- });
2029
- });
2030
-
2031
- // =============================================================================
2032
- // SideNavItem — actions slot (row-level secondary controls, #4988)
2033
- // =============================================================================
2034
-
2035
- describe('SideNavItem — actions slot', () => {
2036
- const rowAction = (onClick?: (e: React.MouseEvent) => void) => (
2037
- <button type="button" data-testid="row-action" onClick={onClick}>
2038
- ⋯
2039
- </button>
2040
- );
2041
-
2042
- it('renders actions outside the primary interactive element', () => {
2043
- render(
2044
- <SideNavItem label="Project" href="/project" actions={rowAction()} />,
2045
- );
2046
- const action = screen.getByTestId('row-action');
2047
- const link = screen.getByRole('link', {name: 'Project'});
2048
- // Sibling, not nested: the action's closest interactive element is itself.
2049
- expect(within(link).queryByTestId('row-action')).toBeNull();
2050
- expect(action.closest('a, button')).toBe(action);
2051
- });
2052
-
2053
- it('places actions after the primary element and before nested children', () => {
2054
- render(
2055
- <SideNavItem
2056
- label="Project"
2057
- href="/project"
2058
- collapsible
2059
- actions={rowAction()}>
2060
- <SideNavItem label="Session" href="/project/session" />
2061
- </SideNavItem>,
2062
- );
2063
- const primary = screen.getByRole('link', {name: 'Project'});
2064
- const action = screen.getByTestId('row-action');
2065
- const group = screen.getByRole('group');
2066
- // Compare real node positions, not serialized markup.
2067
- expect(
2068
- primary.compareDocumentPosition(action) &
2069
- Node.DOCUMENT_POSITION_FOLLOWING,
2070
- ).toBeTruthy();
2071
- expect(
2072
- action.compareDocumentPosition(group) & Node.DOCUMENT_POSITION_FOLLOWING,
2073
- ).toBeTruthy();
2074
- });
2075
-
2076
- it('reaches every row-level control before nested items when tabbing', async () => {
2077
- const user = userEvent.setup();
2078
- render(
2079
- <SideNavItem
2080
- label="Project"
2081
- href="/project"
2082
- collapsible
2083
- actions={rowAction()}>
2084
- <SideNavItem label="Session" href="/project/session" />
2085
- </SideNavItem>,
2086
- );
2087
- await user.tab();
2088
- expect(screen.getByRole('link', {name: 'Project'})).toHaveFocus();
2089
- await user.tab();
2090
- expect(
2091
- screen.getByRole('button', {name: /collapse project/i}),
2092
- ).toHaveFocus();
2093
- await user.tab();
2094
- expect(screen.getByTestId('row-action')).toHaveFocus();
2095
- await user.tab();
2096
- expect(screen.getByRole('link', {name: 'Session'})).toHaveFocus();
2097
- });
2098
-
2099
- it('clicking an action does not activate the item', async () => {
2100
- const user = userEvent.setup();
2101
- const onItemClick = vi.fn();
2102
- const onActionClick = vi.fn();
2103
- render(
2104
- <SideNavItem
2105
- label="Project"
2106
- onClick={onItemClick}
2107
- actions={rowAction(onActionClick)}
2108
- />,
2109
- );
2110
- await user.click(screen.getByTestId('row-action'));
2111
- expect(onActionClick).toHaveBeenCalledTimes(1);
2112
- expect(onItemClick).not.toHaveBeenCalled();
2113
- });
2114
-
2115
- it('clicking an action does not toggle collapse of a whole-row toggle item', async () => {
2116
- const user = userEvent.setup();
2117
- render(
2118
- <SideNavItem label="Project" collapsible actions={rowAction()}>
2119
- <SideNavItem label="Session" />
2120
- </SideNavItem>,
2121
- );
2122
- const rowToggle = screen.getByRole('button', {name: 'Project'});
2123
- expect(rowToggle).toHaveAttribute('aria-expanded', 'true');
2124
- await user.click(screen.getByTestId('row-action'));
2125
- expect(rowToggle).toHaveAttribute('aria-expanded', 'true');
2126
- });
2127
-
2128
- it('whole-row toggle still collapses when actions are present', async () => {
2129
- const user = userEvent.setup();
2130
- render(
2131
- <SideNavItem label="Project" collapsible actions={rowAction()}>
2132
- <SideNavItem label="Session" />
2133
- </SideNavItem>,
2134
- );
2135
- expect(screen.getByTestId('row-action')).toBeInTheDocument();
2136
- const rowToggle = screen.getByRole('button', {name: 'Project'});
2137
- await user.click(rowToggle);
2138
- expect(rowToggle).toHaveAttribute('aria-expanded', 'false');
2139
- });
2140
-
2141
- it('adds a row wrapper only when actions are present', () => {
2142
- const {unmount} = render(<SideNavItem label="Project" href="/project" />);
2143
- // Without actions, the link itself is the styled row element.
2144
- expect(screen.getByRole('link', {name: 'Project'})).toHaveClass(
2145
- 'astryx-side-nav-item',
2146
- );
2147
- unmount();
2148
-
2149
- render(
2150
- <SideNavItem
2151
- label="Project"
2152
- href="/project"
2153
- data-testid="row"
2154
- actions={rowAction()}
2155
- />,
2156
- );
2157
- // With actions, a wrapper div is the styled row; the link sits inside it.
2158
- const link = screen.getByRole('link', {name: 'Project'});
2159
- expect(link).not.toHaveClass('astryx-side-nav-item');
2160
- const row = screen.getByTestId('row');
2161
- expect(row).toHaveClass('astryx-side-nav-item');
2162
- expect(row).toContainElement(link);
2163
- expect(row).toContainElement(screen.getByTestId('row-action'));
2164
- });
2165
-
2166
- it('adds no row wrapper for a falsy actions value', () => {
2167
- // `actions={canEdit ? <Menu /> : null}` is the ordinary consumer shape.
2168
- // A wrapper there would change markup, focus order and the ring's
2169
- // owner for a row that has no row controls at all.
2170
- const {rerender} = render(
2171
- <SideNavItem label="Project" href="/project" actions={null} />,
2172
- );
2173
- expect(screen.getByRole('link', {name: 'Project'})).toHaveClass(
2174
- 'astryx-side-nav-item',
2175
- );
2176
-
2177
- rerender(<SideNavItem label="Project" href="/project" actions={false} />);
2178
- expect(screen.getByRole('link', {name: 'Project'})).toHaveClass(
2179
- 'astryx-side-nav-item',
2180
- );
2181
- });
2182
-
2183
- it('keeps aria wiring on the toggle when actions are present', () => {
2184
- render(
2185
- <SideNavItem
2186
- label="Project"
2187
- href="/project"
2188
- collapsible
2189
- actions={rowAction()}>
2190
- <SideNavItem label="Session" />
2191
- </SideNavItem>,
2192
- );
2193
- expect(screen.getByTestId('row-action')).toBeInTheDocument();
2194
- const toggle = screen.getByRole('button', {name: /collapse project/i});
2195
- const group = screen.getByRole('group');
2196
- expect(toggle).toHaveAttribute('aria-expanded', 'true');
2197
- expect(toggle).toHaveAttribute('aria-controls', group.id);
2198
- expect(screen.getByRole('link', {name: 'Project'})).not.toHaveAttribute(
2199
- 'aria-expanded',
2200
- );
2201
- });
2202
-
2203
- it('keeps endContent inside the primary element when actions are present', () => {
2204
- render(
2205
- <SideNavItem
2206
- label="Project"
2207
- href="/project"
2208
- endContent={<span data-testid="badge">3</span>}
2209
- actions={rowAction()}
2210
- />,
2211
- );
2212
- const link = screen.getByRole('link', {name: /Project/});
2213
- expect(within(link).getByTestId('badge')).toBeInTheDocument();
2214
- expect(within(link).queryByTestId('row-action')).toBeNull();
2215
- expect(screen.getByTestId('row-action')).toBeInTheDocument();
2216
- });
2217
-
2218
- it('keeps collapsed children inert while the action stays in the row', () => {
2219
- render(
2220
- <SideNavItem
2221
- label="Project"
2222
- collapsible={{defaultIsCollapsed: true}}
2223
- actions={rowAction()}>
2224
- <SideNavItem label="Session" href="/project/session" />
2225
- </SideNavItem>,
2226
- );
2227
- const rowToggle = screen.getByRole('button', {name: 'Project'});
2228
- const group = document.getElementById(
2229
- rowToggle.getAttribute('aria-controls')!,
2230
- );
2231
- expect(group).toHaveAttribute('inert');
2232
- expect(screen.getByTestId('row-action')).toBeInTheDocument();
2233
- });
2234
-
2235
- it('does not render actions in the collapsed rail', () => {
2236
- const {unmount} = renderCollapsed(
2237
- <SideNavItem
2238
- label="Project"
2239
- icon={StubIcon}
2240
- href="/project"
2241
- actions={rowAction()}
2242
- />,
2243
- );
2244
- expect(screen.getByLabelText('Project')).toBeInTheDocument();
2245
- expect(screen.queryByTestId('row-action')).toBeNull();
2246
- unmount();
2247
-
2248
- renderCollapsed(
2249
- <SideNavItem label="Project" icon={StubIcon} actions={rowAction()}>
2250
- <SideNavItem label="Session" href="/project/session" />
2251
- </SideNavItem>,
2252
- );
2253
- expect(screen.getByLabelText('Project')).toBeInTheDocument();
2254
- expect(screen.queryByTestId('row-action')).toBeNull();
2255
- });
2256
-
2257
- it('keeps actions clickable on a disabled item', async () => {
2258
- const user = userEvent.setup();
2259
- const onActionClick = vi.fn();
2260
- render(
2261
- <SideNavItem
2262
- label="Project"
2263
- href="/project"
2264
- isDisabled
2265
- actions={rowAction(onActionClick)}
2266
- />,
2267
- );
2268
- // The row's disabled styling must not swallow the sibling action:
2269
- // the action control owns its own disabled state.
2270
- await user.click(screen.getByTestId('row-action'));
2271
- expect(onActionClick).toHaveBeenCalledTimes(1);
2272
- });
2273
-
2274
- it('places actions after the toggle when endContent is also present', () => {
2275
- render(
2276
- <SideNavItem
2277
- label="Project"
2278
- href="/project"
2279
- collapsible
2280
- endContent={<span data-testid="badge">3</span>}
2281
- actions={rowAction()}>
2282
- <SideNavItem label="Session" href="/project/session" />
2283
- </SideNavItem>,
2284
- );
2285
- const link = screen.getByRole('link', {name: /Project/});
2286
- const toggle = screen.getByRole('button', {name: /collapse project/i});
2287
- const action = screen.getByTestId('row-action');
2288
- expect(within(link).getByTestId('badge')).toBeInTheDocument();
2289
- expect(
2290
- link.compareDocumentPosition(toggle) & Node.DOCUMENT_POSITION_FOLLOWING,
2291
- ).toBeTruthy();
2292
- expect(
2293
- toggle.compareDocumentPosition(action) & Node.DOCUMENT_POSITION_FOLLOWING,
2294
- ).toBeTruthy();
2295
- });
2296
-
2297
- it('reaches the action after the whole-row toggle and before nested items', async () => {
2298
- const user = userEvent.setup();
2299
- render(
2300
- <SideNavItem label="Project" collapsible actions={rowAction()}>
2301
- <SideNavItem label="Session" href="/project/session" />
2302
- </SideNavItem>,
2303
- );
2304
- await user.tab();
2305
- expect(screen.getByRole('button', {name: 'Project'})).toHaveFocus();
2306
- await user.tab();
2307
- expect(screen.getByTestId('row-action')).toHaveFocus();
2308
- await user.tab();
2309
- expect(screen.getByRole('link', {name: 'Session'})).toHaveFocus();
2310
- });
2311
-
2312
- it('renders the custom link component inside the row wrapper', () => {
2313
- render(
2314
- <LinkProvider component={CustomLink}>
2315
- <SideNavItem label="Project" href="/project" actions={rowAction()} />
2316
- </LinkProvider>,
2317
- );
2318
- const link = screen.getByRole('link', {name: 'Project'});
2319
- expect(link).toHaveAttribute('data-custom-link');
2320
- expect(link.parentElement).toHaveClass('astryx-side-nav-item');
2321
- });
2322
-
2323
- it('controlled collapse reports toggle intent without flipping until the prop changes', async () => {
2324
- const user = userEvent.setup();
2325
- const onCollapsedChange = vi.fn();
2326
- render(
2327
- <SideNavItem
2328
- label="Project"
2329
- href="/project"
2330
- collapsible={{isCollapsed: false, onCollapsedChange}}
2331
- actions={rowAction()}>
2332
- <SideNavItem label="Session" href="/project/session" />
2333
- </SideNavItem>,
2334
- );
2335
- const toggle = screen.getByRole('button', {name: /collapse project/i});
2336
- await user.click(toggle);
2337
- expect(onCollapsedChange).toHaveBeenCalledWith(true);
2338
- // Controlled mode: stays expanded until the consumer flips the prop.
2339
- expect(toggle).toHaveAttribute('aria-expanded', 'true');
2340
- });
2341
- });
2342
-
2343
- import {SideNavRenderContext} from './SideNavRenderContext';
2344
- import {AppShellMobileContext} from '../AppShell/AppShellMobileContext';
2345
-
2346
- describe('SideNavItem — mobile drawer close-on-activate', () => {
2347
- function renderInDrawer(ui: ReactNode, closeMobileNav = vi.fn()) {
2348
- return {
2349
- closeMobileNav,
2350
- ...render(
2351
- <AppShellMobileContext
2352
- value={{
2353
- isMobile: true,
2354
- isMobileNavOpen: true,
2355
- toggleMobileNav: vi.fn(),
2356
- openMobileNav: vi.fn(),
2357
- closeMobileNav,
2358
- isMobileNavEnabled: true,
2359
- hasAutoToggle: true,
2360
- }}>
2361
- <SideNavRenderContext value="drawer">{ui}</SideNavRenderContext>
2362
- </AppShellMobileContext>,
2363
- ),
2364
- };
2365
- }
2366
-
2367
- it('closes the mobile nav when a link item is clicked', async () => {
2368
- const user = userEvent.setup();
2369
- const {closeMobileNav} = renderInDrawer(
2370
- <SideNavItem label="Home" href="/" data-testid="item" />,
2371
- );
2372
- await user.click(screen.getByTestId('item'));
2373
- expect(closeMobileNav).toHaveBeenCalledTimes(1);
2374
- });
2375
-
2376
- it('closes the mobile nav when a button item is clicked', async () => {
2377
- const user = userEvent.setup();
2378
- const onClick = vi.fn();
2379
- const {closeMobileNav} = renderInDrawer(
2380
- <SideNavItem label="Action" onClick={onClick} data-testid="item" />,
2381
- );
2382
- await user.click(screen.getByTestId('item'));
2383
- expect(onClick).toHaveBeenCalledTimes(1);
2384
- expect(closeMobileNav).toHaveBeenCalledTimes(1);
2385
- });
2386
-
2387
- it('does NOT close when a collapsible parent is toggled', async () => {
2388
- const user = userEvent.setup();
2389
- const {closeMobileNav} = renderInDrawer(
2390
- <SideNavItem
2391
- label="Settings"
2392
- icon={StubIcon}
2393
- collapsible
2394
- data-testid="parent">
2395
- <SideNavItem label="General" href="/settings/general" />
2396
- </SideNavItem>,
2397
- );
2398
- await user.click(screen.getByTestId('parent'));
2399
- expect(closeMobileNav).not.toHaveBeenCalled();
2400
- });
2401
-
2402
- it('does NOT close when not inside a drawer', async () => {
2403
- const user = userEvent.setup();
2404
- const closeMobileNav = vi.fn();
2405
- render(
2406
- <AppShellMobileContext
2407
- value={{
2408
- isMobile: false,
2409
- isMobileNavOpen: false,
2410
- toggleMobileNav: vi.fn(),
2411
- openMobileNav: vi.fn(),
2412
- closeMobileNav,
2413
- isMobileNavEnabled: false,
2414
- hasAutoToggle: true,
2415
- }}>
2416
- <SideNavItem label="Home" href="/" data-testid="item" />
2417
- </AppShellMobileContext>,
2418
- );
2419
- await user.click(screen.getByTestId('item'));
2420
- expect(closeMobileNav).not.toHaveBeenCalled();
2421
- });
2422
-
2423
- it('does not close the mobile nav when a row action is clicked', async () => {
2424
- const user = userEvent.setup();
2425
- const onActionClick = vi.fn();
2426
- const {closeMobileNav} = renderInDrawer(
2427
- <SideNavItem
2428
- label="Home"
2429
- href="/"
2430
- actions={
2431
- <button
2432
- type="button"
2433
- data-testid="drawer-row-action"
2434
- onClick={onActionClick}>
2435
- ⋯
2436
- </button>
2437
- }
2438
- />,
2439
- );
2440
- await user.click(screen.getByTestId('drawer-row-action'));
2441
- expect(onActionClick).toHaveBeenCalledTimes(1);
2442
- expect(closeMobileNav).not.toHaveBeenCalled();
2443
- });
2444
- });
2445
-
2446
- // =============================================================================
2447
- // Keyboard operation (audit §6 V6)
2448
- // =============================================================================
2449
-
2450
- describe('SideNav keyboard operation', () => {
2451
- it('reaches every nav item with Tab, in document order', async () => {
2452
- const user = userEvent.setup();
2453
- render(
2454
- <SideNav>
2455
- <SideNavSection title="Main">
2456
- <SideNavItem label="Dashboard" href="/dashboard" />
2457
- <SideNavItem label="Projects" href="/projects" />
2458
- <SideNavItem label="Settings" onClick={() => {}} />
2459
- </SideNavSection>
2460
- </SideNav>,
2461
- );
2462
-
2463
- await user.tab();
2464
- expect(screen.getByRole('link', {name: 'Dashboard'})).toHaveFocus();
2465
- await user.tab();
2466
- expect(screen.getByRole('link', {name: 'Projects'})).toHaveFocus();
2467
- await user.tab();
2468
- expect(screen.getByRole('button', {name: 'Settings'})).toHaveFocus();
2469
- });
2470
-
2471
- it('skips a disabled item in the tab order', async () => {
2472
- const user = userEvent.setup();
2473
- render(
2474
- <SideNav>
2475
- <SideNavItem label="Dashboard" onClick={() => {}} />
2476
- <SideNavItem label="Archived" onClick={() => {}} isDisabled />
2477
- <SideNavItem label="Settings" onClick={() => {}} />
2478
- </SideNav>,
2479
- );
2480
-
2481
- await user.tab();
2482
- expect(screen.getByRole('button', {name: 'Dashboard'})).toHaveFocus();
2483
- await user.tab();
2484
- expect(screen.getByRole('button', {name: 'Settings'})).toHaveFocus();
2485
- });
2486
-
2487
- it('toggles a collapsible group with Enter and with Space', async () => {
2488
- const user = userEvent.setup();
2489
- render(
2490
- <SideNavItem label="Settings" onClick={() => {}} collapsible>
2491
- <SideNavItem label="General" href="/settings/general" />
2492
- </SideNavItem>,
2493
- );
2494
-
2495
- const toggle = screen.getByRole('button', {name: 'Collapse Settings'});
2496
- toggle.focus();
2497
-
2498
- await user.keyboard('{Enter}');
2499
- expect(toggle).toHaveAttribute('aria-expanded', 'false');
2500
-
2501
- await user.keyboard(' ');
2502
- expect(toggle).toHaveAttribute('aria-expanded', 'true');
2503
- });
2504
-
2505
- it('activates a button item with Enter', async () => {
2506
- const user = userEvent.setup();
2507
- const onClick = vi.fn();
2508
- render(<SideNavItem label="Log out" onClick={onClick} />);
2509
-
2510
- await user.tab();
2511
- await user.keyboard('{Enter}');
2512
- expect(onClick).toHaveBeenCalledTimes(1);
2513
- });
2514
-
2515
- it('opens the collapsed submenu from the keyboard and closes it with Escape', async () => {
2516
- const user = userEvent.setup();
2517
- renderCollapsed(
2518
- <SideNavItem label="Settings" icon={StubIcon} data-testid="parent">
2519
- <SideNavItem label="General" href="/settings/general" />
2520
- </SideNavItem>,
2521
- );
2522
-
2523
- const trigger = screen.getByTestId('parent');
2524
- trigger.focus();
2525
- await user.keyboard('{Enter}');
2526
- expect(trigger).toHaveAttribute('aria-expanded', 'true');
2527
-
2528
- await user.keyboard('{Escape}');
2529
- expect(trigger).toHaveAttribute('aria-expanded', 'false');
2530
- });
2531
- });
2532
-
2533
- // =============================================================================
2534
- // Focus management (audit §6 V7)
2535
- // =============================================================================
2536
-
2537
- /**
2538
- * Holds requestAnimationFrame callbacks until `flush`, so a test decides when
2539
- * a popover's auto-focus frame runs instead of racing a timer.
2540
- */
2541
- function holdAnimationFrames() {
2542
- const frames = new Map<number, FrameRequestCallback>();
2543
- let nextFrame = 0;
2544
- vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => {
2545
- nextFrame += 1;
2546
- frames.set(nextFrame, callback);
2547
- return nextFrame;
2548
- });
2549
- vi.stubGlobal('cancelAnimationFrame', (frame: number) => {
2550
- frames.delete(frame);
2551
- });
2552
- return {
2553
- flush() {
2554
- act(() => {
2555
- const due = [...frames.values()];
2556
- frames.clear();
2557
- due.forEach(callback => callback(performance.now()));
2558
- });
2559
- },
2560
- };
2561
- }
2562
-
2563
- describe('SideNav focus management', () => {
2564
- afterEach(() => {
2565
- vi.unstubAllGlobals();
2566
- });
2567
-
2568
- it('keeps focus on the toggle when a group collapses', async () => {
2569
- const user = userEvent.setup();
2570
- render(
2571
- <SideNavItem label="Settings" href="/settings" collapsible>
2572
- <SideNavItem label="General" href="/settings/general" />
2573
- </SideNavItem>,
2574
- );
2575
-
2576
- const toggle = screen.getByRole('button', {name: 'Collapse Settings'});
2577
- await user.click(toggle);
2578
-
2579
- expect(toggle).toHaveAttribute('aria-expanded', 'false');
2580
- expect(toggle).toHaveFocus();
2581
- });
2582
-
2583
- it('does not leave focus on a child that the group just hid', async () => {
2584
- const user = userEvent.setup();
2585
- render(
2586
- <SideNavItem label="Settings" href="/settings" collapsible>
2587
- <SideNavItem label="General" href="/settings/general" />
2588
- </SideNavItem>,
2589
- );
2590
-
2591
- const child = screen.getByRole('link', {name: 'General'});
2592
- child.focus();
2593
-
2594
- const toggle = screen.getByRole('button', {name: 'Collapse Settings'});
2595
- await user.click(toggle);
2596
-
2597
- // The collapsed group is `inert`: focus may land neither inside it nor
2598
- // on <body>.
2599
- expect(document.activeElement).not.toBe(child);
2600
- expect(document.activeElement).not.toBe(document.body);
2601
- expect(toggle).toHaveFocus();
2602
- });
2603
-
2604
- it('returns focus to the trigger when the collapsed submenu closes', async () => {
2605
- const frames = holdAnimationFrames();
2606
- const user = userEvent.setup();
2607
- renderCollapsed(
2608
- <SideNavItem label="Settings" icon={StubIcon} data-testid="parent">
2609
- <SideNavItem label="General" href="/settings/general" />
2610
- </SideNavItem>,
2611
- );
2612
-
2613
- const trigger = screen.getByTestId('parent');
2614
- await user.click(trigger);
2615
- expect(trigger).toHaveAttribute('aria-expanded', 'true');
2616
- // The submenu focuses its first link in the frame after it opens.
2617
- frames.flush();
2618
- expect(screen.getByText('General').closest('a')).toHaveFocus();
2619
-
2620
- // Closing restores focus as part of the close itself.
2621
- await user.keyboard('{Escape}');
2622
- expect(trigger).toHaveFocus();
2623
- });
2624
-
2625
- it('keeps focus on the trigger when the submenu closes before its first frame', async () => {
2626
- const frames = holdAnimationFrames();
2627
- const user = userEvent.setup();
2628
- renderCollapsed(
2629
- <SideNavItem label="Settings" icon={StubIcon} data-testid="parent">
2630
- <SideNavItem label="General" href="/settings/general" />
2631
- </SideNavItem>,
2632
- );
2633
-
2634
- const trigger = screen.getByTestId('parent');
2635
- await user.click(trigger);
2636
- await user.keyboard('{Escape}');
2637
- expect(trigger).toHaveAttribute('aria-expanded', 'false');
2638
-
2639
- // The open's auto-focus frame, late on a busy runner, must not land
2640
- // focus in the closed submenu.
2641
- frames.flush();
2642
- expect(trigger).toHaveFocus();
2643
- });
2644
- });
2645
-
2646
- // =============================================================================
2647
- // Audit regressions — the defects this pass fixed
2648
- // =============================================================================
2649
-
2650
- describe('SideNav audit regressions', () => {
2651
- it('names the collapsed submenu dialog from the catalog, not a concatenation', async () => {
2652
- const user = userEvent.setup();
2653
- renderCollapsed(
2654
- <SideNavItem label="Settings" icon={StubIcon} data-testid="parent">
2655
- <SideNavItem label="General" href="/settings/general" />
2656
- </SideNavItem>,
2657
- );
2658
-
2659
- await user.click(screen.getByTestId('parent'));
2660
- const dialog = document.querySelector('[role="dialog"]');
2661
- expect(dialog).not.toBeNull();
2662
- expect(dialog).toHaveAttribute('aria-label', 'Settings submenu');
2663
- });
2664
-
2665
- it('translates the collapsed submenu dialog name', async () => {
2666
- const user = userEvent.setup();
2667
- render(
2668
- <InternationalizationProvider
2669
- locale="pseudo"
2670
- messages={{pseudo: pseudoCatalog}}>
2671
- <SideNavCollapseContext
2672
- value={{isCollapsed: true, toggle: () => {}, isCollapsible: true}}>
2673
- <SideNavItem label="Settings" icon={StubIcon} data-testid="parent">
2674
- <SideNavItem label="General" href="/settings/general" />
2675
- </SideNavItem>
2676
- </SideNavCollapseContext>
2677
- </InternationalizationProvider>,
2678
- );
2679
-
2680
- await user.click(screen.getByTestId('parent'));
2681
- const label = document
2682
- .querySelector('[role="dialog"]')
2683
- ?.getAttribute('aria-label');
2684
-
2685
- // A concatenated `${label} submenu` would stay plain English here.
2686
- expect(label).toContain('Settings');
2687
- expect(label).not.toBe('Settings submenu');
2688
- });
2689
-
2690
- it('forwards unrecognised props on SideNavItem through to the control', () => {
2691
- render(
2692
- <SideNavItem
2693
- label="Dashboard"
2694
- href="/dashboard"
2695
- data-analytics="nav-dashboard"
2696
- aria-describedby="hint"
2697
- />,
2698
- );
2699
-
2700
- const link = screen.getByRole('link', {name: 'Dashboard'});
2701
- expect(link).toHaveAttribute('data-analytics', 'nav-dashboard');
2702
- expect(link).toHaveAttribute('aria-describedby', 'hint');
2703
- });
2704
-
2705
- it('forwards unrecognised props on a collapsed SideNavItem', () => {
2706
- renderCollapsed(
2707
- <SideNavItem
2708
- label="Dashboard"
2709
- icon={StubIcon}
2710
- href="/dashboard"
2711
- data-analytics="nav-dashboard"
2712
- />,
2713
- );
2714
-
2715
- expect(screen.getByRole('link')).toHaveAttribute(
2716
- 'data-analytics',
2717
- 'nav-dashboard',
2718
- );
2719
- });
2720
-
2721
- it('exposes a disabled item as a theming state', () => {
2722
- render(<SideNavItem label="Archived" onClick={() => {}} isDisabled />);
2723
-
2724
- expect(screen.getByRole('button', {name: 'Archived'})).toHaveAttribute(
2725
- 'data-disabled',
2726
- 'disabled',
2727
- );
2728
- });
2729
-
2730
- it('keeps a hidden section header readable by assistive technology', () => {
2731
- render(
2732
- <SideNavSection title="Main" isHeaderHidden>
2733
- <SideNavItem label="Dashboard" href="/dashboard" />
2734
- </SideNavSection>,
2735
- );
2736
-
2737
- expect(screen.getByRole('group')).toHaveAccessibleName('Main');
2738
- expect(screen.getByText('Main')).toBeInTheDocument();
2739
- });
2740
-
2741
- it('keeps a SideNavCollapseButton outside the sidenav in step with it', async () => {
2742
- const user = userEvent.setup();
2743
-
2744
- function App() {
2745
- const [isCollapsed, setIsCollapsed] = useState(false);
2746
- const collapsible = {isCollapsed, onCollapsedChange: setIsCollapsed};
2747
- return (
2748
- <>
2749
- <SideNavCollapseButton collapsible={collapsible} />
2750
- <SideNav collapsible={{...collapsible, hasButton: false}}>
2751
- <SideNavItem label="Dashboard" href="/dashboard" />
2752
- </SideNav>
2753
- </>
2754
- );
2755
- }
2756
-
2757
- render(<App />);
2758
-
2759
- await user.click(screen.getByRole('button', {name: 'Collapse sidebar'}));
2760
- const expand = await screen.findByRole('button', {name: 'Expand sidebar'});
2761
-
2762
- await user.click(expand);
2763
- expect(
2764
- await screen.findByRole('button', {name: 'Collapse sidebar'}),
2765
- ).toBeInTheDocument();
2766
- });
2767
- });
2768
-
2769
- // =============================================================================
2770
- // A15 — the shared focus ring
2771
- //
2772
- // jsdom does not derive `:focus-visible` from `.focus()` for an element
2773
- // matched directly, so the ring a tab stop draws on itself cannot be read
2774
- // back here; the geometry is measured in a real browser instead. These pin
2775
- // the composition — that the focusable element carries the classes
2776
- // `focusOutlineStyles.focusVisible` compiles to.
2777
- //
2778
- // Composition is only proof for a tab stop. A row wrapper is not one, so
2779
- // `:focus-visible` atoms sitting on it can never match, and asserting them
2780
- // there proves nothing: the actions-row tests below passed with the
2781
- // wrapper's ring deleted outright. A wrapper's ring is guarded by `:has()`,
2782
- // which jsdom *does* re-evaluate against live focus, so those rules are read
2783
- // out of the stylesheet and matched against the DOM instead.
2784
- // =============================================================================
2785
-
2786
- const sharedFocusRingClasses = stylex
2787
- .props(focusOutlineStyles.focusVisible)
2788
- .className!.split(' ');
2789
-
2790
- function expectSharedFocusRing(el: Element) {
2791
- const classes = el.className.split(' ');
2792
- for (const c of sharedFocusRingClasses) {
2793
- expect(classes).toContain(c);
2794
- }
2795
- }
2796
-
2797
- function expectNoSharedFocusRing(el: Element) {
2798
- const classes = el.className.split(' ');
2799
- for (const c of sharedFocusRingClasses) {
2800
- expect(classes).not.toContain(c);
2801
- }
2802
- }
2803
-
2804
- /** Every style rule in the live sheet, flattened out of any grouping rule. */
2805
- function allStyleRules(): CSSStyleRule[] {
2806
- const out: CSSStyleRule[] = [];
2807
- const walk = (rules: CSSRuleList) => {
2808
- for (const rule of Array.from(rules)) {
2809
- const nested = (rule as CSSGroupingRule).cssRules;
2810
- if (nested) {
2811
- walk(nested);
2812
- }
2813
- if ((rule as CSSStyleRule).selectorText) {
2814
- out.push(rule as CSSStyleRule);
2815
- }
2816
- }
2817
- };
2818
- for (const sheet of Array.from(document.styleSheets)) {
2819
- try {
2820
- walk(sheet.cssRules);
2821
- } catch {
2822
- // A sheet we cannot read cannot be one StyleX injected.
2823
- }
2824
- }
2825
- return out;
2826
- }
2827
-
2828
- /**
2829
- * A rule's declared value for `prop`, read out of its text.
2830
- *
2831
- * Not `rule.style`: these values are `var()`s, and jsdom's CSS object model
2832
- * drops longhands it cannot parse, so they read back as an empty string there.
2833
- */
2834
- function declaredValue(rule: CSSStyleRule, prop: string): string | undefined {
2835
- const body = /\{([^}]*)\}/.exec(rule.cssText)?.[1] ?? '';
2836
- return new RegExp(`(?:^|;)\\s*${prop}:\\s*([^;]+)`).exec(body)?.[1].trim();
2837
- }
2838
-
2839
- /** A rule's `outline-style` — the discriminator for a painting focus ring. */
2840
- function outlineStyleOf(rule: CSSStyleRule): string | undefined {
2841
- return declaredValue(rule, 'outline-style');
2842
- }
2843
-
2844
- /**
2845
- * What the live sheet unconditionally declares for `prop` on `el`, gathered
2846
- * from every bare-class rule the element carries. StyleX emits one atomic
2847
- * class per property, so a well-formed element yields exactly one value —
2848
- * comparing those values compares real CSS without pinning a generated hash.
2849
- */
2850
- function declaredOn(el: Element, prop: string): string[] {
2851
- const classes = new Set(el.className.split(' '));
2852
- return allStyleRules()
2853
- .filter(rule => {
2854
- const bare = rule.selectorText.replace(/:not\(#\\?#\)/g, '');
2855
- const owner = /^\.([^:\s]+)$/.exec(bare)?.[1];
2856
- return owner != null && classes.has(owner);
2857
- })
2858
- .map(rule => declaredValue(rule, prop))
2859
- .filter(value => value != null);
2860
- }
2861
-
2862
- /**
2863
- * The nesting indent a row carries, or null for none. The indent is a
2864
- * runtime StyleX value, so it rides the row's inline style as a custom
2865
- * property; read the value rather than the generated property name.
2866
- */
2867
- function indentOf(el: HTMLElement): string | null {
2868
- const {style} = el;
2869
- for (let i = 0; i < style.length; i++) {
2870
- const value = style.getPropertyValue(style[i]).trim();
2871
- if (value.includes('var(--spacing-6)')) {
2872
- return value;
2873
- }
2874
- }
2875
- return null;
2876
- }
2877
-
2878
- /**
2879
- * The selectors that actually paint the shared ring on `el`, read out of the
2880
- * live stylesheet with StyleX's `:not(#\#)` specificity padding stripped so
2881
- * jsdom can evaluate them.
2882
- *
2883
- * `outline-style` is the discriminator: StyleX emits one rule per property,
2884
- * and no outline paints while that one resolves to `none`.
2885
- */
2886
- function ringSelectorsFor(el: Element): string[] {
2887
- const classes = new Set(el.className.split(' '));
2888
- return allStyleRules()
2889
- .filter(rule => {
2890
- const painted = outlineStyleOf(rule);
2891
- if (!painted || painted === 'none') {
2892
- return false;
2893
- }
2894
- const owner = /^\.([^:\s]+)/.exec(rule.selectorText)?.[1];
2895
- return owner != null && classes.has(owner);
2896
- })
2897
- .map(rule => rule.selectorText.replace(/:not\(#\\?#\)/g, ''));
2898
- }
2899
-
2900
- /**
2901
- * Whether `el`'s ring is painting for whatever holds focus right now.
2902
- *
2903
- * `:focus-visible` is substituted with `:focus` before matching. jsdom
2904
- * derives `:focus-visible` from a heuristic over the last interaction, so it
2905
- * answers differently depending on which tests ran before — the same
2906
- * assertion passes alone and fails in the file. `:focus` is unambiguous, and
2907
- * the ring's modality is pinned separately, on the selector text, by the
2908
- * keyboard-only test below.
2909
- */
2910
- function ringIsPainting(el: Element): boolean {
2911
- return ringSelectorsFor(el).some(selector =>
2912
- el.matches(selector.replace(/:focus-visible/g, ':focus')),
2913
- );
2914
- }
2915
-
2916
- /**
2917
- * Whether `el` unconditionally declares `outline-style: none`, which is what
2918
- * keeps the UA's own focus ring off an element whose ring an ancestor paints.
2919
- * Computed style cannot answer this: `none` is also the CSS initial value.
2920
- */
2921
- function suppressesOwnOutline(el: Element): boolean {
2922
- const classes = new Set(el.className.split(' '));
2923
- return allStyleRules().some(rule => {
2924
- if (outlineStyleOf(rule) !== 'none') {
2925
- return false;
2926
- }
2927
- // Unconditional only — a bare class once the padding is stripped.
2928
- const bare = rule.selectorText.replace(/:not\(#\\?#\)/g, '');
2929
- const owner = /^\.([^:\s]+)$/.exec(bare)?.[1];
2930
- return owner != null && classes.has(owner);
2931
- });
2932
- }
2933
-
2934
- describe('SideNav focus ring (A15)', () => {
2935
- it('draws the shared ring on a link item', () => {
2936
- render(<SideNavItem label="Dashboard" href="/dashboard" />);
2937
- expectSharedFocusRing(screen.getByRole('link', {name: 'Dashboard'}));
2938
- });
2939
-
2940
- it('draws the shared ring on a button item', () => {
2941
- render(<SideNavItem label="Dashboard" onClick={() => {}} />);
2942
- expectSharedFocusRing(screen.getByRole('button', {name: 'Dashboard'}));
2943
- });
2944
-
2945
- it('draws the shared ring on a collapsed icon-only item', () => {
2946
- renderCollapsed(
2947
- <SideNavItem label="Dashboard" icon={StubIcon} href="/dashboard" />,
2948
- );
2949
- expectSharedFocusRing(screen.getByRole('link', {name: 'Dashboard'}));
2950
- });
2951
-
2952
- it('draws the shared ring on a collapsed submenu trigger', () => {
2953
- renderCollapsed(
2954
- <SideNavItem label="Settings" icon={StubIcon}>
2955
- <SideNavItem label="General" href="/settings/general" />
2956
- </SideNavItem>,
2957
- );
2958
- expectSharedFocusRing(screen.getByRole('button', {name: 'Settings'}));
2959
- });
2960
-
2961
- it('rings each focusable of a split-action row, and not the row itself', () => {
2962
- render(
2963
- <SideNavItem label="Settings" href="/settings" collapsible>
2964
- <SideNavItem label="General" href="/settings/general" />
2965
- </SideNavItem>,
2966
- );
2967
-
2968
- const link = screen.getByRole('link', {name: 'Settings'});
2969
- const toggle = screen.getByRole('button', {name: 'Collapse Settings'});
2970
- expectSharedFocusRing(link);
2971
- expectSharedFocusRing(toggle);
2972
-
2973
- // A presentational <div> holding two independent tab stops; ringing it
2974
- // too would paint a second outline around the whole row.
2975
- const row = link.parentElement!;
2976
- expect(row.tagName).toBe('DIV');
2977
- expect(row).toContainElement(toggle);
2978
- expectNoSharedFocusRing(row);
2979
- });
2980
-
2981
- it('rings the row wrapper of an actions row, not the truncated primary', async () => {
2982
- const user = userEvent.setup();
2983
- render(
2984
- <SideNavItem
2985
- label="Project"
2986
- href="/project"
2987
- actions={
2988
- <button type="button" data-testid="row-action">
2989
- More
2990
- </button>
2991
- }
2992
- />,
2993
- );
2994
-
2995
- const link = screen.getByRole('link', {name: 'Project'});
2996
- const row = link.parentElement!;
2997
- expect(row.tagName).toBe('DIV');
2998
- expect(row).toContainElement(screen.getByTestId('row-action'));
2999
-
3000
- // Same pill as an ordinary row: the ring lives on the wrapper, whose box
3001
- // includes the actions. Putting it on the primary would clip to the
3002
- // truncated split-action element (square, not full-row).
3003
- expect(ringIsPainting(row)).toBe(false);
3004
- await user.tab();
3005
- expect(link).toHaveFocus();
3006
- expect(ringIsPainting(row)).toBe(true);
3007
- // And exactly one ring: nothing paints a second one on the primary.
3008
- expect(ringSelectorsFor(link)).toEqual([]);
3009
- });
3010
-
3011
- it('leaves the wrapper ring to the primary, not the chevron toggle', async () => {
3012
- const user = userEvent.setup();
3013
- render(
3014
- <SideNavItem
3015
- label="Settings"
3016
- href="/settings"
3017
- collapsible
3018
- actions={
3019
- <button type="button" data-testid="row-action">
3020
- More
3021
- </button>
3022
- }>
3023
- <SideNavItem label="General" href="/settings/general" />
3024
- </SideNavItem>,
3025
- );
3026
-
3027
- const link = screen.getByRole('link', {name: 'Settings'});
3028
- const toggle = screen.getByRole('button', {name: 'Collapse Settings'});
3029
- const row = link.parentElement!;
3030
- expect(row).toContainElement(screen.getByTestId('row-action'));
3031
-
3032
- await user.tab();
3033
- expect(link).toHaveFocus();
3034
- expect(ringIsPainting(row)).toBe(true);
3035
-
3036
- // The toggle owns its own ring, as it does in a split-action row with no
3037
- // actions. Keeping the wrapper lit too would stack a full-row outline
3038
- // around the chevron's own — two rings for one tab stop.
3039
- await user.tab();
3040
- expect(toggle).toHaveFocus();
3041
- expectSharedFocusRing(toggle);
3042
- expect(ringIsPainting(row)).toBe(false);
3043
- });
3044
-
3045
- it('does not ring the row wrapper when a supplied action takes focus', async () => {
3046
- const user = userEvent.setup();
3047
- render(
3048
- <SideNavItem
3049
- label="Project"
3050
- href="/project"
3051
- actions={
3052
- <button type="button" data-testid="row-action">
3053
- More
3054
- </button>
3055
- }
3056
- />,
3057
- );
3058
-
3059
- const row = screen.getByRole('link', {name: 'Project'}).parentElement!;
3060
- await user.tab();
3061
- expect(ringIsPainting(row)).toBe(true);
3062
-
3063
- // An action draws whatever ring its own component draws. Focus is not on
3064
- // the row, so the row must not light up around it.
3065
- await user.tab();
3066
- expect(screen.getByTestId('row-action')).toHaveFocus();
3067
- expect(ringIsPainting(row)).toBe(false);
3068
- });
3069
-
3070
- it('rings the wrapper of a whole-row toggle carrying actions', async () => {
3071
- const user = userEvent.setup();
3072
- render(
3073
- <SideNavItem
3074
- label="Project"
3075
- collapsible
3076
- actions={
3077
- <button type="button" data-testid="row-action">
3078
- More
3079
- </button>
3080
- }>
3081
- <SideNavItem label="Session" href="/project/session" />
3082
- </SideNavItem>,
3083
- );
3084
-
3085
- // No href and no onClick, so the primary *is* the collapse toggle and
3086
- // keeps the chevron inside it — a different row shape from the split.
3087
- const primary = screen.getByRole('button', {name: 'Project'});
3088
- const row = primary.parentElement!;
3089
-
3090
- await user.tab();
3091
- expect(primary).toHaveFocus();
3092
- expect(ringIsPainting(row)).toBe(true);
3093
-
3094
- await user.tab();
3095
- expect(screen.getByTestId('row-action')).toHaveFocus();
3096
- expect(ringIsPainting(row)).toBe(false);
3097
- });
3098
-
3099
- it('keeps the UA focus ring off the primary the wrapper rings for', () => {
3100
- render(
3101
- <SideNavItem
3102
- label="Project"
3103
- href="/project"
3104
- actions={
3105
- <button type="button" data-testid="row-action">
3106
- More
3107
- </button>
3108
- }
3109
- />,
3110
- );
3111
-
3112
- const link = screen.getByRole('link', {name: 'Project'});
3113
- // Dropping the shared ring off the primary also drops the
3114
- // `outline-style: none` default that came with it, and a bare <a> keeps
3115
- // the browser's own focus ring — which would paint inside the wrapper's.
3116
- expect(suppressesOwnOutline(link)).toBe(true);
3117
- });
3118
-
3119
- it('paints the actions-row ring for keyboard focus only', () => {
3120
- render(
3121
- <SideNavItem
3122
- label="Project"
3123
- href="/project"
3124
- actions={
3125
- <button type="button" data-testid="row-action">
3126
- More
3127
- </button>
3128
- }
3129
- />,
3130
- );
3131
-
3132
- const link = screen.getByRole('link', {name: 'Project'});
3133
- const row = link.parentElement!;
3134
- // The wrapper is not a tab stop, so its ring has to be guarded by a
3135
- // relational selector. `:focus-within` or `:focus` would light the row
3136
- // for mouse users too; only `:focus-visible` keeps it to the keyboard.
3137
- const selectors = ringSelectorsFor(row);
3138
- expect(selectors.length).toBeGreaterThan(0);
3139
- for (const selector of selectors) {
3140
- expect(selector).toContain(':focus-visible');
3141
- }
3142
-
3143
- // The guard is scoped to the wrapper's first child. Reordering the row
3144
- // would hand the ring to the toggle or the actions instead.
3145
- expect(row.firstElementChild).toBe(link);
3146
- });
3147
-
3148
- it('draws the shared ring on a heading rendered as one link', () => {
3149
- render(<SideNavHeading heading="My App" headingHref="/" />);
3150
- expectSharedFocusRing(screen.getByRole('link', {name: /My App/}));
3151
- });
3152
-
3153
- it('draws the shared ring on a collapsed heading link', () => {
3154
- renderCollapsed(
3155
- <SideNavHeading heading="My App" headingHref="/" icon={<span>i</span>} />,
3156
- );
3157
- expectSharedFocusRing(screen.getByRole('link', {name: 'My App'}));
3158
- });
3159
-
3160
- it('draws the shared ring on the heading menu trigger', () => {
3161
- render(<SideNavHeading heading="My App" menu={<div>menu</div>} />);
3162
- expectSharedFocusRing(screen.getByRole('button', {name: 'Open menu'}));
3163
- });
3164
- });
3165
-
3166
- // jsdom cannot emulate forced-colors rendering, so these assert that the
3167
- // compiled output carries the rules the selected row relies on. The rendered
3168
- // behaviour was measured in Chromium under `forced-colors: active` — see the
3169
- // evidence on #4880.
3170
- describe('forced colors (WCAG 1.4.11)', () => {
3171
- it('compiles forced-colors overrides so the current page survives Windows High Contrast', () => {
3172
- render(
3173
- <SideNav>
3174
- <SideNavSection title="Main">
3175
- <SideNavItem label="Dashboard" href="/dashboard" isSelected />
3176
- <SideNavItem label="Projects" href="/projects" />
3177
- </SideNavSection>
3178
- </SideNav>,
3179
- );
3180
- const css = getForcedColorsRules();
3181
- // `--color-neutral` is a background: forced colors flatten it to Canvas
3182
- // and the selected row loses every trace of being selected. Highlight /
3183
- // HighlightText is the platform convention that survives.
3184
- expect(css).toContain('background-color: highlight;');
3185
- expect(css).toContain('color: highlighttext;');
3186
- // The hover fill has to hold the Highlight too, or hovering the current
3187
- // page repaints it with the ordinary hover overlay and erases the state.
3188
- // Nesting it inside the hover media query is what wins that fight: the
3189
- // rule compiles to a triple-class selector, which outranks `item`'s
3190
- // single-class hover overlay no matter the source order.
3191
- expect(css).toMatch(
3192
- /@media \(hover: hover\)[\s\S]*?forced-colors: active[\s\S]*?:hover[\s\S]*?background-color: highlight/,
3193
- );
3194
- });
3195
-
3196
- it('does not opt nav rows out of UA colour remapping', () => {
3197
- render(
3198
- <SideNav>
3199
- <SideNavSection title="Main">
3200
- <SideNavItem label="Dashboard" href="/dashboard" isSelected />
3201
- </SideNavSection>
3202
- </SideNav>,
3203
- );
3204
- // A nav row is not a native form control — it resets `appearance` and
3205
- // paints its own background, so the system keywords land without
3206
- // `forced-color-adjust: none`. Setting it would inherit into
3207
- // `endContent`, keeping a Badge's authored fill instead of remapping it.
3208
- const row = screen.getByRole('link', {name: 'Dashboard'});
3209
- expect(getComputedStyle(row).forcedColorAdjust).not.toBe('none');
3210
- });
3211
- });
3212
-
3213
- // =============================================================================
3214
- // Footer icon row — one size for the whole row
3215
- // =============================================================================
3216
-
3217
- describe('SideNav footer icon row sizing', () => {
3218
- /**
3219
- * The class(es) a `size="sm"` Button carries that a `size="md"` one does
3220
- * not — i.e. whatever StyleX compiled the sm height into. Comparing class
3221
- * sets keeps the assertion off the generated hashes.
3222
- */
3223
- function sizeOnlyClasses(size: 'sm' | 'lg'): string[] {
3224
- const {unmount} = render(
3225
- <>
3226
- <Button
3227
- label="sized reference"
3228
- size={size}
3229
- isIconOnly
3230
- icon={<span />}
3231
- data-testid="sized-ref"
3232
- />
3233
- <Button
3234
- label="md reference"
3235
- size="md"
3236
- isIconOnly
3237
- icon={<span />}
3238
- data-testid="md-ref"
3239
- />
3240
- </>,
3241
- );
3242
- const sized = new Set(screen.getByTestId('sized-ref').classList);
3243
- const md = new Set(screen.getByTestId('md-ref').classList);
3244
- const only = [...sized].filter(c => !md.has(c));
3245
- unmount();
3246
- // Guard against a vacuous pass: if the two sizes compiled to the same
3247
- // classes there is nothing to assert and every check would trivially hold.
3248
- expect(only.length).toBeGreaterThan(0);
3249
- return only;
3250
- }
3251
-
3252
- it('renders the built-in collapse button at the row size, not the Button default', () => {
3253
- const smClasses = sizeOnlyClasses('sm');
3254
- render(
3255
- <SideNav collapsible>
3256
- <SideNavSection title="Main">
3257
- <SideNavItem label="Dashboard" href="/dashboard" />
3258
- </SideNavSection>
3259
- </SideNav>,
3260
- );
3261
- const collapseButton = screen.getByRole('button', {
3262
- name: /collapse sidebar/i,
3263
- });
3264
- for (const className of smClasses) {
3265
- expect(collapseButton).toHaveClass(className);
3266
- }
3267
- });
3268
-
3269
- it('cascades the same size to footerIcons the consumer did not size', () => {
3270
- const smClasses = sizeOnlyClasses('sm');
3271
- render(
3272
- <SideNav
3273
- collapsible
3274
- footerIcons={
3275
- <Button label="Help" isIconOnly icon={<span />} data-testid="help" />
3276
- }>
3277
- <SideNavSection title="Main">
3278
- <SideNavItem label="Dashboard" href="/dashboard" />
3279
- </SideNavSection>
3280
- </SideNav>,
3281
- );
3282
- const help = screen.getByTestId('help');
3283
- for (const className of smClasses) {
3284
- expect(help).toHaveClass(className);
3285
- }
3286
- });
3287
-
3288
- it('lets an explicit size on a footer icon win over the cascade', () => {
3289
- const lgClasses = sizeOnlyClasses('lg');
3290
- render(
3291
- <SideNav
3292
- collapsible
3293
- footerIcons={
3294
- <Button
3295
- label="Help"
3296
- size="lg"
3297
- isIconOnly
3298
- icon={<span />}
3299
- data-testid="help"
3300
- />
3301
- }>
3302
- <SideNavSection title="Main">
3303
- <SideNavItem label="Dashboard" href="/dashboard" />
3304
- </SideNavSection>
3305
- </SideNav>,
3306
- );
3307
- const help = screen.getByTestId('help');
3308
- for (const className of lgClasses) {
3309
- expect(help).toHaveClass(className);
3310
- }
3311
- });
3312
-
3313
- it('honours an explicit size on SideNavCollapseButton outside the nav', () => {
3314
- const lgClasses = sizeOnlyClasses('lg');
3315
- render(
3316
- <SideNavCollapseContext
3317
- value={{isCollapsed: false, toggle: () => {}, isCollapsible: true}}>
3318
- <SideNavCollapseButton size="lg" data-testid="collapse" />
3319
- </SideNavCollapseContext>,
3320
- );
3321
- const collapse = screen.getByTestId('collapse');
3322
- for (const className of lgClasses) {
3323
- expect(collapse).toHaveClass(className);
3324
- }
3325
- });
3326
-
3327
- it('centres the chevron instead of seating it on a text baseline', () => {
3328
- renderExpanded(<SideNavCollapseButton data-testid="collapse" />);
3329
- // Icon renders its own `astryx-icon` span around the svg; the RTL-mirror
3330
- // wrapper this component adds is that span's parent.
3331
- const iconSpan = screen
3332
- .getByTestId('collapse')
3333
- .querySelector('span.astryx-icon');
3334
- const mirror = iconSpan?.parentElement;
3335
- // The RTL-mirror wrapper is a flex item of Button's icon slot, so it
3336
- // blockifies; as a block it gets a line box and the glyph sits on the
3337
- // baseline, 2.42px above centre (measured in Chromium). A flex container
3338
- // has no line box.
3339
- expect(mirror && getComputedStyle(mirror).display).toBe('flex');
3340
- });
3341
- });
3342
-
3343
- describe('SideNav row control sizing', () => {
3344
- /**
3345
- * The height an icon-only `Button` of this size declares — the box every
3346
- * control sharing a nav row is measured against.
3347
- */
3348
- function iconButtonHeight(size: 'sm' | 'md' | 'lg'): string[] {
3349
- const {unmount} = render(
3350
- <Button
3351
- label="sized reference"
3352
- size={size}
3353
- isIconOnly
3354
- icon={<span />}
3355
- data-testid="sized-ref"
3356
- />,
3357
- );
3358
- const height = declaredOn(screen.getByTestId('sized-ref'), 'height');
3359
- unmount();
3360
- // Guard against a vacuous pass: nothing below means anything if the
3361
- // reference itself declares no height.
3362
- expect(height).toHaveLength(1);
3363
- return height;
3364
- }
3365
-
3366
- function collapsibleRow(actions: ReactNode) {
3367
- return (
3368
- <SideNavItem
3369
- label="Project"
3370
- href="/project"
3371
- collapsible
3372
- actions={actions}>
3373
- <SideNavItem label="Session" href="/project/session" />
3374
- </SideNavItem>
3375
- );
3376
- }
3377
-
3378
- const iconAction = (props?: {size?: 'sm' | 'md' | 'lg'}) => (
3379
- <Button
3380
- label="Project actions"
3381
- isIconOnly
3382
- icon={<span />}
3383
- data-testid="row-action"
3384
- {...props}
3385
- />
3386
- );
3387
-
3388
- it('sizes the expand toggle like the icon buttons it sits beside', () => {
3389
- const sm = iconButtonHeight('sm');
3390
- render(collapsibleRow(iconAction()));
3391
- const toggle = screen.getByRole('button', {name: /collapse project/i});
3392
- expect(declaredOn(toggle, 'height')).toEqual(sm);
3393
- // Square, so its hover pill reads as the same box as the action's.
3394
- expect(declaredOn(toggle, 'width')).toEqual(sm);
3395
- });
3396
-
3397
- it('cascades that size to an action the consumer did not size', () => {
3398
- const sm = iconButtonHeight('sm');
3399
- // Without the cascade an unsized Button falls back to `md`; if the two
3400
- // compiled to one height the assertion below could not fail.
3401
- expect(sm).not.toEqual(iconButtonHeight('md'));
3402
- render(collapsibleRow(iconAction()));
3403
- expect(declaredOn(screen.getByTestId('row-action'), 'height')).toEqual(sm);
3404
- });
3405
-
3406
- it('lets an explicit size on an action win over the cascade', () => {
3407
- const lg = iconButtonHeight('lg');
3408
- render(collapsibleRow(iconAction({size: 'lg'})));
3409
- expect(declaredOn(screen.getByTestId('row-action'), 'height')).toEqual(lg);
3410
- });
3411
- });
3412
-
3413
- describe('SideNavHeading hover/click guard', () => {
3414
- // tabIndex={-1} matches real menu items; a bare role=menuitem div is not
3415
- // focusable.
3416
- const menuItems = (
3417
- <>
3418
- <div role="menuitem" tabIndex={-1}>
3419
- Alpha
3420
- </div>
3421
- <div role="menuitem" tabIndex={-1}>
3422
- Beta
3423
- </div>
3424
- </>
3425
- );
3426
-
3427
- it('keeps the menu open when a hover-open is immediately clicked', async () => {
3428
- vi.useFakeTimers({shouldAdvanceTime: true});
3429
- const user = userEvent.setup({advanceTimers: vi.advanceTimersByTime});
3430
- render(<SideNavHeading heading="My App" menu={menuItems} />);
3431
- const trigger = screen.getByRole('button', {name: 'Open menu'});
3432
-
3433
- await user.hover(trigger);
3434
- act(() => {
3435
- vi.advanceTimersByTime(50);
3436
- });
3437
- expect(trigger).toHaveAttribute('aria-expanded', 'true');
3438
-
3439
- await user.click(trigger);
3440
- expect(trigger).toHaveAttribute('aria-expanded', 'true');
3441
-
3442
- vi.useRealTimers();
3443
- });
3444
-
3445
- it('closes on a click that lands well after the hover-open', async () => {
3446
- vi.useFakeTimers({shouldAdvanceTime: true});
3447
- const user = userEvent.setup({advanceTimers: vi.advanceTimersByTime});
3448
- render(<SideNavHeading heading="My App" menu={menuItems} />);
3449
- const trigger = screen.getByRole('button', {name: 'Open menu'});
3450
-
3451
- await user.hover(trigger);
3452
- act(() => {
3453
- vi.advanceTimersByTime(50);
3454
- });
3455
- act(() => {
3456
- vi.advanceTimersByTime(1200);
3457
- });
3458
-
3459
- await user.click(trigger);
3460
- expect(trigger).toHaveAttribute('aria-expanded', 'false');
3461
-
3462
- vi.useRealTimers();
3463
- });
3464
-
3465
- it('leaves focus on the trigger for a hover-open, and moves it in on click', async () => {
3466
- vi.useFakeTimers({shouldAdvanceTime: true});
3467
- const user = userEvent.setup({advanceTimers: vi.advanceTimersByTime});
3468
- render(<SideNavHeading heading="My App" menu={menuItems} />);
3469
- const trigger = screen.getByRole('button', {name: 'Open menu'});
3470
-
3471
- await user.hover(trigger);
3472
- act(() => {
3473
- vi.advanceTimersByTime(50);
3474
- });
3475
- const firstItem = screen.getAllByRole('menuitem', {hidden: true})[0];
3476
- expect(firstItem).not.toHaveFocus();
3477
-
3478
- await user.click(trigger);
3479
- expect(firstItem).toHaveFocus();
3480
-
3481
- vi.useRealTimers();
3482
- });
3483
-
3484
- it('returns focus to the trigger on Escape', async () => {
3485
- const user = userEvent.setup();
3486
- render(<SideNavHeading heading="My App" menu={menuItems} />);
3487
- const trigger = screen.getByRole('button', {name: 'Open menu'});
3488
-
3489
- await user.click(trigger);
3490
- expect(trigger).toHaveAttribute('aria-expanded', 'true');
3491
-
3492
- await user.keyboard('{Escape}');
3493
- expect(trigger).toHaveAttribute('aria-expanded', 'false');
3494
- expect(trigger).toHaveFocus();
3495
- });
3496
-
3497
- // jsdom has no light dismiss, so the guard tests above cannot fail for the
3498
- // collapsed heading. In a browser, the collapsed icon button sits outside the
3499
- // auto popover, and only the invoker relationship stops the press that
3500
- // should confirm a hover-open from dismissing it first.
3501
- it('makes the collapsed trigger the native invoker of its flyout', () => {
3502
- render(
3503
- <CollapsedWrapper>
3504
- <SideNavHeading
3505
- heading="My App"
3506
- icon={<span>🏠</span>}
3507
- menu={menuItems}
3508
- />
3509
- </CollapsedWrapper>,
3510
- );
3511
- const trigger = screen.getByRole('button', {name: 'My App'});
3512
- const panelId = trigger.getAttribute('aria-controls');
3513
- expect(panelId).toBeTruthy();
3514
- expect(trigger).toHaveAttribute('popovertarget', panelId);
3515
- });
3516
-
3517
- // Expanded, the panel covers the heading, and the trigger is the heading
3518
- // root itself: an invoker there would default-prevent clicks on its links.
3519
- it('adds no invoker in the expanded layouts', () => {
3520
- const {container} = render(
3521
- <>
3522
- <SideNavHeading heading="Whole" menu={menuItems} />
3523
- <SideNavHeading heading="Mixed" headingHref="/home" menu={menuItems} />
3524
- </>,
3525
- );
3526
- expect(container.querySelector('[popovertarget]')).toBeNull();
3527
- });
3528
- });
3529
-
3530
- // =============================================================================
3531
- // Collapse ownership — collapsible + resizable (#4790, #5073)
3532
- // =============================================================================
3533
-
3534
- const AUTO_SAVE_ID = 'sidenav-collapse-owner';
3535
- const STORAGE_KEY = `astryx-resizable:${AUTO_SAVE_ID}`;
3536
-
3537
- function expectCollapsed(isCollapsed: boolean) {
3538
- const label = isCollapsed ? 'Expand sidebar' : 'Collapse sidebar';
3539
- expect(screen.getByRole('button', {name: label})).toBeInTheDocument();
3540
- }
3541
-
3542
- describe('SideNav collapse ownership', () => {
3543
- beforeEach(() => {
3544
- localStorage.clear();
3545
- });
3546
-
3547
- // Every row is a configuration that works on origin/main today, with the
3548
- // collapse state main resolves for it. Normalizing the two props into one
3549
- // owner must not move any of these — the one case whose outcome does change
3550
- // is the divergence itself, pinned separately below.
3551
- const preservedCases: {
3552
- name: string;
3553
- persisted?: string;
3554
- element: React.ReactElement;
3555
- isCollapsed: boolean;
3556
- }[] = [
3557
- {
3558
- name: 'collapsible alone',
3559
- element: <SideNav collapsible>Content</SideNav>,
3560
- isCollapsed: false,
3561
- },
3562
- {
3563
- name: 'collapsible with defaultIsCollapsed',
3564
- element: (
3565
- <SideNav collapsible={{defaultIsCollapsed: true}}>Content</SideNav>
3566
- ),
3567
- isCollapsed: true,
3568
- },
3569
- {
3570
- name: 'controlled collapsible (collapsed)',
3571
- element: (
3572
- <SideNav collapsible={{isCollapsed: true, onCollapsedChange: () => {}}}>
3573
- Content
3574
- </SideNav>
3575
- ),
3576
- isCollapsed: true,
3577
- },
3578
- {
3579
- name: 'controlled collapsible (expanded)',
3580
- element: (
3581
- <SideNav
3582
- collapsible={{isCollapsed: false, onCollapsedChange: () => {}}}>
3583
- Content
3584
- </SideNav>
3585
- ),
3586
- isCollapsed: false,
3587
- },
3588
- {
3589
- name: 'collapsible + resizable',
3590
- element: (
3591
- <SideNav collapsible resizable>
3592
- Content
3593
- </SideNav>
3594
- ),
3595
- isCollapsed: false,
3596
- },
3597
- {
3598
- name: 'defaultIsCollapsed + resizable',
3599
- element: (
3600
- <SideNav collapsible={{defaultIsCollapsed: true}} resizable>
3601
- Content
3602
- </SideNav>
3603
- ),
3604
- isCollapsed: true,
3605
- },
3606
- {
3607
- name: 'controlled collapsible + resizable',
3608
- element: (
3609
- <SideNav
3610
- collapsible={{isCollapsed: true, onCollapsedChange: () => {}}}
3611
- resizable>
3612
- Content
3613
- </SideNav>
3614
- ),
3615
- isCollapsed: true,
3616
- },
3617
- {
3618
- name: 'defaultIsCollapsed + a persisted legacy width',
3619
- persisted: '300',
3620
- element: (
3621
- <SideNav
3622
- collapsible={{defaultIsCollapsed: true}}
3623
- resizable={{autoSaveId: AUTO_SAVE_ID}}>
3624
- Content
3625
- </SideNav>
3626
- ),
3627
- isCollapsed: true,
3628
- },
3629
- {
3630
- name: 'collapsible + a persisted legacy width',
3631
- persisted: '300',
3632
- element: (
3633
- <SideNav collapsible resizable={{autoSaveId: AUTO_SAVE_ID}}>
3634
- Content
3635
- </SideNav>
3636
- ),
3637
- isCollapsed: false,
3638
- },
3639
- ];
3640
-
3641
- it.each(preservedCases)(
3642
- 'resolves $name the way main does',
3643
- ({persisted, element, isCollapsed}) => {
3644
- if (persisted != null) {
3645
- localStorage.setItem(STORAGE_KEY, persisted);
3646
- }
3647
- render(element);
3648
- expectCollapsed(isCollapsed);
3649
- },
3650
- );
3651
-
3652
- it('keeps resizable alone free of collapse', () => {
3653
- render(<SideNav resizable>Content</SideNav>);
3654
- expect(
3655
- screen.queryByRole('button', {name: /sidebar/i}),
3656
- ).not.toBeInTheDocument();
3657
- expect(screen.getByRole('navigation').style.width).toBe('260px');
3658
- });
3659
-
3660
- it('restores the collapsed rail from a legacy persisted 0 (#4790)', () => {
3661
- // main renders the expanded layout here while the resize hook restores
3662
- // collapsed, so the nav paints at width 0 with no way back.
3663
- localStorage.setItem(STORAGE_KEY, '0');
3664
- render(
3665
- <SideNav collapsible resizable={{autoSaveId: AUTO_SAVE_ID}}>
3666
- Content
3667
- </SideNav>,
3668
- );
3669
-
3670
- expectCollapsed(true);
3671
- expect(screen.getByRole('navigation').style.width).not.toBe('0px');
3672
- });
3673
-
3674
- it('restores the collapsed rail and the pre-collapse width across a reload', async () => {
3675
- const user = userEvent.setup();
3676
- const nav = (
3677
- <SideNav
3678
- collapsible
3679
- resizable={{autoSaveId: AUTO_SAVE_ID, defaultWidth: 260}}>
3680
- Content
3681
- </SideNav>
3682
- );
3683
-
3684
- const first = render(nav);
3685
- await user.click(screen.getByRole('button', {name: 'Collapse sidebar'}));
3686
- expectCollapsed(true);
3687
- first.unmount();
3688
-
3689
- render(nav);
3690
- expectCollapsed(true);
3691
-
3692
- await user.click(screen.getByRole('button', {name: 'Expand sidebar'}));
3693
- expect(screen.getByRole('navigation').style.width).toBe('260px');
3694
- });
3695
-
3696
- it('expands to the persisted pre-collapse width, not the default', async () => {
3697
- const user = userEvent.setup();
3698
- localStorage.setItem(
3699
- STORAGE_KEY,
3700
- JSON.stringify({size: 320, isCollapsed: true}),
3701
- );
3702
- render(
3703
- <SideNav
3704
- collapsible
3705
- resizable={{autoSaveId: AUTO_SAVE_ID, defaultWidth: 260}}>
3706
- Content
3707
- </SideNav>,
3708
- );
3709
-
3710
- await user.click(screen.getByRole('button', {name: 'Expand sidebar'}));
3711
- expect(screen.getByRole('navigation').style.width).toBe('320px');
3712
- });
3713
-
3714
- it('reports one collapse change per toggle', async () => {
3715
- const user = userEvent.setup();
3716
- const onCollapsedChange = vi.fn();
3717
- render(
3718
- <SideNav collapsible={{onCollapsedChange}} resizable>
3719
- Content
3720
- </SideNav>,
3721
- );
3722
-
3723
- await user.click(screen.getByRole('button', {name: 'Collapse sidebar'}));
3724
- expect(onCollapsedChange).toHaveBeenCalledExactlyOnceWith(true);
3725
- });
3726
-
3727
- it('lets a controlled collapsible drive a resizable nav from outside', async () => {
3728
- const user = userEvent.setup();
3729
-
3730
- function Harness() {
3731
- const [isCollapsed, setIsCollapsed] = useState(false);
3732
- const collapsible = {isCollapsed, onCollapsedChange: setIsCollapsed};
3733
- return (
3734
- <>
3735
- <SideNavCollapseButton collapsible={collapsible} label="Outside" />
3736
- <SideNav
3737
- collapsible={{...collapsible, hasButton: false}}
3738
- resizable={{autoSaveId: AUTO_SAVE_ID}}>
3739
- Content
3740
- </SideNav>
3741
- </>
3742
- );
3743
- }
3744
-
3745
- render(<Harness />);
3746
- expect(screen.getByRole('navigation').style.width).toBe('260px');
3747
-
3748
- await user.click(screen.getByRole('button', {name: 'Outside'}));
3749
- expect(screen.getByRole('navigation').style.width).toBe('');
3750
- expect(
3751
- screen.queryByTestId('astryx-sidenav-resize-handle'),
3752
- ).not.toBeInTheDocument();
3753
- });
3754
-
3755
- it('lets resizable own collapse on its own', async () => {
3756
- const user = userEvent.setup();
3757
- const onCollapseChange = vi.fn();
3758
- render(
3759
- <SideNav resizable={{defaultIsCollapsed: true, onCollapseChange}}>
3760
- Content
3761
- </SideNav>,
3762
- );
3763
-
3764
- expectCollapsed(true);
3765
- await user.click(screen.getByRole('button', {name: 'Expand sidebar'}));
3766
- expect(onCollapseChange).toHaveBeenCalledExactlyOnceWith(false);
3767
- });
3768
-
3769
- it('keeps collapse state when resize is toggled off and on', async () => {
3770
- const user = userEvent.setup();
3771
- const {rerender} = render(
3772
- <SideNav collapsible resizable>
3773
- Content
3774
- </SideNav>,
3775
- );
3776
-
3777
- await user.click(screen.getByRole('button', {name: 'Collapse sidebar'}));
3778
- expectCollapsed(true);
3779
-
3780
- rerender(<SideNav collapsible>Content</SideNav>);
3781
- expectCollapsed(true);
3782
-
3783
- rerender(
3784
- <SideNav collapsible resizable>
3785
- Content
3786
- </SideNav>,
3787
- );
3788
- expectCollapsed(true);
3789
- });
3790
-
3791
- it('gives resizable the winning value when both set defaultIsCollapsed', () => {
3792
- render(
3793
- <SideNav
3794
- collapsible={{defaultIsCollapsed: false}}
3795
- resizable={{defaultIsCollapsed: true}}>
3796
- Content
3797
- </SideNav>,
3798
- );
3799
- expectCollapsed(true);
3800
- });
3801
- });
3802
-
3803
- describe('SideNav collapse conflict warning', () => {
3804
- let warn: ReturnType<typeof vi.spyOn>;
3805
-
3806
- beforeEach(() => {
3807
- localStorage.clear();
3808
- warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
3809
- });
3810
-
3811
- afterEach(() => {
3812
- warn.mockRestore();
3813
- });
3814
-
3815
- it('warns when both props carry defaultIsCollapsed', () => {
3816
- render(
3817
- <SideNav
3818
- collapsible={{defaultIsCollapsed: false}}
3819
- resizable={{defaultIsCollapsed: true}}>
3820
- Content
3821
- </SideNav>,
3822
- );
3823
-
3824
- expect(warn).toHaveBeenCalledExactlyOnceWith(
3825
- expect.stringContaining('collapsible.defaultIsCollapsed'),
3826
- );
3827
- expect(warn.mock.calls[0][0]).toContain('resizable.defaultIsCollapsed');
3828
- expect(warn.mock.calls[0][0]).toContain('resizable wins');
3829
- });
3830
-
3831
- it('warns when a controlled collapsible does not drive collapse', () => {
3832
- render(
3833
- <SideNav
3834
- collapsible={{isCollapsed: false, onCollapsedChange: () => {}}}
3835
- resizable={{isCollapsed: true, onCollapseChange: () => {}}}>
3836
- Content
3837
- </SideNav>,
3838
- );
3839
-
3840
- const message = String(warn.mock.calls[0][0]);
3841
- expect(message).toContain('collapsible.isCollapsed');
3842
- expect(message).toContain('collapsible.onCollapsedChange');
3843
- expect(message).toContain('resizable.isCollapsed');
3844
- expect(message).toContain('resizable wins');
3845
- // The winner is the state that actually renders.
3846
- expectCollapsed(true);
3847
- });
3848
-
3849
- it('does not split state and callback ownership across props', async () => {
3850
- const user = userEvent.setup();
3851
- const collapsibleChange = vi.fn();
3852
- const resizableChange = vi.fn();
3853
- render(
3854
- <SideNav
3855
- collapsible={{
3856
- isCollapsed: true,
3857
- onCollapsedChange: collapsibleChange,
3858
- }}
3859
- resizable={{onCollapseChange: resizableChange}}>
3860
- Content
3861
- </SideNav>,
3862
- );
3863
-
3864
- expectCollapsed(false);
3865
- await user.click(screen.getByRole('button', {name: 'Collapse sidebar'}));
3866
- expect(resizableChange).toHaveBeenCalledExactlyOnceWith(true);
3867
- expect(collapsibleChange).not.toHaveBeenCalled();
3868
- });
3869
-
3870
- it('stays silent for collapsible alongside a resize config', () => {
3871
- render(
3872
- <SideNav
3873
- collapsible
3874
- resizable={{autoSaveId: AUTO_SAVE_ID, defaultWidth: 300}}>
3875
- Content
3876
- </SideNav>,
3877
- );
3878
- expect(warn).not.toHaveBeenCalled();
3879
- });
3880
-
3881
- it('stays silent for a controlled collapsible with a plain resize config', () => {
3882
- render(
3883
- <SideNav
3884
- collapsible={{isCollapsed: true, onCollapsedChange: () => {}}}
3885
- resizable={{defaultWidth: 300}}>
3886
- Content
3887
- </SideNav>,
3888
- );
3889
- expect(warn).not.toHaveBeenCalled();
3890
- });
3891
- });
3892
-
3893
- // =============================================================================
3894
- // SideNavItem — collapsed-from-mount rail tooltip
3895
- // =============================================================================
3896
-
3897
- // Two interchangeable link components: switching between them remounts the
3898
- // rendered link element.
3899
- function RailLinkA({ref, ...props}: React.ComponentPropsWithRef<'a'>) {
3900
- return <a ref={ref} {...props} />;
3901
- }
3902
-
3903
- function RailLinkB({ref, ...props}: React.ComponentPropsWithRef<'a'>) {
3904
- return <a ref={ref} {...props} />;
3905
- }
3906
-
3907
- describe('SideNavItem collapsed-from-mount rail tooltip', () => {
3908
- // #7435 / #7692: a childless item that mounts inside an already-collapsed
3909
- // SideNav (the persisted-collapse case) must attach its tooltip on the first
3910
- // render, and the tooltip must describe the control that takes focus — the
3911
- // link or button — not the row around it.
3912
- function railTooltipFor(trigger: HTMLElement) {
3913
- const id = trigger.getAttribute('aria-describedby');
3914
- return id == null ? null : document.getElementById(id);
3915
- }
3916
-
3917
- function renderRail(item: ReactNode) {
3918
- return render(
3919
- <SideNav
3920
- aria-label="Main"
3921
- collapsible={{isCollapsed: true, hasButton: false}}>
3922
- {item}
3923
- </SideNav>,
3924
- );
3925
- }
3926
-
3927
- let showPopover: ReturnType<typeof vi.spyOn> | undefined;
3928
-
3929
- afterEach(() => {
3930
- showPopover?.mockRestore();
3931
- showPopover = undefined;
3932
- vi.useRealTimers();
3933
- });
3934
-
3935
- it('describes the rail link by its tooltip when the nav is collapsed at mount', () => {
3936
- renderRail(
3937
- <SideNavItem label="Dashboard" icon={StubIcon} href="/dashboard" />,
3938
- );
3939
- const link = screen.getByRole('link', {name: 'Dashboard'});
3940
-
3941
- const layer = railTooltipFor(link);
3942
- expect(layer).not.toBeNull();
3943
- expect(layer).toHaveAttribute('role', 'tooltip');
3944
- expect(layer).toHaveTextContent('Dashboard');
3945
- });
3946
-
3947
- it('shows a disabled item its label when the pointer rests on its row', async () => {
3948
- // The disabled control is `pointer-events: none`, so in a browser the
3949
- // pointer lands on the row; the tooltip has to listen there.
3950
- vi.useFakeTimers();
3951
- const spy = vi.spyOn(HTMLElement.prototype, 'showPopover');
3952
- showPopover = spy;
3953
- renderRail(
3954
- <SideNavItem
3955
- label="Analytics (Coming Soon)"
3956
- icon={StubIcon}
3957
- href="/analytics"
3958
- isDisabled
3959
- />,
3960
- );
3961
- // Let the row's tooltip attach after the commit.
3962
- await act(async () => {});
3963
- const button = screen.getByRole('button', {
3964
- name: 'Analytics (Coming Soon)',
3965
- });
3966
- const row = button.parentElement as HTMLElement;
3967
- const layer = screen.getByRole('tooltip', {hidden: true});
3968
- expect(layer).toHaveTextContent('Analytics (Coming Soon)');
3969
-
3970
- fireEvent.mouseEnter(row);
3971
- act(() => {
3972
- vi.advanceTimersByTime(200);
3973
- });
3974
- expect(spy.mock.contexts).toContain(layer);
3975
- });
3976
-
3977
- it('describes the link once a disabled item is enabled in the rail', () => {
3978
- const item = (isDisabled: boolean) => (
3979
- <SideNavItem
3980
- label="Dashboard"
3981
- icon={StubIcon}
3982
- href="/dashboard"
3983
- isDisabled={isDisabled}
3984
- />
3985
- );
3986
- const {rerender} = renderRail(item(true));
3987
- rerender(
3988
- <SideNav
3989
- aria-label="Main"
3990
- collapsible={{isCollapsed: true, hasButton: false}}>
3991
- {item(false)}
3992
- </SideNav>,
3993
- );
3994
-
3995
- const link = screen.getByRole('link', {name: 'Dashboard'});
3996
- expect(railTooltipFor(link)).toHaveTextContent('Dashboard');
3997
- });
3998
-
3999
- it('describes the new control when a rail item gains an href', () => {
4000
- const item = (href?: string) => (
4001
- <SideNavItem
4002
- label="Dashboard"
4003
- icon={StubIcon}
4004
- href={href}
4005
- onClick={() => {}}
4006
- />
4007
- );
4008
- const {rerender} = renderRail(item());
4009
- const button = screen.getByRole('button', {name: 'Dashboard'});
4010
- expect(railTooltipFor(button)).toHaveTextContent('Dashboard');
4011
-
4012
- rerender(
4013
- <SideNav
4014
- aria-label="Main"
4015
- collapsible={{isCollapsed: true, hasButton: false}}>
4016
- {item('/dashboard')}
4017
- </SideNav>,
4018
- );
4019
- const link = screen.getByRole('link', {name: 'Dashboard'});
4020
- expect(railTooltipFor(link)).toHaveTextContent('Dashboard');
4021
- });
4022
-
4023
- it('binds the replacement when a rail link element is replaced', () => {
4024
- // Switching the link component remounts the element; the tooltip must
4025
- // follow the new element.
4026
- vi.useFakeTimers();
4027
- const spy = vi.spyOn(HTMLElement.prototype, 'showPopover');
4028
- showPopover = spy;
4029
- const item = (as: typeof RailLinkA) => (
4030
- <SideNavItem
4031
- label="Dashboard"
4032
- icon={StubIcon}
4033
- href="/dashboard"
4034
- as={as}
4035
- />
4036
- );
4037
- const {rerender} = renderRail(item(RailLinkA));
4038
- const first = screen.getByRole('link', {name: 'Dashboard'});
4039
- rerender(
4040
- <SideNav
4041
- aria-label="Main"
4042
- collapsible={{isCollapsed: true, hasButton: false}}>
4043
- {item(RailLinkB)}
4044
- </SideNav>,
4045
- );
4046
-
4047
- const link = screen.getByRole('link', {name: 'Dashboard'});
4048
- expect(link).not.toBe(first);
4049
- const layer = railTooltipFor(link);
4050
- expect(layer).toHaveTextContent('Dashboard');
4051
- fireEvent.mouseEnter(link);
4052
- act(() => {
4053
- vi.advanceTimersByTime(200);
4054
- });
4055
- expect(spy.mock.contexts).toContain(layer);
4056
- });
4057
-
4058
- it("keeps the tooltip beside a caller's changing aria-describedby", () => {
4059
- const item = (hint: string) => (
4060
- <SideNavItem
4061
- label="Dashboard"
4062
- icon={StubIcon}
4063
- href="/dashboard"
4064
- aria-describedby={hint}
4065
- />
4066
- );
4067
- const {rerender} = renderRail(item('hint-a'));
4068
- rerender(
4069
- <SideNav
4070
- aria-label="Main"
4071
- collapsible={{isCollapsed: true, hasButton: false}}>
4072
- {item('hint-b')}
4073
- </SideNav>,
4074
- );
4075
-
4076
- const link = screen.getByRole('link', {name: 'Dashboard'});
4077
- const tooltip = screen.getByRole('tooltip', {hidden: true});
4078
- const ids = (link.getAttribute('aria-describedby') ?? '').split(' ');
4079
- expect(ids).toContain('hint-b');
4080
- expect(ids).toContain(tooltip.id);
4081
- expect(ids).not.toContain('hint-a');
4082
- });
4083
-
4084
- it('shows the label when keyboard focus reaches a rail link', () => {
4085
- const spy = vi.spyOn(HTMLElement.prototype, 'showPopover');
4086
- showPopover = spy;
4087
- renderRail(
4088
- <SideNavItem label="Dashboard" icon={StubIcon} href="/dashboard" />,
4089
- );
4090
- const link = screen.getByRole('link', {name: 'Dashboard'});
4091
- const layer = railTooltipFor(link);
4092
- // jsdom does not model `:focus-visible`; stand in for a keyboard focus.
4093
- const realMatches = link.matches.bind(link);
4094
- vi.spyOn(link, 'matches').mockImplementation((selector: string) =>
4095
- selector === ':focus-visible' ? true : realMatches(selector),
4096
- );
4097
-
4098
- fireEvent.focusIn(link);
4099
- expect(spy.mock.contexts).toContain(layer);
4100
- });
4101
-
4102
- it('keeps a tooltip that was open when the rail expanded shut on the next collapse', () => {
4103
- vi.useFakeTimers();
4104
- const spy = vi.spyOn(HTMLElement.prototype, 'showPopover');
4105
- showPopover = spy;
4106
- const rail = (isCollapsed: boolean) => (
4107
- <SideNav aria-label="Main" collapsible={{isCollapsed, hasButton: false}}>
4108
- <SideNavItem label="Dashboard" icon={StubIcon} href="/dashboard" />
4109
- </SideNav>
4110
- );
4111
- const {rerender} = render(rail(true));
4112
- const link = screen.getByRole('link', {name: 'Dashboard'});
4113
- fireEvent.mouseEnter(link);
4114
- act(() => {
4115
- vi.advanceTimersByTime(200);
4116
- });
4117
- expect(spy.mock.contexts).toContain(railTooltipFor(link));
4118
-
4119
- // Expanding (from a shortcut, say) removes the trigger without a leave.
4120
- rerender(rail(false));
4121
- spy.mockClear();
4122
- rerender(rail(true));
4123
- act(() => {
4124
- vi.advanceTimersByTime(500);
4125
- });
4126
- expect(spy).not.toHaveBeenCalled();
4127
- });
4128
- });