From b6f2b6eed4df9661598f4e28fdf2d90aa8ba557e Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Tue, 19 May 2026 15:51:08 -0700 Subject: [PATCH] Keep focus in Orca when parking webviews (#2362) - Move focus back to the renderer before detaching a focused webview, including when it is parked rather than destroyed. - Cover focused, descendant-focused, and unfocused detach cases. --- .../components/browser-pane/BrowserPane.tsx | 2 + .../browser-pane/webview-registry.test.ts | 41 ++++++++++++++++++- .../browser-pane/webview-registry.ts | 20 +++++---- 3 files changed, 53 insertions(+), 10 deletions(-) diff --git a/src/renderer/src/components/browser-pane/BrowserPane.tsx b/src/renderer/src/components/browser-pane/BrowserPane.tsx index 790fafab0..058006601 100644 --- a/src/renderer/src/components/browser-pane/BrowserPane.tsx +++ b/src/renderer/src/components/browser-pane/BrowserPane.tsx @@ -75,6 +75,7 @@ import { destroyPersistentWebview, getHiddenContainer, MAX_PARKED_WEBVIEWS, + moveFocusToRendererBeforeWebviewDetach, parkedAtByTabId, registerPersistentWebview, registeredWebContentsIds, @@ -3571,6 +3572,7 @@ function BrowserPagePane({ } if (webviewRegistry.get(browserTab.id) === webview) { + moveFocusToRendererBeforeWebviewDetach(webview) getHiddenContainer().appendChild(webview) parkedAtByTabId.set(browserTab.id, Date.now()) evictParkedWebviews(browserTab.id) diff --git a/src/renderer/src/components/browser-pane/webview-registry.test.ts b/src/renderer/src/components/browser-pane/webview-registry.test.ts index 7e8ddbcfe..3ebf38d32 100644 --- a/src/renderer/src/components/browser-pane/webview-registry.test.ts +++ b/src/renderer/src/components/browser-pane/webview-registry.test.ts @@ -6,11 +6,13 @@ type ListenerRecord = { options?: boolean | AddEventListenerOptions } -function createWebview(): Electron.WebviewTag { +function createWebview(overrides: Partial = {}): Electron.WebviewTag { return { style: {}, + blur: vi.fn(), remove: vi.fn(), - contains: vi.fn(() => false) + contains: vi.fn(() => false), + ...overrides } as unknown as Electron.WebviewTag } @@ -96,4 +98,39 @@ describe('webview registry drag listeners', () => { expect(addedListeners).toHaveLength(3) }) + + it('moves focus back to the renderer before detaching the focused webview', async () => { + const { moveFocusToRendererBeforeWebviewDetach } = await import('./webview-registry') + const webview = createWebview() + vi.stubGlobal('document', { activeElement: webview }) + + moveFocusToRendererBeforeWebviewDetach(webview) + + expect(webview.blur).toHaveBeenCalledTimes(1) + expect(window.focus).toHaveBeenCalledTimes(1) + }) + + it('moves focus back to the renderer before detaching a webview that contains focus', async () => { + const { moveFocusToRendererBeforeWebviewDetach } = await import('./webview-registry') + const activeElement = { blur: vi.fn() } as unknown as HTMLElement + const webview = createWebview({ contains: vi.fn(() => true) }) + vi.stubGlobal('document', { activeElement }) + + moveFocusToRendererBeforeWebviewDetach(webview) + + expect(activeElement.blur).toHaveBeenCalledTimes(1) + expect(window.focus).toHaveBeenCalledTimes(1) + }) + + it('leaves focus alone before detaching an unfocused webview', async () => { + const { moveFocusToRendererBeforeWebviewDetach } = await import('./webview-registry') + const activeElement = { blur: vi.fn() } as unknown as HTMLElement + const webview = createWebview() + vi.stubGlobal('document', { activeElement }) + + moveFocusToRendererBeforeWebviewDetach(webview) + + expect(activeElement.blur).not.toHaveBeenCalled() + expect(window.focus).not.toHaveBeenCalled() + }) }) diff --git a/src/renderer/src/components/browser-pane/webview-registry.ts b/src/renderer/src/components/browser-pane/webview-registry.ts index 5178aa087..6a630c56b 100644 --- a/src/renderer/src/components/browser-pane/webview-registry.ts +++ b/src/renderer/src/components/browser-pane/webview-registry.ts @@ -100,6 +100,17 @@ export function unregisterPersistentWebview(browserTabId: string): void { } } +export function moveFocusToRendererBeforeWebviewDetach(webview: Electron.WebviewTag): void { + // Why: if this webview currently owns focus, removing it lets macOS hand + // activation back to the previously-active app (Slack, etc.) because the + // focused webContents is gone with no replacement. Move focus back into the + // main renderer first so Electron keeps focus inside the Orca window. + if (webview === document.activeElement || webview.contains(document.activeElement)) { + ;(document.activeElement as HTMLElement | null)?.blur?.() + window.focus() + } +} + export function destroyPersistentWebview(browserTabId: string): void { const webview = webviewRegistry.get(browserTabId) if (!webview) { @@ -109,14 +120,7 @@ export function destroyPersistentWebview(browserTabId: string): void { return } void window.api.browser.unregisterGuest({ browserPageId: browserTabId }) - // Why: if this webview currently owns focus, removing it lets macOS hand - // activation back to the previously-active app (Slack, etc.) because the - // focused webContents is gone with no replacement. Move focus back into the - // main renderer first so Electron keeps focus inside the Orca window. - if (webview === document.activeElement || webview.contains(document.activeElement)) { - ;(document.activeElement as HTMLElement | null)?.blur?.() - window.focus() - } + moveFocusToRendererBeforeWebviewDetach(webview) webview.remove() unregisterPersistentWebview(browserTabId) registeredWebContentsIds.delete(browserTabId)