@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,317 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import { vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { Select } from "../Select";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Discriminating assertions that pin down behaviour equivalent mutants would
|
|
9
|
+
* otherwise slip past (preventDefault calls, the toggle-sync listener, and
|
|
10
|
+
* the exact ARIA / data-state / role wiring). Complements the behavioural
|
|
11
|
+
* suites, which assert *where focus lands* but not *how* it got there.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
function open() {
|
|
15
|
+
render(
|
|
16
|
+
<Select.Root defaultValue="vue">
|
|
17
|
+
<Select.Trigger>
|
|
18
|
+
<Select.Value placeholder="Pick" />
|
|
19
|
+
</Select.Trigger>
|
|
20
|
+
<Select.Content>
|
|
21
|
+
<Select.Item value="react">React</Select.Item>
|
|
22
|
+
<Select.Item value="vue">Vue</Select.Item>
|
|
23
|
+
<Select.Item value="solid">Solid</Select.Item>
|
|
24
|
+
</Select.Content>
|
|
25
|
+
</Select.Root>,
|
|
26
|
+
);
|
|
27
|
+
fireEvent.click(screen.getByRole("button"));
|
|
28
|
+
return screen.getByRole("listbox", { hidden: true });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("Select preventDefault on handled keys", () => {
|
|
32
|
+
it.each(["ArrowDown", "ArrowUp", "Home", "End", "Enter", " ", "Escape"])(
|
|
33
|
+
"cancels the default action for %s",
|
|
34
|
+
(key) => {
|
|
35
|
+
const list = open();
|
|
36
|
+
// fireEvent returns false when a handler called preventDefault.
|
|
37
|
+
const notCancelled = fireEvent.keyDown(list, { key });
|
|
38
|
+
expect(notCancelled).toBe(false);
|
|
39
|
+
},
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
it("cancels the default action for a typeahead match", () => {
|
|
43
|
+
const list = open();
|
|
44
|
+
const notCancelled = fireEvent.keyDown(list, { key: "s" }); // matches Solid
|
|
45
|
+
expect(notCancelled).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("does not cancel an unhandled key", () => {
|
|
49
|
+
const list = open();
|
|
50
|
+
// A modifier / multi-char key falls through untouched.
|
|
51
|
+
const notCancelled = fireEvent.keyDown(list, { key: "Shift" });
|
|
52
|
+
expect(notCancelled).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("Select toggle-event sync", () => {
|
|
57
|
+
it("closes when the popover dispatches a toggle→closed (browser light-dismiss)", () => {
|
|
58
|
+
const list = open();
|
|
59
|
+
expect(screen.getByRole("button")).toHaveAttribute("aria-expanded", "true");
|
|
60
|
+
|
|
61
|
+
// Simulate the browser's own light-dismiss: a toggle event to "closed".
|
|
62
|
+
const event = new Event("toggle");
|
|
63
|
+
Object.defineProperty(event, "newState", { value: "closed" });
|
|
64
|
+
fireEvent(list, event);
|
|
65
|
+
|
|
66
|
+
expect(screen.getByRole("button")).toHaveAttribute("aria-expanded", "false");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("ignores a toggle→open event (does not thrash state)", () => {
|
|
70
|
+
const list = open();
|
|
71
|
+
const openEvent = new Event("toggle");
|
|
72
|
+
Object.defineProperty(openEvent, "newState", { value: "open" });
|
|
73
|
+
fireEvent(list, openEvent);
|
|
74
|
+
expect(screen.getByRole("button")).toHaveAttribute("aria-expanded", "true");
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("Select selection commit + hidden form field", () => {
|
|
79
|
+
it("fires onOpenChange exactly once (to false) when a selection commits", async () => {
|
|
80
|
+
const user = userEvent.setup();
|
|
81
|
+
const onOpenChange = vi.fn();
|
|
82
|
+
render(
|
|
83
|
+
<Select.Root onOpenChange={onOpenChange}>
|
|
84
|
+
<Select.Trigger>
|
|
85
|
+
<Select.Value placeholder="Pick" />
|
|
86
|
+
</Select.Trigger>
|
|
87
|
+
<Select.Content>
|
|
88
|
+
<Select.Item value="react">React</Select.Item>
|
|
89
|
+
<Select.Item value="vue">Vue</Select.Item>
|
|
90
|
+
</Select.Content>
|
|
91
|
+
</Select.Root>,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
await user.click(screen.getByRole("button")); // opens
|
|
95
|
+
onOpenChange.mockClear();
|
|
96
|
+
await user.click(screen.getByText("Vue")); // selecting closes — exactly once
|
|
97
|
+
|
|
98
|
+
expect(onOpenChange).toHaveBeenCalledTimes(1);
|
|
99
|
+
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("renders a hidden form <select> option for each registered item value", () => {
|
|
103
|
+
const { container } = render(
|
|
104
|
+
<Select.Root name="framework">
|
|
105
|
+
<Select.Trigger>
|
|
106
|
+
<Select.Value placeholder="Pick" />
|
|
107
|
+
</Select.Trigger>
|
|
108
|
+
<Select.Content>
|
|
109
|
+
<Select.Item value="react">React</Select.Item>
|
|
110
|
+
<Select.Item value="vue">Vue</Select.Item>
|
|
111
|
+
</Select.Content>
|
|
112
|
+
</Select.Root>,
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const hidden = container.querySelector<HTMLSelectElement>(
|
|
116
|
+
'select[name="framework"]',
|
|
117
|
+
)!;
|
|
118
|
+
const values = Array.from(hidden.querySelectorAll("option")).map(
|
|
119
|
+
(o) => o.value,
|
|
120
|
+
);
|
|
121
|
+
expect(values).toContain("react");
|
|
122
|
+
expect(values).toContain("vue");
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("Select native-mode edges", () => {
|
|
127
|
+
it("keeps a numeric child as the option text under native", () => {
|
|
128
|
+
render(
|
|
129
|
+
<Select.Root native defaultValue="1">
|
|
130
|
+
<Select.Item value="1">{42}</Select.Item>
|
|
131
|
+
</Select.Root>,
|
|
132
|
+
);
|
|
133
|
+
expect(screen.getByRole("option", { name: "42" })).toBeInTheDocument();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("marks the matching option selected for a native controlled value", () => {
|
|
137
|
+
render(
|
|
138
|
+
<Select.Root native value="b" onValueChange={() => {}}>
|
|
139
|
+
<Select.Item value="a">A</Select.Item>
|
|
140
|
+
<Select.Item value="b">B</Select.Item>
|
|
141
|
+
</Select.Root>,
|
|
142
|
+
);
|
|
143
|
+
expect(
|
|
144
|
+
(screen.getByRole("option", { name: "B" }) as HTMLOptionElement).selected,
|
|
145
|
+
).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("does not throw on native change when onValueChange is omitted", async () => {
|
|
149
|
+
const user = userEvent.setup();
|
|
150
|
+
render(
|
|
151
|
+
<Select.Root native defaultValue="a">
|
|
152
|
+
<Select.Item value="a">A</Select.Item>
|
|
153
|
+
<Select.Item value="b">B</Select.Item>
|
|
154
|
+
</Select.Root>,
|
|
155
|
+
);
|
|
156
|
+
await user.selectOptions(screen.getByRole("combobox"), "b");
|
|
157
|
+
expect(
|
|
158
|
+
(screen.getByRole("option", { name: "B" }) as HTMLOptionElement).selected,
|
|
159
|
+
).toBe(true);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("filters an empty value out of the hidden select's generated options", () => {
|
|
163
|
+
const { container } = render(
|
|
164
|
+
<Select.Root name="framework">
|
|
165
|
+
<Select.Trigger>
|
|
166
|
+
<Select.Value placeholder="Pick" />
|
|
167
|
+
</Select.Trigger>
|
|
168
|
+
<Select.Content>
|
|
169
|
+
<Select.Item value="">None</Select.Item>
|
|
170
|
+
<Select.Item value="a">A</Select.Item>
|
|
171
|
+
</Select.Content>
|
|
172
|
+
</Select.Root>,
|
|
173
|
+
);
|
|
174
|
+
const hidden = container.querySelector<HTMLSelectElement>(
|
|
175
|
+
'select[name="framework"]',
|
|
176
|
+
)!;
|
|
177
|
+
const empties = Array.from(hidden.querySelectorAll("option")).filter(
|
|
178
|
+
(o) => o.value === "",
|
|
179
|
+
);
|
|
180
|
+
// Only the single literal placeholder <option value="">, not a duplicate
|
|
181
|
+
// generated from the registered empty value.
|
|
182
|
+
expect(empties).toHaveLength(1);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("calls onValueChange with the chosen value under native", async () => {
|
|
186
|
+
const user = userEvent.setup();
|
|
187
|
+
const onValueChange = vi.fn();
|
|
188
|
+
render(
|
|
189
|
+
<Select.Root native defaultValue="a" onValueChange={onValueChange}>
|
|
190
|
+
<Select.Item value="a">A</Select.Item>
|
|
191
|
+
<Select.Item value="b">B</Select.Item>
|
|
192
|
+
</Select.Root>,
|
|
193
|
+
);
|
|
194
|
+
await user.selectOptions(screen.getByRole("combobox"), "b");
|
|
195
|
+
expect(onValueChange).toHaveBeenCalledWith("b");
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe("Select hidden field + Content prop forwarding", () => {
|
|
200
|
+
it("renders the hidden form <select> non-focusable and aria-hidden", () => {
|
|
201
|
+
const { container } = render(
|
|
202
|
+
<Select.Root name="framework">
|
|
203
|
+
<Select.Trigger>
|
|
204
|
+
<Select.Value placeholder="Pick" />
|
|
205
|
+
</Select.Trigger>
|
|
206
|
+
<Select.Content>
|
|
207
|
+
<Select.Item value="a">A</Select.Item>
|
|
208
|
+
</Select.Content>
|
|
209
|
+
</Select.Root>,
|
|
210
|
+
);
|
|
211
|
+
const hidden = container.querySelector('select[name="framework"]')!;
|
|
212
|
+
expect(hidden).toHaveAttribute("tabindex", "-1");
|
|
213
|
+
expect(hidden).toHaveAttribute("aria-hidden", "true");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("forwards arbitrary props from Select.Content onto the listbox", async () => {
|
|
217
|
+
const user = userEvent.setup();
|
|
218
|
+
render(
|
|
219
|
+
<Select.Root>
|
|
220
|
+
<Select.Trigger>
|
|
221
|
+
<Select.Value placeholder="Pick" />
|
|
222
|
+
</Select.Trigger>
|
|
223
|
+
<Select.Content data-testid="lb" className="my-list">
|
|
224
|
+
<Select.Item value="a">A</Select.Item>
|
|
225
|
+
</Select.Content>
|
|
226
|
+
</Select.Root>,
|
|
227
|
+
);
|
|
228
|
+
await user.click(screen.getByRole("button"));
|
|
229
|
+
const lb = screen.getByRole("listbox", { hidden: true });
|
|
230
|
+
expect(lb).toHaveAttribute("data-testid", "lb");
|
|
231
|
+
expect(lb).toHaveClass("my-list");
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe("Select rich data-disabled + typeahead reach", () => {
|
|
236
|
+
it("exposes data-disabled on a rich item only when disabled", async () => {
|
|
237
|
+
const user = userEvent.setup();
|
|
238
|
+
render(
|
|
239
|
+
<Select.Root>
|
|
240
|
+
<Select.Trigger>
|
|
241
|
+
<Select.Value placeholder="Pick" />
|
|
242
|
+
</Select.Trigger>
|
|
243
|
+
<Select.Content>
|
|
244
|
+
<Select.Item value="a">Enabled</Select.Item>
|
|
245
|
+
<Select.Item value="b" disabled>
|
|
246
|
+
Disabled
|
|
247
|
+
</Select.Item>
|
|
248
|
+
</Select.Content>
|
|
249
|
+
</Select.Root>,
|
|
250
|
+
);
|
|
251
|
+
await user.click(screen.getByRole("button"));
|
|
252
|
+
expect(
|
|
253
|
+
screen.getByRole("option", { name: "Enabled", hidden: true }),
|
|
254
|
+
).not.toHaveAttribute("data-disabled");
|
|
255
|
+
expect(screen.getByText("Disabled")).toHaveAttribute("data-disabled", "");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("typeahead reaches the last option in a 5-item list (loop bound + modulo)", async () => {
|
|
259
|
+
const user = userEvent.setup();
|
|
260
|
+
render(
|
|
261
|
+
<Select.Root>
|
|
262
|
+
<Select.Trigger>
|
|
263
|
+
<Select.Value placeholder="Pick" />
|
|
264
|
+
</Select.Trigger>
|
|
265
|
+
<Select.Content>
|
|
266
|
+
<Select.Item value="a">Alpha</Select.Item>
|
|
267
|
+
<Select.Item value="b">Bravo</Select.Item>
|
|
268
|
+
<Select.Item value="c">Charlie</Select.Item>
|
|
269
|
+
<Select.Item value="d">Delta</Select.Item>
|
|
270
|
+
<Select.Item value="e">Echo</Select.Item>
|
|
271
|
+
</Select.Content>
|
|
272
|
+
</Select.Root>,
|
|
273
|
+
);
|
|
274
|
+
await user.click(screen.getByRole("button")); // Alpha focused (index 0)
|
|
275
|
+
await user.keyboard("e"); // only Echo (index 4) matches — loop must reach it
|
|
276
|
+
expect(screen.getByRole("option", { name: "Echo", hidden: true })).toHaveFocus();
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe("Select ARIA / data-state wiring", () => {
|
|
281
|
+
it("wires the trigger, listbox, and options with the exact contract", async () => {
|
|
282
|
+
const user = userEvent.setup();
|
|
283
|
+
render(
|
|
284
|
+
<Select.Root defaultValue="vue">
|
|
285
|
+
<Select.Trigger>
|
|
286
|
+
<Select.Value placeholder="Pick" />
|
|
287
|
+
</Select.Trigger>
|
|
288
|
+
<Select.Content>
|
|
289
|
+
<Select.Item value="react">React</Select.Item>
|
|
290
|
+
<Select.Item value="vue">
|
|
291
|
+
Vue
|
|
292
|
+
<Select.ItemIndicator>✓</Select.ItemIndicator>
|
|
293
|
+
</Select.Item>
|
|
294
|
+
</Select.Content>
|
|
295
|
+
</Select.Root>,
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
const trigger = screen.getByRole("button");
|
|
299
|
+
expect(trigger).toHaveAttribute("type", "button");
|
|
300
|
+
expect(trigger).toHaveAttribute("aria-haspopup", "listbox");
|
|
301
|
+
|
|
302
|
+
await user.click(trigger);
|
|
303
|
+
|
|
304
|
+
const list = screen.getByRole("listbox", { hidden: true });
|
|
305
|
+
expect(list).toHaveAttribute("tabindex", "-1");
|
|
306
|
+
|
|
307
|
+
const react = screen.getByRole("option", { name: "React", hidden: true });
|
|
308
|
+
const vue = screen.getByRole("option", { name: /Vue/, hidden: true });
|
|
309
|
+
expect(react).toHaveAttribute("tabindex", "-1");
|
|
310
|
+
expect(react).toHaveAttribute("data-state", "unchecked");
|
|
311
|
+
expect(react).toHaveAttribute("aria-selected", "false");
|
|
312
|
+
expect(vue).toHaveAttribute("data-state", "checked");
|
|
313
|
+
expect(vue).toHaveAttribute("aria-selected", "true");
|
|
314
|
+
// The indicator only mounts on the selected item, as "checked".
|
|
315
|
+
expect(screen.getByText("✓")).toHaveAttribute("data-state", "checked");
|
|
316
|
+
});
|
|
317
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
|
|
3
|
+
import { Select } from "../Select";
|
|
4
|
+
|
|
5
|
+
describe("Select.Item in native mode", () => {
|
|
6
|
+
it("renders a native <option> for each item so its value is in the DOM", () => {
|
|
7
|
+
render(
|
|
8
|
+
<Select.Root native>
|
|
9
|
+
<Select.Item value="apple">Apple</Select.Item>
|
|
10
|
+
</Select.Root>,
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
expect(screen.getByRole("option", { name: "Apple" })).toBeInTheDocument();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("keeps only string/number children as the option text, dropping element children", () => {
|
|
17
|
+
render(
|
|
18
|
+
<Select.Root native defaultValue="apple">
|
|
19
|
+
<Select.Item value="apple">
|
|
20
|
+
<svg data-testid="leading-icon" />
|
|
21
|
+
Apple
|
|
22
|
+
</Select.Item>
|
|
23
|
+
</Select.Root>,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const option = screen.getByRole("option", { name: "Apple" });
|
|
27
|
+
expect(option).toHaveTextContent("Apple");
|
|
28
|
+
expect(screen.queryByTestId("leading-icon")).not.toBeInTheDocument();
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
|
|
4
|
+
import { Select } from "../Select";
|
|
5
|
+
|
|
6
|
+
function renderSelect() {
|
|
7
|
+
return render(
|
|
8
|
+
<Select.Root>
|
|
9
|
+
<Select.Trigger>
|
|
10
|
+
<Select.Value placeholder="Pick one" />
|
|
11
|
+
</Select.Trigger>
|
|
12
|
+
<Select.Content>
|
|
13
|
+
<Select.Item value="apple">Apple</Select.Item>
|
|
14
|
+
</Select.Content>
|
|
15
|
+
</Select.Root>,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("Select rich open/close", () => {
|
|
20
|
+
it("Trigger exposes the listbox ARIA contract and starts collapsed", () => {
|
|
21
|
+
renderSelect();
|
|
22
|
+
|
|
23
|
+
const trigger = screen.getByRole("button");
|
|
24
|
+
expect(trigger).toHaveAttribute("aria-haspopup", "listbox");
|
|
25
|
+
expect(trigger).toHaveAttribute("aria-expanded", "false");
|
|
26
|
+
expect(trigger).toHaveAttribute("aria-controls");
|
|
27
|
+
// Popover polyfill mirrors open state to `data-popover-open`.
|
|
28
|
+
expect(screen.getByRole("listbox", { hidden: true })).not.toHaveAttribute("data-popover-open");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("opens the listbox when the Trigger is clicked", async () => {
|
|
32
|
+
const user = userEvent.setup();
|
|
33
|
+
renderSelect();
|
|
34
|
+
|
|
35
|
+
await user.click(screen.getByRole("button"));
|
|
36
|
+
|
|
37
|
+
expect(screen.getByRole("button")).toHaveAttribute("aria-expanded", "true");
|
|
38
|
+
expect(screen.getByRole("listbox", { hidden: true })).toHaveAttribute("data-popover-open");
|
|
39
|
+
|
|
40
|
+
// A non-Escape key inside the open listbox is a no-op (typeahead / arrow
|
|
41
|
+
// navigation arrive in a later cycle) — the listbox stays open.
|
|
42
|
+
await user.keyboard("a");
|
|
43
|
+
expect(screen.getByRole("button")).toHaveAttribute("aria-expanded", "true");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("closes on Escape and returns focus to the Trigger", async () => {
|
|
47
|
+
const user = userEvent.setup();
|
|
48
|
+
renderSelect();
|
|
49
|
+
|
|
50
|
+
await user.click(screen.getByRole("button"));
|
|
51
|
+
await user.keyboard("{Escape}");
|
|
52
|
+
|
|
53
|
+
expect(screen.getByRole("button")).toHaveAttribute("aria-expanded", "false");
|
|
54
|
+
expect(screen.getByRole("listbox", { hidden: true })).not.toHaveAttribute("data-popover-open");
|
|
55
|
+
expect(screen.getByRole("button")).toHaveFocus();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -6,9 +6,9 @@ describe("Select placeholder", () => {
|
|
|
6
6
|
it("renders a non-selectable placeholder option that holds the initial selection until the user picks something", () => {
|
|
7
7
|
// Arrange & Act
|
|
8
8
|
const { container } = render(
|
|
9
|
-
<Select.Root>
|
|
10
|
-
<Select.Placeholder>Choose a fruit
|
|
11
|
-
<Select.
|
|
9
|
+
<Select.Root native>
|
|
10
|
+
<Select.Placeholder>Choose a fruit...</Select.Placeholder>
|
|
11
|
+
<Select.Item value="apple">Apple</Select.Item>
|
|
12
12
|
</Select.Root>,
|
|
13
13
|
);
|
|
14
14
|
|
|
@@ -22,9 +22,9 @@ describe("Select placeholder", () => {
|
|
|
22
22
|
expect(placeholder).not.toBeNull();
|
|
23
23
|
expect(placeholder).toBeDisabled();
|
|
24
24
|
expect(placeholder).toHaveAttribute("value", "");
|
|
25
|
-
expect(placeholder).toHaveTextContent("Choose a fruit
|
|
25
|
+
expect(placeholder).toHaveTextContent("Choose a fruit...");
|
|
26
26
|
|
|
27
|
-
//
|
|
27
|
+
// ...and is the initial selection because it's the first option with an
|
|
28
28
|
// empty value.
|
|
29
29
|
const select = screen.getByRole("combobox") as HTMLSelectElement;
|
|
30
30
|
expect(select.value).toBe("");
|
|
@@ -35,9 +35,9 @@ describe("Select placeholder", () => {
|
|
|
35
35
|
// whitespace or a conditional that resolved to a string). The placeholder
|
|
36
36
|
// scan must skip it rather than treat it as a candidate.
|
|
37
37
|
const { container } = render(
|
|
38
|
-
<Select.Root>
|
|
38
|
+
<Select.Root native>
|
|
39
39
|
{" "}
|
|
40
|
-
<Select.
|
|
40
|
+
<Select.Item value="apple">Apple</Select.Item>
|
|
41
41
|
</Select.Root>,
|
|
42
42
|
);
|
|
43
43
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
|
|
4
|
+
import { Select } from "../Select";
|
|
5
|
+
|
|
6
|
+
describe("Select rich asChild composition", () => {
|
|
7
|
+
it("composes Trigger props onto a custom element and still toggles open", async () => {
|
|
8
|
+
const user = userEvent.setup();
|
|
9
|
+
render(
|
|
10
|
+
<Select.Root>
|
|
11
|
+
<Select.Trigger asChild>
|
|
12
|
+
<a href="#pick" data-testid="custom-trigger">
|
|
13
|
+
<Select.Value placeholder="Pick one" />
|
|
14
|
+
</a>
|
|
15
|
+
</Select.Trigger>
|
|
16
|
+
<Select.Content asChild>
|
|
17
|
+
<section data-testid="custom-content">
|
|
18
|
+
<Select.Item value="apple">Apple</Select.Item>
|
|
19
|
+
</section>
|
|
20
|
+
</Select.Content>
|
|
21
|
+
</Select.Root>,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const trigger = screen.getByTestId("custom-trigger");
|
|
25
|
+
expect(trigger.tagName).toBe("A");
|
|
26
|
+
expect(trigger).toHaveAttribute("aria-haspopup", "listbox");
|
|
27
|
+
expect(trigger).toHaveAttribute("aria-expanded", "false");
|
|
28
|
+
|
|
29
|
+
const content = screen.getByRole("listbox", { hidden: true });
|
|
30
|
+
expect(content).toBe(screen.getByTestId("custom-content"));
|
|
31
|
+
|
|
32
|
+
await user.click(trigger);
|
|
33
|
+
|
|
34
|
+
expect(trigger).toHaveAttribute("aria-expanded", "true");
|
|
35
|
+
expect(content).toHaveAttribute("data-popover-open");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("delegates Separator to the child element via asChild", () => {
|
|
39
|
+
// Arrange & Act
|
|
40
|
+
render(
|
|
41
|
+
<Select.Root>
|
|
42
|
+
<Select.Trigger>
|
|
43
|
+
<Select.Value placeholder="Pick one" />
|
|
44
|
+
</Select.Trigger>
|
|
45
|
+
<Select.Content>
|
|
46
|
+
<Select.Item value="apple">Apple</Select.Item>
|
|
47
|
+
<Select.Separator asChild>
|
|
48
|
+
<hr data-testid="custom-sep" />
|
|
49
|
+
</Select.Separator>
|
|
50
|
+
<Select.Item value="banana">Banana</Select.Item>
|
|
51
|
+
</Select.Content>
|
|
52
|
+
</Select.Root>,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// Assert
|
|
56
|
+
const sep = screen.getByTestId("custom-sep");
|
|
57
|
+
expect(sep.tagName).toBe("HR");
|
|
58
|
+
expect(sep).toHaveAttribute("role", "separator");
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
|
|
3
|
+
import { Select } from "../Select";
|
|
4
|
+
|
|
5
|
+
describe("Select rich mode (default, native={false})", () => {
|
|
6
|
+
it("does not render a native <select> — it is a context boundary for the rich listbox", () => {
|
|
7
|
+
render(
|
|
8
|
+
<Select.Root>
|
|
9
|
+
<span data-testid="rich-child">rich content</span>
|
|
10
|
+
</Select.Root>,
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
expect(screen.queryByRole("combobox")).not.toBeInTheDocument();
|
|
14
|
+
expect(screen.getByTestId("rich-child")).toBeInTheDocument();
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { render, screen, within } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import { vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { Select } from "../Select";
|
|
6
|
+
import type { SelectRootProps } from "../types";
|
|
7
|
+
|
|
8
|
+
function renderSelect(root: SelectRootProps = {}) {
|
|
9
|
+
return render(
|
|
10
|
+
<Select.Root {...root}>
|
|
11
|
+
<Select.Trigger>
|
|
12
|
+
<Select.Value placeholder="Pick one" />
|
|
13
|
+
</Select.Trigger>
|
|
14
|
+
<Select.Content>
|
|
15
|
+
<Select.Item value="react">React</Select.Item>
|
|
16
|
+
<Select.Item value="vue">Vue</Select.Item>
|
|
17
|
+
<Select.Item value="svelte" disabled>
|
|
18
|
+
Svelte
|
|
19
|
+
</Select.Item>
|
|
20
|
+
</Select.Content>
|
|
21
|
+
</Select.Root>,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("Select rich selection", () => {
|
|
26
|
+
it("renders each Item as role=option, none selected initially", async () => {
|
|
27
|
+
const user = userEvent.setup();
|
|
28
|
+
renderSelect();
|
|
29
|
+
|
|
30
|
+
await user.click(screen.getByRole("button"));
|
|
31
|
+
|
|
32
|
+
// Scope to the listbox — the hidden form <select> also contains (nameless)
|
|
33
|
+
// <option>s that {hidden:true} would otherwise pick up.
|
|
34
|
+
const listbox = screen.getByRole("listbox", { hidden: true });
|
|
35
|
+
const options = within(listbox).getAllByRole("option", { hidden: true });
|
|
36
|
+
expect(options).toHaveLength(3);
|
|
37
|
+
options.forEach((o) => expect(o).toHaveAttribute("aria-selected", "false"));
|
|
38
|
+
expect(screen.getByText("Svelte")).toHaveAttribute("aria-disabled", "true");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("selecting an item fires onValueChange and closes, returning focus to the trigger", async () => {
|
|
42
|
+
const user = userEvent.setup();
|
|
43
|
+
const onValueChange = vi.fn();
|
|
44
|
+
renderSelect({ onValueChange });
|
|
45
|
+
|
|
46
|
+
await user.click(screen.getByRole("button"));
|
|
47
|
+
await user.click(screen.getByText("Vue"));
|
|
48
|
+
|
|
49
|
+
expect(onValueChange).toHaveBeenCalledWith("vue");
|
|
50
|
+
expect(screen.getByRole("button")).toHaveAttribute("aria-expanded", "false");
|
|
51
|
+
expect(screen.getByRole("button")).toHaveFocus();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("reflects the controlled value as aria-selected on the matching item", async () => {
|
|
55
|
+
const user = userEvent.setup();
|
|
56
|
+
renderSelect({ value: "vue", onValueChange: vi.fn() });
|
|
57
|
+
|
|
58
|
+
await user.click(screen.getByRole("button"));
|
|
59
|
+
|
|
60
|
+
// The selected label is mirrored into the trigger too, so scope to the
|
|
61
|
+
// listbox options rather than getByText.
|
|
62
|
+
expect(
|
|
63
|
+
screen.getByRole("option", { name: "Vue", hidden: true }),
|
|
64
|
+
).toHaveAttribute("aria-selected", "true");
|
|
65
|
+
expect(
|
|
66
|
+
screen.getByRole("option", { name: "React", hidden: true }),
|
|
67
|
+
).toHaveAttribute("aria-selected", "false");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("renders a rich Group as role=group with the label as its accessible name", async () => {
|
|
71
|
+
const user = userEvent.setup();
|
|
72
|
+
render(
|
|
73
|
+
<Select.Root>
|
|
74
|
+
<Select.Trigger>
|
|
75
|
+
<Select.Value placeholder="Pick one" />
|
|
76
|
+
</Select.Trigger>
|
|
77
|
+
<Select.Content>
|
|
78
|
+
<Select.Group label="Frontend">
|
|
79
|
+
<Select.Item value="react">React</Select.Item>
|
|
80
|
+
</Select.Group>
|
|
81
|
+
</Select.Content>
|
|
82
|
+
</Select.Root>,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
await user.click(screen.getByRole("button"));
|
|
86
|
+
|
|
87
|
+
expect(screen.getByRole("group", { hidden: true })).toHaveAttribute(
|
|
88
|
+
"aria-label",
|
|
89
|
+
"Frontend",
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("does not select a disabled item and keeps the listbox open", async () => {
|
|
94
|
+
const user = userEvent.setup();
|
|
95
|
+
const onValueChange = vi.fn();
|
|
96
|
+
renderSelect({ onValueChange });
|
|
97
|
+
|
|
98
|
+
await user.click(screen.getByRole("button"));
|
|
99
|
+
await user.click(screen.getByText("Svelte"));
|
|
100
|
+
|
|
101
|
+
expect(onValueChange).not.toHaveBeenCalled();
|
|
102
|
+
expect(screen.getByRole("button")).toHaveAttribute("aria-expanded", "true");
|
|
103
|
+
});
|
|
104
|
+
});
|