@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
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ReactNode, RefObject } from "react";
|
|
2
|
+
|
|
3
|
+
import { createStrictContext } from "../utils/index.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Context shared by the rich (non-`native`) Select sub-components —
|
|
7
|
+
* {@link SelectTrigger}, {@link SelectContent}, and (in later cycles)
|
|
8
|
+
* {@link SelectValue} / {@link SelectItem}. Provided by the rich render
|
|
9
|
+
* path of {@link SelectRoot}. The `native` path does not provide it, so
|
|
10
|
+
* the strict hook throwing outside a rich `<Select.Root>` is correct.
|
|
11
|
+
*/
|
|
12
|
+
export type SelectContextValue = {
|
|
13
|
+
/** Whether the listbox popover is open. */
|
|
14
|
+
open: boolean;
|
|
15
|
+
/** Open/close the listbox. De-dupes repeat transitions. */
|
|
16
|
+
setOpen: (open: boolean) => void;
|
|
17
|
+
/** The currently selected item value (`""` when nothing is selected). */
|
|
18
|
+
value: string;
|
|
19
|
+
/** Commit a selection: sets the value, closes the listbox, focuses the trigger. */
|
|
20
|
+
select: (value: string) => void;
|
|
21
|
+
/** Register (or refresh) a rich item's content for value-mirroring. */
|
|
22
|
+
registerItem: (value: string, children: ReactNode) => void;
|
|
23
|
+
/** Remove a rich item's registration on unmount. */
|
|
24
|
+
unregisterItem: (value: string) => void;
|
|
25
|
+
/** Read the registered content for a value (used by Select.Value). */
|
|
26
|
+
getItemChildren: (value: string) => ReactNode | undefined;
|
|
27
|
+
/** `id` of the listbox content, wired to the trigger's `aria-controls`. */
|
|
28
|
+
contentId: string;
|
|
29
|
+
/** `id` of the trigger, available for labelling. */
|
|
30
|
+
triggerId: string;
|
|
31
|
+
/** Ref to the trigger button so focus can return to it on close. */
|
|
32
|
+
triggerRef: RefObject<HTMLButtonElement | null>;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const [SelectContext, useSelectContext] =
|
|
36
|
+
createStrictContext<SelectContextValue>(
|
|
37
|
+
"Select rich sub-components (Trigger, Value, Content, Item) must be rendered inside a <Select.Root>.",
|
|
38
|
+
);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createStrictContext } from "../utils/index.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Context published by a rich {@link SelectItem} to its
|
|
5
|
+
* {@link SelectItemIndicator} child, carrying whether the item is currently
|
|
6
|
+
* selected so the indicator can render (and expose `data-state`).
|
|
7
|
+
*/
|
|
8
|
+
export type SelectItemIndicatorContextValue = {
|
|
9
|
+
/** Whether the enclosing item is the selected one. */
|
|
10
|
+
selected: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* `true` while the subtree is being rendered by {@link SelectValue} as the
|
|
13
|
+
* mirrored copy of the selected item's content, rather than inside the item
|
|
14
|
+
* itself. Indicators render nothing there: the mark answers "which row is
|
|
15
|
+
* selected", which is redundant on the trigger it already represents.
|
|
16
|
+
*
|
|
17
|
+
* The mirror cannot identify indicators by element type — a styled layer
|
|
18
|
+
* wraps `Select.ItemIndicator` in its own component (the registry does), and
|
|
19
|
+
* any such wrapper is opaque to a `child.type` test. Publishing the intent
|
|
20
|
+
* through context instead lets the indicator opt itself out at any nesting
|
|
21
|
+
* depth.
|
|
22
|
+
*/
|
|
23
|
+
mirrored: boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const [SelectItemIndicatorContext, useSelectItemIndicatorContext] =
|
|
27
|
+
createStrictContext<SelectItemIndicatorContextValue>(
|
|
28
|
+
"Select.ItemIndicator must be rendered inside a <Select.Item>.",
|
|
29
|
+
);
|
|
@@ -18,9 +18,9 @@ describe("Select asChild", () => {
|
|
|
18
18
|
it("delegates to a consumer-supplied <select> wrapper, merging Root's props onto it", () => {
|
|
19
19
|
// Arrange & Act
|
|
20
20
|
render(
|
|
21
|
-
<Select.Root asChild disabled>
|
|
21
|
+
<Select.Root native asChild disabled>
|
|
22
22
|
<StyledSelect>
|
|
23
|
-
<Select.
|
|
23
|
+
<Select.Item value="apple">Apple</Select.Item>
|
|
24
24
|
</StyledSelect>
|
|
25
25
|
</Select.Root>,
|
|
26
26
|
);
|
|
@@ -3,15 +3,34 @@ import { render, screen } from "@testing-library/react";
|
|
|
3
3
|
import { Select } from "../Select";
|
|
4
4
|
|
|
5
5
|
describe("Select basic rendering", () => {
|
|
6
|
-
it("renders an <option> for each Select.
|
|
6
|
+
it("renders an <option> for each Select.Item child so the value is in the DOM", () => {
|
|
7
7
|
// Arrange & Act
|
|
8
8
|
render(
|
|
9
|
-
<Select.Root>
|
|
10
|
-
<Select.
|
|
9
|
+
<Select.Root native>
|
|
10
|
+
<Select.Item value="apple">Apple</Select.Item>
|
|
11
11
|
</Select.Root>,
|
|
12
12
|
);
|
|
13
13
|
|
|
14
14
|
// Assert
|
|
15
15
|
expect(screen.getByRole("option", { name: "Apple" })).toBeInTheDocument();
|
|
16
16
|
});
|
|
17
|
+
|
|
18
|
+
it("sets a displayName on the compound and each sub-component", () => {
|
|
19
|
+
// Assert — empty displayNames would render each as anonymous in DevTools.
|
|
20
|
+
// `Select`, `Select.Root` and the underlying function are one object
|
|
21
|
+
// (Object.assign compound), so the compound's "Select" alias is the
|
|
22
|
+
// observable Root displayName; the sub-components are distinct objects.
|
|
23
|
+
// SelectPlaceholder's name is load-bearing beyond DevTools: Root detects a
|
|
24
|
+
// placeholder among its direct children by it (see the placeholder tests),
|
|
25
|
+
// and so must any consumer wrapper that wants that inference.
|
|
26
|
+
expect(Select.displayName).toBe("Select");
|
|
27
|
+
expect(Select.Trigger.displayName).toBe("SelectTrigger");
|
|
28
|
+
expect(Select.Value.displayName).toBe("SelectValue");
|
|
29
|
+
expect(Select.Content.displayName).toBe("SelectContent");
|
|
30
|
+
expect(Select.Item.displayName).toBe("SelectItem");
|
|
31
|
+
expect(Select.ItemIndicator.displayName).toBe("SelectItemIndicator");
|
|
32
|
+
expect(Select.Group.displayName).toBe("SelectGroup");
|
|
33
|
+
expect(Select.Separator.displayName).toBe("SelectSeparator");
|
|
34
|
+
expect(Select.Placeholder.displayName).toBe("SelectPlaceholder");
|
|
35
|
+
});
|
|
17
36
|
});
|
|
@@ -17,9 +17,9 @@ describe("Select controlled state", () => {
|
|
|
17
17
|
setValue(next);
|
|
18
18
|
};
|
|
19
19
|
return (
|
|
20
|
-
<Select.Root value={value} onValueChange={handle}>
|
|
21
|
-
<Select.
|
|
22
|
-
<Select.
|
|
20
|
+
<Select.Root native value={value} onValueChange={handle}>
|
|
21
|
+
<Select.Item value="apple">Apple</Select.Item>
|
|
22
|
+
<Select.Item value="banana">Banana</Select.Item>
|
|
23
23
|
</Select.Root>
|
|
24
24
|
);
|
|
25
25
|
}
|
|
@@ -43,7 +43,7 @@ describe("Select controlled state", () => {
|
|
|
43
43
|
function Wrapper() {
|
|
44
44
|
const [value, setValue] = useState("apple");
|
|
45
45
|
return (
|
|
46
|
-
<Select.Root
|
|
46
|
+
<Select.Root native
|
|
47
47
|
value={value}
|
|
48
48
|
onChange={(event) => {
|
|
49
49
|
onChange(event.target.value);
|
|
@@ -51,8 +51,8 @@ describe("Select controlled state", () => {
|
|
|
51
51
|
}}
|
|
52
52
|
onValueChange={onValueChange}
|
|
53
53
|
>
|
|
54
|
-
<Select.
|
|
55
|
-
<Select.
|
|
54
|
+
<Select.Item value="apple">Apple</Select.Item>
|
|
55
|
+
<Select.Item value="banana">Banana</Select.Item>
|
|
56
56
|
</Select.Root>
|
|
57
57
|
);
|
|
58
58
|
}
|
|
@@ -6,8 +6,8 @@ describe("Select data attributes", () => {
|
|
|
6
6
|
it("renders data-disabled='' on the root <select> when disabled is true so CSS can style the disabled state", () => {
|
|
7
7
|
// Arrange & Act
|
|
8
8
|
render(
|
|
9
|
-
<Select.Root disabled>
|
|
10
|
-
<Select.
|
|
9
|
+
<Select.Root native disabled>
|
|
10
|
+
<Select.Item value="apple">Apple</Select.Item>
|
|
11
11
|
</Select.Root>,
|
|
12
12
|
);
|
|
13
13
|
|
|
@@ -18,8 +18,8 @@ describe("Select data attributes", () => {
|
|
|
18
18
|
it("omits data-disabled entirely when disabled is false or absent so the enabled state has nothing to override", () => {
|
|
19
19
|
// Arrange & Act
|
|
20
20
|
render(
|
|
21
|
-
<Select.Root>
|
|
22
|
-
<Select.
|
|
21
|
+
<Select.Root native>
|
|
22
|
+
<Select.Item value="apple">Apple</Select.Item>
|
|
23
23
|
</Select.Root>,
|
|
24
24
|
);
|
|
25
25
|
|
|
@@ -7,8 +7,8 @@ function renderSelect(children?: React.ReactNode) {
|
|
|
7
7
|
return (
|
|
8
8
|
<>
|
|
9
9
|
{children}
|
|
10
|
-
<Select.
|
|
11
|
-
<Select.
|
|
10
|
+
<Select.Item value="apple">Apple</Select.Item>
|
|
11
|
+
<Select.Item value="banana">Banana</Select.Item>
|
|
12
12
|
</>
|
|
13
13
|
);
|
|
14
14
|
}
|
|
@@ -18,7 +18,7 @@ describe("Select — Field integration", () => {
|
|
|
18
18
|
// Arrange & Act
|
|
19
19
|
render(
|
|
20
20
|
<Field.Root id="fruit">
|
|
21
|
-
<Select.Root aria-label="Fruit">{renderSelect()}</Select.Root>
|
|
21
|
+
<Select.Root native aria-label="Fruit">{renderSelect()}</Select.Root>
|
|
22
22
|
</Field.Root>,
|
|
23
23
|
);
|
|
24
24
|
|
|
@@ -33,7 +33,7 @@ describe("Select — Field integration", () => {
|
|
|
33
33
|
// Arrange & Act
|
|
34
34
|
render(
|
|
35
35
|
<Field.Root id="fruit">
|
|
36
|
-
<Select.Root id="my-fruit" aria-label="Fruit">
|
|
36
|
+
<Select.Root native id="my-fruit" aria-label="Fruit">
|
|
37
37
|
{renderSelect()}
|
|
38
38
|
</Select.Root>
|
|
39
39
|
</Field.Root>,
|
|
@@ -50,7 +50,7 @@ describe("Select — Field integration", () => {
|
|
|
50
50
|
// Arrange & Act
|
|
51
51
|
render(
|
|
52
52
|
<Field.Root id="fruit">
|
|
53
|
-
<Select.Root aria-label="Fruit">{renderSelect()}</Select.Root>
|
|
53
|
+
<Select.Root native aria-label="Fruit">{renderSelect()}</Select.Root>
|
|
54
54
|
</Field.Root>,
|
|
55
55
|
);
|
|
56
56
|
|
|
@@ -65,7 +65,7 @@ describe("Select — Field integration", () => {
|
|
|
65
65
|
// Arrange & Act
|
|
66
66
|
render(
|
|
67
67
|
<Field.Root id="fruit" invalid>
|
|
68
|
-
<Select.Root aria-label="Fruit">{renderSelect()}</Select.Root>
|
|
68
|
+
<Select.Root native aria-label="Fruit">{renderSelect()}</Select.Root>
|
|
69
69
|
</Field.Root>,
|
|
70
70
|
);
|
|
71
71
|
|
|
@@ -81,7 +81,7 @@ describe("Select — Field integration", () => {
|
|
|
81
81
|
// Arrange & Act
|
|
82
82
|
render(
|
|
83
83
|
<Field.Root id="fruit">
|
|
84
|
-
<Select.Root aria-label="Fruit" aria-describedby="extra-hint">
|
|
84
|
+
<Select.Root native aria-label="Fruit" aria-describedby="extra-hint">
|
|
85
85
|
{renderSelect()}
|
|
86
86
|
</Select.Root>
|
|
87
87
|
</Field.Root>,
|
|
@@ -99,7 +99,7 @@ describe("Select — Field integration", () => {
|
|
|
99
99
|
// Arrange & Act
|
|
100
100
|
render(
|
|
101
101
|
<Field.Root id="fruit" invalid>
|
|
102
|
-
<Select.Root aria-label="Fruit">{renderSelect()}</Select.Root>
|
|
102
|
+
<Select.Root native aria-label="Fruit">{renderSelect()}</Select.Root>
|
|
103
103
|
</Field.Root>,
|
|
104
104
|
);
|
|
105
105
|
|
|
@@ -114,7 +114,7 @@ describe("Select — Field integration", () => {
|
|
|
114
114
|
// Arrange & Act
|
|
115
115
|
render(
|
|
116
116
|
<Field.Root id="fruit" disabled>
|
|
117
|
-
<Select.Root aria-label="Fruit">{renderSelect()}</Select.Root>
|
|
117
|
+
<Select.Root native aria-label="Fruit">{renderSelect()}</Select.Root>
|
|
118
118
|
</Field.Root>,
|
|
119
119
|
);
|
|
120
120
|
|
|
@@ -126,7 +126,7 @@ describe("Select — Field integration", () => {
|
|
|
126
126
|
// Arrange & Act
|
|
127
127
|
render(
|
|
128
128
|
<Field.Root id="fruit" required>
|
|
129
|
-
<Select.Root aria-label="Fruit">{renderSelect()}</Select.Root>
|
|
129
|
+
<Select.Root native aria-label="Fruit">{renderSelect()}</Select.Root>
|
|
130
130
|
</Field.Root>,
|
|
131
131
|
);
|
|
132
132
|
|
|
@@ -137,7 +137,7 @@ describe("Select — Field integration", () => {
|
|
|
137
137
|
it("Select outside Field.Root behaves identically to before — no field-derived attributes", () => {
|
|
138
138
|
// Arrange & Act
|
|
139
139
|
render(
|
|
140
|
-
<Select.Root aria-label="Fruit">{renderSelect()}</Select.Root>,
|
|
140
|
+
<Select.Root native aria-label="Fruit">{renderSelect()}</Select.Root>,
|
|
141
141
|
);
|
|
142
142
|
const select = screen.getByRole("combobox", { name: "Fruit" });
|
|
143
143
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
|
|
4
|
+
import { Select } from "../Select";
|
|
5
|
+
|
|
6
|
+
describe("Select rich form integration", () => {
|
|
7
|
+
it("submits the selected value through a hidden native <select>", async () => {
|
|
8
|
+
const user = userEvent.setup();
|
|
9
|
+
let submitted: string | null = null;
|
|
10
|
+
render(
|
|
11
|
+
<form
|
|
12
|
+
onSubmit={(event) => {
|
|
13
|
+
event.preventDefault();
|
|
14
|
+
submitted = new FormData(event.currentTarget).get(
|
|
15
|
+
"framework",
|
|
16
|
+
) as string;
|
|
17
|
+
}}
|
|
18
|
+
>
|
|
19
|
+
<Select.Root name="framework">
|
|
20
|
+
<Select.Trigger>
|
|
21
|
+
<Select.Value placeholder="Pick" />
|
|
22
|
+
</Select.Trigger>
|
|
23
|
+
<Select.Content>
|
|
24
|
+
<Select.Item value="react">React</Select.Item>
|
|
25
|
+
<Select.Item value="vue">Vue</Select.Item>
|
|
26
|
+
</Select.Content>
|
|
27
|
+
</Select.Root>
|
|
28
|
+
<button type="submit">Submit</button>
|
|
29
|
+
</form>,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
await user.click(screen.getByRole("button", { name: "Pick" }));
|
|
33
|
+
await user.click(screen.getByText("Vue"));
|
|
34
|
+
await user.click(screen.getByRole("button", { name: "Submit" }));
|
|
35
|
+
|
|
36
|
+
expect(submitted).toBe("vue");
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -6,13 +6,13 @@ describe("Select group", () => {
|
|
|
6
6
|
it("renders an <optgroup> with the given label so options can be visually grouped", () => {
|
|
7
7
|
// Arrange & Act
|
|
8
8
|
render(
|
|
9
|
-
<Select.Root>
|
|
9
|
+
<Select.Root native>
|
|
10
10
|
<Select.Group label="Fruits">
|
|
11
|
-
<Select.
|
|
12
|
-
<Select.
|
|
11
|
+
<Select.Item value="apple">Apple</Select.Item>
|
|
12
|
+
<Select.Item value="banana">Banana</Select.Item>
|
|
13
13
|
</Select.Group>
|
|
14
14
|
<Select.Group label="Vegetables">
|
|
15
|
-
<Select.
|
|
15
|
+
<Select.Item value="carrot">Carrot</Select.Item>
|
|
16
16
|
</Select.Group>
|
|
17
17
|
</Select.Root>,
|
|
18
18
|
);
|
|
@@ -28,9 +28,9 @@ describe("Select group", () => {
|
|
|
28
28
|
it("nests its Option children inside the rendered <optgroup>", () => {
|
|
29
29
|
// Arrange & Act
|
|
30
30
|
render(
|
|
31
|
-
<Select.Root>
|
|
31
|
+
<Select.Root native>
|
|
32
32
|
<Select.Group label="Fruits">
|
|
33
|
-
<Select.
|
|
33
|
+
<Select.Item value="apple">Apple</Select.Item>
|
|
34
34
|
</Select.Group>
|
|
35
35
|
</Select.Root>,
|
|
36
36
|
);
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
|
|
4
|
+
import { Select } from "../Select";
|
|
5
|
+
|
|
6
|
+
function renderSelect() {
|
|
7
|
+
return render(
|
|
8
|
+
<Select.Root>
|
|
9
|
+
<Select.Trigger>
|
|
10
|
+
<Select.Value placeholder="Pick one" />
|
|
11
|
+
</Select.Trigger>
|
|
12
|
+
<Select.Content>
|
|
13
|
+
<Select.Item value="react">React</Select.Item>
|
|
14
|
+
<Select.Item value="vue">Vue</Select.Item>
|
|
15
|
+
<Select.Item value="solid">Solid</Select.Item>
|
|
16
|
+
</Select.Content>
|
|
17
|
+
</Select.Root>,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("Select keyboard edge cases", () => {
|
|
22
|
+
it("focuses the listbox itself and no-ops arrow keys when there are no options", async () => {
|
|
23
|
+
const user = userEvent.setup();
|
|
24
|
+
render(
|
|
25
|
+
<Select.Root>
|
|
26
|
+
<Select.Trigger>
|
|
27
|
+
<Select.Value placeholder="Empty" />
|
|
28
|
+
</Select.Trigger>
|
|
29
|
+
<Select.Content />
|
|
30
|
+
</Select.Root>,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
await user.click(screen.getByRole("button"));
|
|
34
|
+
const list = screen.getByRole("listbox", { hidden: true });
|
|
35
|
+
expect(list).toHaveFocus();
|
|
36
|
+
|
|
37
|
+
await user.keyboard("{ArrowDown}");
|
|
38
|
+
expect(list).toHaveFocus();
|
|
39
|
+
expect(screen.getByRole("button")).toHaveAttribute("aria-expanded", "true");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("ignores unhandled multi-character keys", async () => {
|
|
43
|
+
const user = userEvent.setup();
|
|
44
|
+
renderSelect();
|
|
45
|
+
|
|
46
|
+
await user.click(screen.getByRole("button"));
|
|
47
|
+
await user.keyboard("{Shift}"); // key.length > 1, not a nav/select key → no-op
|
|
48
|
+
|
|
49
|
+
expect(screen.getByRole("button")).toHaveAttribute("aria-expanded", "true");
|
|
50
|
+
expect(screen.getByRole("option", { name: "React", hidden: true })).toHaveFocus();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("Enter is a no-op when focus is on the listbox rather than an option", async () => {
|
|
54
|
+
const user = userEvent.setup();
|
|
55
|
+
renderSelect();
|
|
56
|
+
|
|
57
|
+
await user.click(screen.getByRole("button"));
|
|
58
|
+
const list = screen.getByRole("listbox", { hidden: true });
|
|
59
|
+
list.focus();
|
|
60
|
+
await user.keyboard("{Enter}");
|
|
61
|
+
|
|
62
|
+
expect(screen.getByRole("button")).toHaveAttribute("aria-expanded", "true");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("typeahead handles a repeated character and a no-option starting point", async () => {
|
|
66
|
+
const user = userEvent.setup();
|
|
67
|
+
renderSelect();
|
|
68
|
+
|
|
69
|
+
await user.click(screen.getByRole("button"));
|
|
70
|
+
screen.getByRole("listbox", { hidden: true }).focus();
|
|
71
|
+
|
|
72
|
+
await user.keyboard("s"); // from the listbox (no option focused) → Solid
|
|
73
|
+
expect(screen.getByRole("option", { name: "Solid", hidden: true })).toHaveFocus();
|
|
74
|
+
|
|
75
|
+
await user.keyboard("s"); // repeated char → cycles, stays on the only "s" option
|
|
76
|
+
expect(screen.getByRole("option", { name: "Solid", hidden: true })).toHaveFocus();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("Select typeahead reset", () => {
|
|
81
|
+
it("clears the accumulated query after the reset window", async () => {
|
|
82
|
+
const user = userEvent.setup();
|
|
83
|
+
renderSelect();
|
|
84
|
+
|
|
85
|
+
await user.click(screen.getByRole("button"));
|
|
86
|
+
await user.keyboard("s"); // → Solid
|
|
87
|
+
expect(screen.getByRole("option", { name: "Solid", hidden: true })).toHaveFocus();
|
|
88
|
+
|
|
89
|
+
// Wait past the typeahead reset window, then start a fresh search.
|
|
90
|
+
await new Promise((resolve) => setTimeout(resolve, 600));
|
|
91
|
+
|
|
92
|
+
await user.keyboard("v"); // fresh query → Vue
|
|
93
|
+
expect(screen.getByRole("option", { name: "Vue", hidden: true })).toHaveFocus();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import { vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { Select } from "../Select";
|
|
6
|
+
|
|
7
|
+
function renderSelect(props: Parameters<typeof Select.Root>[0] = {}) {
|
|
8
|
+
return render(
|
|
9
|
+
<Select.Root {...props}>
|
|
10
|
+
<Select.Trigger>
|
|
11
|
+
<Select.Value placeholder="Pick one" />
|
|
12
|
+
</Select.Trigger>
|
|
13
|
+
<Select.Content>
|
|
14
|
+
<Select.Item value="react">React</Select.Item>
|
|
15
|
+
<Select.Item value="vue">Vue</Select.Item>
|
|
16
|
+
<Select.Item value="svelte" disabled>
|
|
17
|
+
Svelte
|
|
18
|
+
</Select.Item>
|
|
19
|
+
<Select.Item value="solid">Solid</Select.Item>
|
|
20
|
+
</Select.Content>
|
|
21
|
+
</Select.Root>,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function option(name: RegExp | string) {
|
|
26
|
+
return screen.getByRole("option", { name, hidden: true });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("Select keyboard navigation", () => {
|
|
30
|
+
it("focuses the first enabled option on open when nothing is selected", async () => {
|
|
31
|
+
const user = userEvent.setup();
|
|
32
|
+
renderSelect();
|
|
33
|
+
|
|
34
|
+
await user.click(screen.getByRole("button"));
|
|
35
|
+
|
|
36
|
+
expect(option("React")).toHaveFocus();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("focuses the selected option on open", async () => {
|
|
40
|
+
const user = userEvent.setup();
|
|
41
|
+
renderSelect({ defaultValue: "vue" });
|
|
42
|
+
|
|
43
|
+
await user.click(screen.getByRole("button"));
|
|
44
|
+
|
|
45
|
+
expect(option("Vue")).toHaveFocus();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("moves focus with ArrowDown/ArrowUp, skipping disabled and wrapping", async () => {
|
|
49
|
+
const user = userEvent.setup();
|
|
50
|
+
renderSelect();
|
|
51
|
+
|
|
52
|
+
await user.click(screen.getByRole("button"));
|
|
53
|
+
await user.keyboard("{ArrowDown}"); // React -> Vue
|
|
54
|
+
expect(option("Vue")).toHaveFocus();
|
|
55
|
+
|
|
56
|
+
await user.keyboard("{ArrowDown}"); // Vue -> Solid (skips disabled Svelte)
|
|
57
|
+
expect(option("Solid")).toHaveFocus();
|
|
58
|
+
|
|
59
|
+
await user.keyboard("{ArrowDown}"); // Solid -> React (wrap)
|
|
60
|
+
expect(option("React")).toHaveFocus();
|
|
61
|
+
|
|
62
|
+
await user.keyboard("{ArrowUp}"); // React -> Solid (wrap back)
|
|
63
|
+
expect(option("Solid")).toHaveFocus();
|
|
64
|
+
|
|
65
|
+
await user.keyboard("{ArrowUp}"); // Solid -> Vue (previous, no wrap)
|
|
66
|
+
expect(option("Vue")).toHaveFocus();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("jumps to first/last enabled option with Home/End", async () => {
|
|
70
|
+
const user = userEvent.setup();
|
|
71
|
+
renderSelect();
|
|
72
|
+
|
|
73
|
+
await user.click(screen.getByRole("button"));
|
|
74
|
+
await user.keyboard("{End}");
|
|
75
|
+
expect(option("Solid")).toHaveFocus();
|
|
76
|
+
|
|
77
|
+
await user.keyboard("{Home}");
|
|
78
|
+
expect(option("React")).toHaveFocus();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("selects the focused option with Enter and closes", async () => {
|
|
82
|
+
const user = userEvent.setup();
|
|
83
|
+
const onValueChange = vi.fn();
|
|
84
|
+
renderSelect({ onValueChange });
|
|
85
|
+
|
|
86
|
+
await user.click(screen.getByRole("button"));
|
|
87
|
+
await user.keyboard("{ArrowDown}"); // focus Vue
|
|
88
|
+
await user.keyboard("{Enter}");
|
|
89
|
+
|
|
90
|
+
expect(onValueChange).toHaveBeenCalledWith("vue");
|
|
91
|
+
expect(screen.getByRole("button")).toHaveAttribute("aria-expanded", "false");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("selects the first option with Enter when it is the focused one", async () => {
|
|
95
|
+
const user = userEvent.setup();
|
|
96
|
+
const onValueChange = vi.fn();
|
|
97
|
+
render(
|
|
98
|
+
<Select.Root onValueChange={onValueChange}>
|
|
99
|
+
<Select.Trigger>
|
|
100
|
+
<Select.Value placeholder="Pick one" />
|
|
101
|
+
</Select.Trigger>
|
|
102
|
+
<Select.Content>
|
|
103
|
+
<Select.Item value="react">React</Select.Item>
|
|
104
|
+
<Select.Item value="vue">Vue</Select.Item>
|
|
105
|
+
</Select.Content>
|
|
106
|
+
</Select.Root>,
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
await user.click(screen.getByRole("button")); // opens, focuses React (index 0)
|
|
110
|
+
await user.keyboard("{Enter}"); // index 0 is a valid selection, not "< 0"
|
|
111
|
+
|
|
112
|
+
expect(onValueChange).toHaveBeenCalledWith("react");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("typeahead focuses the option whose label starts with the typed text", async () => {
|
|
116
|
+
const user = userEvent.setup();
|
|
117
|
+
renderSelect();
|
|
118
|
+
|
|
119
|
+
await user.click(screen.getByRole("button"));
|
|
120
|
+
await user.keyboard("so");
|
|
121
|
+
|
|
122
|
+
expect(option("Solid")).toHaveFocus();
|
|
123
|
+
});
|
|
124
|
+
});
|