@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
|
@@ -132,8 +132,8 @@ describe("Slot tests", () => {
|
|
|
132
132
|
const baseElement = React.createElement("div", null, "text");
|
|
133
133
|
const react18Element = {
|
|
134
134
|
...baseElement,
|
|
135
|
-
ref: childRef, // React ≤18: ref lives here
|
|
136
|
-
props: { children: "text" }, //
|
|
135
|
+
ref: childRef, // React ≤18: ref lives here ...
|
|
136
|
+
props: { children: "text" }, // ... not in props
|
|
137
137
|
} as unknown as React.ReactElement;
|
|
138
138
|
const SlotWithRef = React.forwardRef<HTMLElement>((_props, ref) => (
|
|
139
139
|
<Slot ref={ref}>{react18Element}</Slot>
|
|
@@ -159,5 +159,115 @@ describe("Slot tests", () => {
|
|
|
159
159
|
// Assert
|
|
160
160
|
expect(screen.getByText("text")).toHaveClass("slot-class", "child-class");
|
|
161
161
|
});
|
|
162
|
+
|
|
163
|
+
it("drops a falsy className from either side (no stray whitespace)", () => {
|
|
164
|
+
// Arrange — child provides an empty className; the filter must remove it.
|
|
165
|
+
const { container } = render(
|
|
166
|
+
<Slot className="slot-class">
|
|
167
|
+
<span className="">text</span>
|
|
168
|
+
</Slot>,
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
// Assert — exactly "slot-class", not "slot-class " with a trailing space
|
|
172
|
+
expect((container.firstChild as HTMLElement).getAttribute("class")).toBe(
|
|
173
|
+
"slot-class",
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe("ref composition (exercised through the component)", () => {
|
|
179
|
+
it("calls a function ref forwarded through the Slot with the element", () => {
|
|
180
|
+
// Arrange
|
|
181
|
+
const fnRef = vi.fn();
|
|
182
|
+
|
|
183
|
+
// Act — a function ref on the Slot is composed onto the child element
|
|
184
|
+
render(
|
|
185
|
+
<Slot ref={fnRef}>
|
|
186
|
+
<button type="button">Click me</button>
|
|
187
|
+
</Slot>,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
// Assert
|
|
191
|
+
expect(fnRef).toHaveBeenCalledWith(screen.getByRole("button"));
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("populates an object ref and tolerates a child with no ref of its own", () => {
|
|
195
|
+
// Arrange — the Slot's object ref composes with the child's (absent) ref;
|
|
196
|
+
// the absent side must be skipped, not dereferenced.
|
|
197
|
+
const objRef = createRef<HTMLElement>();
|
|
198
|
+
|
|
199
|
+
// Act
|
|
200
|
+
render(
|
|
201
|
+
<Slot ref={objRef}>
|
|
202
|
+
<button type="button">Click me</button>
|
|
203
|
+
</Slot>,
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
// Assert
|
|
207
|
+
expect(objRef.current).toBe(screen.getByRole("button"));
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe("mergeProps — event handlers (both sides)", () => {
|
|
212
|
+
it("composes handlers when both provide one — child first, then slot", async () => {
|
|
213
|
+
// Arrange
|
|
214
|
+
const user = userEvent.setup();
|
|
215
|
+
const calls: string[] = [];
|
|
216
|
+
const slotClick = vi.fn(() => calls.push("slot"));
|
|
217
|
+
const childClick = vi.fn(() => calls.push("child"));
|
|
218
|
+
render(
|
|
219
|
+
<Slot onClick={slotClick}>
|
|
220
|
+
<button type="button" onClick={childClick}>
|
|
221
|
+
Click me
|
|
222
|
+
</button>
|
|
223
|
+
</Slot>,
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
// Act
|
|
227
|
+
await user.click(screen.getByRole("button"));
|
|
228
|
+
|
|
229
|
+
// Assert — both fire, child's handler first
|
|
230
|
+
expect(childClick).toHaveBeenCalledTimes(1);
|
|
231
|
+
expect(slotClick).toHaveBeenCalledTimes(1);
|
|
232
|
+
expect(calls).toEqual(["child", "slot"]);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe("mergeProps — other props", () => {
|
|
237
|
+
it("keeps non-special props, child winning over the slot on collision", () => {
|
|
238
|
+
// Arrange
|
|
239
|
+
const { container } = render(
|
|
240
|
+
<Slot id="slot-id" data-origin="slot">
|
|
241
|
+
<span id="child-id">text</span>
|
|
242
|
+
</Slot>,
|
|
243
|
+
);
|
|
244
|
+
const el = container.firstChild as HTMLElement;
|
|
245
|
+
|
|
246
|
+
// Assert — child's id wins; the slot-only prop still passes through
|
|
247
|
+
expect(el.id).toBe("child-id");
|
|
248
|
+
expect(el.getAttribute("data-origin")).toBe("slot");
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("does not treat a prop merely containing on[A-Z] as an event handler", () => {
|
|
252
|
+
// Arrange — "iconOnly" contains "onO" but is not a handler; only a
|
|
253
|
+
// start-anchored /^on[A-Z]/ correctly excludes it, so the child value must
|
|
254
|
+
// win by the default merge rather than being composed into a function.
|
|
255
|
+
// "iconOnly" is not a real DOM attribute, hence the spread casts.
|
|
256
|
+
const { container } = render(
|
|
257
|
+
<Slot {...({ iconOnly: "slot" } as object)}>
|
|
258
|
+
<span {...({ iconOnly: "child" } as object)}>text</span>
|
|
259
|
+
</Slot>,
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
// Assert
|
|
263
|
+
expect((container.firstChild as HTMLElement).getAttribute("icononly")).toBe(
|
|
264
|
+
"child",
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('sets displayName to "Slot" for React DevTools and stack traces', () => {
|
|
270
|
+
// Assert — an empty displayName would render it as an anonymous component.
|
|
271
|
+
expect(Slot.displayName).toBe("Slot");
|
|
162
272
|
});
|
|
163
273
|
});
|
|
@@ -5,8 +5,25 @@
|
|
|
5
5
|
*
|
|
6
6
|
* This is the standard composition pattern used throughout headless
|
|
7
7
|
* components to let consumers attach their own listeners onto a
|
|
8
|
-
* sub-component
|
|
9
|
-
*
|
|
8
|
+
* sub-component's *own* native event props (e.g. `Tabs.Trigger`'s
|
|
9
|
+
* `onClick`, `Modal.Overlay`'s `onClick`) without clobbering the
|
|
10
|
+
* component's own behaviour, while still giving them an opt-out via
|
|
11
|
+
* `preventDefault()`. Wrap it around the component's internal handler at
|
|
12
|
+
* the point the prop is consumed:
|
|
13
|
+
*
|
|
14
|
+
* ```tsx
|
|
15
|
+
* const handleClick = composeEventHandlers(onClick, () => {
|
|
16
|
+
* // the component's own behaviour, e.g. activating a tab
|
|
17
|
+
* });
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* **Not the same mechanism as {@link Slot}.** `Slot`'s own prop-merging
|
|
21
|
+
* (used for the `asChild` pattern) also composes event handlers, but with
|
|
22
|
+
* the *opposite* order (child fires first, then Slot) and *no*
|
|
23
|
+
* `preventDefault()` opt-out — both handlers always run there. Reach for
|
|
24
|
+
* `composeEventHandlers` when a component owns a native DOM prop directly;
|
|
25
|
+
* reach for `Slot` when delegating rendering to a consumer-supplied
|
|
26
|
+
* element via `asChild`. The two are never used together on the same prop.
|
|
10
27
|
*
|
|
11
28
|
* @example Consumer logs every click; component still closes the modal
|
|
12
29
|
* afterwards:
|
|
@@ -20,6 +37,13 @@
|
|
|
20
37
|
* if (formIsDirty) e.preventDefault(); // don't close
|
|
21
38
|
* }} />
|
|
22
39
|
* ```
|
|
40
|
+
*
|
|
41
|
+
* @example Opting out of the veto — always run our handler too
|
|
42
|
+
* ```tsx
|
|
43
|
+
* const handleClick = composeEventHandlers(onClick, ourHandler, {
|
|
44
|
+
* checkForDefaultPrevented: false,
|
|
45
|
+
* });
|
|
46
|
+
* ```
|
|
23
47
|
*/
|
|
24
48
|
export function composeEventHandlers<E>(
|
|
25
49
|
theirHandler: ((event: E) => void) | undefined,
|
package/src/Slot/types.ts
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
import { HTMLAttributes, ReactNode } from "react";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Props for {@link Slot} — all native `HTMLAttributes` (event handlers,
|
|
5
|
+
* `style`, `className`, `aria-*`, `data-*`, ...), merged onto the single
|
|
6
|
+
* child element rather than rendered onto an element `Slot` creates
|
|
7
|
+
* itself. See {@link Slot} for the exact prop-merging rules (event
|
|
8
|
+
* handlers compose child-first, `style` shallow-merges child-wins,
|
|
9
|
+
* `className` concatenates, everything else defaults to the child).
|
|
10
|
+
*
|
|
11
|
+
* @extends HTMLElement
|
|
12
|
+
*/
|
|
3
13
|
export type SlotProps = HTMLAttributes<HTMLElement> & {
|
|
14
|
+
/**
|
|
15
|
+
* The element to clone `Slot`'s props onto. Must be exactly one valid
|
|
16
|
+
* React element that accepts a `ref` — passing zero children, more than
|
|
17
|
+
* one, or a non-element (e.g. a plain string) throws
|
|
18
|
+
* `"Slot requires exactly one React element child."`.
|
|
19
|
+
*/
|
|
4
20
|
children?: ReactNode;
|
|
5
21
|
};
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# SplitButton
|
|
2
|
+
|
|
3
|
+
One primary action paired with a menu of related alternatives, bound into
|
|
4
|
+
a single `role="group"` widget. Zero styles ship.
|
|
5
|
+
|
|
6
|
+
```tsx
|
|
7
|
+
import { SplitButton, VisuallyHidden } from "@primitiv-ui/react";
|
|
8
|
+
|
|
9
|
+
<SplitButton>
|
|
10
|
+
<SplitButton.Action onClick={squashAndMerge}>
|
|
11
|
+
Squash and merge
|
|
12
|
+
</SplitButton.Action>
|
|
13
|
+
<SplitButton.Trigger>
|
|
14
|
+
<ChevronDown aria-hidden="true" />
|
|
15
|
+
<VisuallyHidden>More merge options</VisuallyHidden>
|
|
16
|
+
</SplitButton.Trigger>
|
|
17
|
+
<SplitButton.Menu>
|
|
18
|
+
<SplitButton.Item onSelect={mergeCommit}>
|
|
19
|
+
Create a merge commit
|
|
20
|
+
</SplitButton.Item>
|
|
21
|
+
<SplitButton.Item onSelect={rebase}>Rebase and merge</SplitButton.Item>
|
|
22
|
+
</SplitButton.Menu>
|
|
23
|
+
</SplitButton>;
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Split button, not select
|
|
27
|
+
|
|
28
|
+
A `Select` answers *"which option is currently selected?"* — picking an
|
|
29
|
+
item changes a **value**. A split button is an **actions** control: the
|
|
30
|
+
left half runs the default action immediately, and the right half opens a
|
|
31
|
+
menu of alternatives. Those alternatives may run their own action, or
|
|
32
|
+
re-point what the primary half does next; either way nothing about the
|
|
33
|
+
widget models a selected value.
|
|
34
|
+
|
|
35
|
+
If you want a value picker, use [`Select`](../Select/README.md). If you
|
|
36
|
+
want a plain menu button with no default action, use
|
|
37
|
+
[`Dropdown`](../Dropdown/README.md) on its own.
|
|
38
|
+
|
|
39
|
+
## Sub-components
|
|
40
|
+
|
|
41
|
+
| Export | Element | Role | Notes |
|
|
42
|
+
| ------------------------ | -------------------------- | ------------ | ----------------------------------------------------------------------- |
|
|
43
|
+
| `SplitButton` / `.Root` | `<div>` | `group` | Owns the menu's open state; uncontrolled (`defaultOpen`) or controlled |
|
|
44
|
+
| `SplitButton.Action` | `<button type="button">` | `button` | The primary action; `ArrowDown` opens the menu |
|
|
45
|
+
| `SplitButton.Trigger` | `<button type="button">` | `button` | `aria-haspopup="menu"`; name derived from the action |
|
|
46
|
+
| `SplitButton.Menu` | `<menu popover="auto">` | `menu` | The panel of alternatives |
|
|
47
|
+
| `SplitButton.Item` | `<div>` | `menuitem` | One alternative action, with `onSelect` |
|
|
48
|
+
| `SplitButton.Separator` | `<div>` | `separator` | Divider between groups of alternatives |
|
|
49
|
+
|
|
50
|
+
`Root`, `Action` and the group accept `asChild`; `Trigger`, `Menu`,
|
|
51
|
+
`Item` and `Separator` inherit `asChild` from the Dropdown parts they
|
|
52
|
+
delegate to.
|
|
53
|
+
|
|
54
|
+
## Anatomy and styling hooks
|
|
55
|
+
|
|
56
|
+
| Part | Hook |
|
|
57
|
+
| ------------- | ------------------------------------------------------ |
|
|
58
|
+
| Root | `data-split-button=""`, `data-state="open" \| "closed"` |
|
|
59
|
+
| Action | `data-split-button-action=""` |
|
|
60
|
+
| Trigger | `data-split-button-trigger=""` |
|
|
61
|
+
| Menu | `data-split-button-menu=""` |
|
|
62
|
+
| Any disabled | `data-disabled=""` |
|
|
63
|
+
|
|
64
|
+
`data-state` on the **root** tracks the menu, which is what makes the
|
|
65
|
+
seam between the two halves styleable while the menu is open (a pressed
|
|
66
|
+
chevron, a joined border) without reaching for `:has()`.
|
|
67
|
+
|
|
68
|
+
## What this component adds over `Dropdown`
|
|
69
|
+
|
|
70
|
+
The menu half *is* a `Dropdown` — Root renders a `Dropdown.Root`, and
|
|
71
|
+
`Menu` / `Item` / `Separator` delegate to `Dropdown.Content` / `.Item` /
|
|
72
|
+
`.Separator`. All the arrow-key navigation, typeahead, Escape handling,
|
|
73
|
+
light dismiss and focus restoration are Dropdown's, tested once over
|
|
74
|
+
there.
|
|
75
|
+
|
|
76
|
+
What `SplitButton` owns is the coordination **between the two halves**,
|
|
77
|
+
which is the whole reason it exists as a primitive rather than a styled
|
|
78
|
+
recipe:
|
|
79
|
+
|
|
80
|
+
1. **The `role="group"` boundary** — assistive technology reads two
|
|
81
|
+
buttons as one widget rather than two unrelated commands.
|
|
82
|
+
2. **The trigger's accessible name, derived from the action** — see
|
|
83
|
+
below. This is the thing hand-rolled split buttons get wrong most
|
|
84
|
+
often.
|
|
85
|
+
3. **`disabled` propagation** — one prop on the group disables both
|
|
86
|
+
halves; it is OR-ed with each half's own `disabled`, so a part can
|
|
87
|
+
never override the group back to enabled.
|
|
88
|
+
4. **`ArrowDown` on the action opens the menu** — the action is *not*
|
|
89
|
+
the menu trigger, so nothing in `Dropdown` wires this. Note this
|
|
90
|
+
affordance is specific to `SplitButton`; `Dropdown.Trigger` itself
|
|
91
|
+
opens on click/Enter/Space only.
|
|
92
|
+
|
|
93
|
+
## The derived accessible name
|
|
94
|
+
|
|
95
|
+
A chevron-only control has no text of its own. Unless you pass
|
|
96
|
+
`aria-label` or `aria-labelledby`, `SplitButton.Trigger` defaults to:
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
aria-labelledby="<the trigger's own id> <the action's id>"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Self-reference is valid in `aria-labelledby` and resolves to the
|
|
103
|
+
element's own contents, so putting visually-hidden text inside the
|
|
104
|
+
trigger produces *"More merge options, Squash and merge"*:
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
<SplitButton.Trigger>
|
|
108
|
+
<ChevronDown aria-hidden="true" />
|
|
109
|
+
<VisuallyHidden>More merge options</VisuallyHidden>
|
|
110
|
+
</SplitButton.Trigger>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
With no text inside the trigger at all, the name falls back to just the
|
|
114
|
+
action's label — ambiguous, but still better than an unlabelled button.
|
|
115
|
+
Passing either ARIA attribute yourself opts out of the derivation
|
|
116
|
+
completely:
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
<SplitButton.Trigger aria-label="More merge options">
|
|
120
|
+
<ChevronDown aria-hidden="true" />
|
|
121
|
+
</SplitButton.Trigger>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Because the derivation depends on both ids, **`id` is component-owned on
|
|
125
|
+
`Action` and `Trigger`** and is `Omit`-ted from their prop types. Each
|
|
126
|
+
id is derived from the root's `useId()` with a stable `-split-button-action`
|
|
127
|
+
/ `-split-button-trigger` suffix.
|
|
128
|
+
|
|
129
|
+
## Keyboard interaction
|
|
130
|
+
|
|
131
|
+
| Key | On | Behaviour |
|
|
132
|
+
| ----------------------- | ----------------- | ------------------------------------------------ |
|
|
133
|
+
| `Enter` / `Space` | Action | Runs the primary action (native button) |
|
|
134
|
+
| `ArrowDown` | Action | Opens the menu, focusing its first item |
|
|
135
|
+
| `Enter` / `Space` | Trigger | Toggles the menu |
|
|
136
|
+
| `ArrowDown` / `ArrowUp` | Menu | Moves between items (wraps) |
|
|
137
|
+
| `Home` / `End` | Menu | First / last enabled item |
|
|
138
|
+
| printable characters | Menu | Typeahead |
|
|
139
|
+
| `Escape` | Menu | Closes and returns focus to the trigger |
|
|
140
|
+
|
|
141
|
+
**Both halves are separate tab stops.** They are two genuinely distinct
|
|
142
|
+
commands, so each is independently tabbable — this is deliberately *not*
|
|
143
|
+
a roving-tabindex widget, and shouldn't become one. (Contrast
|
|
144
|
+
`NavigationMenu`, which uses the roving-tabindex hook for its keymap only,
|
|
145
|
+
for the same underlying reason.)
|
|
146
|
+
|
|
147
|
+
## State modes
|
|
148
|
+
|
|
149
|
+
Identical to `Dropdown.Root`'s, and statically discriminated at the type
|
|
150
|
+
level:
|
|
151
|
+
|
|
152
|
+
- **Uncontrolled** — pass `defaultOpen` (or omit it to start closed).
|
|
153
|
+
Optional `onOpenChange` observes user-driven transitions.
|
|
154
|
+
- **Controlled** — pass `open` and `onOpenChange` together.
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
const [open, setOpen] = useState(false);
|
|
158
|
+
|
|
159
|
+
<SplitButton open={open} onOpenChange={setOpen}>
|
|
160
|
+
...
|
|
161
|
+
</SplitButton>;
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Disabled
|
|
165
|
+
|
|
166
|
+
```tsx
|
|
167
|
+
{/* Both halves disabled */}
|
|
168
|
+
<SplitButton disabled>...</SplitButton>
|
|
169
|
+
|
|
170
|
+
{/* Only the action — alternatives stay reachable */}
|
|
171
|
+
<SplitButton.Action disabled>Squash and merge</SplitButton.Action>
|
|
172
|
+
|
|
173
|
+
{/* Only the menu — the default action still works */}
|
|
174
|
+
<SplitButton.Trigger disabled aria-label="More merge options" />
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Richer menus
|
|
178
|
+
|
|
179
|
+
`SplitButton` re-exports only the two menu parts a split button almost
|
|
180
|
+
always needs. Root provides the same Dropdown context, so every other
|
|
181
|
+
`Dropdown` part composes inside `SplitButton.Menu` directly:
|
|
182
|
+
|
|
183
|
+
```tsx
|
|
184
|
+
import { Dropdown, SplitButton } from "@primitiv-ui/react";
|
|
185
|
+
|
|
186
|
+
<SplitButton.Menu>
|
|
187
|
+
<Dropdown.Group>
|
|
188
|
+
<Dropdown.Label>Merge strategy</Dropdown.Label>
|
|
189
|
+
<SplitButton.Item onSelect={mergeCommit}>
|
|
190
|
+
Create a merge commit
|
|
191
|
+
</SplitButton.Item>
|
|
192
|
+
</Dropdown.Group>
|
|
193
|
+
<SplitButton.Separator />
|
|
194
|
+
<Dropdown.Sub>
|
|
195
|
+
<Dropdown.SubTrigger>Advanced</Dropdown.SubTrigger>
|
|
196
|
+
<Dropdown.SubContent>
|
|
197
|
+
<SplitButton.Item onSelect={rebase}>Rebase and merge</SplitButton.Item>
|
|
198
|
+
</Dropdown.SubContent>
|
|
199
|
+
</Dropdown.Sub>
|
|
200
|
+
</SplitButton.Menu>;
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
`dir` on `SplitButton.Root` is forwarded to the Dropdown, so submenus
|
|
204
|
+
invert their open/close arrow keys in RTL.
|
|
205
|
+
|
|
206
|
+
## `asChild`
|
|
207
|
+
|
|
208
|
+
```tsx
|
|
209
|
+
{/* The group is a <section> */}
|
|
210
|
+
<SplitButton asChild>
|
|
211
|
+
<section aria-label="Merge">...</section>
|
|
212
|
+
</SplitButton>
|
|
213
|
+
|
|
214
|
+
{/* The primary action navigates instead of firing a handler.
|
|
215
|
+
type="button" is not forwarded in asChild mode. */}
|
|
216
|
+
<SplitButton.Action asChild>
|
|
217
|
+
<a href="/merge">Squash and merge</a>
|
|
218
|
+
</SplitButton.Action>
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Positioning
|
|
222
|
+
|
|
223
|
+
Like `Dropdown`, the menu is a native `popover="auto"` element in the top
|
|
224
|
+
layer — placement is the consumer's CSS concern (CSS anchor positioning,
|
|
225
|
+
as the registry `dropdown` component does).
|