@nationaldesignstudio/react 0.2.0 → 0.3.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/components/atoms/background/background.d.ts +13 -27
- package/dist/components/atoms/button/button.d.ts +55 -71
- package/dist/components/atoms/button/icon-button.d.ts +62 -110
- package/dist/components/atoms/input/input-group.d.ts +278 -0
- package/dist/components/atoms/input/input.d.ts +121 -0
- package/dist/components/atoms/select/select.d.ts +131 -0
- package/dist/components/organisms/card/card.d.ts +2 -2
- package/dist/components/sections/prose/prose.d.ts +3 -3
- package/dist/components/sections/river/river.d.ts +1 -1
- package/dist/components/sections/tout/tout.d.ts +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +11034 -7824
- package/dist/index.js.map +1 -1
- package/dist/lib/form-control.d.ts +105 -0
- package/dist/tokens.css +2132 -17329
- package/package.json +1 -1
- package/src/components/atoms/background/background.tsx +71 -109
- package/src/components/atoms/button/button.stories.tsx +42 -0
- package/src/components/atoms/button/button.test.tsx +1 -1
- package/src/components/atoms/button/button.tsx +38 -103
- package/src/components/atoms/button/button.visual.test.tsx +70 -24
- package/src/components/atoms/button/icon-button.tsx +81 -224
- package/src/components/atoms/input/index.ts +17 -0
- package/src/components/atoms/input/input-group.stories.tsx +650 -0
- package/src/components/atoms/input/input-group.test.tsx +376 -0
- package/src/components/atoms/input/input-group.tsx +384 -0
- package/src/components/atoms/input/input.stories.tsx +232 -0
- package/src/components/atoms/input/input.test.tsx +183 -0
- package/src/components/atoms/input/input.tsx +97 -0
- package/src/components/atoms/select/index.ts +18 -0
- package/src/components/atoms/select/select.stories.tsx +455 -0
- package/src/components/atoms/select/select.tsx +320 -0
- package/src/components/dev-tools/dev-toolbar/dev-toolbar.stories.tsx +2 -6
- package/src/components/foundation/typography/typography.stories.tsx +401 -0
- package/src/components/organisms/card/card.stories.tsx +11 -11
- package/src/components/organisms/card/card.test.tsx +1 -1
- package/src/components/organisms/card/card.tsx +2 -2
- package/src/components/organisms/card/card.visual.test.tsx +6 -6
- package/src/components/organisms/navbar/navbar.tsx +2 -2
- package/src/components/organisms/navbar/navbar.visual.test.tsx +2 -2
- package/src/components/sections/card-grid/card-grid.tsx +1 -1
- package/src/components/sections/faq-section/faq-section.tsx +2 -2
- package/src/components/sections/hero/hero.test.tsx +5 -5
- package/src/components/sections/prose/prose.test.tsx +2 -2
- package/src/components/sections/prose/prose.tsx +4 -5
- package/src/components/sections/river/river.stories.tsx +8 -8
- package/src/components/sections/river/river.test.tsx +1 -1
- package/src/components/sections/river/river.tsx +2 -4
- package/src/components/sections/tout/tout.test.tsx +1 -1
- package/src/components/sections/tout/tout.tsx +2 -2
- package/src/index.ts +41 -0
- package/src/lib/form-control.ts +69 -0
- package/src/stories/Introduction.mdx +29 -15
- package/src/stories/ThemeProvider.stories.tsx +1 -3
- package/src/stories/TokenShowcase.stories.tsx +0 -19
- package/src/stories/TokenShowcase.tsx +714 -1366
- package/src/styles.css +3 -0
- package/src/tests/token-resolution.test.tsx +301 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { describe, expect, test, vi } from "vitest";
|
|
2
|
+
import { page, userEvent } from "vitest/browser";
|
|
3
|
+
import { render } from "vitest-browser-react";
|
|
4
|
+
import {
|
|
5
|
+
InputGroup,
|
|
6
|
+
InputGroupAddon,
|
|
7
|
+
InputGroupButton,
|
|
8
|
+
InputGroupInput,
|
|
9
|
+
InputGroupText,
|
|
10
|
+
InputGroupTextarea,
|
|
11
|
+
} from "./input-group";
|
|
12
|
+
|
|
13
|
+
// Simple placeholder icon for tests
|
|
14
|
+
const SearchIcon = () => (
|
|
15
|
+
<svg
|
|
16
|
+
width="16"
|
|
17
|
+
height="16"
|
|
18
|
+
viewBox="0 0 16 16"
|
|
19
|
+
fill="none"
|
|
20
|
+
data-testid="search-icon"
|
|
21
|
+
aria-hidden="true"
|
|
22
|
+
>
|
|
23
|
+
<circle cx="7" cy="7" r="5" stroke="currentColor" strokeWidth="1.5" />
|
|
24
|
+
<path d="M14 14L11 11" stroke="currentColor" strokeWidth="1.5" />
|
|
25
|
+
</svg>
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
describe("InputGroup", () => {
|
|
29
|
+
describe("Accessibility", () => {
|
|
30
|
+
test("has group role", async () => {
|
|
31
|
+
render(
|
|
32
|
+
<InputGroup>
|
|
33
|
+
<InputGroupInput placeholder="Test" />
|
|
34
|
+
</InputGroup>,
|
|
35
|
+
);
|
|
36
|
+
await expect.element(page.getByRole("group")).toBeInTheDocument();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("input is focusable via keyboard", async () => {
|
|
40
|
+
render(
|
|
41
|
+
<InputGroup>
|
|
42
|
+
<InputGroupInput placeholder="Focusable" />
|
|
43
|
+
</InputGroup>,
|
|
44
|
+
);
|
|
45
|
+
await userEvent.keyboard("{Tab}");
|
|
46
|
+
await expect.element(page.getByRole("textbox")).toHaveFocus();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("disabled group prevents input focus", async () => {
|
|
50
|
+
render(
|
|
51
|
+
<>
|
|
52
|
+
<InputGroup disabled>
|
|
53
|
+
<InputGroupInput disabled placeholder="Disabled" />
|
|
54
|
+
</InputGroup>
|
|
55
|
+
<InputGroup>
|
|
56
|
+
<InputGroupInput placeholder="After" />
|
|
57
|
+
</InputGroup>
|
|
58
|
+
</>,
|
|
59
|
+
);
|
|
60
|
+
await userEvent.keyboard("{Tab}");
|
|
61
|
+
await expect.element(page.getByPlaceholder("After")).toHaveFocus();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("error state sets aria-invalid on input", async () => {
|
|
65
|
+
render(
|
|
66
|
+
<InputGroup>
|
|
67
|
+
<InputGroupInput aria-invalid="true" placeholder="Error" />
|
|
68
|
+
</InputGroup>,
|
|
69
|
+
);
|
|
70
|
+
await expect
|
|
71
|
+
.element(page.getByRole("textbox"))
|
|
72
|
+
.toHaveAttribute("aria-invalid", "true");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("addon button is accessible", async () => {
|
|
76
|
+
render(
|
|
77
|
+
<InputGroup>
|
|
78
|
+
<InputGroupInput placeholder="Input" />
|
|
79
|
+
<InputGroupAddon align="inline-end">
|
|
80
|
+
<InputGroupButton aria-label="Submit">Submit</InputGroupButton>
|
|
81
|
+
</InputGroupAddon>
|
|
82
|
+
</InputGroup>,
|
|
83
|
+
);
|
|
84
|
+
await expect
|
|
85
|
+
.element(page.getByRole("button", { name: "Submit" }))
|
|
86
|
+
.toBeInTheDocument();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("Interactions", () => {
|
|
91
|
+
test("input accepts text", async () => {
|
|
92
|
+
render(
|
|
93
|
+
<InputGroup>
|
|
94
|
+
<InputGroupInput placeholder="Type here" />
|
|
95
|
+
</InputGroup>,
|
|
96
|
+
);
|
|
97
|
+
const input = page.getByRole("textbox");
|
|
98
|
+
await input.fill("Hello World");
|
|
99
|
+
await expect.element(input).toHaveValue("Hello World");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("clicking addon focuses input", async () => {
|
|
103
|
+
render(
|
|
104
|
+
<InputGroup>
|
|
105
|
+
<InputGroupAddon data-testid="addon">
|
|
106
|
+
<SearchIcon />
|
|
107
|
+
</InputGroupAddon>
|
|
108
|
+
<InputGroupInput placeholder="Test" />
|
|
109
|
+
</InputGroup>,
|
|
110
|
+
);
|
|
111
|
+
await page.getByTestId("addon").click();
|
|
112
|
+
await expect.element(page.getByRole("textbox")).toHaveFocus();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("clicking button in addon does not focus input", async () => {
|
|
116
|
+
const handleClick = vi.fn();
|
|
117
|
+
render(
|
|
118
|
+
<InputGroup>
|
|
119
|
+
<InputGroupInput placeholder="Test" />
|
|
120
|
+
<InputGroupAddon align="inline-end">
|
|
121
|
+
<InputGroupButton onClick={handleClick}>Click</InputGroupButton>
|
|
122
|
+
</InputGroupAddon>
|
|
123
|
+
</InputGroup>,
|
|
124
|
+
);
|
|
125
|
+
await page.getByRole("button", { name: "Click" }).click();
|
|
126
|
+
expect(handleClick).toHaveBeenCalledOnce();
|
|
127
|
+
// Input should not have focus (button click should work independently)
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("input onChange is called", async () => {
|
|
131
|
+
const handleChange = vi.fn();
|
|
132
|
+
render(
|
|
133
|
+
<InputGroup>
|
|
134
|
+
<InputGroupInput onChange={handleChange} placeholder="Type" />
|
|
135
|
+
</InputGroup>,
|
|
136
|
+
);
|
|
137
|
+
await page.getByRole("textbox").fill("Test");
|
|
138
|
+
expect(handleChange).toHaveBeenCalled();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("textarea accepts multiline text", async () => {
|
|
142
|
+
render(
|
|
143
|
+
<InputGroup>
|
|
144
|
+
<InputGroupTextarea placeholder="Enter text" rows={4} />
|
|
145
|
+
</InputGroup>,
|
|
146
|
+
);
|
|
147
|
+
const textarea = page.getByRole("textbox");
|
|
148
|
+
await textarea.fill("Line 1\nLine 2");
|
|
149
|
+
await expect.element(textarea).toHaveValue("Line 1\nLine 2");
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("Variants", () => {
|
|
154
|
+
test("applies default size classes", async () => {
|
|
155
|
+
render(
|
|
156
|
+
<InputGroup>
|
|
157
|
+
<InputGroupInput placeholder="Default" />
|
|
158
|
+
</InputGroup>,
|
|
159
|
+
);
|
|
160
|
+
const group = page.getByRole("group");
|
|
161
|
+
await expect.element(group).toHaveClass(/h-36/);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("applies small size classes", async () => {
|
|
165
|
+
render(
|
|
166
|
+
<InputGroup size="sm">
|
|
167
|
+
<InputGroupInput placeholder="Small" />
|
|
168
|
+
</InputGroup>,
|
|
169
|
+
);
|
|
170
|
+
const group = page.getByRole("group");
|
|
171
|
+
await expect.element(group).toHaveClass(/h-32/);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("applies large size classes", async () => {
|
|
175
|
+
render(
|
|
176
|
+
<InputGroup size="lg">
|
|
177
|
+
<InputGroupInput placeholder="Large" />
|
|
178
|
+
</InputGroup>,
|
|
179
|
+
);
|
|
180
|
+
const group = page.getByRole("group");
|
|
181
|
+
await expect.element(group).toHaveClass(/h-48/);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("applies disabled data attribute", async () => {
|
|
185
|
+
render(
|
|
186
|
+
<InputGroup disabled>
|
|
187
|
+
<InputGroupInput disabled placeholder="Disabled" />
|
|
188
|
+
</InputGroup>,
|
|
189
|
+
);
|
|
190
|
+
const group = page.getByRole("group").first();
|
|
191
|
+
await expect.element(group).toHaveAttribute("data-disabled", "true");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("applies semantic token classes for background", async () => {
|
|
195
|
+
render(
|
|
196
|
+
<InputGroup>
|
|
197
|
+
<InputGroupInput placeholder="Default" />
|
|
198
|
+
</InputGroup>,
|
|
199
|
+
);
|
|
200
|
+
const group = page.getByRole("group").first();
|
|
201
|
+
await expect.element(group).toHaveClass(/bg-ui-control-background/);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("applies semantic token classes for border", async () => {
|
|
205
|
+
render(
|
|
206
|
+
<InputGroup>
|
|
207
|
+
<InputGroupInput placeholder="Default" />
|
|
208
|
+
</InputGroup>,
|
|
209
|
+
);
|
|
210
|
+
const group = page.getByRole("group").first();
|
|
211
|
+
await expect.element(group).toHaveClass(/border-ui-color-border/);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe("Addon Positioning", () => {
|
|
216
|
+
test("inline-start addon appears first", async () => {
|
|
217
|
+
render(
|
|
218
|
+
<InputGroup>
|
|
219
|
+
<InputGroupAddon data-testid="start-addon">
|
|
220
|
+
<SearchIcon />
|
|
221
|
+
</InputGroupAddon>
|
|
222
|
+
<InputGroupInput placeholder="Input" />
|
|
223
|
+
</InputGroup>,
|
|
224
|
+
);
|
|
225
|
+
const addon = page.getByTestId("start-addon");
|
|
226
|
+
await expect.element(addon).toHaveAttribute("data-align", "inline-start");
|
|
227
|
+
await expect.element(addon).toHaveClass(/order-first/);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("inline-end addon appears last", async () => {
|
|
231
|
+
render(
|
|
232
|
+
<InputGroup>
|
|
233
|
+
<InputGroupInput placeholder="Input" />
|
|
234
|
+
<InputGroupAddon align="inline-end" data-testid="end-addon">
|
|
235
|
+
<SearchIcon />
|
|
236
|
+
</InputGroupAddon>
|
|
237
|
+
</InputGroup>,
|
|
238
|
+
);
|
|
239
|
+
const addon = page.getByTestId("end-addon");
|
|
240
|
+
await expect.element(addon).toHaveAttribute("data-align", "inline-end");
|
|
241
|
+
await expect.element(addon).toHaveClass(/order-last/);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("block-start addon appears first with full width", async () => {
|
|
245
|
+
render(
|
|
246
|
+
<InputGroup>
|
|
247
|
+
<InputGroupAddon align="block-start" data-testid="block-start-addon">
|
|
248
|
+
<InputGroupText>Label</InputGroupText>
|
|
249
|
+
</InputGroupAddon>
|
|
250
|
+
<InputGroupInput placeholder="Input" />
|
|
251
|
+
</InputGroup>,
|
|
252
|
+
);
|
|
253
|
+
const addon = page.getByTestId("block-start-addon");
|
|
254
|
+
await expect.element(addon).toHaveAttribute("data-align", "block-start");
|
|
255
|
+
await expect.element(addon).toHaveClass(/w-full/);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("block-end addon appears last with full width", async () => {
|
|
259
|
+
render(
|
|
260
|
+
<InputGroup>
|
|
261
|
+
<InputGroupInput placeholder="Input" />
|
|
262
|
+
<InputGroupAddon align="block-end" data-testid="block-end-addon">
|
|
263
|
+
<InputGroupText>Helper</InputGroupText>
|
|
264
|
+
</InputGroupAddon>
|
|
265
|
+
</InputGroup>,
|
|
266
|
+
);
|
|
267
|
+
const addon = page.getByTestId("block-end-addon");
|
|
268
|
+
await expect.element(addon).toHaveAttribute("data-align", "block-end");
|
|
269
|
+
await expect.element(addon).toHaveClass(/w-full/);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe("InputGroupButton", () => {
|
|
274
|
+
test("renders as button with ghost variant by default", async () => {
|
|
275
|
+
render(
|
|
276
|
+
<InputGroup>
|
|
277
|
+
<InputGroupInput placeholder="Input" />
|
|
278
|
+
<InputGroupAddon align="inline-end">
|
|
279
|
+
<InputGroupButton>Click</InputGroupButton>
|
|
280
|
+
</InputGroupAddon>
|
|
281
|
+
</InputGroup>,
|
|
282
|
+
);
|
|
283
|
+
const button = page.getByRole("button", { name: "Click" });
|
|
284
|
+
await expect.element(button).toHaveAttribute("type", "button");
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("button click is handled", async () => {
|
|
288
|
+
const handleClick = vi.fn();
|
|
289
|
+
render(
|
|
290
|
+
<InputGroup>
|
|
291
|
+
<InputGroupInput placeholder="Input" />
|
|
292
|
+
<InputGroupAddon align="inline-end">
|
|
293
|
+
<InputGroupButton onClick={handleClick}>Submit</InputGroupButton>
|
|
294
|
+
</InputGroupAddon>
|
|
295
|
+
</InputGroup>,
|
|
296
|
+
);
|
|
297
|
+
await page.getByRole("button", { name: "Submit" }).click();
|
|
298
|
+
expect(handleClick).toHaveBeenCalledOnce();
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("supports different sizes", async () => {
|
|
302
|
+
render(
|
|
303
|
+
<InputGroup>
|
|
304
|
+
<InputGroupInput placeholder="Input" />
|
|
305
|
+
<InputGroupAddon align="inline-end">
|
|
306
|
+
<InputGroupButton size="sm" data-testid="sm-button">
|
|
307
|
+
Small
|
|
308
|
+
</InputGroupButton>
|
|
309
|
+
</InputGroupAddon>
|
|
310
|
+
</InputGroup>,
|
|
311
|
+
);
|
|
312
|
+
const button = page.getByTestId("sm-button");
|
|
313
|
+
await expect.element(button).toHaveAttribute("data-size", "sm");
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
describe("InputGroupText", () => {
|
|
318
|
+
test("renders text content", async () => {
|
|
319
|
+
render(
|
|
320
|
+
<InputGroup>
|
|
321
|
+
<InputGroupAddon>
|
|
322
|
+
<InputGroupText>https://</InputGroupText>
|
|
323
|
+
</InputGroupAddon>
|
|
324
|
+
<InputGroupInput placeholder="example.com" />
|
|
325
|
+
</InputGroup>,
|
|
326
|
+
);
|
|
327
|
+
await expect.element(page.getByText("https://")).toBeInTheDocument();
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test("applies text styling", async () => {
|
|
331
|
+
render(
|
|
332
|
+
<InputGroup>
|
|
333
|
+
<InputGroupAddon>
|
|
334
|
+
<InputGroupText data-testid="text">Prefix</InputGroupText>
|
|
335
|
+
</InputGroupAddon>
|
|
336
|
+
<InputGroupInput placeholder="Input" />
|
|
337
|
+
</InputGroup>,
|
|
338
|
+
);
|
|
339
|
+
const text = page.getByTestId("text");
|
|
340
|
+
await expect.element(text).toHaveClass(/text-text-muted/);
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
describe("InputGroupTextarea", () => {
|
|
345
|
+
test("renders textarea element", async () => {
|
|
346
|
+
render(
|
|
347
|
+
<InputGroup>
|
|
348
|
+
<InputGroupTextarea placeholder="Enter text" rows={4} />
|
|
349
|
+
</InputGroup>,
|
|
350
|
+
);
|
|
351
|
+
await expect.element(page.getByRole("textbox")).toBeInTheDocument();
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test("supports rows attribute", async () => {
|
|
355
|
+
render(
|
|
356
|
+
<InputGroup>
|
|
357
|
+
<InputGroupTextarea placeholder="Enter text" rows={6} />
|
|
358
|
+
</InputGroup>,
|
|
359
|
+
);
|
|
360
|
+
await expect
|
|
361
|
+
.element(page.getByRole("textbox"))
|
|
362
|
+
.toHaveAttribute("rows", "6");
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("has data-slot attribute", async () => {
|
|
366
|
+
render(
|
|
367
|
+
<InputGroup>
|
|
368
|
+
<InputGroupTextarea placeholder="Enter text" />
|
|
369
|
+
</InputGroup>,
|
|
370
|
+
);
|
|
371
|
+
await expect
|
|
372
|
+
.element(page.getByRole("textbox"))
|
|
373
|
+
.toHaveAttribute("data-slot", "input-group-control");
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
});
|