@nationaldesignstudio/react 0.0.19 → 0.2.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.
- package/dist/component-registry.md +46 -19
- package/dist/components/atoms/accordion/accordion.d.ts +9 -9
- package/dist/components/atoms/background/background.d.ts +23 -0
- package/dist/components/atoms/button/icon-button.d.ts +44 -16
- package/dist/components/sections/banner/banner.d.ts +9 -9
- package/dist/components/sections/faq-section/faq-section.d.ts +1 -1
- package/dist/components/sections/hero/hero.d.ts +115 -18
- package/dist/components/sections/tout/tout.d.ts +8 -8
- package/dist/components/sections/two-column-section/two-column-section.d.ts +7 -21
- package/dist/index.js +2177 -2027
- package/dist/index.js.map +1 -1
- package/dist/tokens.css +16 -16
- package/package.json +1 -1
- package/src/components/atoms/accordion/accordion.test.tsx +231 -0
- package/src/components/atoms/accordion/accordion.tsx +21 -19
- package/src/components/atoms/background/background.test.tsx +213 -0
- package/src/components/atoms/background/background.tsx +104 -27
- package/src/components/atoms/button/button.tsx +11 -1
- package/src/components/atoms/button/icon-button.test.tsx +254 -0
- package/src/components/atoms/button/icon-button.tsx +108 -16
- package/src/components/atoms/pager-control/pager-control.tsx +32 -3
- package/src/components/dev-tools/dev-toolbar/dev-toolbar.tsx +2 -0
- package/src/components/organisms/card/card.test.tsx +4 -2
- package/src/components/organisms/navbar/navbar.tsx +2 -0
- package/src/components/sections/banner/banner.stories.tsx +5 -1
- package/src/components/sections/banner/banner.tsx +10 -10
- package/src/components/sections/faq-section/faq-section.stories.tsx +7 -7
- package/src/components/sections/faq-section/faq-section.tsx +3 -3
- package/src/components/sections/hero/hero.tsx +33 -51
- package/src/components/sections/tout/tout.stories.tsx +31 -7
- package/src/components/sections/tout/tout.tsx +6 -8
- package/src/components/sections/two-column-section/two-column-section.stories.tsx +11 -11
- package/src/components/sections/two-column-section/two-column-section.tsx +16 -10
- package/src/stories/ThemeProvider.stories.tsx +9 -3
package/dist/tokens.css
CHANGED
|
@@ -1448,26 +1448,26 @@
|
|
|
1448
1448
|
|
|
1449
1449
|
:root {
|
|
1450
1450
|
/* Semantic Color Tokens */
|
|
1451
|
-
--color-accent-brand-soft: color(srgb 0.
|
|
1452
|
-
--color-accent-brand: color(srgb 0.
|
|
1453
|
-
--color-bg-muted: color(srgb
|
|
1454
|
-
--color-bg-page: color(srgb 0.
|
|
1455
|
-
--color-bg-section: color(srgb 0.9608 0.
|
|
1456
|
-
--color-border-divider: color(srgb 0
|
|
1457
|
-
--color-border-focus: color(srgb 0.
|
|
1458
|
-
--color-border-strong: color(srgb 0
|
|
1459
|
-
--color-border-subtle: color(srgb 0
|
|
1460
|
-
--color-button-primary-bg-hover: color(srgb 0.
|
|
1461
|
-
--color-button-primary-bg: color(srgb 0.
|
|
1451
|
+
--color-accent-brand-soft: color(srgb 0.9686 0.8078 0.749);
|
|
1452
|
+
--color-accent-brand: color(srgb 0.9216 0.3098 0.1529);
|
|
1453
|
+
--color-bg-muted: color(srgb 1 1 1);
|
|
1454
|
+
--color-bg-page: color(srgb 0.9804 0.9804 0.9804);
|
|
1455
|
+
--color-bg-section: color(srgb 0.9608 0.9608 0.9608);
|
|
1456
|
+
--color-border-divider: color(srgb 0 0 0 / 0.1);
|
|
1457
|
+
--color-border-focus: color(srgb 0.9216 0.3098 0.1529);
|
|
1458
|
+
--color-border-strong: color(srgb 0 0 0 / 0.2);
|
|
1459
|
+
--color-border-subtle: color(srgb 0 0 0 / 0.1);
|
|
1460
|
+
--color-button-primary-bg-hover: color(srgb 0.4588 0.4588 0.4588);
|
|
1461
|
+
--color-button-primary-bg: color(srgb 0.0784 0.0784 0.0784);
|
|
1462
1462
|
--color-button-secondary-bg-hover: color(srgb 1 1 1);
|
|
1463
1463
|
--color-button-secondary-bg: color(srgb 1 1 1);
|
|
1464
|
-
--color-card-background: color(srgb
|
|
1465
|
-
--color-text-inverted: color(srgb 0.
|
|
1464
|
+
--color-card-background: color(srgb 1 1 1);
|
|
1465
|
+
--color-text-inverted: color(srgb 0.9608 0.9608 0.9608);
|
|
1466
1466
|
--color-text-link-hover: color(srgb 0.3804 0.3804 0.3804);
|
|
1467
1467
|
--color-text-link: color(srgb 0.0784 0.0784 0.0784);
|
|
1468
|
-
--color-text-muted: color(srgb 0.
|
|
1469
|
-
--color-text-primary: color(srgb 0.
|
|
1470
|
-
--color-text-secondary: color(srgb 0.
|
|
1468
|
+
--color-text-muted: color(srgb 0.4588 0.4588 0.4588);
|
|
1469
|
+
--color-text-primary: color(srgb 0.0784 0.0784 0.0784);
|
|
1470
|
+
--color-text-secondary: color(srgb 0.2588 0.2588 0.2588);
|
|
1471
1471
|
--radius-surface-button: var(--radius-6);
|
|
1472
1472
|
--radius-surface-card: var(--radius-4);
|
|
1473
1473
|
--surface-button-stroke: 1px;
|
package/package.json
CHANGED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { page, userEvent } from "vitest/browser";
|
|
3
|
+
import { render } from "vitest-browser-react";
|
|
4
|
+
import { Accordion, AccordionItem } from "./accordion";
|
|
5
|
+
|
|
6
|
+
describe("Accordion", () => {
|
|
7
|
+
describe("Rendering", () => {
|
|
8
|
+
test("renders accordion with items", async () => {
|
|
9
|
+
render(
|
|
10
|
+
<Accordion>
|
|
11
|
+
<AccordionItem id="item-1" title="Question 1">
|
|
12
|
+
Answer 1
|
|
13
|
+
</AccordionItem>
|
|
14
|
+
<AccordionItem id="item-2" title="Question 2">
|
|
15
|
+
Answer 2
|
|
16
|
+
</AccordionItem>
|
|
17
|
+
</Accordion>,
|
|
18
|
+
);
|
|
19
|
+
await expect
|
|
20
|
+
.element(page.getByRole("button", { name: "Question 1" }))
|
|
21
|
+
.toBeInTheDocument();
|
|
22
|
+
await expect
|
|
23
|
+
.element(page.getByRole("button", { name: "Question 2" }))
|
|
24
|
+
.toBeInTheDocument();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("renders with default expanded item", async () => {
|
|
28
|
+
render(
|
|
29
|
+
<Accordion defaultExpanded="item-1">
|
|
30
|
+
<AccordionItem id="item-1" title="Question 1">
|
|
31
|
+
Answer 1
|
|
32
|
+
</AccordionItem>
|
|
33
|
+
<AccordionItem id="item-2" title="Question 2">
|
|
34
|
+
Answer 2
|
|
35
|
+
</AccordionItem>
|
|
36
|
+
</Accordion>,
|
|
37
|
+
);
|
|
38
|
+
// The first item should be expanded (has data-open attribute)
|
|
39
|
+
await expect
|
|
40
|
+
.element(page.getByRole("button", { name: "Question 1" }))
|
|
41
|
+
.toHaveAttribute("aria-expanded", "true");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("renders with multiple default expanded items", async () => {
|
|
45
|
+
render(
|
|
46
|
+
<Accordion allowMultiple defaultExpanded={["item-1", "item-2"]}>
|
|
47
|
+
<AccordionItem id="item-1" title="Question 1">
|
|
48
|
+
Answer 1
|
|
49
|
+
</AccordionItem>
|
|
50
|
+
<AccordionItem id="item-2" title="Question 2">
|
|
51
|
+
Answer 2
|
|
52
|
+
</AccordionItem>
|
|
53
|
+
</Accordion>,
|
|
54
|
+
);
|
|
55
|
+
await expect
|
|
56
|
+
.element(page.getByRole("button", { name: "Question 1" }))
|
|
57
|
+
.toHaveAttribute("aria-expanded", "true");
|
|
58
|
+
await expect
|
|
59
|
+
.element(page.getByRole("button", { name: "Question 2" }))
|
|
60
|
+
.toHaveAttribute("aria-expanded", "true");
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("Keyboard Navigation", () => {
|
|
65
|
+
test("items are focusable via Tab", async () => {
|
|
66
|
+
render(
|
|
67
|
+
<Accordion>
|
|
68
|
+
<AccordionItem id="item-1" title="Question 1">
|
|
69
|
+
Answer 1
|
|
70
|
+
</AccordionItem>
|
|
71
|
+
<AccordionItem id="item-2" title="Question 2">
|
|
72
|
+
Answer 2
|
|
73
|
+
</AccordionItem>
|
|
74
|
+
</Accordion>,
|
|
75
|
+
);
|
|
76
|
+
await userEvent.keyboard("{Tab}");
|
|
77
|
+
await expect
|
|
78
|
+
.element(page.getByRole("button", { name: "Question 1" }))
|
|
79
|
+
.toHaveFocus();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("Enter key expands/collapses item", async () => {
|
|
83
|
+
render(
|
|
84
|
+
<Accordion>
|
|
85
|
+
<AccordionItem id="item-1" title="Question 1">
|
|
86
|
+
Answer 1
|
|
87
|
+
</AccordionItem>
|
|
88
|
+
</Accordion>,
|
|
89
|
+
);
|
|
90
|
+
const trigger = page.getByRole("button", { name: "Question 1" });
|
|
91
|
+
trigger.element().focus();
|
|
92
|
+
await userEvent.keyboard("{Enter}");
|
|
93
|
+
await expect.element(trigger).toHaveAttribute("aria-expanded", "true");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("Space key expands/collapses item", async () => {
|
|
97
|
+
render(
|
|
98
|
+
<Accordion>
|
|
99
|
+
<AccordionItem id="item-1" title="Question 1">
|
|
100
|
+
Answer 1
|
|
101
|
+
</AccordionItem>
|
|
102
|
+
</Accordion>,
|
|
103
|
+
);
|
|
104
|
+
const trigger = page.getByRole("button", { name: "Question 1" });
|
|
105
|
+
trigger.element().focus();
|
|
106
|
+
await userEvent.keyboard(" ");
|
|
107
|
+
await expect.element(trigger).toHaveAttribute("aria-expanded", "true");
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("Click Interactions", () => {
|
|
112
|
+
test("clicking trigger expands item", async () => {
|
|
113
|
+
render(
|
|
114
|
+
<Accordion>
|
|
115
|
+
<AccordionItem id="item-1" title="Question 1">
|
|
116
|
+
Answer 1
|
|
117
|
+
</AccordionItem>
|
|
118
|
+
</Accordion>,
|
|
119
|
+
);
|
|
120
|
+
const trigger = page.getByRole("button", { name: "Question 1" });
|
|
121
|
+
await trigger.click();
|
|
122
|
+
await expect.element(trigger).toHaveAttribute("aria-expanded", "true");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("clicking expanded trigger collapses item", async () => {
|
|
126
|
+
render(
|
|
127
|
+
<Accordion defaultExpanded="item-1">
|
|
128
|
+
<AccordionItem id="item-1" title="Question 1">
|
|
129
|
+
Answer 1
|
|
130
|
+
</AccordionItem>
|
|
131
|
+
</Accordion>,
|
|
132
|
+
);
|
|
133
|
+
const trigger = page.getByRole("button", { name: "Question 1" });
|
|
134
|
+
await expect.element(trigger).toHaveAttribute("aria-expanded", "true");
|
|
135
|
+
await trigger.click();
|
|
136
|
+
await expect.element(trigger).toHaveAttribute("aria-expanded", "false");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("single mode collapses other items when opening new one", async () => {
|
|
140
|
+
render(
|
|
141
|
+
<Accordion defaultExpanded="item-1">
|
|
142
|
+
<AccordionItem id="item-1" title="Question 1">
|
|
143
|
+
Answer 1
|
|
144
|
+
</AccordionItem>
|
|
145
|
+
<AccordionItem id="item-2" title="Question 2">
|
|
146
|
+
Answer 2
|
|
147
|
+
</AccordionItem>
|
|
148
|
+
</Accordion>,
|
|
149
|
+
);
|
|
150
|
+
const trigger1 = page.getByRole("button", { name: "Question 1" });
|
|
151
|
+
const trigger2 = page.getByRole("button", { name: "Question 2" });
|
|
152
|
+
|
|
153
|
+
await expect.element(trigger1).toHaveAttribute("aria-expanded", "true");
|
|
154
|
+
await trigger2.click();
|
|
155
|
+
await expect.element(trigger2).toHaveAttribute("aria-expanded", "true");
|
|
156
|
+
await expect.element(trigger1).toHaveAttribute("aria-expanded", "false");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("multiple mode allows multiple items open", async () => {
|
|
160
|
+
render(
|
|
161
|
+
<Accordion allowMultiple defaultExpanded="item-1">
|
|
162
|
+
<AccordionItem id="item-1" title="Question 1">
|
|
163
|
+
Answer 1
|
|
164
|
+
</AccordionItem>
|
|
165
|
+
<AccordionItem id="item-2" title="Question 2">
|
|
166
|
+
Answer 2
|
|
167
|
+
</AccordionItem>
|
|
168
|
+
</Accordion>,
|
|
169
|
+
);
|
|
170
|
+
const trigger1 = page.getByRole("button", { name: "Question 1" });
|
|
171
|
+
const trigger2 = page.getByRole("button", { name: "Question 2" });
|
|
172
|
+
|
|
173
|
+
await expect.element(trigger1).toHaveAttribute("aria-expanded", "true");
|
|
174
|
+
await trigger2.click();
|
|
175
|
+
await expect.element(trigger1).toHaveAttribute("aria-expanded", "true");
|
|
176
|
+
await expect.element(trigger2).toHaveAttribute("aria-expanded", "true");
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe("Color Schemes", () => {
|
|
181
|
+
test("applies light colorScheme by default", async () => {
|
|
182
|
+
render(
|
|
183
|
+
<Accordion>
|
|
184
|
+
<AccordionItem id="item-1" title="Question 1">
|
|
185
|
+
Answer 1
|
|
186
|
+
</AccordionItem>
|
|
187
|
+
</Accordion>,
|
|
188
|
+
);
|
|
189
|
+
const trigger = page.getByRole("button", { name: "Question 1" });
|
|
190
|
+
await expect.element(trigger).toHaveClass(/text-gray-800/);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("applies dark colorScheme classes", async () => {
|
|
194
|
+
render(
|
|
195
|
+
<Accordion colorScheme="dark">
|
|
196
|
+
<AccordionItem id="item-1" title="Question 1">
|
|
197
|
+
Answer 1
|
|
198
|
+
</AccordionItem>
|
|
199
|
+
</Accordion>,
|
|
200
|
+
);
|
|
201
|
+
const trigger = page.getByRole("button", { name: "Question 1" });
|
|
202
|
+
await expect.element(trigger).toHaveClass(/text-gray-100/);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe("Content", () => {
|
|
207
|
+
test("displays title text in trigger", async () => {
|
|
208
|
+
render(
|
|
209
|
+
<Accordion>
|
|
210
|
+
<AccordionItem id="item-1" title="Custom Title">
|
|
211
|
+
Content here
|
|
212
|
+
</AccordionItem>
|
|
213
|
+
</Accordion>,
|
|
214
|
+
);
|
|
215
|
+
await expect.element(page.getByText("Custom Title")).toBeInTheDocument();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("displays content when expanded", async () => {
|
|
219
|
+
render(
|
|
220
|
+
<Accordion defaultExpanded="item-1">
|
|
221
|
+
<AccordionItem id="item-1" title="Question 1">
|
|
222
|
+
Answer content here
|
|
223
|
+
</AccordionItem>
|
|
224
|
+
</Accordion>,
|
|
225
|
+
);
|
|
226
|
+
await expect
|
|
227
|
+
.element(page.getByText("Answer content here"))
|
|
228
|
+
.toBeInTheDocument();
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
});
|
|
@@ -12,26 +12,26 @@ import { cn } from "@/lib/utils";
|
|
|
12
12
|
const accordionVariants = tv({
|
|
13
13
|
base: "flex flex-col",
|
|
14
14
|
variants: {
|
|
15
|
-
|
|
15
|
+
colorScheme: {
|
|
16
16
|
dark: "",
|
|
17
17
|
light: "",
|
|
18
18
|
},
|
|
19
19
|
},
|
|
20
20
|
defaultVariants: {
|
|
21
|
-
|
|
21
|
+
colorScheme: "light",
|
|
22
22
|
},
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
const accordionItemVariants = tv({
|
|
26
26
|
base: "border-b overflow-hidden",
|
|
27
27
|
variants: {
|
|
28
|
-
|
|
28
|
+
colorScheme: {
|
|
29
29
|
dark: "border-gray-300",
|
|
30
30
|
light: "border-gray-500",
|
|
31
31
|
},
|
|
32
32
|
},
|
|
33
33
|
defaultVariants: {
|
|
34
|
-
|
|
34
|
+
colorScheme: "light",
|
|
35
35
|
},
|
|
36
36
|
});
|
|
37
37
|
|
|
@@ -42,13 +42,13 @@ const accordionTriggerVariants = tv({
|
|
|
42
42
|
"typography-body-large transition-colors cursor-pointer",
|
|
43
43
|
],
|
|
44
44
|
variants: {
|
|
45
|
-
|
|
45
|
+
colorScheme: {
|
|
46
46
|
dark: "text-gray-100 data-[open]:text-white",
|
|
47
47
|
light: "text-gray-800 data-[open]:text-gray-900",
|
|
48
48
|
},
|
|
49
49
|
},
|
|
50
50
|
defaultVariants: {
|
|
51
|
-
|
|
51
|
+
colorScheme: "light",
|
|
52
52
|
},
|
|
53
53
|
});
|
|
54
54
|
|
|
@@ -56,13 +56,13 @@ const accordionPanelVariants = tv({
|
|
|
56
56
|
// Uses primitive spacing tokens
|
|
57
57
|
base: "typography-body-large pb-spacing-24",
|
|
58
58
|
variants: {
|
|
59
|
-
|
|
59
|
+
colorScheme: {
|
|
60
60
|
dark: "text-gray-100",
|
|
61
61
|
light: "text-gray-800",
|
|
62
62
|
},
|
|
63
63
|
},
|
|
64
64
|
defaultVariants: {
|
|
65
|
-
|
|
65
|
+
colorScheme: "light",
|
|
66
66
|
},
|
|
67
67
|
});
|
|
68
68
|
|
|
@@ -71,9 +71,9 @@ const accordionPanelVariants = tv({
|
|
|
71
71
|
// =============================================================================
|
|
72
72
|
|
|
73
73
|
const AccordionContext = React.createContext<{
|
|
74
|
-
|
|
74
|
+
colorScheme: "dark" | "light";
|
|
75
75
|
}>({
|
|
76
|
-
|
|
76
|
+
colorScheme: "light",
|
|
77
77
|
});
|
|
78
78
|
|
|
79
79
|
// =============================================================================
|
|
@@ -100,8 +100,8 @@ export interface AccordionProps
|
|
|
100
100
|
* Built on Base UI's Accordion primitive.
|
|
101
101
|
*
|
|
102
102
|
* Variants:
|
|
103
|
-
* -
|
|
104
|
-
* -
|
|
103
|
+
* - light: Light theme styling (default)
|
|
104
|
+
* - dark: Dark theme styling
|
|
105
105
|
*
|
|
106
106
|
* @example
|
|
107
107
|
* ```tsx
|
|
@@ -121,7 +121,7 @@ const Accordion = React.forwardRef<HTMLDivElement, AccordionProps>(
|
|
|
121
121
|
className,
|
|
122
122
|
allowMultiple = false,
|
|
123
123
|
defaultExpanded,
|
|
124
|
-
|
|
124
|
+
colorScheme = "light",
|
|
125
125
|
children,
|
|
126
126
|
},
|
|
127
127
|
ref,
|
|
@@ -134,10 +134,10 @@ const Accordion = React.forwardRef<HTMLDivElement, AccordionProps>(
|
|
|
134
134
|
}, [defaultExpanded]);
|
|
135
135
|
|
|
136
136
|
return (
|
|
137
|
-
<AccordionContext.Provider value={{
|
|
137
|
+
<AccordionContext.Provider value={{ colorScheme }}>
|
|
138
138
|
<BaseAccordion.Root
|
|
139
139
|
ref={ref}
|
|
140
|
-
className={accordionVariants({
|
|
140
|
+
className={accordionVariants({ colorScheme, class: className })}
|
|
141
141
|
defaultValue={defaultValue}
|
|
142
142
|
multiple={allowMultiple}
|
|
143
143
|
>
|
|
@@ -175,18 +175,18 @@ export interface AccordionItemProps
|
|
|
175
175
|
*/
|
|
176
176
|
const AccordionItem = React.forwardRef<HTMLDivElement, AccordionItemProps>(
|
|
177
177
|
({ className, id, title, children }, ref) => {
|
|
178
|
-
const {
|
|
178
|
+
const { colorScheme } = React.useContext(AccordionContext);
|
|
179
179
|
|
|
180
180
|
return (
|
|
181
181
|
<BaseAccordion.Item
|
|
182
182
|
ref={ref}
|
|
183
183
|
value={id}
|
|
184
|
-
className={accordionItemVariants({
|
|
184
|
+
className={accordionItemVariants({ colorScheme, class: className })}
|
|
185
185
|
>
|
|
186
186
|
{/* Header - always visible */}
|
|
187
187
|
<BaseAccordion.Header>
|
|
188
188
|
<BaseAccordion.Trigger
|
|
189
|
-
className={accordionTriggerVariants({
|
|
189
|
+
className={accordionTriggerVariants({ colorScheme })}
|
|
190
190
|
>
|
|
191
191
|
<span>{title}</span>
|
|
192
192
|
<span
|
|
@@ -208,7 +208,9 @@ const AccordionItem = React.forwardRef<HTMLDivElement, AccordionItemProps>(
|
|
|
208
208
|
"[&[data-starting-style]]:h-0 [&[data-ending-style]]:h-0",
|
|
209
209
|
)}
|
|
210
210
|
>
|
|
211
|
-
<div className={accordionPanelVariants({
|
|
211
|
+
<div className={accordionPanelVariants({ colorScheme })}>
|
|
212
|
+
{children}
|
|
213
|
+
</div>
|
|
212
214
|
</BaseAccordion.Panel>
|
|
213
215
|
</BaseAccordion.Item>
|
|
214
216
|
);
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { page } from "vitest/browser";
|
|
3
|
+
import { render } from "vitest-browser-react";
|
|
4
|
+
import {
|
|
5
|
+
Background,
|
|
6
|
+
BackgroundGradient,
|
|
7
|
+
BackgroundImage,
|
|
8
|
+
BackgroundOverlay,
|
|
9
|
+
BackgroundVideo,
|
|
10
|
+
} from "./background";
|
|
11
|
+
|
|
12
|
+
describe("Background", () => {
|
|
13
|
+
describe("Background Container", () => {
|
|
14
|
+
test("renders as a div with absolute positioning", async () => {
|
|
15
|
+
render(<Background data-testid="bg-container">Content</Background>);
|
|
16
|
+
const container = page.getByTestId("bg-container");
|
|
17
|
+
await expect.element(container).toBeInTheDocument();
|
|
18
|
+
await expect.element(container).toHaveClass(/absolute/);
|
|
19
|
+
await expect.element(container).toHaveClass(/inset-0/);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("has aria-hidden attribute", async () => {
|
|
23
|
+
render(<Background data-testid="bg-container" />);
|
|
24
|
+
await expect
|
|
25
|
+
.element(page.getByTestId("bg-container"))
|
|
26
|
+
.toHaveAttribute("aria-hidden", "true");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("accepts custom className", async () => {
|
|
30
|
+
render(<Background data-testid="bg" className="custom-class" />);
|
|
31
|
+
await expect.element(page.getByTestId("bg")).toHaveClass(/custom-class/);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("BackgroundImage", () => {
|
|
36
|
+
test("renders an img element with src", async () => {
|
|
37
|
+
render(<BackgroundImage src="/test-image.jpg" data-testid="bg-img" />);
|
|
38
|
+
const img = page.getByTestId("bg-img");
|
|
39
|
+
await expect.element(img).toBeInTheDocument();
|
|
40
|
+
await expect.element(img).toHaveAttribute("src", "/test-image.jpg");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("has object-cover class for proper fitting", async () => {
|
|
44
|
+
render(<BackgroundImage src="/test.jpg" data-testid="bg-img" />);
|
|
45
|
+
await expect
|
|
46
|
+
.element(page.getByTestId("bg-img"))
|
|
47
|
+
.toHaveClass(/object-cover/);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("sets default alt to empty string for decorative images", async () => {
|
|
51
|
+
render(<BackgroundImage src="/test.jpg" data-testid="bg-img" />);
|
|
52
|
+
await expect
|
|
53
|
+
.element(page.getByTestId("bg-img"))
|
|
54
|
+
.toHaveAttribute("alt", "");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("accepts custom alt text", async () => {
|
|
58
|
+
render(
|
|
59
|
+
<BackgroundImage
|
|
60
|
+
src="/test.jpg"
|
|
61
|
+
alt="Custom alt text"
|
|
62
|
+
data-testid="bg-img"
|
|
63
|
+
/>,
|
|
64
|
+
);
|
|
65
|
+
await expect
|
|
66
|
+
.element(page.getByTestId("bg-img"))
|
|
67
|
+
.toHaveAttribute("alt", "Custom alt text");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("applies custom position via style", async () => {
|
|
71
|
+
render(
|
|
72
|
+
<BackgroundImage
|
|
73
|
+
src="/test.jpg"
|
|
74
|
+
position="top left"
|
|
75
|
+
data-testid="bg-img"
|
|
76
|
+
/>,
|
|
77
|
+
);
|
|
78
|
+
const img = page.getByTestId("bg-img");
|
|
79
|
+
// Check the style attribute contains the position
|
|
80
|
+
await expect.element(img).toHaveStyle({ objectPosition: "top left" });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("supports render prop for custom element", async () => {
|
|
84
|
+
render(
|
|
85
|
+
<BackgroundImage
|
|
86
|
+
src="/test.jpg"
|
|
87
|
+
// biome-ignore lint/a11y/useAltText: Test case for custom element
|
|
88
|
+
render={<img className="custom-img-class" />}
|
|
89
|
+
data-testid="bg-img"
|
|
90
|
+
/>,
|
|
91
|
+
);
|
|
92
|
+
await expect
|
|
93
|
+
.element(page.getByTestId("bg-img"))
|
|
94
|
+
.toHaveClass(/custom-img-class/);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("BackgroundVideo", () => {
|
|
99
|
+
test("renders a video element", async () => {
|
|
100
|
+
render(<BackgroundVideo src="/test-video.mp4" data-testid="bg-video" />);
|
|
101
|
+
const video = page.getByTestId("bg-video");
|
|
102
|
+
await expect.element(video).toBeInTheDocument();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("has autoplay and loop by default", async () => {
|
|
106
|
+
render(<BackgroundVideo src="/test.mp4" data-testid="bg-video" />);
|
|
107
|
+
const video = page.getByTestId("bg-video");
|
|
108
|
+
await expect.element(video).toHaveAttribute("autoplay");
|
|
109
|
+
await expect.element(video).toHaveAttribute("loop");
|
|
110
|
+
// Note: muted is a boolean prop in React that may not render as an HTML attribute
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("has object-cover class", async () => {
|
|
114
|
+
render(<BackgroundVideo src="/test.mp4" data-testid="bg-video" />);
|
|
115
|
+
await expect
|
|
116
|
+
.element(page.getByTestId("bg-video"))
|
|
117
|
+
.toHaveClass(/object-cover/);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("accepts poster prop", async () => {
|
|
121
|
+
render(
|
|
122
|
+
<BackgroundVideo
|
|
123
|
+
src="/test.mp4"
|
|
124
|
+
poster="/poster.jpg"
|
|
125
|
+
data-testid="bg-video"
|
|
126
|
+
/>,
|
|
127
|
+
);
|
|
128
|
+
await expect
|
|
129
|
+
.element(page.getByTestId("bg-video"))
|
|
130
|
+
.toHaveAttribute("poster", "/poster.jpg");
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("BackgroundOverlay", () => {
|
|
135
|
+
test("renders with default opacity", async () => {
|
|
136
|
+
render(<BackgroundOverlay data-testid="bg-overlay" />);
|
|
137
|
+
const overlay = page.getByTestId("bg-overlay");
|
|
138
|
+
await expect.element(overlay).toBeInTheDocument();
|
|
139
|
+
await expect.element(overlay).toHaveStyle({ opacity: "0.4" });
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("accepts custom opacity", async () => {
|
|
143
|
+
render(<BackgroundOverlay opacity={0.7} data-testid="bg-overlay" />);
|
|
144
|
+
await expect
|
|
145
|
+
.element(page.getByTestId("bg-overlay"))
|
|
146
|
+
.toHaveStyle({ opacity: "0.7" });
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("has aria-hidden attribute", async () => {
|
|
150
|
+
render(<BackgroundOverlay data-testid="bg-overlay" />);
|
|
151
|
+
await expect
|
|
152
|
+
.element(page.getByTestId("bg-overlay"))
|
|
153
|
+
.toHaveAttribute("aria-hidden", "true");
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe("BackgroundGradient", () => {
|
|
158
|
+
test("renders with default gradient direction", async () => {
|
|
159
|
+
render(<BackgroundGradient data-testid="bg-gradient" />);
|
|
160
|
+
const gradient = page.getByTestId("bg-gradient");
|
|
161
|
+
await expect.element(gradient).toBeInTheDocument();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("has aria-hidden attribute", async () => {
|
|
165
|
+
render(<BackgroundGradient data-testid="bg-gradient" />);
|
|
166
|
+
await expect
|
|
167
|
+
.element(page.getByTestId("bg-gradient"))
|
|
168
|
+
.toHaveAttribute("aria-hidden", "true");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("accepts custom gradient via gradient prop", async () => {
|
|
172
|
+
render(
|
|
173
|
+
<BackgroundGradient
|
|
174
|
+
gradient="linear-gradient(45deg, red, blue)"
|
|
175
|
+
data-testid="bg-gradient"
|
|
176
|
+
/>,
|
|
177
|
+
);
|
|
178
|
+
await expect
|
|
179
|
+
.element(page.getByTestId("bg-gradient"))
|
|
180
|
+
.toHaveStyle({ backgroundImage: "linear-gradient(45deg, red, blue)" });
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("Compound Component", () => {
|
|
185
|
+
test("Background.Image is accessible via dot notation", async () => {
|
|
186
|
+
render(
|
|
187
|
+
<Background data-testid="bg-container">
|
|
188
|
+
<Background.Image src="/test.jpg" data-testid="bg-img" />
|
|
189
|
+
</Background>,
|
|
190
|
+
);
|
|
191
|
+
await expect
|
|
192
|
+
.element(page.getByTestId("bg-container"))
|
|
193
|
+
.toBeInTheDocument();
|
|
194
|
+
await expect.element(page.getByTestId("bg-img")).toBeInTheDocument();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("can compose multiple background layers", async () => {
|
|
198
|
+
render(
|
|
199
|
+
<Background data-testid="bg-container">
|
|
200
|
+
<Background.Image src="/test.jpg" data-testid="bg-img" />
|
|
201
|
+
<Background.Overlay data-testid="bg-overlay" />
|
|
202
|
+
<Background.Gradient data-testid="bg-gradient" />
|
|
203
|
+
</Background>,
|
|
204
|
+
);
|
|
205
|
+
await expect
|
|
206
|
+
.element(page.getByTestId("bg-container"))
|
|
207
|
+
.toBeInTheDocument();
|
|
208
|
+
await expect.element(page.getByTestId("bg-img")).toBeInTheDocument();
|
|
209
|
+
await expect.element(page.getByTestId("bg-overlay")).toBeInTheDocument();
|
|
210
|
+
await expect.element(page.getByTestId("bg-gradient")).toBeInTheDocument();
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
});
|