@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,102 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
-
|
|
3
|
-
import { Label } from "@/components/label";
|
|
4
|
-
|
|
5
|
-
import { RadioGroup, RadioGroupItem } from "./radio-group";
|
|
6
|
-
|
|
7
|
-
const meta = {
|
|
8
|
-
title: "Forms/Radio Group",
|
|
9
|
-
component: RadioGroup,
|
|
10
|
-
parameters: { controls: { disable: true } },
|
|
11
|
-
} satisfies Meta<typeof RadioGroup>;
|
|
12
|
-
|
|
13
|
-
export default meta;
|
|
14
|
-
type Story = StoryObj<typeof meta>;
|
|
15
|
-
|
|
16
|
-
export const Default: Story = {
|
|
17
|
-
render: () => (
|
|
18
|
-
<fieldset>
|
|
19
|
-
<legend className="mb-3 text-sm font-medium">Density</legend>
|
|
20
|
-
<RadioGroup defaultValue="comfortable">
|
|
21
|
-
{[
|
|
22
|
-
{ value: "default", label: "Default" },
|
|
23
|
-
{ value: "comfortable", label: "Comfortable" },
|
|
24
|
-
{ value: "compact", label: "Compact" },
|
|
25
|
-
].map((option) => (
|
|
26
|
-
<div key={option.value} className="flex items-center gap-2">
|
|
27
|
-
<RadioGroupItem value={option.value} id={option.value} />
|
|
28
|
-
<Label htmlFor={option.value}>{option.label}</Label>
|
|
29
|
-
</div>
|
|
30
|
-
))}
|
|
31
|
-
</RadioGroup>
|
|
32
|
-
</fieldset>
|
|
33
|
-
),
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export const WithDescriptions: Story = {
|
|
37
|
-
render: () => (
|
|
38
|
-
<fieldset className="w-80">
|
|
39
|
-
<legend className="mb-3 text-sm font-medium">Plan</legend>
|
|
40
|
-
<RadioGroup defaultValue="pro" className="gap-3">
|
|
41
|
-
{[
|
|
42
|
-
{ value: "hobby", label: "Hobby", hint: "For personal projects. Free forever." },
|
|
43
|
-
{ value: "pro", label: "Pro", hint: "For teams shipping to production. $20/month." },
|
|
44
|
-
{ value: "enterprise", label: "Enterprise", hint: "SSO, audit logs and support." },
|
|
45
|
-
].map((plan) => (
|
|
46
|
-
<div key={plan.value} className="flex items-start gap-2">
|
|
47
|
-
<RadioGroupItem value={plan.value} id={plan.value} className="mt-0.5" />
|
|
48
|
-
<div className="grid gap-1">
|
|
49
|
-
<Label htmlFor={plan.value}>{plan.label}</Label>
|
|
50
|
-
<p className="text-xs text-muted-foreground">{plan.hint}</p>
|
|
51
|
-
</div>
|
|
52
|
-
</div>
|
|
53
|
-
))}
|
|
54
|
-
</RadioGroup>
|
|
55
|
-
</fieldset>
|
|
56
|
-
),
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
/** A selected radio cannot be cleared, so offer an explicit "None". */
|
|
60
|
-
export const WithNoneOption: Story = {
|
|
61
|
-
render: () => (
|
|
62
|
-
<fieldset>
|
|
63
|
-
<legend className="mb-3 text-sm font-medium">Notification sound</legend>
|
|
64
|
-
<RadioGroup defaultValue="none">
|
|
65
|
-
{[
|
|
66
|
-
{ value: "none", label: "None" },
|
|
67
|
-
{ value: "chime", label: "Chime" },
|
|
68
|
-
{ value: "ping", label: "Ping" },
|
|
69
|
-
].map((option) => (
|
|
70
|
-
<div key={option.value} className="flex items-center gap-2">
|
|
71
|
-
<RadioGroupItem value={option.value} id={`sound-${option.value}`} />
|
|
72
|
-
<Label htmlFor={`sound-${option.value}`}>{option.label}</Label>
|
|
73
|
-
</div>
|
|
74
|
-
))}
|
|
75
|
-
</RadioGroup>
|
|
76
|
-
</fieldset>
|
|
77
|
-
),
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
export const WithDisabledOption: Story = {
|
|
81
|
-
render: () => (
|
|
82
|
-
<fieldset>
|
|
83
|
-
<legend className="mb-3 text-sm font-medium">Region</legend>
|
|
84
|
-
<RadioGroup defaultValue="us">
|
|
85
|
-
{[
|
|
86
|
-
{ value: "us", label: "United States", disabled: false },
|
|
87
|
-
{ value: "eu", label: "Europe", disabled: false },
|
|
88
|
-
{ value: "ap", label: "Asia Pacific (coming soon)", disabled: true },
|
|
89
|
-
].map((option) => (
|
|
90
|
-
<div key={option.value} className="flex items-center gap-2">
|
|
91
|
-
<RadioGroupItem
|
|
92
|
-
value={option.value}
|
|
93
|
-
id={`region-${option.value}`}
|
|
94
|
-
disabled={option.disabled}
|
|
95
|
-
/>
|
|
96
|
-
<Label htmlFor={`region-${option.value}`}>{option.label}</Label>
|
|
97
|
-
</div>
|
|
98
|
-
))}
|
|
99
|
-
</RadioGroup>
|
|
100
|
-
</fieldset>
|
|
101
|
-
),
|
|
102
|
-
};
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import { render, screen } 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 { Label } from "../label";
|
|
8
|
-
import { RadioGroup, RadioGroupItem } from "./radio-group";
|
|
9
|
-
|
|
10
|
-
function Example({
|
|
11
|
-
onValueChange,
|
|
12
|
-
defaultValue = "comfortable",
|
|
13
|
-
}: {
|
|
14
|
-
onValueChange?: (value: string) => void;
|
|
15
|
-
defaultValue?: string;
|
|
16
|
-
} = {}) {
|
|
17
|
-
return (
|
|
18
|
-
<>
|
|
19
|
-
<span id="density-label">Density</span>
|
|
20
|
-
<RadioGroup
|
|
21
|
-
aria-labelledby="density-label"
|
|
22
|
-
defaultValue={defaultValue}
|
|
23
|
-
onValueChange={onValueChange}
|
|
24
|
-
>
|
|
25
|
-
{[
|
|
26
|
-
{ value: "default", label: "Default" },
|
|
27
|
-
{ value: "comfortable", label: "Comfortable" },
|
|
28
|
-
{ value: "compact", label: "Compact", disabled: true },
|
|
29
|
-
].map((option) => (
|
|
30
|
-
<div key={option.value} className="flex items-center gap-2">
|
|
31
|
-
<RadioGroupItem value={option.value} id={option.value} disabled={option.disabled} />
|
|
32
|
-
<Label htmlFor={option.value}>{option.label}</Label>
|
|
33
|
-
</div>
|
|
34
|
-
))}
|
|
35
|
-
</RadioGroup>
|
|
36
|
-
</>
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
describe("RadioGroup", () => {
|
|
41
|
-
it("exposes a named group of radios", () => {
|
|
42
|
-
render(<Example />);
|
|
43
|
-
expect(screen.getByRole("radiogroup", { name: "Density" })).toBeInTheDocument();
|
|
44
|
-
expect(screen.getAllByRole("radio")).toHaveLength(3);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("marks the default value as checked", () => {
|
|
48
|
-
render(<Example />);
|
|
49
|
-
expect(screen.getByRole("radio", { name: "Comfortable" })).toBeChecked();
|
|
50
|
-
expect(screen.getByRole("radio", { name: "Default" })).not.toBeChecked();
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("selects on click", async () => {
|
|
54
|
-
const user = userEvent.setup();
|
|
55
|
-
render(<Example />);
|
|
56
|
-
|
|
57
|
-
await user.click(screen.getByRole("radio", { name: "Default" }));
|
|
58
|
-
expect(screen.getByRole("radio", { name: "Default" })).toBeChecked();
|
|
59
|
-
expect(screen.getByRole("radio", { name: "Comfortable" })).not.toBeChecked();
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("selects via its label", async () => {
|
|
63
|
-
const user = userEvent.setup();
|
|
64
|
-
render(<Example />);
|
|
65
|
-
|
|
66
|
-
await user.click(screen.getByText("Default"));
|
|
67
|
-
expect(screen.getByRole("radio", { name: "Default" })).toBeChecked();
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("is a single tab stop", async () => {
|
|
71
|
-
const user = userEvent.setup();
|
|
72
|
-
render(<Example />);
|
|
73
|
-
|
|
74
|
-
await user.tab();
|
|
75
|
-
expect(screen.getByRole("radio", { name: "Comfortable" })).toHaveFocus();
|
|
76
|
-
|
|
77
|
-
await user.tab();
|
|
78
|
-
expect(screen.getByRole("radio", { name: "Default" })).not.toHaveFocus();
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("moves focus with the arrow keys", async () => {
|
|
82
|
-
const user = userEvent.setup();
|
|
83
|
-
render(<Example />);
|
|
84
|
-
|
|
85
|
-
await user.tab();
|
|
86
|
-
await user.keyboard("{ArrowUp}");
|
|
87
|
-
expect(screen.getByRole("radio", { name: "Default" })).toHaveFocus();
|
|
88
|
-
|
|
89
|
-
await user.keyboard("{ArrowDown}");
|
|
90
|
-
expect(screen.getByRole("radio", { name: "Comfortable" })).toHaveFocus();
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
// Selection-on-arrow is not asserted here. The primitive sets its
|
|
94
|
-
// "an arrow key is down" flag from a bubble-phase document listener, but
|
|
95
|
-
// roving focus moves focus during the item's own keydown handler, so under
|
|
96
|
-
// jsdom's focus timing the flag is still false when the focus handler runs.
|
|
97
|
-
// Asserting it would test the simulation, not the behaviour; the real-browser
|
|
98
|
-
// path is covered by the Storybook a11y run.
|
|
99
|
-
|
|
100
|
-
it("does not select a disabled option", async () => {
|
|
101
|
-
const user = userEvent.setup();
|
|
102
|
-
render(<Example />);
|
|
103
|
-
|
|
104
|
-
await user.click(screen.getByRole("radio", { name: "Compact" }));
|
|
105
|
-
expect(screen.getByRole("radio", { name: "Compact" })).not.toBeChecked();
|
|
106
|
-
expect(screen.getByRole("radio", { name: "Comfortable" })).toBeChecked();
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("reports changes", async () => {
|
|
110
|
-
const onValueChange = vi.fn();
|
|
111
|
-
const user = userEvent.setup();
|
|
112
|
-
render(<Example onValueChange={onValueChange} />);
|
|
113
|
-
|
|
114
|
-
await user.click(screen.getByRole("radio", { name: "Default" }));
|
|
115
|
-
expect(onValueChange).toHaveBeenCalledWith("default");
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it("works controlled", async () => {
|
|
119
|
-
const user = userEvent.setup();
|
|
120
|
-
|
|
121
|
-
function Controlled() {
|
|
122
|
-
const [value, setValue] = useState("a");
|
|
123
|
-
return (
|
|
124
|
-
<RadioGroup aria-label="Choice" value={value} onValueChange={setValue}>
|
|
125
|
-
<RadioGroupItem value="a" aria-label="A" />
|
|
126
|
-
<RadioGroupItem value="b" aria-label="B" />
|
|
127
|
-
</RadioGroup>
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
render(<Controlled />);
|
|
132
|
-
await user.click(screen.getByRole("radio", { name: "B" }));
|
|
133
|
-
expect(screen.getByRole("radio", { name: "B" })).toBeChecked();
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it("lets a consumer className override a conflicting utility", () => {
|
|
137
|
-
render(
|
|
138
|
-
<RadioGroup aria-label="Choice" className="gap-8" data-testid="group">
|
|
139
|
-
<RadioGroupItem value="a" aria-label="A" />
|
|
140
|
-
</RadioGroup>,
|
|
141
|
-
);
|
|
142
|
-
|
|
143
|
-
const group = screen.getByTestId("group");
|
|
144
|
-
expect(group).toHaveClass("gap-8");
|
|
145
|
-
expect(group).not.toHaveClass("gap-3");
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it("has no accessibility violations", async () => {
|
|
149
|
-
const { container } = render(<Example />);
|
|
150
|
-
await expectNoA11yViolations(container);
|
|
151
|
-
});
|
|
152
|
-
});
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import type { Decorator, Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
-
|
|
3
|
-
import { Label } from "@/components/label";
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
Select,
|
|
7
|
-
SelectContent,
|
|
8
|
-
SelectGroup,
|
|
9
|
-
SelectItem,
|
|
10
|
-
SelectLabel,
|
|
11
|
-
SelectSeparator,
|
|
12
|
-
SelectTrigger,
|
|
13
|
-
SelectValue,
|
|
14
|
-
} from "./select";
|
|
15
|
-
|
|
16
|
-
/** Named so its type is nameable in declaration output (TS2883). */
|
|
17
|
-
const withFixedWidth: Decorator = (Story) => (
|
|
18
|
-
<div className="w-64">
|
|
19
|
-
<Story />
|
|
20
|
-
</div>
|
|
21
|
-
);
|
|
22
|
-
|
|
23
|
-
const meta = {
|
|
24
|
-
title: "Forms/Select",
|
|
25
|
-
component: SelectTrigger,
|
|
26
|
-
args: { triggerSize: "md" },
|
|
27
|
-
argTypes: { triggerSize: { control: "inline-radio", options: ["sm", "md", "lg"] } },
|
|
28
|
-
decorators: [withFixedWidth],
|
|
29
|
-
} satisfies Meta<typeof SelectTrigger>;
|
|
30
|
-
|
|
31
|
-
export default meta;
|
|
32
|
-
type Story = StoryObj<typeof meta>;
|
|
33
|
-
|
|
34
|
-
export const Default: Story = {
|
|
35
|
-
render: (args) => (
|
|
36
|
-
<div className="grid gap-2">
|
|
37
|
-
<Label htmlFor="fruit">Fruit</Label>
|
|
38
|
-
<Select>
|
|
39
|
-
<SelectTrigger id="fruit" {...args}>
|
|
40
|
-
<SelectValue placeholder="Select a fruit" />
|
|
41
|
-
</SelectTrigger>
|
|
42
|
-
<SelectContent>
|
|
43
|
-
<SelectItem value="apple">Apple</SelectItem>
|
|
44
|
-
<SelectItem value="banana">Banana</SelectItem>
|
|
45
|
-
<SelectItem value="blueberry">Blueberry</SelectItem>
|
|
46
|
-
<SelectItem value="grapes">Grapes</SelectItem>
|
|
47
|
-
</SelectContent>
|
|
48
|
-
</Select>
|
|
49
|
-
</div>
|
|
50
|
-
),
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
export const Grouped: Story = {
|
|
54
|
-
parameters: { controls: { disable: true } },
|
|
55
|
-
render: () => (
|
|
56
|
-
<div className="grid gap-2">
|
|
57
|
-
<Label htmlFor="food">Food</Label>
|
|
58
|
-
<Select>
|
|
59
|
-
<SelectTrigger id="food">
|
|
60
|
-
<SelectValue placeholder="Select something" />
|
|
61
|
-
</SelectTrigger>
|
|
62
|
-
<SelectContent>
|
|
63
|
-
<SelectGroup>
|
|
64
|
-
<SelectLabel>Fruits</SelectLabel>
|
|
65
|
-
<SelectItem value="apple">Apple</SelectItem>
|
|
66
|
-
<SelectItem value="banana">Banana</SelectItem>
|
|
67
|
-
</SelectGroup>
|
|
68
|
-
<SelectSeparator />
|
|
69
|
-
<SelectGroup>
|
|
70
|
-
<SelectLabel>Vegetables</SelectLabel>
|
|
71
|
-
<SelectItem value="carrot">Carrot</SelectItem>
|
|
72
|
-
<SelectItem value="potato">Potato</SelectItem>
|
|
73
|
-
</SelectGroup>
|
|
74
|
-
</SelectContent>
|
|
75
|
-
</Select>
|
|
76
|
-
</div>
|
|
77
|
-
),
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
export const WithDisabledOption: Story = {
|
|
81
|
-
parameters: { controls: { disable: true } },
|
|
82
|
-
render: () => (
|
|
83
|
-
<Select>
|
|
84
|
-
<SelectTrigger aria-label="Plan">
|
|
85
|
-
<SelectValue placeholder="Select a plan" />
|
|
86
|
-
</SelectTrigger>
|
|
87
|
-
<SelectContent>
|
|
88
|
-
<SelectItem value="hobby">Hobby</SelectItem>
|
|
89
|
-
<SelectItem value="pro">Pro</SelectItem>
|
|
90
|
-
<SelectItem value="enterprise" disabled>
|
|
91
|
-
Enterprise (contact sales)
|
|
92
|
-
</SelectItem>
|
|
93
|
-
</SelectContent>
|
|
94
|
-
</Select>
|
|
95
|
-
),
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
/** Past about a dozen options, people want to type — reach for Combobox. */
|
|
99
|
-
export const LongList: Story = {
|
|
100
|
-
parameters: { controls: { disable: true } },
|
|
101
|
-
render: () => (
|
|
102
|
-
<Select>
|
|
103
|
-
<SelectTrigger aria-label="Timezone">
|
|
104
|
-
<SelectValue placeholder="Select a timezone" />
|
|
105
|
-
</SelectTrigger>
|
|
106
|
-
<SelectContent>
|
|
107
|
-
{Array.from({ length: 24 }, (_, index) => (
|
|
108
|
-
<SelectItem key={index} value={`utc-${String(index)}`}>
|
|
109
|
-
UTC{index >= 12 ? "+" : "-"}
|
|
110
|
-
{String(Math.abs(index - 12)).padStart(2, "0")}:00
|
|
111
|
-
</SelectItem>
|
|
112
|
-
))}
|
|
113
|
-
</SelectContent>
|
|
114
|
-
</Select>
|
|
115
|
-
),
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
export const Invalid: Story = {
|
|
119
|
-
parameters: { controls: { disable: true } },
|
|
120
|
-
render: () => (
|
|
121
|
-
<div className="grid gap-2">
|
|
122
|
-
<Label htmlFor="required-select">Region</Label>
|
|
123
|
-
<Select>
|
|
124
|
-
<SelectTrigger id="required-select" aria-invalid aria-describedby="region-error">
|
|
125
|
-
<SelectValue placeholder="Select a region" />
|
|
126
|
-
</SelectTrigger>
|
|
127
|
-
<SelectContent>
|
|
128
|
-
<SelectItem value="us">United States</SelectItem>
|
|
129
|
-
<SelectItem value="eu">Europe</SelectItem>
|
|
130
|
-
</SelectContent>
|
|
131
|
-
</Select>
|
|
132
|
-
<p id="region-error" className="text-xs text-destructive">
|
|
133
|
-
Choose a region to continue.
|
|
134
|
-
</p>
|
|
135
|
-
</div>
|
|
136
|
-
),
|
|
137
|
-
};
|
|
@@ -1,209 +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
|
-
Select,
|
|
9
|
-
SelectContent,
|
|
10
|
-
SelectGroup,
|
|
11
|
-
SelectItem,
|
|
12
|
-
SelectLabel,
|
|
13
|
-
SelectSeparator,
|
|
14
|
-
SelectTrigger,
|
|
15
|
-
SelectValue,
|
|
16
|
-
} from "./select";
|
|
17
|
-
|
|
18
|
-
function Example({ onValueChange }: { onValueChange?: (value: string) => void } = {}) {
|
|
19
|
-
return (
|
|
20
|
-
<>
|
|
21
|
-
<span id="fruit-label">Fruit</span>
|
|
22
|
-
<Select onValueChange={onValueChange}>
|
|
23
|
-
<SelectTrigger aria-labelledby="fruit-label">
|
|
24
|
-
<SelectValue placeholder="Select a fruit" />
|
|
25
|
-
</SelectTrigger>
|
|
26
|
-
<SelectContent>
|
|
27
|
-
<SelectGroup>
|
|
28
|
-
<SelectLabel>Fruits</SelectLabel>
|
|
29
|
-
<SelectItem value="apple">Apple</SelectItem>
|
|
30
|
-
<SelectItem value="banana">Banana</SelectItem>
|
|
31
|
-
<SelectItem value="cherry" disabled>
|
|
32
|
-
Cherry
|
|
33
|
-
</SelectItem>
|
|
34
|
-
</SelectGroup>
|
|
35
|
-
<SelectSeparator />
|
|
36
|
-
<SelectItem value="carrot">Carrot</SelectItem>
|
|
37
|
-
</SelectContent>
|
|
38
|
-
</Select>
|
|
39
|
-
</>
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
describe("Select", () => {
|
|
44
|
-
it("shows the placeholder before anything is chosen", () => {
|
|
45
|
-
render(<Example />);
|
|
46
|
-
expect(screen.getByRole("combobox")).toHaveTextContent("Select a fruit");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("is closed until the trigger is activated", async () => {
|
|
50
|
-
const user = userEvent.setup();
|
|
51
|
-
render(<Example />);
|
|
52
|
-
|
|
53
|
-
expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
|
|
54
|
-
await user.click(screen.getByRole("combobox"));
|
|
55
|
-
expect(await screen.findByRole("listbox")).toBeInTheDocument();
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("reports its state on the trigger", async () => {
|
|
59
|
-
const user = userEvent.setup();
|
|
60
|
-
render(<Example />);
|
|
61
|
-
|
|
62
|
-
const trigger = screen.getByRole("combobox");
|
|
63
|
-
expect(trigger).toHaveAttribute("aria-expanded", "false");
|
|
64
|
-
|
|
65
|
-
await user.click(trigger);
|
|
66
|
-
await waitFor(() => {
|
|
67
|
-
expect(trigger).toHaveAttribute("aria-expanded", "true");
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("selects an option and shows it on the trigger", async () => {
|
|
72
|
-
const user = userEvent.setup();
|
|
73
|
-
render(<Example />);
|
|
74
|
-
|
|
75
|
-
await user.click(screen.getByRole("combobox"));
|
|
76
|
-
await user.click(await screen.findByRole("option", { name: "Banana" }));
|
|
77
|
-
|
|
78
|
-
await waitFor(() => {
|
|
79
|
-
expect(screen.getByRole("combobox")).toHaveTextContent("Banana");
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("reports the selected value", async () => {
|
|
84
|
-
const onValueChange = vi.fn();
|
|
85
|
-
const user = userEvent.setup();
|
|
86
|
-
render(<Example onValueChange={onValueChange} />);
|
|
87
|
-
|
|
88
|
-
await user.click(screen.getByRole("combobox"));
|
|
89
|
-
await user.click(await screen.findByRole("option", { name: "Apple" }));
|
|
90
|
-
|
|
91
|
-
expect(onValueChange).toHaveBeenCalledWith("apple");
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("opens from the keyboard", async () => {
|
|
95
|
-
const user = userEvent.setup();
|
|
96
|
-
render(<Example />);
|
|
97
|
-
|
|
98
|
-
await user.tab();
|
|
99
|
-
expect(screen.getByRole("combobox")).toHaveFocus();
|
|
100
|
-
|
|
101
|
-
await user.keyboard("{Enter}");
|
|
102
|
-
expect(await screen.findByRole("listbox")).toBeInTheDocument();
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("does not select a disabled option", async () => {
|
|
106
|
-
const user = userEvent.setup();
|
|
107
|
-
render(<Example />);
|
|
108
|
-
|
|
109
|
-
await user.click(screen.getByRole("combobox"));
|
|
110
|
-
const disabled = await screen.findByRole("option", { name: "Cherry" });
|
|
111
|
-
expect(disabled).toHaveAttribute("data-disabled");
|
|
112
|
-
|
|
113
|
-
await user.click(disabled);
|
|
114
|
-
expect(screen.getByRole("listbox")).toBeInTheDocument();
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it("closes on Escape and restores focus", async () => {
|
|
118
|
-
const user = userEvent.setup();
|
|
119
|
-
render(<Example />);
|
|
120
|
-
|
|
121
|
-
const trigger = screen.getByRole("combobox");
|
|
122
|
-
await user.click(trigger);
|
|
123
|
-
await screen.findByRole("listbox");
|
|
124
|
-
|
|
125
|
-
await user.keyboard("{Escape}");
|
|
126
|
-
await waitFor(() => {
|
|
127
|
-
expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
|
|
128
|
-
});
|
|
129
|
-
await waitFor(() => {
|
|
130
|
-
expect(trigger).toHaveFocus();
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it("works controlled", async () => {
|
|
135
|
-
const user = userEvent.setup();
|
|
136
|
-
|
|
137
|
-
function Controlled() {
|
|
138
|
-
const [value, setValue] = useState("apple");
|
|
139
|
-
return (
|
|
140
|
-
<Select value={value} onValueChange={setValue}>
|
|
141
|
-
<SelectTrigger aria-label="Fruit">
|
|
142
|
-
<SelectValue />
|
|
143
|
-
</SelectTrigger>
|
|
144
|
-
<SelectContent>
|
|
145
|
-
<SelectItem value="apple">Apple</SelectItem>
|
|
146
|
-
<SelectItem value="banana">Banana</SelectItem>
|
|
147
|
-
</SelectContent>
|
|
148
|
-
</Select>
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
render(<Controlled />);
|
|
153
|
-
expect(screen.getByRole("combobox")).toHaveTextContent("Apple");
|
|
154
|
-
|
|
155
|
-
await user.click(screen.getByRole("combobox"));
|
|
156
|
-
await user.click(await screen.findByRole("option", { name: "Banana" }));
|
|
157
|
-
|
|
158
|
-
await waitFor(() => {
|
|
159
|
-
expect(screen.getByRole("combobox")).toHaveTextContent("Banana");
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it.each([
|
|
164
|
-
["sm", "h-8"],
|
|
165
|
-
["md", "h-9"],
|
|
166
|
-
["lg", "h-10"],
|
|
167
|
-
] as const)("applies the %s trigger size", (triggerSize, expectedClass) => {
|
|
168
|
-
render(
|
|
169
|
-
<Select>
|
|
170
|
-
<SelectTrigger triggerSize={triggerSize} aria-label="Fruit">
|
|
171
|
-
<SelectValue placeholder="Pick" />
|
|
172
|
-
</SelectTrigger>
|
|
173
|
-
<SelectContent>
|
|
174
|
-
<SelectItem value="a">A</SelectItem>
|
|
175
|
-
</SelectContent>
|
|
176
|
-
</Select>,
|
|
177
|
-
);
|
|
178
|
-
expect(screen.getByRole("combobox")).toHaveClass(expectedClass);
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it("exposes the invalid state", () => {
|
|
182
|
-
render(
|
|
183
|
-
<Select>
|
|
184
|
-
<SelectTrigger aria-label="Fruit" aria-invalid>
|
|
185
|
-
<SelectValue placeholder="Pick" />
|
|
186
|
-
</SelectTrigger>
|
|
187
|
-
<SelectContent>
|
|
188
|
-
<SelectItem value="a">A</SelectItem>
|
|
189
|
-
</SelectContent>
|
|
190
|
-
</Select>,
|
|
191
|
-
);
|
|
192
|
-
expect(screen.getByRole("combobox")).toHaveAttribute("aria-invalid", "true");
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it("has no accessibility violations when closed", async () => {
|
|
196
|
-
const { container } = render(<Example />);
|
|
197
|
-
await expectNoA11yViolations(container);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it("has no accessibility violations while open", async () => {
|
|
201
|
-
const user = userEvent.setup();
|
|
202
|
-
const { baseElement } = render(<Example />);
|
|
203
|
-
|
|
204
|
-
await user.click(screen.getByRole("combobox"));
|
|
205
|
-
await screen.findByRole("listbox");
|
|
206
|
-
|
|
207
|
-
await expectNoA11yViolations(baseElement);
|
|
208
|
-
});
|
|
209
|
-
});
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
-
|
|
3
|
-
import { Separator } from "./separator";
|
|
4
|
-
|
|
5
|
-
const meta = {
|
|
6
|
-
title: "Layout/Separator",
|
|
7
|
-
component: Separator,
|
|
8
|
-
parameters: { controls: { disable: true } },
|
|
9
|
-
} satisfies Meta<typeof Separator>;
|
|
10
|
-
|
|
11
|
-
export default meta;
|
|
12
|
-
type Story = StoryObj<typeof meta>;
|
|
13
|
-
|
|
14
|
-
export const Horizontal: Story = {
|
|
15
|
-
render: () => (
|
|
16
|
-
<div className="w-72">
|
|
17
|
-
<div className="space-y-1">
|
|
18
|
-
<h4 className="text-sm font-medium">Radix primitives</h4>
|
|
19
|
-
<p className="text-sm text-muted-foreground">An open source component library.</p>
|
|
20
|
-
</div>
|
|
21
|
-
<Separator className="my-4" />
|
|
22
|
-
<div className="flex h-5 items-center gap-4 text-sm">
|
|
23
|
-
<span>Blog</span>
|
|
24
|
-
<Separator orientation="vertical" />
|
|
25
|
-
<span>Docs</span>
|
|
26
|
-
<Separator orientation="vertical" />
|
|
27
|
-
<span>Source</span>
|
|
28
|
-
</div>
|
|
29
|
-
</div>
|
|
30
|
-
),
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
/** Exposed to assistive technology when it divides genuinely distinct groups. */
|
|
34
|
-
export const Semantic: Story = {
|
|
35
|
-
render: () => (
|
|
36
|
-
<div className="w-72 space-y-4">
|
|
37
|
-
<p className="text-sm">Account settings</p>
|
|
38
|
-
<Separator decorative={false} />
|
|
39
|
-
<p className="text-sm">Danger zone</p>
|
|
40
|
-
</div>
|
|
41
|
-
),
|
|
42
|
-
};
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { render, screen } from "@testing-library/react";
|
|
2
|
-
import { describe, expect, it } from "vitest";
|
|
3
|
-
|
|
4
|
-
import { expectNoA11yViolations } from "../../../test/a11y";
|
|
5
|
-
import { Separator } from "./separator";
|
|
6
|
-
|
|
7
|
-
describe("Separator", () => {
|
|
8
|
-
it("is decorative by default and stays out of the accessibility tree", () => {
|
|
9
|
-
render(<Separator data-testid="rule" />);
|
|
10
|
-
expect(screen.getByTestId("rule")).toHaveAttribute("role", "none");
|
|
11
|
-
expect(screen.queryByRole("separator")).not.toBeInTheDocument();
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it("exposes a separator role when it is not decorative", () => {
|
|
15
|
-
render(<Separator decorative={false} />);
|
|
16
|
-
expect(screen.getByRole("separator")).toBeInTheDocument();
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("renders horizontally by default", () => {
|
|
20
|
-
render(<Separator data-testid="rule" />);
|
|
21
|
-
expect(screen.getByTestId("rule")).toHaveClass("h-px", "w-full");
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("renders vertically when asked", () => {
|
|
25
|
-
render(<Separator orientation="vertical" data-testid="rule" />);
|
|
26
|
-
const rule = screen.getByTestId("rule");
|
|
27
|
-
expect(rule).toHaveClass("w-px", "h-full");
|
|
28
|
-
expect(rule).toHaveAttribute("data-orientation", "vertical");
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("merges a consumer className", () => {
|
|
32
|
-
render(<Separator className="my-4" data-testid="rule" />);
|
|
33
|
-
expect(screen.getByTestId("rule")).toHaveClass("my-4", "bg-border");
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("has no accessibility violations", async () => {
|
|
37
|
-
const { container } = render(
|
|
38
|
-
<div>
|
|
39
|
-
<p>Above</p>
|
|
40
|
-
<Separator decorative={false} />
|
|
41
|
-
<p>Below</p>
|
|
42
|
-
</div>,
|
|
43
|
-
);
|
|
44
|
-
await expectNoA11yViolations(container);
|
|
45
|
-
});
|
|
46
|
-
});
|