From d3604e81153c42c387cd2dcec71912b80ef9c1a2 Mon Sep 17 00:00:00 2001 From: Mauro <34335419+Velin92@users.noreply.github.com> Date: Fri, 2 Oct 2026 12:29:18 +0200 Subject: [PATCH] Announce toasts with VoiceOver (#6250) * Announce toasts with VoiceOver Toasts are posted from a separate overlay window and never take VoiceOver focus, so their text was never spoken. UserIndicatorController now posts a high priority announcement with the title whenever a toast with readable text is submitted, including re-submissions of an already visible toast with the same ID. Modal indicators are not announced. * Remove empty-title toast announcement check Co-authored-by: Velin92 <34335419+Velin92@users.noreply.github.com> * Hide toasts from VoiceOver The toast title is now announced by UserIndicatorController when the toast is submitted, so letting VoiceOver also focus the toast itself would only read the same text twice. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- .../UserIndicator/UserIndicatorController.swift | 12 ++++++++++++ .../Other/UserIndicator/UserIndicatorToastView.swift | 1 + 2 files changed, 13 insertions(+) diff --git a/ElementX/Sources/Other/UserIndicator/UserIndicatorController.swift b/ElementX/Sources/Other/UserIndicator/UserIndicatorController.swift index 2fa5dfe35..6f33ed079 100644 --- a/ElementX/Sources/Other/UserIndicator/UserIndicatorController.swift +++ b/ElementX/Sources/Other/UserIndicator/UserIndicatorController.swift @@ -44,6 +44,7 @@ class UserIndicatorController: ObservableObject, UserIndicatorControllerProtocol if let index = indicatorQueue.firstIndex(where: { $0.id == indicator.id }) { indicatorQueue[index] = indicator displayTimes[indicator.id] = .now + announceIfNeeded(indicator) } else { if let delay { delayedIndicators.insert(indicator.id) @@ -90,5 +91,16 @@ class UserIndicatorController: ObservableObject, UserIndicatorControllerProtocol retractIndicatorWithId(indicator.id) indicatorQueue.append(indicator) displayTimes[indicator.id] = .now + announceIfNeeded(indicator) + } + + private func announceIfNeeded(_ indicator: UserIndicator) { + guard case .toast = indicator.type else { + return + } + + var announcement = AttributedString(indicator.title) + announcement.accessibilitySpeechAnnouncementPriority = .high + AccessibilityNotification.Announcement(announcement).post() } } diff --git a/ElementX/Sources/Other/UserIndicator/UserIndicatorToastView.swift b/ElementX/Sources/Other/UserIndicator/UserIndicatorToastView.swift index 127f156f3..84c70e753 100644 --- a/ElementX/Sources/Other/UserIndicator/UserIndicatorToastView.swift +++ b/ElementX/Sources/Other/UserIndicator/UserIndicatorToastView.swift @@ -33,6 +33,7 @@ struct UserIndicatorToastView: View { .background(Color.compound.bgSubtlePrimary) .clipShape(RoundedCornerShape(radius: 24.0, corners: .allCorners)) .shadow(color: .black.opacity(0.1), radius: 6.0, y: 4.0) + .accessibilityHidden(true) .transition(toastTransition) }