@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
|
@@ -28,6 +28,64 @@ describe("Activation mode tests", () => {
|
|
|
28
28
|
expect(secondTabPanel).not.toHaveAttribute("hidden");
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
+
it("does not activate a tab on arrow navigation in manual mode", async () => {
|
|
32
|
+
// Directly asserts the manual guard: arrow moves focus without activating,
|
|
33
|
+
// so Tab 1 stays selected and Tab 2 does not.
|
|
34
|
+
const user = userEvent.setup();
|
|
35
|
+
render(
|
|
36
|
+
<Tabs.Root defaultValue="tab1" activationMode="manual">
|
|
37
|
+
<Tabs.List label="Test tabs">
|
|
38
|
+
<Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
|
|
39
|
+
<Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
|
|
40
|
+
</Tabs.List>
|
|
41
|
+
<Tabs.Content value="tab1">Content 1</Tabs.Content>
|
|
42
|
+
<Tabs.Content value="tab2">Content 2</Tabs.Content>
|
|
43
|
+
</Tabs.Root>,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
await user.tab();
|
|
47
|
+
await user.keyboard("{ArrowRight}");
|
|
48
|
+
|
|
49
|
+
expect(screen.getByRole("tab", { name: "Tab 2" })).toHaveAttribute(
|
|
50
|
+
"aria-selected",
|
|
51
|
+
"false",
|
|
52
|
+
);
|
|
53
|
+
expect(screen.getByRole("tab", { name: "Tab 1" })).toHaveAttribute(
|
|
54
|
+
"aria-selected",
|
|
55
|
+
"true",
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("activates a non-native asChild tab via the roving Enter handler (manual)", async () => {
|
|
60
|
+
// A plain <div> fires no synthetic click on Enter, so activation can only
|
|
61
|
+
// come from the roving hook's includeActivate mapping — the guarantee a
|
|
62
|
+
// native <button> would mask.
|
|
63
|
+
const user = userEvent.setup();
|
|
64
|
+
render(
|
|
65
|
+
<Tabs.Root defaultValue="tab1" activationMode="manual">
|
|
66
|
+
<Tabs.List label="Test tabs">
|
|
67
|
+
<Tabs.Trigger asChild value="tab1">
|
|
68
|
+
<div>Tab 1</div>
|
|
69
|
+
</Tabs.Trigger>
|
|
70
|
+
<Tabs.Trigger asChild value="tab2">
|
|
71
|
+
<div>Tab 2</div>
|
|
72
|
+
</Tabs.Trigger>
|
|
73
|
+
</Tabs.List>
|
|
74
|
+
<Tabs.Content value="tab1">Content 1</Tabs.Content>
|
|
75
|
+
<Tabs.Content value="tab2">Content 2</Tabs.Content>
|
|
76
|
+
</Tabs.Root>,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
await user.tab();
|
|
80
|
+
await user.keyboard("{ArrowRight}");
|
|
81
|
+
await user.keyboard("{Enter}");
|
|
82
|
+
|
|
83
|
+
expect(screen.getByRole("tab", { name: "Tab 2" })).toHaveAttribute(
|
|
84
|
+
"aria-selected",
|
|
85
|
+
"true",
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
31
89
|
it("should not activate the second tab when focusing its tab in manual mode", async () => {
|
|
32
90
|
// Arrange
|
|
33
91
|
const user = userEvent.setup();
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import { render, screen } from "@testing-library/react";
|
|
2
2
|
|
|
3
3
|
import { Tabs, TabsOrientation } from "../index.ts";
|
|
4
|
+
import { TabsContext } from "../TabsContext";
|
|
5
|
+
|
|
6
|
+
describe("Tabs displayNames", () => {
|
|
7
|
+
it("sets a displayName on the compound, each sub-component, and the context", () => {
|
|
8
|
+
// Assert — empty displayNames would render each as anonymous in DevTools.
|
|
9
|
+
// `Tabs`, `Tabs.Root`, and the underlying component are one object
|
|
10
|
+
// (Object.assign compound), so the compound's "Tabs" alias is the
|
|
11
|
+
// observable Root displayName; the sub-components are distinct objects.
|
|
12
|
+
expect(Tabs.displayName).toBe("Tabs");
|
|
13
|
+
expect(Tabs.List.displayName).toBe("TabsList");
|
|
14
|
+
expect(Tabs.Trigger.displayName).toBe("TabsTrigger");
|
|
15
|
+
expect(Tabs.Content.displayName).toBe("TabsContent");
|
|
16
|
+
expect(TabsContext.displayName).toBe("TabsContext");
|
|
17
|
+
});
|
|
18
|
+
});
|
|
4
19
|
|
|
5
20
|
describe("Tabs basic rendering tests", () => {
|
|
6
21
|
describe("Tabs.Root", () => {
|
|
@@ -110,6 +110,30 @@ describe('Tabs change event callbacks', () => {
|
|
|
110
110
|
expect(handleChange).not.toHaveBeenCalled();
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
+
it('does not re-fire onChange when re-clicking a tab activated after mount', async () => {
|
|
114
|
+
// No defaultValue: the guard reads an activeValue that changes after the
|
|
115
|
+
// first click, so a stale activateTab closure would re-fire on the second.
|
|
116
|
+
const user = userEvent.setup();
|
|
117
|
+
const handleChange = vi.fn();
|
|
118
|
+
|
|
119
|
+
render(
|
|
120
|
+
<Tabs.Root onChange={handleChange}>
|
|
121
|
+
<Tabs.List label="Test tabs">
|
|
122
|
+
<Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
|
|
123
|
+
<Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
|
|
124
|
+
</Tabs.List>
|
|
125
|
+
<Tabs.Content value="tab1">Content 1</Tabs.Content>
|
|
126
|
+
<Tabs.Content value="tab2">Content 2</Tabs.Content>
|
|
127
|
+
</Tabs.Root>,
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const tab2 = screen.getByRole('tab', { name: 'Tab 2' });
|
|
131
|
+
await user.click(tab2);
|
|
132
|
+
await user.click(tab2);
|
|
133
|
+
|
|
134
|
+
expect(handleChange).toHaveBeenCalledTimes(1);
|
|
135
|
+
});
|
|
136
|
+
|
|
113
137
|
it('should not call onValueChange if clicking already active tab in controlled mode', async () => {
|
|
114
138
|
const user = userEvent.setup();
|
|
115
139
|
const handleValueChange = vi.fn();
|
|
@@ -2,6 +2,42 @@ import { render, screen } from "@testing-library/react";
|
|
|
2
2
|
import { Tabs } from "../Tabs";
|
|
3
3
|
import userEvent from "@testing-library/user-event";
|
|
4
4
|
|
|
5
|
+
describe("Tabs disabled re-registration", () => {
|
|
6
|
+
it("re-registers a trigger's disabled flag when it changes after mount", async () => {
|
|
7
|
+
// Arrange — automatic mode: arrowing onto a tab activates it unless the
|
|
8
|
+
// trigger is registered as disabled. Tab 2 starts enabled, then disables.
|
|
9
|
+
const user = userEvent.setup();
|
|
10
|
+
function Fixture({ tab2Disabled }: { tab2Disabled: boolean }) {
|
|
11
|
+
return (
|
|
12
|
+
<Tabs.Root defaultValue="tab1">
|
|
13
|
+
<Tabs.List label="Test tabs">
|
|
14
|
+
<Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
|
|
15
|
+
<Tabs.Trigger value="tab2" disabled={tab2Disabled}>
|
|
16
|
+
Tab 2
|
|
17
|
+
</Tabs.Trigger>
|
|
18
|
+
</Tabs.List>
|
|
19
|
+
<Tabs.Content value="tab1">Content 1</Tabs.Content>
|
|
20
|
+
<Tabs.Content value="tab2">Content 2</Tabs.Content>
|
|
21
|
+
</Tabs.Root>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
const { rerender } = render(<Fixture tab2Disabled={false} />);
|
|
25
|
+
|
|
26
|
+
// Act — disable Tab 2 after mount, then arrow onto it.
|
|
27
|
+
rerender(<Fixture tab2Disabled />);
|
|
28
|
+
await user.tab();
|
|
29
|
+
await user.keyboard("{ArrowRight}");
|
|
30
|
+
|
|
31
|
+
// Assert — the registrar effect re-ran, so Tab 2 is focused but NOT
|
|
32
|
+
// activated. A stale registration would treat it as enabled and activate
|
|
33
|
+
// it on arrow (automatic mode).
|
|
34
|
+
expect(screen.getByRole("tab", { name: "Tab 2" })).toHaveAttribute(
|
|
35
|
+
"aria-selected",
|
|
36
|
+
"false",
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
5
41
|
describe("Tabs disabled tabs tests", () => {
|
|
6
42
|
it("should have the aria-disabled attribute set to false by default", () => {
|
|
7
43
|
// Arrange
|
|
@@ -26,6 +26,23 @@ describe("Tabs lazyMount", () => {
|
|
|
26
26
|
expect(screen.getByText("Content 3")).toBeInTheDocument();
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
+
it("defaults lazyMount to off when the prop is omitted (eager render)", () => {
|
|
30
|
+
// Omitting the prop must fall back to eager mounting — a default of `true`
|
|
31
|
+
// would hide inactive panel children on mount.
|
|
32
|
+
render(
|
|
33
|
+
<Tabs.Root defaultValue="tab1">
|
|
34
|
+
<Tabs.List label="Test tabs">
|
|
35
|
+
<Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
|
|
36
|
+
<Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
|
|
37
|
+
</Tabs.List>
|
|
38
|
+
<Tabs.Content value="tab1">Content 1</Tabs.Content>
|
|
39
|
+
<Tabs.Content value="tab2">Content 2</Tabs.Content>
|
|
40
|
+
</Tabs.Root>,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
expect(screen.getByText("Content 2")).toBeInTheDocument();
|
|
44
|
+
});
|
|
45
|
+
|
|
29
46
|
it("should not render inactive panel children on initial mount when lazyMount is true", () => {
|
|
30
47
|
renderTabs(true);
|
|
31
48
|
|
|
@@ -14,6 +14,11 @@ export function useTabsContent({ value }: Pick<TabsContentProps, "value">) {
|
|
|
14
14
|
|
|
15
15
|
// Track whether this panel has ever been active. Starts true for the
|
|
16
16
|
// default active panel; flips once on first activation for all others.
|
|
17
|
+
// The mount effect below sets `activated` for the active panel within the
|
|
18
|
+
// same commit, so the initialiser's value is only ever observable as a
|
|
19
|
+
// pre-effect first-paint flash (not reproducible in jsdom); for inactive
|
|
20
|
+
// panels the initialiser's `false` and a mutated `undefined` are both falsy.
|
|
21
|
+
// Stryker disable next-line ArrowFunction: equivalent under the test env — see above.
|
|
17
22
|
const [activated, setActivated] = useState(() => isActive);
|
|
18
23
|
useEffect(() => {
|
|
19
24
|
if (isActive) setActivated(true);
|
|
@@ -18,15 +18,23 @@ type TriggerMeta = {
|
|
|
18
18
|
|
|
19
19
|
export function useTabsRoot(
|
|
20
20
|
{
|
|
21
|
-
orientation
|
|
22
|
-
dir
|
|
23
|
-
activationMode
|
|
21
|
+
orientation,
|
|
22
|
+
dir,
|
|
23
|
+
activationMode,
|
|
24
24
|
defaultValue,
|
|
25
25
|
value,
|
|
26
26
|
onValueChange,
|
|
27
27
|
onChange,
|
|
28
|
-
lazyMount
|
|
29
|
-
}:
|
|
28
|
+
lazyMount,
|
|
29
|
+
}: // `TabsRoot` always resolves these four before calling the hook, so they
|
|
30
|
+
// are required here (a default would be dead code the tests never reach).
|
|
31
|
+
Omit<TabsRootProps, "className"> &
|
|
32
|
+
Required<
|
|
33
|
+
Pick<
|
|
34
|
+
TabsRootProps,
|
|
35
|
+
"orientation" | "dir" | "activationMode" | "lazyMount"
|
|
36
|
+
>
|
|
37
|
+
>,
|
|
30
38
|
ref: Ref<TabsImperativeApi>,
|
|
31
39
|
) {
|
|
32
40
|
const tabsId = useId();
|
|
@@ -47,6 +55,10 @@ export function useTabsRoot(
|
|
|
47
55
|
(
|
|
48
56
|
triggerValue: string,
|
|
49
57
|
element: HTMLButtonElement | null,
|
|
58
|
+
// The default is only reached by the cleanup call `registerTrigger(value,
|
|
59
|
+
// null)`, where `element` is null and `disabled` is discarded — so its
|
|
60
|
+
// value is never observable.
|
|
61
|
+
// Stryker disable next-line BooleanLiteral
|
|
50
62
|
disabled = false,
|
|
51
63
|
) => {
|
|
52
64
|
registerTriggerBase(
|
|
@@ -54,6 +66,9 @@ export function useTabsRoot(
|
|
|
54
66
|
element ? { element, disabled } : null,
|
|
55
67
|
);
|
|
56
68
|
},
|
|
69
|
+
// `registerTriggerBase` is a stable useCollection callback, so emptying this
|
|
70
|
+
// array yields the identical memoised function.
|
|
71
|
+
// Stryker disable next-line ArrayDeclaration: equivalent — stable dependency.
|
|
57
72
|
[registerTriggerBase],
|
|
58
73
|
);
|
|
59
74
|
|
|
@@ -71,8 +86,14 @@ export function useTabsRoot(
|
|
|
71
86
|
|
|
72
87
|
const focusTrigger = useCallback(
|
|
73
88
|
(triggerValue: string) => {
|
|
89
|
+
// `triggerValue` is always sourced from the navigable set (registered
|
|
90
|
+
// triggers), so a value that reaches here has a live entry.
|
|
91
|
+
// Stryker disable next-line OptionalChaining: unreachable given that invariant.
|
|
74
92
|
triggersRef.current.get(triggerValue)?.element.focus();
|
|
75
93
|
},
|
|
94
|
+
// `triggersRef` is a stable RefObject, so emptying this array yields the
|
|
95
|
+
// identical memoised function.
|
|
96
|
+
// Stryker disable next-line ArrayDeclaration: equivalent — stable dependency.
|
|
76
97
|
[triggersRef],
|
|
77
98
|
);
|
|
78
99
|
|
|
@@ -100,6 +121,9 @@ export function useTabsRoot(
|
|
|
100
121
|
}
|
|
101
122
|
|
|
102
123
|
if (isControlled) {
|
|
124
|
+
// Controlled mode requires `onValueChange` (discriminated-union prop
|
|
125
|
+
// type), so the optional call can never no-op in valid usage.
|
|
126
|
+
// Stryker disable next-line OptionalChaining: unreachable — controlled requires onValueChange.
|
|
103
127
|
onValueChange?.(newValue);
|
|
104
128
|
} else {
|
|
105
129
|
setActiveValue(newValue);
|
|
@@ -51,6 +51,9 @@ export function useTabsTrigger({
|
|
|
51
51
|
(newValue: string, index: number) => {
|
|
52
52
|
if (activeValue === newValue) return;
|
|
53
53
|
if (isControlled) {
|
|
54
|
+
// Controlled mode requires `onValueChange` (discriminated-union prop
|
|
55
|
+
// type), so the optional call can never no-op in valid usage.
|
|
56
|
+
// Stryker disable next-line OptionalChaining: unreachable — controlled requires onValueChange.
|
|
54
57
|
onValueChange?.(newValue);
|
|
55
58
|
} else {
|
|
56
59
|
setActiveValue(newValue);
|
|
@@ -87,10 +90,12 @@ export function useTabsTrigger({
|
|
|
87
90
|
includeActivate: true,
|
|
88
91
|
onNavigate: (targetValue, action) => {
|
|
89
92
|
const isDisabled = disabledTriggerValues.has(targetValue);
|
|
93
|
+
// Automatic mode activates on any navigation; manual mode activates only
|
|
94
|
+
// on an explicit activation key (Enter/Space). Since the mode is always
|
|
95
|
+
// "automatic" or "manual", `automatic || isActivationKey` captures both.
|
|
90
96
|
const shouldActivate =
|
|
91
97
|
!isDisabled &&
|
|
92
|
-
(activationMode === "automatic" ||
|
|
93
|
-
(activationMode === "manual" && action === "activate"));
|
|
98
|
+
(activationMode === "automatic" || action === "activate");
|
|
94
99
|
if (shouldActivate) {
|
|
95
100
|
activateTab(targetValue, triggerValues.indexOf(targetValue));
|
|
96
101
|
}
|
package/src/Tabs/types.ts
CHANGED
|
@@ -60,9 +60,18 @@ export type ControlledTabsRootProps = {
|
|
|
60
60
|
* the `HTMLDivElement` `ref` left in, a styled wrapper that spreads these props
|
|
61
61
|
* straight back into `Tabs.Root` (`<Tabs.Root {...props} />`) would fail to
|
|
62
62
|
* type-check on incompatible `ref` types (D58). */
|
|
63
|
-
export type TabsRootProps = Omit<
|
|
63
|
+
export type TabsRootProps = Omit<
|
|
64
|
+
ComponentProps<"div">,
|
|
65
|
+
"onChange" | "ref" | "dir" | "defaultValue"
|
|
66
|
+
> & {
|
|
67
|
+
/** Layout axis; see {@link TabsOrientation}.
|
|
68
|
+
* @default "horizontal" */
|
|
64
69
|
orientation?: TabsOrientation;
|
|
70
|
+
/** Reading direction; see {@link TabsReadingDirection}. Inherited from the
|
|
71
|
+
* nearest `DirectionProvider` when omitted, falling back to `"ltr"`. */
|
|
65
72
|
dir?: TabsReadingDirection;
|
|
73
|
+
/** When a focused trigger becomes active; see {@link TabsActivationMode}.
|
|
74
|
+
* @default "automatic" */
|
|
66
75
|
activationMode?: TabsActivationMode;
|
|
67
76
|
/** When `true`, a panel's children are not rendered until that tab is first
|
|
68
77
|
* activated. Once mounted they remain in the DOM across subsequent tab
|
|
@@ -105,8 +114,19 @@ export type TabsListProps = Omit<
|
|
|
105
114
|
"label" | "aria-labelledby"
|
|
106
115
|
> &
|
|
107
116
|
(
|
|
108
|
-
| {
|
|
109
|
-
|
|
117
|
+
| {
|
|
118
|
+
/** Accessible name for the tablist, announced as `aria-label`. Pick a
|
|
119
|
+
* short, human-readable description of the set (e.g. `"Account
|
|
120
|
+
* sections"`, not `"Tabs"`). Mutually exclusive with `ariaLabelledBy`. */
|
|
121
|
+
label: string;
|
|
122
|
+
ariaLabelledBy?: never;
|
|
123
|
+
}
|
|
124
|
+
| {
|
|
125
|
+
label?: never;
|
|
126
|
+
/** Id of an existing element to use as the tablist's accessible name,
|
|
127
|
+
* set as `aria-labelledby`. Mutually exclusive with `label`. */
|
|
128
|
+
ariaLabelledBy: string;
|
|
129
|
+
}
|
|
110
130
|
);
|
|
111
131
|
/** Props for `Tabs.Trigger` — an individual `role="tab"` button. Identified by
|
|
112
132
|
* its required `value`, which links it to the `Tabs.Content` of the same value.
|
|
@@ -114,9 +134,14 @@ export type TabsListProps = Omit<
|
|
|
114
134
|
* `asChild` and the `ref` type parameter. */
|
|
115
135
|
export type TabsTriggerProps<T extends HTMLElement = HTMLButtonElement> = Omit<
|
|
116
136
|
ComponentProps<"button">,
|
|
117
|
-
"ref"
|
|
137
|
+
"ref" | "value"
|
|
118
138
|
> & {
|
|
139
|
+
/** Removes the trigger from the roving tab order and marks it
|
|
140
|
+
* `aria-disabled`/`data-disabled`; arrow-key navigation skips it.
|
|
141
|
+
* @default false */
|
|
119
142
|
disabled?: boolean;
|
|
143
|
+
/** Identifies this trigger, linking it to the `Tabs.Content` of the same
|
|
144
|
+
* value. Not the visible label — use `children` for that. */
|
|
120
145
|
value: string;
|
|
121
146
|
/** Render the child element instead of the default `<button>`. All tab
|
|
122
147
|
* ARIA attributes and event handlers are merged onto the child. The child
|
|
@@ -39,10 +39,18 @@ import { TextareaProps } from "./types";
|
|
|
39
39
|
* textarea[data-disabled] { opacity: 0.5; cursor: not-allowed; }
|
|
40
40
|
* ```
|
|
41
41
|
*
|
|
42
|
+
* **Styling hooks.**
|
|
43
|
+
* - `data-disabled=""` — present when `disabled` is `true`; omitted otherwise.
|
|
44
|
+
* Target with `[data-disabled]` rather than the `:disabled` pseudo-class so
|
|
45
|
+
* styles work identically in both native and `asChild` modes.
|
|
46
|
+
*
|
|
42
47
|
* **`asChild` composition.** Renders the consumer's element instead of
|
|
43
48
|
* `<textarea>`, merging all props (aria-*, data-*, event handlers, ref)
|
|
44
49
|
* via the {@link Slot} utility. Use it to wrap an autosizing textarea
|
|
45
|
-
* implementation while keeping this component's prop contract.
|
|
50
|
+
* implementation while keeping this component's prop contract. Event handlers
|
|
51
|
+
* compose with the child's own handlers (child runs first).
|
|
52
|
+
*
|
|
53
|
+
* @extends HTMLTextAreaElement
|
|
46
54
|
*
|
|
47
55
|
* @example Basic usage
|
|
48
56
|
* ```tsx
|
|
@@ -104,4 +104,9 @@ describe("Textarea basic rendering", () => {
|
|
|
104
104
|
// Assert
|
|
105
105
|
expect(onChange).toHaveBeenCalledTimes(2);
|
|
106
106
|
});
|
|
107
|
+
|
|
108
|
+
it('sets displayName to "Textarea" for React DevTools and stack traces', () => {
|
|
109
|
+
// Assert — an empty displayName would render it as an anonymous component.
|
|
110
|
+
expect(Textarea.displayName).toBe("Textarea");
|
|
111
|
+
});
|
|
107
112
|
});
|
package/src/Textarea/types.ts
CHANGED
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
import { ComponentProps, ReactNode, Ref } from "react";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Props for {@link Textarea} — all native `<textarea>` attributes
|
|
5
|
-
* `asChild` escape hatch
|
|
4
|
+
* Props for {@link Textarea} — all native `<textarea>` attributes (with `ref`
|
|
5
|
+
* typed as `Ref<HTMLTextAreaElement>`) plus the `asChild` escape hatch.
|
|
6
6
|
*/
|
|
7
|
-
export type TextareaProps = ComponentProps<"textarea"> & {
|
|
7
|
+
export type TextareaProps = Omit<ComponentProps<"textarea">, "ref"> & {
|
|
8
|
+
/**
|
|
9
|
+
* Renders the child element instead of a native `<textarea>`, merging all
|
|
10
|
+
* props — aria-*, data-*, event handlers, ref — onto it via {@link Slot}.
|
|
11
|
+
* Event handlers compose (child runs first). Use this to wrap a third-party
|
|
12
|
+
* autosizing textarea while keeping this component's prop contract.
|
|
13
|
+
* @default false
|
|
14
|
+
*/
|
|
8
15
|
asChild?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Forwarded to the underlying `HTMLTextAreaElement`. Under `asChild`, merged
|
|
18
|
+
* onto the rendered child via {@link Slot}.
|
|
19
|
+
*/
|
|
9
20
|
ref?: Ref<HTMLTextAreaElement>;
|
|
21
|
+
/**
|
|
22
|
+
* Child content. Under `asChild`, must be a single React element that
|
|
23
|
+
* {@link Slot} merges props onto. Not used when rendering a native
|
|
24
|
+
* `<textarea>` (the element's content is controlled by `value` /
|
|
25
|
+
* `defaultValue`, not children).
|
|
26
|
+
*/
|
|
10
27
|
children?: ReactNode;
|
|
11
28
|
};
|
package/src/Toggle/Toggle.tsx
CHANGED
|
@@ -11,24 +11,50 @@ import { ToggleProps } from "./types";
|
|
|
11
11
|
* with the `aria-pressed` attribute. Renders a native
|
|
12
12
|
* `<button type="button">` by default.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
14
|
+
* **Controlled vs uncontrolled.** Two state modes, statically discriminated at
|
|
15
|
+
* the type level so TypeScript rejects mixing them:
|
|
15
16
|
*
|
|
16
17
|
* - **Uncontrolled** — pass
|
|
17
|
-
* {@link ToggleProps.defaultPressed | `defaultPressed`} (or omit
|
|
18
|
-
* unpressed on mount). The component owns the value internally.
|
|
19
|
-
* - **Controlled** — pass
|
|
20
|
-
* {@link ToggleProps.
|
|
21
|
-
*
|
|
22
|
-
* The parent owns the value; every click defers back through the callback.
|
|
18
|
+
* {@link ToggleProps.defaultPressed | `defaultPressed`} (or omit entirely
|
|
19
|
+
* for unpressed on mount). The component owns the pressed value internally.
|
|
20
|
+
* - **Controlled** — pass {@link ToggleProps.pressed | `pressed`} *and*
|
|
21
|
+
* {@link ToggleProps.onPressedChange | `onPressedChange`} together. The
|
|
22
|
+
* parent owns the value; every click defers back through the callback.
|
|
23
23
|
*
|
|
24
|
-
* **
|
|
24
|
+
* **Keyboard support.** Keyboard activation is provided natively by the
|
|
25
|
+
* underlying `<button>` element — no custom `keydown` listeners are needed:
|
|
25
26
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
27
|
+
* | Key | Behaviour |
|
|
28
|
+
* | ------- | -------------------------------------- |
|
|
29
|
+
* | `Space` | Toggles the button (native `<button>`) |
|
|
30
|
+
* | `Enter` | Toggles the button (native `<button>`) |
|
|
28
31
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
+
* **Styling hooks.**
|
|
33
|
+
* - `data-state="on" | "off"` — mirrors the pressed state; use
|
|
34
|
+
* `[data-state="on"]` to style the active appearance.
|
|
35
|
+
* - `data-disabled=""` — set when `disabled` is `true`; lets CSS target
|
|
36
|
+
* `[data-disabled]` without relying on the `:disabled` pseudo-class.
|
|
37
|
+
*
|
|
38
|
+
* **`asChild` composition.** Pass `asChild` to render any consumer-supplied
|
|
39
|
+
* element instead of the default `<button>`. The toggle's `aria-pressed`,
|
|
40
|
+
* `data-state`, composed `onClick`, and `ref` are merged onto the child via
|
|
41
|
+
* {@link Slot}. The native `<button>` is dropped; the consumer is responsible
|
|
42
|
+
* for making the element focusable (e.g. `tabIndex={0}`, `role="button"`).
|
|
43
|
+
*
|
|
44
|
+
* **Ref forwarding.** Pass a `ref` prop to access the underlying
|
|
45
|
+
* `HTMLButtonElement` directly (or the `asChild` element when `asChild` is
|
|
46
|
+
* set):
|
|
47
|
+
*
|
|
48
|
+
* ```tsx
|
|
49
|
+
* const ref = useRef<HTMLButtonElement>(null);
|
|
50
|
+
* <Toggle ref={ref} aria-label="Bold">B</Toggle>
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* **Disabled.** Sets the native `disabled` attribute (removing the button
|
|
54
|
+
* from the tab order and suppressing clicks) **and** sets `data-disabled=""`
|
|
55
|
+
* so CSS can target `[data-disabled]` without relying on `:disabled`.
|
|
56
|
+
*
|
|
57
|
+
* @extends HTMLButtonElement
|
|
32
58
|
*
|
|
33
59
|
* @example Uncontrolled
|
|
34
60
|
* ```tsx
|
|
@@ -41,6 +67,18 @@ import { ToggleProps } from "./types";
|
|
|
41
67
|
*
|
|
42
68
|
* <Toggle pressed={bold} onPressedChange={setBold} aria-label="Bold">B</Toggle>
|
|
43
69
|
* ```
|
|
70
|
+
*
|
|
71
|
+
* @example Disabled
|
|
72
|
+
* ```tsx
|
|
73
|
+
* <Toggle disabled aria-label="Bold (unavailable)">B</Toggle>
|
|
74
|
+
* ```
|
|
75
|
+
*
|
|
76
|
+
* @example asChild — render a custom element with toggle semantics
|
|
77
|
+
* ```tsx
|
|
78
|
+
* <Toggle asChild aria-label="Bold">
|
|
79
|
+
* <div role="button" tabIndex={0}>B</div>
|
|
80
|
+
* </Toggle>
|
|
81
|
+
* ```
|
|
44
82
|
*/
|
|
45
83
|
function Toggle({
|
|
46
84
|
defaultPressed,
|
|
@@ -25,4 +25,9 @@ describe("Toggle basic rendering", () => {
|
|
|
25
25
|
"off",
|
|
26
26
|
);
|
|
27
27
|
});
|
|
28
|
+
|
|
29
|
+
it('sets displayName to "Toggle" for React DevTools and stack traces', () => {
|
|
30
|
+
// Assert — an empty displayName would render it as an anonymous component.
|
|
31
|
+
expect(Toggle.displayName).toBe("Toggle");
|
|
32
|
+
});
|
|
28
33
|
});
|
package/src/Toggle/types.ts
CHANGED
|
@@ -1,27 +1,77 @@
|
|
|
1
1
|
import { ButtonHTMLAttributes, ReactNode, Ref } from "react";
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Uncontrolled {@link Toggle} props: seed the initial pressed state with
|
|
5
|
+
* `defaultPressed`; `pressed` and `onPressedChange` are disallowed in this
|
|
6
|
+
* mode. The component owns the pressed value internally.
|
|
7
|
+
*/
|
|
4
8
|
export type UncontrolledToggleProps = {
|
|
9
|
+
/**
|
|
10
|
+
* The pressed state on first render. Omit to start unpressed.
|
|
11
|
+
* Mutually exclusive with {@link ControlledToggleProps.pressed | `pressed`}.
|
|
12
|
+
* @default false
|
|
13
|
+
*/
|
|
5
14
|
defaultPressed?: boolean;
|
|
6
15
|
pressed?: never;
|
|
7
16
|
onPressedChange?: never;
|
|
8
17
|
};
|
|
9
18
|
|
|
10
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Controlled {@link Toggle} props: drive the pressed state with `pressed` and
|
|
21
|
+
* respond to toggle requests via `onPressedChange`; `defaultPressed` is
|
|
22
|
+
* disallowed in this mode. The parent owns the value.
|
|
23
|
+
*/
|
|
11
24
|
export type ControlledToggleProps = {
|
|
25
|
+
/**
|
|
26
|
+
* The current pressed state, owned by the caller. Every click fires
|
|
27
|
+
* {@link ControlledToggleProps.onPressedChange | `onPressedChange`} with the
|
|
28
|
+
* next value — the component does not update state itself.
|
|
29
|
+
*/
|
|
12
30
|
pressed: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Called with the next pressed value when the user clicks the toggle.
|
|
33
|
+
* The caller is responsible for updating the `pressed` prop accordingly.
|
|
34
|
+
*/
|
|
13
35
|
onPressedChange: (pressed: boolean) => void;
|
|
14
36
|
defaultPressed?: never;
|
|
15
37
|
};
|
|
16
38
|
|
|
17
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* Props common to both controlled and uncontrolled {@link Toggle} modes:
|
|
41
|
+
* `disabled`, `asChild`, `children`, `ref`, and all native `<button>`
|
|
42
|
+
* attributes except `type` (which is fixed to `"button"` by the component).
|
|
43
|
+
*/
|
|
18
44
|
export type ToggleBaseProps = {
|
|
45
|
+
/**
|
|
46
|
+
* When `true`, sets the native `disabled` attribute (removing the button
|
|
47
|
+
* from the tab order and suppressing clicks) and adds `data-disabled=""`
|
|
48
|
+
* so CSS can target `[data-disabled]` without relying on `:disabled`.
|
|
49
|
+
*/
|
|
19
50
|
disabled?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Renders the child element instead of the default `<button>`, merging
|
|
53
|
+
* `aria-pressed`, `data-state`, the composed `onClick`, and `ref` onto
|
|
54
|
+
* it via {@link Slot}. The child must be focusable (e.g. carry
|
|
55
|
+
* `tabIndex={0}` and `role="button"`).
|
|
56
|
+
* @default false
|
|
57
|
+
*/
|
|
20
58
|
asChild?: boolean;
|
|
59
|
+
/** Toggle content — text, icons, or any React node. */
|
|
21
60
|
children?: ReactNode;
|
|
61
|
+
/**
|
|
62
|
+
* Forwarded to the underlying `HTMLButtonElement`, or — under
|
|
63
|
+
* `asChild` — merged onto the rendered child element.
|
|
64
|
+
*/
|
|
22
65
|
ref?: Ref<HTMLButtonElement>;
|
|
23
66
|
} & Omit<ButtonHTMLAttributes<HTMLButtonElement>, "type">;
|
|
24
67
|
|
|
25
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* Props for the {@link Toggle} button — {@link ToggleBaseProps} combined with
|
|
70
|
+
* the controlled/uncontrolled discriminated union
|
|
71
|
+
* ({@link ControlledToggleProps} | {@link UncontrolledToggleProps}).
|
|
72
|
+
*
|
|
73
|
+
* TypeScript statically rejects mixing controlled and uncontrolled props
|
|
74
|
+
* (e.g. passing both `pressed` and `defaultPressed`).
|
|
75
|
+
*/
|
|
26
76
|
export type ToggleProps = ToggleBaseProps &
|
|
27
77
|
(UncontrolledToggleProps | ControlledToggleProps);
|
|
@@ -30,13 +30,13 @@ At most one item is pressed at a time. Pressing the active item again
|
|
|
30
30
|
```tsx
|
|
31
31
|
// Uncontrolled
|
|
32
32
|
<ToggleGroup.Root type="single" defaultValue="center" aria-label="Alignment">
|
|
33
|
-
|
|
33
|
+
...
|
|
34
34
|
</ToggleGroup.Root>
|
|
35
35
|
|
|
36
36
|
// Controlled — onValueChange receives string | undefined
|
|
37
37
|
const [align, setAlign] = useState<string | undefined>("center");
|
|
38
38
|
<ToggleGroup.Root type="single" value={align} onValueChange={setAlign} aria-label="Alignment">
|
|
39
|
-
|
|
39
|
+
...
|
|
40
40
|
</ToggleGroup.Root>
|
|
41
41
|
```
|
|
42
42
|
|
|
@@ -48,13 +48,13 @@ independently.
|
|
|
48
48
|
```tsx
|
|
49
49
|
// Uncontrolled
|
|
50
50
|
<ToggleGroup.Root type="multiple" defaultValue={["bold"]} aria-label="Formatting">
|
|
51
|
-
|
|
51
|
+
...
|
|
52
52
|
</ToggleGroup.Root>
|
|
53
53
|
|
|
54
54
|
// Controlled — onValueChange receives string[]
|
|
55
55
|
const [formats, setFormats] = useState<string[]>([]);
|
|
56
56
|
<ToggleGroup.Root type="multiple" value={formats} onValueChange={setFormats} aria-label="Formatting">
|
|
57
|
-
|
|
57
|
+
...
|
|
58
58
|
</ToggleGroup.Root>
|
|
59
59
|
```
|
|
60
60
|
|