From a369cf44195f5022e177fcf524e258c22d6132cd Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Sun, 22 Feb 2026 19:13:15 -0800 Subject: [PATCH] Prevent background webview autofocus from stealing focus --- Sources/Panels/BrowserPanelView.swift | 14 ++++++++ Sources/Panels/CmuxWebView.swift | 8 +++++ cmuxTests/CmuxWebViewKeyEquivalentTests.swift | 33 +++++++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/Sources/Panels/BrowserPanelView.swift b/Sources/Panels/BrowserPanelView.swift index 37f0af44..98e311e3 100644 --- a/Sources/Panels/BrowserPanelView.swift +++ b/Sources/Panels/BrowserPanelView.swift @@ -3112,6 +3112,11 @@ struct WebViewRepresentable: NSViewRepresentable { let webView = panel.webView context.coordinator.panel = panel context.coordinator.webView = webView + Self.applyWebViewFirstResponderPolicy( + panel: panel, + webView: webView, + isPanelFocused: isPanelFocused + ) let shouldUseWindowPortal = panel.shouldPreserveWebViewAttachmentDuringTransientHide() if shouldUseWindowPortal { @@ -3359,6 +3364,15 @@ struct WebViewRepresentable: NSViewRepresentable { } } + private static func applyWebViewFirstResponderPolicy( + panel: BrowserPanel, + webView: WKWebView, + isPanelFocused: Bool + ) { + guard let cmuxWebView = webView as? CmuxWebView else { return } + cmuxWebView.allowsFirstResponderAcquisition = isPanelFocused && !panel.shouldSuppressWebViewFocus() + } + static func dismantleNSView(_ nsView: NSView, coordinator: Coordinator) { coordinator.attachRetryWorkItem?.cancel() coordinator.attachRetryWorkItem = nil diff --git a/Sources/Panels/CmuxWebView.swift b/Sources/Panels/CmuxWebView.swift index 7ee2d00a..93f5a321 100644 --- a/Sources/Panels/CmuxWebView.swift +++ b/Sources/Panels/CmuxWebView.swift @@ -22,6 +22,14 @@ final class CmuxWebView: WKWebView { var onContextMenuDownloadStateChanged: ((Bool) -> Void)? var contextMenuLinkURLProvider: ((CmuxWebView, NSPoint, @escaping (URL?) -> Void) -> Void)? var contextMenuDefaultBrowserOpener: ((URL) -> Bool)? + /// Guard against background panes stealing first responder (e.g. page autofocus). + /// BrowserPanelView updates this as pane focus state changes. + var allowsFirstResponderAcquisition: Bool = true + + override func becomeFirstResponder() -> Bool { + guard allowsFirstResponderAcquisition else { return false } + return super.becomeFirstResponder() + } override func performKeyEquivalent(with event: NSEvent) -> Bool { // Preserve Cmd+Return/Enter for web content (e.g. editors/forms). Do not diff --git a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift index 137ede9e..d536fed5 100644 --- a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift +++ b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift @@ -99,6 +99,39 @@ final class CmuxWebViewKeyEquivalentTests: XCTestCase { XCTAssertTrue(spy.invoked) } + @MainActor + func testCanBlockFirstResponderAcquisitionWhenPaneIsUnfocused() { + _ = NSApplication.shared + + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 640, height: 420), + styleMask: [.titled, .closable], + backing: .buffered, + defer: false + ) + let container = NSView(frame: window.contentRect(forFrameRect: window.frame)) + window.contentView = container + + let webView = CmuxWebView(frame: container.bounds, configuration: WKWebViewConfiguration()) + webView.autoresizingMask = [.width, .height] + container.addSubview(webView) + + window.makeKeyAndOrderFront(nil) + defer { window.orderOut(nil) } + + webView.allowsFirstResponderAcquisition = true + XCTAssertTrue(window.makeFirstResponder(webView)) + + _ = window.makeFirstResponder(nil) + webView.allowsFirstResponderAcquisition = false + XCTAssertFalse(webView.becomeFirstResponder()) + + _ = window.makeFirstResponder(webView) + if let firstResponderView = window.firstResponder as? NSView { + XCTAssertFalse(firstResponderView === webView || firstResponderView.isDescendant(of: webView)) + } + } + private func installMenu(spy: ActionSpy, key: String, modifiers: NSEvent.ModifierFlags) { let mainMenu = NSMenu()