Allow returning from global search with the escape key

cmd+k jumps to the search tab but left users with no way back. Pressing
escape in the search field now returns to the tab they came from (never
into search itself).

Escape is handled at the search field's pressesBegan, alongside the
existing arrow-key navigation, since the focused UIKit field swallows
SwiftUI keyboard shortcuts.
This commit is contained in:
Stefan Ceriu
2026-06-22 16:22:57 +03:00
parent a04a4c2691
commit c5653c2208
7 changed files with 44 additions and 4 deletions
@@ -15,6 +15,7 @@ struct SearchScreenCoordinatorParameters {
enum SearchScreenCoordinatorAction {
case presentRoom(roomID: String)
case cancel
}
final class SearchScreenCoordinator: CoordinatorProtocol {
@@ -38,6 +39,8 @@ final class SearchScreenCoordinator: CoordinatorProtocol {
switch action {
case .presentRoom(let roomID):
actionsSubject.send(.presentRoom(roomID: roomID))
case .cancel:
actionsSubject.send(.cancel)
}
}
.store(in: &cancellables)
@@ -9,6 +9,7 @@ import Foundation
enum SearchScreenViewModelAction {
case presentRoom(roomID: String)
case cancel
}
struct SearchScreenViewState: BindableState {
@@ -29,6 +30,7 @@ enum SearchScreenViewAction {
case selectRoom(roomID: String)
case reachedTop
case reachedBottom
case cancel
}
struct SearchScreenRoom: Identifiable, Equatable {
@@ -65,6 +65,8 @@ class SearchScreenViewModel: SearchScreenViewModelType, SearchScreenViewModelPro
updateVisibleRange(edge: .top)
case .reachedBottom:
updateVisibleRange(edge: .bottom)
case .cancel:
actionsSubject.send(.cancel)
}
}
@@ -47,7 +47,8 @@ struct SearchScreen: View {
}
}
.searchResultsKeyboardNavigation(moveUp: { moveSelection(backwards: true) },
moveDown: { moveSelection(backwards: false) })
moveDown: { moveSelection(backwards: false) },
cancel: { context.send(viewAction: .cancel) })
// The TabView calls onAppear each time the search tab is selected, so the field re-focuses
// and the selection resets on every switch.
.onAppear {
@@ -189,12 +190,12 @@ private struct SearchScreenRoomCellButtonStyle: ButtonStyle {
}
private extension View {
/// Forwards hardware up/down arrow presses from the system search field to the given closures.
/// Forwards hardware up/down arrow and escape presses from the system search field to the given closures.
///
/// The `.search` role tab owns its text field, so there's no SwiftUI hook for its key presses. We reach the
/// field through the navigation stack (as ``SearchFieldStyle`` does) and swap its class for one that overrides
/// `pressesBegan` — the same key interception the old global search performed on its own field.
func searchResultsKeyboardNavigation(moveUp: @escaping () -> Void, moveDown: @escaping () -> Void) -> some View {
func searchResultsKeyboardNavigation(moveUp: @escaping () -> Void, moveDown: @escaping () -> Void, cancel: @escaping () -> Void) -> some View {
introspect(.navigationStack, on: .supportedVersions, scope: .ancestor) { navigationController in
guard let textField = navigationController.navigationBar.topItem?.searchController?.searchBar.searchTextField else {
return
@@ -210,6 +211,8 @@ private extension View {
moveUp()
case .keyboardDownArrow:
moveDown()
case .keyboardEscape:
cancel()
default:
return false
}