@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,144 +0,0 @@
1
- import type { Decorator, Meta, StoryObj } from "@storybook/react-vite";
2
- import { useState } from "react";
3
-
4
- import { Button } from "@/components/button";
5
- import { Checkbox } from "@/components/checkbox";
6
- import { Input } from "@/components/input";
7
- import {
8
- Select,
9
- SelectContent,
10
- SelectItem,
11
- SelectTrigger,
12
- SelectValue,
13
- } from "@/components/select";
14
-
15
- import { Form, FormControl, FormDescription, FormField, FormLabel, FormMessage } from "./form";
16
-
17
- /** Named so its type is nameable in declaration output (TS2883). */
18
- const withFixedWidth: Decorator = (Story) => (
19
- <div className="w-80">
20
- <Story />
21
- </div>
22
- );
23
-
24
- const meta = {
25
- title: "Forms/Form",
26
- component: Form,
27
- parameters: { controls: { disable: true } },
28
- decorators: [withFixedWidth],
29
- } satisfies Meta<typeof Form>;
30
-
31
- export default meta;
32
- type Story = StoryObj<typeof meta>;
33
-
34
- export const Default: Story = {
35
- render: () => (
36
- <Form>
37
- <FormField name="username">
38
- <FormLabel>Username</FormLabel>
39
- <FormControl>
40
- <Input placeholder="ada" />
41
- </FormControl>
42
- <FormDescription>This is your public display name.</FormDescription>
43
- <FormMessage />
44
- </FormField>
45
- <Button type="submit">Save</Button>
46
- </Form>
47
- ),
48
- };
49
-
50
- export const WithError: Story = {
51
- render: () => (
52
- <Form>
53
- <FormField name="email" error="Enter a valid email address.">
54
- <FormLabel>Email</FormLabel>
55
- <FormControl>
56
- <Input type="email" defaultValue="not-an-email" />
57
- </FormControl>
58
- <FormDescription>We will never share it.</FormDescription>
59
- <FormMessage />
60
- </FormField>
61
- <Button type="submit">Save</Button>
62
- </Form>
63
- ),
64
- };
65
-
66
- /**
67
- * The field wiring is state-agnostic — this uses plain `useState`, and the
68
- * markup would be identical with React Hook Form or a server action. Only the
69
- * source of `error` changes.
70
- */
71
- export const LiveValidation: Story = {
72
- render: function LiveValidation() {
73
- const [email, setEmail] = useState("");
74
- const [touched, setTouched] = useState(false);
75
- const error = touched && !email.includes("@") ? "Enter a valid email address." : undefined;
76
-
77
- return (
78
- <Form
79
- onSubmit={(event) => {
80
- event.preventDefault();
81
- setTouched(true);
82
- }}
83
- >
84
- <FormField name="email" error={error}>
85
- <FormLabel>Email</FormLabel>
86
- <FormControl>
87
- <Input
88
- type="email"
89
- value={email}
90
- onChange={(event) => setEmail(event.target.value)}
91
- onBlur={() => setTouched(true)}
92
- placeholder="you@example.com"
93
- />
94
- </FormControl>
95
- <FormDescription>Used for deploy notifications.</FormDescription>
96
- <FormMessage />
97
- </FormField>
98
- <Button type="submit">Continue</Button>
99
- </Form>
100
- );
101
- },
102
- };
103
-
104
- export const MixedControls: Story = {
105
- render: () => (
106
- <Form>
107
- <FormField name="project">
108
- <FormLabel>Project name</FormLabel>
109
- <FormControl>
110
- <Input placeholder="acme-inc" />
111
- </FormControl>
112
- <FormMessage />
113
- </FormField>
114
-
115
- <FormField name="region" error="Choose a region to continue.">
116
- <FormLabel>Region</FormLabel>
117
- <FormControl>
118
- <Select>
119
- <SelectTrigger>
120
- <SelectValue placeholder="Select a region" />
121
- </SelectTrigger>
122
- <SelectContent>
123
- <SelectItem value="us">United States</SelectItem>
124
- <SelectItem value="eu">Europe</SelectItem>
125
- </SelectContent>
126
- </Select>
127
- </FormControl>
128
- <FormMessage />
129
- </FormField>
130
-
131
- <FormField name="terms">
132
- <div className="flex items-center gap-2">
133
- <FormControl>
134
- <Checkbox />
135
- </FormControl>
136
- <FormLabel>Accept terms and conditions</FormLabel>
137
- </div>
138
- <FormMessage />
139
- </FormField>
140
-
141
- <Button type="submit">Create project</Button>
142
- </Form>
143
- ),
144
- };
@@ -1,203 +0,0 @@
1
- import { render, screen } from "@testing-library/react";
2
- import userEvent from "@testing-library/user-event";
3
- import { useState } from "react";
4
- import { describe, expect, it, vi } from "vitest";
5
-
6
- import { expectNoA11yViolations } from "../../../test/a11y";
7
- import { Input } from "../input";
8
- import { Form, FormControl, FormDescription, FormField, FormLabel, FormMessage } from "./form";
9
-
10
- function Example({
11
- error,
12
- withDescription = true,
13
- }: {
14
- error?: string;
15
- withDescription?: boolean;
16
- } = {}) {
17
- return (
18
- <Form>
19
- <FormField name="email" error={error}>
20
- <FormLabel>Email</FormLabel>
21
- <FormControl>
22
- <Input type="email" />
23
- </FormControl>
24
- {withDescription ? <FormDescription>We will never share it.</FormDescription> : null}
25
- <FormMessage />
26
- </FormField>
27
- </Form>
28
- );
29
- }
30
-
31
- describe("Form", () => {
32
- it("disables native validation so custom messages are the only ones shown", () => {
33
- const { container } = render(<Example />);
34
- expect(container.querySelector("form")).toHaveAttribute("novalidate");
35
- });
36
-
37
- it("allows native validation back in", () => {
38
- const { container } = render(
39
- <Form noValidate={false}>
40
- <div />
41
- </Form>,
42
- );
43
- expect(container.querySelector("form")).not.toHaveAttribute("novalidate");
44
- });
45
- });
46
-
47
- describe("FormField", () => {
48
- it("points the label at the control", () => {
49
- render(<Example />);
50
- expect(screen.getByLabelText("Email")).toBe(screen.getByRole("textbox"));
51
- });
52
-
53
- it("describes the control with its description", () => {
54
- render(<Example />);
55
- expect(screen.getByRole("textbox")).toHaveAccessibleDescription("We will never share it.");
56
- });
57
-
58
- it("does not reference a description that was never rendered", () => {
59
- render(<Example withDescription={false} />);
60
- expect(screen.getByRole("textbox")).not.toHaveAttribute("aria-describedby");
61
- });
62
-
63
- it("is not invalid without an error", () => {
64
- render(<Example />);
65
- expect(screen.getByRole("textbox")).not.toHaveAttribute("aria-invalid");
66
- expect(screen.queryByText("Enter a valid email address.")).not.toBeInTheDocument();
67
- });
68
-
69
- describe("with an error", () => {
70
- it("marks the control invalid", () => {
71
- render(<Example error="Enter a valid email address." />);
72
- expect(screen.getByRole("textbox")).toHaveAttribute("aria-invalid", "true");
73
- });
74
-
75
- it("renders the message", () => {
76
- render(<Example error="Enter a valid email address." />);
77
- expect(screen.getByText("Enter a valid email address.")).toBeInTheDocument();
78
- });
79
-
80
- it("describes the control with both the description and the message", () => {
81
- render(<Example error="Enter a valid email address." />);
82
- expect(screen.getByRole("textbox")).toHaveAccessibleDescription(
83
- "We will never share it. Enter a valid email address.",
84
- );
85
- });
86
-
87
- it("announces the message politely", () => {
88
- render(<Example error="Enter a valid email address." />);
89
- const message = screen.getByText("Enter a valid email address.");
90
- expect(message).toHaveAttribute("role", "status");
91
- expect(message).toHaveAttribute("aria-live", "polite");
92
- });
93
-
94
- it("marks the label and wrapper as invalid for styling", () => {
95
- const { container } = render(<Example error="Bad" />);
96
- expect(container.querySelector("[data-slot='form-field']")).toHaveAttribute(
97
- "data-invalid",
98
- "true",
99
- );
100
- expect(screen.getByText("Email")).toHaveClass("text-destructive");
101
- });
102
- });
103
-
104
- it("mirrors the field name for form-library integration", () => {
105
- const { container } = render(<Example />);
106
- expect(container.querySelector("[data-slot='form-field']")).toHaveAttribute(
107
- "data-name",
108
- "email",
109
- );
110
- });
111
-
112
- it("gives each field its own ids", () => {
113
- render(
114
- <Form>
115
- <FormField name="first">
116
- <FormLabel>First</FormLabel>
117
- <FormControl>
118
- <Input />
119
- </FormControl>
120
- </FormField>
121
- <FormField name="last">
122
- <FormLabel>Last</FormLabel>
123
- <FormControl>
124
- <Input />
125
- </FormControl>
126
- </FormField>
127
- </Form>,
128
- );
129
-
130
- const first = screen.getByLabelText("First");
131
- const last = screen.getByLabelText("Last");
132
- expect(first.id).not.toBe(last.id);
133
- });
134
-
135
- it("works with any state source, updating as validation changes", async () => {
136
- const user = userEvent.setup();
137
-
138
- function Validated() {
139
- const [value, setValue] = useState("");
140
- const error = value.includes("@") ? undefined : "Enter a valid email address.";
141
- return (
142
- <Form>
143
- <FormField name="email" error={value ? error : undefined}>
144
- <FormLabel>Email</FormLabel>
145
- <FormControl>
146
- <Input value={value} onChange={(event) => setValue(event.target.value)} />
147
- </FormControl>
148
- <FormMessage />
149
- </FormField>
150
- </Form>
151
- );
152
- }
153
-
154
- render(<Validated />);
155
- const input = screen.getByRole("textbox");
156
-
157
- await user.type(input, "nope");
158
- expect(input).toHaveAttribute("aria-invalid", "true");
159
- expect(screen.getByText("Enter a valid email address.")).toBeInTheDocument();
160
-
161
- await user.type(input, "@example.com");
162
- expect(input).not.toHaveAttribute("aria-invalid");
163
- expect(screen.queryByText("Enter a valid email address.")).not.toBeInTheDocument();
164
- });
165
-
166
- it("lets FormMessage render its own children", () => {
167
- render(
168
- <Form>
169
- <FormField name="email">
170
- <FormLabel>Email</FormLabel>
171
- <FormControl>
172
- <Input />
173
- </FormControl>
174
- <FormMessage>Custom hint</FormMessage>
175
- </FormField>
176
- </Form>,
177
- );
178
- expect(screen.getByText("Custom hint")).toBeInTheDocument();
179
- });
180
-
181
- it.each([
182
- ["FormLabel", <FormLabel key="l">Label</FormLabel>],
183
- [
184
- "FormControl",
185
- <FormControl key="c">
186
- <input />
187
- </FormControl>,
188
- ],
189
- ["FormDescription", <FormDescription key="d">Text</FormDescription>],
190
- ["FormMessage", <FormMessage key="m">Text</FormMessage>],
191
- ])("throws a useful error if %s is used outside FormField", (name, element) => {
192
- const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined);
193
- expect(() => render(element)).toThrow(
194
- new RegExp(`${name} must be rendered inside <FormField>`),
195
- );
196
- consoleError.mockRestore();
197
- });
198
-
199
- it("has no accessibility violations", async () => {
200
- const { container } = render(<Example error="Enter a valid email address." />);
201
- await expectNoA11yViolations(container);
202
- });
203
- });
@@ -1,84 +0,0 @@
1
- import type { Decorator, Meta, StoryObj } from "@storybook/react-vite";
2
-
3
- import { Label } from "@/components/label";
4
-
5
- import { Input } from "./input";
6
-
7
- /** Named so its type is nameable in declaration output (TS2883). */
8
- const withFixedWidth: Decorator = (Story) => (
9
- <div className="w-72">
10
- <Story />
11
- </div>
12
- );
13
-
14
- const meta = {
15
- title: "Forms/Input",
16
- component: Input,
17
- args: {
18
- placeholder: "acme-inc",
19
- inputSize: "md",
20
- },
21
- argTypes: {
22
- inputSize: { control: "select", options: ["sm", "md", "lg"] },
23
- disabled: { control: "boolean" },
24
- type: { control: "select", options: ["text", "email", "password", "number", "search"] },
25
- },
26
- decorators: [withFixedWidth],
27
- } satisfies Meta<typeof Input>;
28
-
29
- export default meta;
30
- type Story = StoryObj<typeof meta>;
31
-
32
- export const Default: Story = {
33
- render: (args) => (
34
- <div className="grid gap-2">
35
- <Label htmlFor="project">Project name</Label>
36
- <Input id="project" {...args} />
37
- </div>
38
- ),
39
- };
40
-
41
- export const Sizes: Story = {
42
- parameters: { controls: { disable: true } },
43
- render: () => (
44
- <div className="grid gap-3">
45
- <Input inputSize="sm" placeholder="Small" aria-label="Small" />
46
- <Input inputSize="md" placeholder="Medium" aria-label="Medium" />
47
- <Input inputSize="lg" placeholder="Large" aria-label="Large" />
48
- </div>
49
- ),
50
- };
51
-
52
- export const Invalid: Story = {
53
- render: () => (
54
- <div className="grid gap-2">
55
- <Label htmlFor="email">Email</Label>
56
- <Input
57
- id="email"
58
- type="email"
59
- defaultValue="not-an-email"
60
- aria-invalid
61
- aria-describedby="email-error"
62
- />
63
- <p id="email-error" className="text-xs text-destructive">
64
- Enter a valid email address.
65
- </p>
66
- </div>
67
- ),
68
- };
69
-
70
- export const Disabled: Story = {
71
- args: { disabled: true, value: "Read only" },
72
- };
73
-
74
- export const Types: Story = {
75
- parameters: { controls: { disable: true } },
76
- render: () => (
77
- <div className="grid gap-3">
78
- <Input type="email" placeholder="you@example.com" aria-label="Email" />
79
- <Input type="password" placeholder="••••••••" aria-label="Password" />
80
- <Input type="number" placeholder="42" aria-label="Quantity" />
81
- <Input type="search" placeholder="Search…" aria-label="Search" />
82
- </div>
83
- ),
84
- };
@@ -1,108 +0,0 @@
1
- import { render, screen } from "@testing-library/react";
2
- import userEvent from "@testing-library/user-event";
3
- import { createRef, useState } from "react";
4
- import { describe, expect, it, vi } from "vitest";
5
-
6
- import { expectNoA11yViolations } from "../../../test/a11y";
7
- import { Input } from "./input";
8
-
9
- describe("Input", () => {
10
- it("renders a text input by default", () => {
11
- render(<Input aria-label="Name" />);
12
- expect(screen.getByRole("textbox", { name: "Name" })).toHaveAttribute("type", "text");
13
- });
14
-
15
- it("honours an explicit type", () => {
16
- render(<Input type="email" aria-label="Email" />);
17
- expect(screen.getByRole("textbox")).toHaveAttribute("type", "email");
18
- });
19
-
20
- it.each([
21
- ["sm", "h-8"],
22
- ["md", "h-9"],
23
- ["lg", "h-10"],
24
- ] as const)("applies the %s size", (inputSize, expectedClass) => {
25
- render(<Input inputSize={inputSize} aria-label="Name" />);
26
- expect(screen.getByRole("textbox")).toHaveClass(expectedClass);
27
- });
28
-
29
- it("does not shadow the native size attribute", () => {
30
- render(<Input size={5} aria-label="Name" />);
31
- expect(screen.getByRole("textbox")).toHaveAttribute("size", "5");
32
- });
33
-
34
- it("lets a consumer className override a conflicting utility", () => {
35
- render(<Input className="h-16" aria-label="Name" />);
36
- const input = screen.getByRole("textbox");
37
- expect(input).toHaveClass("h-16");
38
- expect(input).not.toHaveClass("h-9");
39
- });
40
-
41
- it("forwards a ref", () => {
42
- const ref = createRef<HTMLInputElement>();
43
- render(<Input ref={ref} aria-label="Name" />);
44
- expect(ref.current).toBeInstanceOf(HTMLInputElement);
45
- });
46
-
47
- it("works uncontrolled", async () => {
48
- const user = userEvent.setup();
49
- render(<Input defaultValue="ab" aria-label="Name" />);
50
-
51
- const input = screen.getByRole<HTMLInputElement>("textbox");
52
- await user.type(input, "cd");
53
- expect(input.value).toBe("abcd");
54
- });
55
-
56
- it("works controlled", async () => {
57
- const user = userEvent.setup();
58
-
59
- function Controlled() {
60
- const [value, setValue] = useState("");
61
- return (
62
- <Input
63
- value={value}
64
- onChange={(event) => setValue(event.target.value)}
65
- aria-label="Name"
66
- />
67
- );
68
- }
69
-
70
- render(<Controlled />);
71
- const input = screen.getByRole<HTMLInputElement>("textbox");
72
- await user.type(input, "hello");
73
- expect(input.value).toBe("hello");
74
- });
75
-
76
- it("does not accept input while disabled", async () => {
77
- const onChange = vi.fn();
78
- const user = userEvent.setup();
79
- render(<Input disabled onChange={onChange} aria-label="Name" />);
80
-
81
- const input = screen.getByRole("textbox");
82
- expect(input).toBeDisabled();
83
- await user.type(input, "x");
84
- expect(onChange).not.toHaveBeenCalled();
85
- });
86
-
87
- it("exposes the invalid state through aria-invalid", () => {
88
- render(<Input aria-invalid aria-label="Name" />);
89
- expect(screen.getByRole("textbox")).toHaveAttribute("aria-invalid", "true");
90
- });
91
-
92
- it("is reachable by keyboard", async () => {
93
- const user = userEvent.setup();
94
- render(<Input aria-label="Name" />);
95
- await user.tab();
96
- expect(screen.getByRole("textbox")).toHaveFocus();
97
- });
98
-
99
- it("has no accessibility violations", async () => {
100
- const { container } = render(
101
- <>
102
- <label htmlFor="field">Name</label>
103
- <Input id="field" />
104
- </>,
105
- );
106
- await expectNoA11yViolations(container);
107
- });
108
- });
@@ -1,51 +0,0 @@
1
- import type { Meta, StoryObj } from "@storybook/react-vite";
2
-
3
- import { Input } from "@/components/input";
4
-
5
- import { Label } from "./label";
6
-
7
- const meta = {
8
- title: "Forms/Label",
9
- component: Label,
10
- args: { children: "Project name" },
11
- } satisfies Meta<typeof Label>;
12
-
13
- export default meta;
14
- type Story = StoryObj<typeof meta>;
15
-
16
- export const Default: Story = {
17
- render: (args) => (
18
- <div className="grid w-72 gap-2">
19
- <Label htmlFor="project" {...args} />
20
- <Input id="project" placeholder="acme-inc" />
21
- </div>
22
- ),
23
- };
24
-
25
- export const Required: Story = {
26
- parameters: { controls: { disable: true } },
27
- render: () => (
28
- <div className="grid w-72 gap-2">
29
- <Label htmlFor="email">
30
- Email
31
- <span aria-hidden="true" className="text-destructive">
32
- *
33
- </span>
34
- </Label>
35
- <Input id="email" type="email" required placeholder="you@example.com" />
36
- </div>
37
- ),
38
- };
39
-
40
- /** A label dims alongside the control it describes. */
41
- export const WithDisabledControl: Story = {
42
- parameters: { controls: { disable: true } },
43
- render: () => (
44
- <div className="grid w-72 gap-2">
45
- <Input id="locked" disabled placeholder="Locked" className="peer order-2" />
46
- <Label htmlFor="locked" className="order-1">
47
- Workspace slug
48
- </Label>
49
- </div>
50
- ),
51
- };
@@ -1,55 +0,0 @@
1
- import { render, screen } from "@testing-library/react";
2
- import userEvent from "@testing-library/user-event";
3
- import { describe, expect, it } from "vitest";
4
-
5
- import { expectNoA11yViolations } from "../../../test/a11y";
6
- import { Label } from "./label";
7
-
8
- describe("Label", () => {
9
- it("renders its text", () => {
10
- render(<Label htmlFor="name">Full name</Label>);
11
- expect(screen.getByText("Full name")).toBeInTheDocument();
12
- });
13
-
14
- it("associates itself with a control via htmlFor", () => {
15
- render(
16
- <>
17
- <Label htmlFor="name">Full name</Label>
18
- <input id="name" />
19
- </>,
20
- );
21
- expect(screen.getByLabelText("Full name")).toBeInTheDocument();
22
- });
23
-
24
- it("moves focus to its control when clicked", async () => {
25
- const user = userEvent.setup();
26
- render(
27
- <>
28
- <Label htmlFor="name">Full name</Label>
29
- <input id="name" />
30
- </>,
31
- );
32
-
33
- await user.click(screen.getByText("Full name"));
34
- expect(screen.getByLabelText("Full name")).toHaveFocus();
35
- });
36
-
37
- it("merges a consumer className", () => {
38
- render(
39
- <Label htmlFor="name" className="text-destructive">
40
- Full name
41
- </Label>,
42
- );
43
- expect(screen.getByText("Full name")).toHaveClass("text-destructive", "font-medium");
44
- });
45
-
46
- it("has no accessibility violations", async () => {
47
- const { container } = render(
48
- <>
49
- <Label htmlFor="name">Full name</Label>
50
- <input id="name" />
51
- </>,
52
- );
53
- await expectNoA11yViolations(container);
54
- });
55
- });