diff --git a/docs/SMOKE_TEST.md b/docs/SMOKE_TEST.md index a028df7..7f25017 100644 --- a/docs/SMOKE_TEST.md +++ b/docs/SMOKE_TEST.md @@ -42,7 +42,7 @@ Use this checklist before a release or after changes to panel, pasteboard, setti 9. With a card or collection chip focused, type a normal character and confirm focus returns to search with that character inserted and results filtered. 10. Use a mouse wheel or two-finger vertical scroll over the card shelf and a crowded collection rail; confirm each pans horizontally, clamps at both ends, and shows subtle edge fades only where more content is hidden. 11. Right-click a filtered result and choose Show in Clipboard, or press `Command + G`, and confirm search clears while the same card stays selected in Most Recent. -12. Press `Esc` once with a non-empty search field and confirm search clears. +12. Press `Esc` once with a non-empty search while the search field, a card, or a collection chip is focused and confirm search clears without closing the panel. 13. Press `Esc` again and confirm the panel closes. 14. Reopen the panel, change sort segments, and confirm each segment updates results. 15. Press `Shift + Command + N` or the collection rail `+`, enter `Client Work`, choose a color, and confirm a Client Work chip appears with 0 clips and an empty collection view. diff --git a/sources/clipbored/views/ClipboardPanelController.swift b/sources/clipbored/views/ClipboardPanelController.swift index 8fb9fe7..172cda1 100644 --- a/sources/clipbored/views/ClipboardPanelController.swift +++ b/sources/clipbored/views/ClipboardPanelController.swift @@ -390,7 +390,9 @@ final class ClipboardPanelController: NSObject, NSWindowDelegate, QLPreviewPanel } switch event.keyCode { case 53: - self.hide() + if !self.panelView.clearSearchForKeyboardCancel() { + self.hide() + } return nil case 49: self.previewSelected() diff --git a/sources/clipbored/views/ClipboardPanelView.swift b/sources/clipbored/views/ClipboardPanelView.swift index 5508321..3c4c70d 100644 --- a/sources/clipbored/views/ClipboardPanelView.swift +++ b/sources/clipbored/views/ClipboardPanelView.swift @@ -1231,6 +1231,15 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate { window?.makeFirstResponder(searchField) } + @discardableResult + func clearSearchForKeyboardCancel() -> Bool { + guard !searchField.stringValue.clipboardTrimmed.isEmpty else { return false } + searchField.stringValue = "" + updateSearchText() + focusSearchField() + return true + } + private func startSearchFromShelf(_ text: String) { guard !text.isEmpty else { return } focusSearchField() @@ -1495,6 +1504,11 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate { debugPressFocusedResponder(characters: characters, keyCode: keyCode) } + func debugSetSearchFieldText(_ text: String) { + searchField.stringValue = text + updateSearchText() + } + private func debugPressFocusedResponder(characters: String, keyCode: UInt16) { guard let window, let event = NSEvent.keyEvent( @@ -1635,11 +1649,10 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate { viewModel.pasteSelected() return true case #selector(NSResponder.cancelOperation(_:)): - if searchField.stringValue.clipboardTrimmed.isEmpty { - onClose() + if clearSearchForKeyboardCancel() { + return true } else { - searchField.stringValue = "" - updateSearchText() + onClose() } return true case #selector(NSResponder.moveUp(_:)): diff --git a/tests/clipboredtests/ClipboardPanelViewTests.swift b/tests/clipboredtests/ClipboardPanelViewTests.swift index b8b8fad..442de1c 100644 --- a/tests/clipboredtests/ClipboardPanelViewTests.swift +++ b/tests/clipboredtests/ClipboardPanelViewTests.swift @@ -263,6 +263,28 @@ final class ClipboardPanelViewTests: XCTestCase { XCTAssertEqual(fixture.viewModel.visibleItems.map(\.payload), ["Quantum reference"]) } + func testKeyboardCancelClearsSearchFromFocusedCollectionChip() { + let fixture = makePanelFixture() + fixture.store.upsert(makeTextItem("Alpha note", store: fixture.store)) + fixture.store.upsert(makeTextItem("Quantum reference", store: fixture.store)) + drainMainQueue() + fixture.window.contentView?.layoutSubtreeIfNeeded() + fixture.view.debugSetSearchFieldText("quantum") + drainMainQueue() + fixture.window.contentView?.layoutSubtreeIfNeeded() + + XCTAssertEqual(fixture.viewModel.visibleItems.map(\.payload), ["Quantum reference"]) + XCTAssertTrue(fixture.view.debugFocusCollectionChip(.text)) + + XCTAssertTrue(fixture.view.clearSearchForKeyboardCancel()) + drainMainQueue() + fixture.window.contentView?.layoutSubtreeIfNeeded() + + XCTAssertTrue(fixture.view.isSearchFieldEditing) + XCTAssertEqual(fixture.view.debugSearchFieldText, "") + XCTAssertEqual(fixture.viewModel.visibleItems.map(\.payload), ["Quantum reference", "Alpha note"]) + } + func testCollectionRailAddButtonCreatesEmptyCollection() { let fixture = makePanelFixture() @@ -414,6 +436,29 @@ final class ClipboardPanelViewTests: XCTestCase { XCTAssertEqual(fixture.viewModel.visibleItems.map(\.payload), ["Quantum card"]) } + func testKeyboardCancelClearsSearchFromFocusedCard() { + let fixture = makePanelFixture() + fixture.store.upsert(makeTextItem("Alpha note", store: fixture.store)) + fixture.store.upsert(makeTextItem("Quantum card", store: fixture.store)) + drainMainQueue() + fixture.window.contentView?.layoutSubtreeIfNeeded() + fixture.view.debugSetSearchFieldText("quantum") + drainMainQueue() + fixture.window.contentView?.layoutSubtreeIfNeeded() + + XCTAssertEqual(fixture.viewModel.visibleItems.map(\.payload), ["Quantum card"]) + XCTAssertTrue(fixture.view.debugFocusCard(at: 0)) + + XCTAssertTrue(fixture.view.clearSearchForKeyboardCancel()) + drainMainQueue() + fixture.window.contentView?.layoutSubtreeIfNeeded() + + XCTAssertTrue(fixture.view.isSearchFieldEditing) + XCTAssertEqual(fixture.view.debugSearchFieldText, "") + XCTAssertEqual(fixture.viewModel.visibleItems.map(\.payload), ["Quantum card", "Alpha note"]) + XCTAssertFalse(fixture.view.clearSearchForKeyboardCancel()) + } + func testFocusedPreviewableCardSpaceOpensQuickLook() { let fixture = makePanelFixture() fixture.store.upsert(makeItem(kind: .url, text: "https://example.com/read", store: fixture.store))