@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.
- package/dist/components/ai-agent-status/ai-agent-status.d.ts +1 -1
- package/dist/components/ai-tool/ai-tool.d.ts +1 -1
- package/dist/components/alert/alert.d.ts +1 -1
- package/dist/components/avatar/avatar.d.ts +1 -1
- package/dist/components/badge/badge.d.ts +1 -1
- package/dist/components/button/button.d.ts +1 -1
- package/dist/components/progress/progress.d.ts +1 -1
- package/dist/components/spinner/spinner.d.ts +1 -1
- package/dist/components/toast/toast.d.ts +1 -1
- package/package.json +7 -4
- package/src/blocks/admin-users/admin-users.stories.tsx +0 -83
- package/src/blocks/admin-users/admin-users.test.tsx +0 -159
- package/src/blocks/ai-chat/ai-chat.stories.tsx +0 -149
- package/src/blocks/ai-chat/ai-chat.test.tsx +0 -166
- package/src/blocks/dashboard/dashboard.stories.tsx +0 -106
- package/src/blocks/dashboard/dashboard.test.tsx +0 -94
- package/src/blocks/forgot-password/forgot-password.stories.tsx +0 -41
- package/src/blocks/forgot-password/forgot-password.test.tsx +0 -62
- package/src/blocks/login/login.stories.tsx +0 -46
- package/src/blocks/login/login.test.tsx +0 -100
- package/src/blocks/pricing/pricing.stories.tsx +0 -62
- package/src/blocks/pricing/pricing.test.tsx +0 -101
- package/src/blocks/settings/settings.stories.tsx +0 -94
- package/src/blocks/settings/settings.test.tsx +0 -167
- package/src/blocks/signup/signup.stories.tsx +0 -27
- package/src/blocks/signup/signup.test.tsx +0 -97
- package/src/components/accordion/accordion.stories.tsx +0 -90
- package/src/components/accordion/accordion.test.tsx +0 -156
- package/src/components/activity-feed/activity-feed.stories.tsx +0 -163
- package/src/components/activity-feed/activity-feed.test.tsx +0 -125
- package/src/components/ai-agent-status/ai-agent-status.stories.tsx +0 -86
- package/src/components/ai-agent-status/ai-agent-status.test.tsx +0 -66
- package/src/components/ai-conversation/ai-conversation.stories.tsx +0 -126
- package/src/components/ai-conversation/ai-conversation.test.tsx +0 -144
- package/src/components/ai-message/ai-message.stories.tsx +0 -106
- package/src/components/ai-message/ai-message.test.tsx +0 -160
- package/src/components/ai-model-selector/ai-model-selector.stories.tsx +0 -66
- package/src/components/ai-model-selector/ai-model-selector.test.tsx +0 -120
- package/src/components/ai-prompt-input/ai-prompt-input.stories.tsx +0 -145
- package/src/components/ai-prompt-input/ai-prompt-input.test.tsx +0 -273
- package/src/components/ai-reasoning/ai-reasoning.stories.tsx +0 -60
- package/src/components/ai-reasoning/ai-reasoning.test.tsx +0 -95
- package/src/components/ai-response/ai-response.stories.tsx +0 -75
- package/src/components/ai-response/ai-response.test.tsx +0 -96
- package/src/components/ai-sources/ai-sources.stories.tsx +0 -93
- package/src/components/ai-sources/ai-sources.test.tsx +0 -102
- package/src/components/ai-token-usage/ai-token-usage.stories.tsx +0 -59
- package/src/components/ai-token-usage/ai-token-usage.test.tsx +0 -83
- package/src/components/ai-tool/ai-tool.stories.tsx +0 -96
- package/src/components/ai-tool/ai-tool.test.tsx +0 -137
- package/src/components/alert/alert.stories.tsx +0 -101
- package/src/components/alert/alert.test.tsx +0 -94
- package/src/components/avatar/avatar.stories.tsx +0 -58
- package/src/components/avatar/avatar.test.tsx +0 -64
- package/src/components/badge/badge.stories.tsx +0 -68
- package/src/components/badge/badge.test.tsx +0 -61
- package/src/components/button/button.stories.tsx +0 -128
- package/src/components/button/button.test.tsx +0 -224
- package/src/components/calendar/calendar.stories.tsx +0 -105
- package/src/components/calendar/calendar.test.tsx +0 -255
- package/src/components/card/card.stories.tsx +0 -61
- package/src/components/card/card.test.tsx +0 -66
- package/src/components/checkbox/checkbox.stories.tsx +0 -124
- package/src/components/checkbox/checkbox.test.tsx +0 -121
- package/src/components/code-block/code-block.stories.tsx +0 -62
- package/src/components/code-block/code-block.test.tsx +0 -139
- package/src/components/combobox/combobox.stories.tsx +0 -171
- package/src/components/combobox/combobox.test.tsx +0 -429
- package/src/components/command/command.stories.tsx +0 -152
- package/src/components/command/command.test.tsx +0 -380
- package/src/components/data-table/data-table.stories.tsx +0 -151
- package/src/components/data-table/data-table.test.tsx +0 -288
- package/src/components/date-picker/date-picker.stories.tsx +0 -103
- package/src/components/date-picker/date-picker.test.tsx +0 -205
- package/src/components/dialog/dialog.stories.tsx +0 -140
- package/src/components/dialog/dialog.test.tsx +0 -186
- package/src/components/drawer/drawer.stories.tsx +0 -111
- package/src/components/drawer/drawer.test.tsx +0 -187
- package/src/components/dropdown-menu/dropdown-menu.stories.tsx +0 -128
- package/src/components/dropdown-menu/dropdown-menu.test.tsx +0 -321
- package/src/components/empty-state/empty-state.stories.tsx +0 -70
- package/src/components/empty-state/empty-state.test.tsx +0 -97
- package/src/components/form/form.stories.tsx +0 -144
- package/src/components/form/form.test.tsx +0 -203
- package/src/components/input/input.stories.tsx +0 -84
- package/src/components/input/input.test.tsx +0 -108
- package/src/components/label/label.stories.tsx +0 -51
- package/src/components/label/label.test.tsx +0 -55
- package/src/components/pagination/pagination.stories.tsx +0 -129
- package/src/components/pagination/pagination.test.tsx +0 -119
- package/src/components/popover/popover.stories.tsx +0 -84
- package/src/components/popover/popover.test.tsx +0 -163
- package/src/components/progress/progress.stories.tsx +0 -90
- package/src/components/progress/progress.test.tsx +0 -78
- package/src/components/radio-group/radio-group.stories.tsx +0 -102
- package/src/components/radio-group/radio-group.test.tsx +0 -152
- package/src/components/select/select.stories.tsx +0 -137
- package/src/components/select/select.test.tsx +0 -209
- package/src/components/separator/separator.stories.tsx +0 -42
- package/src/components/separator/separator.test.tsx +0 -46
- package/src/components/sheet/sheet.stories.tsx +0 -107
- package/src/components/sheet/sheet.test.tsx +0 -152
- package/src/components/skeleton/skeleton.stories.tsx +0 -46
- package/src/components/skeleton/skeleton.test.tsx +0 -57
- package/src/components/slider/slider.stories.tsx +0 -87
- package/src/components/slider/slider.test.tsx +0 -148
- package/src/components/spinner/spinner.stories.tsx +0 -47
- package/src/components/spinner/spinner.test.tsx +0 -41
- package/src/components/switch/switch.stories.tsx +0 -70
- package/src/components/switch/switch.test.tsx +0 -101
- package/src/components/table/table.stories.tsx +0 -131
- package/src/components/table/table.test.tsx +0 -153
- package/src/components/tabs/tabs.stories.tsx +0 -139
- package/src/components/tabs/tabs.test.tsx +0 -148
- package/src/components/toast/toast.stories.tsx +0 -167
- package/src/components/toast/toast.test.tsx +0 -306
- package/src/components/tooltip/tooltip.stories.tsx +0 -89
- package/src/components/tooltip/tooltip.test.tsx +0 -120
- package/src/lib/utils.test.ts +0 -32
- package/src/registry/meta.test.ts +0 -234
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
import { render, screen } from "@testing-library/react";
|
|
2
|
-
import userEvent from "@testing-library/user-event";
|
|
3
|
-
import { createRef } from "react";
|
|
4
|
-
import { describe, expect, it, vi } from "vitest";
|
|
5
|
-
|
|
6
|
-
import { expectNoA11yViolations } from "../../../test/a11y";
|
|
7
|
-
import { Button } from "./button";
|
|
8
|
-
|
|
9
|
-
describe("Button", () => {
|
|
10
|
-
it("renders its children inside a button element", () => {
|
|
11
|
-
render(<Button>Continue</Button>);
|
|
12
|
-
expect(screen.getByRole("button", { name: "Continue" })).toBeInTheDocument();
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it("applies the default variant and size", () => {
|
|
16
|
-
render(<Button>Save</Button>);
|
|
17
|
-
const button = screen.getByRole("button");
|
|
18
|
-
expect(button).toHaveClass("bg-primary");
|
|
19
|
-
expect(button).toHaveClass("h-9");
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it.each([
|
|
23
|
-
["secondary", "bg-secondary"],
|
|
24
|
-
["outline", "border-input"],
|
|
25
|
-
["ghost", "hover:bg-accent"],
|
|
26
|
-
["destructive", "bg-destructive"],
|
|
27
|
-
["link", "underline-offset-4"],
|
|
28
|
-
] as const)("applies the %s variant", (variant, expectedClass) => {
|
|
29
|
-
render(<Button variant={variant}>Action</Button>);
|
|
30
|
-
expect(screen.getByRole("button")).toHaveClass(expectedClass);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it.each([
|
|
34
|
-
["sm", "h-8"],
|
|
35
|
-
["md", "h-9"],
|
|
36
|
-
["lg", "h-10"],
|
|
37
|
-
["icon", "size-9"],
|
|
38
|
-
] as const)("applies the %s size", (size, expectedClass) => {
|
|
39
|
-
render(
|
|
40
|
-
<Button size={size} aria-label="Action">
|
|
41
|
-
A
|
|
42
|
-
</Button>,
|
|
43
|
-
);
|
|
44
|
-
expect(screen.getByRole("button")).toHaveClass(expectedClass);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("drops box sizing for the link variant so it aligns with surrounding text", () => {
|
|
48
|
-
render(
|
|
49
|
-
<Button variant="link" size="md">
|
|
50
|
-
Read more
|
|
51
|
-
</Button>,
|
|
52
|
-
);
|
|
53
|
-
const button = screen.getByRole("button");
|
|
54
|
-
expect(button).toHaveClass("h-auto");
|
|
55
|
-
expect(button).not.toHaveClass("h-9");
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("lets a consumer className override a conflicting variant utility", () => {
|
|
59
|
-
render(<Button className="h-20 bg-card">Tall</Button>);
|
|
60
|
-
const button = screen.getByRole("button");
|
|
61
|
-
expect(button).toHaveClass("h-20", "bg-card");
|
|
62
|
-
expect(button).not.toHaveClass("h-9");
|
|
63
|
-
expect(button).not.toHaveClass("bg-primary");
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("forwards a ref to the underlying element", () => {
|
|
67
|
-
const ref = createRef<HTMLButtonElement>();
|
|
68
|
-
render(<Button ref={ref}>Save</Button>);
|
|
69
|
-
expect(ref.current).toBeInstanceOf(HTMLButtonElement);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("forwards arbitrary props", () => {
|
|
73
|
-
render(
|
|
74
|
-
<Button type="submit" data-testid="submit">
|
|
75
|
-
Submit
|
|
76
|
-
</Button>,
|
|
77
|
-
);
|
|
78
|
-
expect(screen.getByTestId("submit")).toHaveAttribute("type", "submit");
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("calls onClick when activated with a pointer", async () => {
|
|
82
|
-
const onClick = vi.fn();
|
|
83
|
-
const user = userEvent.setup();
|
|
84
|
-
render(<Button onClick={onClick}>Save</Button>);
|
|
85
|
-
|
|
86
|
-
await user.click(screen.getByRole("button"));
|
|
87
|
-
expect(onClick).toHaveBeenCalledTimes(1);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("activates on Enter and Space from the keyboard", async () => {
|
|
91
|
-
const onClick = vi.fn();
|
|
92
|
-
const user = userEvent.setup();
|
|
93
|
-
render(<Button onClick={onClick}>Save</Button>);
|
|
94
|
-
|
|
95
|
-
await user.tab();
|
|
96
|
-
expect(screen.getByRole("button")).toHaveFocus();
|
|
97
|
-
|
|
98
|
-
await user.keyboard("{Enter}");
|
|
99
|
-
await user.keyboard(" ");
|
|
100
|
-
expect(onClick).toHaveBeenCalledTimes(2);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
describe("disabled", () => {
|
|
104
|
-
it("sets the disabled attribute and blocks activation", async () => {
|
|
105
|
-
const onClick = vi.fn();
|
|
106
|
-
const user = userEvent.setup();
|
|
107
|
-
render(
|
|
108
|
-
<Button disabled onClick={onClick}>
|
|
109
|
-
Save
|
|
110
|
-
</Button>,
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
const button = screen.getByRole("button");
|
|
114
|
-
expect(button).toBeDisabled();
|
|
115
|
-
await user.click(button);
|
|
116
|
-
expect(onClick).not.toHaveBeenCalled();
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
describe("loading", () => {
|
|
121
|
-
it("exposes a busy, disabled state to assistive technology", () => {
|
|
122
|
-
render(<Button loading>Save</Button>);
|
|
123
|
-
const button = screen.getByRole("button");
|
|
124
|
-
expect(button).toHaveAttribute("aria-busy", "true");
|
|
125
|
-
expect(button).toHaveAttribute("aria-disabled", "true");
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it("renders a spinner alongside the label", () => {
|
|
129
|
-
const { container } = render(<Button loading>Save</Button>);
|
|
130
|
-
expect(container.querySelector("svg")).toBeInTheDocument();
|
|
131
|
-
expect(screen.getByRole("button", { name: "Save" })).toBeInTheDocument();
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it("blocks activation", async () => {
|
|
135
|
-
const onClick = vi.fn();
|
|
136
|
-
const user = userEvent.setup();
|
|
137
|
-
render(
|
|
138
|
-
<Button loading onClick={onClick}>
|
|
139
|
-
Save
|
|
140
|
-
</Button>,
|
|
141
|
-
);
|
|
142
|
-
|
|
143
|
-
await user.click(screen.getByRole("button"));
|
|
144
|
-
expect(onClick).not.toHaveBeenCalled();
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it("scales the spinner with the button size", () => {
|
|
148
|
-
const { container: large } = render(
|
|
149
|
-
<Button loading size="lg">
|
|
150
|
-
Save
|
|
151
|
-
</Button>,
|
|
152
|
-
);
|
|
153
|
-
expect(large.querySelector("svg")).toHaveClass("size-5");
|
|
154
|
-
|
|
155
|
-
const { container: small } = render(
|
|
156
|
-
<Button loading size="sm">
|
|
157
|
-
Save
|
|
158
|
-
</Button>,
|
|
159
|
-
);
|
|
160
|
-
expect(small.querySelector("svg")).toHaveClass("size-3.5");
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("stays focusable, so the user's keyboard position is not lost", async () => {
|
|
164
|
-
const user = userEvent.setup();
|
|
165
|
-
render(<Button loading>Save</Button>);
|
|
166
|
-
|
|
167
|
-
await user.tab();
|
|
168
|
-
expect(screen.getByRole("button")).toHaveFocus();
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it("does not submit the form it belongs to", async () => {
|
|
172
|
-
const onSubmit = vi.fn((event: React.FormEvent) => event.preventDefault());
|
|
173
|
-
const user = userEvent.setup();
|
|
174
|
-
render(
|
|
175
|
-
<form onSubmit={onSubmit}>
|
|
176
|
-
<Button type="submit" loading>
|
|
177
|
-
Save
|
|
178
|
-
</Button>
|
|
179
|
-
</form>,
|
|
180
|
-
);
|
|
181
|
-
|
|
182
|
-
await user.click(screen.getByRole("button"));
|
|
183
|
-
expect(onSubmit).not.toHaveBeenCalled();
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
describe("asChild", () => {
|
|
188
|
-
it("renders the child element instead of a button", () => {
|
|
189
|
-
render(
|
|
190
|
-
<Button asChild>
|
|
191
|
-
<a href="/pricing">Pricing</a>
|
|
192
|
-
</Button>,
|
|
193
|
-
);
|
|
194
|
-
|
|
195
|
-
const link = screen.getByRole("link", { name: "Pricing" });
|
|
196
|
-
expect(link).toHaveAttribute("href", "/pricing");
|
|
197
|
-
expect(screen.queryByRole("button")).not.toBeInTheDocument();
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it("passes its styles down to the child", () => {
|
|
201
|
-
render(
|
|
202
|
-
<Button asChild variant="destructive">
|
|
203
|
-
<a href="/delete">Delete</a>
|
|
204
|
-
</Button>,
|
|
205
|
-
);
|
|
206
|
-
expect(screen.getByRole("link")).toHaveClass("bg-destructive");
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it("has no accessibility violations", async () => {
|
|
211
|
-
const { container } = render(
|
|
212
|
-
<>
|
|
213
|
-
<Button>Save</Button>
|
|
214
|
-
<Button variant="outline" loading>
|
|
215
|
-
Loading
|
|
216
|
-
</Button>
|
|
217
|
-
<Button size="icon" aria-label="Close">
|
|
218
|
-
x
|
|
219
|
-
</Button>
|
|
220
|
-
</>,
|
|
221
|
-
);
|
|
222
|
-
await expectNoA11yViolations(container);
|
|
223
|
-
});
|
|
224
|
-
});
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
-
import { useState } from "react";
|
|
3
|
-
import type { DateRange } from "react-day-picker";
|
|
4
|
-
|
|
5
|
-
import { Calendar } from "./calendar";
|
|
6
|
-
|
|
7
|
-
const JANUARY_2026 = new Date(2026, 0, 1);
|
|
8
|
-
|
|
9
|
-
const meta = {
|
|
10
|
-
title: "Forms/Calendar",
|
|
11
|
-
component: Calendar,
|
|
12
|
-
parameters: { controls: { disable: true } },
|
|
13
|
-
} satisfies Meta<typeof Calendar>;
|
|
14
|
-
|
|
15
|
-
export default meta;
|
|
16
|
-
type Story = StoryObj<typeof Calendar>;
|
|
17
|
-
|
|
18
|
-
export const Default: Story = {
|
|
19
|
-
render: function Default() {
|
|
20
|
-
const [date, setDate] = useState<Date | undefined>(new Date(2026, 0, 15));
|
|
21
|
-
return (
|
|
22
|
-
<Calendar
|
|
23
|
-
mode="single"
|
|
24
|
-
defaultMonth={JANUARY_2026}
|
|
25
|
-
selected={date}
|
|
26
|
-
onSelect={setDate}
|
|
27
|
-
className="rounded-lg border border-border"
|
|
28
|
-
/>
|
|
29
|
-
);
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export const Range: Story = {
|
|
34
|
-
render: function Range() {
|
|
35
|
-
const [range, setRange] = useState<DateRange | undefined>({
|
|
36
|
-
from: new Date(2026, 0, 10),
|
|
37
|
-
to: new Date(2026, 0, 16),
|
|
38
|
-
});
|
|
39
|
-
return (
|
|
40
|
-
<Calendar
|
|
41
|
-
mode="range"
|
|
42
|
-
defaultMonth={JANUARY_2026}
|
|
43
|
-
selected={range}
|
|
44
|
-
onSelect={setRange}
|
|
45
|
-
numberOfMonths={2}
|
|
46
|
-
className="rounded-lg border border-border"
|
|
47
|
-
/>
|
|
48
|
-
);
|
|
49
|
-
},
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
export const Multiple: Story = {
|
|
53
|
-
render: function Multiple() {
|
|
54
|
-
const [days, setDays] = useState<Date[] | undefined>([
|
|
55
|
-
new Date(2026, 0, 6),
|
|
56
|
-
new Date(2026, 0, 13),
|
|
57
|
-
new Date(2026, 0, 20),
|
|
58
|
-
]);
|
|
59
|
-
return (
|
|
60
|
-
<Calendar
|
|
61
|
-
mode="multiple"
|
|
62
|
-
defaultMonth={JANUARY_2026}
|
|
63
|
-
selected={days}
|
|
64
|
-
onSelect={setDays}
|
|
65
|
-
className="rounded-lg border border-border"
|
|
66
|
-
/>
|
|
67
|
-
);
|
|
68
|
-
},
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
/** Past dates are disabled — useful for scheduling. */
|
|
72
|
-
export const WithDisabledDates: Story = {
|
|
73
|
-
render: () => (
|
|
74
|
-
<Calendar
|
|
75
|
-
mode="single"
|
|
76
|
-
defaultMonth={JANUARY_2026}
|
|
77
|
-
disabled={{ before: new Date(2026, 0, 12) }}
|
|
78
|
-
className="rounded-lg border border-border"
|
|
79
|
-
/>
|
|
80
|
-
),
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
export const WithDropdownNavigation: Story = {
|
|
84
|
-
render: () => (
|
|
85
|
-
<Calendar
|
|
86
|
-
mode="single"
|
|
87
|
-
defaultMonth={JANUARY_2026}
|
|
88
|
-
captionLayout="dropdown"
|
|
89
|
-
startMonth={new Date(2020, 0)}
|
|
90
|
-
endMonth={new Date(2030, 11)}
|
|
91
|
-
className="rounded-lg border border-border"
|
|
92
|
-
/>
|
|
93
|
-
),
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
export const WithoutOutsideDays: Story = {
|
|
97
|
-
render: () => (
|
|
98
|
-
<Calendar
|
|
99
|
-
mode="single"
|
|
100
|
-
defaultMonth={JANUARY_2026}
|
|
101
|
-
showOutsideDays={false}
|
|
102
|
-
className="rounded-lg border border-border"
|
|
103
|
-
/>
|
|
104
|
-
),
|
|
105
|
-
};
|
|
@@ -1,255 +0,0 @@
|
|
|
1
|
-
import { render, screen, waitFor, within } from "@testing-library/react";
|
|
2
|
-
import userEvent from "@testing-library/user-event";
|
|
3
|
-
import { useState } from "react";
|
|
4
|
-
import type { DateRange } from "react-day-picker";
|
|
5
|
-
import { describe, expect, it, vi } from "vitest";
|
|
6
|
-
|
|
7
|
-
import { expectNoA11yViolations } from "../../../test/a11y";
|
|
8
|
-
import { Calendar } from "./calendar";
|
|
9
|
-
|
|
10
|
-
/** January 2026 — a fixed month so the tests never depend on today's date. */
|
|
11
|
-
const JANUARY_2026 = new Date(2026, 0, 1);
|
|
12
|
-
|
|
13
|
-
function dayButton(container: HTMLElement, iso: string): HTMLElement {
|
|
14
|
-
const cell = container.querySelector<HTMLElement>(`[data-day="${iso}"]`);
|
|
15
|
-
if (!cell) throw new Error(`No day cell for ${iso}`);
|
|
16
|
-
return within(cell).getByRole("button");
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
describe("Calendar", () => {
|
|
20
|
-
it("renders a date grid", () => {
|
|
21
|
-
render(<Calendar mode="single" defaultMonth={JANUARY_2026} />);
|
|
22
|
-
expect(screen.getByRole("grid")).toBeInTheDocument();
|
|
23
|
-
expect(screen.getAllByRole("gridcell").length).toBeGreaterThan(27);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("shows the month it was given", () => {
|
|
27
|
-
render(<Calendar mode="single" defaultMonth={JANUARY_2026} />);
|
|
28
|
-
expect(screen.getByText(/january 2026/i)).toBeInTheDocument();
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("selects a day on click", async () => {
|
|
32
|
-
const onSelect = vi.fn();
|
|
33
|
-
const user = userEvent.setup();
|
|
34
|
-
const { container } = render(
|
|
35
|
-
<Calendar mode="single" defaultMonth={JANUARY_2026} onSelect={onSelect} />,
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
await user.click(dayButton(container, "2026-01-15"));
|
|
39
|
-
expect(onSelect).toHaveBeenCalled();
|
|
40
|
-
expect(onSelect.mock.calls[0]?.[0]).toBeInstanceOf(Date);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("marks the selected day", () => {
|
|
44
|
-
const { container } = render(
|
|
45
|
-
<Calendar mode="single" defaultMonth={JANUARY_2026} selected={new Date(2026, 0, 15)} />,
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
const cell = container.querySelector('[data-day="2026-01-15"]');
|
|
49
|
-
expect(cell).toHaveAttribute("data-selected", "true");
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it("navigates to the next and previous month", async () => {
|
|
53
|
-
const user = userEvent.setup();
|
|
54
|
-
render(<Calendar mode="single" defaultMonth={JANUARY_2026} />);
|
|
55
|
-
|
|
56
|
-
await user.click(screen.getByRole("button", { name: /next/i }));
|
|
57
|
-
await waitFor(() => {
|
|
58
|
-
expect(screen.getByText(/february 2026/i)).toBeInTheDocument();
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
await user.click(screen.getByRole("button", { name: /previous/i }));
|
|
62
|
-
await waitFor(() => {
|
|
63
|
-
expect(screen.getByText(/january 2026/i)).toBeInTheDocument();
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("announces the visible month in a live region", () => {
|
|
68
|
-
render(<Calendar mode="single" defaultMonth={JANUARY_2026} />);
|
|
69
|
-
expect(screen.getByRole("status")).toBeInTheDocument();
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("moves through the grid with the arrow keys", async () => {
|
|
73
|
-
const user = userEvent.setup();
|
|
74
|
-
const { container } = render(<Calendar mode="single" defaultMonth={JANUARY_2026} />);
|
|
75
|
-
|
|
76
|
-
const start = dayButton(container, "2026-01-15");
|
|
77
|
-
start.focus();
|
|
78
|
-
expect(start).toHaveFocus();
|
|
79
|
-
|
|
80
|
-
await user.keyboard("{ArrowRight}");
|
|
81
|
-
expect(dayButton(container, "2026-01-16")).toHaveFocus();
|
|
82
|
-
|
|
83
|
-
// Down moves a whole week, which is what makes the grid a grid.
|
|
84
|
-
await user.keyboard("{ArrowDown}");
|
|
85
|
-
expect(dayButton(container, "2026-01-23")).toHaveFocus();
|
|
86
|
-
|
|
87
|
-
await user.keyboard("{ArrowLeft}");
|
|
88
|
-
expect(dayButton(container, "2026-01-22")).toHaveFocus();
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("selects the focused day with Enter", async () => {
|
|
92
|
-
const onSelect = vi.fn();
|
|
93
|
-
const user = userEvent.setup();
|
|
94
|
-
const { container } = render(
|
|
95
|
-
<Calendar mode="single" defaultMonth={JANUARY_2026} onSelect={onSelect} />,
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
dayButton(container, "2026-01-15").focus();
|
|
99
|
-
await user.keyboard("{Enter}");
|
|
100
|
-
expect(onSelect).toHaveBeenCalled();
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it("disables days that are not allowed", async () => {
|
|
104
|
-
const onSelect = vi.fn();
|
|
105
|
-
const user = userEvent.setup();
|
|
106
|
-
const { container } = render(
|
|
107
|
-
<Calendar
|
|
108
|
-
mode="single"
|
|
109
|
-
defaultMonth={JANUARY_2026}
|
|
110
|
-
disabled={{ before: new Date(2026, 0, 10) }}
|
|
111
|
-
onSelect={onSelect}
|
|
112
|
-
/>,
|
|
113
|
-
);
|
|
114
|
-
|
|
115
|
-
const early = dayButton(container, "2026-01-05");
|
|
116
|
-
expect(early).toBeDisabled();
|
|
117
|
-
await user.click(early);
|
|
118
|
-
expect(onSelect).not.toHaveBeenCalled();
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("marks days outside the visible month", () => {
|
|
122
|
-
const { container } = render(<Calendar mode="single" defaultMonth={JANUARY_2026} />);
|
|
123
|
-
expect(container.querySelector('[data-outside="true"]')).toBeInTheDocument();
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it("can hide days outside the visible month", () => {
|
|
127
|
-
const { container } = render(
|
|
128
|
-
<Calendar mode="single" defaultMonth={JANUARY_2026} showOutsideDays={false} />,
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
// The cells stay, so the grid keeps its shape; they are marked hidden and
|
|
132
|
-
// render no button, which is what removes them from the keyboard path.
|
|
133
|
-
const hidden = container.querySelectorAll('[data-hidden="true"]');
|
|
134
|
-
expect(hidden.length).toBeGreaterThan(0);
|
|
135
|
-
for (const cell of hidden) {
|
|
136
|
-
expect(within(cell as HTMLElement).queryByRole("button")).not.toBeInTheDocument();
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it("supports range selection", async () => {
|
|
141
|
-
const user = userEvent.setup();
|
|
142
|
-
|
|
143
|
-
function RangeExample() {
|
|
144
|
-
const [range, setRange] = useState<DateRange | undefined>();
|
|
145
|
-
return (
|
|
146
|
-
<>
|
|
147
|
-
<Calendar
|
|
148
|
-
mode="range"
|
|
149
|
-
defaultMonth={JANUARY_2026}
|
|
150
|
-
selected={range}
|
|
151
|
-
onSelect={setRange}
|
|
152
|
-
/>
|
|
153
|
-
{/* A plain span, not <output>: <output> carries an implicit
|
|
154
|
-
role="status", which would collide with the calendar's own live
|
|
155
|
-
region. */}
|
|
156
|
-
<span data-testid="range">
|
|
157
|
-
{range?.from ? `${range.from.getDate()}-${String(range.to?.getDate())}` : "none"}
|
|
158
|
-
</span>
|
|
159
|
-
</>
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const { container } = render(<RangeExample />);
|
|
164
|
-
expect(screen.getByTestId("range")).toHaveTextContent("none");
|
|
165
|
-
|
|
166
|
-
// The first click produces a one-day range, not a half-open one: `to` is
|
|
167
|
-
// set to the same day. The second click extends it.
|
|
168
|
-
await user.click(dayButton(container, "2026-01-10"));
|
|
169
|
-
await waitFor(() => {
|
|
170
|
-
expect(screen.getByTestId("range")).toHaveTextContent("10-10");
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
await user.click(dayButton(container, "2026-01-14"));
|
|
174
|
-
await waitFor(() => {
|
|
175
|
-
expect(screen.getByTestId("range")).toHaveTextContent("10-14");
|
|
176
|
-
});
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it("marks the days between the ends of a range", async () => {
|
|
180
|
-
const user = userEvent.setup();
|
|
181
|
-
|
|
182
|
-
function RangeExample() {
|
|
183
|
-
const [range, setRange] = useState<DateRange | undefined>();
|
|
184
|
-
return (
|
|
185
|
-
<Calendar
|
|
186
|
-
mode="range"
|
|
187
|
-
defaultMonth={JANUARY_2026}
|
|
188
|
-
selected={range}
|
|
189
|
-
onSelect={setRange}
|
|
190
|
-
/>
|
|
191
|
-
);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const { container } = render(<RangeExample />);
|
|
195
|
-
await user.click(dayButton(container, "2026-01-10"));
|
|
196
|
-
await user.click(dayButton(container, "2026-01-14"));
|
|
197
|
-
|
|
198
|
-
await waitFor(() => {
|
|
199
|
-
expect(container.querySelector('[data-day="2026-01-12"]')).toHaveAttribute(
|
|
200
|
-
"data-selected",
|
|
201
|
-
"true",
|
|
202
|
-
);
|
|
203
|
-
});
|
|
204
|
-
expect(container.querySelector('[data-day="2026-01-16"]')).not.toHaveAttribute(
|
|
205
|
-
"data-selected",
|
|
206
|
-
);
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
it("shows more than one month when asked", () => {
|
|
210
|
-
render(<Calendar mode="single" defaultMonth={JANUARY_2026} numberOfMonths={2} />);
|
|
211
|
-
expect(screen.getByText(/january 2026/i)).toBeInTheDocument();
|
|
212
|
-
expect(screen.getByText(/february 2026/i)).toBeInTheDocument();
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
it("merges a consumer className", () => {
|
|
216
|
-
const { container } = render(
|
|
217
|
-
<Calendar mode="single" defaultMonth={JANUARY_2026} className="p-6" />,
|
|
218
|
-
);
|
|
219
|
-
expect(container.querySelector(".rdp-root")).toHaveClass("p-6");
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it("lets a consumer override a generated classNames entry", () => {
|
|
223
|
-
const { container } = render(
|
|
224
|
-
<Calendar
|
|
225
|
-
mode="single"
|
|
226
|
-
defaultMonth={JANUARY_2026}
|
|
227
|
-
classNames={{ day: "custom-day-class" }}
|
|
228
|
-
/>,
|
|
229
|
-
);
|
|
230
|
-
expect(container.querySelector(".custom-day-class")).toBeInTheDocument();
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
it("renders dropdown navigation with its own chevrons", () => {
|
|
234
|
-
render(
|
|
235
|
-
<Calendar
|
|
236
|
-
mode="single"
|
|
237
|
-
defaultMonth={JANUARY_2026}
|
|
238
|
-
captionLayout="dropdown"
|
|
239
|
-
startMonth={new Date(2024, 0)}
|
|
240
|
-
endMonth={new Date(2028, 11)}
|
|
241
|
-
/>,
|
|
242
|
-
);
|
|
243
|
-
|
|
244
|
-
// Dropdown captions render comboboxes for month and year, each with a
|
|
245
|
-
// downward chevron — a different orientation from the nav arrows.
|
|
246
|
-
expect(screen.getAllByRole("combobox").length).toBeGreaterThanOrEqual(2);
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
it("has no accessibility violations", async () => {
|
|
250
|
-
const { container } = render(
|
|
251
|
-
<Calendar mode="single" defaultMonth={JANUARY_2026} aria-label="Event date" />,
|
|
252
|
-
);
|
|
253
|
-
await expectNoA11yViolations(container);
|
|
254
|
-
});
|
|
255
|
-
});
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
-
|
|
3
|
-
import { Button } from "@/components/button";
|
|
4
|
-
import { Input } from "@/components/input";
|
|
5
|
-
import { Label } from "@/components/label";
|
|
6
|
-
|
|
7
|
-
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./card";
|
|
8
|
-
|
|
9
|
-
const meta = {
|
|
10
|
-
title: "Display/Card",
|
|
11
|
-
component: Card,
|
|
12
|
-
parameters: { controls: { disable: true } },
|
|
13
|
-
} satisfies Meta<typeof Card>;
|
|
14
|
-
|
|
15
|
-
export default meta;
|
|
16
|
-
type Story = StoryObj<typeof meta>;
|
|
17
|
-
|
|
18
|
-
export const Default: Story = {
|
|
19
|
-
render: () => (
|
|
20
|
-
<Card className="w-80">
|
|
21
|
-
<CardHeader>
|
|
22
|
-
<CardTitle>Create project</CardTitle>
|
|
23
|
-
<CardDescription>Projects group your deployments and environments.</CardDescription>
|
|
24
|
-
</CardHeader>
|
|
25
|
-
<CardContent className="grid gap-2">
|
|
26
|
-
<Label htmlFor="name">Project name</Label>
|
|
27
|
-
<Input id="name" placeholder="acme-inc" />
|
|
28
|
-
</CardContent>
|
|
29
|
-
<CardFooter className="justify-end">
|
|
30
|
-
<Button variant="ghost">Cancel</Button>
|
|
31
|
-
<Button>Create project</Button>
|
|
32
|
-
</CardFooter>
|
|
33
|
-
</Card>
|
|
34
|
-
),
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
export const ContentOnly: Story = {
|
|
38
|
-
render: () => (
|
|
39
|
-
<Card className="w-80">
|
|
40
|
-
<CardContent className="pt-6">
|
|
41
|
-
<p className="text-sm text-muted-foreground">
|
|
42
|
-
A card does not require a header or a footer.
|
|
43
|
-
</p>
|
|
44
|
-
</CardContent>
|
|
45
|
-
</Card>
|
|
46
|
-
),
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
export const Stat: Story = {
|
|
50
|
-
render: () => (
|
|
51
|
-
<Card className="w-56">
|
|
52
|
-
<CardHeader className="pb-2">
|
|
53
|
-
<CardDescription>Monthly recurring revenue</CardDescription>
|
|
54
|
-
<CardTitle className="text-3xl">$48,120</CardTitle>
|
|
55
|
-
</CardHeader>
|
|
56
|
-
<CardContent>
|
|
57
|
-
<p className="text-xs text-success">+12.4% from last month</p>
|
|
58
|
-
</CardContent>
|
|
59
|
-
</Card>
|
|
60
|
-
),
|
|
61
|
-
};
|