From b6f4d70ce7cf979395c698bc194c9c02269afd40 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Tue, 17 Jun 2025 19:54:08 +0200 Subject: [PATCH] improved reactions a11y --- .../en.lproj/Localizable.strings | 5 ++++- ElementX/Sources/Generated/Strings.swift | 12 ++++++++++- .../View/EmojiPickerScreen.swift | 9 ++++++++ .../View/ItemMenu/TimelineItemMenu.swift | 16 ++++++++------ .../Supplementary/TimelineReactionsView.swift | 21 +++++++++++++++++-- 5 files changed, 53 insertions(+), 10 deletions(-) diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index e15a24c1f..32d9c15ab 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -1,4 +1,5 @@ "Notification" = "Notification"; +"a11y_add_reaction" = "Add reaction: %1$@"; "a11y_avatar" = "Avatar"; "a11y_delete" = "Delete"; "a11y_hide_password" = "Hide password"; @@ -18,6 +19,7 @@ "a11y_read_receipts_multiple" = "Read by %1$@ and %2$@"; "a11y_read_receipts_single" = "Read by %1$@"; "a11y_read_receipts_tap_to_show_all" = "Tap to show all"; +"a11y_remove_reaction" = "Remove reaction: %1$@"; "a11y_remove_reaction_with" = "Remove reaction with %1$@"; "a11y_send_files" = "Send files"; "a11y_show_password" = "Show password"; @@ -357,6 +359,7 @@ "notification_channel_noisy" = "Noisy notifications"; "notification_channel_ringing_calls" = "Ringing calls"; "notification_channel_silent" = "Silent notifications"; +"notification_fallback_content" = "You have new message(s)."; "notification_incoming_call" = "📹 Incoming call"; "notification_inline_reply_failed" = "** Failed to send - please open room"; "notification_invite_body" = "Invited you to chat"; @@ -534,6 +537,7 @@ "screen_room_timeline_tombstoned_room_message" = "This room has been replaced and is no longer active"; "screen_room_timeline_upgraded_room_action" = "See old messages"; "screen_room_timeline_upgraded_room_message" = "This room is a continuation of another room"; +"screen_room_timeline_reactions_show_reactions_summary" = "Show reactions summary"; "screen_roomlist_tombstoned_room_description" = "This room has been upgraded"; "screen_security_and_privacy_add_room_address_action" = "Add room address"; "screen_security_and_privacy_ask_to_join_option_description" = "Anyone can ask to join the room but an administrator or moderator will have to accept the request."; @@ -1212,7 +1216,6 @@ "banner_set_up_recovery_submit" = "Set up recovery"; "dialog_title_error" = "Error"; "dialog_title_success" = "Success"; -"notification_fallback_content" = "Notification"; "notification_invitation_action_join" = "Join"; "notification_invitation_action_reject" = "Reject"; "notification_room_action_mark_as_read" = "Mark as read"; diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index 207258881..368dd6017 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -10,6 +10,10 @@ import Foundation // swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length // swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces internal enum L10n { + /// Add reaction: %1$@ + internal static func a11yAddReaction(_ p1: Any) -> String { + return L10n.tr("Localizable", "a11y_add_reaction", String(describing: p1)) + } /// Avatar internal static var a11yAvatar: String { return L10n.tr("Localizable", "a11y_avatar") } /// Delete @@ -72,6 +76,10 @@ internal enum L10n { } /// Tap to show all internal static var a11yReadReceiptsTapToShowAll: String { return L10n.tr("Localizable", "a11y_read_receipts_tap_to_show_all") } + /// Remove reaction: %1$@ + internal static func a11yRemoveReaction(_ p1: Any) -> String { + return L10n.tr("Localizable", "a11y_remove_reaction", String(describing: p1)) + } /// Remove reaction with %1$@ internal static func a11yRemoveReactionWith(_ p1: Any) -> String { return L10n.tr("Localizable", "a11y_remove_reaction_with", String(describing: p1)) @@ -838,7 +846,7 @@ internal enum L10n { internal static func notificationCompatSummaryTitle(_ p1: Int) -> String { return L10n.tr("Localizable", "notification_compat_summary_title", p1) } - /// Notification + /// You have new message(s). internal static var notificationFallbackContent: String { return L10n.tr("Localizable", "notification_fallback_content") } /// 📹 Incoming call internal static var notificationIncomingCall: String { return L10n.tr("Localizable", "notification_incoming_call") } @@ -2366,6 +2374,8 @@ internal enum L10n { internal static var screenRoomTimelineReactionsShowLess: String { return L10n.tr("Localizable", "screen_room_timeline_reactions_show_less") } /// Show more internal static var screenRoomTimelineReactionsShowMore: String { return L10n.tr("Localizable", "screen_room_timeline_reactions_show_more") } + /// Show reactions summary + internal static var screenRoomTimelineReactionsShowReactionsSummary: String { return L10n.tr("Localizable", "screen_room_timeline_reactions_show_reactions_summary") } /// New internal static var screenRoomTimelineReadMarkerTitle: String { return L10n.tr("Localizable", "screen_room_timeline_read_marker_title") } /// Plural format key: "%#@COUNT@" diff --git a/ElementX/Sources/Screens/EmojiPickerScreen/View/EmojiPickerScreen.swift b/ElementX/Sources/Screens/EmojiPickerScreen/View/EmojiPickerScreen.swift index 61169607a..96bb1d5b3 100644 --- a/ElementX/Sources/Screens/EmojiPickerScreen/View/EmojiPickerScreen.swift +++ b/ElementX/Sources/Screens/EmojiPickerScreen/View/EmojiPickerScreen.swift @@ -36,6 +36,7 @@ struct EmojiPickerScreen: View { .background(Circle() .foregroundColor(emojiBackgroundColor(for: emoji.value))) } + .accessibilityLabel(accessibilityLabel(for: emoji.value)) } } header: { EmojiPickerScreenHeaderView(title: category.name) @@ -62,6 +63,14 @@ struct EmojiPickerScreen: View { } } + private func accessibilityLabel(for emoji: String) -> String { + if selectedEmojis.contains(emoji) { + return L10n.a11yRemoveReaction(emoji) + } else { + return L10n.a11yAddReaction(emoji) + } + } + private func emojiBackgroundColor(for emoji: String) -> Color { if selectedEmojis.contains(emoji) { return .compound.bgActionPrimaryRest diff --git a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenu.swift b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenu.swift index 9eedd71a5..6dcacad0b 100644 --- a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenu.swift +++ b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenu.swift @@ -162,15 +162,19 @@ struct TimelineItemMenu: View { .foregroundColor(reactionBackgroundColor(for: emoji))) .frame(maxWidth: .infinity, alignment: .leading) } + .accessibilityLabel(hasReacted(to: emoji) ? L10n.a11yRemoveReaction(emoji) : L10n.a11yAddReaction(emoji)) + } + + private func hasReacted(to emoji: String) -> Bool { + if let reaction = item.properties.reactions.first(where: { $0.key == emoji }), + reaction.isHighlighted { + return true + } + return false } private func reactionBackgroundColor(for emoji: String) -> Color { - if let reaction = item.properties.reactions.first(where: { $0.key == emoji }), - reaction.isHighlighted { - return .compound.bgActionPrimaryRest - } else { - return .clear - } + hasReacted(to: emoji) ? .compound.bgActionPrimaryRest : .clear } private func viewsForActions(_ actions: [TimelineItemMenuAction]) -> some View { diff --git a/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReactionsView.swift b/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReactionsView.swift index de905a5a2..c73b8e763 100644 --- a/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReactionsView.swift +++ b/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReactionsView.swift @@ -138,11 +138,24 @@ struct TimelineCollapseButtonLabel: View { } struct TimelineReactionButton: View { + @Environment(\.accessibilityVoiceOverEnabled) private var voiceOverEnabled + let reaction: AggregatedReaction let toggleReaction: (String) -> Void let showReactionSummary: (String) -> Void @ScaledMetric(relativeTo: .subheadline) private var lineHeight = 20 + private var accessibilityLabel: String { + if reaction.isHighlighted { + return reaction.count > 1 ? L10n.tr("Localizable", "screen_room_timeline_reaction_including_you_a11y", reaction.count - 1, reaction.displayKey) : L10n.screenRoomTimelineReactionYouA11y(reaction.displayKey) + } + return L10n.tr("Localizable", "screen_room_timeline_reaction_a11y", reaction.count, reaction.displayKey) + } + + private var toggleReactionAccessibilityActionName: String { + reaction.isHighlighted ? L10n.a11yRemoveReaction(reaction.displayKey) : L10n.a11yAddReaction(reaction.displayKey) + } + var body: some View { label .onTapGesture { @@ -151,8 +164,12 @@ struct TimelineReactionButton: View { .longPressWithFeedback { showReactionSummary(reaction.key) } - .accessibilityHint(L10n.commonReaction) - .accessibilityAddTraits(reaction.isHighlighted ? .isSelected : []) + .accessibilityAddTraits(.isButton) + .accessibilityLabel(accessibilityLabel) + .accessibilityHint(toggleReactionAccessibilityActionName) + .accessibilityAction(named: L10n.screenRoomTimelineReactionsShowReactionsSummary) { + showReactionSummary(reaction.key) + } } var label: some View {