@primitiv-ui/react 0.1.28 → 0.1.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -2
- package/package.json +6 -1
- package/src/AccessibleIcon/AccessibleIcon.tsx +32 -9
- package/src/AccessibleIcon/__tests__/AccessibleIcon.test.tsx +5 -0
- package/src/AccessibleIcon/types.ts +15 -2
- package/src/Accordion/Accordion.tsx +32 -11
- package/src/Accordion/README.md +2 -2
- package/src/Accordion/__tests__/Accordion.asChild.test.tsx +46 -0
- package/src/Accordion/__tests__/Accordion.basic-rendering.test.tsx +15 -0
- package/src/Accordion/__tests__/Accordion.disabled-items.test.tsx +42 -0
- package/src/Accordion/__tests__/Accordion.error-handling.test.tsx +33 -0
- package/src/Accordion/hooks/useAccordionRoot.ts +44 -7
- package/src/Accordion/hooks/useAccordionTrigger.ts +9 -1
- package/src/Accordion/types.ts +72 -21
- package/src/Alert/Alert.tsx +36 -7
- package/src/Alert/__tests__/Alert.test.tsx +5 -0
- package/src/Alert/types.ts +18 -2
- package/src/Avatar/Avatar.tsx +99 -15
- package/src/Avatar/__tests__/Avatar.basic-rendering.test.tsx +10 -0
- package/src/Avatar/__tests__/Avatar.fallback.test.tsx +30 -0
- package/src/Avatar/__tests__/Avatar.image-loading.test.tsx +35 -0
- package/src/Avatar/hooks/useAvatarImage.ts +16 -0
- package/src/Avatar/types.ts +15 -3
- package/src/Breadcrumb/Breadcrumb.tsx +139 -21
- package/src/Breadcrumb/README.md +47 -0
- package/src/Breadcrumb/__tests__/Breadcrumb.basic-rendering.test.tsx +11 -0
- package/src/Breadcrumb/__tests__/Breadcrumb.ellipsis.test.tsx +110 -0
- package/src/Breadcrumb/types.ts +73 -7
- package/src/Button/Button.tsx +2 -0
- package/src/Button/__tests__/Button.basic-rendering.test.tsx +6 -0
- package/src/Button/__tests__/Button.contract.test.tsx +1 -1
- package/src/Button/types.ts +21 -0
- package/src/Carousel/Carousel.tsx +268 -20
- package/src/Carousel/README.md +764 -87
- package/src/Carousel/__tests__/Carousel.autoplay-status.test.tsx +239 -0
- package/src/Carousel/__tests__/Carousel.drag-status.test.tsx +292 -0
- package/src/Carousel/__tests__/Carousel.dynamic-slides.test.tsx +38 -0
- package/src/Carousel/__tests__/Carousel.indicators.test.tsx +101 -0
- package/src/Carousel/__tests__/Carousel.infinite.test.tsx +1453 -0
- package/src/Carousel/__tests__/Carousel.instant-scroll.test.tsx +124 -0
- package/src/Carousel/__tests__/Carousel.intersection-observer.test.tsx +90 -2
- package/src/Carousel/__tests__/Carousel.loop.test.tsx +163 -0
- package/src/Carousel/__tests__/Carousel.loopEngine.test.ts +77 -0
- package/src/Carousel/__tests__/Carousel.mouse-drag.test.tsx +504 -0
- package/src/Carousel/__tests__/Carousel.multi-slide-bounds.test.tsx +91 -0
- package/src/Carousel/__tests__/Carousel.orientation.test.tsx +207 -0
- package/src/Carousel/__tests__/Carousel.overscroll.test.tsx +532 -0
- package/src/Carousel/__tests__/Carousel.page-snap-points.test.tsx +50 -0
- package/src/Carousel/__tests__/Carousel.progress-text.test.tsx +76 -0
- package/src/Carousel/__tests__/Carousel.reduced-motion.test.tsx +3 -6
- package/src/Carousel/__tests__/Carousel.refresh-progress.test.tsx +2 -5
- package/src/Carousel/__tests__/Carousel.scroll-margin.test.tsx +167 -0
- package/src/Carousel/__tests__/Carousel.scroll-progress.test.tsx +518 -0
- package/src/Carousel/__tests__/Carousel.scroll-snap-change.test.tsx +32 -11
- package/src/Carousel/__tests__/Carousel.scroll-sync.test.tsx +237 -24
- package/src/Carousel/__tests__/Carousel.scroll-to-index.test.tsx +115 -0
- package/src/Carousel/__tests__/Carousel.slide-snap.test.tsx +172 -0
- package/src/Carousel/__tests__/Carousel.slides-per-move.test.tsx +179 -1
- package/src/Carousel/__tests__/Carousel.slides-per-page.test.tsx +91 -7
- package/src/Carousel/__tests__/Carousel.snap-type.test.tsx +35 -0
- package/src/Carousel/__tests__/Carousel.transition-modes.test.tsx +30 -5
- package/src/Carousel/__tests__/Carousel.translations.test.tsx +23 -0
- package/src/Carousel/__tests__/Carousel.wheel.test.tsx +160 -0
- package/src/Carousel/hooks/index.ts +1 -0
- package/src/Carousel/hooks/useCarouselLoop.ts +869 -0
- package/src/Carousel/hooks/useCarouselRoot.ts +338 -42
- package/src/Carousel/hooks/useCarouselSlide.ts +44 -9
- package/src/Carousel/hooks/useCarouselViewport.ts +649 -46
- package/src/Carousel/loopEngine.ts +54 -0
- package/src/Carousel/types.ts +458 -36
- package/src/Checkbox/Checkbox.tsx +11 -3
- package/src/Checkbox/__tests__/Checkbox.basic-rendering.test.tsx +14 -0
- package/src/Checkbox/hooks/useCheckboxInput.ts +4 -0
- package/src/Checkbox/types.ts +19 -0
- package/src/CheckboxCard/CheckboxCard.tsx +14 -5
- package/src/CheckboxCard/README.md +1 -1
- package/src/CheckboxCard/__tests__/CheckboxCard.basic-rendering.test.tsx +7 -0
- package/src/CheckboxCard/types.ts +65 -17
- package/src/Collapsible/Collapsible.tsx +49 -3
- package/src/Collapsible/README.md +41 -1
- package/src/Collapsible/__tests__/Collapsible.asChild.test.tsx +89 -0
- package/src/Collapsible/__tests__/Collapsible.basic-rendering.test.tsx +27 -0
- package/src/Collapsible/__tests__/Collapsible.collapsedHeight.test.tsx +140 -0
- package/src/Collapsible/hooks/useCollapsibleTrigger.ts +3 -1
- package/src/Collapsible/types.ts +89 -22
- package/src/Combobox/Combobox.tsx +508 -0
- package/src/Combobox/ComboboxContext.ts +23 -0
- package/src/Combobox/README.md +170 -0
- package/src/Combobox/__tests__/Combobox.asChild.test.tsx +62 -0
- package/src/Combobox/__tests__/Combobox.basic-rendering.test.tsx +16 -0
- package/src/Combobox/__tests__/Combobox.content.test.tsx +36 -0
- package/src/Combobox/__tests__/Combobox.controlled-state.test.tsx +88 -0
- package/src/Combobox/__tests__/Combobox.empty.test.tsx +42 -0
- package/src/Combobox/__tests__/Combobox.ids.test.tsx +66 -0
- package/src/Combobox/__tests__/Combobox.item.test.tsx +76 -0
- package/src/Combobox/__tests__/Combobox.keyboard-interaction.test.tsx +196 -0
- package/src/Combobox/__tests__/Combobox.light-dismiss.test.tsx +101 -0
- package/src/Combobox/__tests__/Combobox.query.test.tsx +105 -0
- package/src/Combobox/hooks/index.ts +1 -0
- package/src/Combobox/hooks/useComboboxRoot.ts +2 -0
- package/src/Combobox/index.ts +3 -0
- package/src/Combobox/types.ts +133 -0
- package/src/ContextMenu/ContextMenu.tsx +270 -4
- package/src/ContextMenu/README.md +1 -1
- package/src/ContextMenu/types.ts +148 -6
- package/src/DirectionProvider/DirectionProvider.tsx +41 -9
- package/src/DirectionProvider/README.md +4 -4
- package/src/DirectionProvider/__tests__/DirectionProvider.test.tsx +5 -0
- package/src/DirectionProvider/types.ts +28 -3
- package/src/Divider/Divider.tsx +24 -10
- package/src/Divider/__tests__/Divider.contract.test.tsx +1 -1
- package/src/Divider/__tests__/Divider.test.tsx +5 -0
- package/src/Divider/types.ts +10 -3
- package/src/Drawer/Drawer.tsx +315 -0
- package/src/Drawer/README.md +124 -0
- package/src/Drawer/__tests__/Drawer.behavior.test.tsx +100 -0
- package/src/Drawer/__tests__/Drawer.focus-trap.test.tsx +55 -0
- package/src/Drawer/__tests__/Drawer.rendering.test.tsx +111 -0
- package/src/Drawer/__tests__/Drawer.side.test.tsx +69 -0
- package/src/Drawer/__tests__/dialog-polyfill.ts +39 -0
- package/src/Drawer/index.ts +2 -0
- package/src/Drawer/types.ts +66 -0
- package/src/Dropdown/Dropdown.tsx +119 -2
- package/src/Dropdown/README.md +7 -5
- package/src/Dropdown/types.ts +241 -11
- package/src/EmptyState/EmptyState.tsx +74 -4
- package/src/EmptyState/README.md +2 -2
- package/src/EmptyState/__tests__/EmptyState.Actions.test.tsx +7 -2
- package/src/EmptyState/__tests__/EmptyState.Description.test.tsx +3 -1
- package/src/EmptyState/__tests__/EmptyState.Root.test.tsx +10 -0
- package/src/EmptyState/__tests__/EmptyState.Title.test.tsx +3 -1
- package/src/EmptyState/types.ts +49 -6
- package/src/Field/Field.tsx +23 -0
- package/src/Field/README.md +1 -1
- package/src/Field/__tests__/Field.basic-rendering.test.tsx +11 -0
- package/src/Field/__tests__/Field.error-handling.test.tsx +23 -0
- package/src/Field/__tests__/Field.state-cascade.test.tsx +21 -0
- package/src/Field/hooks/__tests__/useFieldProps.test.tsx +104 -9
- package/src/Field/hooks/useFieldProps.ts +1 -1
- package/src/Field/types.ts +60 -12
- package/src/Fieldset/Fieldset.tsx +55 -19
- package/src/Fieldset/README.md +2 -2
- package/src/Fieldset/__tests__/Fieldset.basic-rendering.test.tsx +9 -0
- package/src/Fieldset/types.ts +20 -2
- package/src/Input/Input.tsx +15 -10
- package/src/Input/README.md +2 -2
- package/src/Input/__tests__/Input.basic-rendering.test.tsx +5 -0
- package/src/Input/types.ts +19 -2
- package/src/InputGroup/InputGroup.tsx +11 -3
- package/src/InputGroup/__tests__/InputGroup.basic-rendering.test.tsx +14 -0
- package/src/InputGroup/types.ts +26 -5
- package/src/Listbox/Listbox.tsx +475 -0
- package/src/Listbox/ListboxContext.ts +48 -0
- package/src/Listbox/README.md +248 -0
- package/src/Listbox/__tests__/Listbox.active-descendant.test.tsx +128 -0
- package/src/Listbox/__tests__/Listbox.asChild.test.tsx +91 -0
- package/src/Listbox/__tests__/Listbox.basic-rendering.test.tsx +16 -0
- package/src/Listbox/__tests__/Listbox.controlled-state.test.tsx +114 -0
- package/src/Listbox/__tests__/Listbox.disabled.test.tsx +90 -0
- package/src/Listbox/__tests__/Listbox.fixtures.ts +27 -0
- package/src/Listbox/__tests__/Listbox.group-label.test.tsx +97 -0
- package/src/Listbox/__tests__/Listbox.group.test.tsx +45 -0
- package/src/Listbox/__tests__/Listbox.keyboard-edge-cases.test.tsx +144 -0
- package/src/Listbox/__tests__/Listbox.keyboard-interaction.test.tsx +112 -0
- package/src/Listbox/__tests__/Listbox.modifier-keys.test.tsx +86 -0
- package/src/Listbox/__tests__/Listbox.multi-select-keyboard.test.tsx +368 -0
- package/src/Listbox/__tests__/Listbox.multiple-mode.test.tsx +91 -0
- package/src/Listbox/__tests__/Listbox.mutation-hardening.test.tsx +480 -0
- package/src/Listbox/__tests__/Listbox.orientation.test.tsx +59 -0
- package/src/Listbox/__tests__/Listbox.reading-direction.test.tsx +65 -0
- package/src/Listbox/__tests__/Listbox.reorder.test.tsx +78 -0
- package/src/Listbox/__tests__/Listbox.selection-follows-focus.test.tsx +64 -0
- package/src/Listbox/__tests__/Listbox.strict-context.test.tsx +21 -0
- package/src/Listbox/__tests__/Listbox.typeahead.test.tsx +123 -0
- package/src/Listbox/__tests__/Listbox.uncontrolled-state.test.tsx +48 -0
- package/src/Listbox/constants.ts +2 -0
- package/src/Listbox/hooks/index.ts +1 -0
- package/src/Listbox/hooks/useListboxRoot.ts +407 -0
- package/src/Listbox/index.ts +3 -0
- package/src/Listbox/types.ts +238 -0
- package/src/MillerColumns/MillerColumns.tsx +186 -45
- package/src/MillerColumns/README.md +90 -20
- package/src/MillerColumns/__tests__/MillerColumns.aria.test.tsx +45 -0
- package/src/MillerColumns/__tests__/MillerColumns.auto-scroll.test.tsx +4 -2
- package/src/MillerColumns/__tests__/MillerColumns.basic-rendering.test.tsx +18 -3
- package/src/MillerColumns/__tests__/MillerColumns.data-attributes.test.tsx +48 -0
- package/src/MillerColumns/__tests__/MillerColumns.keyboard-interaction.test.tsx +35 -2
- package/src/MillerColumns/__tests__/MillerColumns.keyboard-selection-state.test.tsx +96 -0
- package/src/MillerColumns/__tests__/MillerColumns.preview-panel.test.tsx +29 -4
- package/src/MillerColumns/__tests__/MillerColumns.preview-visibility.test.tsx +79 -0
- package/src/MillerColumns/__tests__/MillerColumns.resize.test.tsx +189 -0
- package/src/MillerColumns/__tests__/MillerColumns.styled-parts.test.tsx +143 -0
- package/src/MillerColumns/__tests__/MillerColumns.typeahead.test.tsx +104 -0
- package/src/MillerColumns/constants.ts +2 -0
- package/src/MillerColumns/hooks/useMillerColumnsColumn.ts +5 -2
- package/src/MillerColumns/hooks/useMillerColumnsItem.ts +29 -7
- package/src/MillerColumns/hooks/useMillerColumnsResizeHandle.ts +80 -10
- package/src/MillerColumns/hooks/useMillerColumnsRoot.ts +0 -0
- package/src/MillerColumns/index.ts +4 -0
- package/src/MillerColumns/types.ts +136 -30
- package/src/MillerColumns/utils.ts +69 -3
- package/src/Modal/Modal.tsx +35 -8
- package/src/Modal/README.md +24 -15
- package/src/Modal/__tests__/Modal.focus-trap.test.tsx +162 -0
- package/src/Modal/hooks/useModalContent.ts +32 -0
- package/src/Modal/types.ts +110 -29
- package/src/NavigationMenu/NavigationMenu.tsx +890 -0
- package/src/NavigationMenu/NavigationMenuContext.ts +67 -0
- package/src/NavigationMenu/README.md +355 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.anchor-positioning.test.tsx +132 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.asChild.test.tsx +73 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.basic-rendering.test.tsx +92 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.contract.test.tsx +45 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.controlled-state.test.tsx +134 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.error-handling.test.tsx +62 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.fixtures.tsx +79 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.forceMount.test.tsx +52 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.hover-interaction.test.tsx +254 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.indicator.test.tsx +252 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-interaction.test.tsx +294 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-navigation.test.tsx +126 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.link.test.tsx +139 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.motion.test.tsx +568 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.mouse-interaction.test.tsx +76 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.uncontrolled-state.test.tsx +30 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.viewport.test.tsx +113 -0
- package/src/NavigationMenu/constants.ts +5 -0
- package/src/NavigationMenu/hooks/index.ts +7 -0
- package/src/NavigationMenu/hooks/useNavigationMenuEntry.ts +33 -0
- package/src/NavigationMenu/hooks/useNavigationMenuIndicator.ts +70 -0
- package/src/NavigationMenu/hooks/useNavigationMenuLink.ts +72 -0
- package/src/NavigationMenu/hooks/useNavigationMenuRoot.ts +301 -0
- package/src/NavigationMenu/hooks/useNavigationMenuTopLevelEntry.ts +65 -0
- package/src/NavigationMenu/hooks/useNavigationMenuTrigger.ts +158 -0
- package/src/NavigationMenu/hooks/useNavigationMenuViewport.ts +101 -0
- package/src/NavigationMenu/index.ts +3 -0
- package/src/NavigationMenu/types.ts +259 -0
- package/src/NavigationMenu/utils.ts +89 -0
- package/src/Popover/Popover.tsx +427 -0
- package/src/Popover/PopoverContext.ts +13 -0
- package/src/Popover/README.md +106 -0
- package/src/Popover/__tests__/Popover.anchor.test.tsx +38 -0
- package/src/Popover/__tests__/Popover.asChild.test.tsx +160 -0
- package/src/Popover/__tests__/Popover.basic-rendering.test.tsx +106 -0
- package/src/Popover/__tests__/Popover.click-outside.test.tsx +149 -0
- package/src/Popover/__tests__/Popover.close.test.tsx +70 -0
- package/src/Popover/__tests__/Popover.content.test.tsx +58 -0
- package/src/Popover/__tests__/Popover.controlled-state.test.tsx +45 -0
- package/src/Popover/__tests__/Popover.error-handling.test.tsx +51 -0
- package/src/Popover/__tests__/Popover.focus.test.tsx +43 -0
- package/src/Popover/__tests__/Popover.keyboard-interaction.test.tsx +71 -0
- package/src/Popover/__tests__/Popover.mouse-interaction.test.tsx +69 -0
- package/src/Popover/__tests__/Popover.uncontrolled-state.test.tsx +44 -0
- package/src/Popover/hooks/index.ts +3 -0
- package/src/Popover/hooks/usePopoverContent.ts +98 -0
- package/src/Popover/hooks/usePopoverRoot.ts +84 -0
- package/src/Popover/hooks/usePopoverTrigger.ts +33 -0
- package/src/Popover/index.ts +2 -0
- package/src/Popover/types.ts +197 -0
- package/src/Portal/Portal.tsx +50 -10
- package/src/Portal/README.md +2 -2
- package/src/Portal/__tests__/Portal.basic-rendering.test.tsx +5 -0
- package/src/Portal/types.ts +5 -0
- package/src/Progress/Progress.tsx +33 -1
- package/src/Progress/README.md +1 -1
- package/src/Progress/__tests__/Progress.basic-rendering.test.tsx +10 -0
- package/src/Progress/__tests__/Progress.value.test.tsx +21 -0
- package/src/Progress/types.ts +20 -4
- package/src/Radio/Radio.tsx +10 -2
- package/src/Radio/__tests__/Radio.basic-rendering.test.tsx +14 -0
- package/src/Radio/hooks/useRadioRoot.ts +3 -0
- package/src/Radio/types.ts +20 -0
- package/src/RadioCard/RadioCard.tsx +11 -0
- package/src/RadioCard/__tests__/RadioCard.basic-rendering.test.tsx +8 -0
- package/src/RadioCard/__tests__/RadioCard.disabled-items.test.tsx +27 -0
- package/src/RadioCard/__tests__/RadioCard.keyboard-interaction.test.tsx +53 -0
- package/src/RadioCard/__tests__/RadioCard.ref-forwarding.test.tsx +54 -0
- package/src/RadioCard/__tests__/RadioCard.uncontrolled-state.test.tsx +21 -0
- package/src/RadioCard/hooks/useRadioCardRoot.ts +11 -0
- package/src/RadioCard/types.ts +95 -22
- package/src/RadioGroup/README.md +2 -2
- package/src/RadioGroup/RadioGroup.tsx +12 -1
- package/src/RadioGroup/__tests__/RadioGroup.basic-rendering.test.tsx +8 -0
- package/src/RadioGroup/__tests__/RadioGroup.disabled.test.tsx +27 -0
- package/src/RadioGroup/__tests__/RadioGroup.ref-forwarding.test.tsx +29 -0
- package/src/RadioGroup/__tests__/RadioGroup.roving-tabindex.test.tsx +31 -0
- package/src/RadioGroup/hooks/useRadioGroupRoot.ts +11 -0
- package/src/RadioGroup/types.ts +73 -9
- package/src/SegmentedControl/README.md +154 -0
- package/src/SegmentedControl/SegmentedControl.tsx +309 -0
- package/src/SegmentedControl/SegmentedControlContext.ts +28 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.asChild.test.tsx +50 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.basic-rendering.test.tsx +100 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.controlled-state.test.tsx +107 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.disabled-keydown-guards.test.tsx +62 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.disabled.test.tsx +151 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.error-handling.test.tsx +17 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.keyboard-interaction.test.tsx +117 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.orientation.test.tsx +54 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.reading-direction.test.tsx +56 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.ref-forwarding.test.tsx +45 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.roving-tabindex.test.tsx +104 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.uncontrolled-state.test.tsx +94 -0
- package/src/SegmentedControl/hooks/index.ts +2 -0
- package/src/SegmentedControl/hooks/useSegmentedControlContext.ts +1 -0
- package/src/SegmentedControl/hooks/useSegmentedControlRoot.ts +87 -0
- package/src/SegmentedControl/index.ts +2 -0
- package/src/SegmentedControl/types.ts +138 -0
- package/src/Select/README.md +122 -157
- package/src/Select/Select.tsx +596 -69
- package/src/Select/SelectContext.ts +38 -0
- package/src/Select/SelectItemIndicatorContext.ts +29 -0
- package/src/Select/__tests__/Select.asChild.test.tsx +2 -2
- package/src/Select/__tests__/Select.basic-rendering.test.tsx +22 -3
- package/src/Select/__tests__/Select.controlled-state.test.tsx +6 -6
- package/src/Select/__tests__/Select.data-attributes.test.tsx +4 -4
- package/src/Select/__tests__/Select.field-integration.test.tsx +11 -11
- package/src/Select/__tests__/Select.form.test.tsx +38 -0
- package/src/Select/__tests__/Select.group.test.tsx +6 -6
- package/src/Select/__tests__/Select.keyboard-edge.test.tsx +95 -0
- package/src/Select/__tests__/Select.keyboard.test.tsx +124 -0
- package/src/Select/__tests__/Select.mutation-hardening.test.tsx +317 -0
- package/src/Select/__tests__/Select.native-item.test.tsx +30 -0
- package/src/Select/__tests__/Select.open-close.test.tsx +57 -0
- package/src/Select/__tests__/Select.placeholder.test.tsx +7 -7
- package/src/Select/__tests__/Select.rich-asChild.test.tsx +60 -0
- package/src/Select/__tests__/Select.rich-mode.test.tsx +16 -0
- package/src/Select/__tests__/Select.selection.test.tsx +104 -0
- package/src/Select/__tests__/Select.separator.test.tsx +71 -0
- package/src/Select/__tests__/Select.strict-context.test.tsx +36 -0
- package/src/Select/__tests__/Select.typeahead.test.tsx +156 -0
- package/src/Select/__tests__/Select.value-mirror.test.tsx +240 -0
- package/src/Select/constants.ts +8 -0
- package/src/Select/hooks/index.ts +2 -0
- package/src/Select/hooks/useSelectContent.ts +149 -0
- package/src/Select/hooks/useSelectRoot.ts +149 -0
- package/src/Select/types.ts +199 -31
- package/src/SkipNav/README.md +3 -3
- package/src/SkipNav/SkipNav.tsx +72 -32
- package/src/SkipNav/__tests__/SkipNav.basic-rendering.test.tsx +6 -0
- package/src/SkipNav/types.ts +17 -9
- package/src/Slider/README.md +37 -7
- package/src/Slider/Slider.tsx +42 -4
- package/src/Slider/SliderContext.ts +28 -1
- package/src/Slider/types.ts +157 -9
- package/src/Slot/Slot.tsx +94 -19
- package/src/Slot/__tests__/Slot.test.tsx +112 -2
- package/src/Slot/composeEventHandlers.ts +26 -2
- package/src/Slot/types.ts +16 -0
- package/src/SplitButton/README.md +225 -0
- package/src/SplitButton/SplitButton.tsx +457 -0
- package/src/SplitButton/SplitButtonContext.ts +26 -0
- package/src/SplitButton/__tests__/SplitButton.accessibility.test.tsx +55 -0
- package/src/SplitButton/__tests__/SplitButton.asChild.test.tsx +51 -0
- package/src/SplitButton/__tests__/SplitButton.basic-rendering.test.tsx +57 -0
- package/src/SplitButton/__tests__/SplitButton.data-attributes.test.tsx +28 -0
- package/src/SplitButton/__tests__/SplitButton.disabled.test.tsx +79 -0
- package/src/SplitButton/__tests__/SplitButton.error-handling.test.tsx +29 -0
- package/src/SplitButton/__tests__/SplitButton.keyboard-interaction.test.tsx +56 -0
- package/src/SplitButton/__tests__/SplitButton.state-modes.test.tsx +68 -0
- package/src/SplitButton/index.ts +2 -0
- package/src/SplitButton/types.ts +186 -0
- package/src/Status/Status.tsx +36 -15
- package/src/Status/__tests__/Status.test.tsx +5 -0
- package/src/Status/types.ts +12 -1
- package/src/Switch/README.md +1 -1
- package/src/Switch/Switch.tsx +17 -4
- package/src/Switch/__tests__/Switch.basic-rendering.test.tsx +7 -0
- package/src/Switch/hooks/useSwitchRoot.ts +3 -0
- package/src/Switch/types.ts +49 -22
- package/src/Table/README.md +110 -8
- package/src/Table/Table.tsx +246 -16
- package/src/Table/TableExpandableContext.ts +7 -0
- package/src/Table/__tests__/Table.DetailRow.test.tsx +83 -0
- package/src/Table/__tests__/Table.ExpandTrigger.test.tsx +102 -0
- package/src/Table/__tests__/Table.Expandable.test.tsx +121 -0
- package/src/Table/types.ts +191 -17
- package/src/Tabs/README.md +4 -4
- package/src/Tabs/Tabs.tsx +13 -4
- package/src/Tabs/__tests__/Tabs.activation-mode.test.tsx +58 -0
- package/src/Tabs/__tests__/Tabs.basic-rendering.test.tsx +15 -0
- package/src/Tabs/__tests__/Tabs.change-event-callbacks.test.tsx +24 -0
- package/src/Tabs/__tests__/Tabs.disabled-tabs.test.tsx +36 -0
- package/src/Tabs/__tests__/Tabs.lazy-mount.test.tsx +17 -0
- package/src/Tabs/hooks/useTabsContent.ts +5 -0
- package/src/Tabs/hooks/useTabsRoot.ts +29 -5
- package/src/Tabs/hooks/useTabsTrigger.ts +7 -2
- package/src/Tabs/types.ts +29 -4
- package/src/Textarea/Textarea.tsx +9 -1
- package/src/Textarea/__tests__/Textarea.basic-rendering.test.tsx +5 -0
- package/src/Textarea/types.ts +20 -3
- package/src/Toggle/Toggle.tsx +51 -13
- package/src/Toggle/__tests__/Toggle.basic-rendering.test.tsx +5 -0
- package/src/Toggle/types.ts +54 -4
- package/src/ToggleGroup/README.md +4 -4
- package/src/ToggleGroup/ToggleGroup.tsx +14 -1
- package/src/ToggleGroup/__tests__/ToggleGroup.asChild.test.tsx +20 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.basic-rendering.test.tsx +9 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.dynamic-items.test.tsx +70 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.error-handling.test.tsx +23 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.keyboard-interaction.test.tsx +23 -1
- package/src/ToggleGroup/__tests__/ToggleGroup.ref-forwarding.test.tsx +58 -0
- package/src/ToggleGroup/hooks/useToggleGroupRoot.ts +21 -1
- package/src/ToggleGroup/types.ts +55 -2
- package/src/Tooltip/README.md +7 -7
- package/src/Tooltip/Tooltip.tsx +100 -22
- package/src/Tooltip/__tests__/Tooltip.basic-rendering.test.tsx +50 -0
- package/src/Tooltip/__tests__/Tooltip.error-handling.test.tsx +37 -0
- package/src/Tooltip/__tests__/Tooltip.escape-hatches.test.tsx +73 -0
- package/src/Tooltip/__tests__/Tooltip.keyboard-interaction.test.tsx +134 -0
- package/src/Tooltip/__tests__/Tooltip.skip-delay.test.tsx +216 -0
- package/src/Tooltip/__tests__/Tooltip.timer-coordination.test.tsx +223 -0
- package/src/Tooltip/hooks/__tests__/useTooltipProvider.test.tsx +40 -0
- package/src/Tooltip/hooks/useTooltipContent.ts +3 -0
- package/src/Tooltip/hooks/useTooltipProvider.ts +46 -8
- package/src/Tooltip/hooks/useTooltipRoot.ts +30 -6
- package/src/Tooltip/types.ts +89 -8
- package/src/Tree/README.md +69 -5
- package/src/Tree/Tree.tsx +188 -34
- package/src/Tree/__tests__/Tree.styled-wrapper.test.tsx +159 -0
- package/src/Tree/hooks/useTreePath.ts +33 -1
- package/src/Tree/index.ts +6 -1
- package/src/Tree/types.ts +54 -12
- package/src/Tree/utils.ts +46 -4
- package/src/VisuallyHidden/VisuallyHidden.tsx +33 -8
- package/src/VisuallyHidden/__tests__/VisuallyHidden.test.tsx +15 -1
- package/src/VisuallyHidden/types.ts +11 -1
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useTypeahead.ts +95 -0
- package/src/index.ts +10 -0
- package/src/test/intersectionObserverPolyfill.ts +7 -0
- package/src/test/resizeObserverPolyfill.ts +80 -0
- package/src/useLocalStorage/__tests__/useLocalStorage.reading.test.tsx +31 -0
- package/src/useLocalStorage/__tests__/useLocalStorage.resilience.test.tsx +97 -0
- package/src/useLocalStorage/__tests__/useLocalStorage.ssr.test.tsx +20 -0
- package/src/useLocalStorage/__tests__/useLocalStorage.writing.test.tsx +72 -0
- package/src/useLocalStorage/index.ts +1 -0
- package/src/useLocalStorage/useLocalStorage.ts +140 -0
- package/src/useMediaQuery/README.md +66 -0
- package/src/useMediaQuery/__tests__/useMediaQuery.matching.test.tsx +42 -0
- package/src/useMediaQuery/__tests__/useMediaQuery.ssr.test.tsx +19 -0
- package/src/useMediaQuery/__tests__/useMediaQuery.subscription.test.tsx +89 -0
- package/src/useMediaQuery/index.ts +1 -0
- package/src/useMediaQuery/useMediaQuery.ts +49 -0
- package/src/usePagination/README.md +170 -0
- package/src/usePagination/__tests__/usePagination.navigation.test.tsx +109 -0
- package/src/usePagination/__tests__/usePagination.page-count.test.tsx +56 -0
- package/src/usePagination/__tests__/usePagination.page-state.test.tsx +87 -0
- package/src/usePagination/__tests__/usePagination.range.test.tsx +82 -0
- package/src/usePagination/__tests__/usePagination.slice.test.tsx +59 -0
- package/src/usePagination/__tests__/usePagination.windowing.test.tsx +68 -0
- package/src/usePagination/index.ts +1 -0
- package/src/usePagination/usePagination.ts +292 -0
- package/src/utils/focusable.ts +15 -0
- package/src/utils/getKeyToActionMap.ts +1 -1
- package/src/utils/index.ts +1 -0
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
import { act, render, screen, waitFor } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import type { ReactElement } from "react";
|
|
4
|
+
|
|
5
|
+
import { NavigationMenu } from "../NavigationMenu";
|
|
6
|
+
import { getPanelMotion } from "../utils";
|
|
7
|
+
|
|
8
|
+
/** jsdom reports every offset as 0, so the size the Viewport reads has to be
|
|
9
|
+
* planted on the panel first — the same approach the Indicator tests take. */
|
|
10
|
+
function plantSize(
|
|
11
|
+
element: HTMLElement,
|
|
12
|
+
box: { width: number; height: number },
|
|
13
|
+
) {
|
|
14
|
+
Object.defineProperty(element, "offsetWidth", {
|
|
15
|
+
value: box.width,
|
|
16
|
+
configurable: true,
|
|
17
|
+
});
|
|
18
|
+
Object.defineProperty(element, "offsetHeight", {
|
|
19
|
+
value: box.height,
|
|
20
|
+
configurable: true,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const widthOf = (el: HTMLElement) =>
|
|
25
|
+
el.style.getPropertyValue("--primitiv-navigation-menu-viewport-width");
|
|
26
|
+
const heightOf = (el: HTMLElement) =>
|
|
27
|
+
el.style.getPropertyValue("--primitiv-navigation-menu-viewport-height");
|
|
28
|
+
|
|
29
|
+
/** Three disclosure entries plus a plain bar link, all force-mounted so a closing
|
|
30
|
+
* panel stays in the DOM long enough to report the direction it left in. */
|
|
31
|
+
function MotionNav(): ReactElement {
|
|
32
|
+
return (
|
|
33
|
+
<NavigationMenu.Root>
|
|
34
|
+
<NavigationMenu.List>
|
|
35
|
+
<NavigationMenu.Item value="one">
|
|
36
|
+
<NavigationMenu.Trigger>One</NavigationMenu.Trigger>
|
|
37
|
+
<NavigationMenu.Content forceMount data-testid="panel-one" />
|
|
38
|
+
</NavigationMenu.Item>
|
|
39
|
+
<NavigationMenu.Item value="two">
|
|
40
|
+
<NavigationMenu.Trigger>Two</NavigationMenu.Trigger>
|
|
41
|
+
<NavigationMenu.Content forceMount data-testid="panel-two" />
|
|
42
|
+
</NavigationMenu.Item>
|
|
43
|
+
<NavigationMenu.Item value="three">
|
|
44
|
+
<NavigationMenu.Trigger>Three</NavigationMenu.Trigger>
|
|
45
|
+
<NavigationMenu.Content forceMount data-testid="panel-three" />
|
|
46
|
+
</NavigationMenu.Item>
|
|
47
|
+
</NavigationMenu.List>
|
|
48
|
+
<NavigationMenu.Viewport forceMount data-testid="viewport" />
|
|
49
|
+
</NavigationMenu.Root>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe("getPanelMotion", () => {
|
|
54
|
+
const itemValues = ["one", "two", "three"];
|
|
55
|
+
|
|
56
|
+
it("gives an entering panel the side it travelled from", () => {
|
|
57
|
+
// one -> two moves toward the end, so two arrives from the end.
|
|
58
|
+
expect(
|
|
59
|
+
getPanelMotion({
|
|
60
|
+
value: "two",
|
|
61
|
+
openValue: "two",
|
|
62
|
+
previousValue: "one",
|
|
63
|
+
itemValues,
|
|
64
|
+
}),
|
|
65
|
+
).toBe("from-end");
|
|
66
|
+
expect(
|
|
67
|
+
getPanelMotion({
|
|
68
|
+
value: "two",
|
|
69
|
+
openValue: "two",
|
|
70
|
+
previousValue: "three",
|
|
71
|
+
itemValues,
|
|
72
|
+
}),
|
|
73
|
+
).toBe("from-start");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("gives a leaving panel the side it travelled toward", () => {
|
|
77
|
+
expect(
|
|
78
|
+
getPanelMotion({
|
|
79
|
+
value: "one",
|
|
80
|
+
openValue: "two",
|
|
81
|
+
previousValue: "one",
|
|
82
|
+
itemValues,
|
|
83
|
+
}),
|
|
84
|
+
).toBe("to-start");
|
|
85
|
+
expect(
|
|
86
|
+
getPanelMotion({
|
|
87
|
+
value: "three",
|
|
88
|
+
openValue: "two",
|
|
89
|
+
previousValue: "three",
|
|
90
|
+
itemValues,
|
|
91
|
+
}),
|
|
92
|
+
).toBe("to-end");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("has no direction for the first open or the full close", () => {
|
|
96
|
+
// Nothing to travel from.
|
|
97
|
+
expect(
|
|
98
|
+
getPanelMotion({
|
|
99
|
+
value: "two",
|
|
100
|
+
openValue: "two",
|
|
101
|
+
previousValue: "",
|
|
102
|
+
itemValues,
|
|
103
|
+
}),
|
|
104
|
+
).toBeUndefined();
|
|
105
|
+
// Nothing to travel to.
|
|
106
|
+
expect(
|
|
107
|
+
getPanelMotion({
|
|
108
|
+
value: "two",
|
|
109
|
+
openValue: "",
|
|
110
|
+
previousValue: "two",
|
|
111
|
+
itemValues,
|
|
112
|
+
}),
|
|
113
|
+
).toBeUndefined();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("leaves bystanders and unregistered entries alone", () => {
|
|
117
|
+
// Neither opening nor closing.
|
|
118
|
+
expect(
|
|
119
|
+
getPanelMotion({
|
|
120
|
+
value: "three",
|
|
121
|
+
openValue: "two",
|
|
122
|
+
previousValue: "one",
|
|
123
|
+
itemValues,
|
|
124
|
+
}),
|
|
125
|
+
).toBeUndefined();
|
|
126
|
+
// A value with no registered trigger has no position to derive from.
|
|
127
|
+
expect(
|
|
128
|
+
getPanelMotion({
|
|
129
|
+
value: "ghost",
|
|
130
|
+
openValue: "ghost",
|
|
131
|
+
previousValue: "one",
|
|
132
|
+
itemValues,
|
|
133
|
+
}),
|
|
134
|
+
).toBeUndefined();
|
|
135
|
+
expect(
|
|
136
|
+
getPanelMotion({
|
|
137
|
+
value: "two",
|
|
138
|
+
openValue: "two",
|
|
139
|
+
previousValue: "ghost",
|
|
140
|
+
itemValues,
|
|
141
|
+
}),
|
|
142
|
+
).toBeUndefined();
|
|
143
|
+
expect(
|
|
144
|
+
getPanelMotion({
|
|
145
|
+
value: "two",
|
|
146
|
+
openValue: "ghost",
|
|
147
|
+
previousValue: "two",
|
|
148
|
+
itemValues,
|
|
149
|
+
}),
|
|
150
|
+
).toBeUndefined();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("treats an empty value as no panel at all", () => {
|
|
154
|
+
expect(
|
|
155
|
+
getPanelMotion({
|
|
156
|
+
value: "",
|
|
157
|
+
openValue: "",
|
|
158
|
+
previousValue: "",
|
|
159
|
+
itemValues,
|
|
160
|
+
}),
|
|
161
|
+
).toBeUndefined();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe("NavigationMenu.Content motion", () => {
|
|
166
|
+
// Switching is driven by hover, not a second click: once a panel is open,
|
|
167
|
+
// crossing to a sibling trigger swaps instantly, whereas a click would toggle
|
|
168
|
+
// against whatever the arriving pointer had already opened (see the hover-intent
|
|
169
|
+
// notes on Trigger) and close the menu instead of switching it.
|
|
170
|
+
it("publishes the travel direction on both panels when switching", async () => {
|
|
171
|
+
const user = userEvent.setup();
|
|
172
|
+
render(<MotionNav />);
|
|
173
|
+
|
|
174
|
+
await user.click(screen.getByRole("button", { name: "One" }));
|
|
175
|
+
// First open: no direction to travel from.
|
|
176
|
+
expect(screen.getByTestId("panel-one")).not.toHaveAttribute("data-motion");
|
|
177
|
+
|
|
178
|
+
await user.hover(screen.getByRole("button", { name: "Three" }));
|
|
179
|
+
await waitFor(() =>
|
|
180
|
+
expect(screen.getByTestId("panel-three")).toHaveAttribute(
|
|
181
|
+
"data-motion",
|
|
182
|
+
"from-end",
|
|
183
|
+
),
|
|
184
|
+
);
|
|
185
|
+
expect(screen.getByTestId("panel-one")).toHaveAttribute(
|
|
186
|
+
"data-motion",
|
|
187
|
+
"to-start",
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("mirrors the direction when travelling back toward the start", async () => {
|
|
192
|
+
const user = userEvent.setup();
|
|
193
|
+
render(<MotionNav />);
|
|
194
|
+
|
|
195
|
+
await user.click(screen.getByRole("button", { name: "Three" }));
|
|
196
|
+
await user.hover(screen.getByRole("button", { name: "One" }));
|
|
197
|
+
|
|
198
|
+
await waitFor(() =>
|
|
199
|
+
expect(screen.getByTestId("panel-one")).toHaveAttribute(
|
|
200
|
+
"data-motion",
|
|
201
|
+
"from-start",
|
|
202
|
+
),
|
|
203
|
+
);
|
|
204
|
+
expect(screen.getByTestId("panel-three")).toHaveAttribute(
|
|
205
|
+
"data-motion",
|
|
206
|
+
"to-end",
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("re-registers an entry whose value changes, so the order stays true", async () => {
|
|
211
|
+
// The order comes from a value-keyed registry, so a value that changes has to
|
|
212
|
+
// unregister the old key and register the new one. Leaving the stale key behind
|
|
213
|
+
// (or never re-registering) puts the entries in the wrong order and the panels
|
|
214
|
+
// slide the wrong way.
|
|
215
|
+
function RenamableNav({ firstValue }: { firstValue: string }): ReactElement {
|
|
216
|
+
return (
|
|
217
|
+
<NavigationMenu.Root>
|
|
218
|
+
<NavigationMenu.List>
|
|
219
|
+
<NavigationMenu.Item value={firstValue}>
|
|
220
|
+
<NavigationMenu.Trigger>First</NavigationMenu.Trigger>
|
|
221
|
+
<NavigationMenu.Content forceMount data-testid="panel-first" />
|
|
222
|
+
</NavigationMenu.Item>
|
|
223
|
+
<NavigationMenu.Item value="second">
|
|
224
|
+
<NavigationMenu.Trigger>Second</NavigationMenu.Trigger>
|
|
225
|
+
<NavigationMenu.Content forceMount data-testid="panel-second" />
|
|
226
|
+
</NavigationMenu.Item>
|
|
227
|
+
</NavigationMenu.List>
|
|
228
|
+
<NavigationMenu.Viewport forceMount data-testid="viewport" />
|
|
229
|
+
</NavigationMenu.Root>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const user = userEvent.setup();
|
|
234
|
+
const { rerender } = render(<RenamableNav firstValue="alpha" />);
|
|
235
|
+
rerender(<RenamableNav firstValue="renamed" />);
|
|
236
|
+
|
|
237
|
+
await user.click(screen.getByRole("button", { name: "First" }));
|
|
238
|
+
await user.hover(screen.getByRole("button", { name: "Second" }));
|
|
239
|
+
|
|
240
|
+
await waitFor(() =>
|
|
241
|
+
expect(screen.getByTestId("panel-second")).toHaveAttribute(
|
|
242
|
+
"data-motion",
|
|
243
|
+
"from-end",
|
|
244
|
+
),
|
|
245
|
+
);
|
|
246
|
+
expect(screen.getByTestId("panel-first")).toHaveAttribute(
|
|
247
|
+
"data-motion",
|
|
248
|
+
"to-start",
|
|
249
|
+
);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("drops the direction once the menu closes completely", async () => {
|
|
253
|
+
const user = userEvent.setup();
|
|
254
|
+
render(<MotionNav />);
|
|
255
|
+
|
|
256
|
+
await user.click(screen.getByRole("button", { name: "Two" }));
|
|
257
|
+
await user.click(screen.getByRole("button", { name: "Two" }));
|
|
258
|
+
|
|
259
|
+
await waitFor(() =>
|
|
260
|
+
expect(screen.getByTestId("panel-two")).not.toHaveAttribute(
|
|
261
|
+
"data-motion",
|
|
262
|
+
),
|
|
263
|
+
);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe("NavigationMenu.Viewport measurement", () => {
|
|
268
|
+
it("publishes the open panel's measured size", async () => {
|
|
269
|
+
const user = userEvent.setup();
|
|
270
|
+
render(<MotionNav />);
|
|
271
|
+
|
|
272
|
+
const viewport = screen.getByTestId("viewport");
|
|
273
|
+
// Nothing has ever been open, so there is nothing to publish.
|
|
274
|
+
expect(widthOf(viewport)).toBe("");
|
|
275
|
+
|
|
276
|
+
plantSize(screen.getByTestId("panel-two"), { width: 240, height: 96 });
|
|
277
|
+
await user.click(screen.getByRole("button", { name: "Two" }));
|
|
278
|
+
|
|
279
|
+
await waitFor(() => expect(widthOf(viewport)).toBe("240px"));
|
|
280
|
+
expect(heightOf(viewport)).toBe("96px");
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("keeps the last measurement through the close so the exit has a size", async () => {
|
|
284
|
+
const user = userEvent.setup();
|
|
285
|
+
render(<MotionNav />);
|
|
286
|
+
|
|
287
|
+
const viewport = screen.getByTestId("viewport");
|
|
288
|
+
plantSize(screen.getByTestId("panel-two"), { width: 240, height: 96 });
|
|
289
|
+
await user.click(screen.getByRole("button", { name: "Two" }));
|
|
290
|
+
await waitFor(() => expect(widthOf(viewport)).toBe("240px"));
|
|
291
|
+
|
|
292
|
+
await user.click(screen.getByRole("button", { name: "Two" }));
|
|
293
|
+
|
|
294
|
+
// Clearing it here would collapse the box at the moment the exit needs its size.
|
|
295
|
+
expect(widthOf(viewport)).toBe("240px");
|
|
296
|
+
expect(heightOf(viewport)).toBe("96px");
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("re-measures the open panel when the window resizes", async () => {
|
|
300
|
+
const user = userEvent.setup();
|
|
301
|
+
render(<MotionNav />);
|
|
302
|
+
|
|
303
|
+
const viewport = screen.getByTestId("viewport");
|
|
304
|
+
const panel = screen.getByTestId("panel-two");
|
|
305
|
+
plantSize(panel, { width: 240, height: 96 });
|
|
306
|
+
await user.click(screen.getByRole("button", { name: "Two" }));
|
|
307
|
+
await waitFor(() => expect(widthOf(viewport)).toBe("240px"));
|
|
308
|
+
|
|
309
|
+
// Size is layout-derived, so a reflow has to invalidate it.
|
|
310
|
+
plantSize(panel, { width: 320, height: 128 });
|
|
311
|
+
act(() => {
|
|
312
|
+
window.dispatchEvent(new Event("resize"));
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
expect(widthOf(viewport)).toBe("320px");
|
|
316
|
+
expect(heightOf(viewport)).toBe("128px");
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("removes its resize listener on unmount", () => {
|
|
320
|
+
const addEventListener = vi.spyOn(window, "addEventListener");
|
|
321
|
+
const removeEventListener = vi.spyOn(window, "removeEventListener");
|
|
322
|
+
|
|
323
|
+
const { unmount } = render(<MotionNav />);
|
|
324
|
+
const measure = addEventListener.mock.calls.find(
|
|
325
|
+
([type]) => type === "resize",
|
|
326
|
+
)?.[1];
|
|
327
|
+
unmount();
|
|
328
|
+
|
|
329
|
+
expect(measure).toBeTypeOf("function");
|
|
330
|
+
expect(removeEventListener).toHaveBeenCalledWith("resize", measure);
|
|
331
|
+
|
|
332
|
+
addEventListener.mockRestore();
|
|
333
|
+
removeEventListener.mockRestore();
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
/** jsdom doesn't implement the CSS Font Loading API at all (`document.fonts`
|
|
337
|
+
* is `undefined`), so a minimal EventTarget-shaped stub stands in for it —
|
|
338
|
+
* just enough surface for the hook's addEventListener/removeEventListener
|
|
339
|
+
* calls to record and fire a "loadingdone" listener. */
|
|
340
|
+
function stubDocumentFonts() {
|
|
341
|
+
const listeners = new Map<string, EventListener>();
|
|
342
|
+
const stub = {
|
|
343
|
+
addEventListener: (type: string, listener: EventListener) =>
|
|
344
|
+
listeners.set(type, listener),
|
|
345
|
+
removeEventListener: (type: string, listener: EventListener) => {
|
|
346
|
+
if (listeners.get(type) === listener) listeners.delete(type);
|
|
347
|
+
},
|
|
348
|
+
dispatch: (type: string) => listeners.get(type)?.(new Event(type)),
|
|
349
|
+
listeners,
|
|
350
|
+
};
|
|
351
|
+
const original = Object.getOwnPropertyDescriptor(document, "fonts");
|
|
352
|
+
Object.defineProperty(document, "fonts", { value: stub, configurable: true });
|
|
353
|
+
return {
|
|
354
|
+
stub,
|
|
355
|
+
restore: () => {
|
|
356
|
+
if (original) Object.defineProperty(document, "fonts", original);
|
|
357
|
+
else delete (document as { fonts?: unknown }).fonts;
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
it("re-measures the open panel once a swapped web font finishes loading", async () => {
|
|
363
|
+
// A late font swap (e.g. Google Fonts' display=swap) reflows text after the
|
|
364
|
+
// effect's own measurement already ran — nothing else invalidates it, so the
|
|
365
|
+
// published size would otherwise stay locked to the pre-swap layout forever.
|
|
366
|
+
const { stub, restore } = stubDocumentFonts();
|
|
367
|
+
try {
|
|
368
|
+
const user = userEvent.setup();
|
|
369
|
+
render(<MotionNav />);
|
|
370
|
+
|
|
371
|
+
const viewport = screen.getByTestId("viewport");
|
|
372
|
+
const panel = screen.getByTestId("panel-two");
|
|
373
|
+
plantSize(panel, { width: 240, height: 96 });
|
|
374
|
+
await user.click(screen.getByRole("button", { name: "Two" }));
|
|
375
|
+
await waitFor(() => expect(widthOf(viewport)).toBe("240px"));
|
|
376
|
+
|
|
377
|
+
plantSize(panel, { width: 320, height: 128 });
|
|
378
|
+
act(() => {
|
|
379
|
+
stub.dispatch("loadingdone");
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
expect(widthOf(viewport)).toBe("320px");
|
|
383
|
+
expect(heightOf(viewport)).toBe("128px");
|
|
384
|
+
} finally {
|
|
385
|
+
restore();
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it("removes its font-loading listener on unmount", () => {
|
|
390
|
+
const { stub, restore } = stubDocumentFonts();
|
|
391
|
+
try {
|
|
392
|
+
const addEventListener = vi.spyOn(stub, "addEventListener");
|
|
393
|
+
const removeEventListener = vi.spyOn(stub, "removeEventListener");
|
|
394
|
+
|
|
395
|
+
const { unmount } = render(<MotionNav />);
|
|
396
|
+
const measure = addEventListener.mock.calls.find(
|
|
397
|
+
([type]) => type === "loadingdone",
|
|
398
|
+
)?.[1];
|
|
399
|
+
unmount();
|
|
400
|
+
|
|
401
|
+
expect(measure).toBeTypeOf("function");
|
|
402
|
+
expect(removeEventListener).toHaveBeenCalledWith("loadingdone", measure);
|
|
403
|
+
} finally {
|
|
404
|
+
restore();
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it("skips the font-loading listener when the Font Loading API is unavailable", () => {
|
|
409
|
+
const original = Object.getOwnPropertyDescriptor(document, "fonts");
|
|
410
|
+
// jsdom already lacks document.fonts, but assert the guard explicitly so this
|
|
411
|
+
// doesn't rely on that being jsdom's default forever.
|
|
412
|
+
Object.defineProperty(document, "fonts", {
|
|
413
|
+
value: undefined,
|
|
414
|
+
configurable: true,
|
|
415
|
+
});
|
|
416
|
+
try {
|
|
417
|
+
expect(() => render(<MotionNav />)).not.toThrow();
|
|
418
|
+
} finally {
|
|
419
|
+
if (original) Object.defineProperty(document, "fonts", original);
|
|
420
|
+
else delete (document as { fonts?: unknown }).fonts;
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it("cancels its pending re-measurement frame on unmount", () => {
|
|
425
|
+
const requestAnimationFrame = vi.spyOn(window, "requestAnimationFrame");
|
|
426
|
+
const cancelAnimationFrame = vi.spyOn(window, "cancelAnimationFrame");
|
|
427
|
+
|
|
428
|
+
const { unmount } = render(<MotionNav />);
|
|
429
|
+
const frameId = requestAnimationFrame.mock.results[0]?.value;
|
|
430
|
+
unmount();
|
|
431
|
+
|
|
432
|
+
expect(frameId).toBeTypeOf("number");
|
|
433
|
+
expect(cancelAnimationFrame).toHaveBeenCalledWith(frameId);
|
|
434
|
+
|
|
435
|
+
requestAnimationFrame.mockRestore();
|
|
436
|
+
cancelAnimationFrame.mockRestore();
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it("publishes nothing when the open value has no panel to measure", () => {
|
|
440
|
+
// Mirrors the Indicator's no-rendered-trigger case: better an unsized box than
|
|
441
|
+
// one reporting a size that belongs to nothing.
|
|
442
|
+
render(
|
|
443
|
+
<NavigationMenu.Root defaultValue="ghost">
|
|
444
|
+
<NavigationMenu.List>
|
|
445
|
+
<NavigationMenu.Item value="one">
|
|
446
|
+
<NavigationMenu.Trigger>One</NavigationMenu.Trigger>
|
|
447
|
+
<NavigationMenu.Content forceMount />
|
|
448
|
+
</NavigationMenu.Item>
|
|
449
|
+
</NavigationMenu.List>
|
|
450
|
+
<NavigationMenu.Viewport forceMount data-testid="viewport" />
|
|
451
|
+
</NavigationMenu.Root>,
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
const viewport = screen.getByTestId("viewport");
|
|
455
|
+
expect(widthOf(viewport)).toBe("");
|
|
456
|
+
expect(heightOf(viewport)).toBe("");
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it("lets a consumer style override the published size", () => {
|
|
460
|
+
render(
|
|
461
|
+
<NavigationMenu.Root defaultValue="one">
|
|
462
|
+
<NavigationMenu.List>
|
|
463
|
+
<NavigationMenu.Item value="one">
|
|
464
|
+
<NavigationMenu.Trigger>One</NavigationMenu.Trigger>
|
|
465
|
+
<NavigationMenu.Content forceMount />
|
|
466
|
+
</NavigationMenu.Item>
|
|
467
|
+
</NavigationMenu.List>
|
|
468
|
+
<NavigationMenu.Viewport
|
|
469
|
+
data-testid="viewport"
|
|
470
|
+
style={{ outline: "1px solid red" }}
|
|
471
|
+
/>
|
|
472
|
+
</NavigationMenu.Root>,
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
expect(screen.getByTestId("viewport")).toHaveStyle({
|
|
476
|
+
outline: "1px solid red",
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
describe("NavigationMenu.Link hover", () => {
|
|
482
|
+
function BarLinkNav(props: { openOnHover?: boolean }): ReactElement {
|
|
483
|
+
return (
|
|
484
|
+
<NavigationMenu.Root {...props}>
|
|
485
|
+
<NavigationMenu.List>
|
|
486
|
+
<NavigationMenu.Item value="one">
|
|
487
|
+
<NavigationMenu.Trigger>One</NavigationMenu.Trigger>
|
|
488
|
+
<NavigationMenu.Content forceMount data-testid="panel-one">
|
|
489
|
+
<NavigationMenu.Link href="/row">Row</NavigationMenu.Link>
|
|
490
|
+
</NavigationMenu.Content>
|
|
491
|
+
</NavigationMenu.Item>
|
|
492
|
+
<NavigationMenu.Item>
|
|
493
|
+
<NavigationMenu.Link href="/changelog">
|
|
494
|
+
Changelog
|
|
495
|
+
</NavigationMenu.Link>
|
|
496
|
+
</NavigationMenu.Item>
|
|
497
|
+
</NavigationMenu.List>
|
|
498
|
+
<NavigationMenu.Viewport forceMount data-testid="viewport" />
|
|
499
|
+
</NavigationMenu.Root>
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
it("closes the open panel when the pointer reaches a top-level link", async () => {
|
|
504
|
+
const user = userEvent.setup();
|
|
505
|
+
render(<BarLinkNav />);
|
|
506
|
+
|
|
507
|
+
await user.click(screen.getByRole("button", { name: "One" }));
|
|
508
|
+
expect(screen.getByTestId("viewport")).toHaveAttribute(
|
|
509
|
+
"data-state",
|
|
510
|
+
"open",
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
await user.hover(screen.getByRole("link", { name: "Changelog" }));
|
|
514
|
+
|
|
515
|
+
await waitFor(() =>
|
|
516
|
+
expect(screen.getByTestId("viewport")).toHaveAttribute(
|
|
517
|
+
"data-state",
|
|
518
|
+
"closed",
|
|
519
|
+
),
|
|
520
|
+
);
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it("does not close when the pointer reaches a link inside the panel", async () => {
|
|
524
|
+
const user = userEvent.setup();
|
|
525
|
+
render(<BarLinkNav />);
|
|
526
|
+
|
|
527
|
+
await user.click(screen.getByRole("button", { name: "One" }));
|
|
528
|
+
await user.hover(screen.getByRole("link", { name: "Row" }));
|
|
529
|
+
|
|
530
|
+
expect(screen.getByTestId("viewport")).toHaveAttribute(
|
|
531
|
+
"data-state",
|
|
532
|
+
"open",
|
|
533
|
+
);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it("leaves a click-only nav click-only", async () => {
|
|
537
|
+
const user = userEvent.setup();
|
|
538
|
+
render(<BarLinkNav openOnHover={false} />);
|
|
539
|
+
|
|
540
|
+
await user.click(screen.getByRole("button", { name: "One" }));
|
|
541
|
+
await user.hover(screen.getByRole("link", { name: "Changelog" }));
|
|
542
|
+
|
|
543
|
+
expect(screen.getByTestId("viewport")).toHaveAttribute(
|
|
544
|
+
"data-state",
|
|
545
|
+
"open",
|
|
546
|
+
);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it("still runs a consumer's own pointer-enter handler", async () => {
|
|
550
|
+
const user = userEvent.setup();
|
|
551
|
+
const onPointerEnter = vi.fn();
|
|
552
|
+
render(
|
|
553
|
+
<NavigationMenu.Root>
|
|
554
|
+
<NavigationMenu.List>
|
|
555
|
+
<NavigationMenu.Item>
|
|
556
|
+
<NavigationMenu.Link href="/x" onPointerEnter={onPointerEnter}>
|
|
557
|
+
Changelog
|
|
558
|
+
</NavigationMenu.Link>
|
|
559
|
+
</NavigationMenu.Item>
|
|
560
|
+
</NavigationMenu.List>
|
|
561
|
+
</NavigationMenu.Root>,
|
|
562
|
+
);
|
|
563
|
+
|
|
564
|
+
await user.hover(screen.getByRole("link", { name: "Changelog" }));
|
|
565
|
+
|
|
566
|
+
expect(onPointerEnter).toHaveBeenCalledTimes(1);
|
|
567
|
+
});
|
|
568
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
|
|
4
|
+
import { NavigationMenu } from "../NavigationMenu";
|
|
5
|
+
|
|
6
|
+
import { ThreeEntryNav } from "./NavigationMenu.fixtures";
|
|
7
|
+
|
|
8
|
+
describe("NavigationMenu — mouse interaction", () => {
|
|
9
|
+
it("opens a panel on trigger click and closes it on a second click", async () => {
|
|
10
|
+
const user = userEvent.setup();
|
|
11
|
+
render(<ThreeEntryNav />);
|
|
12
|
+
|
|
13
|
+
const trigger = screen.getByRole("button", { name: "Concepts" });
|
|
14
|
+
const panel = screen.getByTestId("concepts-panel");
|
|
15
|
+
|
|
16
|
+
await user.click(trigger);
|
|
17
|
+
|
|
18
|
+
expect(trigger).toHaveAttribute("aria-expanded", "true");
|
|
19
|
+
expect(trigger).toHaveAttribute("data-state", "open");
|
|
20
|
+
expect(panel).toHaveAttribute("data-state", "open");
|
|
21
|
+
expect(panel).toBeVisible();
|
|
22
|
+
|
|
23
|
+
await user.click(trigger);
|
|
24
|
+
|
|
25
|
+
expect(trigger).toHaveAttribute("aria-expanded", "false");
|
|
26
|
+
expect(panel).not.toBeVisible();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("switches the open panel when a different trigger is clicked", async () => {
|
|
30
|
+
const user = userEvent.setup();
|
|
31
|
+
render(<ThreeEntryNav />);
|
|
32
|
+
|
|
33
|
+
await user.click(screen.getByRole("button", { name: "Concepts" }));
|
|
34
|
+
await user.click(screen.getByRole("button", { name: "Registry & CLI" }));
|
|
35
|
+
|
|
36
|
+
expect(screen.getByTestId("concepts-panel")).not.toBeVisible();
|
|
37
|
+
expect(screen.getByTestId("registry-panel")).toBeVisible();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("composes the consumer's own trigger handlers with its own", async () => {
|
|
41
|
+
const user = userEvent.setup();
|
|
42
|
+
const onClick = vi.fn();
|
|
43
|
+
const onPointerEnter = vi.fn();
|
|
44
|
+
const onPointerLeave = vi.fn();
|
|
45
|
+
const onKeyDown = vi.fn();
|
|
46
|
+
render(
|
|
47
|
+
<NavigationMenu.Root openOnHover={false}>
|
|
48
|
+
<NavigationMenu.List>
|
|
49
|
+
<NavigationMenu.Item value="concepts">
|
|
50
|
+
<NavigationMenu.Trigger
|
|
51
|
+
onClick={onClick}
|
|
52
|
+
onPointerEnter={onPointerEnter}
|
|
53
|
+
onPointerLeave={onPointerLeave}
|
|
54
|
+
onKeyDown={onKeyDown}
|
|
55
|
+
>
|
|
56
|
+
Concepts
|
|
57
|
+
</NavigationMenu.Trigger>
|
|
58
|
+
<NavigationMenu.Content data-testid="panel" />
|
|
59
|
+
</NavigationMenu.Item>
|
|
60
|
+
</NavigationMenu.List>
|
|
61
|
+
</NavigationMenu.Root>,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const trigger = screen.getByRole("button", { name: "Concepts" });
|
|
65
|
+
await user.click(trigger);
|
|
66
|
+
await user.keyboard("{ArrowRight}");
|
|
67
|
+
await user.unhover(trigger);
|
|
68
|
+
|
|
69
|
+
expect(onClick).toHaveBeenCalledOnce();
|
|
70
|
+
expect(onPointerEnter).toHaveBeenCalledOnce();
|
|
71
|
+
expect(onPointerLeave).toHaveBeenCalledOnce();
|
|
72
|
+
expect(onKeyDown).toHaveBeenCalled();
|
|
73
|
+
// ...and the component's own behaviour still runs alongside them.
|
|
74
|
+
expect(screen.getByTestId("panel")).toBeVisible();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
|
|
4
|
+
import { ThreeEntryNav } from "./NavigationMenu.fixtures";
|
|
5
|
+
|
|
6
|
+
describe("NavigationMenu — uncontrolled state", () => {
|
|
7
|
+
it("starts with everything closed when no defaultValue is given", () => {
|
|
8
|
+
render(<ThreeEntryNav />);
|
|
9
|
+
|
|
10
|
+
expect(screen.getByTestId("concepts-panel")).not.toBeVisible();
|
|
11
|
+
expect(screen.getByTestId("registry-panel")).not.toBeVisible();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("opens the defaultValue entry's panel on first render", () => {
|
|
15
|
+
render(<ThreeEntryNav defaultValue="registry" />);
|
|
16
|
+
|
|
17
|
+
expect(screen.getByTestId("registry-panel")).toBeVisible();
|
|
18
|
+
expect(screen.getByTestId("concepts-panel")).not.toBeVisible();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("still owns the open value after a defaultValue seed", async () => {
|
|
22
|
+
const user = userEvent.setup();
|
|
23
|
+
render(<ThreeEntryNav defaultValue="registry" />);
|
|
24
|
+
|
|
25
|
+
await user.click(screen.getByRole("button", { name: "Concepts" }));
|
|
26
|
+
|
|
27
|
+
expect(screen.getByTestId("concepts-panel")).toBeVisible();
|
|
28
|
+
expect(screen.getByTestId("registry-panel")).not.toBeVisible();
|
|
29
|
+
});
|
|
30
|
+
});
|