@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.
Files changed (120) hide show
  1. package/dist/components/ai-agent-status/ai-agent-status.d.ts +1 -1
  2. package/dist/components/ai-tool/ai-tool.d.ts +1 -1
  3. package/dist/components/alert/alert.d.ts +1 -1
  4. package/dist/components/avatar/avatar.d.ts +1 -1
  5. package/dist/components/badge/badge.d.ts +1 -1
  6. package/dist/components/button/button.d.ts +1 -1
  7. package/dist/components/progress/progress.d.ts +1 -1
  8. package/dist/components/spinner/spinner.d.ts +1 -1
  9. package/dist/components/toast/toast.d.ts +1 -1
  10. package/package.json +7 -4
  11. package/src/blocks/admin-users/admin-users.stories.tsx +0 -83
  12. package/src/blocks/admin-users/admin-users.test.tsx +0 -159
  13. package/src/blocks/ai-chat/ai-chat.stories.tsx +0 -149
  14. package/src/blocks/ai-chat/ai-chat.test.tsx +0 -166
  15. package/src/blocks/dashboard/dashboard.stories.tsx +0 -106
  16. package/src/blocks/dashboard/dashboard.test.tsx +0 -94
  17. package/src/blocks/forgot-password/forgot-password.stories.tsx +0 -41
  18. package/src/blocks/forgot-password/forgot-password.test.tsx +0 -62
  19. package/src/blocks/login/login.stories.tsx +0 -46
  20. package/src/blocks/login/login.test.tsx +0 -100
  21. package/src/blocks/pricing/pricing.stories.tsx +0 -62
  22. package/src/blocks/pricing/pricing.test.tsx +0 -101
  23. package/src/blocks/settings/settings.stories.tsx +0 -94
  24. package/src/blocks/settings/settings.test.tsx +0 -167
  25. package/src/blocks/signup/signup.stories.tsx +0 -27
  26. package/src/blocks/signup/signup.test.tsx +0 -97
  27. package/src/components/accordion/accordion.stories.tsx +0 -90
  28. package/src/components/accordion/accordion.test.tsx +0 -156
  29. package/src/components/activity-feed/activity-feed.stories.tsx +0 -163
  30. package/src/components/activity-feed/activity-feed.test.tsx +0 -125
  31. package/src/components/ai-agent-status/ai-agent-status.stories.tsx +0 -86
  32. package/src/components/ai-agent-status/ai-agent-status.test.tsx +0 -66
  33. package/src/components/ai-conversation/ai-conversation.stories.tsx +0 -126
  34. package/src/components/ai-conversation/ai-conversation.test.tsx +0 -144
  35. package/src/components/ai-message/ai-message.stories.tsx +0 -106
  36. package/src/components/ai-message/ai-message.test.tsx +0 -160
  37. package/src/components/ai-model-selector/ai-model-selector.stories.tsx +0 -66
  38. package/src/components/ai-model-selector/ai-model-selector.test.tsx +0 -120
  39. package/src/components/ai-prompt-input/ai-prompt-input.stories.tsx +0 -145
  40. package/src/components/ai-prompt-input/ai-prompt-input.test.tsx +0 -273
  41. package/src/components/ai-reasoning/ai-reasoning.stories.tsx +0 -60
  42. package/src/components/ai-reasoning/ai-reasoning.test.tsx +0 -95
  43. package/src/components/ai-response/ai-response.stories.tsx +0 -75
  44. package/src/components/ai-response/ai-response.test.tsx +0 -96
  45. package/src/components/ai-sources/ai-sources.stories.tsx +0 -93
  46. package/src/components/ai-sources/ai-sources.test.tsx +0 -102
  47. package/src/components/ai-token-usage/ai-token-usage.stories.tsx +0 -59
  48. package/src/components/ai-token-usage/ai-token-usage.test.tsx +0 -83
  49. package/src/components/ai-tool/ai-tool.stories.tsx +0 -96
  50. package/src/components/ai-tool/ai-tool.test.tsx +0 -137
  51. package/src/components/alert/alert.stories.tsx +0 -101
  52. package/src/components/alert/alert.test.tsx +0 -94
  53. package/src/components/avatar/avatar.stories.tsx +0 -58
  54. package/src/components/avatar/avatar.test.tsx +0 -64
  55. package/src/components/badge/badge.stories.tsx +0 -68
  56. package/src/components/badge/badge.test.tsx +0 -61
  57. package/src/components/button/button.stories.tsx +0 -128
  58. package/src/components/button/button.test.tsx +0 -224
  59. package/src/components/calendar/calendar.stories.tsx +0 -105
  60. package/src/components/calendar/calendar.test.tsx +0 -255
  61. package/src/components/card/card.stories.tsx +0 -61
  62. package/src/components/card/card.test.tsx +0 -66
  63. package/src/components/checkbox/checkbox.stories.tsx +0 -124
  64. package/src/components/checkbox/checkbox.test.tsx +0 -121
  65. package/src/components/code-block/code-block.stories.tsx +0 -62
  66. package/src/components/code-block/code-block.test.tsx +0 -139
  67. package/src/components/combobox/combobox.stories.tsx +0 -171
  68. package/src/components/combobox/combobox.test.tsx +0 -429
  69. package/src/components/command/command.stories.tsx +0 -152
  70. package/src/components/command/command.test.tsx +0 -380
  71. package/src/components/data-table/data-table.stories.tsx +0 -151
  72. package/src/components/data-table/data-table.test.tsx +0 -288
  73. package/src/components/date-picker/date-picker.stories.tsx +0 -103
  74. package/src/components/date-picker/date-picker.test.tsx +0 -205
  75. package/src/components/dialog/dialog.stories.tsx +0 -140
  76. package/src/components/dialog/dialog.test.tsx +0 -186
  77. package/src/components/drawer/drawer.stories.tsx +0 -111
  78. package/src/components/drawer/drawer.test.tsx +0 -187
  79. package/src/components/dropdown-menu/dropdown-menu.stories.tsx +0 -128
  80. package/src/components/dropdown-menu/dropdown-menu.test.tsx +0 -321
  81. package/src/components/empty-state/empty-state.stories.tsx +0 -70
  82. package/src/components/empty-state/empty-state.test.tsx +0 -97
  83. package/src/components/form/form.stories.tsx +0 -144
  84. package/src/components/form/form.test.tsx +0 -203
  85. package/src/components/input/input.stories.tsx +0 -84
  86. package/src/components/input/input.test.tsx +0 -108
  87. package/src/components/label/label.stories.tsx +0 -51
  88. package/src/components/label/label.test.tsx +0 -55
  89. package/src/components/pagination/pagination.stories.tsx +0 -129
  90. package/src/components/pagination/pagination.test.tsx +0 -119
  91. package/src/components/popover/popover.stories.tsx +0 -84
  92. package/src/components/popover/popover.test.tsx +0 -163
  93. package/src/components/progress/progress.stories.tsx +0 -90
  94. package/src/components/progress/progress.test.tsx +0 -78
  95. package/src/components/radio-group/radio-group.stories.tsx +0 -102
  96. package/src/components/radio-group/radio-group.test.tsx +0 -152
  97. package/src/components/select/select.stories.tsx +0 -137
  98. package/src/components/select/select.test.tsx +0 -209
  99. package/src/components/separator/separator.stories.tsx +0 -42
  100. package/src/components/separator/separator.test.tsx +0 -46
  101. package/src/components/sheet/sheet.stories.tsx +0 -107
  102. package/src/components/sheet/sheet.test.tsx +0 -152
  103. package/src/components/skeleton/skeleton.stories.tsx +0 -46
  104. package/src/components/skeleton/skeleton.test.tsx +0 -57
  105. package/src/components/slider/slider.stories.tsx +0 -87
  106. package/src/components/slider/slider.test.tsx +0 -148
  107. package/src/components/spinner/spinner.stories.tsx +0 -47
  108. package/src/components/spinner/spinner.test.tsx +0 -41
  109. package/src/components/switch/switch.stories.tsx +0 -70
  110. package/src/components/switch/switch.test.tsx +0 -101
  111. package/src/components/table/table.stories.tsx +0 -131
  112. package/src/components/table/table.test.tsx +0 -153
  113. package/src/components/tabs/tabs.stories.tsx +0 -139
  114. package/src/components/tabs/tabs.test.tsx +0 -148
  115. package/src/components/toast/toast.stories.tsx +0 -167
  116. package/src/components/toast/toast.test.tsx +0 -306
  117. package/src/components/tooltip/tooltip.stories.tsx +0 -89
  118. package/src/components/tooltip/tooltip.test.tsx +0 -120
  119. package/src/lib/utils.test.ts +0 -32
  120. 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
- };