@primitiv-ui/react 0.1.27 → 0.1.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/package.json +6 -1
- package/src/AccessibleIcon/AccessibleIcon.tsx +32 -9
- package/src/AccessibleIcon/__tests__/AccessibleIcon.test.tsx +5 -0
- package/src/AccessibleIcon/types.ts +15 -2
- package/src/Accordion/Accordion.tsx +25 -4
- package/src/Accordion/__tests__/Accordion.asChild.test.tsx +46 -0
- package/src/Accordion/__tests__/Accordion.basic-rendering.test.tsx +15 -0
- package/src/Accordion/__tests__/Accordion.disabled-items.test.tsx +42 -0
- package/src/Accordion/__tests__/Accordion.error-handling.test.tsx +33 -0
- package/src/Accordion/hooks/useAccordionRoot.ts +44 -7
- package/src/Accordion/hooks/useAccordionTrigger.ts +9 -1
- package/src/Accordion/types.ts +72 -21
- package/src/Alert/Alert.tsx +36 -7
- package/src/Alert/__tests__/Alert.test.tsx +5 -0
- package/src/Alert/types.ts +18 -2
- package/src/Avatar/Avatar.tsx +99 -15
- package/src/Avatar/__tests__/Avatar.basic-rendering.test.tsx +10 -0
- package/src/Avatar/__tests__/Avatar.fallback.test.tsx +30 -0
- package/src/Avatar/__tests__/Avatar.image-loading.test.tsx +35 -0
- package/src/Avatar/hooks/useAvatarImage.ts +16 -0
- package/src/Avatar/types.ts +15 -3
- package/src/Breadcrumb/Breadcrumb.tsx +136 -18
- package/src/Breadcrumb/README.md +47 -0
- package/src/Breadcrumb/__tests__/Breadcrumb.basic-rendering.test.tsx +11 -0
- package/src/Breadcrumb/__tests__/Breadcrumb.ellipsis.test.tsx +110 -0
- package/src/Breadcrumb/types.ts +73 -7
- package/src/Button/Button.tsx +2 -0
- package/src/Button/__tests__/Button.basic-rendering.test.tsx +6 -0
- package/src/Button/types.ts +21 -0
- package/src/Carousel/Carousel.tsx +263 -15
- package/src/Carousel/README.md +749 -72
- package/src/Carousel/__tests__/Carousel.autoplay-status.test.tsx +239 -0
- package/src/Carousel/__tests__/Carousel.drag-status.test.tsx +292 -0
- package/src/Carousel/__tests__/Carousel.dynamic-slides.test.tsx +38 -0
- package/src/Carousel/__tests__/Carousel.indicators.test.tsx +101 -0
- package/src/Carousel/__tests__/Carousel.infinite.test.tsx +1453 -0
- package/src/Carousel/__tests__/Carousel.instant-scroll.test.tsx +124 -0
- package/src/Carousel/__tests__/Carousel.intersection-observer.test.tsx +88 -0
- package/src/Carousel/__tests__/Carousel.loop.test.tsx +163 -0
- package/src/Carousel/__tests__/Carousel.loopEngine.test.ts +77 -0
- package/src/Carousel/__tests__/Carousel.mouse-drag.test.tsx +504 -0
- package/src/Carousel/__tests__/Carousel.multi-slide-bounds.test.tsx +91 -0
- package/src/Carousel/__tests__/Carousel.orientation.test.tsx +207 -0
- package/src/Carousel/__tests__/Carousel.overscroll.test.tsx +532 -0
- package/src/Carousel/__tests__/Carousel.page-snap-points.test.tsx +50 -0
- package/src/Carousel/__tests__/Carousel.progress-text.test.tsx +76 -0
- package/src/Carousel/__tests__/Carousel.reduced-motion.test.tsx +3 -6
- package/src/Carousel/__tests__/Carousel.refresh-progress.test.tsx +2 -5
- package/src/Carousel/__tests__/Carousel.scroll-margin.test.tsx +167 -0
- package/src/Carousel/__tests__/Carousel.scroll-progress.test.tsx +518 -0
- package/src/Carousel/__tests__/Carousel.scroll-snap-change.test.tsx +32 -11
- package/src/Carousel/__tests__/Carousel.scroll-sync.test.tsx +237 -24
- package/src/Carousel/__tests__/Carousel.scroll-to-index.test.tsx +115 -0
- package/src/Carousel/__tests__/Carousel.slide-snap.test.tsx +172 -0
- package/src/Carousel/__tests__/Carousel.slides-per-move.test.tsx +179 -1
- package/src/Carousel/__tests__/Carousel.slides-per-page.test.tsx +91 -7
- package/src/Carousel/__tests__/Carousel.snap-type.test.tsx +35 -0
- package/src/Carousel/__tests__/Carousel.transition-modes.test.tsx +30 -5
- package/src/Carousel/__tests__/Carousel.translations.test.tsx +23 -0
- package/src/Carousel/__tests__/Carousel.wheel.test.tsx +160 -0
- package/src/Carousel/hooks/index.ts +1 -0
- package/src/Carousel/hooks/useCarouselLoop.ts +869 -0
- package/src/Carousel/hooks/useCarouselRoot.ts +338 -42
- package/src/Carousel/hooks/useCarouselSlide.ts +44 -9
- package/src/Carousel/hooks/useCarouselViewport.ts +649 -46
- package/src/Carousel/loopEngine.ts +54 -0
- package/src/Carousel/types.ts +457 -35
- package/src/Checkbox/Checkbox.tsx +9 -1
- package/src/Checkbox/__tests__/Checkbox.basic-rendering.test.tsx +14 -0
- package/src/Checkbox/hooks/useCheckboxInput.ts +4 -0
- package/src/Checkbox/types.ts +19 -0
- package/src/CheckboxCard/CheckboxCard.tsx +13 -4
- package/src/CheckboxCard/__tests__/CheckboxCard.basic-rendering.test.tsx +7 -0
- package/src/CheckboxCard/types.ts +65 -17
- package/src/Collapsible/Collapsible.tsx +49 -3
- package/src/Collapsible/README.md +40 -0
- package/src/Collapsible/__tests__/Collapsible.asChild.test.tsx +89 -0
- package/src/Collapsible/__tests__/Collapsible.basic-rendering.test.tsx +27 -0
- package/src/Collapsible/__tests__/Collapsible.collapsedHeight.test.tsx +140 -0
- package/src/Collapsible/hooks/useCollapsibleTrigger.ts +3 -1
- package/src/Collapsible/types.ts +89 -22
- package/src/ContextMenu/ContextMenu.tsx +270 -4
- package/src/ContextMenu/types.ts +148 -6
- package/src/DirectionProvider/DirectionProvider.tsx +41 -9
- package/src/DirectionProvider/__tests__/DirectionProvider.test.tsx +5 -0
- package/src/DirectionProvider/types.ts +28 -3
- package/src/Divider/Divider.tsx +24 -10
- package/src/Divider/__tests__/Divider.test.tsx +5 -0
- package/src/Divider/types.ts +10 -3
- package/src/Drawer/Drawer.tsx +315 -0
- package/src/Drawer/README.md +124 -0
- package/src/Drawer/__tests__/Drawer.behavior.test.tsx +100 -0
- package/src/Drawer/__tests__/Drawer.focus-trap.test.tsx +55 -0
- package/src/Drawer/__tests__/Drawer.rendering.test.tsx +111 -0
- package/src/Drawer/__tests__/Drawer.side.test.tsx +69 -0
- package/src/Drawer/__tests__/dialog-polyfill.ts +39 -0
- package/src/Drawer/index.ts +2 -0
- package/src/Drawer/types.ts +66 -0
- package/src/Dropdown/Dropdown.tsx +117 -0
- package/src/Dropdown/types.ts +241 -11
- package/src/EmptyState/EmptyState.tsx +74 -4
- package/src/EmptyState/__tests__/EmptyState.Actions.test.tsx +7 -2
- package/src/EmptyState/__tests__/EmptyState.Description.test.tsx +3 -1
- package/src/EmptyState/__tests__/EmptyState.Root.test.tsx +10 -0
- package/src/EmptyState/__tests__/EmptyState.Title.test.tsx +3 -1
- package/src/EmptyState/types.ts +49 -6
- package/src/Field/Field.tsx +23 -0
- package/src/Field/__tests__/Field.basic-rendering.test.tsx +11 -0
- package/src/Field/__tests__/Field.error-handling.test.tsx +23 -0
- package/src/Field/__tests__/Field.state-cascade.test.tsx +21 -0
- package/src/Field/hooks/__tests__/useFieldProps.test.tsx +104 -9
- package/src/Field/types.ts +60 -12
- package/src/Fieldset/Fieldset.tsx +54 -18
- package/src/Fieldset/__tests__/Fieldset.basic-rendering.test.tsx +9 -0
- package/src/Fieldset/types.ts +20 -2
- package/src/Input/Input.tsx +14 -9
- package/src/Input/__tests__/Input.basic-rendering.test.tsx +5 -0
- package/src/Input/types.ts +19 -2
- package/src/InputGroup/InputGroup.tsx +11 -3
- package/src/InputGroup/__tests__/InputGroup.basic-rendering.test.tsx +14 -0
- package/src/InputGroup/types.ts +26 -5
- package/src/MillerColumns/MillerColumns.tsx +86 -19
- package/src/MillerColumns/types.ts +83 -27
- package/src/Modal/Modal.tsx +31 -4
- package/src/Modal/README.md +19 -10
- package/src/Modal/__tests__/Modal.focus-trap.test.tsx +162 -0
- package/src/Modal/hooks/useModalContent.ts +32 -0
- package/src/Modal/types.ts +110 -29
- package/src/NavigationMenu/NavigationMenu.tsx +890 -0
- package/src/NavigationMenu/NavigationMenuContext.ts +67 -0
- package/src/NavigationMenu/README.md +355 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.anchor-positioning.test.tsx +132 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.asChild.test.tsx +73 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.basic-rendering.test.tsx +92 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.contract.test.tsx +45 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.controlled-state.test.tsx +134 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.error-handling.test.tsx +62 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.fixtures.tsx +79 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.forceMount.test.tsx +52 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.hover-interaction.test.tsx +254 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.indicator.test.tsx +252 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-interaction.test.tsx +294 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-navigation.test.tsx +126 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.link.test.tsx +139 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.motion.test.tsx +568 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.mouse-interaction.test.tsx +76 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.uncontrolled-state.test.tsx +30 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.viewport.test.tsx +113 -0
- package/src/NavigationMenu/constants.ts +5 -0
- package/src/NavigationMenu/hooks/index.ts +7 -0
- package/src/NavigationMenu/hooks/useNavigationMenuEntry.ts +33 -0
- package/src/NavigationMenu/hooks/useNavigationMenuIndicator.ts +70 -0
- package/src/NavigationMenu/hooks/useNavigationMenuLink.ts +72 -0
- package/src/NavigationMenu/hooks/useNavigationMenuRoot.ts +301 -0
- package/src/NavigationMenu/hooks/useNavigationMenuTopLevelEntry.ts +65 -0
- package/src/NavigationMenu/hooks/useNavigationMenuTrigger.ts +158 -0
- package/src/NavigationMenu/hooks/useNavigationMenuViewport.ts +101 -0
- package/src/NavigationMenu/index.ts +3 -0
- package/src/NavigationMenu/types.ts +259 -0
- package/src/NavigationMenu/utils.ts +89 -0
- package/src/Popover/Popover.tsx +427 -0
- package/src/Popover/PopoverContext.ts +13 -0
- package/src/Popover/README.md +106 -0
- package/src/Popover/__tests__/Popover.anchor.test.tsx +38 -0
- package/src/Popover/__tests__/Popover.asChild.test.tsx +160 -0
- package/src/Popover/__tests__/Popover.basic-rendering.test.tsx +106 -0
- package/src/Popover/__tests__/Popover.click-outside.test.tsx +149 -0
- package/src/Popover/__tests__/Popover.close.test.tsx +70 -0
- package/src/Popover/__tests__/Popover.content.test.tsx +58 -0
- package/src/Popover/__tests__/Popover.controlled-state.test.tsx +45 -0
- package/src/Popover/__tests__/Popover.error-handling.test.tsx +51 -0
- package/src/Popover/__tests__/Popover.focus.test.tsx +43 -0
- package/src/Popover/__tests__/Popover.keyboard-interaction.test.tsx +71 -0
- package/src/Popover/__tests__/Popover.mouse-interaction.test.tsx +69 -0
- package/src/Popover/__tests__/Popover.uncontrolled-state.test.tsx +44 -0
- package/src/Popover/hooks/index.ts +3 -0
- package/src/Popover/hooks/usePopoverContent.ts +98 -0
- package/src/Popover/hooks/usePopoverRoot.ts +84 -0
- package/src/Popover/hooks/usePopoverTrigger.ts +33 -0
- package/src/Popover/index.ts +2 -0
- package/src/Popover/types.ts +197 -0
- package/src/Portal/Portal.tsx +50 -10
- package/src/Portal/__tests__/Portal.basic-rendering.test.tsx +5 -0
- package/src/Portal/types.ts +5 -0
- package/src/Progress/Progress.tsx +33 -1
- package/src/Progress/__tests__/Progress.basic-rendering.test.tsx +10 -0
- package/src/Progress/__tests__/Progress.value.test.tsx +21 -0
- package/src/Progress/types.ts +20 -4
- package/src/Radio/Radio.tsx +9 -1
- package/src/Radio/__tests__/Radio.basic-rendering.test.tsx +14 -0
- package/src/Radio/hooks/useRadioRoot.ts +3 -0
- package/src/Radio/types.ts +20 -0
- package/src/RadioCard/RadioCard.tsx +11 -0
- package/src/RadioCard/__tests__/RadioCard.basic-rendering.test.tsx +8 -0
- package/src/RadioCard/__tests__/RadioCard.disabled-items.test.tsx +27 -0
- package/src/RadioCard/__tests__/RadioCard.keyboard-interaction.test.tsx +53 -0
- package/src/RadioCard/__tests__/RadioCard.ref-forwarding.test.tsx +54 -0
- package/src/RadioCard/__tests__/RadioCard.uncontrolled-state.test.tsx +21 -0
- package/src/RadioCard/hooks/useRadioCardRoot.ts +11 -0
- package/src/RadioCard/types.ts +95 -22
- package/src/RadioGroup/RadioGroup.tsx +11 -0
- package/src/RadioGroup/__tests__/RadioGroup.basic-rendering.test.tsx +8 -0
- package/src/RadioGroup/__tests__/RadioGroup.disabled.test.tsx +27 -0
- package/src/RadioGroup/__tests__/RadioGroup.ref-forwarding.test.tsx +29 -0
- package/src/RadioGroup/__tests__/RadioGroup.roving-tabindex.test.tsx +31 -0
- package/src/RadioGroup/hooks/useRadioGroupRoot.ts +11 -0
- package/src/RadioGroup/types.ts +73 -9
- package/src/SegmentedControl/README.md +154 -0
- package/src/SegmentedControl/SegmentedControl.tsx +309 -0
- package/src/SegmentedControl/SegmentedControlContext.ts +28 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.asChild.test.tsx +50 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.basic-rendering.test.tsx +100 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.controlled-state.test.tsx +107 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.disabled-keydown-guards.test.tsx +62 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.disabled.test.tsx +151 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.error-handling.test.tsx +17 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.keyboard-interaction.test.tsx +117 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.orientation.test.tsx +54 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.reading-direction.test.tsx +56 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.ref-forwarding.test.tsx +45 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.roving-tabindex.test.tsx +104 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.uncontrolled-state.test.tsx +94 -0
- package/src/SegmentedControl/hooks/index.ts +2 -0
- package/src/SegmentedControl/hooks/useSegmentedControlContext.ts +1 -0
- package/src/SegmentedControl/hooks/useSegmentedControlRoot.ts +86 -0
- package/src/SegmentedControl/index.ts +2 -0
- package/src/SegmentedControl/types.ts +138 -0
- package/src/Select/README.md +122 -157
- package/src/Select/Select.tsx +595 -68
- package/src/Select/SelectContext.ts +38 -0
- package/src/Select/SelectItemIndicatorContext.ts +29 -0
- package/src/Select/__tests__/Select.asChild.test.tsx +2 -2
- package/src/Select/__tests__/Select.basic-rendering.test.tsx +22 -3
- package/src/Select/__tests__/Select.controlled-state.test.tsx +6 -6
- package/src/Select/__tests__/Select.data-attributes.test.tsx +4 -4
- package/src/Select/__tests__/Select.field-integration.test.tsx +11 -11
- package/src/Select/__tests__/Select.form.test.tsx +38 -0
- package/src/Select/__tests__/Select.group.test.tsx +6 -6
- package/src/Select/__tests__/Select.keyboard-edge.test.tsx +95 -0
- package/src/Select/__tests__/Select.keyboard.test.tsx +124 -0
- package/src/Select/__tests__/Select.mutation-hardening.test.tsx +317 -0
- package/src/Select/__tests__/Select.native-item.test.tsx +30 -0
- package/src/Select/__tests__/Select.open-close.test.tsx +57 -0
- package/src/Select/__tests__/Select.placeholder.test.tsx +4 -4
- package/src/Select/__tests__/Select.rich-asChild.test.tsx +60 -0
- package/src/Select/__tests__/Select.rich-mode.test.tsx +16 -0
- package/src/Select/__tests__/Select.selection.test.tsx +104 -0
- package/src/Select/__tests__/Select.separator.test.tsx +71 -0
- package/src/Select/__tests__/Select.strict-context.test.tsx +36 -0
- package/src/Select/__tests__/Select.typeahead.test.tsx +156 -0
- package/src/Select/__tests__/Select.value-mirror.test.tsx +240 -0
- package/src/Select/constants.ts +8 -0
- package/src/Select/hooks/index.ts +2 -0
- package/src/Select/hooks/useSelectContent.ts +149 -0
- package/src/Select/hooks/useSelectRoot.ts +149 -0
- package/src/Select/types.ts +199 -31
- package/src/SkipNav/SkipNav.tsx +69 -29
- package/src/SkipNav/__tests__/SkipNav.basic-rendering.test.tsx +6 -0
- package/src/SkipNav/types.ts +17 -9
- package/src/Slider/Slider.tsx +33 -1
- package/src/Slider/SliderContext.ts +28 -1
- package/src/Slider/types.ts +157 -9
- package/src/Slot/Slot.tsx +94 -19
- package/src/Slot/__tests__/Slot.test.tsx +110 -0
- package/src/Slot/composeEventHandlers.ts +26 -2
- package/src/Slot/types.ts +16 -0
- package/src/Status/Status.tsx +36 -15
- package/src/Status/__tests__/Status.test.tsx +5 -0
- package/src/Status/types.ts +12 -1
- package/src/Switch/Switch.tsx +15 -2
- package/src/Switch/__tests__/Switch.basic-rendering.test.tsx +7 -0
- package/src/Switch/hooks/useSwitchRoot.ts +3 -0
- package/src/Switch/types.ts +49 -22
- package/src/Table/Table.tsx +44 -10
- package/src/Table/types.ts +66 -17
- package/src/Tabs/Tabs.tsx +8 -0
- package/src/Tabs/types.ts +29 -4
- package/src/Textarea/Textarea.tsx +9 -1
- package/src/Textarea/__tests__/Textarea.basic-rendering.test.tsx +5 -0
- package/src/Textarea/types.ts +20 -3
- package/src/Toggle/Toggle.tsx +51 -13
- package/src/Toggle/__tests__/Toggle.basic-rendering.test.tsx +5 -0
- package/src/Toggle/types.ts +54 -4
- package/src/ToggleGroup/ToggleGroup.tsx +14 -1
- package/src/ToggleGroup/__tests__/ToggleGroup.asChild.test.tsx +20 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.basic-rendering.test.tsx +9 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.dynamic-items.test.tsx +70 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.error-handling.test.tsx +23 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.keyboard-interaction.test.tsx +23 -1
- package/src/ToggleGroup/__tests__/ToggleGroup.ref-forwarding.test.tsx +58 -0
- package/src/ToggleGroup/hooks/useToggleGroupRoot.ts +21 -1
- package/src/ToggleGroup/types.ts +55 -2
- package/src/Tooltip/Tooltip.tsx +98 -20
- package/src/Tooltip/hooks/__tests__/useTooltipProvider.test.tsx +40 -0
- package/src/Tooltip/hooks/useTooltipProvider.ts +13 -1
- package/src/Tooltip/types.ts +89 -8
- package/src/Tree/Tree.tsx +187 -33
- package/src/Tree/types.ts +54 -12
- package/src/VisuallyHidden/VisuallyHidden.tsx +33 -8
- package/src/VisuallyHidden/__tests__/VisuallyHidden.test.tsx +15 -1
- package/src/VisuallyHidden/types.ts +11 -1
- package/src/index.ts +4 -0
- package/src/test/intersectionObserverPolyfill.ts +7 -0
- package/src/test/resizeObserverPolyfill.ts +80 -0
- package/src/utils/focusable.ts +15 -0
- package/src/utils/index.ts +1 -0
package/src/Tooltip/Tooltip.tsx
CHANGED
|
@@ -24,11 +24,27 @@ import type {
|
|
|
24
24
|
} from "./types";
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
* Wraps one or more `Tooltip.Root` instances and provides
|
|
28
|
-
* delay configuration
|
|
29
|
-
* without a Provider throws a context
|
|
27
|
+
* Wraps one or more {@link TooltipRoot | `Tooltip.Root`} instances and provides
|
|
28
|
+
* the shared delay configuration and group-wide open coordination. Required
|
|
29
|
+
* ancestor — rendering `Tooltip.Root` without a Provider throws a context
|
|
30
|
+
* error. Renders no DOM of its own (a `TooltipProviderContext` provider only).
|
|
30
31
|
*
|
|
31
|
-
*
|
|
32
|
+
* **Delay coordination.** Two timings govern how a group of tooltips feels:
|
|
33
|
+
*
|
|
34
|
+
* - `delayDuration` — how long a pointer must rest on a trigger before its
|
|
35
|
+
* tooltip opens (hover only; focus always opens immediately).
|
|
36
|
+
* - `skipDelayDuration` — the grace window after a tooltip closes during which
|
|
37
|
+
* moving to *another* trigger in the same Provider opens that tooltip
|
|
38
|
+
* instantly, with no `delayDuration` wait. Rest longer than this window and
|
|
39
|
+
* the next hover pays the full `delayDuration` again. This is what makes a
|
|
40
|
+
* toolbar of icon buttons feel responsive once the first tip has shown.
|
|
41
|
+
*
|
|
42
|
+
* Place a single Provider high in the tree (typically at the app root) so every
|
|
43
|
+
* tooltip shares one skip window. A per-instance
|
|
44
|
+
* {@link TooltipRootProps.delayDuration | `delayDuration`} on `Tooltip.Root`
|
|
45
|
+
* overrides the Provider's value for that one tooltip.
|
|
46
|
+
*
|
|
47
|
+
* @example App-wide provider
|
|
32
48
|
* ```tsx
|
|
33
49
|
* <Tooltip.Provider delayDuration={400}>
|
|
34
50
|
* <App />
|
|
@@ -55,13 +71,27 @@ export function TooltipProviderComponent({
|
|
|
55
71
|
TooltipProviderComponent.displayName = "TooltipProvider";
|
|
56
72
|
|
|
57
73
|
/**
|
|
58
|
-
* The state boundary for a single tooltip. Owns the open/closed
|
|
59
|
-
*
|
|
74
|
+
* The state boundary for a single tooltip. Owns the open/closed state, runs the
|
|
75
|
+
* open-delay and grace-period timers, and wires everything down to the
|
|
76
|
+
* sub-components via `TooltipContext`. Renders no DOM of its own. Must be a
|
|
77
|
+
* descendant of {@link TooltipProviderComponent | `Tooltip.Provider`}.
|
|
60
78
|
*
|
|
61
|
-
*
|
|
79
|
+
* **Controlled vs uncontrolled**, statically discriminated at the type level so
|
|
80
|
+
* TypeScript accepts only one shape:
|
|
62
81
|
*
|
|
63
|
-
* - **Uncontrolled** — pass `defaultOpen`
|
|
64
|
-
* -
|
|
82
|
+
* - **Uncontrolled** — pass {@link UncontrolledTooltipRootProps.defaultOpen | `defaultOpen`}
|
|
83
|
+
* (or omit it for closed-on-mount). The component owns the flag; observe
|
|
84
|
+
* transitions with `onOpenChange`.
|
|
85
|
+
* - **Controlled** — pass {@link ControlledTooltipRootProps.open | `open`}
|
|
86
|
+
* (with an optional `onOpenChange`). The parent owns the flag; the delay and
|
|
87
|
+
* grace timers still run and drive `onOpenChange`, but the tooltip only moves
|
|
88
|
+
* when the parent updates `open`.
|
|
89
|
+
*
|
|
90
|
+
* **Timing overrides.** {@link TooltipRootProps.delayDuration | `delayDuration`}
|
|
91
|
+
* overrides the Provider's open delay for this tooltip only.
|
|
92
|
+
* {@link TooltipRootProps.disableHoverableContent | `disableHoverableContent`}
|
|
93
|
+
* removes the grace period, so the tooltip closes the instant the pointer
|
|
94
|
+
* leaves the trigger (the user cannot move into the content).
|
|
65
95
|
*
|
|
66
96
|
* @example
|
|
67
97
|
* ```tsx
|
|
@@ -94,13 +124,24 @@ export function TooltipRoot({
|
|
|
94
124
|
TooltipRoot.displayName = "TooltipRoot";
|
|
95
125
|
|
|
96
126
|
/**
|
|
97
|
-
* The element that
|
|
98
|
-
*
|
|
99
|
-
* element.
|
|
127
|
+
* The element that opens the tooltip. Renders a `<button type="button">` by
|
|
128
|
+
* default; pass `asChild` to project the trigger behaviour onto a custom
|
|
129
|
+
* element (a link, an icon button, etc.).
|
|
130
|
+
*
|
|
131
|
+
* **Open / close behaviour.** Pointer-enter opens after the effective
|
|
132
|
+
* `delayDuration` (immediately if the group's skip window is active);
|
|
133
|
+
* pointer-leave closes after a short grace period so the pointer can travel
|
|
134
|
+
* into hoverable content (immediately when `disableHoverableContent` is set on
|
|
135
|
+
* Root). Focus opens immediately and blur closes immediately, and `Escape`
|
|
136
|
+
* while focused closes immediately — matching the WAI-ARIA tooltip pattern.
|
|
137
|
+
* Consumer `onPointerEnter` / `onPointerLeave` / `onFocus` / `onBlur` /
|
|
138
|
+
* `onKeyDown` all compose (they run first).
|
|
100
139
|
*
|
|
101
|
-
* ARIA wiring
|
|
102
|
-
* - `aria-describedby` points at `Tooltip.Content`'s id.
|
|
103
|
-
* - `data-state="open" | "closed"` mirrors the tooltip state.
|
|
140
|
+
* **ARIA wiring.**
|
|
141
|
+
* - `aria-describedby` points at {@link TooltipContent | `Tooltip.Content`}'s id.
|
|
142
|
+
* - `data-state="open" | "closed"` mirrors the tooltip state for CSS.
|
|
143
|
+
*
|
|
144
|
+
* @extends HTMLButtonElement
|
|
104
145
|
*
|
|
105
146
|
* @example
|
|
106
147
|
* ```tsx
|
|
@@ -148,7 +189,8 @@ TooltipTrigger.displayName = "TooltipTrigger";
|
|
|
148
189
|
*
|
|
149
190
|
* By default the portal only renders while the tooltip is open. Pass
|
|
150
191
|
* `forceMount` to keep the subtree mounted when closed — useful for
|
|
151
|
-
* CSS exit animations driven by `data-state="closed"`.
|
|
192
|
+
* CSS exit animations driven by `data-state="closed"`. Renders no wrapper
|
|
193
|
+
* element of its own.
|
|
152
194
|
*
|
|
153
195
|
* @example
|
|
154
196
|
* ```tsx
|
|
@@ -192,6 +234,8 @@ TooltipPortal.displayName = "TooltipPortal";
|
|
|
192
234
|
* `anchor-name` to the trigger and `position-anchor` / `position-area`
|
|
193
235
|
* to the content in your own stylesheet.
|
|
194
236
|
*
|
|
237
|
+
* @extends HTMLDivElement
|
|
238
|
+
*
|
|
195
239
|
* @example
|
|
196
240
|
* ```tsx
|
|
197
241
|
* <Tooltip.Content className="tooltip__content">
|
|
@@ -233,6 +277,8 @@ TooltipContent.displayName = "TooltipContent";
|
|
|
233
277
|
*
|
|
234
278
|
* All positioning and styling is the consumer's responsibility via CSS.
|
|
235
279
|
*
|
|
280
|
+
* @extends HTMLSpanElement
|
|
281
|
+
*
|
|
236
282
|
* @example
|
|
237
283
|
* ```tsx
|
|
238
284
|
* <Tooltip.Content>
|
|
@@ -270,11 +316,43 @@ export type TooltipCompound = typeof TooltipRoot & {
|
|
|
270
316
|
* Headless, accessible **Tooltip** — a compound component implementing the
|
|
271
317
|
* [WAI-ARIA tooltip pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/):
|
|
272
318
|
* a deferred, hoverable label anchored to a trigger, with a shared
|
|
273
|
-
* {@link Tooltip.Provider} governing open / skip
|
|
274
|
-
*
|
|
319
|
+
* {@link TooltipProviderComponent | `Tooltip.Provider`} governing open / skip
|
|
320
|
+
* delays across a group. Opens on hover (after a delay) and on focus
|
|
321
|
+
* (immediately); closes on blur, on `Escape`, and after a short grace period on
|
|
322
|
+
* pointer-leave so the pointer can travel into hoverable content. Zero styles
|
|
323
|
+
* ship; visual placement is your CSS concern (CSS anchor positioning).
|
|
324
|
+
*
|
|
325
|
+
* `Tooltip` is both callable (an alias of {@link TooltipRoot | `Tooltip.Root`})
|
|
326
|
+
* and carries its sub-components as static properties. A `Tooltip.Provider`
|
|
327
|
+
* ancestor is required — `Tooltip.Root` throws without one.
|
|
328
|
+
*
|
|
329
|
+
* - {@link TooltipProviderComponent | `Tooltip.Provider`} — shared delay config + group open coordination.
|
|
330
|
+
* - {@link TooltipRoot | `Tooltip.Root`} — per-tooltip state owner and timing.
|
|
331
|
+
* - {@link TooltipTrigger | `Tooltip.Trigger`} — `<button>` that opens on hover/focus.
|
|
332
|
+
* - {@link TooltipPortal | `Tooltip.Portal`} — optional portal to `document.body`.
|
|
333
|
+
* - {@link TooltipContent | `Tooltip.Content`} — the `role="tooltip"` panel.
|
|
334
|
+
* - {@link TooltipArrow | `Tooltip.Arrow`} — optional visual pointer.
|
|
335
|
+
*
|
|
336
|
+
* @example Minimal usage
|
|
337
|
+
* ```tsx
|
|
338
|
+
* import { Tooltip } from "@primitiv-ui/react";
|
|
339
|
+
*
|
|
340
|
+
* <Tooltip.Provider>
|
|
341
|
+
* <Tooltip.Root>
|
|
342
|
+
* <Tooltip.Trigger>Save</Tooltip.Trigger>
|
|
343
|
+
* <Tooltip.Portal>
|
|
344
|
+
* <Tooltip.Content>
|
|
345
|
+
* Save your changes
|
|
346
|
+
* <Tooltip.Arrow />
|
|
347
|
+
* </Tooltip.Content>
|
|
348
|
+
* </Tooltip.Portal>
|
|
349
|
+
* </Tooltip.Root>
|
|
350
|
+
* </Tooltip.Provider>;
|
|
351
|
+
* ```
|
|
275
352
|
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
353
|
+
* @see {@link TooltipProviderComponent} for delay / skip-delay coordination.
|
|
354
|
+
* @see {@link TooltipRoot} for the controlled/uncontrolled state modes and timing overrides.
|
|
355
|
+
* @see {@link TooltipContent} for the grace period and the Escape / outside-pointer escape hatches.
|
|
278
356
|
*/
|
|
279
357
|
const TooltipCompound: TooltipCompound = Object.assign(TooltipRoot, {
|
|
280
358
|
Provider: TooltipProviderComponent,
|
|
@@ -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
|
+
});
|
|
@@ -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
|
|
|
@@ -29,6 +29,18 @@ export function useTooltipProvider({
|
|
|
29
29
|
}, skipDelayDuration);
|
|
30
30
|
}, [skipDelayDuration]);
|
|
31
31
|
|
|
32
|
+
// The skip-delay reset is a raw `setTimeout`, so it outlives the
|
|
33
|
+
// component it was scheduled from unless explicitly cancelled — without
|
|
34
|
+
// this, an in-flight timer fires after unmount and can throw once its
|
|
35
|
+
// surrounding environment (e.g. a test's jsdom `window`) is gone.
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
return () => {
|
|
38
|
+
if (skipDelayTimerRef.current !== null) {
|
|
39
|
+
clearTimeout(skipDelayTimerRef.current);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}, []);
|
|
43
|
+
|
|
32
44
|
const contextValue: TooltipProviderContextValue = {
|
|
33
45
|
delayDuration,
|
|
34
46
|
skipDelayDuration,
|
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
|
};
|