@primitiv-ui/react 0.1.28 → 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,160 @@
|
|
|
1
|
+
import { createRef } from "react";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
|
|
5
|
+
import { Popover } from "../Popover";
|
|
6
|
+
|
|
7
|
+
describe("Popover asChild composition", () => {
|
|
8
|
+
it("renders the Trigger as a custom element with the trigger ARIA merged in", () => {
|
|
9
|
+
// Arrange
|
|
10
|
+
render(
|
|
11
|
+
<Popover.Root>
|
|
12
|
+
<Popover.Trigger<HTMLAnchorElement> asChild>
|
|
13
|
+
<a href="#open">Open</a>
|
|
14
|
+
</Popover.Trigger>
|
|
15
|
+
<Popover.Content>Content</Popover.Content>
|
|
16
|
+
</Popover.Root>,
|
|
17
|
+
);
|
|
18
|
+
const trigger = screen.getByRole("link", { name: "Open" });
|
|
19
|
+
|
|
20
|
+
// Assert
|
|
21
|
+
expect(trigger).toHaveAttribute("aria-haspopup", "dialog");
|
|
22
|
+
expect(trigger).toHaveAttribute("aria-expanded", "false");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("toggles the popover when the asChild Trigger is clicked and composes the child onClick", async () => {
|
|
26
|
+
// Arrange
|
|
27
|
+
const user = userEvent.setup();
|
|
28
|
+
const onClick = vi.fn();
|
|
29
|
+
render(
|
|
30
|
+
<Popover.Root>
|
|
31
|
+
<Popover.Trigger<HTMLAnchorElement> asChild>
|
|
32
|
+
<a href="#open" onClick={onClick}>
|
|
33
|
+
Open
|
|
34
|
+
</a>
|
|
35
|
+
</Popover.Trigger>
|
|
36
|
+
<Popover.Content>Content</Popover.Content>
|
|
37
|
+
</Popover.Root>,
|
|
38
|
+
);
|
|
39
|
+
const trigger = screen.getByRole("link", { name: "Open" });
|
|
40
|
+
|
|
41
|
+
// Act
|
|
42
|
+
await user.click(trigger);
|
|
43
|
+
|
|
44
|
+
// Assert
|
|
45
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
46
|
+
expect(trigger).toHaveAttribute("aria-expanded", "true");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("renders the Content as a custom element with the dialog props merged in", () => {
|
|
50
|
+
// Arrange
|
|
51
|
+
render(
|
|
52
|
+
<Popover.Root defaultOpen>
|
|
53
|
+
<Popover.Trigger>Open</Popover.Trigger>
|
|
54
|
+
<Popover.Content asChild>
|
|
55
|
+
<section data-testid="content">Content</section>
|
|
56
|
+
</Popover.Content>
|
|
57
|
+
</Popover.Root>,
|
|
58
|
+
);
|
|
59
|
+
const content = screen.getByTestId("content");
|
|
60
|
+
|
|
61
|
+
// Assert
|
|
62
|
+
expect(content.tagName).toBe("SECTION");
|
|
63
|
+
expect(content).toHaveAttribute("role", "dialog");
|
|
64
|
+
expect(content).toHaveAttribute("popover", "auto");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("renders the Close as a custom element that still closes the popover", async () => {
|
|
68
|
+
// Arrange
|
|
69
|
+
const user = userEvent.setup();
|
|
70
|
+
render(
|
|
71
|
+
<Popover.Root defaultOpen>
|
|
72
|
+
<Popover.Trigger>Open</Popover.Trigger>
|
|
73
|
+
<Popover.Content data-testid="content">
|
|
74
|
+
<Popover.Close asChild>
|
|
75
|
+
<a href="#close">Dismiss</a>
|
|
76
|
+
</Popover.Close>
|
|
77
|
+
</Popover.Content>
|
|
78
|
+
</Popover.Root>,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// Act
|
|
82
|
+
await user.click(screen.getByRole("link", { name: "Dismiss", hidden: true }));
|
|
83
|
+
|
|
84
|
+
// Assert
|
|
85
|
+
expect(screen.getByTestId("content")).toHaveAttribute("data-state", "closed");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("renders the Anchor as a custom element", () => {
|
|
89
|
+
// Arrange
|
|
90
|
+
render(
|
|
91
|
+
<Popover.Root>
|
|
92
|
+
<Popover.Anchor asChild>
|
|
93
|
+
<label data-testid="anchor">Email</label>
|
|
94
|
+
</Popover.Anchor>
|
|
95
|
+
<Popover.Trigger>Open</Popover.Trigger>
|
|
96
|
+
<Popover.Content>Content</Popover.Content>
|
|
97
|
+
</Popover.Root>,
|
|
98
|
+
);
|
|
99
|
+
const anchor = screen.getByTestId("anchor");
|
|
100
|
+
|
|
101
|
+
// Assert
|
|
102
|
+
expect(anchor.tagName).toBe("LABEL");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("renders the Title and Description as custom elements with ids still wired", () => {
|
|
106
|
+
// Arrange
|
|
107
|
+
render(
|
|
108
|
+
<Popover.Root defaultOpen>
|
|
109
|
+
<Popover.Trigger>Open</Popover.Trigger>
|
|
110
|
+
<Popover.Content data-testid="content">
|
|
111
|
+
<Popover.Title asChild>
|
|
112
|
+
<h3>Filters</h3>
|
|
113
|
+
</Popover.Title>
|
|
114
|
+
<Popover.Description asChild>
|
|
115
|
+
<div>Refine below.</div>
|
|
116
|
+
</Popover.Description>
|
|
117
|
+
</Popover.Content>
|
|
118
|
+
</Popover.Root>,
|
|
119
|
+
);
|
|
120
|
+
const content = screen.getByTestId("content");
|
|
121
|
+
const title = screen.getByRole("heading", { name: "Filters", hidden: true });
|
|
122
|
+
const description = screen.getByText("Refine below.");
|
|
123
|
+
|
|
124
|
+
// Assert
|
|
125
|
+
expect(title.tagName).toBe("H3");
|
|
126
|
+
expect(content).toHaveAttribute("aria-labelledby", title.id);
|
|
127
|
+
expect(content).toHaveAttribute("aria-describedby", description.id);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("forwards a ref to the underlying Trigger element", () => {
|
|
131
|
+
// Arrange
|
|
132
|
+
const ref = createRef<HTMLButtonElement>();
|
|
133
|
+
render(
|
|
134
|
+
<Popover.Root>
|
|
135
|
+
<Popover.Trigger ref={ref}>Open</Popover.Trigger>
|
|
136
|
+
<Popover.Content>Content</Popover.Content>
|
|
137
|
+
</Popover.Root>,
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// Assert
|
|
141
|
+
expect(ref.current).toBe(screen.getByRole("button", { name: "Open" }));
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("forwards a ref to the underlying Content element without breaking open state", () => {
|
|
145
|
+
// Arrange
|
|
146
|
+
const ref = createRef<HTMLDivElement>();
|
|
147
|
+
render(
|
|
148
|
+
<Popover.Root defaultOpen>
|
|
149
|
+
<Popover.Trigger>Open</Popover.Trigger>
|
|
150
|
+
<Popover.Content ref={ref} data-testid="content">Content</Popover.Content>
|
|
151
|
+
</Popover.Root>,
|
|
152
|
+
);
|
|
153
|
+
const content = screen.getByTestId("content");
|
|
154
|
+
|
|
155
|
+
// Assert
|
|
156
|
+
expect(ref.current).toBe(content);
|
|
157
|
+
expect(content).toHaveAttribute("data-state", "open");
|
|
158
|
+
expect(content).toHaveAttribute("data-popover-open");
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
|
|
3
|
+
import { Popover } from "../Popover";
|
|
4
|
+
|
|
5
|
+
describe("Popover basic rendering tests", () => {
|
|
6
|
+
it("should render Popover.Trigger as a button with type='button'", () => {
|
|
7
|
+
// Arrange
|
|
8
|
+
render(
|
|
9
|
+
<Popover.Root>
|
|
10
|
+
<Popover.Trigger>Open</Popover.Trigger>
|
|
11
|
+
<Popover.Content>Content</Popover.Content>
|
|
12
|
+
</Popover.Root>,
|
|
13
|
+
);
|
|
14
|
+
const trigger = screen.getByRole("button", { name: "Open" });
|
|
15
|
+
|
|
16
|
+
// Assert
|
|
17
|
+
expect(trigger.tagName).toBe("BUTTON");
|
|
18
|
+
expect(trigger).toHaveAttribute("type", "button");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("should give Popover.Trigger aria-haspopup='dialog'", () => {
|
|
22
|
+
// Arrange
|
|
23
|
+
render(
|
|
24
|
+
<Popover.Root>
|
|
25
|
+
<Popover.Trigger>Open</Popover.Trigger>
|
|
26
|
+
<Popover.Content>Content</Popover.Content>
|
|
27
|
+
</Popover.Root>,
|
|
28
|
+
);
|
|
29
|
+
const trigger = screen.getByRole("button", { name: "Open" });
|
|
30
|
+
|
|
31
|
+
// Assert
|
|
32
|
+
expect(trigger).toHaveAttribute("aria-haspopup", "dialog");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should give Popover.Trigger aria-expanded='false' by default", () => {
|
|
36
|
+
// Arrange
|
|
37
|
+
render(
|
|
38
|
+
<Popover.Root>
|
|
39
|
+
<Popover.Trigger>Open</Popover.Trigger>
|
|
40
|
+
<Popover.Content>Content</Popover.Content>
|
|
41
|
+
</Popover.Root>,
|
|
42
|
+
);
|
|
43
|
+
const trigger = screen.getByRole("button", { name: "Open" });
|
|
44
|
+
|
|
45
|
+
// Assert
|
|
46
|
+
expect(trigger).toHaveAttribute("aria-expanded", "false");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should wire aria-controls on Trigger to the id of Content", () => {
|
|
50
|
+
// Arrange
|
|
51
|
+
render(
|
|
52
|
+
<Popover.Root>
|
|
53
|
+
<Popover.Trigger>Open</Popover.Trigger>
|
|
54
|
+
<Popover.Content data-testid="content">Content</Popover.Content>
|
|
55
|
+
</Popover.Root>,
|
|
56
|
+
);
|
|
57
|
+
const trigger = screen.getByRole("button", { name: "Open" });
|
|
58
|
+
const content = screen.getByTestId("content");
|
|
59
|
+
|
|
60
|
+
// Assert
|
|
61
|
+
expect(trigger).toHaveAttribute("aria-controls", content.id);
|
|
62
|
+
expect(content.id).not.toBe("");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("should render Popover.Content as a native-popover div with role='dialog'", () => {
|
|
66
|
+
// Arrange
|
|
67
|
+
render(
|
|
68
|
+
<Popover.Root>
|
|
69
|
+
<Popover.Trigger>Open</Popover.Trigger>
|
|
70
|
+
<Popover.Content>Content</Popover.Content>
|
|
71
|
+
</Popover.Root>,
|
|
72
|
+
);
|
|
73
|
+
const content = screen.getByRole("dialog", { hidden: true });
|
|
74
|
+
|
|
75
|
+
// Assert
|
|
76
|
+
expect(content.tagName).toBe("DIV");
|
|
77
|
+
expect(content).toHaveAttribute("popover", "auto");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("should give Popover.Content data-state='closed' by default", () => {
|
|
81
|
+
// Arrange
|
|
82
|
+
render(
|
|
83
|
+
<Popover.Root>
|
|
84
|
+
<Popover.Trigger>Open</Popover.Trigger>
|
|
85
|
+
<Popover.Content data-testid="content">Content</Popover.Content>
|
|
86
|
+
</Popover.Root>,
|
|
87
|
+
);
|
|
88
|
+
const content = screen.getByTestId("content");
|
|
89
|
+
|
|
90
|
+
// Assert
|
|
91
|
+
expect(content).toHaveAttribute("data-state", "closed");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should render no DOM element for Popover.Root itself", () => {
|
|
95
|
+
// Arrange
|
|
96
|
+
const { container } = render(
|
|
97
|
+
<Popover.Root>
|
|
98
|
+
<Popover.Trigger>Open</Popover.Trigger>
|
|
99
|
+
<Popover.Content>Content</Popover.Content>
|
|
100
|
+
</Popover.Root>,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
// Assert — the trigger button is the first rendered node, not a Root wrapper
|
|
104
|
+
expect(container.firstElementChild?.tagName).toBe("BUTTON");
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { act, render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
|
|
4
|
+
import { Popover } from "../Popover";
|
|
5
|
+
|
|
6
|
+
describe("Popover light-dismiss", () => {
|
|
7
|
+
it("closes when the user clicks outside the open popover", async () => {
|
|
8
|
+
// Arrange
|
|
9
|
+
const user = userEvent.setup();
|
|
10
|
+
render(
|
|
11
|
+
<div>
|
|
12
|
+
<Popover.Root defaultOpen>
|
|
13
|
+
<Popover.Trigger>Open</Popover.Trigger>
|
|
14
|
+
<Popover.Content data-testid="content">Content</Popover.Content>
|
|
15
|
+
</Popover.Root>
|
|
16
|
+
<button type="button">Outside</button>
|
|
17
|
+
</div>,
|
|
18
|
+
);
|
|
19
|
+
const trigger = screen.getByRole("button", { name: "Open" });
|
|
20
|
+
const content = screen.getByTestId("content");
|
|
21
|
+
|
|
22
|
+
// Act
|
|
23
|
+
await user.click(screen.getByRole("button", { name: "Outside" }));
|
|
24
|
+
|
|
25
|
+
// Assert
|
|
26
|
+
expect(trigger).toHaveAttribute("aria-expanded", "false");
|
|
27
|
+
expect(content).toHaveAttribute("data-state", "closed");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("stays open when the user clicks inside the content", async () => {
|
|
31
|
+
// Arrange
|
|
32
|
+
const user = userEvent.setup();
|
|
33
|
+
render(
|
|
34
|
+
<Popover.Root defaultOpen>
|
|
35
|
+
<Popover.Trigger>Open</Popover.Trigger>
|
|
36
|
+
<Popover.Content data-testid="content">
|
|
37
|
+
<span>Inside</span>
|
|
38
|
+
</Popover.Content>
|
|
39
|
+
</Popover.Root>,
|
|
40
|
+
);
|
|
41
|
+
const trigger = screen.getByRole("button", { name: "Open" });
|
|
42
|
+
const content = screen.getByTestId("content");
|
|
43
|
+
|
|
44
|
+
// Act
|
|
45
|
+
await user.click(screen.getByText("Inside"));
|
|
46
|
+
|
|
47
|
+
// Assert
|
|
48
|
+
expect(trigger).toHaveAttribute("aria-expanded", "true");
|
|
49
|
+
expect(content).toHaveAttribute("data-state", "open");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("syncs React state to closed when the browser light-dismisses (native toggle event)", () => {
|
|
53
|
+
// Arrange
|
|
54
|
+
render(
|
|
55
|
+
<Popover.Root defaultOpen>
|
|
56
|
+
<Popover.Trigger>Open</Popover.Trigger>
|
|
57
|
+
<Popover.Content data-testid="content">Content</Popover.Content>
|
|
58
|
+
</Popover.Root>,
|
|
59
|
+
);
|
|
60
|
+
const trigger = screen.getByRole("button", { name: "Open" });
|
|
61
|
+
const content = screen.getByTestId("content");
|
|
62
|
+
|
|
63
|
+
// Act — simulate the browser closing the popover on its own
|
|
64
|
+
const toggleEvent = new Event("toggle");
|
|
65
|
+
Object.defineProperty(toggleEvent, "newState", { value: "closed" });
|
|
66
|
+
act(() => {
|
|
67
|
+
content.dispatchEvent(toggleEvent);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Assert
|
|
71
|
+
expect(trigger).toHaveAttribute("aria-expanded", "false");
|
|
72
|
+
expect(content).toHaveAttribute("data-state", "closed");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("ignores a native toggle event that reports the popover is opening", () => {
|
|
76
|
+
// Arrange
|
|
77
|
+
render(
|
|
78
|
+
<Popover.Root defaultOpen>
|
|
79
|
+
<Popover.Trigger>Open</Popover.Trigger>
|
|
80
|
+
<Popover.Content data-testid="content">Content</Popover.Content>
|
|
81
|
+
</Popover.Root>,
|
|
82
|
+
);
|
|
83
|
+
const content = screen.getByTestId("content");
|
|
84
|
+
|
|
85
|
+
// Act
|
|
86
|
+
const toggleEvent = new Event("toggle");
|
|
87
|
+
Object.defineProperty(toggleEvent, "newState", { value: "open" });
|
|
88
|
+
content.dispatchEvent(toggleEvent);
|
|
89
|
+
|
|
90
|
+
// Assert
|
|
91
|
+
expect(content).toHaveAttribute("data-state", "open");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("skips the document-click fallback when the native Popover API is present", async () => {
|
|
95
|
+
// Simulate a real browser (native light-dismiss + toggle event handle
|
|
96
|
+
// outside clicks). The document-click fallback must not run there — it
|
|
97
|
+
// would catch the click that opened the popover from an external control
|
|
98
|
+
// and re-close it. With the fallback gated out and no native light-dismiss
|
|
99
|
+
// in jsdom, an outside click leaves the popover open.
|
|
100
|
+
Object.defineProperty(HTMLElement.prototype, "popover", {
|
|
101
|
+
configurable: true,
|
|
102
|
+
get() {
|
|
103
|
+
return "auto";
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
try {
|
|
107
|
+
const user = userEvent.setup();
|
|
108
|
+
render(
|
|
109
|
+
<div>
|
|
110
|
+
<Popover.Root defaultOpen>
|
|
111
|
+
<Popover.Trigger>Open</Popover.Trigger>
|
|
112
|
+
<Popover.Content data-testid="content">Content</Popover.Content>
|
|
113
|
+
</Popover.Root>
|
|
114
|
+
<button type="button">Outside</button>
|
|
115
|
+
</div>,
|
|
116
|
+
);
|
|
117
|
+
const content = screen.getByTestId("content");
|
|
118
|
+
|
|
119
|
+
await user.click(screen.getByRole("button", { name: "Outside" }));
|
|
120
|
+
|
|
121
|
+
expect(content).toHaveAttribute("data-state", "open");
|
|
122
|
+
} finally {
|
|
123
|
+
delete (HTMLElement.prototype as unknown as { popover?: unknown })
|
|
124
|
+
.popover;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("closes only once when the trigger itself is clicked to close (no double dismiss)", async () => {
|
|
129
|
+
// Arrange
|
|
130
|
+
const user = userEvent.setup();
|
|
131
|
+
const onOpenChange = vi.fn();
|
|
132
|
+
render(
|
|
133
|
+
<Popover.Root onOpenChange={onOpenChange}>
|
|
134
|
+
<Popover.Trigger>Open</Popover.Trigger>
|
|
135
|
+
<Popover.Content>Content</Popover.Content>
|
|
136
|
+
</Popover.Root>,
|
|
137
|
+
);
|
|
138
|
+
const trigger = screen.getByRole("button", { name: "Open" });
|
|
139
|
+
|
|
140
|
+
// Act
|
|
141
|
+
await user.click(trigger); // open
|
|
142
|
+
await user.click(trigger); // close via the trigger, not light-dismiss
|
|
143
|
+
|
|
144
|
+
// Assert
|
|
145
|
+
expect(onOpenChange).toHaveBeenNthCalledWith(1, true);
|
|
146
|
+
expect(onOpenChange).toHaveBeenNthCalledWith(2, false);
|
|
147
|
+
expect(onOpenChange).toHaveBeenCalledTimes(2);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
|
|
4
|
+
import { Popover } from "../Popover";
|
|
5
|
+
|
|
6
|
+
describe("Popover.Close", () => {
|
|
7
|
+
it("renders a button that closes the popover when clicked", async () => {
|
|
8
|
+
// Arrange
|
|
9
|
+
const user = userEvent.setup();
|
|
10
|
+
render(
|
|
11
|
+
<Popover.Root defaultOpen>
|
|
12
|
+
<Popover.Trigger>Open</Popover.Trigger>
|
|
13
|
+
<Popover.Content data-testid="content">
|
|
14
|
+
<Popover.Close>Dismiss</Popover.Close>
|
|
15
|
+
</Popover.Content>
|
|
16
|
+
</Popover.Root>,
|
|
17
|
+
);
|
|
18
|
+
const trigger = screen.getByRole("button", { name: "Open" });
|
|
19
|
+
const content = screen.getByTestId("content");
|
|
20
|
+
|
|
21
|
+
// Act
|
|
22
|
+
await user.click(screen.getByRole("button", { name: "Dismiss", hidden: true }));
|
|
23
|
+
|
|
24
|
+
// Assert
|
|
25
|
+
expect(content).toHaveAttribute("data-state", "closed");
|
|
26
|
+
expect(trigger).toHaveAttribute("aria-expanded", "false");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("returns focus to the trigger after closing", async () => {
|
|
30
|
+
// Arrange
|
|
31
|
+
const user = userEvent.setup();
|
|
32
|
+
render(
|
|
33
|
+
<Popover.Root defaultOpen>
|
|
34
|
+
<Popover.Trigger>Open</Popover.Trigger>
|
|
35
|
+
<Popover.Content>
|
|
36
|
+
<Popover.Close>Dismiss</Popover.Close>
|
|
37
|
+
</Popover.Content>
|
|
38
|
+
</Popover.Root>,
|
|
39
|
+
);
|
|
40
|
+
const trigger = screen.getByRole("button", { name: "Open" });
|
|
41
|
+
|
|
42
|
+
// Act
|
|
43
|
+
await user.click(screen.getByRole("button", { name: "Dismiss", hidden: true }));
|
|
44
|
+
|
|
45
|
+
// Assert
|
|
46
|
+
expect(trigger).toHaveFocus();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("lets a consumer onClick veto the close via preventDefault", async () => {
|
|
50
|
+
// Arrange
|
|
51
|
+
const user = userEvent.setup();
|
|
52
|
+
render(
|
|
53
|
+
<Popover.Root defaultOpen>
|
|
54
|
+
<Popover.Trigger>Open</Popover.Trigger>
|
|
55
|
+
<Popover.Content data-testid="content">
|
|
56
|
+
<Popover.Close onClick={(event) => event.preventDefault()}>
|
|
57
|
+
Dismiss
|
|
58
|
+
</Popover.Close>
|
|
59
|
+
</Popover.Content>
|
|
60
|
+
</Popover.Root>,
|
|
61
|
+
);
|
|
62
|
+
const content = screen.getByTestId("content");
|
|
63
|
+
|
|
64
|
+
// Act
|
|
65
|
+
await user.click(screen.getByRole("button", { name: "Dismiss", hidden: true }));
|
|
66
|
+
|
|
67
|
+
// Assert
|
|
68
|
+
expect(content).toHaveAttribute("data-state", "open");
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
|
|
3
|
+
import { Popover } from "../Popover";
|
|
4
|
+
|
|
5
|
+
describe("Popover.Title and Popover.Description", () => {
|
|
6
|
+
it("wires Content aria-labelledby to a Popover.Title", () => {
|
|
7
|
+
// Arrange
|
|
8
|
+
render(
|
|
9
|
+
<Popover.Root defaultOpen>
|
|
10
|
+
<Popover.Trigger>Open</Popover.Trigger>
|
|
11
|
+
<Popover.Content data-testid="content">
|
|
12
|
+
<Popover.Title>Filters</Popover.Title>
|
|
13
|
+
</Popover.Content>
|
|
14
|
+
</Popover.Root>,
|
|
15
|
+
);
|
|
16
|
+
const content = screen.getByTestId("content");
|
|
17
|
+
const title = screen.getByRole("heading", { name: "Filters", hidden: true });
|
|
18
|
+
|
|
19
|
+
// Assert
|
|
20
|
+
expect(title.tagName).toBe("H2");
|
|
21
|
+
expect(content).toHaveAttribute("aria-labelledby", title.id);
|
|
22
|
+
expect(title.id).not.toBe("");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("wires Content aria-describedby to a Popover.Description", () => {
|
|
26
|
+
// Arrange
|
|
27
|
+
render(
|
|
28
|
+
<Popover.Root defaultOpen>
|
|
29
|
+
<Popover.Trigger>Open</Popover.Trigger>
|
|
30
|
+
<Popover.Content data-testid="content">
|
|
31
|
+
<Popover.Description>Narrow the results below.</Popover.Description>
|
|
32
|
+
</Popover.Content>
|
|
33
|
+
</Popover.Root>,
|
|
34
|
+
);
|
|
35
|
+
const content = screen.getByTestId("content");
|
|
36
|
+
const description = screen.getByText("Narrow the results below.");
|
|
37
|
+
|
|
38
|
+
// Assert
|
|
39
|
+
expect(description.tagName).toBe("P");
|
|
40
|
+
expect(content).toHaveAttribute("aria-describedby", description.id);
|
|
41
|
+
expect(description.id).not.toBe("");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("omits aria-labelledby and aria-describedby when neither is present", () => {
|
|
45
|
+
// Arrange
|
|
46
|
+
render(
|
|
47
|
+
<Popover.Root defaultOpen>
|
|
48
|
+
<Popover.Trigger>Open</Popover.Trigger>
|
|
49
|
+
<Popover.Content data-testid="content">Content</Popover.Content>
|
|
50
|
+
</Popover.Root>,
|
|
51
|
+
);
|
|
52
|
+
const content = screen.getByTestId("content");
|
|
53
|
+
|
|
54
|
+
// Assert
|
|
55
|
+
expect(content).not.toHaveAttribute("aria-labelledby");
|
|
56
|
+
expect(content).not.toHaveAttribute("aria-describedby");
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
|
|
4
|
+
import { Popover } from "../Popover";
|
|
5
|
+
|
|
6
|
+
describe("Popover controlled state", () => {
|
|
7
|
+
it("reflects the controlled open prop", () => {
|
|
8
|
+
// Arrange & Act
|
|
9
|
+
render(
|
|
10
|
+
<Popover.Root open onOpenChange={() => {}}>
|
|
11
|
+
<Popover.Trigger>Open</Popover.Trigger>
|
|
12
|
+
<Popover.Content data-testid="content">Content</Popover.Content>
|
|
13
|
+
</Popover.Root>,
|
|
14
|
+
);
|
|
15
|
+
const trigger = screen.getByRole("button", { name: "Open" });
|
|
16
|
+
const content = screen.getByTestId("content");
|
|
17
|
+
|
|
18
|
+
// Assert
|
|
19
|
+
expect(trigger).toHaveAttribute("aria-expanded", "true");
|
|
20
|
+
expect(content).toHaveAttribute("data-state", "open");
|
|
21
|
+
expect(content).toHaveAttribute("data-popover-open");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("defers state changes to the parent — clicking calls onOpenChange but does not self-open", async () => {
|
|
25
|
+
// Arrange
|
|
26
|
+
const user = userEvent.setup();
|
|
27
|
+
const onOpenChange = vi.fn();
|
|
28
|
+
render(
|
|
29
|
+
<Popover.Root open={false} onOpenChange={onOpenChange}>
|
|
30
|
+
<Popover.Trigger>Open</Popover.Trigger>
|
|
31
|
+
<Popover.Content data-testid="content">Content</Popover.Content>
|
|
32
|
+
</Popover.Root>,
|
|
33
|
+
);
|
|
34
|
+
const trigger = screen.getByRole("button", { name: "Open" });
|
|
35
|
+
const content = screen.getByTestId("content");
|
|
36
|
+
|
|
37
|
+
// Act
|
|
38
|
+
await user.click(trigger);
|
|
39
|
+
|
|
40
|
+
// Assert
|
|
41
|
+
expect(onOpenChange).toHaveBeenCalledExactlyOnceWith(true);
|
|
42
|
+
expect(trigger).toHaveAttribute("aria-expanded", "false");
|
|
43
|
+
expect(content).toHaveAttribute("data-state", "closed");
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { render } from "@testing-library/react";
|
|
2
|
+
|
|
3
|
+
import { Popover } from "../Popover";
|
|
4
|
+
|
|
5
|
+
describe("Popover error handling", () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
// Suppress React's noisy uncaught-error logging while we deliberately
|
|
8
|
+
// render sub-components outside their Root.
|
|
9
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
vi.restoreAllMocks();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("throws when Popover.Trigger is rendered without a Popover.Root", () => {
|
|
17
|
+
expect(() => render(<Popover.Trigger>Open</Popover.Trigger>)).toThrow(
|
|
18
|
+
/Popover\.Root/,
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("throws when Popover.Anchor is rendered without a Popover.Root", () => {
|
|
23
|
+
expect(() => render(<Popover.Anchor>Ref</Popover.Anchor>)).toThrow(
|
|
24
|
+
/Popover\.Root/,
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("throws when Popover.Content is rendered without a Popover.Root", () => {
|
|
29
|
+
expect(() => render(<Popover.Content>Content</Popover.Content>)).toThrow(
|
|
30
|
+
/Popover\.Root/,
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("throws when Popover.Close is rendered without a Popover.Root", () => {
|
|
35
|
+
expect(() => render(<Popover.Close>Close</Popover.Close>)).toThrow(
|
|
36
|
+
/Popover\.Root/,
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("throws when Popover.Title is rendered without a Popover.Root", () => {
|
|
41
|
+
expect(() => render(<Popover.Title>Title</Popover.Title>)).toThrow(
|
|
42
|
+
/Popover\.Root/,
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("throws when Popover.Description is rendered without a Popover.Root", () => {
|
|
47
|
+
expect(() =>
|
|
48
|
+
render(<Popover.Description>Desc</Popover.Description>),
|
|
49
|
+
).toThrow(/Popover\.Root/);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
|
|
4
|
+
import { Popover } from "../Popover";
|
|
5
|
+
|
|
6
|
+
describe("Popover focus management", () => {
|
|
7
|
+
it("moves focus to the first focusable element inside the content on open", async () => {
|
|
8
|
+
// Arrange
|
|
9
|
+
const user = userEvent.setup();
|
|
10
|
+
render(
|
|
11
|
+
<Popover.Root>
|
|
12
|
+
<Popover.Trigger>Open</Popover.Trigger>
|
|
13
|
+
<Popover.Content>
|
|
14
|
+
<button type="button">First</button>
|
|
15
|
+
<button type="button">Second</button>
|
|
16
|
+
</Popover.Content>
|
|
17
|
+
</Popover.Root>,
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
// Act
|
|
21
|
+
await user.click(screen.getByRole("button", { name: "Open" }));
|
|
22
|
+
|
|
23
|
+
// Assert
|
|
24
|
+
expect(screen.getByRole("button", { name: "First", hidden: true })).toHaveFocus();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("moves focus to the content element itself when it has no focusable children", async () => {
|
|
28
|
+
// Arrange
|
|
29
|
+
const user = userEvent.setup();
|
|
30
|
+
render(
|
|
31
|
+
<Popover.Root>
|
|
32
|
+
<Popover.Trigger>Open</Popover.Trigger>
|
|
33
|
+
<Popover.Content data-testid="content">Just text, nothing to focus.</Popover.Content>
|
|
34
|
+
</Popover.Root>,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
// Act
|
|
38
|
+
await user.click(screen.getByRole("button", { name: "Open" }));
|
|
39
|
+
|
|
40
|
+
// Assert
|
|
41
|
+
expect(screen.getByTestId("content")).toHaveFocus();
|
|
42
|
+
});
|
|
43
|
+
});
|