@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,148 +0,0 @@
|
|
|
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 { expectNoA11yViolations } from "../../../test/a11y";
|
|
6
|
-
import { Tabs, TabsContent, TabsList, TabsTrigger, type TabsListProps } from "./tabs";
|
|
7
|
-
|
|
8
|
-
function Example({
|
|
9
|
-
variant,
|
|
10
|
-
onValueChange,
|
|
11
|
-
activationMode,
|
|
12
|
-
}: {
|
|
13
|
-
variant?: TabsListProps["variant"];
|
|
14
|
-
onValueChange?: (value: string) => void;
|
|
15
|
-
activationMode?: "automatic" | "manual";
|
|
16
|
-
} = {}) {
|
|
17
|
-
return (
|
|
18
|
-
<Tabs defaultValue="account" onValueChange={onValueChange} activationMode={activationMode}>
|
|
19
|
-
<TabsList variant={variant}>
|
|
20
|
-
<TabsTrigger value="account" variant={variant}>
|
|
21
|
-
Account
|
|
22
|
-
</TabsTrigger>
|
|
23
|
-
<TabsTrigger value="password" variant={variant}>
|
|
24
|
-
Password
|
|
25
|
-
</TabsTrigger>
|
|
26
|
-
<TabsTrigger value="team" variant={variant} disabled>
|
|
27
|
-
Team
|
|
28
|
-
</TabsTrigger>
|
|
29
|
-
</TabsList>
|
|
30
|
-
<TabsContent value="account">Account panel</TabsContent>
|
|
31
|
-
<TabsContent value="password">Password panel</TabsContent>
|
|
32
|
-
<TabsContent value="team">Team panel</TabsContent>
|
|
33
|
-
</Tabs>
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
describe("Tabs", () => {
|
|
38
|
-
it("shows only the active panel", () => {
|
|
39
|
-
render(<Example />);
|
|
40
|
-
expect(screen.getByText("Account panel")).toBeInTheDocument();
|
|
41
|
-
expect(screen.queryByText("Password panel")).not.toBeInTheDocument();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("marks the active tab as selected", () => {
|
|
45
|
-
render(<Example />);
|
|
46
|
-
expect(screen.getByRole("tab", { name: "Account" })).toHaveAttribute(
|
|
47
|
-
"aria-selected",
|
|
48
|
-
"true",
|
|
49
|
-
);
|
|
50
|
-
expect(screen.getByRole("tab", { name: "Password" })).toHaveAttribute(
|
|
51
|
-
"aria-selected",
|
|
52
|
-
"false",
|
|
53
|
-
);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("switches panels on click", async () => {
|
|
57
|
-
const user = userEvent.setup();
|
|
58
|
-
render(<Example />);
|
|
59
|
-
|
|
60
|
-
await user.click(screen.getByRole("tab", { name: "Password" }));
|
|
61
|
-
expect(screen.getByText("Password panel")).toBeInTheDocument();
|
|
62
|
-
expect(screen.queryByText("Account panel")).not.toBeInTheDocument();
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("moves between tabs with the arrow keys", async () => {
|
|
66
|
-
const user = userEvent.setup();
|
|
67
|
-
render(<Example />);
|
|
68
|
-
|
|
69
|
-
await user.click(screen.getByRole("tab", { name: "Account" }));
|
|
70
|
-
await user.keyboard("{ArrowRight}");
|
|
71
|
-
|
|
72
|
-
expect(screen.getByRole("tab", { name: "Password" })).toHaveFocus();
|
|
73
|
-
expect(screen.getByText("Password panel")).toBeInTheDocument();
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("keeps only the active tab in the tab sequence", async () => {
|
|
77
|
-
const user = userEvent.setup();
|
|
78
|
-
render(<Example />);
|
|
79
|
-
|
|
80
|
-
await user.tab();
|
|
81
|
-
expect(screen.getByRole("tab", { name: "Account" })).toHaveFocus();
|
|
82
|
-
|
|
83
|
-
// A second Tab leaves the tablist entirely rather than visiting each tab.
|
|
84
|
-
await user.tab();
|
|
85
|
-
expect(screen.getByRole("tab", { name: "Password" })).not.toHaveFocus();
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("skips disabled tabs when arrowing", async () => {
|
|
89
|
-
const user = userEvent.setup();
|
|
90
|
-
render(<Example />);
|
|
91
|
-
|
|
92
|
-
await user.click(screen.getByRole("tab", { name: "Password" }));
|
|
93
|
-
await user.keyboard("{ArrowRight}");
|
|
94
|
-
|
|
95
|
-
expect(screen.getByRole("tab", { name: "Team" })).not.toHaveFocus();
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it("defers activation in manual mode", async () => {
|
|
99
|
-
const user = userEvent.setup();
|
|
100
|
-
render(<Example activationMode="manual" />);
|
|
101
|
-
|
|
102
|
-
await user.click(screen.getByRole("tab", { name: "Account" }));
|
|
103
|
-
await user.keyboard("{ArrowRight}");
|
|
104
|
-
|
|
105
|
-
expect(screen.getByRole("tab", { name: "Password" })).toHaveFocus();
|
|
106
|
-
expect(screen.getByText("Account panel")).toBeInTheDocument();
|
|
107
|
-
|
|
108
|
-
await user.keyboard("{Enter}");
|
|
109
|
-
expect(screen.getByText("Password panel")).toBeInTheDocument();
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it("reports value changes", async () => {
|
|
113
|
-
const onValueChange = vi.fn();
|
|
114
|
-
const user = userEvent.setup();
|
|
115
|
-
render(<Example onValueChange={onValueChange} />);
|
|
116
|
-
|
|
117
|
-
await user.click(screen.getByRole("tab", { name: "Password" }));
|
|
118
|
-
expect(onValueChange).toHaveBeenCalledWith("password");
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it.each([
|
|
122
|
-
["solid", "bg-muted"],
|
|
123
|
-
["underline", "border-b"],
|
|
124
|
-
] as const)("applies the %s list variant", (variant, expectedClass) => {
|
|
125
|
-
render(<Example variant={variant} />);
|
|
126
|
-
expect(screen.getByRole("tablist")).toHaveClass(expectedClass);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it("lets a consumer className override a conflicting utility", () => {
|
|
130
|
-
render(
|
|
131
|
-
<Tabs defaultValue="a">
|
|
132
|
-
<TabsList className="bg-card">
|
|
133
|
-
<TabsTrigger value="a">A</TabsTrigger>
|
|
134
|
-
</TabsList>
|
|
135
|
-
<TabsContent value="a">Panel</TabsContent>
|
|
136
|
-
</Tabs>,
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
const list = screen.getByRole("tablist");
|
|
140
|
-
expect(list).toHaveClass("bg-card");
|
|
141
|
-
expect(list).not.toHaveClass("bg-muted");
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it("has no accessibility violations", async () => {
|
|
145
|
-
const { container } = render(<Example />);
|
|
146
|
-
await expectNoA11yViolations(container);
|
|
147
|
-
});
|
|
148
|
-
});
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
-
|
|
3
|
-
import { Button } from "@/components/button";
|
|
4
|
-
|
|
5
|
-
import { Toaster, type ToastPosition } from "./toast";
|
|
6
|
-
import { toast } from "./toast-store";
|
|
7
|
-
|
|
8
|
-
const meta = {
|
|
9
|
-
title: "Feedback/Toast",
|
|
10
|
-
component: Toaster,
|
|
11
|
-
args: { position: "bottom-right" },
|
|
12
|
-
argTypes: {
|
|
13
|
-
position: {
|
|
14
|
-
control: "select",
|
|
15
|
-
options: [
|
|
16
|
-
"top-left",
|
|
17
|
-
"top-center",
|
|
18
|
-
"top-right",
|
|
19
|
-
"bottom-left",
|
|
20
|
-
"bottom-center",
|
|
21
|
-
"bottom-right",
|
|
22
|
-
] satisfies ToastPosition[],
|
|
23
|
-
},
|
|
24
|
-
duration: { control: { type: "number", min: 1000, max: 20000, step: 500 } },
|
|
25
|
-
},
|
|
26
|
-
} satisfies Meta<typeof Toaster>;
|
|
27
|
-
|
|
28
|
-
export default meta;
|
|
29
|
-
type Story = StoryObj<typeof meta>;
|
|
30
|
-
|
|
31
|
-
export const Default: Story = {
|
|
32
|
-
render: (args) => (
|
|
33
|
-
<>
|
|
34
|
-
<Button onClick={() => toast("Project created")}>Show toast</Button>
|
|
35
|
-
<Toaster {...args} />
|
|
36
|
-
</>
|
|
37
|
-
),
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
export const Variants: Story = {
|
|
41
|
-
render: (args) => (
|
|
42
|
-
<>
|
|
43
|
-
<div className="flex flex-wrap gap-2">
|
|
44
|
-
<Button variant="outline" onClick={() => toast("Saved")}>
|
|
45
|
-
Default
|
|
46
|
-
</Button>
|
|
47
|
-
<Button variant="outline" onClick={() => toast.success("Deployment succeeded")}>
|
|
48
|
-
Success
|
|
49
|
-
</Button>
|
|
50
|
-
<Button variant="outline" onClick={() => toast.info("Build queued")}>
|
|
51
|
-
Info
|
|
52
|
-
</Button>
|
|
53
|
-
<Button variant="outline" onClick={() => toast.warning("Approaching your quota")}>
|
|
54
|
-
Warning
|
|
55
|
-
</Button>
|
|
56
|
-
<Button variant="outline" onClick={() => toast.error("Payment failed")}>
|
|
57
|
-
Error
|
|
58
|
-
</Button>
|
|
59
|
-
</div>
|
|
60
|
-
<Toaster {...args} />
|
|
61
|
-
</>
|
|
62
|
-
),
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
export const WithDescription: Story = {
|
|
66
|
-
render: (args) => (
|
|
67
|
-
<>
|
|
68
|
-
<Button
|
|
69
|
-
onClick={() =>
|
|
70
|
-
toast.success("Project created", {
|
|
71
|
-
description: "acme-inc is ready. Deploy when you are.",
|
|
72
|
-
})
|
|
73
|
-
}
|
|
74
|
-
>
|
|
75
|
-
Show toast
|
|
76
|
-
</Button>
|
|
77
|
-
<Toaster {...args} />
|
|
78
|
-
</>
|
|
79
|
-
),
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
/** An action needs alt text: "Undo" alone is meaningless once the toast is gone. */
|
|
83
|
-
export const WithAction: Story = {
|
|
84
|
-
render: (args) => (
|
|
85
|
-
<>
|
|
86
|
-
<Button
|
|
87
|
-
onClick={() =>
|
|
88
|
-
toast("Message archived", {
|
|
89
|
-
action: {
|
|
90
|
-
label: "Undo",
|
|
91
|
-
altText: "Undo archiving the message",
|
|
92
|
-
onClick: () => toast.success("Message restored"),
|
|
93
|
-
},
|
|
94
|
-
})
|
|
95
|
-
}
|
|
96
|
-
>
|
|
97
|
-
Archive message
|
|
98
|
-
</Button>
|
|
99
|
-
<Toaster {...args} />
|
|
100
|
-
</>
|
|
101
|
-
),
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
export const Persistent: Story = {
|
|
105
|
-
render: (args) => (
|
|
106
|
-
<>
|
|
107
|
-
<Button
|
|
108
|
-
variant="outline"
|
|
109
|
-
onClick={() =>
|
|
110
|
-
toast.custom({
|
|
111
|
-
title: "Connection lost",
|
|
112
|
-
description: "Reconnecting…",
|
|
113
|
-
variant: "destructive",
|
|
114
|
-
duration: Infinity,
|
|
115
|
-
})
|
|
116
|
-
}
|
|
117
|
-
>
|
|
118
|
-
Show until dismissed
|
|
119
|
-
</Button>
|
|
120
|
-
<Toaster {...args} />
|
|
121
|
-
</>
|
|
122
|
-
),
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
export const Promise: Story = {
|
|
126
|
-
render: (args) => (
|
|
127
|
-
<>
|
|
128
|
-
<Button
|
|
129
|
-
variant="outline"
|
|
130
|
-
onClick={() => {
|
|
131
|
-
const work = new window.Promise<string>((resolve) => {
|
|
132
|
-
window.setTimeout(() => {
|
|
133
|
-
resolve("acme-inc");
|
|
134
|
-
}, 1500);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
void toast.promise(work, {
|
|
138
|
-
loading: "Creating project…",
|
|
139
|
-
success: (name) => `Created ${name}`,
|
|
140
|
-
error: "Could not create the project",
|
|
141
|
-
});
|
|
142
|
-
}}
|
|
143
|
-
>
|
|
144
|
-
Create project
|
|
145
|
-
</Button>
|
|
146
|
-
<Toaster {...args} />
|
|
147
|
-
</>
|
|
148
|
-
),
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
export const Stacking: Story = {
|
|
152
|
-
render: (args) => (
|
|
153
|
-
<>
|
|
154
|
-
<Button
|
|
155
|
-
variant="outline"
|
|
156
|
-
onClick={() => {
|
|
157
|
-
for (let index = 1; index <= 6; index += 1) {
|
|
158
|
-
toast(`Event ${String(index)}`);
|
|
159
|
-
}
|
|
160
|
-
}}
|
|
161
|
-
>
|
|
162
|
-
Raise six toasts
|
|
163
|
-
</Button>
|
|
164
|
-
<Toaster {...args} />
|
|
165
|
-
</>
|
|
166
|
-
),
|
|
167
|
-
};
|
|
@@ -1,306 +0,0 @@
|
|
|
1
|
-
import { act, render, screen, waitFor } from "@testing-library/react";
|
|
2
|
-
import userEvent from "@testing-library/user-event";
|
|
3
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
-
|
|
5
|
-
import { expectNoA11yViolations } from "../../../test/a11y";
|
|
6
|
-
import { Toaster } from "./toast";
|
|
7
|
-
import { TOAST_LIMIT, resetToasts, toast } from "./toast-store";
|
|
8
|
-
|
|
9
|
-
afterEach(() => {
|
|
10
|
-
act(() => {
|
|
11
|
-
resetToasts();
|
|
12
|
-
});
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
function raise(fn: () => void) {
|
|
16
|
-
act(() => {
|
|
17
|
-
fn();
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
describe("toast store", () => {
|
|
22
|
-
it("returns the id it created", () => {
|
|
23
|
-
let id = "";
|
|
24
|
-
raise(() => {
|
|
25
|
-
id = toast("Saved");
|
|
26
|
-
});
|
|
27
|
-
expect(id).toMatch(/^toast-\d+$/);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("honours an explicit id, replacing rather than duplicating", async () => {
|
|
31
|
-
render(<Toaster />);
|
|
32
|
-
|
|
33
|
-
raise(() => toast.custom({ id: "sync", title: "Syncing" }));
|
|
34
|
-
expect(await screen.findByText("Syncing")).toBeInTheDocument();
|
|
35
|
-
|
|
36
|
-
raise(() => toast.custom({ id: "sync", title: "Synced" }));
|
|
37
|
-
await waitFor(() => {
|
|
38
|
-
expect(screen.getByText("Synced")).toBeInTheDocument();
|
|
39
|
-
});
|
|
40
|
-
expect(screen.queryByText("Syncing")).not.toBeInTheDocument();
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("keeps only the most recent toasts", async () => {
|
|
44
|
-
render(<Toaster />);
|
|
45
|
-
|
|
46
|
-
raise(() => {
|
|
47
|
-
for (let index = 0; index < TOAST_LIMIT + 3; index += 1) {
|
|
48
|
-
toast(`Message ${String(index)}`);
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
await waitFor(() => {
|
|
53
|
-
expect(document.querySelectorAll("[data-slot='toast']")).toHaveLength(TOAST_LIMIT);
|
|
54
|
-
});
|
|
55
|
-
expect(screen.queryByText("Message 0")).not.toBeInTheDocument();
|
|
56
|
-
expect(screen.getByText(`Message ${String(TOAST_LIMIT + 2)}`)).toBeInTheDocument();
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("calls onDismiss when a toast is removed", () => {
|
|
60
|
-
const onDismiss = vi.fn();
|
|
61
|
-
let id = "";
|
|
62
|
-
raise(() => {
|
|
63
|
-
id = toast.custom({ title: "Saved", onDismiss });
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
raise(() => {
|
|
67
|
-
toast.dismiss(id);
|
|
68
|
-
});
|
|
69
|
-
expect(onDismiss).toHaveBeenCalledWith(id);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("updates a toast in place", async () => {
|
|
73
|
-
render(<Toaster />);
|
|
74
|
-
|
|
75
|
-
let id = "";
|
|
76
|
-
raise(() => {
|
|
77
|
-
id = toast("Uploading");
|
|
78
|
-
});
|
|
79
|
-
await screen.findByText("Uploading");
|
|
80
|
-
|
|
81
|
-
raise(() => {
|
|
82
|
-
toast.update(id, { title: "Uploaded", variant: "success" });
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
await waitFor(() => {
|
|
86
|
-
expect(screen.getByText("Uploaded")).toBeInTheDocument();
|
|
87
|
-
});
|
|
88
|
-
expect(document.querySelector("[data-slot='toast']")).toHaveClass("border-success/30");
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("exposes a helper per variant", async () => {
|
|
92
|
-
render(<Toaster />);
|
|
93
|
-
|
|
94
|
-
raise(() => {
|
|
95
|
-
toast.default("Plain");
|
|
96
|
-
toast.info("Informational");
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
expect(await screen.findByText("Plain")).toBeInTheDocument();
|
|
100
|
-
expect(screen.getByText("Informational")).toBeInTheDocument();
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it("dismisses everything at once", async () => {
|
|
104
|
-
render(<Toaster />);
|
|
105
|
-
|
|
106
|
-
raise(() => {
|
|
107
|
-
toast("One");
|
|
108
|
-
toast("Two");
|
|
109
|
-
});
|
|
110
|
-
await screen.findByText("One");
|
|
111
|
-
|
|
112
|
-
raise(() => {
|
|
113
|
-
toast.dismissAll();
|
|
114
|
-
});
|
|
115
|
-
await waitFor(() => {
|
|
116
|
-
expect(screen.queryByText("One")).not.toBeInTheDocument();
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
describe("promise", () => {
|
|
121
|
-
it("resolves the pending toast into a success toast", async () => {
|
|
122
|
-
render(<Toaster />);
|
|
123
|
-
|
|
124
|
-
let resolve!: (value: string) => void;
|
|
125
|
-
const pending = new Promise<string>((r) => {
|
|
126
|
-
resolve = r;
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
raise(() => {
|
|
130
|
-
void toast.promise(pending, {
|
|
131
|
-
loading: "Saving…",
|
|
132
|
-
success: (value) => `Saved ${value}`,
|
|
133
|
-
error: "Failed",
|
|
134
|
-
});
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
expect(await screen.findByText("Saving…")).toBeInTheDocument();
|
|
138
|
-
|
|
139
|
-
await act(async () => {
|
|
140
|
-
resolve("project");
|
|
141
|
-
await pending;
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
await waitFor(() => {
|
|
145
|
-
expect(screen.getByText("Saved project")).toBeInTheDocument();
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it("turns a rejection into an error toast and still rejects", async () => {
|
|
150
|
-
render(<Toaster />);
|
|
151
|
-
|
|
152
|
-
const failure = new Error("network");
|
|
153
|
-
const pending = Promise.reject(failure);
|
|
154
|
-
|
|
155
|
-
let returned!: Promise<never>;
|
|
156
|
-
raise(() => {
|
|
157
|
-
returned = toast.promise(pending, {
|
|
158
|
-
loading: "Saving…",
|
|
159
|
-
success: "Saved",
|
|
160
|
-
error: (error) => `Failed: ${(error as Error).message}`,
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
// The original promise is returned untouched, so callers still handle it.
|
|
165
|
-
await expect(returned).rejects.toThrow("network");
|
|
166
|
-
await waitFor(() => {
|
|
167
|
-
expect(screen.getByText("Failed: network")).toBeInTheDocument();
|
|
168
|
-
});
|
|
169
|
-
});
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
describe("Toaster", () => {
|
|
174
|
-
it("renders nothing until a toast is raised", () => {
|
|
175
|
-
render(<Toaster />);
|
|
176
|
-
expect(document.querySelector("[data-slot='toast']")).not.toBeInTheDocument();
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it("renders a title and description", async () => {
|
|
180
|
-
render(<Toaster />);
|
|
181
|
-
raise(() => toast("Project created", { description: "It is ready to use." }));
|
|
182
|
-
|
|
183
|
-
expect(await screen.findByText("Project created")).toBeInTheDocument();
|
|
184
|
-
expect(screen.getByText("It is ready to use.")).toBeInTheDocument();
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it.each([
|
|
188
|
-
["success", "border-success/30"],
|
|
189
|
-
["warning", "border-warning/35"],
|
|
190
|
-
["destructive", "border-destructive/30"],
|
|
191
|
-
["info", "border-info/30"],
|
|
192
|
-
] as const)("applies the %s variant", async (variant, expectedClass) => {
|
|
193
|
-
render(<Toaster />);
|
|
194
|
-
raise(() => toast.custom({ title: "Message", variant }));
|
|
195
|
-
await screen.findByText("Message");
|
|
196
|
-
|
|
197
|
-
expect(document.querySelector("[data-slot='toast']")).toHaveClass(expectedClass);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it("interrupts for problems and waits for a pause otherwise", async () => {
|
|
201
|
-
render(<Toaster />);
|
|
202
|
-
|
|
203
|
-
raise(() => toast.error("Payment failed"));
|
|
204
|
-
const problem = await screen.findByText("Payment failed");
|
|
205
|
-
expect(problem.closest("[data-slot='toast']")).toHaveAttribute("data-type", "foreground");
|
|
206
|
-
|
|
207
|
-
raise(() => toast.success("Saved"));
|
|
208
|
-
const routine = await screen.findByText("Saved");
|
|
209
|
-
expect(routine.closest("[data-slot='toast']")).toHaveAttribute("data-type", "background");
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
it("announces a problem assertively through a live region", async () => {
|
|
213
|
-
render(<Toaster />);
|
|
214
|
-
raise(() => toast.error("Payment failed"));
|
|
215
|
-
|
|
216
|
-
const announcement = await screen.findByRole("status");
|
|
217
|
-
expect(announcement).toHaveAttribute("aria-live", "assertive");
|
|
218
|
-
// The primitive mounts the region first and fills it on a later tick.
|
|
219
|
-
await waitFor(() => {
|
|
220
|
-
expect(announcement).toHaveTextContent("Payment failed");
|
|
221
|
-
});
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
it("dismisses from the close button", async () => {
|
|
225
|
-
const user = userEvent.setup();
|
|
226
|
-
render(<Toaster />);
|
|
227
|
-
raise(() => toast("Saved"));
|
|
228
|
-
await screen.findByText("Saved");
|
|
229
|
-
|
|
230
|
-
await user.click(screen.getByRole("button", { name: "Dismiss" }));
|
|
231
|
-
await waitFor(() => {
|
|
232
|
-
expect(screen.queryByText("Saved")).not.toBeInTheDocument();
|
|
233
|
-
});
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
it("runs an action and exposes alt text for screen readers", async () => {
|
|
237
|
-
const onClick = vi.fn();
|
|
238
|
-
const user = userEvent.setup();
|
|
239
|
-
render(<Toaster />);
|
|
240
|
-
|
|
241
|
-
raise(() =>
|
|
242
|
-
toast.custom({
|
|
243
|
-
title: "Message archived",
|
|
244
|
-
action: { label: "Undo", altText: "Undo archiving the message", onClick },
|
|
245
|
-
}),
|
|
246
|
-
);
|
|
247
|
-
|
|
248
|
-
const action = await screen.findByRole("button", { name: "Undo" });
|
|
249
|
-
// The primitive announces the alt text in place of the visible label, since
|
|
250
|
-
// "Undo" alone is meaningless once the toast is gone.
|
|
251
|
-
expect(action).toHaveAttribute(
|
|
252
|
-
"data-radix-toast-announce-alt",
|
|
253
|
-
"Undo archiving the message",
|
|
254
|
-
);
|
|
255
|
-
const announcement = await screen.findByRole("status");
|
|
256
|
-
await waitFor(() => {
|
|
257
|
-
expect(announcement).toHaveTextContent("Undo archiving the message");
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
await user.click(action);
|
|
261
|
-
expect(onClick).toHaveBeenCalledTimes(1);
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
it("falls back to the action label for alt text", async () => {
|
|
265
|
-
render(<Toaster />);
|
|
266
|
-
raise(() =>
|
|
267
|
-
toast.custom({
|
|
268
|
-
title: "Message archived",
|
|
269
|
-
action: { label: "Undo", onClick: () => undefined },
|
|
270
|
-
}),
|
|
271
|
-
);
|
|
272
|
-
|
|
273
|
-
expect(await screen.findByRole("button", { name: "Undo" })).toHaveAttribute(
|
|
274
|
-
"data-radix-toast-announce-alt",
|
|
275
|
-
"Undo",
|
|
276
|
-
);
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
it.each([
|
|
280
|
-
["top-left", "top-0"],
|
|
281
|
-
["bottom-right", "bottom-0"],
|
|
282
|
-
["bottom-center", "left-1/2"],
|
|
283
|
-
] as const)("positions the viewport for %s", async (position, expectedClass) => {
|
|
284
|
-
const { baseElement } = render(<Toaster position={position} />);
|
|
285
|
-
raise(() => toast("Message"));
|
|
286
|
-
await screen.findByText("Message");
|
|
287
|
-
|
|
288
|
-
expect(baseElement.querySelector("[data-slot='toast-viewport']")).toHaveClass(
|
|
289
|
-
expectedClass,
|
|
290
|
-
);
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
it("has no accessibility violations", async () => {
|
|
294
|
-
const { baseElement } = render(<Toaster />);
|
|
295
|
-
raise(() =>
|
|
296
|
-
toast.custom({
|
|
297
|
-
title: "Project created",
|
|
298
|
-
description: "It is ready to use.",
|
|
299
|
-
action: { label: "View", onClick: () => undefined },
|
|
300
|
-
}),
|
|
301
|
-
);
|
|
302
|
-
await screen.findByText("Project created");
|
|
303
|
-
|
|
304
|
-
await expectNoA11yViolations(baseElement);
|
|
305
|
-
});
|
|
306
|
-
});
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
-
import { Bold, Italic, Underline } from "lucide-react";
|
|
3
|
-
|
|
4
|
-
import { Button } from "@/components/button";
|
|
5
|
-
|
|
6
|
-
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./tooltip";
|
|
7
|
-
|
|
8
|
-
const meta = {
|
|
9
|
-
title: "Overlays/Tooltip",
|
|
10
|
-
component: Tooltip,
|
|
11
|
-
args: { delayDuration: 200 },
|
|
12
|
-
argTypes: {
|
|
13
|
-
delayDuration: { control: { type: "number", min: 0, max: 1000, step: 50 } },
|
|
14
|
-
},
|
|
15
|
-
} satisfies Meta<typeof Tooltip>;
|
|
16
|
-
|
|
17
|
-
export default meta;
|
|
18
|
-
type Story = StoryObj<typeof meta>;
|
|
19
|
-
|
|
20
|
-
export const Default: Story = {
|
|
21
|
-
render: (args) => (
|
|
22
|
-
<Tooltip {...args}>
|
|
23
|
-
<TooltipTrigger asChild>
|
|
24
|
-
<Button variant="outline">Hover or focus me</Button>
|
|
25
|
-
</TooltipTrigger>
|
|
26
|
-
<TooltipContent>Deploys the current branch</TooltipContent>
|
|
27
|
-
</Tooltip>
|
|
28
|
-
),
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* On an icon-only control the accessible name comes from aria-label. The
|
|
33
|
-
* tooltip repeats it visually; it does not supply it.
|
|
34
|
-
*/
|
|
35
|
-
export const IconButtons: Story = {
|
|
36
|
-
parameters: { controls: { disable: true } },
|
|
37
|
-
render: () => (
|
|
38
|
-
<TooltipProvider delayDuration={150}>
|
|
39
|
-
<div className="flex items-center gap-1">
|
|
40
|
-
{[
|
|
41
|
-
{ icon: Bold, label: "Bold" },
|
|
42
|
-
{ icon: Italic, label: "Italic" },
|
|
43
|
-
{ icon: Underline, label: "Underline" },
|
|
44
|
-
].map(({ icon: Icon, label }) => (
|
|
45
|
-
<Tooltip key={label}>
|
|
46
|
-
<TooltipTrigger asChild>
|
|
47
|
-
<Button variant="ghost" size="icon" aria-label={label}>
|
|
48
|
-
<Icon />
|
|
49
|
-
</Button>
|
|
50
|
-
</TooltipTrigger>
|
|
51
|
-
<TooltipContent>{label}</TooltipContent>
|
|
52
|
-
</Tooltip>
|
|
53
|
-
))}
|
|
54
|
-
</div>
|
|
55
|
-
</TooltipProvider>
|
|
56
|
-
),
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
export const Sides: Story = {
|
|
60
|
-
parameters: { controls: { disable: true } },
|
|
61
|
-
render: () => (
|
|
62
|
-
<TooltipProvider delayDuration={0}>
|
|
63
|
-
<div className="flex flex-wrap gap-3">
|
|
64
|
-
{(["top", "right", "bottom", "left"] as const).map((side) => (
|
|
65
|
-
<Tooltip key={side}>
|
|
66
|
-
<TooltipTrigger asChild>
|
|
67
|
-
<Button variant="outline">{side}</Button>
|
|
68
|
-
</TooltipTrigger>
|
|
69
|
-
<TooltipContent side={side}>Anchored {side}</TooltipContent>
|
|
70
|
-
</Tooltip>
|
|
71
|
-
))}
|
|
72
|
-
</div>
|
|
73
|
-
</TooltipProvider>
|
|
74
|
-
),
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
export const LongText: Story = {
|
|
78
|
-
parameters: { controls: { disable: true } },
|
|
79
|
-
render: () => (
|
|
80
|
-
<Tooltip delayDuration={0}>
|
|
81
|
-
<TooltipTrigger asChild>
|
|
82
|
-
<Button variant="outline">Retention</Button>
|
|
83
|
-
</TooltipTrigger>
|
|
84
|
-
<TooltipContent>
|
|
85
|
-
Logs are kept for 30 days on the current plan, then aggregated into daily summaries.
|
|
86
|
-
</TooltipContent>
|
|
87
|
-
</Tooltip>
|
|
88
|
-
),
|
|
89
|
-
};
|