@primitiv-ui/react 0.1.27 → 0.1.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/package.json +6 -1
- package/src/AccessibleIcon/AccessibleIcon.tsx +32 -9
- package/src/AccessibleIcon/__tests__/AccessibleIcon.test.tsx +5 -0
- package/src/AccessibleIcon/types.ts +15 -2
- package/src/Accordion/Accordion.tsx +25 -4
- package/src/Accordion/__tests__/Accordion.asChild.test.tsx +46 -0
- package/src/Accordion/__tests__/Accordion.basic-rendering.test.tsx +15 -0
- package/src/Accordion/__tests__/Accordion.disabled-items.test.tsx +42 -0
- package/src/Accordion/__tests__/Accordion.error-handling.test.tsx +33 -0
- package/src/Accordion/hooks/useAccordionRoot.ts +44 -7
- package/src/Accordion/hooks/useAccordionTrigger.ts +9 -1
- package/src/Accordion/types.ts +72 -21
- package/src/Alert/Alert.tsx +36 -7
- package/src/Alert/__tests__/Alert.test.tsx +5 -0
- package/src/Alert/types.ts +18 -2
- package/src/Avatar/Avatar.tsx +99 -15
- package/src/Avatar/__tests__/Avatar.basic-rendering.test.tsx +10 -0
- package/src/Avatar/__tests__/Avatar.fallback.test.tsx +30 -0
- package/src/Avatar/__tests__/Avatar.image-loading.test.tsx +35 -0
- package/src/Avatar/hooks/useAvatarImage.ts +16 -0
- package/src/Avatar/types.ts +15 -3
- package/src/Breadcrumb/Breadcrumb.tsx +136 -18
- package/src/Breadcrumb/README.md +47 -0
- package/src/Breadcrumb/__tests__/Breadcrumb.basic-rendering.test.tsx +11 -0
- package/src/Breadcrumb/__tests__/Breadcrumb.ellipsis.test.tsx +110 -0
- package/src/Breadcrumb/types.ts +73 -7
- package/src/Button/Button.tsx +2 -0
- package/src/Button/__tests__/Button.basic-rendering.test.tsx +6 -0
- package/src/Button/types.ts +21 -0
- package/src/Carousel/Carousel.tsx +263 -15
- package/src/Carousel/README.md +749 -72
- package/src/Carousel/__tests__/Carousel.autoplay-status.test.tsx +239 -0
- package/src/Carousel/__tests__/Carousel.drag-status.test.tsx +292 -0
- package/src/Carousel/__tests__/Carousel.dynamic-slides.test.tsx +38 -0
- package/src/Carousel/__tests__/Carousel.indicators.test.tsx +101 -0
- package/src/Carousel/__tests__/Carousel.infinite.test.tsx +1453 -0
- package/src/Carousel/__tests__/Carousel.instant-scroll.test.tsx +124 -0
- package/src/Carousel/__tests__/Carousel.intersection-observer.test.tsx +88 -0
- package/src/Carousel/__tests__/Carousel.loop.test.tsx +163 -0
- package/src/Carousel/__tests__/Carousel.loopEngine.test.ts +77 -0
- package/src/Carousel/__tests__/Carousel.mouse-drag.test.tsx +504 -0
- package/src/Carousel/__tests__/Carousel.multi-slide-bounds.test.tsx +91 -0
- package/src/Carousel/__tests__/Carousel.orientation.test.tsx +207 -0
- package/src/Carousel/__tests__/Carousel.overscroll.test.tsx +532 -0
- package/src/Carousel/__tests__/Carousel.page-snap-points.test.tsx +50 -0
- package/src/Carousel/__tests__/Carousel.progress-text.test.tsx +76 -0
- package/src/Carousel/__tests__/Carousel.reduced-motion.test.tsx +3 -6
- package/src/Carousel/__tests__/Carousel.refresh-progress.test.tsx +2 -5
- package/src/Carousel/__tests__/Carousel.scroll-margin.test.tsx +167 -0
- package/src/Carousel/__tests__/Carousel.scroll-progress.test.tsx +518 -0
- package/src/Carousel/__tests__/Carousel.scroll-snap-change.test.tsx +32 -11
- package/src/Carousel/__tests__/Carousel.scroll-sync.test.tsx +237 -24
- package/src/Carousel/__tests__/Carousel.scroll-to-index.test.tsx +115 -0
- package/src/Carousel/__tests__/Carousel.slide-snap.test.tsx +172 -0
- package/src/Carousel/__tests__/Carousel.slides-per-move.test.tsx +179 -1
- package/src/Carousel/__tests__/Carousel.slides-per-page.test.tsx +91 -7
- package/src/Carousel/__tests__/Carousel.snap-type.test.tsx +35 -0
- package/src/Carousel/__tests__/Carousel.transition-modes.test.tsx +30 -5
- package/src/Carousel/__tests__/Carousel.translations.test.tsx +23 -0
- package/src/Carousel/__tests__/Carousel.wheel.test.tsx +160 -0
- package/src/Carousel/hooks/index.ts +1 -0
- package/src/Carousel/hooks/useCarouselLoop.ts +869 -0
- package/src/Carousel/hooks/useCarouselRoot.ts +338 -42
- package/src/Carousel/hooks/useCarouselSlide.ts +44 -9
- package/src/Carousel/hooks/useCarouselViewport.ts +649 -46
- package/src/Carousel/loopEngine.ts +54 -0
- package/src/Carousel/types.ts +457 -35
- package/src/Checkbox/Checkbox.tsx +9 -1
- package/src/Checkbox/__tests__/Checkbox.basic-rendering.test.tsx +14 -0
- package/src/Checkbox/hooks/useCheckboxInput.ts +4 -0
- package/src/Checkbox/types.ts +19 -0
- package/src/CheckboxCard/CheckboxCard.tsx +13 -4
- package/src/CheckboxCard/__tests__/CheckboxCard.basic-rendering.test.tsx +7 -0
- package/src/CheckboxCard/types.ts +65 -17
- package/src/Collapsible/Collapsible.tsx +49 -3
- package/src/Collapsible/README.md +40 -0
- package/src/Collapsible/__tests__/Collapsible.asChild.test.tsx +89 -0
- package/src/Collapsible/__tests__/Collapsible.basic-rendering.test.tsx +27 -0
- package/src/Collapsible/__tests__/Collapsible.collapsedHeight.test.tsx +140 -0
- package/src/Collapsible/hooks/useCollapsibleTrigger.ts +3 -1
- package/src/Collapsible/types.ts +89 -22
- package/src/ContextMenu/ContextMenu.tsx +270 -4
- package/src/ContextMenu/types.ts +148 -6
- package/src/DirectionProvider/DirectionProvider.tsx +41 -9
- package/src/DirectionProvider/__tests__/DirectionProvider.test.tsx +5 -0
- package/src/DirectionProvider/types.ts +28 -3
- package/src/Divider/Divider.tsx +24 -10
- package/src/Divider/__tests__/Divider.test.tsx +5 -0
- package/src/Divider/types.ts +10 -3
- package/src/Drawer/Drawer.tsx +315 -0
- package/src/Drawer/README.md +124 -0
- package/src/Drawer/__tests__/Drawer.behavior.test.tsx +100 -0
- package/src/Drawer/__tests__/Drawer.focus-trap.test.tsx +55 -0
- package/src/Drawer/__tests__/Drawer.rendering.test.tsx +111 -0
- package/src/Drawer/__tests__/Drawer.side.test.tsx +69 -0
- package/src/Drawer/__tests__/dialog-polyfill.ts +39 -0
- package/src/Drawer/index.ts +2 -0
- package/src/Drawer/types.ts +66 -0
- package/src/Dropdown/Dropdown.tsx +117 -0
- package/src/Dropdown/types.ts +241 -11
- package/src/EmptyState/EmptyState.tsx +74 -4
- package/src/EmptyState/__tests__/EmptyState.Actions.test.tsx +7 -2
- package/src/EmptyState/__tests__/EmptyState.Description.test.tsx +3 -1
- package/src/EmptyState/__tests__/EmptyState.Root.test.tsx +10 -0
- package/src/EmptyState/__tests__/EmptyState.Title.test.tsx +3 -1
- package/src/EmptyState/types.ts +49 -6
- package/src/Field/Field.tsx +23 -0
- package/src/Field/__tests__/Field.basic-rendering.test.tsx +11 -0
- package/src/Field/__tests__/Field.error-handling.test.tsx +23 -0
- package/src/Field/__tests__/Field.state-cascade.test.tsx +21 -0
- package/src/Field/hooks/__tests__/useFieldProps.test.tsx +104 -9
- package/src/Field/types.ts +60 -12
- package/src/Fieldset/Fieldset.tsx +54 -18
- package/src/Fieldset/__tests__/Fieldset.basic-rendering.test.tsx +9 -0
- package/src/Fieldset/types.ts +20 -2
- package/src/Input/Input.tsx +14 -9
- package/src/Input/__tests__/Input.basic-rendering.test.tsx +5 -0
- package/src/Input/types.ts +19 -2
- package/src/InputGroup/InputGroup.tsx +11 -3
- package/src/InputGroup/__tests__/InputGroup.basic-rendering.test.tsx +14 -0
- package/src/InputGroup/types.ts +26 -5
- package/src/MillerColumns/MillerColumns.tsx +86 -19
- package/src/MillerColumns/types.ts +83 -27
- package/src/Modal/Modal.tsx +31 -4
- package/src/Modal/README.md +19 -10
- package/src/Modal/__tests__/Modal.focus-trap.test.tsx +162 -0
- package/src/Modal/hooks/useModalContent.ts +32 -0
- package/src/Modal/types.ts +110 -29
- package/src/NavigationMenu/NavigationMenu.tsx +890 -0
- package/src/NavigationMenu/NavigationMenuContext.ts +67 -0
- package/src/NavigationMenu/README.md +355 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.anchor-positioning.test.tsx +132 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.asChild.test.tsx +73 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.basic-rendering.test.tsx +92 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.contract.test.tsx +45 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.controlled-state.test.tsx +134 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.error-handling.test.tsx +62 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.fixtures.tsx +79 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.forceMount.test.tsx +52 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.hover-interaction.test.tsx +254 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.indicator.test.tsx +252 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-interaction.test.tsx +294 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-navigation.test.tsx +126 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.link.test.tsx +139 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.motion.test.tsx +568 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.mouse-interaction.test.tsx +76 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.uncontrolled-state.test.tsx +30 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.viewport.test.tsx +113 -0
- package/src/NavigationMenu/constants.ts +5 -0
- package/src/NavigationMenu/hooks/index.ts +7 -0
- package/src/NavigationMenu/hooks/useNavigationMenuEntry.ts +33 -0
- package/src/NavigationMenu/hooks/useNavigationMenuIndicator.ts +70 -0
- package/src/NavigationMenu/hooks/useNavigationMenuLink.ts +72 -0
- package/src/NavigationMenu/hooks/useNavigationMenuRoot.ts +301 -0
- package/src/NavigationMenu/hooks/useNavigationMenuTopLevelEntry.ts +65 -0
- package/src/NavigationMenu/hooks/useNavigationMenuTrigger.ts +158 -0
- package/src/NavigationMenu/hooks/useNavigationMenuViewport.ts +101 -0
- package/src/NavigationMenu/index.ts +3 -0
- package/src/NavigationMenu/types.ts +259 -0
- package/src/NavigationMenu/utils.ts +89 -0
- package/src/Popover/Popover.tsx +427 -0
- package/src/Popover/PopoverContext.ts +13 -0
- package/src/Popover/README.md +106 -0
- package/src/Popover/__tests__/Popover.anchor.test.tsx +38 -0
- package/src/Popover/__tests__/Popover.asChild.test.tsx +160 -0
- package/src/Popover/__tests__/Popover.basic-rendering.test.tsx +106 -0
- package/src/Popover/__tests__/Popover.click-outside.test.tsx +149 -0
- package/src/Popover/__tests__/Popover.close.test.tsx +70 -0
- package/src/Popover/__tests__/Popover.content.test.tsx +58 -0
- package/src/Popover/__tests__/Popover.controlled-state.test.tsx +45 -0
- package/src/Popover/__tests__/Popover.error-handling.test.tsx +51 -0
- package/src/Popover/__tests__/Popover.focus.test.tsx +43 -0
- package/src/Popover/__tests__/Popover.keyboard-interaction.test.tsx +71 -0
- package/src/Popover/__tests__/Popover.mouse-interaction.test.tsx +69 -0
- package/src/Popover/__tests__/Popover.uncontrolled-state.test.tsx +44 -0
- package/src/Popover/hooks/index.ts +3 -0
- package/src/Popover/hooks/usePopoverContent.ts +98 -0
- package/src/Popover/hooks/usePopoverRoot.ts +84 -0
- package/src/Popover/hooks/usePopoverTrigger.ts +33 -0
- package/src/Popover/index.ts +2 -0
- package/src/Popover/types.ts +197 -0
- package/src/Portal/Portal.tsx +50 -10
- package/src/Portal/__tests__/Portal.basic-rendering.test.tsx +5 -0
- package/src/Portal/types.ts +5 -0
- package/src/Progress/Progress.tsx +33 -1
- package/src/Progress/__tests__/Progress.basic-rendering.test.tsx +10 -0
- package/src/Progress/__tests__/Progress.value.test.tsx +21 -0
- package/src/Progress/types.ts +20 -4
- package/src/Radio/Radio.tsx +9 -1
- package/src/Radio/__tests__/Radio.basic-rendering.test.tsx +14 -0
- package/src/Radio/hooks/useRadioRoot.ts +3 -0
- package/src/Radio/types.ts +20 -0
- package/src/RadioCard/RadioCard.tsx +11 -0
- package/src/RadioCard/__tests__/RadioCard.basic-rendering.test.tsx +8 -0
- package/src/RadioCard/__tests__/RadioCard.disabled-items.test.tsx +27 -0
- package/src/RadioCard/__tests__/RadioCard.keyboard-interaction.test.tsx +53 -0
- package/src/RadioCard/__tests__/RadioCard.ref-forwarding.test.tsx +54 -0
- package/src/RadioCard/__tests__/RadioCard.uncontrolled-state.test.tsx +21 -0
- package/src/RadioCard/hooks/useRadioCardRoot.ts +11 -0
- package/src/RadioCard/types.ts +95 -22
- package/src/RadioGroup/RadioGroup.tsx +11 -0
- package/src/RadioGroup/__tests__/RadioGroup.basic-rendering.test.tsx +8 -0
- package/src/RadioGroup/__tests__/RadioGroup.disabled.test.tsx +27 -0
- package/src/RadioGroup/__tests__/RadioGroup.ref-forwarding.test.tsx +29 -0
- package/src/RadioGroup/__tests__/RadioGroup.roving-tabindex.test.tsx +31 -0
- package/src/RadioGroup/hooks/useRadioGroupRoot.ts +11 -0
- package/src/RadioGroup/types.ts +73 -9
- package/src/SegmentedControl/README.md +154 -0
- package/src/SegmentedControl/SegmentedControl.tsx +309 -0
- package/src/SegmentedControl/SegmentedControlContext.ts +28 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.asChild.test.tsx +50 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.basic-rendering.test.tsx +100 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.controlled-state.test.tsx +107 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.disabled-keydown-guards.test.tsx +62 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.disabled.test.tsx +151 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.error-handling.test.tsx +17 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.keyboard-interaction.test.tsx +117 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.orientation.test.tsx +54 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.reading-direction.test.tsx +56 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.ref-forwarding.test.tsx +45 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.roving-tabindex.test.tsx +104 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.uncontrolled-state.test.tsx +94 -0
- package/src/SegmentedControl/hooks/index.ts +2 -0
- package/src/SegmentedControl/hooks/useSegmentedControlContext.ts +1 -0
- package/src/SegmentedControl/hooks/useSegmentedControlRoot.ts +86 -0
- package/src/SegmentedControl/index.ts +2 -0
- package/src/SegmentedControl/types.ts +138 -0
- package/src/Select/README.md +122 -157
- package/src/Select/Select.tsx +595 -68
- package/src/Select/SelectContext.ts +38 -0
- package/src/Select/SelectItemIndicatorContext.ts +29 -0
- package/src/Select/__tests__/Select.asChild.test.tsx +2 -2
- package/src/Select/__tests__/Select.basic-rendering.test.tsx +22 -3
- package/src/Select/__tests__/Select.controlled-state.test.tsx +6 -6
- package/src/Select/__tests__/Select.data-attributes.test.tsx +4 -4
- package/src/Select/__tests__/Select.field-integration.test.tsx +11 -11
- package/src/Select/__tests__/Select.form.test.tsx +38 -0
- package/src/Select/__tests__/Select.group.test.tsx +6 -6
- package/src/Select/__tests__/Select.keyboard-edge.test.tsx +95 -0
- package/src/Select/__tests__/Select.keyboard.test.tsx +124 -0
- package/src/Select/__tests__/Select.mutation-hardening.test.tsx +317 -0
- package/src/Select/__tests__/Select.native-item.test.tsx +30 -0
- package/src/Select/__tests__/Select.open-close.test.tsx +57 -0
- package/src/Select/__tests__/Select.placeholder.test.tsx +4 -4
- package/src/Select/__tests__/Select.rich-asChild.test.tsx +60 -0
- package/src/Select/__tests__/Select.rich-mode.test.tsx +16 -0
- package/src/Select/__tests__/Select.selection.test.tsx +104 -0
- package/src/Select/__tests__/Select.separator.test.tsx +71 -0
- package/src/Select/__tests__/Select.strict-context.test.tsx +36 -0
- package/src/Select/__tests__/Select.typeahead.test.tsx +156 -0
- package/src/Select/__tests__/Select.value-mirror.test.tsx +240 -0
- package/src/Select/constants.ts +8 -0
- package/src/Select/hooks/index.ts +2 -0
- package/src/Select/hooks/useSelectContent.ts +149 -0
- package/src/Select/hooks/useSelectRoot.ts +149 -0
- package/src/Select/types.ts +199 -31
- package/src/SkipNav/SkipNav.tsx +69 -29
- package/src/SkipNav/__tests__/SkipNav.basic-rendering.test.tsx +6 -0
- package/src/SkipNav/types.ts +17 -9
- package/src/Slider/Slider.tsx +33 -1
- package/src/Slider/SliderContext.ts +28 -1
- package/src/Slider/types.ts +157 -9
- package/src/Slot/Slot.tsx +94 -19
- package/src/Slot/__tests__/Slot.test.tsx +110 -0
- package/src/Slot/composeEventHandlers.ts +26 -2
- package/src/Slot/types.ts +16 -0
- package/src/Status/Status.tsx +36 -15
- package/src/Status/__tests__/Status.test.tsx +5 -0
- package/src/Status/types.ts +12 -1
- package/src/Switch/Switch.tsx +15 -2
- package/src/Switch/__tests__/Switch.basic-rendering.test.tsx +7 -0
- package/src/Switch/hooks/useSwitchRoot.ts +3 -0
- package/src/Switch/types.ts +49 -22
- package/src/Table/Table.tsx +44 -10
- package/src/Table/types.ts +66 -17
- package/src/Tabs/Tabs.tsx +8 -0
- package/src/Tabs/types.ts +29 -4
- package/src/Textarea/Textarea.tsx +9 -1
- package/src/Textarea/__tests__/Textarea.basic-rendering.test.tsx +5 -0
- package/src/Textarea/types.ts +20 -3
- package/src/Toggle/Toggle.tsx +51 -13
- package/src/Toggle/__tests__/Toggle.basic-rendering.test.tsx +5 -0
- package/src/Toggle/types.ts +54 -4
- package/src/ToggleGroup/ToggleGroup.tsx +14 -1
- package/src/ToggleGroup/__tests__/ToggleGroup.asChild.test.tsx +20 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.basic-rendering.test.tsx +9 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.dynamic-items.test.tsx +70 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.error-handling.test.tsx +23 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.keyboard-interaction.test.tsx +23 -1
- package/src/ToggleGroup/__tests__/ToggleGroup.ref-forwarding.test.tsx +58 -0
- package/src/ToggleGroup/hooks/useToggleGroupRoot.ts +21 -1
- package/src/ToggleGroup/types.ts +55 -2
- package/src/Tooltip/Tooltip.tsx +98 -20
- package/src/Tooltip/hooks/__tests__/useTooltipProvider.test.tsx +40 -0
- package/src/Tooltip/hooks/useTooltipProvider.ts +13 -1
- package/src/Tooltip/types.ts +89 -8
- package/src/Tree/Tree.tsx +187 -33
- package/src/Tree/types.ts +54 -12
- package/src/VisuallyHidden/VisuallyHidden.tsx +33 -8
- package/src/VisuallyHidden/__tests__/VisuallyHidden.test.tsx +15 -1
- package/src/VisuallyHidden/types.ts +11 -1
- package/src/index.ts +4 -0
- package/src/test/intersectionObserverPolyfill.ts +7 -0
- package/src/test/resizeObserverPolyfill.ts +80 -0
- package/src/utils/focusable.ts +15 -0
- package/src/utils/index.ts +1 -0
package/src/Select/Select.tsx
CHANGED
|
@@ -1,20 +1,58 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
ChangeEvent,
|
|
3
|
+
Children,
|
|
4
|
+
isValidElement,
|
|
5
|
+
ReactNode,
|
|
6
|
+
useContext,
|
|
7
|
+
useEffect,
|
|
8
|
+
useMemo,
|
|
9
|
+
} from "react";
|
|
2
10
|
import type { ReactElement } from "react";
|
|
3
11
|
|
|
4
12
|
import { useFieldProps } from "../Field/hooks/index.ts";
|
|
5
|
-
import { Slot } from "../Slot/index.ts";
|
|
13
|
+
import { composeEventHandlers, Slot } from "../Slot/index.ts";
|
|
6
14
|
|
|
15
|
+
import { SelectContext, useSelectContext } from "./SelectContext";
|
|
16
|
+
import {
|
|
17
|
+
SelectItemIndicatorContext,
|
|
18
|
+
useSelectItemIndicatorContext,
|
|
19
|
+
} from "./SelectItemIndicatorContext";
|
|
20
|
+
import { useSelectContent, useSelectRoot } from "./hooks/index.ts";
|
|
7
21
|
import {
|
|
22
|
+
SelectContentProps,
|
|
8
23
|
SelectGroupProps,
|
|
9
|
-
|
|
24
|
+
SelectItemIndicatorProps,
|
|
25
|
+
SelectItemProps,
|
|
10
26
|
SelectPlaceholderProps,
|
|
11
27
|
SelectRootProps,
|
|
28
|
+
SelectSeparatorProps,
|
|
29
|
+
SelectTriggerProps,
|
|
30
|
+
SelectValueProps,
|
|
12
31
|
} from "./types";
|
|
13
32
|
|
|
33
|
+
const ITEM_INDICATOR_DISPLAY_NAME = "SelectItemIndicator";
|
|
34
|
+
|
|
35
|
+
/** Stable context for {@link SelectValue}'s mirrored copy — see `mirrored`. */
|
|
36
|
+
const MIRRORED_CONTEXT = { selected: false, mirrored: true } as const;
|
|
37
|
+
|
|
38
|
+
/** Off-screen but still submitted — the rich mode's hidden form `<select>`. */
|
|
39
|
+
const VISUALLY_HIDDEN = {
|
|
40
|
+
position: "absolute",
|
|
41
|
+
width: 1,
|
|
42
|
+
height: 1,
|
|
43
|
+
padding: 0,
|
|
44
|
+
margin: -1,
|
|
45
|
+
overflow: "hidden",
|
|
46
|
+
clip: "rect(0, 0, 0, 0)",
|
|
47
|
+
whiteSpace: "nowrap",
|
|
48
|
+
border: 0,
|
|
49
|
+
} as const;
|
|
50
|
+
|
|
14
51
|
const PLACEHOLDER_DISPLAY_NAME = "SelectPlaceholder";
|
|
15
52
|
|
|
16
53
|
function hasPlaceholderChild(children: ReactNode): boolean {
|
|
17
54
|
return Children.toArray(children).some((child) => {
|
|
55
|
+
// Stryker disable next-line BooleanLiteral: equivalent — a non-element child is never a placeholder; returning true would infer defaultValue="", but with no matching <option value=""> the browser still selects the first option, so the rendered selection is unchanged.
|
|
18
56
|
if (!isValidElement(child)) return false;
|
|
19
57
|
const type = child.type as { displayName?: string };
|
|
20
58
|
return type.displayName === PLACEHOLDER_DISPLAY_NAME;
|
|
@@ -22,46 +60,137 @@ function hasPlaceholderChild(children: ReactNode): boolean {
|
|
|
22
60
|
}
|
|
23
61
|
|
|
24
62
|
/**
|
|
25
|
-
* The root of a Select —
|
|
26
|
-
*
|
|
63
|
+
* The root of a Select — owns the selection (and, in rich mode, the open)
|
|
64
|
+
* state and provides it to the sub-components.
|
|
27
65
|
*
|
|
28
|
-
*
|
|
29
|
-
* Home/End, typeahead), the platform popup, mobile UX, and form
|
|
30
|
-
* submission all work without additional JS.
|
|
66
|
+
* **Two render paths, one API** — chosen by {@link SelectRootBaseProps.native | `native`}:
|
|
31
67
|
*
|
|
32
|
-
*
|
|
68
|
+
* - `native={false}` (**default**) — the rich Popover-API listbox, composed
|
|
69
|
+
* from {@link SelectTrigger}, {@link SelectValue}, {@link SelectContent},
|
|
70
|
+
* {@link SelectItem} and {@link SelectItemIndicator}. Items carry arbitrary
|
|
71
|
+
* content; a visually-hidden `<select name>` is rendered for form
|
|
72
|
+
* submission.
|
|
73
|
+
* - `native={true}` — a thin wrapper over a real `<select>` (implicit
|
|
74
|
+
* `role="combobox"`); the browser owns the popup, keyboard, mobile UX and
|
|
75
|
+
* form submission. {@link SelectItem} becomes an `<option>` (string/number
|
|
76
|
+
* children only), {@link SelectGroup} an `<optgroup>`.
|
|
33
77
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* transition defers back through `onValueChange`.
|
|
78
|
+
* **Controlled vs uncontrolled.** Two selection modes are statically
|
|
79
|
+
* discriminated at the type level so only one shape is accepted by
|
|
80
|
+
* TypeScript at a time:
|
|
38
81
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
82
|
+
* - **Uncontrolled** — pass {@link SelectRootUncontrolledProps.defaultValue | `defaultValue`}
|
|
83
|
+
* (or omit it). The browser owns the selection. `onValueChange` is optional.
|
|
84
|
+
* - **Controlled** — pass {@link SelectRootControlledProps.value | `value`}
|
|
85
|
+
* and {@link SelectRootControlledProps.onValueChange | `onValueChange`} together.
|
|
86
|
+
* Every transition defers back through `onValueChange`, which receives the
|
|
87
|
+
* new selection as a plain string. The consumer's own `onChange` (the raw
|
|
88
|
+
* `ChangeEvent`) still fires alongside it.
|
|
42
89
|
*
|
|
43
|
-
* **Placeholder integration.** When a {@link Select.Placeholder}
|
|
44
|
-
* among the direct children and neither `value` nor `defaultValue`
|
|
45
|
-
* set, Root infers `defaultValue=""` so the placeholder — not the first
|
|
46
|
-
* selectable option — is the initial selection.
|
|
90
|
+
* **Placeholder integration.** When a {@link SelectPlaceholder | `Select.Placeholder`}
|
|
91
|
+
* appears among the direct children and neither `value` nor `defaultValue`
|
|
92
|
+
* is set, Root infers `defaultValue=""` so the placeholder — not the first
|
|
93
|
+
* selectable option — is the initial selection. Pair with `required` to
|
|
94
|
+
* make the browser's native form validation catch an unchosen value at
|
|
95
|
+
* submission.
|
|
47
96
|
*
|
|
48
97
|
* **Field integration.** When rendered inside a `<Field.Root>`, Select
|
|
49
98
|
* opts into `FieldContext` and inherits `id`, `aria-describedby`,
|
|
50
|
-
* `aria-invalid`, `disabled`, and `required` from the field. Any prop
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
99
|
+
* `aria-invalid`, `disabled`, and `required` from the field. Any prop the
|
|
100
|
+
* consumer passes wins; `aria-describedby` is composed (consumer ids first,
|
|
101
|
+
* then field-supplied description / error ids). Outside a `<Field.Root>`,
|
|
102
|
+
* behaviour is unchanged.
|
|
103
|
+
*
|
|
104
|
+
* **`asChild` composition.** Pass `asChild` to delegate rendering to a
|
|
105
|
+
* single consumer-supplied element (e.g. a styled `<select>` wrapper).
|
|
106
|
+
* Root's `onChange`, `data-disabled`, `value` / `defaultValue`, and other
|
|
107
|
+
* native attributes are merged onto the child via the {@link Slot} pattern.
|
|
108
|
+
* Placeholder-detection walks direct children only in this mode, so
|
|
109
|
+
* `asChild` + placeholder requires the consumer to set `defaultValue=""`
|
|
110
|
+
* explicitly.
|
|
111
|
+
*
|
|
112
|
+
* **Styling hooks.**
|
|
113
|
+
* - `data-disabled=""` — present on the `<select>` when `disabled` is set,
|
|
114
|
+
* so CSS can target `[data-disabled]` without relying on `:disabled`.
|
|
115
|
+
*
|
|
116
|
+
* Root renders no single semantic element of its own in the default (rich)
|
|
117
|
+
* path — it is a context boundary that also emits a visually-hidden form
|
|
118
|
+
* `<select>`. Under `native` it renders the real `<select>`.
|
|
119
|
+
*
|
|
120
|
+
* @example Rich (default)
|
|
121
|
+
* ```tsx
|
|
122
|
+
* <Select.Root value={framework} onValueChange={setFramework}>
|
|
123
|
+
* <Select.Trigger>
|
|
124
|
+
* <Select.Value placeholder="Pick a framework…" />
|
|
125
|
+
* </Select.Trigger>
|
|
126
|
+
* <Select.Content>
|
|
127
|
+
* <Select.Item value="react">
|
|
128
|
+
* <ReactIcon />
|
|
129
|
+
* React
|
|
130
|
+
* <Select.ItemIndicator>✓</Select.ItemIndicator>
|
|
131
|
+
* </Select.Item>
|
|
132
|
+
* <Select.Item value="vue">
|
|
133
|
+
* <VueIcon />
|
|
134
|
+
* Vue
|
|
135
|
+
* <Select.ItemIndicator>✓</Select.ItemIndicator>
|
|
136
|
+
* </Select.Item>
|
|
137
|
+
* </Select.Content>
|
|
138
|
+
* </Select.Root>
|
|
139
|
+
* ```
|
|
140
|
+
*
|
|
141
|
+
* @example Native
|
|
142
|
+
* ```tsx
|
|
143
|
+
* <Select.Root native defaultValue="apple" aria-label="Pick a fruit">
|
|
144
|
+
* <Select.Placeholder>Choose…</Select.Placeholder>
|
|
145
|
+
* <Select.Group label="Fruits">
|
|
146
|
+
* <Select.Item value="apple">Apple</Select.Item>
|
|
147
|
+
* <Select.Item value="banana">Banana</Select.Item>
|
|
148
|
+
* </Select.Group>
|
|
149
|
+
* </Select.Root>
|
|
150
|
+
* ```
|
|
151
|
+
*
|
|
152
|
+
* @example Field integration
|
|
153
|
+
* ```tsx
|
|
154
|
+
* <Field.Root invalid={!!errors.framework}>
|
|
155
|
+
* <Field.Label>Framework</Field.Label>
|
|
156
|
+
* <Select.Root name="framework" value={value} onValueChange={onChange}>
|
|
157
|
+
* <Select.Trigger>
|
|
158
|
+
* <Select.Value placeholder="Choose…" />
|
|
159
|
+
* </Select.Trigger>
|
|
160
|
+
* <Select.Content>
|
|
161
|
+
* <Select.Item value="react">React</Select.Item>
|
|
162
|
+
* </Select.Content>
|
|
163
|
+
* </Select.Root>
|
|
164
|
+
* <Field.ErrorText>{errors.framework?.message}</Field.ErrorText>
|
|
165
|
+
* </Field.Root>
|
|
166
|
+
* ```
|
|
54
167
|
*/
|
|
55
168
|
export function SelectRoot({
|
|
56
169
|
children,
|
|
170
|
+
native = false,
|
|
57
171
|
asChild = false,
|
|
58
172
|
onChange,
|
|
59
173
|
onValueChange,
|
|
60
174
|
value,
|
|
61
175
|
defaultValue,
|
|
176
|
+
defaultOpen,
|
|
177
|
+
open,
|
|
178
|
+
onOpenChange,
|
|
62
179
|
...consumer
|
|
63
180
|
}: SelectRootProps): ReactElement {
|
|
64
181
|
const merged = useFieldProps(consumer);
|
|
182
|
+
const {
|
|
183
|
+
contextValue,
|
|
184
|
+
value: selectedValue,
|
|
185
|
+
itemValues,
|
|
186
|
+
} = useSelectRoot({
|
|
187
|
+
defaultOpen,
|
|
188
|
+
open,
|
|
189
|
+
onOpenChange,
|
|
190
|
+
value,
|
|
191
|
+
defaultValue,
|
|
192
|
+
onValueChange,
|
|
193
|
+
});
|
|
65
194
|
|
|
66
195
|
const handleChange = (event: ChangeEvent<HTMLSelectElement>) => {
|
|
67
196
|
onChange?.(event);
|
|
@@ -90,6 +219,37 @@ export function SelectRoot({
|
|
|
90
219
|
onChange: handleChange,
|
|
91
220
|
};
|
|
92
221
|
|
|
222
|
+
// Rich (non-native) render path — the fully-styleable Popover listbox.
|
|
223
|
+
// Root provides open/selection state to Trigger / Content / Value via
|
|
224
|
+
// context, and renders a visually-hidden native <select> so the selection
|
|
225
|
+
// still submits through the browser (mirroring what `native` gets free).
|
|
226
|
+
// The hidden select is uncontrolled + remounted by `key` on each change,
|
|
227
|
+
// so it needs no onChange and stays warning-free.
|
|
228
|
+
if (!native) {
|
|
229
|
+
return (
|
|
230
|
+
<SelectContext.Provider value={contextValue}>
|
|
231
|
+
{children}
|
|
232
|
+
<select
|
|
233
|
+
key={selectedValue}
|
|
234
|
+
name={merged.name}
|
|
235
|
+
defaultValue={selectedValue}
|
|
236
|
+
required={merged.required}
|
|
237
|
+
disabled={merged.disabled}
|
|
238
|
+
tabIndex={-1}
|
|
239
|
+
aria-hidden
|
|
240
|
+
style={VISUALLY_HIDDEN}
|
|
241
|
+
>
|
|
242
|
+
<option value="" />
|
|
243
|
+
{itemValues
|
|
244
|
+
.filter((itemValue) => itemValue !== "")
|
|
245
|
+
.map((itemValue) => (
|
|
246
|
+
<option key={itemValue} value={itemValue} />
|
|
247
|
+
))}
|
|
248
|
+
</select>
|
|
249
|
+
</SelectContext.Provider>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
93
253
|
if (asChild) {
|
|
94
254
|
return <Slot {...rootProps}>{children}</Slot>;
|
|
95
255
|
}
|
|
@@ -100,46 +260,390 @@ export function SelectRoot({
|
|
|
100
260
|
SelectRoot.displayName = "SelectRoot";
|
|
101
261
|
|
|
102
262
|
/**
|
|
103
|
-
* An individual choice inside a Select
|
|
104
|
-
*
|
|
263
|
+
* An individual choice inside a Select.
|
|
264
|
+
*
|
|
265
|
+
* In `native` mode (the case handled in this cycle) it renders a native
|
|
266
|
+
* `<option>` element with an implicit `role="option"`. Because an
|
|
267
|
+
* `<option>` can only hold text, `Select.Item` keeps **only the
|
|
268
|
+
* string/number parts** of its children — joined as the option's visible
|
|
269
|
+
* text — and **drops every element child** (icons, indicators). This is
|
|
270
|
+
* the inverse of the string-vs-element split used elsewhere in the
|
|
271
|
+
* library, and it means an icon-only item with no text renders an empty,
|
|
272
|
+
* unlabelled `<option>` under `native`.
|
|
273
|
+
*
|
|
274
|
+
* Pass `disabled` to make a single choice unreachable from the dropdown
|
|
275
|
+
* while still visible.
|
|
105
276
|
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
277
|
+
* @extends HTMLOptionElement
|
|
278
|
+
*
|
|
279
|
+
* @example Native
|
|
280
|
+
* ```tsx
|
|
281
|
+
* <Select.Root native>
|
|
282
|
+
* <Select.Item value="apple">Apple</Select.Item>
|
|
283
|
+
* <Select.Item value="durian" disabled>Durian (sold out)</Select.Item>
|
|
284
|
+
* </Select.Root>
|
|
285
|
+
* ```
|
|
108
286
|
*/
|
|
109
|
-
export function
|
|
287
|
+
export function SelectItem({
|
|
110
288
|
children,
|
|
289
|
+
value,
|
|
290
|
+
disabled,
|
|
291
|
+
onClick,
|
|
111
292
|
...rest
|
|
112
|
-
}:
|
|
113
|
-
|
|
293
|
+
}: SelectItemProps): ReactElement {
|
|
294
|
+
const ctx = useContext(SelectContext);
|
|
295
|
+
const registerItem = ctx?.registerItem;
|
|
296
|
+
const unregisterItem = ctx?.unregisterItem;
|
|
297
|
+
const selected = ctx?.value === value;
|
|
298
|
+
|
|
299
|
+
// Keep the registered content current every render (silent — no bump).
|
|
300
|
+
useEffect(() => {
|
|
301
|
+
registerItem?.(value, children);
|
|
302
|
+
});
|
|
303
|
+
// Drop the registration when the item unmounts or changes value.
|
|
304
|
+
useEffect(() => () => unregisterItem?.(value), [unregisterItem, value]);
|
|
305
|
+
|
|
306
|
+
const indicatorContext = useMemo(
|
|
307
|
+
() => ({ selected, mirrored: false }),
|
|
308
|
+
[selected],
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
// Native mode — no rich context is provided, so render an <option> whose
|
|
312
|
+
// text is only the string/number children (element children are dropped).
|
|
313
|
+
if (!ctx) {
|
|
314
|
+
const text = Children.toArray(children).filter(
|
|
315
|
+
(child): child is string | number =>
|
|
316
|
+
typeof child === "string" || typeof child === "number",
|
|
317
|
+
);
|
|
318
|
+
return (
|
|
319
|
+
<option value={value} disabled={disabled} onClick={onClick} {...rest}>
|
|
320
|
+
{text}
|
|
321
|
+
</option>
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Rich mode — a listbox option that can carry arbitrary content. Clicking
|
|
326
|
+
// it (when enabled) commits the selection and closes the listbox.
|
|
327
|
+
const handleClick = () => {
|
|
328
|
+
if (disabled) return;
|
|
329
|
+
ctx.select(value);
|
|
330
|
+
};
|
|
331
|
+
const itemProps = {
|
|
332
|
+
...rest,
|
|
333
|
+
role: "option" as const,
|
|
334
|
+
tabIndex: -1,
|
|
335
|
+
"aria-selected": selected,
|
|
336
|
+
"aria-disabled": disabled || undefined,
|
|
337
|
+
"data-disabled": disabled ? "" : undefined,
|
|
338
|
+
"data-state": selected ? "checked" : "unchecked",
|
|
339
|
+
onClick: composeEventHandlers(onClick, handleClick),
|
|
340
|
+
};
|
|
341
|
+
return (
|
|
342
|
+
<SelectItemIndicatorContext.Provider value={indicatorContext}>
|
|
343
|
+
<div {...itemProps}>{children}</div>
|
|
344
|
+
</SelectItemIndicatorContext.Provider>
|
|
345
|
+
);
|
|
114
346
|
}
|
|
115
347
|
|
|
116
348
|
/** @internal */
|
|
117
|
-
|
|
349
|
+
SelectItem.displayName = "SelectItem";
|
|
118
350
|
|
|
119
351
|
/**
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
352
|
+
* The rich-mode trigger button that opens the listbox. Exposes the
|
|
353
|
+
* WAI-ARIA listbox-combobox contract: `aria-haspopup="listbox"`,
|
|
354
|
+
* `aria-expanded` reflecting the open state, and `aria-controls` pointing
|
|
355
|
+
* at the {@link SelectContent | `Select.Content`} id. Place a
|
|
356
|
+
* {@link SelectValue | `Select.Value`} inside it to show the current
|
|
357
|
+
* selection.
|
|
358
|
+
*
|
|
359
|
+
* Renders a `<button type="button">` by default; pass `asChild` to compose
|
|
360
|
+
* the ARIA attributes and click handler onto a consumer element via the
|
|
361
|
+
* {@link Slot} pattern.
|
|
362
|
+
*
|
|
363
|
+
* @extends HTMLButtonElement
|
|
364
|
+
*
|
|
365
|
+
* @example
|
|
366
|
+
* ```tsx
|
|
367
|
+
* <Select.Trigger>
|
|
368
|
+
* <Select.Value placeholder="Pick a framework…" />
|
|
369
|
+
* </Select.Trigger>
|
|
370
|
+
* ```
|
|
371
|
+
*/
|
|
372
|
+
export function SelectTrigger({
|
|
373
|
+
children,
|
|
374
|
+
onClick,
|
|
375
|
+
asChild = false,
|
|
376
|
+
...rest
|
|
377
|
+
}: SelectTriggerProps): ReactElement {
|
|
378
|
+
const { open, setOpen, contentId, triggerId, triggerRef } = useSelectContext();
|
|
379
|
+
|
|
380
|
+
const triggerProps = {
|
|
381
|
+
...rest,
|
|
382
|
+
ref: triggerRef,
|
|
383
|
+
id: triggerId,
|
|
384
|
+
type: "button" as const,
|
|
385
|
+
"aria-haspopup": "listbox" as const,
|
|
386
|
+
"aria-expanded": open,
|
|
387
|
+
"aria-controls": contentId,
|
|
388
|
+
onClick: composeEventHandlers(onClick, () => setOpen(!open)),
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
if (asChild) {
|
|
392
|
+
return <Slot {...triggerProps}>{children}</Slot>;
|
|
393
|
+
}
|
|
394
|
+
return <button {...triggerProps}>{children}</button>;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** @internal */
|
|
398
|
+
SelectTrigger.displayName = "SelectTrigger";
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* The rich-mode display of the current selection, placed inside a
|
|
402
|
+
* {@link SelectTrigger | `Select.Trigger`}. Shows the `placeholder` when
|
|
403
|
+
* nothing is selected; mirroring the selected item's content is layered on
|
|
404
|
+
* in a later cycle.
|
|
405
|
+
*
|
|
406
|
+
* Renders a `<span>`.
|
|
407
|
+
*
|
|
408
|
+
* **Styling hooks.**
|
|
409
|
+
* - `data-placeholder=""` — present while nothing is selected (the
|
|
410
|
+
* `placeholder` is what's rendered), absent as soon as an item's content is
|
|
411
|
+
* mirrored. This is the hook the styled layer uses to render the muted
|
|
412
|
+
* placeholder colour versus the filled one.
|
|
413
|
+
*
|
|
414
|
+
* @extends HTMLSpanElement
|
|
415
|
+
*
|
|
416
|
+
* @example
|
|
417
|
+
* ```tsx
|
|
418
|
+
* <Select.Value placeholder="Select a framework…" />
|
|
419
|
+
* ```
|
|
420
|
+
*/
|
|
421
|
+
export function SelectValue({
|
|
422
|
+
placeholder,
|
|
423
|
+
...rest
|
|
424
|
+
}: SelectValueProps): ReactElement {
|
|
425
|
+
const { value, getItemChildren } = useSelectContext();
|
|
426
|
+
const selectedChildren = value ? getItemChildren(value) : undefined;
|
|
427
|
+
|
|
428
|
+
// Mirror the selected item's content — icons, badges, text all carry through.
|
|
429
|
+
// Falls back to the placeholder when nothing is selected (or the selected
|
|
430
|
+
// value has no registered item yet).
|
|
431
|
+
//
|
|
432
|
+
// Indicators are excluded, but NOT by filtering on element type: a styled
|
|
433
|
+
// layer wraps Select.ItemIndicator in its own component, and such a wrapper is
|
|
434
|
+
// opaque to a `child.type` test — the indicator would then be mirrored into
|
|
435
|
+
// the trigger, render outside any Select.Item, and throw on the strict
|
|
436
|
+
// context. Instead the mirror publishes `mirrored: true` and each indicator
|
|
437
|
+
// opts itself out, however deeply it is nested.
|
|
438
|
+
const mirrored = selectedChildren ?? null;
|
|
439
|
+
|
|
440
|
+
return (
|
|
441
|
+
<span {...rest} data-placeholder={mirrored === null ? "" : undefined}>
|
|
442
|
+
{mirrored === null ? (
|
|
443
|
+
placeholder
|
|
444
|
+
) : (
|
|
445
|
+
<SelectItemIndicatorContext.Provider value={MIRRORED_CONTEXT}>
|
|
446
|
+
{mirrored}
|
|
447
|
+
</SelectItemIndicatorContext.Provider>
|
|
448
|
+
)}
|
|
449
|
+
</span>
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** @internal */
|
|
454
|
+
SelectValue.displayName = "SelectValue";
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* The selection mark rendered inside a rich {@link SelectItem} — typically a
|
|
458
|
+
* checkmark. Reads its item's selected state from context and renders only
|
|
459
|
+
* when that item is selected (pass `forceMount` to keep it mounted for
|
|
460
|
+
* CSS/animation), exposing `data-state` (`"checked"` / `"unchecked"`).
|
|
461
|
+
*
|
|
462
|
+
* `Select.Value` deliberately excludes the indicator when mirroring the
|
|
463
|
+
* selection into the trigger.
|
|
464
|
+
*
|
|
465
|
+
* Renders a `<span>` by default; pass `asChild` to compose onto any element
|
|
466
|
+
* (commonly an SVG icon) via the {@link Slot} pattern.
|
|
467
|
+
*
|
|
468
|
+
* @extends HTMLSpanElement
|
|
469
|
+
*
|
|
470
|
+
* @example
|
|
471
|
+
* ```tsx
|
|
472
|
+
* <Select.Item value="react">
|
|
473
|
+
* <ReactIcon />
|
|
474
|
+
* React
|
|
475
|
+
* <Select.ItemIndicator>✓</Select.ItemIndicator>
|
|
476
|
+
* </Select.Item>
|
|
477
|
+
* ```
|
|
478
|
+
*/
|
|
479
|
+
export function SelectItemIndicator({
|
|
480
|
+
children,
|
|
481
|
+
asChild = false,
|
|
482
|
+
forceMount = false,
|
|
483
|
+
...rest
|
|
484
|
+
}: SelectItemIndicatorProps): ReactElement | null {
|
|
485
|
+
const { selected, mirrored } = useSelectItemIndicatorContext();
|
|
486
|
+
|
|
487
|
+
// The trigger's mirrored copy never shows the mark — not even under
|
|
488
|
+
// forceMount, whose job is keeping the mark mounted for CSS animation inside
|
|
489
|
+
// the row.
|
|
490
|
+
if (mirrored) return null;
|
|
491
|
+
if (!forceMount && !selected) return null;
|
|
492
|
+
|
|
493
|
+
const indicatorProps = {
|
|
494
|
+
...rest,
|
|
495
|
+
"data-state": selected ? "checked" : "unchecked",
|
|
496
|
+
};
|
|
497
|
+
if (asChild) {
|
|
498
|
+
return <Slot {...indicatorProps}>{children}</Slot>;
|
|
499
|
+
}
|
|
500
|
+
return <span {...indicatorProps}>{children}</span>;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/** @internal */
|
|
504
|
+
SelectItemIndicator.displayName = ITEM_INDICATOR_DISPLAY_NAME;
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* The rich-mode listbox panel, rendered with the native
|
|
508
|
+
* [Popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API)
|
|
509
|
+
* (`popover="auto"`) — no portal, no floating-ui. The browser manages the
|
|
510
|
+
* top layer and light-dismiss (outside click / Escape). Opens and closes in
|
|
511
|
+
* sync with the trigger; on open, focus moves into the listbox, and Escape
|
|
512
|
+
* closes it and returns focus to the trigger.
|
|
513
|
+
*
|
|
514
|
+
* Renders a `<div role="listbox">` by default; pass `asChild` to compose the
|
|
515
|
+
* listbox props onto a consumer element via the {@link Slot} pattern.
|
|
516
|
+
*
|
|
517
|
+
* @extends HTMLDivElement
|
|
518
|
+
*
|
|
519
|
+
* @example
|
|
520
|
+
* ```tsx
|
|
521
|
+
* <Select.Content>
|
|
522
|
+
* <Select.Item value="react">React</Select.Item>
|
|
523
|
+
* <Select.Item value="vue">Vue</Select.Item>
|
|
524
|
+
* </Select.Content>
|
|
525
|
+
* ```
|
|
526
|
+
*/
|
|
527
|
+
export function SelectContent({
|
|
528
|
+
children,
|
|
529
|
+
onKeyDown,
|
|
530
|
+
asChild = false,
|
|
531
|
+
...rest
|
|
532
|
+
}: SelectContentProps): ReactElement {
|
|
533
|
+
const { contentProps } = useSelectContent({ onKeyDown, restProps: rest });
|
|
534
|
+
|
|
535
|
+
if (asChild) {
|
|
536
|
+
return <Slot {...contentProps}>{children}</Slot>;
|
|
537
|
+
}
|
|
538
|
+
return <div {...contentProps}>{children}</div>;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/** @internal */
|
|
542
|
+
SelectContent.displayName = "SelectContent";
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Visually groups related options. In `native` mode it renders an
|
|
546
|
+
* `<optgroup>` (the browser shows {@link SelectGroupProps.label | `label`}
|
|
547
|
+
* as a non-selectable heading); in rich mode it renders a
|
|
548
|
+
* `<div role="group">` with `aria-label={label}`. Either way the `label` is
|
|
549
|
+
* the group's accessible name.
|
|
550
|
+
*
|
|
551
|
+
* @example
|
|
552
|
+
* ```tsx
|
|
553
|
+
* <Select.Group label="Fruits">
|
|
554
|
+
* <Select.Item value="apple">Apple</Select.Item>
|
|
555
|
+
* <Select.Item value="banana">Banana</Select.Item>
|
|
556
|
+
* </Select.Group>
|
|
557
|
+
* ```
|
|
123
558
|
*/
|
|
124
559
|
export function SelectGroup({
|
|
125
560
|
children,
|
|
561
|
+
label,
|
|
126
562
|
...rest
|
|
127
563
|
}: SelectGroupProps): ReactElement {
|
|
128
|
-
|
|
564
|
+
const ctx = useContext(SelectContext);
|
|
565
|
+
if (!ctx) {
|
|
566
|
+
return (
|
|
567
|
+
<optgroup label={label} {...rest}>
|
|
568
|
+
{children}
|
|
569
|
+
</optgroup>
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
return (
|
|
573
|
+
<div role="group" aria-label={label} {...rest}>
|
|
574
|
+
{children}
|
|
575
|
+
</div>
|
|
576
|
+
);
|
|
129
577
|
}
|
|
130
578
|
|
|
131
579
|
/** @internal */
|
|
132
580
|
SelectGroup.displayName = "SelectGroup";
|
|
133
581
|
|
|
582
|
+
/**
|
|
583
|
+
* A visual divider between groups of options in rich mode — typically
|
|
584
|
+
* placed between two {@link SelectGroup | `Select.Group`} elements (or any
|
|
585
|
+
* two {@link SelectItem | `Select.Item`}s). Renders a `<div role="separator">`
|
|
586
|
+
* by default; pass `asChild` to render any element with separator semantics.
|
|
587
|
+
* Non-interactive — {@link SelectContent | `Select.Content`}'s keyboard
|
|
588
|
+
* navigation only ever targets `[role="option"]`, so it is skipped by arrow
|
|
589
|
+
* key traversal without any extra wiring.
|
|
590
|
+
*
|
|
591
|
+
* @extends HTMLDivElement
|
|
592
|
+
*
|
|
593
|
+
* @example
|
|
594
|
+
* ```tsx
|
|
595
|
+
* <Select.Item value="apple">Apple</Select.Item>
|
|
596
|
+
* <Select.Separator />
|
|
597
|
+
* <Select.Item value="banana">Banana</Select.Item>
|
|
598
|
+
* ```
|
|
599
|
+
*/
|
|
600
|
+
export function SelectSeparator({
|
|
601
|
+
asChild = false,
|
|
602
|
+
children,
|
|
603
|
+
...rest
|
|
604
|
+
}: SelectSeparatorProps): ReactElement {
|
|
605
|
+
const separatorProps = { ...rest, role: "separator" as const };
|
|
606
|
+
|
|
607
|
+
if (asChild) {
|
|
608
|
+
return <Slot {...separatorProps}>{children}</Slot>;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return <div {...separatorProps} />;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/** @internal */
|
|
615
|
+
SelectSeparator.displayName = "SelectSeparator";
|
|
616
|
+
|
|
134
617
|
/**
|
|
135
618
|
* A non-selectable hint shown as the initial selection of a Select.
|
|
136
619
|
* Renders a native `<option value="" disabled hidden>` so the browser
|
|
137
|
-
* displays it before the user picks anything but
|
|
138
|
-
* from the dropdown afterwards.
|
|
139
|
-
* {@link Select.Root} (above any `Select.Option` or `Select.Group`).
|
|
620
|
+
* displays it in the closed control before the user picks anything but
|
|
621
|
+
* makes it unreachable from the dropdown afterwards.
|
|
140
622
|
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
623
|
+
* Always render it as the **first** child of
|
|
624
|
+
* {@link SelectRoot | `Select.Root`}, above any
|
|
625
|
+
* {@link SelectItem | `Select.Item`} or
|
|
626
|
+
* {@link SelectGroup | `Select.Group`}.
|
|
627
|
+
*
|
|
628
|
+
* When `Select.Placeholder` is present among Root's direct children and
|
|
629
|
+
* neither `value` nor `defaultValue` is set, Root automatically infers
|
|
630
|
+
* `defaultValue=""` so the placeholder — not the first selectable option
|
|
631
|
+
* — is the initial selection. Pair `required` on Root to make the
|
|
632
|
+
* browser's native form validation catch an unchosen value at submission.
|
|
633
|
+
*
|
|
634
|
+
* `value=""`, `disabled`, and `hidden` are fixed by the component and
|
|
635
|
+
* cannot be overridden. See {@link SelectPlaceholderProps}.
|
|
636
|
+
*
|
|
637
|
+
* @extends HTMLOptionElement
|
|
638
|
+
*
|
|
639
|
+
* @example
|
|
640
|
+
* ```tsx
|
|
641
|
+
* <Select.Root native required aria-label="Pick a fruit">
|
|
642
|
+
* <Select.Placeholder>Choose a fruit…</Select.Placeholder>
|
|
643
|
+
* <Select.Item value="apple">Apple</Select.Item>
|
|
644
|
+
* <Select.Item value="banana">Banana</Select.Item>
|
|
645
|
+
* </Select.Root>
|
|
646
|
+
* ```
|
|
143
647
|
*/
|
|
144
648
|
export function SelectPlaceholder({
|
|
145
649
|
children,
|
|
@@ -158,59 +662,82 @@ SelectPlaceholder.displayName = "SelectPlaceholder";
|
|
|
158
662
|
/** Type of the {@link Select} compound: the root callable plus its attached sub-components. */
|
|
159
663
|
export type TSelectCompound = typeof SelectRoot & {
|
|
160
664
|
Root: typeof SelectRoot;
|
|
161
|
-
|
|
665
|
+
Trigger: typeof SelectTrigger;
|
|
666
|
+
Value: typeof SelectValue;
|
|
667
|
+
Content: typeof SelectContent;
|
|
668
|
+
Item: typeof SelectItem;
|
|
669
|
+
ItemIndicator: typeof SelectItemIndicator;
|
|
162
670
|
Group: typeof SelectGroup;
|
|
671
|
+
Separator: typeof SelectSeparator;
|
|
163
672
|
Placeholder: typeof SelectPlaceholder;
|
|
164
673
|
};
|
|
165
674
|
|
|
166
675
|
/**
|
|
167
|
-
* Headless **Select** — a compound
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
* owns the popup, keyboard interaction (arrow keys, Home/End,
|
|
172
|
-
* typeahead), mobile UX (wheel pickers), and form submission. No
|
|
173
|
-
* positioning JS or Portal is involved.
|
|
676
|
+
* Headless, accessible single-select **Select** — a compound with two render
|
|
677
|
+
* paths behind one API, chosen by {@link SelectRootBaseProps.native | `native`}:
|
|
678
|
+
* a rich Popover-API listbox (default) or a native `<select>` wrapper. Zero
|
|
679
|
+
* styles ship.
|
|
174
680
|
*
|
|
175
681
|
* `Select` is both callable (an alias of {@link SelectRoot | `Select.Root`})
|
|
176
|
-
* and carries its sub-components as static properties.
|
|
682
|
+
* and carries its sub-components as static properties. Prefer the namespaced
|
|
683
|
+
* form in application code:
|
|
177
684
|
*
|
|
178
|
-
* - {@link SelectRoot | `Select.Root`} — state owner
|
|
179
|
-
* - {@link
|
|
180
|
-
* - {@link
|
|
181
|
-
* - {@link
|
|
182
|
-
*
|
|
685
|
+
* - {@link SelectRoot | `Select.Root`} — state owner + context boundary.
|
|
686
|
+
* - {@link SelectTrigger | `Select.Trigger`} — rich mode: the listbox button.
|
|
687
|
+
* - {@link SelectValue | `Select.Value`} — rich mode: mirrors the selection.
|
|
688
|
+
* - {@link SelectContent | `Select.Content`} — rich mode: the popover listbox.
|
|
689
|
+
* - {@link SelectItem | `Select.Item`} — an option (rich `<div>` / native `<option>`).
|
|
690
|
+
* - {@link SelectItemIndicator | `Select.ItemIndicator`} — rich mode: the selected mark.
|
|
691
|
+
* - {@link SelectGroup | `Select.Group`} — a group (rich `role="group"` / native `<optgroup>`).
|
|
692
|
+
* - {@link SelectSeparator | `Select.Separator`} — rich mode: a divider between groups/items.
|
|
693
|
+
* - {@link SelectPlaceholder | `Select.Placeholder`} — native mode: the initial hint.
|
|
183
694
|
*
|
|
184
|
-
* @example
|
|
695
|
+
* @example Rich (default)
|
|
185
696
|
* ```tsx
|
|
186
697
|
* import { Select } from "@primitiv-ui/react";
|
|
187
698
|
*
|
|
188
|
-
* <Select.Root
|
|
189
|
-
* <Select.
|
|
190
|
-
*
|
|
699
|
+
* <Select.Root value={framework} onValueChange={setFramework}>
|
|
700
|
+
* <Select.Trigger>
|
|
701
|
+
* <Select.Value placeholder="Pick a framework…" />
|
|
702
|
+
* </Select.Trigger>
|
|
703
|
+
* <Select.Content>
|
|
704
|
+
* <Select.Item value="react">
|
|
705
|
+
* React
|
|
706
|
+
* <Select.ItemIndicator>✓</Select.ItemIndicator>
|
|
707
|
+
* </Select.Item>
|
|
708
|
+
* <Select.Item value="vue">
|
|
709
|
+
* Vue
|
|
710
|
+
* <Select.ItemIndicator>✓</Select.ItemIndicator>
|
|
711
|
+
* </Select.Item>
|
|
712
|
+
* </Select.Content>
|
|
191
713
|
* </Select.Root>
|
|
192
714
|
* ```
|
|
193
715
|
*
|
|
194
|
-
* @example
|
|
716
|
+
* @example Native
|
|
195
717
|
* ```tsx
|
|
196
|
-
* <Select.Root
|
|
718
|
+
* <Select.Root native defaultValue="apple" aria-label="Pick a fruit">
|
|
197
719
|
* <Select.Placeholder>Choose…</Select.Placeholder>
|
|
198
720
|
* <Select.Group label="Fruits">
|
|
199
|
-
* <Select.
|
|
200
|
-
*
|
|
201
|
-
* <Select.Group label="Vegetables">
|
|
202
|
-
* <Select.Option value="carrot">Carrot</Select.Option>
|
|
721
|
+
* <Select.Item value="apple">Apple</Select.Item>
|
|
722
|
+
* <Select.Item value="banana">Banana</Select.Item>
|
|
203
723
|
* </Select.Group>
|
|
204
724
|
* </Select.Root>
|
|
205
725
|
* ```
|
|
206
726
|
*
|
|
207
|
-
* @see {@link SelectRoot} for
|
|
208
|
-
* @see {@link
|
|
727
|
+
* @see {@link SelectRoot} for the mode + state contract and Field integration.
|
|
728
|
+
* @see {@link SelectValue} for how the selection mirrors into the trigger.
|
|
729
|
+
* @see {@link SelectContent} for the listbox keyboard contract.
|
|
730
|
+
* @see {@link SelectPlaceholder} for the native placeholder + `defaultValue` interaction.
|
|
209
731
|
*/
|
|
210
732
|
const SelectCompound: TSelectCompound = Object.assign(SelectRoot, {
|
|
211
733
|
Root: SelectRoot,
|
|
212
|
-
|
|
734
|
+
Trigger: SelectTrigger,
|
|
735
|
+
Value: SelectValue,
|
|
736
|
+
Content: SelectContent,
|
|
737
|
+
Item: SelectItem,
|
|
738
|
+
ItemIndicator: SelectItemIndicator,
|
|
213
739
|
Group: SelectGroup,
|
|
740
|
+
Separator: SelectSeparator,
|
|
214
741
|
Placeholder: SelectPlaceholder,
|
|
215
742
|
});
|
|
216
743
|
|