@gradio/dataframe 0.24.3 → 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 +21 -0
- package/Dataframe.test.ts +469 -43
- 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 +1 -2
- package/dist/shared/HeaderCell.svelte +17 -3
- package/dist/shared/HeaderCell.svelte.d.ts +3 -1
- package/dist/shared/Table.svelte +373 -140
- package/dist/shared/icons/SelectionButtons.svelte +1 -0
- package/package.json +7 -7
- package/shared/BooleanCell.svelte +1 -0
- package/shared/CellMenuButton.svelte +8 -1
- package/shared/DataCell.svelte +26 -2
- package/shared/EditableCell.svelte +1 -2
- package/shared/HeaderCell.svelte +17 -3
- package/shared/Table.svelte +373 -140
- package/shared/icons/SelectionButtons.svelte +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
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
|
+
|
|
3
24
|
## 0.24.3
|
|
4
25
|
|
|
5
26
|
### 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}']`
|
|
@@ -125,7 +135,7 @@ describe("Dataframe rendering", () => {
|
|
|
125
135
|
});
|
|
126
136
|
|
|
127
137
|
test("renders label when show_label is true", async () => {
|
|
128
|
-
const { container } = await render(Dataframe, {
|
|
138
|
+
const { container, getByRole } = await render(Dataframe, {
|
|
129
139
|
...default_props,
|
|
130
140
|
label: "Test Table",
|
|
131
141
|
show_label: true
|
|
@@ -133,6 +143,9 @@ describe("Dataframe rendering", () => {
|
|
|
133
143
|
await wait();
|
|
134
144
|
|
|
135
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();
|
|
136
149
|
});
|
|
137
150
|
|
|
138
151
|
test("renders row numbers when show_row_numbers is true", async () => {
|
|
@@ -144,6 +157,47 @@ describe("Dataframe rendering", () => {
|
|
|
144
157
|
|
|
145
158
|
const row_number_cells = container.querySelectorAll(".row-number-cell");
|
|
146
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");
|
|
147
201
|
});
|
|
148
202
|
|
|
149
203
|
test("renders search input when show_search is 'search'", async () => {
|
|
@@ -284,22 +338,6 @@ describe("Cell selection", () => {
|
|
|
284
338
|
expect(next_cell.className).toContain("cell-selected");
|
|
285
339
|
});
|
|
286
340
|
|
|
287
|
-
test("Tab moves to next cell", async () => {
|
|
288
|
-
const { container } = await render(Dataframe, default_props);
|
|
289
|
-
await wait();
|
|
290
|
-
|
|
291
|
-
const cell = get_cell(container, 0, 0)!;
|
|
292
|
-
await fireEvent.mouseDown(cell);
|
|
293
|
-
await wait();
|
|
294
|
-
|
|
295
|
-
const table_wrap = get_table_wrap(container)!;
|
|
296
|
-
await fireEvent.keyDown(table_wrap, { key: "Tab" });
|
|
297
|
-
await wait();
|
|
298
|
-
|
|
299
|
-
const next_cell = get_cell(container, 0, 1)!;
|
|
300
|
-
expect(next_cell.className).toContain("cell-selected");
|
|
301
|
-
});
|
|
302
|
-
|
|
303
341
|
test("shift+click selects a range", async () => {
|
|
304
342
|
const { container } = await render(Dataframe, default_props);
|
|
305
343
|
await wait();
|
|
@@ -345,6 +383,389 @@ describe("Cell selection", () => {
|
|
|
345
383
|
});
|
|
346
384
|
});
|
|
347
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
|
+
|
|
348
769
|
describe("Header overflow", () => {
|
|
349
770
|
let wrapper: HTMLDivElement;
|
|
350
771
|
|
|
@@ -515,20 +936,23 @@ describe("Add/remove rows and columns", () => {
|
|
|
515
936
|
row_count: [3, "dynamic"] as [number, "fixed" | "dynamic"]
|
|
516
937
|
};
|
|
517
938
|
|
|
518
|
-
test("add row button appends a
|
|
519
|
-
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
|
+
});
|
|
520
949
|
await wait();
|
|
521
950
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
// The empty row button should be present for dynamic row_count
|
|
525
|
-
const add_btn = container.querySelector(".empty-row-button") as HTMLElement;
|
|
526
|
-
if (add_btn) {
|
|
527
|
-
await fireEvent.click(add_btn);
|
|
528
|
-
await wait();
|
|
951
|
+
await fireEvent.click(getByRole("button", { name: "Add row" }));
|
|
529
952
|
|
|
530
|
-
|
|
531
|
-
|
|
953
|
+
const added_cell = await waitFor(() => getByTestId("cell-0-0"));
|
|
954
|
+
expect(get_rows(container)).toHaveLength(1);
|
|
955
|
+
expect(added_cell).toHaveFocus();
|
|
532
956
|
});
|
|
533
957
|
|
|
534
958
|
// Cell menu add row tests: The CellMenu renders outside the table-wrap parent,
|
|
@@ -539,7 +963,7 @@ describe("Add/remove rows and columns", () => {
|
|
|
539
963
|
test.todo("add row below via cell menu");
|
|
540
964
|
|
|
541
965
|
test("delete row via cell menu", async () => {
|
|
542
|
-
const { container } = await render(Dataframe, dynamic_props);
|
|
966
|
+
const { container, getByTestId } = await render(Dataframe, dynamic_props);
|
|
543
967
|
await wait();
|
|
544
968
|
|
|
545
969
|
const cell = get_cell(container, 1, 0)!;
|
|
@@ -559,10 +983,11 @@ describe("Add/remove rows and columns", () => {
|
|
|
559
983
|
await wait();
|
|
560
984
|
|
|
561
985
|
expect(get_rows(container).length).toBe(2);
|
|
986
|
+
await waitFor(() => expect(getByTestId("cell-1-0")).toHaveFocus());
|
|
562
987
|
});
|
|
563
988
|
|
|
564
989
|
test("add column via header menu", async () => {
|
|
565
|
-
const { container } = await render(Dataframe, dynamic_props);
|
|
990
|
+
const { container, getByTestId } = await render(Dataframe, dynamic_props);
|
|
566
991
|
await wait();
|
|
567
992
|
|
|
568
993
|
const initial_headers = get_header_cells(container).length;
|
|
@@ -571,24 +996,24 @@ describe("Add/remove rows and columns", () => {
|
|
|
571
996
|
const headers = get_header_cells(container);
|
|
572
997
|
const header = headers[0] as HTMLElement;
|
|
573
998
|
const menu_btn = header.querySelector(".cell-menu-button") as HTMLElement;
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
999
|
+
expect(menu_btn).toBeTruthy();
|
|
1000
|
+
await fireEvent.click(menu_btn);
|
|
1001
|
+
await wait();
|
|
577
1002
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
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);
|
|
584
1008
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
1009
|
+
await waitFor(() =>
|
|
1010
|
+
expect(get_header_cells(container)).toHaveLength(initial_headers + 1)
|
|
1011
|
+
);
|
|
1012
|
+
expect(getByTestId("header-1")).toHaveFocus();
|
|
588
1013
|
});
|
|
589
1014
|
|
|
590
1015
|
test("delete column via header menu", async () => {
|
|
591
|
-
const { container } = await render(Dataframe, dynamic_props);
|
|
1016
|
+
const { container, getByTestId } = await render(Dataframe, dynamic_props);
|
|
592
1017
|
await wait();
|
|
593
1018
|
|
|
594
1019
|
const initial_headers = get_header_cells(container).length;
|
|
@@ -608,6 +1033,7 @@ describe("Add/remove rows and columns", () => {
|
|
|
608
1033
|
await wait();
|
|
609
1034
|
|
|
610
1035
|
expect(get_header_cells(container).length).toBe(initial_headers - 1);
|
|
1036
|
+
await waitFor(() => expect(getByTestId("cell-0-0")).toHaveFocus());
|
|
611
1037
|
}
|
|
612
1038
|
}
|
|
613
1039
|
});
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
let {
|
|
2
|
+
let {
|
|
3
|
+
on_click,
|
|
4
|
+
tab_index = undefined
|
|
5
|
+
}: {
|
|
6
|
+
on_click: (event: MouseEvent) => void;
|
|
7
|
+
tab_index?: number;
|
|
8
|
+
} = $props();
|
|
3
9
|
</script>
|
|
4
10
|
|
|
5
11
|
<button
|
|
6
12
|
aria-label="Open cell menu"
|
|
7
13
|
class="cell-menu-button"
|
|
8
14
|
aria-haspopup="menu"
|
|
15
|
+
tabindex={tab_index}
|
|
9
16
|
onclick={on_click}
|
|
10
17
|
ontouchstart={(event) => {
|
|
11
18
|
event.preventDefault();
|