@gradio/dataframe 0.24.2 → 0.24.4
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 +33 -0
- package/Dataframe.test.ts +497 -43
- package/Example.svelte +19 -10
- package/dist/Example.svelte +19 -10
- package/dist/Example.svelte.d.ts +4 -18
- package/dist/shared/BooleanCell.svelte +1 -0
- package/dist/shared/CellMenuButton.svelte +8 -1
- package/dist/shared/CellMenuButton.svelte.d.ts +1 -0
- package/dist/shared/DataCell.svelte +26 -2
- package/dist/shared/DataCell.svelte.d.ts +4 -0
- package/dist/shared/EditableCell.svelte +6 -3
- package/dist/shared/Example.svelte +1 -1
- package/dist/shared/Example.svelte.d.ts +4 -18
- package/dist/shared/HeaderCell.svelte +17 -3
- package/dist/shared/HeaderCell.svelte.d.ts +3 -1
- package/dist/shared/Table.svelte +374 -140
- package/dist/shared/icons/SelectionButtons.svelte +1 -0
- package/dist/shared/tanstack/virtual.svelte.js +2 -2
- package/dist/standalone/stubs/Upload.svelte +12 -5
- package/dist/standalone/stubs/Upload.svelte.d.ts +8 -34
- package/package.json +8 -8
- package/shared/BooleanCell.svelte +1 -0
- package/shared/CellMenuButton.svelte +8 -1
- package/shared/DataCell.svelte +26 -2
- package/shared/EditableCell.svelte +6 -3
- package/shared/Example.svelte +1 -1
- package/shared/HeaderCell.svelte +17 -3
- package/shared/Table.svelte +374 -140
- package/shared/icons/SelectionButtons.svelte +1 -0
- package/shared/tanstack/virtual.svelte.ts +2 -2
- package/standalone/stubs/Upload.svelte +12 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# @gradio/dataframe
|
|
2
2
|
|
|
3
|
+
## 0.24.4
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
|
|
7
|
+
- [#13830](https://github.com/gradio-app/gradio/pull/13830) [`c398576`](https://github.com/gradio-app/gradio/commit/c39857647609361ea6548972ced702861b775b1f) - Restore the Dataframe's outer border. Thanks @hysts!
|
|
8
|
+
- [#13729](https://github.com/gradio-app/gradio/pull/13729) [`82ca180`](https://github.com/gradio-app/gradio/commit/82ca180c5c9a77d36b9097a62e63318317dc9d1f) - Fix Dataframe keyboard navigation accessibility. Thanks @dawoodkhan82!
|
|
9
|
+
|
|
10
|
+
### Dependency updates
|
|
11
|
+
|
|
12
|
+
- @gradio/client@2.6.0
|
|
13
|
+
- @gradio/atoms@0.26.2
|
|
14
|
+
- @gradio/utils@0.14.1
|
|
15
|
+
- @gradio/upload@0.18.3
|
|
16
|
+
- @gradio/checkbox@0.8.3
|
|
17
|
+
|
|
18
|
+
## 0.24.3
|
|
19
|
+
|
|
20
|
+
### Dependency updates
|
|
21
|
+
|
|
22
|
+
- @gradio/client@2.5.1
|
|
23
|
+
|
|
24
|
+
## 0.24.3
|
|
25
|
+
|
|
26
|
+
### Fixes
|
|
27
|
+
|
|
28
|
+
- [#13744](https://github.com/gradio-app/gradio/pull/13744) [`ca9dec0`](https://github.com/gradio-app/gradio/commit/ca9dec0e2e07d33cf3551f5062ff35ab9acf3026) - Re-read the Dataframe virtual window whenever the row count changes. Thanks @abidlabs!
|
|
29
|
+
|
|
30
|
+
## 0.24.2
|
|
31
|
+
|
|
32
|
+
### Dependency updates
|
|
33
|
+
|
|
34
|
+
- @gradio/client@2.5.0
|
|
35
|
+
|
|
3
36
|
## 0.24.2
|
|
4
37
|
|
|
5
38
|
### Fixes
|
package/Dataframe.test.ts
CHANGED
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
within
|
|
8
8
|
} from "@self/tootils/render";
|
|
9
9
|
import { tick } from "svelte";
|
|
10
|
+
import event from "@testing-library/user-event";
|
|
11
|
+
import { run_shared_prop_tests } from "@self/tootils/shared-prop-tests";
|
|
10
12
|
|
|
11
13
|
import Dataframe from "./Index.svelte";
|
|
12
14
|
|
|
@@ -37,6 +39,14 @@ const default_props = {
|
|
|
37
39
|
max_height: 500
|
|
38
40
|
};
|
|
39
41
|
|
|
42
|
+
run_shared_prop_tests({
|
|
43
|
+
component: Dataframe,
|
|
44
|
+
name: "Dataframe",
|
|
45
|
+
base_props: default_props,
|
|
46
|
+
has_label: false,
|
|
47
|
+
has_validation_error: false
|
|
48
|
+
});
|
|
49
|
+
|
|
40
50
|
function get_cell(container: HTMLElement, row: number, col: number) {
|
|
41
51
|
return container.querySelector(
|
|
42
52
|
`[data-row='${row}'][data-col='${col}']`
|
|
@@ -96,8 +106,36 @@ describe("Dataframe rendering", () => {
|
|
|
96
106
|
expect(rows.length).toBe(3);
|
|
97
107
|
});
|
|
98
108
|
|
|
109
|
+
test("resets inherited table padding so headers align with body cells", async () => {
|
|
110
|
+
const global_style = document.createElement("style");
|
|
111
|
+
global_style.textContent = "table { padding: 10px; }";
|
|
112
|
+
document.head.appendChild(global_style);
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const { container } = await render(Dataframe, default_props);
|
|
116
|
+
await wait();
|
|
117
|
+
|
|
118
|
+
const header_table = container.querySelector(
|
|
119
|
+
".header-table"
|
|
120
|
+
) as HTMLTableElement;
|
|
121
|
+
const first_header = get_header_cells(container)[0];
|
|
122
|
+
const first_body_cell = get_cell(container, 0, 0)!;
|
|
123
|
+
const header_rect = first_header.getBoundingClientRect();
|
|
124
|
+
const body_rect = first_body_cell.getBoundingClientRect();
|
|
125
|
+
|
|
126
|
+
expect(getComputedStyle(header_table).padding).toBe("0px");
|
|
127
|
+
expect(header_table.getBoundingClientRect().height).toBe(
|
|
128
|
+
header_rect.height
|
|
129
|
+
);
|
|
130
|
+
expect(header_rect.left).toBeCloseTo(body_rect.left);
|
|
131
|
+
expect(header_rect.right).toBeCloseTo(body_rect.right);
|
|
132
|
+
} finally {
|
|
133
|
+
global_style.remove();
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
99
137
|
test("renders label when show_label is true", async () => {
|
|
100
|
-
const { container } = await render(Dataframe, {
|
|
138
|
+
const { container, getByRole } = await render(Dataframe, {
|
|
101
139
|
...default_props,
|
|
102
140
|
label: "Test Table",
|
|
103
141
|
show_label: true
|
|
@@ -105,6 +143,9 @@ describe("Dataframe rendering", () => {
|
|
|
105
143
|
await wait();
|
|
106
144
|
|
|
107
145
|
expect(container.textContent).toContain("Test Table");
|
|
146
|
+
const grid = getByRole("grid");
|
|
147
|
+
expect(grid).toHaveAttribute("aria-label", "Test Table");
|
|
148
|
+
expect(grid.querySelector(".sr-only")).not.toBeInTheDocument();
|
|
108
149
|
});
|
|
109
150
|
|
|
110
151
|
test("renders row numbers when show_row_numbers is true", async () => {
|
|
@@ -116,6 +157,47 @@ describe("Dataframe rendering", () => {
|
|
|
116
157
|
|
|
117
158
|
const row_number_cells = container.querySelectorAll(".row-number-cell");
|
|
118
159
|
expect(row_number_cells.length).toBeGreaterThan(0);
|
|
160
|
+
expect(get_table_wrap(container)).toHaveAttribute("aria-colcount", "4");
|
|
161
|
+
expect(container.querySelector(".row-number-header")).toHaveAttribute(
|
|
162
|
+
"aria-colindex",
|
|
163
|
+
"1"
|
|
164
|
+
);
|
|
165
|
+
expect(get_header_cells(container)[0]).toHaveAttribute(
|
|
166
|
+
"aria-colindex",
|
|
167
|
+
"2"
|
|
168
|
+
);
|
|
169
|
+
const row_header = container.querySelector('[role="rowheader"]');
|
|
170
|
+
expect(row_header).toHaveAttribute("aria-colindex", "1");
|
|
171
|
+
expect(get_cell(container, 0, 0)).toHaveAttribute("aria-colindex", "2");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("exposes grid rows through presentational layout wrappers", async () => {
|
|
175
|
+
const { getByRole } = await render(Dataframe, default_props);
|
|
176
|
+
await wait();
|
|
177
|
+
|
|
178
|
+
const grid = getByRole("grid");
|
|
179
|
+
const upload_container = grid.querySelector<HTMLElement>(
|
|
180
|
+
":scope > .upload-container"
|
|
181
|
+
)!;
|
|
182
|
+
const viewport = upload_container.querySelector<HTMLElement>(
|
|
183
|
+
":scope > .virtual-table-viewport"
|
|
184
|
+
)!;
|
|
185
|
+
const header_table = viewport.querySelector<HTMLElement>(
|
|
186
|
+
":scope > .header-table"
|
|
187
|
+
)!;
|
|
188
|
+
const header_rowgroup = header_table.querySelector(":scope > thead");
|
|
189
|
+
const virtual_body = viewport.querySelector<HTMLElement>(
|
|
190
|
+
":scope > .virtual-body"
|
|
191
|
+
)!;
|
|
192
|
+
|
|
193
|
+
expect(upload_container).toHaveAttribute("role", "none");
|
|
194
|
+
expect(upload_container).not.toHaveAttribute("tabindex");
|
|
195
|
+
expect(upload_container).not.toHaveAttribute("aria-label");
|
|
196
|
+
expect(upload_container).not.toHaveAttribute("aria-dropeffect");
|
|
197
|
+
expect(viewport).toHaveAttribute("role", "none");
|
|
198
|
+
expect(header_table).toHaveAttribute("role", "none");
|
|
199
|
+
expect(header_rowgroup).toHaveAttribute("role", "rowgroup");
|
|
200
|
+
expect(virtual_body).toHaveAttribute("role", "none");
|
|
119
201
|
});
|
|
120
202
|
|
|
121
203
|
test("renders search input when show_search is 'search'", async () => {
|
|
@@ -256,22 +338,6 @@ describe("Cell selection", () => {
|
|
|
256
338
|
expect(next_cell.className).toContain("cell-selected");
|
|
257
339
|
});
|
|
258
340
|
|
|
259
|
-
test("Tab moves to next cell", async () => {
|
|
260
|
-
const { container } = await render(Dataframe, default_props);
|
|
261
|
-
await wait();
|
|
262
|
-
|
|
263
|
-
const cell = get_cell(container, 0, 0)!;
|
|
264
|
-
await fireEvent.mouseDown(cell);
|
|
265
|
-
await wait();
|
|
266
|
-
|
|
267
|
-
const table_wrap = get_table_wrap(container)!;
|
|
268
|
-
await fireEvent.keyDown(table_wrap, { key: "Tab" });
|
|
269
|
-
await wait();
|
|
270
|
-
|
|
271
|
-
const next_cell = get_cell(container, 0, 1)!;
|
|
272
|
-
expect(next_cell.className).toContain("cell-selected");
|
|
273
|
-
});
|
|
274
|
-
|
|
275
341
|
test("shift+click selects a range", async () => {
|
|
276
342
|
const { container } = await render(Dataframe, default_props);
|
|
277
343
|
await wait();
|
|
@@ -317,6 +383,389 @@ describe("Cell selection", () => {
|
|
|
317
383
|
});
|
|
318
384
|
});
|
|
319
385
|
|
|
386
|
+
describe("Keyboard accessibility", () => {
|
|
387
|
+
const external_buttons: HTMLButtonElement[] = [];
|
|
388
|
+
|
|
389
|
+
function append_external_button(text: string): HTMLButtonElement {
|
|
390
|
+
const button = document.createElement("button");
|
|
391
|
+
button.textContent = text;
|
|
392
|
+
document.body.appendChild(button);
|
|
393
|
+
external_buttons.push(button);
|
|
394
|
+
return button;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
afterEach(() => {
|
|
398
|
+
cleanup();
|
|
399
|
+
external_buttons.forEach((button) => button.remove());
|
|
400
|
+
external_buttons.length = 0;
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
const navigation_props = {
|
|
404
|
+
...default_props,
|
|
405
|
+
buttons: [] as string[]
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
test("Tab bypasses the grid container and enters on a single active cell", async () => {
|
|
409
|
+
const before = append_external_button("Before dataframe");
|
|
410
|
+
|
|
411
|
+
const { getByRole, getByTestId } = await render(
|
|
412
|
+
Dataframe,
|
|
413
|
+
navigation_props
|
|
414
|
+
);
|
|
415
|
+
const first_cell = await waitFor(() => getByTestId("cell-0-0"));
|
|
416
|
+
const second_cell = getByTestId("cell-0-1");
|
|
417
|
+
|
|
418
|
+
before.focus();
|
|
419
|
+
await event.tab();
|
|
420
|
+
|
|
421
|
+
expect(first_cell).toHaveFocus();
|
|
422
|
+
expect(first_cell).toHaveAttribute("tabindex", "0");
|
|
423
|
+
expect(second_cell).toHaveAttribute("tabindex", "-1");
|
|
424
|
+
const grid = getByRole("grid");
|
|
425
|
+
expect(grid).toHaveAttribute("tabindex", "-1");
|
|
426
|
+
expect(grid).not.toHaveFocus();
|
|
427
|
+
expect(grid).toHaveAttribute("aria-rowcount", "4");
|
|
428
|
+
expect(grid).toHaveAttribute("aria-colcount", "3");
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
test("Tab and Shift+Tab leave the grid in navigation mode", async () => {
|
|
432
|
+
const before = append_external_button("Before dataframe");
|
|
433
|
+
|
|
434
|
+
const { getByTestId } = await render(Dataframe, navigation_props);
|
|
435
|
+
const after = append_external_button("After dataframe");
|
|
436
|
+
const first_cell = await waitFor(() => getByTestId("cell-0-0"));
|
|
437
|
+
|
|
438
|
+
first_cell.focus();
|
|
439
|
+
await event.tab();
|
|
440
|
+
expect(after).toHaveFocus();
|
|
441
|
+
|
|
442
|
+
first_cell.focus();
|
|
443
|
+
await event.tab({ shift: true });
|
|
444
|
+
expect(before).toHaveFocus();
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test("arrow keys move DOM focus between cells and headers", async () => {
|
|
448
|
+
const { getByTestId } = await render(Dataframe, navigation_props);
|
|
449
|
+
const first_cell = await waitFor(() => getByTestId("cell-0-0"));
|
|
450
|
+
const second_cell = getByTestId("cell-0-1");
|
|
451
|
+
const second_header = getByTestId("header-1");
|
|
452
|
+
|
|
453
|
+
first_cell.focus();
|
|
454
|
+
await event.keyboard("{ArrowRight}");
|
|
455
|
+
expect(second_cell).toHaveFocus();
|
|
456
|
+
|
|
457
|
+
await event.keyboard("{ArrowUp}");
|
|
458
|
+
expect(second_header).toHaveFocus();
|
|
459
|
+
|
|
460
|
+
await event.keyboard("{ArrowDown}");
|
|
461
|
+
expect(second_cell).toHaveFocus();
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
test("Space does not change a static boolean column from its header", async () => {
|
|
465
|
+
const { getByTestId, listen } = await render(Dataframe, {
|
|
466
|
+
...navigation_props,
|
|
467
|
+
value: {
|
|
468
|
+
data: [[true], [false], [true]],
|
|
469
|
+
headers: ["Flag"],
|
|
470
|
+
metadata: null
|
|
471
|
+
},
|
|
472
|
+
datatype: ["bool"] as const,
|
|
473
|
+
static_columns: [0],
|
|
474
|
+
col_count: [1, "fixed"] as [number, "fixed"],
|
|
475
|
+
row_count: [3, "fixed"] as [number, "fixed"]
|
|
476
|
+
});
|
|
477
|
+
const change = listen("change");
|
|
478
|
+
const cell = await waitFor(() => getByTestId("cell-0-0"));
|
|
479
|
+
const header = getByTestId("header-0");
|
|
480
|
+
|
|
481
|
+
cell.focus();
|
|
482
|
+
await event.keyboard("{ArrowUp}");
|
|
483
|
+
expect(header).toHaveFocus();
|
|
484
|
+
await event.keyboard(" ");
|
|
485
|
+
|
|
486
|
+
expect(change).not.toHaveBeenCalled();
|
|
487
|
+
expect(header).toHaveFocus();
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
test("static headers named by label never enter edit mode", async () => {
|
|
491
|
+
const { getByTestId, queryByRole } = await render(Dataframe, {
|
|
492
|
+
...navigation_props,
|
|
493
|
+
static_columns: ["Name"]
|
|
494
|
+
});
|
|
495
|
+
const cell = await waitFor(() => getByTestId("cell-0-0"));
|
|
496
|
+
const header = getByTestId("header-0");
|
|
497
|
+
|
|
498
|
+
cell.focus();
|
|
499
|
+
await event.keyboard("{ArrowUp}{Enter}");
|
|
500
|
+
expect(queryByRole("textbox", { name: "Cell is read-only" })).toBeNull();
|
|
501
|
+
expect(header).toHaveFocus();
|
|
502
|
+
|
|
503
|
+
await fireEvent.click(header);
|
|
504
|
+
await waitFor(() => expect(header).toHaveFocus());
|
|
505
|
+
expect(queryByRole("textbox", { name: "Cell is read-only" })).toBeNull();
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
test("clicking a read-only header selects and focuses it", async () => {
|
|
509
|
+
const { getByTestId, queryByRole } = await render(Dataframe, {
|
|
510
|
+
...navigation_props,
|
|
511
|
+
interactive: false,
|
|
512
|
+
editable: false
|
|
513
|
+
});
|
|
514
|
+
const header = await waitFor(() => getByTestId("header-0"));
|
|
515
|
+
|
|
516
|
+
await fireEvent.click(header);
|
|
517
|
+
|
|
518
|
+
await waitFor(() => expect(header).toHaveFocus());
|
|
519
|
+
expect(header).toHaveAttribute("tabindex", "0");
|
|
520
|
+
expect(queryByRole("textbox")).toBeNull();
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
test("restores a cell tab stop when the selected header is removed", async () => {
|
|
524
|
+
const result = await render(Dataframe, navigation_props);
|
|
525
|
+
const last_cell = await waitFor(() => result.getByTestId("cell-0-2"));
|
|
526
|
+
|
|
527
|
+
last_cell.focus();
|
|
528
|
+
await event.keyboard("{ArrowUp}");
|
|
529
|
+
expect(result.getByTestId("header-2")).toHaveFocus();
|
|
530
|
+
|
|
531
|
+
await result.set_data({
|
|
532
|
+
value: {
|
|
533
|
+
data: [["Alice"], ["Bob"], ["Carol"]],
|
|
534
|
+
headers: ["Name"],
|
|
535
|
+
metadata: null
|
|
536
|
+
},
|
|
537
|
+
col_count: [1, "fixed"]
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
const first_cell = await waitFor(() => result.getByTestId("cell-0-0"));
|
|
541
|
+
expect(first_cell).toHaveAttribute("tabindex", "0");
|
|
542
|
+
expect(
|
|
543
|
+
result.getByRole("grid").querySelectorAll('[tabindex="0"]')
|
|
544
|
+
).toHaveLength(1);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
test("link key events are handled by the link instead of grid navigation", async () => {
|
|
548
|
+
const { getByTestId, queryByRole } = await render(Dataframe, {
|
|
549
|
+
...navigation_props,
|
|
550
|
+
value: {
|
|
551
|
+
data: [['<a href="#model">Model</a>', "Details"]],
|
|
552
|
+
headers: ["Model", "Details"],
|
|
553
|
+
metadata: null
|
|
554
|
+
},
|
|
555
|
+
datatype: ["markdown", "str"] as const,
|
|
556
|
+
col_count: [2, "fixed"] as [number, "fixed"],
|
|
557
|
+
row_count: [1, "fixed"] as [number, "fixed"]
|
|
558
|
+
});
|
|
559
|
+
const first_cell = await waitFor(() => getByTestId("cell-0-0"));
|
|
560
|
+
const second_cell = getByTestId("cell-0-1");
|
|
561
|
+
const link = await waitFor(() => within(first_cell).getByRole("link"));
|
|
562
|
+
|
|
563
|
+
first_cell.focus();
|
|
564
|
+
await event.tab();
|
|
565
|
+
expect(link).toHaveFocus();
|
|
566
|
+
|
|
567
|
+
await fireEvent.keyDown(link, { key: "Enter" });
|
|
568
|
+
expect(queryByRole("textbox", { name: "Edit cell" })).toBeNull();
|
|
569
|
+
expect(link).toHaveFocus();
|
|
570
|
+
|
|
571
|
+
await fireEvent.keyDown(link, { key: "ArrowRight" });
|
|
572
|
+
expect(link).toHaveFocus();
|
|
573
|
+
expect(second_cell).not.toHaveFocus();
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
test("Home and End move to row and grid boundaries", async () => {
|
|
577
|
+
const { getByTestId } = await render(Dataframe, navigation_props);
|
|
578
|
+
const first_cell = await waitFor(() => getByTestId("cell-0-0"));
|
|
579
|
+
|
|
580
|
+
first_cell.focus();
|
|
581
|
+
await event.keyboard("{End}");
|
|
582
|
+
expect(getByTestId("cell-0-2")).toHaveFocus();
|
|
583
|
+
|
|
584
|
+
await event.keyboard("{Control>}{End}{/Control}");
|
|
585
|
+
expect(getByTestId("cell-2-2")).toHaveFocus();
|
|
586
|
+
|
|
587
|
+
await event.keyboard("{Home}");
|
|
588
|
+
expect(getByTestId("cell-2-0")).toHaveFocus();
|
|
589
|
+
|
|
590
|
+
await event.keyboard("{Control>}{Home}{/Control}");
|
|
591
|
+
expect(first_cell).toHaveFocus();
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
test("Enter activates the selected cell, enters editing, and commits back to grid navigation", async () => {
|
|
595
|
+
const { getByRole, getByTestId, queryByRole, listen } = await render(
|
|
596
|
+
Dataframe,
|
|
597
|
+
navigation_props
|
|
598
|
+
);
|
|
599
|
+
const select = listen("select");
|
|
600
|
+
const first_cell = await waitFor(() => getByTestId("cell-0-0"));
|
|
601
|
+
|
|
602
|
+
first_cell.focus();
|
|
603
|
+
await event.keyboard("{Enter}");
|
|
604
|
+
|
|
605
|
+
const editor = getByRole("textbox", { name: "Edit cell" });
|
|
606
|
+
await waitFor(() => expect(editor).toHaveFocus());
|
|
607
|
+
expect(select).toHaveBeenCalledWith({
|
|
608
|
+
index: [0, 0],
|
|
609
|
+
value: "Alice",
|
|
610
|
+
row_value: ["Alice", "30", "Engineer"],
|
|
611
|
+
col_value: ["Alice", "Bob", "Carol"]
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
await event.keyboard("{Enter}");
|
|
615
|
+
expect(
|
|
616
|
+
queryByRole("textbox", { name: "Edit cell" })
|
|
617
|
+
).not.toBeInTheDocument();
|
|
618
|
+
expect(first_cell).toHaveFocus();
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
test("Space activates a cell without entering edit mode", async () => {
|
|
622
|
+
const { getByTestId, queryByRole, listen } = await render(
|
|
623
|
+
Dataframe,
|
|
624
|
+
navigation_props
|
|
625
|
+
);
|
|
626
|
+
const select = listen("select");
|
|
627
|
+
const first_cell = await waitFor(() => getByTestId("cell-0-0"));
|
|
628
|
+
|
|
629
|
+
first_cell.focus();
|
|
630
|
+
await event.keyboard(" ");
|
|
631
|
+
|
|
632
|
+
expect(select).toHaveBeenCalledTimes(1);
|
|
633
|
+
expect(
|
|
634
|
+
queryByRole("textbox", { name: "Edit cell" })
|
|
635
|
+
).not.toBeInTheDocument();
|
|
636
|
+
expect(first_cell).toHaveFocus();
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
test("Space toggles an editable boolean cell", async () => {
|
|
640
|
+
const { getByTestId } = await render(Dataframe, {
|
|
641
|
+
...navigation_props,
|
|
642
|
+
value: {
|
|
643
|
+
data: [[false]],
|
|
644
|
+
headers: ["Enabled"],
|
|
645
|
+
metadata: null
|
|
646
|
+
},
|
|
647
|
+
datatype: ["bool"] as const,
|
|
648
|
+
col_count: [1, "fixed"] as [number, "fixed"],
|
|
649
|
+
row_count: [1, "fixed"] as [number, "fixed"]
|
|
650
|
+
});
|
|
651
|
+
const cell = await waitFor(() => getByTestId("cell-0-0"));
|
|
652
|
+
const checkbox = within(cell).getByTestId("checkbox");
|
|
653
|
+
|
|
654
|
+
cell.focus();
|
|
655
|
+
await event.keyboard(" ");
|
|
656
|
+
|
|
657
|
+
await waitFor(() => expect(checkbox).toBeChecked());
|
|
658
|
+
expect(cell).toHaveFocus();
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
test("F2 toggles edit mode and Escape returns to grid navigation", async () => {
|
|
662
|
+
const { getByRole, getByTestId, queryByRole } = await render(
|
|
663
|
+
Dataframe,
|
|
664
|
+
navigation_props
|
|
665
|
+
);
|
|
666
|
+
const first_cell = await waitFor(() => getByTestId("cell-0-0"));
|
|
667
|
+
|
|
668
|
+
first_cell.focus();
|
|
669
|
+
await event.keyboard("{F2}");
|
|
670
|
+
await waitFor(() =>
|
|
671
|
+
expect(getByRole("textbox", { name: "Edit cell" })).toHaveFocus()
|
|
672
|
+
);
|
|
673
|
+
await event.keyboard("{F2}");
|
|
674
|
+
expect(
|
|
675
|
+
queryByRole("textbox", { name: "Edit cell" })
|
|
676
|
+
).not.toBeInTheDocument();
|
|
677
|
+
expect(first_cell).toHaveFocus();
|
|
678
|
+
|
|
679
|
+
await event.keyboard("{F2}");
|
|
680
|
+
await waitFor(() =>
|
|
681
|
+
expect(getByRole("textbox", { name: "Edit cell" })).toHaveFocus()
|
|
682
|
+
);
|
|
683
|
+
|
|
684
|
+
await event.keyboard("{Escape}");
|
|
685
|
+
expect(
|
|
686
|
+
queryByRole("textbox", { name: "Edit cell" })
|
|
687
|
+
).not.toBeInTheDocument();
|
|
688
|
+
expect(first_cell).toHaveFocus();
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
test("Tab moves between cells while editing and leaves at the grid boundary", async () => {
|
|
692
|
+
const before = append_external_button("Before dataframe");
|
|
693
|
+
const { getByRole, getByTestId } = await render(
|
|
694
|
+
Dataframe,
|
|
695
|
+
navigation_props
|
|
696
|
+
);
|
|
697
|
+
const after = append_external_button("After dataframe");
|
|
698
|
+
const first_cell = await waitFor(() => getByTestId("cell-0-0"));
|
|
699
|
+
const last_cell = getByTestId("cell-2-2");
|
|
700
|
+
|
|
701
|
+
first_cell.focus();
|
|
702
|
+
await event.keyboard("{Enter}");
|
|
703
|
+
await waitFor(() =>
|
|
704
|
+
expect(getByRole("textbox", { name: "Edit cell" })).toHaveFocus()
|
|
705
|
+
);
|
|
706
|
+
await event.tab();
|
|
707
|
+
await waitFor(() =>
|
|
708
|
+
expect(getByRole("textbox", { name: "Edit cell" })).toHaveFocus()
|
|
709
|
+
);
|
|
710
|
+
expect(getByTestId("cell-0-1")).toHaveAttribute("tabindex", "0");
|
|
711
|
+
|
|
712
|
+
last_cell.focus();
|
|
713
|
+
await event.keyboard("{Enter}");
|
|
714
|
+
await event.tab();
|
|
715
|
+
expect(after).toHaveFocus();
|
|
716
|
+
|
|
717
|
+
first_cell.focus();
|
|
718
|
+
await event.keyboard("{Enter}");
|
|
719
|
+
await waitFor(() =>
|
|
720
|
+
expect(getByRole("textbox", { name: "Edit cell" })).toHaveFocus()
|
|
721
|
+
);
|
|
722
|
+
await event.tab({ shift: true });
|
|
723
|
+
expect(before).toHaveFocus();
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
test("arrow navigation follows visible filtered rows while select reports original indices", async () => {
|
|
727
|
+
const filtered_props = {
|
|
728
|
+
...navigation_props,
|
|
729
|
+
value: {
|
|
730
|
+
data: [["match one"], ["skip"], ["match two"]],
|
|
731
|
+
headers: ["Value"],
|
|
732
|
+
metadata: null
|
|
733
|
+
},
|
|
734
|
+
col_count: [1, "fixed"] as [number, "fixed"],
|
|
735
|
+
row_count: [3, "fixed"] as [number, "fixed"],
|
|
736
|
+
show_search: "search" as const
|
|
737
|
+
};
|
|
738
|
+
const { getByPlaceholderText, getByTestId, queryByTestId, listen } =
|
|
739
|
+
await render(Dataframe, filtered_props);
|
|
740
|
+
const search = getByPlaceholderText("Search...");
|
|
741
|
+
|
|
742
|
+
await event.type(search, "match");
|
|
743
|
+
await waitFor(() => {
|
|
744
|
+
expect(queryByTestId("cell-1-0")).not.toBeInTheDocument();
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
const first_match = getByTestId("cell-0-0");
|
|
748
|
+
const second_match = getByTestId("cell-2-0");
|
|
749
|
+
first_match.focus();
|
|
750
|
+
await event.keyboard("{ArrowDown}");
|
|
751
|
+
expect(second_match).toHaveFocus();
|
|
752
|
+
expect(second_match).toHaveAttribute("aria-rowindex", "3");
|
|
753
|
+
|
|
754
|
+
const select = listen("select");
|
|
755
|
+
await event.keyboard(" ");
|
|
756
|
+
expect(select).toHaveBeenCalledWith({
|
|
757
|
+
index: [2, 0],
|
|
758
|
+
value: "match two",
|
|
759
|
+
row_value: ["match two"],
|
|
760
|
+
col_value: ["match one", "skip", "match two"]
|
|
761
|
+
});
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
test.todo(
|
|
765
|
+
"VISUAL: keyboard focus shows a visible outline around the active Dataframe cell — needs Playwright visual regression screenshot comparison"
|
|
766
|
+
);
|
|
767
|
+
});
|
|
768
|
+
|
|
320
769
|
describe("Header overflow", () => {
|
|
321
770
|
let wrapper: HTMLDivElement;
|
|
322
771
|
|
|
@@ -487,20 +936,23 @@ describe("Add/remove rows and columns", () => {
|
|
|
487
936
|
row_count: [3, "dynamic"] as [number, "fixed" | "dynamic"]
|
|
488
937
|
};
|
|
489
938
|
|
|
490
|
-
test("add row button appends a
|
|
491
|
-
const { container } = await render(Dataframe,
|
|
939
|
+
test("add row button appends a row and focuses its first cell", async () => {
|
|
940
|
+
const { container, getByRole, getByTestId } = await render(Dataframe, {
|
|
941
|
+
...dynamic_props,
|
|
942
|
+
value: {
|
|
943
|
+
data: [],
|
|
944
|
+
headers: default_props.value.headers,
|
|
945
|
+
metadata: null
|
|
946
|
+
},
|
|
947
|
+
row_count: [0, "dynamic"] as [number, "dynamic"]
|
|
948
|
+
});
|
|
492
949
|
await wait();
|
|
493
950
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
// The empty row button should be present for dynamic row_count
|
|
497
|
-
const add_btn = container.querySelector(".empty-row-button") as HTMLElement;
|
|
498
|
-
if (add_btn) {
|
|
499
|
-
await fireEvent.click(add_btn);
|
|
500
|
-
await wait();
|
|
951
|
+
await fireEvent.click(getByRole("button", { name: "Add row" }));
|
|
501
952
|
|
|
502
|
-
|
|
503
|
-
|
|
953
|
+
const added_cell = await waitFor(() => getByTestId("cell-0-0"));
|
|
954
|
+
expect(get_rows(container)).toHaveLength(1);
|
|
955
|
+
expect(added_cell).toHaveFocus();
|
|
504
956
|
});
|
|
505
957
|
|
|
506
958
|
// Cell menu add row tests: The CellMenu renders outside the table-wrap parent,
|
|
@@ -511,7 +963,7 @@ describe("Add/remove rows and columns", () => {
|
|
|
511
963
|
test.todo("add row below via cell menu");
|
|
512
964
|
|
|
513
965
|
test("delete row via cell menu", async () => {
|
|
514
|
-
const { container } = await render(Dataframe, dynamic_props);
|
|
966
|
+
const { container, getByTestId } = await render(Dataframe, dynamic_props);
|
|
515
967
|
await wait();
|
|
516
968
|
|
|
517
969
|
const cell = get_cell(container, 1, 0)!;
|
|
@@ -531,10 +983,11 @@ describe("Add/remove rows and columns", () => {
|
|
|
531
983
|
await wait();
|
|
532
984
|
|
|
533
985
|
expect(get_rows(container).length).toBe(2);
|
|
986
|
+
await waitFor(() => expect(getByTestId("cell-1-0")).toHaveFocus());
|
|
534
987
|
});
|
|
535
988
|
|
|
536
989
|
test("add column via header menu", async () => {
|
|
537
|
-
const { container } = await render(Dataframe, dynamic_props);
|
|
990
|
+
const { container, getByTestId } = await render(Dataframe, dynamic_props);
|
|
538
991
|
await wait();
|
|
539
992
|
|
|
540
993
|
const initial_headers = get_header_cells(container).length;
|
|
@@ -543,24 +996,24 @@ describe("Add/remove rows and columns", () => {
|
|
|
543
996
|
const headers = get_header_cells(container);
|
|
544
997
|
const header = headers[0] as HTMLElement;
|
|
545
998
|
const menu_btn = header.querySelector(".cell-menu-button") as HTMLElement;
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
999
|
+
expect(menu_btn).toBeTruthy();
|
|
1000
|
+
await fireEvent.click(menu_btn);
|
|
1001
|
+
await wait();
|
|
549
1002
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
await wait();
|
|
1003
|
+
const add_col_btn = document.querySelector(
|
|
1004
|
+
'[aria-label="Add column to the right"]'
|
|
1005
|
+
) as HTMLElement;
|
|
1006
|
+
expect(add_col_btn).toBeTruthy();
|
|
1007
|
+
await fireEvent.click(add_col_btn);
|
|
556
1008
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
1009
|
+
await waitFor(() =>
|
|
1010
|
+
expect(get_header_cells(container)).toHaveLength(initial_headers + 1)
|
|
1011
|
+
);
|
|
1012
|
+
expect(getByTestId("header-1")).toHaveFocus();
|
|
560
1013
|
});
|
|
561
1014
|
|
|
562
1015
|
test("delete column via header menu", async () => {
|
|
563
|
-
const { container } = await render(Dataframe, dynamic_props);
|
|
1016
|
+
const { container, getByTestId } = await render(Dataframe, dynamic_props);
|
|
564
1017
|
await wait();
|
|
565
1018
|
|
|
566
1019
|
const initial_headers = get_header_cells(container).length;
|
|
@@ -580,6 +1033,7 @@ describe("Add/remove rows and columns", () => {
|
|
|
580
1033
|
await wait();
|
|
581
1034
|
|
|
582
1035
|
expect(get_header_cells(container).length).toBe(initial_headers - 1);
|
|
1036
|
+
await waitFor(() => expect(getByTestId("cell-0-0")).toHaveFocus());
|
|
583
1037
|
}
|
|
584
1038
|
}
|
|
585
1039
|
});
|