Repository: gradio-app/gradio
Pull request: Fix event triggers and recent regressions related to `gr.DataFrame`
Fixes a few things that were broken or broke as a result of recent PRs

* Does not fire .change() and .input() events when the page first loads (closes: #8231 and adds a test)
* Fixes .input() event to fire when a user inputs data (because of a recent regression, it was not firing at all)
* Fixes the cell selection logic so that a header and cell cannot be selected at the same time (recent regression)

Some other minor refactoring of `Table.svelte` (more to come in a separate PR)

Problem statement:
The `.input()` and `.change()` events in gr.Dataframe are also triggered at function updates and at app start
### Describe the bug

The input event of the dataframe should only be triggered, when the user makes inputs but it is also triggered at app startup and when a function makes changes.

### Have you searched existing issues?  🔎

- [X] I have searched and found no existing issues

### Reproduction

```python
import gradio as gr


def trigger():
    return "TRIGGER"

def change_df():
    return [['INPUT']]


with gr.Blocks() as demo:
    dataframe = gr.Dataframe(label="Name", interactive=True)
    textbox = gr.Textbox(label="Output Box", interactive=True)
    button = gr.Button('Change Dataframe')
  
    dataframe.input(fn=trigger, outputs=textbox)
    button.click(fn=change_df, outputs=dataframe)

if __name__ == "__main__":
    demo.launch()

```


### Screenshot

_No response_

### Logs

_No response_

### System Info

```shell
Example from Gradio Playground
```


### Severity

I can work around it

Hints from issue discussion:
Hi, apologies for the late follow up. We haven't had a chance to look into this issue yet, but the Gradio codebase has changed quite significantly since this issue was created, particularly with the release of Gradio 5. Could you let us know if this is still an issue in the latest version of Gradio (`pip install --upgrade gradio`)? Thanks!

I posted the code to reproduce the error. You should be able to check it by yourself.
Looks like it is still an issue, thanks for providing the repro @bergerchris

Commit message: changes

Patch to review:
diff --git a/.changeset/sixty-impalas-bathe.md b/.changeset/sixty-impalas-bathe.md
new file mode 100644
index 0000000000..d04a93a42e
--- /dev/null
+++ b/.changeset/sixty-impalas-bathe.md
@@ -0,0 +1,6 @@
+---
+"@gradio/dataframe": patch
+"gradio": patch
+---
+
+fix:Fix event triggers and recent regressions related to `gr.DataFrame`
diff --git a/demo/dataframe_streaming/run.ipynb b/demo/dataframe_streaming/run.ipynb
index 1079464f9d..0be32c2ab0 100644
--- a/demo/dataframe_streaming/run.ipynb
+++ b/demo/dataframe_streaming/run.ipynb
@@ -1 +1 @@
-{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: dataframe_streaming"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "import pandas as pd\n", "import time\n", "\n", "def update_dataframe(df):\n", "    df.iloc[:, :] = 1\n", "    yield df, 1\n", "    time.sleep(0.1)\n", "    df.iloc[:, :] = 2\n", "    yield df, 2\n", "\n", "initial_df = pd.DataFrame(0, index=range(5), columns=range(5))\n", "\n", "with gr.Blocks() as demo:\n", "    with gr.Row():\n", "        button = gr.Button(\"Update DataFrame\")\n", "        number = gr.Number(value=0, label=\"Number\")\n", "    dataframe = gr.Dataframe(value=initial_df, label=\"Dataframe\")\n", "    button.click(fn=update_dataframe, inputs=dataframe, outputs=[dataframe, number])\n", "\n", "if __name__ == \"__main__\":\n", "    demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
\ No newline at end of file
+{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: dataframe_streaming"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "import pandas as pd\n", "import time\n", "\n", "def update_dataframe(df):\n", "    df.iloc[:, :] = 1\n", "    yield df, 1\n", "    time.sleep(0.1)\n", "    df.iloc[:, :] = 2\n", "    yield df, 2\n", "\n", "initial_df = pd.DataFrame(0, index=range(5), columns=range(5))\n", "\n", "with gr.Blocks() as demo:\n", "    with gr.Row():\n", "        button = gr.Button(\"Update DataFrame\")\n", "        number = gr.Number(value=0, label=\"Number\")\n", "    dataframe = gr.Dataframe(value=initial_df, label=\"Dataframe\")\n", "    button.click(fn=update_dataframe, inputs=dataframe, outputs=[dataframe, number])\n", "    with gr.Row():\n", "        change_events = gr.Number(label=\"Change events\")\n", "        input_events = gr.Number(label=\"Input events\")\n", "\n", "    dataframe.change(lambda x:x+1, inputs=change_events, outputs=change_events)\n", "    dataframe.input(lambda x:x+1, inputs=input_events, outputs=input_events)\n", "\n", "if __name__ == \"__main__\":\n", "    demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
\ No newline at end of file
diff --git a/demo/dataframe_streaming/run.py b/demo/dataframe_streaming/run.py
index d92e597d34..24ea58503e 100644
--- a/demo/dataframe_streaming/run.py
+++ b/demo/dataframe_streaming/run.py
@@ -17,6 +17,12 @@ def update_dataframe(df):
         number = gr.Number(value=0, label="Number")
     dataframe = gr.Dataframe(value=initial_df, label="Dataframe")
     button.click(fn=update_dataframe, inputs=dataframe, outputs=[dataframe, number])
+    with gr.Row():
+        change_events = gr.Number(label="Change events")
+        input_events = gr.Number(label="Input events")
+
+    dataframe.change(lambda x:x+1, inputs=change_events, outputs=change_events)
+    dataframe.input(lambda x:x+1, inputs=input_events, outputs=input_events)
 
 if __name__ == "__main__":
     demo.launch()
diff --git a/js/dataframe/Index.svelte b/js/dataframe/Index.svelte
index 531d2f24b9..6ef6c75d74 100644
--- a/js/dataframe/Index.svelte
+++ b/js/dataframe/Index.svelte
@@ -88,6 +88,7 @@
 		{styling}
 		headers={_headers}
 		on:change={(e) => gradio.dispatch("change")}
+		on:input={(e) => gradio.dispatch("input")}
 		on:select={(e) => gradio.dispatch("select", e.detail)}
 		{wrap}
 		{datatype}
diff --git a/js/dataframe/shared/Table.svelte b/js/dataframe/shared/Table.svelte
index d055b2ef34..b1ecbc3ca1 100644
--- a/js/dataframe/shared/Table.svelte
+++ b/js/dataframe/shared/Table.svelte
@@ -144,34 +144,42 @@
 	}
 
 	let _headers = make_headers(headers);
-	let old_headers: string[] | undefined;
+	let old_headers: string[] = headers;
 
 	$: {
 		if (!dequal(headers, old_headers)) {
-			trigger_headers();
+			_headers = make_headers(headers);
+			old_headers = JSON.parse(JSON.stringify(headers));
 		}
 	}
 
-	function trigger_headers(): void {
-		_headers = make_headers(headers);
-
-		old_headers = headers.slice();
-		trigger_change();
-	}
+	let data: { id: string; value: string | number }[][] = [[]];
+	let old_val: undefined | (string | number)[][] = undefined;
 
 	$: if (!dequal(values, old_val)) {
 		data = process_data(values as (string | number)[][]);
 		old_val = JSON.parse(JSON.stringify(values)) as (string | number)[][];
 	}
 
-	let data: { id: string; value: string | number }[][] = [[]];
-
-	let old_val: undefined | (string | number)[][] = undefined;
+	let previous_data_string = "[]";
+	let previous_headers_string = "[]";
 
 	async function trigger_change(): Promise<void> {
-		dispatch("change");
-		if (!value_is_output) {
-			dispatch("input");
+		const current_headers_string = JSON.stringify(_headers.map((h) => h.value));
+		const current_data_string = JSON.stringify(
+			data.map((row) => row.map((cell) => String(cell.value)))
+		);
+
+		if (
+			current_data_string !== previous_data_string ||
+			current_headers_string !== previous_headers_string
+		) {
+			dispatch("change");
+			if (!value_is_output) {
+				dispatch("input");
+			}
+			previous_data_string = current_data_string;
+			previous_headers_string = current_headers_string;
 		}
 	}
 
@@ -414,7 +422,7 @@
 		selected = [index !== undefined ? index : data.length - 1, 0];
 	}
 
-	$: (data || selected_header) && trigger_change();
+	$: (data || _headers) && trigger_change();
 
 	async function add_col(index?: number): Promise<void> {
 		parent.focus();
@@ -639,8 +647,18 @@
 
 		observer.observe(parent);
 
+		document.addEventListener("click", handle_click_outside);
+		window.addEventListener("resize", handle_resize);
+		document.addEventListener("fullscreenchange", handle_fullscreen_change);
+
 		return () => {
 			observer.disconnect();
+			document.removeEventListener("click", handle_click_outside);
+			window.removeEventListener("resize", handle_resize);
+			document.removeEventListener(
+				"fullscreenchange",
+				handle_fullscreen_change
+			);
 		};
 	});
 
@@ -695,20 +713,6 @@
 		set_cell_widths();
 	}
 
-	onMount(() => {
-		document.addEventListener("click", handle_click_outside);
-		window.addEventListener("resize", handle_resize);
-		document.addEventListener("fullscreenchange", handle_fullscreen_change);
-		return () => {
-			document.removeEventListener("click", handle_click_outside);
-			window.removeEventListener("resize", handle_resize);
-			document.removeEventListener(
-				"fullscreenchange",
-				handle_fullscreen_change
-			);
-		};
-	});
-
 	let active_button: {
 		type: "header" | "cell";
 		row?: number;
@@ -969,6 +973,8 @@
 								clear_on_focus = false;
 								clicked_cell = { row: index, col: j };
 								selected = [index, j];
+								selected_header = false;
+								header_edit = false;
 								if (editable) {
 									editing = [index, j];
 								}
@@ -986,6 +992,8 @@
 								active_header_menu = null;
 								clicked_cell = { row: index, col: j };
 								selected = [index, j];
+								selected_header = false;
+								header_edit = false;
 								if (editable) {
 									editing = [index, j];
 								}
diff --git a/js/spa/test/dataframe_streaming.spec.ts b/js/spa/test/dataframe_streaming.spec.ts
index 54fa8fd3a7..8eddac7402 100644
--- a/js/spa/test/dataframe_streaming.spec.ts
+++ b/js/spa/test/dataframe_streaming.spec.ts
@@ -1,8 +1,14 @@
 import { test, expect } from "@self/tootils";
 
-test("DataFrame updates correctly when button is clicked", async ({ page }) => {
+test("DataFrame updates and events are tracked correctly", async ({ page }) => {
+	await expect(page.getByLabel("Change events")).toHaveValue("0");
+	await expect(page.getByLabel("Input events")).toHaveValue("0");
+
 	await page.getByRole("button", { name: "Update DataFrame" }).click();
 	await expect(
 		page.getByRole("table", { name: "Dataframe" }).locator("td").first()
 	).toHaveText("2");
+
+	await expect(page.getByLabel("Change events")).toHaveValue("2");
+	await expect(page.getByLabel("Input events")).toHaveValue("0");
 });


Code review comments:
diff_hunk: @@ -144,34 +144,42 @@
 	}
 
 	let _headers = make_headers(headers);
-	let old_headers: string[] | undefined;
+	let old_headers: string[] = headers;
 
 	$: {
 		if (!dequal(headers, old_headers)) {
-			trigger_headers();
+			_headers = make_headers(headers);
+			old_headers = JSON.parse(JSON.stringify(headers));
 		}
 	}
 
-	function trigger_headers(): void {
-		_headers = make_headers(headers);
-
-		old_headers = headers.slice();
-		trigger_change();
-	}
+	let data: { id: string; value: string | number }[][] = [[]];
+	let old_val: undefined | (string | number)[][] = undefined;
 
 	$: if (!dequal(values, old_val)) {
 		data = process_data(values as (string | number)[][]);
 		old_val = JSON.parse(JSON.stringify(values)) as (string | number)[][];
 	}
 
-	let data: { id: string; value: string | number }[][] = [[]];
-
-	let old_val: undefined | (string | number)[][] = undefined;
+	let previous_data_string = "[]";
+	let previous_headers_string = "[]";
path: js/dataframe/shared/Table.svelte
text: @user1:
why arent these
```js
	let previous_data_string = JSON.stringify(
			data.map((row) => row.map((cell) => String(cell.value)));
	let previous_headers_string = JSON.stringify(...)
```

Won't this cause a change dispatch immediately because of the
```
 if (
			current_data_string !== previous_data_string ||
			current_headers_string !== previous_headers_string
		)
```
later

@author:
It doesn't dispatch immediately, but I'm not sure why. I can make the change though

@author:
Fixed

@user1:
initial dispatches often don't, but I think it's flaky. Better to remove