@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,94 +0,0 @@
1
- import type { Decorator, Meta, StoryObj } from "@storybook/react-vite";
2
- import { useState } from "react";
3
-
4
- import { SettingsBlock, type SettingsNotification } from "./settings";
5
-
6
- /** Named so its type is nameable in declaration output (TS2883). */
7
- const withPageWidth: Decorator = (Story) => (
8
- <div className="w-[48rem] max-w-full">
9
- <Story />
10
- </div>
11
- );
12
-
13
- const PROFILE = {
14
- name: "Ada Lovelace",
15
- email: "ada@example.com",
16
- bio: "Working on analytical engines.",
17
- };
18
-
19
- const NOTIFICATIONS: SettingsNotification[] = [
20
- {
21
- id: "deploys",
22
- label: "Deployment updates",
23
- description: "When a deploy starts or finishes.",
24
- enabled: true,
25
- },
26
- {
27
- id: "failures",
28
- label: "Build failures",
29
- description: "Only when something breaks.",
30
- enabled: true,
31
- },
32
- {
33
- id: "digest",
34
- label: "Weekly digest",
35
- description: "A summary every Monday morning.",
36
- enabled: false,
37
- },
38
- ];
39
-
40
- const meta = {
41
- title: "Blocks/Settings",
42
- component: SettingsBlock,
43
- args: { profile: PROFILE, notifications: NOTIFICATIONS },
44
- parameters: { controls: { disable: true } },
45
- decorators: [withPageWidth],
46
- } satisfies Meta<typeof SettingsBlock>;
47
-
48
- export default meta;
49
- type Story = StoryObj<typeof meta>;
50
-
51
- export const Default: Story = {};
52
-
53
- /** Switches apply immediately. There is no Save button, and that is the point. */
54
- export const Interactive: Story = {
55
- render: function Interactive() {
56
- const [notifications, setNotifications] = useState(NOTIFICATIONS);
57
- const [pending, setPending] = useState(false);
58
-
59
- return (
60
- <SettingsBlock
61
- profile={PROFILE}
62
- notifications={notifications}
63
- pending={pending}
64
- onToggleNotification={(id, enabled) => {
65
- setNotifications((current) =>
66
- current.map((item) => (item.id === id ? { ...item, enabled } : item)),
67
- );
68
- }}
69
- onSaveProfile={() => {
70
- setPending(true);
71
- window.setTimeout(() => {
72
- setPending(false);
73
- }, 1000);
74
- }}
75
- />
76
- );
77
- },
78
- };
79
-
80
- export const WithExtraTab: Story = {
81
- args: {
82
- extraTabs: [
83
- {
84
- value: "billing",
85
- label: "Billing",
86
- content: (
87
- <p className="p-6 text-sm text-muted-foreground">
88
- You are on the Pro plan, billed yearly. Next invoice 1 October.
89
- </p>
90
- ),
91
- },
92
- ],
93
- },
94
- };
@@ -1,167 +0,0 @@
1
- import { render, screen, waitFor, within } 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 { SettingsBlock, type SettingsNotification } from "./settings";
7
-
8
- const PROFILE = { name: "Ada Lovelace", email: "ada@example.com", bio: "Building things." };
9
-
10
- const NOTIFICATIONS: SettingsNotification[] = [
11
- {
12
- id: "deploys",
13
- label: "Deployment updates",
14
- description: "When a deploy finishes.",
15
- enabled: true,
16
- },
17
- {
18
- id: "digest",
19
- label: "Weekly digest",
20
- description: "A summary every Monday.",
21
- enabled: false,
22
- },
23
- ];
24
-
25
- describe("SettingsBlock", () => {
26
- it("opens on the profile tab", () => {
27
- render(<SettingsBlock profile={PROFILE} notifications={NOTIFICATIONS} />);
28
- expect(screen.getByLabelText("Name")).toHaveValue("Ada Lovelace");
29
- });
30
-
31
- it("saves the profile as a staged change", async () => {
32
- const onSaveProfile = vi.fn();
33
- const user = userEvent.setup();
34
- render(
35
- <SettingsBlock
36
- profile={PROFILE}
37
- notifications={NOTIFICATIONS}
38
- onSaveProfile={onSaveProfile}
39
- />,
40
- );
41
-
42
- await user.clear(screen.getByLabelText("Name"));
43
- await user.type(screen.getByLabelText("Name"), "Ada L");
44
- await user.click(screen.getByRole("button", { name: "Save changes" }));
45
-
46
- expect(onSaveProfile).toHaveBeenCalledWith(expect.objectContaining({ name: "Ada L" }));
47
- });
48
-
49
- it("refuses to save an invalid profile", async () => {
50
- const onSaveProfile = vi.fn();
51
- const user = userEvent.setup();
52
- render(
53
- <SettingsBlock
54
- profile={PROFILE}
55
- notifications={NOTIFICATIONS}
56
- onSaveProfile={onSaveProfile}
57
- />,
58
- );
59
-
60
- await user.clear(screen.getByLabelText("Email"));
61
- await user.type(screen.getByLabelText("Email"), "nope");
62
- await user.click(screen.getByRole("button", { name: "Save changes" }));
63
-
64
- expect(onSaveProfile).not.toHaveBeenCalled();
65
- expect(screen.getByLabelText("Email")).toHaveAttribute("aria-invalid", "true");
66
- });
67
-
68
- describe("notifications", () => {
69
- it("apply immediately, with no Save button", async () => {
70
- const onToggle = vi.fn();
71
- const user = userEvent.setup();
72
- render(
73
- <SettingsBlock
74
- profile={PROFILE}
75
- notifications={NOTIFICATIONS}
76
- onToggleNotification={onToggle}
77
- />,
78
- );
79
-
80
- await user.click(screen.getByRole("tab", { name: "Notifications" }));
81
- await user.click(await screen.findByRole("switch", { name: "Weekly digest" }));
82
-
83
- expect(onToggle).toHaveBeenCalledWith("digest", true);
84
- // A switch that needs saving is a broken promise.
85
- expect(screen.queryByRole("button", { name: "Save changes" })).not.toBeInTheDocument();
86
- });
87
-
88
- it("labels every switch", async () => {
89
- const user = userEvent.setup();
90
- render(<SettingsBlock profile={PROFILE} notifications={NOTIFICATIONS} />);
91
-
92
- await user.click(screen.getByRole("tab", { name: "Notifications" }));
93
- expect(await screen.findByRole("switch", { name: "Deployment updates" })).toBeChecked();
94
- });
95
- });
96
-
97
- describe("the danger zone", () => {
98
- async function openDialog() {
99
- const user = userEvent.setup();
100
- const onDeleteAccount = vi.fn();
101
- render(
102
- <SettingsBlock
103
- profile={PROFILE}
104
- notifications={NOTIFICATIONS}
105
- onDeleteAccount={onDeleteAccount}
106
- />,
107
- );
108
-
109
- await user.click(screen.getByRole("tab", { name: "Danger zone" }));
110
- await user.click(await screen.findByRole("button", { name: "Delete account" }));
111
- const dialog = await screen.findByRole("dialog");
112
- return { user, onDeleteAccount, dialog };
113
- }
114
-
115
- it("names what will be destroyed", async () => {
116
- await openDialog();
117
- // Not "Are you sure?" — that tells nobody anything they did not know.
118
- expect(
119
- screen.getByRole("dialog", { name: /Delete Ada Lovelace.s account\?/ }),
120
- ).toBeInTheDocument();
121
- expect(screen.getByText(/permanently deletes the account/)).toBeInTheDocument();
122
- });
123
-
124
- it("stays disabled until the email is typed exactly", async () => {
125
- const { user, dialog } = await openDialog();
126
-
127
- // Scoped to the dialog: while it is open the trigger behind it is inert,
128
- // so a document-wide query finds only one button and the index is a trap.
129
- const confirm = within(dialog).getByRole("button", { name: "Delete account" });
130
- expect(confirm).toBeDisabled();
131
-
132
- await user.type(screen.getByLabelText(/to confirm/), "ada@example.com");
133
- await waitFor(() => {
134
- expect(confirm).toBeEnabled();
135
- });
136
- });
137
-
138
- it("deletes only after confirmation", async () => {
139
- const { user, onDeleteAccount, dialog } = await openDialog();
140
-
141
- await user.type(screen.getByLabelText(/to confirm/), "ada@example.com");
142
- await user.click(within(dialog).getByRole("button", { name: "Delete account" }));
143
- expect(onDeleteAccount).toHaveBeenCalledTimes(1);
144
- });
145
- });
146
-
147
- it("accepts extra tabs", async () => {
148
- const user = userEvent.setup();
149
- render(
150
- <SettingsBlock
151
- profile={PROFILE}
152
- notifications={NOTIFICATIONS}
153
- extraTabs={[{ value: "billing", label: "Billing", content: <p>Your plan is Pro.</p> }]}
154
- />,
155
- );
156
-
157
- await user.click(screen.getByRole("tab", { name: "Billing" }));
158
- expect(await screen.findByText("Your plan is Pro.")).toBeInTheDocument();
159
- });
160
-
161
- it("has no accessibility violations", async () => {
162
- const { container } = render(
163
- <SettingsBlock profile={PROFILE} notifications={NOTIFICATIONS} />,
164
- );
165
- await expectNoA11yViolations(container);
166
- });
167
- });
@@ -1,27 +0,0 @@
1
- import type { Meta, StoryObj } from "@storybook/react-vite";
2
-
3
- import { SignupBlock } from "./signup";
4
-
5
- const meta = {
6
- title: "Blocks/Sign up",
7
- component: SignupBlock,
8
- parameters: { layout: "centered", controls: { disable: true } },
9
- } satisfies Meta<typeof SignupBlock>;
10
-
11
- export default meta;
12
- type Story = StoryObj<typeof meta>;
13
-
14
- /** Type a password to see the strength hint — it advises, it does not gate. */
15
- export const Default: Story = {};
16
-
17
- export const ShorterMinimum: Story = {
18
- args: { minPasswordLength: 8 },
19
- };
20
-
21
- export const WithServerError: Story = {
22
- args: { error: "An account already exists for that address." },
23
- };
24
-
25
- export const Submitting: Story = {
26
- args: { pending: true },
27
- };
@@ -1,97 +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 { SignupBlock, estimatePasswordStrength } from "./signup";
7
-
8
- describe("estimatePasswordStrength", () => {
9
- it("reports too short below the minimum", () => {
10
- expect(estimatePasswordStrength("abc", 12).label).toBe("Too short");
11
- });
12
-
13
- it("rises with length and variety", () => {
14
- const short = estimatePasswordStrength("aaaaaaaaaaaa", 12);
15
- const varied = estimatePasswordStrength("Correct-Horse-9-Battery", 12);
16
- expect(varied.score).toBeGreaterThan(short.score);
17
- });
18
-
19
- it("never exceeds 100", () => {
20
- expect(estimatePasswordStrength("A".repeat(200) + "1!aA", 12).score).toBeLessThanOrEqual(
21
- 100,
22
- );
23
- });
24
-
25
- it("prompts when empty", () => {
26
- expect(estimatePasswordStrength("", 12).label).toBe("Enter a password");
27
- });
28
- });
29
-
30
- describe("SignupBlock", () => {
31
- it("labels every field", () => {
32
- render(<SignupBlock />);
33
- expect(screen.getByLabelText("Name")).toBeInTheDocument();
34
- expect(screen.getByLabelText("Email")).toBeInTheDocument();
35
- expect(screen.getByLabelText("Password")).toBeInTheDocument();
36
- });
37
-
38
- it("asks password managers for a new password, not an existing one", () => {
39
- render(<SignupBlock />);
40
- expect(screen.getByLabelText("Password")).toHaveAttribute("autocomplete", "new-password");
41
- });
42
-
43
- it("states the length requirement before anyone gets it wrong", () => {
44
- render(<SignupBlock minPasswordLength={12} />);
45
- expect(screen.getByLabelText("Password")).toHaveAccessibleDescription(
46
- "At least 12 characters.",
47
- );
48
- });
49
-
50
- it("shows the strength as text, with the bar hidden", async () => {
51
- const user = userEvent.setup();
52
- const { container } = render(<SignupBlock />);
53
-
54
- await user.type(screen.getByLabelText("Password"), "Correct-Horse-9-Battery");
55
-
56
- expect(screen.getByText("Strong")).toBeInTheDocument();
57
- // The label already says it; the bar must not be announced as well.
58
- expect(container.querySelector("[data-slot='progress']")).toHaveAttribute(
59
- "aria-hidden",
60
- "true",
61
- );
62
- });
63
-
64
- it("gates on length, not on the strength estimate", async () => {
65
- const onSubmit = vi.fn();
66
- const user = userEvent.setup();
67
- render(<SignupBlock onSubmit={onSubmit} minPasswordLength={12} />);
68
-
69
- await user.type(screen.getByLabelText("Email"), "ada@example.com");
70
- // Weak by the estimate, but long enough — and length is the actual rule.
71
- await user.type(screen.getByLabelText("Password"), "aaaaaaaaaaaaaa");
72
- await user.click(screen.getByLabelText(/I agree to the terms/));
73
- await user.click(screen.getByRole("button", { name: "Create account" }));
74
-
75
- expect(onSubmit).toHaveBeenCalledWith(
76
- expect.objectContaining({ email: "ada@example.com" }),
77
- );
78
- });
79
-
80
- it("requires the terms to be accepted, and says so", async () => {
81
- const onSubmit = vi.fn();
82
- const user = userEvent.setup();
83
- render(<SignupBlock onSubmit={onSubmit} />);
84
-
85
- await user.type(screen.getByLabelText("Email"), "ada@example.com");
86
- await user.type(screen.getByLabelText("Password"), "aaaaaaaaaaaaaa");
87
- await user.click(screen.getByRole("button", { name: "Create account" }));
88
-
89
- expect(onSubmit).not.toHaveBeenCalled();
90
- expect(screen.getByText("Accept the terms to continue.")).toBeInTheDocument();
91
- });
92
-
93
- it("has no accessibility violations", async () => {
94
- const { container } = render(<SignupBlock />);
95
- await expectNoA11yViolations(container);
96
- });
97
- });
@@ -1,90 +0,0 @@
1
- import type { Meta, StoryObj } from "@storybook/react-vite";
2
-
3
- import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "./accordion";
4
-
5
- /**
6
- * Annotated rather than inferred with `satisfies`. These components are direct
7
- * re-exports of Radix primitives, and inferring the meta type makes the emitted
8
- * declaration reference Radix-internal prop types that are not nameable from
9
- * this path (TS2883).
10
- */
11
- const meta: Meta<typeof Accordion> = {
12
- title: "Data/Accordion",
13
- component: Accordion,
14
- parameters: { controls: { disable: true } },
15
- decorators: [
16
- (Story) => (
17
- <div className="w-96">
18
- <Story />
19
- </div>
20
- ),
21
- ],
22
- };
23
-
24
- export default meta;
25
-
26
- /** Typed from the component: Accordion's props are a discriminated union on
27
- * `type`, which collapses to `never` if inferred through the meta. */
28
- type Story = StoryObj<typeof Accordion>;
29
-
30
- export const Default: Story = {
31
- render: () => (
32
- <Accordion type="single" collapsible defaultValue="shipping">
33
- <AccordionItem value="shipping">
34
- <AccordionTrigger>How long does shipping take?</AccordionTrigger>
35
- <AccordionContent>
36
- Orders ship within two business days and arrive in three to five days after that.
37
- </AccordionContent>
38
- </AccordionItem>
39
- <AccordionItem value="returns">
40
- <AccordionTrigger>What is the return policy?</AccordionTrigger>
41
- <AccordionContent>
42
- Return anything unused within thirty days for a full refund.
43
- </AccordionContent>
44
- </AccordionItem>
45
- <AccordionItem value="support">
46
- <AccordionTrigger>How do I contact support?</AccordionTrigger>
47
- <AccordionContent>
48
- Email support@example.com and someone will reply within one business day.
49
- </AccordionContent>
50
- </AccordionItem>
51
- </Accordion>
52
- ),
53
- };
54
-
55
- /** Independent sections, so more than one can be open at a time. */
56
- export const Multiple: Story = {
57
- render: () => (
58
- <Accordion type="multiple" defaultValue={["build"]}>
59
- <AccordionItem value="build">
60
- <AccordionTrigger>Build settings</AccordionTrigger>
61
- <AccordionContent>
62
- Framework preset, build command and output directory.
63
- </AccordionContent>
64
- </AccordionItem>
65
- <AccordionItem value="env">
66
- <AccordionTrigger>Environment variables</AccordionTrigger>
67
- <AccordionContent>Values available at build and run time.</AccordionContent>
68
- </AccordionItem>
69
- <AccordionItem value="domains">
70
- <AccordionTrigger>Domains</AccordionTrigger>
71
- <AccordionContent>Custom domains and certificates.</AccordionContent>
72
- </AccordionItem>
73
- </Accordion>
74
- ),
75
- };
76
-
77
- export const WithDisabledItem: Story = {
78
- render: () => (
79
- <Accordion type="single" collapsible>
80
- <AccordionItem value="a">
81
- <AccordionTrigger>Available section</AccordionTrigger>
82
- <AccordionContent>Anyone can open this.</AccordionContent>
83
- </AccordionItem>
84
- <AccordionItem value="b">
85
- <AccordionTrigger disabled>Enterprise only</AccordionTrigger>
86
- <AccordionContent>Upgrade to view.</AccordionContent>
87
- </AccordionItem>
88
- </Accordion>
89
- ),
90
- };
@@ -1,156 +0,0 @@
1
- import { render, screen, waitFor } 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 { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "./accordion";
7
-
8
- function Single({ onValueChange }: { onValueChange?: (value: string) => void } = {}) {
9
- return (
10
- <Accordion type="single" collapsible onValueChange={onValueChange}>
11
- <AccordionItem value="shipping">
12
- <AccordionTrigger>Shipping</AccordionTrigger>
13
- <AccordionContent>Ships in two days.</AccordionContent>
14
- </AccordionItem>
15
- <AccordionItem value="returns">
16
- <AccordionTrigger>Returns</AccordionTrigger>
17
- <AccordionContent>Thirty day returns.</AccordionContent>
18
- </AccordionItem>
19
- </Accordion>
20
- );
21
- }
22
-
23
- describe("Accordion", () => {
24
- it("starts collapsed", () => {
25
- render(<Single />);
26
- expect(screen.queryByText("Ships in two days.")).not.toBeInTheDocument();
27
- });
28
-
29
- it("expands a section on click", async () => {
30
- const user = userEvent.setup();
31
- render(<Single />);
32
-
33
- await user.click(screen.getByRole("button", { name: "Shipping" }));
34
- expect(await screen.findByText("Ships in two days.")).toBeInTheDocument();
35
- });
36
-
37
- it("exposes each trigger as a button inside a heading", () => {
38
- render(<Single />);
39
-
40
- const heading = screen.getByRole("heading", { name: "Shipping" });
41
- expect(heading).toBeInTheDocument();
42
- expect(heading).toContainElement(screen.getByRole("button", { name: "Shipping" }));
43
- });
44
-
45
- it("reports expanded state on the trigger", async () => {
46
- const user = userEvent.setup();
47
- render(<Single />);
48
-
49
- const trigger = screen.getByRole("button", { name: "Shipping" });
50
- expect(trigger).toHaveAttribute("aria-expanded", "false");
51
-
52
- await user.click(trigger);
53
- await waitFor(() => {
54
- expect(trigger).toHaveAttribute("aria-expanded", "true");
55
- });
56
- });
57
-
58
- it("closes the previous section in single mode", async () => {
59
- const user = userEvent.setup();
60
- render(<Single />);
61
-
62
- await user.click(screen.getByRole("button", { name: "Shipping" }));
63
- await screen.findByText("Ships in two days.");
64
-
65
- await user.click(screen.getByRole("button", { name: "Returns" }));
66
- await screen.findByText("Thirty day returns.");
67
- await waitFor(() => {
68
- expect(screen.queryByText("Ships in two days.")).not.toBeInTheDocument();
69
- });
70
- });
71
-
72
- it("collapses an open section when collapsible", async () => {
73
- const user = userEvent.setup();
74
- render(<Single />);
75
-
76
- const trigger = screen.getByRole("button", { name: "Shipping" });
77
- await user.click(trigger);
78
- await screen.findByText("Ships in two days.");
79
-
80
- await user.click(trigger);
81
- await waitFor(() => {
82
- expect(screen.queryByText("Ships in two days.")).not.toBeInTheDocument();
83
- });
84
- });
85
-
86
- it("keeps sections independent in multiple mode", async () => {
87
- const user = userEvent.setup();
88
- render(
89
- <Accordion type="multiple">
90
- <AccordionItem value="a">
91
- <AccordionTrigger>A</AccordionTrigger>
92
- <AccordionContent>Panel A</AccordionContent>
93
- </AccordionItem>
94
- <AccordionItem value="b">
95
- <AccordionTrigger>B</AccordionTrigger>
96
- <AccordionContent>Panel B</AccordionContent>
97
- </AccordionItem>
98
- </Accordion>,
99
- );
100
-
101
- await user.click(screen.getByRole("button", { name: "A" }));
102
- await user.click(screen.getByRole("button", { name: "B" }));
103
-
104
- expect(screen.getByText("Panel A")).toBeInTheDocument();
105
- expect(screen.getByText("Panel B")).toBeInTheDocument();
106
- });
107
-
108
- it("moves between triggers with the arrow keys", async () => {
109
- const user = userEvent.setup();
110
- render(<Single />);
111
-
112
- await user.tab();
113
- expect(screen.getByRole("button", { name: "Shipping" })).toHaveFocus();
114
-
115
- await user.keyboard("{ArrowDown}");
116
- expect(screen.getByRole("button", { name: "Returns" })).toHaveFocus();
117
- });
118
-
119
- it("toggles from the keyboard", async () => {
120
- const user = userEvent.setup();
121
- render(<Single />);
122
-
123
- await user.tab();
124
- await user.keyboard("{Enter}");
125
- expect(await screen.findByText("Ships in two days.")).toBeInTheDocument();
126
- });
127
-
128
- it("reports value changes", async () => {
129
- const onValueChange = vi.fn();
130
- const user = userEvent.setup();
131
- render(<Single onValueChange={onValueChange} />);
132
-
133
- await user.click(screen.getByRole("button", { name: "Shipping" }));
134
- expect(onValueChange).toHaveBeenCalledWith("shipping");
135
- });
136
-
137
- it("lets a consumer className override a conflicting utility", () => {
138
- render(
139
- <Accordion type="single" collapsible>
140
- <AccordionItem value="a" className="border-b-2" data-testid="item">
141
- <AccordionTrigger>A</AccordionTrigger>
142
- <AccordionContent>Panel</AccordionContent>
143
- </AccordionItem>
144
- </Accordion>,
145
- );
146
-
147
- const item = screen.getByTestId("item");
148
- expect(item).toHaveClass("border-b-2");
149
- expect(item).not.toHaveClass("border-b");
150
- });
151
-
152
- it("has no accessibility violations", async () => {
153
- const { container } = render(<Single />);
154
- await expectNoA11yViolations(container);
155
- });
156
- });