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.
This commit is contained in:
Mauro Romito
2026-10-01 16:23:32 +02:00
committed by Mauro
parent 8ddd161625
commit e67a4c388f
2 changed files with 12 additions and 3 deletions
@@ -13,7 +13,7 @@ enum TimelineItemAccessibilityMessageSelection {
/// No selection is active, the item offers its regular message actions. /// No selection is active, the item offers its regular message actions.
case none case none
/// A selection is active, the item acts as a toggle when it can be selected. /// 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 { private struct TimelineItemAccessibilityModifier: ViewModifier {
@@ -76,9 +76,13 @@ private struct TimelineItemAccessibilityModifier: ViewModifier {
action() action()
} }
} }
case .selecting(let isSelected): case .selecting(let isSelected, let selectedCount):
if let item = timelineItem as? EventBasedTimelineItemProtocol, item.isForwardable { if let item = timelineItem as? EventBasedTimelineItemProtocol, item.isForwardable {
view view
.accessibilityLabel { label in
Text(L10n.screenRoomSelectionCount(selectedCount))
label
}
.accessibilityAddTraits(isSelected ? [.isButton, .isSelected] : .isButton) .accessibilityAddTraits(isSelected ? [.isButton, .isSelected] : .isButton)
.accessibilityAction(.default, action) .accessibilityAction(.default, action)
} else { } else {
@@ -50,6 +50,11 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
context.viewState.messageSelection.contains(timelineItem.id.eventID) 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. /// 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. /// **Note:** This is on top of the insets applied to the cells by the table view.
@@ -171,7 +176,7 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
// Figma overlaps reactions by 3 // Figma overlaps reactions by 3
VStack(alignment: alignment, spacing: -3) { VStack(alignment: alignment, spacing: -3) {
messageBubbleWithActions messageBubbleWithActions
.timelineItemAccessibility(timelineItem, selection: isMessageSelectionActive ? .selecting(isSelected: isSelected) : .none) { .timelineItemAccessibility(timelineItem, selection: accessibilitySelection) {
if isMessageSelectionActive { if isMessageSelectionActive {
toggleSelection() toggleSelection()
} else { } else {