From cdab97c2abc4d72884b93ab5dd3c3d40e8462bd9 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sat, 30 May 2026 13:53:13 +0700 Subject: [PATCH] feat(connections): keyboard focus and VoiceOver in the connection form and welcome screen (#1490) --- CHANGELOG.md | 1 + TablePro/Views/Components/ColorPaletteView.swift | 4 +++- TablePro/Views/Connection/HostListFieldRow.swift | 1 + TablePro/Views/Connection/OnboardingContentView.swift | 2 ++ TablePro/Views/Connection/WelcomeWindowView.swift | 3 +++ TablePro/Views/ConnectionForm/Panes/GeneralPaneView.swift | 3 +++ 6 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a74cc19e..f447677b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- The connection form opens with the Name field focused, Return or the Down arrow in the welcome search moves to the connection list, and focus returns to the list after a sheet closes, so you can set up a connection without the mouse. (#1490) - Escape now dismisses search-based sheets and popovers (database switcher, quick switcher, column and connection pickers). Pressing Escape clears the search text first; a second Escape closes the sheet. (#1490) - Running `EXPLAIN` or `EXPLAIN ANALYZE` typed in the editor now opens the plan viewer instead of squashing the plan into one truncated grid cell. (#1480) - Filtering the data grid keeps you on the keyboard. Applying or clearing a filter returns focus to the grid so you can keep moving through cells, Return applies the filter, and Escape closes the filter panel and returns to the grid. (#1490) diff --git a/TablePro/Views/Components/ColorPaletteView.swift b/TablePro/Views/Components/ColorPaletteView.swift index b67b65ce9..798a23306 100644 --- a/TablePro/Views/Components/ColorPaletteView.swift +++ b/TablePro/Views/Components/ColorPaletteView.swift @@ -36,11 +36,13 @@ struct ColorPaletteView: View { var body: some View { HStack(spacing: size.spacing) { ForEach(colors) { color in + let isSelected = selectedColor == color Button { selectedColor = color } label: { - ColorSwatch(color: color, isSelected: selectedColor == color, size: size) + ColorSwatch(color: color, isSelected: isSelected, size: size) } .buttonStyle(.plain) .accessibilityLabel(String(format: String(localized: "Color %@"), color.rawValue)) + .accessibilityAddTraits(isSelected ? [.isSelected] : []) } } } diff --git a/TablePro/Views/Connection/HostListFieldRow.swift b/TablePro/Views/Connection/HostListFieldRow.swift index 04b11f317..5cfab7b9a 100644 --- a/TablePro/Views/Connection/HostListFieldRow.swift +++ b/TablePro/Views/Connection/HostListFieldRow.swift @@ -46,6 +46,7 @@ struct HostListFieldRow: View { ForEach(entries) { entry in TextField("", text: bindingForEntry(entry), prompt: Text(verbatim: "hostname:\(defaultPort)")) .tag(entry.id) + .accessibilityLabel(String(localized: "Host")) } } .listStyle(.bordered(alternatesRowBackgrounds: false)) diff --git a/TablePro/Views/Connection/OnboardingContentView.swift b/TablePro/Views/Connection/OnboardingContentView.swift index 2083a3d90..2162ba53e 100644 --- a/TablePro/Views/Connection/OnboardingContentView.swift +++ b/TablePro/Views/Connection/OnboardingContentView.swift @@ -202,6 +202,7 @@ struct OnboardingContentView: View { } .buttonStyle(.borderedProminent) .controlSize(.large) + .keyboardShortcut(.defaultAction) .transition(.opacity) } else { Button("Get Started") { @@ -209,6 +210,7 @@ struct OnboardingContentView: View { } .buttonStyle(.borderedProminent) .controlSize(.large) + .keyboardShortcut(.defaultAction) .transition(.opacity) } } diff --git a/TablePro/Views/Connection/WelcomeWindowView.swift b/TablePro/Views/Connection/WelcomeWindowView.swift index 83b59f549..cd46243bc 100644 --- a/TablePro/Views/Connection/WelcomeWindowView.swift +++ b/TablePro/Views/Connection/WelcomeWindowView.swift @@ -79,6 +79,7 @@ struct WelcomeWindowView: View { vm.importResultCount = count vm.pendingImportResultCount = nil } + focus = .connectionList }) { sheet in switch sheet { case .newGroup(let parentId): @@ -279,6 +280,8 @@ struct WelcomeWindowView: View { text: $vm.searchText, placeholder: String(localized: "Search for connection..."), controlSize: .regular, + onMoveDown: { focus = .connectionList }, + onSubmit: { focus = .connectionList }, focusTrigger: searchFocusTrigger, maxWidth: 240 ) diff --git a/TablePro/Views/ConnectionForm/Panes/GeneralPaneView.swift b/TablePro/Views/ConnectionForm/Panes/GeneralPaneView.swift index b78d11d11..848bb67ae 100644 --- a/TablePro/Views/ConnectionForm/Panes/GeneralPaneView.swift +++ b/TablePro/Views/ConnectionForm/Panes/GeneralPaneView.swift @@ -10,6 +10,7 @@ import UniformTypeIdentifiers struct GeneralPaneView: View { @Bindable var coordinator: ConnectionFormCoordinator + @FocusState private var nameFocused: Bool private var type: DatabaseType { coordinator.network.type } private var connectionMode: ConnectionMode { @@ -35,6 +36,7 @@ struct GeneralPaneView: View { text: $coordinator.network.name, prompt: Text(String(localized: "Connection name")) ) + .focused($nameFocused) } connectionSection @@ -42,6 +44,7 @@ struct GeneralPaneView: View { testConnectionSection } .formStyle(.grouped) + .defaultFocus($nameFocused, true) } @ViewBuilder