@primitiv-ui/react 0.1.0
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 +79 -0
- package/package.json +59 -0
- package/src/AccessibleIcon/AccessibleIcon.tsx +40 -0
- package/src/AccessibleIcon/README.md +42 -0
- package/src/AccessibleIcon/__tests__/AccessibleIcon.test.tsx +47 -0
- package/src/AccessibleIcon/index.ts +2 -0
- package/src/AccessibleIcon/types.ts +8 -0
- package/src/Accordion/Accordion.tsx +412 -0
- package/src/Accordion/AccordionContext.ts +12 -0
- package/src/Accordion/README.md +202 -0
- package/src/Accordion/__tests__/Accordion.asChild.test.tsx +237 -0
- package/src/Accordion/__tests__/Accordion.basic-rendering.test.tsx +333 -0
- package/src/Accordion/__tests__/Accordion.controlled-state.test.tsx +175 -0
- package/src/Accordion/__tests__/Accordion.data-attributes.test.tsx +272 -0
- package/src/Accordion/__tests__/Accordion.disabled-items.test.tsx +311 -0
- package/src/Accordion/__tests__/Accordion.error-handling.test.tsx +119 -0
- package/src/Accordion/__tests__/Accordion.forceMount.test.tsx +119 -0
- package/src/Accordion/__tests__/Accordion.keyboard-interaction.test.tsx +736 -0
- package/src/Accordion/__tests__/Accordion.mouse-interaction.test.tsx +212 -0
- package/src/Accordion/__tests__/Accordion.multiple-mode.test.tsx +90 -0
- package/src/Accordion/__tests__/Accordion.reading-direction.test.tsx +139 -0
- package/src/Accordion/__tests__/Accordion.uncontrolled-state.test.tsx +154 -0
- package/src/Accordion/hooks/index.ts +6 -0
- package/src/Accordion/hooks/useAccordionContext.ts +1 -0
- package/src/Accordion/hooks/useAccordionHeaderContext.ts +10 -0
- package/src/Accordion/hooks/useAccordionItem.ts +22 -0
- package/src/Accordion/hooks/useAccordionItemContext.ts +1 -0
- package/src/Accordion/hooks/useAccordionRoot.ts +151 -0
- package/src/Accordion/hooks/useAccordionTrigger.ts +90 -0
- package/src/Accordion/index.ts +1 -0
- package/src/Accordion/types.ts +81 -0
- package/src/Alert/Alert.tsx +43 -0
- package/src/Alert/README.md +54 -0
- package/src/Alert/__tests__/Alert.test.tsx +28 -0
- package/src/Alert/index.ts +2 -0
- package/src/Alert/types.ts +5 -0
- package/src/Avatar/Avatar.tsx +149 -0
- package/src/Avatar/AvatarContext.ts +20 -0
- package/src/Avatar/README.md +116 -0
- package/src/Avatar/__tests__/Avatar.asChild.test.tsx +53 -0
- package/src/Avatar/__tests__/Avatar.basic-rendering.test.tsx +14 -0
- package/src/Avatar/__tests__/Avatar.error-handling.test.tsx +30 -0
- package/src/Avatar/__tests__/Avatar.fallback.test.tsx +75 -0
- package/src/Avatar/__tests__/Avatar.image-loading.test.tsx +81 -0
- package/src/Avatar/hooks/index.ts +2 -0
- package/src/Avatar/hooks/useAvatarContext.ts +1 -0
- package/src/Avatar/hooks/useAvatarImage.ts +40 -0
- package/src/Avatar/index.ts +3 -0
- package/src/Avatar/types.ts +44 -0
- package/src/Breadcrumb/Breadcrumb.tsx +234 -0
- package/src/Breadcrumb/README.md +111 -0
- package/src/Breadcrumb/__tests__/Breadcrumb.asChild.test.tsx +33 -0
- package/src/Breadcrumb/__tests__/Breadcrumb.basic-rendering.test.tsx +132 -0
- package/src/Breadcrumb/index.ts +2 -0
- package/src/Breadcrumb/types.ts +22 -0
- package/src/Button/Button.tsx +95 -0
- package/src/Button/README.md +112 -0
- package/src/Button/__tests__/Button.asChild.test.tsx +91 -0
- package/src/Button/__tests__/Button.basic-rendering.test.tsx +126 -0
- package/src/Button/__tests__/Button.contract.test.tsx +72 -0
- package/src/Button/__tests__/Button.disabled.test.tsx +52 -0
- package/src/Button/__tests__/Button.icon-usage.test.tsx +57 -0
- package/src/Button/__tests__/Button.keyboard-interaction.test.tsx +70 -0
- package/src/Button/index.ts +2 -0
- package/src/Button/types.ts +8 -0
- package/src/Carousel/Carousel.tsx +708 -0
- package/src/Carousel/CarouselContext.ts +11 -0
- package/src/Carousel/README.md +848 -0
- package/src/Carousel/__tests__/Carousel.asChild.test.tsx +178 -0
- package/src/Carousel/__tests__/Carousel.auto-play.test.tsx +617 -0
- package/src/Carousel/__tests__/Carousel.basic-rendering.test.tsx +569 -0
- package/src/Carousel/__tests__/Carousel.controlled-state.test.tsx +137 -0
- package/src/Carousel/__tests__/Carousel.error-handling.test.tsx +81 -0
- package/src/Carousel/__tests__/Carousel.ids.test.tsx +111 -0
- package/src/Carousel/__tests__/Carousel.imperative-api.test.tsx +213 -0
- package/src/Carousel/__tests__/Carousel.indicators.test.tsx +560 -0
- package/src/Carousel/__tests__/Carousel.intersection-observer.test.tsx +276 -0
- package/src/Carousel/__tests__/Carousel.keyboard-navigation.test.tsx +158 -0
- package/src/Carousel/__tests__/Carousel.play-pause.test.tsx +232 -0
- package/src/Carousel/__tests__/Carousel.prev-next.test.tsx +68 -0
- package/src/Carousel/__tests__/Carousel.reduced-motion.test.tsx +49 -0
- package/src/Carousel/__tests__/Carousel.refresh-progress.test.tsx +87 -0
- package/src/Carousel/__tests__/Carousel.scroll-snap-change.test.tsx +179 -0
- package/src/Carousel/__tests__/Carousel.scroll-sync.test.tsx +109 -0
- package/src/Carousel/__tests__/Carousel.slides-per-move.test.tsx +151 -0
- package/src/Carousel/__tests__/Carousel.slides-per-page.test.tsx +183 -0
- package/src/Carousel/__tests__/Carousel.touch-interaction.test.tsx +96 -0
- package/src/Carousel/__tests__/Carousel.transition-modes.test.tsx +70 -0
- package/src/Carousel/__tests__/Carousel.translations.test.tsx +157 -0
- package/src/Carousel/__tests__/Carousel.uncontrolled-state.test.tsx +146 -0
- package/src/Carousel/hooks/index.ts +4 -0
- package/src/Carousel/hooks/useCarouselContext.ts +13 -0
- package/src/Carousel/hooks/useCarouselRoot.ts +450 -0
- package/src/Carousel/hooks/useCarouselSlide.ts +45 -0
- package/src/Carousel/hooks/useCarouselViewport.ts +290 -0
- package/src/Carousel/index.ts +3 -0
- package/src/Carousel/types.ts +400 -0
- package/src/Checkbox/Checkbox.tsx +228 -0
- package/src/Checkbox/CheckboxContext.ts +12 -0
- package/src/Checkbox/README.md +156 -0
- package/src/Checkbox/__tests__/Checkbox.asChild.test.tsx +69 -0
- package/src/Checkbox/__tests__/Checkbox.basic-rendering.test.tsx +41 -0
- package/src/Checkbox/__tests__/Checkbox.controlled-state.test.tsx +82 -0
- package/src/Checkbox/__tests__/Checkbox.disabled.test.tsx +15 -0
- package/src/Checkbox/__tests__/Checkbox.indeterminate.test.tsx +82 -0
- package/src/Checkbox/__tests__/Checkbox.indicator.test.tsx +117 -0
- package/src/Checkbox/__tests__/Checkbox.uncontrolled-state.test.tsx +89 -0
- package/src/Checkbox/hooks/index.ts +2 -0
- package/src/Checkbox/hooks/useCheckboxContext.ts +1 -0
- package/src/Checkbox/hooks/useCheckboxRoot.ts +32 -0
- package/src/Checkbox/index.ts +1 -0
- package/src/Checkbox/types.ts +33 -0
- package/src/CheckboxCard/CheckboxCard.tsx +208 -0
- package/src/CheckboxCard/CheckboxCardContext.ts +12 -0
- package/src/CheckboxCard/README.md +114 -0
- package/src/CheckboxCard/__tests__/CheckboxCard.asChild.test.tsx +54 -0
- package/src/CheckboxCard/__tests__/CheckboxCard.basic-rendering.test.tsx +58 -0
- package/src/CheckboxCard/__tests__/CheckboxCard.controlled-state.test.tsx +77 -0
- package/src/CheckboxCard/__tests__/CheckboxCard.disabled.test.tsx +55 -0
- package/src/CheckboxCard/__tests__/CheckboxCard.error-handling.test.tsx +20 -0
- package/src/CheckboxCard/__tests__/CheckboxCard.indeterminate.test.tsx +60 -0
- package/src/CheckboxCard/__tests__/CheckboxCard.indicator.test.tsx +136 -0
- package/src/CheckboxCard/__tests__/CheckboxCard.uncontrolled-state.test.tsx +73 -0
- package/src/CheckboxCard/hooks/index.ts +2 -0
- package/src/CheckboxCard/hooks/useCheckboxCardContext.ts +1 -0
- package/src/CheckboxCard/hooks/useCheckboxCardRoot.ts +30 -0
- package/src/CheckboxCard/index.ts +3 -0
- package/src/CheckboxCard/types.ts +33 -0
- package/src/Collapsible/Collapsible.tsx +316 -0
- package/src/Collapsible/CollapsibleContext.ts +7 -0
- package/src/Collapsible/README.md +174 -0
- package/src/Collapsible/__tests__/Collapsible.asChild.test.tsx +240 -0
- package/src/Collapsible/__tests__/Collapsible.basic-rendering.test.tsx +118 -0
- package/src/Collapsible/__tests__/Collapsible.controlled-state.test.tsx +134 -0
- package/src/Collapsible/__tests__/Collapsible.disabled.test.tsx +132 -0
- package/src/Collapsible/__tests__/Collapsible.error-handling.test.tsx +40 -0
- package/src/Collapsible/__tests__/Collapsible.forceMount.test.tsx +111 -0
- package/src/Collapsible/__tests__/Collapsible.triggerIcon.test.tsx +93 -0
- package/src/Collapsible/__tests__/Collapsible.uncontrolled-state.test.tsx +125 -0
- package/src/Collapsible/hooks/index.ts +2 -0
- package/src/Collapsible/hooks/useCollapsibleRoot.ts +34 -0
- package/src/Collapsible/hooks/useCollapsibleTrigger.ts +49 -0
- package/src/Collapsible/index.ts +1 -0
- package/src/Collapsible/types.ts +48 -0
- package/src/ContextMenu/ContextMenu.tsx +1004 -0
- package/src/ContextMenu/ContextMenuContentContext.ts +15 -0
- package/src/ContextMenu/ContextMenuContext.ts +21 -0
- package/src/ContextMenu/ContextMenuGroupContext.ts +8 -0
- package/src/ContextMenu/ContextMenuItemIndicatorContext.ts +8 -0
- package/src/ContextMenu/ContextMenuRadioGroupContext.ts +9 -0
- package/src/ContextMenu/ContextMenuSubContext.ts +15 -0
- package/src/ContextMenu/README.md +275 -0
- package/src/ContextMenu/__tests__/ContextMenu.asChild.test.tsx +186 -0
- package/src/ContextMenu/__tests__/ContextMenu.basic-rendering.test.tsx +39 -0
- package/src/ContextMenu/__tests__/ContextMenu.checkbox-item.test.tsx +145 -0
- package/src/ContextMenu/__tests__/ContextMenu.error-handling.test.tsx +113 -0
- package/src/ContextMenu/__tests__/ContextMenu.group-label.test.tsx +48 -0
- package/src/ContextMenu/__tests__/ContextMenu.item-indicator.test.tsx +88 -0
- package/src/ContextMenu/__tests__/ContextMenu.item.test.tsx +106 -0
- package/src/ContextMenu/__tests__/ContextMenu.keyboard-interaction.test.tsx +172 -0
- package/src/ContextMenu/__tests__/ContextMenu.mouse-interaction.test.tsx +227 -0
- package/src/ContextMenu/__tests__/ContextMenu.radio-item.test.tsx +127 -0
- package/src/ContextMenu/__tests__/ContextMenu.reading-direction.test.tsx +152 -0
- package/src/ContextMenu/__tests__/ContextMenu.separator.test.tsx +47 -0
- package/src/ContextMenu/__tests__/ContextMenu.state-modes.test.tsx +119 -0
- package/src/ContextMenu/__tests__/ContextMenu.sub.test.tsx +262 -0
- package/src/ContextMenu/__tests__/ContextMenu.typeahead.test.tsx +89 -0
- package/src/ContextMenu/constants.ts +4 -0
- package/src/ContextMenu/index.ts +1 -0
- package/src/ContextMenu/types.ts +199 -0
- package/src/DirectionProvider/DirectionContext.ts +21 -0
- package/src/DirectionProvider/DirectionProvider.tsx +31 -0
- package/src/DirectionProvider/README.md +62 -0
- package/src/DirectionProvider/__tests__/DirectionProvider.test.tsx +29 -0
- package/src/DirectionProvider/index.ts +3 -0
- package/src/DirectionProvider/types.ts +10 -0
- package/src/Divider/Divider.tsx +57 -0
- package/src/Divider/README.md +57 -0
- package/src/Divider/__tests__/Divider.test.tsx +41 -0
- package/src/Divider/index.ts +1 -0
- package/src/Divider/types.ts +5 -0
- package/src/Dropdown/Dropdown.tsx +842 -0
- package/src/Dropdown/DropdownContentContext.ts +15 -0
- package/src/Dropdown/DropdownContext.ts +17 -0
- package/src/Dropdown/DropdownGroupContext.ts +8 -0
- package/src/Dropdown/DropdownItemIndicatorContext.ts +13 -0
- package/src/Dropdown/DropdownRadioGroupContext.ts +9 -0
- package/src/Dropdown/DropdownSubContext.ts +15 -0
- package/src/Dropdown/README.md +284 -0
- package/src/Dropdown/__tests__/Dropdown.asChild.test.tsx +286 -0
- package/src/Dropdown/__tests__/Dropdown.basic-rendering.test.tsx +43 -0
- package/src/Dropdown/__tests__/Dropdown.checkbox-item.test.tsx +121 -0
- package/src/Dropdown/__tests__/Dropdown.disabled.test.tsx +143 -0
- package/src/Dropdown/__tests__/Dropdown.error-handling.test.tsx +85 -0
- package/src/Dropdown/__tests__/Dropdown.group-label.test.tsx +68 -0
- package/src/Dropdown/__tests__/Dropdown.item-indicator.test.tsx +260 -0
- package/src/Dropdown/__tests__/Dropdown.item.test.tsx +72 -0
- package/src/Dropdown/__tests__/Dropdown.keyboard-edge-cases.test.tsx +77 -0
- package/src/Dropdown/__tests__/Dropdown.keyboard-interaction.test.tsx +310 -0
- package/src/Dropdown/__tests__/Dropdown.mouse-interaction.test.tsx +347 -0
- package/src/Dropdown/__tests__/Dropdown.radio-item.test.tsx +134 -0
- package/src/Dropdown/__tests__/Dropdown.reading-direction.test.tsx +153 -0
- package/src/Dropdown/__tests__/Dropdown.separator.test.tsx +46 -0
- package/src/Dropdown/__tests__/Dropdown.state-modes.test.tsx +100 -0
- package/src/Dropdown/__tests__/Dropdown.sub.test.tsx +185 -0
- package/src/Dropdown/__tests__/Dropdown.trigger.test.tsx +110 -0
- package/src/Dropdown/__tests__/Dropdown.typeahead.test.tsx +133 -0
- package/src/Dropdown/constants.ts +4 -0
- package/src/Dropdown/hooks/index.ts +9 -0
- package/src/Dropdown/hooks/useCloseSiblingSub.ts +13 -0
- package/src/Dropdown/hooks/useDropdownContent.ts +162 -0
- package/src/Dropdown/hooks/useDropdownContext.ts +1 -0
- package/src/Dropdown/hooks/useDropdownGroup.ts +18 -0
- package/src/Dropdown/hooks/useDropdownItem.ts +49 -0
- package/src/Dropdown/hooks/useDropdownLabel.ts +15 -0
- package/src/Dropdown/hooks/useDropdownRoot.ts +57 -0
- package/src/Dropdown/hooks/useDropdownSubContext.ts +1 -0
- package/src/Dropdown/hooks/useDropdownTrigger.ts +31 -0
- package/src/Dropdown/index.ts +1 -0
- package/src/Dropdown/types.ts +200 -0
- package/src/EmptyState/EmptyState.tsx +245 -0
- package/src/EmptyState/README.md +129 -0
- package/src/EmptyState/__tests__/EmptyState.Actions.test.tsx +32 -0
- package/src/EmptyState/__tests__/EmptyState.Description.test.tsx +30 -0
- package/src/EmptyState/__tests__/EmptyState.Media.test.tsx +34 -0
- package/src/EmptyState/__tests__/EmptyState.Root.test.tsx +28 -0
- package/src/EmptyState/__tests__/EmptyState.Title.test.tsx +26 -0
- package/src/EmptyState/index.ts +2 -0
- package/src/EmptyState/types.ts +21 -0
- package/src/Field/Field.tsx +239 -0
- package/src/Field/FieldContext.ts +22 -0
- package/src/Field/README.md +167 -0
- package/src/Field/__tests__/Field.asChild.test.tsx +83 -0
- package/src/Field/__tests__/Field.basic-rendering.test.tsx +104 -0
- package/src/Field/__tests__/Field.state-cascade.test.tsx +75 -0
- package/src/Field/hooks/index.ts +2 -0
- package/src/Field/hooks/useFieldContext.ts +1 -0
- package/src/Field/hooks/useFieldProps.ts +57 -0
- package/src/Field/index.ts +2 -0
- package/src/Field/types.ts +33 -0
- package/src/Fieldset/Fieldset.tsx +104 -0
- package/src/Fieldset/README.md +74 -0
- package/src/Fieldset/__tests__/Fieldset.basic-rendering.test.tsx +81 -0
- package/src/Fieldset/__tests__/Fieldset.disabled.test.tsx +41 -0
- package/src/Fieldset/index.ts +2 -0
- package/src/Fieldset/types.ts +5 -0
- package/src/Input/Input.tsx +120 -0
- package/src/Input/README.md +180 -0
- package/src/Input/__tests__/Input.asChild.test.tsx +85 -0
- package/src/Input/__tests__/Input.basic-rendering.test.tsx +118 -0
- package/src/Input/__tests__/Input.disabled.test.tsx +49 -0
- package/src/Input/__tests__/Input.field-integration.test.tsx +148 -0
- package/src/Input/index.ts +2 -0
- package/src/Input/types.ts +7 -0
- package/src/InputGroup/InputGroup.tsx +228 -0
- package/src/InputGroup/README.md +178 -0
- package/src/InputGroup/__tests__/InputGroup.asChild.test.tsx +109 -0
- package/src/InputGroup/__tests__/InputGroup.basic-rendering.test.tsx +106 -0
- package/src/InputGroup/index.ts +2 -0
- package/src/InputGroup/types.ts +13 -0
- package/src/MillerColumns/MillerColumns.tsx +329 -0
- package/src/MillerColumns/MillerColumnsContext.ts +25 -0
- package/src/MillerColumns/README.md +278 -0
- package/src/MillerColumns/__tests__/MillerColumns.aria.test.tsx +82 -0
- package/src/MillerColumns/__tests__/MillerColumns.asChild.test.tsx +106 -0
- package/src/MillerColumns/__tests__/MillerColumns.auto-scroll.test.tsx +68 -0
- package/src/MillerColumns/__tests__/MillerColumns.basic-rendering.test.tsx +52 -0
- package/src/MillerColumns/__tests__/MillerColumns.column-projection.test.tsx +161 -0
- package/src/MillerColumns/__tests__/MillerColumns.controlled-state.test.tsx +90 -0
- package/src/MillerColumns/__tests__/MillerColumns.data-attributes.test.tsx +77 -0
- package/src/MillerColumns/__tests__/MillerColumns.disabled-items.test.tsx +65 -0
- package/src/MillerColumns/__tests__/MillerColumns.error-handling.test.tsx +57 -0
- package/src/MillerColumns/__tests__/MillerColumns.fixtures.ts +15 -0
- package/src/MillerColumns/__tests__/MillerColumns.item-indicator.test.tsx +57 -0
- package/src/MillerColumns/__tests__/MillerColumns.keyboard-interaction.test.tsx +181 -0
- package/src/MillerColumns/__tests__/MillerColumns.preview-panel.test.tsx +47 -0
- package/src/MillerColumns/__tests__/MillerColumns.resize.test.tsx +137 -0
- package/src/MillerColumns/__tests__/MillerColumns.roving-tabindex.test.tsx +91 -0
- package/src/MillerColumns/__tests__/MillerColumns.selection.test.tsx +54 -0
- package/src/MillerColumns/__tests__/MillerColumns.uncontrolled-state.test.tsx +70 -0
- package/src/MillerColumns/hooks/index.ts +7 -0
- package/src/MillerColumns/hooks/useMillerColumnsColumn.ts +23 -0
- package/src/MillerColumns/hooks/useMillerColumnsColumnContext.ts +1 -0
- package/src/MillerColumns/hooks/useMillerColumnsContext.ts +1 -0
- package/src/MillerColumns/hooks/useMillerColumnsItem.ts +157 -0
- package/src/MillerColumns/hooks/useMillerColumnsItemContext.ts +1 -0
- package/src/MillerColumns/hooks/useMillerColumnsResizeHandle.ts +76 -0
- package/src/MillerColumns/hooks/useMillerColumnsRoot.ts +0 -0
- package/src/MillerColumns/index.ts +3 -0
- package/src/MillerColumns/types.ts +93 -0
- package/src/MillerColumns/useMillerColumnsSelection.ts +31 -0
- package/src/MillerColumns/utils.ts +75 -0
- package/src/Modal/Modal.tsx +474 -0
- package/src/Modal/ModalContext.ts +13 -0
- package/src/Modal/README.md +207 -0
- package/src/Modal/__tests__/Modal.accessibility.test.tsx +167 -0
- package/src/Modal/__tests__/Modal.asChild.test.tsx +162 -0
- package/src/Modal/__tests__/Modal.click-outside.test.tsx +115 -0
- package/src/Modal/__tests__/Modal.content.test.tsx +193 -0
- package/src/Modal/__tests__/Modal.controlled-state.test.tsx +120 -0
- package/src/Modal/__tests__/Modal.error-handling.test.tsx +30 -0
- package/src/Modal/__tests__/Modal.escape-hatches.test.tsx +99 -0
- package/src/Modal/__tests__/Modal.imperative-api.test.tsx +119 -0
- package/src/Modal/__tests__/Modal.nested.test.tsx +106 -0
- package/src/Modal/__tests__/Modal.overlay.test.tsx +99 -0
- package/src/Modal/__tests__/Modal.portal.test.tsx +90 -0
- package/src/Modal/__tests__/Modal.presence.test.tsx +111 -0
- package/src/Modal/__tests__/Modal.trigger.test.tsx +70 -0
- package/src/Modal/__tests__/Modal.uncontrolled-state.test.tsx +72 -0
- package/src/Modal/__tests__/dialog-polyfill.ts +40 -0
- package/src/Modal/hooks/index.ts +4 -0
- package/src/Modal/hooks/useModalContent.ts +62 -0
- package/src/Modal/hooks/useModalContext.ts +1 -0
- package/src/Modal/hooks/useModalRoot.ts +81 -0
- package/src/Modal/hooks/useModalTrigger.ts +25 -0
- package/src/Modal/index.ts +3 -0
- package/src/Modal/types.ts +76 -0
- package/src/Portal/Portal.tsx +28 -0
- package/src/Portal/README.md +70 -0
- package/src/Portal/__tests__/Portal.basic-rendering.test.tsx +17 -0
- package/src/Portal/index.ts +2 -0
- package/src/Portal/types.ts +6 -0
- package/src/Progress/Progress.tsx +178 -0
- package/src/Progress/ProgressContext.ts +15 -0
- package/src/Progress/README.md +112 -0
- package/src/Progress/__tests__/Progress.asChild.test.tsx +37 -0
- package/src/Progress/__tests__/Progress.basic-rendering.test.tsx +65 -0
- package/src/Progress/__tests__/Progress.error-handling.test.tsx +40 -0
- package/src/Progress/__tests__/Progress.fixtures.ts +7 -0
- package/src/Progress/__tests__/Progress.value.test.tsx +83 -0
- package/src/Progress/hooks/index.ts +2 -0
- package/src/Progress/hooks/useProgressContext.ts +1 -0
- package/src/Progress/hooks/useProgressRoot.ts +45 -0
- package/src/Progress/index.ts +3 -0
- package/src/Progress/types.ts +43 -0
- package/src/RadioCard/README.md +133 -0
- package/src/RadioCard/RadioCard.tsx +334 -0
- package/src/RadioCard/RadioCardContext.ts +23 -0
- package/src/RadioCard/RadioCardItemContext.ts +10 -0
- package/src/RadioCard/__tests__/RadioCard.asChild.test.tsx +76 -0
- package/src/RadioCard/__tests__/RadioCard.basic-rendering.test.tsx +87 -0
- package/src/RadioCard/__tests__/RadioCard.controlled-state.test.tsx +107 -0
- package/src/RadioCard/__tests__/RadioCard.disabled-items.test.tsx +61 -0
- package/src/RadioCard/__tests__/RadioCard.error-handling.test.tsx +35 -0
- package/src/RadioCard/__tests__/RadioCard.indicator.test.tsx +119 -0
- package/src/RadioCard/__tests__/RadioCard.keyboard-interaction.test.tsx +158 -0
- package/src/RadioCard/__tests__/RadioCard.orientation.test.tsx +90 -0
- package/src/RadioCard/__tests__/RadioCard.reading-direction.test.tsx +65 -0
- package/src/RadioCard/__tests__/RadioCard.uncontrolled-state.test.tsx +108 -0
- package/src/RadioCard/hooks/index.ts +3 -0
- package/src/RadioCard/hooks/useRadioCardContext.ts +1 -0
- package/src/RadioCard/hooks/useRadioCardItemContext.ts +1 -0
- package/src/RadioCard/hooks/useRadioCardRoot.ts +77 -0
- package/src/RadioCard/index.ts +4 -0
- package/src/RadioCard/types.ts +51 -0
- package/src/RadioGroup/README.md +185 -0
- package/src/RadioGroup/RadioGroup.tsx +353 -0
- package/src/RadioGroup/RadioGroupContext.ts +23 -0
- package/src/RadioGroup/RadioGroupItemContext.ts +10 -0
- package/src/RadioGroup/__tests__/RadioGroup.asChild.test.tsx +105 -0
- package/src/RadioGroup/__tests__/RadioGroup.basic-rendering.test.tsx +72 -0
- package/src/RadioGroup/__tests__/RadioGroup.controlled-state.test.tsx +109 -0
- package/src/RadioGroup/__tests__/RadioGroup.disabled-keydown-guards.test.tsx +68 -0
- package/src/RadioGroup/__tests__/RadioGroup.disabled.test.tsx +79 -0
- package/src/RadioGroup/__tests__/RadioGroup.error-handling.test.tsx +33 -0
- package/src/RadioGroup/__tests__/RadioGroup.indicator.test.tsx +85 -0
- package/src/RadioGroup/__tests__/RadioGroup.keyboard-interaction.test.tsx +135 -0
- package/src/RadioGroup/__tests__/RadioGroup.orientation.test.tsx +90 -0
- package/src/RadioGroup/__tests__/RadioGroup.reading-direction.test.tsx +65 -0
- package/src/RadioGroup/__tests__/RadioGroup.ref-forwarding.test.tsx +45 -0
- package/src/RadioGroup/__tests__/RadioGroup.roving-tabindex.test.tsx +105 -0
- package/src/RadioGroup/__tests__/RadioGroup.uncontrolled-state.test.tsx +96 -0
- package/src/RadioGroup/hooks/index.ts +3 -0
- package/src/RadioGroup/hooks/useRadioGroupContext.ts +1 -0
- package/src/RadioGroup/hooks/useRadioGroupItemContext.ts +1 -0
- package/src/RadioGroup/hooks/useRadioGroupRoot.ts +87 -0
- package/src/RadioGroup/index.ts +1 -0
- package/src/RadioGroup/types.ts +51 -0
- package/src/Select/README.md +203 -0
- package/src/Select/Select.tsx +204 -0
- package/src/Select/__tests__/Select.asChild.test.tsx +36 -0
- package/src/Select/__tests__/Select.basic-rendering.test.tsx +17 -0
- package/src/Select/__tests__/Select.controlled-state.test.tsx +69 -0
- package/src/Select/__tests__/Select.data-attributes.test.tsx +29 -0
- package/src/Select/__tests__/Select.field-integration.test.tsx +150 -0
- package/src/Select/__tests__/Select.group.test.tsx +42 -0
- package/src/Select/__tests__/Select.placeholder.test.tsx +32 -0
- package/src/Select/index.ts +2 -0
- package/src/Select/types.ts +89 -0
- package/src/SkipNav/README.md +87 -0
- package/src/SkipNav/SkipNav.tsx +116 -0
- package/src/SkipNav/__tests__/SkipNav.basic-rendering.test.tsx +23 -0
- package/src/SkipNav/__tests__/SkipNav.ids.test.tsx +19 -0
- package/src/SkipNav/index.ts +1 -0
- package/src/SkipNav/types.ts +26 -0
- package/src/Slider/README.md +215 -0
- package/src/Slider/Slider.tsx +308 -0
- package/src/Slider/SliderContext.ts +24 -0
- package/src/Slider/__tests__/Slider.asChild.test.tsx +119 -0
- package/src/Slider/__tests__/Slider.basic-rendering.test.tsx +157 -0
- package/src/Slider/__tests__/Slider.controlled-state.test.tsx +78 -0
- package/src/Slider/__tests__/Slider.disabled.test.tsx +82 -0
- package/src/Slider/__tests__/Slider.error-handling.test.tsx +45 -0
- package/src/Slider/__tests__/Slider.fixtures.ts +53 -0
- package/src/Slider/__tests__/Slider.form.test.tsx +67 -0
- package/src/Slider/__tests__/Slider.inverted.test.tsx +112 -0
- package/src/Slider/__tests__/Slider.keyboard-interaction.test.tsx +118 -0
- package/src/Slider/__tests__/Slider.multiple-thumbs.test.tsx +84 -0
- package/src/Slider/__tests__/Slider.orientation.test.tsx +101 -0
- package/src/Slider/__tests__/Slider.pointer-interaction.test.tsx +205 -0
- package/src/Slider/__tests__/Slider.reading-direction.test.tsx +99 -0
- package/src/Slider/__tests__/Slider.uncontrolled-state.test.tsx +69 -0
- package/src/Slider/__tests__/Slider.value-commit.test.tsx +103 -0
- package/src/Slider/hooks/index.ts +3 -0
- package/src/Slider/hooks/useSliderContext.ts +1 -0
- package/src/Slider/hooks/useSliderRoot.ts +197 -0
- package/src/Slider/hooks/useSliderThumb.ts +77 -0
- package/src/Slider/index.ts +3 -0
- package/src/Slider/types.ts +48 -0
- package/src/Slider/utils.ts +155 -0
- package/src/Slot/Slot.tsx +158 -0
- package/src/Slot/__tests__/Slot.test.tsx +163 -0
- package/src/Slot/__tests__/composeEventHandlers.test.ts +74 -0
- package/src/Slot/composeEventHandlers.ts +38 -0
- package/src/Slot/index.ts +3 -0
- package/src/Slot/types.ts +5 -0
- package/src/Status/README.md +50 -0
- package/src/Status/Status.tsx +44 -0
- package/src/Status/__tests__/Status.test.tsx +28 -0
- package/src/Status/index.ts +2 -0
- package/src/Status/types.ts +5 -0
- package/src/Switch/README.md +121 -0
- package/src/Switch/Switch.tsx +167 -0
- package/src/Switch/SwitchContext.ts +10 -0
- package/src/Switch/__tests__/Switch.asChild.test.tsx +56 -0
- package/src/Switch/__tests__/Switch.basic-rendering.test.tsx +76 -0
- package/src/Switch/__tests__/Switch.contract.test.tsx +109 -0
- package/src/Switch/__tests__/Switch.controlled-state.test.tsx +79 -0
- package/src/Switch/__tests__/Switch.disabled.test.tsx +60 -0
- package/src/Switch/__tests__/Switch.error-handling.test.tsx +20 -0
- package/src/Switch/__tests__/Switch.keyboard-interaction.test.tsx +56 -0
- package/src/Switch/__tests__/Switch.thumb.test.tsx +84 -0
- package/src/Switch/__tests__/Switch.uncontrolled-state.test.tsx +83 -0
- package/src/Switch/hooks/index.ts +2 -0
- package/src/Switch/hooks/useSwitchContext.ts +1 -0
- package/src/Switch/hooks/useSwitchRoot.ts +28 -0
- package/src/Switch/index.ts +3 -0
- package/src/Switch/types.ts +37 -0
- package/src/Table/README.md +205 -0
- package/src/Table/Table.tsx +380 -0
- package/src/Table/__tests__/Table.Body.test.tsx +61 -0
- package/src/Table/__tests__/Table.Caption.test.tsx +70 -0
- package/src/Table/__tests__/Table.Cell.test.tsx +73 -0
- package/src/Table/__tests__/Table.Footer.test.tsx +61 -0
- package/src/Table/__tests__/Table.Head.test.tsx +61 -0
- package/src/Table/__tests__/Table.Header.test.tsx +73 -0
- package/src/Table/__tests__/Table.Root.test.tsx +49 -0
- package/src/Table/__tests__/Table.Row.test.tsx +67 -0
- package/src/Table/__tests__/Table.ScrollArea.test.tsx +83 -0
- package/src/Table/index.ts +1 -0
- package/src/Table/types.ts +63 -0
- package/src/Tabs/README.md +110 -0
- package/src/Tabs/Tabs.tsx +434 -0
- package/src/Tabs/TabsContext.ts +13 -0
- package/src/Tabs/__tests__/Tabs.activation-mode.test.tsx +114 -0
- package/src/Tabs/__tests__/Tabs.asChild.test.tsx +91 -0
- package/src/Tabs/__tests__/Tabs.basic-rendering.test.tsx +483 -0
- package/src/Tabs/__tests__/Tabs.change-event-callbacks.test.tsx +133 -0
- package/src/Tabs/__tests__/Tabs.controlled-state.test.tsx +152 -0
- package/src/Tabs/__tests__/Tabs.disabled-tabs.test.tsx +203 -0
- package/src/Tabs/__tests__/Tabs.error-handling.test.tsx +82 -0
- package/src/Tabs/__tests__/Tabs.fixtures.ts +171 -0
- package/src/Tabs/__tests__/Tabs.imperative-api.test.tsx +118 -0
- package/src/Tabs/__tests__/Tabs.keyboard-interaction.test.tsx +192 -0
- package/src/Tabs/__tests__/Tabs.lazy-mount.test.tsx +61 -0
- package/src/Tabs/__tests__/Tabs.mouse-interaction.test.tsx +216 -0
- package/src/Tabs/__tests__/Tabs.reading-direction.test.tsx +58 -0
- package/src/Tabs/__tests__/Tabs.uncontrolled-state.test.tsx +197 -0
- package/src/Tabs/hooks/index.ts +4 -0
- package/src/Tabs/hooks/useTabsContent.ts +27 -0
- package/src/Tabs/hooks/useTabsContext.ts +1 -0
- package/src/Tabs/hooks/useTabsRoot.ts +148 -0
- package/src/Tabs/hooks/useTabsTrigger.ts +111 -0
- package/src/Tabs/index.ts +3 -0
- package/src/Tabs/types.ts +99 -0
- package/src/Tabs/utils.ts +8 -0
- package/src/Textarea/README.md +98 -0
- package/src/Textarea/Textarea.tsx +93 -0
- package/src/Textarea/__tests__/Textarea.asChild.test.tsx +85 -0
- package/src/Textarea/__tests__/Textarea.basic-rendering.test.tsx +107 -0
- package/src/Textarea/__tests__/Textarea.disabled.test.tsx +49 -0
- package/src/Textarea/__tests__/Textarea.field-integration.test.tsx +134 -0
- package/src/Textarea/index.ts +2 -0
- package/src/Textarea/types.ts +7 -0
- package/src/Toggle/README.md +97 -0
- package/src/Toggle/Toggle.tsx +81 -0
- package/src/Toggle/__tests__/Toggle.asChild.test.tsx +42 -0
- package/src/Toggle/__tests__/Toggle.basic-rendering.test.tsx +28 -0
- package/src/Toggle/__tests__/Toggle.controlled-state.test.tsx +60 -0
- package/src/Toggle/__tests__/Toggle.disabled.test.tsx +34 -0
- package/src/Toggle/__tests__/Toggle.keyboard-interaction.test.tsx +42 -0
- package/src/Toggle/__tests__/Toggle.uncontrolled-state.test.tsx +40 -0
- package/src/Toggle/index.ts +2 -0
- package/src/Toggle/types.ts +23 -0
- package/src/ToggleGroup/README.md +137 -0
- package/src/ToggleGroup/ToggleGroup.tsx +298 -0
- package/src/ToggleGroup/ToggleGroupContext.ts +9 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.asChild.test.tsx +65 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.basic-rendering.test.tsx +50 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.disabled.test.tsx +54 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.keyboard-interaction.test.tsx +151 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.multiple-mode.test.tsx +144 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.reading-direction.test.tsx +28 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.single-mode.test.tsx +139 -0
- package/src/ToggleGroup/hooks/index.ts +2 -0
- package/src/ToggleGroup/hooks/useToggleGroupContext.ts +1 -0
- package/src/ToggleGroup/hooks/useToggleGroupRoot.ts +110 -0
- package/src/ToggleGroup/index.ts +2 -0
- package/src/ToggleGroup/types.ts +72 -0
- package/src/Tooltip/README.md +214 -0
- package/src/Tooltip/Tooltip.tsx +260 -0
- package/src/Tooltip/TooltipContext.ts +20 -0
- package/src/Tooltip/__tests__/Tooltip.asChild.test.tsx +77 -0
- package/src/Tooltip/__tests__/Tooltip.basic-rendering.test.tsx +180 -0
- package/src/Tooltip/__tests__/Tooltip.controlled-state.test.tsx +128 -0
- package/src/Tooltip/__tests__/Tooltip.escape-hatches.test.tsx +73 -0
- package/src/Tooltip/__tests__/Tooltip.focus-interaction.test.tsx +88 -0
- package/src/Tooltip/__tests__/Tooltip.hover-interaction.test.tsx +179 -0
- package/src/Tooltip/__tests__/Tooltip.keyboard-interaction.test.tsx +85 -0
- package/src/Tooltip/__tests__/Tooltip.uncontrolled-state.test.tsx +67 -0
- package/src/Tooltip/hooks/index.ts +4 -0
- package/src/Tooltip/hooks/useTooltipContent.ts +53 -0
- package/src/Tooltip/hooks/useTooltipProvider.ts +41 -0
- package/src/Tooltip/hooks/useTooltipRoot.ts +106 -0
- package/src/Tooltip/hooks/useTooltipTrigger.ts +44 -0
- package/src/Tooltip/index.ts +1 -0
- package/src/Tooltip/types.ts +64 -0
- package/src/Tree/README.md +339 -0
- package/src/Tree/Tree.tsx +571 -0
- package/src/Tree/TreeContext.ts +24 -0
- package/src/Tree/__tests__/Tree.aria.test.tsx +53 -0
- package/src/Tree/__tests__/Tree.asChild.test.tsx +134 -0
- package/src/Tree/__tests__/Tree.basic-rendering.test.tsx +111 -0
- package/src/Tree/__tests__/Tree.branch-behaviour.test.tsx +87 -0
- package/src/Tree/__tests__/Tree.controlled-expansion.test.tsx +92 -0
- package/src/Tree/__tests__/Tree.data-attributes.test.tsx +88 -0
- package/src/Tree/__tests__/Tree.disabled-items.test.tsx +196 -0
- package/src/Tree/__tests__/Tree.error-handling.test.tsx +71 -0
- package/src/Tree/__tests__/Tree.forceMount.test.tsx +72 -0
- package/src/Tree/__tests__/Tree.keyboard-interaction.test.tsx +150 -0
- package/src/Tree/__tests__/Tree.multiple-selection.test.tsx +151 -0
- package/src/Tree/__tests__/Tree.range-selection.test.tsx +200 -0
- package/src/Tree/__tests__/Tree.recursion-depth.test.tsx +73 -0
- package/src/Tree/__tests__/Tree.roving-tabindex.test.tsx +117 -0
- package/src/Tree/__tests__/Tree.selection-path.test.tsx +404 -0
- package/src/Tree/__tests__/Tree.single-selection.test.tsx +108 -0
- package/src/Tree/__tests__/Tree.uncontrolled-expansion.test.tsx +69 -0
- package/src/Tree/hooks/index.ts +3 -0
- package/src/Tree/hooks/useTreeItemKeyboard.ts +86 -0
- package/src/Tree/hooks/useTreePath.ts +68 -0
- package/src/Tree/hooks/useTreeRoot.ts +279 -0
- package/src/Tree/index.ts +3 -0
- package/src/Tree/types.ts +224 -0
- package/src/Tree/utils.ts +59 -0
- package/src/VisuallyHidden/README.md +58 -0
- package/src/VisuallyHidden/VisuallyHidden.tsx +67 -0
- package/src/VisuallyHidden/__tests__/VisuallyHidden.test.tsx +59 -0
- package/src/VisuallyHidden/index.ts +2 -0
- package/src/VisuallyHidden/types.ts +5 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useCollection.ts +74 -0
- package/src/hooks/useControllableState.ts +81 -0
- package/src/hooks/useRovingTabindex.ts +178 -0
- package/src/index.ts +38 -0
- package/src/test/intersectionObserverPolyfill.ts +83 -0
- package/src/test/popoverPolyfill.ts +86 -0
- package/src/test/scrollPolyfill.ts +23 -0
- package/src/types.ts +13 -0
- package/src/utils/__tests__/createStrictContext.test.tsx +69 -0
- package/src/utils/__tests__/deriveId.test.ts +28 -0
- package/src/utils/__tests__/getKeyToActionMap.test.ts +106 -0
- package/src/utils/createStrictContext.ts +49 -0
- package/src/utils/deriveId.ts +31 -0
- package/src/utils/getKeyToActionMap.ts +95 -0
- package/src/utils/index.ts +3 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { KeyboardEvent, useCallback, useEffect, useMemo, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
import { useCarouselContext } from "./useCarouselContext";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Owns the Viewport-side scroll-state sync — bidirectional.
|
|
7
|
+
*
|
|
8
|
+
* **State → scroll.** When `currentPage` flips for any reason
|
|
9
|
+
* (NextTrigger, Indicator click, autoplay tick, imperative goTo), the
|
|
10
|
+
* effect locates the first slide of the target page via the published
|
|
11
|
+
* `slidesRef`, reads its `getBoundingClientRect` relative to the
|
|
12
|
+
* viewport, and calls `scrollTo` so the visual surface tracks React
|
|
13
|
+
* state. The current `scrollLeft` is included in the target so the
|
|
14
|
+
* calculation is correct mid-scroll. The run also asserts
|
|
15
|
+
* `isProgrammaticScrollRef` so the IntersectionObserver fallback
|
|
16
|
+
* doesn't treat the in-flight animation as a user scroll and undo the
|
|
17
|
+
* page change; the flag clears on `scrollend` (with a timeout
|
|
18
|
+
* fallback for environments that don't fire it).
|
|
19
|
+
*
|
|
20
|
+
* **Scroll → state.** When the user swipes the viewport, the browser
|
|
21
|
+
* fires `scrollsnapchange` with the new snap target. The handler
|
|
22
|
+
* looks up which slide that target is, computes
|
|
23
|
+
* `floor(slideIndex / slidesPerPage)`, and calls `goTo` on the new
|
|
24
|
+
* page (skipping when the page is unchanged so consumers don't see
|
|
25
|
+
* spurious onPageChange callbacks, and bailing when the snap target
|
|
26
|
+
* isn't one of our registered slides). An `IntersectionObserver`
|
|
27
|
+
* provides the same page-drive for browsers without `scrollsnapchange`,
|
|
28
|
+
* but stands down when the event is supported (it is authoritative);
|
|
29
|
+
* the observer still always feeds `isInView`.
|
|
30
|
+
*
|
|
31
|
+
* **Keyboard → state.** Returns an `onKeyDown` handler that wires the
|
|
32
|
+
* WAI-ARIA Carousel pattern arrow keys (`ArrowRight` / `ArrowLeft` for
|
|
33
|
+
* next / previous) plus `Home` / `End` (first / last) onto the same
|
|
34
|
+
* imperative API the trigger buttons call, so smooth scroll matches
|
|
35
|
+
* the click path. The handler only fires when the Viewport itself is
|
|
36
|
+
* the focus target — focus inside a slide keeps its native arrow-key
|
|
37
|
+
* semantics — and respects `canGoNext` / `canGoPrevious` so it clamps
|
|
38
|
+
* at the ends.
|
|
39
|
+
*/
|
|
40
|
+
export function useCarouselViewport() {
|
|
41
|
+
const {
|
|
42
|
+
slidesRef,
|
|
43
|
+
slideKeys,
|
|
44
|
+
effectiveSlidesPerMove,
|
|
45
|
+
totalPages,
|
|
46
|
+
currentPage,
|
|
47
|
+
goTo,
|
|
48
|
+
next,
|
|
49
|
+
previous,
|
|
50
|
+
canGoNext,
|
|
51
|
+
canGoPrevious,
|
|
52
|
+
transition,
|
|
53
|
+
snapAlign,
|
|
54
|
+
refreshTick,
|
|
55
|
+
visibleSlideIndicesRef,
|
|
56
|
+
setSlideInView,
|
|
57
|
+
isProgrammaticScrollRef,
|
|
58
|
+
} = useCarouselContext();
|
|
59
|
+
const internalRef = useRef<HTMLDivElement>(null);
|
|
60
|
+
// Set to true by the scrollsnapchange handler and the IntersectionObserver
|
|
61
|
+
// callback before they call goTo(), so the scroll effect knows the page
|
|
62
|
+
// change originated from a user scroll (CSS snap already positioned the
|
|
63
|
+
// viewport) and must not call scrollTo() again.
|
|
64
|
+
const isUserScrollRef = useRef(false);
|
|
65
|
+
|
|
66
|
+
// Callback ref so the consumer can compose their own ref with ours
|
|
67
|
+
// via `composeRefs` later (cycle 22 introduces asChild). For now,
|
|
68
|
+
// it just stashes the node.
|
|
69
|
+
const viewportRef = useCallback((node: HTMLDivElement | null) => {
|
|
70
|
+
internalRef.current = node;
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
73
|
+
// Read prefers-reduced-motion once on mount; choose scrollTo
|
|
74
|
+
// behavior accordingly so we don't fight the OS-level setting.
|
|
75
|
+
const scrollBehavior = useMemo<ScrollBehavior>(
|
|
76
|
+
() =>
|
|
77
|
+
window.matchMedia?.("(prefers-reduced-motion: reduce)").matches
|
|
78
|
+
? "instant"
|
|
79
|
+
: "smooth",
|
|
80
|
+
[],
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
// transition="none" hands the visual to consumer CSS; we don't
|
|
85
|
+
// touch viewport.scrollTo at all in that mode.
|
|
86
|
+
if (transition !== "slide") return;
|
|
87
|
+
const firstSlideIndex = currentPage * effectiveSlidesPerMove;
|
|
88
|
+
const firstSlideKey = slideKeys[firstSlideIndex];
|
|
89
|
+
// No slides registered yet, or page out of range: nothing to scroll to.
|
|
90
|
+
if (!firstSlideKey) return;
|
|
91
|
+
|
|
92
|
+
// Both viewport ref and the slide element are guaranteed populated
|
|
93
|
+
// here — the effect runs post-commit (after callback refs fire) and
|
|
94
|
+
// any key in slideKeys was just registered into slidesRef in
|
|
95
|
+
// lockstep by useCarouselRoot.registerSlide.
|
|
96
|
+
// A user swipe via CSS scroll-snap has already positioned the viewport;
|
|
97
|
+
// calling scrollTo on top would start a second animation and cause jank.
|
|
98
|
+
if (isUserScrollRef.current) {
|
|
99
|
+
isUserScrollRef.current = false;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const viewport = internalRef.current!;
|
|
104
|
+
|
|
105
|
+
// Mark the scroll as programmatic so the IntersectionObserver
|
|
106
|
+
// doesn't treat the in-flight animation as a user scroll and undo
|
|
107
|
+
// the page change. next() / previous() also set this — re-asserting
|
|
108
|
+
// here covers indicator-driven goTo and the initial scroll on
|
|
109
|
+
// mount, neither of which goes through next() / previous().
|
|
110
|
+
isProgrammaticScrollRef.current = true;
|
|
111
|
+
|
|
112
|
+
// Native-first: delegate the horizontal scroll to the browser via
|
|
113
|
+
// scrollIntoView rather than computing scrollLeft ourselves. The
|
|
114
|
+
// consumer's `scroll-snap-align` then makes the final correction so
|
|
115
|
+
// we never fight the snap engine. `inline` maps to snapAlign;
|
|
116
|
+
// `block: "nearest"` keeps the page from scrolling vertically.
|
|
117
|
+
const targetEl = slidesRef.current!.get(firstSlideKey)!;
|
|
118
|
+
targetEl.scrollIntoView({
|
|
119
|
+
behavior: scrollBehavior,
|
|
120
|
+
inline: snapAlign === "center" ? "center" : "start",
|
|
121
|
+
block: "nearest",
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Clear the programmatic-scroll guard once the animation settles.
|
|
125
|
+
// `scrollend` is the reliable signal in real browsers; the setTimeout
|
|
126
|
+
// is a fallback for environments (jsdom, older Safari) that don't fire
|
|
127
|
+
// it. The timeout is longer than any typical smooth-scroll duration so
|
|
128
|
+
// real-browser IO entries that fire mid-animation are still suppressed.
|
|
129
|
+
// Re-clearing the flag is harmless, so no idempotency guard is needed.
|
|
130
|
+
const clearFlag = () => {
|
|
131
|
+
isProgrammaticScrollRef.current = false;
|
|
132
|
+
};
|
|
133
|
+
viewport.addEventListener("scrollend", clearFlag, { once: true });
|
|
134
|
+
const timeoutId = setTimeout(() => {
|
|
135
|
+
viewport.removeEventListener("scrollend", clearFlag);
|
|
136
|
+
clearFlag();
|
|
137
|
+
}, 600);
|
|
138
|
+
return () => {
|
|
139
|
+
clearTimeout(timeoutId);
|
|
140
|
+
viewport.removeEventListener("scrollend", clearFlag);
|
|
141
|
+
};
|
|
142
|
+
}, [
|
|
143
|
+
transition,
|
|
144
|
+
snapAlign,
|
|
145
|
+
currentPage,
|
|
146
|
+
effectiveSlidesPerMove,
|
|
147
|
+
slideKeys,
|
|
148
|
+
slidesRef,
|
|
149
|
+
refreshTick,
|
|
150
|
+
scrollBehavior,
|
|
151
|
+
]);
|
|
152
|
+
|
|
153
|
+
// User-driven scroll → state. Listen for scrollsnapchange and update
|
|
154
|
+
// currentPage from the snapped slide's index. The viewport ref is
|
|
155
|
+
// guaranteed populated post-commit (callback ref runs first).
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
if (transition !== "slide") return;
|
|
158
|
+
const viewport = internalRef.current!;
|
|
159
|
+
|
|
160
|
+
const handler = (event: Event) => {
|
|
161
|
+
const target = (event as Event & { snapTargetInline?: Element })
|
|
162
|
+
.snapTargetInline;
|
|
163
|
+
|
|
164
|
+
// findIndex returns -1 when the snap target isn't one of our
|
|
165
|
+
// registered slides — e.g. a consumer-wrapped element inside the
|
|
166
|
+
// viewport. Ignore those; only registered slides drive the page.
|
|
167
|
+
const slideIndex = slideKeys.findIndex(
|
|
168
|
+
(key) => slidesRef.current!.get(key) === target,
|
|
169
|
+
);
|
|
170
|
+
if (slideIndex < 0) return;
|
|
171
|
+
|
|
172
|
+
const targetPage = Math.floor(slideIndex / effectiveSlidesPerMove);
|
|
173
|
+
if (targetPage !== currentPage) {
|
|
174
|
+
isUserScrollRef.current = true;
|
|
175
|
+
goTo(targetPage);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
viewport.addEventListener("scrollsnapchange", handler);
|
|
180
|
+
return () => viewport.removeEventListener("scrollsnapchange", handler);
|
|
181
|
+
}, [
|
|
182
|
+
transition,
|
|
183
|
+
slideKeys,
|
|
184
|
+
slidesRef,
|
|
185
|
+
effectiveSlidesPerMove,
|
|
186
|
+
currentPage,
|
|
187
|
+
goTo,
|
|
188
|
+
]);
|
|
189
|
+
|
|
190
|
+
// IntersectionObserver fallback for browsers without scrollsnapchange,
|
|
191
|
+
// and the source of truth for isInView() on the imperative API. The
|
|
192
|
+
// observer fires whenever a slide crosses the 0.6 visibility
|
|
193
|
+
// threshold; the lowest-index visible slide derives the active page.
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
if (transition !== "slide") return;
|
|
196
|
+
|
|
197
|
+
// When the browser fires scrollsnapchange (Chrome 129+), that event is
|
|
198
|
+
// the authoritative source of the active page — snapTargetInline is the
|
|
199
|
+
// precisely-snapped slide. The observer below then only feeds isInView.
|
|
200
|
+
// Its lowest-index-visible heuristic is a coarse page fallback for
|
|
201
|
+
// browsers without the event, and would mis-track a centre-aligned
|
|
202
|
+
// carousel that shows several slides at once (it follows the leftmost
|
|
203
|
+
// visible slide, not the centred one), so it must not drive the page
|
|
204
|
+
// when the event is available.
|
|
205
|
+
const supportsSnapEvents = "onscrollsnapchange" in window;
|
|
206
|
+
|
|
207
|
+
const observer = new IntersectionObserver(
|
|
208
|
+
(entries) => {
|
|
209
|
+
// Both lookups (slideKeys.findIndex → registered key, and the
|
|
210
|
+
// slidesRef get → element) are guaranteed to resolve: the
|
|
211
|
+
// observer only observes elements registered into slidesRef
|
|
212
|
+
// alongside their slideKey, and is disconnected on cleanup
|
|
213
|
+
// before slides can unmount.
|
|
214
|
+
for (const entry of entries) {
|
|
215
|
+
const idx = slideKeys.findIndex(
|
|
216
|
+
(key) => slidesRef.current!.get(key) === entry.target,
|
|
217
|
+
);
|
|
218
|
+
setSlideInView(
|
|
219
|
+
idx,
|
|
220
|
+
entry.isIntersecting && entry.intersectionRatio >= 0.6,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// isInView is updated above regardless; only the page-drive below
|
|
225
|
+
// is the fallback that scrollsnapchange supersedes.
|
|
226
|
+
if (supportsSnapEvents) return;
|
|
227
|
+
|
|
228
|
+
const visible = visibleSlideIndicesRef.current;
|
|
229
|
+
if (visible.size === 0) return;
|
|
230
|
+
const firstVisible = Math.min(...visible);
|
|
231
|
+
const targetPage = Math.floor(firstVisible / effectiveSlidesPerMove);
|
|
232
|
+
// Guard: if a programmatic scroll is in flight (e.g. user clicked
|
|
233
|
+
// NextTrigger and the smooth-scroll animation hasn't settled), the
|
|
234
|
+
// IO may still see the old slide as ≥0.6 visible. Calling goTo
|
|
235
|
+
// here would undo the navigation, so skip until the flag clears.
|
|
236
|
+
if (targetPage !== currentPage && !isProgrammaticScrollRef.current) {
|
|
237
|
+
isUserScrollRef.current = true;
|
|
238
|
+
goTo(targetPage);
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
{ threshold: 0.6 },
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
for (const key of slideKeys) {
|
|
245
|
+
observer.observe(slidesRef.current!.get(key)!);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return () => observer.disconnect();
|
|
249
|
+
}, [
|
|
250
|
+
transition,
|
|
251
|
+
slideKeys,
|
|
252
|
+
slidesRef,
|
|
253
|
+
effectiveSlidesPerMove,
|
|
254
|
+
currentPage,
|
|
255
|
+
goTo,
|
|
256
|
+
setSlideInView,
|
|
257
|
+
visibleSlideIndicesRef,
|
|
258
|
+
]);
|
|
259
|
+
|
|
260
|
+
// Keyboard navigation per the WAI-ARIA Carousel pattern: arrow keys
|
|
261
|
+
// route through the same imperative API as the trigger buttons so the
|
|
262
|
+
// smooth scroll and loop-wrap animation match the click path. The
|
|
263
|
+
// event.target === currentTarget guard restricts handling to the
|
|
264
|
+
// Viewport itself — focus inside a slide (e.g. on a link or form
|
|
265
|
+
// control) keeps its native arrow-key semantics.
|
|
266
|
+
const onKeyDown = useCallback(
|
|
267
|
+
(event: KeyboardEvent<HTMLDivElement>) => {
|
|
268
|
+
// Restrict to keypresses originating on the Viewport itself —
|
|
269
|
+
// focus inside a slide (e.g. on a link or form control) keeps
|
|
270
|
+
// its native arrow-key semantics.
|
|
271
|
+
if (event.target !== event.currentTarget) return;
|
|
272
|
+
if (event.key === "ArrowRight") {
|
|
273
|
+
event.preventDefault();
|
|
274
|
+
if (canGoNext) next();
|
|
275
|
+
} else if (event.key === "ArrowLeft") {
|
|
276
|
+
event.preventDefault();
|
|
277
|
+
if (canGoPrevious) previous();
|
|
278
|
+
} else if (event.key === "Home") {
|
|
279
|
+
event.preventDefault();
|
|
280
|
+
goTo(0);
|
|
281
|
+
} else if (event.key === "End") {
|
|
282
|
+
event.preventDefault();
|
|
283
|
+
goTo(totalPages - 1);
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
[canGoNext, canGoPrevious, next, previous, goTo, totalPages],
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
return { viewportRef, onKeyDown };
|
|
290
|
+
}
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import { ComponentProps, ReactNode, RefObject } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Discriminated label shape for `Carousel.Root` — exactly one of
|
|
5
|
+
* `ariaLabel` or `ariaLabelledBy` must be supplied so every carousel
|
|
6
|
+
* has an accessible name (per the WAI-ARIA Carousel pattern). TypeScript
|
|
7
|
+
* rejects shapes that supply both or neither.
|
|
8
|
+
*/
|
|
9
|
+
export type CarouselRootLabelProps =
|
|
10
|
+
| { ariaLabel: string; ariaLabelledBy?: never }
|
|
11
|
+
| { ariaLabel?: never; ariaLabelledBy: string };
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Uncontrolled page state — the Root owns the active page internally,
|
|
15
|
+
* optionally seeded by `defaultPage`. The discriminated union below
|
|
16
|
+
* rejects passing `page` or `onPageChange` alongside it.
|
|
17
|
+
*/
|
|
18
|
+
export type UncontrolledCarouselPageProps = {
|
|
19
|
+
/** Uncontrolled active page index. Defaults to `0`. */
|
|
20
|
+
defaultPage?: number;
|
|
21
|
+
page?: never;
|
|
22
|
+
onPageChange?: never;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Controlled page state — the parent owns the active page; the Root
|
|
27
|
+
* defers every state change back through `onPageChange`. Both props
|
|
28
|
+
* must be supplied together.
|
|
29
|
+
*/
|
|
30
|
+
export type ControlledCarouselPageProps = {
|
|
31
|
+
/** Controlled active page index. */
|
|
32
|
+
page: number;
|
|
33
|
+
/** Callback invoked when the active page should change (e.g. when the
|
|
34
|
+
* user clicks `Carousel.NextTrigger` or `Carousel.PreviousTrigger`).
|
|
35
|
+
* The callback is responsible for re-rendering with the new `page`. */
|
|
36
|
+
onPageChange: (page: number) => void;
|
|
37
|
+
defaultPage?: never;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Discriminated state union — TypeScript rejects mixed shapes (e.g.
|
|
42
|
+
* `defaultPage` + `page`, or `page` without `onPageChange`).
|
|
43
|
+
*/
|
|
44
|
+
export type CarouselRootPageStateProps =
|
|
45
|
+
| UncontrolledCarouselPageProps
|
|
46
|
+
| ControlledCarouselPageProps;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Uncontrolled playing state — the Root owns the "playing" flag
|
|
50
|
+
* internally, optionally seeded by `defaultPlaying`.
|
|
51
|
+
*/
|
|
52
|
+
export type UncontrolledCarouselPlayingProps = {
|
|
53
|
+
/** Uncontrolled initial playing flag. Defaults to `false`. */
|
|
54
|
+
defaultPlaying?: boolean;
|
|
55
|
+
playing?: never;
|
|
56
|
+
onPlayingChange?: never;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Controlled playing state — the parent owns the "playing" flag; the
|
|
61
|
+
* Root defers every change back through `onPlayingChange`. Both props
|
|
62
|
+
* must be supplied together.
|
|
63
|
+
*/
|
|
64
|
+
export type ControlledCarouselPlayingProps = {
|
|
65
|
+
/** Controlled playing flag. */
|
|
66
|
+
playing: boolean;
|
|
67
|
+
/** Callback invoked when the playing flag should toggle. The callback
|
|
68
|
+
* is responsible for re-rendering with the new `playing` value. */
|
|
69
|
+
onPlayingChange: (playing: boolean) => void;
|
|
70
|
+
defaultPlaying?: never;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Discriminated playing-state union — mirrors the page-state pattern.
|
|
75
|
+
*/
|
|
76
|
+
export type CarouselRootPlayingStateProps =
|
|
77
|
+
| UncontrolledCarouselPlayingProps
|
|
78
|
+
| ControlledCarouselPlayingProps;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Autoplay configuration. Pass `true` for the default 4000ms cadence,
|
|
82
|
+
* `false` (default) to disable autoplay entirely, or `{ delay: N }` to
|
|
83
|
+
* tune the interval. The active page advances on each tick while
|
|
84
|
+
* `playing` is `true`; the timer stops once the active page reaches the
|
|
85
|
+
* last slide.
|
|
86
|
+
*/
|
|
87
|
+
export type CarouselAutoplay = boolean | { delay: number };
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Visual transition mode for the viewport.
|
|
91
|
+
*
|
|
92
|
+
* - `"slide"` (default) — relies on native CSS scroll-snap; the
|
|
93
|
+
* Viewport scrolls programmatically when the page changes and
|
|
94
|
+
* listens for `scrollsnapchange` to update React state when the
|
|
95
|
+
* user swipes.
|
|
96
|
+
* - `"none"` — the Viewport installs no scroll wiring at all.
|
|
97
|
+
* Consumer CSS owns the visual transition (e.g. crossfade,
|
|
98
|
+
* dissolve) via the `data-state` hook on each slide, which still
|
|
99
|
+
* flips with the active page.
|
|
100
|
+
*/
|
|
101
|
+
export type CarouselTransition = "slide" | "none";
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Scroll-snap alignment that the Viewport should target when
|
|
105
|
+
* programmatically scrolling to a page.
|
|
106
|
+
*
|
|
107
|
+
* - `"start"` (default) — scrolls so the first slide of the target
|
|
108
|
+
* page aligns with the **start** (left) edge of the Viewport.
|
|
109
|
+
* Matches `scroll-snap-align: start` in consumer CSS.
|
|
110
|
+
* - `"center"` — scrolls so the first slide of the target page is
|
|
111
|
+
* **centred** in the Viewport. The target is offset inward by
|
|
112
|
+
* `(viewportWidth − slideWidth) / 2`. Use this with
|
|
113
|
+
* `scroll-snap-align: center` in consumer CSS (e.g. Cover Flow
|
|
114
|
+
* layouts where slides are narrower than the Viewport).
|
|
115
|
+
*/
|
|
116
|
+
export type CarouselSnapAlign = "start" | "center";
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Pin DOM `id`s on the rendered sub-components for SSR / hydration
|
|
120
|
+
* stability or for external `aria-controls` references. Any keys you
|
|
121
|
+
* omit leave the corresponding element unidentified (or with whatever
|
|
122
|
+
* the consumer attaches to that sub-component directly via its own
|
|
123
|
+
* `id` prop, which always wins).
|
|
124
|
+
*/
|
|
125
|
+
export type CarouselIds = {
|
|
126
|
+
root?: string;
|
|
127
|
+
viewport?: string;
|
|
128
|
+
previousTrigger?: string;
|
|
129
|
+
nextTrigger?: string;
|
|
130
|
+
playPauseTrigger?: string;
|
|
131
|
+
indicatorGroup?: string;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Override the default user-visible strings the component owns —
|
|
136
|
+
* intended for internationalisation. Any keys you omit fall back to
|
|
137
|
+
* the English defaults.
|
|
138
|
+
*/
|
|
139
|
+
export type CarouselTranslations = {
|
|
140
|
+
/** Format used for the auto-generated slide aria-label. Receives
|
|
141
|
+
* 1-indexed `index` and the live `total`. Default:
|
|
142
|
+
* `({ index, total }) => "${index} of ${total}"`. */
|
|
143
|
+
slideLabel?: (params: { index: number; total: number }) => string;
|
|
144
|
+
/** Format used for the auto-generated indicator aria-label. Receives
|
|
145
|
+
* the 1-indexed page position. Default:
|
|
146
|
+
* `({ index }) => "Slide ${index}"`. */
|
|
147
|
+
indicatorLabel?: (params: { index: number }) => string;
|
|
148
|
+
/** Accessible name for `Carousel.PlayPauseTrigger` while paused.
|
|
149
|
+
* Default: `"Start automatic slide show"`. */
|
|
150
|
+
startSlideshow?: string;
|
|
151
|
+
/** Accessible name for `Carousel.PlayPauseTrigger` while playing.
|
|
152
|
+
* Default: `"Stop automatic slide show"`. */
|
|
153
|
+
stopSlideshow?: string;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export type CarouselRootProps = Omit<
|
|
157
|
+
ComponentProps<"section">,
|
|
158
|
+
"aria-label" | "aria-labelledby"
|
|
159
|
+
> &
|
|
160
|
+
CarouselRootLabelProps &
|
|
161
|
+
CarouselRootPageStateProps &
|
|
162
|
+
CarouselRootPlayingStateProps & {
|
|
163
|
+
/** Autoplay configuration — see {@link CarouselAutoplay}. */
|
|
164
|
+
autoplay?: CarouselAutoplay;
|
|
165
|
+
/** Visual transition mode — see {@link CarouselTransition}.
|
|
166
|
+
* Defaults to `"slide"`. */
|
|
167
|
+
transition?: CarouselTransition;
|
|
168
|
+
/** Number of slides visible per page. Defaults to `1`. With values
|
|
169
|
+
* greater than `1`, slides are grouped into pages of that size for
|
|
170
|
+
* navigation purposes: indicators auto-render per page, boundary
|
|
171
|
+
* clamp moves to the last page, and `Carousel.NextTrigger` /
|
|
172
|
+
* `Carousel.PreviousTrigger` advance one page at a time. */
|
|
173
|
+
slidesPerPage?: number;
|
|
174
|
+
/** Number of slides advanced by `Carousel.NextTrigger` /
|
|
175
|
+
* `Carousel.PreviousTrigger`. `"auto"` (default) advances one
|
|
176
|
+
* full page at a time (= `slidesPerPage`); a number advances
|
|
177
|
+
* exactly that many slides per click and pages are computed so
|
|
178
|
+
* the visible window always stays full. */
|
|
179
|
+
slidesPerMove?: number | "auto";
|
|
180
|
+
/** Override the default user-visible strings the component owns —
|
|
181
|
+
* see {@link CarouselTranslations}. Useful for i18n. */
|
|
182
|
+
translations?: CarouselTranslations;
|
|
183
|
+
/** Pin DOM `id`s on the rendered sub-components — see
|
|
184
|
+
* {@link CarouselIds}. Useful for SSR hydration stability and
|
|
185
|
+
* external `aria-controls` linkage. */
|
|
186
|
+
ids?: CarouselIds;
|
|
187
|
+
/** Scroll-snap alignment the Viewport targets when programmatically
|
|
188
|
+
* scrolling to a page — see {@link CarouselSnapAlign}.
|
|
189
|
+
* Defaults to `"start"`. Set to `"center"` when consumer CSS uses
|
|
190
|
+
* `scroll-snap-align: center` on slides (e.g. Cover Flow layouts
|
|
191
|
+
* where slides are narrower than the Viewport). */
|
|
192
|
+
snapAlign?: CarouselSnapAlign;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Shape of the context published by `Carousel.Root` to descendants.
|
|
197
|
+
* Fields are added as future cycles introduce shared state (active
|
|
198
|
+
* page, autoplay, etc.).
|
|
199
|
+
*/
|
|
200
|
+
export type CarouselContextValue = {
|
|
201
|
+
/** Self-registers a slide. Called as a callback ref by `Carousel.Slide`
|
|
202
|
+
* with the rendered DOM node on mount and `null` on unmount. */
|
|
203
|
+
registerSlide: (key: string, element: HTMLDivElement | null) => void;
|
|
204
|
+
/** Live map from slide key to rendered DOM node. Used by the
|
|
205
|
+
* Viewport to read `getBoundingClientRect` on the first slide of
|
|
206
|
+
* the target page when programmatically scrolling. */
|
|
207
|
+
slidesRef: RefObject<Map<string, HTMLDivElement>>;
|
|
208
|
+
/** Ordered list of currently-mounted slide keys. The slide's index is
|
|
209
|
+
* its position in this array; the array's length is the total. */
|
|
210
|
+
slideKeys: string[];
|
|
211
|
+
/** Number of slides visible per page (default `1`). */
|
|
212
|
+
slidesPerPage: number;
|
|
213
|
+
/** Resolved slides advanced per Prev/Next click — equal to
|
|
214
|
+
* `slidesPerPage` when the consumer left `slidesPerMove="auto"`,
|
|
215
|
+
* else the numeric value. */
|
|
216
|
+
effectiveSlidesPerMove: number;
|
|
217
|
+
/** Live total page count — `ceil(total / slidesPerPage)` in `"auto"`
|
|
218
|
+
* mode (partial last page allowed), else
|
|
219
|
+
* `floor((total - slidesPerPage) / effectiveSlidesPerMove) + 1`
|
|
220
|
+
* (always full-windowed). Drives indicator count and boundary
|
|
221
|
+
* clamp. */
|
|
222
|
+
totalPages: number;
|
|
223
|
+
/** Zero-based index of the currently-active page. */
|
|
224
|
+
currentPage: number;
|
|
225
|
+
/** `true` when there is a forward navigation target (a page ahead of
|
|
226
|
+
* the active one). Drives the `disabled` attribute on
|
|
227
|
+
* `Carousel.NextTrigger` and short-circuits `next()` when there's
|
|
228
|
+
* nowhere to go. */
|
|
229
|
+
canGoNext: boolean;
|
|
230
|
+
/** `true` when there is a backward navigation target. Drives the
|
|
231
|
+
* `disabled` attribute on `Carousel.PreviousTrigger`. */
|
|
232
|
+
canGoPrevious: boolean;
|
|
233
|
+
/** Advance the active page by one step. No-op when `!canGoNext`. */
|
|
234
|
+
next: () => void;
|
|
235
|
+
/** Retreat the active page by one step. No-op when `!canGoPrevious`. */
|
|
236
|
+
previous: () => void;
|
|
237
|
+
/** Jump directly to `target` (zero-based page index). Used by
|
|
238
|
+
* `Carousel.Indicator` to dispatch click-to-jump. */
|
|
239
|
+
goTo: (target: number) => void;
|
|
240
|
+
/** Whether autoplay is currently in the "playing" state. */
|
|
241
|
+
playing: boolean;
|
|
242
|
+
/** Toggles `playing`. Used by `Carousel.PlayPauseTrigger`. */
|
|
243
|
+
togglePlaying: () => void;
|
|
244
|
+
/** `true` when the consumer enabled autoplay (regardless of the
|
|
245
|
+
* `playing` flag). Used by `Carousel.PlayPauseTrigger` to validate
|
|
246
|
+
* its own configuration. */
|
|
247
|
+
autoplayEnabled: boolean;
|
|
248
|
+
/** `true` when autoplay is enabled AND `playing` is `true` — i.e. the
|
|
249
|
+
* timer could fire any moment. Drives the Viewport's `aria-live`
|
|
250
|
+
* flip ("off" while auto-rotating, "polite" otherwise) so screen
|
|
251
|
+
* readers don't get spammed by every tick. */
|
|
252
|
+
isAutoRotating: boolean;
|
|
253
|
+
/** Translations merged with English defaults — every field is
|
|
254
|
+
* present, even if the consumer passed only a subset. */
|
|
255
|
+
translations: Required<CarouselTranslations>;
|
|
256
|
+
/** Custom DOM ids — every field optional. Sub-components apply
|
|
257
|
+
* their respective entry via spread, so consumer-supplied `id`
|
|
258
|
+
* props on the sub-component still win. */
|
|
259
|
+
ids: CarouselIds;
|
|
260
|
+
/** Resolved visual transition mode (defaults to `"slide"`). */
|
|
261
|
+
transition: CarouselTransition;
|
|
262
|
+
/** Resolved scroll-snap alignment (defaults to `"start"`). */
|
|
263
|
+
snapAlign: CarouselSnapAlign;
|
|
264
|
+
/** Bumped by `refresh()` to force the viewport's scroll-align
|
|
265
|
+
* effect to re-run without a page change. */
|
|
266
|
+
refreshTick: number;
|
|
267
|
+
/** Live set of slide indices currently visible per IntersectionObserver
|
|
268
|
+
* (≥ 60% intersection). Mutated by the Viewport hook and read by
|
|
269
|
+
* the imperative `isInView`. */
|
|
270
|
+
visibleSlideIndicesRef: RefObject<Set<number>>;
|
|
271
|
+
/** Used by the Viewport hook to record visibility transitions. */
|
|
272
|
+
setSlideInView: (slideIndex: number, inView: boolean) => void;
|
|
273
|
+
/** Set to `true` by `next()` and `previous()` the moment programmatic
|
|
274
|
+
* navigation begins, and cleared by the Viewport hook once the scroll
|
|
275
|
+
* animation settles (`scrollend` or a timeout fallback). The
|
|
276
|
+
* IntersectionObserver callback checks this flag before calling `goTo`
|
|
277
|
+
* so that IO entries firing mid-animation cannot undo the navigation. */
|
|
278
|
+
isProgrammaticScrollRef: RefObject<boolean>;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
export type CarouselViewportProps = ComponentProps<"div">;
|
|
282
|
+
|
|
283
|
+
export type CarouselSlideProps = Omit<ComponentProps<"div">, "aria-label"> & {
|
|
284
|
+
/** Override the auto-generated `"N of M"` `aria-label`. Use this when
|
|
285
|
+
* the slide has a more meaningful description than its position
|
|
286
|
+
* (e.g. `"Hand-picked for you"`). When omitted, slides are labelled
|
|
287
|
+
* with their live index and total in registration order. */
|
|
288
|
+
ariaLabel?: string;
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
export type CarouselNextTriggerProps = ComponentProps<"button"> & {
|
|
292
|
+
/** Render the child element instead of the default `<button>`. All
|
|
293
|
+
* trigger props (onClick, disabled, ids.nextTrigger, …) are merged
|
|
294
|
+
* onto the child via `Slot`. The child must accept a `ref`. */
|
|
295
|
+
asChild?: boolean;
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
export type CarouselPreviousTriggerProps = ComponentProps<"button"> & {
|
|
299
|
+
/** See `CarouselNextTriggerProps.asChild`. */
|
|
300
|
+
asChild?: boolean;
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Discriminated label shape for `Carousel.IndicatorGroup` — exactly one
|
|
305
|
+
* of `label` (becomes `aria-label`) or `ariaLabelledBy` (points at an
|
|
306
|
+
* external label element) must be supplied. TypeScript rejects
|
|
307
|
+
* both-or-neither at compile time.
|
|
308
|
+
*/
|
|
309
|
+
export type CarouselIndicatorGroupProps = Omit<
|
|
310
|
+
ComponentProps<"div">,
|
|
311
|
+
"label" | "aria-labelledby"
|
|
312
|
+
> &
|
|
313
|
+
(
|
|
314
|
+
| { label: string; ariaLabelledBy?: never }
|
|
315
|
+
| { label?: never; ariaLabelledBy: string }
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
export type CarouselIndicatorProps = ComponentProps<"button"> & {
|
|
319
|
+
/** Zero-based page this indicator targets. Clicking jumps to it. */
|
|
320
|
+
index: number;
|
|
321
|
+
/** Render the child element instead of the default `<button>`.
|
|
322
|
+
* Trigger props (onClick, aria-label, aria-disabled, data-state)
|
|
323
|
+
* are merged onto the child via `Slot`. The child must accept a
|
|
324
|
+
* `ref`. */
|
|
325
|
+
asChild?: boolean;
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Imperative handle exposed via `ref` on `Carousel.Root`. Routes
|
|
330
|
+
* through the same internal state machine the trigger components
|
|
331
|
+
* use, so controlled-mode `onPageChange` / `onPlayingChange` still
|
|
332
|
+
* fire as if the user had clicked.
|
|
333
|
+
*/
|
|
334
|
+
export type CarouselImperativeApi = {
|
|
335
|
+
/** Advance the active page by one. No-op on the last page. */
|
|
336
|
+
next: () => void;
|
|
337
|
+
/** Retreat the active page by one. No-op on the first page. */
|
|
338
|
+
previous: () => void;
|
|
339
|
+
/** Jump directly to `target` (zero-based page index). */
|
|
340
|
+
goTo: (target: number) => void;
|
|
341
|
+
/** Set `playing` to `true`. Dismisses the hover/focus pause for the
|
|
342
|
+
* lifetime of the resulting playing session. */
|
|
343
|
+
play: () => void;
|
|
344
|
+
/** Set `playing` to `false`. */
|
|
345
|
+
pause: () => void;
|
|
346
|
+
/** Re-issue the viewport's scrollTo for the current page. Call when
|
|
347
|
+
* external layout changes (window resize, container reflow) leave
|
|
348
|
+
* the scroll position misaligned with React state. */
|
|
349
|
+
refresh: () => void;
|
|
350
|
+
/** Live progress snapshot: the active page, the total page count,
|
|
351
|
+
* and a normalised `value` in `[0, 1]` (0 when there's at most one
|
|
352
|
+
* page). */
|
|
353
|
+
getProgress: () => { page: number; totalPages: number; value: number };
|
|
354
|
+
/** Reports whether the slide at the zero-based index is currently
|
|
355
|
+
* visible in the viewport (per IntersectionObserver, ≥ 60%
|
|
356
|
+
* intersection). Useful for lazy-loading slide content. */
|
|
357
|
+
isInView: (slideIndex: number) => boolean;
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
export type CarouselProgress = {
|
|
361
|
+
page: number;
|
|
362
|
+
totalPages: number;
|
|
363
|
+
value: number;
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Convenience wrapper that auto-renders one `Carousel.Indicator` per
|
|
368
|
+
* registered slide. Reuses the same discriminated label shape as
|
|
369
|
+
* `Carousel.IndicatorGroup`. `children` is reserved (the auto-mapped
|
|
370
|
+
* indicators take that slot) — drop down to `Carousel.IndicatorGroup`
|
|
371
|
+
* + `Carousel.Indicator` if you need custom indicator content.
|
|
372
|
+
*/
|
|
373
|
+
export type CarouselIndicatorsProps = Omit<
|
|
374
|
+
ComponentProps<"div">,
|
|
375
|
+
"label" | "aria-labelledby" | "children"
|
|
376
|
+
> &
|
|
377
|
+
(
|
|
378
|
+
| { label: string; ariaLabelledBy?: never }
|
|
379
|
+
| { label?: never; ariaLabelledBy: string }
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Render-prop or static children supported by
|
|
384
|
+
* `Carousel.PlayPauseTrigger`. The function form receives the live
|
|
385
|
+
* `playing` flag so consumers can swap icons / labels per state.
|
|
386
|
+
*/
|
|
387
|
+
export type CarouselPlayPauseTriggerChildren =
|
|
388
|
+
| ReactNode
|
|
389
|
+
| ((state: { playing: boolean }) => ReactNode);
|
|
390
|
+
|
|
391
|
+
export type CarouselPlayPauseTriggerProps = Omit<
|
|
392
|
+
ComponentProps<"button">,
|
|
393
|
+
"children"
|
|
394
|
+
> & {
|
|
395
|
+
children?: CarouselPlayPauseTriggerChildren;
|
|
396
|
+
/** Render the child element instead of the default `<button>`.
|
|
397
|
+
* The child must accept a `ref`. The render-prop form of `children`
|
|
398
|
+
* is not supported under `asChild`; pass a single element instead. */
|
|
399
|
+
asChild?: boolean;
|
|
400
|
+
};
|