Repository: gradio-app/gradio
Pull request: Fix #10281: Dragging image replaces existing instead of opening new tab
## Description of the problem
In Gradio 5.x.x, dragging a second image onto an already populated `<gr.Image>` component did **not** replace the existing image—instead, the browser opened the dropped file in a new tab. Previously, the `<div class="upload-container">` had **no** `on:dragover` or `on:drop` handlers, so the second drop was never intercepted by Gradio’s upload logic and fell back to the browser’s default behavior.
## Proposed solution
- In `ImageUploader.svelte`, *added* `on_drag_over` and `on_drop` handlers to prevent default browser behavior, clear any existing `value` (via `handle_clear()` + `await tick()`), and dispatch the upload by calling `upload_input.loadFilesFromDrop(evt)`.  
- In `Upload.svelte`, *restored* the original `loadFilesFromDrop(e: DragEvent)` function that had been removed during a prior rebase.  
- In `js/spa/test/image_component_events.spec.ts`, *extended* the drag-to-upload test to capture the Playwright `context`, verifying that no new tabs were opened.

> **Note:** The `loadFilesFromDrop` function wasn’t newly authored—it existed before the rebase and was simply restored to its original behavior.

## Reproduction code
```python
import gradio as gr

with gr.Blocks() as demo:
    with gr.Row():
        input_image = gr.Image(
            label="Insert an image",
            type="pil",
            height=600,
            width=400,
            interactive=True
        )

if __name__ == "__main__":
    demo.launch()
```

Closes: #10281 

Problem statement:
Dragging in an image a second time will not replace the original image, it will open in a new tab
### Describe the bug

Dragging in an image a second time will not replace the original image, it will open in a new tab

### Have you searched existing issues?  🔎

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

### Reproduction

```python
import gradio as gr

    
with gr.Blocks() as demo:  
    with gr.Row():
        input_image = gr.Image(
                        label="输入图像", 
                        type="pil",
                        height=600,
                        width=400,
                        interactive=True
                    )

if __name__ == "__main__":
    demo.launch(
        server_name="0.0.0.0",
        server_port=37865
    )

```


### Screenshot

_No response_

### Logs

_No response_

### System Info

```shell
gradio                       5.6.0
gradio_client                1.4.3
```


### Severity

I can work around it

Hints from issue discussion:
Not sure if this intentional or not, cc @hannahblair @pngwn but it seems reasonable to support
When I use gradio 4.x.x I can replace the image with the newly dragged in image when it already exists in the gr.Image() interactive interface, but with gradio 5.x.x the newly dragged in image will be displayed in the new tab when it already exists.
Hi @abidlabs, @hannahblair, @pngwn – I'd like to follow up on this issue. Is the behavior described (dragging an image a second time opens a new tab instead of replacing the existing one) intentional in Gradio 5.x.x?

If it's not intended, I'd be happy to take a look at fixing this. Could you please confirm whether I can be assigned?

Sorry for bothering, and thanks again for your time!
It is not intentional!

Commit message: Fix #10281: Dragging image replaces existing instead of opening new tab
I updated the drag and drop event handlers in the imageUploader
component (on_drag_over and on_drop) to clear the
existing image before processing the newly dropped file.

Additionally, I modified an end-to-end test to verify that the number
of browser tabs remains the same after dropping a new file.

Patch to review:
diff --git a/js/image/shared/ImageUploader.svelte b/js/image/shared/ImageUploader.svelte
index ae2c5a88ff..4b2d6cfc2a 100644
--- a/js/image/shared/ImageUploader.svelte
+++ b/js/image/shared/ImageUploader.svelte
@@ -57,6 +57,8 @@
 			} else {
 				value = detail;
 			}
+
+			await tick();
 			dispatch("upload");
 		}
 	}
@@ -132,6 +134,31 @@
 	}
 
 	let image_container: HTMLElement;
+
+	function on_drag_over(evt: DragEvent): void {
+		evt.preventDefault();
+		evt.stopPropagation();
+		if (evt.dataTransfer) {
+			evt.dataTransfer.dropEffect = "copy";
+		}
+
+		dragging = true;
+	}
+
+	async function on_drop(evt: DragEvent): Promise<void> {
+		evt.preventDefault();
+		evt.stopPropagation();
+		dragging = false;
+
+		if (value) {
+			handle_clear();
+			await tick();
+		}
+
+		active_source = "upload";
+		await tick();
+		upload_input.loadFilesFromDrop(evt);
+	}
 </script>
 
 <BlockLabel {show_label} Icon={ImageIcon} label={label || "Image"} />
@@ -153,10 +180,13 @@
 			/>
 		{/if}
 	</IconButtonWrapper>
+	<!-- svelte-ignore a11y-no-static-element-interactions -->
 	<div
 		class="upload-container"
 		class:reduced-height={sources.length > 1}
 		style:width={value ? "auto" : "100%"}
+		on:dragover={on_drag_over}
+		on:drop={on_drop}
 	>
 		<Upload
 			hidden={value !== null || active_source === "webcam"}
diff --git a/js/spa/test/image_component_events.spec.ts b/js/spa/test/image_component_events.spec.ts
index 90a8de017d..1a9853f789 100644
--- a/js/spa/test/image_component_events.spec.ts
+++ b/js/spa/test/image_component_events.spec.ts
@@ -57,8 +57,10 @@ test("Image drag-to-upload uploads image successfuly.", async ({ page }) => {
 });
 
 test("Image drag-to-upload replaces an image successfully.", async ({
-	page
+	page,
+	context
 }) => {
+	const initialPages = context.pages();
 	await drag_and_drop_file(
 		page,
 		"input[type=file]",
@@ -79,6 +81,8 @@ test("Image drag-to-upload replaces an image successfully.", async ({
 
 	await expect(page.getByLabel("# Change Events").first()).toHaveValue("2");
 	await expect(page.getByLabel("# Upload Events")).toHaveValue("2");
+	const newPages = context.pages();
+	expect(newPages.length).toBe(initialPages.length);
 });
 
 test("Image copy from clipboard dispatches upload event.", async ({ page }) => {
diff --git a/js/upload/src/Upload.svelte b/js/upload/src/Upload.svelte
index 43e4693567..23bba616c2 100644
--- a/js/upload/src/Upload.svelte
+++ b/js/upload/src/Upload.svelte
@@ -240,6 +240,39 @@
 			dispatch("load", files_to_load);
 		}
 	}
+
+	export async function loadFilesFromDrop(e: DragEvent): Promise<void> {
+		dragging = false;
+		if (!e.dataTransfer?.files) return;
+		const files_to_load = Array.from(e.dataTransfer.files).filter((file) => {
+			const file_extension = "." + file.name.split(".").pop();
+			if (
+				file_extension &&
+				is_valid_mimetype(accept_file_types, file_extension, file.type)
+			) {
+				return true;
+			}
+			if (
+				file_extension && Array.isArray(filetype)
+					? filetype.includes(file_extension)
+					: file_extension === filetype
+			) {
+				return true;
+			}
+			dispatch("error", `Invalid file type only ${filetype} allowed.`);
+			return false;
+		});
+
+		if (format != "blob") {
+			await load_files(files_to_load);
+		} else {
+			if (file_count === "single") {
+				dispatch("load", files_to_load[0]);
+				return;
+			}
+			dispatch("load", files_to_load);
+		}
+	}
 </script>
 
 {#if filetype === "clipboard"}


Code review comments:
diff_hunk: @@ -240,6 +240,39 @@
 			dispatch("load", files_to_load);
 		}
 	}
+
+	export async function loadFilesFromDrop(e: DragEvent): Promise<void> {
path: js/upload/src/Upload.svelte
text: @user1:
Use camel case for the function name to be consistent

@user2:
Did you mean snake?

@user1:
Yea lol

@author:
I think it is done.

diff_hunk: @@ -240,6 +240,39 @@
 			dispatch("load", files_to_load);
 		}
 	}
+
+	export async function loadFilesFromDrop(e: DragEvent): Promise<void> {
+		dragging = false;
+		if (!e.dataTransfer?.files) return;
+		const files_to_load = Array.from(e.dataTransfer.files).filter((file) => {
path: js/upload/src/Upload.svelte
text: @user1:
You can just use `is_valid_file` instead of implementing a new filter function

```js
		const files_to_load = Array.from(e.dataTransfer.files).filter(is_valid_file);
```

@author:
I think it is done.