Timeline & Timeline detail accessibility uplift (#1323)

* Improve accessibility of StatusPollView

Previously, this view did not provide the proper context to indicate that it represented a poll.

Now, we’ve added
- A container that will stay “Active poll” or “Poll results” when the cursor first hits one of the options;
- A prefix to say “Option X of Y” before each option;
- A Selected trait on the selected option(s), if present
- Consolidating and adding an `.updatesFrequently` trait to the footer view with the countdown.

* Add poll description in StatusRowView combinedAccessibilityLabel

This largely duplicates the logic in `StatusPollView`.

* Improve accessibility of media attachments

Previously, the media attachments without alt text would not show up in the consolidated `StatusRowView`, nor would they be meaningfully explained on the status detail screen.

Now, they are presented with their attachment type.

* Change accessibilityRepresentation of AppAcountsSelectorView

* Change Notifications tab title view accessibility representation to Menu

Previously it would present as a button

* Hide layout `Rectangle`s from accessibility

* Consolidate `StatusRowDetailView` accessibility representation

* Improve readability of Poll accessibility label

* Ensure poll options don’t present as interactive when the poll is finished

* Improve accessibility of StatusRowCardView

Previously, it would present as four separate elements, including an image without a description, all interactive, none with an interactive trait.

Now, it presents as a single element with the `.link` trait

* Improve accessibility of StatusRowHeaderView

Previously, it had no traits and no actions except inherited ones.

Now it presents as a button, triggering its primary action.

It also has custom actions corresponding to its context menu

* Avoid applying the StatusRowView custom actions to every view when contained

* Provide context for the application name

* Add pauses to StatusRowView combinedAccessibilityLabel

* Hide `TimelineView.scrollToTopView` from accessibility

* Set appropriate font style on Notification header

After the change the Text needed a `.headline` style to match the prior appearance.

* Fix bug in accessibilityRepresentation of TimelineView nav bar title

Previously, it would not display the proper label for .remoteLocal filter options.

* Ensure that pop-up button nav bar titles are interactive

* Ensure TextView responds to Environment.sizeCategory

This resolves #1309

* Fix button

---------

Co-authored-by: Thomas Ricouard <ricouard77@gmail.com>
This commit is contained in:
Chris Kolbu 2023-03-29 03:48:58 +11:00 committed by GitHub
parent 59af600945
commit 9e347c75b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 368 additions and 30 deletions

View File

@ -569,6 +569,16 @@
"accessibility.image.alt-text-%@" = "Image alt text: %@";
"accessibility.image.alt-text-more.label" = "More alt text available";
"accessibility.tabs.messages.unread.label" = "Unread";
"accessibility.status.poll.option-prefix-%lld-of-%lld" = "Option %lld of %lld";
"accessibility.status.poll.active.label" = "Active poll";
"accessibility.status.poll.finished.label" = "Poll results";
"accessibility.status.poll.selected.label" = "Selected";
"accessibility.media.supported-type.image.label" = "Image";
"accessibility.media.supported-type.gifv.label" = "Animated Gif";
"accessibility.media.supported-type.video.label" = "Video";
"accessibility.media.supported-type.audio.label" = "Audio";
"accessibility.status.contains-media.label-%@" = "Contains %@";
"accessibility.status.application.label" = "App";
// MARK: Report
"report.comment.placeholder" = "Дадатковая інфармацыя";

View File

@ -563,6 +563,16 @@
"accessibility.image.alt-text-%@" = "Image alt text: %@";
"accessibility.image.alt-text-more.label" = "More alt text available";
"accessibility.tabs.messages.unread.label" = "Unread";
"accessibility.status.poll.option-prefix-%lld-of-%lld" = "Option %lld of %lld";
"accessibility.status.poll.active.label" = "Active poll";
"accessibility.status.poll.finished.label" = "Poll results";
"accessibility.status.poll.selected.label" = "Selected";
"accessibility.media.supported-type.image.label" = "Image";
"accessibility.media.supported-type.gifv.label" = "Animated Gif";
"accessibility.media.supported-type.video.label" = "Video";
"accessibility.media.supported-type.audio.label" = "Audio";
"accessibility.status.contains-media.label-%@" = "Contains %@";
"accessibility.status.application.label" = "App";
// MARK: Report
"report.comment.placeholder" = "Additional Info";

View File

@ -559,6 +559,17 @@
"accessibility.image.alt-text-%@" = "Alternativer Bildtext: %@";
"accessibility.image.alt-text-more.label" = "Weiterer Alt.-Text verfügbar";
"accessibility.tabs.messages.unread.label" = "Ungelesen";
"accessibility.status.poll.option-prefix-%lld-of-%lld" = "Option %lld of %lld";
"accessibility.status.poll.active.label" = "Active poll";
"accessibility.status.poll.finished.label" = "Poll results";
"accessibility.status.poll.selected.label" = "Selected";
"accessibility.media.supported-type.image.label" = "Image";
"accessibility.media.supported-type.gifv.label" = "Animated Gif";
"accessibility.media.supported-type.video.label" = "Video";
"accessibility.media.supported-type.audio.label" = "Audio";
"accessibility.status.contains-media.label-%@" = "Contains %@";
"accessibility.status.application.label" = "App";
// MARK: Report
"report.comment.placeholder" = "Zusätzliche Informationen";

View File

@ -564,6 +564,16 @@
"accessibility.image.alt-text-%@" = "Image alt text: %@";
"accessibility.image.alt-text-more.label" = "More alt text available";
"accessibility.tabs.messages.unread.label" = "Unread";
"accessibility.status.poll.option-prefix-%lld-of-%lld" = "Option %lld of %lld";
"accessibility.status.poll.active.label" = "Active poll";
"accessibility.status.poll.finished.label" = "Poll results";
"accessibility.status.poll.selected.label" = "Selected";
"accessibility.media.supported-type.image.label" = "Image";
"accessibility.media.supported-type.gifv.label" = "Animated Gif";
"accessibility.media.supported-type.video.label" = "Video";
"accessibility.media.supported-type.audio.label" = "Audio";
"accessibility.status.contains-media.label-%@" = "Contains %@";
"accessibility.status.application.label" = "App";
// MARK: Report
"report.comment.placeholder" = "Additional Info";

View File

@ -565,6 +565,16 @@
"accessibility.image.alt-text-%@" = "Image alt text: %@";
"accessibility.image.alt-text-more.label" = "More alt text available";
"accessibility.tabs.messages.unread.label" = "Unread";
"accessibility.status.poll.option-prefix-%lld-of-%lld" = "Option %lld of %lld";
"accessibility.status.poll.active.label" = "Active poll";
"accessibility.status.poll.finished.label" = "Poll results";
"accessibility.status.poll.selected.label" = "Selected";
"accessibility.media.supported-type.image.label" = "Image";
"accessibility.media.supported-type.gifv.label" = "Animated Gif";
"accessibility.media.supported-type.video.label" = "Video";
"accessibility.media.supported-type.audio.label" = "Audio";
"accessibility.status.contains-media.label-%@" = "Contains %@";
"accessibility.status.application.label" = "App";
// MARK: Report
"report.comment.placeholder" = "Additional Info";

View File

@ -565,6 +565,16 @@
"accessibility.image.alt-text-%@" = "Texto alt de la imagen: %@";
"accessibility.image.alt-text-more.label" = "Hay más text alt disponible";
"accessibility.tabs.messages.unread.label" = "Unread";
"accessibility.status.poll.option-prefix-%lld-of-%lld" = "Option %lld of %lld";
"accessibility.status.poll.active.label" = "Active poll";
"accessibility.status.poll.finished.label" = "Poll results";
"accessibility.status.poll.selected.label" = "Selected";
"accessibility.media.supported-type.image.label" = "Image";
"accessibility.media.supported-type.gifv.label" = "Animated Gif";
"accessibility.media.supported-type.video.label" = "Video";
"accessibility.media.supported-type.audio.label" = "Audio";
"accessibility.status.contains-media.label-%@" = "Contains %@";
"accessibility.status.application.label" = "App";
// MARK: Report
"report.comment.placeholder" = "Información adicional";

View File

@ -553,6 +553,16 @@
"accessibility.image.alt-text-%@" = "Irudiaren deskribapena: %@";
"accessibility.image.alt-text-more.label" = "Deskribapen testu gehiago dago";
"accessibility.tabs.messages.unread.label" = "Irakurri gabe";
"accessibility.status.poll.option-prefix-%lld-of-%lld" = "Option %lld of %lld";
"accessibility.status.poll.active.label" = "Active poll";
"accessibility.status.poll.finished.label" = "Poll results";
"accessibility.status.poll.selected.label" = "Selected";
"accessibility.media.supported-type.image.label" = "Image";
"accessibility.media.supported-type.gifv.label" = "Animated Gif";
"accessibility.media.supported-type.video.label" = "Video";
"accessibility.media.supported-type.audio.label" = "Audio";
"accessibility.status.contains-media.label-%@" = "Contains %@";
"accessibility.status.application.label" = "App";
// MARK: Report
"report.comment.placeholder" = "Informazio gehigarria";

View File

@ -560,6 +560,16 @@
"accessibility.image.alt-text-%@" = "Image alt text: %@";
"accessibility.image.alt-text-more.label" = "More alt text available";
"accessibility.tabs.messages.unread.label" = "Unread";
"accessibility.status.poll.option-prefix-%lld-of-%lld" = "Option %lld of %lld";
"accessibility.status.poll.active.label" = "Active poll";
"accessibility.status.poll.finished.label" = "Poll results";
"accessibility.status.poll.selected.label" = "Selected";
"accessibility.media.supported-type.image.label" = "Image";
"accessibility.media.supported-type.gifv.label" = "Animated Gif";
"accessibility.media.supported-type.video.label" = "Video";
"accessibility.media.supported-type.audio.label" = "Audio";
"accessibility.status.contains-media.label-%@" = "Contains %@";
"accessibility.status.application.label" = "App";
// MARK: Report
"report.comment.placeholder" = "Information supplémentaire";

View File

@ -564,6 +564,16 @@
"accessibility.image.alt-text-%@" = "Image alt text: %@";
"accessibility.image.alt-text-more.label" = "More alt text available";
"accessibility.tabs.messages.unread.label" = "Unread";
"accessibility.status.poll.option-prefix-%lld-of-%lld" = "Option %lld of %lld";
"accessibility.status.poll.active.label" = "Active poll";
"accessibility.status.poll.finished.label" = "Poll results";
"accessibility.status.poll.selected.label" = "Selected";
"accessibility.media.supported-type.image.label" = "Image";
"accessibility.media.supported-type.gifv.label" = "Animated Gif";
"accessibility.media.supported-type.video.label" = "Video";
"accessibility.media.supported-type.audio.label" = "Audio";
"accessibility.status.contains-media.label-%@" = "Contains %@";
"accessibility.status.application.label" = "App";
// MARK: Report
"report.comment.placeholder" = "Informazioni aggiuntive";

View File

@ -564,6 +564,16 @@
"accessibility.image.alt-text-%@" = "画像の代替テキスト: %@";
"accessibility.image.alt-text-more.label" = "より多くの代替テキストを利用できます";
"accessibility.tabs.messages.unread.label" = "未読";
"accessibility.status.poll.option-prefix-%lld-of-%lld" = "Option %lld of %lld";
"accessibility.status.poll.active.label" = "Active poll";
"accessibility.status.poll.finished.label" = "Poll results";
"accessibility.status.poll.selected.label" = "Selected";
"accessibility.media.supported-type.image.label" = "Image";
"accessibility.media.supported-type.gifv.label" = "Animated Gif";
"accessibility.media.supported-type.video.label" = "Video";
"accessibility.media.supported-type.audio.label" = "Audio";
"accessibility.status.contains-media.label-%@" = "Contains %@";
"accessibility.status.application.label" = "App";
// MARK: Report
"report.comment.placeholder" = "追加情報";

View File

@ -566,6 +566,16 @@
"accessibility.image.alt-text-%@" = "미디어 설명: %@";
"accessibility.image.alt-text-more.label" = "더 많은 미디어 설명 사용 가능"; /* 추가 확인 필요 */
"accessibility.tabs.messages.unread.label" = "읽지 않음";
"accessibility.status.poll.option-prefix-%lld-of-%lld" = "Option %lld of %lld";
"accessibility.status.poll.active.label" = "Active poll";
"accessibility.status.poll.finished.label" = "Poll results";
"accessibility.status.poll.selected.label" = "Selected";
"accessibility.media.supported-type.image.label" = "Image";
"accessibility.media.supported-type.gifv.label" = "Animated Gif";
"accessibility.media.supported-type.video.label" = "Video";
"accessibility.media.supported-type.audio.label" = "Audio";
"accessibility.status.contains-media.label-%@" = "Contains %@";
"accessibility.status.application.label" = "App";
// MARK: Report
"report.comment.placeholder" = "추가 정보";

View File

@ -564,6 +564,16 @@
"accessibility.image.alt-text-%@" = "Image alt text: %@";
"accessibility.image.alt-text-more.label" = "More alt text available";
"accessibility.tabs.messages.unread.label" = "Unread";
"accessibility.status.poll.option-prefix-%lld-of-%lld" = "Option %lld of %lld";
"accessibility.status.poll.active.label" = "Active poll";
"accessibility.status.poll.finished.label" = "Poll results";
"accessibility.status.poll.selected.label" = "Selected";
"accessibility.media.supported-type.image.label" = "Image";
"accessibility.media.supported-type.gifv.label" = "Animated Gif";
"accessibility.media.supported-type.video.label" = "Video";
"accessibility.media.supported-type.audio.label" = "Audio";
"accessibility.status.contains-media.label-%@" = "Contains %@";
"accessibility.status.application.label" = "App";
// MARK: Report
"report.comment.placeholder" = "Additional Info";

View File

@ -561,6 +561,16 @@
"accessibility.image.alt-text-%@" = "Tekst voor afbeedling: %@";
"accessibility.image.alt-text-more.label" = "Meer tekst beschikbaar";
"accessibility.tabs.messages.unread.label" = "Unread";
"accessibility.status.poll.option-prefix-%lld-of-%lld" = "Option %lld of %lld";
"accessibility.status.poll.active.label" = "Active poll";
"accessibility.status.poll.finished.label" = "Poll results";
"accessibility.status.poll.selected.label" = "Selected";
"accessibility.media.supported-type.image.label" = "Image";
"accessibility.media.supported-type.gifv.label" = "Animated Gif";
"accessibility.media.supported-type.video.label" = "Video";
"accessibility.media.supported-type.audio.label" = "Audio";
"accessibility.status.contains-media.label-%@" = "Contains %@";
"accessibility.status.application.label" = "App";
// MARK: Report
"report.comment.placeholder" = "Aanvullende informatie";

View File

@ -555,6 +555,16 @@
"accessibility.image.alt-text-%@" = "Tekst alternatywny obrazka: %@";
"accessibility.image.alt-text-more.label" = "Dostępna jest większa ilość tekstu alternatywnego";
"accessibility.tabs.messages.unread.label" = "Nieprzeczytane";
"accessibility.status.poll.option-prefix-%lld-of-%lld" = "Option %lld of %lld";
"accessibility.status.poll.active.label" = "Active poll";
"accessibility.status.poll.finished.label" = "Poll results";
"accessibility.status.poll.selected.label" = "Selected";
"accessibility.media.supported-type.image.label" = "Image";
"accessibility.media.supported-type.gifv.label" = "Animated Gif";
"accessibility.media.supported-type.video.label" = "Video";
"accessibility.media.supported-type.audio.label" = "Audio";
"accessibility.status.contains-media.label-%@" = "Contains %@";
"accessibility.status.application.label" = "App";
// MARK: Report
"report.comment.placeholder" = "Informacja dodatkowa";

View File

@ -564,6 +564,16 @@
"accessibility.image.alt-text-%@" = "Texto alternativo da imagem: %@";
"accessibility.image.alt-text-more.label" = "Mais texto alternativo disponível";
"accessibility.tabs.messages.unread.label" = "Não lido";
"accessibility.status.poll.option-prefix-%lld-of-%lld" = "Option %lld of %lld";
"accessibility.status.poll.active.label" = "Active poll";
"accessibility.status.poll.finished.label" = "Poll results";
"accessibility.status.poll.selected.label" = "Selected";
"accessibility.media.supported-type.image.label" = "Image";
"accessibility.media.supported-type.gifv.label" = "Animated Gif";
"accessibility.media.supported-type.video.label" = "Video";
"accessibility.media.supported-type.audio.label" = "Audio";
"accessibility.status.contains-media.label-%@" = "Contains %@";
"accessibility.status.application.label" = "App";
// MARK: Report
"report.comment.placeholder" = "Informação Adicional";

View File

@ -564,6 +564,16 @@
"accessibility.image.alt-text-%@" = "Image alt text: %@";
"accessibility.image.alt-text-more.label" = "More alt text available";
"accessibility.tabs.messages.unread.label" = "Unread";
"accessibility.status.poll.option-prefix-%lld-of-%lld" = "Option %lld of %lld";
"accessibility.status.poll.active.label" = "Active poll";
"accessibility.status.poll.finished.label" = "Poll results";
"accessibility.status.poll.selected.label" = "Selected";
"accessibility.media.supported-type.image.label" = "Image";
"accessibility.media.supported-type.gifv.label" = "Animated Gif";
"accessibility.media.supported-type.video.label" = "Video";
"accessibility.media.supported-type.audio.label" = "Audio";
"accessibility.status.contains-media.label-%@" = "Contains %@";
"accessibility.status.application.label" = "App";
// MARK: Report
"report.comment.placeholder" = "Additional Info";

View File

@ -565,6 +565,16 @@
"accessibility.image.alt-text-%@" = "Image alt text: %@";
"accessibility.image.alt-text-more.label" = "More alt text available";
"accessibility.tabs.messages.unread.label" = "Unread";
"accessibility.status.poll.option-prefix-%lld-of-%lld" = "Option %lld of %lld";
"accessibility.status.poll.active.label" = "Active poll";
"accessibility.status.poll.finished.label" = "Poll results";
"accessibility.status.poll.selected.label" = "Selected";
"accessibility.media.supported-type.image.label" = "Image";
"accessibility.media.supported-type.gifv.label" = "Animated Gif";
"accessibility.media.supported-type.video.label" = "Video";
"accessibility.media.supported-type.audio.label" = "Audio";
"accessibility.status.contains-media.label-%@" = "Contains %@";
"accessibility.status.application.label" = "App";
// MARK: Report
"report.comment.placeholder" = "Додаткова інформація";

View File

@ -564,6 +564,16 @@
"accessibility.image.alt-text-%@" = "图片描述文本:%@";
"accessibility.image.alt-text-more.label" = "更多描述文本可用";
"accessibility.tabs.messages.unread.label" = "未读";
"accessibility.status.poll.option-prefix-%lld-of-%lld" = "Option %lld of %lld";
"accessibility.status.poll.active.label" = "Active poll";
"accessibility.status.poll.finished.label" = "Poll results";
"accessibility.status.poll.selected.label" = "Selected";
"accessibility.media.supported-type.image.label" = "Image";
"accessibility.media.supported-type.gifv.label" = "Animated Gif";
"accessibility.media.supported-type.video.label" = "Video";
"accessibility.media.supported-type.audio.label" = "Audio";
"accessibility.status.contains-media.label-%@" = "Contains %@";
"accessibility.status.application.label" = "App";
// MARK: Report
"report.comment.placeholder" = "附加信息";

View File

@ -565,6 +565,16 @@
"accessibility.image.alt-text-%@" = "圖片描述文字:%@";
"accessibility.image.alt-text-more.label" = "更多圖片描述文字";
"accessibility.tabs.messages.unread.label" = "未讀";
"accessibility.status.poll.option-prefix-%lld-of-%lld" = "Option %lld of %lld";
"accessibility.status.poll.active.label" = "Active poll";
"accessibility.status.poll.finished.label" = "Poll results";
"accessibility.status.poll.selected.label" = "Selected";
"accessibility.media.supported-type.image.label" = "Image";
"accessibility.media.supported-type.gifv.label" = "Animated Gif";
"accessibility.media.supported-type.video.label" = "Video";
"accessibility.media.supported-type.audio.label" = "Audio";
"accessibility.status.contains-media.label-%@" = "Contains %@";
"accessibility.status.application.label" = "App";
// MARK: Report
"report.comment.placeholder" = "附加資訊";

View File

@ -50,6 +50,7 @@ struct AccountDetailHeaderView: View {
Rectangle()
.foregroundColor(theme.secondaryBackgroundColor)
.frame(height: Constants.headerHeight)
.accessibilityHidden(true)
} else {
LazyImage(url: account.header) { state in
if let image = state.image {

View File

@ -299,6 +299,7 @@ public struct AccountDetailView: View {
.frame(height: 12)
.listRowInsets(.init())
.listRowSeparator(.hidden)
.accessibilityHidden(true)
}
}

View File

@ -67,6 +67,11 @@ public struct AppAccountsSelectorView: View {
.onAppear {
refreshAccounts()
}
.accessibilityRepresentation {
Menu("accessibility.app-account.selector.accounts") {}
.accessibilityHint("accessibility.app-account.selector.accounts.hint")
.accessibilityRemoveTraits(.isButton)
}
}
@ViewBuilder
@ -85,8 +90,6 @@ public struct AppAccountsSelectorView: View {
.frame(width: 9, height: 9)
}
}
.accessibilityLabel("accessibility.app-account.selector.accounts")
.accessibilityHint("accessibility.app-account.selector.accounts.hint")
}
private var accountBackgroundColor: Color {

View File

@ -110,6 +110,7 @@ public struct ConversationDetailView: View {
.fill(Color.clear)
.frame(height: 40)
.id(Constants.bottomAnchor)
.accessibilityHidden(true)
}
private var inputTextView: some View {

View File

@ -38,6 +38,7 @@ struct ThemeBoxView: View {
.frame(maxWidth: .infinity, maxHeight: .infinity)
.cornerRadius(4)
.shadow(radius: 2, x: 2, y: 4)
.accessibilityHidden(true)
VStack(spacing: gutterSpace) {
Text(color.name.rawValue)

View File

@ -24,6 +24,22 @@ public struct MediaAttachment: Codable, Identifiable, Hashable, Equatable {
SupportedType(rawValue: type)
}
public var localizedTypeDescription: String? {
if let supportedType {
switch supportedType {
case .image:
return NSLocalizedString("accessibility.media.supported-type.image.label", bundle: .main, comment: "A localized description of SupportedType.image")
case .gifv:
return NSLocalizedString("accessibility.media.supported-type.gifv.label", bundle: .main, comment: "A localized description of SupportedType.gifv")
case .video:
return NSLocalizedString("accessibility.media.supported-type.video.label", bundle: .main, comment: "A localized description of SupportedType.video")
case .audio:
return NSLocalizedString("accessibility.media.supported-type.audio.label", bundle: .main, comment: "A localized description of SupportedType.audio")
}
}
return nil
}
public let url: URL?
public let previewUrl: URL?
public let description: String?

View File

@ -28,8 +28,25 @@ public struct NotificationsListView: View {
.id(account.account?.id)
.environment(\.defaultMinListRowHeight, 1)
.listStyle(.plain)
.navigationTitle(lockedType?.menuTitle() ?? viewModel.selectedType?.menuTitle() ?? "notifications.navigation-title")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .principal) {
let title = lockedType?.menuTitle() ?? viewModel.selectedType?.menuTitle() ?? "notifications.navigation-title"
if lockedType == nil {
Text(title)
.font(.headline)
.accessibilityRepresentation {
Menu(title) {}
}
.accessibilityAddTraits(.isHeader)
.accessibilityRemoveTraits(.isButton)
.accessibilityRespondsToUserInteraction(true)
} else {
Text(title)
.font(.headline)
.accessibilityAddTraits(.isHeader)
}
}
}
.toolbar {
if lockedType == nil {
ToolbarTitleMenu {
@ -53,6 +70,7 @@ public struct NotificationsListView: View {
}
}
}
.navigationBarTitleDisplayMode(.inline)
.scrollContentBackground(.hidden)
.background(theme.primaryBackgroundColor)
.task {

View File

@ -11,8 +11,9 @@ public struct StatusDetailView: View {
@EnvironmentObject private var watcher: StreamWatcher
@EnvironmentObject private var client: Client
@EnvironmentObject private var routerPath: RouterPath
@Environment(\.openURL) private var openURL
@StateObject private var viewModel: StatusDetailViewModel
@State private var isLoaded: Bool = false
@State private var statusHeight: CGFloat = 0
@ -54,6 +55,7 @@ public struct StatusDetailView: View {
.listRowSeparator(.hidden)
.listRowBackground(theme.secondaryBackgroundColor)
.listRowInsets(.init())
.accessibilityHidden(true)
case .error:
errorView
@ -119,6 +121,7 @@ public struct StatusDetailView: View {
Rectangle()
.fill(theme.tintColor)
.frame(width: 2)
.accessibilityHidden(true)
Spacer(minLength: 8)
}
if self.viewModel.statusId == status.id {

View File

@ -158,5 +158,6 @@ struct StatusEditorMediaView: View {
Rectangle()
.foregroundColor(theme.secondaryBackgroundColor)
.frame(width: 150, height: 150)
.accessibilityHidden(true)
}
}

View File

@ -7,6 +7,7 @@ extension TextView.Representable {
private var originalText: NSMutableAttributedString = .init()
private var text: Binding<NSMutableAttributedString>
private var sizeCategory: ContentSizeCategory
private var calculatedHeight: Binding<CGFloat>
var didBecomeFirstResponder = false
@ -15,6 +16,7 @@ extension TextView.Representable {
init(text: Binding<NSMutableAttributedString>,
calculatedHeight: Binding<CGFloat>,
sizeCategory: ContentSizeCategory,
getTextView: ((UITextView) -> Void)?)
{
textView = UIKitTextView()
@ -26,6 +28,7 @@ extension TextView.Representable {
self.text = text
self.calculatedHeight = calculatedHeight
self.sizeCategory = sizeCategory
self.getTextView = getTextView
super.init()

View File

@ -4,6 +4,7 @@ extension TextView {
struct Representable: UIViewRepresentable {
@Binding var text: NSMutableAttributedString
@Binding var calculatedHeight: CGFloat
@Environment(\.sizeCategory) var sizeCategory
let keyboard: UIKeyboardType
var getTextView: ((UITextView) -> Void)?
@ -24,6 +25,7 @@ extension TextView {
Coordinator(
text: $text,
calculatedHeight: $calculatedHeight,
sizeCategory: sizeCategory,
getTextView: getTextView
)
}

View File

@ -55,6 +55,7 @@ struct VideoPlayerView: View {
var body: some View {
ZStack {
VideoPlayer(player: viewModel.player)
.accessibilityAddTraits(.startsMediaSession)
if !preferences.autoPlayVideo {
Image(systemName: "play.fill")

View File

@ -70,11 +70,12 @@ public struct StatusPollView: View {
}
public var body: some View {
let isInteractive = viewModel.poll.expired == false && (viewModel.poll.voted ?? true) == false
VStack(alignment: .leading) {
ForEach(viewModel.poll.options) { option in
ForEach(Array(viewModel.poll.options.enumerated()), id: \.element.id) { index, option in
HStack {
makeBarView(for: option, buttonImage: buttonImage(option: option))
.disabled(viewModel.poll.expired || (viewModel.poll.voted ?? false))
.disabled(isInteractive == false)
if viewModel.showResults || status.account.id == currentAccount.account?.id {
Spacer()
// Make sure they're all the same width using a ZStack with 100% hiding behind the
@ -88,6 +89,12 @@ public struct StatusPollView: View {
}
}
}
.accessibilityElement(children: .combine)
.accessibilityLabel(combinedAccessibilityLabel(for: option, index: index))
.accessibilityRespondsToUserInteraction(isInteractive)
.accessibilityAddTraits(isSelected(option: option) ? .isSelected : [])
.accessibilityAddTraits(isInteractive ? [] : .isStaticText)
.accessibilityRemoveTraits(isInteractive ? [] : .isButton)
}
if !viewModel.poll.expired, !(viewModel.poll.voted ?? false), !viewModel.votes.isEmpty {
Button("status.poll.send") {
@ -108,6 +115,18 @@ public struct StatusPollView: View {
await viewModel.fetchPoll()
}
}
.accessibilityElement(children: .contain)
.accessibilityLabel(viewModel.poll.expired ? "accessibility.status.poll.finished.label" : "accessibility.status.poll.active.label")
}
func combinedAccessibilityLabel(for option: Poll.Option, index: Int) -> Text {
let showPercentage = viewModel.poll.expired || viewModel.poll.voted ?? false
return Text("accessibility.status.poll.option-prefix-\(index + 1)-of-\(viewModel.poll.options.count)") +
Text(", ") +
Text(option.title) +
Text(showPercentage ? ", \(percentForOption(option: option))%" : "")
}
private var footerView: some View {
@ -118,6 +137,7 @@ public struct StatusPollView: View {
Text("status.poll.n-votes \(viewModel.poll.votesCount)")
}
Text("")
.accessibilityHidden(true)
if viewModel.poll.expired {
Text("status.poll.closed")
} else if let date = viewModel.poll.expiresAt.value?.asDate {
@ -127,6 +147,8 @@ public struct StatusPollView: View {
}
.font(.scaledFootnote)
.foregroundColor(.gray)
.accessibilityElement(children: .combine)
.accessibilityAddTraits(.updatesFrequently)
}
@ViewBuilder

View File

@ -67,6 +67,11 @@ public struct StatusRowView: View {
.onTapGesture {
viewModel.navigateToDetail()
}
.accessibilityActions {
if viewModel.isFocused, viewModel.showActions {
accessibilityActions
}
}
}
if viewModel.showActions, viewModel.isFocused || theme.statusActionsDisplay != .none, !isInCaptureMode {
StatusRowActionsView(viewModel: viewModel)
@ -118,7 +123,7 @@ public struct StatusRowView: View {
viewModel.navigateToDetail()
}
.accessibilityActions {
if viewModel.showActions {
if viewModel.isFocused == false, viewModel.showActions {
accessibilityActions
}
}
@ -272,8 +277,9 @@ private struct CombinedAccessibilityLabel {
Text(hasSpoiler
? "status.editor.spoiler"
: ""
) +
imageAltText() + Text(", ") +
) + Text(", ") +
pollText() +
imageAltText() +
Text(viewModel.finalStatus.createdAt.relativeFormatted) + Text(", ") +
Text("status.summary.n-replies \(viewModel.finalStatus.repliesCount)") + Text(", ") +
Text("status.summary.n-boosts \(viewModel.finalStatus.reblogsCount)") + Text(", ") +
@ -308,11 +314,40 @@ private struct CombinedAccessibilityLabel {
.compactMap(\.description)
if descriptions.count == 1 {
return Text("accessibility.image.alt-text-\(descriptions[0])")
return Text("accessibility.image.alt-text-\(descriptions[0])") + Text(", ")
} else if descriptions.count > 1 {
return Text("accessibility.image.alt-text-\(descriptions[0])") + Text(", ") + Text("accessibility.image.alt-text-more.label")
return Text("accessibility.image.alt-text-\(descriptions[0])") + Text(", ") + Text("accessibility.image.alt-text-more.label") + Text(", ")
} else if viewModel.finalStatus.mediaAttachments.isEmpty == false {
let differentTypes = Set(viewModel.finalStatus.mediaAttachments.compactMap(\.localizedTypeDescription)).sorted()
return Text("accessibility.status.contains-media.label-\(ListFormatter.localizedString(byJoining: differentTypes))") + Text(", ")
} else {
return Text("")
}
}
func pollText() -> Text {
if let poll = viewModel.finalStatus.poll {
let showPercentage = poll.expired || poll.voted ?? false
let title: LocalizedStringKey = poll.expired
? "accessibility.status.poll.finished.label"
: "accessibility.status.poll.active.label"
return poll.options.enumerated().reduce(into: Text(title)) { text, pair in
let (index, option) = pair
let selected = poll.ownVotes?.contains(index) ?? false
let percentage = poll.safeVotersCount > 0
? Int(round(Double(option.votesCount) / Double(poll.safeVotersCount) * 100))
: 0
text = text +
Text(selected ? "accessibility.status.poll.selected.label" : "") +
Text(", ") +
Text("accessibility.status.poll.option-prefix-\(index + 1)-of-\(poll.options.count)") +
Text(", ") +
Text(option.title) +
Text(showPercentage ? ", \(percentage)%. " : ". ")
}
}
return Text("")
}
}

View File

@ -60,6 +60,8 @@ public struct StatusRowCardView: View {
}
}
.processors(processors)
// This image is decorative
.accessibilityHidden(true)
}
.frame(height: imageHeight)
}
@ -107,6 +109,9 @@ public struct StatusRowCardView: View {
Label("status.card.copy", systemImage: "doc.on.doc")
}
}
.accessibilityElement(children: .combine)
.accessibilityAddTraits(.isLink)
.accessibilityRemoveTraits(.isStaticText)
}
}
}

View File

@ -14,18 +14,27 @@ struct StatusRowDetailView: View {
Group {
Divider()
HStack {
Group {
Text(viewModel.status.createdAt.asDate, style: .date) +
Text("status.summary.at-time") +
Text(viewModel.status.createdAt.asDate, style: .time) +
Text(" ·")
Image(systemName: viewModel.status.visibility.iconName)
.accessibilityHidden(true)
}.accessibilityElement(children: .combine)
Spacer()
Text(viewModel.status.application?.name ?? "")
.underline()
.onTapGesture {
if let url = viewModel.status.application?.website {
if let name = viewModel.status.application?.name, let url = viewModel.status.application?.website {
Button {
openURL(url)
} label: {
Text(name)
.underline()
}
.buttonStyle(.plain)
.accessibilityLabel("accessibility.status.application.label")
.accessibilityValue(name)
.accessibilityAddTraits(.isLink)
.accessibilityRemoveTraits(.isButton)
}
}
.font(.scaledCaption)

View File

@ -23,8 +23,16 @@ struct StatusRowHeaderView: View {
contextMenuButton
}
}
.accessibilityElement()
.accessibilityLabel(Text("\(viewModel.finalStatus.account.safeDisplayName)"))
.accessibilityElement(children: .combine)
.accessibilityLabel(Text("\(viewModel.finalStatus.account.safeDisplayName)") + Text(", ") + Text(viewModel.finalStatus.createdAt.relativeFormatted))
.accessibilityAction {
viewModel.navigateToAccountDetail(account: viewModel.finalStatus.account)
}
.accessibilityActions {
if viewModel.isFocused {
StatusRowContextMenu(viewModel: viewModel)
}
}
}
@ViewBuilder

View File

@ -254,11 +254,13 @@ public struct StatusRowMediaPreviewView: View {
.cornerRadius(4)
}
}
.accessibilityAddTraits(.isImage)
case .gifv, .video, .audio:
if let url = attachment.url {
VideoPlayerView(viewModel: .init(url: url))
.frame(width: isCompact ? imageMaxHeight : proxy.frame(in: .local).width)
.frame(height: imageMaxHeight)
.accessibilityAddTraits(.startsMediaSession)
}
}
}
@ -274,7 +276,7 @@ public struct StatusRowMediaPreviewView: View {
}
.accessibilityElement(children: .combine)
.modifier(ConditionalAccessibilityLabelAltTextModifier(attachment: attachment))
.accessibilityAddTraits([.isButton, .isImage])
.accessibilityAddTraits(.isButton)
}
}
@ -343,6 +345,9 @@ private struct ConditionalAccessibilityLabelAltTextModifier: ViewModifier {
if let altText = attachment.description {
content
.accessibilityLabel("accessibility.image.alt-text-\(altText)")
} else if let typeDescription = attachment.localizedTypeDescription {
content
.accessibilityLabel(typeDescription)
} else {
content
}

View File

@ -101,14 +101,25 @@ public struct TimelineView: View {
}
}
.accessibilityRepresentation {
switch timeline {
case let .remoteLocal(_, filter):
if canFilterTimeline {
Menu(filter.localizedTitle()) {}
} else {
Text(filter.localizedTitle())
}
default:
if canFilterTimeline {
Menu(timeline.localizedTitle()) {}
} else {
Text(timeline.localizedTitle())
}
}
}
.accessibilityAddTraits(.isHeader)
.accessibilityRemoveTraits(.isButton)
.accessibilityRespondsToUserInteraction(canFilterTimeline)
}
}
.navigationBarTitleDisplayMode(.inline)
@ -217,5 +228,6 @@ public struct TimelineView: View {
.onDisappear {
viewModel.scrollToTopVisible = false
}
.accessibilityHidden(true)
}
}