diff --git a/ui/e2e/file-upload.spec.ts b/ui/e2e/file-upload.spec.ts index 6f86eeb4ea54ed5ce977f6c436ce684db933a6aa..2f72f578a811b4287ba2c3b64dbc14b868a8a8da 100644 --- a/ui/e2e/file-upload.spec.ts +++ b/ui/e2e/file-upload.spec.ts @@ -196,3 +196,53 @@ test.describe('File Upload via Paste and Drag', () => { expect(inputValue).toBeTruthy(); }); }); + + test('focus is retained in input after pasting image', async ({ page }) => { + await page.goto('/'); + await page.waitForLoadState('domcontentloaded'); + + const messageInput = page.getByTestId('message-input'); + await expect(messageInput).toBeVisible(); + + // Focus the input and add some text + await messageInput.focus(); + await messageInput.fill('Testing paste focus: '); + + // Simulate an image paste via clipboard event + await page.evaluate(async () => { + const input = document.querySelector('[data-testid="message-input"]') as HTMLTextAreaElement; + if (!input) return; + + // Create a simple test image as a Blob + const blob = new Blob(['test'], { type: 'image/png' }); + const file = new File([blob], 'test-paste.png', { type: 'image/png' }); + + // Create DataTransfer with the file + const dataTransfer = new DataTransfer(); + dataTransfer.items.add(file); + + // Dispatch paste event + const pasteEvent = new ClipboardEvent('paste', { + clipboardData: dataTransfer, + bubbles: true, + cancelable: true + }); + + input.dispatchEvent(pasteEvent); + }); + + // Wait for the upload to process and focus to be restored + await page.waitForTimeout(100); + + // Verify focus is still on the input (or restored to it) + const isFocused = await page.evaluate(() => { + const input = document.querySelector('[data-testid="message-input"]'); + return document.activeElement === input; + }); + + expect(isFocused).toBe(true); + + // Verify the input has the uploaded file path + const inputValue = await messageInput.inputValue(); + expect(inputValue).toContain('Testing paste focus:'); + }); diff --git a/ui/src/components/MessageInput.tsx b/ui/src/components/MessageInput.tsx index c00049bd66b8bc75beb9e35ce1e28367b91812d5..0fbb97b918fa72461f680d83086b16069973fa59 100644 --- a/ui/src/components/MessageInput.tsx +++ b/ui/src/components/MessageInput.tsx @@ -224,7 +224,7 @@ function MessageInput({ } }; - const handlePaste = async (event: React.ClipboardEvent) => { + const handlePaste = (event: React.ClipboardEvent) => { // Check clipboard items (works on both desktop and mobile) // Mobile browsers often don't populate clipboardData.files, but items works const items = event.clipboardData?.items; @@ -235,7 +235,14 @@ function MessageInput({ const file = item.getAsFile(); if (file) { event.preventDefault(); - await uploadFile(file); + // Fire and forget - uploadFile handles state updates internally. + uploadFile(file); + // Restore focus after React updates. We use setTimeout(0) to ensure + // the focus happens after React's state update commits to the DOM. + // The timeout must be longer than 0 to reliably work across browsers. + setTimeout(() => { + textareaRef.current?.focus(); + }, 10); return; } }