@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,172 @@
1
+ import { render, screen } from "@testing-library/react";
2
+
3
+ import { Carousel } from "../index.ts";
4
+
5
+ // scroll-snap-align must only land on a page's *leading* slide when
6
+ // slidesPerPage > 1 — an interior slide is not a valid resting position,
7
+ // since the browser's mandatory snap would stop there and leave the
8
+ // viewport showing a straddled mix of two pages while currentPage/the
9
+ // indicators still claim a single clean page is active (the mouse-wheel /
10
+ // drag desync). data-snap-align is the CSS hook the registry stylesheet
11
+ // scopes scroll-snap-align to — its *value* mirrors the resolved root
12
+ // `snapAlign` ("center" default, "start"/"end"), so a user's native scroll
13
+ // snap lands wherever the programmatic scroll targets, instead of always
14
+ // snapping to "center" regardless of `snapAlign`.
15
+ describe("Carousel slide snap-align hook", () => {
16
+ it('should mark every slide data-snap-align="center" when slidesPerPage is 1 (default)', () => {
17
+ render(
18
+ <Carousel.Root ariaLabel="Featured products">
19
+ <Carousel.Viewport>
20
+ <Carousel.Slide data-testid="slide-0" />
21
+ <Carousel.Slide data-testid="slide-1" />
22
+ <Carousel.Slide data-testid="slide-2" />
23
+ </Carousel.Viewport>
24
+ </Carousel.Root>,
25
+ );
26
+
27
+ expect(screen.getByTestId("slide-0")).toHaveAttribute(
28
+ "data-snap-align",
29
+ "center",
30
+ );
31
+ expect(screen.getByTestId("slide-1")).toHaveAttribute(
32
+ "data-snap-align",
33
+ "center",
34
+ );
35
+ expect(screen.getByTestId("slide-2")).toHaveAttribute(
36
+ "data-snap-align",
37
+ "center",
38
+ );
39
+ });
40
+
41
+ it("should only mark page-start slides when slidesPerPage > 1 (clean paging)", () => {
42
+ render(
43
+ <Carousel.Root ariaLabel="Featured products" slidesPerPage={2}>
44
+ <Carousel.Viewport>
45
+ <Carousel.Slide data-testid="slide-0" />
46
+ <Carousel.Slide data-testid="slide-1" />
47
+ <Carousel.Slide data-testid="slide-2" />
48
+ <Carousel.Slide data-testid="slide-3" />
49
+ </Carousel.Viewport>
50
+ </Carousel.Root>,
51
+ );
52
+
53
+ expect(screen.getByTestId("slide-0")).toHaveAttribute(
54
+ "data-snap-align",
55
+ "center",
56
+ );
57
+ expect(screen.getByTestId("slide-1")).not.toHaveAttribute(
58
+ "data-snap-align",
59
+ );
60
+ expect(screen.getByTestId("slide-2")).toHaveAttribute(
61
+ "data-snap-align",
62
+ "center",
63
+ );
64
+ expect(screen.getByTestId("slide-3")).not.toHaveAttribute(
65
+ "data-snap-align",
66
+ );
67
+ });
68
+
69
+ it("should mark the end-aligned last page's leading slide, even off the move step (uneven last page)", () => {
70
+ // perPage 2, 5 slides: offsets 0, 2, 3 (end-aligned) — slide 4 is never
71
+ // a page start (it only ever appears as page 2's *trailing* slide).
72
+ render(
73
+ <Carousel.Root ariaLabel="Featured products" slidesPerPage={2}>
74
+ <Carousel.Viewport>
75
+ <Carousel.Slide data-testid="slide-0" />
76
+ <Carousel.Slide data-testid="slide-1" />
77
+ <Carousel.Slide data-testid="slide-2" />
78
+ <Carousel.Slide data-testid="slide-3" />
79
+ <Carousel.Slide data-testid="slide-4" />
80
+ </Carousel.Viewport>
81
+ </Carousel.Root>,
82
+ );
83
+
84
+ expect(screen.getByTestId("slide-0")).toHaveAttribute(
85
+ "data-snap-align",
86
+ "center",
87
+ );
88
+ expect(screen.getByTestId("slide-1")).not.toHaveAttribute(
89
+ "data-snap-align",
90
+ );
91
+ expect(screen.getByTestId("slide-2")).toHaveAttribute(
92
+ "data-snap-align",
93
+ "center",
94
+ );
95
+ expect(screen.getByTestId("slide-3")).toHaveAttribute(
96
+ "data-snap-align",
97
+ "center",
98
+ );
99
+ expect(screen.getByTestId("slide-4")).not.toHaveAttribute(
100
+ "data-snap-align",
101
+ );
102
+ });
103
+
104
+ it('should mark page-start slides data-snap-align="start" when the root snapAlign is "start"', () => {
105
+ render(
106
+ <Carousel.Root ariaLabel="Featured products" snapAlign="start">
107
+ <Carousel.Viewport>
108
+ <Carousel.Slide data-testid="slide-0" />
109
+ <Carousel.Slide data-testid="slide-1" />
110
+ <Carousel.Slide data-testid="slide-2" />
111
+ </Carousel.Viewport>
112
+ </Carousel.Root>,
113
+ );
114
+
115
+ expect(screen.getByTestId("slide-0")).toHaveAttribute(
116
+ "data-snap-align",
117
+ "start",
118
+ );
119
+ expect(screen.getByTestId("slide-1")).toHaveAttribute(
120
+ "data-snap-align",
121
+ "start",
122
+ );
123
+ expect(screen.getByTestId("slide-2")).toHaveAttribute(
124
+ "data-snap-align",
125
+ "start",
126
+ );
127
+ });
128
+
129
+ it('should let a slide\'s own snapAlign override the root default, including "start" and "end"', () => {
130
+ render(
131
+ <Carousel.Root ariaLabel="Featured products">
132
+ <Carousel.Viewport>
133
+ <Carousel.Slide data-testid="slide-0" />
134
+ <Carousel.Slide data-testid="slide-1" snapAlign="end" />
135
+ <Carousel.Slide data-testid="slide-2" snapAlign="start" />
136
+ </Carousel.Viewport>
137
+ </Carousel.Root>,
138
+ );
139
+
140
+ expect(screen.getByTestId("slide-0")).toHaveAttribute(
141
+ "data-snap-align",
142
+ "center",
143
+ );
144
+ expect(screen.getByTestId("slide-1")).toHaveAttribute(
145
+ "data-snap-align",
146
+ "end",
147
+ );
148
+ expect(screen.getByTestId("slide-2")).toHaveAttribute(
149
+ "data-snap-align",
150
+ "start",
151
+ );
152
+ });
153
+
154
+ it("should not publish a per-slide snapAlign override on a slide that isn't a valid snap-start position", () => {
155
+ render(
156
+ <Carousel.Root ariaLabel="Featured products" slidesPerPage={2}>
157
+ <Carousel.Viewport>
158
+ <Carousel.Slide data-testid="slide-0" />
159
+ <Carousel.Slide data-testid="slide-1" snapAlign="end" />
160
+ <Carousel.Slide data-testid="slide-2" />
161
+ <Carousel.Slide data-testid="slide-3" />
162
+ </Carousel.Viewport>
163
+ </Carousel.Root>,
164
+ );
165
+
166
+ // slide-1 is an interior slide of page 0 (perPage 2) — no valid resting
167
+ // position, so its override must not leak into scroll-snap-align.
168
+ expect(screen.getByTestId("slide-1")).not.toHaveAttribute(
169
+ "data-snap-align",
170
+ );
171
+ });
172
+ });
@@ -1,8 +1,19 @@
1
- import { render, screen } from "@testing-library/react";
1
+ import { act, render, screen } from "@testing-library/react";
2
2
  import userEvent from "@testing-library/user-event";
3
3
 
4
4
  import { Carousel } from "../index.ts";
5
5
 
6
+ function fireScrollSnapChange(viewport: HTMLElement, snapTarget: HTMLElement) {
7
+ const event = new Event("scrollsnapchange", { bubbles: false });
8
+ Object.defineProperty(event, "snapTargetInline", {
9
+ value: snapTarget,
10
+ writable: false,
11
+ });
12
+ act(() => {
13
+ viewport.dispatchEvent(event);
14
+ });
15
+ }
16
+
6
17
  describe("Carousel numeric slidesPerMove", () => {
7
18
  it("should render `floor((total - slidesPerPage) / slidesPerMove) + 1` indicators", () => {
8
19
  render(
@@ -148,4 +159,171 @@ describe("Carousel numeric slidesPerMove", () => {
148
159
  );
149
160
  });
150
161
 
162
+ it("should end-align the last window so an inexact move leaves no slide unreachable", () => {
163
+ // 6 slides, perPage=3, move=2. A naive floor((6-3)/2)+1 = 2 pages
164
+ // ([0,1,2] [2,3,4]) orphans slide 5. End-aligning the last window
165
+ // adds a third page ([3,4,5]) so every slide is reachable.
166
+ render(
167
+ <Carousel.Root
168
+ ariaLabel="Featured products"
169
+ slidesPerPage={3}
170
+ slidesPerMove={2}
171
+ >
172
+ <Carousel.Viewport>
173
+ <Carousel.Slide />
174
+ <Carousel.Slide />
175
+ <Carousel.Slide />
176
+ <Carousel.Slide />
177
+ <Carousel.Slide />
178
+ <Carousel.Slide />
179
+ </Carousel.Viewport>
180
+ <Carousel.Indicators label="Choose page" />
181
+ </Carousel.Root>,
182
+ );
183
+
184
+ expect(
185
+ screen.getAllByRole("button", { name: /^Slide \d+$/ }),
186
+ ).toHaveLength(3);
187
+ });
188
+
189
+ it("should snap the end-aligned last window to the track end", async () => {
190
+ const user = userEvent.setup();
191
+ render(
192
+ <Carousel.Root
193
+ ariaLabel="Featured products"
194
+ slidesPerPage={3}
195
+ slidesPerMove={2}
196
+ >
197
+ <Carousel.Viewport>
198
+ <Carousel.Slide data-testid="slide-0" />
199
+ <Carousel.Slide data-testid="slide-1" />
200
+ <Carousel.Slide data-testid="slide-2" />
201
+ <Carousel.Slide data-testid="slide-3" />
202
+ <Carousel.Slide data-testid="slide-4" />
203
+ <Carousel.Slide data-testid="slide-5" />
204
+ </Carousel.Viewport>
205
+ <Carousel.NextTrigger>Next</Carousel.NextTrigger>
206
+ </Carousel.Root>,
207
+ );
208
+
209
+ // page 0 [0,1,2] → page 1 [2,3,4] → page 2 end-aligned to [3,4,5].
210
+ await user.click(screen.getByRole("button", { name: "Next" }));
211
+ await user.click(screen.getByRole("button", { name: "Next" }));
212
+
213
+ expect(screen.getByTestId("slide-3")).toHaveAttribute(
214
+ "data-state",
215
+ "active",
216
+ );
217
+ expect(screen.getByTestId("slide-5")).toHaveAttribute(
218
+ "data-state",
219
+ "active",
220
+ );
221
+ expect(screen.getByTestId("slide-2")).toHaveAttribute(
222
+ "data-state",
223
+ "inactive",
224
+ );
225
+ expect(screen.getByRole("button", { name: "Next" })).toBeDisabled();
226
+ });
227
+
228
+ it("should clamp slidesPerMove to slidesPerPage so a move can't skip past a page", () => {
229
+ // move=3 > perPage=2 would leave a gap (window [0,1] then [3,4],
230
+ // skipping slide 2). Clamp move to perPage so windows stay contiguous:
231
+ // 6 slides → [0,1] [2,3] [4,5] → 3 pages.
232
+ render(
233
+ <Carousel.Root
234
+ ariaLabel="Featured products"
235
+ slidesPerPage={2}
236
+ slidesPerMove={3}
237
+ >
238
+ <Carousel.Viewport>
239
+ <Carousel.Slide />
240
+ <Carousel.Slide />
241
+ <Carousel.Slide />
242
+ <Carousel.Slide />
243
+ <Carousel.Slide />
244
+ <Carousel.Slide />
245
+ </Carousel.Viewport>
246
+ <Carousel.Indicators label="Choose page" />
247
+ </Carousel.Root>,
248
+ );
249
+
250
+ expect(
251
+ screen.getAllByRole("button", { name: /^Slide \d+$/ }),
252
+ ).toHaveLength(3);
253
+ });
254
+
255
+ it("should advance by at most slidesPerPage when slidesPerMove exceeds it", async () => {
256
+ const user = userEvent.setup();
257
+ render(
258
+ <Carousel.Root
259
+ ariaLabel="Featured products"
260
+ slidesPerPage={2}
261
+ slidesPerMove={3}
262
+ >
263
+ <Carousel.Viewport>
264
+ <Carousel.Slide data-testid="slide-0" />
265
+ <Carousel.Slide data-testid="slide-1" />
266
+ <Carousel.Slide data-testid="slide-2" />
267
+ <Carousel.Slide data-testid="slide-3" />
268
+ </Carousel.Viewport>
269
+ <Carousel.NextTrigger>Next</Carousel.NextTrigger>
270
+ </Carousel.Root>,
271
+ );
272
+
273
+ await user.click(screen.getByRole("button", { name: "Next" }));
274
+
275
+ // Clamped move=2 → window [2,3], not the [3,…] a move of 3 would give.
276
+ expect(screen.getByTestId("slide-2")).toHaveAttribute(
277
+ "data-state",
278
+ "active",
279
+ );
280
+ expect(screen.getByTestId("slide-3")).toHaveAttribute(
281
+ "data-state",
282
+ "active",
283
+ );
284
+ expect(screen.getByTestId("slide-1")).toHaveAttribute(
285
+ "data-state",
286
+ "inactive",
287
+ );
288
+ });
289
+
290
+ it("should map a user swipe to the nearest window in numeric slidesPerMove mode", () => {
291
+ render(
292
+ <Carousel.Root
293
+ ariaLabel="Featured products"
294
+ slidesPerPage={3}
295
+ slidesPerMove={2}
296
+ >
297
+ <Carousel.Viewport data-testid="viewport">
298
+ <Carousel.Slide data-testid="slide-0" />
299
+ <Carousel.Slide data-testid="slide-1" />
300
+ <Carousel.Slide data-testid="slide-2" />
301
+ <Carousel.Slide data-testid="slide-3" />
302
+ <Carousel.Slide data-testid="slide-4" />
303
+ <Carousel.Slide data-testid="slide-5" />
304
+ </Carousel.Viewport>
305
+ </Carousel.Root>,
306
+ );
307
+
308
+ // The user swipes so slide 3 leads the viewport. With move=2 the nearest
309
+ // window start is the end-aligned last page [3,4,5], not floor(3/2)=1's
310
+ // [2,3,4].
311
+ fireScrollSnapChange(
312
+ screen.getByTestId("viewport"),
313
+ screen.getByTestId("slide-3"),
314
+ );
315
+
316
+ expect(screen.getByTestId("slide-3")).toHaveAttribute(
317
+ "data-state",
318
+ "active",
319
+ );
320
+ expect(screen.getByTestId("slide-5")).toHaveAttribute(
321
+ "data-state",
322
+ "active",
323
+ );
324
+ expect(screen.getByTestId("slide-2")).toHaveAttribute(
325
+ "data-state",
326
+ "inactive",
327
+ );
328
+ });
151
329
  });
@@ -1,8 +1,19 @@
1
- import { render, screen } from "@testing-library/react";
1
+ import { act, render, screen } from "@testing-library/react";
2
2
  import userEvent from "@testing-library/user-event";
3
3
 
4
4
  import { Carousel } from "../index.ts";
5
5
 
6
+ function fireScrollSnapChange(viewport: HTMLElement, snapTarget: HTMLElement) {
7
+ const event = new Event("scrollsnapchange", { bubbles: false });
8
+ Object.defineProperty(event, "snapTargetInline", {
9
+ value: snapTarget,
10
+ writable: false,
11
+ });
12
+ act(() => {
13
+ viewport.dispatchEvent(event);
14
+ });
15
+ }
16
+
6
17
  describe("Carousel slidesPerPage", () => {
7
18
  it("should mark every slide on the active page as active when slidesPerPage > 1", () => {
8
19
  render(
@@ -142,7 +153,12 @@ describe("Carousel slidesPerPage", () => {
142
153
  );
143
154
  });
144
155
 
145
- it("should activate only the remaining slides on a partial last page", () => {
156
+ it("should end-align the last page so its window is full (not a partial page that can't start-snap)", () => {
157
+ // perPage 2, 5 slides, last page. A naive offset of 2×2 = 4 leaves a
158
+ // single-slide page [4] whose leading slide can't align to the viewport
159
+ // start (there's nothing after it), so scroll-snap lands on slide 3 and the
160
+ // active page desyncs. End-aligning the last page to offset 3 → window
161
+ // [3,4], which snaps cleanly to the track end.
146
162
  render(
147
163
  <Carousel.Root
148
164
  ariaLabel="Featured products"
@@ -159,25 +175,93 @@ describe("Carousel slidesPerPage", () => {
159
175
  </Carousel.Root>,
160
176
  );
161
177
 
162
- expect(screen.getByTestId("slide-0")).toHaveAttribute(
178
+ expect(screen.getByTestId("slide-2")).toHaveAttribute(
163
179
  "data-state",
164
180
  "inactive",
165
181
  );
166
- expect(screen.getByTestId("slide-1")).toHaveAttribute(
182
+ expect(screen.getByTestId("slide-3")).toHaveAttribute(
167
183
  "data-state",
168
- "inactive",
184
+ "active",
169
185
  );
170
- expect(screen.getByTestId("slide-2")).toHaveAttribute(
186
+ expect(screen.getByTestId("slide-4")).toHaveAttribute(
171
187
  "data-state",
172
- "inactive",
188
+ "active",
189
+ );
190
+ });
191
+
192
+ it("should reach the end-aligned last page in two clicks without desyncing (perPage 3, 7 slides)", async () => {
193
+ const user = userEvent.setup();
194
+ render(
195
+ <Carousel.Root
196
+ ariaLabel="Featured products"
197
+ slidesPerPage={3}
198
+ >
199
+ <Carousel.Viewport>
200
+ <Carousel.Slide data-testid="slide-0" />
201
+ <Carousel.Slide data-testid="slide-1" />
202
+ <Carousel.Slide data-testid="slide-2" />
203
+ <Carousel.Slide data-testid="slide-3" />
204
+ <Carousel.Slide data-testid="slide-4" />
205
+ <Carousel.Slide data-testid="slide-5" />
206
+ <Carousel.Slide data-testid="slide-6" />
207
+ </Carousel.Viewport>
208
+ <Carousel.NextTrigger>Next</Carousel.NextTrigger>
209
+ </Carousel.Root>,
210
+ );
211
+
212
+ // 3 pages: [0,1,2] [3,4,5] [4,5,6] (last end-aligned). Two clicks reach it.
213
+ await user.click(screen.getByRole("button", { name: "Next" }));
214
+ await user.click(screen.getByRole("button", { name: "Next" }));
215
+
216
+ expect(screen.getByTestId("slide-4")).toHaveAttribute(
217
+ "data-state",
218
+ "active",
219
+ );
220
+ expect(screen.getByTestId("slide-6")).toHaveAttribute(
221
+ "data-state",
222
+ "active",
173
223
  );
174
224
  expect(screen.getByTestId("slide-3")).toHaveAttribute(
175
225
  "data-state",
176
226
  "inactive",
177
227
  );
228
+ expect(screen.getByRole("button", { name: "Next" })).toBeDisabled();
229
+ });
230
+
231
+ it("should map a swipe onto the end-aligned last page to the last page (not the page a floor would give)", () => {
232
+ render(
233
+ <Carousel.Root ariaLabel="Featured products" slidesPerPage={3}>
234
+ <Carousel.Viewport data-testid="viewport">
235
+ <Carousel.Slide data-testid="slide-0" />
236
+ <Carousel.Slide data-testid="slide-1" />
237
+ <Carousel.Slide data-testid="slide-2" />
238
+ <Carousel.Slide data-testid="slide-3" />
239
+ <Carousel.Slide data-testid="slide-4" />
240
+ <Carousel.Slide data-testid="slide-5" />
241
+ <Carousel.Slide data-testid="slide-6" />
242
+ </Carousel.Viewport>
243
+ </Carousel.Root>,
244
+ );
245
+
246
+ // The last page's leading slide is 4 (offset end-aligned to 7 − 3 = 4).
247
+ // A floor(4 / 3) would map it to page 1; the nearest-offset inverse maps it
248
+ // to the last page.
249
+ fireScrollSnapChange(
250
+ screen.getByTestId("viewport"),
251
+ screen.getByTestId("slide-4"),
252
+ );
253
+
178
254
  expect(screen.getByTestId("slide-4")).toHaveAttribute(
179
255
  "data-state",
180
256
  "active",
181
257
  );
258
+ expect(screen.getByTestId("slide-6")).toHaveAttribute(
259
+ "data-state",
260
+ "active",
261
+ );
262
+ expect(screen.getByTestId("slide-3")).toHaveAttribute(
263
+ "data-state",
264
+ "inactive",
265
+ );
182
266
  });
183
267
  });
@@ -0,0 +1,35 @@
1
+ import { render, screen } from "@testing-library/react";
2
+
3
+ import { Carousel } from "../index.ts";
4
+
5
+ // scroll-snap-type strictness ("mandatory" forces a rest at a snap point;
6
+ // "proximity" only nudges toward one) is a Root-level prop, published as
7
+ // data-snap-type on the Viewport so the registry stylesheet can key off it —
8
+ // mirrors Ark UI's snapType.
9
+ describe("Carousel snapType", () => {
10
+ it('should default to data-snap-type="mandatory" on the viewport', () => {
11
+ render(
12
+ <Carousel.Root ariaLabel="Featured products">
13
+ <Carousel.Viewport data-testid="viewport" />
14
+ </Carousel.Root>,
15
+ );
16
+
17
+ expect(screen.getByTestId("viewport")).toHaveAttribute(
18
+ "data-snap-type",
19
+ "mandatory",
20
+ );
21
+ });
22
+
23
+ it('should reflect snapType="proximity" as data-snap-type on the viewport', () => {
24
+ render(
25
+ <Carousel.Root ariaLabel="Featured products" snapType="proximity">
26
+ <Carousel.Viewport data-testid="viewport" />
27
+ </Carousel.Root>,
28
+ );
29
+
30
+ expect(screen.getByTestId("viewport")).toHaveAttribute(
31
+ "data-snap-type",
32
+ "proximity",
33
+ );
34
+ });
35
+ });
@@ -18,6 +18,34 @@ function fireScrollSnapChange(
18
18
  }
19
19
 
20
20
  describe("Carousel transition modes", () => {
21
+ it('should publish data-transition="slide" on the root by default (the CSS styling hook)', () => {
22
+ render(
23
+ <Carousel.Root ariaLabel="Featured products">
24
+ <Carousel.Viewport>
25
+ <Carousel.Slide />
26
+ </Carousel.Viewport>
27
+ </Carousel.Root>,
28
+ );
29
+
30
+ expect(
31
+ screen.getByRole("region", { name: "Featured products" }),
32
+ ).toHaveAttribute("data-transition", "slide");
33
+ });
34
+
35
+ it('should publish data-transition="fade" when transition="fade" so CSS can crossfade off the hook', () => {
36
+ render(
37
+ <Carousel.Root ariaLabel="Featured products" transition="fade">
38
+ <Carousel.Viewport>
39
+ <Carousel.Slide />
40
+ </Carousel.Viewport>
41
+ </Carousel.Root>,
42
+ );
43
+
44
+ expect(
45
+ screen.getByRole("region", { name: "Featured products" }),
46
+ ).toHaveAttribute("data-transition", "fade");
47
+ });
48
+
21
49
  it('should skip the programmatic scroll on page change when transition="none"', async () => {
22
50
  const user = userEvent.setup();
23
51
  render(
@@ -30,10 +58,7 @@ describe("Carousel transition modes", () => {
30
58
  </Carousel.Root>,
31
59
  );
32
60
 
33
- const scrollIntoViewSpy = vi.spyOn(
34
- screen.getByTestId("slide-1"),
35
- "scrollIntoView",
36
- );
61
+ const scrollToSpy = vi.spyOn(screen.getByTestId("viewport"), "scrollTo");
37
62
 
38
63
  await user.click(screen.getByRole("button", { name: "Next" }));
39
64
 
@@ -43,7 +68,7 @@ describe("Carousel transition modes", () => {
43
68
  "data-state",
44
69
  "active",
45
70
  );
46
- expect(scrollIntoViewSpy).not.toHaveBeenCalled();
71
+ expect(scrollToSpy).not.toHaveBeenCalled();
47
72
  });
48
73
 
49
74
  it('should ignore scrollsnapchange events on the viewport when transition="none"', () => {
@@ -108,6 +108,26 @@ describe("Carousel translations", () => {
108
108
  ).toBeVisible();
109
109
  });
110
110
 
111
+ it("should override the ProgressText format via translations.progressText", () => {
112
+ render(
113
+ <Carousel.Root
114
+ ariaLabel="Featured products"
115
+ translations={{
116
+ progressText: ({ page, totalPages }) =>
117
+ `Page ${page + 1} sur ${totalPages}`,
118
+ }}
119
+ >
120
+ <Carousel.Viewport>
121
+ <Carousel.Slide />
122
+ <Carousel.Slide />
123
+ </Carousel.Viewport>
124
+ <Carousel.ProgressText />
125
+ </Carousel.Root>,
126
+ );
127
+
128
+ expect(screen.getByText("Page 1 sur 2")).toBeVisible();
129
+ });
130
+
111
131
  it("should fall back to defaults for any translation keys not specified", () => {
112
132
  render(
113
133
  <Carousel.Root
@@ -123,6 +143,7 @@ describe("Carousel translations", () => {
123
143
  <Carousel.Indicator index={0} />
124
144
  </Carousel.IndicatorGroup>
125
145
  <Carousel.PlayPauseTrigger />
146
+ <Carousel.ProgressText />
126
147
  </Carousel.Root>,
127
148
  );
128
149
 
@@ -135,6 +156,8 @@ describe("Carousel translations", () => {
135
156
  );
136
157
  // indicatorLabel default still applied
137
158
  expect(screen.getByRole("button", { name: "Slide 1" })).toBeVisible();
159
+ // progressText default still applied
160
+ expect(screen.getByText("1 of 2")).toBeVisible();
138
161
  });
139
162
 
140
163
  it("should apply translations.indicatorLabel to indicators auto-rendered by Carousel.Indicators", () => {