Skip to content

Commit ac075ad

Browse files
Fix cell menu not showing in non-editable dataframes (#10819)
* remove editable condition * - add test - improve html semantics * add changeset * fix test * fix test * - fix test - fix column widths changing on sort * swap e2e for story --------- Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com>
1 parent cf36ddd commit ac075ad

File tree

8 files changed

+130
-37
lines changed

8 files changed

+130
-37
lines changed

.changeset/tired-coats-sing.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@gradio/dataframe": patch
3+
"gradio": patch
4+
---
5+
6+
fix:Fix cell menu not showing in non-editable dataframes

js/dataframe/Dataframe.stories.svelte

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -675,7 +675,7 @@
675675
/>
676676

677677
<Story
678-
name="Dataframe with sorting by multiple columns"
678+
name="Non-interactive dataframe with sorting by multiple columns"
679679
args={{
680680
values: [
681681
[1, 2, 3],
@@ -685,7 +685,7 @@
685685
headers: ["A", "B", "C"],
686686
col_count: [3, "dynamic"],
687687
row_count: [3, "dynamic"],
688-
editable: true,
688+
editable: false,
689689
sort_columns: [
690690
{ col: 0, direction: "asc" },
691691
{ col: 1, direction: "desc" }
@@ -708,7 +708,7 @@
708708
const cell_menu_button = canvas.getAllByLabelText("Open cell menu")[0];
709709
await userEvent.click(cell_menu_button);
710710

711-
const sort_ascending_button = canvas.getByRole("button", {
711+
const sort_ascending_button = canvas.getByRole("menuitem", {
712712
name: "Sort ascending"
713713
});
714714
await userEvent.click(sort_ascending_button);
@@ -719,7 +719,7 @@
719719
const cell_menu_button_2 = canvas.getAllByLabelText("Open cell menu")[1];
720720
await userEvent.click(cell_menu_button_2);
721721

722-
const sort_descending_button = canvas.getByRole("button", {
722+
const sort_descending_button = canvas.getByRole("menuitem", {
723723
name: "Sort descending"
724724
});
725725
await userEvent.click(sort_descending_button);
@@ -730,7 +730,7 @@
730730
const cell_menu_button_3 = canvas.getAllByLabelText("Open cell menu")[2];
731731
await userEvent.click(cell_menu_button_3);
732732

733-
const sort_ascending_button_3 = canvas.getByRole("button", {
733+
const sort_ascending_button_3 = canvas.getByRole("menuitem", {
734734
name: "Sort ascending"
735735
});
736736
await userEvent.click(sort_ascending_button_3);

js/dataframe/shared/CellMenu.svelte

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,14 @@
2121
export let on_clear_sort: () => void = () => {};
2222
export let sort_direction: SortDirection | null = null;
2323
export let sort_priority: number | null = null;
24+
export let editable = true;
2425
2526
export let i18n: I18nFormatter;
2627
let menu_element: HTMLDivElement;
2728
2829
$: is_header = row === -1;
29-
$: can_add_rows = row_count[1] === "dynamic";
30-
$: can_add_columns = col_count[1] === "dynamic";
30+
$: can_add_rows = editable && row_count[1] === "dynamic";
31+
$: can_add_columns = editable && col_count[1] === "dynamic";
3132
3233
onMount(() => {
3334
position_menu();
@@ -56,9 +57,10 @@
5657
}
5758
</script>
5859

59-
<div bind:this={menu_element} class="cell-menu">
60+
<div bind:this={menu_element} class="cell-menu" role="menu">
6061
{#if is_header}
6162
<button
63+
role="menuitem"
6264
on:click={() => on_sort("asc")}
6365
class:active={sort_direction === "asc"}
6466
>
@@ -69,6 +71,7 @@
6971
{/if}
7072
</button>
7173
<button
74+
role="menuitem"
7275
on:click={() => on_sort("desc")}
7376
class:active={sort_direction === "desc"}
7477
>
@@ -78,39 +81,65 @@
7881
<span class="priority">{sort_priority}</span>
7982
{/if}
8083
</button>
81-
<button on:click={on_clear_sort}>
84+
<button role="menuitem" on:click={on_clear_sort}>
8285
<CellMenuIcons icon="clear-sort" />
8386
{i18n("dataframe.clear_sort")}
8487
</button>
8588
{/if}
8689

8790
{#if !is_header && can_add_rows}
88-
<button on:click={() => on_add_row_above()}>
91+
<button
92+
role="menuitem"
93+
on:click={() => on_add_row_above()}
94+
aria-label="Add row above"
95+
>
8996
<CellMenuIcons icon="add-row-above" />
9097
{i18n("dataframe.add_row_above")}
9198
</button>
92-
<button on:click={() => on_add_row_below()}>
99+
<button
100+
role="menuitem"
101+
on:click={() => on_add_row_below()}
102+
aria-label="Add row below"
103+
>
93104
<CellMenuIcons icon="add-row-below" />
94105
{i18n("dataframe.add_row_below")}
95106
</button>
96107
{#if can_delete_rows}
97-
<button on:click={on_delete_row} class="delete">
108+
<button
109+
role="menuitem"
110+
on:click={on_delete_row}
111+
class="delete"
112+
aria-label="Delete row"
113+
>
98114
<CellMenuIcons icon="delete-row" />
99115
{i18n("dataframe.delete_row")}
100116
</button>
101117
{/if}
102118
{/if}
103119
{#if can_add_columns}
104-
<button on:click={() => on_add_column_left()}>
120+
<button
121+
role="menuitem"
122+
on:click={() => on_add_column_left()}
123+
aria-label="Add column to the left"
124+
>
105125
<CellMenuIcons icon="add-column-left" />
106126
{i18n("dataframe.add_column_left")}
107127
</button>
108-
<button on:click={() => on_add_column_right()}>
128+
<button
129+
role="menuitem"
130+
on:click={() => on_add_column_right()}
131+
aria-label="Add column to the right"
132+
>
109133
<CellMenuIcons icon="add-column-right" />
110134
{i18n("dataframe.add_column_right")}
111135
</button>
112136
{#if can_delete_cols}
113-
<button on:click={on_delete_col} class="delete">
137+
<button
138+
role="menuitem"
139+
on:click={on_delete_col}
140+
class="delete"
141+
aria-label="Delete column"
142+
>
114143
<CellMenuIcons icon="delete-column" />
115144
{i18n("dataframe.delete_column")}
116145
</button>

js/dataframe/shared/CellMenuButton.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<button
66
aria-label="Open cell menu"
77
class="cell-menu-button"
8+
aria-haspopup="menu"
89
on:click={on_click}
910
on:touchstart={(event) => {
1011
event.preventDefault();

js/dataframe/shared/Table.svelte

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,7 @@
3838
import {
3939
copy_table_data,
4040
get_max,
41-
handle_file_upload,
42-
sort_table_data
41+
handle_file_upload
4342
} from "./utils/table_utils";
4443
import { make_headers, process_data } from "./utils/data_processing";
4544
import { handle_keydown } from "./utils/keyboard_utils";
@@ -48,6 +47,7 @@
4847
type DragState,
4948
type DragHandlers
5049
} from "./utils/drag_utils";
50+
import { sort_data_and_preserve_selection } from "./utils/sort_utils";
5151
5252
export let datatype: Datatype | Datatype[];
5353
export let label: string | null = null;
@@ -177,10 +177,23 @@
177177
178178
$: if (!dequal(values, old_val)) {
179179
if (parent) {
180-
for (let i = 0; i < 50; i++) {
181-
parent.style.removeProperty(`--cell-width-${i}`);
180+
// only clear column widths when the data structure changes
181+
const is_reset =
182+
values.length === 0 || (values.length === 1 && values[0].length === 0);
183+
const is_different_structure =
184+
old_val !== undefined &&
185+
(values.length !== old_val.length ||
186+
(values[0] && old_val[0] && values[0].length !== old_val[0].length));
187+
188+
if (is_reset || is_different_structure) {
189+
for (let i = 0; i < 50; i++) {
190+
parent.style.removeProperty(`--cell-width-${i}`);
191+
}
192+
last_width_data_length = 0;
193+
last_width_column_count = 0;
182194
}
183195
}
196+
184197
// only reset sort state when values are changed
185198
const is_reset =
186199
values.length === 0 || (values.length === 1 && values[0].length === 0);
@@ -366,12 +379,28 @@
366379
367380
$: max = get_max(data);
368381
369-
$: cells[0] && set_cell_widths();
382+
// Modify how we trigger cell width calculations
383+
// Only recalculate when cells actually change, not during sort
384+
$: cells[0] && cells[0]?.clientWidth && set_cell_widths();
370385
let cells: HTMLTableCellElement[] = [];
371386
let parent: HTMLDivElement;
372387
let table: HTMLTableElement;
388+
let last_width_data_length = 0;
389+
let last_width_column_count = 0;
373390
374391
function set_cell_widths(): void {
392+
const column_count = data[0]?.length || 0;
393+
if (
394+
last_width_data_length === data.length &&
395+
last_width_column_count === column_count &&
396+
$df_state.sort_state.sort_columns.length > 0
397+
) {
398+
return;
399+
}
400+
401+
last_width_data_length = data.length;
402+
last_width_column_count = column_count;
403+
375404
const widths = cells.map((el) => el?.clientWidth || 0);
376405
if (widths.length === 0) return;
377406
@@ -412,23 +441,17 @@
412441
_display_value: string[][] | null,
413442
_styling: string[][] | null
414443
): void {
415-
let id = null;
416-
if (selected && selected[0] in _data && selected[1] in _data[selected[0]]) {
417-
id = _data[selected[0]][selected[1]].id;
418-
}
419-
420-
sort_table_data(
444+
const result = sort_data_and_preserve_selection(
421445
_data,
422446
_display_value,
423447
_styling,
424-
$df_state.sort_state.sort_columns
448+
$df_state.sort_state.sort_columns,
449+
selected,
450+
get_current_indices
425451
);
426-
data = data;
427452
428-
if (id) {
429-
const [i, j] = get_current_indices(id, data);
430-
selected = [i, j];
431-
}
453+
data = result.data;
454+
selected = result.selected;
432455
}
433456
434457
$: sort_data(data, display_value, styling);
@@ -968,8 +991,9 @@
968991
on_delete_row={() => delete_row_at(active_cell_menu?.row ?? -1)}
969992
on_delete_col={() =>
970993
delete_col_at(active_cell_menu?.col ?? active_header_menu?.col ?? -1)}
971-
can_delete_rows={!active_header_menu && data.length > 1}
972-
can_delete_cols={data.length > 0 && data[0]?.length > 1}
994+
{editable}
995+
can_delete_rows={!active_header_menu && data.length > 1 && editable}
996+
can_delete_cols={data.length > 0 && data[0]?.length > 1 && editable}
973997
{i18n}
974998
on_sort={active_header_menu
975999
? (direction) => {

js/dataframe/shared/TableHeader.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@
135135
<Padlock />
136136
{/if}
137137
</div>
138-
{#if editable && can_add_columns}
138+
{#if can_add_columns}
139139
<CellMenuButton on_click={(event) => toggle_header_menu(event, i)} />
140140
{/if}
141141
</div>

js/dataframe/shared/utils/sort_utils.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Headers } from "../types";
2+
import { sort_table_data } from "./table_utils";
23

34
export type SortDirection = "asc" | "desc";
45

@@ -61,3 +62,30 @@ export function sort_data(
6162
}
6263
return [...Array(data.length)].map((_, i) => i);
6364
}
65+
66+
export function sort_data_and_preserve_selection(
67+
data: { id: string; value: string | number }[][],
68+
display_value: string[][] | null,
69+
styling: string[][] | null,
70+
sort_columns: { col: number; direction: SortDirection }[],
71+
selected: [number, number] | false,
72+
get_current_indices: (
73+
id: string,
74+
data: { id: string; value: string | number }[][]
75+
) => [number, number]
76+
): { data: typeof data; selected: [number, number] | false } {
77+
let id = null;
78+
if (selected && selected[0] in data && selected[1] in data[selected[0]]) {
79+
id = data[selected[0]][selected[1]].id;
80+
}
81+
82+
sort_table_data(data, display_value, styling, sort_columns);
83+
84+
let new_selected = selected;
85+
if (id) {
86+
const [i, j] = get_current_indices(id, data);
87+
new_selected = [i, j];
88+
}
89+
90+
return { data, selected: new_selected };
91+
}

js/spa/test/dataframe_events.spec.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { test, expect } from "@self/tootils";
22
import { Locator } from "@playwright/test";
3+
import { toBeVisible } from "@testing-library/jest-dom/matchers";
4+
5+
function get_header_cell(element: Locator, col: number) {
6+
return element.locator(`th[title='col_${col}']`).nth(1);
7+
}
38

49
// returns a cell in a dataframe by row and column indices
510
function get_cell(element: Locator, row: number, col: number) {
@@ -133,7 +138,7 @@ test("Tall dataframe has vertical scrolling", async ({ page }) => {
133138
expect(column_count).toBe(4);
134139
});
135140

136-
test("Tall dataframe updates with buttons", async ({ page }) => {
141+
test("Dataframe can be cleared and updated indirectly", async ({ page }) => {
137142
await page.getByRole("button", { name: "Clear dataframe" }).click();
138143
await page.waitForTimeout(500);
139144

@@ -152,7 +157,7 @@ test("Tall dataframe updates with buttons", async ({ page }) => {
152157
.allTextContents();
153158

154159
const trimmed_headers = headers.slice(1).map((header) => header.trim());
155-
expect(trimmed_headers).toEqual(["A", "B", "C"]);
160+
expect(trimmed_headers).toEqual(["A", "B", "C"]);
156161
});
157162

158163
test("Non-interactive dataframe cannot be edited", async ({ page }) => {

0 commit comments

Comments
 (0)