@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
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { Combobox } from "../Combobox";
|
|
6
|
+
|
|
7
|
+
function renderCombobox() {
|
|
8
|
+
return render(
|
|
9
|
+
<Combobox.Root defaultOpen>
|
|
10
|
+
<Combobox.Input aria-label="Framework" />
|
|
11
|
+
<Combobox.Content aria-label="Frameworks">
|
|
12
|
+
<Combobox.Item value="react">React</Combobox.Item>
|
|
13
|
+
<Combobox.Item value="preact">Preact</Combobox.Item>
|
|
14
|
+
</Combobox.Content>
|
|
15
|
+
</Combobox.Root>,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/*
|
|
20
|
+
* Cursor scrolling. The registry panel caps at 18rem and scrolls past it, so a
|
|
21
|
+
* cursor that moves without bringing itself into view walks straight off the
|
|
22
|
+
* bottom of a filtered list — APG requires the referenced option to be made
|
|
23
|
+
* visible. `block: "nearest"` specifically: it is the option that does nothing
|
|
24
|
+
* when the item is already on screen, so ordinary arrowing does not jerk the list
|
|
25
|
+
* about. Same contract, and the same spy technique, as Listbox.
|
|
26
|
+
*/
|
|
27
|
+
describe("Combobox cursor scrolling", () => {
|
|
28
|
+
it("scrolls the seeded item into view when ArrowDown opens the cursor", async () => {
|
|
29
|
+
const user = userEvent.setup();
|
|
30
|
+
renderCombobox();
|
|
31
|
+
|
|
32
|
+
const react = screen.getByRole("option", { hidden: true, name: "React" });
|
|
33
|
+
const scrollIntoView = vi.spyOn(react, "scrollIntoView");
|
|
34
|
+
|
|
35
|
+
await user.click(screen.getByRole("combobox", { name: "Framework" }));
|
|
36
|
+
await user.keyboard("{ArrowDown}");
|
|
37
|
+
|
|
38
|
+
expect(scrollIntoView).toHaveBeenCalledWith({ block: "nearest" });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("scrolls each item into view as the cursor moves onto it", async () => {
|
|
42
|
+
const user = userEvent.setup();
|
|
43
|
+
renderCombobox();
|
|
44
|
+
|
|
45
|
+
const preact = screen.getByRole("option", { hidden: true, name: "Preact" });
|
|
46
|
+
const scrollIntoView = vi.spyOn(preact, "scrollIntoView");
|
|
47
|
+
|
|
48
|
+
await user.click(screen.getByRole("combobox", { name: "Framework" }));
|
|
49
|
+
await user.keyboard("{ArrowDown}{ArrowDown}");
|
|
50
|
+
|
|
51
|
+
expect(scrollIntoView).toHaveBeenCalledWith({ block: "nearest" });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("scrolls the last item into view on End", async () => {
|
|
55
|
+
const user = userEvent.setup();
|
|
56
|
+
renderCombobox();
|
|
57
|
+
|
|
58
|
+
const preact = screen.getByRole("option", { hidden: true, name: "Preact" });
|
|
59
|
+
const scrollIntoView = vi.spyOn(preact, "scrollIntoView");
|
|
60
|
+
|
|
61
|
+
await user.click(screen.getByRole("combobox", { name: "Framework" }));
|
|
62
|
+
// Seed the cursor first: Home/End move an existing cursor rather than
|
|
63
|
+
// creating one.
|
|
64
|
+
await user.keyboard("{ArrowDown}{End}");
|
|
65
|
+
|
|
66
|
+
expect(scrollIntoView).toHaveBeenCalledWith({ block: "nearest" });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("scrolls the seeded item into view when ArrowUp opens the cursor at the end", async () => {
|
|
70
|
+
const user = userEvent.setup();
|
|
71
|
+
renderCombobox();
|
|
72
|
+
|
|
73
|
+
const preact = screen.getByRole("option", { hidden: true, name: "Preact" });
|
|
74
|
+
const scrollIntoView = vi.spyOn(preact, "scrollIntoView");
|
|
75
|
+
|
|
76
|
+
await user.click(screen.getByRole("combobox", { name: "Framework" }));
|
|
77
|
+
await user.keyboard("{ArrowUp}");
|
|
78
|
+
|
|
79
|
+
expect(scrollIntoView).toHaveBeenCalledWith({ block: "nearest" });
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("Combobox keyboard interaction", () => {
|
|
84
|
+
it("closes the popup on Escape while leaving focus in the input", async () => {
|
|
85
|
+
const user = userEvent.setup();
|
|
86
|
+
renderCombobox();
|
|
87
|
+
|
|
88
|
+
const input = screen.getByRole("combobox", { name: "Framework" });
|
|
89
|
+
await user.click(input);
|
|
90
|
+
expect(screen.getByRole("listbox", { hidden: true })).toBeInTheDocument();
|
|
91
|
+
|
|
92
|
+
await user.keyboard("{Escape}");
|
|
93
|
+
|
|
94
|
+
expect(screen.queryByRole("listbox", { hidden: true })).not.toBeInTheDocument();
|
|
95
|
+
expect(input).toHaveFocus();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("moves the cursor with the arrow keys and publishes it as aria-activedescendant", async () => {
|
|
99
|
+
const user = userEvent.setup();
|
|
100
|
+
renderCombobox();
|
|
101
|
+
|
|
102
|
+
const input = screen.getByRole("combobox", { name: "Framework" });
|
|
103
|
+
await user.click(input);
|
|
104
|
+
|
|
105
|
+
// no cursor until the user asks for one
|
|
106
|
+
expect(input).not.toHaveAttribute("aria-activedescendant");
|
|
107
|
+
|
|
108
|
+
await user.keyboard("{ArrowDown}");
|
|
109
|
+
const react = screen.getByRole("option", { hidden: true, name: "React" });
|
|
110
|
+
expect(input).toHaveAttribute("aria-activedescendant", react.id);
|
|
111
|
+
expect(react).toHaveAttribute("data-highlighted", "");
|
|
112
|
+
|
|
113
|
+
await user.keyboard("{ArrowDown}");
|
|
114
|
+
const preact = screen.getByRole("option", { hidden: true, name: "Preact" });
|
|
115
|
+
expect(input).toHaveAttribute("aria-activedescendant", preact.id);
|
|
116
|
+
expect(react).not.toHaveAttribute("data-highlighted");
|
|
117
|
+
|
|
118
|
+
await user.keyboard("{ArrowUp}");
|
|
119
|
+
expect(input).toHaveAttribute("aria-activedescendant", react.id);
|
|
120
|
+
|
|
121
|
+
// virtual focus: the cursor moves without an option ever taking DOM focus
|
|
122
|
+
expect(input).toHaveFocus();
|
|
123
|
+
expect(react).not.toHaveFocus();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("seeds the cursor on the last item when ArrowUp opens the cursor", async () => {
|
|
127
|
+
const user = userEvent.setup();
|
|
128
|
+
renderCombobox();
|
|
129
|
+
|
|
130
|
+
const input = screen.getByRole("combobox", { name: "Framework" });
|
|
131
|
+
await user.click(input);
|
|
132
|
+
await user.keyboard("{ArrowUp}");
|
|
133
|
+
|
|
134
|
+
expect(input).toHaveAttribute(
|
|
135
|
+
"aria-activedescendant",
|
|
136
|
+
screen.getByRole("option", { hidden: true, name: "Preact" }).id,
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("commits the cursor item on Enter", async () => {
|
|
141
|
+
const user = userEvent.setup();
|
|
142
|
+
const onValueChange = vi.fn();
|
|
143
|
+
|
|
144
|
+
render(
|
|
145
|
+
<Combobox.Root defaultOpen onValueChange={onValueChange}>
|
|
146
|
+
<Combobox.Input aria-label="Framework" />
|
|
147
|
+
<Combobox.Content aria-label="Frameworks">
|
|
148
|
+
<Combobox.Item value="react">React</Combobox.Item>
|
|
149
|
+
<Combobox.Item value="preact">Preact</Combobox.Item>
|
|
150
|
+
</Combobox.Content>
|
|
151
|
+
</Combobox.Root>,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const input = screen.getByRole("combobox", { name: "Framework" });
|
|
155
|
+
await user.click(input);
|
|
156
|
+
await user.keyboard("{ArrowDown}{ArrowDown}");
|
|
157
|
+
await user.keyboard("{Enter}");
|
|
158
|
+
|
|
159
|
+
expect(onValueChange).toHaveBeenCalledWith("preact");
|
|
160
|
+
expect(screen.queryByRole("listbox", { hidden: true })).not.toBeInTheDocument();
|
|
161
|
+
expect(input).toHaveValue("Preact");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("ignores Enter while there is no cursor", async () => {
|
|
165
|
+
const user = userEvent.setup();
|
|
166
|
+
const onValueChange = vi.fn();
|
|
167
|
+
|
|
168
|
+
render(
|
|
169
|
+
<Combobox.Root defaultOpen onValueChange={onValueChange}>
|
|
170
|
+
<Combobox.Input aria-label="Framework" />
|
|
171
|
+
<Combobox.Content aria-label="Frameworks">
|
|
172
|
+
<Combobox.Item value="react">React</Combobox.Item>
|
|
173
|
+
</Combobox.Content>
|
|
174
|
+
</Combobox.Root>,
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
await user.click(screen.getByRole("combobox", { name: "Framework" }));
|
|
178
|
+
await user.keyboard("{Enter}");
|
|
179
|
+
|
|
180
|
+
expect(onValueChange).not.toHaveBeenCalled();
|
|
181
|
+
expect(screen.getByRole("listbox", { hidden: true })).toBeInTheDocument();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("keeps the committed value when Escape closes the popup", async () => {
|
|
185
|
+
const user = userEvent.setup();
|
|
186
|
+
renderCombobox();
|
|
187
|
+
|
|
188
|
+
const input = screen.getByRole("combobox", { name: "Framework" });
|
|
189
|
+
await user.click(screen.getByRole("option", { hidden: true, name: "React" }));
|
|
190
|
+
await user.type(input, "zzz");
|
|
191
|
+
await user.keyboard("{Escape}");
|
|
192
|
+
|
|
193
|
+
// Escape abandons the query, so the text returns to the committed value
|
|
194
|
+
expect(input).toHaveValue("React");
|
|
195
|
+
});
|
|
196
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { Combobox } from "../Combobox";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Light dismiss — clicking outside an open combobox closes it.
|
|
9
|
+
*
|
|
10
|
+
* The popup is a `popover="auto"` element, so the browser does the outside-click
|
|
11
|
+
* detection itself and reports it back as a `toggle` event; there is deliberately
|
|
12
|
+
* no hand-rolled pointerdown-outside listener. That is the same pattern
|
|
13
|
+
* `useSelectContent` uses, and these tests drive it the same way Select's do: by
|
|
14
|
+
* dispatching the `toggle` the browser would have sent, since jsdom implements
|
|
15
|
+
* neither the top layer nor real light-dismiss.
|
|
16
|
+
*/
|
|
17
|
+
function renderCombobox(onOpenChange?: (open: boolean) => void) {
|
|
18
|
+
return render(
|
|
19
|
+
<Combobox.Root defaultOpen onOpenChange={onOpenChange}>
|
|
20
|
+
<Combobox.Input aria-label="Framework" />
|
|
21
|
+
<Combobox.Content aria-label="Frameworks">
|
|
22
|
+
<Combobox.Item value="react">React</Combobox.Item>
|
|
23
|
+
<Combobox.Item value="preact">Preact</Combobox.Item>
|
|
24
|
+
</Combobox.Content>
|
|
25
|
+
</Combobox.Root>,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const listbox = () => screen.getByRole("listbox", { hidden: true });
|
|
30
|
+
|
|
31
|
+
const dismiss = (element: HTMLElement) => {
|
|
32
|
+
const event = new Event("toggle");
|
|
33
|
+
Object.defineProperty(event, "newState", { value: "closed" });
|
|
34
|
+
fireEvent(element, event);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
describe("Combobox light dismiss", () => {
|
|
38
|
+
it("closes when the popup reports a browser light-dismiss", () => {
|
|
39
|
+
renderCombobox();
|
|
40
|
+
const input = screen.getByRole("combobox", { name: "Framework" });
|
|
41
|
+
expect(input).toHaveAttribute("aria-expanded", "true");
|
|
42
|
+
|
|
43
|
+
dismiss(listbox());
|
|
44
|
+
|
|
45
|
+
expect(input).toHaveAttribute("aria-expanded", "false");
|
|
46
|
+
expect(screen.queryByRole("listbox", { hidden: true })).not.toBeInTheDocument();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("ignores a toggle to open, so an opening popup does not thrash state", () => {
|
|
50
|
+
const onOpenChange = vi.fn();
|
|
51
|
+
renderCombobox(onOpenChange);
|
|
52
|
+
const input = screen.getByRole("combobox", { name: "Framework" });
|
|
53
|
+
|
|
54
|
+
const event = new Event("toggle");
|
|
55
|
+
Object.defineProperty(event, "newState", { value: "open" });
|
|
56
|
+
fireEvent(listbox(), event);
|
|
57
|
+
|
|
58
|
+
expect(input).toHaveAttribute("aria-expanded", "true");
|
|
59
|
+
expect(onOpenChange).not.toHaveBeenCalled();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("still dismisses when the consumer attaches its own ref to the popup", () => {
|
|
63
|
+
// The popup needs an internal ref to call showPopover() on. A consumer ref
|
|
64
|
+
// must compose with it rather than replace it, or the whole popover
|
|
65
|
+
// machinery — top layer and light dismiss alike — silently stops working.
|
|
66
|
+
const consumerRef = { current: null as HTMLDivElement | null };
|
|
67
|
+
render(
|
|
68
|
+
<Combobox.Root defaultOpen>
|
|
69
|
+
<Combobox.Input aria-label="Framework" />
|
|
70
|
+
<Combobox.Content aria-label="Frameworks" ref={consumerRef}>
|
|
71
|
+
<Combobox.Item value="react">React</Combobox.Item>
|
|
72
|
+
</Combobox.Content>
|
|
73
|
+
</Combobox.Root>,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
expect(consumerRef.current).not.toBeNull();
|
|
77
|
+
|
|
78
|
+
dismiss(listbox());
|
|
79
|
+
|
|
80
|
+
expect(screen.getByRole("combobox", { name: "Framework" })).toHaveAttribute(
|
|
81
|
+
"aria-expanded",
|
|
82
|
+
"false",
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("restores the committed label, so a dismissed query never lingers", async () => {
|
|
87
|
+
const user = userEvent.setup();
|
|
88
|
+
renderCombobox();
|
|
89
|
+
const input = screen.getByRole("combobox", { name: "Framework" });
|
|
90
|
+
|
|
91
|
+
await user.click(screen.getByRole("option", { hidden: true, name: "React" }));
|
|
92
|
+
await user.type(input, "zzz");
|
|
93
|
+
expect(input).toHaveValue("Reactzzz");
|
|
94
|
+
|
|
95
|
+
dismiss(listbox());
|
|
96
|
+
|
|
97
|
+
// Same contract as Escape: dismissing abandons the query rather than leaving
|
|
98
|
+
// the field showing text that is not the value.
|
|
99
|
+
expect(input).toHaveValue("React");
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { Combobox } from "../Combobox";
|
|
6
|
+
|
|
7
|
+
describe("Combobox query", () => {
|
|
8
|
+
it("opens the popup and shows the typed text as the user types", async () => {
|
|
9
|
+
const user = userEvent.setup();
|
|
10
|
+
|
|
11
|
+
render(
|
|
12
|
+
<Combobox.Root>
|
|
13
|
+
<Combobox.Input aria-label="Framework" />
|
|
14
|
+
<Combobox.Content aria-label="Frameworks" />
|
|
15
|
+
</Combobox.Root>,
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const input = screen.getByRole("combobox", { name: "Framework" });
|
|
19
|
+
expect(screen.queryByRole("listbox", { hidden: true })).not.toBeInTheDocument();
|
|
20
|
+
|
|
21
|
+
await user.type(input, "Rea");
|
|
22
|
+
|
|
23
|
+
expect(input).toHaveValue("Rea");
|
|
24
|
+
expect(input).toHaveAttribute("aria-expanded", "true");
|
|
25
|
+
expect(screen.getByRole("listbox", { hidden: true })).toBeInTheDocument();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("reports the query to onQueryChange on every keystroke", async () => {
|
|
29
|
+
const user = userEvent.setup();
|
|
30
|
+
const onQueryChange = vi.fn();
|
|
31
|
+
|
|
32
|
+
render(
|
|
33
|
+
<Combobox.Root onQueryChange={onQueryChange}>
|
|
34
|
+
<Combobox.Input aria-label="Framework" />
|
|
35
|
+
<Combobox.Content aria-label="Frameworks" />
|
|
36
|
+
</Combobox.Root>,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
await user.type(screen.getByRole("combobox", { name: "Framework" }), "Re");
|
|
40
|
+
|
|
41
|
+
expect(onQueryChange.mock.calls.map(([q]) => q)).toEqual(["R", "Re"]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/*
|
|
45
|
+
* Emptying the field is a deselect.
|
|
46
|
+
*
|
|
47
|
+
* The exploration (§D1) settled that closing resets the text FROM the value, so
|
|
48
|
+
* the field can never sit showing something that is not the value. Taken alone
|
|
49
|
+
* that made a cleared field un-clearable: Escape put the old label straight
|
|
50
|
+
* back. Clearing the value too keeps §D1's invariant intact — empty text, empty
|
|
51
|
+
* value — while letting the gesture mean what the user intended.
|
|
52
|
+
*/
|
|
53
|
+
it("clears the committed value when the query is emptied", async () => {
|
|
54
|
+
const user = userEvent.setup();
|
|
55
|
+
const onValueChange = vi.fn();
|
|
56
|
+
|
|
57
|
+
render(
|
|
58
|
+
<Combobox.Root defaultOpen onValueChange={onValueChange}>
|
|
59
|
+
<Combobox.Input aria-label="Framework" />
|
|
60
|
+
<Combobox.Content aria-label="Frameworks">
|
|
61
|
+
<Combobox.Item value="react">React</Combobox.Item>
|
|
62
|
+
</Combobox.Content>
|
|
63
|
+
</Combobox.Root>,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const input = screen.getByRole("combobox", { name: "Framework" });
|
|
67
|
+
await user.click(screen.getByRole("option", { hidden: true, name: "React" }));
|
|
68
|
+
expect(input).toHaveValue("React");
|
|
69
|
+
onValueChange.mockClear();
|
|
70
|
+
|
|
71
|
+
await user.clear(input);
|
|
72
|
+
|
|
73
|
+
expect(onValueChange).toHaveBeenCalledWith("");
|
|
74
|
+
expect(screen.getByRole("option", { hidden: true, name: "React" })).toHaveAttribute(
|
|
75
|
+
"aria-selected",
|
|
76
|
+
"false",
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// The point of the change: Escape must not resurrect the label just deleted.
|
|
80
|
+
await user.keyboard("{Escape}");
|
|
81
|
+
expect(input).toHaveValue("");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("does not report a value change when an already-empty field is cleared", async () => {
|
|
85
|
+
const user = userEvent.setup();
|
|
86
|
+
const onValueChange = vi.fn();
|
|
87
|
+
|
|
88
|
+
render(
|
|
89
|
+
<Combobox.Root onValueChange={onValueChange}>
|
|
90
|
+
<Combobox.Input aria-label="Framework" />
|
|
91
|
+
<Combobox.Content aria-label="Frameworks">
|
|
92
|
+
<Combobox.Item value="react">React</Combobox.Item>
|
|
93
|
+
</Combobox.Content>
|
|
94
|
+
</Combobox.Root>,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const input = screen.getByRole("combobox", { name: "Framework" });
|
|
98
|
+
// Typing and deleting back to empty never committed anything, so there is no
|
|
99
|
+
// selection to clear and no reason to tell the consumer otherwise.
|
|
100
|
+
await user.type(input, "Re");
|
|
101
|
+
await user.clear(input);
|
|
102
|
+
|
|
103
|
+
expect(onValueChange).not.toHaveBeenCalled();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./useComboboxRoot";
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { ComponentProps, KeyboardEvent } from "react";
|
|
2
|
+
|
|
3
|
+
/** What a mounted `Combobox.Item` registers with the root. */
|
|
4
|
+
export interface ComboboxItemMeta {
|
|
5
|
+
/**
|
|
6
|
+
* The rendered element, used to scroll the cursor into view. Non-nullable:
|
|
7
|
+
* registration happens in the item's own effect, so the ref has always been
|
|
8
|
+
* attached by then — an item is never in the registry without its element.
|
|
9
|
+
*/
|
|
10
|
+
element: HTMLElement;
|
|
11
|
+
/** The label to show in the input once this item is committed. */
|
|
12
|
+
label: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** The value carried from `Combobox.Root` to its parts. */
|
|
16
|
+
export interface ComboboxContextValue {
|
|
17
|
+
/** Whether the popup listbox is open. */
|
|
18
|
+
open: boolean;
|
|
19
|
+
/** DOM id of the popup listbox, wired to the input's `aria-controls`. */
|
|
20
|
+
listboxId: string;
|
|
21
|
+
/** The text currently in the input — the live query while open. */
|
|
22
|
+
query: string;
|
|
23
|
+
/** Sets the query, opening the popup, as the user types. */
|
|
24
|
+
setQuery: (query: string) => void;
|
|
25
|
+
/** The selected value, or `""` when nothing is selected. */
|
|
26
|
+
value: string;
|
|
27
|
+
/** Commits a selection: sets the value, closes, and resets the input text. */
|
|
28
|
+
select: (value: string, label: string) => void;
|
|
29
|
+
/** The input's whole keydown behaviour: Escape, cursor seeding, arrow keys. */
|
|
30
|
+
handleInputKeyDown: (event: KeyboardEvent<HTMLElement>) => void;
|
|
31
|
+
/**
|
|
32
|
+
* Closes the popup, clears the cursor and resets the input text from the
|
|
33
|
+
* committed value. Shared by Escape and by the browser's own light-dismiss, so
|
|
34
|
+
* clicking away and pressing Escape leave the field in the same state.
|
|
35
|
+
*/
|
|
36
|
+
dismiss: () => void;
|
|
37
|
+
/** The value the virtual-focus cursor sits on, or `null` for no cursor. */
|
|
38
|
+
activeValue: string | null;
|
|
39
|
+
/** Builds the DOM id for an item, so the input can point at it. */
|
|
40
|
+
getItemId: (value: string) => string;
|
|
41
|
+
/**
|
|
42
|
+
* Registers a mounted item so the arrow keys know the order and Enter can
|
|
43
|
+
* resolve the same label a click would.
|
|
44
|
+
*/
|
|
45
|
+
registerItem: (value: string, meta: ComboboxItemMeta | null) => void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Props shared by every asChild-capable part. */
|
|
49
|
+
export interface ComboboxSlottableProps {
|
|
50
|
+
/**
|
|
51
|
+
* Render the consumer's own element instead of the default one, merging
|
|
52
|
+
* behaviour onto it.
|
|
53
|
+
*
|
|
54
|
+
* @default false
|
|
55
|
+
*/
|
|
56
|
+
asChild?: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Props for `Combobox.Root` — the wrapper that owns the combobox's state. */
|
|
60
|
+
export interface ComboboxRootProps extends ComponentProps<"div">, ComboboxSlottableProps {
|
|
61
|
+
/**
|
|
62
|
+
* Whether the popup is open on first render, for the uncontrolled case.
|
|
63
|
+
*
|
|
64
|
+
* @default false
|
|
65
|
+
*/
|
|
66
|
+
defaultOpen?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Whether the popup is open, for the controlled case. Pair with
|
|
69
|
+
* {@link ComboboxRootProps.onOpenChange | `onOpenChange`} or it can never
|
|
70
|
+
* change.
|
|
71
|
+
*
|
|
72
|
+
* @default undefined
|
|
73
|
+
*/
|
|
74
|
+
open?: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Called when the combobox wants the popup opened or closed. Under a
|
|
77
|
+
* controlled {@link ComboboxRootProps.open | `open`} the combobox only asks —
|
|
78
|
+
* the parent decides.
|
|
79
|
+
*
|
|
80
|
+
* @default undefined
|
|
81
|
+
*/
|
|
82
|
+
onOpenChange?: (open: boolean) => void;
|
|
83
|
+
/**
|
|
84
|
+
* The selected value on first render, for the uncontrolled case.
|
|
85
|
+
*
|
|
86
|
+
* @default ""
|
|
87
|
+
*/
|
|
88
|
+
defaultValue?: string;
|
|
89
|
+
/**
|
|
90
|
+
* The selected value, for the controlled case. Pair with
|
|
91
|
+
* {@link ComboboxRootProps.onValueChange | `onValueChange`}.
|
|
92
|
+
*
|
|
93
|
+
* @default undefined
|
|
94
|
+
*/
|
|
95
|
+
value?: string;
|
|
96
|
+
/**
|
|
97
|
+
* Called with the input's text on every keystroke. Filtering is
|
|
98
|
+
* **consumer-owned** — narrow the options you render in response. The
|
|
99
|
+
* component deliberately ships no `filter` prop.
|
|
100
|
+
*
|
|
101
|
+
* @default undefined
|
|
102
|
+
*/
|
|
103
|
+
onQueryChange?: (query: string) => void;
|
|
104
|
+
/**
|
|
105
|
+
* Called with the newly selected value when the user commits a choice,
|
|
106
|
+
* whether by click or by Enter.
|
|
107
|
+
*
|
|
108
|
+
* @default undefined
|
|
109
|
+
*/
|
|
110
|
+
onValueChange?: (value: string) => void;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Props for `Combobox.Input` — the editable text field. */
|
|
114
|
+
export interface ComboboxInputProps extends ComponentProps<"input">, ComboboxSlottableProps {}
|
|
115
|
+
|
|
116
|
+
/** Props for `Combobox.Content` — the popup listbox. */
|
|
117
|
+
export interface ComboboxContentProps extends ComponentProps<"div">, ComboboxSlottableProps {}
|
|
118
|
+
|
|
119
|
+
/** Props for `Combobox.Item` — one selectable option in the popup. */
|
|
120
|
+
export interface ComboboxItemProps
|
|
121
|
+
extends Omit<ComponentProps<"div">, "value">,
|
|
122
|
+
ComboboxSlottableProps {
|
|
123
|
+
/**
|
|
124
|
+
* The value committed when this item is chosen. This narrows the native
|
|
125
|
+
* `value` attribute, so `value` is `Omit`-ted from the base props above —
|
|
126
|
+
* without that the two resolve to an intersection artifact that leaks into
|
|
127
|
+
* consumer types and the generated prop tables.
|
|
128
|
+
*/
|
|
129
|
+
value: string;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Props for `Combobox.Empty` — the no-results message inside the popup. */
|
|
133
|
+
export interface ComboboxEmptyProps extends ComponentProps<"div">, ComboboxSlottableProps {}
|
|
@@ -88,7 +88,7 @@ import { MENUITEM_SELECTOR, TYPEAHEAD_RESET_MS } from "./constants";
|
|
|
88
88
|
* const [open, setOpen] = useState(false);
|
|
89
89
|
*
|
|
90
90
|
* <ContextMenu.Root open={open} onOpenChange={setOpen}>
|
|
91
|
-
*
|
|
91
|
+
* ...
|
|
92
92
|
* </ContextMenu.Root>
|
|
93
93
|
* ```
|
|
94
94
|
*/
|
|
@@ -182,7 +182,7 @@ function useCloseSiblingSub() {
|
|
|
182
182
|
* @example asChild — attach the gesture to any element
|
|
183
183
|
* ```tsx
|
|
184
184
|
* <ContextMenu.Trigger asChild>
|
|
185
|
-
* <ImageCard src="
|
|
185
|
+
* <ImageCard src="..." alt="..." />
|
|
186
186
|
* </ContextMenu.Trigger>
|
|
187
187
|
* ```
|
|
188
188
|
*/
|
|
@@ -1050,7 +1050,7 @@ ContextMenuSub.displayName = "ContextMenuSub";
|
|
|
1050
1050
|
* ```tsx
|
|
1051
1051
|
* <ContextMenu.Sub>
|
|
1052
1052
|
* <ContextMenu.SubTrigger>More tools</ContextMenu.SubTrigger>
|
|
1053
|
-
* <ContextMenu.SubContent
|
|
1053
|
+
* <ContextMenu.SubContent>...</ContextMenu.SubContent>
|
|
1054
1054
|
* </ContextMenu.Sub>
|
|
1055
1055
|
* ```
|
|
1056
1056
|
*/
|
|
@@ -21,9 +21,9 @@ import { DirectionProviderProps } from "./types";
|
|
|
21
21
|
* ```tsx
|
|
22
22
|
* <DirectionProvider dir="rtl">
|
|
23
23
|
* {/* inherits rtl *\/}
|
|
24
|
-
* <Tabs.Root defaultValue="a"
|
|
24
|
+
* <Tabs.Root defaultValue="a">...</Tabs.Root>
|
|
25
25
|
* {/* explicit ltr overrides the provider *\/}
|
|
26
|
-
* <Tabs.Root dir="ltr" defaultValue="a"
|
|
26
|
+
* <Tabs.Root dir="ltr" defaultValue="a">...</Tabs.Root>
|
|
27
27
|
* </DirectionProvider>
|
|
28
28
|
* ```
|
|
29
29
|
*
|
|
@@ -39,7 +39,7 @@ import { DirectionProviderProps } from "./types";
|
|
|
39
39
|
* import { DirectionProvider } from "@primitiv-ui/react";
|
|
40
40
|
*
|
|
41
41
|
* <DirectionProvider dir="rtl">
|
|
42
|
-
* <Tabs.Root defaultValue="overview"
|
|
42
|
+
* <Tabs.Root defaultValue="overview">...</Tabs.Root>
|
|
43
43
|
* <Slider.Root defaultValue={[50]} aria-label="Volume" />
|
|
44
44
|
* </DirectionProvider>
|
|
45
45
|
* ```
|
|
@@ -47,9 +47,9 @@ import { DirectionProviderProps } from "./types";
|
|
|
47
47
|
* @example Nested providers — inner overrides outer for its subtree
|
|
48
48
|
* ```tsx
|
|
49
49
|
* <DirectionProvider dir="rtl">
|
|
50
|
-
* <Tabs.Root defaultValue="a"
|
|
50
|
+
* <Tabs.Root defaultValue="a">...</Tabs.Root>
|
|
51
51
|
* <DirectionProvider dir="ltr">
|
|
52
|
-
* <Tabs.Root defaultValue="b"
|
|
52
|
+
* <Tabs.Root defaultValue="b">...</Tabs.Root>
|
|
53
53
|
* </DirectionProvider>
|
|
54
54
|
* </DirectionProvider>
|
|
55
55
|
* ```
|
|
@@ -12,7 +12,7 @@ Renders **no DOM** — only a React context provider.
|
|
|
12
12
|
import { DirectionProvider } from "@primitiv-ui/react";
|
|
13
13
|
|
|
14
14
|
<DirectionProvider dir="rtl">
|
|
15
|
-
<Tabs.Root defaultValue="overview"
|
|
15
|
+
<Tabs.Root defaultValue="overview">...</Tabs.Root>
|
|
16
16
|
<Slider.Root defaultValue={[50]} aria-label="Volume" />
|
|
17
17
|
</DirectionProvider>;
|
|
18
18
|
```
|
|
@@ -23,9 +23,9 @@ components fall back to the provider only when `dir` is omitted:
|
|
|
23
23
|
```tsx
|
|
24
24
|
<DirectionProvider dir="rtl">
|
|
25
25
|
{/* inherits rtl */}
|
|
26
|
-
<Tabs.Root defaultValue="a"
|
|
26
|
+
<Tabs.Root defaultValue="a">...</Tabs.Root>
|
|
27
27
|
{/* explicit ltr overrides the provider */}
|
|
28
|
-
<Tabs.Root dir="ltr" defaultValue="a"
|
|
28
|
+
<Tabs.Root dir="ltr" defaultValue="a">...</Tabs.Root>
|
|
29
29
|
</DirectionProvider>;
|
|
30
30
|
```
|
|
31
31
|
|
|
@@ -48,7 +48,7 @@ import { useDirection } from "@primitiv-ui/react";
|
|
|
48
48
|
|
|
49
49
|
function MyControl({ dir }: { dir?: "ltr" | "rtl" }) {
|
|
50
50
|
const resolvedDir = dir ?? useDirection();
|
|
51
|
-
//
|
|
51
|
+
// ...
|
|
52
52
|
}
|
|
53
53
|
```
|
|
54
54
|
|
|
@@ -12,7 +12,7 @@ import { Divider } from "../Divider";
|
|
|
12
12
|
* and asserted against the rendered headless component* so it can never drift
|
|
13
13
|
* from what the component actually emits. Divider has **no** data-* surface — it
|
|
14
14
|
* styles off `aria-orientation` — so the guard also pins that hook: the registry
|
|
15
|
-
* stylesheet's `[aria-orientation="
|
|
15
|
+
* stylesheet's `[aria-orientation="..."]` selectors depend on it.
|
|
16
16
|
*/
|
|
17
17
|
const contractPath = resolve(
|
|
18
18
|
dirname(fileURLToPath(import.meta.url)),
|