@primitiv-ui/react 0.1.28 → 0.1.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/package.json +6 -1
- package/src/AccessibleIcon/AccessibleIcon.tsx +32 -9
- package/src/AccessibleIcon/__tests__/AccessibleIcon.test.tsx +5 -0
- package/src/AccessibleIcon/types.ts +15 -2
- package/src/Accordion/Accordion.tsx +25 -4
- package/src/Accordion/__tests__/Accordion.asChild.test.tsx +46 -0
- package/src/Accordion/__tests__/Accordion.basic-rendering.test.tsx +15 -0
- package/src/Accordion/__tests__/Accordion.disabled-items.test.tsx +42 -0
- package/src/Accordion/__tests__/Accordion.error-handling.test.tsx +33 -0
- package/src/Accordion/hooks/useAccordionRoot.ts +44 -7
- package/src/Accordion/hooks/useAccordionTrigger.ts +9 -1
- package/src/Accordion/types.ts +72 -21
- package/src/Alert/Alert.tsx +36 -7
- package/src/Alert/__tests__/Alert.test.tsx +5 -0
- package/src/Alert/types.ts +18 -2
- package/src/Avatar/Avatar.tsx +99 -15
- package/src/Avatar/__tests__/Avatar.basic-rendering.test.tsx +10 -0
- package/src/Avatar/__tests__/Avatar.fallback.test.tsx +30 -0
- package/src/Avatar/__tests__/Avatar.image-loading.test.tsx +35 -0
- package/src/Avatar/hooks/useAvatarImage.ts +16 -0
- package/src/Avatar/types.ts +15 -3
- package/src/Breadcrumb/Breadcrumb.tsx +136 -18
- package/src/Breadcrumb/README.md +47 -0
- package/src/Breadcrumb/__tests__/Breadcrumb.basic-rendering.test.tsx +11 -0
- package/src/Breadcrumb/__tests__/Breadcrumb.ellipsis.test.tsx +110 -0
- package/src/Breadcrumb/types.ts +73 -7
- package/src/Button/Button.tsx +2 -0
- package/src/Button/__tests__/Button.basic-rendering.test.tsx +6 -0
- package/src/Button/types.ts +21 -0
- package/src/Carousel/Carousel.tsx +263 -15
- package/src/Carousel/README.md +749 -72
- package/src/Carousel/__tests__/Carousel.autoplay-status.test.tsx +239 -0
- package/src/Carousel/__tests__/Carousel.drag-status.test.tsx +292 -0
- package/src/Carousel/__tests__/Carousel.dynamic-slides.test.tsx +38 -0
- package/src/Carousel/__tests__/Carousel.indicators.test.tsx +101 -0
- package/src/Carousel/__tests__/Carousel.infinite.test.tsx +1453 -0
- package/src/Carousel/__tests__/Carousel.instant-scroll.test.tsx +124 -0
- package/src/Carousel/__tests__/Carousel.intersection-observer.test.tsx +88 -0
- package/src/Carousel/__tests__/Carousel.loop.test.tsx +163 -0
- package/src/Carousel/__tests__/Carousel.loopEngine.test.ts +77 -0
- package/src/Carousel/__tests__/Carousel.mouse-drag.test.tsx +504 -0
- package/src/Carousel/__tests__/Carousel.multi-slide-bounds.test.tsx +91 -0
- package/src/Carousel/__tests__/Carousel.orientation.test.tsx +207 -0
- package/src/Carousel/__tests__/Carousel.overscroll.test.tsx +532 -0
- package/src/Carousel/__tests__/Carousel.page-snap-points.test.tsx +50 -0
- package/src/Carousel/__tests__/Carousel.progress-text.test.tsx +76 -0
- package/src/Carousel/__tests__/Carousel.reduced-motion.test.tsx +3 -6
- package/src/Carousel/__tests__/Carousel.refresh-progress.test.tsx +2 -5
- package/src/Carousel/__tests__/Carousel.scroll-margin.test.tsx +167 -0
- package/src/Carousel/__tests__/Carousel.scroll-progress.test.tsx +518 -0
- package/src/Carousel/__tests__/Carousel.scroll-snap-change.test.tsx +32 -11
- package/src/Carousel/__tests__/Carousel.scroll-sync.test.tsx +237 -24
- package/src/Carousel/__tests__/Carousel.scroll-to-index.test.tsx +115 -0
- package/src/Carousel/__tests__/Carousel.slide-snap.test.tsx +172 -0
- package/src/Carousel/__tests__/Carousel.slides-per-move.test.tsx +179 -1
- package/src/Carousel/__tests__/Carousel.slides-per-page.test.tsx +91 -7
- package/src/Carousel/__tests__/Carousel.snap-type.test.tsx +35 -0
- package/src/Carousel/__tests__/Carousel.transition-modes.test.tsx +30 -5
- package/src/Carousel/__tests__/Carousel.translations.test.tsx +23 -0
- package/src/Carousel/__tests__/Carousel.wheel.test.tsx +160 -0
- package/src/Carousel/hooks/index.ts +1 -0
- package/src/Carousel/hooks/useCarouselLoop.ts +869 -0
- package/src/Carousel/hooks/useCarouselRoot.ts +338 -42
- package/src/Carousel/hooks/useCarouselSlide.ts +44 -9
- package/src/Carousel/hooks/useCarouselViewport.ts +649 -46
- package/src/Carousel/loopEngine.ts +54 -0
- package/src/Carousel/types.ts +457 -35
- package/src/Checkbox/Checkbox.tsx +9 -1
- package/src/Checkbox/__tests__/Checkbox.basic-rendering.test.tsx +14 -0
- package/src/Checkbox/hooks/useCheckboxInput.ts +4 -0
- package/src/Checkbox/types.ts +19 -0
- package/src/CheckboxCard/CheckboxCard.tsx +13 -4
- package/src/CheckboxCard/__tests__/CheckboxCard.basic-rendering.test.tsx +7 -0
- package/src/CheckboxCard/types.ts +65 -17
- package/src/Collapsible/Collapsible.tsx +49 -3
- package/src/Collapsible/README.md +40 -0
- package/src/Collapsible/__tests__/Collapsible.asChild.test.tsx +89 -0
- package/src/Collapsible/__tests__/Collapsible.basic-rendering.test.tsx +27 -0
- package/src/Collapsible/__tests__/Collapsible.collapsedHeight.test.tsx +140 -0
- package/src/Collapsible/hooks/useCollapsibleTrigger.ts +3 -1
- package/src/Collapsible/types.ts +89 -22
- package/src/ContextMenu/ContextMenu.tsx +270 -4
- package/src/ContextMenu/types.ts +148 -6
- package/src/DirectionProvider/DirectionProvider.tsx +41 -9
- package/src/DirectionProvider/__tests__/DirectionProvider.test.tsx +5 -0
- package/src/DirectionProvider/types.ts +28 -3
- package/src/Divider/Divider.tsx +24 -10
- package/src/Divider/__tests__/Divider.test.tsx +5 -0
- package/src/Divider/types.ts +10 -3
- package/src/Drawer/Drawer.tsx +315 -0
- package/src/Drawer/README.md +124 -0
- package/src/Drawer/__tests__/Drawer.behavior.test.tsx +100 -0
- package/src/Drawer/__tests__/Drawer.focus-trap.test.tsx +55 -0
- package/src/Drawer/__tests__/Drawer.rendering.test.tsx +111 -0
- package/src/Drawer/__tests__/Drawer.side.test.tsx +69 -0
- package/src/Drawer/__tests__/dialog-polyfill.ts +39 -0
- package/src/Drawer/index.ts +2 -0
- package/src/Drawer/types.ts +66 -0
- package/src/Dropdown/Dropdown.tsx +117 -0
- package/src/Dropdown/types.ts +241 -11
- package/src/EmptyState/EmptyState.tsx +74 -4
- package/src/EmptyState/__tests__/EmptyState.Actions.test.tsx +7 -2
- package/src/EmptyState/__tests__/EmptyState.Description.test.tsx +3 -1
- package/src/EmptyState/__tests__/EmptyState.Root.test.tsx +10 -0
- package/src/EmptyState/__tests__/EmptyState.Title.test.tsx +3 -1
- package/src/EmptyState/types.ts +49 -6
- package/src/Field/Field.tsx +23 -0
- package/src/Field/__tests__/Field.basic-rendering.test.tsx +11 -0
- package/src/Field/__tests__/Field.error-handling.test.tsx +23 -0
- package/src/Field/__tests__/Field.state-cascade.test.tsx +21 -0
- package/src/Field/hooks/__tests__/useFieldProps.test.tsx +104 -9
- package/src/Field/types.ts +60 -12
- package/src/Fieldset/Fieldset.tsx +54 -18
- package/src/Fieldset/__tests__/Fieldset.basic-rendering.test.tsx +9 -0
- package/src/Fieldset/types.ts +20 -2
- package/src/Input/Input.tsx +14 -9
- package/src/Input/__tests__/Input.basic-rendering.test.tsx +5 -0
- package/src/Input/types.ts +19 -2
- package/src/InputGroup/InputGroup.tsx +11 -3
- package/src/InputGroup/__tests__/InputGroup.basic-rendering.test.tsx +14 -0
- package/src/InputGroup/types.ts +26 -5
- package/src/MillerColumns/MillerColumns.tsx +86 -19
- package/src/MillerColumns/types.ts +83 -27
- package/src/Modal/Modal.tsx +31 -4
- package/src/Modal/README.md +19 -10
- package/src/Modal/__tests__/Modal.focus-trap.test.tsx +162 -0
- package/src/Modal/hooks/useModalContent.ts +32 -0
- package/src/Modal/types.ts +110 -29
- package/src/NavigationMenu/NavigationMenu.tsx +890 -0
- package/src/NavigationMenu/NavigationMenuContext.ts +67 -0
- package/src/NavigationMenu/README.md +355 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.anchor-positioning.test.tsx +132 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.asChild.test.tsx +73 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.basic-rendering.test.tsx +92 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.contract.test.tsx +45 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.controlled-state.test.tsx +134 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.error-handling.test.tsx +62 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.fixtures.tsx +79 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.forceMount.test.tsx +52 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.hover-interaction.test.tsx +254 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.indicator.test.tsx +252 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-interaction.test.tsx +294 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-navigation.test.tsx +126 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.link.test.tsx +139 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.motion.test.tsx +568 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.mouse-interaction.test.tsx +76 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.uncontrolled-state.test.tsx +30 -0
- package/src/NavigationMenu/__tests__/NavigationMenu.viewport.test.tsx +113 -0
- package/src/NavigationMenu/constants.ts +5 -0
- package/src/NavigationMenu/hooks/index.ts +7 -0
- package/src/NavigationMenu/hooks/useNavigationMenuEntry.ts +33 -0
- package/src/NavigationMenu/hooks/useNavigationMenuIndicator.ts +70 -0
- package/src/NavigationMenu/hooks/useNavigationMenuLink.ts +72 -0
- package/src/NavigationMenu/hooks/useNavigationMenuRoot.ts +301 -0
- package/src/NavigationMenu/hooks/useNavigationMenuTopLevelEntry.ts +65 -0
- package/src/NavigationMenu/hooks/useNavigationMenuTrigger.ts +158 -0
- package/src/NavigationMenu/hooks/useNavigationMenuViewport.ts +101 -0
- package/src/NavigationMenu/index.ts +3 -0
- package/src/NavigationMenu/types.ts +259 -0
- package/src/NavigationMenu/utils.ts +89 -0
- package/src/Popover/Popover.tsx +427 -0
- package/src/Popover/PopoverContext.ts +13 -0
- package/src/Popover/README.md +106 -0
- package/src/Popover/__tests__/Popover.anchor.test.tsx +38 -0
- package/src/Popover/__tests__/Popover.asChild.test.tsx +160 -0
- package/src/Popover/__tests__/Popover.basic-rendering.test.tsx +106 -0
- package/src/Popover/__tests__/Popover.click-outside.test.tsx +149 -0
- package/src/Popover/__tests__/Popover.close.test.tsx +70 -0
- package/src/Popover/__tests__/Popover.content.test.tsx +58 -0
- package/src/Popover/__tests__/Popover.controlled-state.test.tsx +45 -0
- package/src/Popover/__tests__/Popover.error-handling.test.tsx +51 -0
- package/src/Popover/__tests__/Popover.focus.test.tsx +43 -0
- package/src/Popover/__tests__/Popover.keyboard-interaction.test.tsx +71 -0
- package/src/Popover/__tests__/Popover.mouse-interaction.test.tsx +69 -0
- package/src/Popover/__tests__/Popover.uncontrolled-state.test.tsx +44 -0
- package/src/Popover/hooks/index.ts +3 -0
- package/src/Popover/hooks/usePopoverContent.ts +98 -0
- package/src/Popover/hooks/usePopoverRoot.ts +84 -0
- package/src/Popover/hooks/usePopoverTrigger.ts +33 -0
- package/src/Popover/index.ts +2 -0
- package/src/Popover/types.ts +197 -0
- package/src/Portal/Portal.tsx +50 -10
- package/src/Portal/__tests__/Portal.basic-rendering.test.tsx +5 -0
- package/src/Portal/types.ts +5 -0
- package/src/Progress/Progress.tsx +33 -1
- package/src/Progress/__tests__/Progress.basic-rendering.test.tsx +10 -0
- package/src/Progress/__tests__/Progress.value.test.tsx +21 -0
- package/src/Progress/types.ts +20 -4
- package/src/Radio/Radio.tsx +9 -1
- package/src/Radio/__tests__/Radio.basic-rendering.test.tsx +14 -0
- package/src/Radio/hooks/useRadioRoot.ts +3 -0
- package/src/Radio/types.ts +20 -0
- package/src/RadioCard/RadioCard.tsx +11 -0
- package/src/RadioCard/__tests__/RadioCard.basic-rendering.test.tsx +8 -0
- package/src/RadioCard/__tests__/RadioCard.disabled-items.test.tsx +27 -0
- package/src/RadioCard/__tests__/RadioCard.keyboard-interaction.test.tsx +53 -0
- package/src/RadioCard/__tests__/RadioCard.ref-forwarding.test.tsx +54 -0
- package/src/RadioCard/__tests__/RadioCard.uncontrolled-state.test.tsx +21 -0
- package/src/RadioCard/hooks/useRadioCardRoot.ts +11 -0
- package/src/RadioCard/types.ts +95 -22
- package/src/RadioGroup/RadioGroup.tsx +11 -0
- package/src/RadioGroup/__tests__/RadioGroup.basic-rendering.test.tsx +8 -0
- package/src/RadioGroup/__tests__/RadioGroup.disabled.test.tsx +27 -0
- package/src/RadioGroup/__tests__/RadioGroup.ref-forwarding.test.tsx +29 -0
- package/src/RadioGroup/__tests__/RadioGroup.roving-tabindex.test.tsx +31 -0
- package/src/RadioGroup/hooks/useRadioGroupRoot.ts +11 -0
- package/src/RadioGroup/types.ts +73 -9
- package/src/SegmentedControl/README.md +154 -0
- package/src/SegmentedControl/SegmentedControl.tsx +309 -0
- package/src/SegmentedControl/SegmentedControlContext.ts +28 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.asChild.test.tsx +50 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.basic-rendering.test.tsx +100 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.controlled-state.test.tsx +107 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.disabled-keydown-guards.test.tsx +62 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.disabled.test.tsx +151 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.error-handling.test.tsx +17 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.keyboard-interaction.test.tsx +117 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.orientation.test.tsx +54 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.reading-direction.test.tsx +56 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.ref-forwarding.test.tsx +45 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.roving-tabindex.test.tsx +104 -0
- package/src/SegmentedControl/__tests__/SegmentedControl.uncontrolled-state.test.tsx +94 -0
- package/src/SegmentedControl/hooks/index.ts +2 -0
- package/src/SegmentedControl/hooks/useSegmentedControlContext.ts +1 -0
- package/src/SegmentedControl/hooks/useSegmentedControlRoot.ts +86 -0
- package/src/SegmentedControl/index.ts +2 -0
- package/src/SegmentedControl/types.ts +138 -0
- package/src/Select/README.md +122 -157
- package/src/Select/Select.tsx +595 -68
- package/src/Select/SelectContext.ts +38 -0
- package/src/Select/SelectItemIndicatorContext.ts +29 -0
- package/src/Select/__tests__/Select.asChild.test.tsx +2 -2
- package/src/Select/__tests__/Select.basic-rendering.test.tsx +22 -3
- package/src/Select/__tests__/Select.controlled-state.test.tsx +6 -6
- package/src/Select/__tests__/Select.data-attributes.test.tsx +4 -4
- package/src/Select/__tests__/Select.field-integration.test.tsx +11 -11
- package/src/Select/__tests__/Select.form.test.tsx +38 -0
- package/src/Select/__tests__/Select.group.test.tsx +6 -6
- package/src/Select/__tests__/Select.keyboard-edge.test.tsx +95 -0
- package/src/Select/__tests__/Select.keyboard.test.tsx +124 -0
- package/src/Select/__tests__/Select.mutation-hardening.test.tsx +317 -0
- package/src/Select/__tests__/Select.native-item.test.tsx +30 -0
- package/src/Select/__tests__/Select.open-close.test.tsx +57 -0
- package/src/Select/__tests__/Select.placeholder.test.tsx +4 -4
- package/src/Select/__tests__/Select.rich-asChild.test.tsx +60 -0
- package/src/Select/__tests__/Select.rich-mode.test.tsx +16 -0
- package/src/Select/__tests__/Select.selection.test.tsx +104 -0
- package/src/Select/__tests__/Select.separator.test.tsx +71 -0
- package/src/Select/__tests__/Select.strict-context.test.tsx +36 -0
- package/src/Select/__tests__/Select.typeahead.test.tsx +156 -0
- package/src/Select/__tests__/Select.value-mirror.test.tsx +240 -0
- package/src/Select/constants.ts +8 -0
- package/src/Select/hooks/index.ts +2 -0
- package/src/Select/hooks/useSelectContent.ts +149 -0
- package/src/Select/hooks/useSelectRoot.ts +149 -0
- package/src/Select/types.ts +199 -31
- package/src/SkipNav/SkipNav.tsx +69 -29
- package/src/SkipNav/__tests__/SkipNav.basic-rendering.test.tsx +6 -0
- package/src/SkipNav/types.ts +17 -9
- package/src/Slider/Slider.tsx +33 -1
- package/src/Slider/SliderContext.ts +28 -1
- package/src/Slider/types.ts +157 -9
- package/src/Slot/Slot.tsx +94 -19
- package/src/Slot/__tests__/Slot.test.tsx +110 -0
- package/src/Slot/composeEventHandlers.ts +26 -2
- package/src/Slot/types.ts +16 -0
- package/src/Status/Status.tsx +36 -15
- package/src/Status/__tests__/Status.test.tsx +5 -0
- package/src/Status/types.ts +12 -1
- package/src/Switch/Switch.tsx +15 -2
- package/src/Switch/__tests__/Switch.basic-rendering.test.tsx +7 -0
- package/src/Switch/hooks/useSwitchRoot.ts +3 -0
- package/src/Switch/types.ts +49 -22
- package/src/Table/Table.tsx +44 -10
- package/src/Table/types.ts +66 -17
- package/src/Tabs/Tabs.tsx +8 -0
- package/src/Tabs/types.ts +29 -4
- package/src/Textarea/Textarea.tsx +9 -1
- package/src/Textarea/__tests__/Textarea.basic-rendering.test.tsx +5 -0
- package/src/Textarea/types.ts +20 -3
- package/src/Toggle/Toggle.tsx +51 -13
- package/src/Toggle/__tests__/Toggle.basic-rendering.test.tsx +5 -0
- package/src/Toggle/types.ts +54 -4
- package/src/ToggleGroup/ToggleGroup.tsx +14 -1
- package/src/ToggleGroup/__tests__/ToggleGroup.asChild.test.tsx +20 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.basic-rendering.test.tsx +9 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.dynamic-items.test.tsx +70 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.error-handling.test.tsx +23 -0
- package/src/ToggleGroup/__tests__/ToggleGroup.keyboard-interaction.test.tsx +23 -1
- package/src/ToggleGroup/__tests__/ToggleGroup.ref-forwarding.test.tsx +58 -0
- package/src/ToggleGroup/hooks/useToggleGroupRoot.ts +21 -1
- package/src/ToggleGroup/types.ts +55 -2
- package/src/Tooltip/Tooltip.tsx +98 -20
- package/src/Tooltip/hooks/__tests__/useTooltipProvider.test.tsx +40 -0
- package/src/Tooltip/hooks/useTooltipProvider.ts +13 -1
- package/src/Tooltip/types.ts +89 -8
- package/src/Tree/Tree.tsx +187 -33
- package/src/Tree/types.ts +54 -12
- package/src/VisuallyHidden/VisuallyHidden.tsx +33 -8
- package/src/VisuallyHidden/__tests__/VisuallyHidden.test.tsx +15 -1
- package/src/VisuallyHidden/types.ts +11 -1
- package/src/index.ts +4 -0
- package/src/test/intersectionObserverPolyfill.ts +7 -0
- package/src/test/resizeObserverPolyfill.ts +80 -0
- package/src/utils/focusable.ts +15 -0
- package/src/utils/index.ts +1 -0
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);
|
|
@@ -46,6 +46,8 @@ import { ToggleGroupItemProps, ToggleGroupRootProps } from "./types";
|
|
|
46
46
|
* element in place of the native `<div>`, with the group's `role`,
|
|
47
47
|
* `data-orientation`, and ref merged in.
|
|
48
48
|
*
|
|
49
|
+
* @extends HTMLDivElement
|
|
50
|
+
*
|
|
49
51
|
* @example Single mode — text alignment
|
|
50
52
|
* ```tsx
|
|
51
53
|
* <ToggleGroup.Root type="single" defaultValue="left" aria-label="Alignment">
|
|
@@ -140,6 +142,7 @@ export function ToggleGroupRoot({
|
|
|
140
142
|
}
|
|
141
143
|
|
|
142
144
|
/** @internal */
|
|
145
|
+
// Stryker disable next-line StringLiteral: overwritten by the compound alias — an equivalent mutant.
|
|
143
146
|
ToggleGroupRoot.displayName = "ToggleGroupRoot";
|
|
144
147
|
|
|
145
148
|
/**
|
|
@@ -165,9 +168,19 @@ ToggleGroupRoot.displayName = "ToggleGroupRoot";
|
|
|
165
168
|
*
|
|
166
169
|
* **`asChild` prop.** Pass `asChild` to render any consumer element in
|
|
167
170
|
* place of the native `<button>`, with the Item's `aria-pressed`,
|
|
168
|
-
* `data-state`, `tabIndex`, and event handlers merged onto it.
|
|
171
|
+
* `data-state`, `tabIndex`, and event handlers merged onto it. The child
|
|
172
|
+
* must accept a `ref`. Useful for a link or a styled control that needs
|
|
173
|
+
* toggle-button semantics.
|
|
169
174
|
*
|
|
170
175
|
* @throws if rendered outside a `ToggleGroup.Root`.
|
|
176
|
+
*
|
|
177
|
+
* @extends HTMLButtonElement
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* ```tsx
|
|
181
|
+
* <ToggleGroup.Item value="bold">Bold</ToggleGroup.Item>
|
|
182
|
+
* <ToggleGroup.Item value="strike" disabled>Strikethrough</ToggleGroup.Item>
|
|
183
|
+
* ```
|
|
171
184
|
*/
|
|
172
185
|
export function ToggleGroupItem({
|
|
173
186
|
value,
|
|
@@ -62,4 +62,24 @@ describe("ToggleGroup asChild composition", () => {
|
|
|
62
62
|
expect(consumerClick).toHaveBeenCalledOnce();
|
|
63
63
|
expect(item).toHaveAttribute("aria-pressed", "true");
|
|
64
64
|
});
|
|
65
|
+
|
|
66
|
+
it("Item asChild activates on Space through the keyboard handler", async () => {
|
|
67
|
+
// A non-button element fires no native click on Space, so toggling can
|
|
68
|
+
// only come from the roving handler's activate mapping — the guarantee a
|
|
69
|
+
// native <button> would otherwise mask.
|
|
70
|
+
const user = userEvent.setup();
|
|
71
|
+
render(
|
|
72
|
+
<ToggleGroup.Root type="single" aria-label="Alignment">
|
|
73
|
+
<ToggleGroup.Item value="left" asChild>
|
|
74
|
+
<div>Left</div>
|
|
75
|
+
</ToggleGroup.Item>
|
|
76
|
+
</ToggleGroup.Root>,
|
|
77
|
+
);
|
|
78
|
+
const item = screen
|
|
79
|
+
.getByLabelText("Alignment")
|
|
80
|
+
.querySelector<HTMLElement>("[data-state]")!;
|
|
81
|
+
item.focus();
|
|
82
|
+
await user.keyboard(" ");
|
|
83
|
+
expect(item).toHaveAttribute("aria-pressed", "true");
|
|
84
|
+
});
|
|
65
85
|
});
|
|
@@ -47,4 +47,13 @@ describe("ToggleGroup basic rendering", () => {
|
|
|
47
47
|
"horizontal",
|
|
48
48
|
);
|
|
49
49
|
});
|
|
50
|
+
|
|
51
|
+
it("sets a displayName on the compound and Item", () => {
|
|
52
|
+
// Assert — empty displayNames would render each as anonymous in DevTools.
|
|
53
|
+
// `ToggleGroup`, `ToggleGroup.Root`, and the underlying function are one
|
|
54
|
+
// object (Object.assign compound), so the compound's "ToggleGroup" alias is
|
|
55
|
+
// the observable Root displayName; only Item is a distinct object.
|
|
56
|
+
expect(ToggleGroup.displayName).toBe("ToggleGroup");
|
|
57
|
+
expect(ToggleGroup.Item.displayName).toBe("ToggleGroupItem");
|
|
58
|
+
});
|
|
50
59
|
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
|
|
4
|
+
import { ToggleGroup } from "../ToggleGroup";
|
|
5
|
+
|
|
6
|
+
function ItemsFixture({ items }: { items: string[] }) {
|
|
7
|
+
return (
|
|
8
|
+
<ToggleGroup.Root type="single" aria-label="Alignment">
|
|
9
|
+
{items.map((v) => (
|
|
10
|
+
<ToggleGroup.Item key={v} value={v}>
|
|
11
|
+
{v}
|
|
12
|
+
</ToggleGroup.Item>
|
|
13
|
+
))}
|
|
14
|
+
</ToggleGroup.Root>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function DisabledFixture({ disabledItem }: { disabledItem?: string }) {
|
|
19
|
+
return (
|
|
20
|
+
<ToggleGroup.Root type="single" aria-label="Alignment">
|
|
21
|
+
<ToggleGroup.Item value="left" disabled={disabledItem === "left"}>
|
|
22
|
+
Left
|
|
23
|
+
</ToggleGroup.Item>
|
|
24
|
+
<ToggleGroup.Item value="center" disabled={disabledItem === "center"}>
|
|
25
|
+
Center
|
|
26
|
+
</ToggleGroup.Item>
|
|
27
|
+
<ToggleGroup.Item value="right" disabled={disabledItem === "right"}>
|
|
28
|
+
Right
|
|
29
|
+
</ToggleGroup.Item>
|
|
30
|
+
</ToggleGroup.Root>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe("ToggleGroup dynamic items — registration lifecycle", () => {
|
|
35
|
+
it("unregisters an unmounted item so the tab stop moves to the survivor", () => {
|
|
36
|
+
// Arrange — "a" is the first enabled item, so it holds the tab stop.
|
|
37
|
+
const { rerender } = render(<ItemsFixture items={["a", "b"]} />);
|
|
38
|
+
expect(screen.getByRole("button", { name: "a" })).toHaveAttribute(
|
|
39
|
+
"tabindex",
|
|
40
|
+
"0",
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// Act — remove "a"; its effect cleanup must unregister it.
|
|
44
|
+
rerender(<ItemsFixture items={["b"]} />);
|
|
45
|
+
|
|
46
|
+
// Assert — with "a" gone from the registry, "b" is now the tab stop. A
|
|
47
|
+
// no-op cleanup would leave "a" registered ahead of "b", stranding the
|
|
48
|
+
// tab stop on the detached element and leaving "b" at tabindex -1.
|
|
49
|
+
expect(screen.getByRole("button", { name: "b" })).toHaveAttribute(
|
|
50
|
+
"tabindex",
|
|
51
|
+
"0",
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("re-registers an item's disabled flag when it changes after mount", async () => {
|
|
56
|
+
// Arrange — all three enabled; focus can travel Left → Center → Right.
|
|
57
|
+
const user = userEvent.setup();
|
|
58
|
+
const { rerender } = render(<DisabledFixture />);
|
|
59
|
+
|
|
60
|
+
// Act — disable Center after mount, then navigate rightward from Left.
|
|
61
|
+
rerender(<DisabledFixture disabledItem="center" />);
|
|
62
|
+
screen.getByRole("button", { name: "Left" }).focus();
|
|
63
|
+
await user.keyboard("{ArrowRight}");
|
|
64
|
+
|
|
65
|
+
// Assert — the registrar effect re-ran, so Center is skipped and focus
|
|
66
|
+
// lands on Right. A stale registration would keep Center navigable and
|
|
67
|
+
// strand focus (a disabled button cannot receive it).
|
|
68
|
+
expect(screen.getByRole("button", { name: "Right" })).toHaveFocus();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { render } from "@testing-library/react";
|
|
2
|
+
|
|
3
|
+
import { ToggleGroup } from "../ToggleGroup";
|
|
4
|
+
import { ToggleGroupContext } from "../ToggleGroupContext";
|
|
5
|
+
|
|
6
|
+
describe("ToggleGroup error handling", () => {
|
|
7
|
+
it("throws a helpful error when ToggleGroup.Item is rendered outside Root", () => {
|
|
8
|
+
// Arrange
|
|
9
|
+
const error = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
10
|
+
|
|
11
|
+
// Assert
|
|
12
|
+
expect(() =>
|
|
13
|
+
render(<ToggleGroup.Item value="left">Left</ToggleGroup.Item>),
|
|
14
|
+
).toThrow(/ToggleGroup\.Item must be rendered inside a ToggleGroup\.Root/);
|
|
15
|
+
|
|
16
|
+
error.mockRestore();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("names the context for React DevTools", () => {
|
|
20
|
+
// Assert — an empty displayName would render the provider as anonymous.
|
|
21
|
+
expect(ToggleGroupContext.displayName).toBe("ToggleGroupContext");
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { render, screen } from "@testing-library/react";
|
|
1
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
2
2
|
import userEvent from "@testing-library/user-event";
|
|
3
3
|
|
|
4
4
|
import { ToggleGroup } from "../ToggleGroup";
|
|
@@ -45,6 +45,28 @@ describe("ToggleGroup keyboard — roving tabindex", () => {
|
|
|
45
45
|
"-1",
|
|
46
46
|
);
|
|
47
47
|
});
|
|
48
|
+
|
|
49
|
+
it("moves the tab stop to the focused item (onFocus updates the home base)", () => {
|
|
50
|
+
// Arrange — nothing selected, so the first item is the initial tab stop.
|
|
51
|
+
render(<SingleFixture />);
|
|
52
|
+
expect(screen.getByRole("button", { name: "Left" })).toHaveAttribute(
|
|
53
|
+
"tabindex",
|
|
54
|
+
"0",
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
// Act — focusing another item should hand it the tab stop.
|
|
58
|
+
fireEvent.focus(screen.getByRole("button", { name: "Right" }));
|
|
59
|
+
|
|
60
|
+
// Assert — Right is now the sole tab stop; Left drops out of the sequence.
|
|
61
|
+
expect(screen.getByRole("button", { name: "Right" })).toHaveAttribute(
|
|
62
|
+
"tabindex",
|
|
63
|
+
"0",
|
|
64
|
+
);
|
|
65
|
+
expect(screen.getByRole("button", { name: "Left" })).toHaveAttribute(
|
|
66
|
+
"tabindex",
|
|
67
|
+
"-1",
|
|
68
|
+
);
|
|
69
|
+
});
|
|
48
70
|
});
|
|
49
71
|
|
|
50
72
|
describe("ToggleGroup keyboard — arrow key navigation (no auto-select)", () => {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
|
|
3
|
+
import { ToggleGroup } from "../ToggleGroup";
|
|
4
|
+
|
|
5
|
+
describe("ToggleGroup.Item ref forwarding", () => {
|
|
6
|
+
it("forwards the underlying button element to a consumer function ref", () => {
|
|
7
|
+
// Arrange
|
|
8
|
+
const received: (HTMLButtonElement | null)[] = [];
|
|
9
|
+
const functionRef = (node: HTMLButtonElement | null) => {
|
|
10
|
+
received.push(node);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// Act
|
|
14
|
+
render(
|
|
15
|
+
<ToggleGroup.Root type="single" aria-label="Alignment">
|
|
16
|
+
<ToggleGroup.Item value="left" ref={functionRef}>
|
|
17
|
+
Left
|
|
18
|
+
</ToggleGroup.Item>
|
|
19
|
+
</ToggleGroup.Root>,
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
// Assert
|
|
23
|
+
expect(received).toContain(screen.getByRole("button", { name: "Left" }));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("re-attaches to a replacement ref when the ref prop changes", () => {
|
|
27
|
+
// Arrange — the item's ref memo must depend on `ref`, or a changed ref
|
|
28
|
+
// callback is never invoked with the element.
|
|
29
|
+
const firstReceived: (HTMLButtonElement | null)[] = [];
|
|
30
|
+
const secondReceived: (HTMLButtonElement | null)[] = [];
|
|
31
|
+
const firstRef = (node: HTMLButtonElement | null) => {
|
|
32
|
+
firstReceived.push(node);
|
|
33
|
+
};
|
|
34
|
+
const secondRef = (node: HTMLButtonElement | null) => {
|
|
35
|
+
secondReceived.push(node);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Act — mount with the first ref, then re-render with a different one.
|
|
39
|
+
const { rerender } = render(
|
|
40
|
+
<ToggleGroup.Root type="single" aria-label="Alignment">
|
|
41
|
+
<ToggleGroup.Item value="left" ref={firstRef}>
|
|
42
|
+
Left
|
|
43
|
+
</ToggleGroup.Item>
|
|
44
|
+
</ToggleGroup.Root>,
|
|
45
|
+
);
|
|
46
|
+
rerender(
|
|
47
|
+
<ToggleGroup.Root type="single" aria-label="Alignment">
|
|
48
|
+
<ToggleGroup.Item value="left" ref={secondRef}>
|
|
49
|
+
Left
|
|
50
|
+
</ToggleGroup.Item>
|
|
51
|
+
</ToggleGroup.Root>,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// Assert — the replacement ref received the button.
|
|
55
|
+
const button = screen.getByRole("button", { name: "Left" });
|
|
56
|
+
expect(secondReceived).toContain(button);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -42,7 +42,16 @@ export function useToggleGroupRoot({
|
|
|
42
42
|
(itemValue: string) => {
|
|
43
43
|
const isSelected = value.includes(itemValue);
|
|
44
44
|
if (type === "single") {
|
|
45
|
-
|
|
45
|
+
// Deselecting clears the selection to the empty array. Stryker's
|
|
46
|
+
// ArrayDeclaration mutant fills this with a phantom value, but since
|
|
47
|
+
// that value can never equal a registered item's `value` — and single
|
|
48
|
+
// mode always overwrites the whole array on the next selection — no
|
|
49
|
+
// item ever reads it as pressed. The mutation is observationally
|
|
50
|
+
// identical, so it is isolated here to keep the disable off the tested
|
|
51
|
+
// `[itemValue]` literal below.
|
|
52
|
+
// Stryker disable next-line ArrayDeclaration
|
|
53
|
+
const cleared: string[] = [];
|
|
54
|
+
const next = isSelected ? cleared : [itemValue];
|
|
46
55
|
setValue(next);
|
|
47
56
|
(onValueChange as ((v: string | undefined) => void) | undefined)?.(
|
|
48
57
|
isSelected ? undefined : itemValue,
|
|
@@ -68,6 +77,9 @@ export function useToggleGroupRoot({
|
|
|
68
77
|
(itemValue: string, element: HTMLButtonElement | null, disabled = false) => {
|
|
69
78
|
registerBase(itemValue, element ? { element, disabled } : null);
|
|
70
79
|
},
|
|
80
|
+
// `registerBase` is a stable useCollection callback (its own deps never
|
|
81
|
+
// change), so emptying this array yields the identical memoised function.
|
|
82
|
+
// Stryker disable next-line ArrayDeclaration: equivalent — stable dependency.
|
|
71
83
|
[registerBase],
|
|
72
84
|
);
|
|
73
85
|
|
|
@@ -83,8 +95,16 @@ export function useToggleGroupRoot({
|
|
|
83
95
|
|
|
84
96
|
const focusItem = useCallback(
|
|
85
97
|
(itemValue: string) => {
|
|
98
|
+
// `itemValue` is always sourced from `navigable` (the enabled subset of
|
|
99
|
+
// `itemValues`), and useCollection's register() writes `itemsRef` and the
|
|
100
|
+
// `itemValues` state together — so a value that reaches here is guaranteed
|
|
101
|
+
// to have a live entry in the map.
|
|
102
|
+
// Stryker disable next-line OptionalChaining: unreachable given that invariant.
|
|
86
103
|
itemsRef.current.get(itemValue)?.element.focus();
|
|
87
104
|
},
|
|
105
|
+
// `itemsRef` is a stable RefObject, so emptying this array yields the
|
|
106
|
+
// identical memoised function.
|
|
107
|
+
// Stryker disable next-line ArrayDeclaration: equivalent — stable dependency.
|
|
88
108
|
[itemsRef],
|
|
89
109
|
);
|
|
90
110
|
|
package/src/ToggleGroup/types.ts
CHANGED
|
@@ -7,12 +7,27 @@ import { HTMLAttributes, ButtonHTMLAttributes, ReactNode, Ref } from "react";
|
|
|
7
7
|
*/
|
|
8
8
|
export type ToggleGroupRootBaseProps = Omit<
|
|
9
9
|
HTMLAttributes<HTMLDivElement>,
|
|
10
|
-
"dir"
|
|
10
|
+
"dir" | "defaultValue"
|
|
11
11
|
> & {
|
|
12
|
+
/** Layout axis for keyboard navigation. `"horizontal"` binds
|
|
13
|
+
* ArrowLeft/ArrowRight; `"vertical"` binds ArrowUp/ArrowDown. Surfaces as
|
|
14
|
+
* `data-orientation` on the root for styling; it does not itself apply any
|
|
15
|
+
* flex/grid layout.
|
|
16
|
+
* @default "horizontal" */
|
|
12
17
|
orientation?: "horizontal" | "vertical";
|
|
18
|
+
/** Reading direction. In `"rtl"` the horizontal arrow keys are mirrored so
|
|
19
|
+
* focus follows the visual order. Inherited from the nearest
|
|
20
|
+
* {@link DirectionProvider} when omitted, falling back to `"ltr"`. */
|
|
13
21
|
dir?: "ltr" | "rtl";
|
|
22
|
+
/** Render a single consumer-supplied element in place of the native
|
|
23
|
+
* `<div>`, with the group's `role="group"`, `data-orientation`, and `ref`
|
|
24
|
+
* merged onto it via the {@link Slot} pattern.
|
|
25
|
+
* @default false */
|
|
14
26
|
asChild?: boolean;
|
|
27
|
+
/** The {@link ToggleGroupItemProps | `ToggleGroup.Item`} elements that make
|
|
28
|
+
* up the segmented control. */
|
|
15
29
|
children?: ReactNode;
|
|
30
|
+
/** Forwarded to the underlying `HTMLDivElement`. */
|
|
16
31
|
ref?: Ref<HTMLDivElement>;
|
|
17
32
|
};
|
|
18
33
|
|
|
@@ -21,9 +36,15 @@ export type ToggleGroupRootBaseProps = Omit<
|
|
|
21
36
|
* initial value supplied via `defaultValue`.
|
|
22
37
|
*/
|
|
23
38
|
export type SingleUncontrolledProps = {
|
|
39
|
+
/** Selects single-selection semantics: at most one item pressed at a time,
|
|
40
|
+
* and pressing the active item again clears the selection. */
|
|
24
41
|
type: "single";
|
|
42
|
+
/** Value of the item pressed on first render. Omit to start with nothing
|
|
43
|
+
* pressed. */
|
|
25
44
|
defaultValue?: string;
|
|
45
|
+
/** Forbidden in uncontrolled mode — use `defaultValue`. */
|
|
26
46
|
value?: never;
|
|
47
|
+
/** Forbidden in uncontrolled mode. */
|
|
27
48
|
onValueChange?: never;
|
|
28
49
|
};
|
|
29
50
|
|
|
@@ -32,9 +53,16 @@ export type SingleUncontrolledProps = {
|
|
|
32
53
|
* caller via `value` / `onValueChange`.
|
|
33
54
|
*/
|
|
34
55
|
export type SingleControlledProps = {
|
|
56
|
+
/** Selects single-selection semantics: at most one item pressed at a time,
|
|
57
|
+
* and pressing the active item again clears the selection. */
|
|
35
58
|
type: "single";
|
|
59
|
+
/** The currently pressed item's value, or `undefined` when none is pressed.
|
|
60
|
+
* Must be kept in sync by the caller via `onValueChange`. */
|
|
36
61
|
value: string | undefined;
|
|
62
|
+
/** Called with the requested next value — the pressed item's value, or
|
|
63
|
+
* `undefined` when the active item is pressed again to deselect it. */
|
|
37
64
|
onValueChange: (value: string | undefined) => void;
|
|
65
|
+
/** Forbidden in controlled mode — use `value`. */
|
|
38
66
|
defaultValue?: never;
|
|
39
67
|
};
|
|
40
68
|
|
|
@@ -43,9 +71,15 @@ export type SingleControlledProps = {
|
|
|
43
71
|
* the initial set supplied via `defaultValue`.
|
|
44
72
|
*/
|
|
45
73
|
export type MultipleUncontrolledProps = {
|
|
74
|
+
/** Selects multiple-selection semantics: any number of items can be pressed
|
|
75
|
+
* simultaneously and each toggles independently. */
|
|
46
76
|
type: "multiple";
|
|
77
|
+
/** Values of the items pressed on first render. Omit to start with nothing
|
|
78
|
+
* pressed. */
|
|
47
79
|
defaultValue?: string[];
|
|
80
|
+
/** Forbidden in uncontrolled mode — use `defaultValue`. */
|
|
48
81
|
value?: never;
|
|
82
|
+
/** Forbidden in uncontrolled mode. */
|
|
49
83
|
onValueChange?: never;
|
|
50
84
|
};
|
|
51
85
|
|
|
@@ -54,9 +88,16 @@ export type MultipleUncontrolledProps = {
|
|
|
54
88
|
* by the caller via `value` / `onValueChange`.
|
|
55
89
|
*/
|
|
56
90
|
export type MultipleControlledProps = {
|
|
91
|
+
/** Selects multiple-selection semantics: any number of items can be pressed
|
|
92
|
+
* simultaneously and each toggles independently. */
|
|
57
93
|
type: "multiple";
|
|
94
|
+
/** The full set of currently pressed item values. Must be kept in sync by
|
|
95
|
+
* the caller via `onValueChange`. */
|
|
58
96
|
value: string[];
|
|
97
|
+
/** Called with the complete next array of pressed values whenever the user
|
|
98
|
+
* toggles any item. */
|
|
59
99
|
onValueChange: (value: string[]) => void;
|
|
100
|
+
/** Forbidden in controlled mode — use `value`. */
|
|
60
101
|
defaultValue?: never;
|
|
61
102
|
};
|
|
62
103
|
|
|
@@ -81,11 +122,23 @@ export type ToggleGroupRootProps = ToggleGroupRootBaseProps &
|
|
|
81
122
|
*/
|
|
82
123
|
export type ToggleGroupItemProps = Omit<
|
|
83
124
|
ButtonHTMLAttributes<HTMLButtonElement>,
|
|
84
|
-
"type"
|
|
125
|
+
"type" | "value"
|
|
85
126
|
> & {
|
|
127
|
+
/** Identifies this item within the group. It is this string — not the
|
|
128
|
+
* visible label — that is compared against the group's pressed
|
|
129
|
+
* `value` / `defaultValue`; use `children` for the label. */
|
|
86
130
|
value: string;
|
|
131
|
+
/** Forwards the native `disabled` attribute and removes the item from the
|
|
132
|
+
* roving tab order so arrow-key navigation skips it. Also sets
|
|
133
|
+
* `data-disabled=""` for CSS targeting.
|
|
134
|
+
* @default false */
|
|
87
135
|
disabled?: boolean;
|
|
136
|
+
/** Render a single consumer-supplied element in place of the native
|
|
137
|
+
* `<button>`, with the item's `aria-pressed`, `data-state`, `tabIndex`, and
|
|
138
|
+
* event handlers merged onto it via the {@link Slot} pattern.
|
|
139
|
+
* @default false */
|
|
88
140
|
asChild?: boolean;
|
|
141
|
+
/** Forwarded to the underlying `HTMLButtonElement`. */
|
|
89
142
|
ref?: Ref<HTMLButtonElement>;
|
|
90
143
|
};
|
|
91
144
|
|