@primitiv-ui/react 0.1.28 → 0.1.30

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 (456) hide show
  1. package/README.md +19 -2
  2. package/package.json +6 -1
  3. package/src/AccessibleIcon/AccessibleIcon.tsx +32 -9
  4. package/src/AccessibleIcon/__tests__/AccessibleIcon.test.tsx +5 -0
  5. package/src/AccessibleIcon/types.ts +15 -2
  6. package/src/Accordion/Accordion.tsx +32 -11
  7. package/src/Accordion/README.md +2 -2
  8. package/src/Accordion/__tests__/Accordion.asChild.test.tsx +46 -0
  9. package/src/Accordion/__tests__/Accordion.basic-rendering.test.tsx +15 -0
  10. package/src/Accordion/__tests__/Accordion.disabled-items.test.tsx +42 -0
  11. package/src/Accordion/__tests__/Accordion.error-handling.test.tsx +33 -0
  12. package/src/Accordion/hooks/useAccordionRoot.ts +44 -7
  13. package/src/Accordion/hooks/useAccordionTrigger.ts +9 -1
  14. package/src/Accordion/types.ts +72 -21
  15. package/src/Alert/Alert.tsx +36 -7
  16. package/src/Alert/__tests__/Alert.test.tsx +5 -0
  17. package/src/Alert/types.ts +18 -2
  18. package/src/Avatar/Avatar.tsx +99 -15
  19. package/src/Avatar/__tests__/Avatar.basic-rendering.test.tsx +10 -0
  20. package/src/Avatar/__tests__/Avatar.fallback.test.tsx +30 -0
  21. package/src/Avatar/__tests__/Avatar.image-loading.test.tsx +35 -0
  22. package/src/Avatar/hooks/useAvatarImage.ts +16 -0
  23. package/src/Avatar/types.ts +15 -3
  24. package/src/Breadcrumb/Breadcrumb.tsx +139 -21
  25. package/src/Breadcrumb/README.md +47 -0
  26. package/src/Breadcrumb/__tests__/Breadcrumb.basic-rendering.test.tsx +11 -0
  27. package/src/Breadcrumb/__tests__/Breadcrumb.ellipsis.test.tsx +110 -0
  28. package/src/Breadcrumb/types.ts +73 -7
  29. package/src/Button/Button.tsx +2 -0
  30. package/src/Button/__tests__/Button.basic-rendering.test.tsx +6 -0
  31. package/src/Button/__tests__/Button.contract.test.tsx +1 -1
  32. package/src/Button/types.ts +21 -0
  33. package/src/Carousel/Carousel.tsx +268 -20
  34. package/src/Carousel/README.md +764 -87
  35. package/src/Carousel/__tests__/Carousel.autoplay-status.test.tsx +239 -0
  36. package/src/Carousel/__tests__/Carousel.drag-status.test.tsx +292 -0
  37. package/src/Carousel/__tests__/Carousel.dynamic-slides.test.tsx +38 -0
  38. package/src/Carousel/__tests__/Carousel.indicators.test.tsx +101 -0
  39. package/src/Carousel/__tests__/Carousel.infinite.test.tsx +1453 -0
  40. package/src/Carousel/__tests__/Carousel.instant-scroll.test.tsx +124 -0
  41. package/src/Carousel/__tests__/Carousel.intersection-observer.test.tsx +90 -2
  42. package/src/Carousel/__tests__/Carousel.loop.test.tsx +163 -0
  43. package/src/Carousel/__tests__/Carousel.loopEngine.test.ts +77 -0
  44. package/src/Carousel/__tests__/Carousel.mouse-drag.test.tsx +504 -0
  45. package/src/Carousel/__tests__/Carousel.multi-slide-bounds.test.tsx +91 -0
  46. package/src/Carousel/__tests__/Carousel.orientation.test.tsx +207 -0
  47. package/src/Carousel/__tests__/Carousel.overscroll.test.tsx +532 -0
  48. package/src/Carousel/__tests__/Carousel.page-snap-points.test.tsx +50 -0
  49. package/src/Carousel/__tests__/Carousel.progress-text.test.tsx +76 -0
  50. package/src/Carousel/__tests__/Carousel.reduced-motion.test.tsx +3 -6
  51. package/src/Carousel/__tests__/Carousel.refresh-progress.test.tsx +2 -5
  52. package/src/Carousel/__tests__/Carousel.scroll-margin.test.tsx +167 -0
  53. package/src/Carousel/__tests__/Carousel.scroll-progress.test.tsx +518 -0
  54. package/src/Carousel/__tests__/Carousel.scroll-snap-change.test.tsx +32 -11
  55. package/src/Carousel/__tests__/Carousel.scroll-sync.test.tsx +237 -24
  56. package/src/Carousel/__tests__/Carousel.scroll-to-index.test.tsx +115 -0
  57. package/src/Carousel/__tests__/Carousel.slide-snap.test.tsx +172 -0
  58. package/src/Carousel/__tests__/Carousel.slides-per-move.test.tsx +179 -1
  59. package/src/Carousel/__tests__/Carousel.slides-per-page.test.tsx +91 -7
  60. package/src/Carousel/__tests__/Carousel.snap-type.test.tsx +35 -0
  61. package/src/Carousel/__tests__/Carousel.transition-modes.test.tsx +30 -5
  62. package/src/Carousel/__tests__/Carousel.translations.test.tsx +23 -0
  63. package/src/Carousel/__tests__/Carousel.wheel.test.tsx +160 -0
  64. package/src/Carousel/hooks/index.ts +1 -0
  65. package/src/Carousel/hooks/useCarouselLoop.ts +869 -0
  66. package/src/Carousel/hooks/useCarouselRoot.ts +338 -42
  67. package/src/Carousel/hooks/useCarouselSlide.ts +44 -9
  68. package/src/Carousel/hooks/useCarouselViewport.ts +649 -46
  69. package/src/Carousel/loopEngine.ts +54 -0
  70. package/src/Carousel/types.ts +458 -36
  71. package/src/Checkbox/Checkbox.tsx +11 -3
  72. package/src/Checkbox/__tests__/Checkbox.basic-rendering.test.tsx +14 -0
  73. package/src/Checkbox/hooks/useCheckboxInput.ts +4 -0
  74. package/src/Checkbox/types.ts +19 -0
  75. package/src/CheckboxCard/CheckboxCard.tsx +14 -5
  76. package/src/CheckboxCard/README.md +1 -1
  77. package/src/CheckboxCard/__tests__/CheckboxCard.basic-rendering.test.tsx +7 -0
  78. package/src/CheckboxCard/types.ts +65 -17
  79. package/src/Collapsible/Collapsible.tsx +49 -3
  80. package/src/Collapsible/README.md +41 -1
  81. package/src/Collapsible/__tests__/Collapsible.asChild.test.tsx +89 -0
  82. package/src/Collapsible/__tests__/Collapsible.basic-rendering.test.tsx +27 -0
  83. package/src/Collapsible/__tests__/Collapsible.collapsedHeight.test.tsx +140 -0
  84. package/src/Collapsible/hooks/useCollapsibleTrigger.ts +3 -1
  85. package/src/Collapsible/types.ts +89 -22
  86. package/src/Combobox/Combobox.tsx +508 -0
  87. package/src/Combobox/ComboboxContext.ts +23 -0
  88. package/src/Combobox/README.md +170 -0
  89. package/src/Combobox/__tests__/Combobox.asChild.test.tsx +62 -0
  90. package/src/Combobox/__tests__/Combobox.basic-rendering.test.tsx +16 -0
  91. package/src/Combobox/__tests__/Combobox.content.test.tsx +36 -0
  92. package/src/Combobox/__tests__/Combobox.controlled-state.test.tsx +88 -0
  93. package/src/Combobox/__tests__/Combobox.empty.test.tsx +42 -0
  94. package/src/Combobox/__tests__/Combobox.ids.test.tsx +66 -0
  95. package/src/Combobox/__tests__/Combobox.item.test.tsx +76 -0
  96. package/src/Combobox/__tests__/Combobox.keyboard-interaction.test.tsx +196 -0
  97. package/src/Combobox/__tests__/Combobox.light-dismiss.test.tsx +101 -0
  98. package/src/Combobox/__tests__/Combobox.query.test.tsx +105 -0
  99. package/src/Combobox/hooks/index.ts +1 -0
  100. package/src/Combobox/hooks/useComboboxRoot.ts +2 -0
  101. package/src/Combobox/index.ts +3 -0
  102. package/src/Combobox/types.ts +133 -0
  103. package/src/ContextMenu/ContextMenu.tsx +270 -4
  104. package/src/ContextMenu/README.md +1 -1
  105. package/src/ContextMenu/types.ts +148 -6
  106. package/src/DirectionProvider/DirectionProvider.tsx +41 -9
  107. package/src/DirectionProvider/README.md +4 -4
  108. package/src/DirectionProvider/__tests__/DirectionProvider.test.tsx +5 -0
  109. package/src/DirectionProvider/types.ts +28 -3
  110. package/src/Divider/Divider.tsx +24 -10
  111. package/src/Divider/__tests__/Divider.contract.test.tsx +1 -1
  112. package/src/Divider/__tests__/Divider.test.tsx +5 -0
  113. package/src/Divider/types.ts +10 -3
  114. package/src/Drawer/Drawer.tsx +315 -0
  115. package/src/Drawer/README.md +124 -0
  116. package/src/Drawer/__tests__/Drawer.behavior.test.tsx +100 -0
  117. package/src/Drawer/__tests__/Drawer.focus-trap.test.tsx +55 -0
  118. package/src/Drawer/__tests__/Drawer.rendering.test.tsx +111 -0
  119. package/src/Drawer/__tests__/Drawer.side.test.tsx +69 -0
  120. package/src/Drawer/__tests__/dialog-polyfill.ts +39 -0
  121. package/src/Drawer/index.ts +2 -0
  122. package/src/Drawer/types.ts +66 -0
  123. package/src/Dropdown/Dropdown.tsx +119 -2
  124. package/src/Dropdown/README.md +7 -5
  125. package/src/Dropdown/types.ts +241 -11
  126. package/src/EmptyState/EmptyState.tsx +74 -4
  127. package/src/EmptyState/README.md +2 -2
  128. package/src/EmptyState/__tests__/EmptyState.Actions.test.tsx +7 -2
  129. package/src/EmptyState/__tests__/EmptyState.Description.test.tsx +3 -1
  130. package/src/EmptyState/__tests__/EmptyState.Root.test.tsx +10 -0
  131. package/src/EmptyState/__tests__/EmptyState.Title.test.tsx +3 -1
  132. package/src/EmptyState/types.ts +49 -6
  133. package/src/Field/Field.tsx +23 -0
  134. package/src/Field/README.md +1 -1
  135. package/src/Field/__tests__/Field.basic-rendering.test.tsx +11 -0
  136. package/src/Field/__tests__/Field.error-handling.test.tsx +23 -0
  137. package/src/Field/__tests__/Field.state-cascade.test.tsx +21 -0
  138. package/src/Field/hooks/__tests__/useFieldProps.test.tsx +104 -9
  139. package/src/Field/hooks/useFieldProps.ts +1 -1
  140. package/src/Field/types.ts +60 -12
  141. package/src/Fieldset/Fieldset.tsx +55 -19
  142. package/src/Fieldset/README.md +2 -2
  143. package/src/Fieldset/__tests__/Fieldset.basic-rendering.test.tsx +9 -0
  144. package/src/Fieldset/types.ts +20 -2
  145. package/src/Input/Input.tsx +15 -10
  146. package/src/Input/README.md +2 -2
  147. package/src/Input/__tests__/Input.basic-rendering.test.tsx +5 -0
  148. package/src/Input/types.ts +19 -2
  149. package/src/InputGroup/InputGroup.tsx +11 -3
  150. package/src/InputGroup/__tests__/InputGroup.basic-rendering.test.tsx +14 -0
  151. package/src/InputGroup/types.ts +26 -5
  152. package/src/Listbox/Listbox.tsx +475 -0
  153. package/src/Listbox/ListboxContext.ts +48 -0
  154. package/src/Listbox/README.md +248 -0
  155. package/src/Listbox/__tests__/Listbox.active-descendant.test.tsx +128 -0
  156. package/src/Listbox/__tests__/Listbox.asChild.test.tsx +91 -0
  157. package/src/Listbox/__tests__/Listbox.basic-rendering.test.tsx +16 -0
  158. package/src/Listbox/__tests__/Listbox.controlled-state.test.tsx +114 -0
  159. package/src/Listbox/__tests__/Listbox.disabled.test.tsx +90 -0
  160. package/src/Listbox/__tests__/Listbox.fixtures.ts +27 -0
  161. package/src/Listbox/__tests__/Listbox.group-label.test.tsx +97 -0
  162. package/src/Listbox/__tests__/Listbox.group.test.tsx +45 -0
  163. package/src/Listbox/__tests__/Listbox.keyboard-edge-cases.test.tsx +144 -0
  164. package/src/Listbox/__tests__/Listbox.keyboard-interaction.test.tsx +112 -0
  165. package/src/Listbox/__tests__/Listbox.modifier-keys.test.tsx +86 -0
  166. package/src/Listbox/__tests__/Listbox.multi-select-keyboard.test.tsx +368 -0
  167. package/src/Listbox/__tests__/Listbox.multiple-mode.test.tsx +91 -0
  168. package/src/Listbox/__tests__/Listbox.mutation-hardening.test.tsx +480 -0
  169. package/src/Listbox/__tests__/Listbox.orientation.test.tsx +59 -0
  170. package/src/Listbox/__tests__/Listbox.reading-direction.test.tsx +65 -0
  171. package/src/Listbox/__tests__/Listbox.reorder.test.tsx +78 -0
  172. package/src/Listbox/__tests__/Listbox.selection-follows-focus.test.tsx +64 -0
  173. package/src/Listbox/__tests__/Listbox.strict-context.test.tsx +21 -0
  174. package/src/Listbox/__tests__/Listbox.typeahead.test.tsx +123 -0
  175. package/src/Listbox/__tests__/Listbox.uncontrolled-state.test.tsx +48 -0
  176. package/src/Listbox/constants.ts +2 -0
  177. package/src/Listbox/hooks/index.ts +1 -0
  178. package/src/Listbox/hooks/useListboxRoot.ts +407 -0
  179. package/src/Listbox/index.ts +3 -0
  180. package/src/Listbox/types.ts +238 -0
  181. package/src/MillerColumns/MillerColumns.tsx +186 -45
  182. package/src/MillerColumns/README.md +90 -20
  183. package/src/MillerColumns/__tests__/MillerColumns.aria.test.tsx +45 -0
  184. package/src/MillerColumns/__tests__/MillerColumns.auto-scroll.test.tsx +4 -2
  185. package/src/MillerColumns/__tests__/MillerColumns.basic-rendering.test.tsx +18 -3
  186. package/src/MillerColumns/__tests__/MillerColumns.data-attributes.test.tsx +48 -0
  187. package/src/MillerColumns/__tests__/MillerColumns.keyboard-interaction.test.tsx +35 -2
  188. package/src/MillerColumns/__tests__/MillerColumns.keyboard-selection-state.test.tsx +96 -0
  189. package/src/MillerColumns/__tests__/MillerColumns.preview-panel.test.tsx +29 -4
  190. package/src/MillerColumns/__tests__/MillerColumns.preview-visibility.test.tsx +79 -0
  191. package/src/MillerColumns/__tests__/MillerColumns.resize.test.tsx +189 -0
  192. package/src/MillerColumns/__tests__/MillerColumns.styled-parts.test.tsx +143 -0
  193. package/src/MillerColumns/__tests__/MillerColumns.typeahead.test.tsx +104 -0
  194. package/src/MillerColumns/constants.ts +2 -0
  195. package/src/MillerColumns/hooks/useMillerColumnsColumn.ts +5 -2
  196. package/src/MillerColumns/hooks/useMillerColumnsItem.ts +29 -7
  197. package/src/MillerColumns/hooks/useMillerColumnsResizeHandle.ts +80 -10
  198. package/src/MillerColumns/hooks/useMillerColumnsRoot.ts +0 -0
  199. package/src/MillerColumns/index.ts +4 -0
  200. package/src/MillerColumns/types.ts +136 -30
  201. package/src/MillerColumns/utils.ts +69 -3
  202. package/src/Modal/Modal.tsx +35 -8
  203. package/src/Modal/README.md +24 -15
  204. package/src/Modal/__tests__/Modal.focus-trap.test.tsx +162 -0
  205. package/src/Modal/hooks/useModalContent.ts +32 -0
  206. package/src/Modal/types.ts +110 -29
  207. package/src/NavigationMenu/NavigationMenu.tsx +890 -0
  208. package/src/NavigationMenu/NavigationMenuContext.ts +67 -0
  209. package/src/NavigationMenu/README.md +355 -0
  210. package/src/NavigationMenu/__tests__/NavigationMenu.anchor-positioning.test.tsx +132 -0
  211. package/src/NavigationMenu/__tests__/NavigationMenu.asChild.test.tsx +73 -0
  212. package/src/NavigationMenu/__tests__/NavigationMenu.basic-rendering.test.tsx +92 -0
  213. package/src/NavigationMenu/__tests__/NavigationMenu.contract.test.tsx +45 -0
  214. package/src/NavigationMenu/__tests__/NavigationMenu.controlled-state.test.tsx +134 -0
  215. package/src/NavigationMenu/__tests__/NavigationMenu.error-handling.test.tsx +62 -0
  216. package/src/NavigationMenu/__tests__/NavigationMenu.fixtures.tsx +79 -0
  217. package/src/NavigationMenu/__tests__/NavigationMenu.forceMount.test.tsx +52 -0
  218. package/src/NavigationMenu/__tests__/NavigationMenu.hover-interaction.test.tsx +254 -0
  219. package/src/NavigationMenu/__tests__/NavigationMenu.indicator.test.tsx +252 -0
  220. package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-interaction.test.tsx +294 -0
  221. package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-navigation.test.tsx +126 -0
  222. package/src/NavigationMenu/__tests__/NavigationMenu.link.test.tsx +139 -0
  223. package/src/NavigationMenu/__tests__/NavigationMenu.motion.test.tsx +568 -0
  224. package/src/NavigationMenu/__tests__/NavigationMenu.mouse-interaction.test.tsx +76 -0
  225. package/src/NavigationMenu/__tests__/NavigationMenu.uncontrolled-state.test.tsx +30 -0
  226. package/src/NavigationMenu/__tests__/NavigationMenu.viewport.test.tsx +113 -0
  227. package/src/NavigationMenu/constants.ts +5 -0
  228. package/src/NavigationMenu/hooks/index.ts +7 -0
  229. package/src/NavigationMenu/hooks/useNavigationMenuEntry.ts +33 -0
  230. package/src/NavigationMenu/hooks/useNavigationMenuIndicator.ts +70 -0
  231. package/src/NavigationMenu/hooks/useNavigationMenuLink.ts +72 -0
  232. package/src/NavigationMenu/hooks/useNavigationMenuRoot.ts +301 -0
  233. package/src/NavigationMenu/hooks/useNavigationMenuTopLevelEntry.ts +65 -0
  234. package/src/NavigationMenu/hooks/useNavigationMenuTrigger.ts +158 -0
  235. package/src/NavigationMenu/hooks/useNavigationMenuViewport.ts +101 -0
  236. package/src/NavigationMenu/index.ts +3 -0
  237. package/src/NavigationMenu/types.ts +259 -0
  238. package/src/NavigationMenu/utils.ts +89 -0
  239. package/src/Popover/Popover.tsx +427 -0
  240. package/src/Popover/PopoverContext.ts +13 -0
  241. package/src/Popover/README.md +106 -0
  242. package/src/Popover/__tests__/Popover.anchor.test.tsx +38 -0
  243. package/src/Popover/__tests__/Popover.asChild.test.tsx +160 -0
  244. package/src/Popover/__tests__/Popover.basic-rendering.test.tsx +106 -0
  245. package/src/Popover/__tests__/Popover.click-outside.test.tsx +149 -0
  246. package/src/Popover/__tests__/Popover.close.test.tsx +70 -0
  247. package/src/Popover/__tests__/Popover.content.test.tsx +58 -0
  248. package/src/Popover/__tests__/Popover.controlled-state.test.tsx +45 -0
  249. package/src/Popover/__tests__/Popover.error-handling.test.tsx +51 -0
  250. package/src/Popover/__tests__/Popover.focus.test.tsx +43 -0
  251. package/src/Popover/__tests__/Popover.keyboard-interaction.test.tsx +71 -0
  252. package/src/Popover/__tests__/Popover.mouse-interaction.test.tsx +69 -0
  253. package/src/Popover/__tests__/Popover.uncontrolled-state.test.tsx +44 -0
  254. package/src/Popover/hooks/index.ts +3 -0
  255. package/src/Popover/hooks/usePopoverContent.ts +98 -0
  256. package/src/Popover/hooks/usePopoverRoot.ts +84 -0
  257. package/src/Popover/hooks/usePopoverTrigger.ts +33 -0
  258. package/src/Popover/index.ts +2 -0
  259. package/src/Popover/types.ts +197 -0
  260. package/src/Portal/Portal.tsx +50 -10
  261. package/src/Portal/README.md +2 -2
  262. package/src/Portal/__tests__/Portal.basic-rendering.test.tsx +5 -0
  263. package/src/Portal/types.ts +5 -0
  264. package/src/Progress/Progress.tsx +33 -1
  265. package/src/Progress/README.md +1 -1
  266. package/src/Progress/__tests__/Progress.basic-rendering.test.tsx +10 -0
  267. package/src/Progress/__tests__/Progress.value.test.tsx +21 -0
  268. package/src/Progress/types.ts +20 -4
  269. package/src/Radio/Radio.tsx +10 -2
  270. package/src/Radio/__tests__/Radio.basic-rendering.test.tsx +14 -0
  271. package/src/Radio/hooks/useRadioRoot.ts +3 -0
  272. package/src/Radio/types.ts +20 -0
  273. package/src/RadioCard/RadioCard.tsx +11 -0
  274. package/src/RadioCard/__tests__/RadioCard.basic-rendering.test.tsx +8 -0
  275. package/src/RadioCard/__tests__/RadioCard.disabled-items.test.tsx +27 -0
  276. package/src/RadioCard/__tests__/RadioCard.keyboard-interaction.test.tsx +53 -0
  277. package/src/RadioCard/__tests__/RadioCard.ref-forwarding.test.tsx +54 -0
  278. package/src/RadioCard/__tests__/RadioCard.uncontrolled-state.test.tsx +21 -0
  279. package/src/RadioCard/hooks/useRadioCardRoot.ts +11 -0
  280. package/src/RadioCard/types.ts +95 -22
  281. package/src/RadioGroup/README.md +2 -2
  282. package/src/RadioGroup/RadioGroup.tsx +12 -1
  283. package/src/RadioGroup/__tests__/RadioGroup.basic-rendering.test.tsx +8 -0
  284. package/src/RadioGroup/__tests__/RadioGroup.disabled.test.tsx +27 -0
  285. package/src/RadioGroup/__tests__/RadioGroup.ref-forwarding.test.tsx +29 -0
  286. package/src/RadioGroup/__tests__/RadioGroup.roving-tabindex.test.tsx +31 -0
  287. package/src/RadioGroup/hooks/useRadioGroupRoot.ts +11 -0
  288. package/src/RadioGroup/types.ts +73 -9
  289. package/src/SegmentedControl/README.md +154 -0
  290. package/src/SegmentedControl/SegmentedControl.tsx +309 -0
  291. package/src/SegmentedControl/SegmentedControlContext.ts +28 -0
  292. package/src/SegmentedControl/__tests__/SegmentedControl.asChild.test.tsx +50 -0
  293. package/src/SegmentedControl/__tests__/SegmentedControl.basic-rendering.test.tsx +100 -0
  294. package/src/SegmentedControl/__tests__/SegmentedControl.controlled-state.test.tsx +107 -0
  295. package/src/SegmentedControl/__tests__/SegmentedControl.disabled-keydown-guards.test.tsx +62 -0
  296. package/src/SegmentedControl/__tests__/SegmentedControl.disabled.test.tsx +151 -0
  297. package/src/SegmentedControl/__tests__/SegmentedControl.error-handling.test.tsx +17 -0
  298. package/src/SegmentedControl/__tests__/SegmentedControl.keyboard-interaction.test.tsx +117 -0
  299. package/src/SegmentedControl/__tests__/SegmentedControl.orientation.test.tsx +54 -0
  300. package/src/SegmentedControl/__tests__/SegmentedControl.reading-direction.test.tsx +56 -0
  301. package/src/SegmentedControl/__tests__/SegmentedControl.ref-forwarding.test.tsx +45 -0
  302. package/src/SegmentedControl/__tests__/SegmentedControl.roving-tabindex.test.tsx +104 -0
  303. package/src/SegmentedControl/__tests__/SegmentedControl.uncontrolled-state.test.tsx +94 -0
  304. package/src/SegmentedControl/hooks/index.ts +2 -0
  305. package/src/SegmentedControl/hooks/useSegmentedControlContext.ts +1 -0
  306. package/src/SegmentedControl/hooks/useSegmentedControlRoot.ts +87 -0
  307. package/src/SegmentedControl/index.ts +2 -0
  308. package/src/SegmentedControl/types.ts +138 -0
  309. package/src/Select/README.md +122 -157
  310. package/src/Select/Select.tsx +596 -69
  311. package/src/Select/SelectContext.ts +38 -0
  312. package/src/Select/SelectItemIndicatorContext.ts +29 -0
  313. package/src/Select/__tests__/Select.asChild.test.tsx +2 -2
  314. package/src/Select/__tests__/Select.basic-rendering.test.tsx +22 -3
  315. package/src/Select/__tests__/Select.controlled-state.test.tsx +6 -6
  316. package/src/Select/__tests__/Select.data-attributes.test.tsx +4 -4
  317. package/src/Select/__tests__/Select.field-integration.test.tsx +11 -11
  318. package/src/Select/__tests__/Select.form.test.tsx +38 -0
  319. package/src/Select/__tests__/Select.group.test.tsx +6 -6
  320. package/src/Select/__tests__/Select.keyboard-edge.test.tsx +95 -0
  321. package/src/Select/__tests__/Select.keyboard.test.tsx +124 -0
  322. package/src/Select/__tests__/Select.mutation-hardening.test.tsx +317 -0
  323. package/src/Select/__tests__/Select.native-item.test.tsx +30 -0
  324. package/src/Select/__tests__/Select.open-close.test.tsx +57 -0
  325. package/src/Select/__tests__/Select.placeholder.test.tsx +7 -7
  326. package/src/Select/__tests__/Select.rich-asChild.test.tsx +60 -0
  327. package/src/Select/__tests__/Select.rich-mode.test.tsx +16 -0
  328. package/src/Select/__tests__/Select.selection.test.tsx +104 -0
  329. package/src/Select/__tests__/Select.separator.test.tsx +71 -0
  330. package/src/Select/__tests__/Select.strict-context.test.tsx +36 -0
  331. package/src/Select/__tests__/Select.typeahead.test.tsx +156 -0
  332. package/src/Select/__tests__/Select.value-mirror.test.tsx +240 -0
  333. package/src/Select/constants.ts +8 -0
  334. package/src/Select/hooks/index.ts +2 -0
  335. package/src/Select/hooks/useSelectContent.ts +149 -0
  336. package/src/Select/hooks/useSelectRoot.ts +149 -0
  337. package/src/Select/types.ts +199 -31
  338. package/src/SkipNav/README.md +3 -3
  339. package/src/SkipNav/SkipNav.tsx +72 -32
  340. package/src/SkipNav/__tests__/SkipNav.basic-rendering.test.tsx +6 -0
  341. package/src/SkipNav/types.ts +17 -9
  342. package/src/Slider/README.md +37 -7
  343. package/src/Slider/Slider.tsx +42 -4
  344. package/src/Slider/SliderContext.ts +28 -1
  345. package/src/Slider/types.ts +157 -9
  346. package/src/Slot/Slot.tsx +94 -19
  347. package/src/Slot/__tests__/Slot.test.tsx +112 -2
  348. package/src/Slot/composeEventHandlers.ts +26 -2
  349. package/src/Slot/types.ts +16 -0
  350. package/src/SplitButton/README.md +225 -0
  351. package/src/SplitButton/SplitButton.tsx +457 -0
  352. package/src/SplitButton/SplitButtonContext.ts +26 -0
  353. package/src/SplitButton/__tests__/SplitButton.accessibility.test.tsx +55 -0
  354. package/src/SplitButton/__tests__/SplitButton.asChild.test.tsx +51 -0
  355. package/src/SplitButton/__tests__/SplitButton.basic-rendering.test.tsx +57 -0
  356. package/src/SplitButton/__tests__/SplitButton.data-attributes.test.tsx +28 -0
  357. package/src/SplitButton/__tests__/SplitButton.disabled.test.tsx +79 -0
  358. package/src/SplitButton/__tests__/SplitButton.error-handling.test.tsx +29 -0
  359. package/src/SplitButton/__tests__/SplitButton.keyboard-interaction.test.tsx +56 -0
  360. package/src/SplitButton/__tests__/SplitButton.state-modes.test.tsx +68 -0
  361. package/src/SplitButton/index.ts +2 -0
  362. package/src/SplitButton/types.ts +186 -0
  363. package/src/Status/Status.tsx +36 -15
  364. package/src/Status/__tests__/Status.test.tsx +5 -0
  365. package/src/Status/types.ts +12 -1
  366. package/src/Switch/README.md +1 -1
  367. package/src/Switch/Switch.tsx +17 -4
  368. package/src/Switch/__tests__/Switch.basic-rendering.test.tsx +7 -0
  369. package/src/Switch/hooks/useSwitchRoot.ts +3 -0
  370. package/src/Switch/types.ts +49 -22
  371. package/src/Table/README.md +110 -8
  372. package/src/Table/Table.tsx +246 -16
  373. package/src/Table/TableExpandableContext.ts +7 -0
  374. package/src/Table/__tests__/Table.DetailRow.test.tsx +83 -0
  375. package/src/Table/__tests__/Table.ExpandTrigger.test.tsx +102 -0
  376. package/src/Table/__tests__/Table.Expandable.test.tsx +121 -0
  377. package/src/Table/types.ts +191 -17
  378. package/src/Tabs/README.md +4 -4
  379. package/src/Tabs/Tabs.tsx +13 -4
  380. package/src/Tabs/__tests__/Tabs.activation-mode.test.tsx +58 -0
  381. package/src/Tabs/__tests__/Tabs.basic-rendering.test.tsx +15 -0
  382. package/src/Tabs/__tests__/Tabs.change-event-callbacks.test.tsx +24 -0
  383. package/src/Tabs/__tests__/Tabs.disabled-tabs.test.tsx +36 -0
  384. package/src/Tabs/__tests__/Tabs.lazy-mount.test.tsx +17 -0
  385. package/src/Tabs/hooks/useTabsContent.ts +5 -0
  386. package/src/Tabs/hooks/useTabsRoot.ts +29 -5
  387. package/src/Tabs/hooks/useTabsTrigger.ts +7 -2
  388. package/src/Tabs/types.ts +29 -4
  389. package/src/Textarea/Textarea.tsx +9 -1
  390. package/src/Textarea/__tests__/Textarea.basic-rendering.test.tsx +5 -0
  391. package/src/Textarea/types.ts +20 -3
  392. package/src/Toggle/Toggle.tsx +51 -13
  393. package/src/Toggle/__tests__/Toggle.basic-rendering.test.tsx +5 -0
  394. package/src/Toggle/types.ts +54 -4
  395. package/src/ToggleGroup/README.md +4 -4
  396. package/src/ToggleGroup/ToggleGroup.tsx +14 -1
  397. package/src/ToggleGroup/__tests__/ToggleGroup.asChild.test.tsx +20 -0
  398. package/src/ToggleGroup/__tests__/ToggleGroup.basic-rendering.test.tsx +9 -0
  399. package/src/ToggleGroup/__tests__/ToggleGroup.dynamic-items.test.tsx +70 -0
  400. package/src/ToggleGroup/__tests__/ToggleGroup.error-handling.test.tsx +23 -0
  401. package/src/ToggleGroup/__tests__/ToggleGroup.keyboard-interaction.test.tsx +23 -1
  402. package/src/ToggleGroup/__tests__/ToggleGroup.ref-forwarding.test.tsx +58 -0
  403. package/src/ToggleGroup/hooks/useToggleGroupRoot.ts +21 -1
  404. package/src/ToggleGroup/types.ts +55 -2
  405. package/src/Tooltip/README.md +7 -7
  406. package/src/Tooltip/Tooltip.tsx +100 -22
  407. package/src/Tooltip/__tests__/Tooltip.basic-rendering.test.tsx +50 -0
  408. package/src/Tooltip/__tests__/Tooltip.error-handling.test.tsx +37 -0
  409. package/src/Tooltip/__tests__/Tooltip.escape-hatches.test.tsx +73 -0
  410. package/src/Tooltip/__tests__/Tooltip.keyboard-interaction.test.tsx +134 -0
  411. package/src/Tooltip/__tests__/Tooltip.skip-delay.test.tsx +216 -0
  412. package/src/Tooltip/__tests__/Tooltip.timer-coordination.test.tsx +223 -0
  413. package/src/Tooltip/hooks/__tests__/useTooltipProvider.test.tsx +40 -0
  414. package/src/Tooltip/hooks/useTooltipContent.ts +3 -0
  415. package/src/Tooltip/hooks/useTooltipProvider.ts +46 -8
  416. package/src/Tooltip/hooks/useTooltipRoot.ts +30 -6
  417. package/src/Tooltip/types.ts +89 -8
  418. package/src/Tree/README.md +69 -5
  419. package/src/Tree/Tree.tsx +188 -34
  420. package/src/Tree/__tests__/Tree.styled-wrapper.test.tsx +159 -0
  421. package/src/Tree/hooks/useTreePath.ts +33 -1
  422. package/src/Tree/index.ts +6 -1
  423. package/src/Tree/types.ts +54 -12
  424. package/src/Tree/utils.ts +46 -4
  425. package/src/VisuallyHidden/VisuallyHidden.tsx +33 -8
  426. package/src/VisuallyHidden/__tests__/VisuallyHidden.test.tsx +15 -1
  427. package/src/VisuallyHidden/types.ts +11 -1
  428. package/src/hooks/index.ts +1 -0
  429. package/src/hooks/useTypeahead.ts +95 -0
  430. package/src/index.ts +10 -0
  431. package/src/test/intersectionObserverPolyfill.ts +7 -0
  432. package/src/test/resizeObserverPolyfill.ts +80 -0
  433. package/src/useLocalStorage/__tests__/useLocalStorage.reading.test.tsx +31 -0
  434. package/src/useLocalStorage/__tests__/useLocalStorage.resilience.test.tsx +97 -0
  435. package/src/useLocalStorage/__tests__/useLocalStorage.ssr.test.tsx +20 -0
  436. package/src/useLocalStorage/__tests__/useLocalStorage.writing.test.tsx +72 -0
  437. package/src/useLocalStorage/index.ts +1 -0
  438. package/src/useLocalStorage/useLocalStorage.ts +140 -0
  439. package/src/useMediaQuery/README.md +66 -0
  440. package/src/useMediaQuery/__tests__/useMediaQuery.matching.test.tsx +42 -0
  441. package/src/useMediaQuery/__tests__/useMediaQuery.ssr.test.tsx +19 -0
  442. package/src/useMediaQuery/__tests__/useMediaQuery.subscription.test.tsx +89 -0
  443. package/src/useMediaQuery/index.ts +1 -0
  444. package/src/useMediaQuery/useMediaQuery.ts +49 -0
  445. package/src/usePagination/README.md +170 -0
  446. package/src/usePagination/__tests__/usePagination.navigation.test.tsx +109 -0
  447. package/src/usePagination/__tests__/usePagination.page-count.test.tsx +56 -0
  448. package/src/usePagination/__tests__/usePagination.page-state.test.tsx +87 -0
  449. package/src/usePagination/__tests__/usePagination.range.test.tsx +82 -0
  450. package/src/usePagination/__tests__/usePagination.slice.test.tsx +59 -0
  451. package/src/usePagination/__tests__/usePagination.windowing.test.tsx +68 -0
  452. package/src/usePagination/index.ts +1 -0
  453. package/src/usePagination/usePagination.ts +292 -0
  454. package/src/utils/focusable.ts +15 -0
  455. package/src/utils/getKeyToActionMap.ts +1 -1
  456. package/src/utils/index.ts +1 -0
@@ -0,0 +1,532 @@
1
+ import { createRef } from "react";
2
+ import { act, fireEvent, render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+
5
+ import { Carousel } from "../index.ts";
6
+ import type { CarouselImperativeApi, CarouselOverscrollStatus } from "../index.ts";
7
+
8
+ function fireWheel(
9
+ viewport: HTMLElement,
10
+ init: { deltaX?: number; deltaY?: number },
11
+ ) {
12
+ const event = new WheelEvent("wheel", {
13
+ deltaX: init.deltaX ?? 0,
14
+ deltaY: init.deltaY ?? 0,
15
+ cancelable: true,
16
+ bubbles: true,
17
+ });
18
+ act(() => {
19
+ viewport.dispatchEvent(event);
20
+ });
21
+ }
22
+
23
+ describe("Carousel overscroll — keyboard", () => {
24
+ it("should fire onOverscrollStatusChange with edge 'end' when ArrowRight is pressed on the last page", async () => {
25
+ const onOverscrollStatusChange = vi.fn();
26
+ const user = userEvent.setup();
27
+ render(
28
+ <Carousel.Root
29
+ ariaLabel="Featured products"
30
+ defaultPage={2}
31
+ onOverscrollStatusChange={onOverscrollStatusChange}
32
+ >
33
+ <Carousel.Viewport data-testid="viewport">
34
+ <Carousel.Slide data-testid="slide-0" />
35
+ <Carousel.Slide data-testid="slide-1" />
36
+ <Carousel.Slide data-testid="slide-2" />
37
+ </Carousel.Viewport>
38
+ </Carousel.Root>,
39
+ );
40
+
41
+ await user.tab();
42
+ await user.keyboard("{ArrowRight}");
43
+
44
+ expect(onOverscrollStatusChange).toHaveBeenCalledWith({
45
+ type: "overscroll",
46
+ edge: "end",
47
+ source: "keyboard",
48
+ amount: 0,
49
+ page: 2,
50
+ } satisfies CarouselOverscrollStatus);
51
+ });
52
+
53
+ it("should fire onOverscrollStatusChange with edge 'start' when ArrowLeft is pressed on the first page", async () => {
54
+ const onOverscrollStatusChange = vi.fn();
55
+ const user = userEvent.setup();
56
+ render(
57
+ <Carousel.Root
58
+ ariaLabel="Featured products"
59
+ onOverscrollStatusChange={onOverscrollStatusChange}
60
+ >
61
+ <Carousel.Viewport data-testid="viewport">
62
+ <Carousel.Slide data-testid="slide-0" />
63
+ <Carousel.Slide data-testid="slide-1" />
64
+ </Carousel.Viewport>
65
+ </Carousel.Root>,
66
+ );
67
+
68
+ await user.tab();
69
+ await user.keyboard("{ArrowLeft}");
70
+
71
+ expect(onOverscrollStatusChange).toHaveBeenCalledWith({
72
+ type: "overscroll",
73
+ edge: "start",
74
+ source: "keyboard",
75
+ amount: 0,
76
+ page: 0,
77
+ } satisfies CarouselOverscrollStatus);
78
+ });
79
+
80
+ it("should not fire onOverscrollStatusChange when the arrow key can still advance", async () => {
81
+ const onOverscrollStatusChange = vi.fn();
82
+ const user = userEvent.setup();
83
+ render(
84
+ <Carousel.Root
85
+ ariaLabel="Featured products"
86
+ onOverscrollStatusChange={onOverscrollStatusChange}
87
+ >
88
+ <Carousel.Viewport data-testid="viewport">
89
+ <Carousel.Slide data-testid="slide-0" />
90
+ <Carousel.Slide data-testid="slide-1" />
91
+ </Carousel.Viewport>
92
+ </Carousel.Root>,
93
+ );
94
+
95
+ await user.tab();
96
+ await user.keyboard("{ArrowRight}");
97
+
98
+ expect(onOverscrollStatusChange).not.toHaveBeenCalled();
99
+ });
100
+
101
+ it("should not treat Home/End as overscroll even when already at that boundary", async () => {
102
+ const onOverscrollStatusChange = vi.fn();
103
+ const user = userEvent.setup();
104
+ render(
105
+ <Carousel.Root
106
+ ariaLabel="Featured products"
107
+ onOverscrollStatusChange={onOverscrollStatusChange}
108
+ >
109
+ <Carousel.Viewport data-testid="viewport">
110
+ <Carousel.Slide data-testid="slide-0" />
111
+ <Carousel.Slide data-testid="slide-1" />
112
+ </Carousel.Viewport>
113
+ </Carousel.Root>,
114
+ );
115
+
116
+ await user.tab();
117
+ await user.keyboard("{Home}");
118
+ await user.keyboard("{End}");
119
+ await user.keyboard("{End}");
120
+
121
+ expect(onOverscrollStatusChange).not.toHaveBeenCalled();
122
+ });
123
+ });
124
+
125
+ describe("Carousel overscroll — wheel", () => {
126
+ it("should fire onOverscrollStatusChange with edge 'end' when wheeling forward on the last page", () => {
127
+ const onOverscrollStatusChange = vi.fn();
128
+ render(
129
+ <Carousel.Root
130
+ ariaLabel="Featured products"
131
+ defaultPage={1}
132
+ onOverscrollStatusChange={onOverscrollStatusChange}
133
+ >
134
+ <Carousel.Viewport data-testid="viewport">
135
+ <Carousel.Slide data-testid="slide-0" />
136
+ <Carousel.Slide data-testid="slide-1" />
137
+ </Carousel.Viewport>
138
+ </Carousel.Root>,
139
+ );
140
+
141
+ fireWheel(screen.getByTestId("viewport"), { deltaY: 30 });
142
+
143
+ expect(onOverscrollStatusChange).toHaveBeenCalledWith({
144
+ type: "overscroll",
145
+ edge: "end",
146
+ source: "wheel",
147
+ amount: 0,
148
+ page: 1,
149
+ } satisfies CarouselOverscrollStatus);
150
+ });
151
+
152
+ it("should fire onOverscrollStatusChange with edge 'start' when wheeling backward on the first page", () => {
153
+ const onOverscrollStatusChange = vi.fn();
154
+ render(
155
+ <Carousel.Root
156
+ ariaLabel="Featured products"
157
+ onOverscrollStatusChange={onOverscrollStatusChange}
158
+ >
159
+ <Carousel.Viewport data-testid="viewport">
160
+ <Carousel.Slide data-testid="slide-0" />
161
+ <Carousel.Slide data-testid="slide-1" />
162
+ </Carousel.Viewport>
163
+ </Carousel.Root>,
164
+ );
165
+
166
+ fireWheel(screen.getByTestId("viewport"), { deltaY: -30 });
167
+
168
+ expect(onOverscrollStatusChange).toHaveBeenCalledWith({
169
+ type: "overscroll",
170
+ edge: "start",
171
+ source: "wheel",
172
+ amount: 0,
173
+ page: 0,
174
+ } satisfies CarouselOverscrollStatus);
175
+ });
176
+
177
+ it("should not fire onOverscrollStatusChange when wheeling within bounds", () => {
178
+ const onOverscrollStatusChange = vi.fn();
179
+ render(
180
+ <Carousel.Root
181
+ ariaLabel="Featured products"
182
+ onOverscrollStatusChange={onOverscrollStatusChange}
183
+ >
184
+ <Carousel.Viewport data-testid="viewport">
185
+ <Carousel.Slide data-testid="slide-0" />
186
+ <Carousel.Slide data-testid="slide-1" />
187
+ </Carousel.Viewport>
188
+ </Carousel.Root>,
189
+ );
190
+
191
+ fireWheel(screen.getByTestId("viewport"), { deltaY: 30 });
192
+
193
+ expect(onOverscrollStatusChange).not.toHaveBeenCalled();
194
+ });
195
+ });
196
+
197
+ describe("Carousel overscroll — mouse drag", () => {
198
+ it("should fire overscroll.start then overscroll while dragging past the start edge, and set data-overscroll", () => {
199
+ const onOverscrollStatusChange = vi.fn();
200
+ render(
201
+ <Carousel.Root
202
+ ariaLabel="Featured products"
203
+ allowMouseDrag
204
+ onOverscrollStatusChange={onOverscrollStatusChange}
205
+ >
206
+ <Carousel.Viewport data-testid="viewport">
207
+ <Carousel.Slide data-testid="slide-0" />
208
+ <Carousel.Slide data-testid="slide-1" />
209
+ </Carousel.Viewport>
210
+ </Carousel.Root>,
211
+ );
212
+ const viewport = screen.getByTestId("viewport");
213
+ Object.defineProperty(viewport, "scrollLeft", { value: 0, writable: true });
214
+
215
+ act(() => {
216
+ fireEvent.pointerDown(viewport, {
217
+ pointerType: "mouse",
218
+ pointerId: 1,
219
+ clientX: 200,
220
+ clientY: 0,
221
+ });
222
+ // Positive delta drags toward the start edge — already page 0, so
223
+ // this is an overscroll.
224
+ fireEvent.pointerMove(viewport, {
225
+ pointerType: "mouse",
226
+ pointerId: 1,
227
+ clientX: 210,
228
+ clientY: 0,
229
+ });
230
+ });
231
+
232
+ expect(onOverscrollStatusChange).toHaveBeenCalledWith({
233
+ type: "overscroll.start",
234
+ edge: "start",
235
+ source: "drag",
236
+ amount: 20,
237
+ page: 0,
238
+ } satisfies CarouselOverscrollStatus);
239
+ expect(viewport).toHaveAttribute("data-overscroll", "start");
240
+
241
+ act(() => {
242
+ fireEvent.pointerMove(viewport, {
243
+ pointerType: "mouse",
244
+ pointerId: 1,
245
+ clientX: 215,
246
+ clientY: 0,
247
+ });
248
+ });
249
+
250
+ expect(onOverscrollStatusChange).toHaveBeenLastCalledWith({
251
+ type: "overscroll",
252
+ edge: "start",
253
+ source: "drag",
254
+ amount: 30,
255
+ page: 0,
256
+ } satisfies CarouselOverscrollStatus);
257
+ });
258
+
259
+ it("should fire overscroll.end and clear data-overscroll on pointerup", () => {
260
+ const onOverscrollStatusChange = vi.fn();
261
+ render(
262
+ <Carousel.Root
263
+ ariaLabel="Featured products"
264
+ allowMouseDrag
265
+ onOverscrollStatusChange={onOverscrollStatusChange}
266
+ >
267
+ <Carousel.Viewport data-testid="viewport">
268
+ <Carousel.Slide data-testid="slide-0" />
269
+ <Carousel.Slide data-testid="slide-1" />
270
+ </Carousel.Viewport>
271
+ </Carousel.Root>,
272
+ );
273
+ const viewport = screen.getByTestId("viewport");
274
+ Object.defineProperty(viewport, "scrollLeft", { value: 0, writable: true });
275
+
276
+ act(() => {
277
+ fireEvent.pointerDown(viewport, {
278
+ pointerType: "mouse",
279
+ pointerId: 1,
280
+ clientX: 200,
281
+ clientY: 0,
282
+ });
283
+ fireEvent.pointerMove(viewport, {
284
+ pointerType: "mouse",
285
+ pointerId: 1,
286
+ clientX: 210,
287
+ clientY: 0,
288
+ });
289
+ fireEvent.pointerUp(viewport, { pointerType: "mouse", pointerId: 1 });
290
+ });
291
+
292
+ expect(onOverscrollStatusChange).toHaveBeenLastCalledWith({
293
+ type: "overscroll.end",
294
+ edge: "start",
295
+ source: "drag",
296
+ amount: 0,
297
+ page: 0,
298
+ } satisfies CarouselOverscrollStatus);
299
+ expect(viewport).not.toHaveAttribute("data-overscroll");
300
+ });
301
+
302
+ it("should fire overscroll.end mid-drag when the pointer reverses back within bounds without releasing", () => {
303
+ const onOverscrollStatusChange = vi.fn();
304
+ render(
305
+ <Carousel.Root
306
+ ariaLabel="Featured products"
307
+ allowMouseDrag
308
+ onOverscrollStatusChange={onOverscrollStatusChange}
309
+ >
310
+ <Carousel.Viewport data-testid="viewport">
311
+ <Carousel.Slide data-testid="slide-0" />
312
+ <Carousel.Slide data-testid="slide-1" />
313
+ </Carousel.Viewport>
314
+ </Carousel.Root>,
315
+ );
316
+ const viewport = screen.getByTestId("viewport");
317
+ Object.defineProperty(viewport, "scrollLeft", { value: 0, writable: true });
318
+
319
+ act(() => {
320
+ fireEvent.pointerDown(viewport, {
321
+ pointerType: "mouse",
322
+ pointerId: 1,
323
+ clientX: 200,
324
+ clientY: 0,
325
+ });
326
+ // Overscroll toward start.
327
+ fireEvent.pointerMove(viewport, {
328
+ pointerType: "mouse",
329
+ pointerId: 1,
330
+ clientX: 210,
331
+ clientY: 0,
332
+ });
333
+ // Reverse toward the end — back within bounds (page 0 can go next).
334
+ fireEvent.pointerMove(viewport, {
335
+ pointerType: "mouse",
336
+ pointerId: 1,
337
+ clientX: 195,
338
+ clientY: 0,
339
+ });
340
+ });
341
+
342
+ expect(onOverscrollStatusChange).toHaveBeenCalledWith({
343
+ type: "overscroll.end",
344
+ edge: "start",
345
+ source: "drag",
346
+ amount: 0,
347
+ page: 0,
348
+ } satisfies CarouselOverscrollStatus);
349
+ expect(viewport).not.toHaveAttribute("data-overscroll");
350
+ });
351
+
352
+ it("should fire edge 'end' when dragging past the end edge on the last page", () => {
353
+ const onOverscrollStatusChange = vi.fn();
354
+ render(
355
+ <Carousel.Root
356
+ ariaLabel="Featured products"
357
+ defaultPage={1}
358
+ allowMouseDrag
359
+ onOverscrollStatusChange={onOverscrollStatusChange}
360
+ >
361
+ <Carousel.Viewport data-testid="viewport">
362
+ <Carousel.Slide data-testid="slide-0" />
363
+ <Carousel.Slide data-testid="slide-1" />
364
+ </Carousel.Viewport>
365
+ </Carousel.Root>,
366
+ );
367
+ const viewport = screen.getByTestId("viewport");
368
+ Object.defineProperty(viewport, "scrollLeft", { value: 0, writable: true });
369
+
370
+ act(() => {
371
+ fireEvent.pointerDown(viewport, {
372
+ pointerType: "mouse",
373
+ pointerId: 1,
374
+ clientX: 200,
375
+ clientY: 0,
376
+ });
377
+ // Negative delta drags toward the end edge — already on the last
378
+ // page, so this is an overscroll.
379
+ fireEvent.pointerMove(viewport, {
380
+ pointerType: "mouse",
381
+ pointerId: 1,
382
+ clientX: 190,
383
+ clientY: 0,
384
+ });
385
+ });
386
+
387
+ expect(onOverscrollStatusChange).toHaveBeenCalledWith({
388
+ type: "overscroll.start",
389
+ edge: "end",
390
+ source: "drag",
391
+ amount: 20,
392
+ page: 1,
393
+ } satisfies CarouselOverscrollStatus);
394
+ expect(viewport).toHaveAttribute("data-overscroll", "end");
395
+ });
396
+
397
+ it("should not fire onOverscrollStatusChange while dragging within bounds", () => {
398
+ const onOverscrollStatusChange = vi.fn();
399
+ render(
400
+ <Carousel.Root
401
+ ariaLabel="Featured products"
402
+ defaultPage={1}
403
+ allowMouseDrag
404
+ onOverscrollStatusChange={onOverscrollStatusChange}
405
+ >
406
+ <Carousel.Viewport data-testid="viewport">
407
+ <Carousel.Slide data-testid="slide-0" />
408
+ <Carousel.Slide data-testid="slide-1" />
409
+ <Carousel.Slide data-testid="slide-2" />
410
+ </Carousel.Viewport>
411
+ </Carousel.Root>,
412
+ );
413
+ const viewport = screen.getByTestId("viewport");
414
+ Object.defineProperty(viewport, "scrollLeft", { value: 100, writable: true });
415
+
416
+ act(() => {
417
+ fireEvent.pointerDown(viewport, {
418
+ pointerType: "mouse",
419
+ pointerId: 1,
420
+ clientX: 200,
421
+ clientY: 0,
422
+ });
423
+ fireEvent.pointerMove(viewport, {
424
+ pointerType: "mouse",
425
+ pointerId: 1,
426
+ clientX: 240,
427
+ clientY: 0,
428
+ });
429
+ });
430
+
431
+ expect(onOverscrollStatusChange).not.toHaveBeenCalled();
432
+ expect(viewport).not.toHaveAttribute("data-overscroll");
433
+ });
434
+
435
+ it("should not fire overscroll.end on release when the drag never overscrolled", () => {
436
+ const onOverscrollStatusChange = vi.fn();
437
+ render(
438
+ <Carousel.Root
439
+ ariaLabel="Featured products"
440
+ defaultPage={1}
441
+ allowMouseDrag
442
+ onOverscrollStatusChange={onOverscrollStatusChange}
443
+ >
444
+ <Carousel.Viewport data-testid="viewport">
445
+ <Carousel.Slide data-testid="slide-0" />
446
+ <Carousel.Slide data-testid="slide-1" />
447
+ <Carousel.Slide data-testid="slide-2" />
448
+ </Carousel.Viewport>
449
+ </Carousel.Root>,
450
+ );
451
+ const viewport = screen.getByTestId("viewport");
452
+ Object.defineProperty(viewport, "scrollLeft", { value: 100, writable: true });
453
+
454
+ act(() => {
455
+ fireEvent.pointerDown(viewport, {
456
+ pointerType: "mouse",
457
+ pointerId: 1,
458
+ clientX: 200,
459
+ clientY: 0,
460
+ });
461
+ fireEvent.pointerMove(viewport, {
462
+ pointerType: "mouse",
463
+ pointerId: 1,
464
+ clientX: 240,
465
+ clientY: 0,
466
+ });
467
+ fireEvent.pointerUp(viewport, { pointerType: "mouse", pointerId: 1 });
468
+ });
469
+
470
+ expect(onOverscrollStatusChange).not.toHaveBeenCalled();
471
+ expect(viewport).not.toHaveAttribute("data-overscroll");
472
+ });
473
+ });
474
+
475
+ describe("Carousel imperative isOverscrolling", () => {
476
+ it("should be false by default, true during a drag-overscroll, and false again after release", () => {
477
+ const ref = createRef<CarouselImperativeApi>();
478
+ render(
479
+ <Carousel.Root ref={ref} ariaLabel="Featured products" allowMouseDrag>
480
+ <Carousel.Viewport data-testid="viewport">
481
+ <Carousel.Slide data-testid="slide-0" />
482
+ <Carousel.Slide data-testid="slide-1" />
483
+ </Carousel.Viewport>
484
+ </Carousel.Root>,
485
+ );
486
+ const viewport = screen.getByTestId("viewport");
487
+ Object.defineProperty(viewport, "scrollLeft", { value: 0, writable: true });
488
+
489
+ expect(ref.current!.isOverscrolling()).toBe(false);
490
+
491
+ act(() => {
492
+ fireEvent.pointerDown(viewport, {
493
+ pointerType: "mouse",
494
+ pointerId: 1,
495
+ clientX: 200,
496
+ clientY: 0,
497
+ });
498
+ fireEvent.pointerMove(viewport, {
499
+ pointerType: "mouse",
500
+ pointerId: 1,
501
+ clientX: 210,
502
+ clientY: 0,
503
+ });
504
+ });
505
+
506
+ expect(ref.current!.isOverscrolling()).toBe(true);
507
+
508
+ act(() => {
509
+ fireEvent.pointerUp(viewport, { pointerType: "mouse", pointerId: 1 });
510
+ });
511
+
512
+ expect(ref.current!.isOverscrolling()).toBe(false);
513
+ });
514
+
515
+ it("should stay false for keyboard/wheel overscroll taps (not a sustained state)", async () => {
516
+ const ref = createRef<CarouselImperativeApi>();
517
+ const user = userEvent.setup();
518
+ render(
519
+ <Carousel.Root ref={ref} ariaLabel="Featured products">
520
+ <Carousel.Viewport data-testid="viewport">
521
+ <Carousel.Slide data-testid="slide-0" />
522
+ <Carousel.Slide data-testid="slide-1" />
523
+ </Carousel.Viewport>
524
+ </Carousel.Root>,
525
+ );
526
+
527
+ await user.tab();
528
+ await user.keyboard("{ArrowLeft}");
529
+
530
+ expect(ref.current!.isOverscrolling()).toBe(false);
531
+ });
532
+ });
@@ -0,0 +1,50 @@
1
+ import { createRef } from "react";
2
+ import { render } from "@testing-library/react";
3
+
4
+ import { Carousel } from "../index.ts";
5
+ import type { CarouselImperativeApi } from "../index.ts";
6
+
7
+ describe("Carousel imperative getPageSnapPoints", () => {
8
+ it("should return one offset per slide when there's no multi-slide paging", () => {
9
+ const ref = createRef<CarouselImperativeApi>();
10
+ render(
11
+ <Carousel.Root ref={ref} ariaLabel="Featured products">
12
+ <Carousel.Viewport>
13
+ <Carousel.Slide />
14
+ <Carousel.Slide />
15
+ <Carousel.Slide />
16
+ </Carousel.Viewport>
17
+ </Carousel.Root>,
18
+ );
19
+
20
+ expect(ref.current!.getPageSnapPoints()).toEqual([0, 1, 2]);
21
+ });
22
+
23
+ it("should return the end-aligned offset for the last page when the total isn't a whole number of pages", () => {
24
+ const ref = createRef<CarouselImperativeApi>();
25
+ render(
26
+ <Carousel.Root ref={ref} ariaLabel="Featured products" slidesPerPage={3}>
27
+ <Carousel.Viewport>
28
+ {Array.from({ length: 7 }, (_, index) => (
29
+ <Carousel.Slide key={index} />
30
+ ))}
31
+ </Carousel.Viewport>
32
+ </Carousel.Root>,
33
+ );
34
+
35
+ // 7 slides / perPage 3 → pages [0,1,2] [3,4,5] [4,5,6] (iteration 8's
36
+ // end-align model): the last page's offset shifts back to 4, not 6.
37
+ expect(ref.current!.getPageSnapPoints()).toEqual([0, 3, 4]);
38
+ });
39
+
40
+ it("should return an empty array when no slides are registered", () => {
41
+ const ref = createRef<CarouselImperativeApi>();
42
+ render(
43
+ <Carousel.Root ref={ref} ariaLabel="Featured products">
44
+ <Carousel.Viewport />
45
+ </Carousel.Root>,
46
+ );
47
+
48
+ expect(ref.current!.getPageSnapPoints()).toEqual([]);
49
+ });
50
+ });
@@ -0,0 +1,76 @@
1
+ import { act, fireEvent, render, screen } from "@testing-library/react";
2
+
3
+ import { Carousel } from "../index.ts";
4
+
5
+ // ProgressText: a rendered "N of M" progress announcement, closing the Ark
6
+ // UI parity gap between the imperative getProgress() data and an actual
7
+ // subcomponent a consumer can drop into the DOM without wiring it themselves.
8
+ describe("Carousel ProgressText", () => {
9
+ it("should render the default '1 of 3' text for the first of three pages", () => {
10
+ render(
11
+ <Carousel.Root ariaLabel="Featured products">
12
+ <Carousel.Viewport>
13
+ <Carousel.Slide />
14
+ <Carousel.Slide />
15
+ <Carousel.Slide />
16
+ </Carousel.Viewport>
17
+ <Carousel.ProgressText data-testid="progress" />
18
+ </Carousel.Root>,
19
+ );
20
+
21
+ expect(screen.getByTestId("progress")).toHaveTextContent("1 of 3");
22
+ });
23
+
24
+ it("should update as the active page advances", () => {
25
+ render(
26
+ <Carousel.Root ariaLabel="Featured products">
27
+ <Carousel.Viewport>
28
+ <Carousel.Slide />
29
+ <Carousel.Slide />
30
+ </Carousel.Viewport>
31
+ <Carousel.NextTrigger>Next</Carousel.NextTrigger>
32
+ <Carousel.ProgressText data-testid="progress" />
33
+ </Carousel.Root>,
34
+ );
35
+
36
+ act(() => {
37
+ fireEvent.click(screen.getByRole("button", { name: "Next" }));
38
+ });
39
+
40
+ expect(screen.getByTestId("progress")).toHaveTextContent("2 of 2");
41
+ });
42
+
43
+ it("should render custom children instead of the default text when provided", () => {
44
+ render(
45
+ <Carousel.Root ariaLabel="Featured products">
46
+ <Carousel.Viewport>
47
+ <Carousel.Slide />
48
+ <Carousel.Slide />
49
+ </Carousel.Viewport>
50
+ <Carousel.ProgressText data-testid="progress">
51
+ Custom progress
52
+ </Carousel.ProgressText>
53
+ </Carousel.Root>,
54
+ );
55
+
56
+ expect(screen.getByTestId("progress")).toHaveTextContent(
57
+ "Custom progress",
58
+ );
59
+ });
60
+
61
+ it("should forward native span props like className", () => {
62
+ render(
63
+ <Carousel.Root ariaLabel="Featured products">
64
+ <Carousel.Viewport>
65
+ <Carousel.Slide />
66
+ </Carousel.Viewport>
67
+ <Carousel.ProgressText
68
+ data-testid="progress"
69
+ className="custom-class"
70
+ />
71
+ </Carousel.Root>,
72
+ );
73
+
74
+ expect(screen.getByTestId("progress")).toHaveClass("custom-class");
75
+ });
76
+ });
@@ -22,7 +22,7 @@ describe("Carousel scroll sync respects prefers-reduced-motion", () => {
22
22
  delete window.matchMedia;
23
23
  });
24
24
 
25
- it("should scroll into view with behavior='instant' when prefers-reduced-motion is reduce", async () => {
25
+ it("should scroll with behavior='instant' when prefers-reduced-motion is reduce", async () => {
26
26
  mockReducedMotion(true);
27
27
  const user = userEvent.setup();
28
28
  render(
@@ -35,14 +35,11 @@ describe("Carousel scroll sync respects prefers-reduced-motion", () => {
35
35
  </Carousel.Root>,
36
36
  );
37
37
 
38
- const scrollIntoViewSpy = vi.spyOn(
39
- screen.getByTestId("slide-1"),
40
- "scrollIntoView",
41
- );
38
+ const scrollToSpy = vi.spyOn(screen.getByTestId("viewport"), "scrollTo");
42
39
 
43
40
  await user.click(screen.getByRole("button", { name: "Next" }));
44
41
 
45
- expect(scrollIntoViewSpy).toHaveBeenCalledWith(
42
+ expect(scrollToSpy).toHaveBeenCalledWith(
46
43
  expect.objectContaining({ behavior: "instant" }),
47
44
  );
48
45
  });
@@ -73,15 +73,12 @@ describe("Carousel imperative getProgress / refresh", () => {
73
73
  </Carousel.Root>,
74
74
  );
75
75
 
76
- const scrollIntoViewSpy = vi.spyOn(
77
- screen.getByTestId("slide-0"),
78
- "scrollIntoView",
79
- );
76
+ const scrollToSpy = vi.spyOn(screen.getByTestId("viewport"), "scrollTo");
80
77
 
81
78
  act(() => {
82
79
  ref.current!.refresh();
83
80
  });
84
81
 
85
- expect(scrollIntoViewSpy).toHaveBeenCalled();
82
+ expect(scrollToSpy).toHaveBeenCalled();
86
83
  });
87
84
  });