@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
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
import { useEffect, useId } from "react";
|
|
2
|
+
import type { ReactElement, Ref } from "react";
|
|
3
|
+
|
|
4
|
+
import { Slot, composeEventHandlers } from "../Slot/index.ts";
|
|
5
|
+
import { PopoverContext, usePopoverContext } from "./PopoverContext";
|
|
6
|
+
import {
|
|
7
|
+
usePopoverContent,
|
|
8
|
+
usePopoverRoot,
|
|
9
|
+
usePopoverTrigger,
|
|
10
|
+
} from "./hooks/index.ts";
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
PopoverAnchorProps,
|
|
14
|
+
PopoverCloseProps,
|
|
15
|
+
PopoverContentProps,
|
|
16
|
+
PopoverDescriptionProps,
|
|
17
|
+
PopoverRootProps,
|
|
18
|
+
PopoverTitleProps,
|
|
19
|
+
PopoverTriggerProps,
|
|
20
|
+
} from "./types";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The root of a Popover — owns open state, generates the ids that wire the
|
|
24
|
+
* trigger to the content, and provides context to descendants. Renders no DOM
|
|
25
|
+
* of its own.
|
|
26
|
+
*
|
|
27
|
+
* Supports two state modes, statically discriminated at the type level:
|
|
28
|
+
*
|
|
29
|
+
* - **Uncontrolled** — pass {@link PopoverRootUncontrolledProps.defaultOpen | `defaultOpen`}
|
|
30
|
+
* (or omit it for closed-on-mount). The component owns the open flag
|
|
31
|
+
* internally; observe transitions with `onOpenChange`.
|
|
32
|
+
* - **Controlled** — pass {@link PopoverRootControlledProps.open | `open`} *and*
|
|
33
|
+
* {@link PopoverRootControlledProps.onOpenChange | `onOpenChange`} together.
|
|
34
|
+
* The parent owns the flag; the component defers every change back through
|
|
35
|
+
* the callback.
|
|
36
|
+
*
|
|
37
|
+
* @example Uncontrolled
|
|
38
|
+
* ```tsx
|
|
39
|
+
* <Popover.Root>
|
|
40
|
+
* <Popover.Trigger>Open</Popover.Trigger>
|
|
41
|
+
* <Popover.Content>…</Popover.Content>
|
|
42
|
+
* </Popover.Root>
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* @example Controlled
|
|
46
|
+
* ```tsx
|
|
47
|
+
* const [open, setOpen] = useState(false);
|
|
48
|
+
*
|
|
49
|
+
* <Popover.Root open={open} onOpenChange={setOpen}>
|
|
50
|
+
* <Popover.Trigger>Open</Popover.Trigger>
|
|
51
|
+
* <Popover.Content>…</Popover.Content>
|
|
52
|
+
* </Popover.Root>
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export function PopoverRoot({
|
|
56
|
+
children,
|
|
57
|
+
defaultOpen,
|
|
58
|
+
open,
|
|
59
|
+
onOpenChange,
|
|
60
|
+
}: PopoverRootProps): ReactElement {
|
|
61
|
+
const { contextValue } = usePopoverRoot({ defaultOpen, open, onOpenChange });
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<PopoverContext.Provider value={contextValue}>
|
|
65
|
+
{children}
|
|
66
|
+
</PopoverContext.Provider>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** @internal */
|
|
71
|
+
PopoverRoot.displayName = "PopoverRoot";
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* A button that toggles the popover. Renders `<button type="button">` with
|
|
75
|
+
* full ARIA wiring:
|
|
76
|
+
*
|
|
77
|
+
* - `aria-haspopup="dialog"`
|
|
78
|
+
* - `aria-expanded` tracks open state
|
|
79
|
+
* - `aria-controls` points at the `Popover.Content` dialog's id
|
|
80
|
+
*
|
|
81
|
+
* Clicking toggles the popover; a consumer `onClick` composes (runs first, can
|
|
82
|
+
* `event.preventDefault()` to veto the toggle).
|
|
83
|
+
*
|
|
84
|
+
* **`asChild` prop.** Pass `asChild` to render any consumer-supplied element
|
|
85
|
+
* (e.g. an icon button of your own) with the trigger's ARIA, composed event
|
|
86
|
+
* handlers, and ref merged in. Supply the child's element type via the generic
|
|
87
|
+
* (e.g. `Popover.Trigger<HTMLAnchorElement>`).
|
|
88
|
+
*
|
|
89
|
+
* @extends HTMLButtonElement
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```tsx
|
|
93
|
+
* <Popover.Trigger>Open</Popover.Trigger>
|
|
94
|
+
*
|
|
95
|
+
* <Popover.Trigger<HTMLAnchorElement> asChild>
|
|
96
|
+
* <a href="#panel">Open</a>
|
|
97
|
+
* </Popover.Trigger>
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export function PopoverTrigger<T extends HTMLElement = HTMLButtonElement>({
|
|
101
|
+
children,
|
|
102
|
+
asChild = false,
|
|
103
|
+
ref,
|
|
104
|
+
...rest
|
|
105
|
+
}: PopoverTriggerProps<T>): ReactElement {
|
|
106
|
+
const { triggerProps } = usePopoverTrigger({
|
|
107
|
+
ref: ref as Ref<HTMLButtonElement>,
|
|
108
|
+
asChild,
|
|
109
|
+
...rest,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (asChild) {
|
|
113
|
+
return <Slot {...triggerProps}>{children}</Slot>;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<button type="button" {...triggerProps}>
|
|
118
|
+
{children}
|
|
119
|
+
</button>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** @internal */
|
|
124
|
+
PopoverTrigger.displayName = "PopoverTrigger";
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The floating panel, rendered with the native HTML
|
|
128
|
+
* [Popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API)
|
|
129
|
+
* (`popover="auto"`) — no portal, no floating-ui. The browser manages the top
|
|
130
|
+
* layer and light-dismiss (outside click / Escape); visual placement is your
|
|
131
|
+
* CSS concern (use CSS anchor positioning against the trigger or
|
|
132
|
+
* {@link PopoverAnchor | `Popover.Anchor`}).
|
|
133
|
+
*
|
|
134
|
+
* Renders a `<div role="dialog">`. On open, focus moves into the panel (first
|
|
135
|
+
* focusable descendant, else the panel itself — it carries `tabIndex={-1}`).
|
|
136
|
+
* `Escape` closes and returns focus to the trigger. `aria-labelledby` /
|
|
137
|
+
* `aria-describedby` are wired automatically when `Popover.Title` /
|
|
138
|
+
* `Popover.Description` are rendered inside.
|
|
139
|
+
*
|
|
140
|
+
* **`asChild` prop.** Pass `asChild` to render a consumer element (e.g. a
|
|
141
|
+
* `<section>` or a motion wrapper) with the dialog props and internal ref
|
|
142
|
+
* merged in. A consumer `ref` composes with the internal ref, so open state
|
|
143
|
+
* keeps working.
|
|
144
|
+
*
|
|
145
|
+
* **Styling hook.** `data-state="open" | "closed"`.
|
|
146
|
+
*
|
|
147
|
+
* @extends HTMLDivElement
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```tsx
|
|
151
|
+
* <Popover.Content className="panel">
|
|
152
|
+
* <Popover.Title>Filters</Popover.Title>
|
|
153
|
+
* <Popover.Close>Done</Popover.Close>
|
|
154
|
+
* </Popover.Content>
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
export function PopoverContent({
|
|
158
|
+
children,
|
|
159
|
+
onKeyDown,
|
|
160
|
+
asChild = false,
|
|
161
|
+
ref,
|
|
162
|
+
...rest
|
|
163
|
+
}: PopoverContentProps): ReactElement {
|
|
164
|
+
const { contentProps } = usePopoverContent({
|
|
165
|
+
onKeyDown,
|
|
166
|
+
ref: ref as Ref<HTMLDivElement>,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (asChild) {
|
|
170
|
+
return (
|
|
171
|
+
<Slot {...rest} {...contentProps}>
|
|
172
|
+
{children}
|
|
173
|
+
</Slot>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<div {...rest} {...contentProps}>
|
|
179
|
+
{children}
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** @internal */
|
|
185
|
+
PopoverContent.displayName = "PopoverContent";
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* An optional positioning reference. By default the popover is positioned
|
|
189
|
+
* against the trigger; render `Popover.Anchor` around (or in place of) another
|
|
190
|
+
* element to anchor the panel there instead — useful when the trigger is a
|
|
191
|
+
* small icon inside a larger control.
|
|
192
|
+
*
|
|
193
|
+
* Renders a `<div>`. Positioning is CSS-driven: attach a CSS `anchor-name` to
|
|
194
|
+
* this element and a matching `position-anchor` to `Popover.Content`. Pass
|
|
195
|
+
* `asChild` to project the anchor semantics onto an existing element.
|
|
196
|
+
*
|
|
197
|
+
* @extends HTMLDivElement
|
|
198
|
+
*
|
|
199
|
+
* @example
|
|
200
|
+
* ```tsx
|
|
201
|
+
* <Popover.Anchor asChild>
|
|
202
|
+
* <div className="field" style={{ anchorName: "--popover" }}>
|
|
203
|
+
* <input />
|
|
204
|
+
* <Popover.Trigger>▾</Popover.Trigger>
|
|
205
|
+
* </div>
|
|
206
|
+
* </Popover.Anchor>
|
|
207
|
+
* ```
|
|
208
|
+
*/
|
|
209
|
+
export function PopoverAnchor({
|
|
210
|
+
children,
|
|
211
|
+
asChild = false,
|
|
212
|
+
...rest
|
|
213
|
+
}: PopoverAnchorProps): ReactElement {
|
|
214
|
+
// Consume context so an Anchor rendered outside a Root throws, matching the
|
|
215
|
+
// other sub-components. Positioning itself is a CSS (anchor-name) concern.
|
|
216
|
+
usePopoverContext();
|
|
217
|
+
|
|
218
|
+
if (asChild) {
|
|
219
|
+
return <Slot {...rest}>{children}</Slot>;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return <div {...rest}>{children}</div>;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** @internal */
|
|
226
|
+
PopoverAnchor.displayName = "PopoverAnchor";
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* A button that closes the popover and returns focus to the trigger. Renders a
|
|
230
|
+
* `<button type="button">` whose `onClick` composes with the close — consumer
|
|
231
|
+
* handlers run first and can `event.preventDefault()` to veto closing.
|
|
232
|
+
*
|
|
233
|
+
* Pass `asChild` to render any element (e.g. a text link or your own icon
|
|
234
|
+
* button) with the close behaviour merged in.
|
|
235
|
+
*
|
|
236
|
+
* @extends HTMLButtonElement
|
|
237
|
+
*
|
|
238
|
+
* @example
|
|
239
|
+
* ```tsx
|
|
240
|
+
* <Popover.Close>Done</Popover.Close>
|
|
241
|
+
* ```
|
|
242
|
+
*/
|
|
243
|
+
export function PopoverClose({
|
|
244
|
+
children,
|
|
245
|
+
onClick,
|
|
246
|
+
asChild = false,
|
|
247
|
+
...rest
|
|
248
|
+
}: PopoverCloseProps): ReactElement {
|
|
249
|
+
const { setOpen, triggerRef } = usePopoverContext();
|
|
250
|
+
|
|
251
|
+
const closeProps = {
|
|
252
|
+
...rest,
|
|
253
|
+
onClick: composeEventHandlers(onClick, () => {
|
|
254
|
+
setOpen(false);
|
|
255
|
+
triggerRef.current?.focus();
|
|
256
|
+
}),
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
if (asChild) {
|
|
260
|
+
return <Slot {...closeProps}>{children}</Slot>;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return (
|
|
264
|
+
<button type="button" {...closeProps}>
|
|
265
|
+
{children}
|
|
266
|
+
</button>
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** @internal */
|
|
271
|
+
PopoverClose.displayName = "PopoverClose";
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* The popover's accessible name. Renders an `<h2>` by default and
|
|
275
|
+
* auto-registers its generated id so `Popover.Content` wires it up as
|
|
276
|
+
* `aria-labelledby`. Pass `asChild` to render your own heading element (e.g.
|
|
277
|
+
* an `<h3>`); the id is still registered.
|
|
278
|
+
*
|
|
279
|
+
* @extends HTMLHeadingElement
|
|
280
|
+
*
|
|
281
|
+
* @example
|
|
282
|
+
* ```tsx
|
|
283
|
+
* <Popover.Title>Filters</Popover.Title>
|
|
284
|
+
* ```
|
|
285
|
+
*/
|
|
286
|
+
export function PopoverTitle({
|
|
287
|
+
children,
|
|
288
|
+
asChild = false,
|
|
289
|
+
...rest
|
|
290
|
+
}: PopoverTitleProps): ReactElement {
|
|
291
|
+
const { registerTitle } = usePopoverContext();
|
|
292
|
+
const id = useId();
|
|
293
|
+
|
|
294
|
+
useEffect(() => {
|
|
295
|
+
registerTitle(id);
|
|
296
|
+
return () => registerTitle(undefined);
|
|
297
|
+
}, [registerTitle, id]);
|
|
298
|
+
|
|
299
|
+
if (asChild) {
|
|
300
|
+
return (
|
|
301
|
+
<Slot id={id} {...rest}>
|
|
302
|
+
{children}
|
|
303
|
+
</Slot>
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return (
|
|
308
|
+
<h2 id={id} {...rest}>
|
|
309
|
+
{children}
|
|
310
|
+
</h2>
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** @internal */
|
|
315
|
+
PopoverTitle.displayName = "PopoverTitle";
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* The popover's accessible description. Renders a `<p>` by default and
|
|
319
|
+
* auto-registers its generated id so `Popover.Content` wires it up as
|
|
320
|
+
* `aria-describedby`. Pass `asChild` to render any element; the id is still
|
|
321
|
+
* registered.
|
|
322
|
+
*
|
|
323
|
+
* @extends HTMLParagraphElement
|
|
324
|
+
*
|
|
325
|
+
* @example
|
|
326
|
+
* ```tsx
|
|
327
|
+
* <Popover.Description>Narrow the results below.</Popover.Description>
|
|
328
|
+
* ```
|
|
329
|
+
*/
|
|
330
|
+
export function PopoverDescription({
|
|
331
|
+
children,
|
|
332
|
+
asChild = false,
|
|
333
|
+
...rest
|
|
334
|
+
}: PopoverDescriptionProps): ReactElement {
|
|
335
|
+
const { registerDescription } = usePopoverContext();
|
|
336
|
+
const id = useId();
|
|
337
|
+
|
|
338
|
+
useEffect(() => {
|
|
339
|
+
registerDescription(id);
|
|
340
|
+
return () => registerDescription(undefined);
|
|
341
|
+
}, [registerDescription, id]);
|
|
342
|
+
|
|
343
|
+
if (asChild) {
|
|
344
|
+
return (
|
|
345
|
+
<Slot id={id} {...rest}>
|
|
346
|
+
{children}
|
|
347
|
+
</Slot>
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return (
|
|
352
|
+
<p id={id} {...rest}>
|
|
353
|
+
{children}
|
|
354
|
+
</p>
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** @internal */
|
|
359
|
+
PopoverDescription.displayName = "PopoverDescription";
|
|
360
|
+
|
|
361
|
+
/** Type of the {@link Popover} compound — the Root callable plus its sub-components. */
|
|
362
|
+
type PopoverCompound = typeof PopoverRoot & {
|
|
363
|
+
/** The root, owning open state and context. */
|
|
364
|
+
Root: typeof PopoverRoot;
|
|
365
|
+
/** The button that toggles the popover. */
|
|
366
|
+
Trigger: typeof PopoverTrigger;
|
|
367
|
+
/** An optional positioning reference used instead of the trigger. */
|
|
368
|
+
Anchor: typeof PopoverAnchor;
|
|
369
|
+
/** The floating panel. */
|
|
370
|
+
Content: typeof PopoverContent;
|
|
371
|
+
/** A button that closes the popover. */
|
|
372
|
+
Close: typeof PopoverClose;
|
|
373
|
+
/** The popover's accessible name; auto-wires `aria-labelledby`. */
|
|
374
|
+
Title: typeof PopoverTitle;
|
|
375
|
+
/** The popover's accessible description; auto-wires `aria-describedby`. */
|
|
376
|
+
Description: typeof PopoverDescription;
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Headless, accessible **Popover** — a compound component built on the native
|
|
381
|
+
* HTML Popover API (`popover="auto"`). A non-modal dialog anchored to a
|
|
382
|
+
* trigger: the browser owns the top layer and light-dismiss, focus moves into
|
|
383
|
+
* the panel on open and returns to the trigger on close, and the background
|
|
384
|
+
* stays interactive (no focus trap — use `Modal` for that). Zero styles ship;
|
|
385
|
+
* visual placement is your CSS concern (CSS anchor positioning).
|
|
386
|
+
*
|
|
387
|
+
* `Popover` is both callable (an alias of {@link PopoverRoot | `Popover.Root`})
|
|
388
|
+
* and carries its sub-components as static properties.
|
|
389
|
+
*
|
|
390
|
+
* - {@link PopoverRoot | `Popover.Root`} — state owner and context provider.
|
|
391
|
+
* - {@link PopoverTrigger | `Popover.Trigger`} — `<button>` that toggles the popover.
|
|
392
|
+
* - {@link PopoverAnchor | `Popover.Anchor`} — optional positioning reference.
|
|
393
|
+
* - {@link PopoverContent | `Popover.Content`} — the native-popover `role="dialog"` panel.
|
|
394
|
+
* - {@link PopoverClose | `Popover.Close`} — `<button>` that closes the popover.
|
|
395
|
+
* - {@link PopoverTitle | `Popover.Title`} — accessible name; auto-wires `aria-labelledby`.
|
|
396
|
+
* - {@link PopoverDescription | `Popover.Description`} — auto-wires `aria-describedby`.
|
|
397
|
+
*
|
|
398
|
+
* @example Minimal usage
|
|
399
|
+
* ```tsx
|
|
400
|
+
* import { Popover } from "@primitiv-ui/react";
|
|
401
|
+
*
|
|
402
|
+
* <Popover.Root>
|
|
403
|
+
* <Popover.Trigger>Filters</Popover.Trigger>
|
|
404
|
+
* <Popover.Content>
|
|
405
|
+
* <Popover.Title>Filters</Popover.Title>
|
|
406
|
+
* <Popover.Description>Narrow the results.</Popover.Description>
|
|
407
|
+
* <Popover.Close>Done</Popover.Close>
|
|
408
|
+
* </Popover.Content>
|
|
409
|
+
* </Popover.Root>;
|
|
410
|
+
* ```
|
|
411
|
+
*
|
|
412
|
+
* @see {@link PopoverRoot} for the controlled/uncontrolled state modes.
|
|
413
|
+
* @see {@link PopoverContent} for focus behaviour and ARIA auto-wiring.
|
|
414
|
+
*/
|
|
415
|
+
const PopoverCompound: PopoverCompound = Object.assign(PopoverRoot, {
|
|
416
|
+
Root: PopoverRoot,
|
|
417
|
+
Trigger: PopoverTrigger,
|
|
418
|
+
Anchor: PopoverAnchor,
|
|
419
|
+
Content: PopoverContent,
|
|
420
|
+
Close: PopoverClose,
|
|
421
|
+
Title: PopoverTitle,
|
|
422
|
+
Description: PopoverDescription,
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
PopoverCompound.displayName = "Popover";
|
|
426
|
+
|
|
427
|
+
export { PopoverCompound as Popover };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Context } from "react";
|
|
2
|
+
|
|
3
|
+
import { createStrictContext } from "../utils/index.ts";
|
|
4
|
+
import { PopoverContextValue } from "./types";
|
|
5
|
+
|
|
6
|
+
const popoverContext = createStrictContext<PopoverContextValue>(
|
|
7
|
+
"Popover sub-components must be rendered inside a <Popover.Root>.",
|
|
8
|
+
"PopoverContext",
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
export const PopoverContext: Context<PopoverContextValue | null> =
|
|
12
|
+
popoverContext[0];
|
|
13
|
+
export const usePopoverContext: () => PopoverContextValue = popoverContext[1];
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Popover
|
|
2
|
+
|
|
3
|
+
A headless, accessible **non-modal dialog** anchored to a trigger, built on the
|
|
4
|
+
native HTML [Popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API)
|
|
5
|
+
(`popover="auto"`). The browser owns the top layer and light-dismiss; the
|
|
6
|
+
background stays interactive. For a modal dialog that traps focus and inerts the
|
|
7
|
+
page, use [Modal](../Modal/README.md) instead.
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { Popover } from "@primitiv-ui/react";
|
|
11
|
+
|
|
12
|
+
<Popover.Root>
|
|
13
|
+
<Popover.Trigger>Filters</Popover.Trigger>
|
|
14
|
+
<Popover.Content>
|
|
15
|
+
<Popover.Title>Filters</Popover.Title>
|
|
16
|
+
<Popover.Description>Narrow the results below.</Popover.Description>
|
|
17
|
+
<Popover.Close>Done</Popover.Close>
|
|
18
|
+
</Popover.Content>
|
|
19
|
+
</Popover.Root>;
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Sub-components
|
|
23
|
+
|
|
24
|
+
| Export | Role | Notes |
|
|
25
|
+
| ---------------------- | --------------------- | ----------------------------------------------------------------------------------------- |
|
|
26
|
+
| `Popover.Root` | State owner | Uncontrolled (`defaultOpen`) or controlled (`open` + `onOpenChange`); renders no DOM |
|
|
27
|
+
| `Popover.Trigger` | Toggle button | `<button>` with `aria-haspopup="dialog"` / `aria-expanded` / `aria-controls`; `asChild` |
|
|
28
|
+
| `Popover.Anchor` | Positioning reference | Optional `<div>` to anchor against instead of the trigger; `asChild` |
|
|
29
|
+
| `Popover.Content` | Floating panel | Native-popover `<div role="dialog">`; manages focus + Escape; `asChild`, ref-composing |
|
|
30
|
+
| `Popover.Close` | Close button | Composes `onClick` with close + focus return; `asChild` |
|
|
31
|
+
| `Popover.Title` | Accessible name | `<h2>`; auto-wires `aria-labelledby`; `asChild` |
|
|
32
|
+
| `Popover.Description` | Accessible description | `<p>`; auto-wires `aria-describedby`; `asChild` |
|
|
33
|
+
|
|
34
|
+
## State modes
|
|
35
|
+
|
|
36
|
+
- **Uncontrolled** — pass `defaultOpen` (or omit to start closed). `onOpenChange`
|
|
37
|
+
is optional and fires on every transition.
|
|
38
|
+
- **Controlled** — pass `open` and `onOpenChange` together. The parent owns the
|
|
39
|
+
value; the component defers every change back through the callback.
|
|
40
|
+
|
|
41
|
+
The two shapes are discriminated at the type level: passing `defaultOpen`
|
|
42
|
+
alongside `open` is a type error.
|
|
43
|
+
|
|
44
|
+
## Focus & keyboard
|
|
45
|
+
|
|
46
|
+
| Key | Behaviour |
|
|
47
|
+
| -------- | ---------------------------------------------- |
|
|
48
|
+
| `Escape` | Closes the popover, returns focus to the trigger |
|
|
49
|
+
|
|
50
|
+
On open, focus moves into the panel — to the first focusable descendant, or to
|
|
51
|
+
the `Popover.Content` element itself (it carries `tabIndex={-1}`) when it has
|
|
52
|
+
none. Closing via `Escape` or `Popover.Close` returns focus to the trigger.
|
|
53
|
+
There is **no focus trap**: the popover is non-modal and the rest of the page
|
|
54
|
+
remains reachable.
|
|
55
|
+
|
|
56
|
+
## Light-dismiss
|
|
57
|
+
|
|
58
|
+
Because `Popover.Content` uses `popover="auto"`, the browser light-dismisses it
|
|
59
|
+
on an outside click or `Escape` and promotes it to the top layer. The component
|
|
60
|
+
listens for the native `toggle` event to keep React state in sync, with a
|
|
61
|
+
document-click fallback for environments that don't dispatch it (e.g. jsdom).
|
|
62
|
+
|
|
63
|
+
## Positioning
|
|
64
|
+
|
|
65
|
+
Placement is a **CSS concern** — no floating-ui, no JS measurement. Use CSS
|
|
66
|
+
anchor positioning: give the trigger (or `Popover.Anchor`) an `anchor-name` and
|
|
67
|
+
`Popover.Content` a matching `position-anchor`, then place it with
|
|
68
|
+
`position-area` / `position-try-fallbacks`. Reach for `Popover.Anchor` when the
|
|
69
|
+
panel should be anchored to something other than the trigger — e.g. a whole
|
|
70
|
+
input group whose trigger is a small button.
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
<Popover.Root>
|
|
74
|
+
<Popover.Anchor asChild>
|
|
75
|
+
<div className="field" style={{ anchorName: "--popover" }}>
|
|
76
|
+
<input aria-label="Amount" />
|
|
77
|
+
<Popover.Trigger aria-label="Options">▾</Popover.Trigger>
|
|
78
|
+
</div>
|
|
79
|
+
</Popover.Anchor>
|
|
80
|
+
<Popover.Content /* position-anchor: --popover in CSS */>…</Popover.Content>
|
|
81
|
+
</Popover.Root>;
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## `asChild` composition
|
|
85
|
+
|
|
86
|
+
Every rendered sub-component accepts `asChild` to project its behaviour onto a
|
|
87
|
+
consumer element via [Slot](../Slot.tsx): event handlers compose (child first,
|
|
88
|
+
then the library's, unless the child calls `event.preventDefault()`), `style`
|
|
89
|
+
is shallow-merged, `className` strings concatenate, and refs compose. On
|
|
90
|
+
`Popover.Content`, a consumer `ref` composes with the internal ref so open state
|
|
91
|
+
keeps working.
|
|
92
|
+
|
|
93
|
+
## Styling hooks
|
|
94
|
+
|
|
95
|
+
- `data-state="open" | "closed"` on `Popover.Content`.
|
|
96
|
+
- `aria-expanded` on the trigger tracks the same state.
|
|
97
|
+
|
|
98
|
+
> **Gotcha — don't set `display` on the closed panel.** A closed popover is
|
|
99
|
+
> hidden by the UA rule `[popover]:not(:popover-open) { display: none }`. An
|
|
100
|
+
> author `display` (e.g. `display: flex` for layout) overrides it and leaves the
|
|
101
|
+
> panel visible while closed. Put layout `display` behind `:popover-open`:
|
|
102
|
+
>
|
|
103
|
+
> ```css
|
|
104
|
+
> .panel { padding: 12px; /* no display here */ }
|
|
105
|
+
> .panel:popover-open { display: flex; flex-direction: column; gap: 8px; }
|
|
106
|
+
> ```
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
|
|
3
|
+
import { Popover } from "../Popover";
|
|
4
|
+
|
|
5
|
+
describe("Popover.Anchor", () => {
|
|
6
|
+
it("renders a div wrapping its children as the positioning reference", () => {
|
|
7
|
+
// Arrange
|
|
8
|
+
render(
|
|
9
|
+
<Popover.Root>
|
|
10
|
+
<Popover.Anchor data-testid="anchor">
|
|
11
|
+
<span>Reference</span>
|
|
12
|
+
</Popover.Anchor>
|
|
13
|
+
<Popover.Trigger>Open</Popover.Trigger>
|
|
14
|
+
<Popover.Content>Content</Popover.Content>
|
|
15
|
+
</Popover.Root>,
|
|
16
|
+
);
|
|
17
|
+
const anchor = screen.getByTestId("anchor");
|
|
18
|
+
|
|
19
|
+
// Assert
|
|
20
|
+
expect(anchor.tagName).toBe("DIV");
|
|
21
|
+
expect(anchor).toContainElement(screen.getByText("Reference"));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("forwards arbitrary props to the rendered div", () => {
|
|
25
|
+
// Arrange
|
|
26
|
+
render(
|
|
27
|
+
<Popover.Root>
|
|
28
|
+
<Popover.Anchor data-testid="anchor" className="anchor-name" />
|
|
29
|
+
<Popover.Trigger>Open</Popover.Trigger>
|
|
30
|
+
<Popover.Content>Content</Popover.Content>
|
|
31
|
+
</Popover.Root>,
|
|
32
|
+
);
|
|
33
|
+
const anchor = screen.getByTestId("anchor");
|
|
34
|
+
|
|
35
|
+
// Assert
|
|
36
|
+
expect(anchor).toHaveClass("anchor-name");
|
|
37
|
+
});
|
|
38
|
+
});
|