diff --git a/CHANGELOG.md b/CHANGELOG.md index c66a01dc4..9bd731d9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Installing or updating a plugin right after updating TablePro now refetches the current plugin list first, so it no longer fails against a stale cached list (the error a restart used to clear). (#1380) +- Pressing Esc to close the Raw SQL filter suggestions, or to clear a search field, no longer also exits fullscreen. (#1403) ## [0.44.0] - 2026-05-23 diff --git a/TablePro/Views/Filter/FilterValueTextField.swift b/TablePro/Views/Filter/FilterValueTextField.swift index 7c2533e9b..b4b0268eb 100644 --- a/TablePro/Views/Filter/FilterValueTextField.swift +++ b/TablePro/Views/Filter/FilterValueTextField.swift @@ -38,6 +38,24 @@ struct FilterValueTextField: NSViewRepresentable { return (ns.replacingCharacters(in: range, with: insertText), caret) } + enum SuggestionKeyOutcome: Equatable { + case moveSelection(Int) + case accept(submitting: Bool) + case dismiss + case passThrough + } + + static func suggestionKeyOutcome(for key: KeyCode?, submitsOnAccept: Bool) -> SuggestionKeyOutcome { + switch key { + case .downArrow: return .moveSelection(1) + case .upArrow: return .moveSelection(-1) + case .return: return .accept(submitting: submitsOnAccept) + case .tab: return .accept(submitting: false) + case .escape: return .dismiss + default: return .passThrough + } + } + func makeNSView(context: Context) -> NSTextField { let textField = SubstitutionDisabledTextField() textField.bezelStyle = .roundedBezel @@ -176,6 +194,10 @@ struct FilterValueTextField: NSViewRepresentable { text.wrappedValue = textView.string return true } + if commandSelector == #selector(NSResponder.cancelOperation(_:)) { + dismissSuggestions() + return true + } return false } @@ -291,25 +313,20 @@ struct FilterValueTextField: NSViewRepresentable { nsEvent.window?.firstResponder === textField.currentEditor() else { return nsEvent } - switch nsEvent.semanticKeyCode { - case .downArrow: - self.moveSelection(by: 1) - return nil - case .upArrow: - self.moveSelection(by: -1) - return nil - case .return: - self.acceptCurrentSelection(submitting: self.submitsOnAccept) - return nil - case .tab: - self.acceptCurrentSelection(submitting: false) - return nil - case .escape: + switch FilterValueTextField.suggestionKeyOutcome( + for: nsEvent.semanticKeyCode, + submitsOnAccept: self.submitsOnAccept + ) { + case .moveSelection(let delta): + self.moveSelection(by: delta) + case .accept(let submitting): + self.acceptCurrentSelection(submitting: submitting) + case .dismiss: self.dismissSuggestions() - return nsEvent - default: + case .passThrough: return nsEvent } + return nil } } } diff --git a/TablePro/Views/Sidebar/NativeSearchField.swift b/TablePro/Views/Sidebar/NativeSearchField.swift index 43e862fff..3062710c4 100644 --- a/TablePro/Views/Sidebar/NativeSearchField.swift +++ b/TablePro/Views/Sidebar/NativeSearchField.swift @@ -98,9 +98,8 @@ struct NativeSearchField: NSViewRepresentable { if !field.stringValue.isEmpty { field.stringValue = "" text.wrappedValue = "" - return true } - return false + return true } if commandSelector == #selector(NSResponder.moveUp(_:)), let onMoveUp { onMoveUp() diff --git a/TableProTests/Views/Filter/FilterValueTextFieldTests.swift b/TableProTests/Views/Filter/FilterValueTextFieldTests.swift index 33b1b6f39..debffc3e8 100644 --- a/TableProTests/Views/Filter/FilterValueTextFieldTests.swift +++ b/TableProTests/Views/Filter/FilterValueTextFieldTests.swift @@ -113,4 +113,24 @@ struct FilterValueTextFieldTests { ) #expect(result == nil) } + + @Test("Escape dismisses the suggestions and is consumed, not passed through") + func testKeyOutcome_escapeDismisses() { + #expect(FilterValueTextField.suggestionKeyOutcome(for: .escape, submitsOnAccept: true) == .dismiss) + #expect(FilterValueTextField.suggestionKeyOutcome(for: .escape, submitsOnAccept: false) == .dismiss) + } + + @Test("Arrow and accept keys map to consuming outcomes") + func testKeyOutcome_navigationAndAccept() { + #expect(FilterValueTextField.suggestionKeyOutcome(for: .downArrow, submitsOnAccept: false) == .moveSelection(1)) + #expect(FilterValueTextField.suggestionKeyOutcome(for: .upArrow, submitsOnAccept: false) == .moveSelection(-1)) + #expect(FilterValueTextField.suggestionKeyOutcome(for: .return, submitsOnAccept: true) == .accept(submitting: true)) + #expect(FilterValueTextField.suggestionKeyOutcome(for: .tab, submitsOnAccept: true) == .accept(submitting: false)) + } + + @Test("Unhandled keys pass through unchanged") + func testKeyOutcome_passThrough() { + #expect(FilterValueTextField.suggestionKeyOutcome(for: .space, submitsOnAccept: true) == .passThrough) + #expect(FilterValueTextField.suggestionKeyOutcome(for: nil, submitsOnAccept: true) == .passThrough) + } }