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 <ricouard77@gmail.com>
This commit is contained in:
Chris Kolbu 2023-04-04 16:12:25 +10:00 committed by GitHub
parent 4351cec117
commit 7391c12644
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 63 additions and 2 deletions

View file

@ -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" = "Дадатковая інфармацыя";

View file

@ -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";

View file

@ -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

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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" = "追加情報";

View file

@ -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" = "추가 정보";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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" = "Додаткова інформація";

View file

@ -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" = "附加信息";

View file

@ -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" = "附加資訊";

View file

@ -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
}

View file

@ -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 {

View file

@ -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 {