Fix omnibar focus thrash when another text field takes focus

This commit is contained in:
Lawrence Chen 2026-02-23 19:00:01 -08:00
parent 4e455a185c
commit 88c1dbc5d6
2 changed files with 64 additions and 2 deletions

View file

@ -2181,6 +2181,13 @@ struct OmnibarSuggestion: Identifiable, Hashable {
}
}
func browserOmnibarShouldReacquireFocusAfterEndEditing(
suppressWebViewFocus: Bool,
nextResponderIsOtherTextField: Bool
) -> Bool {
suppressWebViewFocus && !nextResponderIsOtherTextField
}
private final class OmnibarNativeTextField: NSTextField {
var onPointerDown: (() -> Void)?
var onHandleKeyEvent: ((NSEvent, NSTextView?) -> Bool)?
@ -2293,6 +2300,29 @@ private struct OmnibarTextFieldRepresentable: NSViewRepresentable {
}
}
private func nextResponderIsOtherTextField(window: NSWindow?) -> Bool {
guard let window, let field = parentField else { return false }
let responder = window.firstResponder
if let editor = responder as? NSTextView,
let delegateField = editor.delegate as? NSTextField {
return delegateField !== field
}
if let textField = responder as? NSTextField {
return textField !== field
}
return false
}
private func shouldReacquireFocusAfterEndEditing(window: NSWindow?) -> Bool {
return browserOmnibarShouldReacquireFocusAfterEndEditing(
suppressWebViewFocus: parent.shouldSuppressWebViewFocus(),
nextResponderIsOtherTextField: nextResponderIsOtherTextField(window: window)
)
}
func controlTextDidBeginEditing(_ obj: Notification) {
if !parent.isFocused {
DispatchQueue.main.async {
@ -2305,15 +2335,18 @@ private struct OmnibarTextFieldRepresentable: NSViewRepresentable {
func controlTextDidEndEditing(_ obj: Notification) {
if parent.isFocused {
if parent.shouldSuppressWebViewFocus() {
if shouldReacquireFocusAfterEndEditing(window: parentField?.window) {
guard pendingFocusRequest != true else { return }
pendingFocusRequest = true
DispatchQueue.main.async { [weak self] in
guard let self else { return }
self.pendingFocusRequest = nil
guard self.parent.isFocused else { return }
guard self.parent.shouldSuppressWebViewFocus() else { return }
guard let field = self.parentField, let window = field.window else { return }
guard self.shouldReacquireFocusAfterEndEditing(window: window) else {
self.parent.onFieldLostFocus()
return
}
// Check both the field itself AND its field editor (which becomes
// the actual first responder when the text field is being edited).
let fr = window.firstResponder

View file

@ -6012,3 +6012,32 @@ final class TerminalControllerSocketTextChunkTests: XCTestCase {
)
}
}
final class BrowserOmnibarFocusPolicyTests: XCTestCase {
func testReacquiresFocusWhenWebViewSuppressionIsActiveAndNextResponderIsNotAnotherTextField() {
XCTAssertTrue(
browserOmnibarShouldReacquireFocusAfterEndEditing(
suppressWebViewFocus: true,
nextResponderIsOtherTextField: false
)
)
}
func testDoesNotReacquireFocusWhenAnotherTextFieldAlreadyTookFocus() {
XCTAssertFalse(
browserOmnibarShouldReacquireFocusAfterEndEditing(
suppressWebViewFocus: true,
nextResponderIsOtherTextField: true
)
)
}
func testDoesNotReacquireFocusWhenWebViewSuppressionIsInactive() {
XCTAssertFalse(
browserOmnibarShouldReacquireFocusAfterEndEditing(
suppressWebViewFocus: false,
nextResponderIsOtherTextField: false
)
)
}
}