@primitiv-ui/react 0.1.28 → 0.1.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -2
- 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 +32 -11
- package/src/Accordion/README.md +2 -2
- 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 +139 -21
- 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/__tests__/Button.contract.test.tsx +1 -1
- package/src/Button/types.ts +21 -0
- package/src/Carousel/Carousel.tsx +268 -20
- package/src/Carousel/README.md +764 -87
- 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 +90 -2
- 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 +458 -36
- package/src/Checkbox/Checkbox.tsx +11 -3
- 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 +14 -5
- package/src/CheckboxCard/README.md +1 -1
- 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 +41 -1
- 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/Combobox/Combobox.tsx +508 -0
- package/src/Combobox/ComboboxContext.ts +23 -0
- package/src/Combobox/README.md +170 -0
- package/src/Combobox/__tests__/Combobox.asChild.test.tsx +62 -0
- package/src/Combobox/__tests__/Combobox.basic-rendering.test.tsx +16 -0
- package/src/Combobox/__tests__/Combobox.content.test.tsx +36 -0
- package/src/Combobox/__tests__/Combobox.controlled-state.test.tsx +88 -0
- package/src/Combobox/__tests__/Combobox.empty.test.tsx +42 -0
- package/src/Combobox/__tests__/Combobox.ids.test.tsx +66 -0
- package/src/Combobox/__tests__/Combobox.item.test.tsx +76 -0
- package/src/Combobox/__tests__/Combobox.keyboard-interaction.test.tsx +196 -0
- package/src/Combobox/__tests__/Combobox.light-dismiss.test.tsx +101 -0
- package/src/Combobox/__tests__/Combobox.query.test.tsx +105 -0
- package/src/Combobox/hooks/index.ts +1 -0
- package/src/Combobox/hooks/useComboboxRoot.ts +2 -0
- package/src/Combobox/index.ts +3 -0
- package/src/Combobox/types.ts +133 -0
- package/src/ContextMenu/ContextMenu.tsx +270 -4
- package/src/ContextMenu/README.md +1 -1
- package/src/ContextMenu/types.ts +148 -6
- package/src/DirectionProvider/DirectionProvider.tsx +41 -9
- package/src/DirectionProvider/README.md +4 -4
- 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.contract.test.tsx +1 -1
- 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 +119 -2
- package/src/Dropdown/README.md +7 -5
- package/src/Dropdown/types.ts +241 -11
- package/src/EmptyState/EmptyState.tsx +74 -4
- package/src/EmptyState/README.md +2 -2
- 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/README.md +1 -1
- 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/hooks/useFieldProps.ts +1 -1
- package/src/Field/types.ts +60 -12
- package/src/Fieldset/Fieldset.tsx +55 -19
- package/src/Fieldset/README.md +2 -2
- package/src/Fieldset/__tests__/Fieldset.basic-rendering.test.tsx +9 -0
- package/src/Fieldset/types.ts +20 -2
- package/src/Input/Input.tsx +15 -10
- package/src/Input/README.md +2 -2
- 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/Listbox/Listbox.tsx +475 -0
- package/src/Listbox/ListboxContext.ts +48 -0
- package/src/Listbox/README.md +248 -0
- package/src/Listbox/__tests__/Listbox.active-descendant.test.tsx +128 -0
- package/src/Listbox/__tests__/Listbox.asChild.test.tsx +91 -0
- package/src/Listbox/__tests__/Listbox.basic-rendering.test.tsx +16 -0
- package/src/Listbox/__tests__/Listbox.controlled-state.test.tsx +114 -0
- package/src/Listbox/__tests__/Listbox.disabled.test.tsx +90 -0
- package/src/Listbox/__tests__/Listbox.fixtures.ts +27 -0
- package/src/Listbox/__tests__/Listbox.group-label.test.tsx +97 -0
- package/src/Listbox/__tests__/Listbox.group.test.tsx +45 -0
- package/src/Listbox/__tests__/Listbox.keyboard-edge-cases.test.tsx +144 -0
- package/src/Listbox/__tests__/Listbox.keyboard-interaction.test.tsx +112 -0
- package/src/Listbox/__tests__/Listbox.modifier-keys.test.tsx +86 -0
- package/src/Listbox/__tests__/Listbox.multi-select-keyboard.test.tsx +368 -0
- package/src/Listbox/__tests__/Listbox.multiple-mode.test.tsx +91 -0
- package/src/Listbox/__tests__/Listbox.mutation-hardening.test.tsx +480 -0
- package/src/Listbox/__tests__/Listbox.orientation.test.tsx +59 -0
- package/src/Listbox/__tests__/Listbox.reading-direction.test.tsx +65 -0
- package/src/Listbox/__tests__/Listbox.reorder.test.tsx +78 -0
- package/src/Listbox/__tests__/Listbox.selection-follows-focus.test.tsx +64 -0
- package/src/Listbox/__tests__/Listbox.strict-context.test.tsx +21 -0
- package/src/Listbox/__tests__/Listbox.typeahead.test.tsx +123 -0
- package/src/Listbox/__tests__/Listbox.uncontrolled-state.test.tsx +48 -0
- package/src/Listbox/constants.ts +2 -0
- package/src/Listbox/hooks/index.ts +1 -0
- package/src/Listbox/hooks/useListboxRoot.ts +407 -0
- package/src/Listbox/index.ts +3 -0
- package/src/Listbox/types.ts +238 -0
- package/src/MillerColumns/MillerColumns.tsx +186 -45
- package/src/MillerColumns/README.md +90 -20
- package/src/MillerColumns/__tests__/MillerColumns.aria.test.tsx +45 -0
- package/src/MillerColumns/__tests__/MillerColumns.auto-scroll.test.tsx +4 -2
- package/src/MillerColumns/__tests__/MillerColumns.basic-rendering.test.tsx +18 -3
- package/src/MillerColumns/__tests__/MillerColumns.data-attributes.test.tsx +48 -0
- package/src/MillerColumns/__tests__/MillerColumns.keyboard-interaction.test.tsx +35 -2
- package/src/MillerColumns/__tests__/MillerColumns.keyboard-selection-state.test.tsx +96 -0
- package/src/MillerColumns/__tests__/MillerColumns.preview-panel.test.tsx +29 -4
- package/src/MillerColumns/__tests__/MillerColumns.preview-visibility.test.tsx +79 -0
- package/src/MillerColumns/__tests__/MillerColumns.resize.test.tsx +189 -0
- package/src/MillerColumns/__tests__/MillerColumns.styled-parts.test.tsx +143 -0
- package/src/MillerColumns/__tests__/MillerColumns.typeahead.test.tsx +104 -0
- package/src/MillerColumns/constants.ts +2 -0
- package/src/MillerColumns/hooks/useMillerColumnsColumn.ts +5 -2
- package/src/MillerColumns/hooks/useMillerColumnsItem.ts +29 -7
- package/src/MillerColumns/hooks/useMillerColumnsResizeHandle.ts +80 -10
- package/src/MillerColumns/hooks/useMillerColumnsRoot.ts +0 -0
- package/src/MillerColumns/index.ts +4 -0
- package/src/MillerColumns/types.ts +136 -30
- package/src/MillerColumns/utils.ts +69 -3
- package/src/Modal/Modal.tsx +35 -8
- package/src/Modal/README.md +24 -15
- 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/README.md +2 -2
- 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/README.md +1 -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 +10 -2
- 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/README.md +2 -2
- package/src/RadioGroup/RadioGroup.tsx +12 -1
- 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 +87 -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 +596 -69
- 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 +7 -7
- 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/README.md +3 -3
- package/src/SkipNav/SkipNav.tsx +72 -32
- package/src/SkipNav/__tests__/SkipNav.basic-rendering.test.tsx +6 -0
- package/src/SkipNav/types.ts +17 -9
- package/src/Slider/README.md +37 -7
- package/src/Slider/Slider.tsx +42 -4
- 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 +112 -2
- package/src/Slot/composeEventHandlers.ts +26 -2
- package/src/Slot/types.ts +16 -0
- package/src/SplitButton/README.md +225 -0
- package/src/SplitButton/SplitButton.tsx +457 -0
- package/src/SplitButton/SplitButtonContext.ts +26 -0
- package/src/SplitButton/__tests__/SplitButton.accessibility.test.tsx +55 -0
- package/src/SplitButton/__tests__/SplitButton.asChild.test.tsx +51 -0
- package/src/SplitButton/__tests__/SplitButton.basic-rendering.test.tsx +57 -0
- package/src/SplitButton/__tests__/SplitButton.data-attributes.test.tsx +28 -0
- package/src/SplitButton/__tests__/SplitButton.disabled.test.tsx +79 -0
- package/src/SplitButton/__tests__/SplitButton.error-handling.test.tsx +29 -0
- package/src/SplitButton/__tests__/SplitButton.keyboard-interaction.test.tsx +56 -0
- package/src/SplitButton/__tests__/SplitButton.state-modes.test.tsx +68 -0
- package/src/SplitButton/index.ts +2 -0
- package/src/SplitButton/types.ts +186 -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/README.md +1 -1
- package/src/Switch/Switch.tsx +17 -4
- 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/README.md +110 -8
- package/src/Table/Table.tsx +246 -16
- package/src/Table/TableExpandableContext.ts +7 -0
- package/src/Table/__tests__/Table.DetailRow.test.tsx +83 -0
- package/src/Table/__tests__/Table.ExpandTrigger.test.tsx +102 -0
- package/src/Table/__tests__/Table.Expandable.test.tsx +121 -0
- package/src/Table/types.ts +191 -17
- package/src/Tabs/README.md +4 -4
- package/src/Tabs/Tabs.tsx +13 -4
- package/src/Tabs/__tests__/Tabs.activation-mode.test.tsx +58 -0
- package/src/Tabs/__tests__/Tabs.basic-rendering.test.tsx +15 -0
- package/src/Tabs/__tests__/Tabs.change-event-callbacks.test.tsx +24 -0
- package/src/Tabs/__tests__/Tabs.disabled-tabs.test.tsx +36 -0
- package/src/Tabs/__tests__/Tabs.lazy-mount.test.tsx +17 -0
- package/src/Tabs/hooks/useTabsContent.ts +5 -0
- package/src/Tabs/hooks/useTabsRoot.ts +29 -5
- package/src/Tabs/hooks/useTabsTrigger.ts +7 -2
- 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/README.md +4 -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/README.md +7 -7
- package/src/Tooltip/Tooltip.tsx +100 -22
- package/src/Tooltip/__tests__/Tooltip.basic-rendering.test.tsx +50 -0
- package/src/Tooltip/__tests__/Tooltip.error-handling.test.tsx +37 -0
- package/src/Tooltip/__tests__/Tooltip.escape-hatches.test.tsx +73 -0
- package/src/Tooltip/__tests__/Tooltip.keyboard-interaction.test.tsx +134 -0
- package/src/Tooltip/__tests__/Tooltip.skip-delay.test.tsx +216 -0
- package/src/Tooltip/__tests__/Tooltip.timer-coordination.test.tsx +223 -0
- package/src/Tooltip/hooks/__tests__/useTooltipProvider.test.tsx +40 -0
- package/src/Tooltip/hooks/useTooltipContent.ts +3 -0
- package/src/Tooltip/hooks/useTooltipProvider.ts +46 -8
- package/src/Tooltip/hooks/useTooltipRoot.ts +30 -6
- package/src/Tooltip/types.ts +89 -8
- package/src/Tree/README.md +69 -5
- package/src/Tree/Tree.tsx +188 -34
- package/src/Tree/__tests__/Tree.styled-wrapper.test.tsx +159 -0
- package/src/Tree/hooks/useTreePath.ts +33 -1
- package/src/Tree/index.ts +6 -1
- package/src/Tree/types.ts +54 -12
- package/src/Tree/utils.ts +46 -4
- 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/hooks/index.ts +1 -0
- package/src/hooks/useTypeahead.ts +95 -0
- package/src/index.ts +10 -0
- package/src/test/intersectionObserverPolyfill.ts +7 -0
- package/src/test/resizeObserverPolyfill.ts +80 -0
- package/src/useLocalStorage/__tests__/useLocalStorage.reading.test.tsx +31 -0
- package/src/useLocalStorage/__tests__/useLocalStorage.resilience.test.tsx +97 -0
- package/src/useLocalStorage/__tests__/useLocalStorage.ssr.test.tsx +20 -0
- package/src/useLocalStorage/__tests__/useLocalStorage.writing.test.tsx +72 -0
- package/src/useLocalStorage/index.ts +1 -0
- package/src/useLocalStorage/useLocalStorage.ts +140 -0
- package/src/useMediaQuery/README.md +66 -0
- package/src/useMediaQuery/__tests__/useMediaQuery.matching.test.tsx +42 -0
- package/src/useMediaQuery/__tests__/useMediaQuery.ssr.test.tsx +19 -0
- package/src/useMediaQuery/__tests__/useMediaQuery.subscription.test.tsx +89 -0
- package/src/useMediaQuery/index.ts +1 -0
- package/src/useMediaQuery/useMediaQuery.ts +49 -0
- package/src/usePagination/README.md +170 -0
- package/src/usePagination/__tests__/usePagination.navigation.test.tsx +109 -0
- package/src/usePagination/__tests__/usePagination.page-count.test.tsx +56 -0
- package/src/usePagination/__tests__/usePagination.page-state.test.tsx +87 -0
- package/src/usePagination/__tests__/usePagination.range.test.tsx +82 -0
- package/src/usePagination/__tests__/usePagination.slice.test.tsx +59 -0
- package/src/usePagination/__tests__/usePagination.windowing.test.tsx +68 -0
- package/src/usePagination/index.ts +1 -0
- package/src/usePagination/usePagination.ts +292 -0
- package/src/utils/focusable.ts +15 -0
- package/src/utils/getKeyToActionMap.ts +1 -1
- package/src/utils/index.ts +1 -0
|
@@ -12,10 +12,16 @@ import {
|
|
|
12
12
|
|
|
13
13
|
import type {
|
|
14
14
|
CarouselAutoplay,
|
|
15
|
+
CarouselAutoplayStatus,
|
|
15
16
|
CarouselContextValue,
|
|
17
|
+
CarouselDragStatus,
|
|
16
18
|
CarouselIds,
|
|
17
19
|
CarouselImperativeApi,
|
|
20
|
+
CarouselLoopMode,
|
|
21
|
+
CarouselOrientation,
|
|
22
|
+
CarouselOverscrollStatus,
|
|
18
23
|
CarouselSnapAlign,
|
|
24
|
+
CarouselSnapType,
|
|
19
25
|
CarouselTransition,
|
|
20
26
|
CarouselTranslations,
|
|
21
27
|
} from "../types";
|
|
@@ -29,8 +35,20 @@ const DEFAULT_TRANSLATIONS: Required<CarouselTranslations> = {
|
|
|
29
35
|
indicatorLabel: ({ index }) => `Slide ${index}`,
|
|
30
36
|
startSlideshow: "Start automatic slide show",
|
|
31
37
|
stopSlideshow: "Stop automatic slide show",
|
|
38
|
+
progressText: ({ page, totalPages }) => `${page + 1} of ${totalPages}`,
|
|
32
39
|
};
|
|
33
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Coerce a consumer-supplied layout count to a safe integer ≥ `min`. The
|
|
43
|
+
* slide count and `slidesPerPage` / `slidesPerMove` come from the consumer,
|
|
44
|
+
* so 0, negative, fractional, or non-finite values must degrade to a sane
|
|
45
|
+
* integer — otherwise the page maths divides by zero (Infinity pages → a
|
|
46
|
+
* thrown RangeError from the indicator map) or the carousel goes inert.
|
|
47
|
+
*/
|
|
48
|
+
function clampCount(n: number, min: number): number {
|
|
49
|
+
return Number.isFinite(n) ? Math.max(min, Math.floor(n)) : min;
|
|
50
|
+
}
|
|
51
|
+
|
|
34
52
|
function resolveAutoplay(autoplay: CarouselAutoplay | undefined): {
|
|
35
53
|
enabled: boolean;
|
|
36
54
|
delay: number;
|
|
@@ -61,6 +79,9 @@ type UseCarouselRootProps = {
|
|
|
61
79
|
onPlayingChange?: (playing: boolean) => void;
|
|
62
80
|
/** Autoplay configuration — see {@link CarouselAutoplay}. */
|
|
63
81
|
autoplay?: CarouselAutoplay;
|
|
82
|
+
/** Fires on every autoplay status transition — see
|
|
83
|
+
* {@link CarouselAutoplayStatus}. */
|
|
84
|
+
onAutoplayStatusChange?: (status: CarouselAutoplayStatus) => void;
|
|
64
85
|
/** Number of slides visible per page. Defaults to `1`. */
|
|
65
86
|
slidesPerPage?: number;
|
|
66
87
|
/** Slides advanced per Prev/Next click — `"auto"` (default) is
|
|
@@ -76,6 +97,26 @@ type UseCarouselRootProps = {
|
|
|
76
97
|
transition?: CarouselTransition;
|
|
77
98
|
/** Scroll-snap alignment — see {@link CarouselSnapAlign}. */
|
|
78
99
|
snapAlign?: CarouselSnapAlign;
|
|
100
|
+
/** Scroll/pagination axis — see {@link CarouselOrientation}. */
|
|
101
|
+
orientation?: CarouselOrientation;
|
|
102
|
+
/** Wrap navigation around the ends instead of clamping — `boolean` for
|
|
103
|
+
* ergonomics (`true` = `"wrap"`) or a named mode. Defaults to `false`. */
|
|
104
|
+
loop?: boolean | "wrap" | "infinite";
|
|
105
|
+
/** Opt-in mouse click-and-drag scrolling. */
|
|
106
|
+
allowMouseDrag?: boolean;
|
|
107
|
+
/** Fires on every mouse-drag status transition — see
|
|
108
|
+
* {@link CarouselDragStatus}. */
|
|
109
|
+
onDragStatusChange?: (status: CarouselDragStatus) => void;
|
|
110
|
+
/** Fires whenever the keyboard, wheel, or a mouse drag pushes against a
|
|
111
|
+
* boundary with nowhere further to go — see
|
|
112
|
+
* {@link CarouselOverscrollStatus}. */
|
|
113
|
+
onOverscrollStatusChange?: (status: CarouselOverscrollStatus) => void;
|
|
114
|
+
/** IntersectionObserver visibility threshold(s) for the `isInView`
|
|
115
|
+
* fallback. Defaults to `0.6`. */
|
|
116
|
+
inViewThreshold?: number | number[];
|
|
117
|
+
/** `scroll-snap-type` strictness — see {@link CarouselSnapType}.
|
|
118
|
+
* Defaults to `"mandatory"`. */
|
|
119
|
+
snapType?: CarouselSnapType;
|
|
79
120
|
};
|
|
80
121
|
|
|
81
122
|
/**
|
|
@@ -111,12 +152,20 @@ export function useCarouselRoot(
|
|
|
111
152
|
playing,
|
|
112
153
|
onPlayingChange,
|
|
113
154
|
autoplay,
|
|
155
|
+
onAutoplayStatusChange,
|
|
114
156
|
slidesPerPage = 1,
|
|
115
157
|
slidesPerMove = "auto",
|
|
116
158
|
translations,
|
|
117
159
|
ids = EMPTY_IDS,
|
|
118
160
|
transition = "slide",
|
|
119
|
-
snapAlign = "
|
|
161
|
+
snapAlign = "center",
|
|
162
|
+
orientation = "horizontal",
|
|
163
|
+
loop = false,
|
|
164
|
+
allowMouseDrag = false,
|
|
165
|
+
onDragStatusChange,
|
|
166
|
+
onOverscrollStatusChange,
|
|
167
|
+
inViewThreshold = 0.6,
|
|
168
|
+
snapType = "mandatory",
|
|
120
169
|
}: UseCarouselRootProps = {},
|
|
121
170
|
imperativeRef?: Ref<CarouselImperativeApi>,
|
|
122
171
|
) {
|
|
@@ -128,21 +177,41 @@ export function useCarouselRoot(
|
|
|
128
177
|
const isControlled = page !== undefined;
|
|
129
178
|
const rawPage = isControlled ? (page as number) : internalPage;
|
|
130
179
|
const total = slideKeys.length;
|
|
180
|
+
// Resolve the ergonomic boolean/named `loop` prop to a single mode.
|
|
181
|
+
// `true` is sugar for `"wrap"`; `false`/omitted is `"none"`. `"wrap"` and
|
|
182
|
+
// `"infinite"` share the page model below (`looping`), differing only in
|
|
183
|
+
// the scroll/clone layer the styled surface keys off `data-loop`.
|
|
184
|
+
const loopMode: CarouselLoopMode =
|
|
185
|
+
loop === true ? "wrap" : loop === false ? "none" : loop;
|
|
186
|
+
const looping = loopMode !== "none";
|
|
187
|
+
// Guard the layout counts (see clampCount). perPage is an integer ≥ 1; a
|
|
188
|
+
// numeric slidesPerMove is additionally clamped to ≤ perPage so a move can
|
|
189
|
+
// never skip past a page and orphan the slides in the gap. "auto" moves a
|
|
190
|
+
// full page.
|
|
191
|
+
const perPage = clampCount(slidesPerPage, 1);
|
|
192
|
+
// Slides advanced per page step: a full page in "auto" mode, else the numeric
|
|
193
|
+
// move clamped to ≤ perPage so a move can never skip past a page and orphan
|
|
194
|
+
// the slides in the gap.
|
|
131
195
|
const effectiveSlidesPerMove =
|
|
132
|
-
slidesPerMove === "auto"
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
196
|
+
slidesPerMove === "auto"
|
|
197
|
+
? perPage
|
|
198
|
+
: Math.min(perPage, clampCount(slidesPerMove, 1));
|
|
199
|
+
// Pages step by `effectiveSlidesPerMove` from 0, and the **last page always
|
|
200
|
+
// end-aligns** to `maxOffset` (the track end minus a full page). This is the
|
|
201
|
+
// single offset model for both modes: it guarantees every page is a full
|
|
202
|
+
// window that can start-snap cleanly, so a total that isn't a whole number of
|
|
203
|
+
// pages can't leave a partial last page whose leading slide can't reach the
|
|
204
|
+
// viewport start (which desyncs the active page against the scroll). The
|
|
205
|
+
// formula is identical to `ceil(total / perPage)` when the step is a full
|
|
206
|
+
// page, so the page *count* is unchanged — only the last page's offset shifts
|
|
207
|
+
// back to keep its window full.
|
|
208
|
+
const maxOffset = Math.max(0, total - perPage);
|
|
138
209
|
const totalPages =
|
|
139
210
|
total === 0
|
|
140
211
|
? 0
|
|
141
|
-
: total <=
|
|
212
|
+
: total <= perPage
|
|
142
213
|
? 1
|
|
143
|
-
:
|
|
144
|
-
? Math.ceil(total / slidesPerPage)
|
|
145
|
-
: Math.floor((total - slidesPerPage) / effectiveSlidesPerMove) + 1;
|
|
214
|
+
: Math.ceil(maxOffset / effectiveSlidesPerMove) + 1;
|
|
146
215
|
|
|
147
216
|
// In controlled mode an out-of-range page prop is a consumer error —
|
|
148
217
|
// throw loudly so it surfaces during development rather than shipping
|
|
@@ -161,6 +230,36 @@ export function useCarouselRoot(
|
|
|
161
230
|
? Math.max(0, Math.min(rawPage, totalPages - 1))
|
|
162
231
|
: rawPage;
|
|
163
232
|
|
|
233
|
+
// Start slide index of the active window: step by move, but the last page
|
|
234
|
+
// end-aligns (clamped to maxOffset) so its window is always full.
|
|
235
|
+
const currentPageOffset = Math.min(
|
|
236
|
+
currentPage * effectiveSlidesPerMove,
|
|
237
|
+
maxOffset,
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
// Inverse of the offset model: which page does a leading slide index belong
|
|
241
|
+
// to? Because the last page end-aligns, the offsets aren't a clean multiple
|
|
242
|
+
// series (the final step is short), so round/floor can't invert them — pick
|
|
243
|
+
// the page whose start offset is nearest the slide. Ties favour the earlier
|
|
244
|
+
// page (`<`). Returns 0 for the empty carousel (the loop runs zero times) —
|
|
245
|
+
// pageForSlideIndex is only called once slides exist anyway.
|
|
246
|
+
const pageForSlideIndex = useCallback(
|
|
247
|
+
(slideIndex: number) => {
|
|
248
|
+
let best = 0;
|
|
249
|
+
let bestDistance = Infinity;
|
|
250
|
+
for (let page = 0; page < totalPages; page++) {
|
|
251
|
+
const offset = Math.min(page * effectiveSlidesPerMove, maxOffset);
|
|
252
|
+
const distance = Math.abs(offset - slideIndex);
|
|
253
|
+
if (distance < bestDistance) {
|
|
254
|
+
bestDistance = distance;
|
|
255
|
+
best = page;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return best;
|
|
259
|
+
},
|
|
260
|
+
[effectiveSlidesPerMove, maxOffset, totalPages],
|
|
261
|
+
);
|
|
262
|
+
|
|
164
263
|
const [internalPlaying, setInternalPlaying] = useState(defaultPlaying);
|
|
165
264
|
const isPlayingControlled = playing !== undefined;
|
|
166
265
|
const currentPlaying = isPlayingControlled ? playing : internalPlaying;
|
|
@@ -189,11 +288,17 @@ export function useCarouselRoot(
|
|
|
189
288
|
const suspended =
|
|
190
289
|
(hovered || focused || touchActive) && !userInitiatedPlayRef.current;
|
|
191
290
|
|
|
192
|
-
// Boundary derivation: navigation requires at least one page.
|
|
193
|
-
// ends clamp at the last page (which, with slidesPerPage > 1,
|
|
194
|
-
// generally before the last slide).
|
|
195
|
-
|
|
196
|
-
|
|
291
|
+
// Boundary derivation: navigation requires at least one page. Without
|
|
292
|
+
// loop the ends clamp at the last page (which, with slidesPerPage > 1,
|
|
293
|
+
// is generally before the last slide). With loop, navigation wraps, so
|
|
294
|
+
// both directions stay available as long as there's more than one page
|
|
295
|
+
// to wrap between — a single page has no wrap target.
|
|
296
|
+
const canGoPrevious = looping
|
|
297
|
+
? totalPages > 1
|
|
298
|
+
: totalPages > 0 && currentPage > 0;
|
|
299
|
+
const canGoNext = looping
|
|
300
|
+
? totalPages > 1
|
|
301
|
+
: totalPages > 0 && currentPage < totalPages - 1;
|
|
197
302
|
|
|
198
303
|
const registerSlide = useCallback(
|
|
199
304
|
(key: string, element: HTMLDivElement | null) => {
|
|
@@ -214,30 +319,69 @@ export function useCarouselRoot(
|
|
|
214
319
|
// once the imperative API or autoplay land; they're added then.
|
|
215
320
|
const isProgrammaticScrollRef = useRef(false);
|
|
216
321
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
322
|
+
// One-shot override for the resolved smooth/reduced-motion scrollBehavior,
|
|
323
|
+
// consumed by the Viewport hook's scroll effect on the very next run and
|
|
324
|
+
// reset immediately after (see useCarouselViewport.ts) — so it never
|
|
325
|
+
// outlives the single call that requested it.
|
|
326
|
+
const instantScrollRef = useRef(false);
|
|
327
|
+
|
|
328
|
+
// One-shot hint for the infinite-loop engine (useCarouselLoop), consumed and
|
|
329
|
+
// reset the same way as instantScrollRef above. `next()`/`previous()` are a
|
|
330
|
+
// relative step, so under looping they should always take the wrap
|
|
331
|
+
// shortcut — continuing forward off the last slide onto the clone, not
|
|
332
|
+
// rewinding across the whole strip. `goTo()` (and `scrollToIndex`, which
|
|
333
|
+
// calls it) is an absolute jump to a *specific* page, most visibly from an
|
|
334
|
+
// indicator/thumbnail click — there, the wrap shortcut reads wrong: picking
|
|
335
|
+
// the last thumbnail from the first should visibly travel forward through
|
|
336
|
+
// every slide in between, matching the indicators' own left-to-right
|
|
337
|
+
// reading order, not silently take the one-step-back route around the ring
|
|
338
|
+
// just because it happens to be shorter. See RFC 0018 (loop navigation).
|
|
339
|
+
const directJumpRef = useRef(false);
|
|
340
|
+
|
|
341
|
+
const next = useCallback(
|
|
342
|
+
(instant?: boolean) => {
|
|
343
|
+
// At the last page: clamp (no-op) normally, or wrap to the first when
|
|
344
|
+
// looping (provided there's more than one page to wrap between).
|
|
345
|
+
if (currentPage >= totalPages - 1 && !(looping && totalPages > 1)) return;
|
|
346
|
+
isProgrammaticScrollRef.current = true;
|
|
347
|
+
instantScrollRef.current = !!instant;
|
|
348
|
+
directJumpRef.current = false;
|
|
349
|
+
const target = looping
|
|
350
|
+
? (currentPage + 1) % totalPages
|
|
351
|
+
: currentPage + 1;
|
|
352
|
+
if (isControlled) {
|
|
353
|
+
onPageChange?.(target);
|
|
354
|
+
} else {
|
|
355
|
+
setInternalPage(target);
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
[currentPage, totalPages, looping, isControlled, onPageChange],
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
const previous = useCallback(
|
|
362
|
+
(instant?: boolean) => {
|
|
363
|
+
// At the first page: clamp (no-op) normally, or wrap to the last when
|
|
364
|
+
// looping (provided there's more than one page to wrap between).
|
|
365
|
+
if (currentPage <= 0 && !(looping && totalPages > 1)) return;
|
|
366
|
+
isProgrammaticScrollRef.current = true;
|
|
367
|
+
instantScrollRef.current = !!instant;
|
|
368
|
+
directJumpRef.current = false;
|
|
369
|
+
const target = looping
|
|
370
|
+
? (currentPage - 1 + totalPages) % totalPages
|
|
371
|
+
: currentPage - 1;
|
|
372
|
+
if (isControlled) {
|
|
373
|
+
onPageChange?.(target);
|
|
374
|
+
} else {
|
|
375
|
+
setInternalPage(target);
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
[currentPage, totalPages, looping, isControlled, onPageChange],
|
|
379
|
+
);
|
|
238
380
|
|
|
239
381
|
const goTo = useCallback(
|
|
240
|
-
(target: number) => {
|
|
382
|
+
(target: number, instant?: boolean) => {
|
|
383
|
+
instantScrollRef.current = !!instant;
|
|
384
|
+
directJumpRef.current = true;
|
|
241
385
|
if (isControlled) {
|
|
242
386
|
onPageChange?.(target);
|
|
243
387
|
} else {
|
|
@@ -247,17 +391,59 @@ export function useCarouselRoot(
|
|
|
247
391
|
[isControlled, onPageChange],
|
|
248
392
|
);
|
|
249
393
|
|
|
394
|
+
// Slide-granularity counterpart to page-granularity goTo — maps the
|
|
395
|
+
// target slide index to its containing page via pageForSlideIndex (a
|
|
396
|
+
// multi-slide page's interior slides aren't independently reachable,
|
|
397
|
+
// so this always lands on the page start closest to slideIndex) and
|
|
398
|
+
// defers to the same goTo the trigger components use. Matches Ark UI's
|
|
399
|
+
// scrollToIndex(index, instant?).
|
|
400
|
+
const scrollToIndex = useCallback(
|
|
401
|
+
(slideIndex: number, instant?: boolean) => {
|
|
402
|
+
goTo(pageForSlideIndex(slideIndex), instant);
|
|
403
|
+
},
|
|
404
|
+
[goTo, pageForSlideIndex],
|
|
405
|
+
);
|
|
406
|
+
|
|
250
407
|
// Autoplay timer. Schedules a single setTimeout per active page; when
|
|
251
408
|
// next() runs it bumps currentPage, which retriggers the effect with
|
|
252
409
|
// a fresh timer. canGoNext gates the schedule so autoplay stops at
|
|
253
410
|
// the last page.
|
|
254
411
|
// The suspended flag pauses the timer while the user is hovering or
|
|
255
412
|
// has focus inside the Root, per WCAG 2.2.2.
|
|
413
|
+
// isAutoplayRunningRef edge-triggers onAutoplayStatusChange's
|
|
414
|
+
// "autoplay.start"/"autoplay.stop" pair around a running session — the
|
|
415
|
+
// effect reruns on every page change (autoplayRunning stays true, so no
|
|
416
|
+
// duplicate "start"), and "autoplay" fires once per tick from inside the
|
|
417
|
+
// scheduled callback, right before next() advances the page.
|
|
418
|
+
const isAutoplayRunningRef = useRef(false);
|
|
256
419
|
useEffect(() => {
|
|
257
|
-
|
|
420
|
+
const eligible =
|
|
421
|
+
autoplayEnabled && currentPlaying && canGoNext && !suspended;
|
|
422
|
+
if (!eligible) {
|
|
423
|
+
if (isAutoplayRunningRef.current) {
|
|
424
|
+
isAutoplayRunningRef.current = false;
|
|
425
|
+
onAutoplayStatusChange?.({
|
|
426
|
+
type: "autoplay.stop",
|
|
427
|
+
page: currentPage,
|
|
428
|
+
isPlaying: false,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
258
431
|
return;
|
|
259
432
|
}
|
|
433
|
+
if (!isAutoplayRunningRef.current) {
|
|
434
|
+
isAutoplayRunningRef.current = true;
|
|
435
|
+
onAutoplayStatusChange?.({
|
|
436
|
+
type: "autoplay.start",
|
|
437
|
+
page: currentPage,
|
|
438
|
+
isPlaying: true,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
260
441
|
const id = setTimeout(() => {
|
|
442
|
+
onAutoplayStatusChange?.({
|
|
443
|
+
type: "autoplay",
|
|
444
|
+
page: currentPage,
|
|
445
|
+
isPlaying: true,
|
|
446
|
+
});
|
|
261
447
|
next();
|
|
262
448
|
}, autoplayDelay);
|
|
263
449
|
return () => clearTimeout(id);
|
|
@@ -268,6 +454,8 @@ export function useCarouselRoot(
|
|
|
268
454
|
suspended,
|
|
269
455
|
autoplayDelay,
|
|
270
456
|
next,
|
|
457
|
+
currentPage,
|
|
458
|
+
onAutoplayStatusChange,
|
|
271
459
|
]);
|
|
272
460
|
|
|
273
461
|
// Handlers spread onto the Root <section>. mouseEnter / mouseLeave
|
|
@@ -348,6 +536,18 @@ export function useCarouselRoot(
|
|
|
348
536
|
[currentPage, totalPages],
|
|
349
537
|
);
|
|
350
538
|
|
|
539
|
+
// The same offset formula `currentPageOffset` uses for the active page,
|
|
540
|
+
// computed for every page — the leading slide index each page snaps to.
|
|
541
|
+
// Exposed so a consumer can build a custom progress bar / indicator from
|
|
542
|
+
// the raw offsets instead of re-deriving the end-align math themselves.
|
|
543
|
+
const getPageSnapPoints = useCallback(
|
|
544
|
+
() =>
|
|
545
|
+
Array.from({ length: totalPages }, (_, page) =>
|
|
546
|
+
Math.min(page * effectiveSlidesPerMove, maxOffset),
|
|
547
|
+
),
|
|
548
|
+
[totalPages, effectiveSlidesPerMove, maxOffset],
|
|
549
|
+
);
|
|
550
|
+
|
|
351
551
|
// Visibility tracking is a ref (not state) because callers read on
|
|
352
552
|
// demand via isInView() and the IO callback in useCarouselViewport
|
|
353
553
|
// mutates many entries per tick — re-rendering on each addition
|
|
@@ -362,19 +562,83 @@ export function useCarouselRoot(
|
|
|
362
562
|
[],
|
|
363
563
|
);
|
|
364
564
|
|
|
565
|
+
// Same ref-not-state reasoning as visibility tracking above: isDragging()
|
|
566
|
+
// is read on demand, and the Viewport hook's pointermove handler would
|
|
567
|
+
// otherwise re-render on every tick of a drag for no benefit (the DOM
|
|
568
|
+
// data-dragging hook already handles the visual).
|
|
569
|
+
const isDraggingRef = useRef(false);
|
|
570
|
+
const setDragging = useCallback((value: boolean) => {
|
|
571
|
+
isDraggingRef.current = value;
|
|
572
|
+
}, []);
|
|
573
|
+
const isDragging = useCallback(() => isDraggingRef.current, []);
|
|
574
|
+
|
|
575
|
+
// Same ref-not-state reasoning again: isOverscrolling() is read on
|
|
576
|
+
// demand, and set/cleared from inside the Viewport hook's pointermove
|
|
577
|
+
// handler on every tick of a drag-overscroll (the data-overscroll DOM
|
|
578
|
+
// hook already handles the visual).
|
|
579
|
+
const isOverscrollingRef = useRef(false);
|
|
580
|
+
const setOverscrolling = useCallback((value: boolean) => {
|
|
581
|
+
isOverscrollingRef.current = value;
|
|
582
|
+
}, []);
|
|
583
|
+
const isOverscrolling = useCallback(() => isOverscrollingRef.current, []);
|
|
584
|
+
|
|
585
|
+
// Same ref-not-state reasoning again: getScrollProgress()/
|
|
586
|
+
// getSlideProgress() are read on demand, and the Viewport hook's
|
|
587
|
+
// scroll/resize recompute would otherwise re-render on every tick for
|
|
588
|
+
// no benefit (the --carousel-progress/--slide-progress CSS custom
|
|
589
|
+
// properties already handle the visual).
|
|
590
|
+
const scrollProgressRef = useRef(0);
|
|
591
|
+
const setScrollProgress = useCallback((value: number) => {
|
|
592
|
+
scrollProgressRef.current = value;
|
|
593
|
+
}, []);
|
|
594
|
+
const getScrollProgress = useCallback(() => scrollProgressRef.current, []);
|
|
595
|
+
|
|
596
|
+
const slideProgressRef = useRef<Map<number, number>>(new Map());
|
|
597
|
+
const setSlideProgress = useCallback(
|
|
598
|
+
(slideIndex: number, value: number) => {
|
|
599
|
+
slideProgressRef.current.set(slideIndex, value);
|
|
600
|
+
},
|
|
601
|
+
[],
|
|
602
|
+
);
|
|
603
|
+
const getSlideProgress = useCallback(
|
|
604
|
+
(slideIndex: number) => slideProgressRef.current.get(slideIndex) ?? 0,
|
|
605
|
+
[],
|
|
606
|
+
);
|
|
607
|
+
|
|
365
608
|
useImperativeHandle(
|
|
366
609
|
imperativeRef,
|
|
367
610
|
() => ({
|
|
368
611
|
next,
|
|
369
612
|
previous,
|
|
370
613
|
goTo,
|
|
614
|
+
scrollToIndex,
|
|
371
615
|
play,
|
|
372
616
|
pause,
|
|
373
617
|
refresh,
|
|
374
618
|
getProgress,
|
|
375
619
|
isInView,
|
|
620
|
+
getPageSnapPoints,
|
|
621
|
+
isDragging,
|
|
622
|
+
isOverscrolling,
|
|
623
|
+
getScrollProgress,
|
|
624
|
+
getSlideProgress,
|
|
376
625
|
}),
|
|
377
|
-
[
|
|
626
|
+
[
|
|
627
|
+
next,
|
|
628
|
+
previous,
|
|
629
|
+
goTo,
|
|
630
|
+
scrollToIndex,
|
|
631
|
+
play,
|
|
632
|
+
pause,
|
|
633
|
+
refresh,
|
|
634
|
+
getProgress,
|
|
635
|
+
isInView,
|
|
636
|
+
getPageSnapPoints,
|
|
637
|
+
isDragging,
|
|
638
|
+
isOverscrolling,
|
|
639
|
+
getScrollProgress,
|
|
640
|
+
getSlideProgress,
|
|
641
|
+
],
|
|
378
642
|
);
|
|
379
643
|
|
|
380
644
|
// Reset the user-initiated flag when the playing session ends, so a
|
|
@@ -398,8 +662,11 @@ export function useCarouselRoot(
|
|
|
398
662
|
registerSlide,
|
|
399
663
|
slidesRef,
|
|
400
664
|
slideKeys,
|
|
401
|
-
slidesPerPage,
|
|
665
|
+
slidesPerPage: perPage,
|
|
402
666
|
effectiveSlidesPerMove,
|
|
667
|
+
maxOffset,
|
|
668
|
+
currentPageOffset,
|
|
669
|
+
pageForSlideIndex,
|
|
403
670
|
totalPages,
|
|
404
671
|
currentPage,
|
|
405
672
|
canGoNext,
|
|
@@ -415,17 +682,35 @@ export function useCarouselRoot(
|
|
|
415
682
|
ids,
|
|
416
683
|
transition,
|
|
417
684
|
snapAlign,
|
|
685
|
+
orientation,
|
|
686
|
+
loop: loopMode,
|
|
687
|
+
allowMouseDrag,
|
|
688
|
+
onDragStatusChange,
|
|
689
|
+
onOverscrollStatusChange,
|
|
690
|
+
inViewThreshold,
|
|
691
|
+
snapType,
|
|
418
692
|
refreshTick,
|
|
419
693
|
visibleSlideIndicesRef,
|
|
420
694
|
setSlideInView,
|
|
421
695
|
isProgrammaticScrollRef,
|
|
696
|
+
instantScrollRef,
|
|
697
|
+
directJumpRef,
|
|
698
|
+
isDraggingRef,
|
|
699
|
+
setDragging,
|
|
700
|
+
isOverscrollingRef,
|
|
701
|
+
setOverscrolling,
|
|
702
|
+
setScrollProgress,
|
|
703
|
+
setSlideProgress,
|
|
422
704
|
}),
|
|
423
705
|
[
|
|
424
706
|
registerSlide,
|
|
425
707
|
slidesRef,
|
|
426
708
|
slideKeys,
|
|
427
|
-
|
|
709
|
+
perPage,
|
|
428
710
|
effectiveSlidesPerMove,
|
|
711
|
+
maxOffset,
|
|
712
|
+
currentPageOffset,
|
|
713
|
+
pageForSlideIndex,
|
|
429
714
|
totalPages,
|
|
430
715
|
currentPage,
|
|
431
716
|
canGoNext,
|
|
@@ -441,8 +726,19 @@ export function useCarouselRoot(
|
|
|
441
726
|
ids,
|
|
442
727
|
transition,
|
|
443
728
|
snapAlign,
|
|
729
|
+
orientation,
|
|
730
|
+
loopMode,
|
|
731
|
+
allowMouseDrag,
|
|
732
|
+
onDragStatusChange,
|
|
733
|
+
onOverscrollStatusChange,
|
|
734
|
+
inViewThreshold,
|
|
735
|
+
snapType,
|
|
444
736
|
refreshTick,
|
|
445
737
|
setSlideInView,
|
|
738
|
+
setDragging,
|
|
739
|
+
setOverscrolling,
|
|
740
|
+
setScrollProgress,
|
|
741
|
+
setSlideProgress,
|
|
446
742
|
],
|
|
447
743
|
);
|
|
448
744
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCallback, useId } from "react";
|
|
2
2
|
|
|
3
|
+
import type { CarouselSnapAlign } from "../types";
|
|
3
4
|
import { useCarouselContext } from "./useCarouselContext";
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -12,14 +13,20 @@ import { useCarouselContext } from "./useCarouselContext";
|
|
|
12
13
|
* a callback ref (rather than `useEffect`) keeps DOM-attachment and
|
|
13
14
|
* registration in lockstep, which matters when slides mount or unmount
|
|
14
15
|
* during a render pass driven by an external state change.
|
|
16
|
+
*
|
|
17
|
+
* `snapAlignOverride` is the slide's own `Carousel.Slide.snapAlign` prop
|
|
18
|
+
* (if set) — it wins over the root's resolved `snapAlign` for this one
|
|
19
|
+
* slide, but only when the slide is actually a valid snap-start position;
|
|
20
|
+
* see the `snapAlign` field below.
|
|
15
21
|
*/
|
|
16
|
-
export function useCarouselSlide() {
|
|
22
|
+
export function useCarouselSlide(snapAlignOverride?: CarouselSnapAlign) {
|
|
17
23
|
const {
|
|
18
24
|
registerSlide,
|
|
19
25
|
slideKeys,
|
|
20
26
|
slidesPerPage,
|
|
27
|
+
currentPageOffset,
|
|
21
28
|
effectiveSlidesPerMove,
|
|
22
|
-
|
|
29
|
+
snapAlign: rootSnapAlign,
|
|
23
30
|
} = useCarouselContext();
|
|
24
31
|
const slideKey = useId();
|
|
25
32
|
|
|
@@ -32,14 +39,42 @@ export function useCarouselSlide() {
|
|
|
32
39
|
|
|
33
40
|
const index = slideKeys.indexOf(slideKey);
|
|
34
41
|
const total = slideKeys.length;
|
|
35
|
-
// The active
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
const pageOffset = currentPage * effectiveSlidesPerMove;
|
|
42
|
+
// The active page covers a window of slidesPerPage slides starting at
|
|
43
|
+
// currentPageOffset (computed by the Root — non-overlapping page groups
|
|
44
|
+
// in "auto" mode, overlapping/end-aligned windows with a numeric
|
|
45
|
+
// slidesPerMove).
|
|
40
46
|
const isActive =
|
|
41
|
-
index >= 0 &&
|
|
47
|
+
index >= 0 &&
|
|
48
|
+
index >= currentPageOffset &&
|
|
49
|
+
index < currentPageOffset + slidesPerPage;
|
|
42
50
|
const state: "active" | "inactive" = isActive ? "active" : "inactive";
|
|
43
51
|
|
|
44
|
-
|
|
52
|
+
// Only a page's *leading* slide is a valid scroll-snap resting position.
|
|
53
|
+
// The Root's page offsets are `min(page * effectiveSlidesPerMove,
|
|
54
|
+
// maxOffset)` for each page — the same set an interior slide (any other
|
|
55
|
+
// index) is never a member of. Letting every slide snap (the old,
|
|
56
|
+
// unconditional CSS default) lets the browser rest scroll on an interior
|
|
57
|
+
// slide when slidesPerPage > 1, which has no valid page mapping: the
|
|
58
|
+
// viewport shows a straddled mix of two pages while currentPage/the
|
|
59
|
+
// indicators still claim one clean page is active. Mirrors the identical
|
|
60
|
+
// offset formula pageForSlideIndex inverts, so the two always agree.
|
|
61
|
+
const maxOffset = Math.max(0, total - slidesPerPage);
|
|
62
|
+
const isSnapStart =
|
|
63
|
+
index >= 0 &&
|
|
64
|
+
(index === maxOffset ||
|
|
65
|
+
(index <= maxOffset && index % effectiveSlidesPerMove === 0));
|
|
66
|
+
// The *value* mirrors the resolved root snapAlign (or this slide's own
|
|
67
|
+
// override, if set), not just a fixed "start" — so the native scroll-snap
|
|
68
|
+
// resting position agrees with where the programmatic scroll (Viewport's
|
|
69
|
+
// centerOffset maths, which reads this same value off the DOM) actually
|
|
70
|
+
// targets. Previously this was hardcoded to "start" regardless of
|
|
71
|
+
// snapAlign, so a user's own swipe/wheel/drag would snap back to the
|
|
72
|
+
// slide's leading edge even under snapAlign="center". Gated on
|
|
73
|
+
// isSnapStart either way — an interior slide of a multi-slide page must
|
|
74
|
+
// never snap, even if it carries its own snapAlign override.
|
|
75
|
+
const snapAlign = isSnapStart
|
|
76
|
+
? (snapAlignOverride ?? rootSnapAlign)
|
|
77
|
+
: undefined;
|
|
78
|
+
|
|
79
|
+
return { slideRef, index, total, state, snapAlign };
|
|
45
80
|
}
|