feat: make status detail accessible

This commit is contained in:
CMK 2022-02-14 19:34:22 +08:00
parent 0f3764e3af
commit ce80409ead
16 changed files with 195 additions and 9 deletions

View File

@ -156,6 +156,28 @@
<string>%ld reblogs</string>
</dict>
</dict>
<key>plural.count.reply</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@reply_count@</string>
<key>reply_count</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>ld</string>
<key>zero</key>
<string>0 replies</string>
<key>one</key>
<string>1 reply</string>
<key>few</key>
<string>%ld replies</string>
<key>many</key>
<string>%ld replies</string>
<key>other</key>
<string>%ld replies</string>
</dict>
</dict>
<key>plural.count.vote</key>
<dict>
<key>NSStringLocalizedFormatKey</key>

View File

@ -130,6 +130,7 @@
"show_user_profile": "Show user profile",
"content_warning": "Content Warning",
"media_content_warning": "Tap anywhere to reveal",
"tap_to_reveal": "Tap to reveal",
"poll": {
"vote": "Vote",
"closed": "Closed"
@ -141,7 +142,11 @@
"favorite": "Favorite",
"unfavorite": "Unfavorite",
"menu": "Menu",
"hide": "Hide"
"hide": "Hide",
"show_image": "Show image",
"show_gif": "Show GIF",
"show_video_player": "Show video player",
"tap_then_hold_to_show_menu": "Tap then hold to show menu"
},
"tag": {
"url": "URL",
@ -440,6 +445,11 @@
"title": "Unblock Account",
"message": "Confirm to unblock %s"
}
},
"accessibility": {
"show_avatar_image": "Show avatar image",
"edit_avatar_image": "Edit avatar image",
"show_banner_image": "Show banner image"
}
},
"follower": {

View File

@ -21,8 +21,11 @@ extension SearchSection {
) -> UICollectionViewDiffableDataSource<SearchSection, SearchItem> {
let trendCellRegister = UICollectionView.CellRegistration<TrendCollectionViewCell, Mastodon.Entity.Tag> { cell, indexPath, item in
cell.primaryLabel.text = "#" + item.name
cell.secondaryLabel.text = L10n.Scene.Search.Recommend.HashTag.peopleTalking(item.talkingPeopleCount ?? 0)
let primaryLabelText = "#" + item.name
let secondaryLabelText = L10n.Scene.Search.Recommend.HashTag.peopleTalking(item.talkingPeopleCount ?? 0)
cell.primaryLabel.text = primaryLabelText
cell.secondaryLabel.text = secondaryLabelText
cell.lineChartView.data = (item.history ?? [])
.sorted(by: { $0.day < $1.day }) // latest last
@ -32,6 +35,12 @@ extension SearchSection {
}
return CGFloat(point)
}
cell.isAccessibilityElement = true
cell.accessibilityLabel = [
primaryLabelText,
secondaryLabelText
].joined(separator: ", ")
}
let dataSource = UICollectionViewDiffableDataSource<SearchSection, SearchItem>(

View File

@ -75,6 +75,7 @@ final class ProfileHeaderView: UIView {
let avatarButton: AvatarButton = {
let button = AvatarButton()
button.avatarImageView.configure(cornerConfiguration: .init(corner: .fixed(radius: 0)))
button.accessibilityLabel = "Avatar image" // FIXME: i18n
return button
}()

View File

@ -132,3 +132,4 @@ extension TrendCollectionViewCell {
}
}

View File

@ -23,6 +23,7 @@ final class TrendSectionHeaderCollectionReusableView: UICollectionReusableView {
label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 22, weight: .bold))
label.textColor = Asset.Colors.Label.primary.color
label.text = L10n.Scene.Search.Recommend.HashTag.title
label.numberOfLines = 0
return label
}()

View File

@ -79,8 +79,8 @@ extension StatusThreadRootTableViewCell {
statusView.delegate = self
// a11y
statusView.contentMetaText.textView.isSelectable = true
statusView.contentMetaText.textView.isAccessibilityElement = false
statusView.contentMetaText.textView.isSelectable = true
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
@ -91,6 +91,54 @@ extension StatusThreadRootTableViewCell {
}
extension StatusThreadRootTableViewCell {
override var accessibilityElements: [Any]? {
get {
var elements = [
statusView.headerContainerView,
statusView.avatarButton,
statusView.authorNameLabel,
statusView.menuButton,
statusView.authorUsernameLabel,
statusView.dateLabel,
statusView.contentSensitiveeToggleButton,
statusView.spoilerOverlayView,
statusView.contentMetaText.textView,
statusView.mediaGridContainerView,
statusView.pollTableView,
statusView.pollStatusStackView,
statusView.statusVisibilityView,
statusView.actionToolbarContainer,
statusView.statusMetricView
]
if !statusView.viewModel.isSensitive {
elements.removeAll(where: { $0 === statusView.contentSensitiveeToggleButton })
}
if statusView.viewModel.isContentReveal {
elements.removeAll(where: { $0 === statusView.spoilerOverlayView })
} else {
elements.removeAll(where: { $0 === statusView.contentMetaText.textView })
}
if statusView.statusVisibilityView.isHidden {
elements.removeAll(where: { $0 === statusView.statusVisibilityView })
}
if statusView.viewModel.pollItems.isEmpty {
elements.removeAll(where: { $0 === statusView.pollTableView })
elements.removeAll(where: { $0 === statusView.pollStatusStackView })
}
return elements
}
set { }
}
}
extension StatusThreadRootTableViewCell: AdaptiveContainerMarginTableViewCell {
var containerView: StatusView {
statusView

View File

@ -7,6 +7,7 @@
import os.log
import UIKit
import MastodonLocalization
open class AvatarButton: UIControl {
@ -37,6 +38,9 @@ open class AvatarButton: UIControl {
avatarImageView.trailingAnchor.constraint(equalTo: trailingAnchor),
avatarImageView.bottomAnchor.constraint(equalTo: bottomAnchor),
])
isAccessibilityElement = true
accessibilityLabel = L10n.Common.Controls.Status.showUserProfile
}
public override func layoutSubviews() {

View File

@ -74,6 +74,13 @@ public final class MediaGridContainerView: UIView {
super.init(coder: coder)
_init()
}
public override var accessibilityElements: [Any]? {
get {
mediaViews
}
set { }
}
}

View File

@ -104,6 +104,8 @@ extension MediaView {
extension MediaView {
private func _init() {
// lazy load content later
isAccessibilityElement = true
}
public func setup(configuration: Configuration) {
@ -115,13 +117,18 @@ extension MediaView {
case .image(let info):
layoutImage()
bindImage(configuration: configuration, info: info)
accessibilityLabel = "Show image" // TODO: i18n
case .gif(let info):
layoutGIF()
bindGIF(configuration: configuration, info: info)
accessibilityLabel = "Show GIF" // TODO: i18n
case .video(let info):
layoutVideo()
bindVideo(configuration: configuration, info: info)
accessibilityLabel = "Show video player" // TODO: i18n
}
accessibilityHint = "Tap then hold to show menu" // TODO: i18n
layoutBlurhash()
bindBlurhash(configuration: configuration)

View File

@ -50,6 +50,8 @@ extension PollOptionView {
@Published public var primaryStripProgressViewTintColor: UIColor = Asset.Colors.brandBlue.color
@Published public var secondaryStripProgressViewTintColor: UIColor = Asset.Colors.brandBlue.color.withAlphaComponent(0.5)
@Published public var groupedAccessibilityLabel = ""
init() {
// selectState
Publishers.CombineLatest3(
@ -136,9 +138,11 @@ extension PollOptionView.ViewModel {
.sink { metaContent in
guard let metaContent = metaContent else {
view.optionTextField.text = ""
view.optionTextField.accessibilityLabel = ""
return
}
view.optionTextField.text = metaContent.string
view.optionTextField.accessibilityLabel = metaContent.string
}
.store(in: &disposeBag)
// selectState
@ -175,6 +179,21 @@ extension PollOptionView.ViewModel {
}
}
.store(in: &disposeBag)
bindAccessibility(view: view)
}
private func bindAccessibility(view: PollOptionView) {
$selectState
.sink { selectState in
switch selectState {
case .on:
view.accessibilityTraits.insert(.selected)
default:
view.accessibilityTraits.remove(.selected)
}
}
.store(in: &disposeBag)
}
}

View File

@ -177,6 +177,26 @@ extension PollOptionView {
plusCircleImageView.isHidden = true
updateCornerRadius()
isAccessibilityElement = true
}
public override var accessibilityLabel: String? {
get {
switch viewModel.voteState {
case .reveal:
return [
optionTextField,
optionPercentageLabel
]
.compactMap { $0.accessibilityLabel }
.joined(separator: ", ")
case .hidden:
return optionTextField.accessibilityLabel
}
}
set { }
}
public override func layoutSubviews() {

View File

@ -698,6 +698,9 @@ extension StatusView.ViewModel {
if let spoilerContent = spoilerContent, !spoilerContent.string.isEmpty {
strings.append(L10n.Common.Controls.Status.contentWarning)
strings.append(spoilerContent.string)
// TODO: replace with "Tap to reveal"
strings.append(L10n.Common.Controls.Status.mediaContentWarning)
}
if isContentReveal {
@ -707,6 +710,21 @@ extension StatusView.ViewModel {
return strings.compactMap { $0 }.joined(separator: ", ")
}
$isContentReveal
.map { isContentReveal in
isContentReveal ? L10n.Scene.Compose.Accessibility.enableContentWarning : L10n.Scene.Compose.Accessibility.disableContentWarning
}
.sink { label in
statusView.contentSensitiveeToggleButton.accessibilityLabel = label
}
.store(in: &disposeBag)
contentAccessibilityLabel
.sink { contentAccessibilityLabel in
statusView.spoilerOverlayView.accessibilityLabel = contentAccessibilityLabel
}
.store(in: &disposeBag)
let meidaAccessibilityLabel = $mediaViewConfigurations
.map { configurations -> String? in
let count = configurations.count

View File

@ -61,7 +61,7 @@ public final class StatusView: UIView {
}()
// header
let headerContainerView = UIView()
public let headerContainerView = UIView()
// header icon
let headerIconImageView: UIImageView = {
@ -106,6 +106,7 @@ public final class StatusView: UIView {
button.tintColor = Asset.Colors.Label.secondary.color
let image = UIImage(systemName: "ellipsis", withConfiguration: UIImage.SymbolConfiguration(font: .systemFont(ofSize: 15)))
button.setImage(image, for: .normal)
button.accessibilityLabel = L10n.Common.Controls.Status.Actions.menu
return button
}()
@ -152,7 +153,7 @@ public final class StatusView: UIView {
}()
// content warning
let spoilerOverlayView = SpoilerOverlayView()
public let spoilerOverlayView = SpoilerOverlayView()
// media
public let mediaContainerView = UIView()
@ -173,7 +174,7 @@ public final class StatusView: UIView {
public var pollTableViewHeightLayoutConstraint: NSLayoutConstraint!
public var pollTableViewDiffableDataSource: UITableViewDiffableDataSource<PollSection, PollItem>?
let pollStatusStackView = UIStackView()
public let pollStatusStackView = UIStackView()
let pollVoteCountLabel: UILabel = {
let label = UILabel()
label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 12, weight: .regular))
@ -186,6 +187,7 @@ public final class StatusView: UIView {
label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 12, weight: .regular))
label.textColor = Asset.Colors.Label.secondary.color
label.text = " · "
label.isAccessibilityElement = false
return label
}()
let pollCountdownLabel: UILabel = {
@ -542,7 +544,7 @@ extension StatusView.Style {
// pollTableView
statusView.pollContainerView.addArrangedSubview(statusView.pollTableView)
// pollStatusStackView
// pollStatusStackView: H - [ pollVoteCountLabel | pollCountdownLabel | pollVoteButton ]
statusView.pollStatusStackView.axis = .horizontal
statusView.pollContainerView.addArrangedSubview(statusView.pollStatusStackView)

View File

@ -93,7 +93,7 @@ extension ActionToolbarContainer {
replyButton.accessibilityLabel = L10n.Common.Controls.Status.Actions.reply
reblogButton.accessibilityLabel = L10n.Common.Controls.Status.Actions.reblog // needs update to follow state
favoriteButton.accessibilityLabel = L10n.Common.Controls.Status.Actions.favorite // needs update to follow state
shareButton.accessibilityLabel = L10n.Common.Controls.Status.Actions.menu
shareButton.accessibilityLabel = L10n.Common.Controls.Actions.share
switch style {
case .inline:
@ -219,6 +219,7 @@ extension ActionToolbarContainer {
public func configureReply(count: Int, isEnabled: Bool) {
let title = ActionToolbarContainer.title(from: count)
replyButton.setTitle(title, for: .normal)
replyButton.accessibilityLabel = "\(count) reply" // TODO: i18n
}
public func configureReblog(count: Int, isEnabled: Bool, isHighlighted: Bool) {
@ -230,6 +231,13 @@ extension ActionToolbarContainer {
reblogButton.tintColor = tintColor
reblogButton.setTitleColor(tintColor, for: .normal)
reblogButton.setTitleColor(tintColor, for: .highlighted)
if isHighlighted {
reblogButton.accessibilityTraits.insert(.selected)
} else {
reblogButton.accessibilityTraits.remove(.selected)
}
reblogButton.accessibilityLabel = L10n.Plural.Count.reblog(count)
}
public func configureFavorite(count: Int, isEnabled: Bool, isHighlighted: Bool) {
@ -242,6 +250,13 @@ extension ActionToolbarContainer {
favoriteButton.tintColor = tintColor
favoriteButton.setTitleColor(tintColor, for: .normal)
favoriteButton.setTitleColor(tintColor, for: .highlighted)
if isHighlighted {
favoriteButton.accessibilityTraits.insert(.selected)
} else {
favoriteButton.accessibilityTraits.remove(.selected)
}
favoriteButton.accessibilityLabel = L10n.Plural.Count.favorite(count)
}
}

View File

@ -70,6 +70,8 @@ extension SpoilerOverlayView {
bottomPaddingView.setContentCompressionResistancePriority(.defaultLow - 100, for: .vertical)
spoilerMetaLabel.isUserInteractionEnabled = false
isAccessibilityElement = true
}
public func setComponentHidden(_ isHidden: Bool) {