@dowel-ui/react 0.1.0 → 0.1.1
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/dist/components/ai-agent-status/ai-agent-status.d.ts +1 -1
- package/dist/components/ai-tool/ai-tool.d.ts +1 -1
- package/dist/components/alert/alert.d.ts +1 -1
- package/dist/components/avatar/avatar.d.ts +1 -1
- package/dist/components/badge/badge.d.ts +1 -1
- package/dist/components/button/button.d.ts +1 -1
- package/dist/components/progress/progress.d.ts +1 -1
- package/dist/components/spinner/spinner.d.ts +1 -1
- package/dist/components/toast/toast.d.ts +1 -1
- package/package.json +7 -4
- package/src/blocks/admin-users/admin-users.stories.tsx +0 -83
- package/src/blocks/admin-users/admin-users.test.tsx +0 -159
- package/src/blocks/ai-chat/ai-chat.stories.tsx +0 -149
- package/src/blocks/ai-chat/ai-chat.test.tsx +0 -166
- package/src/blocks/dashboard/dashboard.stories.tsx +0 -106
- package/src/blocks/dashboard/dashboard.test.tsx +0 -94
- package/src/blocks/forgot-password/forgot-password.stories.tsx +0 -41
- package/src/blocks/forgot-password/forgot-password.test.tsx +0 -62
- package/src/blocks/login/login.stories.tsx +0 -46
- package/src/blocks/login/login.test.tsx +0 -100
- package/src/blocks/pricing/pricing.stories.tsx +0 -62
- package/src/blocks/pricing/pricing.test.tsx +0 -101
- package/src/blocks/settings/settings.stories.tsx +0 -94
- package/src/blocks/settings/settings.test.tsx +0 -167
- package/src/blocks/signup/signup.stories.tsx +0 -27
- package/src/blocks/signup/signup.test.tsx +0 -97
- package/src/components/accordion/accordion.stories.tsx +0 -90
- package/src/components/accordion/accordion.test.tsx +0 -156
- package/src/components/activity-feed/activity-feed.stories.tsx +0 -163
- package/src/components/activity-feed/activity-feed.test.tsx +0 -125
- package/src/components/ai-agent-status/ai-agent-status.stories.tsx +0 -86
- package/src/components/ai-agent-status/ai-agent-status.test.tsx +0 -66
- package/src/components/ai-conversation/ai-conversation.stories.tsx +0 -126
- package/src/components/ai-conversation/ai-conversation.test.tsx +0 -144
- package/src/components/ai-message/ai-message.stories.tsx +0 -106
- package/src/components/ai-message/ai-message.test.tsx +0 -160
- package/src/components/ai-model-selector/ai-model-selector.stories.tsx +0 -66
- package/src/components/ai-model-selector/ai-model-selector.test.tsx +0 -120
- package/src/components/ai-prompt-input/ai-prompt-input.stories.tsx +0 -145
- package/src/components/ai-prompt-input/ai-prompt-input.test.tsx +0 -273
- package/src/components/ai-reasoning/ai-reasoning.stories.tsx +0 -60
- package/src/components/ai-reasoning/ai-reasoning.test.tsx +0 -95
- package/src/components/ai-response/ai-response.stories.tsx +0 -75
- package/src/components/ai-response/ai-response.test.tsx +0 -96
- package/src/components/ai-sources/ai-sources.stories.tsx +0 -93
- package/src/components/ai-sources/ai-sources.test.tsx +0 -102
- package/src/components/ai-token-usage/ai-token-usage.stories.tsx +0 -59
- package/src/components/ai-token-usage/ai-token-usage.test.tsx +0 -83
- package/src/components/ai-tool/ai-tool.stories.tsx +0 -96
- package/src/components/ai-tool/ai-tool.test.tsx +0 -137
- package/src/components/alert/alert.stories.tsx +0 -101
- package/src/components/alert/alert.test.tsx +0 -94
- package/src/components/avatar/avatar.stories.tsx +0 -58
- package/src/components/avatar/avatar.test.tsx +0 -64
- package/src/components/badge/badge.stories.tsx +0 -68
- package/src/components/badge/badge.test.tsx +0 -61
- package/src/components/button/button.stories.tsx +0 -128
- package/src/components/button/button.test.tsx +0 -224
- package/src/components/calendar/calendar.stories.tsx +0 -105
- package/src/components/calendar/calendar.test.tsx +0 -255
- package/src/components/card/card.stories.tsx +0 -61
- package/src/components/card/card.test.tsx +0 -66
- package/src/components/checkbox/checkbox.stories.tsx +0 -124
- package/src/components/checkbox/checkbox.test.tsx +0 -121
- package/src/components/code-block/code-block.stories.tsx +0 -62
- package/src/components/code-block/code-block.test.tsx +0 -139
- package/src/components/combobox/combobox.stories.tsx +0 -171
- package/src/components/combobox/combobox.test.tsx +0 -429
- package/src/components/command/command.stories.tsx +0 -152
- package/src/components/command/command.test.tsx +0 -380
- package/src/components/data-table/data-table.stories.tsx +0 -151
- package/src/components/data-table/data-table.test.tsx +0 -288
- package/src/components/date-picker/date-picker.stories.tsx +0 -103
- package/src/components/date-picker/date-picker.test.tsx +0 -205
- package/src/components/dialog/dialog.stories.tsx +0 -140
- package/src/components/dialog/dialog.test.tsx +0 -186
- package/src/components/drawer/drawer.stories.tsx +0 -111
- package/src/components/drawer/drawer.test.tsx +0 -187
- package/src/components/dropdown-menu/dropdown-menu.stories.tsx +0 -128
- package/src/components/dropdown-menu/dropdown-menu.test.tsx +0 -321
- package/src/components/empty-state/empty-state.stories.tsx +0 -70
- package/src/components/empty-state/empty-state.test.tsx +0 -97
- package/src/components/form/form.stories.tsx +0 -144
- package/src/components/form/form.test.tsx +0 -203
- package/src/components/input/input.stories.tsx +0 -84
- package/src/components/input/input.test.tsx +0 -108
- package/src/components/label/label.stories.tsx +0 -51
- package/src/components/label/label.test.tsx +0 -55
- package/src/components/pagination/pagination.stories.tsx +0 -129
- package/src/components/pagination/pagination.test.tsx +0 -119
- package/src/components/popover/popover.stories.tsx +0 -84
- package/src/components/popover/popover.test.tsx +0 -163
- package/src/components/progress/progress.stories.tsx +0 -90
- package/src/components/progress/progress.test.tsx +0 -78
- package/src/components/radio-group/radio-group.stories.tsx +0 -102
- package/src/components/radio-group/radio-group.test.tsx +0 -152
- package/src/components/select/select.stories.tsx +0 -137
- package/src/components/select/select.test.tsx +0 -209
- package/src/components/separator/separator.stories.tsx +0 -42
- package/src/components/separator/separator.test.tsx +0 -46
- package/src/components/sheet/sheet.stories.tsx +0 -107
- package/src/components/sheet/sheet.test.tsx +0 -152
- package/src/components/skeleton/skeleton.stories.tsx +0 -46
- package/src/components/skeleton/skeleton.test.tsx +0 -57
- package/src/components/slider/slider.stories.tsx +0 -87
- package/src/components/slider/slider.test.tsx +0 -148
- package/src/components/spinner/spinner.stories.tsx +0 -47
- package/src/components/spinner/spinner.test.tsx +0 -41
- package/src/components/switch/switch.stories.tsx +0 -70
- package/src/components/switch/switch.test.tsx +0 -101
- package/src/components/table/table.stories.tsx +0 -131
- package/src/components/table/table.test.tsx +0 -153
- package/src/components/tabs/tabs.stories.tsx +0 -139
- package/src/components/tabs/tabs.test.tsx +0 -148
- package/src/components/toast/toast.stories.tsx +0 -167
- package/src/components/toast/toast.test.tsx +0 -306
- package/src/components/tooltip/tooltip.stories.tsx +0 -89
- package/src/components/tooltip/tooltip.test.tsx +0 -120
- package/src/lib/utils.test.ts +0 -32
- package/src/registry/meta.test.ts +0 -234
|
@@ -1,429 +0,0 @@
|
|
|
1
|
-
import { render, screen, waitFor } from "@testing-library/react";
|
|
2
|
-
import userEvent from "@testing-library/user-event";
|
|
3
|
-
import { useState } from "react";
|
|
4
|
-
import { describe, expect, it, vi } from "vitest";
|
|
5
|
-
|
|
6
|
-
import { expectNoA11yViolations } from "../../../test/a11y";
|
|
7
|
-
import {
|
|
8
|
-
Combobox,
|
|
9
|
-
ComboboxContent,
|
|
10
|
-
ComboboxEmpty,
|
|
11
|
-
ComboboxInput,
|
|
12
|
-
ComboboxItem,
|
|
13
|
-
ComboboxList,
|
|
14
|
-
ComboboxTrigger,
|
|
15
|
-
defaultComboboxFilter,
|
|
16
|
-
} from "./combobox";
|
|
17
|
-
|
|
18
|
-
const FRAMEWORKS = [
|
|
19
|
-
{ value: "Next.js", keywords: ["vercel", "react"] },
|
|
20
|
-
{ value: "SvelteKit", keywords: ["svelte"] },
|
|
21
|
-
{ value: "Nuxt", keywords: ["vue"] },
|
|
22
|
-
{ value: "Remix", keywords: ["react"] },
|
|
23
|
-
];
|
|
24
|
-
|
|
25
|
-
function Example({
|
|
26
|
-
onValueChange,
|
|
27
|
-
defaultValue,
|
|
28
|
-
disabledValue,
|
|
29
|
-
}: {
|
|
30
|
-
onValueChange?: (value: string) => void;
|
|
31
|
-
defaultValue?: string;
|
|
32
|
-
disabledValue?: string;
|
|
33
|
-
} = {}) {
|
|
34
|
-
return (
|
|
35
|
-
<Combobox defaultValue={defaultValue} onValueChange={onValueChange}>
|
|
36
|
-
<ComboboxTrigger placeholder="Select framework…" />
|
|
37
|
-
<ComboboxContent label="Search frameworks">
|
|
38
|
-
<ComboboxInput placeholder="Search framework…" aria-label="Search framework" />
|
|
39
|
-
<ComboboxEmpty>No framework found.</ComboboxEmpty>
|
|
40
|
-
<ComboboxList>
|
|
41
|
-
{FRAMEWORKS.map((framework) => (
|
|
42
|
-
<ComboboxItem
|
|
43
|
-
key={framework.value}
|
|
44
|
-
value={framework.value}
|
|
45
|
-
keywords={framework.keywords}
|
|
46
|
-
disabled={framework.value === disabledValue}
|
|
47
|
-
/>
|
|
48
|
-
))}
|
|
49
|
-
</ComboboxList>
|
|
50
|
-
</ComboboxContent>
|
|
51
|
-
</Combobox>
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async function open() {
|
|
56
|
-
const user = userEvent.setup();
|
|
57
|
-
render(<Example />);
|
|
58
|
-
await user.click(screen.getByRole("button"));
|
|
59
|
-
const input = await screen.findByRole("combobox");
|
|
60
|
-
return { user, input };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
describe("defaultComboboxFilter", () => {
|
|
64
|
-
it("matches case-insensitively on any entry", () => {
|
|
65
|
-
expect(defaultComboboxFilter("nex", ["Next.js", "react"])).toBe(true);
|
|
66
|
-
expect(defaultComboboxFilter("REACT", ["Next.js", "react"])).toBe(true);
|
|
67
|
-
expect(defaultComboboxFilter("vue", ["Next.js", "react"])).toBe(false);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("matches everything on an empty search", () => {
|
|
71
|
-
expect(defaultComboboxFilter(" ", ["anything"])).toBe(true);
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
describe("Combobox", () => {
|
|
76
|
-
it("shows the placeholder until something is selected", () => {
|
|
77
|
-
render(<Example />);
|
|
78
|
-
expect(screen.getByRole("button")).toHaveTextContent("Select framework…");
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("shows the selected value on the trigger", () => {
|
|
82
|
-
render(<Example defaultValue="Nuxt" />);
|
|
83
|
-
expect(screen.getByRole("button")).toHaveTextContent("Nuxt");
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("is closed until the trigger is activated", async () => {
|
|
87
|
-
const user = userEvent.setup();
|
|
88
|
-
render(<Example />);
|
|
89
|
-
|
|
90
|
-
expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
|
|
91
|
-
expect(screen.getByRole("button")).toHaveAttribute("aria-expanded", "false");
|
|
92
|
-
|
|
93
|
-
await user.click(screen.getByRole("button"));
|
|
94
|
-
expect(await screen.findByRole("listbox")).toBeInTheDocument();
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
describe("ARIA wiring", () => {
|
|
98
|
-
it("puts the combobox role on the input, not the trigger", async () => {
|
|
99
|
-
const { input } = await open();
|
|
100
|
-
|
|
101
|
-
expect(input.tagName).toBe("INPUT");
|
|
102
|
-
expect(input).toHaveAttribute("aria-expanded", "true");
|
|
103
|
-
expect(input).toHaveAttribute("aria-autocomplete", "list");
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it("points the input at the listbox it controls", async () => {
|
|
107
|
-
const { input } = await open();
|
|
108
|
-
const list = screen.getByRole("listbox");
|
|
109
|
-
expect(input).toHaveAttribute("aria-controls", list.id);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it("moves focus into the input, not onto an option", async () => {
|
|
113
|
-
const { input } = await open();
|
|
114
|
-
await waitFor(() => {
|
|
115
|
-
expect(input).toHaveFocus();
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it("tracks the active option with aria-activedescendant", async () => {
|
|
120
|
-
const { user, input } = await open();
|
|
121
|
-
expect(input).not.toHaveAttribute("aria-activedescendant");
|
|
122
|
-
|
|
123
|
-
await user.keyboard("{ArrowDown}");
|
|
124
|
-
|
|
125
|
-
const active = screen.getByRole("option", { name: "Next.js" });
|
|
126
|
-
expect(input).toHaveAttribute("aria-activedescendant", active.id);
|
|
127
|
-
// Focus must stay in the input the whole time.
|
|
128
|
-
expect(input).toHaveFocus();
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it("keeps options out of the tab order", async () => {
|
|
132
|
-
await open();
|
|
133
|
-
for (const option of screen.getAllByRole("option")) {
|
|
134
|
-
expect(option).toHaveAttribute("tabindex", "-1");
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it("marks the selected option", async () => {
|
|
139
|
-
const user = userEvent.setup();
|
|
140
|
-
render(<Example defaultValue="Nuxt" />);
|
|
141
|
-
await user.click(screen.getByRole("button"));
|
|
142
|
-
|
|
143
|
-
expect(await screen.findByRole("option", { name: "Nuxt" })).toHaveAttribute(
|
|
144
|
-
"aria-selected",
|
|
145
|
-
"true",
|
|
146
|
-
);
|
|
147
|
-
expect(screen.getByRole("option", { name: "Remix" })).toHaveAttribute(
|
|
148
|
-
"aria-selected",
|
|
149
|
-
"false",
|
|
150
|
-
);
|
|
151
|
-
});
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
describe("filtering", () => {
|
|
155
|
-
it("narrows the list as you type", async () => {
|
|
156
|
-
const { user } = await open();
|
|
157
|
-
expect(screen.getAllByRole("option")).toHaveLength(4);
|
|
158
|
-
|
|
159
|
-
await user.keyboard("nu");
|
|
160
|
-
expect(screen.getAllByRole("option")).toHaveLength(1);
|
|
161
|
-
expect(screen.getByRole("option", { name: "Nuxt" })).toBeInTheDocument();
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it("matches hidden keywords as well as the visible value", async () => {
|
|
165
|
-
const { user } = await open();
|
|
166
|
-
|
|
167
|
-
await user.keyboard("vercel");
|
|
168
|
-
expect(screen.getAllByRole("option")).toHaveLength(1);
|
|
169
|
-
expect(screen.getByRole("option", { name: "Next.js" })).toBeInTheDocument();
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it("shows the empty state only when nothing matches", async () => {
|
|
173
|
-
const { user } = await open();
|
|
174
|
-
expect(screen.queryByText("No framework found.")).not.toBeInTheDocument();
|
|
175
|
-
|
|
176
|
-
await user.keyboard("zzz");
|
|
177
|
-
expect(screen.getByText("No framework found.")).toBeInTheDocument();
|
|
178
|
-
expect(screen.queryAllByRole("option")).toHaveLength(0);
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it("accepts a custom filter", async () => {
|
|
182
|
-
const user = userEvent.setup();
|
|
183
|
-
render(
|
|
184
|
-
<Combobox
|
|
185
|
-
filter={(search, haystack) => haystack.some((entry) => entry.startsWith(search))}
|
|
186
|
-
>
|
|
187
|
-
<ComboboxTrigger />
|
|
188
|
-
<ComboboxContent>
|
|
189
|
-
<ComboboxInput aria-label="Search" />
|
|
190
|
-
<ComboboxList>
|
|
191
|
-
<ComboboxItem value="Alpha" />
|
|
192
|
-
<ComboboxItem value="Beta" />
|
|
193
|
-
</ComboboxList>
|
|
194
|
-
</ComboboxContent>
|
|
195
|
-
</Combobox>,
|
|
196
|
-
);
|
|
197
|
-
|
|
198
|
-
await user.click(screen.getByRole("button"));
|
|
199
|
-
await user.keyboard("Al");
|
|
200
|
-
expect(screen.getAllByRole("option")).toHaveLength(1);
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it("clears the search when reopened", async () => {
|
|
204
|
-
const { user, input } = await open();
|
|
205
|
-
await user.keyboard("nu");
|
|
206
|
-
expect(screen.getAllByRole("option")).toHaveLength(1);
|
|
207
|
-
|
|
208
|
-
await user.keyboard("{Escape}");
|
|
209
|
-
await waitFor(() => {
|
|
210
|
-
expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
await user.click(screen.getByRole("button"));
|
|
214
|
-
const reopened = await screen.findByRole("combobox");
|
|
215
|
-
expect(reopened).toHaveValue("");
|
|
216
|
-
expect(screen.getAllByRole("option")).toHaveLength(4);
|
|
217
|
-
expect(input).not.toBe(null);
|
|
218
|
-
});
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
describe("keyboard", () => {
|
|
222
|
-
it("moves down and up through the options", async () => {
|
|
223
|
-
const { user, input } = await open();
|
|
224
|
-
|
|
225
|
-
await user.keyboard("{ArrowDown}{ArrowDown}");
|
|
226
|
-
expect(input).toHaveAttribute(
|
|
227
|
-
"aria-activedescendant",
|
|
228
|
-
screen.getByRole("option", { name: "SvelteKit" }).id,
|
|
229
|
-
);
|
|
230
|
-
|
|
231
|
-
await user.keyboard("{ArrowUp}");
|
|
232
|
-
expect(input).toHaveAttribute(
|
|
233
|
-
"aria-activedescendant",
|
|
234
|
-
screen.getByRole("option", { name: "Next.js" }).id,
|
|
235
|
-
);
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
it("wraps at both ends", async () => {
|
|
239
|
-
const { user, input } = await open();
|
|
240
|
-
|
|
241
|
-
await user.keyboard("{ArrowUp}");
|
|
242
|
-
expect(input).toHaveAttribute(
|
|
243
|
-
"aria-activedescendant",
|
|
244
|
-
screen.getByRole("option", { name: "Remix" }).id,
|
|
245
|
-
);
|
|
246
|
-
|
|
247
|
-
await user.keyboard("{ArrowDown}");
|
|
248
|
-
expect(input).toHaveAttribute(
|
|
249
|
-
"aria-activedescendant",
|
|
250
|
-
screen.getByRole("option", { name: "Next.js" }).id,
|
|
251
|
-
);
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
it("jumps to the ends with Home and End", async () => {
|
|
255
|
-
const { user, input } = await open();
|
|
256
|
-
|
|
257
|
-
await user.keyboard("{End}");
|
|
258
|
-
expect(input).toHaveAttribute(
|
|
259
|
-
"aria-activedescendant",
|
|
260
|
-
screen.getByRole("option", { name: "Remix" }).id,
|
|
261
|
-
);
|
|
262
|
-
|
|
263
|
-
await user.keyboard("{Home}");
|
|
264
|
-
expect(input).toHaveAttribute(
|
|
265
|
-
"aria-activedescendant",
|
|
266
|
-
screen.getByRole("option", { name: "Next.js" }).id,
|
|
267
|
-
);
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
it("selects the active option with Enter", async () => {
|
|
271
|
-
const onValueChange = vi.fn();
|
|
272
|
-
const user = userEvent.setup();
|
|
273
|
-
render(<Example onValueChange={onValueChange} />);
|
|
274
|
-
|
|
275
|
-
await user.click(screen.getByRole("button"));
|
|
276
|
-
await screen.findByRole("combobox");
|
|
277
|
-
await user.keyboard("{ArrowDown}{Enter}");
|
|
278
|
-
|
|
279
|
-
expect(onValueChange).toHaveBeenCalledWith("Next.js");
|
|
280
|
-
await waitFor(() => {
|
|
281
|
-
expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
|
|
282
|
-
});
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
it("does nothing on Enter when no option is active", async () => {
|
|
286
|
-
const onValueChange = vi.fn();
|
|
287
|
-
const user = userEvent.setup();
|
|
288
|
-
render(<Example onValueChange={onValueChange} />);
|
|
289
|
-
|
|
290
|
-
await user.click(screen.getByRole("button"));
|
|
291
|
-
await screen.findByRole("combobox");
|
|
292
|
-
await user.keyboard("{Enter}");
|
|
293
|
-
|
|
294
|
-
expect(onValueChange).not.toHaveBeenCalled();
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
it("closes on Escape", async () => {
|
|
298
|
-
const { user } = await open();
|
|
299
|
-
await user.keyboard("{Escape}");
|
|
300
|
-
await waitFor(() => {
|
|
301
|
-
expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
|
|
302
|
-
});
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
it("skips disabled options when navigating", async () => {
|
|
306
|
-
const user = userEvent.setup();
|
|
307
|
-
render(<Example disabledValue="SvelteKit" />);
|
|
308
|
-
|
|
309
|
-
await user.click(screen.getByRole("button"));
|
|
310
|
-
const input = await screen.findByRole("combobox");
|
|
311
|
-
|
|
312
|
-
await user.keyboard("{ArrowDown}{ArrowDown}");
|
|
313
|
-
expect(input).toHaveAttribute(
|
|
314
|
-
"aria-activedescendant",
|
|
315
|
-
screen.getByRole("option", { name: "Nuxt" }).id,
|
|
316
|
-
);
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
it("resets the active option when the search changes", async () => {
|
|
320
|
-
const { user, input } = await open();
|
|
321
|
-
|
|
322
|
-
await user.keyboard("{ArrowDown}");
|
|
323
|
-
expect(input).toHaveAttribute("aria-activedescendant");
|
|
324
|
-
|
|
325
|
-
await user.keyboard("n");
|
|
326
|
-
expect(input).not.toHaveAttribute("aria-activedescendant");
|
|
327
|
-
});
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
describe("selection", () => {
|
|
331
|
-
it("selects on click and closes", async () => {
|
|
332
|
-
const onValueChange = vi.fn();
|
|
333
|
-
const user = userEvent.setup();
|
|
334
|
-
render(<Example onValueChange={onValueChange} />);
|
|
335
|
-
|
|
336
|
-
await user.click(screen.getByRole("button"));
|
|
337
|
-
await user.click(await screen.findByRole("option", { name: "Nuxt" }));
|
|
338
|
-
|
|
339
|
-
expect(onValueChange).toHaveBeenCalledWith("Nuxt");
|
|
340
|
-
await waitFor(() => {
|
|
341
|
-
expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
|
|
342
|
-
});
|
|
343
|
-
expect(screen.getByRole("button")).toHaveTextContent("Nuxt");
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
it("ignores clicks on a disabled option", async () => {
|
|
347
|
-
const onValueChange = vi.fn();
|
|
348
|
-
const user = userEvent.setup();
|
|
349
|
-
render(<Example onValueChange={onValueChange} disabledValue="Nuxt" />);
|
|
350
|
-
|
|
351
|
-
await user.click(screen.getByRole("button"));
|
|
352
|
-
await user.click(await screen.findByRole("option", { name: "Nuxt" }));
|
|
353
|
-
|
|
354
|
-
expect(onValueChange).not.toHaveBeenCalled();
|
|
355
|
-
expect(screen.getByRole("listbox")).toBeInTheDocument();
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
it("works controlled", async () => {
|
|
359
|
-
const user = userEvent.setup();
|
|
360
|
-
|
|
361
|
-
function Controlled() {
|
|
362
|
-
const [value, setValue] = useState("Nuxt");
|
|
363
|
-
return (
|
|
364
|
-
<Combobox value={value} onValueChange={setValue}>
|
|
365
|
-
<ComboboxTrigger />
|
|
366
|
-
<ComboboxContent>
|
|
367
|
-
<ComboboxInput aria-label="Search" />
|
|
368
|
-
<ComboboxList>
|
|
369
|
-
<ComboboxItem value="Nuxt" />
|
|
370
|
-
<ComboboxItem value="Remix" />
|
|
371
|
-
</ComboboxList>
|
|
372
|
-
</ComboboxContent>
|
|
373
|
-
</Combobox>
|
|
374
|
-
);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
render(<Controlled />);
|
|
378
|
-
expect(screen.getByRole("button")).toHaveTextContent("Nuxt");
|
|
379
|
-
|
|
380
|
-
await user.click(screen.getByRole("button"));
|
|
381
|
-
await user.click(await screen.findByRole("option", { name: "Remix" }));
|
|
382
|
-
|
|
383
|
-
await waitFor(() => {
|
|
384
|
-
expect(screen.getByRole("button")).toHaveTextContent("Remix");
|
|
385
|
-
});
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
it("renders rich children while still matching on value", async () => {
|
|
389
|
-
const user = userEvent.setup();
|
|
390
|
-
render(
|
|
391
|
-
<Combobox>
|
|
392
|
-
<ComboboxTrigger />
|
|
393
|
-
<ComboboxContent>
|
|
394
|
-
<ComboboxInput aria-label="Search" />
|
|
395
|
-
<ComboboxList>
|
|
396
|
-
<ComboboxItem value="Nuxt">
|
|
397
|
-
<span>Nuxt</span>
|
|
398
|
-
<span className="text-muted-foreground">Vue</span>
|
|
399
|
-
</ComboboxItem>
|
|
400
|
-
<ComboboxItem value="Remix" />
|
|
401
|
-
</ComboboxList>
|
|
402
|
-
</ComboboxContent>
|
|
403
|
-
</Combobox>,
|
|
404
|
-
);
|
|
405
|
-
|
|
406
|
-
await user.click(screen.getByRole("button"));
|
|
407
|
-
await user.keyboard("nux");
|
|
408
|
-
expect(screen.getAllByRole("option")).toHaveLength(1);
|
|
409
|
-
});
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
it("throws a useful error if a part is used outside the root", () => {
|
|
413
|
-
const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
414
|
-
expect(() => render(<ComboboxList />)).toThrow(
|
|
415
|
-
/ComboboxList must be rendered inside <Combobox>/,
|
|
416
|
-
);
|
|
417
|
-
consoleError.mockRestore();
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
it("has no accessibility violations while open", async () => {
|
|
421
|
-
const user = userEvent.setup();
|
|
422
|
-
const { baseElement } = render(<Example />);
|
|
423
|
-
|
|
424
|
-
await user.click(screen.getByRole("button"));
|
|
425
|
-
await screen.findByRole("listbox");
|
|
426
|
-
|
|
427
|
-
await expectNoA11yViolations(baseElement);
|
|
428
|
-
});
|
|
429
|
-
});
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
-
import { Calculator, Calendar, CreditCard, Settings, Smile, User } from "lucide-react";
|
|
3
|
-
import { useEffect, useState } from "react";
|
|
4
|
-
|
|
5
|
-
import { Button } from "@/components/button";
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
Command,
|
|
9
|
-
CommandDialog,
|
|
10
|
-
CommandEmpty,
|
|
11
|
-
CommandGroup,
|
|
12
|
-
CommandInput,
|
|
13
|
-
CommandItem,
|
|
14
|
-
CommandList,
|
|
15
|
-
CommandSeparator,
|
|
16
|
-
CommandShortcut,
|
|
17
|
-
} from "./command";
|
|
18
|
-
|
|
19
|
-
const meta: Meta<typeof Command> = {
|
|
20
|
-
title: "Navigation/Command",
|
|
21
|
-
component: Command,
|
|
22
|
-
parameters: { controls: { disable: true } },
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export default meta;
|
|
26
|
-
type Story = StoryObj<typeof Command>;
|
|
27
|
-
|
|
28
|
-
function Items() {
|
|
29
|
-
return (
|
|
30
|
-
<>
|
|
31
|
-
<CommandEmpty>No results found.</CommandEmpty>
|
|
32
|
-
<CommandGroup heading="Suggestions">
|
|
33
|
-
<CommandItem value="Calendar" keywords={["date", "schedule"]}>
|
|
34
|
-
<Calendar />
|
|
35
|
-
Calendar
|
|
36
|
-
</CommandItem>
|
|
37
|
-
<CommandItem value="Search Emoji" keywords={["icon", "smiley"]}>
|
|
38
|
-
<Smile />
|
|
39
|
-
Search Emoji
|
|
40
|
-
</CommandItem>
|
|
41
|
-
<CommandItem value="Calculator" disabled>
|
|
42
|
-
<Calculator />
|
|
43
|
-
Calculator
|
|
44
|
-
</CommandItem>
|
|
45
|
-
</CommandGroup>
|
|
46
|
-
<CommandSeparator />
|
|
47
|
-
<CommandGroup heading="Settings">
|
|
48
|
-
<CommandItem value="Profile" keywords={["account", "me"]}>
|
|
49
|
-
<User />
|
|
50
|
-
Profile
|
|
51
|
-
<CommandShortcut>⌘P</CommandShortcut>
|
|
52
|
-
</CommandItem>
|
|
53
|
-
<CommandItem value="Billing" keywords={["invoice", "payment"]}>
|
|
54
|
-
<CreditCard />
|
|
55
|
-
Billing
|
|
56
|
-
<CommandShortcut>⌘B</CommandShortcut>
|
|
57
|
-
</CommandItem>
|
|
58
|
-
<CommandItem value="Settings">
|
|
59
|
-
<Settings />
|
|
60
|
-
Settings
|
|
61
|
-
<CommandShortcut>⌘S</CommandShortcut>
|
|
62
|
-
</CommandItem>
|
|
63
|
-
</CommandGroup>
|
|
64
|
-
</>
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** Try typing "schedule" or "invoice" — keywords are searchable but not shown. */
|
|
69
|
-
export const Default: Story = {
|
|
70
|
-
render: () => (
|
|
71
|
-
<Command className="w-96 rounded-lg border border-border shadow-md">
|
|
72
|
-
<CommandInput placeholder="Type a command or search…" aria-label="Search commands" />
|
|
73
|
-
<CommandList>
|
|
74
|
-
<Items />
|
|
75
|
-
</CommandList>
|
|
76
|
-
</Command>
|
|
77
|
-
),
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
/** Groups whose items all filter out hide their heading rather than labelling nothing. */
|
|
81
|
-
export const Filtering: Story = {
|
|
82
|
-
render: () => (
|
|
83
|
-
<div className="grid gap-3">
|
|
84
|
-
<p className="text-xs text-muted-foreground">
|
|
85
|
-
Type “bill” — the Suggestions group hides.
|
|
86
|
-
</p>
|
|
87
|
-
<Command className="w-96 rounded-lg border border-border shadow-md">
|
|
88
|
-
<CommandInput placeholder="Search…" aria-label="Search commands" />
|
|
89
|
-
<CommandList>
|
|
90
|
-
<Items />
|
|
91
|
-
</CommandList>
|
|
92
|
-
</Command>
|
|
93
|
-
</div>
|
|
94
|
-
),
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
/** The ⌘K pattern. The dialog is always named, even though the title is hidden. */
|
|
98
|
-
export const Palette: Story = {
|
|
99
|
-
render: function Palette() {
|
|
100
|
-
const [open, setOpen] = useState(false);
|
|
101
|
-
|
|
102
|
-
useEffect(() => {
|
|
103
|
-
function onKeyDown(event: KeyboardEvent) {
|
|
104
|
-
if (event.key === "k" && (event.metaKey || event.ctrlKey)) {
|
|
105
|
-
event.preventDefault();
|
|
106
|
-
setOpen((current) => !current);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
document.addEventListener("keydown", onKeyDown);
|
|
111
|
-
return () => {
|
|
112
|
-
document.removeEventListener("keydown", onKeyDown);
|
|
113
|
-
};
|
|
114
|
-
}, []);
|
|
115
|
-
|
|
116
|
-
return (
|
|
117
|
-
<div className="grid gap-3">
|
|
118
|
-
<Button
|
|
119
|
-
variant="outline"
|
|
120
|
-
onClick={() => {
|
|
121
|
-
setOpen(true);
|
|
122
|
-
}}
|
|
123
|
-
>
|
|
124
|
-
Open palette
|
|
125
|
-
<CommandShortcut className="ml-2">⌘K</CommandShortcut>
|
|
126
|
-
</Button>
|
|
127
|
-
<p className="text-xs text-muted-foreground">Or press ⌘K / Ctrl+K.</p>
|
|
128
|
-
<CommandDialog open={open} onOpenChange={setOpen}>
|
|
129
|
-
<CommandInput placeholder="Type a command or search…" aria-label="Search commands" />
|
|
130
|
-
<CommandList>
|
|
131
|
-
<Items />
|
|
132
|
-
</CommandList>
|
|
133
|
-
</CommandDialog>
|
|
134
|
-
</div>
|
|
135
|
-
);
|
|
136
|
-
},
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
export const Empty: Story = {
|
|
140
|
-
render: () => (
|
|
141
|
-
<Command className="w-96 rounded-lg border border-border shadow-md">
|
|
142
|
-
<CommandInput
|
|
143
|
-
placeholder="Search…"
|
|
144
|
-
aria-label="Search commands"
|
|
145
|
-
defaultValue="nothing matches this"
|
|
146
|
-
/>
|
|
147
|
-
<CommandList>
|
|
148
|
-
<Items />
|
|
149
|
-
</CommandList>
|
|
150
|
-
</Command>
|
|
151
|
-
),
|
|
152
|
-
};
|