Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/Mac/App/ApproachNoteMacApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -333,4 +333,7 @@ extension Notification.Name {
static let navigateToSongs = Notification.Name("navigateToSongs")
static let navigateToArtists = Notification.Name("navigateToArtists")
static let navigateToRecordings = Notification.Name("navigateToRecordings")
/// Posted when the Artists tab becomes selected so its search field can
/// take focus.
static let focusArtistSearch = Notification.Name("focusArtistSearch")
}
12 changes: 11 additions & 1 deletion apps/Mac/Views/ArtistsListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ struct ArtistsListView: View {
@State private var searchTask: Task<Void, Never>?
@State private var selectedPerformerId: String?
@State private var hasPerformedInitialLoad = false
@FocusState private var searchFocused: Bool

// MARK: - Sort Name Helpers

Expand Down Expand Up @@ -75,6 +76,14 @@ struct ArtistsListView: View {
hasPerformedInitialLoad = true
}
}
.onReceive(NotificationCenter.default.publisher(for: .focusArtistSearch)) { _ in
// Defer a beat so the tab content is on screen / in the responder
// chain before we move focus to the search field.
Task { @MainActor in
try? await Task.sleep(nanoseconds: 100_000_000)
searchFocused = true
}
}
}

// MARK: - View Components
Expand All @@ -84,7 +93,8 @@ struct ArtistsListView: View {
MacSearchBar(
text: $searchText,
placeholder: "Search artists...",
backgroundColor: ApproachNoteTheme.accent
backgroundColor: ApproachNoteTheme.accent,
focus: $searchFocused
)

List(selection: $selectedPerformerId) {
Expand Down
7 changes: 7 additions & 0 deletions apps/Mac/Views/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,13 @@ struct ContentView: View {
.onReceive(NotificationCenter.default.publisher(for: .navigateToRecordings)) { _ in
selectedTab = .recordings
}
.onChange(of: selectedTab) { _, newTab in
// When the Artists tab is selected (via the tab control or the
// menu command), default the insertion cursor to its search field.
if newTab == .artists {
NotificationCenter.default.post(name: .focusArtistSearch, object: nil)
}
}
.preferredColorScheme(.light)
}
}
Expand Down
18 changes: 18 additions & 0 deletions apps/Mac/Views/MacSearchBar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ struct MacSearchBar: View {
@Binding var text: String
let placeholder: String
let backgroundColor: Color
/// Optional external focus binding so a parent can drive (or observe)
/// focus on the search field.
var focus: FocusState<Bool>.Binding? = nil

var body: some View {
HStack {
Expand All @@ -21,6 +24,7 @@ struct MacSearchBar: View {
.font(ApproachNoteTheme.body())
.bodyLineSpacing()
.foregroundColor(ApproachNoteTheme.textPrimary)
.modifier(OptionalFocusModifier(focus: focus))
if !text.isEmpty {
Button(action: { text = "" }) {
Image(systemName: "xmark.circle.fill")
Expand All @@ -42,6 +46,20 @@ struct MacSearchBar: View {
}
}

/// Applies `.focused` only when a binding is supplied, so callers that don't
/// need programmatic focus can omit it.
private struct OptionalFocusModifier: ViewModifier {
let focus: FocusState<Bool>.Binding?

func body(content: Content) -> some View {
if let focus {
content.focused(focus)
} else {
content
}
}
}

#Preview {
VStack(spacing: 0) {
MacSearchBar(
Expand Down
Loading
Loading