@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/Slider/Slider.tsx
CHANGED
|
@@ -55,6 +55,8 @@ import { getRangeStyle } from "./utils";
|
|
|
55
55
|
*
|
|
56
56
|
* @throws if `min` is not less than `max`, or `step` is not greater than 0.
|
|
57
57
|
*
|
|
58
|
+
* @extends HTMLSpanElement
|
|
59
|
+
*
|
|
58
60
|
* @example Single-thumb, uncontrolled
|
|
59
61
|
* ```tsx
|
|
60
62
|
* <Slider.Root defaultValue={[50]}>
|
|
@@ -153,6 +155,15 @@ SliderRoot.displayName = "SliderRoot";
|
|
|
153
155
|
* **`asChild` prop.** Render a consumer element in place of the `<span>`.
|
|
154
156
|
*
|
|
155
157
|
* @throws if rendered outside a `Slider.Root`.
|
|
158
|
+
*
|
|
159
|
+
* @extends HTMLSpanElement
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```tsx
|
|
163
|
+
* <Slider.Track>
|
|
164
|
+
* <Slider.Range />
|
|
165
|
+
* </Slider.Track>
|
|
166
|
+
* ```
|
|
156
167
|
*/
|
|
157
168
|
export function SliderTrack({
|
|
158
169
|
children,
|
|
@@ -184,6 +195,15 @@ SliderTrack.displayName = "SliderTrack";
|
|
|
184
195
|
* **`asChild` prop.** Render a consumer element in place of the `<span>`.
|
|
185
196
|
*
|
|
186
197
|
* @throws if rendered outside a `Slider.Root`.
|
|
198
|
+
*
|
|
199
|
+
* @extends HTMLSpanElement
|
|
200
|
+
*
|
|
201
|
+
* @example
|
|
202
|
+
* ```tsx
|
|
203
|
+
* <Slider.Track>
|
|
204
|
+
* <Slider.Range />
|
|
205
|
+
* </Slider.Track>
|
|
206
|
+
* ```
|
|
187
207
|
*/
|
|
188
208
|
export function SliderRange({
|
|
189
209
|
style,
|
|
@@ -226,12 +246,24 @@ SliderRange.displayName = "SliderRange";
|
|
|
226
246
|
* **Styling hooks.** `data-orientation` and `data-disabled`. Position is set
|
|
227
247
|
* via an inline `style` offset; consumer `style` is shallow-merged on top.
|
|
228
248
|
*
|
|
229
|
-
* Provide an accessible name with `aria-label` /
|
|
249
|
+
* **Accessibility.** Provide an accessible name with `aria-label` /
|
|
250
|
+
* `aria-labelledby`. `aria-valuenow` always tracks the raw number; when that
|
|
251
|
+
* number is not self-explanatory to a screen reader (a currency amount, a
|
|
252
|
+
* date, a named step), pass `aria-valuetext` with the human-readable form
|
|
253
|
+
* (e.g. `aria-valuetext="$50"`) — it is forwarded straight through to the
|
|
254
|
+
* element and read in place of the bare number.
|
|
230
255
|
*
|
|
231
256
|
* **`asChild` prop.** Render a consumer element in place of the `<span>`;
|
|
232
257
|
* the slider role, ARIA value attributes, keyboard handler, and ref merge in.
|
|
233
258
|
*
|
|
234
259
|
* @throws if rendered outside a `Slider.Root`.
|
|
260
|
+
*
|
|
261
|
+
* @extends HTMLSpanElement
|
|
262
|
+
*
|
|
263
|
+
* @example Named value with `aria-valuetext`
|
|
264
|
+
* ```tsx
|
|
265
|
+
* <Slider.Thumb aria-label="Price" aria-valuetext={`$${value}`} />
|
|
266
|
+
* ```
|
|
235
267
|
*/
|
|
236
268
|
export function SliderThumb({
|
|
237
269
|
style,
|
|
@@ -3,19 +3,46 @@ import { createStrictContext } from "../utils/index.ts";
|
|
|
3
3
|
|
|
4
4
|
import type { SliderDirection, SliderOrientation } from "./types";
|
|
5
5
|
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* Shared state published by {@link SliderRoot | `Slider.Root`} to its
|
|
8
|
+
* sub-components: the current values, range/step bounds, orientation and
|
|
9
|
+
* direction, thumb registration, and the value-change / value-commit
|
|
10
|
+
* callbacks. Read it with {@link useSliderContext}; it throws when a
|
|
11
|
+
* sub-component is rendered outside a `<Slider.Root>`.
|
|
12
|
+
*/
|
|
7
13
|
export type SliderContextValue = {
|
|
14
|
+
/** The current value array — one entry per registered thumb, in order. */
|
|
8
15
|
values: number[];
|
|
16
|
+
/** Lowest value (track start edge). */
|
|
9
17
|
min: number;
|
|
18
|
+
/** Highest value (track end edge). */
|
|
10
19
|
max: number;
|
|
20
|
+
/** Granularity; every value snaps to a multiple of this anchored at `min`. */
|
|
11
21
|
step: number;
|
|
22
|
+
/** Resolved layout axis; see {@link SliderOrientation}. */
|
|
12
23
|
orientation: SliderOrientation;
|
|
24
|
+
/** Resolved reading direction (after inheriting from `DirectionProvider`); see {@link SliderDirection}. */
|
|
13
25
|
dir: SliderDirection;
|
|
26
|
+
/** Whether the axis is reversed relative to its natural direction. */
|
|
14
27
|
inverted: boolean;
|
|
28
|
+
/** Whether interaction is disabled across every part. */
|
|
15
29
|
disabled: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Registers (or, with `null`, unregisters) a thumb by its stable id so Root
|
|
32
|
+
* can order the thumbs and focus them on pointer interaction. Called from
|
|
33
|
+
* each {@link SliderThumb | `Slider.Thumb`} on mount/unmount.
|
|
34
|
+
*/
|
|
16
35
|
registerThumb: (id: string, element: HTMLSpanElement | null) => void;
|
|
36
|
+
/** Registered thumb ids in DOM order; a thumb's index into `values` is its position here. */
|
|
17
37
|
orderedThumbIds: string[];
|
|
38
|
+
/**
|
|
39
|
+
* Sets the thumb at `index` to `value`, clamped between its neighbours (plus
|
|
40
|
+
* `minStepsBetweenThumbs`) and the `[min, max]` bounds. Returns the next
|
|
41
|
+
* value array when it changed, or `null` when the clamped value was
|
|
42
|
+
* unchanged (so callers can skip a redundant commit).
|
|
43
|
+
*/
|
|
18
44
|
setThumbValue: (index: number, value: number) => number[] | null;
|
|
45
|
+
/** Fires `onValueCommit` with the settled array at the end of an interaction. */
|
|
19
46
|
commit: (values: number[]) => void;
|
|
20
47
|
};
|
|
21
48
|
|
package/src/Slider/types.ts
CHANGED
|
@@ -1,57 +1,205 @@
|
|
|
1
1
|
import type { ComponentProps } from "react";
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Layout axis of the slider track. `"horizontal"` lays the track out along
|
|
5
|
+
* the x-axis and binds ArrowLeft/ArrowRight (plus ArrowUp/ArrowDown);
|
|
6
|
+
* `"vertical"` lays it out along the y-axis, positions thumbs from the
|
|
7
|
+
* bottom edge, and maps ArrowUp to *increase*.
|
|
8
|
+
*/
|
|
4
9
|
export type SliderOrientation = "horizontal" | "vertical";
|
|
5
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Reading direction of a **horizontal** slider, affecting which end of the
|
|
12
|
+
* track is the minimum and the arrow-key direction. `"rtl"` mirrors the axis
|
|
13
|
+
* so the value increases leftwards. Ignored on the vertical axis.
|
|
14
|
+
*/
|
|
6
15
|
export type SliderDirection = "ltr" | "rtl";
|
|
7
16
|
|
|
8
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* Props common to both the controlled ({@link SliderRootControlledProps}) and
|
|
19
|
+
* uncontrolled ({@link SliderRootUncontrolledProps}) `Slider.Root` shapes:
|
|
20
|
+
* the range/step bounds, orientation, reading direction, form `name`, and the
|
|
21
|
+
* native `<span>` attributes.
|
|
22
|
+
*
|
|
23
|
+
* `defaultValue` and `dir` are `Omit`-ted from the inherited `<span>` props
|
|
24
|
+
* because both are redeclared with narrower slider-specific types — the
|
|
25
|
+
* native `defaultValue` is a string/number DOM attribute (replaced by the
|
|
26
|
+
* `number[]` value array) and the native `dir` is `string` (narrowed to
|
|
27
|
+
* {@link SliderDirection}). Omitting them keeps the narrowed types (and this
|
|
28
|
+
* JSDoc) visible to the docs-data extractor rather than the wider DOM ones.
|
|
29
|
+
*/
|
|
9
30
|
export type SliderRootSharedProps = Omit<
|
|
10
31
|
ComponentProps<"span">,
|
|
11
32
|
"defaultValue" | "dir"
|
|
12
33
|
> & {
|
|
34
|
+
/**
|
|
35
|
+
* Lowest value the slider can take — the value at the track's start edge.
|
|
36
|
+
* Must be strictly less than {@link SliderRootSharedProps.max | `max`};
|
|
37
|
+
* `Slider.Root` throws during render otherwise.
|
|
38
|
+
* @default 0
|
|
39
|
+
*/
|
|
13
40
|
min?: number;
|
|
41
|
+
/**
|
|
42
|
+
* Highest value the slider can take — the value at the track's end edge.
|
|
43
|
+
* Must be strictly greater than {@link SliderRootSharedProps.min | `min`};
|
|
44
|
+
* `Slider.Root` throws during render otherwise.
|
|
45
|
+
* @default 100
|
|
46
|
+
*/
|
|
14
47
|
max?: number;
|
|
48
|
+
/**
|
|
49
|
+
* Granularity of the value: every thumb value snaps to a multiple of `step`
|
|
50
|
+
* anchored at `min`, and each arrow-key press moves by one `step` (Page
|
|
51
|
+
* Up/Down move by ten). Fractional steps (e.g. `0.1`) are snapped to the
|
|
52
|
+
* step's decimal precision. Must be greater than `0`; `Slider.Root` throws
|
|
53
|
+
* during render otherwise.
|
|
54
|
+
* @default 1
|
|
55
|
+
*/
|
|
15
56
|
step?: number;
|
|
57
|
+
/**
|
|
58
|
+
* Minimum gap, expressed **in steps**, enforced between adjacent thumbs on a
|
|
59
|
+
* multi-thumb slider. The gap in value units is `minStepsBetweenThumbs *
|
|
60
|
+
* step`. A thumb is clamped so it can never come closer than this to its
|
|
61
|
+
* neighbours (and can never cross them). Has no effect on a single-thumb
|
|
62
|
+
* slider.
|
|
63
|
+
* @default 0
|
|
64
|
+
*/
|
|
16
65
|
minStepsBetweenThumbs?: number;
|
|
66
|
+
/**
|
|
67
|
+
* Layout axis of the track; see {@link SliderOrientation}. Sets
|
|
68
|
+
* `data-orientation` and each thumb's `aria-orientation`, and selects which
|
|
69
|
+
* pointer axis and arrow keys drive the value.
|
|
70
|
+
* @default "horizontal"
|
|
71
|
+
*/
|
|
17
72
|
orientation?: SliderOrientation;
|
|
73
|
+
/**
|
|
74
|
+
* Reading direction of a horizontal slider; see {@link SliderDirection}.
|
|
75
|
+
* Affects which end is the minimum, the arrow-key direction, pointer
|
|
76
|
+
* mapping, and the inline positioning styles. When omitted, it is inherited
|
|
77
|
+
* from the nearest {@link DirectionProvider}, falling back to `"ltr"` when
|
|
78
|
+
* there is no provider; an explicit prop always wins.
|
|
79
|
+
*/
|
|
18
80
|
dir?: SliderDirection;
|
|
81
|
+
/**
|
|
82
|
+
* Reverse the direction the value increases along the axis. Combines with
|
|
83
|
+
* {@link SliderRootSharedProps.orientation | `orientation`} and
|
|
84
|
+
* {@link SliderRootSharedProps.dir | `dir`} to flip the pointer mapping,
|
|
85
|
+
* arrow keys, and the edge the thumb/range offset is anchored to.
|
|
86
|
+
* @default false
|
|
87
|
+
*/
|
|
19
88
|
inverted?: boolean;
|
|
89
|
+
/**
|
|
90
|
+
* Disable all interaction. Every part receives `data-disabled=""`, thumbs
|
|
91
|
+
* leave the tab order and gain `aria-disabled="true"`, and both keyboard and
|
|
92
|
+
* pointer handlers become no-ops.
|
|
93
|
+
* @default false
|
|
94
|
+
*/
|
|
20
95
|
disabled?: boolean;
|
|
96
|
+
/**
|
|
97
|
+
* When set, `Slider.Root` renders one hidden `<input>` per thumb carrying
|
|
98
|
+
* that thumb's value so the slider posts with a surrounding `<form>`. A
|
|
99
|
+
* multi-thumb slider suffixes the name with `[]` (e.g. `name="range"` →
|
|
100
|
+
* `range[]`); a single-thumb slider uses the bare name. When omitted, no
|
|
101
|
+
* hidden inputs are rendered.
|
|
102
|
+
*/
|
|
21
103
|
name?: string;
|
|
104
|
+
/**
|
|
105
|
+
* Render the child element in place of the default `<span>` via the
|
|
106
|
+
* {@link Slot} pattern, merging `Slider.Root`'s `dir`, `data-*` hooks,
|
|
107
|
+
* pointer handler, and ref onto it.
|
|
108
|
+
* @default false
|
|
109
|
+
*/
|
|
22
110
|
asChild?: boolean;
|
|
23
111
|
};
|
|
24
112
|
|
|
25
|
-
/**
|
|
113
|
+
/**
|
|
114
|
+
* Uncontrolled variant of {@link SliderRootProps}: the component owns the
|
|
115
|
+
* value array internally. Seed it with `defaultValue` (or omit it for a
|
|
116
|
+
* single thumb seeded at `min`); `value` is forbidden.
|
|
117
|
+
*/
|
|
26
118
|
export type SliderRootUncontrolledProps = SliderRootSharedProps & {
|
|
119
|
+
/**
|
|
120
|
+
* The value array on first render — one number per
|
|
121
|
+
* {@link SliderThumb | `Slider.Thumb`}, in order (`[value]` for a single
|
|
122
|
+
* thumb, `[low, high]` for a range). After mount the component owns the
|
|
123
|
+
* value. Defaults to `[min]` when omitted.
|
|
124
|
+
*/
|
|
27
125
|
defaultValue?: number[];
|
|
126
|
+
/** Forbidden in uncontrolled mode — use `defaultValue` instead. */
|
|
28
127
|
value?: never;
|
|
128
|
+
/**
|
|
129
|
+
* Called with the full next value array on every change (each arrow-key
|
|
130
|
+
* press and every pointer increment during a drag). Optional in
|
|
131
|
+
* uncontrolled mode.
|
|
132
|
+
*/
|
|
29
133
|
onValueChange?: (value: number[]) => void;
|
|
134
|
+
/**
|
|
135
|
+
* Called **once** with the settled value array when an interaction ends — a
|
|
136
|
+
* pointer release, or a completed keyboard press. Use it to persist only the
|
|
137
|
+
* final value rather than every intermediate step.
|
|
138
|
+
*/
|
|
30
139
|
onValueCommit?: (value: number[]) => void;
|
|
31
140
|
};
|
|
32
141
|
|
|
33
|
-
/**
|
|
142
|
+
/**
|
|
143
|
+
* Controlled variant of {@link SliderRootProps}: the parent owns the value
|
|
144
|
+
* array via `value` and is expected to keep it in sync through
|
|
145
|
+
* `onValueChange`. `defaultValue` is forbidden.
|
|
146
|
+
*/
|
|
34
147
|
export type SliderRootControlledProps = SliderRootSharedProps & {
|
|
148
|
+
/** Forbidden in controlled mode — use `value` instead. */
|
|
35
149
|
defaultValue?: never;
|
|
150
|
+
/**
|
|
151
|
+
* The current value array — one number per
|
|
152
|
+
* {@link SliderThumb | `Slider.Thumb`}, in order (`[value]` for a single
|
|
153
|
+
* thumb, `[low, high]` for a range). Must be kept in sync by the parent via
|
|
154
|
+
* `onValueChange`.
|
|
155
|
+
*/
|
|
36
156
|
value: number[];
|
|
157
|
+
/**
|
|
158
|
+
* Called with the full next value array on every change (each arrow-key
|
|
159
|
+
* press and every pointer increment during a drag). Update `value` from it
|
|
160
|
+
* to keep the slider in sync.
|
|
161
|
+
*/
|
|
37
162
|
onValueChange?: (value: number[]) => void;
|
|
163
|
+
/**
|
|
164
|
+
* Called **once** with the settled value array when an interaction ends — a
|
|
165
|
+
* pointer release, or a completed keyboard press. Use it to persist only the
|
|
166
|
+
* final value rather than every intermediate step.
|
|
167
|
+
*/
|
|
38
168
|
onValueCommit?: (value: number[]) => void;
|
|
39
169
|
};
|
|
40
170
|
|
|
41
|
-
/** Props for `Slider.Root` — the discriminated union of controlled ({@link SliderRootControlledProps}) and uncontrolled ({@link SliderRootUncontrolledProps}) modes. */
|
|
171
|
+
/** Props for {@link SliderRoot | `Slider.Root`} — the discriminated union of controlled ({@link SliderRootControlledProps}) and uncontrolled ({@link SliderRootUncontrolledProps}) modes; only one shape is accepted by TypeScript at a time. */
|
|
42
172
|
export type SliderRootProps =
|
|
43
173
|
| SliderRootUncontrolledProps
|
|
44
174
|
| SliderRootControlledProps;
|
|
45
175
|
|
|
46
|
-
/** Props for `Slider.Track` — the full-length rail; native `<span>`
|
|
176
|
+
/** Props for {@link SliderTrack | `Slider.Track`} — the full-length rail the thumbs travel along; all native `<span>` attributes plus `asChild`. */
|
|
47
177
|
export type SliderTrackProps = ComponentProps<"span"> & {
|
|
178
|
+
/**
|
|
179
|
+
* Render the child element in place of the default `<span>` via the
|
|
180
|
+
* {@link Slot} pattern, merging the `data-orientation` / `data-disabled`
|
|
181
|
+
* hooks onto it.
|
|
182
|
+
* @default false
|
|
183
|
+
*/
|
|
48
184
|
asChild?: boolean;
|
|
49
185
|
};
|
|
50
|
-
/** Props for `Slider.Range` — the filled segment between the
|
|
186
|
+
/** Props for {@link SliderRange | `Slider.Range`} — the filled segment between the lowest and highest thumb (or from the track start to the single thumb); all native `<span>` attributes plus `asChild`. */
|
|
51
187
|
export type SliderRangeProps = ComponentProps<"span"> & {
|
|
188
|
+
/**
|
|
189
|
+
* Render the child element in place of the default `<span>` via the
|
|
190
|
+
* {@link Slot} pattern, merging the `data-orientation` / `data-disabled`
|
|
191
|
+
* hooks and the computed inline positioning `style` onto it.
|
|
192
|
+
* @default false
|
|
193
|
+
*/
|
|
52
194
|
asChild?: boolean;
|
|
53
195
|
};
|
|
54
|
-
/** Props for `Slider.Thumb` — a draggable handle; native `<span>`
|
|
196
|
+
/** Props for {@link SliderThumb | `Slider.Thumb`} — a draggable, focusable handle; render one per entry in the value array. All native `<span>` attributes plus `asChild`. */
|
|
55
197
|
export type SliderThumbProps = ComponentProps<"span"> & {
|
|
198
|
+
/**
|
|
199
|
+
* Render the child element in place of the default `<span>` via the
|
|
200
|
+
* {@link Slot} pattern, merging the `role="slider"` semantics, `aria-value*`
|
|
201
|
+
* attributes, keyboard handler, positioning `style`, and ref onto it.
|
|
202
|
+
* @default false
|
|
203
|
+
*/
|
|
56
204
|
asChild?: boolean;
|
|
57
205
|
};
|
package/src/Slot/Slot.tsx
CHANGED
|
@@ -34,7 +34,15 @@ import { AnyProps, PossibleRef } from "../types";
|
|
|
34
34
|
function setRef<T>(ref: PossibleRef<T>, value: T) {
|
|
35
35
|
if (typeof ref === "function") {
|
|
36
36
|
ref(value);
|
|
37
|
-
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
// Forcing this guard true dereferences a null/undefined ref (a child with no
|
|
40
|
+
// ref of its own), throwing during React's commit-phase ref assignment. The
|
|
41
|
+
// "no ref of its own" test catches it, but Stryker's vitest runner can't
|
|
42
|
+
// attribute that commit-phase throw (the Avatar/Slot-guard limitation) — not
|
|
43
|
+
// equivalent.
|
|
44
|
+
// Stryker disable next-line ConditionalExpression
|
|
45
|
+
if (ref !== null && ref !== undefined) {
|
|
38
46
|
(ref as RefObject<T>).current = value;
|
|
39
47
|
}
|
|
40
48
|
}
|
|
@@ -42,7 +50,15 @@ function setRef<T>(ref: PossibleRef<T>, value: T) {
|
|
|
42
50
|
/**
|
|
43
51
|
* Combines multiple refs into a single callback ref that sets all of them
|
|
44
52
|
* simultaneously. Handles function refs, object refs (`{ current }`), and
|
|
45
|
-
* `undefined` — any of which may be mixed freely
|
|
53
|
+
* `undefined` — any of which may be mixed freely, in any order, and any
|
|
54
|
+
* number of them.
|
|
55
|
+
*
|
|
56
|
+
* Used throughout the headless components whenever a sub-component owns an
|
|
57
|
+
* internal ref (for measuring, focus management, roving-tabindex, etc.)
|
|
58
|
+
* *and* needs to forward the consumer's own `ref` to the same DOM node —
|
|
59
|
+
* two refs, one element, one callback ref that satisfies both. `Slot`
|
|
60
|
+
* itself uses `composeRefs` internally to merge its `forwardedRef` with the
|
|
61
|
+
* child element's own ref (see {@link Slot}).
|
|
46
62
|
*
|
|
47
63
|
* @example Compose an internal ref with a consumer-supplied external ref:
|
|
48
64
|
* ```tsx
|
|
@@ -67,6 +83,10 @@ function getRef(element: React.ReactElement): PossibleRef<unknown> {
|
|
|
67
83
|
const { props, ref } = element as React.ReactElement & {
|
|
68
84
|
ref?: PossibleRef<unknown>;
|
|
69
85
|
};
|
|
86
|
+
// Stryker disable next-line ConditionalExpression: in React 19, element.ref
|
|
87
|
+
// returns the same ref as props.ref for real elements, so preferring props.ref
|
|
88
|
+
// vs. falling back to ref is indistinguishable at runtime — an equivalent
|
|
89
|
+
// mutant. The ternary stays for the React <=18 shape, where the two differ.
|
|
70
90
|
return (props as AnyProps).ref !== undefined
|
|
71
91
|
? ((props as AnyProps).ref as PossibleRef<unknown>)
|
|
72
92
|
: ref;
|
|
@@ -109,29 +129,67 @@ function mergeProps(slotProps: AnyProps, childProps: AnyProps): AnyProps {
|
|
|
109
129
|
/**
|
|
110
130
|
* Renders its single child element with the Slot's own props merged in.
|
|
111
131
|
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
132
|
+
* `Slot` is the composition primitive behind every `asChild` prop in this
|
|
133
|
+
* library (`Button`, `Tabs.Trigger`, `Select.Root`, `Breadcrumb.Link`, and
|
|
134
|
+
* more). A component that normally renders its own DOM element (say,
|
|
135
|
+
* `<button>`) can instead delegate rendering to a single consumer-supplied
|
|
136
|
+
* element — typically a routing library's `<Link>` or a custom styled
|
|
137
|
+
* element — while preserving all of its own behaviour: ARIA attributes,
|
|
138
|
+
* data attributes, event handlers, and `ref` all still land on the
|
|
139
|
+
* rendered DOM node, just not the node `Slot` itself would have created.
|
|
140
|
+
* The pattern avoids an extra wrapper element in the DOM, which matters for
|
|
141
|
+
* layout (no unwanted `<button><a>…</a></button>` nesting) and for CSS
|
|
142
|
+
* selectors that expect a single element.
|
|
116
143
|
*
|
|
117
|
-
* **
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
144
|
+
* **Usage shape** — every component using `Slot` follows the same
|
|
145
|
+
* branch:
|
|
146
|
+
*
|
|
147
|
+
* ```tsx
|
|
148
|
+
* if (asChild) {
|
|
149
|
+
* return <Slot {...ownProps}>{children}</Slot>;
|
|
150
|
+
* }
|
|
151
|
+
* return <button {...ownProps}>{children}</button>;
|
|
152
|
+
* ```
|
|
153
|
+
*
|
|
154
|
+
* `ownProps` is the exact same object passed to both branches — `Slot`'s
|
|
155
|
+
* job is only to change *where* those props land, never to change what
|
|
156
|
+
* they are.
|
|
124
157
|
*
|
|
125
|
-
* **
|
|
126
|
-
*
|
|
127
|
-
* -
|
|
128
|
-
*
|
|
158
|
+
* **Prop-merging rules** (`Slot`'s own props are the "slot" side, the
|
|
159
|
+
* single child's own props are the "child" side):
|
|
160
|
+
* - **Event handlers** (any prop matching `/^on[A-Z]/`) compose — the
|
|
161
|
+
* **child's** handler fires first, then the **slot's**. This is the
|
|
162
|
+
* opposite composition direction, and a different merge strategy, from
|
|
163
|
+
* the standalone {@link composeEventHandlers} helper (their-handler-first
|
|
164
|
+
* with a `preventDefault()` veto) — the two are unrelated mechanisms
|
|
165
|
+
* that happen to solve adjacent problems. `Slot`'s handlers always both
|
|
166
|
+
* run; there is no opt-out.
|
|
167
|
+
* - **`style`** is shallow-merged — the child's style object wins on key
|
|
168
|
+
* collisions, so a consumer's inline `style` can override individual
|
|
169
|
+
* properties Slot would otherwise set.
|
|
170
|
+
* - **`className`** strings are concatenated (`"slotClass childClass"`),
|
|
171
|
+
* so both the component's own class hooks and the consumer's class
|
|
172
|
+
* survive.
|
|
173
|
+
* - **All other props** default to the child's value; Slot's value is only
|
|
174
|
+
* used as a fallback when the child doesn't specify that prop at all
|
|
175
|
+
* (key absent, or explicitly `undefined`).
|
|
176
|
+
* - **Refs** from both sides are composed via {@link composeRefs}, so
|
|
177
|
+
* neither the component's internal ref nor the consumer's own ref on the
|
|
178
|
+
* child element is lost.
|
|
129
179
|
*
|
|
130
|
-
* **
|
|
180
|
+
* **Constraints.** Exactly one React element child is required — `Slot`
|
|
181
|
+
* throws `"Slot requires exactly one React element child."` for zero
|
|
182
|
+
* children or a non-element child (e.g. a plain string). The child must
|
|
183
|
+
* also accept a `ref` (a DOM element or a `forwardRef` component); passing
|
|
184
|
+
* a plain function component as the child silently drops the ref.
|
|
185
|
+
*
|
|
186
|
+
* **React version compatibility.** Slot reads the child's own ref from
|
|
131
187
|
* `element.props.ref` (React 19+) with a fallback to `element.ref`
|
|
132
188
|
* (React ≤18) so both runtime versions compose refs correctly.
|
|
133
189
|
*
|
|
134
|
-
* @
|
|
190
|
+
* @extends HTMLElement
|
|
191
|
+
*
|
|
192
|
+
* @example Basic `asChild` branch inside a component
|
|
135
193
|
* ```tsx
|
|
136
194
|
* // Inside a component that normally renders <button>:
|
|
137
195
|
* if (asChild) {
|
|
@@ -139,11 +197,28 @@ function mergeProps(slotProps: AnyProps, childProps: AnyProps): AnyProps {
|
|
|
139
197
|
* }
|
|
140
198
|
* return <button {...buttonProps}>{children}</button>;
|
|
141
199
|
* ```
|
|
200
|
+
*
|
|
201
|
+
* @example Consumer rendering a router link in place of the default element
|
|
202
|
+
* ```tsx
|
|
203
|
+
* <Button asChild onClick={trackClick}>
|
|
204
|
+
* <RouterLink to="/settings" className="settings-link">
|
|
205
|
+
* Settings
|
|
206
|
+
* </RouterLink>
|
|
207
|
+
* </Button>
|
|
208
|
+
* // Rendered DOM: a single <a class="…settings-link"> — trackClick and the
|
|
209
|
+
* // link's own onClick both fire; no wrapping <button>.
|
|
210
|
+
* ```
|
|
142
211
|
*/
|
|
143
212
|
export const Slot: ForwardRefExoticComponent<
|
|
144
213
|
PropsWithoutRef<SlotProps> & RefAttributes<HTMLElement>
|
|
145
214
|
> = forwardRef<HTMLElement, SlotProps>(
|
|
146
215
|
({ children, ...slotProps }, forwardedRef) => {
|
|
216
|
+
// Stryker disable next-line ConditionalExpression: forcing this guard false
|
|
217
|
+
// lets invalid children fall through to a *different* throw (the destructure
|
|
218
|
+
// in getRef below), which the error tests do catch — but Stryker's vitest
|
|
219
|
+
// runner does not attribute that React render-time throw to a covering test
|
|
220
|
+
// (the same limitation seen on Avatar's ref guard). The operator/block twins
|
|
221
|
+
// of this line are scored; only this variant can't be. Not equivalent.
|
|
147
222
|
if (Children.count(children) !== 1 || !isValidElement(children)) {
|
|
148
223
|
throw new Error("Slot requires exactly one React element child.");
|
|
149
224
|
}
|
|
@@ -159,5 +159,115 @@ describe("Slot tests", () => {
|
|
|
159
159
|
// Assert
|
|
160
160
|
expect(screen.getByText("text")).toHaveClass("slot-class", "child-class");
|
|
161
161
|
});
|
|
162
|
+
|
|
163
|
+
it("drops a falsy className from either side (no stray whitespace)", () => {
|
|
164
|
+
// Arrange — child provides an empty className; the filter must remove it.
|
|
165
|
+
const { container } = render(
|
|
166
|
+
<Slot className="slot-class">
|
|
167
|
+
<span className="">text</span>
|
|
168
|
+
</Slot>,
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
// Assert — exactly "slot-class", not "slot-class " with a trailing space
|
|
172
|
+
expect((container.firstChild as HTMLElement).getAttribute("class")).toBe(
|
|
173
|
+
"slot-class",
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe("ref composition (exercised through the component)", () => {
|
|
179
|
+
it("calls a function ref forwarded through the Slot with the element", () => {
|
|
180
|
+
// Arrange
|
|
181
|
+
const fnRef = vi.fn();
|
|
182
|
+
|
|
183
|
+
// Act — a function ref on the Slot is composed onto the child element
|
|
184
|
+
render(
|
|
185
|
+
<Slot ref={fnRef}>
|
|
186
|
+
<button type="button">Click me</button>
|
|
187
|
+
</Slot>,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
// Assert
|
|
191
|
+
expect(fnRef).toHaveBeenCalledWith(screen.getByRole("button"));
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("populates an object ref and tolerates a child with no ref of its own", () => {
|
|
195
|
+
// Arrange — the Slot's object ref composes with the child's (absent) ref;
|
|
196
|
+
// the absent side must be skipped, not dereferenced.
|
|
197
|
+
const objRef = createRef<HTMLElement>();
|
|
198
|
+
|
|
199
|
+
// Act
|
|
200
|
+
render(
|
|
201
|
+
<Slot ref={objRef}>
|
|
202
|
+
<button type="button">Click me</button>
|
|
203
|
+
</Slot>,
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
// Assert
|
|
207
|
+
expect(objRef.current).toBe(screen.getByRole("button"));
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe("mergeProps — event handlers (both sides)", () => {
|
|
212
|
+
it("composes handlers when both provide one — child first, then slot", async () => {
|
|
213
|
+
// Arrange
|
|
214
|
+
const user = userEvent.setup();
|
|
215
|
+
const calls: string[] = [];
|
|
216
|
+
const slotClick = vi.fn(() => calls.push("slot"));
|
|
217
|
+
const childClick = vi.fn(() => calls.push("child"));
|
|
218
|
+
render(
|
|
219
|
+
<Slot onClick={slotClick}>
|
|
220
|
+
<button type="button" onClick={childClick}>
|
|
221
|
+
Click me
|
|
222
|
+
</button>
|
|
223
|
+
</Slot>,
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
// Act
|
|
227
|
+
await user.click(screen.getByRole("button"));
|
|
228
|
+
|
|
229
|
+
// Assert — both fire, child's handler first
|
|
230
|
+
expect(childClick).toHaveBeenCalledTimes(1);
|
|
231
|
+
expect(slotClick).toHaveBeenCalledTimes(1);
|
|
232
|
+
expect(calls).toEqual(["child", "slot"]);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe("mergeProps — other props", () => {
|
|
237
|
+
it("keeps non-special props, child winning over the slot on collision", () => {
|
|
238
|
+
// Arrange
|
|
239
|
+
const { container } = render(
|
|
240
|
+
<Slot id="slot-id" data-origin="slot">
|
|
241
|
+
<span id="child-id">text</span>
|
|
242
|
+
</Slot>,
|
|
243
|
+
);
|
|
244
|
+
const el = container.firstChild as HTMLElement;
|
|
245
|
+
|
|
246
|
+
// Assert — child's id wins; the slot-only prop still passes through
|
|
247
|
+
expect(el.id).toBe("child-id");
|
|
248
|
+
expect(el.getAttribute("data-origin")).toBe("slot");
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("does not treat a prop merely containing on[A-Z] as an event handler", () => {
|
|
252
|
+
// Arrange — "iconOnly" contains "onO" but is not a handler; only a
|
|
253
|
+
// start-anchored /^on[A-Z]/ correctly excludes it, so the child value must
|
|
254
|
+
// win by the default merge rather than being composed into a function.
|
|
255
|
+
// "iconOnly" is not a real DOM attribute, hence the spread casts.
|
|
256
|
+
const { container } = render(
|
|
257
|
+
<Slot {...({ iconOnly: "slot" } as object)}>
|
|
258
|
+
<span {...({ iconOnly: "child" } as object)}>text</span>
|
|
259
|
+
</Slot>,
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
// Assert
|
|
263
|
+
expect((container.firstChild as HTMLElement).getAttribute("icononly")).toBe(
|
|
264
|
+
"child",
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('sets displayName to "Slot" for React DevTools and stack traces', () => {
|
|
270
|
+
// Assert — an empty displayName would render it as an anonymous component.
|
|
271
|
+
expect(Slot.displayName).toBe("Slot");
|
|
162
272
|
});
|
|
163
273
|
});
|
|
@@ -5,8 +5,25 @@
|
|
|
5
5
|
*
|
|
6
6
|
* This is the standard composition pattern used throughout headless
|
|
7
7
|
* components to let consumers attach their own listeners onto a
|
|
8
|
-
* sub-component
|
|
9
|
-
*
|
|
8
|
+
* sub-component's *own* native event props (e.g. `Tabs.Trigger`'s
|
|
9
|
+
* `onClick`, `Modal.Overlay`'s `onClick`) without clobbering the
|
|
10
|
+
* component's own behaviour, while still giving them an opt-out via
|
|
11
|
+
* `preventDefault()`. Wrap it around the component's internal handler at
|
|
12
|
+
* the point the prop is consumed:
|
|
13
|
+
*
|
|
14
|
+
* ```tsx
|
|
15
|
+
* const handleClick = composeEventHandlers(onClick, () => {
|
|
16
|
+
* // the component's own behaviour, e.g. activating a tab
|
|
17
|
+
* });
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* **Not the same mechanism as {@link Slot}.** `Slot`'s own prop-merging
|
|
21
|
+
* (used for the `asChild` pattern) also composes event handlers, but with
|
|
22
|
+
* the *opposite* order (child fires first, then Slot) and *no*
|
|
23
|
+
* `preventDefault()` opt-out — both handlers always run there. Reach for
|
|
24
|
+
* `composeEventHandlers` when a component owns a native DOM prop directly;
|
|
25
|
+
* reach for `Slot` when delegating rendering to a consumer-supplied
|
|
26
|
+
* element via `asChild`. The two are never used together on the same prop.
|
|
10
27
|
*
|
|
11
28
|
* @example Consumer logs every click; component still closes the modal
|
|
12
29
|
* afterwards:
|
|
@@ -20,6 +37,13 @@
|
|
|
20
37
|
* if (formIsDirty) e.preventDefault(); // don't close
|
|
21
38
|
* }} />
|
|
22
39
|
* ```
|
|
40
|
+
*
|
|
41
|
+
* @example Opting out of the veto — always run our handler too
|
|
42
|
+
* ```tsx
|
|
43
|
+
* const handleClick = composeEventHandlers(onClick, ourHandler, {
|
|
44
|
+
* checkForDefaultPrevented: false,
|
|
45
|
+
* });
|
|
46
|
+
* ```
|
|
23
47
|
*/
|
|
24
48
|
export function composeEventHandlers<E>(
|
|
25
49
|
theirHandler: ((event: E) => void) | undefined,
|
package/src/Slot/types.ts
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
import { HTMLAttributes, ReactNode } from "react";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Props for {@link Slot} — all native `HTMLAttributes` (event handlers,
|
|
5
|
+
* `style`, `className`, `aria-*`, `data-*`, …), merged onto the single
|
|
6
|
+
* child element rather than rendered onto an element `Slot` creates
|
|
7
|
+
* itself. See {@link Slot} for the exact prop-merging rules (event
|
|
8
|
+
* handlers compose child-first, `style` shallow-merges child-wins,
|
|
9
|
+
* `className` concatenates, everything else defaults to the child).
|
|
10
|
+
*
|
|
11
|
+
* @extends HTMLElement
|
|
12
|
+
*/
|
|
3
13
|
export type SlotProps = HTMLAttributes<HTMLElement> & {
|
|
14
|
+
/**
|
|
15
|
+
* The element to clone `Slot`'s props onto. Must be exactly one valid
|
|
16
|
+
* React element that accepts a `ref` — passing zero children, more than
|
|
17
|
+
* one, or a non-element (e.g. a plain string) throws
|
|
18
|
+
* `"Slot requires exactly one React element child."`.
|
|
19
|
+
*/
|
|
4
20
|
children?: ReactNode;
|
|
5
21
|
};
|