feat: make status detail accessible
This commit is contained in:
parent
0f3764e3af
commit
ce80409ead
|
@ -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>
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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>(
|
||||
|
|
|
@ -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
|
||||
}()
|
||||
|
||||
|
|
|
@ -132,3 +132,4 @@ extension TrendCollectionViewCell {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -74,6 +74,13 @@ public final class MediaGridContainerView: UIView {
|
|||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
|
||||
public override var accessibilityElements: [Any]? {
|
||||
get {
|
||||
mediaViews
|
||||
}
|
||||
set { }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -70,6 +70,8 @@ extension SpoilerOverlayView {
|
|||
bottomPaddingView.setContentCompressionResistancePriority(.defaultLow - 100, for: .vertical)
|
||||
|
||||
spoilerMetaLabel.isUserInteractionEnabled = false
|
||||
|
||||
isAccessibilityElement = true
|
||||
}
|
||||
|
||||
public func setComponentHidden(_ isHidden: Bool) {
|
||||
|
|
Loading…
Reference in New Issue