@gradio/dataframe 0.22.0 → 0.23.0
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/CHANGELOG.md +14 -0
- package/Dataframe.test.ts +697 -32
- package/Index.svelte +107 -106
- package/dist/Index.svelte +107 -106
- package/dist/Index.svelte.d.ts +0 -1
- package/dist/shared/BooleanCell.svelte +12 -6
- package/dist/shared/BooleanCell.svelte.d.ts +4 -18
- package/dist/shared/CellMenu.svelte +74 -49
- package/dist/shared/CellMenu.svelte.d.ts +5 -19
- package/dist/shared/CellMenuButton.svelte +3 -3
- package/dist/shared/CellMenuButton.svelte.d.ts +4 -18
- package/dist/shared/CellMenuIcons.svelte +1 -1
- package/dist/shared/CellMenuIcons.svelte.d.ts +4 -18
- package/dist/shared/DataCell.svelte +189 -0
- package/dist/shared/DataCell.svelte.d.ts +44 -0
- package/dist/shared/EditableCell.svelte +88 -64
- package/dist/shared/EditableCell.svelte.d.ts +8 -26
- package/dist/shared/EmptyRowButton.svelte +2 -2
- package/dist/shared/EmptyRowButton.svelte.d.ts +4 -18
- package/dist/shared/FilterMenu.svelte +26 -18
- package/dist/shared/FilterMenu.svelte.d.ts +5 -21
- package/dist/shared/HeaderCell.svelte +218 -0
- package/dist/shared/HeaderCell.svelte.d.ts +30 -0
- package/dist/shared/RowNumber.svelte +4 -2
- package/dist/shared/RowNumber.svelte.d.ts +4 -18
- package/dist/shared/Table.svelte +1037 -949
- package/dist/shared/Table.svelte.d.ts +12 -30
- package/dist/shared/Toolbar.svelte +32 -22
- package/dist/shared/Toolbar.svelte.d.ts +6 -21
- package/dist/shared/column_measurement.svelte.d.ts +20 -0
- package/dist/shared/column_measurement.svelte.js +79 -0
- package/dist/shared/icons/FilterIcon.svelte +14 -6
- package/dist/shared/icons/FilterIcon.svelte.d.ts +14 -6
- package/dist/shared/icons/Padlock.svelte +6 -2
- package/dist/shared/icons/Padlock.svelte.d.ts +4 -25
- package/dist/shared/icons/SelectionButtons.svelte +23 -8
- package/dist/shared/icons/SelectionButtons.svelte.d.ts +4 -18
- package/dist/shared/icons/SortButtonDown.svelte +15 -4
- package/dist/shared/icons/SortButtonDown.svelte.d.ts +4 -25
- package/dist/shared/icons/SortButtonUp.svelte +15 -5
- package/dist/shared/icons/SortButtonUp.svelte.d.ts +4 -25
- package/dist/shared/icons/SortIcon.svelte +13 -7
- package/dist/shared/icons/SortIcon.svelte.d.ts +7 -21
- package/dist/shared/tanstack/index.d.ts +3 -0
- package/dist/shared/tanstack/index.js +3 -0
- package/dist/shared/tanstack/table.svelte.d.ts +26 -0
- package/dist/shared/tanstack/table.svelte.js +102 -0
- package/dist/shared/tanstack/virtual.svelte.d.ts +14 -0
- package/dist/shared/tanstack/virtual.svelte.js +76 -0
- package/dist/shared/types.d.ts +2 -0
- package/dist/shared/utils/filter.d.ts +5 -0
- package/dist/shared/utils/filter.js +62 -0
- package/dist/shared/utils/selection_utils.js +1 -1
- package/dist/shared/utils/table_utils.d.ts +1 -14
- package/dist/shared/utils/table_utils.js +0 -61
- package/dist/standalone/Index.svelte +71 -46
- package/dist/standalone/Index.svelte.d.ts +4 -23
- package/dist/types.d.ts +32 -0
- package/dist/types.js +1 -0
- package/package.json +10 -9
- package/shared/BooleanCell.svelte +12 -6
- package/shared/CellMenu.svelte +74 -49
- package/shared/CellMenuButton.svelte +3 -3
- package/shared/CellMenuIcons.svelte +1 -1
- package/shared/DataCell.svelte +189 -0
- package/shared/EditableCell.svelte +88 -64
- package/shared/EmptyRowButton.svelte +2 -2
- package/shared/FilterMenu.svelte +26 -18
- package/shared/HeaderCell.svelte +218 -0
- package/shared/RowNumber.svelte +4 -2
- package/shared/Table.svelte +1037 -949
- package/shared/Toolbar.svelte +32 -22
- package/shared/column_measurement.svelte.ts +103 -0
- package/shared/icons/FilterIcon.svelte +14 -6
- package/shared/icons/Padlock.svelte +6 -2
- package/shared/icons/SelectionButtons.svelte +23 -8
- package/shared/icons/SortButtonDown.svelte +15 -4
- package/shared/icons/SortButtonUp.svelte +15 -5
- package/shared/icons/SortIcon.svelte +13 -7
- package/shared/tanstack/index.ts +23 -0
- package/shared/tanstack/table.svelte.ts +117 -0
- package/shared/tanstack/virtual.svelte.ts +105 -0
- package/shared/types.ts +3 -0
- package/shared/utils/filter.ts +66 -0
- package/shared/utils/selection_utils.ts +1 -1
- package/shared/utils/table_utils.ts +1 -100
- package/standalone/Index.svelte +71 -46
- package/test/filter.test.ts +350 -0
- package/test/selection_utils.test.ts +416 -0
- package/test/table_utils.test.ts +45 -112
- package/types.ts +34 -0
- package/dist/shared/TableCell.svelte +0 -330
- package/dist/shared/TableCell.svelte.d.ts +0 -66
- package/dist/shared/TableHeader.svelte +0 -344
- package/dist/shared/TableHeader.svelte.d.ts +0 -61
- package/dist/shared/VirtualTable.svelte +0 -379
- package/dist/shared/VirtualTable.svelte.d.ts +0 -42
- package/dist/shared/context/dataframe_context.d.ts +0 -180
- package/dist/shared/context/dataframe_context.js +0 -412
- package/dist/shared/icons/SortArrowDown.svelte +0 -25
- package/dist/shared/icons/SortArrowDown.svelte.d.ts +0 -20
- package/dist/shared/icons/SortArrowUp.svelte +0 -25
- package/dist/shared/icons/SortArrowUp.svelte.d.ts +0 -20
- package/dist/shared/utils/data_processing.d.ts +0 -14
- package/dist/shared/utils/data_processing.js +0 -47
- package/dist/shared/utils/drag_utils.d.ts +0 -15
- package/dist/shared/utils/drag_utils.js +0 -67
- package/dist/shared/utils/filter_utils.d.ts +0 -29
- package/dist/shared/utils/filter_utils.js +0 -123
- package/dist/shared/utils/keyboard_utils.d.ts +0 -3
- package/dist/shared/utils/keyboard_utils.js +0 -236
- package/dist/shared/utils/menu_utils.d.ts +0 -42
- package/dist/shared/utils/menu_utils.js +0 -58
- package/dist/shared/utils/sort_utils.d.ts +0 -26
- package/dist/shared/utils/sort_utils.js +0 -59
- package/shared/TableCell.svelte +0 -330
- package/shared/TableHeader.svelte +0 -344
- package/shared/VirtualTable.svelte +0 -379
- package/shared/context/dataframe_context.ts +0 -701
- package/shared/icons/SortArrowDown.svelte +0 -25
- package/shared/icons/SortArrowUp.svelte +0 -25
- package/shared/utils/data_processing.ts +0 -77
- package/shared/utils/drag_utils.ts +0 -105
- package/shared/utils/filter_utils.ts +0 -208
- package/shared/utils/keyboard_utils.ts +0 -309
- package/shared/utils/menu_utils.ts +0 -115
- package/shared/utils/sort_utils.ts +0 -91
- package/test/sort_utils.test.ts +0 -323
package/Dataframe.test.ts
CHANGED
|
@@ -1,44 +1,709 @@
|
|
|
1
|
-
import { test, describe,
|
|
2
|
-
import { cleanup, render } from "@self/tootils/render";
|
|
3
|
-
import {
|
|
4
|
-
// import Dataframe from "./Index.svelte";
|
|
1
|
+
import { test, describe, afterEach, expect } from "vitest";
|
|
2
|
+
import { cleanup, render, fireEvent } from "@self/tootils/render";
|
|
3
|
+
import { tick } from "svelte";
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
import Dataframe from "./Index.svelte";
|
|
6
|
+
|
|
7
|
+
const default_props = {
|
|
8
|
+
value: {
|
|
9
|
+
data: [
|
|
10
|
+
["Alice", "30", "Engineer"],
|
|
11
|
+
["Bob", "25", "Designer"],
|
|
12
|
+
["Carol", "35", "Manager"]
|
|
13
|
+
],
|
|
14
|
+
headers: ["Name", "Age", "Role"],
|
|
15
|
+
metadata: null
|
|
16
|
+
},
|
|
17
|
+
col_count: [3, "fixed"] as [number, "fixed" | "dynamic"],
|
|
18
|
+
row_count: [3, "fixed"] as [number, "fixed" | "dynamic"],
|
|
19
|
+
editable: true,
|
|
20
|
+
datatype: "str" as const,
|
|
21
|
+
root: "",
|
|
22
|
+
static_columns: [] as (string | number)[],
|
|
23
|
+
latex_delimiters: [] as { left: string; right: string; display: boolean }[],
|
|
24
|
+
line_breaks: true,
|
|
25
|
+
wrap: false,
|
|
26
|
+
column_widths: [] as string[],
|
|
27
|
+
show_row_numbers: false,
|
|
28
|
+
show_search: "none" as "none" | "search" | "filter",
|
|
29
|
+
pinned_columns: 0,
|
|
30
|
+
fullscreen: false,
|
|
31
|
+
max_height: 500
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function get_cell(container: HTMLElement, row: number, col: number) {
|
|
35
|
+
return container.querySelector(
|
|
36
|
+
`[data-row='${row}'][data-col='${col}']`
|
|
37
|
+
) as HTMLElement | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function get_header_cells(container: HTMLElement) {
|
|
41
|
+
return container.querySelectorAll("th.header-cell");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function get_rows(container: HTMLElement) {
|
|
45
|
+
return container.querySelectorAll(".virtual-row");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function get_table_wrap(container: HTMLElement) {
|
|
49
|
+
return container.querySelector(".table-wrap") as HTMLElement;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function wait(ms = 50) {
|
|
53
|
+
await new Promise((r) => setTimeout(r, ms));
|
|
54
|
+
await tick();
|
|
55
|
+
await tick();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe("Dataframe rendering", () => {
|
|
59
|
+
afterEach(() => cleanup());
|
|
60
|
+
|
|
61
|
+
test("renders provided headers", async () => {
|
|
62
|
+
const { container } = await render(Dataframe, default_props);
|
|
63
|
+
await wait();
|
|
64
|
+
|
|
65
|
+
const headers = get_header_cells(container);
|
|
66
|
+
const header_texts = Array.from(headers).map(
|
|
67
|
+
(h) => h.textContent?.trim() ?? ""
|
|
68
|
+
);
|
|
69
|
+
expect(header_texts).toContain("Name");
|
|
70
|
+
expect(header_texts).toContain("Age");
|
|
71
|
+
expect(header_texts).toContain("Role");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("renders provided cell values", async () => {
|
|
75
|
+
const { container } = await render(Dataframe, default_props);
|
|
76
|
+
await wait();
|
|
77
|
+
|
|
78
|
+
const alice_cell = get_cell(container, 0, 0);
|
|
79
|
+
expect(alice_cell?.textContent).toContain("Alice");
|
|
80
|
+
|
|
81
|
+
const bob_cell = get_cell(container, 1, 0);
|
|
82
|
+
expect(bob_cell?.textContent).toContain("Bob");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("renders correct number of rows", async () => {
|
|
86
|
+
const { container } = await render(Dataframe, default_props);
|
|
87
|
+
await wait();
|
|
88
|
+
|
|
89
|
+
const rows = get_rows(container);
|
|
90
|
+
expect(rows.length).toBe(3);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("renders label when show_label is true", async () => {
|
|
94
|
+
const { container } = await render(Dataframe, {
|
|
95
|
+
...default_props,
|
|
96
|
+
label: "Test Table",
|
|
97
|
+
show_label: true
|
|
98
|
+
});
|
|
99
|
+
await wait();
|
|
100
|
+
|
|
101
|
+
expect(container.textContent).toContain("Test Table");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("renders row numbers when show_row_numbers is true", async () => {
|
|
105
|
+
const { container } = await render(Dataframe, {
|
|
106
|
+
...default_props,
|
|
107
|
+
show_row_numbers: true
|
|
108
|
+
});
|
|
109
|
+
await wait();
|
|
110
|
+
|
|
111
|
+
const row_number_cells = container.querySelectorAll(".row-number-cell");
|
|
112
|
+
expect(row_number_cells.length).toBeGreaterThan(0);
|
|
9
113
|
});
|
|
10
|
-
|
|
11
|
-
|
|
114
|
+
|
|
115
|
+
test("renders search input when show_search is 'search'", async () => {
|
|
116
|
+
const { container } = await render(Dataframe, {
|
|
117
|
+
...default_props,
|
|
118
|
+
show_search: "search"
|
|
119
|
+
});
|
|
120
|
+
await wait();
|
|
121
|
+
|
|
122
|
+
const search_input = container.querySelector(
|
|
123
|
+
"input.search-input"
|
|
124
|
+
) as HTMLInputElement;
|
|
125
|
+
expect(search_input).not.toBeNull();
|
|
126
|
+
expect(search_input.placeholder).toBe("Search...");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("renders filter input when show_search is 'filter'", async () => {
|
|
130
|
+
const { container } = await render(Dataframe, {
|
|
131
|
+
...default_props,
|
|
132
|
+
show_search: "filter"
|
|
133
|
+
});
|
|
134
|
+
await wait();
|
|
135
|
+
|
|
136
|
+
const filter_input = container.querySelector(
|
|
137
|
+
"input.search-input"
|
|
138
|
+
) as HTMLInputElement;
|
|
139
|
+
expect(filter_input).not.toBeNull();
|
|
140
|
+
expect(filter_input.placeholder).toBe("Filter...");
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("Cell editing", () => {
|
|
145
|
+
afterEach(() => cleanup());
|
|
146
|
+
|
|
147
|
+
test("double-click enters edit mode (shows textarea)", async () => {
|
|
148
|
+
const { container } = await render(Dataframe, default_props);
|
|
149
|
+
await wait();
|
|
150
|
+
|
|
151
|
+
const cell = get_cell(container, 0, 0)!;
|
|
152
|
+
await fireEvent.mouseDown(cell);
|
|
153
|
+
await tick();
|
|
154
|
+
await fireEvent.dblClick(cell);
|
|
155
|
+
await wait();
|
|
156
|
+
|
|
157
|
+
const textarea = container.querySelector(
|
|
158
|
+
"textarea[aria-label='Edit cell']"
|
|
159
|
+
);
|
|
160
|
+
expect(textarea).not.toBeNull();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("Escape key cancels edit", async () => {
|
|
164
|
+
const { container } = await render(Dataframe, default_props);
|
|
165
|
+
await wait();
|
|
166
|
+
|
|
167
|
+
const cell = get_cell(container, 0, 0)!;
|
|
168
|
+
await fireEvent.mouseDown(cell);
|
|
169
|
+
await tick();
|
|
170
|
+
await fireEvent.dblClick(cell);
|
|
171
|
+
await wait();
|
|
172
|
+
|
|
173
|
+
const table_wrap = get_table_wrap(container)!;
|
|
174
|
+
await fireEvent.keyDown(table_wrap, { key: "Escape" });
|
|
175
|
+
await wait();
|
|
176
|
+
|
|
177
|
+
const textarea = container.querySelector(
|
|
178
|
+
"textarea[aria-label='Edit cell']"
|
|
179
|
+
);
|
|
180
|
+
expect(textarea).toBeNull();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("non-editable dataframe prevents editing on double-click", async () => {
|
|
184
|
+
const { container } = await render(Dataframe, {
|
|
185
|
+
...default_props,
|
|
186
|
+
interactive: false,
|
|
187
|
+
editable: false
|
|
188
|
+
});
|
|
189
|
+
await wait();
|
|
190
|
+
|
|
191
|
+
const cell = get_cell(container, 0, 0)!;
|
|
192
|
+
await fireEvent.mouseDown(cell);
|
|
193
|
+
await tick();
|
|
194
|
+
await fireEvent.dblClick(cell);
|
|
195
|
+
await wait();
|
|
196
|
+
|
|
197
|
+
const textarea = container.querySelector(
|
|
198
|
+
"textarea[aria-label='Edit cell']"
|
|
199
|
+
);
|
|
200
|
+
expect(textarea).toBeNull();
|
|
12
201
|
});
|
|
13
202
|
|
|
14
|
-
test("
|
|
15
|
-
await
|
|
16
|
-
|
|
203
|
+
test("static columns cannot be edited", async () => {
|
|
204
|
+
const { container } = await render(Dataframe, {
|
|
205
|
+
...default_props,
|
|
206
|
+
static_columns: [0]
|
|
207
|
+
});
|
|
208
|
+
await wait();
|
|
209
|
+
|
|
210
|
+
const static_cell = get_cell(container, 0, 0)!;
|
|
211
|
+
await fireEvent.mouseDown(static_cell);
|
|
212
|
+
await tick();
|
|
213
|
+
await fireEvent.dblClick(static_cell);
|
|
214
|
+
await wait();
|
|
215
|
+
|
|
216
|
+
// static cell should not have an editable textarea
|
|
217
|
+
const textarea = static_cell.querySelector("textarea");
|
|
218
|
+
expect(textarea).toBeNull();
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe("Cell selection", () => {
|
|
223
|
+
afterEach(() => cleanup());
|
|
224
|
+
|
|
225
|
+
test("click selects a cell", async () => {
|
|
226
|
+
const { container } = await render(Dataframe, default_props);
|
|
227
|
+
await wait();
|
|
228
|
+
|
|
229
|
+
const cell = get_cell(container, 1, 1)!;
|
|
230
|
+
await fireEvent.mouseDown(cell);
|
|
231
|
+
await wait();
|
|
232
|
+
|
|
233
|
+
// cell should have selection styling
|
|
234
|
+
expect(cell.className).toContain("cell-selected");
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("arrow keys move selection", async () => {
|
|
238
|
+
const { container } = await render(Dataframe, default_props);
|
|
239
|
+
await wait();
|
|
240
|
+
|
|
241
|
+
const cell = get_cell(container, 0, 0)!;
|
|
242
|
+
await fireEvent.mouseDown(cell);
|
|
243
|
+
await wait();
|
|
244
|
+
|
|
245
|
+
const table_wrap = get_table_wrap(container)!;
|
|
246
|
+
await fireEvent.keyDown(table_wrap, { key: "ArrowRight" });
|
|
247
|
+
await wait();
|
|
248
|
+
|
|
249
|
+
const next_cell = get_cell(container, 0, 1)!;
|
|
250
|
+
expect(next_cell.className).toContain("cell-selected");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("Tab moves to next cell", async () => {
|
|
254
|
+
const { container } = await render(Dataframe, default_props);
|
|
255
|
+
await wait();
|
|
256
|
+
|
|
257
|
+
const cell = get_cell(container, 0, 0)!;
|
|
258
|
+
await fireEvent.mouseDown(cell);
|
|
259
|
+
await wait();
|
|
260
|
+
|
|
261
|
+
const table_wrap = get_table_wrap(container)!;
|
|
262
|
+
await fireEvent.keyDown(table_wrap, { key: "Tab" });
|
|
263
|
+
await wait();
|
|
264
|
+
|
|
265
|
+
const next_cell = get_cell(container, 0, 1)!;
|
|
266
|
+
expect(next_cell.className).toContain("cell-selected");
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("shift+click selects a range", async () => {
|
|
270
|
+
const { container } = await render(Dataframe, default_props);
|
|
271
|
+
await wait();
|
|
272
|
+
|
|
273
|
+
const first_cell = get_cell(container, 0, 0)!;
|
|
274
|
+
await fireEvent.mouseDown(first_cell);
|
|
275
|
+
await wait();
|
|
276
|
+
|
|
277
|
+
const last_cell = get_cell(container, 1, 1)!;
|
|
278
|
+
await fireEvent.mouseDown(last_cell, { shiftKey: true });
|
|
279
|
+
await wait();
|
|
280
|
+
|
|
281
|
+
// all 4 cells in the 2x2 range should be selected
|
|
282
|
+
expect(get_cell(container, 0, 0)!.className).toContain("cell-selected");
|
|
283
|
+
expect(get_cell(container, 0, 1)!.className).toContain("cell-selected");
|
|
284
|
+
expect(get_cell(container, 1, 0)!.className).toContain("cell-selected");
|
|
285
|
+
expect(get_cell(container, 1, 1)!.className).toContain("cell-selected");
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
describe("Sorting", () => {
|
|
290
|
+
afterEach(() => cleanup());
|
|
291
|
+
|
|
292
|
+
test("sort ascending via header menu", async () => {
|
|
293
|
+
const { container } = await render(Dataframe, {
|
|
294
|
+
...default_props,
|
|
295
|
+
col_count: [3, "dynamic"] as [number, "fixed" | "dynamic"]
|
|
296
|
+
});
|
|
297
|
+
await wait();
|
|
298
|
+
|
|
299
|
+
// Click on the "Age" header — find by content containing "Age"
|
|
300
|
+
const headers = get_header_cells(container);
|
|
301
|
+
const age_header = Array.from(headers).find((h) =>
|
|
302
|
+
h.textContent?.includes("Age")
|
|
303
|
+
) as HTMLElement;
|
|
304
|
+
expect(age_header).toBeTruthy();
|
|
305
|
+
|
|
306
|
+
// Click the menu button on the header
|
|
307
|
+
const menu_btn = age_header.querySelector(
|
|
308
|
+
".cell-menu-button"
|
|
309
|
+
) as HTMLElement;
|
|
310
|
+
expect(menu_btn).toBeTruthy();
|
|
311
|
+
await fireEvent.click(menu_btn);
|
|
312
|
+
await wait();
|
|
313
|
+
|
|
314
|
+
// Click "Sort ascending" in the menu
|
|
315
|
+
const sort_asc_btn = Array.from(
|
|
316
|
+
document.querySelectorAll('[role="menuitem"]')
|
|
317
|
+
).find((el) => el.textContent?.includes("sort_ascending")) as HTMLElement;
|
|
318
|
+
expect(sort_asc_btn).toBeTruthy();
|
|
319
|
+
await fireEvent.click(sort_asc_btn);
|
|
320
|
+
await wait();
|
|
321
|
+
|
|
322
|
+
// After sorting by Age ascending, Bob (25) should be first visible row
|
|
323
|
+
const first_row = container.querySelector(".virtual-row");
|
|
324
|
+
const first_name_cell = first_row?.querySelector("[data-col='0']");
|
|
325
|
+
expect(first_name_cell?.textContent).toContain("Bob");
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
describe("Global search/filter", () => {
|
|
330
|
+
afterEach(() => cleanup());
|
|
331
|
+
|
|
332
|
+
test("search filters visible rows", async () => {
|
|
333
|
+
const { container } = await render(Dataframe, {
|
|
334
|
+
...default_props,
|
|
335
|
+
show_search: "search"
|
|
336
|
+
});
|
|
337
|
+
await wait();
|
|
338
|
+
|
|
339
|
+
const search_input = container.querySelector(
|
|
340
|
+
"input.search-input"
|
|
341
|
+
) as HTMLInputElement;
|
|
342
|
+
expect(search_input).not.toBeNull();
|
|
343
|
+
|
|
344
|
+
// Type a search query
|
|
345
|
+
search_input.focus();
|
|
346
|
+
await fireEvent.input(search_input, { target: { value: "Alice" } });
|
|
347
|
+
await wait(100);
|
|
348
|
+
|
|
349
|
+
const rows = get_rows(container);
|
|
350
|
+
expect(rows.length).toBe(1);
|
|
351
|
+
const first_cell = get_cell(container, 0, 0);
|
|
352
|
+
expect(first_cell?.textContent).toContain("Alice");
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test("clearing search restores all rows", async () => {
|
|
356
|
+
const { container } = await render(Dataframe, {
|
|
357
|
+
...default_props,
|
|
358
|
+
show_search: "search"
|
|
359
|
+
});
|
|
360
|
+
await wait();
|
|
361
|
+
|
|
362
|
+
const search_input = container.querySelector(
|
|
363
|
+
"input.search-input"
|
|
364
|
+
) as HTMLInputElement;
|
|
365
|
+
|
|
366
|
+
// Search then clear
|
|
367
|
+
await fireEvent.input(search_input, { target: { value: "Alice" } });
|
|
368
|
+
await wait(100);
|
|
369
|
+
expect(get_rows(container).length).toBe(1);
|
|
370
|
+
|
|
371
|
+
await fireEvent.input(search_input, { target: { value: "" } });
|
|
372
|
+
await wait(100);
|
|
373
|
+
expect(get_rows(container).length).toBe(3);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test("filter mode shows apply button when query present", async () => {
|
|
377
|
+
const { container } = await render(Dataframe, {
|
|
378
|
+
...default_props,
|
|
379
|
+
show_search: "filter"
|
|
380
|
+
});
|
|
381
|
+
await wait();
|
|
382
|
+
|
|
383
|
+
const filter_input = container.querySelector(
|
|
384
|
+
"input.search-input"
|
|
385
|
+
) as HTMLInputElement;
|
|
386
|
+
await fireEvent.input(filter_input, { target: { value: "Alice" } });
|
|
387
|
+
await wait();
|
|
388
|
+
|
|
389
|
+
const apply_btn = container.querySelector(
|
|
390
|
+
"button[aria-label='Apply filter and update dataframe values']"
|
|
391
|
+
);
|
|
392
|
+
expect(apply_btn).not.toBeNull();
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
describe("Add/remove rows and columns", () => {
|
|
397
|
+
afterEach(() => cleanup());
|
|
398
|
+
|
|
399
|
+
const dynamic_props = {
|
|
400
|
+
...default_props,
|
|
401
|
+
col_count: [3, "dynamic"] as [number, "fixed" | "dynamic"],
|
|
402
|
+
row_count: [3, "dynamic"] as [number, "fixed" | "dynamic"]
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
test("add row button appends a new row", async () => {
|
|
406
|
+
const { container } = await render(Dataframe, dynamic_props);
|
|
407
|
+
await wait();
|
|
408
|
+
|
|
409
|
+
const initial_rows = get_rows(container).length;
|
|
410
|
+
|
|
411
|
+
// The empty row button should be present for dynamic row_count
|
|
412
|
+
const add_btn = container.querySelector(".empty-row-button") as HTMLElement;
|
|
413
|
+
if (add_btn) {
|
|
414
|
+
await fireEvent.click(add_btn);
|
|
415
|
+
await wait();
|
|
416
|
+
|
|
417
|
+
expect(get_rows(container).length).toBe(initial_rows + 1);
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// Cell menu add row tests: The CellMenu renders outside the table-wrap parent,
|
|
422
|
+
// so the document click handler (handle_click_outside) unmounts it before
|
|
423
|
+
// the menu button's onclick fires in synthetic event dispatch. These
|
|
424
|
+
// interactions are covered by E2E tests in dataframe_events.spec.ts.
|
|
425
|
+
test.todo("add row above via cell menu");
|
|
426
|
+
test.todo("add row below via cell menu");
|
|
427
|
+
|
|
428
|
+
test("delete row via cell menu", async () => {
|
|
429
|
+
const { container } = await render(Dataframe, dynamic_props);
|
|
430
|
+
await wait();
|
|
431
|
+
|
|
432
|
+
const cell = get_cell(container, 1, 0)!;
|
|
433
|
+
await fireEvent.mouseDown(cell);
|
|
434
|
+
await wait();
|
|
435
|
+
|
|
436
|
+
const menu_btn = cell.querySelector(".cell-menu-button") as HTMLElement;
|
|
437
|
+
expect(menu_btn).toBeTruthy();
|
|
438
|
+
await fireEvent.click(menu_btn);
|
|
439
|
+
await wait();
|
|
440
|
+
|
|
441
|
+
const delete_btn = document.querySelector(
|
|
442
|
+
'[aria-label="Delete row"]'
|
|
443
|
+
) as HTMLElement;
|
|
444
|
+
expect(delete_btn).toBeTruthy();
|
|
445
|
+
await fireEvent.click(delete_btn);
|
|
446
|
+
await wait();
|
|
447
|
+
|
|
448
|
+
expect(get_rows(container).length).toBe(2);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
test("add column via header menu", async () => {
|
|
452
|
+
const { container } = await render(Dataframe, dynamic_props);
|
|
453
|
+
await wait();
|
|
454
|
+
|
|
455
|
+
const initial_headers = get_header_cells(container).length;
|
|
456
|
+
|
|
457
|
+
// Click a header to select it and show menu
|
|
458
|
+
const headers = get_header_cells(container);
|
|
459
|
+
const header = headers[0] as HTMLElement;
|
|
460
|
+
const menu_btn = header.querySelector(".cell-menu-button") as HTMLElement;
|
|
461
|
+
if (menu_btn) {
|
|
462
|
+
await fireEvent.click(menu_btn);
|
|
463
|
+
await wait();
|
|
464
|
+
|
|
465
|
+
const add_col_btn = document.querySelector(
|
|
466
|
+
'[aria-label="Add column to the right"]'
|
|
467
|
+
) as HTMLElement;
|
|
468
|
+
if (add_col_btn) {
|
|
469
|
+
await fireEvent.click(add_col_btn);
|
|
470
|
+
await wait();
|
|
471
|
+
|
|
472
|
+
expect(get_header_cells(container).length).toBe(initial_headers + 1);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
test("delete column via header menu", async () => {
|
|
478
|
+
const { container } = await render(Dataframe, dynamic_props);
|
|
479
|
+
await wait();
|
|
480
|
+
|
|
481
|
+
const initial_headers = get_header_cells(container).length;
|
|
482
|
+
|
|
483
|
+
const headers = get_header_cells(container);
|
|
484
|
+
const header = headers[0] as HTMLElement;
|
|
485
|
+
const menu_btn = header.querySelector(".cell-menu-button") as HTMLElement;
|
|
486
|
+
if (menu_btn) {
|
|
487
|
+
await fireEvent.click(menu_btn);
|
|
488
|
+
await wait();
|
|
489
|
+
|
|
490
|
+
const delete_col_btn = document.querySelector(
|
|
491
|
+
'[aria-label="Delete column"]'
|
|
492
|
+
) as HTMLElement;
|
|
493
|
+
if (delete_col_btn) {
|
|
494
|
+
await fireEvent.click(delete_col_btn);
|
|
495
|
+
await wait();
|
|
496
|
+
|
|
497
|
+
expect(get_header_cells(container).length).toBe(initial_headers - 1);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
describe("Delete/clear cells", () => {
|
|
504
|
+
afterEach(() => cleanup());
|
|
505
|
+
|
|
506
|
+
test("Delete key clears selected cell value", async () => {
|
|
507
|
+
const { container } = await render(Dataframe, default_props);
|
|
508
|
+
await wait();
|
|
509
|
+
|
|
510
|
+
const cell = get_cell(container, 0, 0)!;
|
|
511
|
+
await fireEvent.mouseDown(cell);
|
|
512
|
+
await wait();
|
|
513
|
+
|
|
514
|
+
const table_wrap = get_table_wrap(container)!;
|
|
515
|
+
// Press Escape first to ensure we're in selection mode, not edit mode
|
|
516
|
+
await fireEvent.keyDown(table_wrap, { key: "Escape" });
|
|
517
|
+
await fireEvent.keyDown(table_wrap, { key: "Delete" });
|
|
518
|
+
await wait();
|
|
519
|
+
|
|
520
|
+
// Cell should be empty
|
|
521
|
+
const cell_text = get_cell(container, 0, 0)?.textContent?.trim();
|
|
522
|
+
expect(cell_text === "" || cell_text === "⋮").toBe(true);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
test("Backspace key clears selected cell value", async () => {
|
|
526
|
+
const { container } = await render(Dataframe, default_props);
|
|
527
|
+
await wait();
|
|
528
|
+
|
|
529
|
+
const cell = get_cell(container, 0, 1)!;
|
|
530
|
+
await fireEvent.mouseDown(cell);
|
|
531
|
+
await wait();
|
|
532
|
+
|
|
533
|
+
const table_wrap = get_table_wrap(container)!;
|
|
534
|
+
await fireEvent.keyDown(table_wrap, { key: "Escape" });
|
|
535
|
+
await fireEvent.keyDown(table_wrap, { key: "Backspace" });
|
|
536
|
+
await wait();
|
|
537
|
+
|
|
538
|
+
const cell_text = get_cell(container, 0, 1)?.textContent?.trim();
|
|
539
|
+
expect(cell_text === "" || cell_text === "⋮").toBe(true);
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
test("Delete key does not clear static column cells", async () => {
|
|
543
|
+
const { container } = await render(Dataframe, {
|
|
544
|
+
...default_props,
|
|
545
|
+
static_columns: [0]
|
|
546
|
+
});
|
|
547
|
+
await wait();
|
|
548
|
+
|
|
549
|
+
const cell = get_cell(container, 0, 0)!;
|
|
550
|
+
await fireEvent.mouseDown(cell);
|
|
551
|
+
await wait();
|
|
552
|
+
|
|
553
|
+
const table_wrap = get_table_wrap(container)!;
|
|
554
|
+
await fireEvent.keyDown(table_wrap, { key: "Escape" });
|
|
555
|
+
await fireEvent.keyDown(table_wrap, { key: "Delete" });
|
|
556
|
+
await wait();
|
|
557
|
+
|
|
558
|
+
// Static cell should still have its value
|
|
559
|
+
expect(get_cell(container, 0, 0)?.textContent).toContain("Alice");
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
describe("Events", () => {
|
|
564
|
+
afterEach(() => cleanup());
|
|
565
|
+
|
|
566
|
+
test("change: emitted when value changes from outside via set_data", async () => {
|
|
567
|
+
const { listen, set_data } = await render(Dataframe, default_props);
|
|
568
|
+
await wait();
|
|
569
|
+
|
|
570
|
+
const change = listen("change");
|
|
571
|
+
|
|
572
|
+
await set_data({
|
|
17
573
|
value: {
|
|
18
574
|
data: [
|
|
575
|
+
["X", "Y", "Z"],
|
|
19
576
|
["1", "2", "3"],
|
|
20
577
|
["4", "5", "6"]
|
|
21
578
|
],
|
|
22
|
-
headers: ["
|
|
579
|
+
headers: ["Name", "Age", "Role"],
|
|
23
580
|
metadata: null
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
await wait();
|
|
584
|
+
|
|
585
|
+
expect(change).toHaveBeenCalled();
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
test("select: emitted when cell is clicked", async () => {
|
|
589
|
+
const { container, listen } = await render(Dataframe, default_props);
|
|
590
|
+
await wait();
|
|
591
|
+
|
|
592
|
+
const select = listen("select");
|
|
593
|
+
|
|
594
|
+
const cell = get_cell(container, 0, 0)!;
|
|
595
|
+
await fireEvent.mouseDown(cell);
|
|
596
|
+
await wait();
|
|
597
|
+
|
|
598
|
+
expect(select).toHaveBeenCalled();
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
test("change: emitted when cell is edited", async () => {
|
|
602
|
+
const { container, listen } = await render(Dataframe, default_props);
|
|
603
|
+
await wait();
|
|
604
|
+
|
|
605
|
+
const change = listen("change");
|
|
606
|
+
|
|
607
|
+
// Double-click to enter edit mode
|
|
608
|
+
const cell = get_cell(container, 0, 0)!;
|
|
609
|
+
await fireEvent.mouseDown(cell);
|
|
610
|
+
await tick();
|
|
611
|
+
await fireEvent.dblClick(cell);
|
|
612
|
+
await wait();
|
|
613
|
+
|
|
614
|
+
const textarea = container.querySelector(
|
|
615
|
+
"textarea[aria-label='Edit cell']"
|
|
616
|
+
) as HTMLTextAreaElement;
|
|
617
|
+
if (textarea) {
|
|
618
|
+
textarea.value = "NewValue";
|
|
619
|
+
await fireEvent.input(textarea);
|
|
620
|
+
await tick();
|
|
621
|
+
|
|
622
|
+
// Press Enter to confirm
|
|
623
|
+
const table_wrap = get_table_wrap(container)!;
|
|
624
|
+
await fireEvent.keyDown(table_wrap, { key: "Enter" });
|
|
625
|
+
await wait();
|
|
626
|
+
|
|
627
|
+
expect(change).toHaveBeenCalled();
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
test("input: emitted on cell edit", async () => {
|
|
632
|
+
const { container, listen } = await render(Dataframe, default_props);
|
|
633
|
+
await wait();
|
|
634
|
+
|
|
635
|
+
const input = listen("input");
|
|
636
|
+
|
|
637
|
+
const cell = get_cell(container, 0, 0)!;
|
|
638
|
+
await fireEvent.mouseDown(cell);
|
|
639
|
+
await tick();
|
|
640
|
+
await fireEvent.dblClick(cell);
|
|
641
|
+
await wait();
|
|
642
|
+
|
|
643
|
+
const textarea = container.querySelector(
|
|
644
|
+
"textarea[aria-label='Edit cell']"
|
|
645
|
+
) as HTMLTextAreaElement;
|
|
646
|
+
if (textarea) {
|
|
647
|
+
textarea.value = "Changed";
|
|
648
|
+
await fireEvent.input(textarea);
|
|
649
|
+
await tick();
|
|
650
|
+
|
|
651
|
+
const table_wrap = get_table_wrap(container)!;
|
|
652
|
+
await fireEvent.keyDown(table_wrap, { key: "Enter" });
|
|
653
|
+
await wait();
|
|
654
|
+
|
|
655
|
+
expect(input).toHaveBeenCalled();
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
test("edit: emitted with old and new value on cell edit", async () => {
|
|
660
|
+
const { container, listen } = await render(Dataframe, default_props);
|
|
661
|
+
await wait();
|
|
662
|
+
|
|
663
|
+
const edit = listen("edit");
|
|
664
|
+
|
|
665
|
+
const cell = get_cell(container, 0, 0)!;
|
|
666
|
+
await fireEvent.mouseDown(cell);
|
|
667
|
+
await tick();
|
|
668
|
+
await fireEvent.dblClick(cell);
|
|
669
|
+
await wait();
|
|
670
|
+
|
|
671
|
+
const textarea = container.querySelector(
|
|
672
|
+
"textarea[aria-label='Edit cell']"
|
|
673
|
+
) as HTMLTextAreaElement;
|
|
674
|
+
if (textarea) {
|
|
675
|
+
textarea.value = "Zara";
|
|
676
|
+
await fireEvent.input(textarea);
|
|
677
|
+
await tick();
|
|
678
|
+
|
|
679
|
+
const table_wrap = get_table_wrap(container)!;
|
|
680
|
+
await fireEvent.keyDown(table_wrap, { key: "Enter" });
|
|
681
|
+
await wait();
|
|
682
|
+
|
|
683
|
+
expect(edit).toHaveBeenCalled();
|
|
684
|
+
const call_args = edit.mock.calls[0][0];
|
|
685
|
+
expect(call_args.value).toBe("Zara");
|
|
686
|
+
expect(call_args.previous_value).toBe("Alice");
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
describe("Copy", () => {
|
|
692
|
+
afterEach(() => cleanup());
|
|
693
|
+
|
|
694
|
+
test("Cmd+C copies selected cell to clipboard", async () => {
|
|
695
|
+
const { container } = await render(Dataframe, default_props);
|
|
696
|
+
await wait();
|
|
697
|
+
|
|
698
|
+
const cell = get_cell(container, 0, 0)!;
|
|
699
|
+
await fireEvent.mouseDown(cell);
|
|
700
|
+
await wait();
|
|
701
|
+
|
|
702
|
+
const table_wrap = get_table_wrap(container)!;
|
|
703
|
+
await fireEvent.keyDown(table_wrap, { key: "c", metaKey: true });
|
|
704
|
+
await wait(100);
|
|
705
|
+
|
|
706
|
+
const clipboard = await navigator.clipboard.readText();
|
|
707
|
+
expect(clipboard).toBe("Alice");
|
|
43
708
|
});
|
|
44
709
|
});
|