From e67a4c388fd8b50ebee51e83a02500dc20945c93 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Thu, 1 Oct 2026 16:23:32 +0200 Subject: [PATCH] Read the number of selected messages to VoiceOver The count was only on the forward button at the bottom, so VoiceOver users had to go and find it after every tap. While selecting, each selectable message now starts its label with "N messages selected". The label changes on every toggle, so VoiceOver reads the new count followed by the message, as in other messaging apps. --- .../View/Style/TimelineItemAccessibilityModifier.swift | 8 ++++++-- .../View/Style/TimelineItemBubbledStylerView.swift | 7 ++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemAccessibilityModifier.swift b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemAccessibilityModifier.swift index 7d2447383..7e6e2b9cb 100644 --- a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemAccessibilityModifier.swift +++ b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemAccessibilityModifier.swift @@ -13,7 +13,7 @@ enum TimelineItemAccessibilityMessageSelection { /// No selection is active, the item offers its regular message actions. case none /// A selection is active, the item acts as a toggle when it can be selected. - case selecting(isSelected: Bool) + case selecting(isSelected: Bool, selectedCount: Int) } private struct TimelineItemAccessibilityModifier: ViewModifier { @@ -76,9 +76,13 @@ private struct TimelineItemAccessibilityModifier: ViewModifier { action() } } - case .selecting(let isSelected): + case .selecting(let isSelected, let selectedCount): if let item = timelineItem as? EventBasedTimelineItemProtocol, item.isForwardable { view + .accessibilityLabel { label in + Text(L10n.screenRoomSelectionCount(selectedCount)) + label + } .accessibilityAddTraits(isSelected ? [.isButton, .isSelected] : .isButton) .accessibilityAction(.default, action) } else { diff --git a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift index 5604c3b44..8c80e8fc4 100644 --- a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift +++ b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift @@ -50,6 +50,11 @@ struct TimelineItemBubbledStylerView: View { context.viewState.messageSelection.contains(timelineItem.id.eventID) } + private var accessibilitySelection: TimelineItemAccessibilityMessageSelection { + guard isMessageSelectionActive else { return .none } + return .selecting(isSelected: isSelected, selectedCount: context.viewState.messageSelection.count) + } + /// The base padding applied to bubbles on either side. /// /// **Note:** This is on top of the insets applied to the cells by the table view. @@ -171,7 +176,7 @@ struct TimelineItemBubbledStylerView: View { // Figma overlaps reactions by 3 VStack(alignment: alignment, spacing: -3) { messageBubbleWithActions - .timelineItemAccessibility(timelineItem, selection: isMessageSelectionActive ? .selecting(isSelected: isSelected) : .none) { + .timelineItemAccessibility(timelineItem, selection: accessibilitySelection) { if isMessageSelectionActive { toggleSelection() } else {