@primitiv-ui/react 0.1.28 → 0.1.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -2
- package/package.json +6 -1
- package/src/AccessibleIcon/AccessibleIcon.tsx +32 -9
- package/src/AccessibleIcon/__tests__/AccessibleIcon.test.tsx +5 -0
- package/src/AccessibleIcon/types.ts +15 -2
- package/src/Accordion/Accordion.tsx +32 -11
- package/src/Accordion/README.md +2 -2
- package/src/Accordion/__tests__/Accordion.asChild.test.tsx +46 -0
- package/src/Accordion/__tests__/Accordion.basic-rendering.test.tsx +15 -0
- package/src/Accordion/__tests__/Accordion.disabled-items.test.tsx +42 -0
- package/src/Accordion/__tests__/Accordion.error-handling.test.tsx +33 -0
- package/src/Accordion/hooks/useAccordionRoot.ts +44 -7
- package/src/Accordion/hooks/useAccordionTrigger.ts +9 -1
- package/src/Accordion/types.ts +72 -21
- package/src/Alert/Alert.tsx +36 -7
- package/src/Alert/__tests__/Alert.test.tsx +5 -0
- package/src/Alert/types.ts +18 -2
- package/src/Avatar/Avatar.tsx +99 -15
- package/src/Avatar/__tests__/Avatar.basic-rendering.test.tsx +10 -0
- package/src/Avatar/__tests__/Avatar.fallback.test.tsx +30 -0
- package/src/Avatar/__tests__/Avatar.image-loading.test.tsx +35 -0
- package/src/Avatar/hooks/useAvatarImage.ts +16 -0
- package/src/Avatar/types.ts +15 -3
- package/src/Breadcrumb/Breadcrumb.tsx +139 -21
- package/src/Breadcrumb/README.md +47 -0
- package/src/Breadcrumb/__tests__/Breadcrumb.basic-rendering.test.tsx +11 -0
- package/src/Breadcrumb/__tests__/Breadcrumb.ellipsis.test.tsx +110 -0
- package/src/Breadcrumb/types.ts +73 -7
- package/src/Button/Button.tsx +2 -0
- package/src/Button/__tests__/Button.basic-rendering.test.tsx +6 -0
- package/src/Button/__tests__/Button.contract.test.tsx +1 -1
- package/src/Button/types.ts +21 -0
- package/src/Carousel/Carousel.tsx +268 -20
- package/src/Carousel/README.md +764 -87
- package/src/Carousel/__tests__/Carousel.autoplay-status.test.tsx +239 -0
- package/src/Carousel/__tests__/Carousel.drag-status.test.tsx +292 -0
- package/src/Carousel/__tests__/Carousel.dynamic-slides.test.tsx +38 -0
- package/src/Carousel/__tests__/Carousel.indicators.test.tsx +101 -0
- package/src/Carousel/__tests__/Carousel.infinite.test.tsx +1453 -0
- package/src/Carousel/__tests__/Carousel.instant-scroll.test.tsx +124 -0
- package/src/Carousel/__tests__/Carousel.intersection-observer.test.tsx +90 -2
- package/src/Carousel/__tests__/Carousel.loop.test.tsx +163 -0
- package/src/Carousel/__tests__/Carousel.loopEngine.test.ts +77 -0
- package/src/Carousel/__tests__/Carousel.mouse-drag.test.tsx +504 -0
- package/src/Carousel/__tests__/Carousel.multi-slide-bounds.test.tsx +91 -0
- package/src/Carousel/__tests__/Carousel.orientation.test.tsx +207 -0
- package/src/Carousel/__tests__/Carousel.overscroll.test.tsx +532 -0
- package/src/Carousel/__tests__/Carousel.page-snap-points.test.tsx +50 -0
- package/src/Carousel/__tests__/Carousel.progress-text.test.tsx +76 -0
- package/src/Carousel/__tests__/Carousel.reduced-motion.test.tsx +3 -6
- package/src/Carousel/__tests__/Carousel.refresh-progress.test.tsx +2 -5
- package/src/Carousel/__tests__/Carousel.scroll-margin.test.tsx +167 -0
- package/src/Carousel/__tests__/Carousel.scroll-progress.test.tsx +518 -0
- package/src/Carousel/__tests__/Carousel.scroll-snap-change.test.tsx +32 -11
- package/src/Carousel/__tests__/Carousel.scroll-sync.test.tsx +237 -24
- package/src/Carousel/__tests__/Carousel.scroll-to-index.test.tsx +115 -0
- package/src/Carousel/__tests__/Carousel.slide-snap.test.tsx +172 -0
- package/src/Carousel/__tests__/Carousel.slides-per-move.test.tsx +179 -1
- package/src/Carousel/__tests__/Carousel.slides-per-page.test.tsx +91 -7
- package/src/Carousel/__tests__/Carousel.snap-type.test.tsx +35 -0
- package/src/Carousel/__tests__/Carousel.transition-modes.test.tsx +30 -5
- package/src/Carousel/__tests__/Carousel.translations.test.tsx +23 -0
- package/src/Carousel/__tests__/Carousel.wheel.test.tsx +160 -0
- package/src/Carousel/hooks/index.ts +1 -0
- package/src/Carousel/hooks/useCarouselLoop.ts +869 -0
- package/src/Carousel/hooks/useCarouselRoot.ts +338 -42
- package/src/Carousel/hooks/useCarouselSlide.ts +44 -9
- package/src/Carousel/hooks/useCarouselViewport.ts +649 -46
- package/src/Carousel/loopEngine.ts +54 -0
- package/src/Carousel/types.ts +458 -36
- package/src/Checkbox/Checkbox.tsx +11 -3
- package/src/Checkbox/__tests__/Checkbox.basic-rendering.test.tsx +14 -0
- package/src/Checkbox/hooks/useCheckboxInput.ts +4 -0
- package/src/Checkbox/types.ts +19 -0
- package/src/CheckboxCard/CheckboxCard.tsx +14 -5
- package/src/CheckboxCard/README.md +1 -1
- package/src/CheckboxCard/__tests__/CheckboxCard.basic-rendering.test.tsx +7 -0
- package/src/CheckboxCard/types.ts +65 -17
- package/src/Collapsible/Collapsible.tsx +49 -3
- package/src/Collapsible/README.md +41 -1
- package/src/Collapsible/__tests__/Collapsible.asChild.test.tsx +89 -0
- package/src/Collapsible/__tests__/Collapsible.basic-rendering.test.tsx +27 -0
- package/src/Collapsible/__tests__/Collapsible.collapsedHeight.test.tsx +140 -0
- package/src/Collapsible/hooks/useCollapsibleTrigger.ts +3 -1
- package/src/Collapsible/types.ts +89 -22
- package/src/Combobox/Combobox.tsx +508 -0
- package/src/Combobox/ComboboxContext.ts +23 -0
- package/src/Combobox/README.md +170 -0
- package/src/Combobox/__tests__/Combobox.asChild.test.tsx +62 -0
- package/src/Combobox/__tests__/Combobox.basic-rendering.test.tsx +16 -0
- package/src/Combobox/__tests__/Combobox.content.test.tsx +36 -0
- package/src/Combobox/__tests__/Combobox.controlled-state.test.tsx +88 -0
- package/src/Combobox/__tests__/Combobox.empty.test.tsx +42 -0
- package/src/Combobox/__tests__/Combobox.ids.test.tsx +66 -0
- package/src/Combobox/__tests__/Combobox.item.test.tsx +76 -0
- package/src/Combobox/__tests__/Combobox.keyboard-interaction.test.tsx +196 -0
- package/src/Combobox/__tests__/Combobox.light-dismiss.test.tsx +101 -0
- package/src/Combobox/__tests__/Combobox.query.test.tsx +105 -0
- package/src/Combobox/hooks/index.ts +1 -0
- package/src/Combobox/hooks/useComboboxRoot.ts +2 -0
- package/src/Combobox/index.ts +3 -0
- package/src/Combobox/types.ts +133 -0
- package/src/ContextMenu/ContextMenu.tsx +270 -4
- package/src/ContextMenu/README.md +1 -1
- package/src/ContextMenu/types.ts +148 -6
- package/src/DirectionProvider/DirectionProvider.tsx +41 -9
- package/src/DirectionProvider/README.md +4 -4
- package/src/DirectionProvider/__tests__/DirectionProvider.test.tsx +5 -0
- package/src/DirectionProvider/types.ts +28 -3
- package/src/Divider/Divider.tsx +24 -10
- package/src/Divider/__tests__/Divider.contract.test.tsx +1 -1
- package/src/Divider/__tests__/Divider.test.tsx +5 -0
- package/src/Divider/types.ts +10 -3
- package/src/Drawer/Drawer.tsx +315 -0
- package/src/Drawer/README.md +124 -0
- package/src/Drawer/__tests__/Drawer.behavior.test.tsx +100 -0
- package/src/Drawer/__tests__/Drawer.focus-trap.test.tsx +55 -0
- package/src/Drawer/__tests__/Drawer.rendering.test.tsx +111 -0
- package/src/Drawer/__tests__/Drawer.side.test.tsx +69 -0
- package/src/Drawer/__tests__/dialog-polyfill.ts +39 -0
- package/src/Drawer/index.ts +2 -0
- package/src/Drawer/types.ts +66 -0
- package/src/Dropdown/Dropdown.tsx +119 -2
- package/src/Dropdown/README.md +7 -5
- package/src/Dropdown/types.ts +241 -11
- package/src/EmptyState/EmptyState.tsx +74 -4
- package/src/EmptyState/README.md +2 -2
- package/src/EmptyState/__tests__/EmptyState.Actions.test.tsx +7 -2
- package/src/EmptyState/__tests__/EmptyState.Description.test.tsx +3 -1
- package/src/EmptyState/__tests__/EmptyState.Root.test.tsx +10 -0
- package/src/EmptyState/__tests__/EmptyState.Title.test.tsx +3 -1
- package/src/EmptyState/types.ts +49 -6
- package/src/Field/Field.tsx +23 -0
- package/src/Field/README.md +1 -1
- package/src/Field/__tests__/Field.basic-rendering.test.tsx +11 -0
- package/src/Field/__tests__/Field.error-handling.test.tsx +23 -0
- package/src/Field/__tests__/Field.state-cascade.test.tsx +21 -0
- package/src/Field/hooks/__tests__/useFieldProps.test.tsx +104 -9
- package/src/Field/hooks/useFieldProps.ts +1 -1
- package/src/Field/types.ts +60 -12
- package/src/Fieldset/Fieldset.tsx +55 -19
- package/src/Fieldset/README.md +2 -2
- package/src/Fieldset/__tests__/Fieldset.basic-rendering.test.tsx +9 -0
- package/src/Fieldset/types.ts +20 -2
- package/src/Input/Input.tsx +15 -10
- package/src/Input/README.md +2 -2
- package/src/Input/__tests__/Input.basic-rendering.test.tsx +5 -0
- package/src/Input/types.ts +19 -2
- package/src/InputGroup/InputGroup.tsx +11 -3
- package/src/InputGroup/__tests__/InputGroup.basic-rendering.test.tsx +14 -0
- package/src/InputGroup/types.ts +26 -5
- package/src/Listbox/Listbox.tsx +475 -0
- package/src/Listbox/ListboxContext.ts +48 -0
- package/src/Listbox/README.md +248 -0
- package/src/Listbox/__tests__/Listbox.active-descendant.test.tsx +128 -0
- package/src/Listbox/__tests__/Listbox.asChild.test.tsx +91 -0
- package/src/Listbox/__tests__/Listbox.basic-rendering.test.tsx +16 -0
- package/src/Listbox/__tests__/Listbox.controlled-state.test.tsx +114 -0
- package/src/Listbox/__tests__/Listbox.disabled.test.tsx +90 -0
- package/src/Listbox/__tests__/Listbox.fixtures.ts +27 -0
- package/src/Listbox/__tests__/Listbox.group-label.test.tsx +97 -0
- package/src/Listbox/__tests__/Listbox.group.test.tsx +45 -0
- package/src/Listbox/__tests__/Listbox.keyboard-edge-cases.test.tsx +144 -0
- package/src/Listbox/__tests__/Listbox.keyboard-interaction.test.tsx +112 -0
- package/src/Listbox/__tests__/Listbox.modifier-keys.test.tsx +86 -0
- package/src/Listbox/__tests__/Listbox.multi-select-keyboard.test.tsx +368 -0
- package/src/Listbox/__tests__/Listbox.multiple-mode.test.tsx +91 -0
- package/src/Listbox/__tests__/Listbox.mutation-hardening.test.tsx +480 -0
- package/src/Listbox/__tests__/Listbox.orientation.test.tsx +59 -0
- package/src/Listbox/__tests__/Listbox.reading-direction.test.tsx +65 -0
- package/src/Listbox/__tests__/Listbox.reorder.test.tsx +78 -0
- package/src/Listbox/__tests__/Listbox.selection-follows-focus.test.tsx +64 -0
- package/src/Listbox/__tests__/Listbox.strict-context.test.tsx +21 -0
- package/src/Listbox/__tests__/Listbox.typeahead.test.tsx +123 -0
- package/src/Listbox/__tests__/Listbox.uncontrolled-state.test.tsx +48 -0
- package/src/Listbox/constants.ts +2 -0
- package/src/Listbox/hooks/index.ts +1 -0
- package/src/Listbox/hooks/useListboxRoot.ts +407 -0
- package/src/Listbox/index.ts +3 -0
- package/src/Listbox/types.ts +238 -0
- package/src/MillerColumns/MillerColumns.tsx +186 -45
- package/src/MillerColumns/README.md +90 -20
- package/src/MillerColumns/__tests__/MillerColumns.aria.test.tsx +45 -0
- package/src/MillerColumns/__tests__/MillerColumns.auto-scroll.test.tsx +4 -2
- package/src/MillerColumns/__tests__/MillerColumns.basic-rendering.test.tsx +18 -3
- package/src/MillerColumns/__tests__/MillerColumns.data-attributes.test.tsx +48 -0
- package/src/MillerColumns/__tests__/MillerColumns.keyboard-interaction.test.tsx +35 -2
- package/src/MillerColumns/__tests__/MillerColumns.keyboard-selection-state.test.tsx +96 -0
- package/src/MillerColumns/__tests__/MillerColumns.preview-panel.test.tsx +29 -4
- package/src/MillerColumns/__tests__/MillerColumns.preview-visibility.test.tsx +79 -0
- package/src/MillerColumns/__tests__/MillerColumns.resize.test.tsx +189 -0
- package/src/MillerColumns/__tests__/MillerColumns.styled-parts.test.tsx +143 -0
- package/src/MillerColumns/__tests__/MillerColumns.typeahead.test.tsx +104 -0
- package/src/MillerColumns/constants.ts +2 -0
- package/src/MillerColumns/hooks/useMillerColumnsColumn.ts +5 -2
- package/src/MillerColumns/hooks/useMillerColumnsItem.ts +29 -7
- package/src/MillerColumns/hooks/useMillerColumnsResizeHandle.ts +80 -10
- package/src/MillerColumns/hooks/useMillerColumnsRoot.ts +0 -0
- package/src/MillerColumns/index.ts +4 -0
- package/src/MillerColumns/types.ts +136 -30
- package/src/MillerColumns/utils.ts +69 -3
- package/src/Modal/Modal.tsx +35 -8
- package/src/Modal/README.md +24 -15
- package/src/Modal/__tests__/Modal.focus-trap.test.tsx +162 -0
- package/src/Modal/hooks/useModalContent.ts +32 -0
- package/src/Modal/types.ts +110 -29
- package/src/NavigationMenu/NavigationMenu.tsx +890 -0
- package/src/NavigationMenu/NavigationMenuContext.ts +67 -0
- package/src/NavigationMenu/README.md +355 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.anchor-positioning.test.tsx +132 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.asChild.test.tsx +73 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.basic-rendering.test.tsx +92 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.contract.test.tsx +45 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.controlled-state.test.tsx +134 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.error-handling.test.tsx +62 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.fixtures.tsx +79 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.forceMount.test.tsx +52 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.hover-interaction.test.tsx +254 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.indicator.test.tsx +252 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-interaction.test.tsx +294 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-navigation.test.tsx +126 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.link.test.tsx +139 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.motion.test.tsx +568 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.mouse-interaction.test.tsx +76 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.uncontrolled-state.test.tsx +30 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.viewport.test.tsx +113 -0
- package/src/NavigationMenu/constants.ts +5 -0
- package/src/NavigationMenu/hooks/index.ts +7 -0
- package/src/NavigationMenu/hooks/useNavigationMenuEntry.ts +33 -0
- package/src/NavigationMenu/hooks/useNavigationMenuIndicator.ts +70 -0
- package/src/NavigationMenu/hooks/useNavigationMenuLink.ts +72 -0
- package/src/NavigationMenu/hooks/useNavigationMenuRoot.ts +301 -0
- package/src/NavigationMenu/hooks/useNavigationMenuTopLevelEntry.ts +65 -0
- package/src/NavigationMenu/hooks/useNavigationMenuTrigger.ts +158 -0
- package/src/NavigationMenu/hooks/useNavigationMenuViewport.ts +101 -0
- package/src/NavigationMenu/index.ts +3 -0
- package/src/NavigationMenu/types.ts +259 -0
- package/src/NavigationMenu/utils.ts +89 -0
- package/src/Popover/Popover.tsx +427 -0
- package/src/Popover/PopoverContext.ts +13 -0
- package/src/Popover/README.md +106 -0
- package/src/Popover/__tests__/Popover.anchor.test.tsx +38 -0
- package/src/Popover/__tests__/Popover.asChild.test.tsx +160 -0
- package/src/Popover/__tests__/Popover.basic-rendering.test.tsx +106 -0
- package/src/Popover/__tests__/Popover.click-outside.test.tsx +149 -0
- package/src/Popover/__tests__/Popover.close.test.tsx +70 -0
- package/src/Popover/__tests__/Popover.content.test.tsx +58 -0
- package/src/Popover/__tests__/Popover.controlled-state.test.tsx +45 -0
- package/src/Popover/__tests__/Popover.error-handling.test.tsx +51 -0
- package/src/Popover/__tests__/Popover.focus.test.tsx +43 -0
- package/src/Popover/__tests__/Popover.keyboard-interaction.test.tsx +71 -0
- package/src/Popover/__tests__/Popover.mouse-interaction.test.tsx +69 -0
- package/src/Popover/__tests__/Popover.uncontrolled-state.test.tsx +44 -0
- package/src/Popover/hooks/index.ts +3 -0
- package/src/Popover/hooks/usePopoverContent.ts +98 -0
- package/src/Popover/hooks/usePopoverRoot.ts +84 -0
- package/src/Popover/hooks/usePopoverTrigger.ts +33 -0
- package/src/Popover/index.ts +2 -0
- package/src/Popover/types.ts +197 -0
- package/src/Portal/Portal.tsx +50 -10
- package/src/Portal/README.md +2 -2
- package/src/Portal/__tests__/Portal.basic-rendering.test.tsx +5 -0
- package/src/Portal/types.ts +5 -0
- package/src/Progress/Progress.tsx +33 -1
- package/src/Progress/README.md +1 -1
- package/src/Progress/__tests__/Progress.basic-rendering.test.tsx +10 -0
- package/src/Progress/__tests__/Progress.value.test.tsx +21 -0
- package/src/Progress/types.ts +20 -4
- package/src/Radio/Radio.tsx +10 -2
- package/src/Radio/__tests__/Radio.basic-rendering.test.tsx +14 -0
- package/src/Radio/hooks/useRadioRoot.ts +3 -0
- package/src/Radio/types.ts +20 -0
- package/src/RadioCard/RadioCard.tsx +11 -0
- package/src/RadioCard/__tests__/RadioCard.basic-rendering.test.tsx +8 -0
- package/src/RadioCard/__tests__/RadioCard.disabled-items.test.tsx +27 -0
- package/src/RadioCard/__tests__/RadioCard.keyboard-interaction.test.tsx +53 -0
- package/src/RadioCard/__tests__/RadioCard.ref-forwarding.test.tsx +54 -0
- package/src/RadioCard/__tests__/RadioCard.uncontrolled-state.test.tsx +21 -0
- package/src/RadioCard/hooks/useRadioCardRoot.ts +11 -0
- package/src/RadioCard/types.ts +95 -22
- package/src/RadioGroup/README.md +2 -2
- package/src/RadioGroup/RadioGroup.tsx +12 -1
- package/src/RadioGroup/__tests__/RadioGroup.basic-rendering.test.tsx +8 -0
- package/src/RadioGroup/__tests__/RadioGroup.disabled.test.tsx +27 -0
- package/src/RadioGroup/__tests__/RadioGroup.ref-forwarding.test.tsx +29 -0
- package/src/RadioGroup/__tests__/RadioGroup.roving-tabindex.test.tsx +31 -0
- package/src/RadioGroup/hooks/useRadioGroupRoot.ts +11 -0
- package/src/RadioGroup/types.ts +73 -9
- package/src/SegmentedControl/README.md +154 -0
- package/src/SegmentedControl/SegmentedControl.tsx +309 -0
- package/src/SegmentedControl/SegmentedControlContext.ts +28 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.asChild.test.tsx +50 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.basic-rendering.test.tsx +100 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.controlled-state.test.tsx +107 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.disabled-keydown-guards.test.tsx +62 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.disabled.test.tsx +151 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.error-handling.test.tsx +17 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.keyboard-interaction.test.tsx +117 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.orientation.test.tsx +54 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.reading-direction.test.tsx +56 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.ref-forwarding.test.tsx +45 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.roving-tabindex.test.tsx +104 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.uncontrolled-state.test.tsx +94 -0
- package/src/SegmentedControl/hooks/index.ts +2 -0
- package/src/SegmentedControl/hooks/useSegmentedControlContext.ts +1 -0
- package/src/SegmentedControl/hooks/useSegmentedControlRoot.ts +87 -0
- package/src/SegmentedControl/index.ts +2 -0
- package/src/SegmentedControl/types.ts +138 -0
- package/src/Select/README.md +122 -157
- package/src/Select/Select.tsx +596 -69
- package/src/Select/SelectContext.ts +38 -0
- package/src/Select/SelectItemIndicatorContext.ts +29 -0
- package/src/Select/__tests__/Select.asChild.test.tsx +2 -2
- package/src/Select/__tests__/Select.basic-rendering.test.tsx +22 -3
- package/src/Select/__tests__/Select.controlled-state.test.tsx +6 -6
- package/src/Select/__tests__/Select.data-attributes.test.tsx +4 -4
- package/src/Select/__tests__/Select.field-integration.test.tsx +11 -11
- package/src/Select/__tests__/Select.form.test.tsx +38 -0
- package/src/Select/__tests__/Select.group.test.tsx +6 -6
- package/src/Select/__tests__/Select.keyboard-edge.test.tsx +95 -0
- package/src/Select/__tests__/Select.keyboard.test.tsx +124 -0
- package/src/Select/__tests__/Select.mutation-hardening.test.tsx +317 -0
- package/src/Select/__tests__/Select.native-item.test.tsx +30 -0
- package/src/Select/__tests__/Select.open-close.test.tsx +57 -0
- package/src/Select/__tests__/Select.placeholder.test.tsx +7 -7
- package/src/Select/__tests__/Select.rich-asChild.test.tsx +60 -0
- package/src/Select/__tests__/Select.rich-mode.test.tsx +16 -0
- package/src/Select/__tests__/Select.selection.test.tsx +104 -0
- package/src/Select/__tests__/Select.separator.test.tsx +71 -0
- package/src/Select/__tests__/Select.strict-context.test.tsx +36 -0
- package/src/Select/__tests__/Select.typeahead.test.tsx +156 -0
- package/src/Select/__tests__/Select.value-mirror.test.tsx +240 -0
- package/src/Select/constants.ts +8 -0
- package/src/Select/hooks/index.ts +2 -0
- package/src/Select/hooks/useSelectContent.ts +149 -0
- package/src/Select/hooks/useSelectRoot.ts +149 -0
- package/src/Select/types.ts +199 -31
- package/src/SkipNav/README.md +3 -3
- package/src/SkipNav/SkipNav.tsx +72 -32
- package/src/SkipNav/__tests__/SkipNav.basic-rendering.test.tsx +6 -0
- package/src/SkipNav/types.ts +17 -9
- package/src/Slider/README.md +37 -7
- package/src/Slider/Slider.tsx +42 -4
- package/src/Slider/SliderContext.ts +28 -1
- package/src/Slider/types.ts +157 -9
- package/src/Slot/Slot.tsx +94 -19
- package/src/Slot/__tests__/Slot.test.tsx +112 -2
- package/src/Slot/composeEventHandlers.ts +26 -2
- package/src/Slot/types.ts +16 -0
- package/src/SplitButton/README.md +225 -0
- package/src/SplitButton/SplitButton.tsx +457 -0
- package/src/SplitButton/SplitButtonContext.ts +26 -0
- package/src/SplitButton/__tests__/SplitButton.accessibility.test.tsx +55 -0
- package/src/SplitButton/__tests__/SplitButton.asChild.test.tsx +51 -0
- package/src/SplitButton/__tests__/SplitButton.basic-rendering.test.tsx +57 -0
- package/src/SplitButton/__tests__/SplitButton.data-attributes.test.tsx +28 -0
- package/src/SplitButton/__tests__/SplitButton.disabled.test.tsx +79 -0
- package/src/SplitButton/__tests__/SplitButton.error-handling.test.tsx +29 -0
- package/src/SplitButton/__tests__/SplitButton.keyboard-interaction.test.tsx +56 -0
- package/src/SplitButton/__tests__/SplitButton.state-modes.test.tsx +68 -0
- package/src/SplitButton/index.ts +2 -0
- package/src/SplitButton/types.ts +186 -0
- package/src/Status/Status.tsx +36 -15
- package/src/Status/__tests__/Status.test.tsx +5 -0
- package/src/Status/types.ts +12 -1
- package/src/Switch/README.md +1 -1
- package/src/Switch/Switch.tsx +17 -4
- package/src/Switch/__tests__/Switch.basic-rendering.test.tsx +7 -0
- package/src/Switch/hooks/useSwitchRoot.ts +3 -0
- package/src/Switch/types.ts +49 -22
- package/src/Table/README.md +110 -8
- package/src/Table/Table.tsx +246 -16
- package/src/Table/TableExpandableContext.ts +7 -0
- package/src/Table/__tests__/Table.DetailRow.test.tsx +83 -0
- package/src/Table/__tests__/Table.ExpandTrigger.test.tsx +102 -0
- package/src/Table/__tests__/Table.Expandable.test.tsx +121 -0
- package/src/Table/types.ts +191 -17
- package/src/Tabs/README.md +4 -4
- package/src/Tabs/Tabs.tsx +13 -4
- package/src/Tabs/__tests__/Tabs.activation-mode.test.tsx +58 -0
- package/src/Tabs/__tests__/Tabs.basic-rendering.test.tsx +15 -0
- package/src/Tabs/__tests__/Tabs.change-event-callbacks.test.tsx +24 -0
- package/src/Tabs/__tests__/Tabs.disabled-tabs.test.tsx +36 -0
- package/src/Tabs/__tests__/Tabs.lazy-mount.test.tsx +17 -0
- package/src/Tabs/hooks/useTabsContent.ts +5 -0
- package/src/Tabs/hooks/useTabsRoot.ts +29 -5
- package/src/Tabs/hooks/useTabsTrigger.ts +7 -2
- package/src/Tabs/types.ts +29 -4
- package/src/Textarea/Textarea.tsx +9 -1
- package/src/Textarea/__tests__/Textarea.basic-rendering.test.tsx +5 -0
- package/src/Textarea/types.ts +20 -3
- package/src/Toggle/Toggle.tsx +51 -13
- package/src/Toggle/__tests__/Toggle.basic-rendering.test.tsx +5 -0
- package/src/Toggle/types.ts +54 -4
- package/src/ToggleGroup/README.md +4 -4
- package/src/ToggleGroup/ToggleGroup.tsx +14 -1
- package/src/ToggleGroup/__tests__/ToggleGroup.asChild.test.tsx +20 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.basic-rendering.test.tsx +9 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.dynamic-items.test.tsx +70 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.error-handling.test.tsx +23 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.keyboard-interaction.test.tsx +23 -1
- package/src/ToggleGroup/__tests__/ToggleGroup.ref-forwarding.test.tsx +58 -0
- package/src/ToggleGroup/hooks/useToggleGroupRoot.ts +21 -1
- package/src/ToggleGroup/types.ts +55 -2
- package/src/Tooltip/README.md +7 -7
- package/src/Tooltip/Tooltip.tsx +100 -22
- package/src/Tooltip/__tests__/Tooltip.basic-rendering.test.tsx +50 -0
- package/src/Tooltip/__tests__/Tooltip.error-handling.test.tsx +37 -0
- package/src/Tooltip/__tests__/Tooltip.escape-hatches.test.tsx +73 -0
- package/src/Tooltip/__tests__/Tooltip.keyboard-interaction.test.tsx +134 -0
- package/src/Tooltip/__tests__/Tooltip.skip-delay.test.tsx +216 -0
- package/src/Tooltip/__tests__/Tooltip.timer-coordination.test.tsx +223 -0
- package/src/Tooltip/hooks/__tests__/useTooltipProvider.test.tsx +40 -0
- package/src/Tooltip/hooks/useTooltipContent.ts +3 -0
- package/src/Tooltip/hooks/useTooltipProvider.ts +46 -8
- package/src/Tooltip/hooks/useTooltipRoot.ts +30 -6
- package/src/Tooltip/types.ts +89 -8
- package/src/Tree/README.md +69 -5
- package/src/Tree/Tree.tsx +188 -34
- package/src/Tree/__tests__/Tree.styled-wrapper.test.tsx +159 -0
- package/src/Tree/hooks/useTreePath.ts +33 -1
- package/src/Tree/index.ts +6 -1
- package/src/Tree/types.ts +54 -12
- package/src/Tree/utils.ts +46 -4
- package/src/VisuallyHidden/VisuallyHidden.tsx +33 -8
- package/src/VisuallyHidden/__tests__/VisuallyHidden.test.tsx +15 -1
- package/src/VisuallyHidden/types.ts +11 -1
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useTypeahead.ts +95 -0
- package/src/index.ts +10 -0
- package/src/test/intersectionObserverPolyfill.ts +7 -0
- package/src/test/resizeObserverPolyfill.ts +80 -0
- package/src/useLocalStorage/__tests__/useLocalStorage.reading.test.tsx +31 -0
- package/src/useLocalStorage/__tests__/useLocalStorage.resilience.test.tsx +97 -0
- package/src/useLocalStorage/__tests__/useLocalStorage.ssr.test.tsx +20 -0
- package/src/useLocalStorage/__tests__/useLocalStorage.writing.test.tsx +72 -0
- package/src/useLocalStorage/index.ts +1 -0
- package/src/useLocalStorage/useLocalStorage.ts +140 -0
- package/src/useMediaQuery/README.md +66 -0
- package/src/useMediaQuery/__tests__/useMediaQuery.matching.test.tsx +42 -0
- package/src/useMediaQuery/__tests__/useMediaQuery.ssr.test.tsx +19 -0
- package/src/useMediaQuery/__tests__/useMediaQuery.subscription.test.tsx +89 -0
- package/src/useMediaQuery/index.ts +1 -0
- package/src/useMediaQuery/useMediaQuery.ts +49 -0
- package/src/usePagination/README.md +170 -0
- package/src/usePagination/__tests__/usePagination.navigation.test.tsx +109 -0
- package/src/usePagination/__tests__/usePagination.page-count.test.tsx +56 -0
- package/src/usePagination/__tests__/usePagination.page-state.test.tsx +87 -0
- package/src/usePagination/__tests__/usePagination.range.test.tsx +82 -0
- package/src/usePagination/__tests__/usePagination.slice.test.tsx +59 -0
- package/src/usePagination/__tests__/usePagination.windowing.test.tsx +68 -0
- package/src/usePagination/index.ts +1 -0
- package/src/usePagination/usePagination.ts +292 -0
- package/src/utils/focusable.ts +15 -0
- package/src/utils/getKeyToActionMap.ts +1 -1
- package/src/utils/index.ts +1 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useEffect,
|
|
3
|
+
useId,
|
|
4
|
+
useMemo,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
type ReactElement,
|
|
8
|
+
} from "react";
|
|
9
|
+
|
|
10
|
+
import { useDirection } from "../DirectionProvider/index.ts";
|
|
11
|
+
import { Slot, composeEventHandlers, composeRefs } from "../Slot/index.ts";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
ListboxGroupProvider,
|
|
15
|
+
ListboxProvider,
|
|
16
|
+
useListboxContext,
|
|
17
|
+
useListboxGroupContext,
|
|
18
|
+
} from "./ListboxContext";
|
|
19
|
+
import { useListboxRoot } from "./hooks/index.ts";
|
|
20
|
+
import type {
|
|
21
|
+
ListboxGroupLabelProps,
|
|
22
|
+
ListboxGroupProps,
|
|
23
|
+
ListboxOptionProps,
|
|
24
|
+
ListboxRootProps,
|
|
25
|
+
} from "./types";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The root of a Listbox — a `<div role="listbox">` that owns the selection,
|
|
29
|
+
* holds the single tab stop, and provides {@link ListboxContext} to its
|
|
30
|
+
* descendant {@link ListboxOption | `Listbox.Option`}s.
|
|
31
|
+
*
|
|
32
|
+
* **Virtual focus, not roving tabindex.** This is the distinguishing choice.
|
|
33
|
+
* Real DOM focus stays on the root; the visually-focused option is published
|
|
34
|
+
* as `aria-activedescendant` and marked `data-highlighted`. Options are never
|
|
35
|
+
* given a `tabIndex` and are never `focus()`ed. That is what lets a text input
|
|
36
|
+
* elsewhere keep DOM focus while driving this list — the combobox / command
|
|
37
|
+
* palette case — which a roving tabstop cannot do. Per APG, the cursor is
|
|
38
|
+
* seeded when the root takes focus (onto the first selected option, or the
|
|
39
|
+
* first enabled one) and the active option is scrolled into view as it moves.
|
|
40
|
+
*
|
|
41
|
+
* Two type modes, statically discriminated at the type level:
|
|
42
|
+
*
|
|
43
|
+
* - **`type="single"`** — at most one option selected. Re-selecting the current
|
|
44
|
+
* option is a no-op, not a deselect. Controlled via `value: string` +
|
|
45
|
+
* `onValueChange: (value: string) => void`.
|
|
46
|
+
* - **`type="multiple"`** — options toggle independently, and the root carries
|
|
47
|
+
* `aria-multiselectable`. Controlled via `value: string[]` +
|
|
48
|
+
* `onValueChange: (value: string[]) => void`.
|
|
49
|
+
*
|
|
50
|
+
* Each mode supports **uncontrolled** (`defaultValue`, or omit for an empty
|
|
51
|
+
* selection) and **controlled** (`value` + `onValueChange`) shapes.
|
|
52
|
+
*
|
|
53
|
+
* **Keyboard.** Arrow keys move the cursor along the
|
|
54
|
+
* {@link ListboxRootBaseProps.orientation | orientation} axis, wrapping at
|
|
55
|
+
* both ends; Home/End jump to the first/last option; Enter and Space select
|
|
56
|
+
* (single) or toggle (multiple) the cursor option; printable characters run a
|
|
57
|
+
* prefix typeahead. Selection is manual by default — set
|
|
58
|
+
* {@link ListboxRootBaseProps.selectionFollowsFocus | `selectionFollowsFocus`}
|
|
59
|
+
* for the APG example's select-as-you-arrow behaviour.
|
|
60
|
+
*
|
|
61
|
+
* In `type="multiple"`, APG's optional modifier shortcuts are implemented too:
|
|
62
|
+
* `Shift+Arrow` moves the cursor and selects what it lands on (without
|
|
63
|
+
* wrapping), `Ctrl`/`Cmd`+`Shift`+`Home`/`End` sweeps the selection to that
|
|
64
|
+
* edge, and `Ctrl`/`Cmd`+`A` selects every enabled option or clears the
|
|
65
|
+
* selection when all are already selected. Every **other** chorded shortcut is
|
|
66
|
+
* deliberately left alone — `Alt+Arrow` and unclaimed `Ctrl`/`Cmd` combinations
|
|
67
|
+
* reach your own `onKeyDown` and the browser untouched, so a toolbar can bind
|
|
68
|
+
* them.
|
|
69
|
+
*
|
|
70
|
+
* **Reordering.** Navigation order is read from the live DOM on each
|
|
71
|
+
* interaction rather than from mount order, so moving an option in place is
|
|
72
|
+
* picked up immediately.
|
|
73
|
+
*
|
|
74
|
+
* **ARIA.** `role="listbox"` on the root, plus `aria-activedescendant`,
|
|
75
|
+
* `aria-multiselectable` (multiple mode only) and `aria-orientation`
|
|
76
|
+
* (horizontal only — vertical is the ARIA default). The root needs an
|
|
77
|
+
* accessible name: pass `aria-label` or `aria-labelledby`.
|
|
78
|
+
*
|
|
79
|
+
* **Styling hooks.** `data-orientation="horizontal" | "vertical"` on the root.
|
|
80
|
+
*
|
|
81
|
+
* @extends HTMLDivElement
|
|
82
|
+
*
|
|
83
|
+
* @example Single selection
|
|
84
|
+
* ```tsx
|
|
85
|
+
* <Listbox.Root type="single" defaultValue="apple" aria-label="Fruit">
|
|
86
|
+
* <Listbox.Option value="apple">Apple</Listbox.Option>
|
|
87
|
+
* <Listbox.Option value="banana">Banana</Listbox.Option>
|
|
88
|
+
* </Listbox.Root>
|
|
89
|
+
* ```
|
|
90
|
+
*
|
|
91
|
+
* @example Multiple selection, controlled
|
|
92
|
+
* ```tsx
|
|
93
|
+
* const [value, setValue] = useState<string[]>([]);
|
|
94
|
+
*
|
|
95
|
+
* <Listbox.Root type="multiple" value={value} onValueChange={setValue} aria-label="Toppings">
|
|
96
|
+
* <Listbox.Option value="cheese">Cheese</Listbox.Option>
|
|
97
|
+
* <Listbox.Option value="olives">Olives</Listbox.Option>
|
|
98
|
+
* </Listbox.Root>
|
|
99
|
+
* ```
|
|
100
|
+
*
|
|
101
|
+
* @example As a semantic list
|
|
102
|
+
* ```tsx
|
|
103
|
+
* <Listbox.Root type="single" asChild aria-label="Fruit">
|
|
104
|
+
* <ul>
|
|
105
|
+
* <Listbox.Option value="apple" asChild>
|
|
106
|
+
* <li>Apple</li>
|
|
107
|
+
* </Listbox.Option>
|
|
108
|
+
* </ul>
|
|
109
|
+
* </Listbox.Root>
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
export function ListboxRoot({
|
|
113
|
+
type,
|
|
114
|
+
defaultValue,
|
|
115
|
+
value: controlledValue,
|
|
116
|
+
onValueChange,
|
|
117
|
+
selectionFollowsFocus = false,
|
|
118
|
+
orientation = "vertical",
|
|
119
|
+
dir,
|
|
120
|
+
asChild = false,
|
|
121
|
+
onFocus,
|
|
122
|
+
onBlur,
|
|
123
|
+
onKeyDown,
|
|
124
|
+
children,
|
|
125
|
+
ref,
|
|
126
|
+
...rest
|
|
127
|
+
}: ListboxRootProps): ReactElement {
|
|
128
|
+
const resolvedDir = dir ?? useDirection();
|
|
129
|
+
const {
|
|
130
|
+
selectedValues,
|
|
131
|
+
select,
|
|
132
|
+
activeValue,
|
|
133
|
+
registerOption,
|
|
134
|
+
getOptionId,
|
|
135
|
+
seedActiveValue,
|
|
136
|
+
clearActiveValue,
|
|
137
|
+
handleKeyDown,
|
|
138
|
+
moveCursor,
|
|
139
|
+
} = useListboxRoot({
|
|
140
|
+
type,
|
|
141
|
+
defaultValue,
|
|
142
|
+
value: controlledValue,
|
|
143
|
+
onValueChange,
|
|
144
|
+
selectionFollowsFocus,
|
|
145
|
+
orientation,
|
|
146
|
+
dir: resolvedDir,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const contextValue = useMemo(
|
|
150
|
+
() => ({
|
|
151
|
+
selectedValues,
|
|
152
|
+
select,
|
|
153
|
+
activeValue,
|
|
154
|
+
registerOption,
|
|
155
|
+
getOptionId,
|
|
156
|
+
moveCursor,
|
|
157
|
+
}),
|
|
158
|
+
[
|
|
159
|
+
selectedValues,
|
|
160
|
+
select,
|
|
161
|
+
activeValue,
|
|
162
|
+
registerOption,
|
|
163
|
+
getOptionId,
|
|
164
|
+
moveCursor,
|
|
165
|
+
],
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const rootProps = {
|
|
169
|
+
...rest,
|
|
170
|
+
ref,
|
|
171
|
+
role: "listbox" as const,
|
|
172
|
+
tabIndex: 0,
|
|
173
|
+
"aria-multiselectable": type === "multiple" ? true : undefined,
|
|
174
|
+
"aria-orientation":
|
|
175
|
+
orientation === "horizontal" ? ("horizontal" as const) : undefined,
|
|
176
|
+
"data-orientation": orientation,
|
|
177
|
+
"aria-activedescendant":
|
|
178
|
+
activeValue === undefined ? undefined : getOptionId(activeValue),
|
|
179
|
+
onFocus: composeEventHandlers(onFocus, seedActiveValue),
|
|
180
|
+
onBlur: composeEventHandlers(onBlur, clearActiveValue),
|
|
181
|
+
onKeyDown: composeEventHandlers(onKeyDown, handleKeyDown),
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<ListboxProvider value={contextValue}>
|
|
186
|
+
{asChild ? (
|
|
187
|
+
<Slot {...rootProps}>{children}</Slot>
|
|
188
|
+
) : (
|
|
189
|
+
<div {...rootProps}>{children}</div>
|
|
190
|
+
)}
|
|
191
|
+
</ListboxProvider>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** @internal */
|
|
196
|
+
// Stryker disable next-line StringLiteral: overwritten by the compound alias — an equivalent mutant.
|
|
197
|
+
ListboxRoot.displayName = "ListboxRoot";
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* A single choice inside a Listbox — a `<div role="option">` reporting its own
|
|
201
|
+
* selected and highlighted state.
|
|
202
|
+
*
|
|
203
|
+
* **No tab stop, no DOM focus.** The option is never focusable; the root owns
|
|
204
|
+
* focus and points at this element with `aria-activedescendant` when the
|
|
205
|
+
* cursor reaches it. Style the cursor from `data-highlighted`, not `:focus`.
|
|
206
|
+
*
|
|
207
|
+
* **Selection.** Clicking an option selects it (single) or toggles it
|
|
208
|
+
* (multiple), as does Enter / Space while the cursor is on it. A click also
|
|
209
|
+
* moves the cursor onto the clicked option, so a following arrow key resumes
|
|
210
|
+
* from there — APG treats the clicked option as the focused one.
|
|
211
|
+
*
|
|
212
|
+
* **Hover does nothing.** Unlike `Select`'s popup listbox, pointing at an
|
|
213
|
+
* option does not move the cursor: a mouse resting over the list would
|
|
214
|
+
* otherwise steal it mid-keystroke. Give hover its own weaker treatment in
|
|
215
|
+
* CSS, distinct from `data-highlighted`.
|
|
216
|
+
*
|
|
217
|
+
* **Disabled.** `disabled` sets `aria-disabled` and removes the option from
|
|
218
|
+
* cursor navigation, focus seeding, and typeahead — it stays in the
|
|
219
|
+
* accessibility tree and in the DOM. This is a house extension: APG's listbox
|
|
220
|
+
* pattern does not itself specify disabled options.
|
|
221
|
+
*
|
|
222
|
+
* **Typeahead matching** reads the option's rendered text, so keep the label
|
|
223
|
+
* in `children`.
|
|
224
|
+
*
|
|
225
|
+
* **Styling hooks.** `data-highlighted=""` when the cursor is on it,
|
|
226
|
+
* `data-disabled=""` when disabled. Selection is read from `aria-selected`.
|
|
227
|
+
*
|
|
228
|
+
* @throws if rendered outside a `Listbox.Root`.
|
|
229
|
+
*
|
|
230
|
+
* @extends HTMLDivElement
|
|
231
|
+
*
|
|
232
|
+
* @example
|
|
233
|
+
* ```tsx
|
|
234
|
+
* <Listbox.Option value="apple">Apple</Listbox.Option>
|
|
235
|
+
* <Listbox.Option value="durian" disabled>Durian (out of stock)</Listbox.Option>
|
|
236
|
+
* ```
|
|
237
|
+
*/
|
|
238
|
+
export function ListboxOption({
|
|
239
|
+
value,
|
|
240
|
+
disabled = false,
|
|
241
|
+
asChild = false,
|
|
242
|
+
onClick,
|
|
243
|
+
children,
|
|
244
|
+
ref,
|
|
245
|
+
...rest
|
|
246
|
+
}: ListboxOptionProps): ReactElement {
|
|
247
|
+
const {
|
|
248
|
+
selectedValues,
|
|
249
|
+
select,
|
|
250
|
+
activeValue,
|
|
251
|
+
registerOption,
|
|
252
|
+
getOptionId,
|
|
253
|
+
moveCursor,
|
|
254
|
+
} = useListboxContext();
|
|
255
|
+
const selected = selectedValues.includes(value);
|
|
256
|
+
|
|
257
|
+
const localRef = useRef<HTMLDivElement | null>(null);
|
|
258
|
+
// Stryker disable next-line ArrayDeclaration: equivalent — localRef is a stable RefObject, so the composed ref behaves identically however the deps are spelled.
|
|
259
|
+
const setRef = useMemo(() => composeRefs(localRef, ref), [ref]);
|
|
260
|
+
|
|
261
|
+
useEffect(() => {
|
|
262
|
+
registerOption(value, localRef.current, disabled);
|
|
263
|
+
return () => registerOption(value, null);
|
|
264
|
+
}, [value, disabled, registerOption]);
|
|
265
|
+
|
|
266
|
+
const optionProps = {
|
|
267
|
+
...rest,
|
|
268
|
+
ref: setRef,
|
|
269
|
+
id: getOptionId(value),
|
|
270
|
+
role: "option" as const,
|
|
271
|
+
"aria-selected": selected,
|
|
272
|
+
"aria-disabled": disabled || undefined,
|
|
273
|
+
"data-disabled": disabled ? "" : undefined,
|
|
274
|
+
"data-highlighted": activeValue === value ? "" : undefined,
|
|
275
|
+
onClick: composeEventHandlers(onClick, () => {
|
|
276
|
+
if (disabled) return;
|
|
277
|
+
select(value);
|
|
278
|
+
// A click is a deliberate act of choosing, so it also becomes the
|
|
279
|
+
// cursor's position — APG treats the clicked option as the focused one.
|
|
280
|
+
// Without this the two ways of choosing disagree about where you are in
|
|
281
|
+
// the list: focus (which fires first) seeds the cursor onto the
|
|
282
|
+
// previously-selected option, and the next arrow key would resume from
|
|
283
|
+
// there rather than from the row just clicked. Deliberately not
|
|
284
|
+
// `navigateTo`, which would re-apply selectionFollowsFocus and, in
|
|
285
|
+
// multiple mode, toggle the option straight back off.
|
|
286
|
+
moveCursor(value);
|
|
287
|
+
}),
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
return asChild ? (
|
|
291
|
+
<Slot {...optionProps}>{children}</Slot>
|
|
292
|
+
) : (
|
|
293
|
+
<div {...optionProps}>{children}</div>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** @internal */
|
|
298
|
+
// Stryker disable next-line StringLiteral: equivalent — a DevTools label, asserted by no behaviour.
|
|
299
|
+
ListboxOption.displayName = "ListboxOption";
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* A titled cluster of options — a `<div role="group">` carrying the accessible
|
|
303
|
+
* name APG requires on every option group.
|
|
304
|
+
*
|
|
305
|
+
* Grouping is presentational only: the cursor still walks every option in DOM
|
|
306
|
+
* order, crossing group boundaries without stopping, and typeahead searches
|
|
307
|
+
* the whole list.
|
|
308
|
+
*
|
|
309
|
+
* @extends HTMLDivElement
|
|
310
|
+
*
|
|
311
|
+
* @example
|
|
312
|
+
* ```tsx
|
|
313
|
+
* <Listbox.Root type="single" aria-label="Fruit">
|
|
314
|
+
* <Listbox.Group label="Citrus">
|
|
315
|
+
* <Listbox.Option value="lemon">Lemon</Listbox.Option>
|
|
316
|
+
* </Listbox.Group>
|
|
317
|
+
* <Listbox.Group label="Berries">
|
|
318
|
+
* <Listbox.Option value="fig">Fig</Listbox.Option>
|
|
319
|
+
* </Listbox.Group>
|
|
320
|
+
* </Listbox.Root>
|
|
321
|
+
* ```
|
|
322
|
+
*/
|
|
323
|
+
export function ListboxGroup({
|
|
324
|
+
label,
|
|
325
|
+
asChild = false,
|
|
326
|
+
children,
|
|
327
|
+
ref,
|
|
328
|
+
...rest
|
|
329
|
+
}: ListboxGroupProps): ReactElement {
|
|
330
|
+
const labelId = useId();
|
|
331
|
+
const [hasLabel, setHasLabel] = useState(false);
|
|
332
|
+
|
|
333
|
+
const contextValue = useMemo(
|
|
334
|
+
() => ({ labelId, registerLabel: setHasLabel }),
|
|
335
|
+
// Stryker disable next-line ArrayDeclaration: equivalent — labelId is a useId() constant and setHasLabel is a stable setState, so an empty dep array memoises identically.
|
|
336
|
+
[labelId],
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
const groupProps = {
|
|
340
|
+
...rest,
|
|
341
|
+
ref,
|
|
342
|
+
role: "group" as const,
|
|
343
|
+
// A rendered GroupLabel wins: aria-labelledby outranks aria-label in the
|
|
344
|
+
// accessible-name algorithm, so emitting both would be misleading.
|
|
345
|
+
"aria-labelledby": hasLabel ? labelId : undefined,
|
|
346
|
+
"aria-label": hasLabel ? undefined : label,
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
return (
|
|
350
|
+
<ListboxGroupProvider value={contextValue}>
|
|
351
|
+
{asChild ? (
|
|
352
|
+
<Slot {...groupProps}>{children}</Slot>
|
|
353
|
+
) : (
|
|
354
|
+
<div {...groupProps}>{children}</div>
|
|
355
|
+
)}
|
|
356
|
+
</ListboxGroupProvider>
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** @internal */
|
|
361
|
+
// Stryker disable next-line StringLiteral: equivalent — a DevTools label, asserted by no behaviour.
|
|
362
|
+
ListboxGroup.displayName = "ListboxGroup";
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* The visible heading that names its enclosing
|
|
366
|
+
* {@link ListboxGroup | `Listbox.Group`} — APG's grouped-options example
|
|
367
|
+
* labels each group this way rather than with an invisible `aria-label`.
|
|
368
|
+
*
|
|
369
|
+
* Renders `role="presentation"` so the heading is not exposed as an option
|
|
370
|
+
* (only `role="option"` elements may sit inside a listbox), while still being
|
|
371
|
+
* referenceable: the Group points `aria-labelledby` at this element's id,
|
|
372
|
+
* which is derived and wired automatically.
|
|
373
|
+
*
|
|
374
|
+
* @throws if rendered outside a `Listbox.Group`.
|
|
375
|
+
*
|
|
376
|
+
* @extends HTMLDivElement
|
|
377
|
+
*
|
|
378
|
+
* @example
|
|
379
|
+
* ```tsx
|
|
380
|
+
* <Listbox.Group>
|
|
381
|
+
* <Listbox.GroupLabel>Land</Listbox.GroupLabel>
|
|
382
|
+
* <Listbox.Option value="cat">Cat</Listbox.Option>
|
|
383
|
+
* </Listbox.Group>
|
|
384
|
+
* ```
|
|
385
|
+
*/
|
|
386
|
+
export function ListboxGroupLabel({
|
|
387
|
+
asChild = false,
|
|
388
|
+
children,
|
|
389
|
+
ref,
|
|
390
|
+
...rest
|
|
391
|
+
}: ListboxGroupLabelProps): ReactElement {
|
|
392
|
+
const { labelId, registerLabel } = useListboxGroupContext();
|
|
393
|
+
|
|
394
|
+
useEffect(
|
|
395
|
+
() => {
|
|
396
|
+
registerLabel(true);
|
|
397
|
+
return () => registerLabel(false);
|
|
398
|
+
},
|
|
399
|
+
// Stryker disable next-line ArrayDeclaration: equivalent — registerLabel is a stable setState from the Group above.
|
|
400
|
+
[registerLabel],
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
const labelProps = {
|
|
404
|
+
...rest,
|
|
405
|
+
ref,
|
|
406
|
+
id: labelId,
|
|
407
|
+
role: "presentation" as const,
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
return asChild ? (
|
|
411
|
+
<Slot {...labelProps}>{children}</Slot>
|
|
412
|
+
) : (
|
|
413
|
+
<div {...labelProps}>{children}</div>
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** @internal */
|
|
418
|
+
// Stryker disable next-line StringLiteral: equivalent — a DevTools label, asserted by no behaviour.
|
|
419
|
+
ListboxGroupLabel.displayName = "ListboxGroupLabel";
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* The shape of the exported `Listbox` value — callable as `Listbox.Root` and
|
|
423
|
+
* carrying `Root`, `Option` and `Group` as static properties.
|
|
424
|
+
*/
|
|
425
|
+
export type TListboxCompound = typeof ListboxRoot & {
|
|
426
|
+
Root: typeof ListboxRoot;
|
|
427
|
+
Option: typeof ListboxOption;
|
|
428
|
+
Group: typeof ListboxGroup;
|
|
429
|
+
GroupLabel: typeof ListboxGroupLabel;
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Headless, accessible **Listbox** — a persistently-visible list of options
|
|
434
|
+
* implementing the WAI-ARIA APG listbox pattern with a virtual-focus cursor.
|
|
435
|
+
*
|
|
436
|
+
* Unlike `Select`, nothing here opens or closes: there is no trigger and no
|
|
437
|
+
* popup layer. And unlike every roving-tabindex collection in this library,
|
|
438
|
+
* DOM focus never moves to an option — the root keeps it and publishes
|
|
439
|
+
* `aria-activedescendant`, so an external control (a search input, a command
|
|
440
|
+
* palette) can hold focus and drive the list.
|
|
441
|
+
*
|
|
442
|
+
* `Listbox` is both callable (an alias of {@link ListboxRoot | `Listbox.Root`})
|
|
443
|
+
* and carries its sub-components as static properties.
|
|
444
|
+
*
|
|
445
|
+
* - {@link ListboxRoot | `Listbox.Root`} — state owner, context provider, tab
|
|
446
|
+
* stop, `<div role="listbox">` wrapper.
|
|
447
|
+
* - {@link ListboxOption | `Listbox.Option`} — one selectable choice.
|
|
448
|
+
* - {@link ListboxGroup | `Listbox.Group`} — a named cluster of options.
|
|
449
|
+
* - {@link ListboxGroupLabel | `Listbox.GroupLabel`} — a group's visible heading.
|
|
450
|
+
*
|
|
451
|
+
* @example
|
|
452
|
+
* ```tsx
|
|
453
|
+
* import { Listbox } from "@primitiv-ui/react";
|
|
454
|
+
*
|
|
455
|
+
* <Listbox.Root type="single" defaultValue="apple" aria-label="Fruit">
|
|
456
|
+
* <Listbox.Option value="apple">Apple</Listbox.Option>
|
|
457
|
+
* <Listbox.Option value="banana">Banana</Listbox.Option>
|
|
458
|
+
* </Listbox.Root>;
|
|
459
|
+
* ```
|
|
460
|
+
*
|
|
461
|
+
* @see {@link ListboxRoot} for selection modes, keyboard, and ARIA.
|
|
462
|
+
* @see {@link ListboxOption} for per-option state and disabling.
|
|
463
|
+
* @see {@link ListboxGroup} for grouping.
|
|
464
|
+
*/
|
|
465
|
+
const ListboxCompound: TListboxCompound = Object.assign(ListboxRoot, {
|
|
466
|
+
Root: ListboxRoot,
|
|
467
|
+
Option: ListboxOption,
|
|
468
|
+
Group: ListboxGroup,
|
|
469
|
+
GroupLabel: ListboxGroupLabel,
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
// Stryker disable next-line StringLiteral: equivalent — a DevTools label, asserted by no behaviour.
|
|
473
|
+
ListboxCompound.displayName = "Listbox";
|
|
474
|
+
|
|
475
|
+
export { ListboxCompound as Listbox };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Context, Provider } from "react";
|
|
2
|
+
|
|
3
|
+
import { createStrictContext } from "../utils/index.ts";
|
|
4
|
+
|
|
5
|
+
import type { ListboxContextValue, ListboxGroupContextValue } from "./types";
|
|
6
|
+
|
|
7
|
+
const listboxContextPair = createStrictContext<ListboxContextValue>(
|
|
8
|
+
"Listbox.Option must be rendered as a child of Listbox.Root",
|
|
9
|
+
// Stryker disable next-line StringLiteral: equivalent — a DevTools label, asserted by no behaviour.
|
|
10
|
+
"ListboxContext",
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
/** Strict React context carrying the {@link ListboxContextValue} from
|
|
14
|
+
* `Listbox.Root` to its options. `null` outside a Root. */
|
|
15
|
+
export const ListboxContext: Context<ListboxContextValue | null> =
|
|
16
|
+
listboxContextPair[0];
|
|
17
|
+
|
|
18
|
+
/** Reads the {@link ListboxContextValue}; throws outside a `Listbox.Root`. */
|
|
19
|
+
export const useListboxContext: () => ListboxContextValue =
|
|
20
|
+
listboxContextPair[1];
|
|
21
|
+
|
|
22
|
+
/** Provider for {@link ListboxContext}, used by `Listbox.Root`. */
|
|
23
|
+
const ListboxProvider: Provider<ListboxContextValue | null> =
|
|
24
|
+
ListboxContext.Provider;
|
|
25
|
+
|
|
26
|
+
export { ListboxProvider };
|
|
27
|
+
|
|
28
|
+
const listboxGroupContextPair = createStrictContext<ListboxGroupContextValue>(
|
|
29
|
+
"Listbox.GroupLabel must be rendered as a child of Listbox.Group",
|
|
30
|
+
// Stryker disable next-line StringLiteral: equivalent — a DevTools label, asserted by no behaviour.
|
|
31
|
+
"ListboxGroupContext",
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
/** Strict React context carrying the {@link ListboxGroupContextValue} from
|
|
35
|
+
* `Listbox.Group` to its label. `null` outside a Group. */
|
|
36
|
+
export const ListboxGroupContext: Context<ListboxGroupContextValue | null> =
|
|
37
|
+
listboxGroupContextPair[0];
|
|
38
|
+
|
|
39
|
+
/** Reads the {@link ListboxGroupContextValue}; throws outside a
|
|
40
|
+
* `Listbox.Group`. */
|
|
41
|
+
export const useListboxGroupContext: () => ListboxGroupContextValue =
|
|
42
|
+
listboxGroupContextPair[1];
|
|
43
|
+
|
|
44
|
+
/** Provider for {@link ListboxGroupContext}, used by `Listbox.Group`. */
|
|
45
|
+
const ListboxGroupProvider: Provider<ListboxGroupContextValue | null> =
|
|
46
|
+
ListboxGroupContext.Provider;
|
|
47
|
+
|
|
48
|
+
export { ListboxGroupProvider };
|