Skip to content

a11y: VoiceOver support for Live Activity / Dynamic Island (T001)#19

Open
bruschill wants to merge 6 commits into
mainfrom
a11y/live-activity
Open

a11y: VoiceOver support for Live Activity / Dynamic Island (T001)#19
bruschill wants to merge 6 commits into
mainfrom
a11y/live-activity

Conversation

@bruschill

Copy link
Copy Markdown
Owner

What changed?

Widgets/WidgetsLiveActivity.swift had zero accessibility calls in the whole file — the Live Activity's Dynamic Island presentations and Lock Screen view were completely silent to VoiceOver. Added .accessibilityElement(children: .ignore) + a localized .accessibilityLabel to the node-count text in four places:

  • The Dynamic Island expanded trailing region — "X of Y nodes online"
  • The Dynamic Island compact leading region — "X nodes online"
  • The Dynamic Island minimal presentation (multiple Live Activities stacked) — "X nodes online"
  • The Lock Screen LiveActivityView header row — "X of Y nodes online"

Also checks off T001 in specs/016-accessibility-audit/tasks.md (no other tracker rows touched).

Why did it change?

T001, Phase 1 "Blocker" severity in the accessibility audit (specs/016-accessibility-audit/): a Live Activity with zero accessibility support is a hard blocker — VoiceOver users get no information at all from the Dynamic Island or Lock Screen presentation, both of which are meant to surface mesh status (nodes online) without opening the app.

How is this tested?

  • swiftc -parse on WidgetsLiveActivity.swift after the edit (syntax-only).
  • Full xcodebuild build for the Meshtastic scheme (Debug, iOS Simulator) — succeeded, including the WidgetsExtension target.
  • Did not verify with on-device/Simulator VoiceOver directly, and did not verify in the actual widget extension target running on a Lock Screen/Dynamic Island (this requires an active Live Activity, which needs a live mesh session) — see Manual Test Instructions below for what a reviewer should do to close this out.

Manual Test Instructions

This one specifically needs verification in the widget extension target, not just the main app — accessibility modifiers on widget views can behave differently there.

  1. Connect to (or simulate) an active mesh session so a Live Activity starts.
  2. Long-press/press the Dynamic Island to see the expanded presentation. With VoiceOver on, focus the "X of Y nodes online" text — should read as one combined stop with that exact phrase, not per-digit/word fragments.
  3. Release to the compact pill — focus it, confirm "X nodes online".
  4. If multiple Live Activities are active, check the minimal stacked presentation the same way.
  5. Lock the device (or check the Lock Screen preview) — focus the Live Activity's header row, confirm "X of Y nodes online".

Pass criteria: all 4 locations read as a single combined VoiceOver stop with the exact node-count phrase — no separate stops per glyph, and no generic "Text"/unlabeled announcement.

Checklist

  • My code adheres to the project's coding and style guidelines.
  • I have conducted a self-review of my code.
  • I have commented my code, particularly in complex areas. (N/A — one-line .accessibilityLabel additions with descriptive comment: parameters already on each localized string.)
  • I have verified whether these changes require updates to the in-app documentation under docs/user/ or docs/developer/, and updated accordingly. No doc update needed — this is a presentational, VoiceOver-only accessibility addition with no visible UI or behavior change. Requesting the skip-docs-check label, consistent with prior presentational-only a11y PRs.
  • I have tested the change to ensure that it works as intended. Built successfully on Simulator; did not verify in the running widget extension with an active Live Activity or with on-device VoiceOver — see Manual Test Instructions above for the recommended pre-merge check.

@github-actions

Copy link
Copy Markdown

📄 Docs staleness warning

This PR modifies user-facing Swift source files but does not update any page under docs/user/ or docs/developer/.

Changed source files:

Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift

What to check:

Changed area Likely doc page
Views/Messages/ docs/user/messages.md
Views/Nodes/ docs/user/nodes.md
Views/Map/ docs/user/map.md
Views/Settings/Bluetooth/ docs/user/bluetooth.md
Views/Settings/Discovery/ docs/user/discovery.md
Views/Settings/MQTT/ docs/user/mqtt.md
Views/Settings/TAK/ docs/user/tak.md
Views/Settings/Firmware/ docs/user/firmware.md
Views/Settings/ (telemetry/sensor) docs/user/telemetry.md
Views/Settings/ (general) docs/user/settings.md
Meshtastic Watch App/ docs/user/watch.md
Model/ docs/developer/swiftdata.md or docs/developer/architecture.md
Accessory/Transports/ docs/developer/transport.md

If this PR does not require a doc update (e.g., internal refactor, bug fix, test change), add the skip-docs-check label to dismiss this warning.

After updating docs/, re-run bash scripts/build-docs.sh --output Meshtastic/Resources/docs locally and commit the regenerated HTML bundle.

Deep a11y audit fan-out (12 parallel agents + cross-validation) over
Meshtastic/Views, Widgets/, and Meshtastic Watch App/Views found 20
confirmed finding clusters (5 Blocker, 11 High, 4 Medium) across ~60
locations. tasks.md is the persistent, resumable checklist — one
checkbox per finding, grouped by severity, with file:line and the
proposed fix pattern for each.
Three icon-only buttons (arrow.up.circle.fill send, x.circle.fill
cancel-reply, face.smiling emoji picker) had no accessibilityLabel,
so VoiceOver announced them as a bare "Button" with no indication of
what they do. Fixed in both TextMessageField's legacy body (iOS <18/
macOS <15) and FormattingComposeArea (iOS 18+/macOS 15+) — six call
sites total, one localized label per action, following the existing
String(localized:) + comment convention from ChannelList.swift and
MessageText.swift.

Closes T002, T015, T016 in specs/016-accessibility-audit/tasks.md.
WidgetsLiveActivity.swift had zero accessibility calls despite being a
Lock Screen / Dynamic Island Live Activity where the icon+number rows
(person.2.fill glyph next to a bare fraction) and StatRow pairs read as
meaningless separate elements, or nothing at all, under VoiceOver.

Grouped each icon+count/fraction pair with .accessibilityElement(children: .ignore)
and a combined, localized .accessibilityLabel:
- Dynamic Island expanded trailing region (nodesOnline/totalNodes)
- Dynamic Island compact-leading region (nodesOnline)
- Dynamic Island minimal presentation (nodesOnline)
- LiveActivityView header row (nodesOnline/totalNodes)

Grouped StatRow (label + value) into one combined "label: value" element
instead of two disconnected VoiceOver stops.

TimerView (the countdown) now announces "Update in" as its label with
the live-updating countdown text carried as accessibilityValue, so
VoiceOver gets both the semantic purpose and the current countdown state
instead of silently skipping the timer glyph and digits.
@bruschill
bruschill force-pushed the a11y/live-activity branch from e43b71f to 1ecf941 Compare July 23, 2026 03:56
…c#2172

Adds the 7 missing keys for this branch's new String(localized:) calls
(node-count announcements, StatRow label:value, countdown timer label,
and the shared TextMessageField labels), matching the pattern every
earlier a11y commit on this branch already established.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant