From 226de5f49eda0c8fe1368ebdc08b495edcccdab9 Mon Sep 17 00:00:00 2001 From: Abubakar Abid Date: Tue, 11 Aug 2026 21:30:50 -0700 Subject: [PATCH 1/4] Re-read the Dataframe virtual window whenever the row count changes The virtualized table only recalculated its rendered window on the initial 0 -> N row transition. Every later row count change called `setOptions()`, which does not notify, so `virtualItems()` -- whose only reactive dependency is the internal `version` counter -- was never re-read and the rendered window stayed frozen at whatever it was after the first row. Streamed dataframes showed only their first row, and a row appended to the end stayed invisible, until an unrelated scroll or resize happened to force a recalculation. Bumping `version` on any row count change re-reads the window while keeping the measured row sizes that `measure()` would discard. Also fixes a dead selector in the add-row test: it queried `.empty-row-button`, which never matches (the class is `.add-row-button`), and the assertion sat inside that lookup, so the test silently asserted nothing. Fixes #13611 Co-Authored-By: Claude Opus 5 (1M context) --- .changeset/brave-rows-appear.md | 6 ++ js/dataframe/Dataframe.test.ts | 89 +++++++++++++++++-- .../shared/tanstack/virtual.svelte.ts | 11 ++- 3 files changed, 95 insertions(+), 11 deletions(-) create mode 100644 .changeset/brave-rows-appear.md diff --git a/.changeset/brave-rows-appear.md b/.changeset/brave-rows-appear.md new file mode 100644 index 00000000000..188c7ad75bb --- /dev/null +++ b/.changeset/brave-rows-appear.md @@ -0,0 +1,6 @@ +--- +"@gradio/dataframe": patch +"gradio": patch +--- + +fix:Render `gr.Dataframe` rows as soon as the row count changes, including streamed updates diff --git a/js/dataframe/Dataframe.test.ts b/js/dataframe/Dataframe.test.ts index 7b829b2bfa1..e183b831915 100644 --- a/js/dataframe/Dataframe.test.ts +++ b/js/dataframe/Dataframe.test.ts @@ -516,19 +516,24 @@ describe("Add/remove rows and columns", () => { }; test("add row button appends a new row", async () => { - const { container } = await render(Dataframe, dynamic_props); + // The button is only offered while the table is empty (`EmptyRowButton`), so + // render an empty one. This test used to query `.empty-row-button`, which + // never matches -- the class is `.add-row-button` -- and the assertion was + // guarded by that lookup, so it silently asserted nothing. + const { container } = await render(Dataframe, { + ...dynamic_props, + value: { data: [], headers: ["Name", "Age", "Role"], metadata: null } + }); await wait(); + expect(get_rows(container).length).toBe(0); - const initial_rows = get_rows(container).length; + const add_btn = container.querySelector(".add-row-button") as HTMLElement; + expect(add_btn).not.toBeNull(); - // The empty row button should be present for dynamic row_count - const add_btn = container.querySelector(".empty-row-button") as HTMLElement; - if (add_btn) { - await fireEvent.click(add_btn); - await wait(); + await fireEvent.click(add_btn); + await wait(); - expect(get_rows(container).length).toBe(initial_rows + 1); - } + expect(get_rows(container).length).toBe(1); }); // Cell menu add row tests: The CellMenu renders outside the table-wrap parent, @@ -1047,3 +1052,69 @@ describe("Boolean column select-all header checkbox", () => { expect(admin.indeterminate).toBe(false); }); }); + +describe("Dataframe row virtualization", () => { + afterEach(() => cleanup()); + + // A generator that yields a growing dataframe streams one row at a time. Every + // row that has arrived must be rendered without the user touching the table. + // See https://github.com/gradio-app/gradio/issues/13611 + test("renders rows as they are streamed in", async () => { + const rows = [ + ["Step 1", "Result 1"], + ["Step 2", "Result 2"], + ["Step 3", "Result 3"], + ["Step 4", "Result 4"], + ["Step 5", "Result 5"] + ]; + + const { container, set_data } = await render(Dataframe, { + ...default_props, + col_count: [2, "fixed"] as [number, "fixed" | "dynamic"], + row_count: [1, "dynamic"] as [number, "fixed" | "dynamic"], + value: { data: [], headers: ["Step", "Value"], metadata: null } + }); + await wait(); + + for (let i = 1; i <= rows.length; i++) { + await set_data({ + value: { + data: rows.slice(0, i), + headers: ["Step", "Value"], + metadata: null + } + }); + await wait(); + expect(get_rows(container).length).toBe(i); + } + }); + + // The same virtualization path backs adding a row interactively: the new last + // row used to stay invisible until something else forced a re-measure. + // See https://github.com/gradio-app/gradio/issues/13272 + test("renders a row appended to the end", async () => { + const { container, set_data } = await render(Dataframe, { + ...default_props, + row_count: [3, "dynamic"] as [number, "fixed" | "dynamic"] + }); + await wait(); + expect(get_rows(container).length).toBe(3); + + await set_data({ + value: { + data: [ + ["Alice", "30", "Engineer"], + ["Bob", "25", "Designer"], + ["Carol", "35", "Manager"], + ["Dave", "28", "Analyst"] + ], + headers: ["Name", "Age", "Role"], + metadata: null + } + }); + await wait(); + + expect(get_rows(container).length).toBe(4); + expect(get_cell(container, 3, 0)?.textContent).toContain("Dave"); + }); +}); diff --git a/js/dataframe/shared/tanstack/virtual.svelte.ts b/js/dataframe/shared/tanstack/virtual.svelte.ts index 625157d94ec..2693a214100 100644 --- a/js/dataframe/shared/tanstack/virtual.svelte.ts +++ b/js/dataframe/shared/tanstack/virtual.svelte.ts @@ -80,8 +80,15 @@ export function createSvelteVirtualizer< } }); - if (prev_count === 0 && current_count > 0) { - virtualizer.measure(); + if (current_count !== prev_count) { + // `virtualItems()` only recomputes when `version` changes, and + // `setOptions()` does not notify on its own, so a row count change alone + // left the rendered window frozen: rows that arrived after the first + // render stayed invisible until an unrelated scroll or resize forced a + // recalculation. Bumping `version` re-reads the window while keeping the + // measured row sizes that `measure()` would throw away. + // See https://github.com/gradio-app/gradio/issues/13611 and #13272. + version += 1; } prev_count = current_count; }); From 09b858300772e8a1b1465eea73d25d961b686612 Mon Sep 17 00:00:00 2001 From: Abubakar Abid Date: Tue, 11 Aug 2026 23:06:40 -0700 Subject: [PATCH 2/4] Drop the changeset: the changeset action generates one from the PR title Co-Authored-By: Claude Opus 5 (1M context) --- .changeset/brave-rows-appear.md | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 .changeset/brave-rows-appear.md diff --git a/.changeset/brave-rows-appear.md b/.changeset/brave-rows-appear.md deleted file mode 100644 index 188c7ad75bb..00000000000 --- a/.changeset/brave-rows-appear.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@gradio/dataframe": patch -"gradio": patch ---- - -fix:Render `gr.Dataframe` rows as soon as the row count changes, including streamed updates From dd8693afe53200e3935a006c6a6a8dfc0ed1fc0e Mon Sep 17 00:00:00 2001 From: gradio-pr-bot Date: Wed, 12 Aug 2026 06:07:34 +0000 Subject: [PATCH 3/4] add changeset --- .changeset/tricky-spies-prove.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/tricky-spies-prove.md diff --git a/.changeset/tricky-spies-prove.md b/.changeset/tricky-spies-prove.md new file mode 100644 index 00000000000..f9627677a3e --- /dev/null +++ b/.changeset/tricky-spies-prove.md @@ -0,0 +1,6 @@ +--- +"@gradio/dataframe": patch +"gradio": patch +--- + +fix:Re-read the Dataframe virtual window whenever the row count changes From 4207dd8ca29f9d46d386ea15cf05fe5aa3503453 Mon Sep 17 00:00:00 2001 From: Abubakar Abid Date: Tue, 11 Aug 2026 23:07:42 -0700 Subject: [PATCH 4/4] Drop the inline comments and the added tests Leaves the change as the one-line behaviour fix. Co-Authored-By: Claude Opus 5 (1M context) --- js/dataframe/Dataframe.test.ts | 89 ++----------------- .../shared/tanstack/virtual.svelte.ts | 7 -- 2 files changed, 9 insertions(+), 87 deletions(-) diff --git a/js/dataframe/Dataframe.test.ts b/js/dataframe/Dataframe.test.ts index e183b831915..7b829b2bfa1 100644 --- a/js/dataframe/Dataframe.test.ts +++ b/js/dataframe/Dataframe.test.ts @@ -516,24 +516,19 @@ describe("Add/remove rows and columns", () => { }; test("add row button appends a new row", async () => { - // The button is only offered while the table is empty (`EmptyRowButton`), so - // render an empty one. This test used to query `.empty-row-button`, which - // never matches -- the class is `.add-row-button` -- and the assertion was - // guarded by that lookup, so it silently asserted nothing. - const { container } = await render(Dataframe, { - ...dynamic_props, - value: { data: [], headers: ["Name", "Age", "Role"], metadata: null } - }); + const { container } = await render(Dataframe, dynamic_props); await wait(); - expect(get_rows(container).length).toBe(0); - const add_btn = container.querySelector(".add-row-button") as HTMLElement; - expect(add_btn).not.toBeNull(); + const initial_rows = get_rows(container).length; - await fireEvent.click(add_btn); - await wait(); + // The empty row button should be present for dynamic row_count + const add_btn = container.querySelector(".empty-row-button") as HTMLElement; + if (add_btn) { + await fireEvent.click(add_btn); + await wait(); - expect(get_rows(container).length).toBe(1); + expect(get_rows(container).length).toBe(initial_rows + 1); + } }); // Cell menu add row tests: The CellMenu renders outside the table-wrap parent, @@ -1052,69 +1047,3 @@ describe("Boolean column select-all header checkbox", () => { expect(admin.indeterminate).toBe(false); }); }); - -describe("Dataframe row virtualization", () => { - afterEach(() => cleanup()); - - // A generator that yields a growing dataframe streams one row at a time. Every - // row that has arrived must be rendered without the user touching the table. - // See https://github.com/gradio-app/gradio/issues/13611 - test("renders rows as they are streamed in", async () => { - const rows = [ - ["Step 1", "Result 1"], - ["Step 2", "Result 2"], - ["Step 3", "Result 3"], - ["Step 4", "Result 4"], - ["Step 5", "Result 5"] - ]; - - const { container, set_data } = await render(Dataframe, { - ...default_props, - col_count: [2, "fixed"] as [number, "fixed" | "dynamic"], - row_count: [1, "dynamic"] as [number, "fixed" | "dynamic"], - value: { data: [], headers: ["Step", "Value"], metadata: null } - }); - await wait(); - - for (let i = 1; i <= rows.length; i++) { - await set_data({ - value: { - data: rows.slice(0, i), - headers: ["Step", "Value"], - metadata: null - } - }); - await wait(); - expect(get_rows(container).length).toBe(i); - } - }); - - // The same virtualization path backs adding a row interactively: the new last - // row used to stay invisible until something else forced a re-measure. - // See https://github.com/gradio-app/gradio/issues/13272 - test("renders a row appended to the end", async () => { - const { container, set_data } = await render(Dataframe, { - ...default_props, - row_count: [3, "dynamic"] as [number, "fixed" | "dynamic"] - }); - await wait(); - expect(get_rows(container).length).toBe(3); - - await set_data({ - value: { - data: [ - ["Alice", "30", "Engineer"], - ["Bob", "25", "Designer"], - ["Carol", "35", "Manager"], - ["Dave", "28", "Analyst"] - ], - headers: ["Name", "Age", "Role"], - metadata: null - } - }); - await wait(); - - expect(get_rows(container).length).toBe(4); - expect(get_cell(container, 3, 0)?.textContent).toContain("Dave"); - }); -}); diff --git a/js/dataframe/shared/tanstack/virtual.svelte.ts b/js/dataframe/shared/tanstack/virtual.svelte.ts index 2693a214100..e77e09eac48 100644 --- a/js/dataframe/shared/tanstack/virtual.svelte.ts +++ b/js/dataframe/shared/tanstack/virtual.svelte.ts @@ -81,13 +81,6 @@ export function createSvelteVirtualizer< }); if (current_count !== prev_count) { - // `virtualItems()` only recomputes when `version` changes, and - // `setOptions()` does not notify on its own, so a row count change alone - // left the rendered window frozen: rows that arrived after the first - // render stayed invisible until an unrelated scroll or resize forced a - // recalculation. Bumping `version` re-reads the window while keeping the - // measured row sizes that `measure()` would throw away. - // See https://github.com/gradio-app/gradio/issues/13611 and #13272. version += 1; } prev_count = current_count;