mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 12:07:55 +00:00
implemented a11y for galleries
This commit is contained in:
@@ -12,6 +12,11 @@
|
||||
"a11y_edit_avatar" = "Edit avatar";
|
||||
"a11y_edit_room_address_hint" = "The full address will be %1$@";
|
||||
"a11y_expand_message_text_field" = "Expand message text field";
|
||||
"a11y_gallery_attachment" = "Gallery, attachment %1$d";
|
||||
"a11y_gallery_failed_scan" = "Gallery, failed scan %1$d";
|
||||
"a11y_gallery_image" = "Gallery, image %1$d";
|
||||
"a11y_gallery_scanning" = "Gallery, scanning %1$d";
|
||||
"a11y_gallery_video" = "Gallery, video %1$d";
|
||||
"a11y_hide_password" = "Hide password";
|
||||
"a11y_info" = "Info";
|
||||
"a11y_join_call" = "Join call";
|
||||
|
||||
@@ -12,6 +12,11 @@
|
||||
"a11y_edit_avatar" = "Edit avatar";
|
||||
"a11y_edit_room_address_hint" = "The full address will be %1$@";
|
||||
"a11y_expand_message_text_field" = "Expand message text field";
|
||||
"a11y_gallery_attachment" = "Gallery, attachment %1$d";
|
||||
"a11y_gallery_failed_scan" = "Gallery, failed scan %1$d";
|
||||
"a11y_gallery_image" = "Gallery, image %1$d";
|
||||
"a11y_gallery_scanning" = "Gallery, scanning %1$d";
|
||||
"a11y_gallery_video" = "Gallery, video %1$d";
|
||||
"a11y_hide_password" = "Hide password";
|
||||
"a11y_info" = "Info";
|
||||
"a11y_join_call" = "Join call";
|
||||
|
||||
@@ -18,6 +18,22 @@
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@COUNT@</string>
|
||||
</dict>
|
||||
<key>a11y_gallery_more_media</key>
|
||||
<dict>
|
||||
<key>COUNT</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>Gallery, %1$d more media</string>
|
||||
<key>other</key>
|
||||
<string>Gallery, %1$d more medias</string>
|
||||
</dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@COUNT@</string>
|
||||
</dict>
|
||||
<key>a11y_polls_percent_of_total</key>
|
||||
<dict>
|
||||
<key>COUNT</key>
|
||||
|
||||
@@ -48,6 +48,30 @@ internal nonisolated enum L10n {
|
||||
}
|
||||
/// Expand message text field
|
||||
internal static var a11yExpandMessageTextField: String { return L10n.tr("Localizable", "a11y_expand_message_text_field") }
|
||||
/// Gallery, attachment %1$d
|
||||
internal static func a11yGalleryAttachment(_ p1: Int) -> String {
|
||||
return L10n.tr("Localizable", "a11y_gallery_attachment", p1)
|
||||
}
|
||||
/// Gallery, failed scan %1$d
|
||||
internal static func a11yGalleryFailedScan(_ p1: Int) -> String {
|
||||
return L10n.tr("Localizable", "a11y_gallery_failed_scan", p1)
|
||||
}
|
||||
/// Gallery, image %1$d
|
||||
internal static func a11yGalleryImage(_ p1: Int) -> String {
|
||||
return L10n.tr("Localizable", "a11y_gallery_image", p1)
|
||||
}
|
||||
/// Plural format key: "%#@COUNT@"
|
||||
internal static func a11yGalleryMoreMedia(_ p1: Int) -> String {
|
||||
return L10n.tr("Localizable", "a11y_gallery_more_media", p1)
|
||||
}
|
||||
/// Gallery, scanning %1$d
|
||||
internal static func a11yGalleryScanning(_ p1: Int) -> String {
|
||||
return L10n.tr("Localizable", "a11y_gallery_scanning", p1)
|
||||
}
|
||||
/// Gallery, video %1$d
|
||||
internal static func a11yGalleryVideo(_ p1: Int) -> String {
|
||||
return L10n.tr("Localizable", "a11y_gallery_video", p1)
|
||||
}
|
||||
/// Hide password
|
||||
internal static var a11yHidePassword: String { return L10n.tr("Localizable", "a11y_hide_password") }
|
||||
/// Info
|
||||
|
||||
@@ -21,6 +21,24 @@ private struct TimelineItemAccessibilityModifier: ViewModifier {
|
||||
action()
|
||||
}
|
||||
}
|
||||
// A gallery is a container so that each of its attachments can be focussed on its own.
|
||||
// Everything that isn't an attachment is announced when entering it, as the caption and
|
||||
// the send info are hidden where they're shown to avoid being read twice.
|
||||
case let timelineItem as GalleryRoomTimelineItem:
|
||||
content
|
||||
.accessibilityElement(children: .contain)
|
||||
.accessibilityLabel { _ in
|
||||
Text(timelineItem.sender.displayName ?? timelineItem.sender.id)
|
||||
if let caption = timelineItem.content.caption, !caption.isBlank {
|
||||
Text(caption)
|
||||
}
|
||||
Text(timelineItem.localizedSendInfo)
|
||||
}
|
||||
.accessibilityActions {
|
||||
Button(L10n.commonMessageActions) {
|
||||
action()
|
||||
}
|
||||
}
|
||||
case let timelineItem as EventBasedTimelineItemProtocol:
|
||||
content
|
||||
.accessibilityRepresentation {
|
||||
|
||||
@@ -18,6 +18,8 @@ extension View {
|
||||
modifier(TimelineItemSendInfoModifier(sendInfo: .init(timelineItem: timelineItem,
|
||||
adjustedDeliveryStatus: adjustedDeliveryStatus,
|
||||
hasContentScanningFailure: hasContentScanningFailure),
|
||||
// A gallery announces this when entering it instead.
|
||||
isAccessibilityHidden: timelineItem is GalleryRoomTimelineItem,
|
||||
context: context))
|
||||
}
|
||||
}
|
||||
@@ -25,6 +27,7 @@ extension View {
|
||||
/// Adds the send info to a view with the correct layout.
|
||||
private struct TimelineItemSendInfoModifier: ViewModifier {
|
||||
let sendInfo: TimelineItemSendInfo
|
||||
let isAccessibilityHidden: Bool
|
||||
let context: TimelineViewModel.Context
|
||||
|
||||
var layout: AnyLayout {
|
||||
@@ -43,6 +46,7 @@ private struct TimelineItemSendInfoModifier: ViewModifier {
|
||||
content
|
||||
|
||||
TimelineItemSendInfoLabel(sendInfo: sendInfo)
|
||||
.accessibilityHidden(isAccessibilityHidden)
|
||||
.contentShape(.rect)
|
||||
// Tap gesture to avoid the message being detected as a button by VoiceOver
|
||||
// (and the action shows a description that is already read to the user).
|
||||
|
||||
@@ -72,6 +72,7 @@ struct GalleryGridView: View {
|
||||
let overflow = (overflowCount > 0 && isLastVisible) ? overflowCount : 0
|
||||
|
||||
GalleryItemTileView(item: items[index],
|
||||
position: index + 1,
|
||||
mediaProvider: mediaProvider,
|
||||
contentScannerService: contentScannerService,
|
||||
overflowCount: overflow) {
|
||||
|
||||
@@ -10,11 +10,25 @@ import SwiftUI
|
||||
|
||||
struct GalleryItemTileView: View {
|
||||
let item: GalleryItem
|
||||
/// The item's position within the gallery, announced instead of its filename.
|
||||
let position: Int
|
||||
let mediaProvider: MediaProviderProtocol?
|
||||
let contentScannerService: ContentScannerServiceProtocol?
|
||||
let overflowCount: Int
|
||||
let onTap: () -> Void
|
||||
|
||||
private var accessibilityLabel: String {
|
||||
if overflowCount > 0 {
|
||||
L10n.a11yGalleryMoreMedia(overflowCount)
|
||||
} else if item.isVideo {
|
||||
L10n.a11yGalleryVideo(position)
|
||||
} else if item.isImage {
|
||||
L10n.a11yGalleryImage(position)
|
||||
} else {
|
||||
L10n.a11yGalleryAttachment(position)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
// Overflow above the scanner: shows on any scan state, survives content updates.
|
||||
ZStack {
|
||||
@@ -33,11 +47,15 @@ struct GalleryItemTileView: View {
|
||||
}
|
||||
}
|
||||
.accessibilityElement(children: .ignore)
|
||||
.accessibilityLabel(item.filename)
|
||||
.accessibilityLabel(accessibilityLabel)
|
||||
} scanningContent: {
|
||||
ScanningMediaEventsTimelineView()
|
||||
.accessibilityElement(children: .ignore)
|
||||
.accessibilityLabel(L10n.a11yGalleryScanning(position))
|
||||
} unsafeContent: { failure in
|
||||
UnsafeMediaEventsTimelineView(failure: failure)
|
||||
.accessibilityElement(children: .ignore)
|
||||
.accessibilityLabel(L10n.a11yGalleryFailedScan(position))
|
||||
}
|
||||
|
||||
if overflowCount > 0 {
|
||||
@@ -47,6 +65,9 @@ struct GalleryItemTileView: View {
|
||||
// Tappable in every scan state, so that an unsafe item can't hide the rest of the gallery.
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture { onTap() }
|
||||
// One element per tile, labelled by whichever state is on show.
|
||||
.accessibilityElement(children: .combine)
|
||||
.accessibilityAddTraits(.isButton)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
@@ -109,6 +130,7 @@ struct GalleryItemTileView: View {
|
||||
.foregroundStyle(.compound.textPrimary)
|
||||
}
|
||||
.allowsHitTesting(false) // Let the tap fall through to the tile below.
|
||||
.accessibilityHidden(true) // The count is already announced by the tile's label.
|
||||
}
|
||||
|
||||
private func formatted(duration: TimeInterval) -> String {
|
||||
|
||||
@@ -24,6 +24,7 @@ struct GalleryListView: View {
|
||||
}
|
||||
|
||||
GalleryListRow(item: item,
|
||||
position: index + 1,
|
||||
mediaProvider: mediaProvider,
|
||||
contentScannerService: contentScannerService) {
|
||||
onItemTap(index)
|
||||
@@ -46,6 +47,8 @@ struct GalleryDivider: View {
|
||||
|
||||
private struct GalleryListRow: View {
|
||||
let item: GalleryItem
|
||||
/// The item's position within the gallery, prepended to the row's own description.
|
||||
let position: Int
|
||||
let mediaProvider: MediaProviderProtocol?
|
||||
let contentScannerService: ContentScannerServiceProtocol?
|
||||
let onTap: () -> Void
|
||||
@@ -71,6 +74,13 @@ private struct GalleryListRow: View {
|
||||
} unsafeContent: { failure in
|
||||
failureRow(failure)
|
||||
}
|
||||
// One element per row, so that each attachment is focussed on its own, with its position
|
||||
// prepended to the description that the row already provides.
|
||||
.accessibilityElement(children: .combine)
|
||||
.accessibilityLabel { label in
|
||||
Text(L10n.a11yGalleryAttachment(position))
|
||||
label
|
||||
}
|
||||
}
|
||||
|
||||
private func row(icon: some View) -> some View {
|
||||
|
||||
@@ -41,6 +41,7 @@ struct GalleryRoomTimelineView: View {
|
||||
GalleryDivider()
|
||||
}
|
||||
caption
|
||||
.accessibilityHidden(true) // Announced when entering the gallery, so not a stop of its own.
|
||||
}
|
||||
}
|
||||
.frame(width: GalleryGridView.groupWidth, alignment: .leading)
|
||||
|
||||
Reference in New Issue
Block a user