@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.
Files changed (65) hide show
  1. package/dist/cjs/bundle.css +501 -67
  2. package/dist/cjs/bundle.js +589 -589
  3. package/dist/cjs/bundle.js.map +1 -1
  4. package/dist/cjs/types/components/DataTable/DataTable.d.ts +195 -4
  5. package/dist/cjs/types/components/DataTable/DataTable.editing.d.ts +20 -0
  6. package/dist/cjs/types/components/DataTable/DataTable.editing.types.d.ts +145 -0
  7. package/dist/cjs/types/components/DataTable/DataTable.stories.d.ts +268 -6
  8. package/dist/cjs/types/components/Dropdown/Dropdown.d.ts +22 -0
  9. package/dist/cjs/types/components/Dropdown/Dropdown.stories.d.ts +4 -0
  10. package/dist/cjs/types/components/ScrollArea/ScrollArea.d.ts +3 -3
  11. package/dist/cjs/types/components/ScrollArea/ScrollArea.stories.d.ts +4 -0
  12. package/dist/cjs/types/components/Table/Table.d.ts +33 -3
  13. package/dist/cjs/types/components/Table/Table.stories.d.ts +86 -4
  14. package/dist/cjs/types/components/TextInput/TextInput.stories.d.ts +8 -0
  15. package/dist/cjs/types/components/TextInput/TextInput.styles.d.ts +1 -0
  16. package/dist/components/DataTable/DataTable.editing.js +385 -0
  17. package/dist/components/DataTable/DataTable.editing.types.js +1 -0
  18. package/dist/components/DataTable/DataTable.js +983 -50
  19. package/dist/components/DataTable/DataTable.stories.js +1077 -25
  20. package/dist/components/Dropdown/Dropdown.js +8 -6
  21. package/dist/components/ScrollArea/ScrollArea.js +2 -2
  22. package/dist/components/ScrollArea/ScrollArea.stories.js +68 -2
  23. package/dist/components/Table/Table.js +103 -13
  24. package/dist/components/Table/Table.stories.js +226 -9
  25. package/dist/components/TextInput/TextInput.js +6 -4
  26. package/dist/components/TextInput/TextInput.stories.js +8 -0
  27. package/dist/components/TextInput/TextInput.styles.js +7 -1
  28. package/dist/esm/bundle.css +501 -67
  29. package/dist/esm/bundle.js +1545 -1545
  30. package/dist/esm/bundle.js.map +1 -1
  31. package/dist/esm/types/components/DataTable/DataTable.d.ts +195 -4
  32. package/dist/esm/types/components/DataTable/DataTable.editing.d.ts +20 -0
  33. package/dist/esm/types/components/DataTable/DataTable.editing.types.d.ts +145 -0
  34. package/dist/esm/types/components/DataTable/DataTable.stories.d.ts +268 -6
  35. package/dist/esm/types/components/Dropdown/Dropdown.d.ts +22 -0
  36. package/dist/esm/types/components/Dropdown/Dropdown.stories.d.ts +4 -0
  37. package/dist/esm/types/components/ScrollArea/ScrollArea.d.ts +3 -3
  38. package/dist/esm/types/components/ScrollArea/ScrollArea.stories.d.ts +4 -0
  39. package/dist/esm/types/components/Table/Table.d.ts +33 -3
  40. package/dist/esm/types/components/Table/Table.stories.d.ts +86 -4
  41. package/dist/esm/types/components/TextInput/TextInput.stories.d.ts +8 -0
  42. package/dist/esm/types/components/TextInput/TextInput.styles.d.ts +1 -0
  43. package/dist/index.d.ts +493 -122
  44. package/dist/src/theme/global.css +747 -96
  45. package/package.json +14 -2
  46. package/src/components/DataTable/DataTable.editing.tsx +861 -0
  47. package/src/components/DataTable/DataTable.editing.types.ts +192 -0
  48. package/src/components/DataTable/DataTable.stories.tsx +2169 -31
  49. package/src/components/DataTable/DataTable.test.tsx +696 -0
  50. package/src/components/DataTable/DataTable.tsx +2260 -94
  51. package/src/components/Dropdown/Dropdown.tsx +22 -6
  52. package/src/components/ScrollArea/ScrollArea.stories.tsx +146 -3
  53. package/src/components/ScrollArea/ScrollArea.tsx +6 -6
  54. package/src/components/Table/Table.stories.tsx +789 -44
  55. package/src/components/Table/Table.tsx +294 -28
  56. package/src/components/TextInput/TextInput.stories.tsx +80 -0
  57. package/src/components/TextInput/TextInput.styles.ts +7 -1
  58. package/src/components/TextInput/TextInput.tsx +21 -14
  59. package/src/test/setup.ts +50 -0
  60. package/src/theme/global.css +81 -42
  61. package/src/theme/presets/colors.js +12 -0
  62. package/src/theme/themes/variable.css +27 -28
  63. package/src/theme/tokens/baseline.css +2 -1
  64. package/src/theme/tokens/components/scrollbar.css +9 -4
  65. 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
+ });