@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,254 @@
|
|
|
1
|
+
import { act, render, screen, waitFor } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
|
|
4
|
+
import { ThreeEntryNav } from "./NavigationMenu.fixtures";
|
|
5
|
+
|
|
6
|
+
describe("NavigationMenu — hover interaction", () => {
|
|
7
|
+
it("opens a panel once the delayDuration elapses on pointer enter", async () => {
|
|
8
|
+
const user = userEvent.setup();
|
|
9
|
+
render(<ThreeEntryNav delayDuration={1} />);
|
|
10
|
+
|
|
11
|
+
await user.hover(screen.getByRole("button", { name: "Concepts" }));
|
|
12
|
+
|
|
13
|
+
await waitFor(() =>
|
|
14
|
+
expect(screen.getByTestId("concepts-panel")).toBeVisible(),
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("cancels the pending open when the pointer leaves before the delay elapses", async () => {
|
|
19
|
+
const user = userEvent.setup();
|
|
20
|
+
render(<ThreeEntryNav delayDuration={5000} />);
|
|
21
|
+
|
|
22
|
+
const trigger = screen.getByRole("button", { name: "Concepts" });
|
|
23
|
+
await user.hover(trigger);
|
|
24
|
+
await user.unhover(trigger);
|
|
25
|
+
|
|
26
|
+
expect(screen.getByTestId("concepts-panel")).not.toBeVisible();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("switches panels with no delay while one is already open", async () => {
|
|
30
|
+
const user = userEvent.setup();
|
|
31
|
+
render(<ThreeEntryNav delayDuration={5000} defaultValue="concepts" />);
|
|
32
|
+
|
|
33
|
+
await user.hover(screen.getByRole("button", { name: "Registry & CLI" }));
|
|
34
|
+
|
|
35
|
+
expect(screen.getByTestId("registry-panel")).toBeVisible();
|
|
36
|
+
expect(screen.getByTestId("concepts-panel")).not.toBeVisible();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("keeps the panel open while the pointer moves from trigger into content", async () => {
|
|
40
|
+
const user = userEvent.setup();
|
|
41
|
+
render(<ThreeEntryNav defaultValue="concepts" closeDelay={1} />);
|
|
42
|
+
|
|
43
|
+
await user.hover(screen.getByRole("button", { name: "Concepts" }));
|
|
44
|
+
await user.hover(screen.getByRole("link", { name: "Tokens" }));
|
|
45
|
+
|
|
46
|
+
expect(screen.getByTestId("concepts-panel")).toBeVisible();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("closes after the closeDelay once the pointer leaves the nav", async () => {
|
|
50
|
+
const user = userEvent.setup();
|
|
51
|
+
render(<ThreeEntryNav defaultValue="concepts" closeDelay={1} />);
|
|
52
|
+
|
|
53
|
+
await user.hover(screen.getByRole("button", { name: "Concepts" }));
|
|
54
|
+
await user.unhover(screen.getByRole("navigation", { name: "Main" }));
|
|
55
|
+
|
|
56
|
+
await waitFor(() =>
|
|
57
|
+
expect(screen.getByTestId("concepts-panel")).not.toBeVisible(),
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("forgets the pointer's arrival once it leaves, so Enter still toggles", async () => {
|
|
62
|
+
const user = userEvent.setup();
|
|
63
|
+
render(<ThreeEntryNav delayDuration={0} closeDelay={5000} />);
|
|
64
|
+
const trigger = screen.getByRole("button", { name: "Concepts" });
|
|
65
|
+
|
|
66
|
+
await user.hover(trigger);
|
|
67
|
+
await user.hover(screen.getByRole("list"));
|
|
68
|
+
trigger.focus();
|
|
69
|
+
await user.keyboard("{Enter}");
|
|
70
|
+
|
|
71
|
+
// The pointer has gone, so there is no arrival state left to toggle
|
|
72
|
+
// against: Enter has to read the panel that is actually open.
|
|
73
|
+
expect(screen.getByTestId("concepts-panel")).not.toBeVisible();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("ignores hover entirely when openOnHover is false", async () => {
|
|
77
|
+
const user = userEvent.setup();
|
|
78
|
+
render(<ThreeEntryNav openOnHover={false} delayDuration={1} />);
|
|
79
|
+
|
|
80
|
+
await user.hover(screen.getByRole("button", { name: "Concepts" }));
|
|
81
|
+
|
|
82
|
+
expect(screen.getByTestId("concepts-panel")).not.toBeVisible();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("leaves the panel open when a click follows the pointer's own hover-open", async () => {
|
|
86
|
+
const user = userEvent.setup();
|
|
87
|
+
render(<ThreeEntryNav delayDuration={0} />);
|
|
88
|
+
|
|
89
|
+
// user.click fires pointerenter first, which opens the panel; the click
|
|
90
|
+
// must not read that as "already open" and undo it.
|
|
91
|
+
await user.click(screen.getByRole("button", { name: "Concepts" }));
|
|
92
|
+
|
|
93
|
+
expect(screen.getByTestId("concepts-panel")).toBeVisible();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("closes on a second click while the trigger is still hovered", async () => {
|
|
97
|
+
const user = userEvent.setup();
|
|
98
|
+
render(<ThreeEntryNav delayDuration={0} />);
|
|
99
|
+
|
|
100
|
+
const trigger = screen.getByRole("button", { name: "Concepts" });
|
|
101
|
+
await user.click(trigger);
|
|
102
|
+
await user.click(trigger);
|
|
103
|
+
|
|
104
|
+
expect(screen.getByTestId("concepts-panel")).not.toBeVisible();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
/** Lets real time pass so a timer that was *not* cancelled gets its chance to
|
|
109
|
+
* fire. Wrapped in `act` at the call site: a state update from a timer that
|
|
110
|
+
* fires outside `act` is queued rather than applied, so an unwrapped wait
|
|
111
|
+
* cannot see the panel it opened. */
|
|
112
|
+
function waitPast(ms: number): Promise<void> {
|
|
113
|
+
return new Promise((resolve) => {
|
|
114
|
+
setTimeout(resolve, ms);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The timing half of hover intent, where "the timer was cancelled" has to stay
|
|
120
|
+
* distinguishable from "the timer hasn't fired yet". Two techniques do that:
|
|
121
|
+
*
|
|
122
|
+
* - **A short delay plus a real wait several times past it.** A panel that is
|
|
123
|
+
* still closed then was cancelled, not merely outrun. The delay also has to
|
|
124
|
+
* outlast the gap before the cancelling action, or a loaded machine lets the
|
|
125
|
+
* timer win that race and the test flakes.
|
|
126
|
+
* - **`delay: null` with a multi-step `user.pointer` call.** `userEvent`
|
|
127
|
+
* normally awaits a macrotask between events, which is long enough for a
|
|
128
|
+
* zero-delay timer to fire and so hides the difference between opening *now*
|
|
129
|
+
* and opening *on the next tick*. With the waits switched off, the whole
|
|
130
|
+
* pointer sequence runs before any timer gets a turn.
|
|
131
|
+
*
|
|
132
|
+
* Where the pointer is meant to stay inside the nav it moves to the `<ul>`
|
|
133
|
+
* rather than out: leaving the `<nav>` runs `closeWithDelay`, which cancels a
|
|
134
|
+
* pending open of its own and would mask what the trigger's own leave handler
|
|
135
|
+
* does.
|
|
136
|
+
*/
|
|
137
|
+
describe("NavigationMenu — hover intent timing", () => {
|
|
138
|
+
it("opens on a zero delay outright, so leaving straight away cannot undo it", async () => {
|
|
139
|
+
const user = userEvent.setup({ delay: null });
|
|
140
|
+
render(<ThreeEntryNav delayDuration={0} />);
|
|
141
|
+
|
|
142
|
+
await user.pointer([
|
|
143
|
+
{ target: screen.getByRole("button", { name: "Concepts" }) },
|
|
144
|
+
{ target: screen.getByRole("list") },
|
|
145
|
+
]);
|
|
146
|
+
|
|
147
|
+
// Zero means "now": the panel is already open by the time the pointer has
|
|
148
|
+
// moved off, so there is no pending open left for the leave to cancel.
|
|
149
|
+
expect(screen.getByTestId("concepts-panel")).toBeVisible();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("drops the pending open when the pointer leaves the trigger for the list", async () => {
|
|
153
|
+
const user = userEvent.setup({ delay: null });
|
|
154
|
+
render(<ThreeEntryNav delayDuration={100} />);
|
|
155
|
+
|
|
156
|
+
await user.pointer([
|
|
157
|
+
{ target: screen.getByRole("button", { name: "Concepts" }) },
|
|
158
|
+
{ target: screen.getByRole("list") },
|
|
159
|
+
]);
|
|
160
|
+
await act(() => waitPast(400));
|
|
161
|
+
|
|
162
|
+
expect(screen.getByTestId("concepts-panel")).not.toBeVisible();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("abandons a pending open when the nav unmounts", async () => {
|
|
166
|
+
const user = userEvent.setup({ delay: null });
|
|
167
|
+
const onValueChange = vi.fn();
|
|
168
|
+
const { unmount } = render(
|
|
169
|
+
<ThreeEntryNav
|
|
170
|
+
value=""
|
|
171
|
+
onValueChange={onValueChange}
|
|
172
|
+
delayDuration={100}
|
|
173
|
+
/>,
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
await user.hover(screen.getByRole("button", { name: "Concepts" }));
|
|
177
|
+
// Nothing has opened yet, so the timer this cancels is genuinely pending.
|
|
178
|
+
expect(onValueChange).not.toHaveBeenCalled();
|
|
179
|
+
unmount();
|
|
180
|
+
await act(() => waitPast(400));
|
|
181
|
+
|
|
182
|
+
expect(onValueChange).not.toHaveBeenCalled();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("keeps the panel open past the closeDelay when the pointer returns", async () => {
|
|
186
|
+
const user = userEvent.setup({ delay: null });
|
|
187
|
+
render(<ThreeEntryNav defaultValue="concepts" closeDelay={100} />);
|
|
188
|
+
const nav = screen.getByRole("navigation", { name: "Main" });
|
|
189
|
+
|
|
190
|
+
await user.hover(screen.getByRole("button", { name: "Concepts" }));
|
|
191
|
+
await user.pointer([{ target: document.body }, { target: nav }]);
|
|
192
|
+
await act(() => waitPast(400));
|
|
193
|
+
|
|
194
|
+
// Returning has to cancel the pending close outright — not merely outrun it.
|
|
195
|
+
expect(screen.getByTestId("concepts-panel")).toBeVisible();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("honours a delayDuration changed after the first render", async () => {
|
|
199
|
+
const user = userEvent.setup();
|
|
200
|
+
const { rerender } = render(<ThreeEntryNav delayDuration={5000} />);
|
|
201
|
+
|
|
202
|
+
rerender(<ThreeEntryNav delayDuration={0} />);
|
|
203
|
+
await user.hover(screen.getByRole("button", { name: "Concepts" }));
|
|
204
|
+
|
|
205
|
+
expect(screen.getByTestId("concepts-panel")).toBeVisible();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("honours a closeDelay changed after the first render", async () => {
|
|
209
|
+
const user = userEvent.setup();
|
|
210
|
+
const { rerender } = render(
|
|
211
|
+
<ThreeEntryNav defaultValue="concepts" closeDelay={5000} />,
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
rerender(<ThreeEntryNav defaultValue="concepts" closeDelay={20} />);
|
|
215
|
+
await user.hover(screen.getByRole("button", { name: "Concepts" }));
|
|
216
|
+
await user.unhover(screen.getByRole("navigation", { name: "Main" }));
|
|
217
|
+
|
|
218
|
+
await waitFor(() =>
|
|
219
|
+
expect(screen.getByTestId("concepts-panel")).not.toBeVisible(),
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("requests the delayed close with the empty string", async () => {
|
|
224
|
+
const user = userEvent.setup();
|
|
225
|
+
const onValueChange = vi.fn();
|
|
226
|
+
render(
|
|
227
|
+
<ThreeEntryNav
|
|
228
|
+
value="concepts"
|
|
229
|
+
onValueChange={onValueChange}
|
|
230
|
+
closeDelay={20}
|
|
231
|
+
/>,
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
await user.hover(screen.getByRole("button", { name: "Concepts" }));
|
|
235
|
+
await user.unhover(screen.getByRole("navigation", { name: "Main" }));
|
|
236
|
+
|
|
237
|
+
await waitFor(() => expect(onValueChange).toHaveBeenCalledWith(""));
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("closes a panel the pointer left and re-entered while it was open", async () => {
|
|
241
|
+
const user = userEvent.setup();
|
|
242
|
+
render(<ThreeEntryNav delayDuration={0} />);
|
|
243
|
+
const trigger = screen.getByRole("button", { name: "Concepts" });
|
|
244
|
+
|
|
245
|
+
await user.hover(trigger);
|
|
246
|
+
await user.hover(screen.getByRole("list"));
|
|
247
|
+
await user.hover(trigger);
|
|
248
|
+
await user.click(trigger);
|
|
249
|
+
|
|
250
|
+
// The second arrival sees an open panel, so the click that follows it is a
|
|
251
|
+
// close — the arrival value has to be read live, not captured at mount.
|
|
252
|
+
expect(screen.getByTestId("concepts-panel")).not.toBeVisible();
|
|
253
|
+
});
|
|
254
|
+
});
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { act, render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
|
|
4
|
+
import { NavigationMenu } from "../NavigationMenu";
|
|
5
|
+
import type { NavigationMenuRootProps } from "../types";
|
|
6
|
+
|
|
7
|
+
/** jsdom reports every offset as 0, so the geometry the Indicator reads has to
|
|
8
|
+
* be planted on the element first. */
|
|
9
|
+
function plantGeometry(
|
|
10
|
+
element: HTMLElement,
|
|
11
|
+
box: { left: number; width: number; top: number; height: number },
|
|
12
|
+
) {
|
|
13
|
+
Object.defineProperty(element, "offsetLeft", {
|
|
14
|
+
value: box.left,
|
|
15
|
+
configurable: true,
|
|
16
|
+
});
|
|
17
|
+
Object.defineProperty(element, "offsetWidth", {
|
|
18
|
+
value: box.width,
|
|
19
|
+
configurable: true,
|
|
20
|
+
});
|
|
21
|
+
Object.defineProperty(element, "offsetTop", {
|
|
22
|
+
value: box.top,
|
|
23
|
+
configurable: true,
|
|
24
|
+
});
|
|
25
|
+
Object.defineProperty(element, "offsetHeight", {
|
|
26
|
+
value: box.height,
|
|
27
|
+
configurable: true,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function Nav(props: NavigationMenuRootProps) {
|
|
32
|
+
return (
|
|
33
|
+
<NavigationMenu.Root {...props}>
|
|
34
|
+
<NavigationMenu.List>
|
|
35
|
+
<NavigationMenu.Item value="concepts">
|
|
36
|
+
<NavigationMenu.Trigger>Concepts</NavigationMenu.Trigger>
|
|
37
|
+
<NavigationMenu.Content data-testid="concepts-panel" />
|
|
38
|
+
</NavigationMenu.Item>
|
|
39
|
+
<NavigationMenu.Item value="registry">
|
|
40
|
+
<NavigationMenu.Trigger>Registry</NavigationMenu.Trigger>
|
|
41
|
+
<NavigationMenu.Content data-testid="registry-panel" />
|
|
42
|
+
</NavigationMenu.Item>
|
|
43
|
+
</NavigationMenu.List>
|
|
44
|
+
<NavigationMenu.Indicator data-testid="indicator" />
|
|
45
|
+
</NavigationMenu.Root>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe("NavigationMenu.Indicator", () => {
|
|
50
|
+
it("stays hidden while nothing is open", () => {
|
|
51
|
+
render(<Nav />);
|
|
52
|
+
|
|
53
|
+
const indicator = screen.getByTestId("indicator");
|
|
54
|
+
|
|
55
|
+
expect(indicator).toHaveAttribute("hidden");
|
|
56
|
+
expect(indicator).toHaveAttribute("data-state", "closed");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("publishes the open trigger's horizontal geometry", async () => {
|
|
60
|
+
const user = userEvent.setup();
|
|
61
|
+
render(<Nav />);
|
|
62
|
+
|
|
63
|
+
plantGeometry(screen.getByRole("button", { name: "Registry" }), {
|
|
64
|
+
left: 120,
|
|
65
|
+
width: 80,
|
|
66
|
+
top: 4,
|
|
67
|
+
height: 32,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
await user.click(screen.getByRole("button", { name: "Registry" }));
|
|
71
|
+
|
|
72
|
+
const indicator = screen.getByTestId("indicator");
|
|
73
|
+
|
|
74
|
+
expect(indicator).not.toHaveAttribute("hidden");
|
|
75
|
+
expect(indicator).toHaveAttribute("data-state", "open");
|
|
76
|
+
expect(indicator.style.getPropertyValue(
|
|
77
|
+
"--primitiv-navigation-menu-indicator-position",
|
|
78
|
+
)).toBe("120px");
|
|
79
|
+
expect(indicator.style.getPropertyValue(
|
|
80
|
+
"--primitiv-navigation-menu-indicator-size",
|
|
81
|
+
)).toBe("80px");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("measures the cross axis when vertical", async () => {
|
|
85
|
+
const user = userEvent.setup();
|
|
86
|
+
render(<Nav orientation="vertical" />);
|
|
87
|
+
|
|
88
|
+
plantGeometry(screen.getByRole("button", { name: "Registry" }), {
|
|
89
|
+
left: 120,
|
|
90
|
+
width: 80,
|
|
91
|
+
top: 48,
|
|
92
|
+
height: 36,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
await user.click(screen.getByRole("button", { name: "Registry" }));
|
|
96
|
+
|
|
97
|
+
const indicator = screen.getByTestId("indicator");
|
|
98
|
+
|
|
99
|
+
expect(indicator.style.getPropertyValue(
|
|
100
|
+
"--primitiv-navigation-menu-indicator-position",
|
|
101
|
+
)).toBe("48px");
|
|
102
|
+
expect(indicator.style.getPropertyValue(
|
|
103
|
+
"--primitiv-navigation-menu-indicator-size",
|
|
104
|
+
)).toBe("36px");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("re-measures when the window resizes", async () => {
|
|
108
|
+
const user = userEvent.setup();
|
|
109
|
+
render(<Nav />);
|
|
110
|
+
|
|
111
|
+
const trigger = screen.getByRole("button", { name: "Concepts" });
|
|
112
|
+
plantGeometry(trigger, { left: 0, width: 60, top: 0, height: 32 });
|
|
113
|
+
await user.click(trigger);
|
|
114
|
+
|
|
115
|
+
plantGeometry(trigger, { left: 10, width: 90, top: 0, height: 32 });
|
|
116
|
+
act(() => {
|
|
117
|
+
window.dispatchEvent(new Event("resize"));
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(
|
|
121
|
+
screen
|
|
122
|
+
.getByTestId("indicator")
|
|
123
|
+
.style.getPropertyValue("--primitiv-navigation-menu-indicator-size"),
|
|
124
|
+
).toBe("90px");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("clears the geometry when the panel closes", async () => {
|
|
128
|
+
const user = userEvent.setup();
|
|
129
|
+
render(<Nav />);
|
|
130
|
+
|
|
131
|
+
const trigger = screen.getByRole("button", { name: "Concepts" });
|
|
132
|
+
plantGeometry(trigger, { left: 8, width: 72, top: 0, height: 32 });
|
|
133
|
+
|
|
134
|
+
await user.click(trigger);
|
|
135
|
+
await user.click(trigger);
|
|
136
|
+
|
|
137
|
+
const indicator = screen.getByTestId("indicator");
|
|
138
|
+
|
|
139
|
+
// A marker left holding the last trigger's geometry would animate from the
|
|
140
|
+
// wrong place on the next open.
|
|
141
|
+
expect(
|
|
142
|
+
indicator.style.getPropertyValue(
|
|
143
|
+
"--primitiv-navigation-menu-indicator-position",
|
|
144
|
+
),
|
|
145
|
+
).toBe("");
|
|
146
|
+
expect(
|
|
147
|
+
indicator.style.getPropertyValue(
|
|
148
|
+
"--primitiv-navigation-menu-indicator-size",
|
|
149
|
+
),
|
|
150
|
+
).toBe("");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("removes its resize listener on unmount", () => {
|
|
154
|
+
const addEventListener = vi.spyOn(window, "addEventListener");
|
|
155
|
+
const removeEventListener = vi.spyOn(window, "removeEventListener");
|
|
156
|
+
|
|
157
|
+
const { unmount } = render(<Nav />);
|
|
158
|
+
const measure = addEventListener.mock.calls.find(
|
|
159
|
+
([type]) => type === "resize",
|
|
160
|
+
)?.[1];
|
|
161
|
+
unmount();
|
|
162
|
+
|
|
163
|
+
expect(measure).toBeTypeOf("function");
|
|
164
|
+
expect(removeEventListener).toHaveBeenCalledWith("resize", measure);
|
|
165
|
+
|
|
166
|
+
addEventListener.mockRestore();
|
|
167
|
+
removeEventListener.mockRestore();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("carries the open value and the orientation as styling hooks", async () => {
|
|
171
|
+
const user = userEvent.setup();
|
|
172
|
+
render(<Nav />);
|
|
173
|
+
|
|
174
|
+
await user.click(screen.getByRole("button", { name: "Concepts" }));
|
|
175
|
+
|
|
176
|
+
const indicator = screen.getByTestId("indicator");
|
|
177
|
+
|
|
178
|
+
expect(indicator).toHaveAttribute("data-value", "concepts");
|
|
179
|
+
expect(indicator).toHaveAttribute("data-orientation", "horizontal");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("publishes no geometry when the open value has no rendered trigger", () => {
|
|
183
|
+
render(<Nav value="ghost" onValueChange={() => {}} />);
|
|
184
|
+
|
|
185
|
+
const indicator = screen.getByTestId("indicator");
|
|
186
|
+
|
|
187
|
+
// Open by state, but there is nothing to measure — better a marker with no
|
|
188
|
+
// geometry than one parked at 0.
|
|
189
|
+
expect(indicator).toHaveAttribute("data-state", "open");
|
|
190
|
+
expect(
|
|
191
|
+
indicator.style.getPropertyValue(
|
|
192
|
+
"--primitiv-navigation-menu-indicator-position",
|
|
193
|
+
),
|
|
194
|
+
).toBe("");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("renders a consumer element instead of the div with asChild", async () => {
|
|
198
|
+
const user = userEvent.setup();
|
|
199
|
+
render(
|
|
200
|
+
<NavigationMenu.Root>
|
|
201
|
+
<NavigationMenu.List>
|
|
202
|
+
<NavigationMenu.Item value="concepts">
|
|
203
|
+
<NavigationMenu.Trigger>Concepts</NavigationMenu.Trigger>
|
|
204
|
+
</NavigationMenu.Item>
|
|
205
|
+
</NavigationMenu.List>
|
|
206
|
+
<NavigationMenu.Indicator asChild>
|
|
207
|
+
<svg data-testid="arrow" viewBox="0 0 10 10" aria-hidden>
|
|
208
|
+
<polygon points="0,10 5,0 10,10" />
|
|
209
|
+
</svg>
|
|
210
|
+
</NavigationMenu.Indicator>
|
|
211
|
+
</NavigationMenu.Root>,
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const arrow = screen.getByTestId("arrow");
|
|
215
|
+
plantGeometry(screen.getByRole("button", { name: "Concepts" }), {
|
|
216
|
+
left: 24,
|
|
217
|
+
width: 64,
|
|
218
|
+
top: 0,
|
|
219
|
+
height: 32,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
expect(arrow.tagName).toBe("svg");
|
|
223
|
+
expect(arrow).toHaveAttribute("data-state", "closed");
|
|
224
|
+
|
|
225
|
+
await user.click(screen.getByRole("button", { name: "Concepts" }));
|
|
226
|
+
|
|
227
|
+
expect(arrow).toHaveAttribute("data-state", "open");
|
|
228
|
+
expect(
|
|
229
|
+
arrow.style.getPropertyValue(
|
|
230
|
+
"--primitiv-navigation-menu-indicator-position",
|
|
231
|
+
),
|
|
232
|
+
).toBe("24px");
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("keeps a force-mounted indicator unhidden so it can animate away", () => {
|
|
236
|
+
render(<Nav />);
|
|
237
|
+
const { container } = render(
|
|
238
|
+
<NavigationMenu.Root>
|
|
239
|
+
<NavigationMenu.List>
|
|
240
|
+
<NavigationMenu.Item value="concepts">
|
|
241
|
+
<NavigationMenu.Trigger>Concepts</NavigationMenu.Trigger>
|
|
242
|
+
</NavigationMenu.Item>
|
|
243
|
+
</NavigationMenu.List>
|
|
244
|
+
<NavigationMenu.Indicator forceMount />
|
|
245
|
+
</NavigationMenu.Root>,
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
const indicator = container.querySelector("nav > div");
|
|
249
|
+
|
|
250
|
+
expect(indicator).not.toHaveAttribute("hidden");
|
|
251
|
+
});
|
|
252
|
+
});
|