@primitiv-ui/react 0.1.27 → 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/Accordion/types.ts
CHANGED
|
@@ -1,30 +1,58 @@
|
|
|
1
1
|
import { ComponentProps, ReactNode, Ref } from "react";
|
|
2
2
|
import { HeadingLevel } from "../types";
|
|
3
3
|
|
|
4
|
-
/** Props shared by both control modes of `Accordion.Root`.
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
/** Props shared by both control modes of `Accordion.Root`. `dir` is `Omit`ted
|
|
5
|
+
* from the native `<div>` props because it is narrowed from the DOM's
|
|
6
|
+
* `string` to {@link AccordionReadingDirection}; without the `Omit` the
|
|
7
|
+
* docs-data extractor picks the wider native declaration and silently drops
|
|
8
|
+
* the narrowed type and its JSDoc (§1.16). */
|
|
9
|
+
export type AccordionRootBaseProps = Omit<
|
|
10
|
+
ComponentProps<"div">,
|
|
11
|
+
"dir" | "defaultValue"
|
|
12
|
+
> & {
|
|
13
|
+
/** Allow more than one section to be expanded at a time. When `false`,
|
|
14
|
+
* opening an item collapses whichever item was previously open.
|
|
15
|
+
* @default false */
|
|
7
16
|
multiple?: boolean;
|
|
8
|
-
/** Layout axis, controlling arrow-key navigation
|
|
17
|
+
/** Layout axis, controlling arrow-key navigation: `"vertical"` binds
|
|
18
|
+
* ArrowUp/ArrowDown, `"horizontal"` binds ArrowLeft/ArrowRight. Surfaces as
|
|
19
|
+
* `data-orientation` on the root.
|
|
20
|
+
* @default "vertical" */
|
|
9
21
|
orientation?: "vertical" | "horizontal";
|
|
10
|
-
/** Reading direction
|
|
22
|
+
/** Reading direction; see {@link AccordionReadingDirection}. In `"rtl"` the
|
|
23
|
+
* horizontal arrow keys are mirrored. Also set as the container's `dir`
|
|
24
|
+
* attribute. Inherited from the nearest {@link DirectionProvider} when
|
|
25
|
+
* omitted, falling back to `"ltr"`. */
|
|
11
26
|
dir?: AccordionReadingDirection;
|
|
12
27
|
};
|
|
13
28
|
|
|
14
|
-
/** `Accordion.Root` props for the uncontrolled (self-managed) expanded state.
|
|
29
|
+
/** `Accordion.Root` props for the uncontrolled (self-managed) expanded state.
|
|
30
|
+
* The component owns and updates the expanded set internally, and — matching
|
|
31
|
+
* the Tabs contract — does **not** call `onValueChange` in this mode. */
|
|
15
32
|
export type AccordionRootUncontrolledProps = AccordionRootBaseProps & {
|
|
16
|
-
/**
|
|
33
|
+
/** Value of the item expanded on first render. A single value even in
|
|
34
|
+
* `multiple` mode — uncontrolled accordions can seed only one open item;
|
|
35
|
+
* pass controlled {@link AccordionRootControlledProps.value | `value`} to
|
|
36
|
+
* start with several. Omit to start with everything collapsed. */
|
|
17
37
|
defaultValue?: string;
|
|
38
|
+
/** Forbidden in uncontrolled mode — use `defaultValue`. */
|
|
18
39
|
value?: never;
|
|
40
|
+
/** Forbidden in uncontrolled mode. */
|
|
19
41
|
onValueChange?: never;
|
|
20
42
|
};
|
|
21
43
|
|
|
22
|
-
/** `Accordion.Root` props for the controlled expanded state
|
|
44
|
+
/** `Accordion.Root` props for the controlled expanded state — the parent owns
|
|
45
|
+
* the expanded set and the component defers every change back through the
|
|
46
|
+
* callback. */
|
|
23
47
|
export type AccordionRootControlledProps = AccordionRootBaseProps & {
|
|
48
|
+
/** Forbidden in controlled mode — use `value`. */
|
|
24
49
|
defaultValue?: never;
|
|
25
|
-
/**
|
|
50
|
+
/** The full set of currently expanded item values. Must be kept in sync by
|
|
51
|
+
* the parent via `onValueChange`. In single (`multiple={false}`) mode this
|
|
52
|
+
* holds at most one value. */
|
|
26
53
|
value: string[];
|
|
27
|
-
/** Called
|
|
54
|
+
/** Called with the complete next array of expanded values whenever the user
|
|
55
|
+
* toggles an item. */
|
|
28
56
|
onValueChange: (values: string[]) => void;
|
|
29
57
|
};
|
|
30
58
|
|
|
@@ -38,20 +66,33 @@ export type AccordionRootProps =
|
|
|
38
66
|
|
|
39
67
|
/** Props for `Accordion.Item` — a single collapsible section. */
|
|
40
68
|
export type AccordionItemProps = ComponentProps<"div"> & {
|
|
41
|
-
/** Section contents — typically
|
|
69
|
+
/** Section contents — typically an {@link AccordionHeaderProps | `Accordion.Header`}
|
|
70
|
+
* + {@link AccordionContentProps | `Accordion.Content`} pair. */
|
|
42
71
|
children: ReactNode;
|
|
43
|
-
/** Stable
|
|
44
|
-
|
|
72
|
+
/** Stable identifier matched against the root's expanded set (and against
|
|
73
|
+
* `value` / `defaultValue`). When omitted, a stable id is generated via
|
|
74
|
+
* `useId()` — fine for anonymous items whose state is never driven from
|
|
75
|
+
* outside. */
|
|
76
|
+
value?: string;
|
|
45
77
|
};
|
|
46
78
|
|
|
47
79
|
/** Props for `Accordion.Trigger` — the button that toggles a section. */
|
|
48
80
|
export type AccordionTriggerProps<T extends HTMLElement = HTMLButtonElement> =
|
|
49
81
|
Omit<ComponentProps<"button">, "disabled" | "ref"> & {
|
|
50
|
-
/** Trigger label / contents
|
|
82
|
+
/** Trigger label / contents — the visible section heading text, plus an
|
|
83
|
+
* optional {@link AccordionTriggerIconProps | `Accordion.TriggerIcon`}. */
|
|
51
84
|
children: ReactNode;
|
|
52
|
-
/** Disable the trigger
|
|
85
|
+
/** Disable the trigger. Rendered as `aria-disabled` / `data-disabled`
|
|
86
|
+
* rather than the native `disabled` attribute, so the button stays
|
|
87
|
+
* focusable (discoverable by keyboard) but is excluded from arrow-key
|
|
88
|
+
* navigation and cannot be activated.
|
|
89
|
+
* @default false */
|
|
53
90
|
disabled?: boolean;
|
|
54
|
-
/** Render
|
|
91
|
+
/** Render the consumer's own element instead of a `<button>`, merging all
|
|
92
|
+
* accordion ARIA attributes, event handlers, and the ref onto it via the
|
|
93
|
+
* {@link Slot} pattern. When combined with `disabled`, `role="button"` is
|
|
94
|
+
* injected so `aria-disabled` is valid on non-button children.
|
|
95
|
+
* @default false */
|
|
55
96
|
asChild?: boolean;
|
|
56
97
|
/** Ref to the rendered element. Defaults to `HTMLButtonElement`; when using
|
|
57
98
|
* `asChild`, specify the child's element type (e.g. `HTMLAnchorElement`). */
|
|
@@ -60,23 +101,33 @@ export type AccordionTriggerProps<T extends HTMLElement = HTMLButtonElement> =
|
|
|
60
101
|
|
|
61
102
|
/** Props for `Accordion.Header` — the heading wrapping a trigger. */
|
|
62
103
|
export type AccordionHeaderProps = ComponentProps<"h3"> & {
|
|
63
|
-
/** Header contents — typically an
|
|
104
|
+
/** Header contents — typically an
|
|
105
|
+
* {@link AccordionTriggerProps | `Accordion.Trigger`}. */
|
|
64
106
|
children: ReactNode;
|
|
65
|
-
/** Heading level
|
|
107
|
+
/** Heading level to render (`h1`–`h6`). Choose the level that fits the
|
|
108
|
+
* surrounding document outline — the WAI-ARIA Accordion pattern requires
|
|
109
|
+
* each trigger to be wrapped in a heading at the right level.
|
|
110
|
+
* @default 3 */
|
|
66
111
|
level?: HeadingLevel;
|
|
67
112
|
};
|
|
68
113
|
|
|
69
114
|
/** Props for `Accordion.Content` — the collapsible panel. */
|
|
70
115
|
export type AccordionContentProps = ComponentProps<"div"> & {
|
|
71
|
-
/** Panel contents. */
|
|
116
|
+
/** Panel contents, revealed when the matching trigger is expanded. */
|
|
72
117
|
children: ReactNode;
|
|
73
|
-
/** Keep the panel mounted even while collapsed
|
|
118
|
+
/** Keep the panel mounted in the DOM even while collapsed (instead of
|
|
119
|
+
* removing it from view with the `hidden` attribute), so CSS open/close
|
|
120
|
+
* transitions can run. While closed it receives `aria-hidden="true"` so
|
|
121
|
+
* assistive tech still ignores it; drive visibility yourself via
|
|
122
|
+
* `[data-state="closed"]`.
|
|
123
|
+
* @default false */
|
|
74
124
|
forceMount?: boolean;
|
|
75
125
|
};
|
|
76
126
|
|
|
77
127
|
/** Props for `Accordion.TriggerIcon` — a decorative open/closed indicator. */
|
|
78
128
|
export type AccordionTriggerIconProps = ComponentProps<"span"> & {
|
|
79
|
-
/** Icon contents.
|
|
129
|
+
/** Icon contents — an inline `<svg>` or any icon component. Rendered inside
|
|
130
|
+
* an `aria-hidden` `<span>` carrying a `data-state` open/close hook. */
|
|
80
131
|
children: ReactNode;
|
|
81
132
|
};
|
|
82
133
|
|
package/src/Alert/Alert.tsx
CHANGED
|
@@ -4,7 +4,8 @@ import { Slot } from "../Slot/index.ts";
|
|
|
4
4
|
import { AlertProps } from "./types";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* An assertive live region for high-priority, time-sensitive messages
|
|
7
|
+
* An assertive live region for high-priority, time-sensitive messages,
|
|
8
|
+
* implementing the WAI-ARIA `alert` role.
|
|
8
9
|
*
|
|
9
10
|
* Renders a `<div role="alert">`. The `alert` role carries an implicit
|
|
10
11
|
* `aria-live="assertive"` and `aria-atomic="true"`, so assistive
|
|
@@ -12,20 +13,48 @@ import { AlertProps } from "./types";
|
|
|
12
13
|
* appears — use it for errors and other content the user must see now.
|
|
13
14
|
* For non-urgent updates, reach for `Status` instead.
|
|
14
15
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* **Announce on appearance.** A live region announces content that
|
|
17
|
+
* changes *after* the region is already in the DOM. Render `Alert`
|
|
18
|
+
* conditionally so the message appears into an already-mounted tree:
|
|
19
|
+
*
|
|
20
|
+
* ```tsx
|
|
21
|
+
* {submitError && <Alert>{submitError}</Alert>}
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* Mounting an `Alert` that already contains its message may not be
|
|
25
|
+
* announced reliably by all assistive technology.
|
|
26
|
+
*
|
|
27
|
+
* **Styling hooks.** The component emits no `data-*` attributes — it
|
|
28
|
+
* carries no internal state. Target the rendered element directly via
|
|
29
|
+
* `className` or the `[role="alert"]` selector.
|
|
18
30
|
*
|
|
19
31
|
* **`asChild` composition.** Renders the consumer's element instead of
|
|
20
|
-
* a `<div>`, merging `role="alert"` and all other props
|
|
21
|
-
* {@link Slot} utility.
|
|
32
|
+
* a `<div>`, merging `role="alert"` and all other props onto it via the
|
|
33
|
+
* {@link Slot} utility. Useful when you already have a semantic element
|
|
34
|
+
* (e.g. `<section>`, `<aside>`) that should also function as an ARIA
|
|
35
|
+
* alert.
|
|
36
|
+
*
|
|
37
|
+
* **Ref forwarding.** Pass a `ref` prop to access the underlying
|
|
38
|
+
* `HTMLDivElement` (or, under `asChild`, the consumer's element):
|
|
39
|
+
*
|
|
40
|
+
* ```tsx
|
|
41
|
+
* const ref = useRef<HTMLDivElement>(null);
|
|
42
|
+
* <Alert ref={ref}>Something went wrong.</Alert>
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* @extends HTMLDivElement
|
|
22
46
|
*
|
|
23
47
|
* @example Form error
|
|
24
48
|
* ```tsx
|
|
25
49
|
* {error && <Alert>{error}</Alert>}
|
|
26
50
|
* ```
|
|
27
51
|
*
|
|
28
|
-
* @example
|
|
52
|
+
* @example Inline styling via className
|
|
53
|
+
* ```tsx
|
|
54
|
+
* <Alert className="alert alert--error">Payment declined.</Alert>
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* @example asChild — apply the alert role to a semantic element
|
|
29
58
|
* ```tsx
|
|
30
59
|
* <Alert asChild>
|
|
31
60
|
* <section>Upload failed — try again.</section>
|
|
@@ -25,4 +25,9 @@ describe("Alert component", () => {
|
|
|
25
25
|
expect(alert.tagName).toBe("SECTION");
|
|
26
26
|
expect(alert).toHaveTextContent("Upload failed");
|
|
27
27
|
});
|
|
28
|
+
|
|
29
|
+
it('sets displayName to "Alert" for React DevTools and stack traces', () => {
|
|
30
|
+
// Assert — an empty displayName would render it as an anonymous component.
|
|
31
|
+
expect(Alert.displayName).toBe("Alert");
|
|
32
|
+
});
|
|
28
33
|
});
|
package/src/Alert/types.ts
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
|
-
import { ComponentProps } from "react";
|
|
1
|
+
import { ComponentProps, ReactNode } from "react";
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Props for {@link Alert} — all native `<div>` attributes plus the `asChild`
|
|
5
|
+
* escape hatch.
|
|
6
|
+
*
|
|
7
|
+
* The `role` attribute is managed internally and set to `"alert"` regardless
|
|
8
|
+
* of what is passed; there is no need to supply it.
|
|
9
|
+
*/
|
|
4
10
|
export type AlertProps = ComponentProps<"div"> & {
|
|
11
|
+
/**
|
|
12
|
+
* Renders the child element instead of a native `<div>`, merging
|
|
13
|
+
* `role="alert"` and all other props onto it via {@link Slot}.
|
|
14
|
+
* Useful when you already have a semantic element (e.g. `<section>`,
|
|
15
|
+
* `<aside>`) that should also function as an ARIA alert.
|
|
16
|
+
* @default false
|
|
17
|
+
*/
|
|
5
18
|
asChild?: boolean;
|
|
19
|
+
/** The live-region content. Injected after the region is already in the
|
|
20
|
+
* DOM so that assistive technology announces the change. */
|
|
21
|
+
children?: ReactNode;
|
|
6
22
|
};
|
package/src/Avatar/Avatar.tsx
CHANGED
|
@@ -18,10 +18,37 @@ import {
|
|
|
18
18
|
* {@link Avatar.Image | `Avatar.Image`} and
|
|
19
19
|
* {@link Avatar.Fallback | `Avatar.Fallback`}.
|
|
20
20
|
*
|
|
21
|
+
* **Load-state model.** Root holds the single source of truth for the image's
|
|
22
|
+
* {@link AvatarImageLoadingStatus} in internal state, starting at `"idle"`.
|
|
23
|
+
* The child `Avatar.Image` reports transitions (`"loading"` → `"loaded"` /
|
|
24
|
+
* `"error"`) up through context; `Avatar.Fallback` reads that status to decide
|
|
25
|
+
* whether to render. There is no controlled/uncontrolled split — status is
|
|
26
|
+
* always driven by the image's own load lifecycle.
|
|
27
|
+
*
|
|
21
28
|
* **Styling hooks.** `data-status="idle" | "loading" | "loaded" | "error"`.
|
|
22
29
|
*
|
|
23
|
-
* **`asChild`
|
|
24
|
-
* the container
|
|
30
|
+
* **`asChild` composition.** Pass `asChild` to render the consumer's own
|
|
31
|
+
* element as the container instead of the native `<span>`, with the
|
|
32
|
+
* `data-status` hook merged in via the {@link Slot} pattern.
|
|
33
|
+
*
|
|
34
|
+
* @extends HTMLSpanElement
|
|
35
|
+
*
|
|
36
|
+
* @example Image with a generated-initials fallback
|
|
37
|
+
* ```tsx
|
|
38
|
+
* <Avatar.Root>
|
|
39
|
+
* <Avatar.Image src={user.avatarUrl} alt={user.name} />
|
|
40
|
+
* <Avatar.Fallback>{initials(user.name)}</Avatar.Fallback>
|
|
41
|
+
* </Avatar.Root>
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* @example Fallback only (no image source)
|
|
45
|
+
* ```tsx
|
|
46
|
+
* <Avatar.Root>
|
|
47
|
+
* <Avatar.Fallback>
|
|
48
|
+
* <UserIcon aria-hidden />
|
|
49
|
+
* </Avatar.Fallback>
|
|
50
|
+
* </Avatar.Root>
|
|
51
|
+
* ```
|
|
25
52
|
*/
|
|
26
53
|
export function AvatarRoot({
|
|
27
54
|
asChild = false,
|
|
@@ -45,19 +72,39 @@ export function AvatarRoot({
|
|
|
45
72
|
);
|
|
46
73
|
}
|
|
47
74
|
|
|
48
|
-
|
|
75
|
+
// Runtime-dead: the compound alias below (same object via Object.assign)
|
|
76
|
+
// overwrites this to "Avatar" at load, so the value is never observable. The
|
|
77
|
+
// assignment stays because it declares `displayName` on `typeof AvatarRoot`,
|
|
78
|
+
// which TAvatarCompound extends.
|
|
79
|
+
// Stryker disable next-line StringLiteral: overwritten by the compound alias — an equivalent mutant.
|
|
49
80
|
AvatarRoot.displayName = "AvatarRoot";
|
|
50
81
|
|
|
51
82
|
/**
|
|
52
83
|
* The image of an Avatar — an `<img>` that reports its load lifecycle to the
|
|
53
84
|
* parent {@link Avatar.Root}.
|
|
54
85
|
*
|
|
86
|
+
* **Cache-hit safe.** The internal callback ref inspects `naturalWidth` on
|
|
87
|
+
* attach, so an image already decoded in the browser cache (whose `load` event
|
|
88
|
+
* fires before React can bind `onLoad`) is still reported as `"loaded"`. Every
|
|
89
|
+
* other image reports `"loading"` until its `load` / `error` event resolves it.
|
|
90
|
+
*
|
|
55
91
|
* **Styling hooks.** `data-status` mirrors the root's status. The image stays
|
|
56
|
-
* mounted on error; hide a broken image with CSS, e.g.
|
|
92
|
+
* mounted on error rather than unmounting; hide a broken image with CSS, e.g.
|
|
57
93
|
* `img:not([data-status="loaded"]) { display: none }`.
|
|
58
94
|
*
|
|
59
|
-
* **`asChild`
|
|
60
|
-
* with the load handlers, ref, and `data-status` hook merged in
|
|
95
|
+
* **`asChild` composition.** Pass `asChild` to render the consumer's own
|
|
96
|
+
* `<img>`, with the load handlers, ref, and `data-status` hook merged in via
|
|
97
|
+
* the {@link Slot} pattern.
|
|
98
|
+
*
|
|
99
|
+
* @extends HTMLImageElement
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```tsx
|
|
103
|
+
* <Avatar.Root>
|
|
104
|
+
* <Avatar.Image src="/ada.png" alt="Ada Lovelace" />
|
|
105
|
+
* <Avatar.Fallback>AL</Avatar.Fallback>
|
|
106
|
+
* </Avatar.Root>
|
|
107
|
+
* ```
|
|
61
108
|
*
|
|
62
109
|
* @throws if rendered outside an `Avatar.Root`.
|
|
63
110
|
*/
|
|
@@ -92,14 +139,28 @@ AvatarImage.displayName = "AvatarImage";
|
|
|
92
139
|
|
|
93
140
|
/**
|
|
94
141
|
* The fallback of an Avatar — a `<span>` shown while the parent
|
|
95
|
-
* {@link Avatar.Root}'s image is anything other than `"loaded"` (
|
|
96
|
-
* loading
|
|
142
|
+
* {@link Avatar.Root}'s image is anything other than `"loaded"` (i.e. `"idle"`,
|
|
143
|
+
* `"loading"`, or `"error"`). Once the image loads, the fallback returns `null`
|
|
144
|
+
* and unmounts. Its children are the consumer's stand-in content — typically
|
|
145
|
+
* generated initials or a placeholder silhouette icon.
|
|
146
|
+
*
|
|
147
|
+
* Pass {@link AvatarFallbackProps.delayMs | `delayMs`} to withhold the fallback
|
|
148
|
+
* for that many milliseconds after mount, avoiding a flash of fallback content
|
|
149
|
+
* when the image loads quickly. Omit it to render the fallback straight away.
|
|
97
150
|
*
|
|
98
|
-
* Pass `
|
|
99
|
-
*
|
|
151
|
+
* **`asChild` composition.** Pass `asChild` to render the consumer's own
|
|
152
|
+
* element as the fallback instead of the native `<span>`, with the
|
|
153
|
+
* `data-status` hook merged in via the {@link Slot} pattern.
|
|
100
154
|
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
155
|
+
* @extends HTMLSpanElement
|
|
156
|
+
*
|
|
157
|
+
* @example Deferred to avoid a flash on fast connections
|
|
158
|
+
* ```tsx
|
|
159
|
+
* <Avatar.Root>
|
|
160
|
+
* <Avatar.Image src={user.avatarUrl} alt={user.name} />
|
|
161
|
+
* <Avatar.Fallback delayMs={600}>{initials(user.name)}</Avatar.Fallback>
|
|
162
|
+
* </Avatar.Root>
|
|
163
|
+
* ```
|
|
103
164
|
*
|
|
104
165
|
* @throws if rendered outside an `Avatar.Root`.
|
|
105
166
|
*/
|
|
@@ -113,6 +174,9 @@ export function AvatarFallback({
|
|
|
113
174
|
const [delayElapsed, setDelayElapsed] = useState(delayMs === undefined);
|
|
114
175
|
|
|
115
176
|
useEffect(() => {
|
|
177
|
+
// Stryker disable next-line all: when delayMs is undefined, delayElapsed is
|
|
178
|
+
// already initialised true, so skipping vs. running this branch only sets a
|
|
179
|
+
// timer whose setDelayElapsed(true) is a no-op — no observable difference.
|
|
116
180
|
if (delayMs === undefined) {
|
|
117
181
|
return;
|
|
118
182
|
}
|
|
@@ -147,9 +211,29 @@ export type TAvatarCompound = typeof AvatarRoot & {
|
|
|
147
211
|
* Headless, accessible **Avatar** — a compound component for a user image
|
|
148
212
|
* with a graceful fallback. Zero styles ship.
|
|
149
213
|
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
214
|
+
* `Avatar` is both callable (an alias of {@link AvatarRoot | `Avatar.Root`})
|
|
215
|
+
* and carries its sub-components as static properties.
|
|
216
|
+
*
|
|
217
|
+
* - {@link Avatar.Root | `Avatar.Root`} — `<span>` container, owns the image
|
|
218
|
+
* loading status, context provider.
|
|
219
|
+
* - {@link Avatar.Image | `Avatar.Image`} — the `<img>`, reports its load
|
|
220
|
+
* status (cache-hit safe).
|
|
221
|
+
* - {@link Avatar.Fallback | `Avatar.Fallback`} — stand-in content shown until
|
|
222
|
+
* the image loads, with an optional `delayMs` anti-flash guard.
|
|
223
|
+
*
|
|
224
|
+
* @example Minimal usage
|
|
225
|
+
* ```tsx
|
|
226
|
+
* import { Avatar } from "@primitiv-ui/react";
|
|
227
|
+
*
|
|
228
|
+
* <Avatar.Root>
|
|
229
|
+
* <Avatar.Image src={user.avatarUrl} alt={user.name} />
|
|
230
|
+
* <Avatar.Fallback>{initials(user.name)}</Avatar.Fallback>
|
|
231
|
+
* </Avatar.Root>
|
|
232
|
+
* ```
|
|
233
|
+
*
|
|
234
|
+
* @see {@link AvatarRoot} for the load-state model and `asChild`.
|
|
235
|
+
* @see {@link AvatarImage} for cache-hit handling and error behaviour.
|
|
236
|
+
* @see {@link AvatarFallback} for the `delayMs` anti-flash guard.
|
|
153
237
|
*/
|
|
154
238
|
const AvatarCompound: TAvatarCompound = Object.assign(AvatarRoot, {
|
|
155
239
|
Root: AvatarRoot,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { render, screen } from "@testing-library/react";
|
|
2
2
|
|
|
3
3
|
import { Avatar } from "../Avatar";
|
|
4
|
+
import { AvatarContext } from "../AvatarContext";
|
|
4
5
|
|
|
5
6
|
describe("Avatar basic rendering", () => {
|
|
6
7
|
it("renders Avatar.Root as a <span>", () => {
|
|
@@ -11,4 +12,13 @@ describe("Avatar basic rendering", () => {
|
|
|
11
12
|
// Assert
|
|
12
13
|
expect(root.tagName).toBe("SPAN");
|
|
13
14
|
});
|
|
15
|
+
|
|
16
|
+
it("sets a displayName on the compound, sub-components, and context", () => {
|
|
17
|
+
// Assert — empty displayNames would render each as anonymous in DevTools.
|
|
18
|
+
// Root aliases the compound (Object.assign), so its name is "Avatar".
|
|
19
|
+
expect(Avatar.displayName).toBe("Avatar");
|
|
20
|
+
expect(Avatar.Image.displayName).toBe("AvatarImage");
|
|
21
|
+
expect(Avatar.Fallback.displayName).toBe("AvatarFallback");
|
|
22
|
+
expect(AvatarContext.displayName).toBe("AvatarContext");
|
|
23
|
+
});
|
|
14
24
|
});
|
|
@@ -71,5 +71,35 @@ describe("Avatar fallback", () => {
|
|
|
71
71
|
// Assert — shown once the delay elapses
|
|
72
72
|
expect(screen.getByText("AL")).toBeInTheDocument();
|
|
73
73
|
});
|
|
74
|
+
|
|
75
|
+
it("resets the timer when delayMs changes", () => {
|
|
76
|
+
// Arrange — start a 300ms delay…
|
|
77
|
+
const { rerender } = render(
|
|
78
|
+
<Avatar.Root>
|
|
79
|
+
<Avatar.Fallback delayMs={300}>AL</Avatar.Fallback>
|
|
80
|
+
</Avatar.Root>,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// Act — …then extend it to 1000ms before the first delay elapses. The
|
|
84
|
+
// effect must clear the old timer and start a new one; a stale timer (no
|
|
85
|
+
// cleanup) or a frozen dependency ([] deps) would fire at 300ms.
|
|
86
|
+
rerender(
|
|
87
|
+
<Avatar.Root>
|
|
88
|
+
<Avatar.Fallback delayMs={1000}>AL</Avatar.Fallback>
|
|
89
|
+
</Avatar.Root>,
|
|
90
|
+
);
|
|
91
|
+
act(() => {
|
|
92
|
+
vi.advanceTimersByTime(300);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Assert — still withheld at 300ms because the delay was reset to 1000ms
|
|
96
|
+
expect(screen.queryByText("AL")).not.toBeInTheDocument();
|
|
97
|
+
|
|
98
|
+
// …and shown once the new delay elapses
|
|
99
|
+
act(() => {
|
|
100
|
+
vi.advanceTimersByTime(700);
|
|
101
|
+
});
|
|
102
|
+
expect(screen.getByText("AL")).toBeInTheDocument();
|
|
103
|
+
});
|
|
74
104
|
});
|
|
75
105
|
});
|
|
@@ -21,6 +21,41 @@ describe("Avatar image loading", () => {
|
|
|
21
21
|
expect(img).toHaveAttribute("src", "/ada.png");
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
+
it('reports data-status="loading" on mount before any load event', () => {
|
|
25
|
+
// Arrange & Act — jsdom images report naturalWidth 0, so the attach-time
|
|
26
|
+
// inspection sees no cache hit and reports "loading" until load/error.
|
|
27
|
+
render(
|
|
28
|
+
<Avatar.Root data-testid="root">
|
|
29
|
+
<Avatar.Image src="/ada.png" alt="Ada" data-testid="img" />
|
|
30
|
+
</Avatar.Root>,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
// Assert
|
|
34
|
+
expect(screen.getByTestId("root")).toHaveAttribute("data-status", "loading");
|
|
35
|
+
expect(screen.getByTestId("img")).toHaveAttribute("data-status", "loading");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("tolerates the image leaving the tree (callback ref receives null)", () => {
|
|
39
|
+
// Arrange — an Avatar whose Image can be swapped out for a Fallback.
|
|
40
|
+
const { rerender } = render(
|
|
41
|
+
<Avatar.Root data-testid="root">
|
|
42
|
+
<Avatar.Image src="/ada.png" alt="Ada" />
|
|
43
|
+
</Avatar.Root>,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// Act — removing the Image detaches its callback ref, calling it with null.
|
|
47
|
+
// Without the `img === null` guard this dereferences null.naturalWidth and
|
|
48
|
+
// throws during the commit, failing the render.
|
|
49
|
+
rerender(
|
|
50
|
+
<Avatar.Root data-testid="root">
|
|
51
|
+
<Avatar.Fallback>AL</Avatar.Fallback>
|
|
52
|
+
</Avatar.Root>,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// Assert
|
|
56
|
+
expect(screen.getByText("AL")).toBeInTheDocument();
|
|
57
|
+
});
|
|
58
|
+
|
|
24
59
|
it('reports data-status="loaded" on the Root and Image once the image loads', () => {
|
|
25
60
|
// Arrange
|
|
26
61
|
render(
|
|
@@ -24,16 +24,32 @@ export function useAvatarImage(
|
|
|
24
24
|
|
|
25
25
|
const ref = useCallback(
|
|
26
26
|
(img: HTMLImageElement | null) => {
|
|
27
|
+
// The `img === null` guard is load-bearing: without it, detaching the ref
|
|
28
|
+
// dereferences null.naturalWidth and throws (see the "leaving the tree"
|
|
29
|
+
// test, which kills the BlockStatement twin of this line). Stryker's
|
|
30
|
+
// vitest runner does not reliably attribute that React commit-phase throw
|
|
31
|
+
// for the ConditionalExpression variant, so it can't be scored despite
|
|
32
|
+
// being covered and defended — a runner limitation, not an equivalent
|
|
33
|
+
// mutant. The condition->true variant is killed by the "loading on mount"
|
|
34
|
+
// test.
|
|
35
|
+
// Stryker disable next-line ConditionalExpression
|
|
27
36
|
if (img === null || reported.current) {
|
|
28
37
|
return;
|
|
29
38
|
}
|
|
30
39
|
reported.current = true;
|
|
31
40
|
setStatus(img.naturalWidth > 0 ? "loaded" : "loading");
|
|
32
41
|
},
|
|
42
|
+
// Stryker disable next-line ArrayDeclaration: setStatus is a useState
|
|
43
|
+
// setter with a stable identity, so [] and [setStatus] recreate the
|
|
44
|
+
// callback identically — the dependency is behaviourally inert (equivalent).
|
|
33
45
|
[setStatus],
|
|
34
46
|
);
|
|
35
47
|
|
|
48
|
+
// Stryker disable next-line ArrayDeclaration: setStatus is a stable useState
|
|
49
|
+
// setter, so the dependency array cannot change the callback — equivalent.
|
|
36
50
|
const onLoad = useCallback(() => setStatus("loaded"), [setStatus]);
|
|
51
|
+
// Stryker disable next-line ArrayDeclaration: setStatus is a stable useState
|
|
52
|
+
// setter, so the dependency array cannot change the callback — equivalent.
|
|
37
53
|
const onError = useCallback(() => setStatus("error"), [setStatus]);
|
|
38
54
|
|
|
39
55
|
return { ref, onLoad, onError };
|
package/src/Avatar/types.ts
CHANGED
|
@@ -16,7 +16,11 @@ export type AvatarImageLoadingStatus = "idle" | "loading" | "loaded" | "error";
|
|
|
16
16
|
* Props for {@link Avatar.Root} — all `<span>` attributes plus `asChild`.
|
|
17
17
|
*/
|
|
18
18
|
export type AvatarRootProps = ComponentProps<"span"> & {
|
|
19
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Render the consumer's own element instead of the native `<span>`, merging
|
|
21
|
+
* the `data-status` hook onto it via {@link Slot}.
|
|
22
|
+
* @default false
|
|
23
|
+
*/
|
|
20
24
|
asChild?: boolean;
|
|
21
25
|
};
|
|
22
26
|
|
|
@@ -24,7 +28,11 @@ export type AvatarRootProps = ComponentProps<"span"> & {
|
|
|
24
28
|
* Props for {@link Avatar.Image} — all `<img>` attributes plus `asChild`.
|
|
25
29
|
*/
|
|
26
30
|
export type AvatarImageProps = ComponentProps<"img"> & {
|
|
27
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* Render the consumer's own `<img>` instead of the native one, merging the
|
|
33
|
+
* load handlers, ref, and `data-status` hook onto it via {@link Slot}.
|
|
34
|
+
* @default false
|
|
35
|
+
*/
|
|
28
36
|
asChild?: boolean;
|
|
29
37
|
};
|
|
30
38
|
|
|
@@ -39,6 +47,10 @@ export type AvatarFallbackProps = ComponentProps<"span"> & {
|
|
|
39
47
|
* loads almost immediately. Omit to render the fallback straight away.
|
|
40
48
|
*/
|
|
41
49
|
delayMs?: number;
|
|
42
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* Render the consumer's own element instead of the native `<span>`, merging
|
|
52
|
+
* the `data-status` hook onto it via {@link Slot}.
|
|
53
|
+
* @default false
|
|
54
|
+
*/
|
|
43
55
|
asChild?: boolean;
|
|
44
56
|
};
|