diff --git a/.Jules/palette.md b/.Jules/palette.md index f61603c8..4da60612 100644 --- a/.Jules/palette.md +++ b/.Jules/palette.md @@ -25,3 +25,7 @@ ## 2026-06-23 - [SwiftUI Button Accessibility] **Learning:** Found a pattern where SwiftUI icon-only buttons or minimal UI elements were given `.help()` modifiers for hover tooltips but lacked `.accessibilityLabel()` modifiers for screen readers. **Action:** When adding `.help()` to buttons, always pair it with a corresponding `.accessibilityLabel()` to ensure full accessibility. + +## 2024-05-18 - [Accessibility context in generic list rows] +**Learning:** Found that generic icon-only buttons (like Edit/Delete) inside lists lacked specific context for screen readers. Using variables from the list loop in string interpolation (`"Edit \(item.name)"`) makes them significantly more navigable. +**Action:** Always include item-specific properties inside `accessibilityLabel` modifiers on buttons inside `ForEach` loops, and ensure fallbacks exist for empty string cases (e.g. `macro.name.isEmpty ? "Unnamed Macro" : macro.name`). diff --git a/XboxControllerMapper/XboxControllerMapper/Views/Macros/MacroListView.swift b/XboxControllerMapper/XboxControllerMapper/Views/Macros/MacroListView.swift index 211ecb1e..471090e4 100644 --- a/XboxControllerMapper/XboxControllerMapper/Views/Macros/MacroListView.swift +++ b/XboxControllerMapper/XboxControllerMapper/Views/Macros/MacroListView.swift @@ -156,7 +156,7 @@ struct SharedMacroRow: View { } .buttonStyle(.borderless) .help("Edit in shared library") - .accessibilityLabel("Edit in shared library") + .accessibilityLabel("Edit \(macro.name.isEmpty ? "Unnamed Macro" : macro.name) in shared library") } .padding(.horizontal, 12) .padding(.vertical, 8) @@ -207,7 +207,7 @@ struct MacroRow: View { } .buttonStyle(.borderless) .help("Edit") - .accessibilityLabel("Edit") + .accessibilityLabel("Edit \(macro.name.isEmpty ? "Unnamed Macro" : macro.name)") Button(action: onDelete) { Image(systemName: "trash") @@ -215,7 +215,7 @@ struct MacroRow: View { } .buttonStyle(.borderless) .help("Delete") - .accessibilityLabel("Delete") + .accessibilityLabel("Delete \(macro.name.isEmpty ? "Unnamed Macro" : macro.name)") } } .padding(.horizontal, 12) diff --git a/XboxControllerMapper/XboxControllerMapper/Views/MainWindow/ChordSequenceListViews.swift b/XboxControllerMapper/XboxControllerMapper/Views/MainWindow/ChordSequenceListViews.swift index 169053d5..c97533f0 100644 --- a/XboxControllerMapper/XboxControllerMapper/Views/MainWindow/ChordSequenceListViews.swift +++ b/XboxControllerMapper/XboxControllerMapper/Views/MainWindow/ChordSequenceListViews.swift @@ -116,7 +116,7 @@ struct ChordRow: View { } .buttonStyle(.borderless) .help("Edit") - .accessibilityLabel("Edit") + .accessibilityLabel("Edit \(chord.actionDisplayString.isEmpty ? "Unnamed Chord" : chord.actionDisplayString)") Button(action: onDelete) { Image(systemName: "trash") @@ -124,7 +124,7 @@ struct ChordRow: View { } .buttonStyle(.borderless) .help("Delete") - .accessibilityLabel("Delete") + .accessibilityLabel("Delete \(chord.actionDisplayString.isEmpty ? "Unnamed Chord" : chord.actionDisplayString)") } } .padding(.horizontal, 12) @@ -262,7 +262,7 @@ struct SequenceRow: View { } .buttonStyle(.borderless) .help("Edit") - .accessibilityLabel("Edit") + .accessibilityLabel("Edit \(sequence.actionDisplayString.isEmpty ? "Unnamed Sequence" : sequence.actionDisplayString)") Button(action: onDelete) { Image(systemName: "trash") @@ -270,7 +270,7 @@ struct SequenceRow: View { } .buttonStyle(.borderless) .help("Delete") - .accessibilityLabel("Delete") + .accessibilityLabel("Delete \(sequence.actionDisplayString.isEmpty ? "Unnamed Sequence" : sequence.actionDisplayString)") } } .padding(.horizontal, 12) diff --git a/XboxControllerMapper/XboxControllerMapper/Views/MainWindow/GestureListViews.swift b/XboxControllerMapper/XboxControllerMapper/Views/MainWindow/GestureListViews.swift index a77232ea..b22f42cf 100644 --- a/XboxControllerMapper/XboxControllerMapper/Views/MainWindow/GestureListViews.swift +++ b/XboxControllerMapper/XboxControllerMapper/Views/MainWindow/GestureListViews.swift @@ -79,7 +79,7 @@ struct GestureRow: View { } .buttonStyle(.borderless) .help("Edit") - .accessibilityLabel("Edit") + .accessibilityLabel("Edit \(gestureType.displayName)") if mapping?.hasAction == true { Button(action: onClear) { @@ -88,7 +88,7 @@ struct GestureRow: View { } .buttonStyle(.borderless) .help("Clear") - .accessibilityLabel("Clear") + .accessibilityLabel("Clear \(gestureType.displayName)") } } } diff --git a/XboxControllerMapper/XboxControllerMapper/Views/MainWindow/LinkedAppsSheet.swift b/XboxControllerMapper/XboxControllerMapper/Views/MainWindow/LinkedAppsSheet.swift index 97ff8a3f..9854807a 100644 --- a/XboxControllerMapper/XboxControllerMapper/Views/MainWindow/LinkedAppsSheet.swift +++ b/XboxControllerMapper/XboxControllerMapper/Views/MainWindow/LinkedAppsSheet.swift @@ -33,7 +33,8 @@ struct LinkedAppsSheet: View { } else { ForEach(liveProfile.linkedApps, id: \.self) { bundleId in HStack { - if let appInfo = appMonitor.appInfo(for: bundleId) { + let appInfo = appMonitor.appInfo(for: bundleId) + if let appInfo = appInfo { if let icon = appInfo.icon { Image(nsImage: icon) .resizable() @@ -58,7 +59,7 @@ struct LinkedAppsSheet: View { } .buttonStyle(.borderless) .help("Remove") - .accessibilityLabel("Remove Linked App") + .accessibilityLabel("Remove \(appInfo?.name ?? bundleId)") } .padding(.vertical, 4) } diff --git a/XboxControllerMapper/XboxControllerMapper/Views/MainWindow/LinkedControllersSheet.swift b/XboxControllerMapper/XboxControllerMapper/Views/MainWindow/LinkedControllersSheet.swift index 46ef3577..08f3efef 100644 --- a/XboxControllerMapper/XboxControllerMapper/Views/MainWindow/LinkedControllersSheet.swift +++ b/XboxControllerMapper/XboxControllerMapper/Views/MainWindow/LinkedControllersSheet.swift @@ -57,7 +57,7 @@ struct LinkedControllersSheet: View { } .buttonStyle(.borderless) .help("Remove") - .accessibilityLabel("Remove Linked Controller") + .accessibilityLabel("Remove \(binding.displayName.isEmpty ? "Unnamed Controller" : binding.displayName)") } .padding(.vertical, 4) } diff --git a/XboxControllerMapper/XboxControllerMapper/Views/Scripts/ScriptListView.swift b/XboxControllerMapper/XboxControllerMapper/Views/Scripts/ScriptListView.swift index 40c6f5ff..9e08b662 100644 --- a/XboxControllerMapper/XboxControllerMapper/Views/Scripts/ScriptListView.swift +++ b/XboxControllerMapper/XboxControllerMapper/Views/Scripts/ScriptListView.swift @@ -223,7 +223,7 @@ struct ScriptRow: View { } .buttonStyle(.borderless) .help("Edit") - .accessibilityLabel("Edit") + .accessibilityLabel("Edit \(script.name.isEmpty ? "Unnamed Script" : script.name)") Button(action: onDelete) { Image(systemName: "trash") @@ -231,7 +231,7 @@ struct ScriptRow: View { } .buttonStyle(.borderless) .help("Delete") - .accessibilityLabel("Delete") + .accessibilityLabel("Delete \(script.name.isEmpty ? "Unnamed Script" : script.name)") } } .padding(.horizontal, 12)