diff --git a/Extensions/Attachment+Extensions.swift b/Extensions/Attachment+Extensions.swift new file mode 100644 index 0000000..e2b5df2 --- /dev/null +++ b/Extensions/Attachment+Extensions.swift @@ -0,0 +1,40 @@ +// Copyright © 2021 Metabolist. All rights reserved. + +import Foundation +import Mastodon + +extension Attachment.AttachmentType { + var accessibilityName: String { + switch self { + case .image, .gifv: + return NSLocalizedString("attachment.type.image", comment: "") + case .video: + return NSLocalizedString("attachment.type.video", comment: "") + case .audio: + return NSLocalizedString("attachment.type.audio", comment: "") + case .unknown: + return NSLocalizedString("attachment.type.unknown", comment: "") + } + } + + func accessibilityNames(count: Int) -> String { + if count == 1 { + return accessibilityName + } + + let format: String + + switch self { + case .image, .gifv: + format = NSLocalizedString("attachment.type.images-%ld", comment: "") + case .video: + format = NSLocalizedString("attachment.type.videos-%ld", comment: "") + case .audio: + format = NSLocalizedString("attachment.type.audios-%ld", comment: "") + case .unknown: + format = NSLocalizedString("attachment.type.unknowns-%ld", comment: "") + } + + return String.localizedStringWithFormat(format, count) + } +} diff --git a/Extensions/NSMutableAttributedString+Extensions.swift b/Extensions/NSMutableAttributedString+Extensions.swift index fde74ea..ddf1f78 100644 --- a/Extensions/NSMutableAttributedString+Extensions.swift +++ b/Extensions/NSMutableAttributedString+Extensions.swift @@ -25,4 +25,13 @@ extension NSMutableAttributedString { attachment.bounds = CGRect(x: 0, y: lineHeight * -0.25, width: lineHeight, height: lineHeight) } } + + func appendWithSeparator(_ string: NSAttributedString) { + append(.init(string: .separator)) + append(string) + } + + func appendWithSeparator(_ string: String) { + appendWithSeparator(.init(string: string)) + } } diff --git a/Extensions/String+Extensions.swift b/Extensions/String+Extensions.swift index 343bb3f..d34fa89 100644 --- a/Extensions/String+Extensions.swift +++ b/Extensions/String+Extensions.swift @@ -4,6 +4,10 @@ import Mastodon import UIKit extension String { + static var separator: Self { + (Locale.autoupdatingCurrent.groupingSeparator ?? ",").appending(" ") + } + func height(width: CGFloat, font: UIFont) -> CGFloat { (self as NSString).boundingRect( with: CGSize(width: width, height: .greatestFiniteMagnitude), @@ -61,9 +65,3 @@ extension String { append(Self.separator.appending(string)) } } - -private extension String { - static var separator: Self { - Locale.autoupdatingCurrent.groupingSeparator ?? "," - } -} diff --git a/Localizations/Localizable.strings b/Localizations/Localizable.strings index b9b6155..3fecc9f 100644 --- a/Localizations/Localizable.strings +++ b/Localizations/Localizable.strings @@ -49,14 +49,26 @@ "attachment.edit.thumbnail.prompt" = "Drag the circle on the preview to choose the focal point which will always be in view on all thumbnails"; "attachment.sensitive-content" = "Sensitive content"; "attachment.media-hidden" = "Media hidden"; +"attachment.type.image" = "Image"; +"attachment.type.images-%ld" = "%ld images"; +"attachment.type.audio" = "Audio"; +"attachment.type.audios-%ld" = "%ld audio files"; +"attachment.type.video" = "Video"; +"attachment.type.videos-%ld" = "%ld videos"; +"attachment.type.unknown" = "Attachment"; +"attachment.type.unknown-%ld" = "%ld attachments"; "attachment.unable-to-export-media" = "Unable to export media"; "bookmarks" = "Bookmarks"; +"card.link.accessibility-label" = "Link"; "camera-access.title" = "Camera access needed"; "camera-access.description" = "Open system settings to allow camera access"; "camera-access.open-system-settings" = "Open system settings"; "cancel" = "Cancel"; "compose.add-button-accessibility-label.post" = "Add another post"; "compose.add-button-accessibility-label.toot" = "Add another toot"; +"compose.attachment.cancel-upload.accessibility-label" = "Cancel uploading attachment"; +"compose.attachment.edit" = "Edit attachment"; +"compose.attachment.remove" = "Remove attachment"; "compose.attachment.uploading" = "Uploading"; "compose.attachments-button.accessibility-label" = "Add attachment"; "compose.attachments-will-be-discarded" = "Attachments will be discarded when changing accounts"; @@ -68,9 +80,9 @@ "compose.emoji-button" = "Emoji picker"; "compose.mark-media-sensitive" = "Mark media as sensitive"; "compose.photo-library" = "Photo Library"; +"compose.poll.accessibility.multiple-choices-allowed" = "Mutliple choices allowed"; "compose.poll.add-choice" = "Add a choice"; "compose.poll.allow-multiple-choices" = "Allow multiple choices"; -"compose.poll.option-%ld" = "Option %ld"; "compose.poll-button.accessibility-label" = "Add a poll"; "compose.prompt" = "What's on your mind?"; "compose.take-photo-or-video" = "Take Photo or Video"; @@ -221,6 +233,9 @@ "share-extension-error.no-account-found" = "No account found"; "status.bookmark" = "Bookmark"; "status.content-warning-abbreviation" = "CW"; +"status.content-warning.accessibility" = "Content warning"; +"status.content-warning.accessibility.closed" = "Closed"; +"status.content-warning.accessibility.opened" = "Opened"; "status.delete" = "Delete"; "status.delete.confirm.post" = "Are you sure you want to delete this post?"; "status.delete.confirm.toot" = "Are you sure you want to delete this toot?"; @@ -231,6 +246,8 @@ "status.pin" = "Pin on profile"; "status.pinned.post" = "Pinned post"; "status.pinned.toot" = "Pinned toot"; +"status.poll.accessibility-label" = "Poll"; +"status.poll.option-%ld" = "Option %ld"; "status.poll.vote" = "Vote"; "status.poll.time-left" = "%@ left"; "status.poll.refresh" = "Refresh"; diff --git a/Metatext.xcodeproj/project.pbxproj b/Metatext.xcodeproj/project.pbxproj index d664768..d3d5220 100644 --- a/Metatext.xcodeproj/project.pbxproj +++ b/Metatext.xcodeproj/project.pbxproj @@ -13,6 +13,10 @@ D00702362555F4C500F38136 /* ConversationContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00702352555F4C500F38136 /* ConversationContentConfiguration.swift */; }; D007023E25562A2800F38136 /* ConversationAvatarsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D007023D25562A2800F38136 /* ConversationAvatarsView.swift */; }; D0070252255921B100F38136 /* AccountFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0070251255921B100F38136 /* AccountFieldView.swift */; }; + D00CB22A25C92C0F008EF267 /* Attachment+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00CB22925C92C0F008EF267 /* Attachment+Extensions.swift */; }; + D00CB23325C92F2D008EF267 /* Attachment+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00CB22925C92C0F008EF267 /* Attachment+Extensions.swift */; }; + D00CB23825C93047008EF267 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D46A24F76169001EBDBB /* String+Extensions.swift */; }; + D00CB23D25C9305D008EF267 /* NSMutableAttributedString+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D46B24F76169001EBDBB /* NSMutableAttributedString+Extensions.swift */; }; D00CB2ED2533ACC00080096B /* StatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00CB2EC2533ACC00080096B /* StatusView.swift */; }; D015B13525A812DD006D88A8 /* AttachmentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41E224F8889700D55A2D /* AttachmentsView.swift */; }; D015B13A25A812E6006D88A8 /* AttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB1F224F8EE8C001B0F04 /* AttachmentView.swift */; }; @@ -227,6 +231,7 @@ D00702352555F4C500F38136 /* ConversationContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationContentConfiguration.swift; sourceTree = ""; }; D007023D25562A2800F38136 /* ConversationAvatarsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationAvatarsView.swift; sourceTree = ""; }; D0070251255921B100F38136 /* AccountFieldView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFieldView.swift; sourceTree = ""; }; + D00CB22925C92C0F008EF267 /* Attachment+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Attachment+Extensions.swift"; sourceTree = ""; }; D00CB2EC2533ACC00080096B /* StatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusView.swift; sourceTree = ""; }; D01EF22325182B1F00650C6B /* AccountHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountHeaderView.swift; sourceTree = ""; }; D01F41D624F880C400D55A2D /* TouchFallthroughTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TouchFallthroughTextView.swift; sourceTree = ""; }; @@ -714,6 +719,7 @@ D0C7D46824F76169001EBDBB /* Extensions */ = { isa = PBXGroup; children = ( + D00CB22925C92C0F008EF267 /* Attachment+Extensions.swift */, D05E688425B55AE8001FB2C6 /* AVURLAsset+Extensions.swift */, D0F0B135251AA12700942152 /* CollectionItem+Extensions.swift */, D0D2AC3825BBEC0F003D5DF2 /* CollectionSection+Extensions.swift */, @@ -1028,6 +1034,7 @@ D021A61425C36BFB008A0C0D /* IdentityView.swift in Sources */, D06BC5E625202AD90079541D /* ProfileViewController.swift in Sources */, D0D2AC4D25BCD2A9003D5DF2 /* TagTableViewCell.swift in Sources */, + D00CB22A25C92C0F008EF267 /* Attachment+Extensions.swift in Sources */, D0D2AC5325BCD2BA003D5DF2 /* TagContentConfiguration.swift in Sources */, D08B8D72254246E200B1EBEF /* PollView.swift in Sources */, D035F8A925B9155900DC75ED /* NewStatusButtonView.swift in Sources */, @@ -1098,10 +1105,12 @@ buildActionMask = 2147483647; files = ( D08E52A6257C61C000FA2C5F /* ShareExtensionNavigationViewController.swift in Sources */, + D00CB23825C93047008EF267 /* String+Extensions.swift in Sources */, D059373425AAEA7000754FDF /* CompositionPollView.swift in Sources */, D021A67B25C3E32A008A0C0D /* PlayerView.swift in Sources */, D021A69025C3E4B8008A0C0D /* EmojiContentConfiguration.swift in Sources */, D08E52D2257C811200FA2C5F /* ShareExtensionError+Extensions.swift in Sources */, + D00CB23D25C9305D008EF267 /* NSMutableAttributedString+Extensions.swift in Sources */, D0E9F9AB258450B300EF503D /* CompositionInputAccessoryView.swift in Sources */, D025B14E25C4E482001C69A8 /* ImageCacheConfiguration.swift in Sources */, D05936D025A8D79800754FDF /* EditAttachmentViewController.swift in Sources */, @@ -1123,6 +1132,7 @@ D0FCC106259C4E62000B67DF /* NewStatusViewController.swift in Sources */, D036EBB3259FE28800EC1CFC /* UIColor+Extensions.swift in Sources */, D088406E25AFBBE200BB749B /* EmojiPickerViewController.swift in Sources */, + D00CB23325C92F2D008EF267 /* Attachment+Extensions.swift in Sources */, D025B14725C4D26B001C69A8 /* ImageCacheSerializer.swift in Sources */, D036EBB8259FE29800EC1CFC /* Status+Extensions.swift in Sources */, D021A6A625C3E584008A0C0D /* EditAttachmentView.swift in Sources */, diff --git a/View Controllers/ImageViewController.swift b/View Controllers/ImageViewController.swift index fae40fc..f53db52 100644 --- a/View Controllers/ImageViewController.swift +++ b/View Controllers/ImageViewController.swift @@ -2,6 +2,7 @@ import AVFoundation import Kingfisher +import Mastodon import UIKit import ViewModels @@ -133,11 +134,21 @@ final class ImageViewController: UIViewController { player.play() default: break } + + var accessibilityLabel = viewModel.attachment.type.accessibilityName + + if let description = viewModel.attachment.description { + accessibilityLabel.appendWithSeparator(description) + } } else if let imageURL = imageURL { imageView.tag = imageURL.hashValue playerView.isHidden = true imageView.kf.setImage(with: imageURL) } + + contentView.accessibilityLabel = viewModel?.attachment.type.accessibilityName + ?? Attachment.AttachmentType.image.accessibilityName + contentView.isAccessibilityElement = true } } diff --git a/Views/UIKit/AttachmentUploadView.swift b/Views/UIKit/AttachmentUploadView.swift index d46cb13..b4a3092 100644 --- a/Views/UIKit/AttachmentUploadView.swift +++ b/Views/UIKit/AttachmentUploadView.swift @@ -13,6 +13,7 @@ final class AttachmentUploadView: UIView { private var progressCancellable: AnyCancellable? private var cancellables = Set() + // swiftlint:disable:next function_body_length init(viewModel: CompositionViewModel) { self.viewModel = viewModel @@ -33,6 +34,8 @@ final class AttachmentUploadView: UIView { cancelButton.titleLabel?.font = .preferredFont(forTextStyle: .callout) cancelButton.setTitle(NSLocalizedString("cancel", comment: ""), for: .normal) cancelButton.addAction(UIAction { _ in viewModel.cancelUpload() }, for: .touchUpInside) + cancelButton.accessibilityLabel = + NSLocalizedString("compose.attachment.cancel-upload.accessibility-label", comment: "") addSubview(progressView) progressView.translatesAutoresizingMaskIntoConstraints = false diff --git a/Views/UIKit/AttachmentView.swift b/Views/UIKit/AttachmentView.swift index 84f3229..7d54087 100644 --- a/Views/UIKit/AttachmentView.swift +++ b/Views/UIKit/AttachmentView.swift @@ -130,6 +130,7 @@ private extension AttachmentView { addSubview(selectionButton) selectionButton.translatesAutoresizingMaskIntoConstraints = false selectionButton.setBackgroundImage(.highlightedButtonBackground, for: .highlighted) + selectionButton.accessibilityLabel = NSLocalizedString("compose.attachment.edit", comment: "") selectionButton.addAction( UIAction { [weak self] _ in guard let self = self else { return } @@ -141,6 +142,7 @@ private extension AttachmentView { addSubview(removeButton) removeButton.translatesAutoresizingMaskIntoConstraints = false removeButton.showsMenuAsPrimaryAction = true + removeButton.accessibilityLabel = NSLocalizedString("compose.attachment.remove", comment: "") removeButton.menu = UIMenu( children: [ UIAction( @@ -210,5 +212,13 @@ private extension AttachmentView { editIcon.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor), editIcon.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor) ]) + + var accessibilityLabel = viewModel.attachment.type.accessibilityName + + if let description = viewModel.attachment.description { + accessibilityLabel.appendWithSeparator(description) + } + + self.accessibilityLabel = accessibilityLabel } } diff --git a/Views/UIKit/AttachmentsView.swift b/Views/UIKit/AttachmentsView.swift index 8331085..ca98a16 100644 --- a/Views/UIKit/AttachmentsView.swift +++ b/Views/UIKit/AttachmentsView.swift @@ -34,6 +34,7 @@ final class AttachmentsView: UIView { attachmentView.playing = viewModel.shouldShowAttachments && attachmentViewModel.shouldAutoplay attachmentView.removeButton.isHidden = !viewModel.canRemoveAttachments attachmentView.editIcon.isHidden = !viewModel.canRemoveAttachments + attachmentView.isAccessibilityElement = !viewModel.canRemoveAttachments if viewModel.attachmentViewModels.count == 2 && index == 1 || viewModel.attachmentViewModels.count == 3 && index != 0 @@ -66,6 +67,25 @@ final class AttachmentsView: UIView { comment: ""), for: .normal) hideButtonBackground.isHidden = !viewModel.shouldShowHideAttachmentsButton + + if curtain.isHidden { + let type: Attachment.AttachmentType + + if viewModel.attachmentViewModels + .allSatisfy({ $0.attachment.type == .image || $0.attachment.type == .gifv }) { + type = .image + } else if viewModel.attachmentViewModels.allSatisfy({ $0.attachment.type == .video }) { + type = .video + } else if viewModel.attachmentViewModels.allSatisfy({ $0.attachment.type == .audio }) { + type = .audio + } else { + type = .unknown + } + + accessibilityLabel = type.accessibilityNames(count: viewModel.attachmentViewModels.count) + } else { + accessibilityLabel = curtainButton.title(for: .normal) + } } } diff --git a/Views/UIKit/CardView.swift b/Views/UIKit/CardView.swift index f9c8b10..3eca6d7 100644 --- a/Views/UIKit/CardView.swift +++ b/Views/UIKit/CardView.swift @@ -16,6 +16,9 @@ final class CardView: UIView { didSet { guard let viewModel = viewModel else { return } + var accessibilityLabel = NSLocalizedString("card.link.accessibility-label", comment: "") + .appendingWithSeparator(viewModel.title) + imageView.isHidden = viewModel.imageURL == nil imageView.kf.setImage(with: viewModel.imageURL) @@ -30,6 +33,12 @@ final class CardView: UIView { } else { urlLabel.text = viewModel.url.host } + + if let urlLabelText = urlLabel.text { + accessibilityLabel.appendWithSeparator(urlLabelText) + } + + self.accessibilityLabel = accessibilityLabel } } @@ -37,6 +46,7 @@ final class CardView: UIView { super.init(frame: frame) initialSetup() + setupAccessibility() } @available(*, unavailable) @@ -120,4 +130,8 @@ private extension CardView { imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor) ]) } + + func setupAccessibility() { + isAccessibilityElement = true + } } diff --git a/Views/UIKit/CompositionPollView.swift b/Views/UIKit/CompositionPollView.swift index 686d956..ef42fe5 100644 --- a/Views/UIKit/CompositionPollView.swift +++ b/Views/UIKit/CompositionPollView.swift @@ -124,7 +124,7 @@ private extension CompositionPollView { option: option) optionView.textField.placeholder = String.localizedStringWithFormat( - NSLocalizedString("compose.poll.option-%ld", comment: ""), + NSLocalizedString("status.poll.option-%ld", comment: ""), index + 1) self.stackView.insertArrangedSubview(optionView, at: index) } diff --git a/Views/UIKit/Content Views/StatusView.swift b/Views/UIKit/Content Views/StatusView.swift index 2ec116c..af46938 100644 --- a/Views/UIKit/Content Views/StatusView.swift +++ b/Views/UIKit/Content Views/StatusView.swift @@ -1,6 +1,7 @@ // Copyright © 2020 Metabolist. All rights reserved. // swiftlint:disable file_length +import Combine import Kingfisher import Mastodon import UIKit @@ -43,6 +44,7 @@ final class StatusView: UIView { private let inReplyToView = UIView() private let hasReplyFollowingView = UIView() private var statusConfiguration: StatusContentConfiguration + private var cancellables = Set() init(configuration: StatusContentConfiguration) { self.statusConfiguration = configuration @@ -327,6 +329,12 @@ private extension StatusView { greaterThanOrEqualToConstant: .minimumButtonDimension / 2), interactionsStackView.heightAnchor.constraint(greaterThanOrEqualToConstant: .minimumButtonDimension) ]) + + + + NotificationCenter.default.publisher(for: UIAccessibility.voiceOverStatusDidChangeNotification) + .sink { [weak self] _ in self?.configureUserInteractionEnabledForAccessibility() } + .store(in: &cancellables) } func applyStatusConfiguration() { @@ -474,6 +482,18 @@ private extension StatusView { shareButton.tag = viewModel.sharingURL?.hashValue ?? 0 menuButton.isEnabled = isAuthenticated + + isAccessibilityElement = !viewModel.configuration.isContextParent + + let accessibilityAttributedLabel = NSMutableAttributedString(attributedString: mutableDisplayName) + + if let bodyAccessibilityAttributedLabel = bodyView.accessibilityAttributedLabel { + accessibilityAttributedLabel.appendWithSeparator(bodyAccessibilityAttributedLabel) + } + + self.accessibilityAttributedLabel = accessibilityAttributedLabel + + configureUserInteractionEnabledForAccessibility() } // swiftlint:enable function_body_length @@ -616,6 +636,11 @@ private extension StatusView { statusConfiguration.viewModel.toggleFavorited() } + + func configureUserInteractionEnabledForAccessibility() { + isUserInteractionEnabled = !UIAccessibility.isVoiceOverRunning + || statusConfiguration.viewModel.configuration.isContextParent + } } private extension UIButton { diff --git a/Views/UIKit/PollResultView.swift b/Views/UIKit/PollResultView.swift index 13dd669..5c3c2e1 100644 --- a/Views/UIKit/PollResultView.swift +++ b/Views/UIKit/PollResultView.swift @@ -4,10 +4,10 @@ import Mastodon import UIKit final class PollResultView: UIView { + let titleLabel = UILabel() + let percentLabel = UILabel() private let verticalStackView = UIStackView() private let horizontalStackView = UIStackView() - private let titleLabel = UILabel() - private let percentLabel = UILabel() private let percentView = UIProgressView() init(option: Poll.Option, emojis: [Emoji], selected: Bool, multipleSelection: Bool, votersCount: Int) { diff --git a/Views/UIKit/PollView.swift b/Views/UIKit/PollView.swift index 8fb3b2e..7bd509e 100644 --- a/Views/UIKit/PollView.swift +++ b/Views/UIKit/PollView.swift @@ -29,6 +29,9 @@ final class PollView: UIView { return } + let accessibilityAttributedLabel = NSMutableAttributedString( + string: NSLocalizedString("status.poll.accessibility-label", comment: "")) + if !viewModel.isPollExpired, !viewModel.hasVotedInPoll { for (index, option) in viewModel.pollOptions.enumerated() { let button = PollOptionButton( @@ -63,6 +66,41 @@ final class PollView: UIView { } } + for (index, view) in stackView.arrangedSubviews.enumerated() { + var title: NSAttributedString? + var percent: String? + let indexLabel = String.localizedStringWithFormat( + NSLocalizedString("status.poll.option-%ld", comment: ""), + index + 1) + + if let optionView = view as? PollOptionButton, + let attributedTitle = optionView.attributedTitle(for: .normal) { + title = attributedTitle + + let optionAccessibilityAttributedLabel = NSMutableAttributedString(string: indexLabel) + + if viewModel.isPollMultipleSelection { + optionAccessibilityAttributedLabel.appendWithSeparator( + NSLocalizedString("compose.poll.accessibility.multiple-choices-allowed", comment: "")) + } + + optionAccessibilityAttributedLabel.appendWithSeparator(attributedTitle) + optionView.accessibilityAttributedLabel = optionAccessibilityAttributedLabel + } else if let resultView = view as? PollResultView { + title = resultView.titleLabel.attributedText + percent = resultView.percentLabel.text + } + + guard let presentTitle = title else { continue } + + accessibilityAttributedLabel.appendWithSeparator(indexLabel) + accessibilityAttributedLabel.appendWithSeparator(presentTitle) + + if let percent = percent { + accessibilityAttributedLabel.appendWithSeparator(percent) + } + } + if !viewModel.isPollExpired, !viewModel.hasVotedInPoll { stackView.addArrangedSubview(voteButton) @@ -81,21 +119,43 @@ final class PollView: UIView { stackView.addArrangedSubview(bottomStackView) - votesCountLabel.text = String.localizedStringWithFormat( + let votesCount = String.localizedStringWithFormat( NSLocalizedString("status.poll.participation-count", comment: ""), viewModel.pollVotersCount) + votesCountLabel.text = votesCount + votesCountLabel.isAccessibilityElement = true + votesCountLabel.accessibilityLabel = votesCountLabel.text + accessibilityAttributedLabel.appendWithSeparator(votesCount) + if !viewModel.isPollExpired, let pollTimeLeft = viewModel.pollTimeLeft { expiryLabel.text = String.localizedStringWithFormat( NSLocalizedString("status.poll.time-left", comment: ""), pollTimeLeft) refreshButton.isHidden = false + accessibilityCustomActions = + [UIAccessibilityCustomAction( + name: NSLocalizedString("status.poll.refresh", comment: "")) { [weak self] _ in + self?.viewModel?.refreshPoll() + + return true + }] } else { expiryLabel.text = NSLocalizedString("status.poll.closed", comment: "") refreshButton.isHidden = true + accessibilityCustomActions = nil + } + + expiryLabel.isAccessibilityElement = true + expiryLabel.accessibilityLabel = expiryLabel.text + + if let expiry = expiryLabel.text { + accessibilityAttributedLabel.appendWithSeparator(expiry) } refreshDividerLabel.isHidden = refreshButton.isHidden + + self.accessibilityAttributedLabel = accessibilityAttributedLabel } } diff --git a/Views/UIKit/StatusBodyView.swift b/Views/UIKit/StatusBodyView.swift index 79d79be..5e2731e 100644 --- a/Views/UIKit/StatusBodyView.swift +++ b/Views/UIKit/StatusBodyView.swift @@ -52,9 +52,45 @@ final class StatusBodyView: UIView { pollView.isHidden = viewModel.pollOptions.isEmpty || !viewModel.shouldShowContent pollView.viewModel = viewModel + pollView.isAccessibilityElement = !isContextParent || viewModel.hasVotedInPoll || viewModel.isPollExpired cardView.viewModel = viewModel.cardViewModel cardView.isHidden = viewModel.cardViewModel == nil + + let accessibilityAttributedLabel = NSMutableAttributedString(string: "") + + if !spoilerTextLabel.isHidden { + accessibilityAttributedLabel.append(mutableSpoilerText) + } + + if !toggleShowContentButton.isHidden { + accessibilityAttributedLabel.appendWithSeparator( + NSLocalizedString("status.content-warning.accessibility", comment: "")) + + if viewModel.shouldShowContent { + accessibilityAttributedLabel.appendWithSeparator( + NSLocalizedString("status.content-warning.accessibility.opened", comment: "")) + } else { + accessibilityAttributedLabel.appendWithSeparator( + NSLocalizedString("status.content-warning.accessibility.closed", comment: "")) + } + } + + if !contentTextView.isHidden { + if spoilerTextLabel.isHidden { + accessibilityAttributedLabel.append(mutableContent) + } else { + accessibilityAttributedLabel.appendWithSeparator(mutableContent) + } + } + + for view in [attachmentsView, pollView, cardView] where !view.isHidden { + guard let viewAccessibilityLabel = view.accessibilityLabel else { continue } + + accessibilityAttributedLabel.appendWithSeparator(viewAccessibilityLabel) + } + + self.accessibilityLabel = accessibilityAttributedLabel.string } } diff --git a/Views/UIKit/Table View Cells/StatusTableViewCell.swift b/Views/UIKit/Table View Cells/StatusTableViewCell.swift index 32956f2..9bf8366 100644 --- a/Views/UIKit/Table View Cells/StatusTableViewCell.swift +++ b/Views/UIKit/Table View Cells/StatusTableViewCell.swift @@ -10,6 +10,7 @@ final class StatusTableViewCell: UITableViewCell { guard let viewModel = viewModel else { return } contentConfiguration = StatusContentConfiguration(viewModel: viewModel).updated(for: state) + accessibilityElements = [contentView] } override func layoutSubviews() {