@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
|
@@ -32,7 +32,7 @@ function dataStateOf(checked: CheckedState) {
|
|
|
32
32
|
*
|
|
33
33
|
* Props routing: `className` / `style` style the **box** (the `<label>` you
|
|
34
34
|
* see — the input is hidden); every other prop (`name`, `value`, `id`,
|
|
35
|
-
* `aria-*`, `required`, `disabled`, `ref`,
|
|
35
|
+
* `aria-*`, `required`, `disabled`, `ref`, ...) spreads onto the input, because
|
|
36
36
|
* semantically the Root *is* the checkbox.
|
|
37
37
|
*
|
|
38
38
|
* Supports two state modes, statically discriminated at the type level:
|
|
@@ -50,6 +50,8 @@ function dataStateOf(checked: CheckedState) {
|
|
|
50
50
|
* mirror the input, but shipped CSS keys the mark off the input's native
|
|
51
51
|
* `:checked` / `:indeterminate` instead; `data-state` is a convenience mirror.
|
|
52
52
|
*
|
|
53
|
+
* @extends HTMLInputElement
|
|
54
|
+
*
|
|
53
55
|
* @example Uncontrolled
|
|
54
56
|
* ```tsx
|
|
55
57
|
* <Checkbox.Root name="terms" value="accepted" defaultChecked aria-label="Accept terms">
|
|
@@ -61,7 +63,7 @@ function dataStateOf(checked: CheckedState) {
|
|
|
61
63
|
* ```tsx
|
|
62
64
|
* const [checked, setChecked] = useState<CheckedState>("indeterminate");
|
|
63
65
|
*
|
|
64
|
-
* <Checkbox.Root checked={checked} onCheckedChange={setChecked} aria-label="
|
|
66
|
+
* <Checkbox.Root checked={checked} onCheckedChange={setChecked} aria-label="...">
|
|
65
67
|
* <Checkbox.Indicator />
|
|
66
68
|
* </Checkbox.Root>
|
|
67
69
|
* ```
|
|
@@ -120,7 +122,11 @@ export function CheckboxRoot(props: CheckboxRootProps): ReactElement {
|
|
|
120
122
|
);
|
|
121
123
|
}
|
|
122
124
|
|
|
123
|
-
|
|
125
|
+
// Runtime-dead: the compound alias below (same object via Object.assign)
|
|
126
|
+
// overwrites this to "Checkbox" at load, so the value is never observable. The
|
|
127
|
+
// assignment stays because it declares `displayName` on `typeof CheckboxRoot`,
|
|
128
|
+
// which TCheckboxCompound extends.
|
|
129
|
+
// Stryker disable next-line StringLiteral: overwritten by the compound alias — an equivalent mutant.
|
|
124
130
|
CheckboxRoot.displayName = "CheckboxRoot";
|
|
125
131
|
|
|
126
132
|
/**
|
|
@@ -138,6 +144,8 @@ CheckboxRoot.displayName = "CheckboxRoot";
|
|
|
138
144
|
* (typically an `<svg>` tick) as the indicator itself, with `data-state`
|
|
139
145
|
* merged onto that element rather than a wrapper.
|
|
140
146
|
*
|
|
147
|
+
* @extends HTMLSpanElement
|
|
148
|
+
*
|
|
141
149
|
* @example Icon as the indicator via `asChild`
|
|
142
150
|
* ```tsx
|
|
143
151
|
* <Checkbox.Indicator asChild>
|
|
@@ -76,4 +76,18 @@ describe("Checkbox basic rendering", () => {
|
|
|
76
76
|
"unchecked",
|
|
77
77
|
);
|
|
78
78
|
});
|
|
79
|
+
|
|
80
|
+
it("sets a displayName on the compound and the Indicator", () => {
|
|
81
|
+
// Assert — empty displayNames would render each as anonymous in DevTools.
|
|
82
|
+
// Root aliases the compound (Object.assign), so its name is "Checkbox".
|
|
83
|
+
expect(Checkbox.displayName).toBe("Checkbox");
|
|
84
|
+
expect(Checkbox.Indicator.displayName).toBe("CheckboxIndicator");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("throws a helpful error when a sub-component renders outside Checkbox.Root", () => {
|
|
88
|
+
// Assert — the strict-context guard names the required parent.
|
|
89
|
+
expect(() => render(<Checkbox.Indicator />)).toThrow(
|
|
90
|
+
"Checkbox sub-components must be rendered inside a <Checkbox.Root>.",
|
|
91
|
+
);
|
|
92
|
+
});
|
|
79
93
|
});
|
|
@@ -37,6 +37,10 @@ export function useCheckboxInput({
|
|
|
37
37
|
onChange?.(event);
|
|
38
38
|
if (event.defaultPrevented) return;
|
|
39
39
|
const next = event.target.checked;
|
|
40
|
+
// Stryker disable next-line ConditionalExpression: in controlled mode
|
|
41
|
+
// `inputStateProps` reads controlledChecked, never `mirror`, so setting
|
|
42
|
+
// mirror here (the mutant's always-run form) has no observable effect —
|
|
43
|
+
// equivalent.
|
|
40
44
|
if (!isControlled) setMirror(next);
|
|
41
45
|
onCheckedChange?.(next);
|
|
42
46
|
};
|
package/src/Checkbox/types.ts
CHANGED
|
@@ -10,7 +10,16 @@ export type CheckedState = boolean | "indeterminate";
|
|
|
10
10
|
* `:indeterminate` state.
|
|
11
11
|
*/
|
|
12
12
|
export type CheckboxIndicatorProps = ComponentProps<"span"> & {
|
|
13
|
+
/** Custom mark content. Omit to let the shipped CSS draw the tick/bar off the
|
|
14
|
+
* input's native `:checked` / `:indeterminate` state; provide your own (an
|
|
15
|
+
* icon, glyph, or nested element) to override it. */
|
|
13
16
|
children?: ReactNode;
|
|
17
|
+
/**
|
|
18
|
+
* When `true`, render `children` as the indicator element itself
|
|
19
|
+
* (via the {@link Slot} pattern) instead of wrapping them in a `<span>`.
|
|
20
|
+
* `data-state` and `aria-hidden` are merged onto that element.
|
|
21
|
+
* @default false
|
|
22
|
+
*/
|
|
14
23
|
asChild?: boolean;
|
|
15
24
|
};
|
|
16
25
|
|
|
@@ -38,7 +47,11 @@ export type CheckboxRootBaseProps = Omit<
|
|
|
38
47
|
* `defaultChecked` may be `"indeterminate"` for a mixed-on-mount checkbox.
|
|
39
48
|
*/
|
|
40
49
|
export type CheckboxRootUncontrolledProps = CheckboxRootBaseProps & {
|
|
50
|
+
/** Checked value on first render; the component owns it thereafter. May be
|
|
51
|
+
* `"indeterminate"` for a mixed-on-mount checkbox. Omit for an initially
|
|
52
|
+
* unchecked box. */
|
|
41
53
|
defaultChecked?: CheckedState;
|
|
54
|
+
/** Forbidden in uncontrolled mode — use `defaultChecked` instead. */
|
|
42
55
|
checked?: never;
|
|
43
56
|
};
|
|
44
57
|
|
|
@@ -48,8 +61,14 @@ export type CheckboxRootUncontrolledProps = CheckboxRootBaseProps & {
|
|
|
48
61
|
* forbidden.
|
|
49
62
|
*/
|
|
50
63
|
export type CheckboxRootControlledProps = CheckboxRootBaseProps & {
|
|
64
|
+
/** Forbidden in controlled mode — use `checked` instead. */
|
|
51
65
|
defaultChecked?: never;
|
|
66
|
+
/** The current checked value, owned by the parent. May be `"indeterminate"`
|
|
67
|
+
* for the tri-state; clicking a mixed checkbox resolves it to `true`. Keep it
|
|
68
|
+
* in sync via `onCheckedChange`. */
|
|
52
69
|
checked: CheckedState;
|
|
70
|
+
/** Called with the new boolean checked value on every user toggle. Required
|
|
71
|
+
* in controlled mode so the parent can keep `checked` in sync. */
|
|
53
72
|
onCheckedChange: (checked: boolean) => void;
|
|
54
73
|
};
|
|
55
74
|
|
|
@@ -40,8 +40,11 @@ function dataStateOf(checked: CheckedState) {
|
|
|
40
40
|
* on the root, plus `data-disabled=""` when disabled.
|
|
41
41
|
*
|
|
42
42
|
* **`asChild` prop.** Pass `asChild` to render any consumer-supplied element
|
|
43
|
-
* with the checkbox's ARIA attributes, data-state
|
|
44
|
-
* merged in.
|
|
43
|
+
* with the checkbox's ARIA attributes, `data-state`, composed `onClick`, and
|
|
44
|
+
* `ref` merged in via the {@link Slot} pattern. See
|
|
45
|
+
* {@link CheckboxCardRootBaseProps}.
|
|
46
|
+
*
|
|
47
|
+
* @extends HTMLButtonElement
|
|
45
48
|
*
|
|
46
49
|
* @example Uncontrolled
|
|
47
50
|
* ```tsx
|
|
@@ -58,7 +61,7 @@ function dataStateOf(checked: CheckedState) {
|
|
|
58
61
|
* ```tsx
|
|
59
62
|
* const [enabled, setEnabled] = useState<CheckedState>(false);
|
|
60
63
|
*
|
|
61
|
-
* <CheckboxCard.Root checked={enabled} onCheckedChange={setEnabled} aria-label="
|
|
64
|
+
* <CheckboxCard.Root checked={enabled} onCheckedChange={setEnabled} aria-label="...">
|
|
62
65
|
* <CheckboxCard.Indicator />
|
|
63
66
|
* Feature name
|
|
64
67
|
* </CheckboxCard.Root>
|
|
@@ -106,7 +109,11 @@ export function CheckboxCardRoot(props: CheckboxCardRootProps): ReactElement {
|
|
|
106
109
|
);
|
|
107
110
|
}
|
|
108
111
|
|
|
109
|
-
|
|
112
|
+
// Runtime-dead: the compound alias below (same object via Object.assign)
|
|
113
|
+
// overwrites this to "CheckboxCard" at load, so the value is never observable.
|
|
114
|
+
// The assignment stays because it declares `displayName` on
|
|
115
|
+
// `typeof CheckboxCardRoot`, which TCheckboxCardCompound extends.
|
|
116
|
+
// Stryker disable next-line StringLiteral: overwritten by the compound alias — an equivalent mutant.
|
|
110
117
|
CheckboxCardRoot.displayName = "CheckboxCardRoot";
|
|
111
118
|
|
|
112
119
|
/**
|
|
@@ -124,7 +131,9 @@ CheckboxCardRoot.displayName = "CheckboxCardRoot";
|
|
|
124
131
|
*
|
|
125
132
|
* **`forceMount` prop.** Pass `forceMount` to keep the indicator in the DOM
|
|
126
133
|
* while unchecked so a CSS exit animation can play against
|
|
127
|
-
* `data-state="unchecked"`.
|
|
134
|
+
* `data-state="unchecked"`. See {@link CheckboxCardIndicatorProps}.
|
|
135
|
+
*
|
|
136
|
+
* @extends HTMLSpanElement
|
|
128
137
|
*
|
|
129
138
|
* @example
|
|
130
139
|
* ```tsx
|
|
@@ -46,7 +46,7 @@ Pass `checked` and `onCheckedChange` together. The parent owns the value.
|
|
|
46
46
|
```tsx
|
|
47
47
|
const [enabled, setEnabled] = useState<CheckedState>(false);
|
|
48
48
|
|
|
49
|
-
<CheckboxCard.Root checked={enabled} onCheckedChange={setEnabled} aria-label="
|
|
49
|
+
<CheckboxCard.Root checked={enabled} onCheckedChange={setEnabled} aria-label="...">
|
|
50
50
|
<CheckboxCard.Indicator />
|
|
51
51
|
Dark mode
|
|
52
52
|
</CheckboxCard.Root>
|
|
@@ -55,4 +55,11 @@ describe("CheckboxCard basic rendering", () => {
|
|
|
55
55
|
expect(screen.getByTestId("icon")).toBeInTheDocument();
|
|
56
56
|
expect(screen.getByText("Feature name")).toBeInTheDocument();
|
|
57
57
|
});
|
|
58
|
+
|
|
59
|
+
it("sets a displayName on the compound and the Indicator", () => {
|
|
60
|
+
// Assert — empty displayNames would render each as anonymous in DevTools.
|
|
61
|
+
// Root aliases the compound (Object.assign), so its name is "CheckboxCard".
|
|
62
|
+
expect(CheckboxCard.displayName).toBe("CheckboxCard");
|
|
63
|
+
expect(CheckboxCard.Indicator.displayName).toBe("CheckboxCardIndicator");
|
|
64
|
+
});
|
|
58
65
|
});
|
|
@@ -1,49 +1,97 @@
|
|
|
1
1
|
import { ComponentProps, ReactNode, Ref } from "react";
|
|
2
2
|
import type { CheckedState } from "../Checkbox/types";
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
/**
|
|
5
|
+
* Props for {@link CheckboxCardIndicator | `CheckboxCard.Indicator`} — the
|
|
6
|
+
* decorative visual marker (typically a tick icon) shown while the card is
|
|
7
|
+
* checked or indeterminate. All `HTMLSpanElement` attributes, plus the
|
|
8
|
+
* `forceMount` and `asChild` escape hatches.
|
|
9
|
+
*/
|
|
7
10
|
export type CheckboxCardIndicatorProps = ComponentProps<"span"> & {
|
|
11
|
+
/** The indicator content — typically an `<svg>` tick or check glyph. */
|
|
8
12
|
children?: ReactNode;
|
|
13
|
+
/**
|
|
14
|
+
* Keep the indicator mounted while the card is **unchecked** so a CSS exit
|
|
15
|
+
* animation can play against `data-state="unchecked"`. By default the
|
|
16
|
+
* indicator unmounts as soon as the card is unchecked.
|
|
17
|
+
* @default false
|
|
18
|
+
*/
|
|
9
19
|
forceMount?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* When `true`, render the consumer's own element (typically an `<svg>`) as
|
|
22
|
+
* the indicator instead of the default `<span>`, merging `aria-hidden` and
|
|
23
|
+
* `data-state` onto it via the {@link Slot} pattern.
|
|
24
|
+
* @default false
|
|
25
|
+
*/
|
|
10
26
|
asChild?: boolean;
|
|
11
27
|
};
|
|
12
28
|
|
|
13
|
-
/**
|
|
14
|
-
*
|
|
29
|
+
/**
|
|
30
|
+
* Shared base for both {@link CheckboxCardRootProps} variants — the native
|
|
31
|
+
* `<button>` attributes (minus the ones the component owns: `type`, `role`,
|
|
32
|
+
* `aria-checked`, and `defaultChecked`), plus the `asChild` escape hatch and a
|
|
33
|
+
* typed `ref`.
|
|
34
|
+
*/
|
|
15
35
|
export type CheckboxCardRootBaseProps = Omit<
|
|
16
36
|
ComponentProps<"button">,
|
|
17
37
|
"type" | "role" | "aria-checked" | "defaultChecked"
|
|
18
38
|
> & {
|
|
19
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* When `true`, render the consumer's own element instead of the default
|
|
41
|
+
* `<button type="button">`, merging `role`, `aria-checked`, `data-state`,
|
|
42
|
+
* the composed `onClick`, `disabled`, and `ref` onto it via the
|
|
43
|
+
* {@link Slot} pattern.
|
|
44
|
+
* @default false
|
|
45
|
+
*/
|
|
20
46
|
asChild?: boolean;
|
|
21
|
-
/**
|
|
47
|
+
/** Forwarded to the rendered element (the `<button>` by default). */
|
|
22
48
|
ref?: Ref<HTMLButtonElement>;
|
|
23
49
|
};
|
|
24
50
|
|
|
25
|
-
/**
|
|
26
|
-
*
|
|
51
|
+
/**
|
|
52
|
+
* Uncontrolled variant of {@link CheckboxCardRootProps}: the component owns the
|
|
53
|
+
* checked state, seeded by an optional `defaultChecked`. Pass `defaultChecked`
|
|
54
|
+
* (or omit it); `checked` is forbidden.
|
|
55
|
+
*/
|
|
27
56
|
export type CheckboxCardRootUncontrolledProps = CheckboxCardRootBaseProps & {
|
|
28
|
-
/**
|
|
57
|
+
/** Tri-state checked value on first render (`true` / `false` /
|
|
58
|
+
* `"indeterminate"`). Defaults to `false` when omitted.
|
|
59
|
+
* @default false */
|
|
29
60
|
defaultChecked?: CheckedState;
|
|
61
|
+
/** Forbidden in uncontrolled mode — use `defaultChecked` instead. */
|
|
30
62
|
checked?: never;
|
|
31
|
-
/** Called with the new boolean checked state
|
|
63
|
+
/** Called with the new **boolean** checked state whenever the card toggles.
|
|
64
|
+
* An indeterminate card resolves to `true` on first click. Optional in
|
|
65
|
+
* uncontrolled mode. */
|
|
32
66
|
onCheckedChange?: (checked: boolean) => void;
|
|
33
67
|
};
|
|
34
68
|
|
|
35
|
-
/**
|
|
36
|
-
*
|
|
69
|
+
/**
|
|
70
|
+
* Controlled variant of {@link CheckboxCardRootProps}: the caller owns the
|
|
71
|
+
* checked state via `checked` and is notified through the required
|
|
72
|
+
* `onCheckedChange`. Pass both together; `defaultChecked` is forbidden.
|
|
73
|
+
*/
|
|
37
74
|
export type CheckboxCardRootControlledProps = CheckboxCardRootBaseProps & {
|
|
75
|
+
/** Forbidden in controlled mode — use `checked` instead. */
|
|
38
76
|
defaultChecked?: never;
|
|
39
|
-
/**
|
|
77
|
+
/** The controlled tri-state checked value (`true` / `false` /
|
|
78
|
+
* `"indeterminate"`). Must be kept in sync by the parent via
|
|
79
|
+
* `onCheckedChange`. */
|
|
40
80
|
checked: CheckedState;
|
|
41
|
-
/** Called with the new boolean checked state when the user toggles.
|
|
81
|
+
/** Called with the new **boolean** checked state when the user toggles. An
|
|
82
|
+
* indeterminate card resolves to `true` on click. Required in controlled
|
|
83
|
+
* mode. */
|
|
42
84
|
onCheckedChange: (checked: boolean) => void;
|
|
43
85
|
};
|
|
44
86
|
|
|
45
|
-
/**
|
|
46
|
-
*
|
|
87
|
+
/**
|
|
88
|
+
* Props for {@link CheckboxCardRoot | `CheckboxCard.Root`}.
|
|
89
|
+
*
|
|
90
|
+
* Resolves to either {@link CheckboxCardRootUncontrolledProps} or
|
|
91
|
+
* {@link CheckboxCardRootControlledProps} — only one shape is accepted by
|
|
92
|
+
* TypeScript at a time. Both accept the tri-state {@link CheckedState}
|
|
93
|
+
* (`boolean | "indeterminate"`).
|
|
94
|
+
*/
|
|
47
95
|
export type CheckboxCardRootProps =
|
|
48
96
|
| CheckboxCardRootUncontrolledProps
|
|
49
97
|
| CheckboxCardRootControlledProps;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Ref } from "react";
|
|
2
|
-
import type { ReactElement } from "react";
|
|
2
|
+
import type { CSSProperties, ReactElement } from "react";
|
|
3
3
|
|
|
4
4
|
import { Slot } from "../Slot/index.ts";
|
|
5
5
|
|
|
@@ -39,9 +39,18 @@ import type {
|
|
|
39
39
|
* mirrored onto Root, Trigger, and Content so a single selector covers
|
|
40
40
|
* every sub-component.
|
|
41
41
|
*
|
|
42
|
+
* **Context.** Root establishes {@link CollapsibleContext}, from which
|
|
43
|
+
* {@link CollapsibleTrigger}, {@link CollapsibleContent}, and
|
|
44
|
+
* {@link CollapsibleTriggerIcon} read the open state, the `disabled` flag,
|
|
45
|
+
* the `toggle` action, and the generated `triggerId` / `contentId` that
|
|
46
|
+
* wire the Trigger's `aria-controls` to the Content's `id`. Rendering any
|
|
47
|
+
* sub-component outside a Root throws.
|
|
48
|
+
*
|
|
42
49
|
* **Styling hooks.** `data-state="open" | "closed"` and
|
|
43
50
|
* `data-disabled="true" | "false"` are emitted on the rendered container.
|
|
44
51
|
*
|
|
52
|
+
* @extends HTMLDivElement
|
|
53
|
+
*
|
|
45
54
|
* @example Uncontrolled
|
|
46
55
|
* ```tsx
|
|
47
56
|
* <Collapsible.Root defaultOpen>
|
|
@@ -89,6 +98,7 @@ export function CollapsibleRoot({
|
|
|
89
98
|
}
|
|
90
99
|
|
|
91
100
|
/** @internal */
|
|
101
|
+
// Stryker disable next-line StringLiteral: overwritten by the compound alias — an equivalent mutant.
|
|
92
102
|
CollapsibleRoot.displayName = "CollapsibleRoot";
|
|
93
103
|
|
|
94
104
|
/**
|
|
@@ -133,6 +143,8 @@ CollapsibleRoot.displayName = "CollapsibleRoot";
|
|
|
133
143
|
* - `data-state="open" | "closed"` on the rendered element.
|
|
134
144
|
* - `data-disabled="true" | "false"`.
|
|
135
145
|
*
|
|
146
|
+
* @extends HTMLButtonElement
|
|
147
|
+
*
|
|
136
148
|
* @example Basic
|
|
137
149
|
* ```tsx
|
|
138
150
|
* <Collapsible.Trigger>Toggle</Collapsible.Trigger>
|
|
@@ -206,10 +218,21 @@ CollapsibleTrigger.displayName = "CollapsibleTrigger";
|
|
|
206
218
|
* this by passing `aria-hidden` explicitly (it appears after the
|
|
207
219
|
* automatic value in the spread).
|
|
208
220
|
*
|
|
221
|
+
* **`collapsedHeight` prop.** Pass `collapsedHeight` to show a **clamped
|
|
222
|
+
* preview** of the panel while closed (the read-more pattern) instead of
|
|
223
|
+
* hiding it. A number is pixels; a string is a verbatim CSS length. When set,
|
|
224
|
+
* the panel is always mounted and never `hidden` / `aria-hidden` (the preview
|
|
225
|
+
* is real content), and the value is published as the
|
|
226
|
+
* `--primitiv-collapsible-collapsed-height` custom property for the styling
|
|
227
|
+
* layer to clamp the closed height to and anchor a bottom fade-shadow, keyed
|
|
228
|
+
* off `data-state="closed"`.
|
|
229
|
+
*
|
|
209
230
|
* **Styling hooks.**
|
|
210
231
|
* - `data-state="open" | "closed"` on the rendered element.
|
|
211
232
|
* - `data-disabled="true" | "false"`.
|
|
212
233
|
*
|
|
234
|
+
* @extends HTMLDivElement
|
|
235
|
+
*
|
|
213
236
|
* @example Default (hidden attribute)
|
|
214
237
|
* ```tsx
|
|
215
238
|
* <Collapsible.Content>Hidden content</Collapsible.Content>
|
|
@@ -225,17 +248,38 @@ CollapsibleTrigger.displayName = "CollapsibleTrigger";
|
|
|
225
248
|
export function CollapsibleContent({
|
|
226
249
|
children,
|
|
227
250
|
forceMount = false,
|
|
251
|
+
collapsedHeight,
|
|
252
|
+
style,
|
|
228
253
|
...rest
|
|
229
254
|
}: CollapsibleContentProps): ReactElement {
|
|
230
255
|
const { open, disabled, contentId } = useCollapsibleContext();
|
|
231
256
|
|
|
257
|
+
// A clamped preview keeps real, readable content on screen while closed, so
|
|
258
|
+
// it is always mounted (forceMount is implied) and never `hidden` /
|
|
259
|
+
// `aria-hidden`. The height it clamps to is published as a custom property
|
|
260
|
+
// the styling layer consumes.
|
|
261
|
+
const clamped = collapsedHeight != null;
|
|
262
|
+
const mounted = forceMount || clamped;
|
|
263
|
+
// Always publish the custom property; an `undefined` value (no
|
|
264
|
+
// `collapsedHeight`) is dropped by React, so there is no redundant guard to
|
|
265
|
+
// go stale. `clamped` gates only the mount / aria behaviour, where it is
|
|
266
|
+
// observable.
|
|
267
|
+
const mergedStyle = {
|
|
268
|
+
"--primitiv-collapsible-collapsed-height":
|
|
269
|
+
typeof collapsedHeight === "number"
|
|
270
|
+
? `${collapsedHeight}px`
|
|
271
|
+
: collapsedHeight,
|
|
272
|
+
...style,
|
|
273
|
+
} as CSSProperties;
|
|
274
|
+
|
|
232
275
|
return (
|
|
233
276
|
<div
|
|
234
277
|
id={contentId}
|
|
235
|
-
hidden={
|
|
236
|
-
aria-hidden={forceMount && !open ? true : undefined}
|
|
278
|
+
hidden={mounted ? undefined : !open}
|
|
279
|
+
aria-hidden={forceMount && !open && !clamped ? true : undefined}
|
|
237
280
|
data-state={open ? "open" : "closed"}
|
|
238
281
|
data-disabled={disabled}
|
|
282
|
+
style={mergedStyle}
|
|
239
283
|
{...rest}
|
|
240
284
|
>
|
|
241
285
|
{children}
|
|
@@ -261,6 +305,8 @@ CollapsibleContent.displayName = "CollapsibleContent";
|
|
|
261
305
|
* - `data-state="open" | "closed"` on the rendered `<span>`.
|
|
262
306
|
* - `aria-hidden="true"` on the rendered `<span>`.
|
|
263
307
|
*
|
|
308
|
+
* @extends HTMLSpanElement
|
|
309
|
+
*
|
|
264
310
|
* @example Inline SVG
|
|
265
311
|
* ```tsx
|
|
266
312
|
* <Collapsible.Trigger>
|
|
@@ -97,6 +97,46 @@ is applied automatically so assistive technology skips the off-screen
|
|
|
97
97
|
content. It is removed when the panel opens. Consumers can override this
|
|
98
98
|
by passing `aria-hidden` explicitly.
|
|
99
99
|
|
|
100
|
+
## `collapsedHeight` — clamped preview (read-more)
|
|
101
|
+
|
|
102
|
+
Pass `collapsedHeight` to `Collapsible.Content` to show a **clamped preview**
|
|
103
|
+
of the panel while closed instead of hiding it — the read-more / "show more"
|
|
104
|
+
pattern. A number is pixels; a string is a verbatim CSS length (`"4rem"`,
|
|
105
|
+
`"20cqi"`).
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
<Collapsible.Content collapsedHeight={96} className="preview">
|
|
109
|
+
A long passage whose first 96px stays visible while collapsed...
|
|
110
|
+
</Collapsible.Content>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
When set, the panel is **always mounted** and never receives `hidden` /
|
|
114
|
+
`aria-hidden` (the preview is real, readable content — so `forceMount` is
|
|
115
|
+
implied), and the value is published as the
|
|
116
|
+
`--primitiv-collapsible-collapsed-height` custom property. The styling layer
|
|
117
|
+
clamps the closed height to it and anchors a bottom fade-shadow, keyed off
|
|
118
|
+
`data-state="closed"`:
|
|
119
|
+
|
|
120
|
+
```css
|
|
121
|
+
.preview {
|
|
122
|
+
overflow: hidden;
|
|
123
|
+
max-block-size: var(--primitiv-collapsible-collapsed-height);
|
|
124
|
+
transition: max-block-size 250ms;
|
|
125
|
+
}
|
|
126
|
+
.preview[data-state="open"] {
|
|
127
|
+
max-block-size: 100vh; /* or a large enough clamp */
|
|
128
|
+
}
|
|
129
|
+
/* Bottom fade over the clamped preview, gone once open. */
|
|
130
|
+
.preview[data-state="closed"]::after {
|
|
131
|
+
content: "";
|
|
132
|
+
position: absolute;
|
|
133
|
+
inset-inline: 0;
|
|
134
|
+
inset-block-end: 0;
|
|
135
|
+
block-size: 2rem;
|
|
136
|
+
background: linear-gradient(to bottom, transparent, var(--surface));
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
100
140
|
## `asChild` composition
|
|
101
141
|
|
|
102
142
|
`Collapsible.Trigger` accepts an `asChild` prop to render any child
|
|
@@ -125,7 +165,7 @@ elements:
|
|
|
125
165
|
<a href="#section">Toggle</a>
|
|
126
166
|
{/* rendered with role="button" aria-disabled="true" */}
|
|
127
167
|
</Collapsible.Trigger>
|
|
128
|
-
<Collapsible.Content
|
|
168
|
+
<Collapsible.Content>...</Collapsible.Content>
|
|
129
169
|
</Collapsible.Root>
|
|
130
170
|
```
|
|
131
171
|
|
|
@@ -119,6 +119,95 @@ describe("Collapsible asChild and keyboard tests", () => {
|
|
|
119
119
|
expect(content).toBeVisible();
|
|
120
120
|
});
|
|
121
121
|
|
|
122
|
+
// A plain <div> (no native button semantics and no role) never fires a
|
|
123
|
+
// synthetic click on Enter/Space, so these isolate the Trigger's own
|
|
124
|
+
// keydown handler — the anchor tests above can toggle via native activation
|
|
125
|
+
// even when the handler does nothing, masking mutations in it.
|
|
126
|
+
it("toggles on Enter for a non-native asChild element", async () => {
|
|
127
|
+
const user = userEvent.setup();
|
|
128
|
+
render(
|
|
129
|
+
<Collapsible.Root>
|
|
130
|
+
<Collapsible.Trigger asChild>
|
|
131
|
+
<div tabIndex={0} data-testid="trigger">
|
|
132
|
+
Toggle
|
|
133
|
+
</div>
|
|
134
|
+
</Collapsible.Trigger>
|
|
135
|
+
<Collapsible.Content data-testid="content">Content</Collapsible.Content>
|
|
136
|
+
</Collapsible.Root>,
|
|
137
|
+
);
|
|
138
|
+
screen.getByTestId("trigger").focus();
|
|
139
|
+
await user.keyboard("{Enter}");
|
|
140
|
+
expect(screen.getByTestId("content")).toBeVisible();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("toggles on Space for a non-native asChild element", async () => {
|
|
144
|
+
const user = userEvent.setup();
|
|
145
|
+
render(
|
|
146
|
+
<Collapsible.Root>
|
|
147
|
+
<Collapsible.Trigger asChild>
|
|
148
|
+
<div tabIndex={0} data-testid="trigger">
|
|
149
|
+
Toggle
|
|
150
|
+
</div>
|
|
151
|
+
</Collapsible.Trigger>
|
|
152
|
+
<Collapsible.Content data-testid="content">Content</Collapsible.Content>
|
|
153
|
+
</Collapsible.Root>,
|
|
154
|
+
);
|
|
155
|
+
screen.getByTestId("trigger").focus();
|
|
156
|
+
await user.keyboard(" ");
|
|
157
|
+
expect(screen.getByTestId("content")).toBeVisible();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("does not toggle on a non-activation key", async () => {
|
|
161
|
+
const user = userEvent.setup();
|
|
162
|
+
render(
|
|
163
|
+
<Collapsible.Root>
|
|
164
|
+
<Collapsible.Trigger asChild>
|
|
165
|
+
<div tabIndex={0} data-testid="trigger">
|
|
166
|
+
Toggle
|
|
167
|
+
</div>
|
|
168
|
+
</Collapsible.Trigger>
|
|
169
|
+
<Collapsible.Content data-testid="content">Content</Collapsible.Content>
|
|
170
|
+
</Collapsible.Root>,
|
|
171
|
+
);
|
|
172
|
+
screen.getByTestId("trigger").focus();
|
|
173
|
+
await user.keyboard("a");
|
|
174
|
+
expect(screen.getByTestId("content")).not.toBeVisible();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("does not toggle on Enter when disabled (non-native asChild)", async () => {
|
|
178
|
+
const user = userEvent.setup();
|
|
179
|
+
render(
|
|
180
|
+
<Collapsible.Root disabled>
|
|
181
|
+
<Collapsible.Trigger asChild>
|
|
182
|
+
<div tabIndex={0} data-testid="trigger">
|
|
183
|
+
Toggle
|
|
184
|
+
</div>
|
|
185
|
+
</Collapsible.Trigger>
|
|
186
|
+
<Collapsible.Content data-testid="content">Content</Collapsible.Content>
|
|
187
|
+
</Collapsible.Root>,
|
|
188
|
+
);
|
|
189
|
+
screen.getByTestId("trigger").focus();
|
|
190
|
+
await user.keyboard("{Enter}");
|
|
191
|
+
expect(screen.getByTestId("content")).not.toBeVisible();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("does not toggle on Space when disabled (non-native asChild)", async () => {
|
|
195
|
+
const user = userEvent.setup();
|
|
196
|
+
render(
|
|
197
|
+
<Collapsible.Root disabled>
|
|
198
|
+
<Collapsible.Trigger asChild>
|
|
199
|
+
<div tabIndex={0} data-testid="trigger">
|
|
200
|
+
Toggle
|
|
201
|
+
</div>
|
|
202
|
+
</Collapsible.Trigger>
|
|
203
|
+
<Collapsible.Content data-testid="content">Content</Collapsible.Content>
|
|
204
|
+
</Collapsible.Root>,
|
|
205
|
+
);
|
|
206
|
+
screen.getByTestId("trigger").focus();
|
|
207
|
+
await user.keyboard(" ");
|
|
208
|
+
expect(screen.getByTestId("content")).not.toBeVisible();
|
|
209
|
+
});
|
|
210
|
+
|
|
122
211
|
it("should NOT toggle on Enter or Space when disabled (asChild)", async () => {
|
|
123
212
|
// Arrange
|
|
124
213
|
const user = userEvent.setup();
|
|
@@ -58,6 +58,33 @@ describe("Collapsible basic rendering tests", () => {
|
|
|
58
58
|
expect(content.id).not.toBe("");
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
+
it("gives the Trigger a '-trigger' suffixed id distinct from the Content id", () => {
|
|
62
|
+
// Arrange
|
|
63
|
+
render(
|
|
64
|
+
<Collapsible.Root>
|
|
65
|
+
<Collapsible.Trigger>Toggle</Collapsible.Trigger>
|
|
66
|
+
<Collapsible.Content data-testid="content">Content</Collapsible.Content>
|
|
67
|
+
</Collapsible.Root>,
|
|
68
|
+
);
|
|
69
|
+
const trigger = screen.getByRole("button", { name: "Toggle" });
|
|
70
|
+
const content = screen.getByTestId("content");
|
|
71
|
+
|
|
72
|
+
// Assert — a derived, non-empty id that cannot collide with the content's.
|
|
73
|
+
expect(trigger.id).toMatch(/-trigger$/);
|
|
74
|
+
expect(trigger.id).not.toBe(content.id);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("sets a displayName on the compound and each sub-component", () => {
|
|
78
|
+
// Assert — empty displayNames would render each as anonymous in DevTools.
|
|
79
|
+
// `Collapsible`, `Collapsible.Root`, and the underlying function are one
|
|
80
|
+
// object (Object.assign compound), so the compound's "Collapsible" alias is
|
|
81
|
+
// the observable Root displayName; the sub-components are distinct objects.
|
|
82
|
+
expect(Collapsible.displayName).toBe("Collapsible");
|
|
83
|
+
expect(Collapsible.Trigger.displayName).toBe("CollapsibleTrigger");
|
|
84
|
+
expect(Collapsible.Content.displayName).toBe("CollapsibleContent");
|
|
85
|
+
expect(Collapsible.TriggerIcon.displayName).toBe("CollapsibleTriggerIcon");
|
|
86
|
+
});
|
|
87
|
+
|
|
61
88
|
it("should hide Content when closed", () => {
|
|
62
89
|
// Arrange
|
|
63
90
|
render(
|