@primitiv-ui/react 0.1.27 → 0.1.29

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 (307) hide show
  1. package/README.md +4 -0
  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 +25 -4
  7. package/src/Accordion/__tests__/Accordion.asChild.test.tsx +46 -0
  8. package/src/Accordion/__tests__/Accordion.basic-rendering.test.tsx +15 -0
  9. package/src/Accordion/__tests__/Accordion.disabled-items.test.tsx +42 -0
  10. package/src/Accordion/__tests__/Accordion.error-handling.test.tsx +33 -0
  11. package/src/Accordion/hooks/useAccordionRoot.ts +44 -7
  12. package/src/Accordion/hooks/useAccordionTrigger.ts +9 -1
  13. package/src/Accordion/types.ts +72 -21
  14. package/src/Alert/Alert.tsx +36 -7
  15. package/src/Alert/__tests__/Alert.test.tsx +5 -0
  16. package/src/Alert/types.ts +18 -2
  17. package/src/Avatar/Avatar.tsx +99 -15
  18. package/src/Avatar/__tests__/Avatar.basic-rendering.test.tsx +10 -0
  19. package/src/Avatar/__tests__/Avatar.fallback.test.tsx +30 -0
  20. package/src/Avatar/__tests__/Avatar.image-loading.test.tsx +35 -0
  21. package/src/Avatar/hooks/useAvatarImage.ts +16 -0
  22. package/src/Avatar/types.ts +15 -3
  23. package/src/Breadcrumb/Breadcrumb.tsx +136 -18
  24. package/src/Breadcrumb/README.md +47 -0
  25. package/src/Breadcrumb/__tests__/Breadcrumb.basic-rendering.test.tsx +11 -0
  26. package/src/Breadcrumb/__tests__/Breadcrumb.ellipsis.test.tsx +110 -0
  27. package/src/Breadcrumb/types.ts +73 -7
  28. package/src/Button/Button.tsx +2 -0
  29. package/src/Button/__tests__/Button.basic-rendering.test.tsx +6 -0
  30. package/src/Button/types.ts +21 -0
  31. package/src/Carousel/Carousel.tsx +263 -15
  32. package/src/Carousel/README.md +749 -72
  33. package/src/Carousel/__tests__/Carousel.autoplay-status.test.tsx +239 -0
  34. package/src/Carousel/__tests__/Carousel.drag-status.test.tsx +292 -0
  35. package/src/Carousel/__tests__/Carousel.dynamic-slides.test.tsx +38 -0
  36. package/src/Carousel/__tests__/Carousel.indicators.test.tsx +101 -0
  37. package/src/Carousel/__tests__/Carousel.infinite.test.tsx +1453 -0
  38. package/src/Carousel/__tests__/Carousel.instant-scroll.test.tsx +124 -0
  39. package/src/Carousel/__tests__/Carousel.intersection-observer.test.tsx +88 -0
  40. package/src/Carousel/__tests__/Carousel.loop.test.tsx +163 -0
  41. package/src/Carousel/__tests__/Carousel.loopEngine.test.ts +77 -0
  42. package/src/Carousel/__tests__/Carousel.mouse-drag.test.tsx +504 -0
  43. package/src/Carousel/__tests__/Carousel.multi-slide-bounds.test.tsx +91 -0
  44. package/src/Carousel/__tests__/Carousel.orientation.test.tsx +207 -0
  45. package/src/Carousel/__tests__/Carousel.overscroll.test.tsx +532 -0
  46. package/src/Carousel/__tests__/Carousel.page-snap-points.test.tsx +50 -0
  47. package/src/Carousel/__tests__/Carousel.progress-text.test.tsx +76 -0
  48. package/src/Carousel/__tests__/Carousel.reduced-motion.test.tsx +3 -6
  49. package/src/Carousel/__tests__/Carousel.refresh-progress.test.tsx +2 -5
  50. package/src/Carousel/__tests__/Carousel.scroll-margin.test.tsx +167 -0
  51. package/src/Carousel/__tests__/Carousel.scroll-progress.test.tsx +518 -0
  52. package/src/Carousel/__tests__/Carousel.scroll-snap-change.test.tsx +32 -11
  53. package/src/Carousel/__tests__/Carousel.scroll-sync.test.tsx +237 -24
  54. package/src/Carousel/__tests__/Carousel.scroll-to-index.test.tsx +115 -0
  55. package/src/Carousel/__tests__/Carousel.slide-snap.test.tsx +172 -0
  56. package/src/Carousel/__tests__/Carousel.slides-per-move.test.tsx +179 -1
  57. package/src/Carousel/__tests__/Carousel.slides-per-page.test.tsx +91 -7
  58. package/src/Carousel/__tests__/Carousel.snap-type.test.tsx +35 -0
  59. package/src/Carousel/__tests__/Carousel.transition-modes.test.tsx +30 -5
  60. package/src/Carousel/__tests__/Carousel.translations.test.tsx +23 -0
  61. package/src/Carousel/__tests__/Carousel.wheel.test.tsx +160 -0
  62. package/src/Carousel/hooks/index.ts +1 -0
  63. package/src/Carousel/hooks/useCarouselLoop.ts +869 -0
  64. package/src/Carousel/hooks/useCarouselRoot.ts +338 -42
  65. package/src/Carousel/hooks/useCarouselSlide.ts +44 -9
  66. package/src/Carousel/hooks/useCarouselViewport.ts +649 -46
  67. package/src/Carousel/loopEngine.ts +54 -0
  68. package/src/Carousel/types.ts +457 -35
  69. package/src/Checkbox/Checkbox.tsx +9 -1
  70. package/src/Checkbox/__tests__/Checkbox.basic-rendering.test.tsx +14 -0
  71. package/src/Checkbox/hooks/useCheckboxInput.ts +4 -0
  72. package/src/Checkbox/types.ts +19 -0
  73. package/src/CheckboxCard/CheckboxCard.tsx +13 -4
  74. package/src/CheckboxCard/__tests__/CheckboxCard.basic-rendering.test.tsx +7 -0
  75. package/src/CheckboxCard/types.ts +65 -17
  76. package/src/Collapsible/Collapsible.tsx +49 -3
  77. package/src/Collapsible/README.md +40 -0
  78. package/src/Collapsible/__tests__/Collapsible.asChild.test.tsx +89 -0
  79. package/src/Collapsible/__tests__/Collapsible.basic-rendering.test.tsx +27 -0
  80. package/src/Collapsible/__tests__/Collapsible.collapsedHeight.test.tsx +140 -0
  81. package/src/Collapsible/hooks/useCollapsibleTrigger.ts +3 -1
  82. package/src/Collapsible/types.ts +89 -22
  83. package/src/ContextMenu/ContextMenu.tsx +270 -4
  84. package/src/ContextMenu/types.ts +148 -6
  85. package/src/DirectionProvider/DirectionProvider.tsx +41 -9
  86. package/src/DirectionProvider/__tests__/DirectionProvider.test.tsx +5 -0
  87. package/src/DirectionProvider/types.ts +28 -3
  88. package/src/Divider/Divider.tsx +24 -10
  89. package/src/Divider/__tests__/Divider.test.tsx +5 -0
  90. package/src/Divider/types.ts +10 -3
  91. package/src/Drawer/Drawer.tsx +315 -0
  92. package/src/Drawer/README.md +124 -0
  93. package/src/Drawer/__tests__/Drawer.behavior.test.tsx +100 -0
  94. package/src/Drawer/__tests__/Drawer.focus-trap.test.tsx +55 -0
  95. package/src/Drawer/__tests__/Drawer.rendering.test.tsx +111 -0
  96. package/src/Drawer/__tests__/Drawer.side.test.tsx +69 -0
  97. package/src/Drawer/__tests__/dialog-polyfill.ts +39 -0
  98. package/src/Drawer/index.ts +2 -0
  99. package/src/Drawer/types.ts +66 -0
  100. package/src/Dropdown/Dropdown.tsx +117 -0
  101. package/src/Dropdown/types.ts +241 -11
  102. package/src/EmptyState/EmptyState.tsx +74 -4
  103. package/src/EmptyState/__tests__/EmptyState.Actions.test.tsx +7 -2
  104. package/src/EmptyState/__tests__/EmptyState.Description.test.tsx +3 -1
  105. package/src/EmptyState/__tests__/EmptyState.Root.test.tsx +10 -0
  106. package/src/EmptyState/__tests__/EmptyState.Title.test.tsx +3 -1
  107. package/src/EmptyState/types.ts +49 -6
  108. package/src/Field/Field.tsx +23 -0
  109. package/src/Field/__tests__/Field.basic-rendering.test.tsx +11 -0
  110. package/src/Field/__tests__/Field.error-handling.test.tsx +23 -0
  111. package/src/Field/__tests__/Field.state-cascade.test.tsx +21 -0
  112. package/src/Field/hooks/__tests__/useFieldProps.test.tsx +104 -9
  113. package/src/Field/types.ts +60 -12
  114. package/src/Fieldset/Fieldset.tsx +54 -18
  115. package/src/Fieldset/__tests__/Fieldset.basic-rendering.test.tsx +9 -0
  116. package/src/Fieldset/types.ts +20 -2
  117. package/src/Input/Input.tsx +14 -9
  118. package/src/Input/__tests__/Input.basic-rendering.test.tsx +5 -0
  119. package/src/Input/types.ts +19 -2
  120. package/src/InputGroup/InputGroup.tsx +11 -3
  121. package/src/InputGroup/__tests__/InputGroup.basic-rendering.test.tsx +14 -0
  122. package/src/InputGroup/types.ts +26 -5
  123. package/src/MillerColumns/MillerColumns.tsx +86 -19
  124. package/src/MillerColumns/types.ts +83 -27
  125. package/src/Modal/Modal.tsx +31 -4
  126. package/src/Modal/README.md +19 -10
  127. package/src/Modal/__tests__/Modal.focus-trap.test.tsx +162 -0
  128. package/src/Modal/hooks/useModalContent.ts +32 -0
  129. package/src/Modal/types.ts +110 -29
  130. package/src/NavigationMenu/NavigationMenu.tsx +890 -0
  131. package/src/NavigationMenu/NavigationMenuContext.ts +67 -0
  132. package/src/NavigationMenu/README.md +355 -0
  133. package/src/NavigationMenu/__tests__/NavigationMenu.anchor-positioning.test.tsx +132 -0
  134. package/src/NavigationMenu/__tests__/NavigationMenu.asChild.test.tsx +73 -0
  135. package/src/NavigationMenu/__tests__/NavigationMenu.basic-rendering.test.tsx +92 -0
  136. package/src/NavigationMenu/__tests__/NavigationMenu.contract.test.tsx +45 -0
  137. package/src/NavigationMenu/__tests__/NavigationMenu.controlled-state.test.tsx +134 -0
  138. package/src/NavigationMenu/__tests__/NavigationMenu.error-handling.test.tsx +62 -0
  139. package/src/NavigationMenu/__tests__/NavigationMenu.fixtures.tsx +79 -0
  140. package/src/NavigationMenu/__tests__/NavigationMenu.forceMount.test.tsx +52 -0
  141. package/src/NavigationMenu/__tests__/NavigationMenu.hover-interaction.test.tsx +254 -0
  142. package/src/NavigationMenu/__tests__/NavigationMenu.indicator.test.tsx +252 -0
  143. package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-interaction.test.tsx +294 -0
  144. package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-navigation.test.tsx +126 -0
  145. package/src/NavigationMenu/__tests__/NavigationMenu.link.test.tsx +139 -0
  146. package/src/NavigationMenu/__tests__/NavigationMenu.motion.test.tsx +568 -0
  147. package/src/NavigationMenu/__tests__/NavigationMenu.mouse-interaction.test.tsx +76 -0
  148. package/src/NavigationMenu/__tests__/NavigationMenu.uncontrolled-state.test.tsx +30 -0
  149. package/src/NavigationMenu/__tests__/NavigationMenu.viewport.test.tsx +113 -0
  150. package/src/NavigationMenu/constants.ts +5 -0
  151. package/src/NavigationMenu/hooks/index.ts +7 -0
  152. package/src/NavigationMenu/hooks/useNavigationMenuEntry.ts +33 -0
  153. package/src/NavigationMenu/hooks/useNavigationMenuIndicator.ts +70 -0
  154. package/src/NavigationMenu/hooks/useNavigationMenuLink.ts +72 -0
  155. package/src/NavigationMenu/hooks/useNavigationMenuRoot.ts +301 -0
  156. package/src/NavigationMenu/hooks/useNavigationMenuTopLevelEntry.ts +65 -0
  157. package/src/NavigationMenu/hooks/useNavigationMenuTrigger.ts +158 -0
  158. package/src/NavigationMenu/hooks/useNavigationMenuViewport.ts +101 -0
  159. package/src/NavigationMenu/index.ts +3 -0
  160. package/src/NavigationMenu/types.ts +259 -0
  161. package/src/NavigationMenu/utils.ts +89 -0
  162. package/src/Popover/Popover.tsx +427 -0
  163. package/src/Popover/PopoverContext.ts +13 -0
  164. package/src/Popover/README.md +106 -0
  165. package/src/Popover/__tests__/Popover.anchor.test.tsx +38 -0
  166. package/src/Popover/__tests__/Popover.asChild.test.tsx +160 -0
  167. package/src/Popover/__tests__/Popover.basic-rendering.test.tsx +106 -0
  168. package/src/Popover/__tests__/Popover.click-outside.test.tsx +149 -0
  169. package/src/Popover/__tests__/Popover.close.test.tsx +70 -0
  170. package/src/Popover/__tests__/Popover.content.test.tsx +58 -0
  171. package/src/Popover/__tests__/Popover.controlled-state.test.tsx +45 -0
  172. package/src/Popover/__tests__/Popover.error-handling.test.tsx +51 -0
  173. package/src/Popover/__tests__/Popover.focus.test.tsx +43 -0
  174. package/src/Popover/__tests__/Popover.keyboard-interaction.test.tsx +71 -0
  175. package/src/Popover/__tests__/Popover.mouse-interaction.test.tsx +69 -0
  176. package/src/Popover/__tests__/Popover.uncontrolled-state.test.tsx +44 -0
  177. package/src/Popover/hooks/index.ts +3 -0
  178. package/src/Popover/hooks/usePopoverContent.ts +98 -0
  179. package/src/Popover/hooks/usePopoverRoot.ts +84 -0
  180. package/src/Popover/hooks/usePopoverTrigger.ts +33 -0
  181. package/src/Popover/index.ts +2 -0
  182. package/src/Popover/types.ts +197 -0
  183. package/src/Portal/Portal.tsx +50 -10
  184. package/src/Portal/__tests__/Portal.basic-rendering.test.tsx +5 -0
  185. package/src/Portal/types.ts +5 -0
  186. package/src/Progress/Progress.tsx +33 -1
  187. package/src/Progress/__tests__/Progress.basic-rendering.test.tsx +10 -0
  188. package/src/Progress/__tests__/Progress.value.test.tsx +21 -0
  189. package/src/Progress/types.ts +20 -4
  190. package/src/Radio/Radio.tsx +9 -1
  191. package/src/Radio/__tests__/Radio.basic-rendering.test.tsx +14 -0
  192. package/src/Radio/hooks/useRadioRoot.ts +3 -0
  193. package/src/Radio/types.ts +20 -0
  194. package/src/RadioCard/RadioCard.tsx +11 -0
  195. package/src/RadioCard/__tests__/RadioCard.basic-rendering.test.tsx +8 -0
  196. package/src/RadioCard/__tests__/RadioCard.disabled-items.test.tsx +27 -0
  197. package/src/RadioCard/__tests__/RadioCard.keyboard-interaction.test.tsx +53 -0
  198. package/src/RadioCard/__tests__/RadioCard.ref-forwarding.test.tsx +54 -0
  199. package/src/RadioCard/__tests__/RadioCard.uncontrolled-state.test.tsx +21 -0
  200. package/src/RadioCard/hooks/useRadioCardRoot.ts +11 -0
  201. package/src/RadioCard/types.ts +95 -22
  202. package/src/RadioGroup/RadioGroup.tsx +11 -0
  203. package/src/RadioGroup/__tests__/RadioGroup.basic-rendering.test.tsx +8 -0
  204. package/src/RadioGroup/__tests__/RadioGroup.disabled.test.tsx +27 -0
  205. package/src/RadioGroup/__tests__/RadioGroup.ref-forwarding.test.tsx +29 -0
  206. package/src/RadioGroup/__tests__/RadioGroup.roving-tabindex.test.tsx +31 -0
  207. package/src/RadioGroup/hooks/useRadioGroupRoot.ts +11 -0
  208. package/src/RadioGroup/types.ts +73 -9
  209. package/src/SegmentedControl/README.md +154 -0
  210. package/src/SegmentedControl/SegmentedControl.tsx +309 -0
  211. package/src/SegmentedControl/SegmentedControlContext.ts +28 -0
  212. package/src/SegmentedControl/__tests__/SegmentedControl.asChild.test.tsx +50 -0
  213. package/src/SegmentedControl/__tests__/SegmentedControl.basic-rendering.test.tsx +100 -0
  214. package/src/SegmentedControl/__tests__/SegmentedControl.controlled-state.test.tsx +107 -0
  215. package/src/SegmentedControl/__tests__/SegmentedControl.disabled-keydown-guards.test.tsx +62 -0
  216. package/src/SegmentedControl/__tests__/SegmentedControl.disabled.test.tsx +151 -0
  217. package/src/SegmentedControl/__tests__/SegmentedControl.error-handling.test.tsx +17 -0
  218. package/src/SegmentedControl/__tests__/SegmentedControl.keyboard-interaction.test.tsx +117 -0
  219. package/src/SegmentedControl/__tests__/SegmentedControl.orientation.test.tsx +54 -0
  220. package/src/SegmentedControl/__tests__/SegmentedControl.reading-direction.test.tsx +56 -0
  221. package/src/SegmentedControl/__tests__/SegmentedControl.ref-forwarding.test.tsx +45 -0
  222. package/src/SegmentedControl/__tests__/SegmentedControl.roving-tabindex.test.tsx +104 -0
  223. package/src/SegmentedControl/__tests__/SegmentedControl.uncontrolled-state.test.tsx +94 -0
  224. package/src/SegmentedControl/hooks/index.ts +2 -0
  225. package/src/SegmentedControl/hooks/useSegmentedControlContext.ts +1 -0
  226. package/src/SegmentedControl/hooks/useSegmentedControlRoot.ts +86 -0
  227. package/src/SegmentedControl/index.ts +2 -0
  228. package/src/SegmentedControl/types.ts +138 -0
  229. package/src/Select/README.md +122 -157
  230. package/src/Select/Select.tsx +595 -68
  231. package/src/Select/SelectContext.ts +38 -0
  232. package/src/Select/SelectItemIndicatorContext.ts +29 -0
  233. package/src/Select/__tests__/Select.asChild.test.tsx +2 -2
  234. package/src/Select/__tests__/Select.basic-rendering.test.tsx +22 -3
  235. package/src/Select/__tests__/Select.controlled-state.test.tsx +6 -6
  236. package/src/Select/__tests__/Select.data-attributes.test.tsx +4 -4
  237. package/src/Select/__tests__/Select.field-integration.test.tsx +11 -11
  238. package/src/Select/__tests__/Select.form.test.tsx +38 -0
  239. package/src/Select/__tests__/Select.group.test.tsx +6 -6
  240. package/src/Select/__tests__/Select.keyboard-edge.test.tsx +95 -0
  241. package/src/Select/__tests__/Select.keyboard.test.tsx +124 -0
  242. package/src/Select/__tests__/Select.mutation-hardening.test.tsx +317 -0
  243. package/src/Select/__tests__/Select.native-item.test.tsx +30 -0
  244. package/src/Select/__tests__/Select.open-close.test.tsx +57 -0
  245. package/src/Select/__tests__/Select.placeholder.test.tsx +4 -4
  246. package/src/Select/__tests__/Select.rich-asChild.test.tsx +60 -0
  247. package/src/Select/__tests__/Select.rich-mode.test.tsx +16 -0
  248. package/src/Select/__tests__/Select.selection.test.tsx +104 -0
  249. package/src/Select/__tests__/Select.separator.test.tsx +71 -0
  250. package/src/Select/__tests__/Select.strict-context.test.tsx +36 -0
  251. package/src/Select/__tests__/Select.typeahead.test.tsx +156 -0
  252. package/src/Select/__tests__/Select.value-mirror.test.tsx +240 -0
  253. package/src/Select/constants.ts +8 -0
  254. package/src/Select/hooks/index.ts +2 -0
  255. package/src/Select/hooks/useSelectContent.ts +149 -0
  256. package/src/Select/hooks/useSelectRoot.ts +149 -0
  257. package/src/Select/types.ts +199 -31
  258. package/src/SkipNav/SkipNav.tsx +69 -29
  259. package/src/SkipNav/__tests__/SkipNav.basic-rendering.test.tsx +6 -0
  260. package/src/SkipNav/types.ts +17 -9
  261. package/src/Slider/Slider.tsx +33 -1
  262. package/src/Slider/SliderContext.ts +28 -1
  263. package/src/Slider/types.ts +157 -9
  264. package/src/Slot/Slot.tsx +94 -19
  265. package/src/Slot/__tests__/Slot.test.tsx +110 -0
  266. package/src/Slot/composeEventHandlers.ts +26 -2
  267. package/src/Slot/types.ts +16 -0
  268. package/src/Status/Status.tsx +36 -15
  269. package/src/Status/__tests__/Status.test.tsx +5 -0
  270. package/src/Status/types.ts +12 -1
  271. package/src/Switch/Switch.tsx +15 -2
  272. package/src/Switch/__tests__/Switch.basic-rendering.test.tsx +7 -0
  273. package/src/Switch/hooks/useSwitchRoot.ts +3 -0
  274. package/src/Switch/types.ts +49 -22
  275. package/src/Table/Table.tsx +44 -10
  276. package/src/Table/types.ts +66 -17
  277. package/src/Tabs/Tabs.tsx +8 -0
  278. package/src/Tabs/types.ts +29 -4
  279. package/src/Textarea/Textarea.tsx +9 -1
  280. package/src/Textarea/__tests__/Textarea.basic-rendering.test.tsx +5 -0
  281. package/src/Textarea/types.ts +20 -3
  282. package/src/Toggle/Toggle.tsx +51 -13
  283. package/src/Toggle/__tests__/Toggle.basic-rendering.test.tsx +5 -0
  284. package/src/Toggle/types.ts +54 -4
  285. package/src/ToggleGroup/ToggleGroup.tsx +14 -1
  286. package/src/ToggleGroup/__tests__/ToggleGroup.asChild.test.tsx +20 -0
  287. package/src/ToggleGroup/__tests__/ToggleGroup.basic-rendering.test.tsx +9 -0
  288. package/src/ToggleGroup/__tests__/ToggleGroup.dynamic-items.test.tsx +70 -0
  289. package/src/ToggleGroup/__tests__/ToggleGroup.error-handling.test.tsx +23 -0
  290. package/src/ToggleGroup/__tests__/ToggleGroup.keyboard-interaction.test.tsx +23 -1
  291. package/src/ToggleGroup/__tests__/ToggleGroup.ref-forwarding.test.tsx +58 -0
  292. package/src/ToggleGroup/hooks/useToggleGroupRoot.ts +21 -1
  293. package/src/ToggleGroup/types.ts +55 -2
  294. package/src/Tooltip/Tooltip.tsx +98 -20
  295. package/src/Tooltip/hooks/__tests__/useTooltipProvider.test.tsx +40 -0
  296. package/src/Tooltip/hooks/useTooltipProvider.ts +13 -1
  297. package/src/Tooltip/types.ts +89 -8
  298. package/src/Tree/Tree.tsx +187 -33
  299. package/src/Tree/types.ts +54 -12
  300. package/src/VisuallyHidden/VisuallyHidden.tsx +33 -8
  301. package/src/VisuallyHidden/__tests__/VisuallyHidden.test.tsx +15 -1
  302. package/src/VisuallyHidden/types.ts +11 -1
  303. package/src/index.ts +4 -0
  304. package/src/test/intersectionObserverPolyfill.ts +7 -0
  305. package/src/test/resizeObserverPolyfill.ts +80 -0
  306. package/src/utils/focusable.ts +15 -0
  307. package/src/utils/index.ts +1 -0
@@ -0,0 +1,124 @@
1
+ import { createRef } from "react";
2
+ import { act, render, screen } from "@testing-library/react";
3
+
4
+ import { Carousel } from "../index.ts";
5
+ import type { CarouselImperativeApi } from "../index.ts";
6
+
7
+ describe("Carousel imperative API — per-call instant scroll override", () => {
8
+ it("should scroll with behavior='instant' when next(true) is called, even without prefers-reduced-motion", () => {
9
+ const ref = createRef<CarouselImperativeApi>();
10
+ render(
11
+ <Carousel.Root ref={ref} ariaLabel="Featured products">
12
+ <Carousel.Viewport data-testid="viewport">
13
+ <Carousel.Slide data-testid="slide-0" />
14
+ <Carousel.Slide data-testid="slide-1" />
15
+ </Carousel.Viewport>
16
+ </Carousel.Root>,
17
+ );
18
+
19
+ const scrollToSpy = vi.spyOn(screen.getByTestId("viewport"), "scrollTo");
20
+
21
+ act(() => {
22
+ ref.current!.next(true);
23
+ });
24
+
25
+ expect(scrollToSpy).toHaveBeenCalledWith(
26
+ expect.objectContaining({ behavior: "instant" }),
27
+ );
28
+ });
29
+
30
+ it("should scroll with behavior='instant' when previous(true) is called", () => {
31
+ const ref = createRef<CarouselImperativeApi>();
32
+ render(
33
+ <Carousel.Root ref={ref} ariaLabel="Featured products" defaultPage={1}>
34
+ <Carousel.Viewport data-testid="viewport">
35
+ <Carousel.Slide data-testid="slide-0" />
36
+ <Carousel.Slide data-testid="slide-1" />
37
+ </Carousel.Viewport>
38
+ </Carousel.Root>,
39
+ );
40
+
41
+ const scrollToSpy = vi.spyOn(screen.getByTestId("viewport"), "scrollTo");
42
+
43
+ act(() => {
44
+ ref.current!.previous(true);
45
+ });
46
+
47
+ expect(scrollToSpy).toHaveBeenCalledWith(
48
+ expect.objectContaining({ behavior: "instant" }),
49
+ );
50
+ });
51
+
52
+ it("should scroll with behavior='instant' when goTo(page, true) is called", () => {
53
+ const ref = createRef<CarouselImperativeApi>();
54
+ render(
55
+ <Carousel.Root ref={ref} ariaLabel="Featured products">
56
+ <Carousel.Viewport data-testid="viewport">
57
+ <Carousel.Slide data-testid="slide-0" />
58
+ <Carousel.Slide data-testid="slide-1" />
59
+ </Carousel.Viewport>
60
+ </Carousel.Root>,
61
+ );
62
+
63
+ const scrollToSpy = vi.spyOn(screen.getByTestId("viewport"), "scrollTo");
64
+
65
+ act(() => {
66
+ ref.current!.goTo(1, true);
67
+ });
68
+
69
+ expect(scrollToSpy).toHaveBeenCalledWith(
70
+ expect.objectContaining({ behavior: "instant" }),
71
+ );
72
+ });
73
+
74
+ it("should only skip the animation for that one call, reverting to smooth for the next navigation", () => {
75
+ const ref = createRef<CarouselImperativeApi>();
76
+ render(
77
+ <Carousel.Root ref={ref} ariaLabel="Featured products">
78
+ <Carousel.Viewport data-testid="viewport">
79
+ <Carousel.Slide data-testid="slide-0" />
80
+ <Carousel.Slide data-testid="slide-1" />
81
+ <Carousel.Slide data-testid="slide-2" />
82
+ </Carousel.Viewport>
83
+ </Carousel.Root>,
84
+ );
85
+
86
+ const scrollToSpy = vi.spyOn(screen.getByTestId("viewport"), "scrollTo");
87
+
88
+ act(() => {
89
+ ref.current!.next(true);
90
+ });
91
+ expect(scrollToSpy).toHaveBeenLastCalledWith(
92
+ expect.objectContaining({ behavior: "instant" }),
93
+ );
94
+
95
+ act(() => {
96
+ ref.current!.next();
97
+ });
98
+ expect(scrollToSpy).toHaveBeenLastCalledWith(
99
+ expect.objectContaining({ behavior: "smooth" }),
100
+ );
101
+ });
102
+
103
+ it("should default to the resolved smooth/reduced-motion behavior when no instant argument is passed", () => {
104
+ const ref = createRef<CarouselImperativeApi>();
105
+ render(
106
+ <Carousel.Root ref={ref} ariaLabel="Featured products">
107
+ <Carousel.Viewport data-testid="viewport">
108
+ <Carousel.Slide data-testid="slide-0" />
109
+ <Carousel.Slide data-testid="slide-1" />
110
+ </Carousel.Viewport>
111
+ </Carousel.Root>,
112
+ );
113
+
114
+ const scrollToSpy = vi.spyOn(screen.getByTestId("viewport"), "scrollTo");
115
+
116
+ act(() => {
117
+ ref.current!.next();
118
+ });
119
+
120
+ expect(scrollToSpy).toHaveBeenCalledWith(
121
+ expect.objectContaining({ behavior: "smooth" }),
122
+ );
123
+ });
124
+ });
@@ -273,4 +273,92 @@ describe("Carousel IntersectionObserver fallback + isInView", () => {
273
273
  "inactive",
274
274
  );
275
275
  });
276
+
277
+ it("should pass 0.6 as the default threshold to the IntersectionObserver", () => {
278
+ render(
279
+ <Carousel.Root ariaLabel="Featured products">
280
+ <Carousel.Viewport>
281
+ <Carousel.Slide data-testid="slide-0" />
282
+ </Carousel.Viewport>
283
+ </Carousel.Root>,
284
+ );
285
+
286
+ expect(MockIntersectionObserver.latest!.options?.threshold).toBe(0.6);
287
+ });
288
+
289
+ it("should use a custom inViewThreshold as the observer's threshold option and isInView cutoff", () => {
290
+ const ref = {
291
+ current: null,
292
+ } as unknown as RefObject<CarouselImperativeApi>;
293
+ render(
294
+ <Carousel.Root
295
+ ref={ref}
296
+ ariaLabel="Featured products"
297
+ inViewThreshold={0.3}
298
+ >
299
+ <Carousel.Viewport>
300
+ <Carousel.Slide data-testid="slide-0" />
301
+ </Carousel.Viewport>
302
+ </Carousel.Root>,
303
+ );
304
+
305
+ const io = MockIntersectionObserver.latest!;
306
+ expect(io.options?.threshold).toBe(0.3);
307
+
308
+ // Below the default 0.6 but above the custom 0.3 — only in view under
309
+ // the custom threshold.
310
+ act(() => {
311
+ io.fire([
312
+ {
313
+ target: screen.getByTestId("slide-0"),
314
+ isIntersecting: true,
315
+ intersectionRatio: 0.4,
316
+ },
317
+ ]);
318
+ });
319
+
320
+ expect(ref.current!.isInView(0)).toBe(true);
321
+ });
322
+
323
+ it("should accept an array of thresholds, using the highest value as the isInView cutoff", () => {
324
+ const ref = {
325
+ current: null,
326
+ } as unknown as RefObject<CarouselImperativeApi>;
327
+ render(
328
+ <Carousel.Root
329
+ ref={ref}
330
+ ariaLabel="Featured products"
331
+ inViewThreshold={[0.2, 0.5, 0.8]}
332
+ >
333
+ <Carousel.Viewport>
334
+ <Carousel.Slide data-testid="slide-0" />
335
+ </Carousel.Viewport>
336
+ </Carousel.Root>,
337
+ );
338
+
339
+ const io = MockIntersectionObserver.latest!;
340
+ expect(io.options?.threshold).toEqual([0.2, 0.5, 0.8]);
341
+
342
+ act(() => {
343
+ io.fire([
344
+ {
345
+ target: screen.getByTestId("slide-0"),
346
+ isIntersecting: true,
347
+ intersectionRatio: 0.6,
348
+ },
349
+ ]);
350
+ });
351
+ expect(ref.current!.isInView(0)).toBe(false);
352
+
353
+ act(() => {
354
+ io.fire([
355
+ {
356
+ target: screen.getByTestId("slide-0"),
357
+ isIntersecting: true,
358
+ intersectionRatio: 0.9,
359
+ },
360
+ ]);
361
+ });
362
+ expect(ref.current!.isInView(0)).toBe(true);
363
+ });
276
364
  });
@@ -0,0 +1,163 @@
1
+ import { act, render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+
4
+ import { Carousel } from "../index.ts";
5
+
6
+ function renderWithSlides(
7
+ rootProps: {
8
+ loop?: boolean | "wrap" | "infinite";
9
+ defaultPage?: number;
10
+ } = {},
11
+ count = 3,
12
+ ) {
13
+ return render(
14
+ <Carousel.Root ariaLabel="Featured products" {...rootProps}>
15
+ <Carousel.Viewport>
16
+ {Array.from({ length: count }).map((_, i) => (
17
+ <Carousel.Slide key={i} data-testid={`slide-${i}`} />
18
+ ))}
19
+ </Carousel.Viewport>
20
+ <Carousel.PreviousTrigger>Previous</Carousel.PreviousTrigger>
21
+ <Carousel.NextTrigger>Next</Carousel.NextTrigger>
22
+ </Carousel.Root>,
23
+ );
24
+ }
25
+
26
+ describe("Carousel loop (semantic wrap)", () => {
27
+ it('should resolve the bare loop flag to data-loop="wrap" on the Root', () => {
28
+ renderWithSlides({ loop: true });
29
+
30
+ expect(
31
+ screen.getByRole("region", { name: "Featured products" }),
32
+ ).toHaveAttribute("data-loop", "wrap");
33
+ });
34
+
35
+ it('should publish data-loop="none" on the Root when loop is left off', () => {
36
+ renderWithSlides();
37
+
38
+ expect(
39
+ screen.getByRole("region", { name: "Featured products" }),
40
+ ).toHaveAttribute("data-loop", "none");
41
+ });
42
+
43
+ it('should resolve loop="wrap" to data-loop="wrap"', () => {
44
+ renderWithSlides({ loop: "wrap" });
45
+
46
+ expect(
47
+ screen.getByRole("region", { name: "Featured products" }),
48
+ ).toHaveAttribute("data-loop", "wrap");
49
+ });
50
+
51
+ it('should resolve loop="infinite" to data-loop="infinite"', () => {
52
+ renderWithSlides({ loop: "infinite" });
53
+
54
+ expect(
55
+ screen.getByRole("region", { name: "Featured products" }),
56
+ ).toHaveAttribute("data-loop", "infinite");
57
+ });
58
+
59
+ it("should share the wrap page model with infinite — Next stays enabled and wraps on the last page", async () => {
60
+ const user = userEvent.setup();
61
+ const { container } = renderWithSlides({
62
+ loop: "infinite",
63
+ defaultPage: 2,
64
+ });
65
+
66
+ const next = screen.getByRole("button", { name: "Next" });
67
+ expect(next).not.toBeDisabled();
68
+
69
+ await user.click(next);
70
+
71
+ // Wrap and infinite share the same page model — Next on the last page sets
72
+ // the active page back to the first (the transform engine only changes how
73
+ // that page is presented, not the page state). Scope to the REAL slide (the
74
+ // clone strip copies data-testid but strips data-index).
75
+ const slide0 = container.querySelector('[data-testid="slide-0"][data-index]');
76
+ expect(slide0).toHaveAttribute("data-state", "active");
77
+ });
78
+
79
+ it("should keep Carousel.NextTrigger enabled on the last page when looping", () => {
80
+ renderWithSlides({ loop: true, defaultPage: 2 });
81
+
82
+ expect(screen.getByRole("button", { name: "Next" })).not.toBeDisabled();
83
+ });
84
+
85
+ it("should keep Carousel.PreviousTrigger enabled on the first page when looping", () => {
86
+ renderWithSlides({ loop: true });
87
+
88
+ expect(
89
+ screen.getByRole("button", { name: "Previous" }),
90
+ ).not.toBeDisabled();
91
+ });
92
+
93
+ it("should wrap Next from the last page back to the first when looping", async () => {
94
+ const user = userEvent.setup();
95
+ renderWithSlides({ loop: true, defaultPage: 2 });
96
+
97
+ await user.click(screen.getByRole("button", { name: "Next" }));
98
+
99
+ expect(screen.getByTestId("slide-0")).toHaveAttribute(
100
+ "data-state",
101
+ "active",
102
+ );
103
+ expect(screen.getByTestId("slide-2")).toHaveAttribute(
104
+ "data-state",
105
+ "inactive",
106
+ );
107
+ });
108
+
109
+ it("should wrap Previous from the first page back to the last when looping", async () => {
110
+ const user = userEvent.setup();
111
+ renderWithSlides({ loop: true });
112
+
113
+ await user.click(screen.getByRole("button", { name: "Previous" }));
114
+
115
+ expect(screen.getByTestId("slide-2")).toHaveAttribute(
116
+ "data-state",
117
+ "active",
118
+ );
119
+ expect(screen.getByTestId("slide-0")).toHaveAttribute(
120
+ "data-state",
121
+ "inactive",
122
+ );
123
+ });
124
+
125
+ it("should still disable both triggers when only one page exists, even with loop", () => {
126
+ renderWithSlides({ loop: true }, 1);
127
+
128
+ expect(screen.getByRole("button", { name: "Previous" })).toBeDisabled();
129
+ expect(screen.getByRole("button", { name: "Next" })).toBeDisabled();
130
+ });
131
+
132
+ it("should let autoplay wrap past the last page back to the first when looping", () => {
133
+ vi.useFakeTimers();
134
+ try {
135
+ render(
136
+ <Carousel.Root
137
+ ariaLabel="Featured products"
138
+ loop
139
+ autoplay
140
+ defaultPlaying
141
+ defaultPage={1}
142
+ >
143
+ <Carousel.Viewport>
144
+ <Carousel.Slide data-testid="slide-0" />
145
+ <Carousel.Slide data-testid="slide-1" />
146
+ </Carousel.Viewport>
147
+ </Carousel.Root>,
148
+ );
149
+
150
+ // Start on the last page; without loop autoplay would stop here.
151
+ act(() => {
152
+ vi.advanceTimersByTime(4000);
153
+ });
154
+
155
+ expect(screen.getByTestId("slide-0")).toHaveAttribute(
156
+ "data-state",
157
+ "active",
158
+ );
159
+ } finally {
160
+ vi.useRealTimers();
161
+ }
162
+ });
163
+ });
@@ -0,0 +1,77 @@
1
+ import {
2
+ shortestStep,
3
+ snapTarget,
4
+ flingTarget,
5
+ normalizeOffset,
6
+ } from "../loopEngine.ts";
7
+
8
+ // Pure positioning math for the infinite transform loop (RFC 0018). No DOM —
9
+ // these are the deterministic heart the interaction layer composes: which way to
10
+ // wrap, where a drag settles, where a fling lands, and how to keep the track
11
+ // offset bounded for seamless repositioning.
12
+
13
+ describe("loopEngine.shortestStep", () => {
14
+ it("steps forward by one when the target is the next slide", () => {
15
+ expect(shortestStep(0, 1, 4)).toBe(1);
16
+ });
17
+
18
+ it("wraps forward the short way from the last slide to the first", () => {
19
+ // 3 → 0 around a ring of 4 is +1 forward, not −3 back (the rewind).
20
+ expect(shortestStep(3, 0, 4)).toBe(1);
21
+ });
22
+
23
+ it("wraps backward the short way from the first slide to the last", () => {
24
+ expect(shortestStep(0, 3, 4)).toBe(-1);
25
+ });
26
+
27
+ it("resolves an exact half-way tie forward", () => {
28
+ // 0 → 2 of 4 is equidistant either way; ties go forward (positive).
29
+ expect(shortestStep(0, 2, 4)).toBe(2);
30
+ });
31
+
32
+ it("takes the backward route when it is strictly shorter", () => {
33
+ // 0 → 3 of 5: forward 3 vs backward 2 → backward.
34
+ expect(shortestStep(0, 3, 5)).toBe(-2);
35
+ });
36
+
37
+ it("is a no-op for a non-positive count", () => {
38
+ expect(shortestStep(0, 1, 0)).toBe(0);
39
+ });
40
+ });
41
+
42
+ describe("loopEngine.snapTarget", () => {
43
+ it("rounds a continuous offset to the nearest slide boundary", () => {
44
+ expect(snapTarget(230, 100)).toBe(200);
45
+ expect(snapTarget(260, 100)).toBe(300);
46
+ });
47
+
48
+ it("returns 0 for a non-positive stride", () => {
49
+ expect(snapTarget(260, 0)).toBe(0);
50
+ });
51
+ });
52
+
53
+ describe("loopEngine.flingTarget", () => {
54
+ it("carries the offset by velocity × deceleration, then snaps to a boundary", () => {
55
+ // offset 100 + (2 px/ms × 60 ms) = 220 → nearest 100-boundary = 200.
56
+ expect(flingTarget(100, 2, 60, 100)).toBe(200);
57
+ });
58
+
59
+ it("projects backward for a negative velocity", () => {
60
+ // 300 + (−2 × 60) = 180 → nearest boundary 200.
61
+ expect(flingTarget(300, -2, 60, 100)).toBe(200);
62
+ });
63
+ });
64
+
65
+ describe("loopEngine.normalizeOffset", () => {
66
+ it("wraps an over-long offset back into [0, trackLength)", () => {
67
+ expect(normalizeOffset(450, 400)).toBe(50);
68
+ });
69
+
70
+ it("wraps a negative offset up into range", () => {
71
+ expect(normalizeOffset(-50, 400)).toBe(350);
72
+ });
73
+
74
+ it("returns 0 for a non-positive track length", () => {
75
+ expect(normalizeOffset(450, 0)).toBe(0);
76
+ });
77
+ });