Render Add Source buttons inline on macOS where toolbar items aren't shown

Pre-macOS 26 doesn't reliably surface a detail-pane NavigationStack's
toolbar confirmation items in the window title bar, leaving the
"Add Source" actions invisible. Gate the toolbar placement behind
macOS 26+ and fall back to an inline form/header button on older
macOS across the add-source views and the sources list.
This commit is contained in:
Arkadiusz Fal
2026-05-15 07:55:30 +02:00
parent bc81b03821
commit 37d11b50a8
5 changed files with 172 additions and 40 deletions

View File

@@ -31,6 +31,21 @@ struct AddLocalFolderView: View {
!name.isEmpty && selectedFolderURL != nil
}
/// macOS surfaces toolbar confirmation items from a pushed-in-sheet view
/// reliably only from macOS 26 onward. On older macOS we render the
/// "Add Source" action button inline in the form instead.
private var usesToolbarActionButton: Bool {
#if os(macOS)
if #available(macOS 26, *) {
return true
} else {
return false
}
#else
return false
#endif
}
// MARK: - Body
var body: some View {
@@ -99,16 +114,22 @@ struct AddLocalFolderView: View {
if let result = testResult {
SourceTestResultSection(result: result)
}
if !usesToolbarActionButton {
actionSection
}
}
.formStyle(.grouped)
.navigationTitle(String(localized: "sources.addLocalFolder"))
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button(String(localized: "sources.addSource")) {
addSource()
if usesToolbarActionButton {
ToolbarItem(placement: .confirmationAction) {
Button(String(localized: "sources.addSource")) {
addSource()
}
.disabled(!canAdd)
.keyboardShortcut(.defaultAction)
}
.disabled(!canAdd)
.keyboardShortcut(.defaultAction)
}
}
}

View File

@@ -87,6 +87,22 @@ struct AddRemoteServerView: View {
return false
}
/// macOS surfaces toolbar confirmation items from a pushed-in-sheet view
/// reliably only from macOS 26 onward. On older macOS (and on iOS/tvOS) we
/// render the "Add Source" action button inline in the form instead, so it
/// is always visible.
private var usesToolbarActionButton: Bool {
#if os(macOS)
if #available(macOS 26, *) {
return true
} else {
return false
}
#else
return false
#endif
}
private var canAdd: Bool {
guard !urlString.isEmpty else { return false }
@@ -172,9 +188,9 @@ struct AddRemoteServerView: View {
if isFieldsRevealed {
serverConfigurationFields
#if !os(macOS)
actionSection
#endif
if !usesToolbarActionButton {
actionSection
}
}
}
#if os(iOS)
@@ -183,8 +199,8 @@ struct AddRemoteServerView: View {
#if os(macOS)
.formStyle(.grouped)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
if isFieldsRevealed {
if isFieldsRevealed, usesToolbarActionButton {
ToolbarItem(placement: .confirmationAction) {
Button {
addSource()
} label: {

View File

@@ -36,6 +36,21 @@ struct AddSMBView: View {
!name.isEmpty && !server.isEmpty
}
/// macOS surfaces toolbar confirmation items from a pushed-in-sheet view
/// reliably only from macOS 26 onward. On older macOS we render the
/// "Add Source" action button inline in the form instead.
private var usesToolbarActionButton: Bool {
#if os(macOS)
if #available(macOS 26, *) {
return true
} else {
return false
}
#else
return false
#endif
}
// MARK: - Body
var body: some View {
@@ -133,25 +148,31 @@ struct AddSMBView: View {
if let result = testResult {
SourceTestResultSection(result: result)
}
if !usesToolbarActionButton {
actionSection
}
}
.formStyle(.grouped)
.navigationTitle(String(localized: "sources.addSMB"))
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button {
addSource()
} label: {
if isTesting {
HStack(spacing: 6) {
ProgressView().controlSize(.small)
Text(testProgress ?? String(localized: "sources.testing"))
if usesToolbarActionButton {
ToolbarItem(placement: .confirmationAction) {
Button {
addSource()
} label: {
if isTesting {
HStack(spacing: 6) {
ProgressView().controlSize(.small)
Text(testProgress ?? String(localized: "sources.testing"))
}
} else {
Text(String(localized: "sources.addSource"))
}
} else {
Text(String(localized: "sources.addSource"))
}
.disabled(!canAdd || isTesting)
.keyboardShortcut(.defaultAction)
}
.disabled(!canAdd || isTesting)
.keyboardShortcut(.defaultAction)
}
}
.onAppear {

View File

@@ -37,6 +37,21 @@ struct AddWebDAVView: View {
!name.isEmpty && !urlString.isEmpty && URL(string: urlString) != nil
}
/// macOS surfaces toolbar confirmation items from a pushed-in-sheet view
/// reliably only from macOS 26 onward. On older macOS we render the
/// "Add Source" action button inline in the form instead.
private var usesToolbarActionButton: Bool {
#if os(macOS)
if #available(macOS 26, *) {
return true
} else {
return false
}
#else
return false
#endif
}
// MARK: - Body
var body: some View {
@@ -134,25 +149,31 @@ struct AddWebDAVView: View {
if let result = testResult {
SourceTestResultSection(result: result)
}
if !usesToolbarActionButton {
actionSection
}
}
.formStyle(.grouped)
.navigationTitle(String(localized: "sources.addWebDAV"))
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button {
addSource()
} label: {
if isTesting {
HStack(spacing: 6) {
ProgressView().controlSize(.small)
Text(testProgress ?? String(localized: "sources.testing"))
if usesToolbarActionButton {
ToolbarItem(placement: .confirmationAction) {
Button {
addSource()
} label: {
if isTesting {
HStack(spacing: 6) {
ProgressView().controlSize(.small)
Text(testProgress ?? String(localized: "sources.testing"))
}
} else {
Text(String(localized: "sources.addSource"))
}
} else {
Text(String(localized: "sources.addSource"))
}
.disabled(!canAdd || isTesting)
.keyboardShortcut(.defaultAction)
}
.disabled(!canAdd || isTesting)
.keyboardShortcut(.defaultAction)
}
}
.onAppear {

View File

@@ -64,6 +64,16 @@ struct SourcesListView: View {
.focusSection()
}
}
#elseif os(macOS)
VStack(spacing: 0) {
// macOS 15 does not surface a detail-pane NavigationStack's
// toolbar items to the window title bar, so the toolbar
// "Add Source" button is invisible there. Show an inline
// header button as a fallback on older macOS. On macOS 26+
// the toolbar button works, so this header is omitted.
macOSLegacyAddHeader
sourcesInner
}
#else
sourcesInner
#endif
@@ -76,13 +86,15 @@ struct SourcesListView: View {
#endif
#if os(iOS) || os(macOS)
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button {
showingAddSheet = true
} label: {
Label(String(localized: "sources.addSource"), systemImage: "plus")
if showAddButtonInToolbar {
ToolbarItem(placement: .primaryAction) {
Button {
showingAddSheet = true
} label: {
Label(String(localized: "sources.addSource"), systemImage: "plus")
}
.accessibilityIdentifier("sources.addButton")
}
.accessibilityIdentifier("sources.addButton")
}
}
#endif
@@ -122,6 +134,47 @@ struct SourcesListView: View {
#endif
}
/// Whether the "Add Source" button should be placed in the window/navigation
/// toolbar. On macOS this only works reliably from macOS 26 onward (see
/// `macOSLegacyAddHeader`); iOS always uses the toolbar.
private var showAddButtonInToolbar: Bool {
#if os(iOS)
return true
#elseif os(macOS)
if #available(macOS 26, *) {
return true
} else {
return false
}
#else
return false
#endif
}
#if os(macOS)
// Inline fallback "Add Source" button for macOS versions where the
// detail-pane toolbar item is not shown (pre-macOS 26). Only needed when
// there are existing sources the empty state already offers its own
// add button.
@ViewBuilder
private var macOSLegacyAddHeader: some View {
if !showAddButtonInToolbar, !isEmpty {
HStack {
Spacer()
Button {
showingAddSheet = true
} label: {
Label(String(localized: "sources.addSource"), systemImage: "plus")
}
.accessibilityIdentifier("sources.addButton")
}
.padding(.horizontal, 16)
.padding(.top, 12)
.padding(.bottom, 4)
}
}
#endif
@ViewBuilder
private var sourcesInner: some View {
if isEmpty {