From 7391c1264444de735cdbc2b63ae68c878eb555fe Mon Sep 17 00:00:00 2001 From: Chris Kolbu Date: Tue, 4 Apr 2023 16:12:25 +1000 Subject: [PATCH] Accessibility fix for Timeline `StatusRowView` and Status detail (#1355) * Add StatusRowView accessibility action to open media attachment viewer Previously, there would be no way to open QuickLook from the timeline. Now, we add a custom accessibility action to do this. * Work around initial accessibility focus bug in StatusDetailView Previously, (due to identity issues?) the focus would be set on the header view. However, moving to the next element in the focus order. would skip over a random number of elements, depending on the context of the detail view. Now, we manually set the focus once, allowing the focus order to work as intended. * Respect filters in Timeline combined accessibility label * Add explicit action to show filtered warnings from `filterView` --------- Co-authored-by: Thomas Ricouard --- .../Localization/be.lproj/Localizable.strings | 1 + .../Localization/ca.lproj/Localizable.strings | 1 + .../Localization/de.lproj/Localizable.strings | 1 + .../en-GB.lproj/Localizable.strings | 1 + .../Localization/en.lproj/Localizable.strings | 2 ++ .../Localization/es.lproj/Localizable.strings | 1 + .../Localization/eu.lproj/Localizable.strings | 1 + .../Localization/fr.lproj/Localizable.strings | 1 + .../Localization/it.lproj/Localizable.strings | 1 + .../Localization/ja.lproj/Localizable.strings | 1 + .../Localization/ko.lproj/Localizable.strings | 1 + .../Localization/nb.lproj/Localizable.strings | 1 + .../Localization/nl.lproj/Localizable.strings | 1 + .../Localization/pl.lproj/Localizable.strings | 1 + .../pt-BR.lproj/Localizable.strings | 1 + .../Localization/tr.lproj/Localizable.strings | 1 + .../Localization/uk.lproj/Localizable.strings | 1 + .../zh-Hans.lproj/Localizable.strings | 1 + .../zh-Hant.lproj/Localizable.strings | 1 + Packages/Models/Sources/Models/Filter.swift | 2 +- .../Status/Detail/StatusDetailView.swift | 9 +++++ .../Sources/Status/Row/StatusRowView.swift | 34 ++++++++++++++++++- 22 files changed, 63 insertions(+), 2 deletions(-) diff --git a/IceCubesApp/Resources/Localization/be.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/be.lproj/Localizable.strings index 32fba5b7..2d75adad 100644 --- a/IceCubesApp/Resources/Localization/be.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/be.lproj/Localizable.strings @@ -582,6 +582,7 @@ "accessibility.media.supported-type.audio.label" = "Audio"; "accessibility.status.contains-media.label-%@" = "Contains %@"; "accessibility.status.application.label" = "App"; +"accessibility.status.media-viewer-action.label" = "Open media viewer"; // MARK: Report "report.comment.placeholder" = "Дадатковая інфармацыя"; diff --git a/IceCubesApp/Resources/Localization/ca.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/ca.lproj/Localizable.strings index cb073c04..25953e94 100644 --- a/IceCubesApp/Resources/Localization/ca.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/ca.lproj/Localizable.strings @@ -576,6 +576,7 @@ "accessibility.media.supported-type.audio.label" = "Audio"; "accessibility.status.contains-media.label-%@" = "Contains %@"; "accessibility.status.application.label" = "App"; +"accessibility.status.media-viewer-action.label" = "Open media viewer"; // MARK: Report "report.comment.placeholder" = "Additional Info"; diff --git a/IceCubesApp/Resources/Localization/de.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/de.lproj/Localizable.strings index 77c4478e..88511bda 100644 --- a/IceCubesApp/Resources/Localization/de.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/de.lproj/Localizable.strings @@ -564,6 +564,7 @@ "accessibility.media.supported-type.audio.label" = "Audio"; "accessibility.status.contains-media.label-%@" = "Contains %@"; "accessibility.status.application.label" = "App"; +"accessibility.status.media-viewer-action.label" = "Open media viewer"; // MARK: Report diff --git a/IceCubesApp/Resources/Localization/en-GB.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/en-GB.lproj/Localizable.strings index a2bd4ac8..3aa5689f 100644 --- a/IceCubesApp/Resources/Localization/en-GB.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/en-GB.lproj/Localizable.strings @@ -577,6 +577,7 @@ "accessibility.media.supported-type.audio.label" = "Audio"; "accessibility.status.contains-media.label-%@" = "Contains %@"; "accessibility.status.application.label" = "App"; +"accessibility.status.media-viewer-action.label" = "Open media viewer"; // MARK: Report "report.comment.placeholder" = "Additional Info"; diff --git a/IceCubesApp/Resources/Localization/en.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/en.lproj/Localizable.strings index b2d4597b..e9a4e116 100644 --- a/IceCubesApp/Resources/Localization/en.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/en.lproj/Localizable.strings @@ -578,6 +578,8 @@ "accessibility.media.supported-type.audio.label" = "Audio"; "accessibility.status.contains-media.label-%@" = "Contains %@"; "accessibility.status.application.label" = "App"; +"accessibility.status.media-viewer-action.label" = "Open media viewer"; + // MARK: Report "report.comment.placeholder" = "Additional Info"; diff --git a/IceCubesApp/Resources/Localization/es.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/es.lproj/Localizable.strings index d86bcd1c..0087a70c 100644 --- a/IceCubesApp/Resources/Localization/es.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/es.lproj/Localizable.strings @@ -578,6 +578,7 @@ "accessibility.media.supported-type.audio.label" = "Audio"; "accessibility.status.contains-media.label-%@" = "Contains %@"; "accessibility.status.application.label" = "App"; +"accessibility.status.media-viewer-action.label" = "Open media viewer"; // MARK: Report "report.comment.placeholder" = "Información adicional"; diff --git a/IceCubesApp/Resources/Localization/eu.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/eu.lproj/Localizable.strings index 4a9a7a68..2b126c3d 100644 --- a/IceCubesApp/Resources/Localization/eu.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/eu.lproj/Localizable.strings @@ -566,6 +566,7 @@ "accessibility.media.supported-type.audio.label" = "Audioa"; "accessibility.status.contains-media.label-%@" = "%@ dauka"; "accessibility.status.application.label" = "Aplikazioa"; +"accessibility.status.media-viewer-action.label" = "Open media viewer"; // MARK: Report "report.comment.placeholder" = "Informazio gehigarria"; diff --git a/IceCubesApp/Resources/Localization/fr.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/fr.lproj/Localizable.strings index 0801c19e..7802d381 100644 --- a/IceCubesApp/Resources/Localization/fr.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/fr.lproj/Localizable.strings @@ -573,6 +573,7 @@ "accessibility.media.supported-type.audio.label" = "Audio"; "accessibility.status.contains-media.label-%@" = "Contains %@"; "accessibility.status.application.label" = "App"; +"accessibility.status.media-viewer-action.label" = "Open media viewer"; // MARK: Report "report.comment.placeholder" = "Information supplémentaire"; diff --git a/IceCubesApp/Resources/Localization/it.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/it.lproj/Localizable.strings index 869271f0..d5304806 100644 --- a/IceCubesApp/Resources/Localization/it.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/it.lproj/Localizable.strings @@ -577,6 +577,7 @@ "accessibility.media.supported-type.audio.label" = "Audio"; "accessibility.status.contains-media.label-%@" = "Contains %@"; "accessibility.status.application.label" = "App"; +"accessibility.status.media-viewer-action.label" = "Open media viewer"; // MARK: Report "report.comment.placeholder" = "Informazioni aggiuntive"; diff --git a/IceCubesApp/Resources/Localization/ja.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/ja.lproj/Localizable.strings index 08581f2a..e416535a 100644 --- a/IceCubesApp/Resources/Localization/ja.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/ja.lproj/Localizable.strings @@ -577,6 +577,7 @@ "accessibility.media.supported-type.audio.label" = "オーディオ"; "accessibility.status.contains-media.label-%@" = "%@ を含む"; "accessibility.status.application.label" = "アプリ"; +"accessibility.status.media-viewer-action.label" = "Open media viewer"; // MARK: Report "report.comment.placeholder" = "追加情報"; diff --git a/IceCubesApp/Resources/Localization/ko.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/ko.lproj/Localizable.strings index f48e0ef1..e64a2cf4 100644 --- a/IceCubesApp/Resources/Localization/ko.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/ko.lproj/Localizable.strings @@ -579,6 +579,7 @@ "accessibility.media.supported-type.audio.label" = "오디오"; "accessibility.status.contains-media.label-%@" = "%@ 첨부됨"; "accessibility.status.application.label" = "글 작성에 사용한 앱"; +"accessibility.status.media-viewer-action.label" = "Open media viewer"; // MARK: Report "report.comment.placeholder" = "추가 정보"; diff --git a/IceCubesApp/Resources/Localization/nb.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/nb.lproj/Localizable.strings index d8e335f0..70579ef1 100644 --- a/IceCubesApp/Resources/Localization/nb.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/nb.lproj/Localizable.strings @@ -577,6 +577,7 @@ "accessibility.media.supported-type.audio.label" = "Audio"; "accessibility.status.contains-media.label-%@" = "Contains %@"; "accessibility.status.application.label" = "App"; +"accessibility.status.media-viewer-action.label" = "Open media viewer"; // MARK: Report "report.comment.placeholder" = "Additional Info"; diff --git a/IceCubesApp/Resources/Localization/nl.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/nl.lproj/Localizable.strings index a844d10a..8b27a7cf 100644 --- a/IceCubesApp/Resources/Localization/nl.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/nl.lproj/Localizable.strings @@ -574,6 +574,7 @@ "accessibility.media.supported-type.audio.label" = "Audio"; "accessibility.status.contains-media.label-%@" = "Contains %@"; "accessibility.status.application.label" = "App"; +"accessibility.status.media-viewer-action.label" = "Open media viewer"; // MARK: Report "report.comment.placeholder" = "Aanvullende informatie"; diff --git a/IceCubesApp/Resources/Localization/pl.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/pl.lproj/Localizable.strings index 9fe116a2..2017253f 100644 --- a/IceCubesApp/Resources/Localization/pl.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/pl.lproj/Localizable.strings @@ -568,6 +568,7 @@ "accessibility.media.supported-type.audio.label" = "Audio"; "accessibility.status.contains-media.label-%@" = "Zawiera %@"; "accessibility.status.application.label" = "Aplikacja"; +"accessibility.status.media-viewer-action.label" = "Open media viewer"; // MARK: Report "report.comment.placeholder" = "Informacja dodatkowa"; diff --git a/IceCubesApp/Resources/Localization/pt-BR.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/pt-BR.lproj/Localizable.strings index 7ea448b2..de265bf9 100644 --- a/IceCubesApp/Resources/Localization/pt-BR.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/pt-BR.lproj/Localizable.strings @@ -577,6 +577,7 @@ "accessibility.media.supported-type.audio.label" = "Audio"; "accessibility.status.contains-media.label-%@" = "Contains %@"; "accessibility.status.application.label" = "App"; +"accessibility.status.media-viewer-action.label" = "Open media viewer"; // MARK: Report "report.comment.placeholder" = "Informação Adicional"; diff --git a/IceCubesApp/Resources/Localization/tr.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/tr.lproj/Localizable.strings index 7e09a364..035a122c 100644 --- a/IceCubesApp/Resources/Localization/tr.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/tr.lproj/Localizable.strings @@ -577,6 +577,7 @@ "accessibility.media.supported-type.audio.label" = "Audio"; "accessibility.status.contains-media.label-%@" = "Contains %@"; "accessibility.status.application.label" = "App"; +"accessibility.status.media-viewer-action.label" = "Open media viewer"; // MARK: Report "report.comment.placeholder" = "Additional Info"; diff --git a/IceCubesApp/Resources/Localization/uk.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/uk.lproj/Localizable.strings index f310d1a5..07f605c4 100644 --- a/IceCubesApp/Resources/Localization/uk.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/uk.lproj/Localizable.strings @@ -578,6 +578,7 @@ "accessibility.media.supported-type.audio.label" = "Audio"; "accessibility.status.contains-media.label-%@" = "Contains %@"; "accessibility.status.application.label" = "App"; +"accessibility.status.media-viewer-action.label" = "Open media viewer"; // MARK: Report "report.comment.placeholder" = "Додаткова інформація"; diff --git a/IceCubesApp/Resources/Localization/zh-Hans.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/zh-Hans.lproj/Localizable.strings index 878a1406..553a93df 100644 --- a/IceCubesApp/Resources/Localization/zh-Hans.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/zh-Hans.lproj/Localizable.strings @@ -577,6 +577,7 @@ "accessibility.media.supported-type.audio.label" = "音频"; "accessibility.status.contains-media.label-%@" = "包含 %@"; "accessibility.status.application.label" = "应用"; +"accessibility.status.media-viewer-action.label" = "Open media viewer"; // MARK: Report "report.comment.placeholder" = "附加信息"; diff --git a/IceCubesApp/Resources/Localization/zh-Hant.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/zh-Hant.lproj/Localizable.strings index dd846a5c..a7eaf33f 100644 --- a/IceCubesApp/Resources/Localization/zh-Hant.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/zh-Hant.lproj/Localizable.strings @@ -578,6 +578,7 @@ "accessibility.media.supported-type.audio.label" = "Audio"; "accessibility.status.contains-media.label-%@" = "Contains %@"; "accessibility.status.application.label" = "App"; +"accessibility.status.media-viewer-action.label" = "Open media viewer"; // MARK: Report "report.comment.placeholder" = "附加資訊"; diff --git a/Packages/Models/Sources/Models/Filter.swift b/Packages/Models/Sources/Models/Filter.swift index 4dd32f89..cf49a13d 100644 --- a/Packages/Models/Sources/Models/Filter.swift +++ b/Packages/Models/Sources/Models/Filter.swift @@ -6,7 +6,7 @@ public struct Filtered: Codable, Equatable, Hashable { } public struct Filter: Codable, Identifiable, Equatable, Hashable { - public enum Action: String, Codable { + public enum Action: String, Codable, Equatable { case warn, hide } diff --git a/Packages/Status/Sources/Status/Detail/StatusDetailView.swift b/Packages/Status/Sources/Status/Detail/StatusDetailView.swift index 7452aeeb..cd9d0ab8 100644 --- a/Packages/Status/Sources/Status/Detail/StatusDetailView.swift +++ b/Packages/Status/Sources/Status/Detail/StatusDetailView.swift @@ -17,6 +17,10 @@ public struct StatusDetailView: View { @State private var isLoaded: Bool = false @State private var statusHeight: CGFloat = 0 + /// April 4th, 2023: Without explicit focus being set, VoiceOver will skip over a seemingly random number of elements on this screen when pushing in from the main timeline. + /// By using ``@AccessibilityFocusState`` and setting focus once, we work around this issue. + @AccessibilityFocusState private var initialFocusBugWorkaround: Bool + public init(statusId: String) { _viewModel = StateObject(wrappedValue: .init(statusId: statusId)) } @@ -145,6 +149,7 @@ public struct StatusDetailView: View { client: client, routerPath: routerPath, isFocused: true) }) + .accessibilityFocused($initialFocusBugWorkaround, equals: true) .overlay { GeometryReader { reader in VStack {} @@ -154,6 +159,10 @@ public struct StatusDetailView: View { } } .id(status.id) + // VoiceOver / Switch Control focus workaround + .onAppear { + self.initialFocusBugWorkaround = true + } } private var errorView: some View { diff --git a/Packages/Status/Sources/Status/Row/StatusRowView.swift b/Packages/Status/Sources/Status/Row/StatusRowView.swift index 95abbdf0..73d20413 100644 --- a/Packages/Status/Sources/Status/Row/StatusRowView.swift +++ b/Packages/Status/Sources/Status/Row/StatusRowView.swift @@ -13,6 +13,7 @@ public struct StatusRowView: View { @Environment(\.isCompact) private var isCompact: Bool @Environment(\.accessibilityEnabled) private var accessibilityEnabled + @EnvironmentObject private var quickLook: QuickLook @EnvironmentObject private var theme: Theme @StateObject var viewModel: StatusRowViewModel @@ -119,6 +120,7 @@ public struct StatusRowView: View { .accessibilityElement(children: viewModel.isFocused ? .contain : .combine) .accessibilityLabel(viewModel.isFocused == false && accessibilityEnabled ? CombinedAccessibilityLabel(viewModel: viewModel).finalLabel() : Text("")) + .accessibilityHidden(viewModel.filter?.filter.filterAction == .hide) .accessibilityAction { viewModel.navigateToDetail() } @@ -175,6 +177,16 @@ public struct StatusRowView: View { viewModel.routerPath.presentedSheet = .quoteStatusEditor(status: viewModel.status) } + if viewModel.finalStatus.mediaAttachments.isEmpty == false { + Button("accessibility.status.media-viewer-action.label") { + HapticManager.shared.fireHaptic(of: .notification(.success)) + Task { + let attachments = viewModel.finalStatus.mediaAttachments + await quickLook.prepareFor(urls: attachments.compactMap { $0.url }, selectedURL: attachments[0].url!) + } + } + } + Button(viewModel.displaySpoiler ? "status.show-more" : "status.show-less") { withAnimation { viewModel.displaySpoiler.toggle() @@ -229,6 +241,9 @@ public struct StatusRowView: View { Text("status.filter.show-anyway") } } + .accessibilityAction { + viewModel.isFiltered = false + } } private var remoteContentLoadingView: some View { @@ -268,8 +283,23 @@ private struct CombinedAccessibilityLabel { viewModel.status.reblog != nil } + var filter: Filter? { + guard viewModel.isFiltered else { + return nil + } + return viewModel.filter?.filter + } + func finalLabel() -> Text { - userNamePreamble() + + if let filter { + switch filter.filterAction { + case .warn: + return Text("status.filter.filtered-by-\(filter.title)") + case .hide: + return Text("") + } + } else { + return userNamePreamble() + Text(hasSpoiler ? viewModel.finalStatus.spoilerText.asRawText : viewModel.finalStatus.content.asRawText @@ -284,6 +314,8 @@ private struct CombinedAccessibilityLabel { Text("status.summary.n-replies \(viewModel.finalStatus.repliesCount)") + Text(", ") + Text("status.summary.n-boosts \(viewModel.finalStatus.reblogsCount)") + Text(", ") + Text("status.summary.n-favorites \(viewModel.finalStatus.favouritesCount)") + + } } func userNamePreamble() -> Text {