@primitiv-ui/react 0.1.29 → 0.1.31
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/RadioCard/README.md +2 -1
- package/src/RadioCard/RadioCard.tsx +5 -2
- package/src/RadioCard/__tests__/RadioCard.disabled-items.test.tsx +23 -0
- 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/src/Table/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
ButtonHTMLAttributes,
|
|
2
3
|
HTMLAttributes,
|
|
3
4
|
ReactNode,
|
|
4
5
|
TableHTMLAttributes,
|
|
@@ -88,6 +89,130 @@ export type TableScrollAreaProps = {
|
|
|
88
89
|
style?: HTMLAttributes<HTMLDivElement>["style"];
|
|
89
90
|
} & Omit<HTMLAttributes<HTMLDivElement>, "style">;
|
|
90
91
|
|
|
92
|
+
/**
|
|
93
|
+
* The value shared by {@link TableExpandable | `Table.Expandable`} with its
|
|
94
|
+
* {@link TableExpandTrigger | `Table.ExpandTrigger`} and
|
|
95
|
+
* {@link TableDetailRow | `Table.DetailRow`} children.
|
|
96
|
+
*
|
|
97
|
+
* @internal
|
|
98
|
+
*/
|
|
99
|
+
export type TableExpandableContextValue = {
|
|
100
|
+
/** Whether the detail row is currently revealed. */
|
|
101
|
+
expanded: boolean;
|
|
102
|
+
/** The id wired to the detail row's `<tr>` and the trigger's `aria-controls`. */
|
|
103
|
+
detailId: string;
|
|
104
|
+
/** Flips `expanded` and notifies `onExpandedChange`. */
|
|
105
|
+
toggle: () => void;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Props for {@link TableExpandable | `Table.Expandable`} — the pairing
|
|
110
|
+
* provider for one expandable row and its detail row.
|
|
111
|
+
*/
|
|
112
|
+
export type TableExpandableProps = {
|
|
113
|
+
/** The parent {@link TableRow | `Table.Row`} and its
|
|
114
|
+
* {@link TableDetailRow | `Table.DetailRow`}, as siblings. */
|
|
115
|
+
children: ReactNode;
|
|
116
|
+
/**
|
|
117
|
+
* Whether the detail row is revealed. Pass this (with
|
|
118
|
+
* `onExpandedChange`) to drive expansion from outside — which is what an
|
|
119
|
+
* external table engine such as TanStack Table does.
|
|
120
|
+
*/
|
|
121
|
+
expanded?: boolean;
|
|
122
|
+
/**
|
|
123
|
+
* Initial expanded state when running uncontrolled. Ignored when
|
|
124
|
+
* `expanded` is supplied.
|
|
125
|
+
* @default false
|
|
126
|
+
*/
|
|
127
|
+
defaultExpanded?: boolean;
|
|
128
|
+
/** Called with the next expanded state whenever the trigger is activated. */
|
|
129
|
+
onExpandedChange?: (expanded: boolean) => void;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Props for {@link TableExpandTrigger | `Table.ExpandTrigger`} — all
|
|
134
|
+
* `ButtonHTMLAttributes` on the native `<button>`.
|
|
135
|
+
*
|
|
136
|
+
* `aria-expanded`, `aria-controls` and `type` are owned by the component and
|
|
137
|
+
* therefore `Omit`-ted: they are derived from the surrounding
|
|
138
|
+
* {@link TableExpandable | `Table.Expandable`} and must not be overridden, or
|
|
139
|
+
* the relationship the pair exists to express would break.
|
|
140
|
+
*
|
|
141
|
+
* @extends HTMLButtonElement
|
|
142
|
+
*/
|
|
143
|
+
export type TableExpandTriggerProps = {
|
|
144
|
+
/**
|
|
145
|
+
* Render a consumer-supplied control instead of the native `<button>`, with
|
|
146
|
+
* this part's `aria-expanded`, `aria-controls`, `data-state` and click
|
|
147
|
+
* handling merged onto it via the {@link Slot} pattern.
|
|
148
|
+
*
|
|
149
|
+
* This is how a styled layer composes a real Button rather than re-creating
|
|
150
|
+
* one: the registry `data-table` renders a ghost Button through it, matching
|
|
151
|
+
* the Figma set, whose Expand Trigger is an Icon Button instance rather than a
|
|
152
|
+
* drawn glyph. The child must be a single element that accepts a `ref`, and it
|
|
153
|
+
* should be a `<button>` — the wiring assumes a control.
|
|
154
|
+
* @default false
|
|
155
|
+
*/
|
|
156
|
+
asChild?: boolean;
|
|
157
|
+
} & Omit<
|
|
158
|
+
ButtonHTMLAttributes<HTMLButtonElement>,
|
|
159
|
+
"aria-expanded" | "aria-controls" | "type"
|
|
160
|
+
>;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Props for {@link TableDetailRow | `Table.DetailRow`} — a required `colSpan`
|
|
164
|
+
* plus all `HTMLAttributes` on the native `<tr>`.
|
|
165
|
+
*
|
|
166
|
+
* `id` is component-owned (it is the target of the trigger's `aria-controls`)
|
|
167
|
+
* and so is `Omit`-ted.
|
|
168
|
+
*
|
|
169
|
+
* @extends HTMLTableRowElement
|
|
170
|
+
*/
|
|
171
|
+
export type TableDetailRowProps = {
|
|
172
|
+
/** The detail panel's content. */
|
|
173
|
+
children?: ReactNode;
|
|
174
|
+
/**
|
|
175
|
+
* How many columns the panel spans — **every** column the table renders,
|
|
176
|
+
* control columns included.
|
|
177
|
+
*
|
|
178
|
+
* Deliberately a required prop rather than a value counted from context:
|
|
179
|
+
* counting would mean the table registering its own columns, and an
|
|
180
|
+
* external table engine already knows the number (TanStack Table:
|
|
181
|
+
* `table.getVisibleLeafColumns().length`).
|
|
182
|
+
*/
|
|
183
|
+
colSpan: number;
|
|
184
|
+
/**
|
|
185
|
+
* How many leading columns to leave empty before the panel starts — the
|
|
186
|
+
* control columns (select, expand) a row renders in front of its data.
|
|
187
|
+
*
|
|
188
|
+
* The panel then spans the columns that remain, so `colSpan` keeps its
|
|
189
|
+
* meaning: the table's full column count, gutter included.
|
|
190
|
+
*
|
|
191
|
+
* This is how a panel aligns with the first **data** column instead of the
|
|
192
|
+
* table's edge (a panel starting under the checkbox reads as a new section of
|
|
193
|
+
* the table rather than an opened row). Alignment by empty cell rather than by
|
|
194
|
+
* a CSS indent is deliberate: the browser's own table layout resolves the
|
|
195
|
+
* width, so it stays exact across every size, density and control-column set
|
|
196
|
+
* with nothing measured and no arithmetic to drift.
|
|
197
|
+
* @default 0
|
|
198
|
+
*/
|
|
199
|
+
gutter?: number;
|
|
200
|
+
/**
|
|
201
|
+
* Keep the row mounted while collapsed so a CSS transition can run.
|
|
202
|
+
*
|
|
203
|
+
* Off by default, and that default matters more here than it does on a
|
|
204
|
+
* `<div>`-based disclosure: a mounted row is still a **row**, so assistive
|
|
205
|
+
* technology counts it, and a table of 24 rows with 24 force-mounted
|
|
206
|
+
* detail rows announces 48. Opt in only when you are animating, and note
|
|
207
|
+
* that the collapsed row remains in the accessibility tree while you do.
|
|
208
|
+
* @default false
|
|
209
|
+
*/
|
|
210
|
+
forceMount?: boolean;
|
|
211
|
+
/** Passed through to the `<td>` that spans the row, for styling the cell
|
|
212
|
+
* itself rather than the row. */
|
|
213
|
+
cellProps?: TdHTMLAttributes<HTMLTableCellElement>;
|
|
214
|
+
} & Omit<HTMLAttributes<HTMLTableRowElement>, "id" | "children">;
|
|
215
|
+
|
|
91
216
|
/**
|
|
92
217
|
* Props for {@link TableCaption | `Table.Caption`} — a required `children`,
|
|
93
218
|
* an optional `captionSide`, plus all `HTMLAttributes` on the native
|
package/src/Tabs/README.md
CHANGED
|
@@ -11,8 +11,8 @@ import { Tabs } from "@primitiv-ui/react";
|
|
|
11
11
|
<Tabs.Trigger value="overview">Overview</Tabs.Trigger>
|
|
12
12
|
<Tabs.Trigger value="settings">Settings</Tabs.Trigger>
|
|
13
13
|
</Tabs.List>
|
|
14
|
-
<Tabs.Content value="overview">Dashboard
|
|
15
|
-
<Tabs.Content value="settings">Preferences
|
|
14
|
+
<Tabs.Content value="overview">Dashboard...</Tabs.Content>
|
|
15
|
+
<Tabs.Content value="settings">Preferences...</Tabs.Content>
|
|
16
16
|
</Tabs.Root>;
|
|
17
17
|
```
|
|
18
18
|
|
|
@@ -63,7 +63,7 @@ trigger and panel is always present.
|
|
|
63
63
|
|
|
64
64
|
```tsx
|
|
65
65
|
<Tabs.Root defaultValue="overview" lazyMount>
|
|
66
|
-
|
|
66
|
+
...
|
|
67
67
|
</Tabs.Root>
|
|
68
68
|
```
|
|
69
69
|
|
|
@@ -76,7 +76,7 @@ position must be computed while the panel has real dimensions.
|
|
|
76
76
|
```tsx
|
|
77
77
|
const ref = useRef<TabsImperativeApi>(null);
|
|
78
78
|
<Tabs.Root ref={ref} defaultValue="a">
|
|
79
|
-
|
|
79
|
+
...
|
|
80
80
|
</Tabs.Root>;
|
|
81
81
|
ref.current?.setActiveTab("b");
|
|
82
82
|
```
|
package/src/Tabs/Tabs.tsx
CHANGED
|
@@ -75,8 +75,8 @@ import type {
|
|
|
75
75
|
* <Tabs.Trigger value="overview">Overview</Tabs.Trigger>
|
|
76
76
|
* <Tabs.Trigger value="settings">Settings</Tabs.Trigger>
|
|
77
77
|
* </Tabs.List>
|
|
78
|
-
* <Tabs.Content value="overview">Dashboard
|
|
79
|
-
* <Tabs.Content value="settings">Preferences
|
|
78
|
+
* <Tabs.Content value="overview">Dashboard...</Tabs.Content>
|
|
79
|
+
* <Tabs.Content value="settings">Preferences...</Tabs.Content>
|
|
80
80
|
* </Tabs.Root>
|
|
81
81
|
* ```
|
|
82
82
|
*
|
|
@@ -89,7 +89,7 @@ import type {
|
|
|
89
89
|
* onValueChange={setTab}
|
|
90
90
|
* onChange={({ name, index }) => track("tab_view", { name, index })}
|
|
91
91
|
* >
|
|
92
|
-
*
|
|
92
|
+
* ...
|
|
93
93
|
* </Tabs.Root>
|
|
94
94
|
* ```
|
|
95
95
|
*
|
|
@@ -98,7 +98,7 @@ import type {
|
|
|
98
98
|
* const ref = useRef<TabsImperativeApi>(null);
|
|
99
99
|
*
|
|
100
100
|
* <Tabs.Root ref={ref} orientation="vertical" defaultValue="one">
|
|
101
|
-
*
|
|
101
|
+
* ...
|
|
102
102
|
* </Tabs.Root>
|
|
103
103
|
*
|
|
104
104
|
* <button onClick={() => ref.current?.setActiveTab("two")}>Go to two</button>
|
|
@@ -148,6 +148,7 @@ export const TabsRoot: ForwardRefExoticComponent<
|
|
|
148
148
|
);
|
|
149
149
|
});
|
|
150
150
|
|
|
151
|
+
// Stryker disable next-line StringLiteral: overwritten by the compound alias — an equivalent mutant.
|
|
151
152
|
TabsRoot.displayName = "TabsRoot";
|
|
152
153
|
|
|
153
154
|
/**
|
|
@@ -28,6 +28,64 @@ describe("Activation mode tests", () => {
|
|
|
28
28
|
expect(secondTabPanel).not.toHaveAttribute("hidden");
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
+
it("does not activate a tab on arrow navigation in manual mode", async () => {
|
|
32
|
+
// Directly asserts the manual guard: arrow moves focus without activating,
|
|
33
|
+
// so Tab 1 stays selected and Tab 2 does not.
|
|
34
|
+
const user = userEvent.setup();
|
|
35
|
+
render(
|
|
36
|
+
<Tabs.Root defaultValue="tab1" activationMode="manual">
|
|
37
|
+
<Tabs.List label="Test tabs">
|
|
38
|
+
<Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
|
|
39
|
+
<Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
|
|
40
|
+
</Tabs.List>
|
|
41
|
+
<Tabs.Content value="tab1">Content 1</Tabs.Content>
|
|
42
|
+
<Tabs.Content value="tab2">Content 2</Tabs.Content>
|
|
43
|
+
</Tabs.Root>,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
await user.tab();
|
|
47
|
+
await user.keyboard("{ArrowRight}");
|
|
48
|
+
|
|
49
|
+
expect(screen.getByRole("tab", { name: "Tab 2" })).toHaveAttribute(
|
|
50
|
+
"aria-selected",
|
|
51
|
+
"false",
|
|
52
|
+
);
|
|
53
|
+
expect(screen.getByRole("tab", { name: "Tab 1" })).toHaveAttribute(
|
|
54
|
+
"aria-selected",
|
|
55
|
+
"true",
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("activates a non-native asChild tab via the roving Enter handler (manual)", async () => {
|
|
60
|
+
// A plain <div> fires no synthetic click on Enter, so activation can only
|
|
61
|
+
// come from the roving hook's includeActivate mapping — the guarantee a
|
|
62
|
+
// native <button> would mask.
|
|
63
|
+
const user = userEvent.setup();
|
|
64
|
+
render(
|
|
65
|
+
<Tabs.Root defaultValue="tab1" activationMode="manual">
|
|
66
|
+
<Tabs.List label="Test tabs">
|
|
67
|
+
<Tabs.Trigger asChild value="tab1">
|
|
68
|
+
<div>Tab 1</div>
|
|
69
|
+
</Tabs.Trigger>
|
|
70
|
+
<Tabs.Trigger asChild value="tab2">
|
|
71
|
+
<div>Tab 2</div>
|
|
72
|
+
</Tabs.Trigger>
|
|
73
|
+
</Tabs.List>
|
|
74
|
+
<Tabs.Content value="tab1">Content 1</Tabs.Content>
|
|
75
|
+
<Tabs.Content value="tab2">Content 2</Tabs.Content>
|
|
76
|
+
</Tabs.Root>,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
await user.tab();
|
|
80
|
+
await user.keyboard("{ArrowRight}");
|
|
81
|
+
await user.keyboard("{Enter}");
|
|
82
|
+
|
|
83
|
+
expect(screen.getByRole("tab", { name: "Tab 2" })).toHaveAttribute(
|
|
84
|
+
"aria-selected",
|
|
85
|
+
"true",
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
31
89
|
it("should not activate the second tab when focusing its tab in manual mode", async () => {
|
|
32
90
|
// Arrange
|
|
33
91
|
const user = userEvent.setup();
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import { render, screen } from "@testing-library/react";
|
|
2
2
|
|
|
3
3
|
import { Tabs, TabsOrientation } from "../index.ts";
|
|
4
|
+
import { TabsContext } from "../TabsContext";
|
|
5
|
+
|
|
6
|
+
describe("Tabs displayNames", () => {
|
|
7
|
+
it("sets a displayName on the compound, each sub-component, and the context", () => {
|
|
8
|
+
// Assert — empty displayNames would render each as anonymous in DevTools.
|
|
9
|
+
// `Tabs`, `Tabs.Root`, and the underlying component are one object
|
|
10
|
+
// (Object.assign compound), so the compound's "Tabs" alias is the
|
|
11
|
+
// observable Root displayName; the sub-components are distinct objects.
|
|
12
|
+
expect(Tabs.displayName).toBe("Tabs");
|
|
13
|
+
expect(Tabs.List.displayName).toBe("TabsList");
|
|
14
|
+
expect(Tabs.Trigger.displayName).toBe("TabsTrigger");
|
|
15
|
+
expect(Tabs.Content.displayName).toBe("TabsContent");
|
|
16
|
+
expect(TabsContext.displayName).toBe("TabsContext");
|
|
17
|
+
});
|
|
18
|
+
});
|
|
4
19
|
|
|
5
20
|
describe("Tabs basic rendering tests", () => {
|
|
6
21
|
describe("Tabs.Root", () => {
|
|
@@ -110,6 +110,30 @@ describe('Tabs change event callbacks', () => {
|
|
|
110
110
|
expect(handleChange).not.toHaveBeenCalled();
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
+
it('does not re-fire onChange when re-clicking a tab activated after mount', async () => {
|
|
114
|
+
// No defaultValue: the guard reads an activeValue that changes after the
|
|
115
|
+
// first click, so a stale activateTab closure would re-fire on the second.
|
|
116
|
+
const user = userEvent.setup();
|
|
117
|
+
const handleChange = vi.fn();
|
|
118
|
+
|
|
119
|
+
render(
|
|
120
|
+
<Tabs.Root onChange={handleChange}>
|
|
121
|
+
<Tabs.List label="Test tabs">
|
|
122
|
+
<Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
|
|
123
|
+
<Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
|
|
124
|
+
</Tabs.List>
|
|
125
|
+
<Tabs.Content value="tab1">Content 1</Tabs.Content>
|
|
126
|
+
<Tabs.Content value="tab2">Content 2</Tabs.Content>
|
|
127
|
+
</Tabs.Root>,
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const tab2 = screen.getByRole('tab', { name: 'Tab 2' });
|
|
131
|
+
await user.click(tab2);
|
|
132
|
+
await user.click(tab2);
|
|
133
|
+
|
|
134
|
+
expect(handleChange).toHaveBeenCalledTimes(1);
|
|
135
|
+
});
|
|
136
|
+
|
|
113
137
|
it('should not call onValueChange if clicking already active tab in controlled mode', async () => {
|
|
114
138
|
const user = userEvent.setup();
|
|
115
139
|
const handleValueChange = vi.fn();
|
|
@@ -2,6 +2,42 @@ import { render, screen } from "@testing-library/react";
|
|
|
2
2
|
import { Tabs } from "../Tabs";
|
|
3
3
|
import userEvent from "@testing-library/user-event";
|
|
4
4
|
|
|
5
|
+
describe("Tabs disabled re-registration", () => {
|
|
6
|
+
it("re-registers a trigger's disabled flag when it changes after mount", async () => {
|
|
7
|
+
// Arrange — automatic mode: arrowing onto a tab activates it unless the
|
|
8
|
+
// trigger is registered as disabled. Tab 2 starts enabled, then disables.
|
|
9
|
+
const user = userEvent.setup();
|
|
10
|
+
function Fixture({ tab2Disabled }: { tab2Disabled: boolean }) {
|
|
11
|
+
return (
|
|
12
|
+
<Tabs.Root defaultValue="tab1">
|
|
13
|
+
<Tabs.List label="Test tabs">
|
|
14
|
+
<Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
|
|
15
|
+
<Tabs.Trigger value="tab2" disabled={tab2Disabled}>
|
|
16
|
+
Tab 2
|
|
17
|
+
</Tabs.Trigger>
|
|
18
|
+
</Tabs.List>
|
|
19
|
+
<Tabs.Content value="tab1">Content 1</Tabs.Content>
|
|
20
|
+
<Tabs.Content value="tab2">Content 2</Tabs.Content>
|
|
21
|
+
</Tabs.Root>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
const { rerender } = render(<Fixture tab2Disabled={false} />);
|
|
25
|
+
|
|
26
|
+
// Act — disable Tab 2 after mount, then arrow onto it.
|
|
27
|
+
rerender(<Fixture tab2Disabled />);
|
|
28
|
+
await user.tab();
|
|
29
|
+
await user.keyboard("{ArrowRight}");
|
|
30
|
+
|
|
31
|
+
// Assert — the registrar effect re-ran, so Tab 2 is focused but NOT
|
|
32
|
+
// activated. A stale registration would treat it as enabled and activate
|
|
33
|
+
// it on arrow (automatic mode).
|
|
34
|
+
expect(screen.getByRole("tab", { name: "Tab 2" })).toHaveAttribute(
|
|
35
|
+
"aria-selected",
|
|
36
|
+
"false",
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
5
41
|
describe("Tabs disabled tabs tests", () => {
|
|
6
42
|
it("should have the aria-disabled attribute set to false by default", () => {
|
|
7
43
|
// Arrange
|
|
@@ -26,6 +26,23 @@ describe("Tabs lazyMount", () => {
|
|
|
26
26
|
expect(screen.getByText("Content 3")).toBeInTheDocument();
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
+
it("defaults lazyMount to off when the prop is omitted (eager render)", () => {
|
|
30
|
+
// Omitting the prop must fall back to eager mounting — a default of `true`
|
|
31
|
+
// would hide inactive panel children on mount.
|
|
32
|
+
render(
|
|
33
|
+
<Tabs.Root defaultValue="tab1">
|
|
34
|
+
<Tabs.List label="Test tabs">
|
|
35
|
+
<Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
|
|
36
|
+
<Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
|
|
37
|
+
</Tabs.List>
|
|
38
|
+
<Tabs.Content value="tab1">Content 1</Tabs.Content>
|
|
39
|
+
<Tabs.Content value="tab2">Content 2</Tabs.Content>
|
|
40
|
+
</Tabs.Root>,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
expect(screen.getByText("Content 2")).toBeInTheDocument();
|
|
44
|
+
});
|
|
45
|
+
|
|
29
46
|
it("should not render inactive panel children on initial mount when lazyMount is true", () => {
|
|
30
47
|
renderTabs(true);
|
|
31
48
|
|
|
@@ -14,6 +14,11 @@ export function useTabsContent({ value }: Pick<TabsContentProps, "value">) {
|
|
|
14
14
|
|
|
15
15
|
// Track whether this panel has ever been active. Starts true for the
|
|
16
16
|
// default active panel; flips once on first activation for all others.
|
|
17
|
+
// The mount effect below sets `activated` for the active panel within the
|
|
18
|
+
// same commit, so the initialiser's value is only ever observable as a
|
|
19
|
+
// pre-effect first-paint flash (not reproducible in jsdom); for inactive
|
|
20
|
+
// panels the initialiser's `false` and a mutated `undefined` are both falsy.
|
|
21
|
+
// Stryker disable next-line ArrowFunction: equivalent under the test env — see above.
|
|
17
22
|
const [activated, setActivated] = useState(() => isActive);
|
|
18
23
|
useEffect(() => {
|
|
19
24
|
if (isActive) setActivated(true);
|
|
@@ -18,15 +18,23 @@ type TriggerMeta = {
|
|
|
18
18
|
|
|
19
19
|
export function useTabsRoot(
|
|
20
20
|
{
|
|
21
|
-
orientation
|
|
22
|
-
dir
|
|
23
|
-
activationMode
|
|
21
|
+
orientation,
|
|
22
|
+
dir,
|
|
23
|
+
activationMode,
|
|
24
24
|
defaultValue,
|
|
25
25
|
value,
|
|
26
26
|
onValueChange,
|
|
27
27
|
onChange,
|
|
28
|
-
lazyMount
|
|
29
|
-
}:
|
|
28
|
+
lazyMount,
|
|
29
|
+
}: // `TabsRoot` always resolves these four before calling the hook, so they
|
|
30
|
+
// are required here (a default would be dead code the tests never reach).
|
|
31
|
+
Omit<TabsRootProps, "className"> &
|
|
32
|
+
Required<
|
|
33
|
+
Pick<
|
|
34
|
+
TabsRootProps,
|
|
35
|
+
"orientation" | "dir" | "activationMode" | "lazyMount"
|
|
36
|
+
>
|
|
37
|
+
>,
|
|
30
38
|
ref: Ref<TabsImperativeApi>,
|
|
31
39
|
) {
|
|
32
40
|
const tabsId = useId();
|
|
@@ -47,6 +55,10 @@ export function useTabsRoot(
|
|
|
47
55
|
(
|
|
48
56
|
triggerValue: string,
|
|
49
57
|
element: HTMLButtonElement | null,
|
|
58
|
+
// The default is only reached by the cleanup call `registerTrigger(value,
|
|
59
|
+
// null)`, where `element` is null and `disabled` is discarded — so its
|
|
60
|
+
// value is never observable.
|
|
61
|
+
// Stryker disable next-line BooleanLiteral
|
|
50
62
|
disabled = false,
|
|
51
63
|
) => {
|
|
52
64
|
registerTriggerBase(
|
|
@@ -54,6 +66,9 @@ export function useTabsRoot(
|
|
|
54
66
|
element ? { element, disabled } : null,
|
|
55
67
|
);
|
|
56
68
|
},
|
|
69
|
+
// `registerTriggerBase` is a stable useCollection callback, so emptying this
|
|
70
|
+
// array yields the identical memoised function.
|
|
71
|
+
// Stryker disable next-line ArrayDeclaration: equivalent — stable dependency.
|
|
57
72
|
[registerTriggerBase],
|
|
58
73
|
);
|
|
59
74
|
|
|
@@ -71,8 +86,14 @@ export function useTabsRoot(
|
|
|
71
86
|
|
|
72
87
|
const focusTrigger = useCallback(
|
|
73
88
|
(triggerValue: string) => {
|
|
89
|
+
// `triggerValue` is always sourced from the navigable set (registered
|
|
90
|
+
// triggers), so a value that reaches here has a live entry.
|
|
91
|
+
// Stryker disable next-line OptionalChaining: unreachable given that invariant.
|
|
74
92
|
triggersRef.current.get(triggerValue)?.element.focus();
|
|
75
93
|
},
|
|
94
|
+
// `triggersRef` is a stable RefObject, so emptying this array yields the
|
|
95
|
+
// identical memoised function.
|
|
96
|
+
// Stryker disable next-line ArrayDeclaration: equivalent — stable dependency.
|
|
76
97
|
[triggersRef],
|
|
77
98
|
);
|
|
78
99
|
|
|
@@ -100,6 +121,9 @@ export function useTabsRoot(
|
|
|
100
121
|
}
|
|
101
122
|
|
|
102
123
|
if (isControlled) {
|
|
124
|
+
// Controlled mode requires `onValueChange` (discriminated-union prop
|
|
125
|
+
// type), so the optional call can never no-op in valid usage.
|
|
126
|
+
// Stryker disable next-line OptionalChaining: unreachable — controlled requires onValueChange.
|
|
103
127
|
onValueChange?.(newValue);
|
|
104
128
|
} else {
|
|
105
129
|
setActiveValue(newValue);
|
|
@@ -51,6 +51,9 @@ export function useTabsTrigger({
|
|
|
51
51
|
(newValue: string, index: number) => {
|
|
52
52
|
if (activeValue === newValue) return;
|
|
53
53
|
if (isControlled) {
|
|
54
|
+
// Controlled mode requires `onValueChange` (discriminated-union prop
|
|
55
|
+
// type), so the optional call can never no-op in valid usage.
|
|
56
|
+
// Stryker disable next-line OptionalChaining: unreachable — controlled requires onValueChange.
|
|
54
57
|
onValueChange?.(newValue);
|
|
55
58
|
} else {
|
|
56
59
|
setActiveValue(newValue);
|
|
@@ -87,10 +90,12 @@ export function useTabsTrigger({
|
|
|
87
90
|
includeActivate: true,
|
|
88
91
|
onNavigate: (targetValue, action) => {
|
|
89
92
|
const isDisabled = disabledTriggerValues.has(targetValue);
|
|
93
|
+
// Automatic mode activates on any navigation; manual mode activates only
|
|
94
|
+
// on an explicit activation key (Enter/Space). Since the mode is always
|
|
95
|
+
// "automatic" or "manual", `automatic || isActivationKey` captures both.
|
|
90
96
|
const shouldActivate =
|
|
91
97
|
!isDisabled &&
|
|
92
|
-
(activationMode === "automatic" ||
|
|
93
|
-
(activationMode === "manual" && action === "activate"));
|
|
98
|
+
(activationMode === "automatic" || action === "activate");
|
|
94
99
|
if (shouldActivate) {
|
|
95
100
|
activateTab(targetValue, triggerValues.indexOf(targetValue));
|
|
96
101
|
}
|
|
@@ -30,13 +30,13 @@ At most one item is pressed at a time. Pressing the active item again
|
|
|
30
30
|
```tsx
|
|
31
31
|
// Uncontrolled
|
|
32
32
|
<ToggleGroup.Root type="single" defaultValue="center" aria-label="Alignment">
|
|
33
|
-
|
|
33
|
+
...
|
|
34
34
|
</ToggleGroup.Root>
|
|
35
35
|
|
|
36
36
|
// Controlled — onValueChange receives string | undefined
|
|
37
37
|
const [align, setAlign] = useState<string | undefined>("center");
|
|
38
38
|
<ToggleGroup.Root type="single" value={align} onValueChange={setAlign} aria-label="Alignment">
|
|
39
|
-
|
|
39
|
+
...
|
|
40
40
|
</ToggleGroup.Root>
|
|
41
41
|
```
|
|
42
42
|
|
|
@@ -48,13 +48,13 @@ independently.
|
|
|
48
48
|
```tsx
|
|
49
49
|
// Uncontrolled
|
|
50
50
|
<ToggleGroup.Root type="multiple" defaultValue={["bold"]} aria-label="Formatting">
|
|
51
|
-
|
|
51
|
+
...
|
|
52
52
|
</ToggleGroup.Root>
|
|
53
53
|
|
|
54
54
|
// Controlled — onValueChange receives string[]
|
|
55
55
|
const [formats, setFormats] = useState<string[]>([]);
|
|
56
56
|
<ToggleGroup.Root type="multiple" value={formats} onValueChange={setFormats} aria-label="Formatting">
|
|
57
|
-
|
|
57
|
+
...
|
|
58
58
|
</ToggleGroup.Root>
|
|
59
59
|
```
|
|
60
60
|
|
package/src/Tooltip/README.md
CHANGED
|
@@ -60,12 +60,12 @@ The two shapes are statically discriminated at the type level.
|
|
|
60
60
|
|
|
61
61
|
```tsx
|
|
62
62
|
// Uncontrolled
|
|
63
|
-
<Tooltip.Root defaultOpen
|
|
63
|
+
<Tooltip.Root defaultOpen>...</Tooltip.Root>
|
|
64
64
|
|
|
65
65
|
// Controlled
|
|
66
66
|
const [open, setOpen] = useState(false);
|
|
67
67
|
<Tooltip.Root open={open} onOpenChange={setOpen}>
|
|
68
|
-
|
|
68
|
+
...
|
|
69
69
|
</Tooltip.Root>
|
|
70
70
|
```
|
|
71
71
|
|
|
@@ -82,7 +82,7 @@ before the normal delay resumes.
|
|
|
82
82
|
|
|
83
83
|
```tsx
|
|
84
84
|
<Tooltip.Provider delayDuration={400} skipDelayDuration={150}>
|
|
85
|
-
|
|
85
|
+
...
|
|
86
86
|
</Tooltip.Provider>
|
|
87
87
|
```
|
|
88
88
|
|
|
@@ -95,7 +95,7 @@ when the cursor leaves the trigger:
|
|
|
95
95
|
|
|
96
96
|
```tsx
|
|
97
97
|
<Tooltip.Root disableHoverableContent>
|
|
98
|
-
|
|
98
|
+
...
|
|
99
99
|
</Tooltip.Root>
|
|
100
100
|
```
|
|
101
101
|
|
|
@@ -114,7 +114,7 @@ tooltip open:
|
|
|
114
114
|
event.preventDefault(); // keep open when clicking outside
|
|
115
115
|
}}
|
|
116
116
|
>
|
|
117
|
-
|
|
117
|
+
...
|
|
118
118
|
</Tooltip.Content>
|
|
119
119
|
```
|
|
120
120
|
|
|
@@ -178,7 +178,7 @@ subtree stays in the DOM while closed so a CSS animation can play on
|
|
|
178
178
|
|
|
179
179
|
```tsx
|
|
180
180
|
<Tooltip.Portal forceMount>
|
|
181
|
-
<Tooltip.Content forceMount
|
|
181
|
+
<Tooltip.Content forceMount>...</Tooltip.Content>
|
|
182
182
|
</Tooltip.Portal>
|
|
183
183
|
```
|
|
184
184
|
|
|
@@ -199,7 +199,7 @@ child element; the child's own handlers run first.
|
|
|
199
199
|
</Tooltip.Trigger>
|
|
200
200
|
|
|
201
201
|
<Tooltip.Arrow asChild>
|
|
202
|
-
<svg viewBox="0 0 10 5"
|
|
202
|
+
<svg viewBox="0 0 10 5">...</svg>
|
|
203
203
|
</Tooltip.Arrow>
|
|
204
204
|
```
|
|
205
205
|
|
package/src/Tooltip/Tooltip.tsx
CHANGED
|
@@ -120,7 +120,7 @@ export function TooltipRoot({
|
|
|
120
120
|
return <TooltipProvider value={contextValue}>{children}</TooltipProvider>;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
|
|
123
|
+
// Stryker disable next-line StringLiteral: overwritten by the compound alias — an equivalent mutant.
|
|
124
124
|
TooltipRoot.displayName = "TooltipRoot";
|
|
125
125
|
|
|
126
126
|
/**
|
|
@@ -195,7 +195,7 @@ TooltipTrigger.displayName = "TooltipTrigger";
|
|
|
195
195
|
* @example
|
|
196
196
|
* ```tsx
|
|
197
197
|
* <Tooltip.Portal>
|
|
198
|
-
* <Tooltip.Content
|
|
198
|
+
* <Tooltip.Content>...</Tooltip.Content>
|
|
199
199
|
* </Tooltip.Portal>
|
|
200
200
|
* ```
|
|
201
201
|
*/
|
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
import { render, screen } from "@testing-library/react";
|
|
2
2
|
|
|
3
3
|
import { Tooltip } from "../Tooltip";
|
|
4
|
+
import { TooltipContext, TooltipProviderContext } from "../TooltipContext";
|
|
5
|
+
|
|
6
|
+
describe("Tooltip displayNames", () => {
|
|
7
|
+
it("sets a displayName on the compound, each sub-component, and both contexts", () => {
|
|
8
|
+
// Assert — empty displayNames would render each as anonymous in DevTools.
|
|
9
|
+
// Tooltip.Root is the same object as the compound (Object.assign), so its
|
|
10
|
+
// own "TooltipRoot" assignment is overwritten and has no observable
|
|
11
|
+
// Root displayName; the sub-components are distinct objects.
|
|
12
|
+
expect(Tooltip.displayName).toBe("Tooltip");
|
|
13
|
+
expect(Tooltip.Provider.displayName).toBe("TooltipProvider");
|
|
14
|
+
expect(Tooltip.Trigger.displayName).toBe("TooltipTrigger");
|
|
15
|
+
expect(Tooltip.Portal.displayName).toBe("TooltipPortal");
|
|
16
|
+
expect(Tooltip.Content.displayName).toBe("TooltipContent");
|
|
17
|
+
expect(Tooltip.Arrow.displayName).toBe("TooltipArrow");
|
|
18
|
+
expect(TooltipProviderContext.displayName).toBe("TooltipProviderContext");
|
|
19
|
+
expect(TooltipContext.displayName).toBe("TooltipContext");
|
|
20
|
+
});
|
|
21
|
+
});
|
|
4
22
|
|
|
5
23
|
describe("Tooltip — basic rendering", () => {
|
|
6
24
|
it("renders Tooltip.Trigger as a button with type='button'", () => {
|
|
@@ -135,6 +153,38 @@ describe("Tooltip — basic rendering", () => {
|
|
|
135
153
|
expect(screen.queryByRole("tooltip")).toBeNull();
|
|
136
154
|
});
|
|
137
155
|
|
|
156
|
+
it("Tooltip.Portal does not render arbitrary children when closed, independent of Content's own guard", () => {
|
|
157
|
+
// Content has its own open guard, so wrapping it can't tell Portal's
|
|
158
|
+
// guard apart from Content's. Wrap a plain element instead.
|
|
159
|
+
render(
|
|
160
|
+
<Tooltip.Provider>
|
|
161
|
+
<Tooltip.Root>
|
|
162
|
+
<Tooltip.Trigger>Hover me</Tooltip.Trigger>
|
|
163
|
+
<Tooltip.Portal>
|
|
164
|
+
<div data-testid="portal-child">child</div>
|
|
165
|
+
</Tooltip.Portal>
|
|
166
|
+
</Tooltip.Root>
|
|
167
|
+
</Tooltip.Provider>,
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
expect(screen.queryByTestId("portal-child")).toBeNull();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("Tooltip.Portal renders arbitrary children via forceMount even when closed", () => {
|
|
174
|
+
render(
|
|
175
|
+
<Tooltip.Provider>
|
|
176
|
+
<Tooltip.Root>
|
|
177
|
+
<Tooltip.Trigger>Hover me</Tooltip.Trigger>
|
|
178
|
+
<Tooltip.Portal forceMount>
|
|
179
|
+
<div data-testid="portal-child">child</div>
|
|
180
|
+
</Tooltip.Portal>
|
|
181
|
+
</Tooltip.Root>
|
|
182
|
+
</Tooltip.Provider>,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
expect(screen.getByTestId("portal-child")).toBeInTheDocument();
|
|
186
|
+
});
|
|
187
|
+
|
|
138
188
|
it("Tooltip.Arrow renders a span inside the content", () => {
|
|
139
189
|
render(
|
|
140
190
|
<Tooltip.Provider>
|