@zentauri-ui/zentauri-components 2.1.5 → 2.1.7
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/README.md +12 -8
- package/cli/cli.integration.test.ts +36 -0
- package/cli/index.mjs +91 -12
- package/cli/index.test.ts +180 -0
- package/cli/props.json +609 -14
- package/cli/registry.json +22 -0
- package/cli/rewrite-imports.mjs +29 -4
- package/cli/rewrite-imports.test.ts +35 -0
- package/dist/{chunk-RENXBUZY.js → chunk-5ELR6MIN.js} +6 -6
- package/dist/{chunk-RENXBUZY.js.map → chunk-5ELR6MIN.js.map} +1 -1
- package/dist/chunk-5FU57ZVQ.js +19 -0
- package/dist/{chunk-D2GISTDL.js.map → chunk-5FU57ZVQ.js.map} +1 -1
- package/dist/chunk-74SKXGTM.js +4 -0
- package/dist/chunk-74SKXGTM.js.map +1 -0
- package/dist/{chunk-WBZKMSXW.mjs → chunk-7UXPXCKV.mjs} +3 -3
- package/dist/{chunk-WBZKMSXW.mjs.map → chunk-7UXPXCKV.mjs.map} +1 -1
- package/dist/chunk-COCPCZMR.mjs +77 -0
- package/dist/chunk-COCPCZMR.mjs.map +1 -0
- package/dist/chunk-CYKSS5S5.mjs +128 -0
- package/dist/chunk-CYKSS5S5.mjs.map +1 -0
- package/dist/chunk-DBNGLT5U.mjs +221 -0
- package/dist/chunk-DBNGLT5U.mjs.map +1 -0
- package/dist/{chunk-BL6UVCV7.mjs → chunk-FUCW5GPE.mjs} +36 -11
- package/dist/chunk-FUCW5GPE.mjs.map +1 -0
- package/dist/chunk-G7FVHZRB.js +225 -0
- package/dist/chunk-G7FVHZRB.js.map +1 -0
- package/dist/chunk-HMDH4BQJ.js +123 -0
- package/dist/chunk-HMDH4BQJ.js.map +1 -0
- package/dist/chunk-I7EBE7BD.js +98 -0
- package/dist/chunk-I7EBE7BD.js.map +1 -0
- package/dist/{chunk-PAG5CTLN.mjs → chunk-KVSRUAXP.mjs} +3 -3
- package/dist/{chunk-PAG5CTLN.mjs.map → chunk-KVSRUAXP.mjs.map} +1 -1
- package/dist/chunk-LHBJD57K.mjs +143 -0
- package/dist/chunk-LHBJD57K.mjs.map +1 -0
- package/dist/chunk-OYAJG2BO.js +83 -0
- package/dist/chunk-OYAJG2BO.js.map +1 -0
- package/dist/chunk-PG7LQVU6.js +86 -0
- package/dist/chunk-PG7LQVU6.js.map +1 -0
- package/dist/chunk-PTU5ZAYX.js +145 -0
- package/dist/chunk-PTU5ZAYX.js.map +1 -0
- package/dist/chunk-QKO5DA4N.mjs +81 -0
- package/dist/chunk-QKO5DA4N.mjs.map +1 -0
- package/dist/chunk-T7PIKDUZ.js +130 -0
- package/dist/chunk-T7PIKDUZ.js.map +1 -0
- package/dist/chunk-TDK5TVJE.mjs +3 -0
- package/dist/chunk-TDK5TVJE.mjs.map +1 -0
- package/dist/{chunk-NZSZE36T.js → chunk-TJ2EWPER.js} +42 -10
- package/dist/chunk-TJ2EWPER.js.map +1 -0
- package/dist/chunk-VBNW2B4D.mjs +3 -0
- package/dist/chunk-VBNW2B4D.mjs.map +1 -0
- package/dist/chunk-W6DO36XD.mjs +96 -0
- package/dist/chunk-W6DO36XD.mjs.map +1 -0
- package/dist/chunk-XR3J46TZ.js +4 -0
- package/dist/chunk-XR3J46TZ.js.map +1 -0
- package/dist/chunk-ZOHCADDL.mjs +121 -0
- package/dist/chunk-ZOHCADDL.mjs.map +1 -0
- package/dist/design-system/audio-player.d.ts +61 -0
- package/dist/design-system/audio-player.d.ts.map +1 -0
- package/dist/design-system/data-table.d.ts +8 -0
- package/dist/design-system/data-table.d.ts.map +1 -0
- package/dist/design-system/facade.js +11 -10
- package/dist/design-system/facade.js.map +1 -1
- package/dist/design-system/facade.mjs +10 -9
- package/dist/design-system/facade.mjs.map +1 -1
- package/dist/design-system/index.d.ts +2 -0
- package/dist/design-system/index.d.ts.map +1 -1
- package/dist/hooks/useTableFilter.js +6 -116
- package/dist/hooks/useTableFilter.js.map +1 -1
- package/dist/hooks/useTableFilter.mjs +1 -118
- package/dist/hooks/useTableFilter.mjs.map +1 -1
- package/dist/hooks/useTableSort.js +6 -91
- package/dist/hooks/useTableSort.js.map +1 -1
- package/dist/hooks/useTableSort.mjs +1 -93
- package/dist/hooks/useTableSort.mjs.map +1 -1
- package/dist/hooks/useVirtualList.js +6 -76
- package/dist/hooks/useVirtualList.js.map +1 -1
- package/dist/hooks/useVirtualList.mjs +1 -78
- package/dist/hooks/useVirtualList.mjs.map +1 -1
- package/dist/ui/audio-player/audio-player-base.d.ts +20 -0
- package/dist/ui/audio-player/audio-player-base.d.ts.map +1 -0
- package/dist/ui/audio-player/audio-player.d.ts +6 -0
- package/dist/ui/audio-player/audio-player.d.ts.map +1 -0
- package/dist/ui/audio-player/index.d.ts +5 -0
- package/dist/ui/audio-player/index.d.ts.map +1 -0
- package/dist/ui/audio-player/types.d.ts +44 -0
- package/dist/ui/audio-player/types.d.ts.map +1 -0
- package/dist/ui/audio-player/variants.d.ts +12 -0
- package/dist/ui/audio-player/variants.d.ts.map +1 -0
- package/dist/ui/audio-player.js +556 -0
- package/dist/ui/audio-player.js.map +1 -0
- package/dist/ui/audio-player.mjs +545 -0
- package/dist/ui/audio-player.mjs.map +1 -0
- package/dist/ui/buttons/animated.js +13 -12
- package/dist/ui/buttons/animated.js.map +1 -1
- package/dist/ui/buttons/animated.mjs +11 -10
- package/dist/ui/buttons/animated.mjs.map +1 -1
- package/dist/ui/buttons.js +15 -13
- package/dist/ui/buttons.mjs +13 -11
- package/dist/ui/checkbox.js +7 -123
- package/dist/ui/checkbox.js.map +1 -1
- package/dist/ui/checkbox.mjs +2 -126
- package/dist/ui/checkbox.mjs.map +1 -1
- package/dist/ui/data-table/data-table-base.d.ts +6 -0
- package/dist/ui/data-table/data-table-base.d.ts.map +1 -0
- package/dist/ui/data-table/data-table.d.ts +6 -0
- package/dist/ui/data-table/data-table.d.ts.map +1 -0
- package/dist/ui/data-table/index.d.ts +4 -0
- package/dist/ui/data-table/index.d.ts.map +1 -0
- package/dist/ui/data-table/types.d.ts +92 -0
- package/dist/ui/data-table/types.d.ts.map +1 -0
- package/dist/ui/data-table/variants.d.ts +8 -0
- package/dist/ui/data-table/variants.d.ts.map +1 -0
- package/dist/ui/data-table.js +620 -0
- package/dist/ui/data-table.js.map +1 -0
- package/dist/ui/data-table.mjs +611 -0
- package/dist/ui/data-table.mjs.map +1 -0
- package/dist/ui/dynamic-stepper.js +23 -22
- package/dist/ui/dynamic-stepper.js.map +1 -1
- package/dist/ui/dynamic-stepper.mjs +12 -11
- package/dist/ui/dynamic-stepper.mjs.map +1 -1
- package/dist/ui/inputs.js +7 -138
- package/dist/ui/inputs.js.map +1 -1
- package/dist/ui/inputs.mjs +2 -141
- package/dist/ui/inputs.mjs.map +1 -1
- package/dist/ui/pagination.js +25 -225
- package/dist/ui/pagination.js.map +1 -1
- package/dist/ui/pagination.mjs +13 -227
- package/dist/ui/pagination.mjs.map +1 -1
- package/dist/ui/table.js +1 -0
- package/dist/ui/table.mjs +1 -0
- package/package.json +1 -1
- package/src/design-system/audio-player.ts +109 -0
- package/src/design-system/data-table.ts +20 -0
- package/src/design-system/index.ts +2 -0
- package/src/ui/audio-player/audio-player-base.tsx +557 -0
- package/src/ui/audio-player/audio-player.test.tsx +485 -0
- package/src/ui/audio-player/audio-player.tsx +8 -0
- package/src/ui/audio-player/index.ts +24 -0
- package/src/ui/audio-player/types.ts +57 -0
- package/src/ui/audio-player/variants.ts +43 -0
- package/src/ui/data-table/data-table-base.tsx +701 -0
- package/src/ui/data-table/data-table.test.tsx +389 -0
- package/src/ui/data-table/data-table.tsx +11 -0
- package/src/ui/data-table/index.ts +24 -0
- package/src/ui/data-table/types.ts +121 -0
- package/src/ui/data-table/variants.ts +21 -0
- package/dist/chunk-BL6UVCV7.mjs.map +0 -1
- package/dist/chunk-D2GISTDL.js +0 -19
- package/dist/chunk-NZSZE36T.js.map +0 -1
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import { act, fireEvent, render, screen, within } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { DataTable } from "./data-table";
|
|
6
|
+
import type { DataTableColumn } from "./types";
|
|
7
|
+
|
|
8
|
+
type Person = {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
email: string;
|
|
12
|
+
role: string;
|
|
13
|
+
status: "Active" | "Invited";
|
|
14
|
+
score: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const people: Person[] = [
|
|
18
|
+
{
|
|
19
|
+
id: "ada",
|
|
20
|
+
name: "Ada Lovelace",
|
|
21
|
+
email: "ada@example.com",
|
|
22
|
+
role: "Engineer",
|
|
23
|
+
status: "Active",
|
|
24
|
+
score: 92,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: "grace",
|
|
28
|
+
name: "Grace Hopper",
|
|
29
|
+
email: "grace@example.com",
|
|
30
|
+
role: "Architect",
|
|
31
|
+
status: "Invited",
|
|
32
|
+
score: 98,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: "katherine",
|
|
36
|
+
name: "Katherine Johnson",
|
|
37
|
+
email: "katherine@example.com",
|
|
38
|
+
role: "Analyst",
|
|
39
|
+
status: "Active",
|
|
40
|
+
score: 95,
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const columns: DataTableColumn<Person>[] = [
|
|
45
|
+
{
|
|
46
|
+
id: "name",
|
|
47
|
+
header: "Name",
|
|
48
|
+
accessor: "name",
|
|
49
|
+
sortable: true,
|
|
50
|
+
filterable: true,
|
|
51
|
+
cell: ({ value }) => <strong>{String(value)}</strong>,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: "email",
|
|
55
|
+
header: "Email",
|
|
56
|
+
accessor: "email",
|
|
57
|
+
filterable: true,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "role",
|
|
61
|
+
header: "Role",
|
|
62
|
+
accessor: "role",
|
|
63
|
+
filterable: true,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "score",
|
|
67
|
+
header: "Score",
|
|
68
|
+
accessor: "score",
|
|
69
|
+
sortable: true,
|
|
70
|
+
textAlign: "right",
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
function renderPeopleTable(
|
|
75
|
+
props: Partial<React.ComponentProps<typeof DataTable<Person>>> = {},
|
|
76
|
+
) {
|
|
77
|
+
return render(
|
|
78
|
+
<DataTable
|
|
79
|
+
aria-label="People"
|
|
80
|
+
columns={columns}
|
|
81
|
+
data={people}
|
|
82
|
+
getRowId={(row) => row.id}
|
|
83
|
+
{...props}
|
|
84
|
+
/>,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getBodyRows() {
|
|
89
|
+
return screen
|
|
90
|
+
.getAllByRole("row")
|
|
91
|
+
.filter((row) => within(row).queryAllByRole("cell").length > 0);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
describe("DataTable", () => {
|
|
95
|
+
it("should expose a stable displayName", () => {
|
|
96
|
+
expect(DataTable.displayName).toBe("DataTable");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("should render typed columns and rows with custom cells", () => {
|
|
100
|
+
renderPeopleTable();
|
|
101
|
+
|
|
102
|
+
expect(screen.getByRole("table", { name: "People" })).toBeVisible();
|
|
103
|
+
expect(screen.getByRole("columnheader", { name: "Name" })).toBeVisible();
|
|
104
|
+
expect(screen.getByRole("columnheader", { name: "Email" })).toBeVisible();
|
|
105
|
+
expect(
|
|
106
|
+
screen.getByRole("rowheader", { name: "Ada Lovelace" }),
|
|
107
|
+
).toContainHTML("strong");
|
|
108
|
+
expect(screen.getByRole("cell", { name: "Engineer" })).toBeVisible();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("should filter rows through the built-in search control", async () => {
|
|
112
|
+
const user = userEvent.setup();
|
|
113
|
+
renderPeopleTable({ search: { placeholder: "Search people" } });
|
|
114
|
+
|
|
115
|
+
await user.type(
|
|
116
|
+
screen.getByRole("searchbox", { name: "Search table" }),
|
|
117
|
+
"grace",
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
expect(
|
|
121
|
+
screen.getByRole("rowheader", { name: "Grace Hopper" }),
|
|
122
|
+
).toBeVisible();
|
|
123
|
+
expect(
|
|
124
|
+
screen.queryByRole("rowheader", { name: "Ada Lovelace" }),
|
|
125
|
+
).toBeNull();
|
|
126
|
+
expect(
|
|
127
|
+
screen.queryByRole("rowheader", { name: "Katherine Johnson" }),
|
|
128
|
+
).toBeNull();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("should warn and fall back to all filterable columns when filterColumnIds is empty", async () => {
|
|
132
|
+
const user = userEvent.setup();
|
|
133
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
134
|
+
|
|
135
|
+
renderPeopleTable({
|
|
136
|
+
search: { filterColumnIds: [], placeholder: "Search people" },
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
await user.type(
|
|
140
|
+
screen.getByRole("searchbox", { name: "Search table" }),
|
|
141
|
+
"grace",
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
expect(warn).toHaveBeenCalledWith(
|
|
145
|
+
"DataTable search.filterColumnIds is empty. It should include at least one column id.",
|
|
146
|
+
);
|
|
147
|
+
expect(
|
|
148
|
+
screen.getByRole("rowheader", { name: "Grace Hopper" }),
|
|
149
|
+
).toBeVisible();
|
|
150
|
+
expect(
|
|
151
|
+
screen.queryByRole("rowheader", { name: "Ada Lovelace" }),
|
|
152
|
+
).toBeNull();
|
|
153
|
+
|
|
154
|
+
warn.mockRestore();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("should sort rows when sortable headers are activated", async () => {
|
|
158
|
+
const user = userEvent.setup();
|
|
159
|
+
renderPeopleTable();
|
|
160
|
+
|
|
161
|
+
await user.click(screen.getByRole("columnheader", { name: "Score" }));
|
|
162
|
+
expect(getBodyRows().map((row) => row.textContent)).toEqual([
|
|
163
|
+
expect.stringContaining("Ada Lovelace"),
|
|
164
|
+
expect.stringContaining("Katherine Johnson"),
|
|
165
|
+
expect.stringContaining("Grace Hopper"),
|
|
166
|
+
]);
|
|
167
|
+
|
|
168
|
+
await user.click(screen.getByRole("columnheader", { name: "Score" }));
|
|
169
|
+
expect(getBodyRows().map((row) => row.textContent)).toEqual([
|
|
170
|
+
expect.stringContaining("Grace Hopper"),
|
|
171
|
+
expect.stringContaining("Katherine Johnson"),
|
|
172
|
+
expect.stringContaining("Ada Lovelace"),
|
|
173
|
+
]);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("should give duplicate rows distinct ids so selection does not collapse", async () => {
|
|
177
|
+
const user = userEvent.setup();
|
|
178
|
+
const handleSelectionChange = vi.fn();
|
|
179
|
+
const dup: Person = {
|
|
180
|
+
id: "dup",
|
|
181
|
+
name: "Dup",
|
|
182
|
+
email: "dup@example.com",
|
|
183
|
+
role: "Engineer",
|
|
184
|
+
status: "Active",
|
|
185
|
+
score: 1,
|
|
186
|
+
};
|
|
187
|
+
render(
|
|
188
|
+
<DataTable
|
|
189
|
+
aria-label="Duplicates"
|
|
190
|
+
columns={columns}
|
|
191
|
+
data={[dup, dup]}
|
|
192
|
+
enableRowSelection
|
|
193
|
+
onRowSelectionChange={handleSelectionChange}
|
|
194
|
+
/>,
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const checkboxes = screen.getAllByRole("checkbox", { name: "Select Dup" });
|
|
198
|
+
expect(checkboxes).toHaveLength(2);
|
|
199
|
+
|
|
200
|
+
const [firstCheckbox, secondCheckbox] = checkboxes;
|
|
201
|
+
|
|
202
|
+
await user.click(firstCheckbox!);
|
|
203
|
+
|
|
204
|
+
expect(firstCheckbox).toBeChecked();
|
|
205
|
+
expect(secondCheckbox).not.toBeChecked();
|
|
206
|
+
expect(handleSelectionChange).toHaveBeenLastCalledWith(["0"], [dup]);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("should support row selection and bulk actions", async () => {
|
|
210
|
+
const user = userEvent.setup();
|
|
211
|
+
const handleArchive = vi.fn();
|
|
212
|
+
renderPeopleTable({
|
|
213
|
+
enableRowSelection: true,
|
|
214
|
+
bulkActions: [{ label: "Archive selected", onSelect: handleArchive }],
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
await user.click(
|
|
218
|
+
screen.getByRole("checkbox", { name: "Select Ada Lovelace" }),
|
|
219
|
+
);
|
|
220
|
+
await user.click(screen.getByRole("button", { name: "Archive selected" }));
|
|
221
|
+
|
|
222
|
+
expect(handleArchive).toHaveBeenCalledWith([
|
|
223
|
+
expect.objectContaining({ id: "ada", name: "Ada Lovelace" }),
|
|
224
|
+
]);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("should keep row selection stable across paginated fallback row ids", async () => {
|
|
228
|
+
const user = userEvent.setup();
|
|
229
|
+
const handleSelectionChange = vi.fn();
|
|
230
|
+
const handleArchive = vi.fn();
|
|
231
|
+
render(
|
|
232
|
+
<DataTable
|
|
233
|
+
aria-label="People"
|
|
234
|
+
columns={columns}
|
|
235
|
+
data={people}
|
|
236
|
+
enableRowSelection
|
|
237
|
+
pagination={{ pageSize: 2 }}
|
|
238
|
+
onRowSelectionChange={handleSelectionChange}
|
|
239
|
+
bulkActions={[{ label: "Archive selected", onSelect: handleArchive }]}
|
|
240
|
+
/>,
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
await user.click(
|
|
244
|
+
screen.getByRole("checkbox", { name: "Select Ada Lovelace" }),
|
|
245
|
+
);
|
|
246
|
+
await user.click(screen.getByRole("button", { name: "Page 2" }));
|
|
247
|
+
|
|
248
|
+
expect(
|
|
249
|
+
screen.getByRole("checkbox", { name: "Select Katherine Johnson" }),
|
|
250
|
+
).not.toBeChecked();
|
|
251
|
+
|
|
252
|
+
await user.click(screen.getByRole("button", { name: "Archive selected" }));
|
|
253
|
+
|
|
254
|
+
expect(handleSelectionChange).toHaveBeenLastCalledWith(
|
|
255
|
+
["0"],
|
|
256
|
+
[expect.objectContaining({ name: "Ada Lovelace" })],
|
|
257
|
+
);
|
|
258
|
+
expect(handleArchive).toHaveBeenCalledWith([
|
|
259
|
+
expect.objectContaining({ name: "Ada Lovelace" }),
|
|
260
|
+
]);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("should let users hide optional columns", async () => {
|
|
264
|
+
const user = userEvent.setup();
|
|
265
|
+
renderPeopleTable({ enableColumnVisibility: true });
|
|
266
|
+
|
|
267
|
+
await user.click(screen.getByRole("button", { name: "Columns" }));
|
|
268
|
+
await user.click(screen.getByRole("checkbox", { name: "Email column" }));
|
|
269
|
+
|
|
270
|
+
expect(screen.queryByRole("columnheader", { name: "Email" })).toBeNull();
|
|
271
|
+
expect(screen.queryByRole("cell", { name: "ada@example.com" })).toBeNull();
|
|
272
|
+
expect(screen.getByRole("columnheader", { name: "Name" })).toBeVisible();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("should paginate processed rows", async () => {
|
|
276
|
+
const user = userEvent.setup();
|
|
277
|
+
renderPeopleTable({ pagination: { pageSize: 2 } });
|
|
278
|
+
|
|
279
|
+
expect(
|
|
280
|
+
screen.getByRole("rowheader", { name: "Ada Lovelace" }),
|
|
281
|
+
).toBeVisible();
|
|
282
|
+
expect(
|
|
283
|
+
screen.queryByRole("rowheader", { name: "Katherine Johnson" }),
|
|
284
|
+
).toBeNull();
|
|
285
|
+
|
|
286
|
+
await user.click(screen.getByRole("button", { name: "Page 2" }));
|
|
287
|
+
|
|
288
|
+
expect(
|
|
289
|
+
screen.getByRole("rowheader", { name: "Katherine Johnson" }),
|
|
290
|
+
).toBeVisible();
|
|
291
|
+
expect(
|
|
292
|
+
screen.queryByRole("rowheader", { name: "Ada Lovelace" }),
|
|
293
|
+
).toBeNull();
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("should hide the row count when showRowCount is false", () => {
|
|
297
|
+
renderPeopleTable({ pagination: { pageSize: 2 }, showRowCount: false });
|
|
298
|
+
|
|
299
|
+
expect(screen.queryByText("Showing 2 of 3")).toBeNull();
|
|
300
|
+
expect(screen.getByRole("button", { name: "Page 2" })).toBeVisible();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("should offset virtualized rows while scrolling", () => {
|
|
304
|
+
const descriptor = Object.getOwnPropertyDescriptor(
|
|
305
|
+
HTMLElement.prototype,
|
|
306
|
+
"clientHeight",
|
|
307
|
+
);
|
|
308
|
+
Object.defineProperty(HTMLElement.prototype, "clientHeight", {
|
|
309
|
+
configurable: true,
|
|
310
|
+
get() {
|
|
311
|
+
return this.getAttribute("data-slot") === "data-table-virtual-scroll"
|
|
312
|
+
? 80
|
|
313
|
+
: 0;
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
const rows = Array.from({ length: 20 }, (_, index) => ({
|
|
317
|
+
id: `person-${index}`,
|
|
318
|
+
name: `Person ${index}`,
|
|
319
|
+
email: `person-${index}@example.com`,
|
|
320
|
+
role: "Engineer",
|
|
321
|
+
status: "Active" as const,
|
|
322
|
+
score: index,
|
|
323
|
+
}));
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
render(
|
|
327
|
+
<DataTable
|
|
328
|
+
aria-label="People"
|
|
329
|
+
columns={columns}
|
|
330
|
+
data={rows}
|
|
331
|
+
getRowId={(row) => row.id}
|
|
332
|
+
virtualization={{
|
|
333
|
+
enabled: true,
|
|
334
|
+
rowHeight: 40,
|
|
335
|
+
height: 80,
|
|
336
|
+
overscan: 0,
|
|
337
|
+
}}
|
|
338
|
+
/>,
|
|
339
|
+
);
|
|
340
|
+
const scrollArea = document.querySelector(
|
|
341
|
+
'[data-slot="data-table-virtual-scroll"]',
|
|
342
|
+
);
|
|
343
|
+
expect(scrollArea).not.toBeNull();
|
|
344
|
+
|
|
345
|
+
act(() => {
|
|
346
|
+
if (scrollArea) {
|
|
347
|
+
scrollArea.scrollTop = 200;
|
|
348
|
+
fireEvent.scroll(scrollArea);
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
const offset = document.querySelector(
|
|
353
|
+
'[data-slot="data-table-virtual-offset"]',
|
|
354
|
+
);
|
|
355
|
+
expect(offset).toHaveStyle({ transform: "translateY(200px)" });
|
|
356
|
+
expect(screen.getByRole("rowheader", { name: "Person 5" })).toBeVisible();
|
|
357
|
+
} finally {
|
|
358
|
+
if (descriptor) {
|
|
359
|
+
Object.defineProperty(
|
|
360
|
+
HTMLElement.prototype,
|
|
361
|
+
"clientHeight",
|
|
362
|
+
descriptor,
|
|
363
|
+
);
|
|
364
|
+
} else {
|
|
365
|
+
delete (HTMLElement.prototype as { clientHeight?: number })
|
|
366
|
+
.clientHeight;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it("should render loading and empty states", () => {
|
|
372
|
+
const { rerender } = render(
|
|
373
|
+
<DataTable
|
|
374
|
+
columns={columns}
|
|
375
|
+
data={[]}
|
|
376
|
+
loading
|
|
377
|
+
loadingContent="Loading people"
|
|
378
|
+
/>,
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
expect(screen.getByText("Loading people")).toBeVisible();
|
|
382
|
+
|
|
383
|
+
rerender(
|
|
384
|
+
<DataTable columns={columns} data={[]} emptyContent="No people found" />,
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
expect(screen.getByText("No people found")).toBeVisible();
|
|
388
|
+
});
|
|
389
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// data-table.tsx — default static entry (no framer-motion)
|
|
2
|
+
import { DataTableBase } from "./data-table-base";
|
|
3
|
+
import type { DataTableProps } from "./types";
|
|
4
|
+
|
|
5
|
+
export function DataTable<TData, TKey extends string = string>(
|
|
6
|
+
props: DataTableProps<TData, TKey>,
|
|
7
|
+
) {
|
|
8
|
+
return <DataTableBase {...props} />;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
DataTable.displayName = "DataTable";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
export { DataTable } from "./data-table";
|
|
4
|
+
export type {
|
|
5
|
+
DataTableBulkAction,
|
|
6
|
+
DataTableCellContext,
|
|
7
|
+
DataTableColumn,
|
|
8
|
+
DataTableColumnTextAlign,
|
|
9
|
+
DataTableColumnValue,
|
|
10
|
+
DataTableHeaderContext,
|
|
11
|
+
DataTablePaginationOptions,
|
|
12
|
+
DataTableProps,
|
|
13
|
+
DataTableSearchOptions,
|
|
14
|
+
DataTableVirtualizationOptions,
|
|
15
|
+
} from "./types";
|
|
16
|
+
export {
|
|
17
|
+
dataTableColumnPanelVariants,
|
|
18
|
+
dataTableRootVariants,
|
|
19
|
+
dataTableStateCellVariants,
|
|
20
|
+
dataTableStatusVariants,
|
|
21
|
+
dataTableToolbarGroupVariants,
|
|
22
|
+
dataTableToolbarVariants,
|
|
23
|
+
dataTableVirtualScrollVariants,
|
|
24
|
+
} from "./variants";
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ComponentPropsWithRef,
|
|
3
|
+
ReactNode,
|
|
4
|
+
Ref,
|
|
5
|
+
TdHTMLAttributes,
|
|
6
|
+
ThHTMLAttributes,
|
|
7
|
+
} from "react";
|
|
8
|
+
|
|
9
|
+
import type { TableProps, TableSortDirection } from "../table";
|
|
10
|
+
|
|
11
|
+
export type DataTableColumnValue =
|
|
12
|
+
| string
|
|
13
|
+
| number
|
|
14
|
+
| boolean
|
|
15
|
+
| Date
|
|
16
|
+
| null
|
|
17
|
+
| undefined;
|
|
18
|
+
|
|
19
|
+
export type DataTableColumnTextAlign = NonNullable<TableProps["textAlign"]>;
|
|
20
|
+
|
|
21
|
+
export type DataTableCellContext<TData> = {
|
|
22
|
+
row: TData;
|
|
23
|
+
value: unknown;
|
|
24
|
+
column: DataTableColumn<TData>;
|
|
25
|
+
rowIndex: number;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type DataTableHeaderContext<TData> = {
|
|
29
|
+
column: DataTableColumn<TData>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type DataTableColumn<TData, TKey extends string = string> = {
|
|
33
|
+
id: TKey;
|
|
34
|
+
header: ReactNode | ((context: DataTableHeaderContext<TData>) => ReactNode);
|
|
35
|
+
accessor?: keyof TData | ((row: TData) => unknown);
|
|
36
|
+
cell?: (context: DataTableCellContext<TData>) => ReactNode;
|
|
37
|
+
sortable?: boolean;
|
|
38
|
+
sortValue?: (row: TData) => DataTableColumnValue;
|
|
39
|
+
filterable?: boolean;
|
|
40
|
+
filterValue?: (row: TData) => DataTableColumnValue;
|
|
41
|
+
visible?: boolean;
|
|
42
|
+
enableHiding?: boolean;
|
|
43
|
+
textAlign?: DataTableColumnTextAlign;
|
|
44
|
+
className?: string;
|
|
45
|
+
headerClassName?: string;
|
|
46
|
+
cellClassName?: string;
|
|
47
|
+
headerProps?: ThHTMLAttributes<HTMLTableCellElement>;
|
|
48
|
+
cellProps?: TdHTMLAttributes<HTMLTableCellElement>;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type DataTableSearchOptions<TKey extends string = string> = {
|
|
52
|
+
value?: string;
|
|
53
|
+
defaultValue?: string;
|
|
54
|
+
onValueChange?: (value: string) => void;
|
|
55
|
+
placeholder?: string;
|
|
56
|
+
label?: string;
|
|
57
|
+
filterColumnIds?: readonly TKey[];
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export type DataTablePaginationOptions = {
|
|
61
|
+
pageSize?: number;
|
|
62
|
+
page?: number;
|
|
63
|
+
defaultPage?: number;
|
|
64
|
+
onPageChange?: (page: number) => void;
|
|
65
|
+
siblingCount?: number;
|
|
66
|
+
boundaryCount?: number;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export type DataTableBulkAction<TData> = {
|
|
70
|
+
label: ReactNode;
|
|
71
|
+
onSelect: (selectedRows: TData[]) => void;
|
|
72
|
+
disabled?: boolean;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export type DataTableVirtualizationOptions = {
|
|
76
|
+
enabled?: boolean;
|
|
77
|
+
rowHeight: number;
|
|
78
|
+
height: number;
|
|
79
|
+
overscan?: number;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export type DataTableProps<TData, TKey extends string = string> = Omit<
|
|
83
|
+
ComponentPropsWithRef<"section">,
|
|
84
|
+
"children" | "onChange"
|
|
85
|
+
> &
|
|
86
|
+
Pick<TableProps, "appearance" | "size" | "stickyHeader" | "textAlign"> & {
|
|
87
|
+
data: readonly TData[];
|
|
88
|
+
columns: readonly DataTableColumn<TData, TKey>[];
|
|
89
|
+
getRowId?: (row: TData, index: number) => string;
|
|
90
|
+
caption?: ReactNode;
|
|
91
|
+
tableClassName?: string;
|
|
92
|
+
tableScrollAreaAriaLabel?: string;
|
|
93
|
+
search?: boolean | DataTableSearchOptions<TKey>;
|
|
94
|
+
sortKey?: TKey;
|
|
95
|
+
defaultSortKey?: TKey;
|
|
96
|
+
sortDirection?: TableSortDirection;
|
|
97
|
+
defaultSortDirection?: TableSortDirection;
|
|
98
|
+
onSortChange?: (sort: {
|
|
99
|
+
sortKey?: TKey;
|
|
100
|
+
sortDirection: TableSortDirection;
|
|
101
|
+
}) => void;
|
|
102
|
+
enableRowSelection?: boolean;
|
|
103
|
+
selectedRowIds?: readonly string[];
|
|
104
|
+
defaultSelectedRowIds?: readonly string[];
|
|
105
|
+
onRowSelectionChange?: (
|
|
106
|
+
selectedRowIds: string[],
|
|
107
|
+
selectedRows: TData[],
|
|
108
|
+
) => void;
|
|
109
|
+
enableColumnVisibility?: boolean;
|
|
110
|
+
visibleColumnIds?: readonly TKey[];
|
|
111
|
+
defaultVisibleColumnIds?: readonly TKey[];
|
|
112
|
+
onColumnVisibilityChange?: (visibleColumnIds: TKey[]) => void;
|
|
113
|
+
bulkActions?: readonly DataTableBulkAction<TData>[];
|
|
114
|
+
pagination?: boolean | DataTablePaginationOptions;
|
|
115
|
+
virtualization?: DataTableVirtualizationOptions;
|
|
116
|
+
showRowCount?: boolean;
|
|
117
|
+
loading?: boolean;
|
|
118
|
+
loadingContent?: ReactNode;
|
|
119
|
+
emptyContent?: ReactNode;
|
|
120
|
+
ref?: Ref<HTMLElement>;
|
|
121
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { cva } from "class-variance-authority";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
zuiDataTableColumnPanelBase,
|
|
5
|
+
zuiDataTableRootBase,
|
|
6
|
+
zuiDataTableStateCellBase,
|
|
7
|
+
zuiDataTableStatusBase,
|
|
8
|
+
zuiDataTableToolbarBase,
|
|
9
|
+
zuiDataTableToolbarGroupBase,
|
|
10
|
+
zuiDataTableVirtualScrollBase,
|
|
11
|
+
} from "../../design-system/data-table";
|
|
12
|
+
|
|
13
|
+
export const dataTableRootVariants = cva(zuiDataTableRootBase);
|
|
14
|
+
export const dataTableToolbarVariants = cva(zuiDataTableToolbarBase);
|
|
15
|
+
export const dataTableToolbarGroupVariants = cva(zuiDataTableToolbarGroupBase);
|
|
16
|
+
export const dataTableColumnPanelVariants = cva(zuiDataTableColumnPanelBase);
|
|
17
|
+
export const dataTableStatusVariants = cva(zuiDataTableStatusBase);
|
|
18
|
+
export const dataTableStateCellVariants = cva(zuiDataTableStateCellBase);
|
|
19
|
+
export const dataTableVirtualScrollVariants = cva(
|
|
20
|
+
zuiDataTableVirtualScrollBase,
|
|
21
|
+
);
|