diff --git a/.Jules/palette.md b/.Jules/palette.md index f61603c8..8ae7258e 100644 --- a/.Jules/palette.md +++ b/.Jules/palette.md @@ -25,3 +25,6 @@ ## 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. +## 2026-06-30 - Dynamic Context for Accessibility in Repeated Lists +**Learning:** When using repeated lists (like `ForEach` loops in SwiftUI), icon-only buttons for actions like Edit and Delete become ambiguous for screen reader users and those relying on pointer tooltips. Using a dynamic context, derived from the item's properties (like `name` or `actionDisplayString`), provides essential context and disambiguates the actions. +**Action:** Always include item-specific context in `.help()` and `.accessibilityLabel()` modifiers for icon-only buttons within repeated lists. diff --git a/XboxControllerMapper/XboxControllerMapper/Views/Macros/MacroListView.swift b/XboxControllerMapper/XboxControllerMapper/Views/Macros/MacroListView.swift index 211ecb1e..6c55a011 100644 --- a/XboxControllerMapper/XboxControllerMapper/Views/Macros/MacroListView.swift +++ b/XboxControllerMapper/XboxControllerMapper/Views/Macros/MacroListView.swift @@ -155,8 +155,8 @@ struct SharedMacroRow: View { .foregroundColor(.accentColor) } .buttonStyle(.borderless) - .help("Edit in shared library") - .accessibilityLabel("Edit in shared library") + .help("Edit \(macro.name.isEmpty ? "Untitled Macro" : macro.name) in shared library") + .accessibilityLabel("Edit \(macro.name.isEmpty ? "Untitled Macro" : macro.name) in shared library") } .padding(.horizontal, 12) .padding(.vertical, 8) @@ -206,16 +206,16 @@ struct MacroRow: View { .foregroundColor(.accentColor) } .buttonStyle(.borderless) - .help("Edit") - .accessibilityLabel("Edit") + .help("Edit \(macro.name.isEmpty ? "Untitled Macro" : macro.name)") + .accessibilityLabel("Edit \(macro.name.isEmpty ? "Untitled Macro" : macro.name)") Button(action: onDelete) { Image(systemName: "trash") .foregroundColor(.red.opacity(0.8)) } .buttonStyle(.borderless) - .help("Delete") - .accessibilityLabel("Delete") + .help("Delete \(macro.name.isEmpty ? "Untitled Macro" : macro.name)") + .accessibilityLabel("Delete \(macro.name.isEmpty ? "Untitled Macro" : macro.name)") } } .padding(.horizontal, 12) diff --git a/XboxControllerMapper/XboxControllerMapper/Views/MainWindow/ChordSequenceListViews.swift b/XboxControllerMapper/XboxControllerMapper/Views/MainWindow/ChordSequenceListViews.swift index 169053d5..81e899b8 100644 --- a/XboxControllerMapper/XboxControllerMapper/Views/MainWindow/ChordSequenceListViews.swift +++ b/XboxControllerMapper/XboxControllerMapper/Views/MainWindow/ChordSequenceListViews.swift @@ -115,16 +115,16 @@ struct ChordRow: View { .foregroundColor(.accentColor) } .buttonStyle(.borderless) - .help("Edit") - .accessibilityLabel("Edit") + .help("Edit chord \(chord.actionDisplayString)") + .accessibilityLabel("Edit chord \(chord.actionDisplayString)") Button(action: onDelete) { Image(systemName: "trash") .foregroundColor(.red.opacity(0.8)) } .buttonStyle(.borderless) - .help("Delete") - .accessibilityLabel("Delete") + .help("Delete chord \(chord.actionDisplayString)") + .accessibilityLabel("Delete chord \(chord.actionDisplayString)") } } .padding(.horizontal, 12) @@ -261,16 +261,16 @@ struct SequenceRow: View { .foregroundColor(.accentColor) } .buttonStyle(.borderless) - .help("Edit") - .accessibilityLabel("Edit") + .help("Edit sequence \(sequence.actionDisplayString)") + .accessibilityLabel("Edit sequence \(sequence.actionDisplayString)") Button(action: onDelete) { Image(systemName: "trash") .foregroundColor(.red.opacity(0.8)) } .buttonStyle(.borderless) - .help("Delete") - .accessibilityLabel("Delete") + .help("Delete sequence \(sequence.actionDisplayString)") + .accessibilityLabel("Delete sequence \(sequence.actionDisplayString)") } } .padding(.horizontal, 12) diff --git a/XboxControllerMapper/XboxControllerMapper/Views/Scripts/ScriptListView.swift b/XboxControllerMapper/XboxControllerMapper/Views/Scripts/ScriptListView.swift index 40c6f5ff..9039c41d 100644 --- a/XboxControllerMapper/XboxControllerMapper/Views/Scripts/ScriptListView.swift +++ b/XboxControllerMapper/XboxControllerMapper/Views/Scripts/ScriptListView.swift @@ -129,6 +129,8 @@ struct ScriptListView: View { .contentShape(Rectangle()) } .buttonStyle(.plain) + .help("Use \(example.name) example") + .accessibilityLabel("Use \(example.name) example") } HStack(spacing: 12) { @@ -222,16 +224,16 @@ struct ScriptRow: View { .foregroundColor(.accentColor) } .buttonStyle(.borderless) - .help("Edit") - .accessibilityLabel("Edit") + .help("Edit \(script.name.isEmpty ? "Untitled Script" : script.name)") + .accessibilityLabel("Edit \(script.name.isEmpty ? "Untitled Script" : script.name)") Button(action: onDelete) { Image(systemName: "trash") .foregroundColor(.red.opacity(0.8)) } .buttonStyle(.borderless) - .help("Delete") - .accessibilityLabel("Delete") + .help("Delete \(script.name.isEmpty ? "Untitled Script" : script.name)") + .accessibilityLabel("Delete \(script.name.isEmpty ? "Untitled Script" : script.name)") } } .padding(.horizontal, 12)