@primitiv-ui/react 0.1.28 → 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.
- package/README.md +4 -0
- package/package.json +6 -1
- package/src/AccessibleIcon/AccessibleIcon.tsx +32 -9
- package/src/AccessibleIcon/__tests__/AccessibleIcon.test.tsx +5 -0
- package/src/AccessibleIcon/types.ts +15 -2
- package/src/Accordion/Accordion.tsx +25 -4
- package/src/Accordion/__tests__/Accordion.asChild.test.tsx +46 -0
- package/src/Accordion/__tests__/Accordion.basic-rendering.test.tsx +15 -0
- package/src/Accordion/__tests__/Accordion.disabled-items.test.tsx +42 -0
- package/src/Accordion/__tests__/Accordion.error-handling.test.tsx +33 -0
- package/src/Accordion/hooks/useAccordionRoot.ts +44 -7
- package/src/Accordion/hooks/useAccordionTrigger.ts +9 -1
- package/src/Accordion/types.ts +72 -21
- package/src/Alert/Alert.tsx +36 -7
- package/src/Alert/__tests__/Alert.test.tsx +5 -0
- package/src/Alert/types.ts +18 -2
- package/src/Avatar/Avatar.tsx +99 -15
- package/src/Avatar/__tests__/Avatar.basic-rendering.test.tsx +10 -0
- package/src/Avatar/__tests__/Avatar.fallback.test.tsx +30 -0
- package/src/Avatar/__tests__/Avatar.image-loading.test.tsx +35 -0
- package/src/Avatar/hooks/useAvatarImage.ts +16 -0
- package/src/Avatar/types.ts +15 -3
- package/src/Breadcrumb/Breadcrumb.tsx +136 -18
- package/src/Breadcrumb/README.md +47 -0
- package/src/Breadcrumb/__tests__/Breadcrumb.basic-rendering.test.tsx +11 -0
- package/src/Breadcrumb/__tests__/Breadcrumb.ellipsis.test.tsx +110 -0
- package/src/Breadcrumb/types.ts +73 -7
- package/src/Button/Button.tsx +2 -0
- package/src/Button/__tests__/Button.basic-rendering.test.tsx +6 -0
- package/src/Button/types.ts +21 -0
- package/src/Carousel/Carousel.tsx +263 -15
- package/src/Carousel/README.md +749 -72
- package/src/Carousel/__tests__/Carousel.autoplay-status.test.tsx +239 -0
- package/src/Carousel/__tests__/Carousel.drag-status.test.tsx +292 -0
- package/src/Carousel/__tests__/Carousel.dynamic-slides.test.tsx +38 -0
- package/src/Carousel/__tests__/Carousel.indicators.test.tsx +101 -0
- package/src/Carousel/__tests__/Carousel.infinite.test.tsx +1453 -0
- package/src/Carousel/__tests__/Carousel.instant-scroll.test.tsx +124 -0
- package/src/Carousel/__tests__/Carousel.intersection-observer.test.tsx +88 -0
- package/src/Carousel/__tests__/Carousel.loop.test.tsx +163 -0
- package/src/Carousel/__tests__/Carousel.loopEngine.test.ts +77 -0
- package/src/Carousel/__tests__/Carousel.mouse-drag.test.tsx +504 -0
- package/src/Carousel/__tests__/Carousel.multi-slide-bounds.test.tsx +91 -0
- package/src/Carousel/__tests__/Carousel.orientation.test.tsx +207 -0
- package/src/Carousel/__tests__/Carousel.overscroll.test.tsx +532 -0
- package/src/Carousel/__tests__/Carousel.page-snap-points.test.tsx +50 -0
- package/src/Carousel/__tests__/Carousel.progress-text.test.tsx +76 -0
- package/src/Carousel/__tests__/Carousel.reduced-motion.test.tsx +3 -6
- package/src/Carousel/__tests__/Carousel.refresh-progress.test.tsx +2 -5
- package/src/Carousel/__tests__/Carousel.scroll-margin.test.tsx +167 -0
- package/src/Carousel/__tests__/Carousel.scroll-progress.test.tsx +518 -0
- package/src/Carousel/__tests__/Carousel.scroll-snap-change.test.tsx +32 -11
- package/src/Carousel/__tests__/Carousel.scroll-sync.test.tsx +237 -24
- package/src/Carousel/__tests__/Carousel.scroll-to-index.test.tsx +115 -0
- package/src/Carousel/__tests__/Carousel.slide-snap.test.tsx +172 -0
- package/src/Carousel/__tests__/Carousel.slides-per-move.test.tsx +179 -1
- package/src/Carousel/__tests__/Carousel.slides-per-page.test.tsx +91 -7
- package/src/Carousel/__tests__/Carousel.snap-type.test.tsx +35 -0
- package/src/Carousel/__tests__/Carousel.transition-modes.test.tsx +30 -5
- package/src/Carousel/__tests__/Carousel.translations.test.tsx +23 -0
- package/src/Carousel/__tests__/Carousel.wheel.test.tsx +160 -0
- package/src/Carousel/hooks/index.ts +1 -0
- package/src/Carousel/hooks/useCarouselLoop.ts +869 -0
- package/src/Carousel/hooks/useCarouselRoot.ts +338 -42
- package/src/Carousel/hooks/useCarouselSlide.ts +44 -9
- package/src/Carousel/hooks/useCarouselViewport.ts +649 -46
- package/src/Carousel/loopEngine.ts +54 -0
- package/src/Carousel/types.ts +457 -35
- package/src/Checkbox/Checkbox.tsx +9 -1
- package/src/Checkbox/__tests__/Checkbox.basic-rendering.test.tsx +14 -0
- package/src/Checkbox/hooks/useCheckboxInput.ts +4 -0
- package/src/Checkbox/types.ts +19 -0
- package/src/CheckboxCard/CheckboxCard.tsx +13 -4
- package/src/CheckboxCard/__tests__/CheckboxCard.basic-rendering.test.tsx +7 -0
- package/src/CheckboxCard/types.ts +65 -17
- package/src/Collapsible/Collapsible.tsx +49 -3
- package/src/Collapsible/README.md +40 -0
- package/src/Collapsible/__tests__/Collapsible.asChild.test.tsx +89 -0
- package/src/Collapsible/__tests__/Collapsible.basic-rendering.test.tsx +27 -0
- package/src/Collapsible/__tests__/Collapsible.collapsedHeight.test.tsx +140 -0
- package/src/Collapsible/hooks/useCollapsibleTrigger.ts +3 -1
- package/src/Collapsible/types.ts +89 -22
- package/src/ContextMenu/ContextMenu.tsx +270 -4
- package/src/ContextMenu/types.ts +148 -6
- package/src/DirectionProvider/DirectionProvider.tsx +41 -9
- package/src/DirectionProvider/__tests__/DirectionProvider.test.tsx +5 -0
- package/src/DirectionProvider/types.ts +28 -3
- package/src/Divider/Divider.tsx +24 -10
- package/src/Divider/__tests__/Divider.test.tsx +5 -0
- package/src/Divider/types.ts +10 -3
- package/src/Drawer/Drawer.tsx +315 -0
- package/src/Drawer/README.md +124 -0
- package/src/Drawer/__tests__/Drawer.behavior.test.tsx +100 -0
- package/src/Drawer/__tests__/Drawer.focus-trap.test.tsx +55 -0
- package/src/Drawer/__tests__/Drawer.rendering.test.tsx +111 -0
- package/src/Drawer/__tests__/Drawer.side.test.tsx +69 -0
- package/src/Drawer/__tests__/dialog-polyfill.ts +39 -0
- package/src/Drawer/index.ts +2 -0
- package/src/Drawer/types.ts +66 -0
- package/src/Dropdown/Dropdown.tsx +117 -0
- package/src/Dropdown/types.ts +241 -11
- package/src/EmptyState/EmptyState.tsx +74 -4
- package/src/EmptyState/__tests__/EmptyState.Actions.test.tsx +7 -2
- package/src/EmptyState/__tests__/EmptyState.Description.test.tsx +3 -1
- package/src/EmptyState/__tests__/EmptyState.Root.test.tsx +10 -0
- package/src/EmptyState/__tests__/EmptyState.Title.test.tsx +3 -1
- package/src/EmptyState/types.ts +49 -6
- package/src/Field/Field.tsx +23 -0
- package/src/Field/__tests__/Field.basic-rendering.test.tsx +11 -0
- package/src/Field/__tests__/Field.error-handling.test.tsx +23 -0
- package/src/Field/__tests__/Field.state-cascade.test.tsx +21 -0
- package/src/Field/hooks/__tests__/useFieldProps.test.tsx +104 -9
- package/src/Field/types.ts +60 -12
- package/src/Fieldset/Fieldset.tsx +54 -18
- package/src/Fieldset/__tests__/Fieldset.basic-rendering.test.tsx +9 -0
- package/src/Fieldset/types.ts +20 -2
- package/src/Input/Input.tsx +14 -9
- package/src/Input/__tests__/Input.basic-rendering.test.tsx +5 -0
- package/src/Input/types.ts +19 -2
- package/src/InputGroup/InputGroup.tsx +11 -3
- package/src/InputGroup/__tests__/InputGroup.basic-rendering.test.tsx +14 -0
- package/src/InputGroup/types.ts +26 -5
- package/src/MillerColumns/MillerColumns.tsx +86 -19
- package/src/MillerColumns/types.ts +83 -27
- package/src/Modal/Modal.tsx +31 -4
- package/src/Modal/README.md +19 -10
- package/src/Modal/__tests__/Modal.focus-trap.test.tsx +162 -0
- package/src/Modal/hooks/useModalContent.ts +32 -0
- package/src/Modal/types.ts +110 -29
- package/src/NavigationMenu/NavigationMenu.tsx +890 -0
- package/src/NavigationMenu/NavigationMenuContext.ts +67 -0
- package/src/NavigationMenu/README.md +355 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.anchor-positioning.test.tsx +132 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.asChild.test.tsx +73 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.basic-rendering.test.tsx +92 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.contract.test.tsx +45 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.controlled-state.test.tsx +134 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.error-handling.test.tsx +62 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.fixtures.tsx +79 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.forceMount.test.tsx +52 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.hover-interaction.test.tsx +254 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.indicator.test.tsx +252 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-interaction.test.tsx +294 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-navigation.test.tsx +126 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.link.test.tsx +139 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.motion.test.tsx +568 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.mouse-interaction.test.tsx +76 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.uncontrolled-state.test.tsx +30 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.viewport.test.tsx +113 -0
- package/src/NavigationMenu/constants.ts +5 -0
- package/src/NavigationMenu/hooks/index.ts +7 -0
- package/src/NavigationMenu/hooks/useNavigationMenuEntry.ts +33 -0
- package/src/NavigationMenu/hooks/useNavigationMenuIndicator.ts +70 -0
- package/src/NavigationMenu/hooks/useNavigationMenuLink.ts +72 -0
- package/src/NavigationMenu/hooks/useNavigationMenuRoot.ts +301 -0
- package/src/NavigationMenu/hooks/useNavigationMenuTopLevelEntry.ts +65 -0
- package/src/NavigationMenu/hooks/useNavigationMenuTrigger.ts +158 -0
- package/src/NavigationMenu/hooks/useNavigationMenuViewport.ts +101 -0
- package/src/NavigationMenu/index.ts +3 -0
- package/src/NavigationMenu/types.ts +259 -0
- package/src/NavigationMenu/utils.ts +89 -0
- package/src/Popover/Popover.tsx +427 -0
- package/src/Popover/PopoverContext.ts +13 -0
- package/src/Popover/README.md +106 -0
- package/src/Popover/__tests__/Popover.anchor.test.tsx +38 -0
- package/src/Popover/__tests__/Popover.asChild.test.tsx +160 -0
- package/src/Popover/__tests__/Popover.basic-rendering.test.tsx +106 -0
- package/src/Popover/__tests__/Popover.click-outside.test.tsx +149 -0
- package/src/Popover/__tests__/Popover.close.test.tsx +70 -0
- package/src/Popover/__tests__/Popover.content.test.tsx +58 -0
- package/src/Popover/__tests__/Popover.controlled-state.test.tsx +45 -0
- package/src/Popover/__tests__/Popover.error-handling.test.tsx +51 -0
- package/src/Popover/__tests__/Popover.focus.test.tsx +43 -0
- package/src/Popover/__tests__/Popover.keyboard-interaction.test.tsx +71 -0
- package/src/Popover/__tests__/Popover.mouse-interaction.test.tsx +69 -0
- package/src/Popover/__tests__/Popover.uncontrolled-state.test.tsx +44 -0
- package/src/Popover/hooks/index.ts +3 -0
- package/src/Popover/hooks/usePopoverContent.ts +98 -0
- package/src/Popover/hooks/usePopoverRoot.ts +84 -0
- package/src/Popover/hooks/usePopoverTrigger.ts +33 -0
- package/src/Popover/index.ts +2 -0
- package/src/Popover/types.ts +197 -0
- package/src/Portal/Portal.tsx +50 -10
- package/src/Portal/__tests__/Portal.basic-rendering.test.tsx +5 -0
- package/src/Portal/types.ts +5 -0
- package/src/Progress/Progress.tsx +33 -1
- package/src/Progress/__tests__/Progress.basic-rendering.test.tsx +10 -0
- package/src/Progress/__tests__/Progress.value.test.tsx +21 -0
- package/src/Progress/types.ts +20 -4
- package/src/Radio/Radio.tsx +9 -1
- package/src/Radio/__tests__/Radio.basic-rendering.test.tsx +14 -0
- package/src/Radio/hooks/useRadioRoot.ts +3 -0
- package/src/Radio/types.ts +20 -0
- package/src/RadioCard/RadioCard.tsx +11 -0
- package/src/RadioCard/__tests__/RadioCard.basic-rendering.test.tsx +8 -0
- package/src/RadioCard/__tests__/RadioCard.disabled-items.test.tsx +27 -0
- package/src/RadioCard/__tests__/RadioCard.keyboard-interaction.test.tsx +53 -0
- package/src/RadioCard/__tests__/RadioCard.ref-forwarding.test.tsx +54 -0
- package/src/RadioCard/__tests__/RadioCard.uncontrolled-state.test.tsx +21 -0
- package/src/RadioCard/hooks/useRadioCardRoot.ts +11 -0
- package/src/RadioCard/types.ts +95 -22
- package/src/RadioGroup/RadioGroup.tsx +11 -0
- package/src/RadioGroup/__tests__/RadioGroup.basic-rendering.test.tsx +8 -0
- package/src/RadioGroup/__tests__/RadioGroup.disabled.test.tsx +27 -0
- package/src/RadioGroup/__tests__/RadioGroup.ref-forwarding.test.tsx +29 -0
- package/src/RadioGroup/__tests__/RadioGroup.roving-tabindex.test.tsx +31 -0
- package/src/RadioGroup/hooks/useRadioGroupRoot.ts +11 -0
- package/src/RadioGroup/types.ts +73 -9
- package/src/SegmentedControl/README.md +154 -0
- package/src/SegmentedControl/SegmentedControl.tsx +309 -0
- package/src/SegmentedControl/SegmentedControlContext.ts +28 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.asChild.test.tsx +50 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.basic-rendering.test.tsx +100 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.controlled-state.test.tsx +107 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.disabled-keydown-guards.test.tsx +62 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.disabled.test.tsx +151 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.error-handling.test.tsx +17 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.keyboard-interaction.test.tsx +117 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.orientation.test.tsx +54 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.reading-direction.test.tsx +56 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.ref-forwarding.test.tsx +45 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.roving-tabindex.test.tsx +104 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.uncontrolled-state.test.tsx +94 -0
- package/src/SegmentedControl/hooks/index.ts +2 -0
- package/src/SegmentedControl/hooks/useSegmentedControlContext.ts +1 -0
- package/src/SegmentedControl/hooks/useSegmentedControlRoot.ts +86 -0
- package/src/SegmentedControl/index.ts +2 -0
- package/src/SegmentedControl/types.ts +138 -0
- package/src/Select/README.md +122 -157
- package/src/Select/Select.tsx +595 -68
- package/src/Select/SelectContext.ts +38 -0
- package/src/Select/SelectItemIndicatorContext.ts +29 -0
- package/src/Select/__tests__/Select.asChild.test.tsx +2 -2
- package/src/Select/__tests__/Select.basic-rendering.test.tsx +22 -3
- package/src/Select/__tests__/Select.controlled-state.test.tsx +6 -6
- package/src/Select/__tests__/Select.data-attributes.test.tsx +4 -4
- package/src/Select/__tests__/Select.field-integration.test.tsx +11 -11
- package/src/Select/__tests__/Select.form.test.tsx +38 -0
- package/src/Select/__tests__/Select.group.test.tsx +6 -6
- package/src/Select/__tests__/Select.keyboard-edge.test.tsx +95 -0
- package/src/Select/__tests__/Select.keyboard.test.tsx +124 -0
- package/src/Select/__tests__/Select.mutation-hardening.test.tsx +317 -0
- package/src/Select/__tests__/Select.native-item.test.tsx +30 -0
- package/src/Select/__tests__/Select.open-close.test.tsx +57 -0
- package/src/Select/__tests__/Select.placeholder.test.tsx +4 -4
- package/src/Select/__tests__/Select.rich-asChild.test.tsx +60 -0
- package/src/Select/__tests__/Select.rich-mode.test.tsx +16 -0
- package/src/Select/__tests__/Select.selection.test.tsx +104 -0
- package/src/Select/__tests__/Select.separator.test.tsx +71 -0
- package/src/Select/__tests__/Select.strict-context.test.tsx +36 -0
- package/src/Select/__tests__/Select.typeahead.test.tsx +156 -0
- package/src/Select/__tests__/Select.value-mirror.test.tsx +240 -0
- package/src/Select/constants.ts +8 -0
- package/src/Select/hooks/index.ts +2 -0
- package/src/Select/hooks/useSelectContent.ts +149 -0
- package/src/Select/hooks/useSelectRoot.ts +149 -0
- package/src/Select/types.ts +199 -31
- package/src/SkipNav/SkipNav.tsx +69 -29
- package/src/SkipNav/__tests__/SkipNav.basic-rendering.test.tsx +6 -0
- package/src/SkipNav/types.ts +17 -9
- package/src/Slider/Slider.tsx +33 -1
- package/src/Slider/SliderContext.ts +28 -1
- package/src/Slider/types.ts +157 -9
- package/src/Slot/Slot.tsx +94 -19
- package/src/Slot/__tests__/Slot.test.tsx +110 -0
- package/src/Slot/composeEventHandlers.ts +26 -2
- package/src/Slot/types.ts +16 -0
- package/src/Status/Status.tsx +36 -15
- package/src/Status/__tests__/Status.test.tsx +5 -0
- package/src/Status/types.ts +12 -1
- package/src/Switch/Switch.tsx +15 -2
- package/src/Switch/__tests__/Switch.basic-rendering.test.tsx +7 -0
- package/src/Switch/hooks/useSwitchRoot.ts +3 -0
- package/src/Switch/types.ts +49 -22
- package/src/Table/Table.tsx +44 -10
- package/src/Table/types.ts +66 -17
- package/src/Tabs/Tabs.tsx +8 -0
- package/src/Tabs/types.ts +29 -4
- package/src/Textarea/Textarea.tsx +9 -1
- package/src/Textarea/__tests__/Textarea.basic-rendering.test.tsx +5 -0
- package/src/Textarea/types.ts +20 -3
- package/src/Toggle/Toggle.tsx +51 -13
- package/src/Toggle/__tests__/Toggle.basic-rendering.test.tsx +5 -0
- package/src/Toggle/types.ts +54 -4
- package/src/ToggleGroup/ToggleGroup.tsx +14 -1
- package/src/ToggleGroup/__tests__/ToggleGroup.asChild.test.tsx +20 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.basic-rendering.test.tsx +9 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.dynamic-items.test.tsx +70 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.error-handling.test.tsx +23 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.keyboard-interaction.test.tsx +23 -1
- package/src/ToggleGroup/__tests__/ToggleGroup.ref-forwarding.test.tsx +58 -0
- package/src/ToggleGroup/hooks/useToggleGroupRoot.ts +21 -1
- package/src/ToggleGroup/types.ts +55 -2
- package/src/Tooltip/Tooltip.tsx +98 -20
- package/src/Tooltip/hooks/__tests__/useTooltipProvider.test.tsx +40 -0
- package/src/Tooltip/hooks/useTooltipProvider.ts +13 -1
- package/src/Tooltip/types.ts +89 -8
- package/src/Tree/Tree.tsx +187 -33
- package/src/Tree/types.ts +54 -12
- package/src/VisuallyHidden/VisuallyHidden.tsx +33 -8
- package/src/VisuallyHidden/__tests__/VisuallyHidden.test.tsx +15 -1
- package/src/VisuallyHidden/types.ts +11 -1
- package/src/index.ts +4 -0
- package/src/test/intersectionObserverPolyfill.ts +7 -0
- package/src/test/resizeObserverPolyfill.ts +80 -0
- package/src/utils/focusable.ts +15 -0
- package/src/utils/index.ts +1 -0
|
@@ -36,7 +36,7 @@ describe("Carousel scroll sync (user-driven via scrollsnapchange)", () => {
|
|
|
36
36
|
);
|
|
37
37
|
});
|
|
38
38
|
|
|
39
|
-
it("should derive the page index from
|
|
39
|
+
it("should derive the page index from the nearest window start when slidesPerPage > 1", () => {
|
|
40
40
|
render(
|
|
41
41
|
<Carousel.Root ariaLabel="Featured products" slidesPerPage={2}>
|
|
42
42
|
<Carousel.Viewport data-testid="viewport">
|
|
@@ -48,7 +48,7 @@ describe("Carousel scroll sync (user-driven via scrollsnapchange)", () => {
|
|
|
48
48
|
</Carousel.Root>,
|
|
49
49
|
);
|
|
50
50
|
|
|
51
|
-
// Snap target is slide 3
|
|
51
|
+
// Snap target is slide 3; the nearest window start (offsets 0, 2) is page 1.
|
|
52
52
|
fireScrollSnapChange(
|
|
53
53
|
screen.getByTestId("viewport"),
|
|
54
54
|
screen.getByTestId("slide-3"),
|
|
@@ -112,6 +112,31 @@ describe("Carousel scroll sync (user-driven via scrollsnapchange)", () => {
|
|
|
112
112
|
expect(onPageChange).not.toHaveBeenCalled();
|
|
113
113
|
});
|
|
114
114
|
|
|
115
|
+
it("should ignore a scrollsnapchange event that carries no snap target", () => {
|
|
116
|
+
const onPageChange = vi.fn();
|
|
117
|
+
render(
|
|
118
|
+
<Carousel.Root
|
|
119
|
+
ariaLabel="Featured products"
|
|
120
|
+
page={0}
|
|
121
|
+
onPageChange={onPageChange}
|
|
122
|
+
>
|
|
123
|
+
<Carousel.Viewport data-testid="viewport">
|
|
124
|
+
<Carousel.Slide data-testid="slide-0" />
|
|
125
|
+
<Carousel.Slide data-testid="slide-1" />
|
|
126
|
+
</Carousel.Viewport>
|
|
127
|
+
</Carousel.Root>,
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const viewport = screen.getByTestId("viewport");
|
|
131
|
+
// A scrollsnapchange with no snapTargetInline (undefined) must be a safe
|
|
132
|
+
// no-op — no page change, no crash.
|
|
133
|
+
act(() => {
|
|
134
|
+
viewport.dispatchEvent(new Event("scrollsnapchange", { bubbles: false }));
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
expect(onPageChange).not.toHaveBeenCalled();
|
|
138
|
+
});
|
|
139
|
+
|
|
115
140
|
it("should not invoke onPageChange when the snap target is on the already-active page, but should fire it when the page genuinely changes", () => {
|
|
116
141
|
const onPageChange = vi.fn();
|
|
117
142
|
function Parent() {
|
|
@@ -149,13 +174,12 @@ describe("Carousel scroll sync (user-driven via scrollsnapchange)", () => {
|
|
|
149
174
|
expect(onPageChange).toHaveBeenCalledWith(1);
|
|
150
175
|
});
|
|
151
176
|
|
|
152
|
-
it("should not scroll the
|
|
177
|
+
it("should not scroll the viewport again after a user-driven scrollsnapchange", () => {
|
|
153
178
|
// Regression: when the user swipes, the browser's CSS scroll-snap
|
|
154
179
|
// already positions the viewport. The scrollsnapchange event updates
|
|
155
180
|
// React state, which re-runs the scroll effect. If the effect calls
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
// changes.
|
|
181
|
+
// scrollTo here, two scroll animations fight and the result is janky.
|
|
182
|
+
// The scroll effect must be a no-op for user-initiated page changes.
|
|
159
183
|
render(
|
|
160
184
|
<Carousel.Root ariaLabel="Featured products">
|
|
161
185
|
<Carousel.Viewport data-testid="viewport">
|
|
@@ -167,13 +191,10 @@ describe("Carousel scroll sync (user-driven via scrollsnapchange)", () => {
|
|
|
167
191
|
);
|
|
168
192
|
|
|
169
193
|
const viewport = screen.getByTestId("viewport");
|
|
170
|
-
const
|
|
171
|
-
screen.getByTestId("slide-1"),
|
|
172
|
-
"scrollIntoView",
|
|
173
|
-
);
|
|
194
|
+
const scrollToSpy = vi.spyOn(viewport, "scrollTo");
|
|
174
195
|
|
|
175
196
|
fireScrollSnapChange(viewport, screen.getByTestId("slide-1"));
|
|
176
197
|
|
|
177
|
-
expect(
|
|
198
|
+
expect(scrollToSpy).not.toHaveBeenCalled();
|
|
178
199
|
});
|
|
179
200
|
});
|
|
@@ -4,7 +4,7 @@ import userEvent from "@testing-library/user-event";
|
|
|
4
4
|
import { Carousel } from "../index.ts";
|
|
5
5
|
|
|
6
6
|
describe("Carousel scroll sync (programmatic page change)", () => {
|
|
7
|
-
it("should
|
|
7
|
+
it("should scroll the viewport itself, not the slide's scrollable ancestors (so the page doesn't scroll)", async () => {
|
|
8
8
|
const user = userEvent.setup();
|
|
9
9
|
render(
|
|
10
10
|
<Carousel.Root ariaLabel="Featured products">
|
|
@@ -16,17 +16,41 @@ describe("Carousel scroll sync (programmatic page change)", () => {
|
|
|
16
16
|
</Carousel.Root>,
|
|
17
17
|
);
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
// scrollIntoView walks every scrollable ancestor (including the page/window),
|
|
20
|
+
// so it scrolls the whole document when a carousel is off-screen. The viewport
|
|
21
|
+
// must scroll itself instead.
|
|
22
|
+
const viewportScrollTo = vi.spyOn(screen.getByTestId("viewport"), "scrollTo");
|
|
23
|
+
const slideScrollIntoView = vi.spyOn(
|
|
20
24
|
screen.getByTestId("slide-1"),
|
|
21
25
|
"scrollIntoView",
|
|
22
26
|
);
|
|
23
27
|
|
|
24
28
|
await user.click(screen.getByRole("button", { name: "Next" }));
|
|
25
29
|
|
|
26
|
-
expect(
|
|
30
|
+
expect(viewportScrollTo).toHaveBeenCalled();
|
|
31
|
+
expect(slideScrollIntoView).not.toHaveBeenCalled();
|
|
27
32
|
});
|
|
28
33
|
|
|
29
|
-
it("should
|
|
34
|
+
it("should call scrollTo on the viewport for the first slide of the target page when the active page changes", async () => {
|
|
35
|
+
const user = userEvent.setup();
|
|
36
|
+
render(
|
|
37
|
+
<Carousel.Root ariaLabel="Featured products">
|
|
38
|
+
<Carousel.Viewport data-testid="viewport">
|
|
39
|
+
<Carousel.Slide data-testid="slide-0" />
|
|
40
|
+
<Carousel.Slide data-testid="slide-1" />
|
|
41
|
+
</Carousel.Viewport>
|
|
42
|
+
<Carousel.NextTrigger>Next</Carousel.NextTrigger>
|
|
43
|
+
</Carousel.Root>,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const scrollToSpy = vi.spyOn(screen.getByTestId("viewport"), "scrollTo");
|
|
47
|
+
|
|
48
|
+
await user.click(screen.getByRole("button", { name: "Next" }));
|
|
49
|
+
|
|
50
|
+
expect(scrollToSpy).toHaveBeenCalled();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("should centre the target slide in the viewport on the inline axis by default (center)", async () => {
|
|
30
54
|
const user = userEvent.setup();
|
|
31
55
|
render(
|
|
32
56
|
<Carousel.Root ariaLabel="Featured products">
|
|
@@ -43,24 +67,29 @@ describe("Carousel scroll sync (programmatic page change)", () => {
|
|
|
43
67
|
</Carousel.Root>,
|
|
44
68
|
);
|
|
45
69
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
70
|
+
// The viewport sits at the origin, is 1000px wide, and the target slide
|
|
71
|
+
// is 320px wide, 640px to its right: 640 - (1000 - 320) / 2 = 300.
|
|
72
|
+
const viewport = screen.getByTestId("viewport");
|
|
73
|
+
Object.defineProperty(viewport, "clientWidth", { value: 1000, configurable: true });
|
|
74
|
+
const scrollToSpy = vi.spyOn(viewport, "scrollTo");
|
|
75
|
+
vi.spyOn(screen.getByTestId("slide-2"), "getBoundingClientRect").mockReturnValue(
|
|
76
|
+
{ left: 640, top: 0, width: 320, height: 180 } as DOMRect,
|
|
49
77
|
);
|
|
50
78
|
|
|
51
79
|
await user.click(screen.getByRole("button", { name: "Slide 3" }));
|
|
52
80
|
|
|
53
|
-
expect(
|
|
54
|
-
behavior: "smooth",
|
|
55
|
-
inline: "start",
|
|
56
|
-
block: "nearest",
|
|
57
|
-
});
|
|
81
|
+
expect(scrollToSpy).toHaveBeenCalledWith({ left: 300, behavior: "smooth" });
|
|
58
82
|
});
|
|
59
83
|
|
|
60
84
|
it("should target the first slide of a multi-slide page", async () => {
|
|
61
85
|
const user = userEvent.setup();
|
|
62
86
|
render(
|
|
63
|
-
|
|
87
|
+
// snapAlign="start" isolates this test's concern (multi-slide page
|
|
88
|
+
// targeting: index = page * slidesPerPage) from the root's default
|
|
89
|
+
// alignment, which centre-aligns and would otherwise shift the
|
|
90
|
+
// expected scroll position for a reason unrelated to what this test
|
|
91
|
+
// is checking.
|
|
92
|
+
<Carousel.Root ariaLabel="Featured products" slidesPerPage={2} snapAlign="start">
|
|
64
93
|
<Carousel.Viewport data-testid="viewport">
|
|
65
94
|
<Carousel.Slide data-testid="slide-0" />
|
|
66
95
|
<Carousel.Slide data-testid="slide-1" />
|
|
@@ -72,18 +101,18 @@ describe("Carousel scroll sync (programmatic page change)", () => {
|
|
|
72
101
|
);
|
|
73
102
|
|
|
74
103
|
// Page 0 → page 1 means the first visible slide becomes slide 2
|
|
75
|
-
// (index = page * slidesPerPage).
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
104
|
+
// (index = page * slidesPerPage): its offset is what drives the scroll.
|
|
105
|
+
const scrollToSpy = vi.spyOn(screen.getByTestId("viewport"), "scrollTo");
|
|
106
|
+
vi.spyOn(screen.getByTestId("slide-2"), "getBoundingClientRect").mockReturnValue(
|
|
107
|
+
{ left: 500, top: 0, width: 240, height: 180 } as DOMRect,
|
|
79
108
|
);
|
|
80
109
|
|
|
81
110
|
await user.click(screen.getByRole("button", { name: "Next" }));
|
|
82
111
|
|
|
83
|
-
expect(
|
|
112
|
+
expect(scrollToSpy).toHaveBeenCalledWith({ left: 500, behavior: "smooth" });
|
|
84
113
|
});
|
|
85
114
|
|
|
86
|
-
it("should
|
|
115
|
+
it("should centre the target slide in the viewport when snapAlign is 'center'", async () => {
|
|
87
116
|
const user = userEvent.setup();
|
|
88
117
|
render(
|
|
89
118
|
<Carousel.Root ariaLabel="Featured products" snapAlign="center">
|
|
@@ -95,15 +124,199 @@ describe("Carousel scroll sync (programmatic page change)", () => {
|
|
|
95
124
|
</Carousel.Root>,
|
|
96
125
|
);
|
|
97
126
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
127
|
+
// A 400-wide slide at inline 400 in a 1000-wide viewport centres at
|
|
128
|
+
// 400 - (1000 - 400) / 2 = 100.
|
|
129
|
+
const viewport = screen.getByTestId("viewport");
|
|
130
|
+
Object.defineProperty(viewport, "clientWidth", { value: 1000, configurable: true });
|
|
131
|
+
const scrollToSpy = vi.spyOn(viewport, "scrollTo");
|
|
132
|
+
vi.spyOn(screen.getByTestId("slide-1"), "getBoundingClientRect").mockReturnValue(
|
|
133
|
+
{ left: 400, top: 0, width: 400, height: 180 } as DOMRect,
|
|
101
134
|
);
|
|
102
135
|
|
|
103
136
|
await user.click(screen.getByRole("button", { name: "Next" }));
|
|
104
137
|
|
|
105
|
-
expect(
|
|
106
|
-
|
|
138
|
+
expect(scrollToSpy).toHaveBeenCalledWith({ left: 100, behavior: "smooth" });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("should align the target slide's trailing edge to the viewport when snapAlign is 'end'", async () => {
|
|
142
|
+
const user = userEvent.setup();
|
|
143
|
+
render(
|
|
144
|
+
<Carousel.Root ariaLabel="Featured products" snapAlign="end">
|
|
145
|
+
<Carousel.Viewport data-testid="viewport">
|
|
146
|
+
<Carousel.Slide data-testid="slide-0" />
|
|
147
|
+
<Carousel.Slide data-testid="slide-1" />
|
|
148
|
+
</Carousel.Viewport>
|
|
149
|
+
<Carousel.NextTrigger>Next</Carousel.NextTrigger>
|
|
150
|
+
</Carousel.Root>,
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
// A 400-wide slide at inline 400 in a 1000-wide viewport end-aligns at
|
|
154
|
+
// 400 - (1000 - 400) = -200 (its trailing edge flush with the
|
|
155
|
+
// viewport's).
|
|
156
|
+
const viewport = screen.getByTestId("viewport");
|
|
157
|
+
Object.defineProperty(viewport, "clientWidth", { value: 1000, configurable: true });
|
|
158
|
+
const scrollToSpy = vi.spyOn(viewport, "scrollTo");
|
|
159
|
+
vi.spyOn(screen.getByTestId("slide-1"), "getBoundingClientRect").mockReturnValue(
|
|
160
|
+
{ left: 400, top: 0, width: 400, height: 180 } as DOMRect,
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
await user.click(screen.getByRole("button", { name: "Next" }));
|
|
164
|
+
|
|
165
|
+
expect(scrollToSpy).toHaveBeenCalledWith({ left: -200, behavior: "smooth" });
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("should let a slide's own snapAlign override the root default", async () => {
|
|
169
|
+
const user = userEvent.setup();
|
|
170
|
+
render(
|
|
171
|
+
<Carousel.Root ariaLabel="Featured products">
|
|
172
|
+
<Carousel.Viewport data-testid="viewport">
|
|
173
|
+
<Carousel.Slide data-testid="slide-0" />
|
|
174
|
+
<Carousel.Slide data-testid="slide-1" snapAlign="start" />
|
|
175
|
+
</Carousel.Viewport>
|
|
176
|
+
<Carousel.NextTrigger>Next</Carousel.NextTrigger>
|
|
177
|
+
</Carousel.Root>,
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
// Root default is "center", but slide-1 opts itself into "start":
|
|
181
|
+
// 400 - 0 = 400 — the alignOffset-zero case, driven by the per-slide
|
|
182
|
+
// prop instead of the root value.
|
|
183
|
+
const viewport = screen.getByTestId("viewport");
|
|
184
|
+
Object.defineProperty(viewport, "clientWidth", { value: 1000, configurable: true });
|
|
185
|
+
const scrollToSpy = vi.spyOn(viewport, "scrollTo");
|
|
186
|
+
vi.spyOn(screen.getByTestId("slide-1"), "getBoundingClientRect").mockReturnValue(
|
|
187
|
+
{ left: 400, top: 0, width: 400, height: 180 } as DOMRect,
|
|
107
188
|
);
|
|
189
|
+
|
|
190
|
+
await user.click(screen.getByRole("button", { name: "Next" }));
|
|
191
|
+
|
|
192
|
+
expect(scrollToSpy).toHaveBeenCalledWith({ left: 400, behavior: "smooth" });
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe("Carousel multi-slide page-span alignment", () => {
|
|
197
|
+
it("should centre the whole multi-slide page, not just its leading slide", async () => {
|
|
198
|
+
const user = userEvent.setup();
|
|
199
|
+
render(
|
|
200
|
+
<Carousel.Root
|
|
201
|
+
ariaLabel="Featured products"
|
|
202
|
+
slidesPerPage={2}
|
|
203
|
+
snapAlign="center"
|
|
204
|
+
>
|
|
205
|
+
<Carousel.Viewport data-testid="viewport">
|
|
206
|
+
<Carousel.Slide data-testid="slide-0" />
|
|
207
|
+
<Carousel.Slide data-testid="slide-1" />
|
|
208
|
+
<Carousel.Slide data-testid="slide-2" />
|
|
209
|
+
<Carousel.Slide data-testid="slide-3" />
|
|
210
|
+
</Carousel.Viewport>
|
|
211
|
+
<Carousel.NextTrigger>Next</Carousel.NextTrigger>
|
|
212
|
+
</Carousel.Root>,
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
// Page 1's window is [slide-2, slide-3]. The page spans 500 (slide-2's
|
|
216
|
+
// left) to 980 (slide-3's right) = 480 wide, in a 1000-wide viewport:
|
|
217
|
+
// 500 - (1000 - 480) / 2 = 500 - 260 = 240. Using only slide-2's own
|
|
218
|
+
// 240px width (the pre-fix bug) would instead leftover-space against a
|
|
219
|
+
// single slide and undershoot, leaving slide-3 clipped off the trailing
|
|
220
|
+
// edge.
|
|
221
|
+
const viewport = screen.getByTestId("viewport");
|
|
222
|
+
Object.defineProperty(viewport, "clientWidth", {
|
|
223
|
+
value: 1000,
|
|
224
|
+
configurable: true,
|
|
225
|
+
});
|
|
226
|
+
const scrollToSpy = vi.spyOn(viewport, "scrollTo");
|
|
227
|
+
vi.spyOn(
|
|
228
|
+
screen.getByTestId("slide-2"),
|
|
229
|
+
"getBoundingClientRect",
|
|
230
|
+
).mockReturnValue({ left: 500, top: 0, width: 240, height: 180 } as DOMRect);
|
|
231
|
+
vi.spyOn(
|
|
232
|
+
screen.getByTestId("slide-3"),
|
|
233
|
+
"getBoundingClientRect",
|
|
234
|
+
).mockReturnValue({ left: 740, top: 0, width: 240, height: 180 } as DOMRect);
|
|
235
|
+
|
|
236
|
+
await user.click(screen.getByRole("button", { name: "Next" }));
|
|
237
|
+
|
|
238
|
+
expect(scrollToSpy).toHaveBeenCalledWith({ left: 240, behavior: "smooth" });
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("should align the whole multi-slide page's trailing edge when snapAlign is 'end'", async () => {
|
|
242
|
+
const user = userEvent.setup();
|
|
243
|
+
render(
|
|
244
|
+
<Carousel.Root
|
|
245
|
+
ariaLabel="Featured products"
|
|
246
|
+
slidesPerPage={2}
|
|
247
|
+
snapAlign="end"
|
|
248
|
+
>
|
|
249
|
+
<Carousel.Viewport data-testid="viewport">
|
|
250
|
+
<Carousel.Slide data-testid="slide-0" />
|
|
251
|
+
<Carousel.Slide data-testid="slide-1" />
|
|
252
|
+
<Carousel.Slide data-testid="slide-2" />
|
|
253
|
+
<Carousel.Slide data-testid="slide-3" />
|
|
254
|
+
</Carousel.Viewport>
|
|
255
|
+
<Carousel.NextTrigger>Next</Carousel.NextTrigger>
|
|
256
|
+
</Carousel.Root>,
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
// Same 480-wide page as above; end-alignment uses the full leftover
|
|
260
|
+
// space (520) rather than halving it: 500 - 520 = -20.
|
|
261
|
+
const viewport = screen.getByTestId("viewport");
|
|
262
|
+
Object.defineProperty(viewport, "clientWidth", {
|
|
263
|
+
value: 1000,
|
|
264
|
+
configurable: true,
|
|
265
|
+
});
|
|
266
|
+
const scrollToSpy = vi.spyOn(viewport, "scrollTo");
|
|
267
|
+
vi.spyOn(
|
|
268
|
+
screen.getByTestId("slide-2"),
|
|
269
|
+
"getBoundingClientRect",
|
|
270
|
+
).mockReturnValue({ left: 500, top: 0, width: 240, height: 180 } as DOMRect);
|
|
271
|
+
vi.spyOn(
|
|
272
|
+
screen.getByTestId("slide-3"),
|
|
273
|
+
"getBoundingClientRect",
|
|
274
|
+
).mockReturnValue({ left: 740, top: 0, width: 240, height: 180 } as DOMRect);
|
|
275
|
+
|
|
276
|
+
await user.click(screen.getByRole("button", { name: "Next" }));
|
|
277
|
+
|
|
278
|
+
expect(scrollToSpy).toHaveBeenCalledWith({ left: -20, behavior: "smooth" });
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("should clamp the page span to the actual last slide when slidesPerPage exceeds the slide count", () => {
|
|
282
|
+
// This exercises the *initial-mount* scroll (there's only one page here,
|
|
283
|
+
// so nothing else would trigger a second effect run) — the rect mocks
|
|
284
|
+
// and the scrollTo spy both have to be in place before that first effect
|
|
285
|
+
// fires, so they're attached to the shared prototypes rather than to
|
|
286
|
+
// specific instances found only after render().
|
|
287
|
+
const scrollToSpy = vi
|
|
288
|
+
.spyOn(HTMLElement.prototype, "scrollTo")
|
|
289
|
+
.mockImplementation(() => {});
|
|
290
|
+
const rectsByTestId: Record<string, Partial<DOMRect>> = {
|
|
291
|
+
"slide-0": { left: 0, top: 0, width: 400, height: 180 },
|
|
292
|
+
"slide-1": { left: 400, top: 0, width: 400, height: 180 },
|
|
293
|
+
};
|
|
294
|
+
vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockImplementation(
|
|
295
|
+
function (this: HTMLElement) {
|
|
296
|
+
const rect = rectsByTestId[this.dataset.testid ?? ""];
|
|
297
|
+
return { left: 0, top: 0, width: 0, height: 0, ...rect } as DOMRect;
|
|
298
|
+
},
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
render(
|
|
302
|
+
<Carousel.Root
|
|
303
|
+
ariaLabel="Featured products"
|
|
304
|
+
slidesPerPage={4}
|
|
305
|
+
snapAlign="center"
|
|
306
|
+
>
|
|
307
|
+
<Carousel.Viewport data-testid="viewport">
|
|
308
|
+
<Carousel.Slide data-testid="slide-0" />
|
|
309
|
+
<Carousel.Slide data-testid="slide-1" />
|
|
310
|
+
</Carousel.Viewport>
|
|
311
|
+
</Carousel.Root>,
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
// Only 2 slides exist for a configured slidesPerPage=4 — the naive last-
|
|
315
|
+
// member index (0 + 4 - 1 = 3) doesn't exist, so it must clamp to the
|
|
316
|
+
// real last slide (index 1) instead of looking up a nonexistent one
|
|
317
|
+
// (which would throw). Page span: slide-0's left (0) to slide-1's right
|
|
318
|
+
// (800) = 800 wide; the viewport's own clientWidth is unmocked (jsdom
|
|
319
|
+
// default 0): 0 - (0 - 800) / 2 = 400.
|
|
320
|
+
expect(scrollToSpy).toHaveBeenCalledWith({ left: 400, behavior: "smooth" });
|
|
108
321
|
});
|
|
109
322
|
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { createRef, useRef } 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 scrollToIndex", () => {
|
|
8
|
+
it("should jump directly to the page containing the given slide index", () => {
|
|
9
|
+
const ref = createRef<CarouselImperativeApi>();
|
|
10
|
+
render(
|
|
11
|
+
<Carousel.Root ref={ref} ariaLabel="Featured products">
|
|
12
|
+
<Carousel.Viewport>
|
|
13
|
+
<Carousel.Slide data-testid="slide-0" />
|
|
14
|
+
<Carousel.Slide data-testid="slide-1" />
|
|
15
|
+
<Carousel.Slide data-testid="slide-2" />
|
|
16
|
+
</Carousel.Viewport>
|
|
17
|
+
</Carousel.Root>,
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
act(() => {
|
|
21
|
+
ref.current!.scrollToIndex(2);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
expect(screen.getByTestId("slide-2")).toHaveAttribute(
|
|
25
|
+
"data-state",
|
|
26
|
+
"active",
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("should map a slide index to its containing multi-slide page, not just its own page", () => {
|
|
31
|
+
const ref = createRef<CarouselImperativeApi>();
|
|
32
|
+
render(
|
|
33
|
+
<Carousel.Root ref={ref} ariaLabel="Featured products" slidesPerPage={3}>
|
|
34
|
+
<Carousel.Viewport>
|
|
35
|
+
{Array.from({ length: 7 }, (_, index) => (
|
|
36
|
+
<Carousel.Slide key={index} data-testid={`slide-${index}`} />
|
|
37
|
+
))}
|
|
38
|
+
</Carousel.Viewport>
|
|
39
|
+
</Carousel.Root>,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// 7 slides / perPage 3 → pages [0,1,2] [3,4,5] [4,5,6] (the end-aligned
|
|
43
|
+
// last page). Slide index 6 belongs to the last page, whose leading
|
|
44
|
+
// slide is 4 — scrollToIndex(6) should land on that page, not treat 6
|
|
45
|
+
// as its own page start.
|
|
46
|
+
act(() => {
|
|
47
|
+
ref.current!.scrollToIndex(6);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(screen.getByTestId("slide-4")).toHaveAttribute(
|
|
51
|
+
"data-state",
|
|
52
|
+
"active",
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("should scroll with behavior='instant' when scrollToIndex(index, true) is called", () => {
|
|
57
|
+
const ref = createRef<CarouselImperativeApi>();
|
|
58
|
+
render(
|
|
59
|
+
<Carousel.Root ref={ref} ariaLabel="Featured products">
|
|
60
|
+
<Carousel.Viewport data-testid="viewport">
|
|
61
|
+
<Carousel.Slide data-testid="slide-0" />
|
|
62
|
+
<Carousel.Slide data-testid="slide-1" />
|
|
63
|
+
</Carousel.Viewport>
|
|
64
|
+
</Carousel.Root>,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const scrollToSpy = vi.spyOn(screen.getByTestId("viewport"), "scrollTo");
|
|
68
|
+
|
|
69
|
+
act(() => {
|
|
70
|
+
ref.current!.scrollToIndex(1, true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(scrollToSpy).toHaveBeenCalledWith(
|
|
74
|
+
expect.objectContaining({ behavior: "instant" }),
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should route through onPageChange in controlled mode, like goTo", () => {
|
|
79
|
+
const onPageChange = vi.fn();
|
|
80
|
+
function Parent() {
|
|
81
|
+
const ref = useRef<CarouselImperativeApi>(null);
|
|
82
|
+
return (
|
|
83
|
+
<>
|
|
84
|
+
<button
|
|
85
|
+
type="button"
|
|
86
|
+
onClick={() => ref.current?.scrollToIndex(2)}
|
|
87
|
+
data-testid="external-jump"
|
|
88
|
+
>
|
|
89
|
+
Jump
|
|
90
|
+
</button>
|
|
91
|
+
<Carousel.Root
|
|
92
|
+
ref={ref}
|
|
93
|
+
ariaLabel="Featured products"
|
|
94
|
+
page={0}
|
|
95
|
+
onPageChange={onPageChange}
|
|
96
|
+
>
|
|
97
|
+
<Carousel.Viewport>
|
|
98
|
+
<Carousel.Slide data-testid="slide-0" />
|
|
99
|
+
<Carousel.Slide data-testid="slide-1" />
|
|
100
|
+
<Carousel.Slide data-testid="slide-2" />
|
|
101
|
+
</Carousel.Viewport>
|
|
102
|
+
</Carousel.Root>
|
|
103
|
+
</>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
render(<Parent />);
|
|
108
|
+
|
|
109
|
+
act(() => {
|
|
110
|
+
screen.getByTestId("external-jump").click();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
expect(onPageChange).toHaveBeenCalledWith(2);
|
|
114
|
+
});
|
|
115
|
+
});
|