Prevent background webview autofocus from stealing focus

This commit is contained in:
Lawrence Chen 2026-02-22 19:13:15 -08:00
parent 0dbe95b797
commit a369cf4419
3 changed files with 55 additions and 0 deletions

View file

@ -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

View file

@ -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

View file

@ -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()