Merge remote-tracking branch 'origin/main' into task-browser-page-pushed-up

# Conflicts:
#	cmuxTests/CmuxWebViewKeyEquivalentTests.swift
This commit is contained in:
Lawrence Chen 2026-03-11 18:33:03 -07:00
commit e35e1cd94c
2 changed files with 78 additions and 1 deletions

View file

@ -1530,6 +1530,12 @@ final class BrowserPaneDropTargetView: NSView {
final class WindowBrowserSlotView: NSView {
override var isOpaque: Bool { false }
override var isHidden: Bool {
didSet {
guard isHidden, !oldValue, let window else { return }
yieldOwnedFirstResponderIfNeeded(in: window, reason: "slotHidden")
}
}
private let paneDropTargetView = BrowserPaneDropTargetView(frame: .zero)
private let dropZoneOverlayView = BrowserDropZoneOverlayView(frame: .zero)
private var searchOverlayHostingView: NSHostingView<BrowserSearchOverlay>?
@ -1571,6 +1577,13 @@ final class WindowBrowserSlotView: NSView {
nil
}
override func viewWillMove(toWindow newWindow: NSWindow?) {
if newWindow == nil, let currentWindow = window {
yieldOwnedFirstResponderIfNeeded(in: currentWindow, reason: "slotWillLeaveWindow")
}
super.viewWillMove(toWindow: newWindow)
}
override func layout() {
super.layout()
paneDropTargetView.frame = bounds
@ -1739,6 +1752,23 @@ final class WindowBrowserSlotView: NSView {
return window.makeFirstResponder(nil)
}
@discardableResult
private func yieldOwnedFirstResponderIfNeeded(in window: NSWindow, reason: String) -> Bool {
guard let firstResponder = window.firstResponder,
let owningView = firstResponder.browserPortalOwningView,
owningView === self || owningView.isDescendant(of: self) else {
return false
}
#if DEBUG
dlog(
"browser.slot.firstResponder.yield reason=\(reason) " +
"slot=\(browserPortalDebugToken(self)) " +
"responder=\(String(describing: type(of: firstResponder)))"
)
#endif
return window.makeFirstResponder(nil)
}
func pinHostedWebView(_ webView: WKWebView) {
guard webView.superview === self else { return }

View file

@ -2434,7 +2434,9 @@ final class BrowserSessionHistoryRestoreTests: XCTestCase {
@MainActor
final class BrowserDeveloperToolsVisibilityPersistenceTests: XCTestCase {
private final class WKInspectorProbeView: NSView {}
private final class WKInspectorProbeView: NSView {
override var acceptsFirstResponder: Bool { true }
}
private final class FakeInspector: NSObject {
private(set) var attachCount = 0
@ -11697,6 +11699,51 @@ final class BrowserWindowPortalLifecycleTests: XCTestCase {
)
}
func testHidingBrowserSlotYieldsOwnedInspectorFirstResponder() {
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 520, height: 320),
styleMask: [.titled, .closable],
backing: .buffered,
defer: false
)
defer { window.orderOut(nil) }
realizeWindowLayout(window)
guard let contentView = window.contentView else {
XCTFail("Expected content view")
return
}
let slot = WindowBrowserSlotView(frame: NSRect(x: 40, y: 24, width: 260, height: 180))
contentView.addSubview(slot)
let inspectorContainer = NSView(frame: slot.bounds)
inspectorContainer.autoresizingMask = [.width, .height]
let inspectorView = WKInspectorProbeView(frame: inspectorContainer.bounds)
inspectorView.autoresizingMask = [.width, .height]
inspectorContainer.addSubview(inspectorView)
slot.addSubview(inspectorContainer)
contentView.layoutSubtreeIfNeeded()
XCTAssertTrue(
window.makeFirstResponder(inspectorView),
"Precondition failed: inspector probe should become first responder"
)
XCTAssertTrue(window.firstResponder === inspectorView)
slot.isHidden = true
XCTAssertFalse(
window.firstResponder === inspectorView,
"Hiding a browser slot should yield any owned inspector responder before it goes off-screen"
)
if let firstResponderView = window.firstResponder as? NSView {
XCTAssertFalse(
firstResponderView === slot || firstResponderView.isDescendant(of: slot),
"Hiding a browser slot should not leave first responder inside the hidden slot"
)
}
}
func testHiddenPortalSyncDoesNotStealLocallyHostedDevToolsWebViewDuringResize() {
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 520, height: 320),