@primitiv-ui/react 0.1.29 → 0.1.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -2
- package/package.json +1 -1
- package/src/AccessibleIcon/AccessibleIcon.tsx +1 -1
- package/src/Accordion/Accordion.tsx +7 -7
- package/src/Accordion/README.md +2 -2
- package/src/Avatar/__tests__/Avatar.fallback.test.tsx +3 -3
- package/src/Breadcrumb/Breadcrumb.tsx +7 -7
- package/src/Breadcrumb/README.md +2 -2
- package/src/Breadcrumb/__tests__/Breadcrumb.ellipsis.test.tsx +1 -1
- package/src/Breadcrumb/types.ts +1 -1
- package/src/Button/__tests__/Button.contract.test.tsx +1 -1
- package/src/Carousel/Carousel.tsx +6 -6
- package/src/Carousel/README.md +22 -22
- package/src/Carousel/__tests__/Carousel.infinite.test.tsx +5 -5
- package/src/Carousel/__tests__/Carousel.intersection-observer.test.tsx +2 -2
- package/src/Carousel/__tests__/Carousel.slides-per-move.test.tsx +1 -1
- package/src/Carousel/loopEngine.ts +1 -1
- package/src/Carousel/types.ts +1 -1
- package/src/Checkbox/Checkbox.tsx +2 -2
- package/src/CheckboxCard/CheckboxCard.tsx +1 -1
- package/src/CheckboxCard/README.md +1 -1
- package/src/Collapsible/README.md +2 -2
- package/src/Combobox/Combobox.tsx +508 -0
- package/src/Combobox/ComboboxContext.ts +23 -0
- package/src/Combobox/README.md +170 -0
- package/src/Combobox/__tests__/Combobox.asChild.test.tsx +62 -0
- package/src/Combobox/__tests__/Combobox.basic-rendering.test.tsx +16 -0
- package/src/Combobox/__tests__/Combobox.content.test.tsx +36 -0
- package/src/Combobox/__tests__/Combobox.controlled-state.test.tsx +88 -0
- package/src/Combobox/__tests__/Combobox.empty.test.tsx +42 -0
- package/src/Combobox/__tests__/Combobox.ids.test.tsx +66 -0
- package/src/Combobox/__tests__/Combobox.item.test.tsx +76 -0
- package/src/Combobox/__tests__/Combobox.keyboard-interaction.test.tsx +196 -0
- package/src/Combobox/__tests__/Combobox.light-dismiss.test.tsx +101 -0
- package/src/Combobox/__tests__/Combobox.query.test.tsx +105 -0
- package/src/Combobox/hooks/index.ts +1 -0
- package/src/Combobox/hooks/useComboboxRoot.ts +2 -0
- package/src/Combobox/index.ts +3 -0
- package/src/Combobox/types.ts +133 -0
- package/src/ContextMenu/ContextMenu.tsx +3 -3
- package/src/ContextMenu/README.md +1 -1
- package/src/DirectionProvider/DirectionProvider.tsx +5 -5
- package/src/DirectionProvider/README.md +4 -4
- package/src/Divider/__tests__/Divider.contract.test.tsx +1 -1
- package/src/Drawer/Drawer.tsx +5 -5
- package/src/Drawer/README.md +2 -2
- package/src/Dropdown/Dropdown.tsx +2 -2
- package/src/Dropdown/README.md +7 -5
- package/src/EmptyState/README.md +2 -2
- package/src/Field/README.md +1 -1
- package/src/Field/hooks/useFieldProps.ts +1 -1
- package/src/Fieldset/Fieldset.tsx +3 -3
- package/src/Fieldset/README.md +2 -2
- package/src/Input/Input.tsx +1 -1
- package/src/Input/README.md +2 -2
- package/src/Listbox/Listbox.tsx +475 -0
- package/src/Listbox/ListboxContext.ts +48 -0
- package/src/Listbox/README.md +248 -0
- package/src/Listbox/__tests__/Listbox.active-descendant.test.tsx +128 -0
- package/src/Listbox/__tests__/Listbox.asChild.test.tsx +91 -0
- package/src/Listbox/__tests__/Listbox.basic-rendering.test.tsx +16 -0
- package/src/Listbox/__tests__/Listbox.controlled-state.test.tsx +114 -0
- package/src/Listbox/__tests__/Listbox.disabled.test.tsx +90 -0
- package/src/Listbox/__tests__/Listbox.fixtures.ts +27 -0
- package/src/Listbox/__tests__/Listbox.group-label.test.tsx +97 -0
- package/src/Listbox/__tests__/Listbox.group.test.tsx +45 -0
- package/src/Listbox/__tests__/Listbox.keyboard-edge-cases.test.tsx +144 -0
- package/src/Listbox/__tests__/Listbox.keyboard-interaction.test.tsx +112 -0
- package/src/Listbox/__tests__/Listbox.modifier-keys.test.tsx +86 -0
- package/src/Listbox/__tests__/Listbox.multi-select-keyboard.test.tsx +368 -0
- package/src/Listbox/__tests__/Listbox.multiple-mode.test.tsx +91 -0
- package/src/Listbox/__tests__/Listbox.mutation-hardening.test.tsx +480 -0
- package/src/Listbox/__tests__/Listbox.orientation.test.tsx +59 -0
- package/src/Listbox/__tests__/Listbox.reading-direction.test.tsx +65 -0
- package/src/Listbox/__tests__/Listbox.reorder.test.tsx +78 -0
- package/src/Listbox/__tests__/Listbox.selection-follows-focus.test.tsx +64 -0
- package/src/Listbox/__tests__/Listbox.strict-context.test.tsx +21 -0
- package/src/Listbox/__tests__/Listbox.typeahead.test.tsx +123 -0
- package/src/Listbox/__tests__/Listbox.uncontrolled-state.test.tsx +48 -0
- package/src/Listbox/constants.ts +2 -0
- package/src/Listbox/hooks/index.ts +1 -0
- package/src/Listbox/hooks/useListboxRoot.ts +407 -0
- package/src/Listbox/index.ts +3 -0
- package/src/Listbox/types.ts +238 -0
- package/src/MillerColumns/MillerColumns.tsx +104 -30
- package/src/MillerColumns/README.md +90 -20
- package/src/MillerColumns/__tests__/MillerColumns.aria.test.tsx +45 -0
- package/src/MillerColumns/__tests__/MillerColumns.auto-scroll.test.tsx +4 -2
- package/src/MillerColumns/__tests__/MillerColumns.basic-rendering.test.tsx +18 -3
- package/src/MillerColumns/__tests__/MillerColumns.data-attributes.test.tsx +48 -0
- package/src/MillerColumns/__tests__/MillerColumns.keyboard-interaction.test.tsx +35 -2
- package/src/MillerColumns/__tests__/MillerColumns.keyboard-selection-state.test.tsx +96 -0
- package/src/MillerColumns/__tests__/MillerColumns.preview-panel.test.tsx +29 -4
- package/src/MillerColumns/__tests__/MillerColumns.preview-visibility.test.tsx +79 -0
- package/src/MillerColumns/__tests__/MillerColumns.resize.test.tsx +189 -0
- package/src/MillerColumns/__tests__/MillerColumns.styled-parts.test.tsx +143 -0
- package/src/MillerColumns/__tests__/MillerColumns.typeahead.test.tsx +104 -0
- package/src/MillerColumns/constants.ts +2 -0
- package/src/MillerColumns/hooks/useMillerColumnsColumn.ts +5 -2
- package/src/MillerColumns/hooks/useMillerColumnsItem.ts +29 -7
- package/src/MillerColumns/hooks/useMillerColumnsResizeHandle.ts +80 -10
- package/src/MillerColumns/hooks/useMillerColumnsRoot.ts +0 -0
- package/src/MillerColumns/index.ts +4 -0
- package/src/MillerColumns/types.ts +56 -6
- package/src/MillerColumns/utils.ts +69 -3
- package/src/Modal/Modal.tsx +4 -4
- package/src/Modal/README.md +5 -5
- package/src/Modal/types.ts +1 -1
- package/src/NavigationMenu/NavigationMenu.tsx +6 -6
- package/src/NavigationMenu/README.md +3 -3
- package/src/NavigationMenu/hooks/useNavigationMenuRoot.ts +1 -1
- package/src/NavigationMenu/hooks/useNavigationMenuTrigger.ts +1 -1
- package/src/Popover/Popover.tsx +2 -2
- package/src/Popover/README.md +1 -1
- package/src/Portal/Portal.tsx +2 -2
- package/src/Portal/README.md +2 -2
- package/src/Progress/README.md +1 -1
- package/src/Radio/Radio.tsx +1 -1
- package/src/RadioGroup/README.md +2 -2
- package/src/RadioGroup/RadioGroup.tsx +1 -1
- package/src/SegmentedControl/README.md +2 -2
- package/src/SegmentedControl/SegmentedControl.tsx +1 -1
- package/src/SegmentedControl/hooks/useSegmentedControlRoot.ts +1 -0
- package/src/Select/README.md +2 -2
- package/src/Select/Select.tsx +8 -8
- package/src/Select/__tests__/Select.placeholder.test.tsx +3 -3
- package/src/Select/__tests__/Select.typeahead.test.tsx +3 -3
- package/src/Select/__tests__/Select.value-mirror.test.tsx +1 -1
- package/src/Select/types.ts +2 -2
- package/src/SkipNav/README.md +3 -3
- package/src/SkipNav/SkipNav.tsx +5 -5
- package/src/Slider/README.md +37 -7
- package/src/Slider/Slider.tsx +9 -3
- package/src/Slot/Slot.tsx +2 -2
- package/src/Slot/__tests__/Slot.test.tsx +2 -2
- package/src/Slot/types.ts +1 -1
- package/src/SplitButton/README.md +225 -0
- package/src/SplitButton/SplitButton.tsx +457 -0
- package/src/SplitButton/SplitButtonContext.ts +26 -0
- package/src/SplitButton/__tests__/SplitButton.accessibility.test.tsx +55 -0
- package/src/SplitButton/__tests__/SplitButton.asChild.test.tsx +51 -0
- package/src/SplitButton/__tests__/SplitButton.basic-rendering.test.tsx +57 -0
- package/src/SplitButton/__tests__/SplitButton.data-attributes.test.tsx +28 -0
- package/src/SplitButton/__tests__/SplitButton.disabled.test.tsx +79 -0
- package/src/SplitButton/__tests__/SplitButton.error-handling.test.tsx +29 -0
- package/src/SplitButton/__tests__/SplitButton.keyboard-interaction.test.tsx +56 -0
- package/src/SplitButton/__tests__/SplitButton.state-modes.test.tsx +68 -0
- package/src/SplitButton/index.ts +2 -0
- package/src/SplitButton/types.ts +186 -0
- package/src/Switch/README.md +1 -1
- package/src/Switch/Switch.tsx +2 -2
- package/src/Switch/types.ts +1 -1
- package/src/Table/README.md +110 -8
- package/src/Table/Table.tsx +206 -10
- package/src/Table/TableExpandableContext.ts +7 -0
- package/src/Table/__tests__/Table.DetailRow.test.tsx +83 -0
- package/src/Table/__tests__/Table.ExpandTrigger.test.tsx +102 -0
- package/src/Table/__tests__/Table.Expandable.test.tsx +121 -0
- package/src/Table/types.ts +125 -0
- package/src/Tabs/README.md +4 -4
- package/src/Tabs/Tabs.tsx +5 -4
- package/src/Tabs/__tests__/Tabs.activation-mode.test.tsx +58 -0
- package/src/Tabs/__tests__/Tabs.basic-rendering.test.tsx +15 -0
- package/src/Tabs/__tests__/Tabs.change-event-callbacks.test.tsx +24 -0
- package/src/Tabs/__tests__/Tabs.disabled-tabs.test.tsx +36 -0
- package/src/Tabs/__tests__/Tabs.lazy-mount.test.tsx +17 -0
- package/src/Tabs/hooks/useTabsContent.ts +5 -0
- package/src/Tabs/hooks/useTabsRoot.ts +29 -5
- package/src/Tabs/hooks/useTabsTrigger.ts +7 -2
- package/src/ToggleGroup/README.md +4 -4
- package/src/Tooltip/README.md +7 -7
- package/src/Tooltip/Tooltip.tsx +2 -2
- package/src/Tooltip/__tests__/Tooltip.basic-rendering.test.tsx +50 -0
- package/src/Tooltip/__tests__/Tooltip.error-handling.test.tsx +37 -0
- package/src/Tooltip/__tests__/Tooltip.escape-hatches.test.tsx +73 -0
- package/src/Tooltip/__tests__/Tooltip.keyboard-interaction.test.tsx +134 -0
- package/src/Tooltip/__tests__/Tooltip.skip-delay.test.tsx +216 -0
- package/src/Tooltip/__tests__/Tooltip.timer-coordination.test.tsx +223 -0
- package/src/Tooltip/hooks/useTooltipContent.ts +3 -0
- package/src/Tooltip/hooks/useTooltipProvider.ts +40 -14
- package/src/Tooltip/hooks/useTooltipRoot.ts +30 -6
- package/src/Tree/README.md +69 -5
- package/src/Tree/Tree.tsx +1 -1
- package/src/Tree/__tests__/Tree.styled-wrapper.test.tsx +159 -0
- package/src/Tree/hooks/useTreePath.ts +33 -1
- package/src/Tree/index.ts +6 -1
- package/src/Tree/utils.ts +46 -4
- package/src/VisuallyHidden/VisuallyHidden.tsx +1 -1
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useTypeahead.ts +95 -0
- package/src/index.ts +6 -0
- package/src/useLocalStorage/__tests__/useLocalStorage.reading.test.tsx +31 -0
- package/src/useLocalStorage/__tests__/useLocalStorage.resilience.test.tsx +97 -0
- package/src/useLocalStorage/__tests__/useLocalStorage.ssr.test.tsx +20 -0
- package/src/useLocalStorage/__tests__/useLocalStorage.writing.test.tsx +72 -0
- package/src/useLocalStorage/index.ts +1 -0
- package/src/useLocalStorage/useLocalStorage.ts +140 -0
- package/src/useMediaQuery/README.md +66 -0
- package/src/useMediaQuery/__tests__/useMediaQuery.matching.test.tsx +42 -0
- package/src/useMediaQuery/__tests__/useMediaQuery.ssr.test.tsx +19 -0
- package/src/useMediaQuery/__tests__/useMediaQuery.subscription.test.tsx +89 -0
- package/src/useMediaQuery/index.ts +1 -0
- package/src/useMediaQuery/useMediaQuery.ts +49 -0
- package/src/usePagination/README.md +170 -0
- package/src/usePagination/__tests__/usePagination.navigation.test.tsx +109 -0
- package/src/usePagination/__tests__/usePagination.page-count.test.tsx +56 -0
- package/src/usePagination/__tests__/usePagination.page-state.test.tsx +87 -0
- package/src/usePagination/__tests__/usePagination.range.test.tsx +82 -0
- package/src/usePagination/__tests__/usePagination.slice.test.tsx +59 -0
- package/src/usePagination/__tests__/usePagination.windowing.test.tsx +68 -0
- package/src/usePagination/index.ts +1 -0
- package/src/usePagination/usePagination.ts +292 -0
- package/src/utils/getKeyToActionMap.ts +1 -1
package/README.md
CHANGED
|
@@ -28,6 +28,7 @@ import { Tabs } from "@primitiv-ui/react";
|
|
|
28
28
|
| [Checkbox](src/Checkbox/README.md) | A real, visually-hidden native `<input type="checkbox">` in a `<label>` box, so it participates in forms; the platform tri-state (`indeterminate` → `aria-checked="mixed"`, `:indeterminate`), controlled/uncontrolled state, `Indicator` mark, and `asChild` on the indicator |
|
|
29
29
|
| [CheckboxCard](src/CheckboxCard/README.md) | WAI-ARIA Checkbox pattern in a card/tile layout — the entire card surface is the interactive element, with tri-state support, controlled/uncontrolled state, `Indicator` slot, `disabled` with `data-disabled` styling hook, and `asChild` composition |
|
|
30
30
|
| [Collapsible](src/Collapsible/README.md) | Single-panel show/hide — boolean controlled/uncontrolled state, `disabled`, `asChild` composition with explicit Enter/Space handling, `forceMount` for CSS-Grid-driven animation, and `TriggerIcon` slot |
|
|
31
|
+
| [Combobox](src/Combobox/README.md) | WAI-ARIA APG Combobox pattern — an editable text field with a filtered popup listbox, using the same **virtual-focus** cursor as `Listbox` (`aria-activedescendant` on the input, `data-highlighted` on the item) so DOM focus never leaves the field; filtering is deliberately **consumer-owned** (no `filter` prop — `onQueryChange` reports keystrokes and you render the matches), the input holds the live query while open and resets from the committed value on Enter, Escape **or a click outside** — the panel is a `popover="auto"`, so the browser supplies both the top layer and light dismiss rather than a hand-rolled outside-click listener, and all three gestures land in the same state; **emptying the field clears the value too** (`onValueChange("")`), so a reset can never resurrect a label the user just deleted; arrow keys seed the cursor on the first/last item, an `Empty` part keeps the panel's height while nothing matches, controlled/uncontrolled `open` and `value`, and `asChild` on every part |
|
|
31
32
|
| [ContextMenu](src/ContextMenu/README.md) | WAI-ARIA Menu pattern for right-click / long-press / context-key menus — native HTML `popover` compound component positioned at the cursor, with nested submenus, checkbox and radio items, `ItemIndicator` icon slot, groups and labels, keyboard navigation, typeahead, disabled-item skipping, `onSelect` escape hatch, and `asChild` composition |
|
|
32
33
|
| [DirectionProvider](src/DirectionProvider/README.md) | Reading-direction context — broadcasts `ltr`/`rtl` to descendants and exposes the `useDirection` hook so direction-aware components inherit `dir` instead of each being passed it explicitly; renders no DOM |
|
|
33
34
|
| [Divider](src/Divider/README.md) | WAI-ARIA separator role — horizontal or vertical content separator, with semantic and decorative use |
|
|
@@ -38,6 +39,7 @@ import { Tabs } from "@primitiv-ui/react";
|
|
|
38
39
|
| [Fieldset](src/Fieldset/README.md) | Headless form-control grouping — a stateless `<fieldset>` / `<legend>` compound with an implicit `group` role, a legend-supplied accessible name, and `disabled` (cascading to every nested control) with a `data-disabled` styling hook |
|
|
39
40
|
| [Input](src/Input/README.md) | Headless single-line text input — native `<input>` with `type="text"` default, full attribute pass-through (works directly with `react-hook-form`'s `register` spread), ref forwarding, native constraint validation, `disabled` with `data-disabled` styling hook, and `asChild` composition |
|
|
40
41
|
| [InputGroup](src/InputGroup/README.md) | Stateless frame for `Input` (or any framed control) with named leading and trailing adornment slots — decorative icons, currency symbols, clear buttons, password-reveal toggles; `data-input-group` / `data-input-group-adornment` styling hooks, CSS-driven disabled / invalid / focus-within state via `:has()`, and `asChild` composition on every part |
|
|
42
|
+
| [Listbox](src/Listbox/README.md) | WAI-ARIA APG Listbox pattern — a persistently-visible option list using a **virtual-focus** cursor (`aria-activedescendant` on the root, `data-highlighted` on the option) rather than a roving tabstop, so an external input can hold DOM focus and drive the list; single/multiple selection with APG's Shift+Arrow / Ctrl+Shift+Home-End / Ctrl+A range shortcuts, opt-in `selectionFollowsFocus`, prefix typeahead, groups named by prop or visible `GroupLabel`, disabled options, DOM-order navigation that survives in-place reordering, orientation/RTL, and `asChild` composition on every part |
|
|
41
43
|
| [MillerColumns](src/MillerColumns/README.md) | Miller columns / cascading-lists pattern — a horizontal strip of `role="group"` columns of `role="treeitem"` items, authored by recursive composition with portal-projected child columns, controlled/uncontrolled active path, single-tabstop roving keyboard navigation, disabled items, and `asChild` composition |
|
|
42
44
|
| [Modal](src/Modal/README.md) | WAI-ARIA Modal Dialog pattern — native `<dialog>`-based compound component with Portal, bounding-rect click-outside, controlled/uncontrolled state, escape hatches, imperative API, `asChild`, and `forceMount` animation hooks |
|
|
43
45
|
| [NavigationMenu](src/NavigationMenu/README.md) | ARIA APG Disclosure Navigation Menu pattern — the desktop dropdown site nav: `<nav>` landmark + `<ul>` of entries, one panel open at a time (`""` means closed), hover-intent open with instant switching and a nav-wide close grace, axis/RTL-aware arrow travel that keeps every entry in the tab order (not a roving tabindex), cross-axis arrow to enter a panel, Escape-to-close with focus return, an optional `Viewport` panels portal into so the open one morphs into the next, an optional `Indicator` publishing the open trigger's measured geometry as custom properties, and a `Link` carrying `aria-current` that the composed mobile nav (`Drawer` + `Collapsible`) reuses |
|
|
@@ -51,9 +53,10 @@ import { Tabs } from "@primitiv-ui/react";
|
|
|
51
53
|
| [Select](src/Select/README.md) | Headless wrapper over native `<select>` / `<option>` / `<optgroup>` — controlled/uncontrolled state via `onValueChange`, `Select.Placeholder` with auto-`defaultValue` inference, `Select.Group` for optgroup labelling, `data-disabled` styling hook, `asChild` composition, and Field-aware (opts into `FieldContext` for auto-wired `id` / `aria-describedby` / `aria-invalid` / `disabled` / `required`) |
|
|
52
54
|
| [SkipNav](src/SkipNav/README.md) | WCAG 2.4.1 Bypass Blocks technique — a stateless "skip to main content" link and its `tabIndex=-1` focus target, connected by a shared content id, working with no JavaScript at runtime |
|
|
53
55
|
| [Slider](src/Slider/README.md) | WAI-ARIA Slider & Multi-Thumb Slider pattern — an array-valued range input with any number of independently focusable thumbs, keyboard and pointer dragging, `min`/`max`/`step`, `minStepsBetweenThumbs`, horizontal/vertical orientation, RTL and `inverted` direction, `onValueCommit`, hidden inputs for form submission, `disabled` with `data-disabled` styling hook, and `asChild` composition |
|
|
56
|
+
| [SplitButton](src/SplitButton/README.md) | One primary action plus a menu of related alternatives, bound into a single `role="group"` widget — a composition over `Dropdown` that adds the coordination Dropdown alone can't provide: the group boundary, the menu trigger's accessible name derived from the primary action (so a chevron-only control is never unlabelled), `disabled` propagating from the group to both halves, and `ArrowDown` on the action opening the menu; both halves stay independently tabbable, and `asChild` composition is available on the group and the action |
|
|
54
57
|
| [Status](src/Status/README.md) | WAI-ARIA `status` role — a polite live region for low-priority, non-urgent status messages, announced once the user is idle, with `asChild` composition |
|
|
55
58
|
| [Switch](src/Switch/README.md) | A real, visually-hidden native `<input type="checkbox" role="switch">` in a `<label>` track, so it participates in forms; binary on/off state, always-mounted `Thumb` for CSS-driven sliding (off the input's native `:checked`), controlled/uncontrolled state, `data-disabled` styling hook, and `asChild` on the thumb |
|
|
56
|
-
| [Table](src/Table/README.md) | WAI-ARIA Table pattern — compound component wrapping standard HTML table elements with accessible headers, captions,
|
|
59
|
+
| [Table](src/Table/README.md) | WAI-ARIA Table pattern — compound component wrapping standard HTML table elements with accessible headers, captions, a responsive scroll wrapper, and an opt-in expandable-row disclosure |
|
|
57
60
|
| [Tabs](src/Tabs/README.md) | WAI-ARIA Tabs pattern — compound component with keyboard navigation, controlled/uncontrolled state, and `asChild` composition |
|
|
58
61
|
| [Textarea](src/Textarea/README.md) | Headless multi-line text input — native `<textarea>` with full attribute pass-through, ref forwarding, `disabled` with `data-disabled` styling hook, `asChild` composition for wrapping autosizing implementations, and Field-aware (opts into `FieldContext` for auto-wired `id` / `aria-describedby` / `aria-invalid` / `disabled` / `required`) |
|
|
59
62
|
| [Toggle](src/Toggle/README.md) | WAI-ARIA Button pattern with `aria-pressed` — single stateful toggle button with controlled/uncontrolled state, `data-state` styling hook, and `asChild` composition |
|
|
@@ -62,10 +65,20 @@ import { Tabs } from "@primitiv-ui/react";
|
|
|
62
65
|
| [Tree](src/Tree/README.md) | WAI-ARIA Tree pattern — hierarchical tree view authored by recursive composition with `Branch`/`BranchControl`/`BranchContent`, controlled/uncontrolled expansion, single or multiple selection with Ctrl/Cmd and Shift+click range, `forceMount` for branch animation, single-tabstop roving keyboard navigation, disabled items, and `asChild` composition |
|
|
63
66
|
| [VisuallyHidden](src/VisuallyHidden/README.md) | Screen-reader-only primitive — visually hides its children with the canonical clip styles while keeping them in the accessibility tree, with consumer `style` merging and `asChild` composition |
|
|
64
67
|
|
|
68
|
+
## Hooks
|
|
69
|
+
|
|
70
|
+
| Hook | Description |
|
|
71
|
+
| ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
72
|
+
| [useMediaQuery](src/useMediaQuery/README.md) | Tracks whether a CSS media query currently matches, re-rendering on change — generic, not tied to Primitiv's own breakpoint scale, with a safe `false` default during server rendering |
|
|
73
|
+
| [usePagination](src/usePagination/README.md) | Page state and arithmetic for any paged data source — clamped current page, prev/next/first/last, half-open `startIndex`/`endIndex` slice bounds for client- or server-side data, and the collapsed page range to render |
|
|
74
|
+
|
|
65
75
|
## Internal utilities
|
|
66
76
|
|
|
67
77
|
**`Slot`** is the `asChild` composition utility used internally by components
|
|
68
|
-
such as `Tabs.Trigger`. It is not a public component API.
|
|
78
|
+
such as `Tabs.Trigger`. It is not a public component API. The `hooks/`
|
|
79
|
+
directory (`useCollection`, `useControllableState`, `useRovingTabindex`) is
|
|
80
|
+
similarly internal — shared implementation details of compound components,
|
|
81
|
+
not part of the public API.
|
|
69
82
|
|
|
70
83
|
## Testing
|
|
71
84
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@primitiv-ui/react",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.30",
|
|
4
4
|
"description": "Headless, accessible React components built on the WAI-ARIA authoring patterns. Zero styles ship with this package — bring your own (CSS, Tailwind, CSS-in-JS, design tokens).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -28,7 +28,7 @@ type DecorativeIconProps = { "aria-hidden": string; focusable: string };
|
|
|
28
28
|
* if your icon library returns a fragment.
|
|
29
29
|
*
|
|
30
30
|
* **Renders no host element.** `AccessibleIcon` returns a React fragment
|
|
31
|
-
* (
|
|
31
|
+
* (`<>...</>`), so it adds no DOM node of its own. The icon element and the
|
|
32
32
|
* {@link VisuallyHidden} span are the only nodes emitted.
|
|
33
33
|
*
|
|
34
34
|
* @example Icon-only button
|
|
@@ -57,8 +57,8 @@ import { useAccordionTrigger } from "./hooks/useAccordionTrigger";
|
|
|
57
57
|
* @example Uncontrolled — single open item
|
|
58
58
|
* ```tsx
|
|
59
59
|
* <Accordion.Root defaultValue="item-1">
|
|
60
|
-
* <Accordion.Item value="item-1"
|
|
61
|
-
* <Accordion.Item value="item-2"
|
|
60
|
+
* <Accordion.Item value="item-1">...</Accordion.Item>
|
|
61
|
+
* <Accordion.Item value="item-2">...</Accordion.Item>
|
|
62
62
|
* </Accordion.Root>
|
|
63
63
|
* ```
|
|
64
64
|
*
|
|
@@ -67,8 +67,8 @@ import { useAccordionTrigger } from "./hooks/useAccordionTrigger";
|
|
|
67
67
|
* const [expanded, setExpanded] = useState<string[]>([]);
|
|
68
68
|
*
|
|
69
69
|
* <Accordion.Root multiple value={expanded} onValueChange={setExpanded}>
|
|
70
|
-
* <Accordion.Item value="shipping"
|
|
71
|
-
* <Accordion.Item value="returns"
|
|
70
|
+
* <Accordion.Item value="shipping">...</Accordion.Item>
|
|
71
|
+
* <Accordion.Item value="returns">...</Accordion.Item>
|
|
72
72
|
* </Accordion.Root>
|
|
73
73
|
* ```
|
|
74
74
|
*/
|
|
@@ -277,7 +277,7 @@ AccordionTrigger.displayName = "AccordionTrigger";
|
|
|
277
277
|
|
|
278
278
|
/**
|
|
279
279
|
* The panel that is revealed when the associated `Accordion.Trigger` is
|
|
280
|
-
* activated. Renders a `<div role="region" aria-labelledby="
|
|
280
|
+
* activated. Renders a `<div role="region" aria-labelledby="...">` whose
|
|
281
281
|
* visibility is controlled by the `hidden` attribute.
|
|
282
282
|
*
|
|
283
283
|
* **`forceMount` prop.** By default the panel is removed from visibility with
|
|
@@ -296,7 +296,7 @@ AccordionTrigger.displayName = "AccordionTrigger";
|
|
|
296
296
|
* default, creating an ARIA landmark. Accordions with many items can produce
|
|
297
297
|
* landmark overload in screen readers. Opt out by passing `role={undefined}`:
|
|
298
298
|
* ```tsx
|
|
299
|
-
* <Accordion.Content role={undefined}
|
|
299
|
+
* <Accordion.Content role={undefined}>...</Accordion.Content>
|
|
300
300
|
* ```
|
|
301
301
|
*
|
|
302
302
|
* **Styling hooks.**
|
|
@@ -378,7 +378,7 @@ AccordionContent.displayName = "AccordionContent";
|
|
|
378
378
|
* <Accordion.Trigger>
|
|
379
379
|
* Shipping policy
|
|
380
380
|
* <Accordion.TriggerIcon>
|
|
381
|
-
* <svg
|
|
381
|
+
* <svg ...><path d="..." /></svg>
|
|
382
382
|
* </Accordion.TriggerIcon>
|
|
383
383
|
* </Accordion.Trigger>
|
|
384
384
|
* ```
|
package/src/Accordion/README.md
CHANGED
|
@@ -61,7 +61,7 @@ simultaneously:
|
|
|
61
61
|
|
|
62
62
|
```tsx
|
|
63
63
|
<Accordion.Root multiple defaultValue="shipping">
|
|
64
|
-
|
|
64
|
+
...
|
|
65
65
|
</Accordion.Root>
|
|
66
66
|
```
|
|
67
67
|
|
|
@@ -168,7 +168,7 @@ to invert the arrow-key direction so `ArrowLeft` moves forward and
|
|
|
168
168
|
|
|
169
169
|
```tsx
|
|
170
170
|
<Accordion.Root orientation="horizontal" dir="rtl">
|
|
171
|
-
|
|
171
|
+
...
|
|
172
172
|
</Accordion.Root>
|
|
173
173
|
```
|
|
174
174
|
|
|
@@ -73,14 +73,14 @@ describe("Avatar fallback", () => {
|
|
|
73
73
|
});
|
|
74
74
|
|
|
75
75
|
it("resets the timer when delayMs changes", () => {
|
|
76
|
-
// Arrange — start a 300ms delay
|
|
76
|
+
// Arrange — start a 300ms delay...
|
|
77
77
|
const { rerender } = render(
|
|
78
78
|
<Avatar.Root>
|
|
79
79
|
<Avatar.Fallback delayMs={300}>AL</Avatar.Fallback>
|
|
80
80
|
</Avatar.Root>,
|
|
81
81
|
);
|
|
82
82
|
|
|
83
|
-
// Act —
|
|
83
|
+
// Act — ...then extend it to 1000ms before the first delay elapses. The
|
|
84
84
|
// effect must clear the old timer and start a new one; a stale timer (no
|
|
85
85
|
// cleanup) or a frozen dependency ([] deps) would fire at 300ms.
|
|
86
86
|
rerender(
|
|
@@ -95,7 +95,7 @@ describe("Avatar fallback", () => {
|
|
|
95
95
|
// Assert — still withheld at 300ms because the delay was reset to 1000ms
|
|
96
96
|
expect(screen.queryByText("AL")).not.toBeInTheDocument();
|
|
97
97
|
|
|
98
|
-
//
|
|
98
|
+
// ...and shown once the new delay elapses
|
|
99
99
|
act(() => {
|
|
100
100
|
vi.advanceTimersByTime(700);
|
|
101
101
|
});
|
|
@@ -32,14 +32,14 @@ import {
|
|
|
32
32
|
* @example
|
|
33
33
|
* ```tsx
|
|
34
34
|
* <Breadcrumb.Root>
|
|
35
|
-
* <Breadcrumb.List
|
|
35
|
+
* <Breadcrumb.List>...</Breadcrumb.List>
|
|
36
36
|
* </Breadcrumb.Root>
|
|
37
37
|
* ```
|
|
38
38
|
*
|
|
39
39
|
* @example Custom accessible name
|
|
40
40
|
* ```tsx
|
|
41
41
|
* <Breadcrumb.Root aria-label="You are here">
|
|
42
|
-
* <Breadcrumb.List
|
|
42
|
+
* <Breadcrumb.List>...</Breadcrumb.List>
|
|
43
43
|
* </Breadcrumb.Root>
|
|
44
44
|
* ```
|
|
45
45
|
*/
|
|
@@ -77,9 +77,9 @@ BreadcrumbRoot.displayName = "BreadcrumbRoot";
|
|
|
77
77
|
* @example
|
|
78
78
|
* ```tsx
|
|
79
79
|
* <Breadcrumb.List>
|
|
80
|
-
* <Breadcrumb.Item
|
|
80
|
+
* <Breadcrumb.Item>...</Breadcrumb.Item>
|
|
81
81
|
* <Breadcrumb.Separator />
|
|
82
|
-
* <Breadcrumb.Item
|
|
82
|
+
* <Breadcrumb.Item>...</Breadcrumb.Item>
|
|
83
83
|
* </Breadcrumb.List>
|
|
84
84
|
* ```
|
|
85
85
|
*/
|
|
@@ -132,7 +132,7 @@ BreadcrumbItem.displayName = "BreadcrumbItem";
|
|
|
132
132
|
* **`asChild` composition.** Pass `asChild` to render a consumer-supplied
|
|
133
133
|
* element — typically a routing library's `<Link>` — instead of the native
|
|
134
134
|
* `<a>`, with all of `Breadcrumb.Link`'s props (`href`, event handlers,
|
|
135
|
-
* `className`, `ref`,
|
|
135
|
+
* `className`, `ref`, ...) merged onto it via the {@link Slot} pattern. The
|
|
136
136
|
* child **must** be a single React element that accepts a `ref`; the
|
|
137
137
|
* native `<a>` is dropped entirely rather than wrapping the child.
|
|
138
138
|
*
|
|
@@ -254,7 +254,7 @@ BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
|
|
|
254
254
|
* label) — `aria-hidden="true"` excludes this glyph from accessible-name
|
|
255
255
|
* computation entirely, exactly like {@link BreadcrumbSeparator}.
|
|
256
256
|
*
|
|
257
|
-
* Defaults to a `"
|
|
257
|
+
* Defaults to a `"..."` glyph; pass `children` to use a custom one (an icon,
|
|
258
258
|
* `"···"`, etc.) — whatever is passed replaces the default entirely rather
|
|
259
259
|
* than being appended to it.
|
|
260
260
|
*
|
|
@@ -285,7 +285,7 @@ BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
|
|
|
285
285
|
* ```
|
|
286
286
|
*/
|
|
287
287
|
export function BreadcrumbEllipsis({
|
|
288
|
-
children = "
|
|
288
|
+
children = "...",
|
|
289
289
|
asChild = false,
|
|
290
290
|
...rest
|
|
291
291
|
}: BreadcrumbEllipsisProps): ReactElement {
|
package/src/Breadcrumb/README.md
CHANGED
|
@@ -86,7 +86,7 @@ It defaults to a `"/"` glyph. Pass `children` to use a custom separator:
|
|
|
86
86
|
which crumbs collapse is a styled-surface concern (see the registry `card`
|
|
87
87
|
composite for the same "no data model in the primitive" philosophy). What
|
|
88
88
|
the headless layer provides is `Breadcrumb.Ellipsis`: a purely decorative
|
|
89
|
-
`"
|
|
89
|
+
`"..."` glyph for the trigger's visual content.
|
|
90
90
|
|
|
91
91
|
Compose it with [`Dropdown`](../Dropdown/README.md) to build the actual
|
|
92
92
|
trigger — a real interactive element (a `<button>`, or anything
|
|
@@ -118,7 +118,7 @@ import { Breadcrumb, Dropdown, VisuallyHidden } from "@primitiv-ui/react";
|
|
|
118
118
|
</Breadcrumb.Item>;
|
|
119
119
|
```
|
|
120
120
|
|
|
121
|
-
`Breadcrumb.Ellipsis` defaults to a `"
|
|
121
|
+
`Breadcrumb.Ellipsis` defaults to a `"..."` glyph; pass `children` to use a
|
|
122
122
|
custom one:
|
|
123
123
|
|
|
124
124
|
```tsx
|
|
@@ -17,7 +17,7 @@ describe("Breadcrumb ellipsis", () => {
|
|
|
17
17
|
expect(ellipsis.tagName).toBe("SPAN");
|
|
18
18
|
expect(ellipsis).toHaveAttribute("role", "presentation");
|
|
19
19
|
expect(ellipsis).toHaveAttribute("aria-hidden", "true");
|
|
20
|
-
expect(ellipsis).toHaveTextContent("
|
|
20
|
+
expect(ellipsis).toHaveTextContent("...");
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
it("lets the consumer override the Ellipsis glyph", () => {
|
package/src/Breadcrumb/types.ts
CHANGED
|
@@ -71,7 +71,7 @@ export type BreadcrumbSeparatorProps = ComponentProps<"li">;
|
|
|
71
71
|
* Props for {@link BreadcrumbEllipsis | `Breadcrumb.Ellipsis`} — all native
|
|
72
72
|
* `<span>` attributes plus the `asChild` escape hatch. `role="presentation"`
|
|
73
73
|
* and `aria-hidden="true"` are fixed by the component; `children` defaults
|
|
74
|
-
* to a `"
|
|
74
|
+
* to a `"..."` glyph when omitted (see {@link BreadcrumbEllipsis}'s
|
|
75
75
|
* custom-glyph example).
|
|
76
76
|
*
|
|
77
77
|
* @extends HTMLSpanElement
|
|
@@ -60,7 +60,7 @@ describe("Button styling contract", () => {
|
|
|
60
60
|
);
|
|
61
61
|
|
|
62
62
|
render(<Button disabled>Save</Button>);
|
|
63
|
-
// Present with the documented value when the documented condition holds
|
|
63
|
+
// Present with the documented value when the documented condition holds...
|
|
64
64
|
expect(screen.getByRole("button")).toHaveAttribute(entry.name, entry.value);
|
|
65
65
|
});
|
|
66
66
|
|
|
@@ -68,7 +68,7 @@ import type {
|
|
|
68
68
|
*
|
|
69
69
|
* @example Labelled inline, uncontrolled
|
|
70
70
|
* ```tsx
|
|
71
|
-
* <Carousel.Root ariaLabel="Featured products" defaultPage={0}
|
|
71
|
+
* <Carousel.Root ariaLabel="Featured products" defaultPage={0}>...</Carousel.Root>
|
|
72
72
|
* ```
|
|
73
73
|
*
|
|
74
74
|
* @example Controlled
|
|
@@ -80,14 +80,14 @@ import type {
|
|
|
80
80
|
* page={page}
|
|
81
81
|
* onPageChange={setPage}
|
|
82
82
|
* >
|
|
83
|
-
*
|
|
83
|
+
* ...
|
|
84
84
|
* </Carousel.Root>
|
|
85
85
|
* ```
|
|
86
86
|
*
|
|
87
87
|
* @example Labelled by an existing heading
|
|
88
88
|
* ```tsx
|
|
89
89
|
* <h2 id="promos">Promotions</h2>
|
|
90
|
-
* <Carousel.Root ariaLabelledBy="promos"
|
|
90
|
+
* <Carousel.Root ariaLabelledBy="promos">...</Carousel.Root>
|
|
91
91
|
* ```
|
|
92
92
|
*/
|
|
93
93
|
export const CarouselRoot: ForwardRefExoticComponent<
|
|
@@ -260,7 +260,7 @@ CarouselRoot.displayName = "CarouselRoot";
|
|
|
260
260
|
* ```tsx
|
|
261
261
|
* <Carousel.Root ariaLabel="Featured products">
|
|
262
262
|
* <Carousel.Viewport>
|
|
263
|
-
* <Carousel.Slide
|
|
263
|
+
* <Carousel.Slide>...</Carousel.Slide>
|
|
264
264
|
* </Carousel.Viewport>
|
|
265
265
|
* </Carousel.Root>
|
|
266
266
|
* ```
|
|
@@ -399,12 +399,12 @@ CarouselViewport.displayName = "CarouselViewport";
|
|
|
399
399
|
*
|
|
400
400
|
* @example Override the auto-label
|
|
401
401
|
* ```tsx
|
|
402
|
-
* <Carousel.Slide ariaLabel="Hand-picked for you"
|
|
402
|
+
* <Carousel.Slide ariaLabel="Hand-picked for you">...</Carousel.Slide>
|
|
403
403
|
* ```
|
|
404
404
|
*
|
|
405
405
|
* @example Per-slide snap alignment (overrides the root default)
|
|
406
406
|
* ```tsx
|
|
407
|
-
* <Carousel.Slide snapAlign="end"
|
|
407
|
+
* <Carousel.Slide snapAlign="end">...</Carousel.Slide>
|
|
408
408
|
* ```
|
|
409
409
|
*/
|
|
410
410
|
export function CarouselSlide({
|
package/src/Carousel/README.md
CHANGED
|
@@ -149,7 +149,7 @@ import { Carousel } from "@primitiv-ui/react";
|
|
|
149
149
|
|
|
150
150
|
```tsx
|
|
151
151
|
<h2 id="promos">Promotions</h2>
|
|
152
|
-
<Carousel.Root ariaLabelledBy="promos"
|
|
152
|
+
<Carousel.Root ariaLabelledBy="promos">...</Carousel.Root>
|
|
153
153
|
```
|
|
154
154
|
|
|
155
155
|
The discriminated union on the props type rejects shapes that supply both
|
|
@@ -170,7 +170,7 @@ Slides go inside `Carousel.Viewport`:
|
|
|
170
170
|
```
|
|
171
171
|
|
|
172
172
|
Each `Carousel.Slide` self-registers with the Root, so every slide
|
|
173
|
-
exposes its own `data-index="0"`, `data-index="1"`,
|
|
173
|
+
exposes its own `data-index="0"`, `data-index="1"`, ... and a live
|
|
174
174
|
`data-total` reflecting the current slide count. Add or remove slides at
|
|
175
175
|
runtime and the indices and totals update automatically.
|
|
176
176
|
|
|
@@ -179,7 +179,7 @@ the WAI-ARIA Carousel APG example uses. To override the auto-label with
|
|
|
179
179
|
a more meaningful description, pass `ariaLabel`:
|
|
180
180
|
|
|
181
181
|
```tsx
|
|
182
|
-
<Carousel.Slide ariaLabel="Hand-picked for you"
|
|
182
|
+
<Carousel.Slide ariaLabel="Hand-picked for you">...</Carousel.Slide>
|
|
183
183
|
```
|
|
184
184
|
|
|
185
185
|
The override remains stable as siblings mount and unmount around it.
|
|
@@ -220,7 +220,7 @@ strip), persisting the active page to a URL, or reacting to deep links.
|
|
|
220
220
|
const [page, setPage] = useState(0);
|
|
221
221
|
|
|
222
222
|
<Carousel.Root ariaLabel="Featured products" page={page} onPageChange={setPage}>
|
|
223
|
-
|
|
223
|
+
...
|
|
224
224
|
</Carousel.Root>;
|
|
225
225
|
```
|
|
226
226
|
|
|
@@ -262,7 +262,7 @@ const [open, setOpen] = useState(false);
|
|
|
262
262
|
slidesPerPage={3}
|
|
263
263
|
>
|
|
264
264
|
<Carousel.Viewport>
|
|
265
|
-
<Carousel.Slide
|
|
265
|
+
<Carousel.Slide>...</Carousel.Slide>...
|
|
266
266
|
</Carousel.Viewport>
|
|
267
267
|
</Carousel.Root>
|
|
268
268
|
|
|
@@ -279,7 +279,7 @@ const [open, setOpen] = useState(false);
|
|
|
279
279
|
onPlayingChange={() => {}}
|
|
280
280
|
>
|
|
281
281
|
<Carousel.Viewport>
|
|
282
|
-
<Carousel.Slide
|
|
282
|
+
<Carousel.Slide>...</Carousel.Slide>...
|
|
283
283
|
</Carousel.Viewport>
|
|
284
284
|
<Carousel.Indicators label="Choose slide" />
|
|
285
285
|
</Carousel.Root>
|
|
@@ -303,7 +303,7 @@ exposes an imperative handle via `ref`:
|
|
|
303
303
|
const carouselRef = useRef<CarouselImperativeApi>(null);
|
|
304
304
|
|
|
305
305
|
<Carousel.Root ref={carouselRef} ariaLabel="Featured products">
|
|
306
|
-
|
|
306
|
+
...
|
|
307
307
|
</Carousel.Root>;
|
|
308
308
|
|
|
309
309
|
carouselRef.current?.next();
|
|
@@ -410,7 +410,7 @@ browser snap-correcting after the scroll:
|
|
|
410
410
|
|
|
411
411
|
```tsx
|
|
412
412
|
<Carousel.Root ariaLabel="Gallery" snapAlign="start">
|
|
413
|
-
|
|
413
|
+
...
|
|
414
414
|
</Carousel.Root>
|
|
415
415
|
```
|
|
416
416
|
|
|
@@ -455,7 +455,7 @@ ends, rather than always forcing a rest on one:
|
|
|
455
455
|
|
|
456
456
|
```tsx
|
|
457
457
|
<Carousel.Root ariaLabel="Gallery" snapType="proximity">
|
|
458
|
-
|
|
458
|
+
...
|
|
459
459
|
</Carousel.Root>
|
|
460
460
|
```
|
|
461
461
|
|
|
@@ -472,7 +472,7 @@ axis** instead:
|
|
|
472
472
|
|
|
473
473
|
```tsx
|
|
474
474
|
<Carousel.Root ariaLabel="Featured products" orientation="vertical">
|
|
475
|
-
|
|
475
|
+
...
|
|
476
476
|
</Carousel.Root>
|
|
477
477
|
```
|
|
478
478
|
|
|
@@ -621,13 +621,13 @@ pin DOM `id`s on the rendered sub-components via the `ids` bag on
|
|
|
621
621
|
indicatorGroup: "promo-indicators",
|
|
622
622
|
}}
|
|
623
623
|
>
|
|
624
|
-
|
|
624
|
+
...
|
|
625
625
|
</Carousel.Root>
|
|
626
626
|
```
|
|
627
627
|
|
|
628
628
|
Any keys you omit leave the corresponding element unidentified. A
|
|
629
629
|
direct `id` prop on a sub-component (e.g.
|
|
630
|
-
`<Carousel.NextTrigger id="
|
|
630
|
+
`<Carousel.NextTrigger id="...">`) wins over `ids.*` because it spreads
|
|
631
631
|
last.
|
|
632
632
|
|
|
633
633
|
### Internationalisation
|
|
@@ -649,7 +649,7 @@ accessible names (`"Start automatic slide show"` and
|
|
|
649
649
|
stopSlideshow: "Arrêter le diaporama",
|
|
650
650
|
}}
|
|
651
651
|
>
|
|
652
|
-
|
|
652
|
+
...
|
|
653
653
|
</Carousel.Root>
|
|
654
654
|
```
|
|
655
655
|
|
|
@@ -700,7 +700,7 @@ With `slidesPerPage={3}` and 5 slides:
|
|
|
700
700
|
active).
|
|
701
701
|
|
|
702
702
|
The slide-level `aria-label="N of M"` continues to count individual
|
|
703
|
-
slides (so a 5-slide carousel announces "1 of 5", "2 of 5",
|
|
703
|
+
slides (so a 5-slide carousel announces "1 of 5", "2 of 5", ... even
|
|
704
704
|
when grouped into 3-per-page).
|
|
705
705
|
|
|
706
706
|
Pass a numeric `slidesPerMove` to advance the visible window by an
|
|
@@ -712,7 +712,7 @@ arbitrary slide count per click instead of a full page:
|
|
|
712
712
|
slidesPerPage={3}
|
|
713
713
|
slidesPerMove={1}
|
|
714
714
|
>
|
|
715
|
-
|
|
715
|
+
...
|
|
716
716
|
</Carousel.Root>
|
|
717
717
|
```
|
|
718
718
|
|
|
@@ -969,7 +969,7 @@ const carouselRef = useRef<CarouselImperativeApi>(null);
|
|
|
969
969
|
// type: "dragging.start" | "dragging" | "dragging.end"
|
|
970
970
|
}}
|
|
971
971
|
>
|
|
972
|
-
|
|
972
|
+
...
|
|
973
973
|
</Carousel.Root>;
|
|
974
974
|
|
|
975
975
|
carouselRef.current?.isDragging(); // live snapshot, read on demand
|
|
@@ -997,7 +997,7 @@ keyboard, the wheel, or a mouse drag — fires `Carousel.Root`'s
|
|
|
997
997
|
// source: "keyboard" | "wheel" | "drag"
|
|
998
998
|
}}
|
|
999
999
|
>
|
|
1000
|
-
|
|
1000
|
+
...
|
|
1001
1001
|
</Carousel.Root>;
|
|
1002
1002
|
```
|
|
1003
1003
|
|
|
@@ -1173,7 +1173,7 @@ timer while `playing` is `true`:
|
|
|
1173
1173
|
```tsx
|
|
1174
1174
|
// Default 4000ms cadence
|
|
1175
1175
|
<Carousel.Root ariaLabel="Featured products" autoplay defaultPlaying>
|
|
1176
|
-
|
|
1176
|
+
...
|
|
1177
1177
|
</Carousel.Root>
|
|
1178
1178
|
|
|
1179
1179
|
// Custom delay
|
|
@@ -1182,7 +1182,7 @@ timer while `playing` is `true`:
|
|
|
1182
1182
|
autoplay={{ delay: 6000 }}
|
|
1183
1183
|
defaultPlaying
|
|
1184
1184
|
>
|
|
1185
|
-
|
|
1185
|
+
...
|
|
1186
1186
|
</Carousel.Root>
|
|
1187
1187
|
```
|
|
1188
1188
|
|
|
@@ -1241,7 +1241,7 @@ play/pause toggles, but a consumer reacting to the timer itself in JS
|
|
|
1241
1241
|
// type: "autoplay.start" | "autoplay" | "autoplay.stop"
|
|
1242
1242
|
}}
|
|
1243
1243
|
>
|
|
1244
|
-
|
|
1244
|
+
...
|
|
1245
1245
|
</Carousel.Root>
|
|
1246
1246
|
```
|
|
1247
1247
|
|
|
@@ -1264,14 +1264,14 @@ anatomy part):
|
|
|
1264
1264
|
|
|
1265
1265
|
```tsx
|
|
1266
1266
|
<Carousel.Root ariaLabel="Featured products">
|
|
1267
|
-
<Carousel.Viewport
|
|
1267
|
+
<Carousel.Viewport>...</Carousel.Viewport>
|
|
1268
1268
|
<Carousel.ProgressText />
|
|
1269
1269
|
</Carousel.Root>
|
|
1270
1270
|
```
|
|
1271
1271
|
|
|
1272
1272
|
Renders `"1 of 3"` by default (1-indexed, matching `slideLabel`); pass a
|
|
1273
1273
|
`progressText` formatter through `translations` (a
|
|
1274
|
-
`({ page, totalPages }) =>
|
|
1274
|
+
`({ page, totalPages }) => ...` function) on the Root to customise the
|
|
1275
1275
|
format, or `children` on `Carousel.ProgressText`
|
|
1276
1276
|
itself to render something else entirely (an icon alongside the
|
|
1277
1277
|
count, say) instead of the computed text. `Carousel.ProgressText`
|
|
@@ -505,11 +505,11 @@ describe("Carousel infinite — transform engine", () => {
|
|
|
505
505
|
await user.click(getByRole("button", { name: "Next" }));
|
|
506
506
|
|
|
507
507
|
// 3 → 0 is +1 forward: offset 300 → 400 (onto the clone of slide 0), a single
|
|
508
|
-
// step onward, not a rewind. The glide animates to the clone
|
|
508
|
+
// step onward, not a rewind. The glide animates to the clone...
|
|
509
509
|
expect(track!.style.transform).toBe("translate(-400px, 0px)");
|
|
510
510
|
expect(track!.style.transition).toContain("transform");
|
|
511
511
|
|
|
512
|
-
//
|
|
512
|
+
// ...and once it settles the track re-bases by one period to the REAL slide 0
|
|
513
513
|
// at the identical pixels (offset 0), instantly and invisibly.
|
|
514
514
|
fireEnd(track!);
|
|
515
515
|
expect(track!.style.transform).toBe("translate(0px, 0px)");
|
|
@@ -768,7 +768,7 @@ describe("Carousel infinite — transform engine", () => {
|
|
|
768
768
|
|
|
769
769
|
await user.click(getByRole("button", { name: "Next" }));
|
|
770
770
|
// Offset 100 is already within [0, 400): the settle is a no-op, the transform
|
|
771
|
-
// stays put (and the transition flips to none on the instant no-op repaint
|
|
771
|
+
// stays put (and the transition flips to none on the instant no-op repaint...
|
|
772
772
|
// which doesn't run — the guard returns early, so the transition is untouched).
|
|
773
773
|
expect(track!.style.transform).toBe("translate(-100px, 0px)");
|
|
774
774
|
fireEnd(track!);
|
|
@@ -848,9 +848,9 @@ describe("Carousel infinite — transform engine", () => {
|
|
|
848
848
|
await user.click(getByRole("button", { name: "Next" })); // page 2 (offset 400)
|
|
849
849
|
await user.click(getByRole("button", { name: "Previous" })); // back to page 1
|
|
850
850
|
|
|
851
|
-
// Page 2 → 1: the track glides back one page (offset 400 → 200)
|
|
851
|
+
// Page 2 → 1: the track glides back one page (offset 400 → 200)...
|
|
852
852
|
expect(track!.style.transform).toBe("translate(-200px, 0px)");
|
|
853
|
-
//
|
|
853
|
+
// ...and no real slide carries a transform of its own.
|
|
854
854
|
for (const slide of track!.querySelectorAll<HTMLElement>(
|
|
855
855
|
"[data-carousel-slide][data-index]",
|
|
856
856
|
)) {
|
|
@@ -209,7 +209,7 @@ describe("Carousel IntersectionObserver fallback + isInView", () => {
|
|
|
209
209
|
]);
|
|
210
210
|
});
|
|
211
211
|
|
|
212
|
-
// The page stays put — scrollsnapchange owns it
|
|
212
|
+
// The page stays put — scrollsnapchange owns it...
|
|
213
213
|
expect(screen.getByTestId("slide-0")).toHaveAttribute(
|
|
214
214
|
"data-state",
|
|
215
215
|
"active",
|
|
@@ -218,7 +218,7 @@ describe("Carousel IntersectionObserver fallback + isInView", () => {
|
|
|
218
218
|
"data-state",
|
|
219
219
|
"inactive",
|
|
220
220
|
);
|
|
221
|
-
//
|
|
221
|
+
// ...but isInView still reflects what the observer reported.
|
|
222
222
|
expect(ref.current!.isInView(2)).toBe(true);
|
|
223
223
|
} finally {
|
|
224
224
|
delete (window as unknown as { onscrollsnapchange?: unknown })
|
|
@@ -272,7 +272,7 @@ describe("Carousel numeric slidesPerMove", () => {
|
|
|
272
272
|
|
|
273
273
|
await user.click(screen.getByRole("button", { name: "Next" }));
|
|
274
274
|
|
|
275
|
-
// Clamped move=2 → window [2,3], not the [3
|
|
275
|
+
// Clamped move=2 → window [2,3], not the [3,...] a move of 3 would give.
|
|
276
276
|
expect(screen.getByTestId("slide-2")).toHaveAttribute(
|
|
277
277
|
"data-state",
|
|
278
278
|
"active",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
export function shortestStep(from: number, to: number, count: number): number {
|
|
15
15
|
if (count <= 0) return 0;
|
|
16
|
-
const forward = (((to - from) % count) + count) % count; // 0
|
|
16
|
+
const forward = (((to - from) % count) + count) % count; // 0 ... count-1
|
|
17
17
|
return forward * 2 > count ? forward - count : forward;
|
|
18
18
|
}
|
|
19
19
|
|
package/src/Carousel/types.ts
CHANGED
|
@@ -627,7 +627,7 @@ export type CarouselSlideProps = Omit<ComponentProps<"div">, "aria-label"> & {
|
|
|
627
627
|
/** Props for `Carousel.NextTrigger` — the button that advances one page; native `<button>` props plus `asChild`. */
|
|
628
628
|
export type CarouselNextTriggerProps = ComponentProps<"button"> & {
|
|
629
629
|
/** Render the child element instead of the default `<button>`. All
|
|
630
|
-
* trigger props (onClick, disabled, ids.nextTrigger,
|
|
630
|
+
* trigger props (onClick, disabled, ids.nextTrigger, ...) are merged
|
|
631
631
|
* onto the child via `Slot`. The child must accept a `ref`. */
|
|
632
632
|
asChild?: boolean;
|
|
633
633
|
};
|
|
@@ -32,7 +32,7 @@ function dataStateOf(checked: CheckedState) {
|
|
|
32
32
|
*
|
|
33
33
|
* Props routing: `className` / `style` style the **box** (the `<label>` you
|
|
34
34
|
* see — the input is hidden); every other prop (`name`, `value`, `id`,
|
|
35
|
-
* `aria-*`, `required`, `disabled`, `ref`,
|
|
35
|
+
* `aria-*`, `required`, `disabled`, `ref`, ...) spreads onto the input, because
|
|
36
36
|
* semantically the Root *is* the checkbox.
|
|
37
37
|
*
|
|
38
38
|
* Supports two state modes, statically discriminated at the type level:
|
|
@@ -63,7 +63,7 @@ function dataStateOf(checked: CheckedState) {
|
|
|
63
63
|
* ```tsx
|
|
64
64
|
* const [checked, setChecked] = useState<CheckedState>("indeterminate");
|
|
65
65
|
*
|
|
66
|
-
* <Checkbox.Root checked={checked} onCheckedChange={setChecked} aria-label="
|
|
66
|
+
* <Checkbox.Root checked={checked} onCheckedChange={setChecked} aria-label="...">
|
|
67
67
|
* <Checkbox.Indicator />
|
|
68
68
|
* </Checkbox.Root>
|
|
69
69
|
* ```
|
|
@@ -61,7 +61,7 @@ function dataStateOf(checked: CheckedState) {
|
|
|
61
61
|
* ```tsx
|
|
62
62
|
* const [enabled, setEnabled] = useState<CheckedState>(false);
|
|
63
63
|
*
|
|
64
|
-
* <CheckboxCard.Root checked={enabled} onCheckedChange={setEnabled} aria-label="
|
|
64
|
+
* <CheckboxCard.Root checked={enabled} onCheckedChange={setEnabled} aria-label="...">
|
|
65
65
|
* <CheckboxCard.Indicator />
|
|
66
66
|
* Feature name
|
|
67
67
|
* </CheckboxCard.Root>
|
|
@@ -46,7 +46,7 @@ Pass `checked` and `onCheckedChange` together. The parent owns the value.
|
|
|
46
46
|
```tsx
|
|
47
47
|
const [enabled, setEnabled] = useState<CheckedState>(false);
|
|
48
48
|
|
|
49
|
-
<CheckboxCard.Root checked={enabled} onCheckedChange={setEnabled} aria-label="
|
|
49
|
+
<CheckboxCard.Root checked={enabled} onCheckedChange={setEnabled} aria-label="...">
|
|
50
50
|
<CheckboxCard.Indicator />
|
|
51
51
|
Dark mode
|
|
52
52
|
</CheckboxCard.Root>
|