@rovula/ui 0.1.28 → 0.1.29
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/cjs/bundle.css +501 -67
- package/dist/cjs/bundle.js +589 -589
- package/dist/cjs/bundle.js.map +1 -1
- package/dist/cjs/types/components/DataTable/DataTable.d.ts +195 -4
- package/dist/cjs/types/components/DataTable/DataTable.editing.d.ts +20 -0
- package/dist/cjs/types/components/DataTable/DataTable.editing.types.d.ts +145 -0
- package/dist/cjs/types/components/DataTable/DataTable.stories.d.ts +268 -6
- package/dist/cjs/types/components/Dropdown/Dropdown.d.ts +22 -0
- package/dist/cjs/types/components/Dropdown/Dropdown.stories.d.ts +4 -0
- package/dist/cjs/types/components/ScrollArea/ScrollArea.d.ts +3 -3
- package/dist/cjs/types/components/ScrollArea/ScrollArea.stories.d.ts +4 -0
- package/dist/cjs/types/components/Table/Table.d.ts +33 -3
- package/dist/cjs/types/components/Table/Table.stories.d.ts +86 -4
- package/dist/cjs/types/components/TextInput/TextInput.stories.d.ts +8 -0
- package/dist/cjs/types/components/TextInput/TextInput.styles.d.ts +1 -0
- package/dist/components/DataTable/DataTable.editing.js +385 -0
- package/dist/components/DataTable/DataTable.editing.types.js +1 -0
- package/dist/components/DataTable/DataTable.js +983 -50
- package/dist/components/DataTable/DataTable.stories.js +1077 -25
- package/dist/components/Dropdown/Dropdown.js +8 -6
- package/dist/components/ScrollArea/ScrollArea.js +2 -2
- package/dist/components/ScrollArea/ScrollArea.stories.js +68 -2
- package/dist/components/Table/Table.js +103 -13
- package/dist/components/Table/Table.stories.js +226 -9
- package/dist/components/TextInput/TextInput.js +6 -4
- package/dist/components/TextInput/TextInput.stories.js +8 -0
- package/dist/components/TextInput/TextInput.styles.js +7 -1
- package/dist/esm/bundle.css +501 -67
- package/dist/esm/bundle.js +1545 -1545
- package/dist/esm/bundle.js.map +1 -1
- package/dist/esm/types/components/DataTable/DataTable.d.ts +195 -4
- package/dist/esm/types/components/DataTable/DataTable.editing.d.ts +20 -0
- package/dist/esm/types/components/DataTable/DataTable.editing.types.d.ts +145 -0
- package/dist/esm/types/components/DataTable/DataTable.stories.d.ts +268 -6
- package/dist/esm/types/components/Dropdown/Dropdown.d.ts +22 -0
- package/dist/esm/types/components/Dropdown/Dropdown.stories.d.ts +4 -0
- package/dist/esm/types/components/ScrollArea/ScrollArea.d.ts +3 -3
- package/dist/esm/types/components/ScrollArea/ScrollArea.stories.d.ts +4 -0
- package/dist/esm/types/components/Table/Table.d.ts +33 -3
- package/dist/esm/types/components/Table/Table.stories.d.ts +86 -4
- package/dist/esm/types/components/TextInput/TextInput.stories.d.ts +8 -0
- package/dist/esm/types/components/TextInput/TextInput.styles.d.ts +1 -0
- package/dist/index.d.ts +493 -122
- package/dist/src/theme/global.css +747 -96
- package/package.json +14 -2
- package/src/components/DataTable/DataTable.editing.tsx +861 -0
- package/src/components/DataTable/DataTable.editing.types.ts +192 -0
- package/src/components/DataTable/DataTable.stories.tsx +2169 -31
- package/src/components/DataTable/DataTable.test.tsx +696 -0
- package/src/components/DataTable/DataTable.tsx +2260 -94
- package/src/components/Dropdown/Dropdown.tsx +22 -6
- package/src/components/ScrollArea/ScrollArea.stories.tsx +146 -3
- package/src/components/ScrollArea/ScrollArea.tsx +6 -6
- package/src/components/Table/Table.stories.tsx +789 -44
- package/src/components/Table/Table.tsx +294 -28
- package/src/components/TextInput/TextInput.stories.tsx +80 -0
- package/src/components/TextInput/TextInput.styles.ts +7 -1
- package/src/components/TextInput/TextInput.tsx +21 -14
- package/src/test/setup.ts +50 -0
- package/src/theme/global.css +81 -42
- package/src/theme/presets/colors.js +12 -0
- package/src/theme/themes/variable.css +27 -28
- package/src/theme/tokens/baseline.css +2 -1
- package/src/theme/tokens/components/scrollbar.css +9 -4
- package/src/theme/tokens/components/table.css +63 -0
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { ColumnDef } from "@tanstack/react-table";
|
|
3
|
+
import {
|
|
4
|
+
act,
|
|
5
|
+
render,
|
|
6
|
+
screen,
|
|
7
|
+
waitFor,
|
|
8
|
+
fireEvent,
|
|
9
|
+
} from "@testing-library/react";
|
|
10
|
+
import userEvent from "@testing-library/user-event";
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
12
|
+
|
|
13
|
+
import { DataTable } from "./DataTable";
|
|
14
|
+
|
|
15
|
+
type TestRow = { id: string; name: string };
|
|
16
|
+
|
|
17
|
+
const baseColumns: ColumnDef<TestRow>[] = [
|
|
18
|
+
{ accessorKey: "name", header: "Name" },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
function tableWrap(node: ReactNode) {
|
|
22
|
+
return <div style={{ height: 420, minHeight: 420 }}>{node}</div>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getScrollHost(container: HTMLElement): HTMLElement {
|
|
26
|
+
const el = container.querySelector(".min-h-0.overflow-auto");
|
|
27
|
+
if (!el || !(el instanceof HTMLElement)) {
|
|
28
|
+
throw new Error("scroll host not found");
|
|
29
|
+
}
|
|
30
|
+
return el;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("DataTable", () => {
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
vi.restoreAllMocks();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("renders headers and row cells", () => {
|
|
39
|
+
const data: TestRow[] = [
|
|
40
|
+
{ id: "a", name: "Alpha" },
|
|
41
|
+
{ id: "b", name: "Bravo" },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
render(
|
|
45
|
+
tableWrap(
|
|
46
|
+
<DataTable
|
|
47
|
+
columns={baseColumns}
|
|
48
|
+
data={data}
|
|
49
|
+
paginationMode="client"
|
|
50
|
+
/>,
|
|
51
|
+
),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
expect(screen.getByText("Name")).toBeInTheDocument();
|
|
55
|
+
expect(screen.getByText("Alpha")).toBeInTheDocument();
|
|
56
|
+
expect(screen.getByText("Bravo")).toBeInTheDocument();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("marks highlighted row with data-highlighted", () => {
|
|
60
|
+
const data: TestRow[] = [
|
|
61
|
+
{ id: "a", name: "Alpha" },
|
|
62
|
+
{ id: "b", name: "Bravo" },
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
const { container } = render(
|
|
66
|
+
tableWrap(
|
|
67
|
+
<DataTable
|
|
68
|
+
columns={baseColumns}
|
|
69
|
+
data={data}
|
|
70
|
+
paginationMode="client"
|
|
71
|
+
highlightRowId="b"
|
|
72
|
+
/>,
|
|
73
|
+
),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const rowB = container.querySelector('tr[data-row-id="b"]');
|
|
77
|
+
expect(rowB).toHaveAttribute("data-highlighted", "true");
|
|
78
|
+
|
|
79
|
+
const rowA = container.querySelector('tr[data-row-id="a"]');
|
|
80
|
+
expect(rowA).not.toHaveAttribute("data-highlighted");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("scrollToHighlightOnMouseLeave calls scrollIntoView on the highlighted body row", async () => {
|
|
84
|
+
const user = userEvent.setup();
|
|
85
|
+
const scrollIntoView = vi
|
|
86
|
+
.spyOn(HTMLElement.prototype, "scrollIntoView")
|
|
87
|
+
.mockImplementation(() => {});
|
|
88
|
+
|
|
89
|
+
const data: TestRow[] = [{ id: "x", name: "Row" }];
|
|
90
|
+
|
|
91
|
+
const { container } = render(
|
|
92
|
+
tableWrap(
|
|
93
|
+
<DataTable
|
|
94
|
+
columns={baseColumns}
|
|
95
|
+
data={data}
|
|
96
|
+
paginationMode="client"
|
|
97
|
+
highlightRowId="x"
|
|
98
|
+
scrollToHighlightOnMouseLeave
|
|
99
|
+
/>,
|
|
100
|
+
),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const scrollHost = getScrollHost(container);
|
|
104
|
+
await user.pointer({ keys: "[MouseLeft>]", target: scrollHost });
|
|
105
|
+
await user.pointer({ keys: "[/MouseLeft]", target: scrollHost });
|
|
106
|
+
|
|
107
|
+
scrollIntoView.mockClear();
|
|
108
|
+
fireEvent.mouseLeave(scrollHost);
|
|
109
|
+
|
|
110
|
+
await waitFor(() => expect(scrollIntoView).toHaveBeenCalled());
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("virtualized scroll highlight", () => {
|
|
114
|
+
let rectSpy: ReturnType<typeof vi.spyOn>;
|
|
115
|
+
|
|
116
|
+
beforeEach(() => {
|
|
117
|
+
rectSpy = vi
|
|
118
|
+
.spyOn(HTMLElement.prototype, "getBoundingClientRect")
|
|
119
|
+
.mockImplementation(function (this: HTMLElement) {
|
|
120
|
+
const className =
|
|
121
|
+
typeof this.className === "string" ? this.className : "";
|
|
122
|
+
if (
|
|
123
|
+
className.includes("min-h-0") &&
|
|
124
|
+
className.includes("overflow-auto")
|
|
125
|
+
) {
|
|
126
|
+
return {
|
|
127
|
+
width: 640,
|
|
128
|
+
height: 320,
|
|
129
|
+
top: 0,
|
|
130
|
+
left: 0,
|
|
131
|
+
bottom: 320,
|
|
132
|
+
right: 640,
|
|
133
|
+
x: 0,
|
|
134
|
+
y: 0,
|
|
135
|
+
toJSON: () => ({}),
|
|
136
|
+
} as DOMRect;
|
|
137
|
+
}
|
|
138
|
+
if (this.tagName === "THEAD") {
|
|
139
|
+
return {
|
|
140
|
+
width: 640,
|
|
141
|
+
height: 48,
|
|
142
|
+
top: 0,
|
|
143
|
+
left: 0,
|
|
144
|
+
bottom: 48,
|
|
145
|
+
right: 640,
|
|
146
|
+
x: 0,
|
|
147
|
+
y: 0,
|
|
148
|
+
toJSON: () => ({}),
|
|
149
|
+
} as DOMRect;
|
|
150
|
+
}
|
|
151
|
+
// Body rows: mock must reflect real row height; 0 breaks the virtualizer (it thinks the whole list fits).
|
|
152
|
+
if (this.tagName === "TR" && this.closest("tbody")) {
|
|
153
|
+
return {
|
|
154
|
+
width: 640,
|
|
155
|
+
height: 42,
|
|
156
|
+
top: 0,
|
|
157
|
+
left: 0,
|
|
158
|
+
bottom: 42,
|
|
159
|
+
right: 640,
|
|
160
|
+
x: 0,
|
|
161
|
+
y: 0,
|
|
162
|
+
toJSON: () => ({}),
|
|
163
|
+
} as DOMRect;
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
width: 0,
|
|
167
|
+
height: 0,
|
|
168
|
+
top: 0,
|
|
169
|
+
left: 0,
|
|
170
|
+
bottom: 0,
|
|
171
|
+
right: 0,
|
|
172
|
+
x: 0,
|
|
173
|
+
y: 0,
|
|
174
|
+
toJSON: () => ({}),
|
|
175
|
+
} as DOMRect;
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
afterEach(() => {
|
|
180
|
+
rectSpy.mockRestore();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("virtualizes: distant body rows are not mounted near scroll top", async () => {
|
|
184
|
+
const n = 400;
|
|
185
|
+
const data: TestRow[] = Array.from({ length: n }, (_, i) => ({
|
|
186
|
+
id: `r-${i}`,
|
|
187
|
+
name: `Row ${i}`,
|
|
188
|
+
}));
|
|
189
|
+
|
|
190
|
+
const { container } = render(
|
|
191
|
+
tableWrap(
|
|
192
|
+
<DataTable
|
|
193
|
+
columns={baseColumns}
|
|
194
|
+
data={data}
|
|
195
|
+
paginationMode="infinite"
|
|
196
|
+
virtualized
|
|
197
|
+
virtualRowEstimate={40}
|
|
198
|
+
/>,
|
|
199
|
+
),
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const scrollHost = getScrollHost(container);
|
|
203
|
+
scrollHost.scrollTo({ top: 0 });
|
|
204
|
+
fireEvent.scroll(scrollHost);
|
|
205
|
+
|
|
206
|
+
await waitFor(() => {
|
|
207
|
+
expect(container.querySelector('[data-row-id="r-0"]')).toBeTruthy();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const mounted = container.querySelectorAll("tbody tr[data-row-id]").length;
|
|
211
|
+
expect(mounted).toBeGreaterThan(0);
|
|
212
|
+
expect(container.querySelector('[data-row-id="r-350"]')).toBeNull();
|
|
213
|
+
expect(mounted).toBeLessThan(350);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("with scrollToHighlightOnMouseLeave, wheel pauses following highlight; mouse leave resumes", async () => {
|
|
217
|
+
const data: TestRow[] = Array.from({ length: 200 }, (_, i) => ({
|
|
218
|
+
id: `r-${i}`,
|
|
219
|
+
name: `Row ${i}`,
|
|
220
|
+
}));
|
|
221
|
+
|
|
222
|
+
const { container, rerender } = render(
|
|
223
|
+
tableWrap(
|
|
224
|
+
<DataTable
|
|
225
|
+
columns={baseColumns}
|
|
226
|
+
data={data}
|
|
227
|
+
paginationMode="infinite"
|
|
228
|
+
virtualized
|
|
229
|
+
virtualRowEstimate={40}
|
|
230
|
+
highlightRowId="r-0"
|
|
231
|
+
scrollToHighlightOnMouseLeave
|
|
232
|
+
/>,
|
|
233
|
+
),
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const scrollHost = getScrollHost(container);
|
|
237
|
+
Object.defineProperty(scrollHost, "scrollHeight", {
|
|
238
|
+
configurable: true,
|
|
239
|
+
value: 80_000,
|
|
240
|
+
});
|
|
241
|
+
Object.defineProperty(scrollHost, "scrollTop", {
|
|
242
|
+
configurable: true,
|
|
243
|
+
writable: true,
|
|
244
|
+
value: 0,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const scrollTo = vi.fn().mockImplementation(() => {});
|
|
248
|
+
scrollHost.scrollTo = scrollTo as typeof scrollHost.scrollTo;
|
|
249
|
+
|
|
250
|
+
await waitFor(() => {
|
|
251
|
+
expect(
|
|
252
|
+
container.querySelectorAll("tbody tr[data-row-id]").length,
|
|
253
|
+
).toBeGreaterThan(0);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
scrollTo.mockClear();
|
|
257
|
+
|
|
258
|
+
await act(async () => {
|
|
259
|
+
rerender(
|
|
260
|
+
tableWrap(
|
|
261
|
+
<DataTable
|
|
262
|
+
columns={baseColumns}
|
|
263
|
+
data={data}
|
|
264
|
+
paginationMode="infinite"
|
|
265
|
+
virtualized
|
|
266
|
+
virtualRowEstimate={40}
|
|
267
|
+
highlightRowId="r-10"
|
|
268
|
+
scrollToHighlightOnMouseLeave
|
|
269
|
+
/>,
|
|
270
|
+
),
|
|
271
|
+
);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
await waitFor(() => expect(scrollTo.mock.calls.length).toBeGreaterThan(0));
|
|
275
|
+
|
|
276
|
+
scrollTo.mockClear();
|
|
277
|
+
|
|
278
|
+
fireEvent.wheel(scrollHost, { deltaY: 10 });
|
|
279
|
+
|
|
280
|
+
await act(async () => {
|
|
281
|
+
rerender(
|
|
282
|
+
tableWrap(
|
|
283
|
+
<DataTable
|
|
284
|
+
columns={baseColumns}
|
|
285
|
+
data={data}
|
|
286
|
+
paginationMode="infinite"
|
|
287
|
+
virtualized
|
|
288
|
+
virtualRowEstimate={40}
|
|
289
|
+
highlightRowId="r-20"
|
|
290
|
+
scrollToHighlightOnMouseLeave
|
|
291
|
+
/>,
|
|
292
|
+
),
|
|
293
|
+
);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
expect(scrollTo.mock.calls.length).toBe(0);
|
|
297
|
+
|
|
298
|
+
scrollTo.mockClear();
|
|
299
|
+
fireEvent.mouseLeave(scrollHost);
|
|
300
|
+
|
|
301
|
+
await waitFor(() => expect(scrollTo.mock.calls.length).toBeGreaterThan(0));
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Regression: infinite-mode `scroll` must not set userInteracting — smooth
|
|
306
|
+
* programmatic highlight scroll emits many scroll events after the programmatic
|
|
307
|
+
* lock unlocks; that used to flip userInteracting and block the next follow.
|
|
308
|
+
*/
|
|
309
|
+
it("still follows highlight after extra scroll events (no false pause from scroll listener)", async () => {
|
|
310
|
+
const data: TestRow[] = Array.from({ length: 200 }, (_, i) => ({
|
|
311
|
+
id: `r-${i}`,
|
|
312
|
+
name: `Row ${i}`,
|
|
313
|
+
}));
|
|
314
|
+
|
|
315
|
+
const { container, rerender } = render(
|
|
316
|
+
tableWrap(
|
|
317
|
+
<DataTable
|
|
318
|
+
columns={baseColumns}
|
|
319
|
+
data={data}
|
|
320
|
+
paginationMode="infinite"
|
|
321
|
+
virtualized
|
|
322
|
+
virtualRowEstimate={40}
|
|
323
|
+
highlightRowId="r-0"
|
|
324
|
+
/>,
|
|
325
|
+
),
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
const scrollHost = getScrollHost(container);
|
|
329
|
+
Object.defineProperty(scrollHost, "scrollHeight", {
|
|
330
|
+
configurable: true,
|
|
331
|
+
value: 80_000,
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
const scrollTo = vi.fn().mockImplementation(() => {});
|
|
335
|
+
scrollHost.scrollTo = scrollTo as typeof scrollHost.scrollTo;
|
|
336
|
+
|
|
337
|
+
await waitFor(() => {
|
|
338
|
+
expect(
|
|
339
|
+
container.querySelectorAll("tbody tr[data-row-id]").length,
|
|
340
|
+
).toBeGreaterThan(0);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
scrollTo.mockClear();
|
|
344
|
+
|
|
345
|
+
await act(async () => {
|
|
346
|
+
rerender(
|
|
347
|
+
tableWrap(
|
|
348
|
+
<DataTable
|
|
349
|
+
columns={baseColumns}
|
|
350
|
+
data={data}
|
|
351
|
+
paginationMode="infinite"
|
|
352
|
+
virtualized
|
|
353
|
+
virtualRowEstimate={40}
|
|
354
|
+
highlightRowId="r-30"
|
|
355
|
+
/>,
|
|
356
|
+
),
|
|
357
|
+
);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
await waitFor(() => expect(scrollTo.mock.calls.length).toBeGreaterThan(0));
|
|
361
|
+
|
|
362
|
+
scrollTo.mockClear();
|
|
363
|
+
for (let i = 0; i < 5; i += 1) {
|
|
364
|
+
fireEvent.scroll(scrollHost);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
await act(async () => {
|
|
368
|
+
rerender(
|
|
369
|
+
tableWrap(
|
|
370
|
+
<DataTable
|
|
371
|
+
columns={baseColumns}
|
|
372
|
+
data={data}
|
|
373
|
+
paginationMode="infinite"
|
|
374
|
+
virtualized
|
|
375
|
+
virtualRowEstimate={40}
|
|
376
|
+
highlightRowId="r-50"
|
|
377
|
+
/>,
|
|
378
|
+
),
|
|
379
|
+
);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
await waitFor(() => expect(scrollTo.mock.calls.length).toBeGreaterThan(0));
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// -------------------------------------------------------------------------
|
|
387
|
+
// Inline editing
|
|
388
|
+
// -------------------------------------------------------------------------
|
|
389
|
+
|
|
390
|
+
describe("Inline editing", () => {
|
|
391
|
+
type EditRow = {
|
|
392
|
+
id: string;
|
|
393
|
+
first: string;
|
|
394
|
+
second: string;
|
|
395
|
+
third: string;
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
const editData: EditRow[] = [
|
|
399
|
+
{ id: "r1", first: "A", second: "B", third: "C" },
|
|
400
|
+
{ id: "r2", first: "D", second: "E", third: "F" },
|
|
401
|
+
];
|
|
402
|
+
|
|
403
|
+
const editColumns: ColumnDef<EditRow>[] & {
|
|
404
|
+
enableEditing?: boolean | ((row: import("@tanstack/react-table").Row<EditRow>) => boolean);
|
|
405
|
+
editVariant?: "text" | "select";
|
|
406
|
+
editTextProps?: { placeholder?: string };
|
|
407
|
+
onCommit?: (row: import("@tanstack/react-table").Row<EditRow>, value: string) => Partial<EditRow>;
|
|
408
|
+
}[] = [
|
|
409
|
+
{
|
|
410
|
+
accessorKey: "first",
|
|
411
|
+
header: "First",
|
|
412
|
+
enableEditing: true,
|
|
413
|
+
editVariant: "text",
|
|
414
|
+
editTextProps: { placeholder: "First" },
|
|
415
|
+
onCommit: (_row: any, v: string) => ({ first: v, second: "" }),
|
|
416
|
+
} as any,
|
|
417
|
+
{
|
|
418
|
+
accessorKey: "second",
|
|
419
|
+
header: "Second",
|
|
420
|
+
enableEditing: true,
|
|
421
|
+
editVariant: "text",
|
|
422
|
+
editTextProps: { placeholder: "Second" },
|
|
423
|
+
} as any,
|
|
424
|
+
{
|
|
425
|
+
accessorKey: "third",
|
|
426
|
+
header: "Third",
|
|
427
|
+
enableEditing: true,
|
|
428
|
+
editVariant: "text",
|
|
429
|
+
editTextProps: { placeholder: "Third" },
|
|
430
|
+
} as any,
|
|
431
|
+
];
|
|
432
|
+
|
|
433
|
+
it("click activation: shows input on cell click", async () => {
|
|
434
|
+
const user = userEvent.setup();
|
|
435
|
+
render(
|
|
436
|
+
tableWrap(
|
|
437
|
+
<DataTable
|
|
438
|
+
columns={editColumns}
|
|
439
|
+
data={editData}
|
|
440
|
+
enableEditing
|
|
441
|
+
editDisplayMode="cell"
|
|
442
|
+
editTrigger="click"
|
|
443
|
+
/>,
|
|
444
|
+
),
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
const cell = screen.getByText("A");
|
|
448
|
+
await user.click(cell);
|
|
449
|
+
|
|
450
|
+
await waitFor(() =>
|
|
451
|
+
expect(screen.getByDisplayValue("A")).toBeInTheDocument(),
|
|
452
|
+
);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it("double-click activation: does NOT activate on single click", async () => {
|
|
456
|
+
const user = userEvent.setup();
|
|
457
|
+
render(
|
|
458
|
+
tableWrap(
|
|
459
|
+
<DataTable
|
|
460
|
+
columns={editColumns}
|
|
461
|
+
data={editData}
|
|
462
|
+
enableEditing
|
|
463
|
+
editDisplayMode="cell"
|
|
464
|
+
editTrigger="doubleClick"
|
|
465
|
+
/>,
|
|
466
|
+
),
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
const cell = screen.getByText("A");
|
|
470
|
+
await user.click(cell);
|
|
471
|
+
|
|
472
|
+
expect(screen.queryByDisplayValue("A")).not.toBeInTheDocument();
|
|
473
|
+
|
|
474
|
+
await user.dblClick(cell);
|
|
475
|
+
|
|
476
|
+
await waitFor(() =>
|
|
477
|
+
expect(screen.getByDisplayValue("A")).toBeInTheDocument(),
|
|
478
|
+
);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it("blur commits value and fires onCellCommit", async () => {
|
|
482
|
+
const user = userEvent.setup();
|
|
483
|
+
const onCommit = vi.fn();
|
|
484
|
+
|
|
485
|
+
render(
|
|
486
|
+
tableWrap(
|
|
487
|
+
<DataTable
|
|
488
|
+
columns={editColumns}
|
|
489
|
+
data={editData}
|
|
490
|
+
enableEditing
|
|
491
|
+
editDisplayMode="cell"
|
|
492
|
+
editTrigger="click"
|
|
493
|
+
onCellCommit={onCommit}
|
|
494
|
+
/>,
|
|
495
|
+
),
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
await user.click(screen.getByText("A"));
|
|
499
|
+
|
|
500
|
+
const input = await screen.findByDisplayValue("A");
|
|
501
|
+
await user.clear(input);
|
|
502
|
+
await user.type(input, "NEW");
|
|
503
|
+
|
|
504
|
+
await user.click(document.body);
|
|
505
|
+
|
|
506
|
+
await waitFor(() => {
|
|
507
|
+
expect(onCommit).toHaveBeenCalledWith(
|
|
508
|
+
"r1",
|
|
509
|
+
"first",
|
|
510
|
+
expect.objectContaining({ first: "NEW" }),
|
|
511
|
+
);
|
|
512
|
+
});
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it("onCommit shapes the patch (clears downstream fields)", async () => {
|
|
516
|
+
const user = userEvent.setup();
|
|
517
|
+
const onCommit = vi.fn();
|
|
518
|
+
|
|
519
|
+
render(
|
|
520
|
+
tableWrap(
|
|
521
|
+
<DataTable
|
|
522
|
+
columns={editColumns}
|
|
523
|
+
data={editData}
|
|
524
|
+
enableEditing
|
|
525
|
+
editDisplayMode="cell"
|
|
526
|
+
editTrigger="click"
|
|
527
|
+
onCellCommit={onCommit}
|
|
528
|
+
/>,
|
|
529
|
+
),
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
await user.click(screen.getByText("A"));
|
|
533
|
+
|
|
534
|
+
const input = await screen.findByDisplayValue("A");
|
|
535
|
+
await user.clear(input);
|
|
536
|
+
await user.type(input, "CHANGED");
|
|
537
|
+
await user.click(document.body);
|
|
538
|
+
|
|
539
|
+
await waitFor(() => {
|
|
540
|
+
expect(onCommit).toHaveBeenCalledWith("r1", "first", {
|
|
541
|
+
first: "CHANGED",
|
|
542
|
+
second: "",
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it("Escape exits row edit mode", async () => {
|
|
548
|
+
const user = userEvent.setup();
|
|
549
|
+
render(
|
|
550
|
+
tableWrap(
|
|
551
|
+
<DataTable
|
|
552
|
+
columns={editColumns}
|
|
553
|
+
data={editData}
|
|
554
|
+
enableEditing
|
|
555
|
+
editDisplayMode="row"
|
|
556
|
+
editTrigger="click"
|
|
557
|
+
/>,
|
|
558
|
+
),
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
await user.click(screen.getByText("A"));
|
|
562
|
+
|
|
563
|
+
await waitFor(() =>
|
|
564
|
+
expect(screen.getByDisplayValue("A")).toBeInTheDocument(),
|
|
565
|
+
);
|
|
566
|
+
|
|
567
|
+
await user.keyboard("{Escape}");
|
|
568
|
+
|
|
569
|
+
await waitFor(() =>
|
|
570
|
+
expect(screen.queryByDisplayValue("A")).not.toBeInTheDocument(),
|
|
571
|
+
);
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
it("row mode: clicking one cell activates entire row", async () => {
|
|
575
|
+
const user = userEvent.setup();
|
|
576
|
+
render(
|
|
577
|
+
tableWrap(
|
|
578
|
+
<DataTable
|
|
579
|
+
columns={editColumns}
|
|
580
|
+
data={editData}
|
|
581
|
+
enableEditing
|
|
582
|
+
editDisplayMode="row"
|
|
583
|
+
editTrigger="click"
|
|
584
|
+
/>,
|
|
585
|
+
),
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
await user.click(screen.getByText("A"));
|
|
589
|
+
|
|
590
|
+
await waitFor(() => {
|
|
591
|
+
expect(screen.getByDisplayValue("A")).toBeInTheDocument();
|
|
592
|
+
expect(screen.getByDisplayValue("B")).toBeInTheDocument();
|
|
593
|
+
expect(screen.getByDisplayValue("C")).toBeInTheDocument();
|
|
594
|
+
});
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it("alwaysEditing renders inputs without activation", () => {
|
|
598
|
+
render(
|
|
599
|
+
tableWrap(
|
|
600
|
+
<DataTable
|
|
601
|
+
columns={editColumns}
|
|
602
|
+
data={editData}
|
|
603
|
+
enableEditing
|
|
604
|
+
editDisplayMode="cell"
|
|
605
|
+
editTrigger="click"
|
|
606
|
+
alwaysEditing={() => true}
|
|
607
|
+
/>,
|
|
608
|
+
),
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
expect(screen.getByDisplayValue("A")).toBeInTheDocument();
|
|
612
|
+
expect(screen.getByDisplayValue("B")).toBeInTheDocument();
|
|
613
|
+
expect(screen.getByDisplayValue("D")).toBeInTheDocument();
|
|
614
|
+
});
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
// -------------------------------------------------------------------------
|
|
618
|
+
// data-testid
|
|
619
|
+
// -------------------------------------------------------------------------
|
|
620
|
+
|
|
621
|
+
describe("testId prop", () => {
|
|
622
|
+
it("sets data-testid on root, thead, and tbody", () => {
|
|
623
|
+
const data: TestRow[] = [{ id: "a", name: "Alpha" }];
|
|
624
|
+
const { container } = render(
|
|
625
|
+
tableWrap(
|
|
626
|
+
<DataTable
|
|
627
|
+
columns={baseColumns}
|
|
628
|
+
data={data}
|
|
629
|
+
testId="my-table"
|
|
630
|
+
/>,
|
|
631
|
+
),
|
|
632
|
+
);
|
|
633
|
+
|
|
634
|
+
expect(container.querySelector('[data-testid="my-table"]')).toBeTruthy();
|
|
635
|
+
expect(
|
|
636
|
+
container.querySelector('[data-testid="my-table-thead"]'),
|
|
637
|
+
).toBeTruthy();
|
|
638
|
+
expect(
|
|
639
|
+
container.querySelector('[data-testid="my-table-tbody"]'),
|
|
640
|
+
).toBeTruthy();
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
it("does not render data-testid when prop is omitted", () => {
|
|
644
|
+
const data: TestRow[] = [{ id: "a", name: "Alpha" }];
|
|
645
|
+
const { container } = render(
|
|
646
|
+
tableWrap(
|
|
647
|
+
<DataTable columns={baseColumns} data={data} />,
|
|
648
|
+
),
|
|
649
|
+
);
|
|
650
|
+
|
|
651
|
+
expect(container.querySelector("[data-testid]")).toBeNull();
|
|
652
|
+
});
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
// -------------------------------------------------------------------------
|
|
656
|
+
// Editing — error display
|
|
657
|
+
// -------------------------------------------------------------------------
|
|
658
|
+
|
|
659
|
+
describe("Inline editing — error message", () => {
|
|
660
|
+
type ErrRow = { id: string; val: string };
|
|
661
|
+
|
|
662
|
+
it("renders error message from editError when cell is active", async () => {
|
|
663
|
+
const user = userEvent.setup();
|
|
664
|
+
const errColumns = [
|
|
665
|
+
{
|
|
666
|
+
accessorKey: "val",
|
|
667
|
+
header: "Value",
|
|
668
|
+
enableEditing: true,
|
|
669
|
+
editVariant: "text" as const,
|
|
670
|
+
editTextProps: { placeholder: "Value" },
|
|
671
|
+
editError: (row: any) =>
|
|
672
|
+
!row.original.val.trim() ? "Required" : undefined,
|
|
673
|
+
},
|
|
674
|
+
];
|
|
675
|
+
|
|
676
|
+
const { container } = render(
|
|
677
|
+
tableWrap(
|
|
678
|
+
<DataTable
|
|
679
|
+
columns={errColumns}
|
|
680
|
+
data={[{ id: "r1", val: "Hello" }]}
|
|
681
|
+
enableEditing
|
|
682
|
+
editDisplayMode="cell"
|
|
683
|
+
editTrigger="click"
|
|
684
|
+
/>,
|
|
685
|
+
),
|
|
686
|
+
);
|
|
687
|
+
|
|
688
|
+
await user.click(screen.getByText("Hello"));
|
|
689
|
+
await waitFor(() =>
|
|
690
|
+
expect(screen.getByDisplayValue("Hello")).toBeInTheDocument(),
|
|
691
|
+
);
|
|
692
|
+
|
|
693
|
+
expect(container.querySelector('[class*="text-error"]')).toBeNull();
|
|
694
|
+
});
|
|
695
|
+
});
|
|
696
|
+
});
|