@mzc-fe/design-system 0.0.1-rc.0

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 (160) hide show
  1. package/.husky/pre-push +21 -0
  2. package/.storybook/main.ts +11 -0
  3. package/.storybook/preview.tsx +30 -0
  4. package/.vscode/settings.json +12 -0
  5. package/.vscode/tailwind.json +105 -0
  6. package/README.md +136 -0
  7. package/bitbucket-pipelines.yml +52 -0
  8. package/components.json +21 -0
  9. package/eslint.config.js +38 -0
  10. package/package.json +98 -0
  11. package/public/vite.svg +1 -0
  12. package/src/components/accordion.stories.tsx +258 -0
  13. package/src/components/accordion.test.tsx +390 -0
  14. package/src/components/accordion.tsx +64 -0
  15. package/src/components/alert-dialog.stories.tsx +213 -0
  16. package/src/components/alert-dialog.test.tsx +80 -0
  17. package/src/components/alert-dialog.tsx +155 -0
  18. package/src/components/alert.stories.tsx +84 -0
  19. package/src/components/alert.test.tsx +35 -0
  20. package/src/components/alert.tsx +66 -0
  21. package/src/components/aspect-ratio.stories.tsx +97 -0
  22. package/src/components/aspect-ratio.test.tsx +47 -0
  23. package/src/components/aspect-ratio.tsx +11 -0
  24. package/src/components/avatar.stories.tsx +76 -0
  25. package/src/components/avatar.test.tsx +50 -0
  26. package/src/components/avatar.tsx +51 -0
  27. package/src/components/badge.stories.tsx +64 -0
  28. package/src/components/badge.test.tsx +34 -0
  29. package/src/components/badge.tsx +46 -0
  30. package/src/components/breadcrumb.stories.tsx +86 -0
  31. package/src/components/breadcrumb.test.tsx +74 -0
  32. package/src/components/breadcrumb.tsx +109 -0
  33. package/src/components/button-group.stories.tsx +62 -0
  34. package/src/components/button-group.tsx +83 -0
  35. package/src/components/button.stories.tsx +118 -0
  36. package/src/components/button.test.tsx +64 -0
  37. package/src/components/button.tsx +62 -0
  38. package/src/components/calendar.stories.tsx +81 -0
  39. package/src/components/calendar.tsx +220 -0
  40. package/src/components/card.stories.tsx +110 -0
  41. package/src/components/card.test.tsx +56 -0
  42. package/src/components/card.tsx +92 -0
  43. package/src/components/carousel.stories.tsx +90 -0
  44. package/src/components/carousel.tsx +239 -0
  45. package/src/components/chart.tsx +357 -0
  46. package/src/components/checkbox.stories.tsx +108 -0
  47. package/src/components/checkbox.test.tsx +67 -0
  48. package/src/components/checkbox.tsx +32 -0
  49. package/src/components/collapsible.stories.tsx +106 -0
  50. package/src/components/collapsible.test.tsx +92 -0
  51. package/src/components/collapsible.tsx +31 -0
  52. package/src/components/command.stories.tsx +90 -0
  53. package/src/components/command.tsx +182 -0
  54. package/src/components/context-menu.stories.tsx +63 -0
  55. package/src/components/context-menu.tsx +252 -0
  56. package/src/components/dialog.stories.tsx +128 -0
  57. package/src/components/dialog.tsx +141 -0
  58. package/src/components/drawer.stories.tsx +104 -0
  59. package/src/components/drawer.tsx +135 -0
  60. package/src/components/dropdown-menu.stories.tsx +97 -0
  61. package/src/components/dropdown-menu.tsx +255 -0
  62. package/src/components/empty.stories.tsx +90 -0
  63. package/src/components/empty.test.tsx +55 -0
  64. package/src/components/empty.tsx +104 -0
  65. package/src/components/field.tsx +246 -0
  66. package/src/components/form.tsx +168 -0
  67. package/src/components/hover-card.stories.tsx +66 -0
  68. package/src/components/hover-card.tsx +44 -0
  69. package/src/components/input-group.stories.tsx +57 -0
  70. package/src/components/input-group.test.tsx +40 -0
  71. package/src/components/input-group.tsx +170 -0
  72. package/src/components/input-otp.stories.tsx +94 -0
  73. package/src/components/input-otp.test.tsx +60 -0
  74. package/src/components/input-otp.tsx +75 -0
  75. package/src/components/input.stories.tsx +94 -0
  76. package/src/components/input.test.tsx +53 -0
  77. package/src/components/input.tsx +21 -0
  78. package/src/components/item.tsx +193 -0
  79. package/src/components/kbd.stories.tsx +100 -0
  80. package/src/components/kbd.test.tsx +28 -0
  81. package/src/components/kbd.tsx +28 -0
  82. package/src/components/label.stories.tsx +48 -0
  83. package/src/components/label.test.tsx +28 -0
  84. package/src/components/label.tsx +24 -0
  85. package/src/components/menubar.tsx +274 -0
  86. package/src/components/navigation-menu.tsx +168 -0
  87. package/src/components/pagination.stories.tsx +107 -0
  88. package/src/components/pagination.tsx +127 -0
  89. package/src/components/popover.stories.tsx +102 -0
  90. package/src/components/popover.tsx +48 -0
  91. package/src/components/progress.stories.tsx +76 -0
  92. package/src/components/progress.test.tsx +36 -0
  93. package/src/components/progress.tsx +29 -0
  94. package/src/components/radio-group.stories.tsx +73 -0
  95. package/src/components/radio-group.test.tsx +74 -0
  96. package/src/components/radio-group.tsx +45 -0
  97. package/src/components/resizable.stories.tsx +120 -0
  98. package/src/components/resizable.tsx +54 -0
  99. package/src/components/scroll-area.stories.tsx +64 -0
  100. package/src/components/scroll-area.test.tsx +46 -0
  101. package/src/components/scroll-area.tsx +58 -0
  102. package/src/components/select.stories.tsx +111 -0
  103. package/src/components/select.test.tsx +90 -0
  104. package/src/components/select.tsx +188 -0
  105. package/src/components/separator.stories.tsx +76 -0
  106. package/src/components/separator.test.tsx +24 -0
  107. package/src/components/separator.tsx +28 -0
  108. package/src/components/sheet.stories.tsx +122 -0
  109. package/src/components/sheet.tsx +137 -0
  110. package/src/components/sidebar.tsx +726 -0
  111. package/src/components/skeleton.stories.tsx +53 -0
  112. package/src/components/skeleton.test.tsx +24 -0
  113. package/src/components/skeleton.tsx +13 -0
  114. package/src/components/slider.stories.tsx +97 -0
  115. package/src/components/slider.test.tsx +49 -0
  116. package/src/components/slider.tsx +63 -0
  117. package/src/components/sonner.stories.tsx +96 -0
  118. package/src/components/sonner.tsx +38 -0
  119. package/src/components/spinner.stories.tsx +54 -0
  120. package/src/components/spinner.test.tsx +30 -0
  121. package/src/components/spinner.tsx +16 -0
  122. package/src/components/switch.stories.tsx +108 -0
  123. package/src/components/switch.test.tsx +62 -0
  124. package/src/components/switch.tsx +31 -0
  125. package/src/components/table.stories.tsx +139 -0
  126. package/src/components/table.test.tsx +85 -0
  127. package/src/components/table.tsx +114 -0
  128. package/src/components/tabs.stories.tsx +99 -0
  129. package/src/components/tabs.test.tsx +64 -0
  130. package/src/components/tabs.tsx +66 -0
  131. package/src/components/textarea.stories.tsx +89 -0
  132. package/src/components/textarea.test.tsx +53 -0
  133. package/src/components/textarea.tsx +18 -0
  134. package/src/components/toggle-group.stories.tsx +108 -0
  135. package/src/components/toggle-group.test.tsx +66 -0
  136. package/src/components/toggle-group.tsx +81 -0
  137. package/src/components/toggle.stories.tsx +98 -0
  138. package/src/components/toggle.test.tsx +42 -0
  139. package/src/components/toggle.tsx +45 -0
  140. package/src/components/tooltip.stories.tsx +111 -0
  141. package/src/components/tooltip.tsx +61 -0
  142. package/src/foundations/README.md +141 -0
  143. package/src/foundations/ThemeProvider.tsx +77 -0
  144. package/src/foundations/color.css +232 -0
  145. package/src/foundations/color.stories.tsx +719 -0
  146. package/src/foundations/palette.css +249 -0
  147. package/src/foundations/spacing.css +8 -0
  148. package/src/foundations/typography.css +143 -0
  149. package/src/foundations/typography.stories.tsx +17 -0
  150. package/src/hooks/use-mobile.ts +19 -0
  151. package/src/index.css +176 -0
  152. package/src/index.ts +336 -0
  153. package/src/lib/utils.ts +6 -0
  154. package/src/test/setup.ts +8 -0
  155. package/src/vite-env.d.ts +1 -0
  156. package/tsconfig.app.json +33 -0
  157. package/tsconfig.json +13 -0
  158. package/tsconfig.node.json +25 -0
  159. package/vite.config.ts +30 -0
  160. package/vitest.config.ts +25 -0
@@ -0,0 +1,258 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import {
3
+ Accordion,
4
+ AccordionItem,
5
+ AccordionTrigger,
6
+ AccordionContent,
7
+ } from "./accordion";
8
+
9
+ const meta = {
10
+ title: "Components/Accordion",
11
+ component: Accordion,
12
+ parameters: {
13
+ layout: "padded",
14
+ },
15
+ tags: ["autodocs"],
16
+ argTypes: {
17
+ type: {
18
+ control: "select",
19
+ options: ["single", "multiple"],
20
+ description:
21
+ "The type of accordion. 'single' allows only one item open at a time, 'multiple' allows multiple items open.",
22
+ table: {
23
+ type: { summary: '"single" | "multiple"' },
24
+ defaultValue: { summary: '"single"' },
25
+ },
26
+ },
27
+ collapsible: {
28
+ control: "boolean",
29
+ description:
30
+ "When type is 'single', allows closing the opened item by clicking it again.",
31
+ table: {
32
+ type: { summary: "boolean" },
33
+ defaultValue: { summary: "false" },
34
+ },
35
+ },
36
+ defaultValue: {
37
+ control: "text",
38
+ description:
39
+ "The default value(s) of the accordion item(s) that should be open.",
40
+ table: {
41
+ type: { summary: "string | string[]" },
42
+ },
43
+ },
44
+ value: {
45
+ control: "text",
46
+ description:
47
+ "The controlled value(s) of the accordion item(s) that should be open.",
48
+ table: {
49
+ type: { summary: "string | string[]" },
50
+ },
51
+ },
52
+ disabled: {
53
+ control: "boolean",
54
+ description: "Disables all accordion items.",
55
+ table: {
56
+ type: { summary: "boolean" },
57
+ defaultValue: { summary: "false" },
58
+ },
59
+ },
60
+ className: {
61
+ control: "text",
62
+ description: "Additional CSS classes to apply to the accordion.",
63
+ table: {
64
+ type: { summary: "string" },
65
+ },
66
+ },
67
+ },
68
+ } satisfies Meta<typeof Accordion>;
69
+
70
+ export default meta;
71
+ type Story = StoryObj<typeof meta>;
72
+
73
+ export const Default: Story = {
74
+ args: {
75
+ type: "single",
76
+ collapsible: true,
77
+ className: "w-full",
78
+ },
79
+ render: (args) => (
80
+ <Accordion {...args}>
81
+ <AccordionItem value="item-1">
82
+ <AccordionTrigger>Is it accessible?</AccordionTrigger>
83
+ <AccordionContent>
84
+ Yes. It adheres to the WAI-ARIA design pattern and is fully
85
+ accessible.
86
+ </AccordionContent>
87
+ </AccordionItem>
88
+ <AccordionItem value="item-2">
89
+ <AccordionTrigger>Is it styled?</AccordionTrigger>
90
+ <AccordionContent>
91
+ Yes. It comes with default styles that match the other components'
92
+ aesthetic.
93
+ </AccordionContent>
94
+ </AccordionItem>
95
+ <AccordionItem value="item-3">
96
+ <AccordionTrigger>Is it animated?</AccordionTrigger>
97
+ <AccordionContent>
98
+ Yes. It's animated by default, but you can disable it if you prefer.
99
+ </AccordionContent>
100
+ </AccordionItem>
101
+ </Accordion>
102
+ ),
103
+ };
104
+
105
+ export const Multiple: Story = {
106
+ args: {
107
+ type: "multiple",
108
+ className: "w-full",
109
+ },
110
+ argTypes: {
111
+ type: {
112
+ table: {
113
+ disable: true,
114
+ },
115
+ },
116
+ collapsible: {
117
+ table: {
118
+ disable: true,
119
+ },
120
+ },
121
+ },
122
+ render: (args) => (
123
+ <Accordion {...args}>
124
+ <AccordionItem value="item-1">
125
+ <AccordionTrigger>Can I open multiple items?</AccordionTrigger>
126
+ <AccordionContent>
127
+ Yes! With the "multiple" type, you can have multiple items open at the
128
+ same time.
129
+ </AccordionContent>
130
+ </AccordionItem>
131
+ <AccordionItem value="item-2">
132
+ <AccordionTrigger>How does it work?</AccordionTrigger>
133
+ <AccordionContent>
134
+ Each accordion item can be independently opened or closed, allowing
135
+ users to view multiple sections simultaneously.
136
+ </AccordionContent>
137
+ </AccordionItem>
138
+ <AccordionItem value="item-3">
139
+ <AccordionTrigger>What are the use cases?</AccordionTrigger>
140
+ <AccordionContent>
141
+ This is useful for FAQ sections, settings panels, or any content where
142
+ users might want to compare information across different sections.
143
+ </AccordionContent>
144
+ </AccordionItem>
145
+ </Accordion>
146
+ ),
147
+ };
148
+
149
+ export const SingleItem: Story = {
150
+ args: {
151
+ type: "single",
152
+ collapsible: true,
153
+ className: "w-full",
154
+ },
155
+ argTypes: {
156
+ type: {
157
+ table: {
158
+ disable: true,
159
+ },
160
+ },
161
+ },
162
+ render: (args) => (
163
+ <Accordion {...args}>
164
+ <AccordionItem value="item-1">
165
+ <AccordionTrigger>What is this component?</AccordionTrigger>
166
+ <AccordionContent>
167
+ This is an accordion component built with Radix UI. It provides a
168
+ collapsible content area that can be toggled open and closed.
169
+ </AccordionContent>
170
+ </AccordionItem>
171
+ </Accordion>
172
+ ),
173
+ };
174
+
175
+ export const LongContent: Story = {
176
+ args: {
177
+ type: "single",
178
+ collapsible: true,
179
+ className: "w-full",
180
+ },
181
+ argTypes: {
182
+ type: {
183
+ table: {
184
+ disable: true,
185
+ },
186
+ },
187
+ },
188
+ render: (args) => (
189
+ <Accordion {...args}>
190
+ <AccordionItem value="item-1">
191
+ <AccordionTrigger>Item with long content</AccordionTrigger>
192
+ <AccordionContent>
193
+ <div className="space-y-2">
194
+ <p>
195
+ This accordion item contains longer content to demonstrate how the
196
+ component handles text that spans multiple lines or paragraphs.
197
+ </p>
198
+ <p>
199
+ The content area will expand to accommodate the full text, and the
200
+ animation will smoothly transition between the collapsed and
201
+ expanded states.
202
+ </p>
203
+ <p>
204
+ This is useful for displaying detailed information, descriptions,
205
+ or any content that requires more space than a simple one-liner.
206
+ </p>
207
+ <ul className="list-disc list-inside space-y-1 ml-4">
208
+ <li>First bullet point</li>
209
+ <li>Second bullet point</li>
210
+ <li>Third bullet point</li>
211
+ </ul>
212
+ </div>
213
+ </AccordionContent>
214
+ </AccordionItem>
215
+ <AccordionItem value="item-2">
216
+ <AccordionTrigger>Another item</AccordionTrigger>
217
+ <AccordionContent>
218
+ This is a shorter content example to show the contrast between items
219
+ with different content lengths.
220
+ </AccordionContent>
221
+ </AccordionItem>
222
+ </Accordion>
223
+ ),
224
+ };
225
+
226
+ export const WithCustomStyling: Story = {
227
+ args: {
228
+ type: "single",
229
+ collapsible: true,
230
+ className: "w-full max-w-2xl",
231
+ },
232
+ argTypes: {
233
+ type: {
234
+ table: {
235
+ disable: true,
236
+ },
237
+ },
238
+ },
239
+ render: (args) => (
240
+ <Accordion {...args}>
241
+ <AccordionItem value="item-1" className="border-2 border-primary/20">
242
+ <AccordionTrigger className="text-lg font-semibold">
243
+ Custom styled item
244
+ </AccordionTrigger>
245
+ <AccordionContent className="text-base">
246
+ This accordion item has custom styling applied to demonstrate how you
247
+ can customize the appearance of individual items.
248
+ </AccordionContent>
249
+ </AccordionItem>
250
+ <AccordionItem value="item-2">
251
+ <AccordionTrigger>Default styled item</AccordionTrigger>
252
+ <AccordionContent>
253
+ This item uses the default styling for comparison.
254
+ </AccordionContent>
255
+ </AccordionItem>
256
+ </Accordion>
257
+ ),
258
+ };
@@ -0,0 +1,390 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { render } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import {
5
+ Accordion,
6
+ AccordionItem,
7
+ AccordionTrigger,
8
+ AccordionContent,
9
+ } from "./accordion";
10
+
11
+ describe("Accordion", () => {
12
+ describe("Default (Single type with collapsible)", () => {
13
+ it("should render accordion with multiple items", () => {
14
+ const { getByText } = render(
15
+ <Accordion type="single" collapsible className="w-full">
16
+ <AccordionItem value="item-1">
17
+ <AccordionTrigger>Is it accessible?</AccordionTrigger>
18
+ <AccordionContent>
19
+ Yes. It adheres to the WAI-ARIA design pattern and is fully
20
+ accessible.
21
+ </AccordionContent>
22
+ </AccordionItem>
23
+ <AccordionItem value="item-2">
24
+ <AccordionTrigger>Is it styled?</AccordionTrigger>
25
+ <AccordionContent>
26
+ Yes. It comes with default styles that match the other components'
27
+ aesthetic.
28
+ </AccordionContent>
29
+ </AccordionItem>
30
+ <AccordionItem value="item-3">
31
+ <AccordionTrigger>Is it animated?</AccordionTrigger>
32
+ <AccordionContent>
33
+ Yes. It's animated by default, but you can disable it if you
34
+ prefer.
35
+ </AccordionContent>
36
+ </AccordionItem>
37
+ </Accordion>
38
+ );
39
+
40
+ expect(getByText("Is it accessible?")).toBeInTheDocument();
41
+ expect(getByText("Is it styled?")).toBeInTheDocument();
42
+ expect(getByText("Is it animated?")).toBeInTheDocument();
43
+ });
44
+
45
+ it("should show content when trigger is clicked", async () => {
46
+ const user = userEvent.setup();
47
+ const { getByText } = render(
48
+ <Accordion type="single" collapsible>
49
+ <AccordionItem value="item-1">
50
+ <AccordionTrigger>Is it accessible?</AccordionTrigger>
51
+ <AccordionContent>
52
+ Yes. It adheres to the WAI-ARIA design pattern and is fully
53
+ accessible.
54
+ </AccordionContent>
55
+ </AccordionItem>
56
+ </Accordion>
57
+ );
58
+
59
+ const trigger = getByText("Is it accessible?");
60
+ await user.click(trigger);
61
+
62
+ expect(
63
+ getByText(
64
+ "Yes. It adheres to the WAI-ARIA design pattern and is fully accessible."
65
+ )
66
+ ).toBeInTheDocument();
67
+ });
68
+
69
+ it("should close content when trigger is clicked again (collapsible)", async () => {
70
+ const user = userEvent.setup();
71
+ const { getByText, queryByText } = render(
72
+ <Accordion type="single" collapsible>
73
+ <AccordionItem value="item-1">
74
+ <AccordionTrigger>Is it accessible?</AccordionTrigger>
75
+ <AccordionContent>
76
+ Yes. It adheres to the WAI-ARIA design pattern and is fully
77
+ accessible.
78
+ </AccordionContent>
79
+ </AccordionItem>
80
+ </Accordion>
81
+ );
82
+
83
+ const trigger = getByText("Is it accessible?");
84
+
85
+ // Open
86
+ await user.click(trigger);
87
+ expect(
88
+ getByText(
89
+ "Yes. It adheres to the WAI-ARIA design pattern and is fully accessible."
90
+ )
91
+ ).toBeInTheDocument();
92
+
93
+ // Close
94
+ await user.click(trigger);
95
+ expect(
96
+ queryByText(
97
+ "Yes. It adheres to the WAI-ARIA design pattern and is fully accessible."
98
+ )
99
+ ).not.toBeInTheDocument();
100
+ });
101
+
102
+ it("should only allow one item open at a time in single mode", async () => {
103
+ const user = userEvent.setup();
104
+ const { getByText, queryByText } = render(
105
+ <Accordion type="single" collapsible>
106
+ <AccordionItem value="item-1">
107
+ <AccordionTrigger>Item 1</AccordionTrigger>
108
+ <AccordionContent>Content 1</AccordionContent>
109
+ </AccordionItem>
110
+ <AccordionItem value="item-2">
111
+ <AccordionTrigger>Item 2</AccordionTrigger>
112
+ <AccordionContent>Content 2</AccordionContent>
113
+ </AccordionItem>
114
+ </Accordion>
115
+ );
116
+
117
+ const trigger1 = getByText("Item 1");
118
+ const trigger2 = getByText("Item 2");
119
+
120
+ // Open first item
121
+ await user.click(trigger1);
122
+ expect(getByText("Content 1")).toBeInTheDocument();
123
+ expect(queryByText("Content 2")).not.toBeInTheDocument();
124
+
125
+ // Open second item (should close first)
126
+ await user.click(trigger2);
127
+ expect(queryByText("Content 1")).not.toBeInTheDocument();
128
+ expect(getByText("Content 2")).toBeInTheDocument();
129
+ });
130
+ });
131
+
132
+ describe("Multiple type", () => {
133
+ it("should allow multiple items to be open simultaneously", async () => {
134
+ const user = userEvent.setup();
135
+ const { getByText } = render(
136
+ <Accordion type="multiple">
137
+ <AccordionItem value="item-1">
138
+ <AccordionTrigger>Can I open multiple items?</AccordionTrigger>
139
+ <AccordionContent>
140
+ Yes! With the "multiple" type, you can have multiple items open at
141
+ the same time.
142
+ </AccordionContent>
143
+ </AccordionItem>
144
+ <AccordionItem value="item-2">
145
+ <AccordionTrigger>How does it work?</AccordionTrigger>
146
+ <AccordionContent>
147
+ Each accordion item can be independently opened or closed,
148
+ allowing users to view multiple sections simultaneously.
149
+ </AccordionContent>
150
+ </AccordionItem>
151
+ <AccordionItem value="item-3">
152
+ <AccordionTrigger>What are the use cases?</AccordionTrigger>
153
+ <AccordionContent>
154
+ This is useful for FAQ sections, settings panels, or any content
155
+ where users might want to compare information across different
156
+ sections.
157
+ </AccordionContent>
158
+ </AccordionItem>
159
+ </Accordion>
160
+ );
161
+
162
+ const trigger1 = getByText("Can I open multiple items?");
163
+ const trigger2 = getByText("How does it work?");
164
+
165
+ // Open first item
166
+ await user.click(trigger1);
167
+ expect(
168
+ getByText(
169
+ 'Yes! With the "multiple" type, you can have multiple items open at the same time.'
170
+ )
171
+ ).toBeInTheDocument();
172
+
173
+ // Open second item (first should still be open)
174
+ await user.click(trigger2);
175
+ expect(
176
+ getByText(
177
+ 'Yes! With the "multiple" type, you can have multiple items open at the same time.'
178
+ )
179
+ ).toBeInTheDocument();
180
+ expect(
181
+ getByText(
182
+ "Each accordion item can be independently opened or closed, allowing users to view multiple sections simultaneously."
183
+ )
184
+ ).toBeInTheDocument();
185
+ });
186
+ });
187
+
188
+ describe("SingleItem", () => {
189
+ it("should render single accordion item", () => {
190
+ const { getByText } = render(
191
+ <Accordion type="single" collapsible className="w-full">
192
+ <AccordionItem value="item-1">
193
+ <AccordionTrigger>What is this component?</AccordionTrigger>
194
+ <AccordionContent>
195
+ This is an accordion component built with Radix UI. It provides a
196
+ collapsible content area that can be toggled open and closed.
197
+ </AccordionContent>
198
+ </AccordionItem>
199
+ </Accordion>
200
+ );
201
+
202
+ expect(getByText("What is this component?")).toBeInTheDocument();
203
+ });
204
+ });
205
+
206
+ describe("LongContent", () => {
207
+ it("should render accordion with long content", async () => {
208
+ const user = userEvent.setup();
209
+ const { getByText } = render(
210
+ <Accordion type="single" collapsible className="w-full">
211
+ <AccordionItem value="item-1">
212
+ <AccordionTrigger>Item with long content</AccordionTrigger>
213
+ <AccordionContent>
214
+ <div className="space-y-2">
215
+ <p>
216
+ This accordion item contains longer content to demonstrate how
217
+ the component handles text that spans multiple lines or
218
+ paragraphs.
219
+ </p>
220
+ <p>
221
+ The content area will expand to accommodate the full text, and
222
+ the animation will smoothly transition between the collapsed
223
+ and expanded states.
224
+ </p>
225
+ <ul className="list-disc list-inside space-y-1 ml-4">
226
+ <li>First bullet point</li>
227
+ <li>Second bullet point</li>
228
+ <li>Third bullet point</li>
229
+ </ul>
230
+ </div>
231
+ </AccordionContent>
232
+ </AccordionItem>
233
+ </Accordion>
234
+ );
235
+
236
+ const trigger = getByText("Item with long content");
237
+ await user.click(trigger);
238
+
239
+ expect(
240
+ getByText(
241
+ "This accordion item contains longer content to demonstrate how the component handles text that spans multiple lines or paragraphs."
242
+ )
243
+ ).toBeInTheDocument();
244
+ expect(getByText("First bullet point")).toBeInTheDocument();
245
+ expect(getByText("Second bullet point")).toBeInTheDocument();
246
+ expect(getByText("Third bullet point")).toBeInTheDocument();
247
+ });
248
+ });
249
+
250
+ describe("WithCustomStyling", () => {
251
+ it("should apply custom className to accordion", () => {
252
+ const { container } = render(
253
+ <Accordion type="single" collapsible className="w-full max-w-2xl">
254
+ <AccordionItem value="item-1" className="border-2 border-primary/20">
255
+ <AccordionTrigger className="text-lg font-semibold">
256
+ Custom styled item
257
+ </AccordionTrigger>
258
+ <AccordionContent className="text-base">
259
+ This accordion item has custom styling applied to demonstrate how
260
+ you can customize the appearance of individual items.
261
+ </AccordionContent>
262
+ </AccordionItem>
263
+ </Accordion>
264
+ );
265
+
266
+ const accordion = container.querySelector('[data-slot="accordion"]');
267
+ expect(accordion).toHaveClass("w-full", "max-w-2xl");
268
+ });
269
+
270
+ it("should apply custom className to accordion item", () => {
271
+ const { container } = render(
272
+ <Accordion type="single" collapsible>
273
+ <AccordionItem value="item-1" className="border-2 border-primary/20">
274
+ <AccordionTrigger>Custom styled item</AccordionTrigger>
275
+ <AccordionContent>Content</AccordionContent>
276
+ </AccordionItem>
277
+ </Accordion>
278
+ );
279
+
280
+ const item = container.querySelector('[data-slot="accordion-item"]');
281
+ expect(item).toHaveClass("border-2", "border-primary/20");
282
+ });
283
+
284
+ it("should apply custom className to accordion trigger", () => {
285
+ const { container } = render(
286
+ <Accordion type="single" collapsible>
287
+ <AccordionItem value="item-1">
288
+ <AccordionTrigger className="text-lg font-semibold">
289
+ Custom styled item
290
+ </AccordionTrigger>
291
+ <AccordionContent>Content</AccordionContent>
292
+ </AccordionItem>
293
+ </Accordion>
294
+ );
295
+
296
+ const trigger = container.querySelector(
297
+ '[data-slot="accordion-trigger"]'
298
+ );
299
+ expect(trigger).toHaveClass("text-lg", "font-semibold");
300
+ });
301
+ });
302
+
303
+ describe("Accessibility", () => {
304
+ it("should have proper ARIA attributes", () => {
305
+ const { getByText } = render(
306
+ <Accordion type="single" collapsible>
307
+ <AccordionItem value="item-1">
308
+ <AccordionTrigger>Is it accessible?</AccordionTrigger>
309
+ <AccordionContent>
310
+ Yes. It adheres to the WAI-ARIA design pattern and is fully
311
+ accessible.
312
+ </AccordionContent>
313
+ </AccordionItem>
314
+ </Accordion>
315
+ );
316
+
317
+ const trigger = getByText("Is it accessible?");
318
+ expect(trigger).toHaveAttribute("aria-expanded");
319
+ });
320
+
321
+ it("should update aria-expanded when opened", async () => {
322
+ const user = userEvent.setup();
323
+ const { getByText } = render(
324
+ <Accordion type="single" collapsible>
325
+ <AccordionItem value="item-1">
326
+ <AccordionTrigger>Is it accessible?</AccordionTrigger>
327
+ <AccordionContent>
328
+ Yes. It adheres to the WAI-ARIA design pattern and is fully
329
+ accessible.
330
+ </AccordionContent>
331
+ </AccordionItem>
332
+ </Accordion>
333
+ );
334
+
335
+ const trigger = getByText("Is it accessible?");
336
+
337
+ // Initially closed
338
+ expect(trigger).toHaveAttribute("aria-expanded", "false");
339
+
340
+ // Open
341
+ await user.click(trigger);
342
+ expect(trigger).toHaveAttribute("aria-expanded", "true");
343
+ });
344
+ });
345
+
346
+ describe("Controlled mode", () => {
347
+ it("should work in controlled mode with value prop", async () => {
348
+ const user = userEvent.setup();
349
+ const onValueChange = vi.fn();
350
+
351
+ const { getByText } = render(
352
+ <Accordion type="single" value="item-1" onValueChange={onValueChange}>
353
+ <AccordionItem value="item-1">
354
+ <AccordionTrigger>Item 1</AccordionTrigger>
355
+ <AccordionContent>Content 1</AccordionContent>
356
+ </AccordionItem>
357
+ <AccordionItem value="item-2">
358
+ <AccordionTrigger>Item 2</AccordionTrigger>
359
+ <AccordionContent>Content 2</AccordionContent>
360
+ </AccordionItem>
361
+ </Accordion>
362
+ );
363
+
364
+ const trigger2 = getByText("Item 2");
365
+ await user.click(trigger2);
366
+
367
+ expect(onValueChange).toHaveBeenCalledWith("item-2");
368
+ });
369
+ });
370
+
371
+ describe("Default value", () => {
372
+ it("should open item with defaultValue", () => {
373
+ const { getByText, queryByText } = render(
374
+ <Accordion type="single" defaultValue="item-2">
375
+ <AccordionItem value="item-1">
376
+ <AccordionTrigger>Item 1</AccordionTrigger>
377
+ <AccordionContent>Content 1</AccordionContent>
378
+ </AccordionItem>
379
+ <AccordionItem value="item-2">
380
+ <AccordionTrigger>Item 2</AccordionTrigger>
381
+ <AccordionContent>Content 2</AccordionContent>
382
+ </AccordionItem>
383
+ </Accordion>
384
+ );
385
+
386
+ expect(getByText("Content 2")).toBeInTheDocument();
387
+ expect(queryByText("Content 1")).not.toBeInTheDocument();
388
+ });
389
+ });
390
+ });