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:
parent
4351cec117
commit
7391c12644
|
@ -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" = "Дадатковая інфармацыя";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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" = "追加情報";
|
||||
|
|
|
@ -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" = "추가 정보";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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" = "Додаткова інформація";
|
||||
|
|
|
@ -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" = "附加信息";
|
||||
|
|
|
@ -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" = "附加資訊";
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue