@primitiv-ui/react 0.1.28 → 0.1.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -2
- package/package.json +6 -1
- package/src/AccessibleIcon/AccessibleIcon.tsx +32 -9
- package/src/AccessibleIcon/__tests__/AccessibleIcon.test.tsx +5 -0
- package/src/AccessibleIcon/types.ts +15 -2
- package/src/Accordion/Accordion.tsx +32 -11
- package/src/Accordion/README.md +2 -2
- package/src/Accordion/__tests__/Accordion.asChild.test.tsx +46 -0
- package/src/Accordion/__tests__/Accordion.basic-rendering.test.tsx +15 -0
- package/src/Accordion/__tests__/Accordion.disabled-items.test.tsx +42 -0
- package/src/Accordion/__tests__/Accordion.error-handling.test.tsx +33 -0
- package/src/Accordion/hooks/useAccordionRoot.ts +44 -7
- package/src/Accordion/hooks/useAccordionTrigger.ts +9 -1
- package/src/Accordion/types.ts +72 -21
- package/src/Alert/Alert.tsx +36 -7
- package/src/Alert/__tests__/Alert.test.tsx +5 -0
- package/src/Alert/types.ts +18 -2
- package/src/Avatar/Avatar.tsx +99 -15
- package/src/Avatar/__tests__/Avatar.basic-rendering.test.tsx +10 -0
- package/src/Avatar/__tests__/Avatar.fallback.test.tsx +30 -0
- package/src/Avatar/__tests__/Avatar.image-loading.test.tsx +35 -0
- package/src/Avatar/hooks/useAvatarImage.ts +16 -0
- package/src/Avatar/types.ts +15 -3
- package/src/Breadcrumb/Breadcrumb.tsx +139 -21
- package/src/Breadcrumb/README.md +47 -0
- package/src/Breadcrumb/__tests__/Breadcrumb.basic-rendering.test.tsx +11 -0
- package/src/Breadcrumb/__tests__/Breadcrumb.ellipsis.test.tsx +110 -0
- package/src/Breadcrumb/types.ts +73 -7
- package/src/Button/Button.tsx +2 -0
- package/src/Button/__tests__/Button.basic-rendering.test.tsx +6 -0
- package/src/Button/__tests__/Button.contract.test.tsx +1 -1
- package/src/Button/types.ts +21 -0
- package/src/Carousel/Carousel.tsx +268 -20
- package/src/Carousel/README.md +764 -87
- package/src/Carousel/__tests__/Carousel.autoplay-status.test.tsx +239 -0
- package/src/Carousel/__tests__/Carousel.drag-status.test.tsx +292 -0
- package/src/Carousel/__tests__/Carousel.dynamic-slides.test.tsx +38 -0
- package/src/Carousel/__tests__/Carousel.indicators.test.tsx +101 -0
- package/src/Carousel/__tests__/Carousel.infinite.test.tsx +1453 -0
- package/src/Carousel/__tests__/Carousel.instant-scroll.test.tsx +124 -0
- package/src/Carousel/__tests__/Carousel.intersection-observer.test.tsx +90 -2
- package/src/Carousel/__tests__/Carousel.loop.test.tsx +163 -0
- package/src/Carousel/__tests__/Carousel.loopEngine.test.ts +77 -0
- package/src/Carousel/__tests__/Carousel.mouse-drag.test.tsx +504 -0
- package/src/Carousel/__tests__/Carousel.multi-slide-bounds.test.tsx +91 -0
- package/src/Carousel/__tests__/Carousel.orientation.test.tsx +207 -0
- package/src/Carousel/__tests__/Carousel.overscroll.test.tsx +532 -0
- package/src/Carousel/__tests__/Carousel.page-snap-points.test.tsx +50 -0
- package/src/Carousel/__tests__/Carousel.progress-text.test.tsx +76 -0
- package/src/Carousel/__tests__/Carousel.reduced-motion.test.tsx +3 -6
- package/src/Carousel/__tests__/Carousel.refresh-progress.test.tsx +2 -5
- package/src/Carousel/__tests__/Carousel.scroll-margin.test.tsx +167 -0
- package/src/Carousel/__tests__/Carousel.scroll-progress.test.tsx +518 -0
- package/src/Carousel/__tests__/Carousel.scroll-snap-change.test.tsx +32 -11
- package/src/Carousel/__tests__/Carousel.scroll-sync.test.tsx +237 -24
- package/src/Carousel/__tests__/Carousel.scroll-to-index.test.tsx +115 -0
- package/src/Carousel/__tests__/Carousel.slide-snap.test.tsx +172 -0
- package/src/Carousel/__tests__/Carousel.slides-per-move.test.tsx +179 -1
- package/src/Carousel/__tests__/Carousel.slides-per-page.test.tsx +91 -7
- package/src/Carousel/__tests__/Carousel.snap-type.test.tsx +35 -0
- package/src/Carousel/__tests__/Carousel.transition-modes.test.tsx +30 -5
- package/src/Carousel/__tests__/Carousel.translations.test.tsx +23 -0
- package/src/Carousel/__tests__/Carousel.wheel.test.tsx +160 -0
- package/src/Carousel/hooks/index.ts +1 -0
- package/src/Carousel/hooks/useCarouselLoop.ts +869 -0
- package/src/Carousel/hooks/useCarouselRoot.ts +338 -42
- package/src/Carousel/hooks/useCarouselSlide.ts +44 -9
- package/src/Carousel/hooks/useCarouselViewport.ts +649 -46
- package/src/Carousel/loopEngine.ts +54 -0
- package/src/Carousel/types.ts +458 -36
- package/src/Checkbox/Checkbox.tsx +11 -3
- package/src/Checkbox/__tests__/Checkbox.basic-rendering.test.tsx +14 -0
- package/src/Checkbox/hooks/useCheckboxInput.ts +4 -0
- package/src/Checkbox/types.ts +19 -0
- package/src/CheckboxCard/CheckboxCard.tsx +14 -5
- package/src/CheckboxCard/README.md +1 -1
- package/src/CheckboxCard/__tests__/CheckboxCard.basic-rendering.test.tsx +7 -0
- package/src/CheckboxCard/types.ts +65 -17
- package/src/Collapsible/Collapsible.tsx +49 -3
- package/src/Collapsible/README.md +41 -1
- package/src/Collapsible/__tests__/Collapsible.asChild.test.tsx +89 -0
- package/src/Collapsible/__tests__/Collapsible.basic-rendering.test.tsx +27 -0
- package/src/Collapsible/__tests__/Collapsible.collapsedHeight.test.tsx +140 -0
- package/src/Collapsible/hooks/useCollapsibleTrigger.ts +3 -1
- package/src/Collapsible/types.ts +89 -22
- package/src/Combobox/Combobox.tsx +508 -0
- package/src/Combobox/ComboboxContext.ts +23 -0
- package/src/Combobox/README.md +170 -0
- package/src/Combobox/__tests__/Combobox.asChild.test.tsx +62 -0
- package/src/Combobox/__tests__/Combobox.basic-rendering.test.tsx +16 -0
- package/src/Combobox/__tests__/Combobox.content.test.tsx +36 -0
- package/src/Combobox/__tests__/Combobox.controlled-state.test.tsx +88 -0
- package/src/Combobox/__tests__/Combobox.empty.test.tsx +42 -0
- package/src/Combobox/__tests__/Combobox.ids.test.tsx +66 -0
- package/src/Combobox/__tests__/Combobox.item.test.tsx +76 -0
- package/src/Combobox/__tests__/Combobox.keyboard-interaction.test.tsx +196 -0
- package/src/Combobox/__tests__/Combobox.light-dismiss.test.tsx +101 -0
- package/src/Combobox/__tests__/Combobox.query.test.tsx +105 -0
- package/src/Combobox/hooks/index.ts +1 -0
- package/src/Combobox/hooks/useComboboxRoot.ts +2 -0
- package/src/Combobox/index.ts +3 -0
- package/src/Combobox/types.ts +133 -0
- package/src/ContextMenu/ContextMenu.tsx +270 -4
- package/src/ContextMenu/README.md +1 -1
- package/src/ContextMenu/types.ts +148 -6
- package/src/DirectionProvider/DirectionProvider.tsx +41 -9
- package/src/DirectionProvider/README.md +4 -4
- package/src/DirectionProvider/__tests__/DirectionProvider.test.tsx +5 -0
- package/src/DirectionProvider/types.ts +28 -3
- package/src/Divider/Divider.tsx +24 -10
- package/src/Divider/__tests__/Divider.contract.test.tsx +1 -1
- package/src/Divider/__tests__/Divider.test.tsx +5 -0
- package/src/Divider/types.ts +10 -3
- package/src/Drawer/Drawer.tsx +315 -0
- package/src/Drawer/README.md +124 -0
- package/src/Drawer/__tests__/Drawer.behavior.test.tsx +100 -0
- package/src/Drawer/__tests__/Drawer.focus-trap.test.tsx +55 -0
- package/src/Drawer/__tests__/Drawer.rendering.test.tsx +111 -0
- package/src/Drawer/__tests__/Drawer.side.test.tsx +69 -0
- package/src/Drawer/__tests__/dialog-polyfill.ts +39 -0
- package/src/Drawer/index.ts +2 -0
- package/src/Drawer/types.ts +66 -0
- package/src/Dropdown/Dropdown.tsx +119 -2
- package/src/Dropdown/README.md +7 -5
- package/src/Dropdown/types.ts +241 -11
- package/src/EmptyState/EmptyState.tsx +74 -4
- package/src/EmptyState/README.md +2 -2
- package/src/EmptyState/__tests__/EmptyState.Actions.test.tsx +7 -2
- package/src/EmptyState/__tests__/EmptyState.Description.test.tsx +3 -1
- package/src/EmptyState/__tests__/EmptyState.Root.test.tsx +10 -0
- package/src/EmptyState/__tests__/EmptyState.Title.test.tsx +3 -1
- package/src/EmptyState/types.ts +49 -6
- package/src/Field/Field.tsx +23 -0
- package/src/Field/README.md +1 -1
- package/src/Field/__tests__/Field.basic-rendering.test.tsx +11 -0
- package/src/Field/__tests__/Field.error-handling.test.tsx +23 -0
- package/src/Field/__tests__/Field.state-cascade.test.tsx +21 -0
- package/src/Field/hooks/__tests__/useFieldProps.test.tsx +104 -9
- package/src/Field/hooks/useFieldProps.ts +1 -1
- package/src/Field/types.ts +60 -12
- package/src/Fieldset/Fieldset.tsx +55 -19
- package/src/Fieldset/README.md +2 -2
- package/src/Fieldset/__tests__/Fieldset.basic-rendering.test.tsx +9 -0
- package/src/Fieldset/types.ts +20 -2
- package/src/Input/Input.tsx +15 -10
- package/src/Input/README.md +2 -2
- package/src/Input/__tests__/Input.basic-rendering.test.tsx +5 -0
- package/src/Input/types.ts +19 -2
- package/src/InputGroup/InputGroup.tsx +11 -3
- package/src/InputGroup/__tests__/InputGroup.basic-rendering.test.tsx +14 -0
- package/src/InputGroup/types.ts +26 -5
- package/src/Listbox/Listbox.tsx +475 -0
- package/src/Listbox/ListboxContext.ts +48 -0
- package/src/Listbox/README.md +248 -0
- package/src/Listbox/__tests__/Listbox.active-descendant.test.tsx +128 -0
- package/src/Listbox/__tests__/Listbox.asChild.test.tsx +91 -0
- package/src/Listbox/__tests__/Listbox.basic-rendering.test.tsx +16 -0
- package/src/Listbox/__tests__/Listbox.controlled-state.test.tsx +114 -0
- package/src/Listbox/__tests__/Listbox.disabled.test.tsx +90 -0
- package/src/Listbox/__tests__/Listbox.fixtures.ts +27 -0
- package/src/Listbox/__tests__/Listbox.group-label.test.tsx +97 -0
- package/src/Listbox/__tests__/Listbox.group.test.tsx +45 -0
- package/src/Listbox/__tests__/Listbox.keyboard-edge-cases.test.tsx +144 -0
- package/src/Listbox/__tests__/Listbox.keyboard-interaction.test.tsx +112 -0
- package/src/Listbox/__tests__/Listbox.modifier-keys.test.tsx +86 -0
- package/src/Listbox/__tests__/Listbox.multi-select-keyboard.test.tsx +368 -0
- package/src/Listbox/__tests__/Listbox.multiple-mode.test.tsx +91 -0
- package/src/Listbox/__tests__/Listbox.mutation-hardening.test.tsx +480 -0
- package/src/Listbox/__tests__/Listbox.orientation.test.tsx +59 -0
- package/src/Listbox/__tests__/Listbox.reading-direction.test.tsx +65 -0
- package/src/Listbox/__tests__/Listbox.reorder.test.tsx +78 -0
- package/src/Listbox/__tests__/Listbox.selection-follows-focus.test.tsx +64 -0
- package/src/Listbox/__tests__/Listbox.strict-context.test.tsx +21 -0
- package/src/Listbox/__tests__/Listbox.typeahead.test.tsx +123 -0
- package/src/Listbox/__tests__/Listbox.uncontrolled-state.test.tsx +48 -0
- package/src/Listbox/constants.ts +2 -0
- package/src/Listbox/hooks/index.ts +1 -0
- package/src/Listbox/hooks/useListboxRoot.ts +407 -0
- package/src/Listbox/index.ts +3 -0
- package/src/Listbox/types.ts +238 -0
- package/src/MillerColumns/MillerColumns.tsx +186 -45
- package/src/MillerColumns/README.md +90 -20
- package/src/MillerColumns/__tests__/MillerColumns.aria.test.tsx +45 -0
- package/src/MillerColumns/__tests__/MillerColumns.auto-scroll.test.tsx +4 -2
- package/src/MillerColumns/__tests__/MillerColumns.basic-rendering.test.tsx +18 -3
- package/src/MillerColumns/__tests__/MillerColumns.data-attributes.test.tsx +48 -0
- package/src/MillerColumns/__tests__/MillerColumns.keyboard-interaction.test.tsx +35 -2
- package/src/MillerColumns/__tests__/MillerColumns.keyboard-selection-state.test.tsx +96 -0
- package/src/MillerColumns/__tests__/MillerColumns.preview-panel.test.tsx +29 -4
- package/src/MillerColumns/__tests__/MillerColumns.preview-visibility.test.tsx +79 -0
- package/src/MillerColumns/__tests__/MillerColumns.resize.test.tsx +189 -0
- package/src/MillerColumns/__tests__/MillerColumns.styled-parts.test.tsx +143 -0
- package/src/MillerColumns/__tests__/MillerColumns.typeahead.test.tsx +104 -0
- package/src/MillerColumns/constants.ts +2 -0
- package/src/MillerColumns/hooks/useMillerColumnsColumn.ts +5 -2
- package/src/MillerColumns/hooks/useMillerColumnsItem.ts +29 -7
- package/src/MillerColumns/hooks/useMillerColumnsResizeHandle.ts +80 -10
- package/src/MillerColumns/hooks/useMillerColumnsRoot.ts +0 -0
- package/src/MillerColumns/index.ts +4 -0
- package/src/MillerColumns/types.ts +136 -30
- package/src/MillerColumns/utils.ts +69 -3
- package/src/Modal/Modal.tsx +35 -8
- package/src/Modal/README.md +24 -15
- package/src/Modal/__tests__/Modal.focus-trap.test.tsx +162 -0
- package/src/Modal/hooks/useModalContent.ts +32 -0
- package/src/Modal/types.ts +110 -29
- package/src/NavigationMenu/NavigationMenu.tsx +890 -0
- package/src/NavigationMenu/NavigationMenuContext.ts +67 -0
- package/src/NavigationMenu/README.md +355 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.anchor-positioning.test.tsx +132 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.asChild.test.tsx +73 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.basic-rendering.test.tsx +92 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.contract.test.tsx +45 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.controlled-state.test.tsx +134 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.error-handling.test.tsx +62 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.fixtures.tsx +79 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.forceMount.test.tsx +52 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.hover-interaction.test.tsx +254 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.indicator.test.tsx +252 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-interaction.test.tsx +294 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-navigation.test.tsx +126 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.link.test.tsx +139 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.motion.test.tsx +568 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.mouse-interaction.test.tsx +76 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.uncontrolled-state.test.tsx +30 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.viewport.test.tsx +113 -0
- package/src/NavigationMenu/constants.ts +5 -0
- package/src/NavigationMenu/hooks/index.ts +7 -0
- package/src/NavigationMenu/hooks/useNavigationMenuEntry.ts +33 -0
- package/src/NavigationMenu/hooks/useNavigationMenuIndicator.ts +70 -0
- package/src/NavigationMenu/hooks/useNavigationMenuLink.ts +72 -0
- package/src/NavigationMenu/hooks/useNavigationMenuRoot.ts +301 -0
- package/src/NavigationMenu/hooks/useNavigationMenuTopLevelEntry.ts +65 -0
- package/src/NavigationMenu/hooks/useNavigationMenuTrigger.ts +158 -0
- package/src/NavigationMenu/hooks/useNavigationMenuViewport.ts +101 -0
- package/src/NavigationMenu/index.ts +3 -0
- package/src/NavigationMenu/types.ts +259 -0
- package/src/NavigationMenu/utils.ts +89 -0
- package/src/Popover/Popover.tsx +427 -0
- package/src/Popover/PopoverContext.ts +13 -0
- package/src/Popover/README.md +106 -0
- package/src/Popover/__tests__/Popover.anchor.test.tsx +38 -0
- package/src/Popover/__tests__/Popover.asChild.test.tsx +160 -0
- package/src/Popover/__tests__/Popover.basic-rendering.test.tsx +106 -0
- package/src/Popover/__tests__/Popover.click-outside.test.tsx +149 -0
- package/src/Popover/__tests__/Popover.close.test.tsx +70 -0
- package/src/Popover/__tests__/Popover.content.test.tsx +58 -0
- package/src/Popover/__tests__/Popover.controlled-state.test.tsx +45 -0
- package/src/Popover/__tests__/Popover.error-handling.test.tsx +51 -0
- package/src/Popover/__tests__/Popover.focus.test.tsx +43 -0
- package/src/Popover/__tests__/Popover.keyboard-interaction.test.tsx +71 -0
- package/src/Popover/__tests__/Popover.mouse-interaction.test.tsx +69 -0
- package/src/Popover/__tests__/Popover.uncontrolled-state.test.tsx +44 -0
- package/src/Popover/hooks/index.ts +3 -0
- package/src/Popover/hooks/usePopoverContent.ts +98 -0
- package/src/Popover/hooks/usePopoverRoot.ts +84 -0
- package/src/Popover/hooks/usePopoverTrigger.ts +33 -0
- package/src/Popover/index.ts +2 -0
- package/src/Popover/types.ts +197 -0
- package/src/Portal/Portal.tsx +50 -10
- package/src/Portal/README.md +2 -2
- package/src/Portal/__tests__/Portal.basic-rendering.test.tsx +5 -0
- package/src/Portal/types.ts +5 -0
- package/src/Progress/Progress.tsx +33 -1
- package/src/Progress/README.md +1 -1
- package/src/Progress/__tests__/Progress.basic-rendering.test.tsx +10 -0
- package/src/Progress/__tests__/Progress.value.test.tsx +21 -0
- package/src/Progress/types.ts +20 -4
- package/src/Radio/Radio.tsx +10 -2
- package/src/Radio/__tests__/Radio.basic-rendering.test.tsx +14 -0
- package/src/Radio/hooks/useRadioRoot.ts +3 -0
- package/src/Radio/types.ts +20 -0
- package/src/RadioCard/RadioCard.tsx +11 -0
- package/src/RadioCard/__tests__/RadioCard.basic-rendering.test.tsx +8 -0
- package/src/RadioCard/__tests__/RadioCard.disabled-items.test.tsx +27 -0
- package/src/RadioCard/__tests__/RadioCard.keyboard-interaction.test.tsx +53 -0
- package/src/RadioCard/__tests__/RadioCard.ref-forwarding.test.tsx +54 -0
- package/src/RadioCard/__tests__/RadioCard.uncontrolled-state.test.tsx +21 -0
- package/src/RadioCard/hooks/useRadioCardRoot.ts +11 -0
- package/src/RadioCard/types.ts +95 -22
- package/src/RadioGroup/README.md +2 -2
- package/src/RadioGroup/RadioGroup.tsx +12 -1
- package/src/RadioGroup/__tests__/RadioGroup.basic-rendering.test.tsx +8 -0
- package/src/RadioGroup/__tests__/RadioGroup.disabled.test.tsx +27 -0
- package/src/RadioGroup/__tests__/RadioGroup.ref-forwarding.test.tsx +29 -0
- package/src/RadioGroup/__tests__/RadioGroup.roving-tabindex.test.tsx +31 -0
- package/src/RadioGroup/hooks/useRadioGroupRoot.ts +11 -0
- package/src/RadioGroup/types.ts +73 -9
- package/src/SegmentedControl/README.md +154 -0
- package/src/SegmentedControl/SegmentedControl.tsx +309 -0
- package/src/SegmentedControl/SegmentedControlContext.ts +28 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.asChild.test.tsx +50 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.basic-rendering.test.tsx +100 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.controlled-state.test.tsx +107 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.disabled-keydown-guards.test.tsx +62 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.disabled.test.tsx +151 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.error-handling.test.tsx +17 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.keyboard-interaction.test.tsx +117 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.orientation.test.tsx +54 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.reading-direction.test.tsx +56 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.ref-forwarding.test.tsx +45 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.roving-tabindex.test.tsx +104 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.uncontrolled-state.test.tsx +94 -0
- package/src/SegmentedControl/hooks/index.ts +2 -0
- package/src/SegmentedControl/hooks/useSegmentedControlContext.ts +1 -0
- package/src/SegmentedControl/hooks/useSegmentedControlRoot.ts +87 -0
- package/src/SegmentedControl/index.ts +2 -0
- package/src/SegmentedControl/types.ts +138 -0
- package/src/Select/README.md +122 -157
- package/src/Select/Select.tsx +596 -69
- package/src/Select/SelectContext.ts +38 -0
- package/src/Select/SelectItemIndicatorContext.ts +29 -0
- package/src/Select/__tests__/Select.asChild.test.tsx +2 -2
- package/src/Select/__tests__/Select.basic-rendering.test.tsx +22 -3
- package/src/Select/__tests__/Select.controlled-state.test.tsx +6 -6
- package/src/Select/__tests__/Select.data-attributes.test.tsx +4 -4
- package/src/Select/__tests__/Select.field-integration.test.tsx +11 -11
- package/src/Select/__tests__/Select.form.test.tsx +38 -0
- package/src/Select/__tests__/Select.group.test.tsx +6 -6
- package/src/Select/__tests__/Select.keyboard-edge.test.tsx +95 -0
- package/src/Select/__tests__/Select.keyboard.test.tsx +124 -0
- package/src/Select/__tests__/Select.mutation-hardening.test.tsx +317 -0
- package/src/Select/__tests__/Select.native-item.test.tsx +30 -0
- package/src/Select/__tests__/Select.open-close.test.tsx +57 -0
- package/src/Select/__tests__/Select.placeholder.test.tsx +7 -7
- package/src/Select/__tests__/Select.rich-asChild.test.tsx +60 -0
- package/src/Select/__tests__/Select.rich-mode.test.tsx +16 -0
- package/src/Select/__tests__/Select.selection.test.tsx +104 -0
- package/src/Select/__tests__/Select.separator.test.tsx +71 -0
- package/src/Select/__tests__/Select.strict-context.test.tsx +36 -0
- package/src/Select/__tests__/Select.typeahead.test.tsx +156 -0
- package/src/Select/__tests__/Select.value-mirror.test.tsx +240 -0
- package/src/Select/constants.ts +8 -0
- package/src/Select/hooks/index.ts +2 -0
- package/src/Select/hooks/useSelectContent.ts +149 -0
- package/src/Select/hooks/useSelectRoot.ts +149 -0
- package/src/Select/types.ts +199 -31
- package/src/SkipNav/README.md +3 -3
- package/src/SkipNav/SkipNav.tsx +72 -32
- package/src/SkipNav/__tests__/SkipNav.basic-rendering.test.tsx +6 -0
- package/src/SkipNav/types.ts +17 -9
- package/src/Slider/README.md +37 -7
- package/src/Slider/Slider.tsx +42 -4
- package/src/Slider/SliderContext.ts +28 -1
- package/src/Slider/types.ts +157 -9
- package/src/Slot/Slot.tsx +94 -19
- package/src/Slot/__tests__/Slot.test.tsx +112 -2
- package/src/Slot/composeEventHandlers.ts +26 -2
- package/src/Slot/types.ts +16 -0
- package/src/SplitButton/README.md +225 -0
- package/src/SplitButton/SplitButton.tsx +457 -0
- package/src/SplitButton/SplitButtonContext.ts +26 -0
- package/src/SplitButton/__tests__/SplitButton.accessibility.test.tsx +55 -0
- package/src/SplitButton/__tests__/SplitButton.asChild.test.tsx +51 -0
- package/src/SplitButton/__tests__/SplitButton.basic-rendering.test.tsx +57 -0
- package/src/SplitButton/__tests__/SplitButton.data-attributes.test.tsx +28 -0
- package/src/SplitButton/__tests__/SplitButton.disabled.test.tsx +79 -0
- package/src/SplitButton/__tests__/SplitButton.error-handling.test.tsx +29 -0
- package/src/SplitButton/__tests__/SplitButton.keyboard-interaction.test.tsx +56 -0
- package/src/SplitButton/__tests__/SplitButton.state-modes.test.tsx +68 -0
- package/src/SplitButton/index.ts +2 -0
- package/src/SplitButton/types.ts +186 -0
- package/src/Status/Status.tsx +36 -15
- package/src/Status/__tests__/Status.test.tsx +5 -0
- package/src/Status/types.ts +12 -1
- package/src/Switch/README.md +1 -1
- package/src/Switch/Switch.tsx +17 -4
- package/src/Switch/__tests__/Switch.basic-rendering.test.tsx +7 -0
- package/src/Switch/hooks/useSwitchRoot.ts +3 -0
- package/src/Switch/types.ts +49 -22
- package/src/Table/README.md +110 -8
- package/src/Table/Table.tsx +246 -16
- package/src/Table/TableExpandableContext.ts +7 -0
- package/src/Table/__tests__/Table.DetailRow.test.tsx +83 -0
- package/src/Table/__tests__/Table.ExpandTrigger.test.tsx +102 -0
- package/src/Table/__tests__/Table.Expandable.test.tsx +121 -0
- package/src/Table/types.ts +191 -17
- package/src/Tabs/README.md +4 -4
- package/src/Tabs/Tabs.tsx +13 -4
- package/src/Tabs/__tests__/Tabs.activation-mode.test.tsx +58 -0
- package/src/Tabs/__tests__/Tabs.basic-rendering.test.tsx +15 -0
- package/src/Tabs/__tests__/Tabs.change-event-callbacks.test.tsx +24 -0
- package/src/Tabs/__tests__/Tabs.disabled-tabs.test.tsx +36 -0
- package/src/Tabs/__tests__/Tabs.lazy-mount.test.tsx +17 -0
- package/src/Tabs/hooks/useTabsContent.ts +5 -0
- package/src/Tabs/hooks/useTabsRoot.ts +29 -5
- package/src/Tabs/hooks/useTabsTrigger.ts +7 -2
- package/src/Tabs/types.ts +29 -4
- package/src/Textarea/Textarea.tsx +9 -1
- package/src/Textarea/__tests__/Textarea.basic-rendering.test.tsx +5 -0
- package/src/Textarea/types.ts +20 -3
- package/src/Toggle/Toggle.tsx +51 -13
- package/src/Toggle/__tests__/Toggle.basic-rendering.test.tsx +5 -0
- package/src/Toggle/types.ts +54 -4
- package/src/ToggleGroup/README.md +4 -4
- package/src/ToggleGroup/ToggleGroup.tsx +14 -1
- package/src/ToggleGroup/__tests__/ToggleGroup.asChild.test.tsx +20 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.basic-rendering.test.tsx +9 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.dynamic-items.test.tsx +70 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.error-handling.test.tsx +23 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.keyboard-interaction.test.tsx +23 -1
- package/src/ToggleGroup/__tests__/ToggleGroup.ref-forwarding.test.tsx +58 -0
- package/src/ToggleGroup/hooks/useToggleGroupRoot.ts +21 -1
- package/src/ToggleGroup/types.ts +55 -2
- package/src/Tooltip/README.md +7 -7
- package/src/Tooltip/Tooltip.tsx +100 -22
- package/src/Tooltip/__tests__/Tooltip.basic-rendering.test.tsx +50 -0
- package/src/Tooltip/__tests__/Tooltip.error-handling.test.tsx +37 -0
- package/src/Tooltip/__tests__/Tooltip.escape-hatches.test.tsx +73 -0
- package/src/Tooltip/__tests__/Tooltip.keyboard-interaction.test.tsx +134 -0
- package/src/Tooltip/__tests__/Tooltip.skip-delay.test.tsx +216 -0
- package/src/Tooltip/__tests__/Tooltip.timer-coordination.test.tsx +223 -0
- package/src/Tooltip/hooks/__tests__/useTooltipProvider.test.tsx +40 -0
- package/src/Tooltip/hooks/useTooltipContent.ts +3 -0
- package/src/Tooltip/hooks/useTooltipProvider.ts +46 -8
- package/src/Tooltip/hooks/useTooltipRoot.ts +30 -6
- package/src/Tooltip/types.ts +89 -8
- package/src/Tree/README.md +69 -5
- package/src/Tree/Tree.tsx +188 -34
- package/src/Tree/__tests__/Tree.styled-wrapper.test.tsx +159 -0
- package/src/Tree/hooks/useTreePath.ts +33 -1
- package/src/Tree/index.ts +6 -1
- package/src/Tree/types.ts +54 -12
- package/src/Tree/utils.ts +46 -4
- package/src/VisuallyHidden/VisuallyHidden.tsx +33 -8
- package/src/VisuallyHidden/__tests__/VisuallyHidden.test.tsx +15 -1
- package/src/VisuallyHidden/types.ts +11 -1
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useTypeahead.ts +95 -0
- package/src/index.ts +10 -0
- package/src/test/intersectionObserverPolyfill.ts +7 -0
- package/src/test/resizeObserverPolyfill.ts +80 -0
- package/src/useLocalStorage/__tests__/useLocalStorage.reading.test.tsx +31 -0
- package/src/useLocalStorage/__tests__/useLocalStorage.resilience.test.tsx +97 -0
- package/src/useLocalStorage/__tests__/useLocalStorage.ssr.test.tsx +20 -0
- package/src/useLocalStorage/__tests__/useLocalStorage.writing.test.tsx +72 -0
- package/src/useLocalStorage/index.ts +1 -0
- package/src/useLocalStorage/useLocalStorage.ts +140 -0
- package/src/useMediaQuery/README.md +66 -0
- package/src/useMediaQuery/__tests__/useMediaQuery.matching.test.tsx +42 -0
- package/src/useMediaQuery/__tests__/useMediaQuery.ssr.test.tsx +19 -0
- package/src/useMediaQuery/__tests__/useMediaQuery.subscription.test.tsx +89 -0
- package/src/useMediaQuery/index.ts +1 -0
- package/src/useMediaQuery/useMediaQuery.ts +49 -0
- package/src/usePagination/README.md +170 -0
- package/src/usePagination/__tests__/usePagination.navigation.test.tsx +109 -0
- package/src/usePagination/__tests__/usePagination.page-count.test.tsx +56 -0
- package/src/usePagination/__tests__/usePagination.page-state.test.tsx +87 -0
- package/src/usePagination/__tests__/usePagination.range.test.tsx +82 -0
- package/src/usePagination/__tests__/usePagination.slice.test.tsx +59 -0
- package/src/usePagination/__tests__/usePagination.windowing.test.tsx +68 -0
- package/src/usePagination/index.ts +1 -0
- package/src/usePagination/usePagination.ts +292 -0
- package/src/utils/focusable.ts +15 -0
- package/src/utils/getKeyToActionMap.ts +1 -1
- package/src/utils/index.ts +1 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { act, fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
|
|
4
|
+
import { Tooltip } from "../Tooltip";
|
|
5
|
+
|
|
6
|
+
describe("Tooltip.Root — timer callbacks stay current across re-renders", () => {
|
|
7
|
+
it("uses the current onOpenChange for both a focus-open and a blur-close after the Provider re-renders", async () => {
|
|
8
|
+
const user = userEvent.setup();
|
|
9
|
+
const firstOnOpenChange = vi.fn();
|
|
10
|
+
const secondOnOpenChange = vi.fn();
|
|
11
|
+
|
|
12
|
+
function Wrapper({
|
|
13
|
+
onOpenChange,
|
|
14
|
+
}: {
|
|
15
|
+
onOpenChange: (open: boolean) => void;
|
|
16
|
+
}) {
|
|
17
|
+
return (
|
|
18
|
+
<Tooltip.Provider>
|
|
19
|
+
<Tooltip.Root onOpenChange={onOpenChange}>
|
|
20
|
+
<Tooltip.Trigger>Focusable</Tooltip.Trigger>
|
|
21
|
+
<Tooltip.Content>Tooltip text</Tooltip.Content>
|
|
22
|
+
</Tooltip.Root>
|
|
23
|
+
<button>Other</button>
|
|
24
|
+
</Tooltip.Provider>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const { rerender } = render(<Wrapper onOpenChange={firstOnOpenChange} />);
|
|
29
|
+
rerender(<Wrapper onOpenChange={secondOnOpenChange} />);
|
|
30
|
+
|
|
31
|
+
await user.tab();
|
|
32
|
+
await user.tab();
|
|
33
|
+
|
|
34
|
+
expect(firstOnOpenChange).not.toHaveBeenCalled();
|
|
35
|
+
expect(secondOnOpenChange).toHaveBeenCalledWith(true);
|
|
36
|
+
expect(secondOnOpenChange).toHaveBeenCalledWith(false);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("Tooltip.Root — timer coordination (fake timers)", () => {
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
vi.useFakeTimers();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
vi.useRealTimers();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("opens synchronously when the effective delay is 0, not via a scheduled timer", () => {
|
|
50
|
+
render(
|
|
51
|
+
<Tooltip.Provider delayDuration={0}>
|
|
52
|
+
<Tooltip.Root>
|
|
53
|
+
<Tooltip.Trigger>Hover me</Tooltip.Trigger>
|
|
54
|
+
<Tooltip.Content>Tooltip text</Tooltip.Content>
|
|
55
|
+
</Tooltip.Root>
|
|
56
|
+
</Tooltip.Provider>,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
fireEvent.pointerEnter(screen.getByRole("button", { name: "Hover me" }));
|
|
60
|
+
|
|
61
|
+
expect(screen.getByRole("tooltip")).toBeInTheDocument();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("cancels a pending hover-delay open timer when the trigger is focused before it elapses", () => {
|
|
65
|
+
const onOpenChange = vi.fn();
|
|
66
|
+
|
|
67
|
+
render(
|
|
68
|
+
<Tooltip.Provider delayDuration={1000}>
|
|
69
|
+
<Tooltip.Root onOpenChange={onOpenChange}>
|
|
70
|
+
<Tooltip.Trigger>Hover me</Tooltip.Trigger>
|
|
71
|
+
<Tooltip.Content>Tooltip text</Tooltip.Content>
|
|
72
|
+
</Tooltip.Root>
|
|
73
|
+
</Tooltip.Provider>,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
fireEvent.pointerEnter(screen.getByRole("button", { name: "Hover me" }));
|
|
77
|
+
fireEvent.focus(screen.getByRole("button", { name: "Hover me" }));
|
|
78
|
+
|
|
79
|
+
expect(onOpenChange).toHaveBeenCalledTimes(1);
|
|
80
|
+
|
|
81
|
+
act(() => {
|
|
82
|
+
vi.advanceTimersByTime(1000);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(onOpenChange).toHaveBeenCalledTimes(1);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("cancels a pending grace-period timer when the tooltip closes immediately during the grace window", () => {
|
|
89
|
+
const onOpenChange = vi.fn();
|
|
90
|
+
|
|
91
|
+
// No Tooltip.Content here: its document-level Escape listener would also
|
|
92
|
+
// fire and call closeImmediate a second time, muddying the call count
|
|
93
|
+
// this test cares about. The trigger's own Escape handler is enough to
|
|
94
|
+
// exercise closeImmediate while a grace-period timer is pending.
|
|
95
|
+
render(
|
|
96
|
+
<Tooltip.Provider delayDuration={0}>
|
|
97
|
+
<Tooltip.Root onOpenChange={onOpenChange}>
|
|
98
|
+
<Tooltip.Trigger>Hover me</Tooltip.Trigger>
|
|
99
|
+
</Tooltip.Root>
|
|
100
|
+
</Tooltip.Provider>,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
fireEvent.pointerEnter(screen.getByRole("button", { name: "Hover me" }));
|
|
104
|
+
expect(onOpenChange).toHaveBeenCalledTimes(1);
|
|
105
|
+
|
|
106
|
+
// Leaving the trigger starts the grace-period close timer...
|
|
107
|
+
fireEvent.pointerLeave(screen.getByRole("button", { name: "Hover me" }));
|
|
108
|
+
// ...then Escape closes immediately, which must cancel that pending timer.
|
|
109
|
+
fireEvent.keyDown(screen.getByRole("button", { name: "Hover me" }), {
|
|
110
|
+
key: "Escape",
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
expect(onOpenChange).toHaveBeenCalledTimes(2);
|
|
114
|
+
|
|
115
|
+
act(() => {
|
|
116
|
+
vi.advanceTimersByTime(100);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
expect(onOpenChange).toHaveBeenCalledTimes(2);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("cancels the grace-period close timer when the pointer moves into the content", () => {
|
|
123
|
+
render(
|
|
124
|
+
<Tooltip.Provider delayDuration={0}>
|
|
125
|
+
<Tooltip.Root>
|
|
126
|
+
<Tooltip.Trigger>Hover me</Tooltip.Trigger>
|
|
127
|
+
<Tooltip.Content>Tooltip text</Tooltip.Content>
|
|
128
|
+
</Tooltip.Root>
|
|
129
|
+
</Tooltip.Provider>,
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
fireEvent.pointerEnter(screen.getByRole("button", { name: "Hover me" }));
|
|
133
|
+
expect(screen.getByRole("tooltip")).toBeInTheDocument();
|
|
134
|
+
|
|
135
|
+
fireEvent.pointerLeave(screen.getByRole("button", { name: "Hover me" }));
|
|
136
|
+
fireEvent.pointerEnter(screen.getByRole("tooltip"));
|
|
137
|
+
|
|
138
|
+
act(() => {
|
|
139
|
+
vi.advanceTimersByTime(100);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(screen.getByRole("tooltip")).toBeInTheDocument();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("closes after the grace period once the pointer leaves the trigger without re-entering the content", () => {
|
|
146
|
+
render(
|
|
147
|
+
<Tooltip.Provider delayDuration={0}>
|
|
148
|
+
<Tooltip.Root>
|
|
149
|
+
<Tooltip.Trigger>Hover me</Tooltip.Trigger>
|
|
150
|
+
<Tooltip.Content>Tooltip text</Tooltip.Content>
|
|
151
|
+
</Tooltip.Root>
|
|
152
|
+
</Tooltip.Provider>,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
fireEvent.pointerEnter(screen.getByRole("button", { name: "Hover me" }));
|
|
156
|
+
expect(screen.getByRole("tooltip")).toBeInTheDocument();
|
|
157
|
+
|
|
158
|
+
fireEvent.pointerLeave(screen.getByRole("button", { name: "Hover me" }));
|
|
159
|
+
|
|
160
|
+
// Still open right up until the grace period elapses.
|
|
161
|
+
expect(screen.getByRole("tooltip")).toBeInTheDocument();
|
|
162
|
+
|
|
163
|
+
act(() => {
|
|
164
|
+
vi.advanceTimersByTime(100);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
expect(screen.queryByRole("tooltip")).toBeNull();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("cancels a pending hover-delay open timer when the pointer leaves before it elapses, so the tooltip does not open later", () => {
|
|
171
|
+
render(
|
|
172
|
+
<Tooltip.Provider delayDuration={500}>
|
|
173
|
+
<Tooltip.Root>
|
|
174
|
+
<Tooltip.Trigger>Hover me</Tooltip.Trigger>
|
|
175
|
+
<Tooltip.Content>Tooltip text</Tooltip.Content>
|
|
176
|
+
</Tooltip.Root>
|
|
177
|
+
</Tooltip.Provider>,
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
fireEvent.pointerEnter(screen.getByRole("button", { name: "Hover me" }));
|
|
181
|
+
fireEvent.pointerLeave(screen.getByRole("button", { name: "Hover me" }));
|
|
182
|
+
|
|
183
|
+
act(() => {
|
|
184
|
+
vi.advanceTimersByTime(500);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
expect(screen.queryByRole("tooltip")).toBeNull();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("uses the current onOpenChange callback for a grace-period close after a re-render", () => {
|
|
191
|
+
const firstOnOpenChange = vi.fn();
|
|
192
|
+
const secondOnOpenChange = vi.fn();
|
|
193
|
+
|
|
194
|
+
function Wrapper({
|
|
195
|
+
onOpenChange,
|
|
196
|
+
}: {
|
|
197
|
+
onOpenChange: (open: boolean) => void;
|
|
198
|
+
}) {
|
|
199
|
+
return (
|
|
200
|
+
<Tooltip.Provider delayDuration={0}>
|
|
201
|
+
<Tooltip.Root onOpenChange={onOpenChange}>
|
|
202
|
+
<Tooltip.Trigger>Hover me</Tooltip.Trigger>
|
|
203
|
+
<Tooltip.Content>Tooltip text</Tooltip.Content>
|
|
204
|
+
</Tooltip.Root>
|
|
205
|
+
</Tooltip.Provider>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const { rerender } = render(<Wrapper onOpenChange={firstOnOpenChange} />);
|
|
210
|
+
rerender(<Wrapper onOpenChange={secondOnOpenChange} />);
|
|
211
|
+
|
|
212
|
+
fireEvent.pointerEnter(screen.getByRole("button", { name: "Hover me" }));
|
|
213
|
+
fireEvent.pointerLeave(screen.getByRole("button", { name: "Hover me" }));
|
|
214
|
+
|
|
215
|
+
act(() => {
|
|
216
|
+
vi.advanceTimersByTime(100);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
expect(firstOnOpenChange).not.toHaveBeenCalled();
|
|
220
|
+
expect(secondOnOpenChange).toHaveBeenCalledWith(true);
|
|
221
|
+
expect(secondOnOpenChange).toHaveBeenCalledWith(false);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
|
|
4
|
+
import { useTooltipProvider } from "../useTooltipProvider";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `useTooltipProvider` schedules its skip-delay reset with a raw
|
|
8
|
+
* `setTimeout`, so it is exercised here directly rather than through the
|
|
9
|
+
* full `Tooltip.Provider` tree — the defect under test (a timer surviving
|
|
10
|
+
* unmount) has no user-facing render output to assert on.
|
|
11
|
+
*/
|
|
12
|
+
function Probe() {
|
|
13
|
+
const { contextValue } = useTooltipProvider({
|
|
14
|
+
delayDuration: 0,
|
|
15
|
+
skipDelayDuration: 300,
|
|
16
|
+
});
|
|
17
|
+
return <button onClick={contextValue.onCloseGlobally}>close</button>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("useTooltipProvider — timer cleanup", () => {
|
|
21
|
+
it("clears the pending skip-delay timeout on unmount instead of letting it fire later", async () => {
|
|
22
|
+
const user = userEvent.setup();
|
|
23
|
+
const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout");
|
|
24
|
+
const clearTimeoutSpy = vi.spyOn(globalThis, "clearTimeout");
|
|
25
|
+
|
|
26
|
+
const { unmount } = render(<Probe />);
|
|
27
|
+
await user.click(screen.getByRole("button", { name: "close" }));
|
|
28
|
+
|
|
29
|
+
// setTimeoutSpy also captures unrelated timers (userEvent's own
|
|
30
|
+
// scheduling, React internals) — find the one this hook scheduled with
|
|
31
|
+
// its 300ms skipDelayDuration.
|
|
32
|
+
const callIndex = setTimeoutSpy.mock.calls.findIndex(
|
|
33
|
+
([, delay]) => delay === 300,
|
|
34
|
+
);
|
|
35
|
+
const timerId = setTimeoutSpy.mock.results[callIndex].value;
|
|
36
|
+
unmount();
|
|
37
|
+
|
|
38
|
+
expect(clearTimeoutSpy).toHaveBeenCalledWith(timerId);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -29,6 +29,9 @@ export function useTooltipContent({
|
|
|
29
29
|
useEffect(() => {
|
|
30
30
|
if (!open) return;
|
|
31
31
|
const handlePointerDown = (event: PointerEvent) => {
|
|
32
|
+
// Stryker disable next-line OptionalChaining: equivalent — this listener
|
|
33
|
+
// is only attached while `open` is true, and the Content `<div>` (and
|
|
34
|
+
// therefore internalRef.current) is always mounted whenever open is true.
|
|
32
35
|
if (internalRef.current?.contains(event.target as Node)) return;
|
|
33
36
|
onPointerDownOutside?.(event);
|
|
34
37
|
if (event.defaultPrevented) return;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useRef, useState } from "react";
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
|
|
3
3
|
import type { TooltipProviderContextValue } from "../types";
|
|
4
4
|
|
|
@@ -14,21 +14,59 @@ export function useTooltipProvider({
|
|
|
14
14
|
const [isOpenGlobally, setIsOpenGlobally] = useState(false);
|
|
15
15
|
const skipDelayTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
16
16
|
|
|
17
|
-
const onOpenGlobally = useCallback(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
const onOpenGlobally = useCallback(
|
|
18
|
+
() => {
|
|
19
|
+
// Stryker disable next-line ConditionalExpression: equivalent —
|
|
20
|
+
// clearTimeout(null) is a spec'd no-op, so forcing this guard true is
|
|
21
|
+
// harmless when no skip-delay timer is pending; the false / never-clear
|
|
22
|
+
// variant is killed by the "clears a pending skip-delay timer when a
|
|
23
|
+
// tooltip opens again within the skip window" test.
|
|
24
|
+
if (skipDelayTimerRef.current !== null) {
|
|
25
|
+
clearTimeout(skipDelayTimerRef.current);
|
|
26
|
+
skipDelayTimerRef.current = null;
|
|
27
|
+
}
|
|
28
|
+
setIsOpenGlobally(true);
|
|
29
|
+
},
|
|
30
|
+
// Stryker disable next-line ArrayDeclaration: equivalent — no dependency to freeze.
|
|
31
|
+
[],
|
|
32
|
+
);
|
|
24
33
|
|
|
25
34
|
const onCloseGlobally = useCallback(() => {
|
|
35
|
+
// Cancel before re-scheduling. The ref tracks only the newest timer, so
|
|
36
|
+
// overwriting it without cancelling orphans the previous one: it survives,
|
|
37
|
+
// fires unobserved, and can no longer be cancelled by a later open. Two
|
|
38
|
+
// closes in a row is not hypothetical — `closeImmediate` is unguarded and
|
|
39
|
+
// both Escape and blur are wired straight to it, so pressing Escape and then
|
|
40
|
+
// tabbing away leaks a timer. `clearTimeout(undefined)` is a spec'd no-op, so
|
|
41
|
+
// this needs no guard for the first close.
|
|
42
|
+
clearTimeout(skipDelayTimerRef.current ?? undefined);
|
|
26
43
|
skipDelayTimerRef.current = setTimeout(() => {
|
|
27
44
|
setIsOpenGlobally(false);
|
|
28
45
|
skipDelayTimerRef.current = null;
|
|
29
46
|
}, skipDelayDuration);
|
|
30
47
|
}, [skipDelayDuration]);
|
|
31
48
|
|
|
49
|
+
// The skip-delay reset is a raw `setTimeout`, so it outlives the
|
|
50
|
+
// component it was scheduled from unless explicitly cancelled — without
|
|
51
|
+
// this, an in-flight timer fires after unmount and can throw once its
|
|
52
|
+
// surrounding environment (e.g. a test's jsdom `window`) is gone.
|
|
53
|
+
useEffect(
|
|
54
|
+
() => {
|
|
55
|
+
return () => {
|
|
56
|
+
// Stryker disable next-line ConditionalExpression: equivalent —
|
|
57
|
+
// clearTimeout(null) is a spec'd no-op, so forcing this guard true is
|
|
58
|
+
// harmless when no skip-delay timer is pending; the false / never-clear
|
|
59
|
+
// variant is killed by the "clears the pending skip-delay timeout on
|
|
60
|
+
// unmount" test.
|
|
61
|
+
if (skipDelayTimerRef.current !== null) {
|
|
62
|
+
clearTimeout(skipDelayTimerRef.current);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
// Stryker disable next-line ArrayDeclaration: equivalent — no dependency to freeze (mount/unmount only).
|
|
67
|
+
[],
|
|
68
|
+
);
|
|
69
|
+
|
|
32
70
|
const contextValue: TooltipProviderContextValue = {
|
|
33
71
|
delayDuration,
|
|
34
72
|
skipDelayDuration,
|
|
@@ -31,6 +31,11 @@ export function useTooltipRoot({
|
|
|
31
31
|
const gracePeriodTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
32
32
|
|
|
33
33
|
const openImmediate = useCallback(() => {
|
|
34
|
+
// Stryker disable next-line ConditionalExpression: equivalent —
|
|
35
|
+
// clearTimeout(null) is a spec'd no-op, so forcing this guard true is
|
|
36
|
+
// harmless when no open timer is pending; the false / never-clear variant
|
|
37
|
+
// is killed by the "cancels a pending hover-delay open timer when the
|
|
38
|
+
// trigger is focused before it elapses" test.
|
|
34
39
|
if (openTimerRef.current !== null) {
|
|
35
40
|
clearTimeout(openTimerRef.current);
|
|
36
41
|
openTimerRef.current = null;
|
|
@@ -52,6 +57,11 @@ export function useTooltipRoot({
|
|
|
52
57
|
}, [openImmediate, effectiveDelayDuration, providerCtx.isOpenGlobally]);
|
|
53
58
|
|
|
54
59
|
const closeImmediate = useCallback(() => {
|
|
60
|
+
// Stryker disable next-line ConditionalExpression: equivalent —
|
|
61
|
+
// clearTimeout(null) is a spec'd no-op, so forcing this guard true is
|
|
62
|
+
// harmless when no grace timer is pending; the false / never-clear variant
|
|
63
|
+
// is killed by the "cancels a pending grace-period timer when the tooltip
|
|
64
|
+
// closes immediately during the grace window" test.
|
|
55
65
|
if (gracePeriodTimerRef.current !== null) {
|
|
56
66
|
clearTimeout(gracePeriodTimerRef.current);
|
|
57
67
|
gracePeriodTimerRef.current = null;
|
|
@@ -60,14 +70,28 @@ export function useTooltipRoot({
|
|
|
60
70
|
providerCtx.onCloseGlobally();
|
|
61
71
|
}, [setOpen, providerCtx]);
|
|
62
72
|
|
|
63
|
-
const cancelGrace = useCallback(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
73
|
+
const cancelGrace = useCallback(
|
|
74
|
+
() => {
|
|
75
|
+
// Stryker disable next-line ConditionalExpression: equivalent —
|
|
76
|
+
// clearTimeout(null) is a spec'd no-op, so forcing this guard true is
|
|
77
|
+
// harmless when no grace timer is pending; the false / never-clear
|
|
78
|
+
// variant is killed by the "cancels the grace-period close timer when
|
|
79
|
+
// the pointer moves into the content" test.
|
|
80
|
+
if (gracePeriodTimerRef.current !== null) {
|
|
81
|
+
clearTimeout(gracePeriodTimerRef.current);
|
|
82
|
+
gracePeriodTimerRef.current = null;
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
// Stryker disable next-line ArrayDeclaration: equivalent — no dependency to freeze.
|
|
86
|
+
[],
|
|
87
|
+
);
|
|
69
88
|
|
|
70
89
|
const closeWithGrace = useCallback(() => {
|
|
90
|
+
// Stryker disable next-line ConditionalExpression: equivalent —
|
|
91
|
+
// clearTimeout(null) is a spec'd no-op, so forcing this guard true is
|
|
92
|
+
// harmless when no open timer is pending; the false / never-clear variant
|
|
93
|
+
// is killed by the "cancels a pending hover-delay open timer when the
|
|
94
|
+
// pointer leaves before it elapses" test.
|
|
71
95
|
if (openTimerRef.current !== null) {
|
|
72
96
|
clearTimeout(openTimerRef.current);
|
|
73
97
|
openTimerRef.current = null;
|
package/src/Tooltip/types.ts
CHANGED
|
@@ -30,12 +30,26 @@ export type TooltipContextValue = {
|
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
|
-
* Props for {@link Tooltip.Provider} — sets the
|
|
34
|
-
*
|
|
33
|
+
* Props for {@link Tooltip.Provider} — sets the delay timings shared by every
|
|
34
|
+
* tooltip in the subtree.
|
|
35
35
|
*/
|
|
36
36
|
export type TooltipProviderProps = {
|
|
37
|
+
/** The tooltips (and other markup) that share this Provider's timings. */
|
|
37
38
|
children?: ReactNode;
|
|
39
|
+
/**
|
|
40
|
+
* How long, in milliseconds, a pointer must rest on a trigger before the
|
|
41
|
+
* tooltip opens on hover. Focus always opens immediately regardless of this.
|
|
42
|
+
* Overridable per tooltip via {@link TooltipRootProps.delayDuration}.
|
|
43
|
+
* @default 700
|
|
44
|
+
*/
|
|
38
45
|
delayDuration?: number;
|
|
46
|
+
/**
|
|
47
|
+
* The grace window, in milliseconds, after a tooltip in this group closes
|
|
48
|
+
* during which hovering *another* trigger in the same Provider opens it
|
|
49
|
+
* instantly (no `delayDuration` wait). Rest longer than this and the next
|
|
50
|
+
* hover pays the full delay again.
|
|
51
|
+
* @default 300
|
|
52
|
+
*/
|
|
39
53
|
skipDelayDuration?: number;
|
|
40
54
|
};
|
|
41
55
|
|
|
@@ -45,8 +59,16 @@ export type TooltipProviderProps = {
|
|
|
45
59
|
* and `open` is forbidden.
|
|
46
60
|
*/
|
|
47
61
|
export type UncontrolledTooltipRootProps = {
|
|
62
|
+
/**
|
|
63
|
+
* Whether the tooltip is open on first render. The component owns the flag
|
|
64
|
+
* from then on (hover/focus/blur/Escape drive it internally).
|
|
65
|
+
* @default false
|
|
66
|
+
*/
|
|
48
67
|
defaultOpen?: boolean;
|
|
68
|
+
/** Forbidden in uncontrolled mode — use `defaultOpen` instead. */
|
|
49
69
|
open?: never;
|
|
70
|
+
/** Called with the new open state after every transition. Optional in
|
|
71
|
+
* uncontrolled mode; use it to observe (not drive) open/close. */
|
|
50
72
|
onOpenChange?: (open: boolean) => void;
|
|
51
73
|
};
|
|
52
74
|
|
|
@@ -56,8 +78,13 @@ export type UncontrolledTooltipRootProps = {
|
|
|
56
78
|
* forbidden.
|
|
57
79
|
*/
|
|
58
80
|
export type ControlledTooltipRootProps = {
|
|
81
|
+
/** The current open state. The delay and grace timers still run and call
|
|
82
|
+
* `onOpenChange`, but the tooltip only moves when the parent updates this. */
|
|
59
83
|
open: boolean;
|
|
84
|
+
/** Called with the requested open state whenever the tooltip wants to open
|
|
85
|
+
* or close. Optional even in controlled mode. */
|
|
60
86
|
onOpenChange?: (open: boolean) => void;
|
|
87
|
+
/** Forbidden in controlled mode — use `open` instead. */
|
|
61
88
|
defaultOpen?: never;
|
|
62
89
|
};
|
|
63
90
|
|
|
@@ -68,45 +95,99 @@ export type ControlledTooltipRootProps = {
|
|
|
68
95
|
* so TypeScript accepts exactly one mode.
|
|
69
96
|
*/
|
|
70
97
|
export type TooltipRootProps = {
|
|
98
|
+
/** The tooltip's {@link TooltipTriggerProps | `Tooltip.Trigger`},
|
|
99
|
+
* {@link TooltipContentProps | `Tooltip.Content`} (usually inside a
|
|
100
|
+
* {@link TooltipPortalProps | `Tooltip.Portal`}), and optional
|
|
101
|
+
* {@link TooltipArrowProps | `Tooltip.Arrow`}. */
|
|
71
102
|
children?: ReactNode;
|
|
103
|
+
/**
|
|
104
|
+
* Overrides {@link TooltipProviderProps.delayDuration} for this tooltip only
|
|
105
|
+
* — the hover open delay in milliseconds. Falls back to the Provider's value
|
|
106
|
+
* when omitted.
|
|
107
|
+
*/
|
|
72
108
|
delayDuration?: number;
|
|
109
|
+
/**
|
|
110
|
+
* When `true`, removes the pointer-leave grace period: the tooltip closes the
|
|
111
|
+
* instant the pointer leaves the trigger, so the user cannot move into the
|
|
112
|
+
* content. Leave `false` for tooltips whose content is meant to be hovered.
|
|
113
|
+
* @default false
|
|
114
|
+
*/
|
|
73
115
|
disableHoverableContent?: boolean;
|
|
74
116
|
} & (UncontrolledTooltipRootProps | ControlledTooltipRootProps);
|
|
75
117
|
|
|
76
118
|
/**
|
|
77
119
|
* Props for {@link Tooltip.Trigger} — all native `<button>` attributes plus
|
|
78
|
-
* the `asChild` escape hatch.
|
|
120
|
+
* the `asChild` escape hatch. The `aria-describedby`, `data-state`, and the
|
|
121
|
+
* hover/focus/blur/Escape open-close handlers are added by the component
|
|
122
|
+
* (consumer handlers compose and run first).
|
|
79
123
|
*/
|
|
80
124
|
export type TooltipTriggerProps = ComponentProps<"button"> & {
|
|
125
|
+
/**
|
|
126
|
+
* Render the single child element (a link, icon button, etc.) instead of the
|
|
127
|
+
* default `<button>`, merging the trigger's ARIA and event handlers onto it
|
|
128
|
+
* via the {@link Slot} pattern.
|
|
129
|
+
* @default false
|
|
130
|
+
*/
|
|
81
131
|
asChild?: boolean;
|
|
82
132
|
};
|
|
83
133
|
|
|
84
134
|
/**
|
|
85
135
|
* Props for {@link Tooltip.Portal} — the `container` to render the content
|
|
86
|
-
* into
|
|
87
|
-
*
|
|
136
|
+
* into and `forceMount` to keep the content in the DOM while closed for exit
|
|
137
|
+
* animations.
|
|
88
138
|
*/
|
|
89
139
|
export type TooltipPortalProps = {
|
|
140
|
+
/** The content to portal — typically a single
|
|
141
|
+
* {@link TooltipContentProps | `Tooltip.Content`}. */
|
|
90
142
|
children?: ReactNode;
|
|
143
|
+
/**
|
|
144
|
+
* The element to portal the content into.
|
|
145
|
+
* @default document.body
|
|
146
|
+
*/
|
|
91
147
|
container?: HTMLElement;
|
|
148
|
+
/**
|
|
149
|
+
* Keep the content mounted while the tooltip is closed (instead of removing
|
|
150
|
+
* it), so a CSS exit animation on `data-state="closed"` can play.
|
|
151
|
+
* @default false
|
|
152
|
+
*/
|
|
92
153
|
forceMount?: boolean;
|
|
93
154
|
};
|
|
94
155
|
|
|
95
156
|
/**
|
|
96
157
|
* Props for {@link Tooltip.Content} — all native `<div>` attributes plus
|
|
97
|
-
* `forceMount` and the dismissal
|
|
98
|
-
*
|
|
158
|
+
* `forceMount` and the dismissal escape hatches. The component adds
|
|
159
|
+
* `role="tooltip"`, the shared `id` (for the trigger's `aria-describedby`),
|
|
160
|
+
* the `data-state` hook, and the pointer handlers that cancel/allow the grace
|
|
161
|
+
* period close.
|
|
99
162
|
*/
|
|
100
163
|
export type TooltipContentProps = ComponentProps<"div"> & {
|
|
164
|
+
/**
|
|
165
|
+
* Keep the content mounted while the tooltip is closed (instead of removing
|
|
166
|
+
* it), so a CSS exit animation on `data-state="closed"` can play.
|
|
167
|
+
* @default false
|
|
168
|
+
*/
|
|
101
169
|
forceMount?: boolean;
|
|
170
|
+
/**
|
|
171
|
+
* Fires when `Escape` is pressed while the tooltip is open. Call
|
|
172
|
+
* `event.preventDefault()` to keep the tooltip open; otherwise it closes.
|
|
173
|
+
*/
|
|
102
174
|
onEscapeKeyDown?: (event: KeyboardEvent) => void;
|
|
175
|
+
/**
|
|
176
|
+
* Fires on a pointer-down outside the content. Call `event.preventDefault()`
|
|
177
|
+
* to keep the tooltip open; otherwise it closes.
|
|
178
|
+
*/
|
|
103
179
|
onPointerDownOutside?: (event: PointerEvent) => void;
|
|
104
180
|
};
|
|
105
181
|
|
|
106
182
|
/**
|
|
107
183
|
* Props for {@link Tooltip.Arrow} — all native `<span>` attributes plus the
|
|
108
|
-
* `asChild` escape hatch.
|
|
184
|
+
* `asChild` escape hatch. Purely presentational; position and size it with CSS.
|
|
109
185
|
*/
|
|
110
186
|
export type TooltipArrowProps = ComponentProps<"span"> & {
|
|
187
|
+
/**
|
|
188
|
+
* Render the single child element (e.g. an `<svg>`) instead of the default
|
|
189
|
+
* `<span>` via the {@link Slot} pattern.
|
|
190
|
+
* @default false
|
|
191
|
+
*/
|
|
111
192
|
asChild?: boolean;
|
|
112
193
|
};
|
package/src/Tree/README.md
CHANGED
|
@@ -56,7 +56,7 @@ Passing both `default*` and the controlled prop is a type error.
|
|
|
56
56
|
|
|
57
57
|
```ts
|
|
58
58
|
// Uncontrolled
|
|
59
|
-
<Tree.Root defaultExpandedValues={["src"]} onExpandedChange={(v) =>
|
|
59
|
+
<Tree.Root defaultExpandedValues={["src"]} onExpandedChange={(v) => ...}>
|
|
60
60
|
|
|
61
61
|
// Controlled
|
|
62
62
|
<Tree.Root expandedValues={values} onExpandedChange={setValues}>
|
|
@@ -70,14 +70,14 @@ The selection shape depends on `selectionMode`:
|
|
|
70
70
|
|
|
71
71
|
```ts
|
|
72
72
|
// Single (default) — selectedValue is `string | null`
|
|
73
|
-
<Tree.Root defaultSelectedValue="readme" onSelectedValueChange={(v) =>
|
|
73
|
+
<Tree.Root defaultSelectedValue="readme" onSelectedValueChange={(v) => ...}>
|
|
74
74
|
<Tree.Root selectedValue={v} onSelectedValueChange={setV}>
|
|
75
75
|
|
|
76
76
|
// Multiple — selectedValues is `string[]`
|
|
77
77
|
<Tree.Root
|
|
78
78
|
selectionMode="multiple"
|
|
79
79
|
defaultSelectedValues={["readme", "index"]}
|
|
80
|
-
onSelectedValuesChange={(v) =>
|
|
80
|
+
onSelectedValuesChange={(v) => ...}
|
|
81
81
|
>
|
|
82
82
|
<Tree.Root
|
|
83
83
|
selectionMode="multiple"
|
|
@@ -187,7 +187,7 @@ styles.
|
|
|
187
187
|
> [role="treeitem"][data-depth="2"] > .row {
|
|
188
188
|
> padding-inline-start: 3rem;
|
|
189
189
|
> }
|
|
190
|
-
> /*
|
|
190
|
+
> /* ...extend as deep as your tree can go */
|
|
191
191
|
> ```
|
|
192
192
|
>
|
|
193
193
|
> **2. Set a CSS variable inline from the render layer** — the
|
|
@@ -223,6 +223,70 @@ styles.
|
|
|
223
223
|
> }
|
|
224
224
|
> ```
|
|
225
225
|
|
|
226
|
+
## Wrapping Tree in a styled layer
|
|
227
|
+
|
|
228
|
+
`Tree.Branch` finds its control and content by **inspecting its children before
|
|
229
|
+
rendering them**, so it sees the component you wrote — not what that component
|
|
230
|
+
renders. A styling layer that wraps each part in its own component to attach
|
|
231
|
+
classes is therefore invisible to it, and the branch throws:
|
|
232
|
+
|
|
233
|
+
```
|
|
234
|
+
A Tree.Branch must contain a <Tree.BranchControl>.
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Set `TREE_PART` on the wrapper and it partitions like the part it renders:
|
|
238
|
+
|
|
239
|
+
```tsx
|
|
240
|
+
import { TREE_PART, Tree } from "@primitiv-ui/react";
|
|
241
|
+
|
|
242
|
+
function StyledBranchControl(props: TreeBranchControlProps) {
|
|
243
|
+
return <Tree.BranchControl {...props} className="row" />;
|
|
244
|
+
}
|
|
245
|
+
StyledBranchControl[TREE_PART] = "branch-control";
|
|
246
|
+
|
|
247
|
+
function StyledBranchContent(props: TreeBranchContentProps) {
|
|
248
|
+
return <Tree.BranchContent {...props} className="group" />;
|
|
249
|
+
}
|
|
250
|
+
StyledBranchContent[TREE_PART] = "branch-content";
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
The accepted names are `"branch-control"` and `"branch-content"`
|
|
254
|
+
(`TreePartName`). Only these two parts need it — every other part is found by
|
|
255
|
+
position or context, not by inspection. The one-control and one-content rules
|
|
256
|
+
still apply: two marked controls in a branch is still an error.
|
|
257
|
+
|
|
258
|
+
This is what the registry `tree` component does; no other primitive in the
|
|
259
|
+
library needs a marker, because none of the others read their children's types.
|
|
260
|
+
|
|
261
|
+
### Indenting without `attr()`
|
|
262
|
+
|
|
263
|
+
`useTreeLevel()` returns `{ depth }` — `0` directly under `Tree.Root`, one
|
|
264
|
+
deeper per enclosing `BranchContent`. A styling layer needs the depth as a
|
|
265
|
+
*number* to apply the indent as the **row's own padding**, which is what keeps
|
|
266
|
+
each row's box full-width so a hover or selected band runs edge to edge rather
|
|
267
|
+
than starting at the nested level. Reading `data-depth` back in CSS would need
|
|
268
|
+
`attr(data-depth type(<integer>))` — Chrome 133+ / Safari 18.2+, unsupported in
|
|
269
|
+
Firefox — so the depth is exposed here instead.
|
|
270
|
+
|
|
271
|
+
```tsx
|
|
272
|
+
function StyledItem(props: TreeItemProps) {
|
|
273
|
+
const { depth } = useTreeLevel();
|
|
274
|
+
return (
|
|
275
|
+
<Tree.Item
|
|
276
|
+
{...props}
|
|
277
|
+
style={{ "--depth": depth } as CSSProperties}
|
|
278
|
+
className="row"
|
|
279
|
+
/>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
```css
|
|
285
|
+
.row {
|
|
286
|
+
padding-inline-start: calc(var(--depth, 0) * 1.25rem + 0.5rem);
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
226
290
|
## Selection path
|
|
227
291
|
|
|
228
292
|
`Tree.Item` and `Tree.Branch` accept an optional `label` prop. It does
|
|
@@ -256,7 +320,7 @@ import { Tree } from "@primitiv-ui/react";
|
|
|
256
320
|
import { ChevronRight } from "@primitiv-ui/icons";
|
|
257
321
|
|
|
258
322
|
<Tree.Root defaultSelectedValue="index">
|
|
259
|
-
|
|
323
|
+
...
|
|
260
324
|
<Tree.SelectionPath separator={<ChevronRight />} />
|
|
261
325
|
</Tree.Root>;
|
|
262
326
|
```
|