@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,288 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
columnVisibilityFeature,
|
|
3
|
-
createPaginatedRowModel,
|
|
4
|
-
createSortedRowModel,
|
|
5
|
-
rowPaginationFeature,
|
|
6
|
-
rowSortingFeature,
|
|
7
|
-
tableFeatures,
|
|
8
|
-
useTable,
|
|
9
|
-
} from "@tanstack/react-table";
|
|
10
|
-
import { render, screen, waitFor, within } from "@testing-library/react";
|
|
11
|
-
import userEvent from "@testing-library/user-event";
|
|
12
|
-
import { describe, expect, it } from "vitest";
|
|
13
|
-
|
|
14
|
-
import { expectNoA11yViolations } from "../../../test/a11y";
|
|
15
|
-
import {
|
|
16
|
-
DataTable,
|
|
17
|
-
DataTableColumnHeader,
|
|
18
|
-
DataTablePagination,
|
|
19
|
-
DataTableViewOptions,
|
|
20
|
-
} from "./data-table";
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Driven by a real table instance rather than a stub, because the point of
|
|
24
|
-
* these components is that they present whatever the table library produces.
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
interface Person {
|
|
28
|
-
name: string;
|
|
29
|
-
role: string;
|
|
30
|
-
age: number;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const PEOPLE: Person[] = [
|
|
34
|
-
{ name: "Ada Lovelace", role: "Engineering", age: 36 },
|
|
35
|
-
{ name: "Grace Hopper", role: "Engineering", age: 45 },
|
|
36
|
-
{ name: "Katherine Johnson", role: "Research", age: 52 },
|
|
37
|
-
{ name: "Radia Perlman", role: "Networking", age: 41 },
|
|
38
|
-
];
|
|
39
|
-
|
|
40
|
-
const features = tableFeatures({
|
|
41
|
-
rowSortingFeature,
|
|
42
|
-
rowPaginationFeature,
|
|
43
|
-
columnVisibilityFeature,
|
|
44
|
-
sortedRowModel: createSortedRowModel(),
|
|
45
|
-
paginatedRowModel: createPaginatedRowModel(),
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
function useDemoTable(data: Person[] = PEOPLE, pageSize = 10) {
|
|
49
|
-
return useTable({
|
|
50
|
-
features,
|
|
51
|
-
data,
|
|
52
|
-
initialState: { pagination: { pageIndex: 0, pageSize } },
|
|
53
|
-
columns: [
|
|
54
|
-
{
|
|
55
|
-
accessorKey: "name",
|
|
56
|
-
header: ({ column }) => <DataTableColumnHeader column={column} title="Name" />,
|
|
57
|
-
},
|
|
58
|
-
{ accessorKey: "role", header: "Role", enableSorting: false },
|
|
59
|
-
{
|
|
60
|
-
accessorKey: "age",
|
|
61
|
-
header: ({ column }) => <DataTableColumnHeader column={column} title="Age" />,
|
|
62
|
-
},
|
|
63
|
-
],
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function Example({ data, pageSize }: { data?: Person[]; pageSize?: number } = {}) {
|
|
68
|
-
const table = useDemoTable(data, pageSize);
|
|
69
|
-
return (
|
|
70
|
-
<div>
|
|
71
|
-
<DataTableViewOptions table={table} />
|
|
72
|
-
<DataTable table={table} aria-label="People" />
|
|
73
|
-
<DataTablePagination table={table} />
|
|
74
|
-
</div>
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function rowTexts(): string[][] {
|
|
79
|
-
const body = screen.getAllByRole("rowgroup")[1];
|
|
80
|
-
if (!body) return [];
|
|
81
|
-
return within(body)
|
|
82
|
-
.getAllByRole("row")
|
|
83
|
-
.map((row) =>
|
|
84
|
-
within(row)
|
|
85
|
-
.getAllByRole("cell")
|
|
86
|
-
.map((cell) => cell.textContent ?? ""),
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
describe("DataTable", () => {
|
|
91
|
-
it("renders a row per record and a cell per column", () => {
|
|
92
|
-
render(<Example />);
|
|
93
|
-
const rows = rowTexts();
|
|
94
|
-
expect(rows).toHaveLength(4);
|
|
95
|
-
expect(rows[0]).toEqual(["Ada Lovelace", "Engineering", "36"]);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it("renders the header for each column", () => {
|
|
99
|
-
render(<Example />);
|
|
100
|
-
expect(screen.getByRole("columnheader", { name: /Name/ })).toBeInTheDocument();
|
|
101
|
-
expect(screen.getByRole("columnheader", { name: "Role" })).toBeInTheDocument();
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it("marks sortable columns with aria-sort", () => {
|
|
105
|
-
render(<Example />);
|
|
106
|
-
expect(screen.getByRole("columnheader", { name: /Name/ })).toHaveAttribute(
|
|
107
|
-
"aria-sort",
|
|
108
|
-
"none",
|
|
109
|
-
);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it("does not claim a non-sortable column is sortable", () => {
|
|
113
|
-
render(<Example />);
|
|
114
|
-
expect(screen.getByRole("columnheader", { name: "Role" })).not.toHaveAttribute("aria-sort");
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it("shows an empty message when there are no rows", () => {
|
|
118
|
-
render(<Example data={[]} />);
|
|
119
|
-
expect(screen.getByText("No results.")).toBeInTheDocument();
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it("accepts a custom empty state", () => {
|
|
123
|
-
function CustomEmpty() {
|
|
124
|
-
const table = useDemoTable([]);
|
|
125
|
-
return <DataTable table={table} empty={<span>Nothing to show yet.</span>} />;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
render(<CustomEmpty />);
|
|
129
|
-
expect(screen.getByText("Nothing to show yet.")).toBeInTheDocument();
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it("has no accessibility violations", async () => {
|
|
133
|
-
const { container } = render(<Example />);
|
|
134
|
-
await expectNoA11yViolations(container);
|
|
135
|
-
});
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
describe("DataTableColumnHeader", () => {
|
|
139
|
-
it("renders a sortable column as a button", () => {
|
|
140
|
-
render(<Example />);
|
|
141
|
-
expect(screen.getByRole("button", { name: /Name, not sorted/ })).toBeInTheDocument();
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it("renders a non-sortable column as text, not a dead button", () => {
|
|
145
|
-
render(<Example />);
|
|
146
|
-
expect(screen.queryByRole("button", { name: /Role/ })).not.toBeInTheDocument();
|
|
147
|
-
expect(screen.getByRole("columnheader", { name: "Role" })).toBeInTheDocument();
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it("sorts ascending, and says so in text as well as in aria-sort", async () => {
|
|
151
|
-
const user = userEvent.setup();
|
|
152
|
-
render(<Example />);
|
|
153
|
-
|
|
154
|
-
await user.click(screen.getByRole("button", { name: /Name, not sorted/ }));
|
|
155
|
-
await user.click(await screen.findByRole("menuitem", { name: "Sort ascending" }));
|
|
156
|
-
|
|
157
|
-
await waitFor(() => {
|
|
158
|
-
expect(screen.getByRole("columnheader", { name: /Name/ })).toHaveAttribute(
|
|
159
|
-
"aria-sort",
|
|
160
|
-
"ascending",
|
|
161
|
-
);
|
|
162
|
-
});
|
|
163
|
-
expect(screen.getByRole("button", { name: /Name, sorted ascending/ })).toBeInTheDocument();
|
|
164
|
-
expect(rowTexts()[0]?.[0]).toBe("Ada Lovelace");
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it("sorts descending", async () => {
|
|
168
|
-
const user = userEvent.setup();
|
|
169
|
-
render(<Example />);
|
|
170
|
-
|
|
171
|
-
await user.click(screen.getByRole("button", { name: /Name, not sorted/ }));
|
|
172
|
-
await user.click(await screen.findByRole("menuitem", { name: "Sort descending" }));
|
|
173
|
-
|
|
174
|
-
await waitFor(() => {
|
|
175
|
-
expect(screen.getByRole("columnheader", { name: /Name/ })).toHaveAttribute(
|
|
176
|
-
"aria-sort",
|
|
177
|
-
"descending",
|
|
178
|
-
);
|
|
179
|
-
});
|
|
180
|
-
expect(rowTexts()[0]?.[0]).toBe("Radia Perlman");
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it("actually reorders the rows when sorting a numeric column", async () => {
|
|
184
|
-
const user = userEvent.setup();
|
|
185
|
-
render(<Example />);
|
|
186
|
-
|
|
187
|
-
await user.click(screen.getByRole("button", { name: /Age, not sorted/ }));
|
|
188
|
-
await user.click(await screen.findByRole("menuitem", { name: "Sort descending" }));
|
|
189
|
-
|
|
190
|
-
await waitFor(() => {
|
|
191
|
-
expect(rowTexts()[0]?.[2]).toBe("52");
|
|
192
|
-
});
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
describe("DataTableViewOptions", () => {
|
|
197
|
-
it("lists the hideable columns", async () => {
|
|
198
|
-
const user = userEvent.setup();
|
|
199
|
-
render(<Example />);
|
|
200
|
-
|
|
201
|
-
await user.click(screen.getByRole("button", { name: "Columns" }));
|
|
202
|
-
for (const id of ["name", "role", "age"]) {
|
|
203
|
-
expect(await screen.findByRole("menuitemcheckbox", { name: id })).toBeInTheDocument();
|
|
204
|
-
}
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
it("hides a column when toggled off", async () => {
|
|
208
|
-
const user = userEvent.setup();
|
|
209
|
-
render(<Example />);
|
|
210
|
-
|
|
211
|
-
await user.click(screen.getByRole("button", { name: "Columns" }));
|
|
212
|
-
await user.click(await screen.findByRole("menuitemcheckbox", { name: "role" }));
|
|
213
|
-
|
|
214
|
-
await waitFor(() => {
|
|
215
|
-
expect(screen.queryByRole("columnheader", { name: "Role" })).not.toBeInTheDocument();
|
|
216
|
-
});
|
|
217
|
-
expect(rowTexts()[0]).toEqual(["Ada Lovelace", "36"]);
|
|
218
|
-
});
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
describe("DataTablePagination", () => {
|
|
222
|
-
it("reports the page position in a live region", () => {
|
|
223
|
-
render(<Example pageSize={2} />);
|
|
224
|
-
|
|
225
|
-
const status = screen.getByText(/Page 1 of 2/);
|
|
226
|
-
expect(status).toHaveAttribute("aria-live", "polite");
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it("moves between pages", async () => {
|
|
230
|
-
const user = userEvent.setup();
|
|
231
|
-
render(<Example pageSize={2} />);
|
|
232
|
-
|
|
233
|
-
expect(rowTexts()).toHaveLength(2);
|
|
234
|
-
expect(rowTexts()[0]?.[0]).toBe("Ada Lovelace");
|
|
235
|
-
|
|
236
|
-
await user.click(screen.getByRole("button", { name: "Go to next page" }));
|
|
237
|
-
await waitFor(() => {
|
|
238
|
-
expect(rowTexts()[0]?.[0]).toBe("Katherine Johnson");
|
|
239
|
-
});
|
|
240
|
-
expect(screen.getByText(/Page 2 of 2/)).toBeInTheDocument();
|
|
241
|
-
|
|
242
|
-
await user.click(screen.getByRole("button", { name: "Go to previous page" }));
|
|
243
|
-
await waitFor(() => {
|
|
244
|
-
expect(rowTexts()[0]?.[0]).toBe("Ada Lovelace");
|
|
245
|
-
});
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
it("disables the controls at the ends", async () => {
|
|
249
|
-
const user = userEvent.setup();
|
|
250
|
-
render(<Example pageSize={2} />);
|
|
251
|
-
|
|
252
|
-
expect(screen.getByRole("button", { name: "Go to previous page" })).toBeDisabled();
|
|
253
|
-
expect(screen.getByRole("button", { name: "Go to next page" })).toBeEnabled();
|
|
254
|
-
|
|
255
|
-
await user.click(screen.getByRole("button", { name: "Go to next page" }));
|
|
256
|
-
await waitFor(() => {
|
|
257
|
-
expect(screen.getByRole("button", { name: "Go to next page" })).toBeDisabled();
|
|
258
|
-
});
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
it("changes the page size", async () => {
|
|
262
|
-
const user = userEvent.setup();
|
|
263
|
-
render(<Example pageSize={2} />);
|
|
264
|
-
|
|
265
|
-
await user.click(screen.getByRole("combobox", { name: "Rows per page" }));
|
|
266
|
-
await user.click(await screen.findByRole("option", { name: "10" }));
|
|
267
|
-
|
|
268
|
-
await waitFor(() => {
|
|
269
|
-
expect(rowTexts()).toHaveLength(4);
|
|
270
|
-
});
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
it("accepts a status slot in place of the page position", () => {
|
|
274
|
-
function WithStatus() {
|
|
275
|
-
const table = useDemoTable();
|
|
276
|
-
return <DataTablePagination table={table} status="2 of 4 rows selected" />;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
render(<WithStatus />);
|
|
280
|
-
expect(screen.getByText("2 of 4 rows selected")).toBeInTheDocument();
|
|
281
|
-
expect(screen.queryByText(/Page 1 of/)).not.toBeInTheDocument();
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
it("has no accessibility violations", async () => {
|
|
285
|
-
const { container } = render(<Example pageSize={2} />);
|
|
286
|
-
await expectNoA11yViolations(container);
|
|
287
|
-
});
|
|
288
|
-
});
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import type { Decorator, Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
-
import { useState } from "react";
|
|
3
|
-
import type { DateRange } from "react-day-picker";
|
|
4
|
-
|
|
5
|
-
import { Input } from "@/components/input";
|
|
6
|
-
import { Label } from "@/components/label";
|
|
7
|
-
|
|
8
|
-
import { DatePicker, DateRangePicker } from "./date-picker";
|
|
9
|
-
|
|
10
|
-
/** Named so its type is nameable in declaration output (TS2883). */
|
|
11
|
-
const withFixedWidth: Decorator = (Story) => (
|
|
12
|
-
<div className="w-64">
|
|
13
|
-
<Story />
|
|
14
|
-
</div>
|
|
15
|
-
);
|
|
16
|
-
|
|
17
|
-
const meta = {
|
|
18
|
-
title: "Forms/Date Picker",
|
|
19
|
-
component: DatePicker,
|
|
20
|
-
args: { placeholder: "Pick a date" },
|
|
21
|
-
decorators: [withFixedWidth],
|
|
22
|
-
} satisfies Meta<typeof DatePicker>;
|
|
23
|
-
|
|
24
|
-
export default meta;
|
|
25
|
-
type Story = StoryObj<typeof meta>;
|
|
26
|
-
|
|
27
|
-
export const Default: Story = {
|
|
28
|
-
render: (args) => (
|
|
29
|
-
<div className="grid gap-2">
|
|
30
|
-
<Label>Start date</Label>
|
|
31
|
-
<DatePicker {...args} />
|
|
32
|
-
</div>
|
|
33
|
-
),
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export const WithValue: Story = {
|
|
37
|
-
args: { defaultValue: new Date(2026, 0, 15) },
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
export const Disabled: Story = {
|
|
41
|
-
args: { disabled: true, defaultValue: new Date(2026, 0, 15) },
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
/** The trigger label follows the locale, and so does its accessible name. */
|
|
45
|
-
export const Locales: Story = {
|
|
46
|
-
parameters: { controls: { disable: true } },
|
|
47
|
-
render: () => (
|
|
48
|
-
<div className="grid gap-3">
|
|
49
|
-
{["en-US", "en-GB", "de-DE", "ja-JP"].map((locale) => (
|
|
50
|
-
<div key={locale} className="grid gap-1">
|
|
51
|
-
<span className="text-xs text-muted-foreground">{locale}</span>
|
|
52
|
-
<DatePicker defaultValue={new Date(2026, 0, 15)} locale={locale} />
|
|
53
|
-
</div>
|
|
54
|
-
))}
|
|
55
|
-
</div>
|
|
56
|
-
),
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
export const Range: Story = {
|
|
60
|
-
parameters: { controls: { disable: true } },
|
|
61
|
-
render: function Range() {
|
|
62
|
-
const [range, setRange] = useState<DateRange | undefined>({
|
|
63
|
-
from: new Date(2026, 0, 10),
|
|
64
|
-
to: new Date(2026, 0, 16),
|
|
65
|
-
});
|
|
66
|
-
return (
|
|
67
|
-
<div className="grid w-80 gap-2">
|
|
68
|
-
<Label>Reporting period</Label>
|
|
69
|
-
<DateRangePicker value={range} onValueChange={setRange} />
|
|
70
|
-
</div>
|
|
71
|
-
);
|
|
72
|
-
},
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* A calendar is for browsing. For a date the user already knows, typing is
|
|
77
|
-
* faster — offer both rather than forcing the picker.
|
|
78
|
-
*/
|
|
79
|
-
export const PairedWithTypedInput: Story = {
|
|
80
|
-
parameters: { controls: { disable: true } },
|
|
81
|
-
render: function Paired() {
|
|
82
|
-
const [date, setDate] = useState<Date | undefined>(new Date(2026, 0, 15));
|
|
83
|
-
const isoValue = date ? date.toISOString().slice(0, 10) : "";
|
|
84
|
-
|
|
85
|
-
return (
|
|
86
|
-
<div className="grid gap-2">
|
|
87
|
-
<Label htmlFor="dob">Date of birth</Label>
|
|
88
|
-
<div className="flex gap-2">
|
|
89
|
-
<Input
|
|
90
|
-
id="dob"
|
|
91
|
-
type="date"
|
|
92
|
-
value={isoValue}
|
|
93
|
-
onChange={(event) => {
|
|
94
|
-
const next = event.target.valueAsDate;
|
|
95
|
-
setDate(next ?? undefined);
|
|
96
|
-
}}
|
|
97
|
-
/>
|
|
98
|
-
<DatePicker value={date} onValueChange={setDate} className="w-auto" />
|
|
99
|
-
</div>
|
|
100
|
-
</div>
|
|
101
|
-
);
|
|
102
|
-
},
|
|
103
|
-
};
|
|
@@ -1,205 +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 { DatePicker, DateRangePicker } from "./date-picker";
|
|
9
|
-
|
|
10
|
-
const JANUARY_2026 = new Date(2026, 0, 15);
|
|
11
|
-
|
|
12
|
-
function dayButton(iso: string): HTMLElement {
|
|
13
|
-
const cell = document.querySelector<HTMLElement>(`[data-day="${iso}"]`);
|
|
14
|
-
if (!cell) throw new Error(`No day cell for ${iso}`);
|
|
15
|
-
return within(cell).getByRole("button");
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
describe("DatePicker", () => {
|
|
19
|
-
it("shows the placeholder until a date is picked", () => {
|
|
20
|
-
render(<DatePicker />);
|
|
21
|
-
expect(screen.getByRole("button")).toHaveTextContent("Pick a date");
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("formats the selected date for the locale", () => {
|
|
25
|
-
render(<DatePicker value={JANUARY_2026} locale="en-GB" />);
|
|
26
|
-
expect(screen.getByRole("button")).toHaveTextContent("15 Jan 2026");
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("formats differently for a different locale", () => {
|
|
30
|
-
render(<DatePicker value={JANUARY_2026} locale="en-US" />);
|
|
31
|
-
expect(screen.getByRole("button")).toHaveTextContent("Jan 15, 2026");
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("opens the calendar from its trigger", async () => {
|
|
35
|
-
const user = userEvent.setup();
|
|
36
|
-
render(<DatePicker />);
|
|
37
|
-
|
|
38
|
-
expect(screen.queryByRole("grid")).not.toBeInTheDocument();
|
|
39
|
-
await user.click(screen.getByRole("button"));
|
|
40
|
-
expect(await screen.findByRole("grid")).toBeInTheDocument();
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("names the popover, which carries a dialog role", async () => {
|
|
44
|
-
const user = userEvent.setup();
|
|
45
|
-
render(<DatePicker label="Choose a start date" />);
|
|
46
|
-
|
|
47
|
-
await user.click(screen.getByRole("button"));
|
|
48
|
-
expect(
|
|
49
|
-
await screen.findByRole("dialog", { name: "Choose a start date" }),
|
|
50
|
-
).toBeInTheDocument();
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("reports and displays the picked date, then closes", async () => {
|
|
54
|
-
const onValueChange = vi.fn();
|
|
55
|
-
const user = userEvent.setup();
|
|
56
|
-
render(
|
|
57
|
-
<DatePicker defaultValue={JANUARY_2026} onValueChange={onValueChange} locale="en-GB" />,
|
|
58
|
-
);
|
|
59
|
-
|
|
60
|
-
await user.click(screen.getByRole("button"));
|
|
61
|
-
await screen.findByRole("grid");
|
|
62
|
-
await user.click(dayButton("2026-01-20"));
|
|
63
|
-
|
|
64
|
-
expect(onValueChange).toHaveBeenCalled();
|
|
65
|
-
await waitFor(() => {
|
|
66
|
-
expect(screen.queryByRole("grid")).not.toBeInTheDocument();
|
|
67
|
-
});
|
|
68
|
-
expect(screen.getByRole("button")).toHaveTextContent("20 Jan 2026");
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("returns focus to the trigger after picking", async () => {
|
|
72
|
-
const user = userEvent.setup();
|
|
73
|
-
render(<DatePicker defaultValue={JANUARY_2026} />);
|
|
74
|
-
|
|
75
|
-
const trigger = screen.getByRole("button");
|
|
76
|
-
await user.click(trigger);
|
|
77
|
-
await screen.findByRole("grid");
|
|
78
|
-
await user.click(dayButton("2026-01-20"));
|
|
79
|
-
|
|
80
|
-
await waitFor(() => {
|
|
81
|
-
expect(trigger).toHaveFocus();
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("works controlled", async () => {
|
|
86
|
-
const user = userEvent.setup();
|
|
87
|
-
|
|
88
|
-
function Controlled() {
|
|
89
|
-
const [date, setDate] = useState<Date | undefined>(JANUARY_2026);
|
|
90
|
-
return <DatePicker value={date} onValueChange={setDate} locale="en-GB" />;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
render(<Controlled />);
|
|
94
|
-
expect(screen.getByRole("button")).toHaveTextContent("15 Jan 2026");
|
|
95
|
-
|
|
96
|
-
await user.click(screen.getByRole("button"));
|
|
97
|
-
await screen.findByRole("grid");
|
|
98
|
-
await user.click(dayButton("2026-01-22"));
|
|
99
|
-
|
|
100
|
-
await waitFor(() => {
|
|
101
|
-
expect(screen.getByRole("button")).toHaveTextContent("22 Jan 2026");
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("closes on Escape", async () => {
|
|
106
|
-
const user = userEvent.setup();
|
|
107
|
-
render(<DatePicker />);
|
|
108
|
-
|
|
109
|
-
await user.click(screen.getByRole("button"));
|
|
110
|
-
await screen.findByRole("grid");
|
|
111
|
-
|
|
112
|
-
await user.keyboard("{Escape}");
|
|
113
|
-
await waitFor(() => {
|
|
114
|
-
expect(screen.queryByRole("grid")).not.toBeInTheDocument();
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it("does not open while disabled", async () => {
|
|
119
|
-
const user = userEvent.setup();
|
|
120
|
-
render(<DatePicker disabled />);
|
|
121
|
-
|
|
122
|
-
const trigger = screen.getByRole("button");
|
|
123
|
-
expect(trigger).toBeDisabled();
|
|
124
|
-
await user.click(trigger);
|
|
125
|
-
expect(screen.queryByRole("grid")).not.toBeInTheDocument();
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it("has no accessibility violations while open", async () => {
|
|
129
|
-
const user = userEvent.setup();
|
|
130
|
-
const { baseElement } = render(<DatePicker defaultValue={JANUARY_2026} />);
|
|
131
|
-
|
|
132
|
-
await user.click(screen.getByRole("button"));
|
|
133
|
-
await screen.findByRole("grid");
|
|
134
|
-
|
|
135
|
-
await expectNoA11yViolations(baseElement);
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
describe("DateRangePicker", () => {
|
|
140
|
-
it("shows the placeholder until a range is picked", () => {
|
|
141
|
-
render(<DateRangePicker />);
|
|
142
|
-
expect(screen.getByRole("button")).toHaveTextContent("Pick a date range");
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it("shows both ends of the range", () => {
|
|
146
|
-
render(
|
|
147
|
-
<DateRangePicker
|
|
148
|
-
value={{ from: new Date(2026, 0, 10), to: new Date(2026, 0, 14) }}
|
|
149
|
-
locale="en-GB"
|
|
150
|
-
/>,
|
|
151
|
-
);
|
|
152
|
-
expect(screen.getByRole("button")).toHaveTextContent("10 Jan 2026 – 14 Jan 2026");
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it("shows two months so most ranges need no paging", async () => {
|
|
156
|
-
const user = userEvent.setup();
|
|
157
|
-
render(<DateRangePicker value={{ from: JANUARY_2026, to: undefined }} />);
|
|
158
|
-
|
|
159
|
-
await user.click(screen.getByRole("button"));
|
|
160
|
-
await screen.findByRole("dialog");
|
|
161
|
-
expect(screen.getByText(/january 2026/i)).toBeInTheDocument();
|
|
162
|
-
expect(screen.getByText(/february 2026/i)).toBeInTheDocument();
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it("stays open after the first click, since one click is half an answer", async () => {
|
|
166
|
-
const user = userEvent.setup();
|
|
167
|
-
|
|
168
|
-
function Controlled() {
|
|
169
|
-
const [range, setRange] = useState<DateRange | undefined>();
|
|
170
|
-
return <DateRangePicker value={range} onValueChange={setRange} numberOfMonths={1} />;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
render(<Controlled />);
|
|
174
|
-
await user.click(screen.getByRole("button", { name: /pick a date range/i }));
|
|
175
|
-
await screen.findByRole("grid");
|
|
176
|
-
|
|
177
|
-
// An empty range picker opens on the current month, so the days are taken
|
|
178
|
-
// from what is actually rendered rather than from a hard-coded date.
|
|
179
|
-
const days = Array.from(
|
|
180
|
-
document.querySelectorAll<HTMLElement>(
|
|
181
|
-
"[data-day]:not([data-outside]):not([data-hidden])",
|
|
182
|
-
),
|
|
183
|
-
);
|
|
184
|
-
const first = within(days[9]!).getByRole("button");
|
|
185
|
-
const second = within(days[13]!).getByRole("button");
|
|
186
|
-
|
|
187
|
-
await user.click(first);
|
|
188
|
-
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
|
189
|
-
|
|
190
|
-
await user.click(second);
|
|
191
|
-
await waitFor(() => {
|
|
192
|
-
expect(screen.getByRole("button", { name: /–/ })).toBeInTheDocument();
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it("has no accessibility violations while open", async () => {
|
|
197
|
-
const user = userEvent.setup();
|
|
198
|
-
const { baseElement } = render(<DateRangePicker />);
|
|
199
|
-
|
|
200
|
-
await user.click(screen.getByRole("button"));
|
|
201
|
-
await screen.findByRole("dialog");
|
|
202
|
-
|
|
203
|
-
await expectNoA11yViolations(baseElement);
|
|
204
|
-
});
|
|
205
|
-
});
|