@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,87 @@
|
|
|
1
|
+
import { useCallback, useMemo } from "react";
|
|
2
|
+
|
|
3
|
+
import { useCollection, useControllableState } from "../../hooks/index.ts";
|
|
4
|
+
|
|
5
|
+
type UseSegmentedControlRootArgs = {
|
|
6
|
+
defaultValue?: string;
|
|
7
|
+
value?: string;
|
|
8
|
+
onValueChange?: (value: string) => void;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type ItemMeta = {
|
|
12
|
+
element: HTMLButtonElement;
|
|
13
|
+
disabled: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function useSegmentedControlRoot({
|
|
17
|
+
defaultValue,
|
|
18
|
+
value: controlledValue,
|
|
19
|
+
onValueChange,
|
|
20
|
+
}: UseSegmentedControlRootArgs) {
|
|
21
|
+
const [value, setValue] = useControllableState<string>(
|
|
22
|
+
controlledValue,
|
|
23
|
+
defaultValue,
|
|
24
|
+
onValueChange,
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const select = useCallback(
|
|
28
|
+
(next: string) => {
|
|
29
|
+
if (value === next) return;
|
|
30
|
+
setValue(next);
|
|
31
|
+
},
|
|
32
|
+
[value, setValue],
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
// Track registered item metadata in a ref (for focus handling) and their
|
|
36
|
+
// ordered values as state — required for the roving-tabindex home base.
|
|
37
|
+
// Disabled values are derived per render from itemsRef; any change that
|
|
38
|
+
// affects the set (mount, unmount, disabled toggle) re-runs the registrar
|
|
39
|
+
// effect, which updates the keys state and forces the recompute.
|
|
40
|
+
const {
|
|
41
|
+
register: registerItemBase,
|
|
42
|
+
itemsRef,
|
|
43
|
+
keys: itemValues,
|
|
44
|
+
} = useCollection<string, ItemMeta>();
|
|
45
|
+
|
|
46
|
+
const registerItem = useCallback(
|
|
47
|
+
(itemValue: string, element: HTMLButtonElement | null, disabled = false) => {
|
|
48
|
+
registerItemBase(itemValue, element ? { element, disabled } : null);
|
|
49
|
+
},
|
|
50
|
+
[registerItemBase],
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const disabledValues = useMemo(
|
|
54
|
+
() =>
|
|
55
|
+
new Set(
|
|
56
|
+
Array.from(itemsRef.current.entries())
|
|
57
|
+
.filter(([, meta]) => meta.disabled)
|
|
58
|
+
.map(([v]) => v),
|
|
59
|
+
),
|
|
60
|
+
// itemValues is a fresh array on every register call (new identity even
|
|
61
|
+
// when the keys are unchanged), so the memo re-runs whenever any item
|
|
62
|
+
// mounts, unmounts, or toggles disabled — exactly the trigger we want.
|
|
63
|
+
[itemValues, itemsRef],
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const focusItem = useCallback(
|
|
67
|
+
(itemValue: string) => {
|
|
68
|
+
// `itemValue` is always sourced from `navigable` (the enabled subset of
|
|
69
|
+
// `itemValues`), and useCollection's register() writes `itemsRef` and
|
|
70
|
+
// the `itemValues` state together — so a value that appears here is
|
|
71
|
+
// guaranteed to already have a live entry in the map.
|
|
72
|
+
// Stryker disable next-line OptionalChaining: unreachable given that invariant.
|
|
73
|
+
itemsRef.current.get(itemValue)?.element.focus();
|
|
74
|
+
},
|
|
75
|
+
// Stryker disable next-line ArrayDeclaration: equivalent — itemsRef is a stable RefObject.
|
|
76
|
+
[itemsRef],
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
value,
|
|
81
|
+
select,
|
|
82
|
+
registerItem,
|
|
83
|
+
itemValues,
|
|
84
|
+
disabledValues,
|
|
85
|
+
focusItem,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { ComponentProps, ReactNode, Ref } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Layout axis of the control. `"horizontal"` (default) binds the
|
|
5
|
+
* Arrow Left/Right pair; `"vertical"` binds Arrow Up/Down. Also
|
|
6
|
+
* reflected as `aria-orientation` and `data-orientation` on the root.
|
|
7
|
+
*/
|
|
8
|
+
export type SegmentedControlOrientation = "horizontal" | "vertical";
|
|
9
|
+
|
|
10
|
+
/** Reading direction — swaps the horizontal arrow pair when `"rtl"`. */
|
|
11
|
+
export type SegmentedControlReadingDirection = "ltr" | "rtl";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Shared base for both {@link SegmentedControlRootProps} variants — the
|
|
15
|
+
* native `<div>` attributes (minus the component-owned `role` and the
|
|
16
|
+
* narrowed `dir`) plus the `asChild` escape hatch, orientation, reading
|
|
17
|
+
* direction, group-level `disabled`, and a typed `ref`.
|
|
18
|
+
*/
|
|
19
|
+
export type SegmentedControlRootBaseProps = Omit<
|
|
20
|
+
ComponentProps<"div">,
|
|
21
|
+
"role" | "dir" | "defaultValue"
|
|
22
|
+
> & {
|
|
23
|
+
/** The control's segments — {@link SegmentedControlItemProps | `SegmentedControl.Item`}
|
|
24
|
+
* elements. */
|
|
25
|
+
children?: ReactNode;
|
|
26
|
+
/** Forwarded to the underlying `HTMLDivElement`. */
|
|
27
|
+
ref?: Ref<HTMLDivElement>;
|
|
28
|
+
/**
|
|
29
|
+
* When `true`, Root delegates rendering to a single consumer-supplied
|
|
30
|
+
* element via the {@link Slot} pattern instead of the default `<div>`;
|
|
31
|
+
* `role="radiogroup"`, `aria-orientation`, `data-orientation`,
|
|
32
|
+
* `data-disabled`, `dir`, and remaining props are merged onto it.
|
|
33
|
+
* @default false
|
|
34
|
+
*/
|
|
35
|
+
asChild?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Which arrow-key pair moves focus and selection through the segments,
|
|
38
|
+
* and the visual axis. `"horizontal"` binds Arrow Left/Right;
|
|
39
|
+
* `"vertical"` binds Arrow Up/Down. Reflected as `aria-orientation` and
|
|
40
|
+
* `data-orientation` on the root.
|
|
41
|
+
* @default "horizontal"
|
|
42
|
+
*/
|
|
43
|
+
orientation?: SegmentedControlOrientation;
|
|
44
|
+
/**
|
|
45
|
+
* Reading direction. In `"rtl"` the horizontal arrow pair is swapped so
|
|
46
|
+
* Arrow Left moves forward. When omitted, it is inherited from the
|
|
47
|
+
* nearest {@link DirectionProvider}, falling back to `"ltr"`.
|
|
48
|
+
*/
|
|
49
|
+
dir?: SegmentedControlReadingDirection;
|
|
50
|
+
/**
|
|
51
|
+
* Disables the entire control — every segment is non-interactive and
|
|
52
|
+
* excluded from arrow-key navigation, and `data-disabled=""` is set on
|
|
53
|
+
* the root. Individual segments can also be disabled via the Item's own
|
|
54
|
+
* `disabled` prop.
|
|
55
|
+
* @default false
|
|
56
|
+
*/
|
|
57
|
+
disabled?: boolean;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Uncontrolled variant of {@link SegmentedControlRootProps}: the component
|
|
62
|
+
* owns the selected value. Pass `defaultValue` (or omit it); `onValueChange`
|
|
63
|
+
* is optional and `value` is forbidden.
|
|
64
|
+
*/
|
|
65
|
+
export type SegmentedControlRootUncontrolledProps =
|
|
66
|
+
SegmentedControlRootBaseProps & {
|
|
67
|
+
/** Value of the segment selected on first render. Omit for nothing
|
|
68
|
+
* selected on mount. */
|
|
69
|
+
defaultValue?: string;
|
|
70
|
+
/** Forbidden in uncontrolled mode — use `defaultValue` instead. */
|
|
71
|
+
value?: never;
|
|
72
|
+
/** Called with the new value whenever the selection changes. Optional
|
|
73
|
+
* in uncontrolled mode. */
|
|
74
|
+
onValueChange?: (value: string) => void;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Controlled variant of {@link SegmentedControlRootProps}: the parent owns
|
|
79
|
+
* the selected value. Pass `value` and `onValueChange` together;
|
|
80
|
+
* `defaultValue` is forbidden.
|
|
81
|
+
*/
|
|
82
|
+
export type SegmentedControlRootControlledProps =
|
|
83
|
+
SegmentedControlRootBaseProps & {
|
|
84
|
+
/** Forbidden in controlled mode — use `value` instead. */
|
|
85
|
+
defaultValue?: never;
|
|
86
|
+
/** The currently selected value. Must be kept in sync by the parent
|
|
87
|
+
* via `onValueChange`. */
|
|
88
|
+
value: string;
|
|
89
|
+
/** Called with the new value whenever the selection changes. Required
|
|
90
|
+
* in controlled mode. */
|
|
91
|
+
onValueChange: (value: string) => void;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Props for {@link SegmentedControlRoot | `SegmentedControl.Root`}. A
|
|
96
|
+
* discriminated union of {@link SegmentedControlRootUncontrolledProps} and
|
|
97
|
+
* {@link SegmentedControlRootControlledProps}, so TypeScript accepts exactly
|
|
98
|
+
* one state mode.
|
|
99
|
+
*/
|
|
100
|
+
export type SegmentedControlRootProps =
|
|
101
|
+
| SegmentedControlRootUncontrolledProps
|
|
102
|
+
| SegmentedControlRootControlledProps;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Props for {@link SegmentedControlItem | `SegmentedControl.Item`} — the
|
|
106
|
+
* segment button. `value` identifies the option; all native `<button>`
|
|
107
|
+
* attributes (minus the component-owned `type`, `role`, `aria-checked`, and
|
|
108
|
+
* `value`) plus the `asChild` escape hatch and a typed `ref` pass through.
|
|
109
|
+
*/
|
|
110
|
+
export type SegmentedControlItemProps = Omit<
|
|
111
|
+
ComponentProps<"button">,
|
|
112
|
+
"type" | "role" | "aria-checked" | "value"
|
|
113
|
+
> & {
|
|
114
|
+
/** Uniquely identifies this segment within the control and is reported to
|
|
115
|
+
* `onValueChange` / matched against Root's `value` when selected. Required. */
|
|
116
|
+
value: string;
|
|
117
|
+
/** The segment's visible content — typically its text label. */
|
|
118
|
+
children?: ReactNode;
|
|
119
|
+
/** Forwarded to the underlying `HTMLButtonElement`. */
|
|
120
|
+
ref?: Ref<HTMLButtonElement>;
|
|
121
|
+
/**
|
|
122
|
+
* Disables just this segment: forwards the native `disabled` attribute,
|
|
123
|
+
* removes it from arrow-key navigation and the roving-tabindex home base,
|
|
124
|
+
* and sets `data-disabled=""`. The whole control can also be disabled via
|
|
125
|
+
* Root's `disabled` prop.
|
|
126
|
+
* @default false
|
|
127
|
+
*/
|
|
128
|
+
disabled?: boolean;
|
|
129
|
+
/**
|
|
130
|
+
* When `true`, renders the single child element via the {@link Slot}
|
|
131
|
+
* pattern instead of the default `<button>`; ARIA, `data-state`,
|
|
132
|
+
* `data-disabled`, `tabIndex`, `onClick`, `onKeyDown`, `disabled`, and
|
|
133
|
+
* `ref` are merged onto it. A non-focusable child must be made focusable
|
|
134
|
+
* by the consumer.
|
|
135
|
+
* @default false
|
|
136
|
+
*/
|
|
137
|
+
asChild?: boolean;
|
|
138
|
+
};
|
package/src/Select/README.md
CHANGED
|
@@ -1,203 +1,168 @@
|
|
|
1
1
|
# Select
|
|
2
2
|
|
|
3
|
-
Headless **Select** — a compound
|
|
4
|
-
|
|
3
|
+
Headless **Select** — a single-select compound with two render paths chosen
|
|
4
|
+
by the `native` prop:
|
|
5
|
+
|
|
6
|
+
- **Rich** (`native={false}`, the default) — a fully-styleable Popover-API
|
|
7
|
+
listbox (`Select.Trigger` / `Select.Value` / `Select.Content` /
|
|
8
|
+
`Select.Item` / `Select.ItemIndicator`). Items carry arbitrary content
|
|
9
|
+
(icons, badges, indicators).
|
|
10
|
+
- **Native** (`native={true}`) — a thin wrapper over a real `<select>` /
|
|
11
|
+
`<option>` / `<optgroup>` for flat, OS-native cases (mobile wheel pickers,
|
|
12
|
+
maximum-compatibility forms).
|
|
13
|
+
|
|
14
|
+
Both modes share the same `value` / `onValueChange` / `disabled` / `name`
|
|
15
|
+
(form) API. Zero styles ship.
|
|
5
16
|
|
|
6
17
|
```tsx
|
|
7
18
|
import { Select } from "@primitiv-ui/react";
|
|
8
19
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
<Select.
|
|
12
|
-
|
|
20
|
+
// Rich (default)
|
|
21
|
+
<Select.Root value={framework} onValueChange={setFramework}>
|
|
22
|
+
<Select.Trigger>
|
|
23
|
+
<Select.Value placeholder="Pick a framework..." />
|
|
24
|
+
</Select.Trigger>
|
|
25
|
+
<Select.Content>
|
|
26
|
+
<Select.Item value="react">
|
|
27
|
+
<ReactIcon />
|
|
28
|
+
React
|
|
29
|
+
<Select.ItemIndicator>✓</Select.ItemIndicator>
|
|
30
|
+
</Select.Item>
|
|
31
|
+
<Select.Item value="vue">
|
|
32
|
+
<VueIcon />
|
|
33
|
+
Vue
|
|
34
|
+
<Select.ItemIndicator>✓</Select.ItemIndicator>
|
|
35
|
+
</Select.Item>
|
|
36
|
+
</Select.Content>
|
|
13
37
|
</Select.Root>;
|
|
14
38
|
```
|
|
15
39
|
|
|
16
|
-
Because the underlying element is the real `<select>`, the browser owns
|
|
17
|
-
the popup, the keyboard interaction (arrow keys, Home/End, typeahead),
|
|
18
|
-
the mobile UX (iOS/Android wheel pickers), and form submission. No
|
|
19
|
-
positioning JS, no Portal, no anchor positioning.
|
|
20
|
-
|
|
21
40
|
## Sub-components
|
|
22
41
|
|
|
23
|
-
| Export
|
|
24
|
-
|
|
|
25
|
-
| `Select.Root`
|
|
26
|
-
| `Select.
|
|
27
|
-
| `Select.
|
|
28
|
-
| `Select.
|
|
42
|
+
| Export | Rich element | Native element | Notes |
|
|
43
|
+
| ---------------------- | ------------------------ | -------------- | ---------------------------------------------- |
|
|
44
|
+
| `Select.Root` | context boundary + hidden `<select>` | `<select>` | owns value + open state |
|
|
45
|
+
| `Select.Trigger` | `<button>` | — | `aria-haspopup="listbox"`, `aria-expanded`, `aria-controls`; `asChild` |
|
|
46
|
+
| `Select.Value` | `<span>` | — | mirrors the selected item's content |
|
|
47
|
+
| `Select.Content` | `<div role="listbox">` (`popover="auto"`) | — | keyboard nav; `asChild` |
|
|
48
|
+
| `Select.Item` | `<div role="option">` | `<option>` | `value` (required), `disabled` |
|
|
49
|
+
| `Select.ItemIndicator` | `<span>` | — | selected-only; `forceMount`, `asChild` |
|
|
50
|
+
| `Select.Group` | `<div role="group">` | `<optgroup>` | `label` (required string) |
|
|
51
|
+
| `Select.Separator` | `<div role="separator">` | — | rich-only divider; skipped by keyboard nav; `asChild` |
|
|
52
|
+
| `Select.Placeholder` | — | `<option value="" hidden disabled>` | native-only initial hint |
|
|
29
53
|
|
|
30
|
-
## State
|
|
54
|
+
## State
|
|
31
55
|
|
|
32
|
-
|
|
56
|
+
**Selection** — controlled (`value` + `onValueChange`) or uncontrolled
|
|
57
|
+
(`defaultValue`, or omit for none), discriminated at the type level so only
|
|
58
|
+
one shape is accepted. `onValueChange` receives the new value as a string.
|
|
33
59
|
|
|
34
|
-
|
|
35
|
-
`
|
|
60
|
+
**Open** (rich only) — controlled (`open` + `onOpenChange`) or uncontrolled
|
|
61
|
+
(`defaultOpen`). The trigger toggles it; selection, Escape, and light-dismiss
|
|
62
|
+
close it.
|
|
36
63
|
|
|
37
|
-
|
|
38
|
-
<Select.Root defaultValue="banana" aria-label="Pick a fruit">
|
|
39
|
-
<Select.Option value="apple">Apple</Select.Option>
|
|
40
|
-
<Select.Option value="banana">Banana</Select.Option>
|
|
41
|
-
</Select.Root>
|
|
42
|
-
```
|
|
64
|
+
## Rich mode
|
|
43
65
|
|
|
44
|
-
###
|
|
66
|
+
### Value mirroring
|
|
45
67
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
68
|
+
`Select.Value` **automatically mirrors the selected `Select.Item`'s
|
|
69
|
+
children** into the trigger — write the icon + label once, on the item, and
|
|
70
|
+
it shows up in the closed trigger. `Select.ItemIndicator` is **excluded** from
|
|
71
|
+
the mirror (the checkmark answers "which row is selected" — redundant on the
|
|
72
|
+
trigger it already represents). `placeholder` shows when nothing is selected.
|
|
49
73
|
|
|
50
|
-
|
|
51
|
-
|
|
74
|
+
The exclusion works **through your own wrappers**: the mirror doesn't test the
|
|
75
|
+
element's type (a wrapper component would be opaque to that, and a styled layer
|
|
76
|
+
always wraps), it publishes its intent through context and each indicator opts
|
|
77
|
+
itself out — at any nesting depth, `forceMount` included. Every other child
|
|
78
|
+
carries through untouched, so a trailing badge on the selected item shows up in
|
|
79
|
+
the trigger too; style it there if it needs different spacing from the row.
|
|
52
80
|
|
|
53
|
-
|
|
54
|
-
<Select.Option value="apple">Apple</Select.Option>
|
|
55
|
-
<Select.Option value="banana">Banana</Select.Option>
|
|
56
|
-
</Select.Root>;
|
|
57
|
-
```
|
|
81
|
+
### Item indicator
|
|
58
82
|
|
|
59
|
-
`
|
|
60
|
-
|
|
61
|
-
it
|
|
83
|
+
`Select.ItemIndicator`, nested in a `Select.Item`, renders **only when that
|
|
84
|
+
item is selected** and exposes `data-state="checked" | "unchecked"`. Pass
|
|
85
|
+
`forceMount` to keep it in the DOM (as `unchecked`) for CSS enter/exit
|
|
86
|
+
animation.
|
|
62
87
|
|
|
63
|
-
|
|
88
|
+
### Keyboard
|
|
64
89
|
|
|
65
|
-
|
|
66
|
-
initial selection. The underlying option is rendered with `value=""`,
|
|
67
|
-
`disabled`, and `hidden`, so it shows in the closed Select's display
|
|
68
|
-
but is unreachable from the dropdown after the user picks something.
|
|
90
|
+
While the listbox is open:
|
|
69
91
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
92
|
+
| Key | Behaviour |
|
|
93
|
+
| ----------------------- | -------------------------------------------------- |
|
|
94
|
+
| `ArrowDown` / `ArrowUp` | Move focus to next / previous option (wraps) |
|
|
95
|
+
| `Home` / `End` | First / last option |
|
|
96
|
+
| `Enter` / `Space` | Select the focused option and close |
|
|
97
|
+
| `Escape` | Close and return focus to the trigger |
|
|
98
|
+
| printable character | Typeahead — focus the next option matching prefix |
|
|
77
99
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
placeholder — not the first selectable option — is the initial
|
|
81
|
-
selection. Pair with `required` on Root to make the browser's native
|
|
82
|
-
form validation catch an unchosen value at submit time.
|
|
100
|
+
Disabled options are skipped by arrows and typeahead. On open, focus moves to
|
|
101
|
+
the selected option (or the first enabled one).
|
|
83
102
|
|
|
84
|
-
|
|
85
|
-
`asChild` + Placeholder combination requires the consumer to set
|
|
86
|
-
`defaultValue=""` explicitly.
|
|
103
|
+
### Popup placement
|
|
87
104
|
|
|
88
|
-
|
|
105
|
+
`Select.Content` uses the native Popover API (`popover="auto"`) for the top
|
|
106
|
+
layer and light-dismiss — no Portal, no positioning JS. Consumers place the
|
|
107
|
+
popup themselves via CSS (the component ships no anchor positioning).
|
|
89
108
|
|
|
90
|
-
|
|
91
|
-
|
|
109
|
+
## Native mode
|
|
110
|
+
|
|
111
|
+
Pass `native` to render a real `<select>`. `Select.Item` becomes an
|
|
112
|
+
`<option>` — keeping **only its string/number children** as the option text
|
|
113
|
+
and **dropping element children** (icons, indicators don't render). An
|
|
114
|
+
icon-only item with no text renders an empty, unlabelled `<option>`.
|
|
92
115
|
|
|
93
116
|
```tsx
|
|
94
|
-
<Select.Root aria-label="Pick a
|
|
117
|
+
<Select.Root native defaultValue="apple" aria-label="Pick a fruit">
|
|
118
|
+
<Select.Placeholder>Choose a fruit...</Select.Placeholder>
|
|
95
119
|
<Select.Group label="Fruits">
|
|
96
|
-
<Select.
|
|
97
|
-
<Select.
|
|
98
|
-
</Select.Group>
|
|
99
|
-
<Select.Group label="Vegetables">
|
|
100
|
-
<Select.Option value="carrot">Carrot</Select.Option>
|
|
120
|
+
<Select.Item value="apple">Apple</Select.Item>
|
|
121
|
+
<Select.Item value="banana">Banana</Select.Item>
|
|
101
122
|
</Select.Group>
|
|
102
123
|
</Select.Root>
|
|
103
124
|
```
|
|
104
125
|
|
|
105
|
-
|
|
106
|
-
|
|
126
|
+
`Select.Placeholder` (native-only) renders `<option value="" disabled hidden>`.
|
|
127
|
+
When present with no `value`/`defaultValue`, Root infers `defaultValue=""` so
|
|
128
|
+
the placeholder is the initial selection; pair with `required` for native form
|
|
129
|
+
validation. `Select.Group` renders `<optgroup label>`.
|
|
107
130
|
|
|
108
|
-
##
|
|
131
|
+
## Forms
|
|
109
132
|
|
|
110
|
-
Pass `
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
```tsx
|
|
116
|
-
<Select.Root disabled aria-label="Pick a fruit">
|
|
117
|
-
<Select.Option value="apple">Apple</Select.Option>
|
|
118
|
-
</Select.Root>
|
|
119
|
-
|
|
120
|
-
<Select.Root aria-label="Pick a fruit">
|
|
121
|
-
<Select.Option value="apple">Apple</Select.Option>
|
|
122
|
-
<Select.Option value="durian" disabled>Durian (sold out)</Select.Option>
|
|
123
|
-
</Select.Root>
|
|
124
|
-
```
|
|
133
|
+
Pass `name` and place the Select in a `<form>`. In native mode the `<select>`
|
|
134
|
+
submits directly; in rich mode Root renders a visually-hidden native
|
|
135
|
+
`<select name>` that carries the value, so submission works identically. Pair
|
|
136
|
+
with `required` for browser validation.
|
|
125
137
|
|
|
126
138
|
## Field integration
|
|
127
139
|
|
|
128
|
-
|
|
129
|
-
`
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
`descriptionId`, then `errorId` when invalid)
|
|
134
|
-
- `aria-invalid` (`"true"` when the field is invalid)
|
|
135
|
-
- `disabled`
|
|
136
|
-
- `required`
|
|
140
|
+
Inside a [`<Field.Root>`](../Field/README.md), `Select.Root` reads
|
|
141
|
+
`FieldContext` and inherits `id`, `aria-describedby`, `aria-invalid`,
|
|
142
|
+
`disabled`, and `required` (consumer props win). In native mode these apply to
|
|
143
|
+
the `<select>`; in rich mode `disabled` / `required` / `name` flow to the
|
|
144
|
+
hidden form `<select>`.
|
|
137
145
|
|
|
138
|
-
|
|
139
|
-
`Select.Root` to override any field-derived one. Outside a `<Field.Root>`,
|
|
140
|
-
behaviour is unchanged.
|
|
146
|
+
## `asChild`
|
|
141
147
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
<Select.Root {...register("fruit")}>
|
|
146
|
-
<Select.Placeholder>Choose a fruit…</Select.Placeholder>
|
|
147
|
-
<Select.Option value="apple">Apple</Select.Option>
|
|
148
|
-
<Select.Option value="banana">Banana</Select.Option>
|
|
149
|
-
</Select.Root>
|
|
150
|
-
<Field.ErrorText>{errors.fruit?.message}</Field.ErrorText>
|
|
151
|
-
</Field.Root>
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
## Form integration
|
|
155
|
-
|
|
156
|
-
Native `<select>` is a form-associated element. Pass `name` and
|
|
157
|
-
`required`, place the Select inside a `<form>`, and submission carries
|
|
158
|
-
the selected value with no extra wiring.
|
|
159
|
-
|
|
160
|
-
```tsx
|
|
161
|
-
<form onSubmit={…}>
|
|
162
|
-
<label>
|
|
163
|
-
Fruit
|
|
164
|
-
<Select.Root name="fruit" required>
|
|
165
|
-
<Select.Placeholder>Choose a fruit…</Select.Placeholder>
|
|
166
|
-
<Select.Option value="apple">Apple</Select.Option>
|
|
167
|
-
</Select.Root>
|
|
168
|
-
</label>
|
|
169
|
-
<button type="submit">Submit</button>
|
|
170
|
-
</form>
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
## `asChild` composition
|
|
174
|
-
|
|
175
|
-
Root accepts `asChild`. The consumer supplies a single element that
|
|
176
|
-
renders a `<select>` (typically a styled wrapper). Root's `onChange`,
|
|
177
|
-
`data-disabled`, `value` / `defaultValue`, and other native attributes
|
|
178
|
-
are merged onto it.
|
|
179
|
-
|
|
180
|
-
```tsx
|
|
181
|
-
function StyledSelect(props: ComponentProps<"select">) {
|
|
182
|
-
return <select {...props} className="ds-select" />;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
<Select.Root asChild value={fruit} onValueChange={setFruit}>
|
|
186
|
-
<StyledSelect>
|
|
187
|
-
<Select.Option value="apple">Apple</Select.Option>
|
|
188
|
-
</StyledSelect>
|
|
189
|
-
</Select.Root>
|
|
190
|
-
```
|
|
148
|
+
- `Select.Root` (native mode) — delegate to a styled `<select>` wrapper.
|
|
149
|
+
- `Select.Trigger` / `Select.Content` / `Select.ItemIndicator` (rich mode) —
|
|
150
|
+
compose the merged props onto a consumer element.
|
|
191
151
|
|
|
192
152
|
## Styling hooks
|
|
193
153
|
|
|
194
|
-
| Attribute | Values
|
|
195
|
-
| --------------- |
|
|
196
|
-
| `
|
|
154
|
+
| Attribute | Values | Set on |
|
|
155
|
+
| --------------- | ------------------------------- | ---------------------- |
|
|
156
|
+
| `aria-expanded` | `"true"` / `"false"` | `Select.Trigger` |
|
|
157
|
+
| `aria-selected` | `"true"` / `"false"` | rich `Select.Item` |
|
|
158
|
+
| `data-state` | `"checked"` / `"unchecked"` | rich `Select.Item`, `Select.ItemIndicator` |
|
|
159
|
+
| `data-disabled` | `""` (present when disabled) | rich `Select.Item`, native `Select.Root` |
|
|
160
|
+
| `data-placeholder` | `""` (present while nothing is selected) | `Select.Value` |
|
|
161
|
+
|
|
162
|
+
`data-placeholder` is what a styled layer keys the muted placeholder colour
|
|
163
|
+
off — it disappears the moment an item's content is mirrored into the trigger.
|
|
197
164
|
|
|
198
|
-
##
|
|
165
|
+
## Future work
|
|
199
166
|
|
|
200
|
-
|
|
201
|
-
(
|
|
202
|
-
a Combobox with filtering are planned — see [Future
|
|
203
|
-
work](../../../../docs/select-future-work.md).
|
|
167
|
+
A Combobox with filtering is planned — see
|
|
168
|
+
[Future work](../../../../docs/select-future-work.md).
|