@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,77 @@
|
|
|
1
|
+
import { useCallback, useEffect, useId, useRef } from "react";
|
|
2
|
+
import type { KeyboardEvent } from "react";
|
|
3
|
+
|
|
4
|
+
import { useSliderContext } from "../SliderContext";
|
|
5
|
+
import { getKeyAction, getThumbStyle, snapToStep } from "../utils";
|
|
6
|
+
|
|
7
|
+
export function useSliderThumb() {
|
|
8
|
+
const {
|
|
9
|
+
values,
|
|
10
|
+
min,
|
|
11
|
+
max,
|
|
12
|
+
step,
|
|
13
|
+
orientation,
|
|
14
|
+
dir,
|
|
15
|
+
inverted,
|
|
16
|
+
disabled,
|
|
17
|
+
registerThumb,
|
|
18
|
+
orderedThumbIds,
|
|
19
|
+
setThumbValue,
|
|
20
|
+
commit,
|
|
21
|
+
} = useSliderContext();
|
|
22
|
+
const id = useId();
|
|
23
|
+
const ref = useRef<HTMLSpanElement>(null);
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
registerThumb(id, ref.current);
|
|
27
|
+
return () => registerThumb(id, null);
|
|
28
|
+
}, [id, registerThumb]);
|
|
29
|
+
|
|
30
|
+
const registeredIndex = orderedThumbIds.indexOf(id);
|
|
31
|
+
const index = registeredIndex === -1 ? 0 : registeredIndex;
|
|
32
|
+
const value = values[index];
|
|
33
|
+
const style = getThumbStyle(value, min, max, { orientation, dir, inverted });
|
|
34
|
+
|
|
35
|
+
const onKeyDown = useCallback(
|
|
36
|
+
(event: KeyboardEvent<HTMLSpanElement>) => {
|
|
37
|
+
if (disabled) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const action = getKeyAction(event.key, { orientation, dir, inverted });
|
|
41
|
+
if (action === null) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
let target: number;
|
|
45
|
+
if (action === "min") {
|
|
46
|
+
target = min;
|
|
47
|
+
} else if (action === "max") {
|
|
48
|
+
target = max;
|
|
49
|
+
} else {
|
|
50
|
+
const isPageKey = event.key === "PageUp" || event.key === "PageDown";
|
|
51
|
+
const magnitude = isPageKey ? step * 10 : step;
|
|
52
|
+
const delta = action === "increase" ? magnitude : -magnitude;
|
|
53
|
+
target = snapToStep(value + delta, min, step);
|
|
54
|
+
}
|
|
55
|
+
event.preventDefault();
|
|
56
|
+
const next = setThumbValue(index, target);
|
|
57
|
+
if (next) {
|
|
58
|
+
commit(next);
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
[
|
|
62
|
+
value,
|
|
63
|
+
index,
|
|
64
|
+
min,
|
|
65
|
+
max,
|
|
66
|
+
step,
|
|
67
|
+
orientation,
|
|
68
|
+
dir,
|
|
69
|
+
inverted,
|
|
70
|
+
disabled,
|
|
71
|
+
setThumbValue,
|
|
72
|
+
commit,
|
|
73
|
+
],
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
return { ref, value, min, max, orientation, disabled, style, onKeyDown };
|
|
77
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ComponentProps } from "react";
|
|
2
|
+
|
|
3
|
+
export type SliderOrientation = "horizontal" | "vertical";
|
|
4
|
+
export type SliderDirection = "ltr" | "rtl";
|
|
5
|
+
|
|
6
|
+
type SliderRootSharedProps = Omit<
|
|
7
|
+
ComponentProps<"span">,
|
|
8
|
+
"defaultValue" | "dir"
|
|
9
|
+
> & {
|
|
10
|
+
min?: number;
|
|
11
|
+
max?: number;
|
|
12
|
+
step?: number;
|
|
13
|
+
minStepsBetweenThumbs?: number;
|
|
14
|
+
orientation?: SliderOrientation;
|
|
15
|
+
dir?: SliderDirection;
|
|
16
|
+
inverted?: boolean;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
name?: string;
|
|
19
|
+
asChild?: boolean;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type SliderRootUncontrolledProps = SliderRootSharedProps & {
|
|
23
|
+
defaultValue?: number[];
|
|
24
|
+
value?: never;
|
|
25
|
+
onValueChange?: (value: number[]) => void;
|
|
26
|
+
onValueCommit?: (value: number[]) => void;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type SliderRootControlledProps = SliderRootSharedProps & {
|
|
30
|
+
defaultValue?: never;
|
|
31
|
+
value: number[];
|
|
32
|
+
onValueChange?: (value: number[]) => void;
|
|
33
|
+
onValueCommit?: (value: number[]) => void;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type SliderRootProps =
|
|
37
|
+
| SliderRootUncontrolledProps
|
|
38
|
+
| SliderRootControlledProps;
|
|
39
|
+
|
|
40
|
+
export type SliderTrackProps = ComponentProps<"span"> & {
|
|
41
|
+
asChild?: boolean;
|
|
42
|
+
};
|
|
43
|
+
export type SliderRangeProps = ComponentProps<"span"> & {
|
|
44
|
+
asChild?: boolean;
|
|
45
|
+
};
|
|
46
|
+
export type SliderThumbProps = ComponentProps<"span"> & {
|
|
47
|
+
asChild?: boolean;
|
|
48
|
+
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
|
|
3
|
+
import type { SliderDirection, SliderOrientation } from "./types";
|
|
4
|
+
|
|
5
|
+
type Edge = "left" | "right" | "top" | "bottom";
|
|
6
|
+
|
|
7
|
+
export type SliderAxisArgs = {
|
|
8
|
+
orientation: SliderOrientation;
|
|
9
|
+
dir: SliderDirection;
|
|
10
|
+
inverted: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type SliderKeyAction = "increase" | "decrease" | "min" | "max";
|
|
14
|
+
|
|
15
|
+
const OPPOSITE_EDGE: Record<Edge, Edge> = {
|
|
16
|
+
left: "right",
|
|
17
|
+
right: "left",
|
|
18
|
+
top: "bottom",
|
|
19
|
+
bottom: "top",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Constrain `value` to the inclusive `[min, max]` range. */
|
|
23
|
+
export function clamp(value: number, min: number, max: number): number {
|
|
24
|
+
return Math.min(Math.max(value, min), max);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Map a value onto its 0–100 position within the `[min, max]` range. */
|
|
28
|
+
export function valueToPercent(value: number, min: number, max: number): number {
|
|
29
|
+
return clamp(((value - min) / (max - min)) * 100, 0, 100);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Round a value to the nearest step, anchored at `min`, at step precision. */
|
|
33
|
+
export function snapToStep(value: number, min: number, step: number): number {
|
|
34
|
+
const snapped = min + Math.round((value - min) / step) * step;
|
|
35
|
+
const decimals = (String(step).split(".")[1] ?? "").length;
|
|
36
|
+
return Number(snapped.toFixed(decimals));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve which physical edge a thumb's offset is anchored to, accounting
|
|
41
|
+
* for orientation, reading direction, and the `inverted` flag.
|
|
42
|
+
*/
|
|
43
|
+
export function getOffsetEdge({ orientation, dir, inverted }: SliderAxisArgs): Edge {
|
|
44
|
+
const base: Edge =
|
|
45
|
+
orientation === "vertical" ? "bottom" : dir === "rtl" ? "right" : "left";
|
|
46
|
+
return inverted ? OPPOSITE_EDGE[base] : base;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Inline style positioning a thumb at its value along the track. */
|
|
50
|
+
export function getThumbStyle(
|
|
51
|
+
value: number,
|
|
52
|
+
min: number,
|
|
53
|
+
max: number,
|
|
54
|
+
edgeArgs: SliderAxisArgs,
|
|
55
|
+
): CSSProperties {
|
|
56
|
+
const edge = getOffsetEdge(edgeArgs);
|
|
57
|
+
return { position: "absolute", [edge]: `${valueToPercent(value, min, max)}%` };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Inline style stretching the range between the lowest and highest thumb
|
|
62
|
+
* (or from the track start to the single thumb).
|
|
63
|
+
*/
|
|
64
|
+
export function getRangeStyle(
|
|
65
|
+
values: number[],
|
|
66
|
+
min: number,
|
|
67
|
+
max: number,
|
|
68
|
+
edgeArgs: SliderAxisArgs,
|
|
69
|
+
): CSSProperties {
|
|
70
|
+
const edge = getOffsetEdge(edgeArgs);
|
|
71
|
+
const percents = values.map((value) => valueToPercent(value, min, max));
|
|
72
|
+
const startPercent = values.length > 1 ? Math.min(...percents) : 0;
|
|
73
|
+
const endPercent = Math.max(...percents);
|
|
74
|
+
return {
|
|
75
|
+
position: "absolute",
|
|
76
|
+
[edge]: `${startPercent}%`,
|
|
77
|
+
[OPPOSITE_EDGE[edge]]: `${100 - endPercent}%`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
type PointerValueArgs = SliderAxisArgs & {
|
|
82
|
+
min: number;
|
|
83
|
+
max: number;
|
|
84
|
+
step: number;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/** Resolve the value at a pointer's position on the track. */
|
|
88
|
+
export function getPointerValue(
|
|
89
|
+
clientX: number,
|
|
90
|
+
clientY: number,
|
|
91
|
+
rect: { left: number; width: number; bottom: number; height: number },
|
|
92
|
+
{ min, max, step, orientation, dir, inverted }: PointerValueArgs,
|
|
93
|
+
): number {
|
|
94
|
+
let percent =
|
|
95
|
+
orientation === "vertical"
|
|
96
|
+
? (rect.bottom - clientY) / rect.height
|
|
97
|
+
: (clientX - rect.left) / rect.width;
|
|
98
|
+
const reversed =
|
|
99
|
+
orientation === "vertical" ? inverted : (dir === "ltr") === inverted;
|
|
100
|
+
if (reversed) {
|
|
101
|
+
percent = 1 - percent;
|
|
102
|
+
}
|
|
103
|
+
percent = clamp(percent, 0, 1);
|
|
104
|
+
return clamp(snapToStep(min + percent * (max - min), min, step), min, max);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Map a key press to an abstract value action, accounting for orientation,
|
|
109
|
+
* reading direction, and the `inverted` flag. Returns `null` for keys the
|
|
110
|
+
* slider does not handle.
|
|
111
|
+
*/
|
|
112
|
+
export function getKeyAction(
|
|
113
|
+
key: string,
|
|
114
|
+
{ orientation, dir, inverted }: SliderAxisArgs,
|
|
115
|
+
): SliderKeyAction | null {
|
|
116
|
+
switch (key) {
|
|
117
|
+
case "Home":
|
|
118
|
+
return "min";
|
|
119
|
+
case "End":
|
|
120
|
+
return "max";
|
|
121
|
+
case "PageUp":
|
|
122
|
+
return "increase";
|
|
123
|
+
case "PageDown":
|
|
124
|
+
return "decrease";
|
|
125
|
+
case "ArrowUp":
|
|
126
|
+
case "ArrowDown": {
|
|
127
|
+
const upIncreases = orientation === "vertical" ? !inverted : true;
|
|
128
|
+
const increases = key === "ArrowUp" ? upIncreases : !upIncreases;
|
|
129
|
+
return increases ? "increase" : "decrease";
|
|
130
|
+
}
|
|
131
|
+
case "ArrowRight":
|
|
132
|
+
case "ArrowLeft": {
|
|
133
|
+
const rightIncreases =
|
|
134
|
+
orientation === "vertical" ? true : (dir === "ltr") !== inverted;
|
|
135
|
+
const increases = key === "ArrowRight" ? rightIncreases : !rightIncreases;
|
|
136
|
+
return increases ? "increase" : "decrease";
|
|
137
|
+
}
|
|
138
|
+
default:
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Index of the thumb whose value sits nearest to `value` (ties favour the first). */
|
|
144
|
+
export function getClosestThumbIndex(value: number, values: number[]): number {
|
|
145
|
+
let closestIndex = 0;
|
|
146
|
+
let smallestDistance = Infinity;
|
|
147
|
+
values.forEach((thumbValue, index) => {
|
|
148
|
+
const distance = Math.abs(thumbValue - value);
|
|
149
|
+
if (distance < smallestDistance) {
|
|
150
|
+
smallestDistance = distance;
|
|
151
|
+
closestIndex = index;
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
return closestIndex;
|
|
155
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slot — a React composition utility following the asChild pattern.
|
|
3
|
+
*
|
|
4
|
+
* When a component renders `<Slot {...ownProps}>{child}</Slot>`, the child
|
|
5
|
+
* element is cloned with ownProps merged in:
|
|
6
|
+
* - Event handlers are **composed** (child's handler runs first, then Slot's).
|
|
7
|
+
* - `style` objects are shallow-merged (child wins on collisions).
|
|
8
|
+
* - `className` strings are concatenated.
|
|
9
|
+
* - All other props default to the child's value, with Slot providing the
|
|
10
|
+
* fallback when the child doesn't specify one.
|
|
11
|
+
* - Refs from both sides are composed via {@link composeRefs}.
|
|
12
|
+
*
|
|
13
|
+
* This file is intentionally self-contained — no external dependencies
|
|
14
|
+
* beyond React itself.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
forwardRef,
|
|
19
|
+
isValidElement,
|
|
20
|
+
cloneElement,
|
|
21
|
+
Children,
|
|
22
|
+
RefObject,
|
|
23
|
+
} from "react";
|
|
24
|
+
import { SlotProps } from "./types";
|
|
25
|
+
import { AnyProps, PossibleRef } from "../types";
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// composeRefs
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
function setRef<T>(ref: PossibleRef<T>, value: T) {
|
|
32
|
+
if (typeof ref === "function") {
|
|
33
|
+
ref(value);
|
|
34
|
+
} else if (ref !== null && ref !== undefined) {
|
|
35
|
+
(ref as RefObject<T>).current = value;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Combines multiple refs into a single callback ref that sets all of them
|
|
41
|
+
* simultaneously. Handles function refs, object refs (`{ current }`), and
|
|
42
|
+
* `undefined` — any of which may be mixed freely.
|
|
43
|
+
*
|
|
44
|
+
* @example Compose an internal ref with a consumer-supplied external ref:
|
|
45
|
+
* ```tsx
|
|
46
|
+
* const internalRef = useRef<HTMLButtonElement>(null);
|
|
47
|
+
* const composedRef = externalRef
|
|
48
|
+
* ? composeRefs(internalRef, externalRef)
|
|
49
|
+
* : internalRef;
|
|
50
|
+
*
|
|
51
|
+
* return <button ref={composedRef} />;
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export function composeRefs<T>(...refs: PossibleRef<T>[]) {
|
|
55
|
+
return (node: T) => refs.forEach((ref) => setRef(ref, node));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// mergeProps — follows the asChild composition pattern rules exactly
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
function getRef(element: React.ReactElement): PossibleRef<unknown> {
|
|
63
|
+
// React 19 exposes ref via props; React ≤18 via element.ref
|
|
64
|
+
const { props, ref } = element as React.ReactElement & {
|
|
65
|
+
ref?: PossibleRef<unknown>;
|
|
66
|
+
};
|
|
67
|
+
return (props as AnyProps).ref !== undefined
|
|
68
|
+
? ((props as AnyProps).ref as PossibleRef<unknown>)
|
|
69
|
+
: ref;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function mergeProps(slotProps: AnyProps, childProps: AnyProps): AnyProps {
|
|
73
|
+
// Child props override by default; event handlers and style/className are
|
|
74
|
+
// special-cased.
|
|
75
|
+
const merged = { ...childProps };
|
|
76
|
+
|
|
77
|
+
for (const key in childProps) {
|
|
78
|
+
const slotVal = slotProps[key];
|
|
79
|
+
const childVal = childProps[key];
|
|
80
|
+
const isEventHandler = /^on[A-Z]/.test(key);
|
|
81
|
+
|
|
82
|
+
if (isEventHandler) {
|
|
83
|
+
if (slotVal && childVal) {
|
|
84
|
+
// Both sides provide a handler — compose: child first, then slot.
|
|
85
|
+
merged[key] = (...args: unknown[]) => {
|
|
86
|
+
(childVal as (...a: unknown[]) => unknown)(...args);
|
|
87
|
+
(slotVal as (...a: unknown[]) => unknown)(...args);
|
|
88
|
+
};
|
|
89
|
+
} else if (slotVal) {
|
|
90
|
+
merged[key] = slotVal;
|
|
91
|
+
}
|
|
92
|
+
} else if (key === "style") {
|
|
93
|
+
merged[key] = { ...(slotVal as object), ...(childVal as object) };
|
|
94
|
+
} else if (key === "className") {
|
|
95
|
+
merged[key] = [slotVal, childVal].filter(Boolean).join(" ");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { ...slotProps, ...merged };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// Slot component
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Renders its single child element with the Slot's own props merged in.
|
|
108
|
+
*
|
|
109
|
+
* Used to implement the `asChild` pattern: a component that normally renders
|
|
110
|
+
* its own DOM element can instead delegate to a consumer-supplied element
|
|
111
|
+
* while preserving all of its own behaviour (ARIA attributes, event handlers,
|
|
112
|
+
* `ref`, etc.).
|
|
113
|
+
*
|
|
114
|
+
* **Prop-merging rules**:
|
|
115
|
+
* - **Event handlers** compose — child's handler fires first, then Slot's.
|
|
116
|
+
* - **`style`** is shallow-merged — child wins on key collisions.
|
|
117
|
+
* - **`className`** strings are concatenated (`slotClass childClass`).
|
|
118
|
+
* - **All other props** default to the child's value; Slot provides the
|
|
119
|
+
* fallback when the child doesn't specify one.
|
|
120
|
+
* - **Refs** from both sides are composed via {@link composeRefs}.
|
|
121
|
+
*
|
|
122
|
+
* **Constraints**
|
|
123
|
+
* - Exactly one React element child is required; Slot throws otherwise.
|
|
124
|
+
* - The child must accept a `ref` (i.e. a DOM element or a `forwardRef`
|
|
125
|
+
* component).
|
|
126
|
+
*
|
|
127
|
+
* **React version compatibility.** Slot reads the child's ref from
|
|
128
|
+
* `element.props.ref` (React 19+) with a fallback to `element.ref`
|
|
129
|
+
* (React ≤18) so both runtime versions compose refs correctly.
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* ```tsx
|
|
133
|
+
* // Inside a component that normally renders <button>:
|
|
134
|
+
* if (asChild) {
|
|
135
|
+
* return <Slot {...buttonProps}>{children}</Slot>;
|
|
136
|
+
* }
|
|
137
|
+
* return <button {...buttonProps}>{children}</button>;
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
export const Slot = forwardRef<HTMLElement, SlotProps>(
|
|
141
|
+
({ children, ...slotProps }, forwardedRef) => {
|
|
142
|
+
if (Children.count(children) !== 1 || !isValidElement(children)) {
|
|
143
|
+
throw new Error("Slot requires exactly one React element child.");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const childRef = getRef(children);
|
|
147
|
+
const composedRef = forwardedRef
|
|
148
|
+
? composeRefs(forwardedRef, childRef)
|
|
149
|
+
: childRef;
|
|
150
|
+
|
|
151
|
+
return cloneElement(children, {
|
|
152
|
+
...mergeProps(slotProps as AnyProps, children.props as AnyProps),
|
|
153
|
+
ref: composedRef,
|
|
154
|
+
} as AnyProps);
|
|
155
|
+
},
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
Slot.displayName = "Slot";
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import React, { createRef } from "react";
|
|
4
|
+
|
|
5
|
+
import { Slot } from "../Slot";
|
|
6
|
+
|
|
7
|
+
describe("Slot tests", () => {
|
|
8
|
+
describe("error handling", () => {
|
|
9
|
+
it("throws when given zero children", () => {
|
|
10
|
+
// Arrange & Assert
|
|
11
|
+
expect(() => {
|
|
12
|
+
render(<Slot />);
|
|
13
|
+
}).toThrow("Slot requires exactly one React element child.");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("throws when given a non-element child (string)", () => {
|
|
17
|
+
expect(() => {
|
|
18
|
+
// Arrange & Assert
|
|
19
|
+
render(<Slot>{"plain text"}</Slot>);
|
|
20
|
+
}).toThrow("Slot requires exactly one React element child.");
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("mergeProps — event handlers", () => {
|
|
25
|
+
it("uses the slot handler when only the slot provides one (key absent from child)", async () => {
|
|
26
|
+
// Arrange
|
|
27
|
+
const user = userEvent.setup();
|
|
28
|
+
const slotClick = vi.fn();
|
|
29
|
+
render(
|
|
30
|
+
<Slot onClick={slotClick}>
|
|
31
|
+
{/* child has no onClick key at all */}
|
|
32
|
+
<button type="button">Click me</button>
|
|
33
|
+
</Slot>,
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
// Act
|
|
37
|
+
await user.click(screen.getByRole("button"));
|
|
38
|
+
|
|
39
|
+
// Assert
|
|
40
|
+
expect(slotClick).toHaveBeenCalledTimes(1);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("uses the slot handler when child has the key but sets it to undefined", async () => {
|
|
44
|
+
// Arrange
|
|
45
|
+
const user = userEvent.setup();
|
|
46
|
+
const slotClick = vi.fn();
|
|
47
|
+
render(
|
|
48
|
+
<Slot onClick={slotClick}>
|
|
49
|
+
{/* child explicitly passes onClick={undefined} — key is present but falsy */}
|
|
50
|
+
<button type="button" onClick={undefined}>
|
|
51
|
+
Click me
|
|
52
|
+
</button>
|
|
53
|
+
</Slot>,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// Act
|
|
57
|
+
await user.click(screen.getByRole("button"));
|
|
58
|
+
|
|
59
|
+
// Assert
|
|
60
|
+
expect(slotClick).toHaveBeenCalledTimes(1);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("preserves the child handler unchanged when the slot has no handler for that key", async () => {
|
|
64
|
+
// Arrange
|
|
65
|
+
const user = userEvent.setup();
|
|
66
|
+
const childClick = vi.fn();
|
|
67
|
+
render(
|
|
68
|
+
// Slot provides no onClick — child's handler should fire unaffected
|
|
69
|
+
<Slot>
|
|
70
|
+
<button type="button" onClick={childClick}>
|
|
71
|
+
Click me
|
|
72
|
+
</button>
|
|
73
|
+
</Slot>,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// Act
|
|
77
|
+
await user.click(screen.getByRole("button"));
|
|
78
|
+
|
|
79
|
+
// Assert
|
|
80
|
+
expect(childClick).toHaveBeenCalledTimes(1);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("mergeProps — style", () => {
|
|
85
|
+
it("shallow-merges style objects, child wins on collisions", () => {
|
|
86
|
+
// Arrange
|
|
87
|
+
const { container } = render(
|
|
88
|
+
<Slot style={{ color: "red", fontWeight: "bold" }}>
|
|
89
|
+
<span style={{ color: "blue" }}>text</span>
|
|
90
|
+
</Slot>,
|
|
91
|
+
);
|
|
92
|
+
const el = container.firstChild as HTMLElement;
|
|
93
|
+
|
|
94
|
+
// Assert
|
|
95
|
+
expect(el.style.color).toBe("blue"); // child wins on collision
|
|
96
|
+
expect(el.style.fontWeight).toBe("bold"); // slot contributes non-colliding prop
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("getRef", () => {
|
|
101
|
+
it("reads ref from props.ref when present (React 19: child element carries its own ref)", () => {
|
|
102
|
+
// Arrange
|
|
103
|
+
// In React 19, a ref passed directly to a child element lands in props.ref.
|
|
104
|
+
// getRef must return it so composeRefs can wire it together with the Slot's
|
|
105
|
+
// own forwardedRef — otherwise the child's ref is silently dropped.
|
|
106
|
+
const childRef = createRef<HTMLDivElement>();
|
|
107
|
+
const slotRef = createRef<HTMLElement>();
|
|
108
|
+
const SlotWithRef = React.forwardRef<HTMLElement>((_props, ref) => (
|
|
109
|
+
// child element carries its own ref — this exercises the props.ref branch
|
|
110
|
+
<Slot ref={ref}>
|
|
111
|
+
<div ref={childRef}>text</div>
|
|
112
|
+
</Slot>
|
|
113
|
+
));
|
|
114
|
+
render(<SlotWithRef ref={slotRef} />);
|
|
115
|
+
|
|
116
|
+
// Assert
|
|
117
|
+
// Both refs populated: childRef via props.ref (React 19 path in getRef),
|
|
118
|
+
// slotRef via the Slot's forwardedRef composed together.
|
|
119
|
+
expect(slotRef.current).not.toBeNull();
|
|
120
|
+
expect(childRef.current).not.toBeNull();
|
|
121
|
+
expect(childRef.current?.tagName).toBe("DIV");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("falls back to element.ref when props.ref is absent (React ≤18 element structure)", () => {
|
|
125
|
+
// Arrange
|
|
126
|
+
// In React ≤18, ref is on element.ref and absent from element.props.
|
|
127
|
+
// getRef must fall back to element.ref so composeRefs can wire both refs.
|
|
128
|
+
const childRef = createRef<HTMLDivElement>();
|
|
129
|
+
const slotRef = createRef<HTMLElement>();
|
|
130
|
+
// Use a real element as the base so $$typeof matches the runtime, then
|
|
131
|
+
// overlay the React ≤18 shape: ref on the element object, absent from props.
|
|
132
|
+
const baseElement = React.createElement("div", null, "text");
|
|
133
|
+
const react18Element = {
|
|
134
|
+
...baseElement,
|
|
135
|
+
ref: childRef, // React ≤18: ref lives here …
|
|
136
|
+
props: { children: "text" }, // … not in props
|
|
137
|
+
} as unknown as React.ReactElement;
|
|
138
|
+
const SlotWithRef = React.forwardRef<HTMLElement>((_props, ref) => (
|
|
139
|
+
<Slot ref={ref}>{react18Element}</Slot>
|
|
140
|
+
));
|
|
141
|
+
render(<SlotWithRef ref={slotRef} />);
|
|
142
|
+
|
|
143
|
+
// Assert
|
|
144
|
+
expect(slotRef.current).not.toBeNull();
|
|
145
|
+
expect(childRef.current).not.toBeNull();
|
|
146
|
+
expect(childRef.current?.tagName).toBe("DIV");
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("mergeProps — className", () => {
|
|
151
|
+
it("concatenates slot and child classNames", () => {
|
|
152
|
+
// Arrange
|
|
153
|
+
render(
|
|
154
|
+
<Slot className="slot-class">
|
|
155
|
+
<span className="child-class">text</span>
|
|
156
|
+
</Slot>,
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
// Assert
|
|
160
|
+
expect(screen.getByText("text")).toHaveClass("slot-class", "child-class");
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { composeEventHandlers } from "../composeEventHandlers";
|
|
2
|
+
|
|
3
|
+
describe("composeEventHandlers", () => {
|
|
4
|
+
it("returns a function that calls the consumer's handler first, then ours", () => {
|
|
5
|
+
// Arrange
|
|
6
|
+
const calls: string[] = [];
|
|
7
|
+
const theirs = () => calls.push("theirs");
|
|
8
|
+
const ours = () => calls.push("ours");
|
|
9
|
+
const handler = composeEventHandlers(theirs, ours);
|
|
10
|
+
|
|
11
|
+
// Act
|
|
12
|
+
handler({} as Event);
|
|
13
|
+
|
|
14
|
+
// Assert
|
|
15
|
+
expect(calls).toEqual(["theirs", "ours"]);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("passes the event through to both handlers unchanged", () => {
|
|
19
|
+
// Arrange
|
|
20
|
+
const theirs = vi.fn();
|
|
21
|
+
const ours = vi.fn();
|
|
22
|
+
const handler = composeEventHandlers(theirs, ours);
|
|
23
|
+
const event = { type: "click" } as unknown as Event;
|
|
24
|
+
|
|
25
|
+
// Act
|
|
26
|
+
handler(event);
|
|
27
|
+
|
|
28
|
+
// Assert
|
|
29
|
+
expect(theirs).toHaveBeenCalledWith(event);
|
|
30
|
+
expect(ours).toHaveBeenCalledWith(event);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("still calls our handler when the consumer's handler is undefined", () => {
|
|
34
|
+
// Arrange
|
|
35
|
+
const ours = vi.fn();
|
|
36
|
+
const handler = composeEventHandlers<Event>(undefined, ours);
|
|
37
|
+
|
|
38
|
+
// Act
|
|
39
|
+
handler({} as Event);
|
|
40
|
+
|
|
41
|
+
// Assert
|
|
42
|
+
expect(ours).toHaveBeenCalledTimes(1);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("skips our handler when the consumer called event.preventDefault()", () => {
|
|
46
|
+
// Arrange
|
|
47
|
+
const theirs = (event: Event) => event.preventDefault();
|
|
48
|
+
const ours = vi.fn();
|
|
49
|
+
const handler = composeEventHandlers(theirs, ours);
|
|
50
|
+
const event = new Event("click", { cancelable: true });
|
|
51
|
+
|
|
52
|
+
// Act
|
|
53
|
+
handler(event);
|
|
54
|
+
|
|
55
|
+
// Assert
|
|
56
|
+
expect(ours).not.toHaveBeenCalled();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("still runs our handler after preventDefault when checkForDefaultPrevented is false", () => {
|
|
60
|
+
// Arrange
|
|
61
|
+
const theirs = (event: Event) => event.preventDefault();
|
|
62
|
+
const ours = vi.fn();
|
|
63
|
+
const handler = composeEventHandlers(theirs, ours, {
|
|
64
|
+
checkForDefaultPrevented: false,
|
|
65
|
+
});
|
|
66
|
+
const event = new Event("click", { cancelable: true });
|
|
67
|
+
|
|
68
|
+
// Act
|
|
69
|
+
handler(event);
|
|
70
|
+
|
|
71
|
+
// Assert
|
|
72
|
+
expect(ours).toHaveBeenCalledTimes(1);
|
|
73
|
+
});
|
|
74
|
+
});
|