mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 10:33:45 +00:00
Various accessibility fixes (#1694)
* Hide the accessibility label for the swipe to reply menu * Fixes #1104 - Add accessibility labels for timeline media * Import new common strings * Add sender to accessibility lables for all messages * Improve accessibility labels for replies * Add hint and selection to reaction accessibility labels * Add changelog * Address PR comments
This commit is contained in:
@@ -82,6 +82,7 @@
|
|||||||
"common_forward_message" = "Forward message";
|
"common_forward_message" = "Forward message";
|
||||||
"common_gif" = "GIF";
|
"common_gif" = "GIF";
|
||||||
"common_image" = "Image";
|
"common_image" = "Image";
|
||||||
|
"common_in_reply_to" = "In reply to %1$@";
|
||||||
"common_invite_unknown_profile" = "This Matrix ID can't be found, so the invite might not be received.";
|
"common_invite_unknown_profile" = "This Matrix ID can't be found, so the invite might not be received.";
|
||||||
"common_leaving_room" = "Leaving room";
|
"common_leaving_room" = "Leaving room";
|
||||||
"common_link_copied_to_clipboard" = "Link copied to clipboard";
|
"common_link_copied_to_clipboard" = "Link copied to clipboard";
|
||||||
@@ -99,6 +100,7 @@
|
|||||||
"common_poll_total_votes" = "Total votes: %1$@";
|
"common_poll_total_votes" = "Total votes: %1$@";
|
||||||
"common_poll_undisclosed_text" = "Results will show after the poll has ended";
|
"common_poll_undisclosed_text" = "Results will show after the poll has ended";
|
||||||
"common_privacy_policy" = "Privacy policy";
|
"common_privacy_policy" = "Privacy policy";
|
||||||
|
"common_reaction" = "Reaction";
|
||||||
"common_reactions" = "Reactions";
|
"common_reactions" = "Reactions";
|
||||||
"common_refreshing" = "Refreshing…";
|
"common_refreshing" = "Refreshing…";
|
||||||
"common_replying_to" = "Replying to %1$@";
|
"common_replying_to" = "Replying to %1$@";
|
||||||
|
|||||||
@@ -182,6 +182,10 @@ public enum L10n {
|
|||||||
public static var commonGif: String { return L10n.tr("Localizable", "common_gif") }
|
public static var commonGif: String { return L10n.tr("Localizable", "common_gif") }
|
||||||
/// Image
|
/// Image
|
||||||
public static var commonImage: String { return L10n.tr("Localizable", "common_image") }
|
public static var commonImage: String { return L10n.tr("Localizable", "common_image") }
|
||||||
|
/// In reply to %1$@
|
||||||
|
public static func commonInReplyTo(_ p1: Any) -> String {
|
||||||
|
return L10n.tr("Localizable", "common_in_reply_to", String(describing: p1))
|
||||||
|
}
|
||||||
/// This Matrix ID can't be found, so the invite might not be received.
|
/// This Matrix ID can't be found, so the invite might not be received.
|
||||||
public static var commonInviteUnknownProfile: String { return L10n.tr("Localizable", "common_invite_unknown_profile") }
|
public static var commonInviteUnknownProfile: String { return L10n.tr("Localizable", "common_invite_unknown_profile") }
|
||||||
/// Leaving room
|
/// Leaving room
|
||||||
@@ -226,6 +230,8 @@ public enum L10n {
|
|||||||
}
|
}
|
||||||
/// Privacy policy
|
/// Privacy policy
|
||||||
public static var commonPrivacyPolicy: String { return L10n.tr("Localizable", "common_privacy_policy") }
|
public static var commonPrivacyPolicy: String { return L10n.tr("Localizable", "common_privacy_policy") }
|
||||||
|
/// Reaction
|
||||||
|
public static var commonReaction: String { return L10n.tr("Localizable", "common_reaction") }
|
||||||
/// Reactions
|
/// Reactions
|
||||||
public static var commonReactions: String { return L10n.tr("Localizable", "common_reactions") }
|
public static var commonReactions: String { return L10n.tr("Localizable", "common_reactions") }
|
||||||
/// Refreshing…
|
/// Refreshing…
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ struct TimelineReplyView: View {
|
|||||||
Text(sender.displayName ?? sender.id)
|
Text(sender.displayName ?? sender.id)
|
||||||
.font(.compound.bodySMSemibold)
|
.font(.compound.bodySMSemibold)
|
||||||
.foregroundColor(.compound.textPrimary)
|
.foregroundColor(.compound.textPrimary)
|
||||||
|
.accessibilityLabel(L10n.commonInReplyTo(sender.displayName ?? sender.id))
|
||||||
|
|
||||||
Text(messagePreview)
|
Text(messagePreview)
|
||||||
.font(.compound.bodyMD)
|
.font(.compound.bodyMD)
|
||||||
|
|||||||
@@ -79,14 +79,13 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
|||||||
if shouldShowSenderDetails {
|
if shouldShowSenderDetails {
|
||||||
HStack(alignment: .top, spacing: 4) {
|
HStack(alignment: .top, spacing: 4) {
|
||||||
TimelineSenderAvatarView(timelineItem: timelineItem)
|
TimelineSenderAvatarView(timelineItem: timelineItem)
|
||||||
.accessibilityHidden(true)
|
|
||||||
Text(timelineItem.sender.displayName ?? timelineItem.sender.id)
|
Text(timelineItem.sender.displayName ?? timelineItem.sender.id)
|
||||||
.font(.compound.bodySMSemibold)
|
.font(.compound.bodySMSemibold)
|
||||||
.foregroundColor(.compound.avatarColor(for: timelineItem.sender.id).foreground)
|
.foregroundColor(.compound.avatarColor(for: timelineItem.sender.id).foreground)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.padding(.vertical, senderNameVerticalPadding)
|
.padding(.vertical, senderNameVerticalPadding)
|
||||||
}
|
}
|
||||||
.accessibilityElement(children: .combine)
|
.accessibilityHidden(true)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
context.send(viewAction: .tappedOnUser(userID: timelineItem.sender.id))
|
context.send(viewAction: .tappedOnUser(userID: timelineItem.sender.id))
|
||||||
}
|
}
|
||||||
@@ -98,6 +97,12 @@ 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) {
|
||||||
messageBubble
|
messageBubble
|
||||||
|
.accessibilityRepresentation {
|
||||||
|
VStack {
|
||||||
|
Text(timelineItem.sender.displayName ?? timelineItem.sender.id)
|
||||||
|
messageBubble
|
||||||
|
}
|
||||||
|
}
|
||||||
.accessibilityElement(children: .combine)
|
.accessibilityElement(children: .combine)
|
||||||
|
|
||||||
if !timelineItem.properties.reactions.isEmpty {
|
if !timelineItem.properties.reactions.isEmpty {
|
||||||
@@ -126,6 +131,7 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
|||||||
}
|
}
|
||||||
.swipeRightAction {
|
.swipeRightAction {
|
||||||
Image(systemName: "arrowshape.turn.up.left")
|
Image(systemName: "arrowshape.turn.up.left")
|
||||||
|
.accessibilityHidden(true)
|
||||||
} shouldStartAction: {
|
} shouldStartAction: {
|
||||||
context.viewState.timelineItemMenuActionProvider?(timelineItem.id)?.canReply ?? false
|
context.viewState.timelineItemMenuActionProvider?(timelineItem.id)?.canReply ?? false
|
||||||
} action: {
|
} action: {
|
||||||
|
|||||||
@@ -122,6 +122,8 @@ struct TimelineReactionButton: View {
|
|||||||
.longPressWithFeedback {
|
.longPressWithFeedback {
|
||||||
showReactionSummary(reaction.key)
|
showReactionSummary(reaction.key)
|
||||||
}
|
}
|
||||||
|
.accessibilityHint(L10n.commonReaction)
|
||||||
|
.accessibilityAddTraits(reaction.isHighlighted ? .isSelected : [])
|
||||||
}
|
}
|
||||||
|
|
||||||
var label: some View {
|
var label: some View {
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ struct AudioRoomTimelineView: View {
|
|||||||
}
|
}
|
||||||
.padding(.vertical, 12)
|
.padding(.vertical, 12)
|
||||||
.padding(.horizontal, 6)
|
.padding(.horizontal, 6)
|
||||||
|
.accessibilityElement(children: .ignore)
|
||||||
|
.accessibilityLabel(L10n.commonAudio)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ struct FileRoomTimelineView: View {
|
|||||||
}
|
}
|
||||||
.padding(.vertical, 12)
|
.padding(.vertical, 12)
|
||||||
.padding(.horizontal, 6)
|
.padding(.horizontal, 6)
|
||||||
|
.accessibilityElement(children: .ignore)
|
||||||
|
.accessibilityLabel(L10n.commonFile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ struct ImageRoomTimelineView: View {
|
|||||||
}
|
}
|
||||||
.frame(maxHeight: min(300, max(100, timelineItem.content.height ?? .infinity)))
|
.frame(maxHeight: min(300, max(100, timelineItem.content.height ?? .infinity)))
|
||||||
.aspectRatio(timelineItem.content.aspectRatio, contentMode: .fit)
|
.aspectRatio(timelineItem.content.aspectRatio, contentMode: .fit)
|
||||||
|
.accessibilityElement(children: .ignore)
|
||||||
|
.accessibilityLabel(L10n.commonImage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,25 +22,40 @@ struct LocationRoomTimelineView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
TimelineStyler(timelineItem: timelineItem) {
|
TimelineStyler(timelineItem: timelineItem) {
|
||||||
if let geoURI = timelineItem.content.geoURI {
|
mainContent
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
.accessibilityElement(children: .ignore)
|
||||||
descriptionView
|
.accessibilityLabel(accessibilityLabel)
|
||||||
.frame(maxWidth: mapAspectRatio * mapMaxHeight, alignment: .leading)
|
}
|
||||||
|
}
|
||||||
MapLibreStaticMapView(geoURI: geoURI, mapSize: .init(width: mapAspectRatio * mapMaxHeight, height: mapMaxHeight)) {
|
|
||||||
LocationMarkerView()
|
@ViewBuilder
|
||||||
}
|
private var mainContent: some View {
|
||||||
.frame(maxHeight: mapMaxHeight)
|
if let geoURI = timelineItem.content.geoURI {
|
||||||
.aspectRatio(mapAspectRatio, contentMode: .fit)
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
.clipped()
|
descriptionView
|
||||||
|
.frame(maxWidth: mapAspectRatio * mapMaxHeight, alignment: .leading)
|
||||||
|
|
||||||
|
MapLibreStaticMapView(geoURI: geoURI, mapSize: .init(width: mapAspectRatio * mapMaxHeight, height: mapMaxHeight)) {
|
||||||
|
LocationMarkerView()
|
||||||
}
|
}
|
||||||
} else {
|
.frame(maxHeight: mapMaxHeight)
|
||||||
FormattedBodyText(text: timelineItem.body, additionalWhitespacesCount: timelineItem.additionalWhitespaces(timelineStyle: timelineStyle))
|
.aspectRatio(mapAspectRatio, contentMode: .fit)
|
||||||
|
.clipped()
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
FormattedBodyText(text: timelineItem.body, additionalWhitespacesCount: timelineItem.additionalWhitespaces(timelineStyle: timelineStyle))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
|
private var accessibilityLabel: String {
|
||||||
|
if let description = timelineItem.content.description {
|
||||||
|
return "\(L10n.commonSharedLocation), \(description)"
|
||||||
|
}
|
||||||
|
|
||||||
|
return L10n.commonSharedLocation
|
||||||
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var descriptionView: some View {
|
private var descriptionView: some View {
|
||||||
|
|||||||
@@ -30,8 +30,9 @@ struct StickerRoomTimelineView: View {
|
|||||||
}
|
}
|
||||||
.frame(maxHeight: min(300, max(100, timelineItem.height ?? .infinity)))
|
.frame(maxHeight: min(300, max(100, timelineItem.height ?? .infinity)))
|
||||||
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
|
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
|
||||||
|
.accessibilityElement(children: .ignore)
|
||||||
|
.accessibilityLabel("\(L10n.commonSticker), \(timelineItem.body)")
|
||||||
}
|
}
|
||||||
.accessibilityLabel(timelineItem.body)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var placeholder: some View {
|
private var placeholder: some View {
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ struct VideoRoomTimelineView: View {
|
|||||||
thumbnail
|
thumbnail
|
||||||
.frame(maxHeight: min(300, max(100, timelineItem.content.height ?? .infinity)))
|
.frame(maxHeight: min(300, max(100, timelineItem.content.height ?? .infinity)))
|
||||||
.aspectRatio(timelineItem.content.aspectRatio, contentMode: .fit)
|
.aspectRatio(timelineItem.content.aspectRatio, contentMode: .fit)
|
||||||
|
.accessibilityElement(children: .ignore)
|
||||||
|
.accessibilityLabel(L10n.commonVideo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
Various accessibility fixes: add labels on timeline media, hide swipe to reply button, add sender on all messages, improve replies and reactions
|
||||||
Reference in New Issue
Block a user