diff --git a/Localization/Localizable.stringsdict b/Localization/Localizable.stringsdict
index ce358b439..4b9a12762 100644
--- a/Localization/Localizable.stringsdict
+++ b/Localization/Localizable.stringsdict
@@ -156,6 +156,28 @@
%ld reblogs
+ plural.count.reply
+
+ NSStringLocalizedFormatKey
+ %#@reply_count@
+ reply_count
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0 replies
+ one
+ 1 reply
+ few
+ %ld replies
+ many
+ %ld replies
+ other
+ %ld replies
+
+
plural.count.vote
NSStringLocalizedFormatKey
diff --git a/Localization/app.json b/Localization/app.json
index ad99e178d..fad7cc206 100644
--- a/Localization/app.json
+++ b/Localization/app.json
@@ -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": {
diff --git a/Mastodon/Diffiable/Search/SearchSection.swift b/Mastodon/Diffiable/Search/SearchSection.swift
index 38c87a76e..21f1d479c 100644
--- a/Mastodon/Diffiable/Search/SearchSection.swift
+++ b/Mastodon/Diffiable/Search/SearchSection.swift
@@ -21,8 +21,11 @@ extension SearchSection {
) -> UICollectionViewDiffableDataSource {
let trendCellRegister = UICollectionView.CellRegistration { 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(
diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift
index 0c1b0423e..aadb0d70f 100644
--- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift
+++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift
@@ -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
}()
diff --git a/Mastodon/Scene/Search/Search/Cell/TrendCollectionViewCell.swift b/Mastodon/Scene/Search/Search/Cell/TrendCollectionViewCell.swift
index 48a7606b5..a43d65df4 100644
--- a/Mastodon/Scene/Search/Search/Cell/TrendCollectionViewCell.swift
+++ b/Mastodon/Scene/Search/Search/Cell/TrendCollectionViewCell.swift
@@ -132,3 +132,4 @@ extension TrendCollectionViewCell {
}
}
+
diff --git a/Mastodon/Scene/Search/Search/Cell/TrendSectionHeaderCollectionReusableView.swift b/Mastodon/Scene/Search/Search/Cell/TrendSectionHeaderCollectionReusableView.swift
index 4632f384c..9d21ee287 100644
--- a/Mastodon/Scene/Search/Search/Cell/TrendSectionHeaderCollectionReusableView.swift
+++ b/Mastodon/Scene/Search/Search/Cell/TrendSectionHeaderCollectionReusableView.swift
@@ -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
}()
diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusThreadRootTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusThreadRootTableViewCell.swift
index e16e4f289..d971cada5 100644
--- a/Mastodon/Scene/Share/View/TableviewCell/StatusThreadRootTableViewCell.swift
+++ b/Mastodon/Scene/Share/View/TableviewCell/StatusThreadRootTableViewCell.swift
@@ -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
diff --git a/MastodonSDK/Sources/MastodonUI/View/Button/AvatarButton.swift b/MastodonSDK/Sources/MastodonUI/View/Button/AvatarButton.swift
index 3854c4700..57257fd89 100644
--- a/MastodonSDK/Sources/MastodonUI/View/Button/AvatarButton.swift
+++ b/MastodonSDK/Sources/MastodonUI/View/Button/AvatarButton.swift
@@ -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() {
diff --git a/MastodonSDK/Sources/MastodonUI/View/Container/MediaGridContainerView.swift b/MastodonSDK/Sources/MastodonUI/View/Container/MediaGridContainerView.swift
index 41d97c0f2..cb9c53f35 100644
--- a/MastodonSDK/Sources/MastodonUI/View/Container/MediaGridContainerView.swift
+++ b/MastodonSDK/Sources/MastodonUI/View/Container/MediaGridContainerView.swift
@@ -74,6 +74,13 @@ public final class MediaGridContainerView: UIView {
super.init(coder: coder)
_init()
}
+
+ public override var accessibilityElements: [Any]? {
+ get {
+ mediaViews
+ }
+ set { }
+ }
}
diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/MediaView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/MediaView.swift
index 68847e747..f4231f02c 100644
--- a/MastodonSDK/Sources/MastodonUI/View/Content/MediaView.swift
+++ b/MastodonSDK/Sources/MastodonUI/View/Content/MediaView.swift
@@ -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)
diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/PollOptionView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/PollOptionView+ViewModel.swift
index ff458e7ad..e25e5d0a8 100644
--- a/MastodonSDK/Sources/MastodonUI/View/Content/PollOptionView+ViewModel.swift
+++ b/MastodonSDK/Sources/MastodonUI/View/Content/PollOptionView+ViewModel.swift
@@ -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)
}
}
diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/PollOptionView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/PollOptionView.swift
index d56ac06e9..df000233c 100644
--- a/MastodonSDK/Sources/MastodonUI/View/Content/PollOptionView.swift
+++ b/MastodonSDK/Sources/MastodonUI/View/Content/PollOptionView.swift
@@ -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() {
diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift
index da529ea12..39a802dbc 100644
--- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift
+++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift
@@ -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
diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift
index d6b37656b..8a1c02df3 100644
--- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift
+++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift
@@ -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?
- 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)
diff --git a/MastodonSDK/Sources/MastodonUI/View/Control/ActionToolbarContainer.swift b/MastodonSDK/Sources/MastodonUI/View/Control/ActionToolbarContainer.swift
index 3b4a97375..c3a7ba378 100644
--- a/MastodonSDK/Sources/MastodonUI/View/Control/ActionToolbarContainer.swift
+++ b/MastodonSDK/Sources/MastodonUI/View/Control/ActionToolbarContainer.swift
@@ -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)
}
}
diff --git a/MastodonSDK/Sources/MastodonUI/View/Control/SpoilerOverlayView.swift b/MastodonSDK/Sources/MastodonUI/View/Control/SpoilerOverlayView.swift
index cf84bfa86..17360d545 100644
--- a/MastodonSDK/Sources/MastodonUI/View/Control/SpoilerOverlayView.swift
+++ b/MastodonSDK/Sources/MastodonUI/View/Control/SpoilerOverlayView.swift
@@ -70,6 +70,8 @@ extension SpoilerOverlayView {
bottomPaddingView.setContentCompressionResistancePriority(.defaultLow - 100, for: .vertical)
spoilerMetaLabel.isUserInteractionEnabled = false
+
+ isAccessibilityElement = true
}
public func setComponentHidden(_ isHidden: Bool) {