forked from zelo72/mastodon-ios
feat: make the home timeline readable for VoiceOver
This commit is contained in:
parent
54e84ed814
commit
ab4d525cec
|
@ -223,3 +223,32 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: a11y
|
||||||
|
extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
|
||||||
|
func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, accessibilityActivate: Void) {
|
||||||
|
Task {
|
||||||
|
let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil)
|
||||||
|
guard let item = await item(from: source) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch item {
|
||||||
|
case .status(let status):
|
||||||
|
await DataSourceFacade.coordinateToStatusThreadScene(
|
||||||
|
provider: self,
|
||||||
|
target: .status, // remove reblog wrapper
|
||||||
|
status: status
|
||||||
|
)
|
||||||
|
case .user(let user):
|
||||||
|
await DataSourceFacade.coordinateToProfileScene(
|
||||||
|
provider: self,
|
||||||
|
user: user
|
||||||
|
)
|
||||||
|
case .notification(let notification):
|
||||||
|
assertionFailure("TODO")
|
||||||
|
default:
|
||||||
|
assertionFailure("TODO")
|
||||||
|
}
|
||||||
|
} // end Task
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -431,3 +431,31 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: a11y
|
||||||
|
extension StatusTableViewCellDelegate where Self: DataSourceProvider {
|
||||||
|
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, accessibilityActivate: Void) {
|
||||||
|
Task {
|
||||||
|
let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil)
|
||||||
|
guard let item = await item(from: source) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch item {
|
||||||
|
case .status(let status):
|
||||||
|
await DataSourceFacade.coordinateToStatusThreadScene(
|
||||||
|
provider: self,
|
||||||
|
target: .status, // remove reblog wrapper
|
||||||
|
status: status
|
||||||
|
)
|
||||||
|
case .user(let user):
|
||||||
|
await DataSourceFacade.coordinateToProfileScene(
|
||||||
|
provider: self,
|
||||||
|
user: user
|
||||||
|
)
|
||||||
|
case .notification(let notification):
|
||||||
|
assertionFailure("TODO")
|
||||||
|
default:
|
||||||
|
assertionFailure("TODO")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ protocol NotificationTableViewCellDelegate: AnyObject, AutoGenerateProtocolDeleg
|
||||||
func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, statusView: StatusView, actionToolbarContainer: ActionToolbarContainer, buttonDidPressed button: UIButton, action: ActionToolbarContainer.Action)
|
func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, statusView: StatusView, actionToolbarContainer: ActionToolbarContainer, buttonDidPressed button: UIButton, action: ActionToolbarContainer.Action)
|
||||||
func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, quoteStatusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton)
|
func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, quoteStatusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton)
|
||||||
func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, quoteStatusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta)
|
func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, quoteStatusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta)
|
||||||
|
func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, accessibilityActivate: Void)
|
||||||
// sourcery:end
|
// sourcery:end
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,5 +60,9 @@ extension NotificationViewDelegate where Self: NotificationViewContainerTableVie
|
||||||
func notificationView(_ notificationView: NotificationView, quoteStatusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) {
|
func notificationView(_ notificationView: NotificationView, quoteStatusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) {
|
||||||
delegate?.tableViewCell(self, notificationView: notificationView, quoteStatusView: quoteStatusView, metaText: metaText, didSelectMeta: meta)
|
delegate?.tableViewCell(self, notificationView: notificationView, quoteStatusView: quoteStatusView, metaText: metaText, didSelectMeta: meta)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func notificationView(_ notificationView: NotificationView, accessibilityActivate: Void) {
|
||||||
|
delegate?.tableViewCell(self, notificationView: notificationView, accessibilityActivate: accessibilityActivate)
|
||||||
|
}
|
||||||
// sourcery:end
|
// sourcery:end
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ final class StatusTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
weak var delegate: StatusTableViewCellDelegate?
|
weak var delegate: StatusTableViewCellDelegate?
|
||||||
var disposeBag = Set<AnyCancellable>()
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
var _disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
let statusView = StatusView()
|
let statusView = StatusView()
|
||||||
let separatorLine = UIView.separatorLine
|
let separatorLine = UIView.separatorLine
|
||||||
|
@ -89,6 +90,16 @@ extension StatusTableViewCell {
|
||||||
])
|
])
|
||||||
|
|
||||||
statusView.delegate = self
|
statusView.delegate = self
|
||||||
|
|
||||||
|
isAccessibilityElement = true
|
||||||
|
accessibilityElements = [statusView]
|
||||||
|
statusView.viewModel.$groupedAccessibilityLabel
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] accessibilityLabel in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.accessibilityLabel = accessibilityLabel
|
||||||
|
}
|
||||||
|
.store(in: &_disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||||
|
@ -97,6 +108,11 @@ extension StatusTableViewCell {
|
||||||
updateContainerViewMarginConstraints()
|
updateContainerViewMarginConstraints()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func accessibilityActivate() -> Bool {
|
||||||
|
delegate?.tableViewCell(self, statusView: statusView, accessibilityActivate: Void())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - AdaptiveContainerMarginTableViewCell
|
// MARK: - AdaptiveContainerMarginTableViewCell
|
||||||
|
|
|
@ -34,6 +34,7 @@ protocol StatusTableViewCellDelegate: AnyObject, AutoGenerateProtocolDelegate {
|
||||||
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, spoilerOverlayViewDidPressed overlayView: SpoilerOverlayView)
|
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, spoilerOverlayViewDidPressed overlayView: SpoilerOverlayView)
|
||||||
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, spoilerBannerViewDidPressed bannerView: SpoilerBannerView)
|
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, spoilerBannerViewDidPressed bannerView: SpoilerBannerView)
|
||||||
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaSensitiveButtonDidPressed button: UIButton)
|
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaSensitiveButtonDidPressed button: UIButton)
|
||||||
|
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, accessibilityActivate: Void)
|
||||||
// sourcery:end
|
// sourcery:end
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,5 +86,9 @@ extension StatusViewDelegate where Self: StatusViewContainerTableViewCell {
|
||||||
func statusView(_ statusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaSensitiveButtonDidPressed button: UIButton) {
|
func statusView(_ statusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaSensitiveButtonDidPressed button: UIButton) {
|
||||||
delegate?.tableViewCell(self, statusView: statusView, mediaGridContainerView: mediaGridContainerView, mediaSensitiveButtonDidPressed: button)
|
delegate?.tableViewCell(self, statusView: statusView, mediaGridContainerView: mediaGridContainerView, mediaSensitiveButtonDidPressed: button)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func statusView(_ statusView: StatusView, accessibilityActivate: Void) {
|
||||||
|
delegate?.tableViewCell(self, statusView: statusView, accessibilityActivate: accessibilityActivate)
|
||||||
|
}
|
||||||
// sourcery:end
|
// sourcery:end
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,9 @@ public protocol NotificationViewDelegate: AnyObject {
|
||||||
|
|
||||||
func notificationView(_ notificationView: NotificationView, quoteStatusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton)
|
func notificationView(_ notificationView: NotificationView, quoteStatusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton)
|
||||||
func notificationView(_ notificationView: NotificationView, quoteStatusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta)
|
func notificationView(_ notificationView: NotificationView, quoteStatusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta)
|
||||||
|
|
||||||
|
// a11y
|
||||||
|
func notificationView(_ notificationView: NotificationView, accessibilityActivate: Void)
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class NotificationView: UIView {
|
public final class NotificationView: UIView {
|
||||||
|
@ -392,6 +395,10 @@ extension NotificationView: StatusViewDelegate {
|
||||||
assertionFailure()
|
assertionFailure()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func statusView(_ statusView: StatusView, accessibilityActivate: Void) {
|
||||||
|
assertionFailure()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - MastodonMenuDelegate
|
// MARK: - MastodonMenuDelegate
|
||||||
|
|
|
@ -43,6 +43,7 @@ extension StatusView {
|
||||||
|
|
||||||
@Published public var timestamp: Date?
|
@Published public var timestamp: Date?
|
||||||
public var timestampFormatter: ((_ date: Date) -> String)?
|
public var timestampFormatter: ((_ date: Date) -> String)?
|
||||||
|
@Published public var timestampText = ""
|
||||||
|
|
||||||
// Spoiler
|
// Spoiler
|
||||||
@Published public var spoilerContent: MetaContent?
|
@Published public var spoilerContent: MetaContent?
|
||||||
|
@ -90,6 +91,8 @@ extension StatusView {
|
||||||
|
|
||||||
public let isNeedsTableViewUpdate = PassthroughSubject<Void, Never>()
|
public let isNeedsTableViewUpdate = PassthroughSubject<Void, Never>()
|
||||||
|
|
||||||
|
@Published public var groupedAccessibilityLabel = ""
|
||||||
|
|
||||||
let timestampUpdatePublisher = Timer.publish(every: 1.0, on: .main, in: .common)
|
let timestampUpdatePublisher = Timer.publish(every: 1.0, on: .main, in: .common)
|
||||||
.autoconnect()
|
.autoconnect()
|
||||||
.share()
|
.share()
|
||||||
|
@ -176,6 +179,7 @@ extension StatusView.ViewModel {
|
||||||
bindToolbar(statusView: statusView)
|
bindToolbar(statusView: statusView)
|
||||||
bindMetric(statusView: statusView)
|
bindMetric(statusView: statusView)
|
||||||
bindMenu(statusView: statusView)
|
bindMenu(statusView: statusView)
|
||||||
|
bindAccessibility(statusView: statusView)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func bindHeader(statusView: StatusView) {
|
private func bindHeader(statusView: StatusView) {
|
||||||
|
@ -246,6 +250,9 @@ extension StatusView.ViewModel {
|
||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
|
.assign(to: &$timestampText)
|
||||||
|
|
||||||
|
$timestampText
|
||||||
.sink { [weak self] text in
|
.sink { [weak self] text in
|
||||||
guard let _ = self else { return }
|
guard let _ = self else { return }
|
||||||
statusView.dateLabel.configure(content: PlaintextMetaContent(string: text))
|
statusView.dateLabel.configure(content: PlaintextMetaContent(string: text))
|
||||||
|
@ -635,6 +642,84 @@ extension StatusView.ViewModel {
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func bindAccessibility(statusView: StatusView) {
|
||||||
|
let authorAccessibilityLabel = Publishers.CombineLatest3(
|
||||||
|
$header,
|
||||||
|
$authorName,
|
||||||
|
$timestampText
|
||||||
|
)
|
||||||
|
.map { header, authorName, timestamp -> String? in
|
||||||
|
var strings: [String?] = []
|
||||||
|
|
||||||
|
switch header {
|
||||||
|
case .none:
|
||||||
|
break
|
||||||
|
case .reply(let info):
|
||||||
|
strings.append(info.header.string)
|
||||||
|
case .repost(let info):
|
||||||
|
strings.append(info.header.string)
|
||||||
|
}
|
||||||
|
|
||||||
|
strings.append(authorName?.string)
|
||||||
|
strings.append(timestamp)
|
||||||
|
|
||||||
|
return strings.compactMap { $0 }.joined(separator: ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
let contentAccessibilityLabel = Publishers.CombineLatest3(
|
||||||
|
$isContentReveal,
|
||||||
|
$spoilerContent,
|
||||||
|
$content
|
||||||
|
)
|
||||||
|
.map { isContentReveal, spoilerContent, content -> String? in
|
||||||
|
var strings: [String?] = []
|
||||||
|
|
||||||
|
if let spoilerContent = spoilerContent, !spoilerContent.string.isEmpty {
|
||||||
|
strings.append(L10n.Common.Controls.Status.contentWarning)
|
||||||
|
strings.append(spoilerContent.string)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isContentReveal {
|
||||||
|
strings.append(content?.string)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.compactMap { $0 }.joined(separator: ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
let meidaAccessibilityLabel = $mediaViewConfigurations
|
||||||
|
.map { configurations -> String? in
|
||||||
|
let count = configurations.count
|
||||||
|
// TODO: i18n
|
||||||
|
return count > 0 ? "\(count) media" : nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Toolbar
|
||||||
|
|
||||||
|
Publishers.CombineLatest3(
|
||||||
|
authorAccessibilityLabel,
|
||||||
|
contentAccessibilityLabel,
|
||||||
|
meidaAccessibilityLabel
|
||||||
|
)
|
||||||
|
.map { author, content, media in
|
||||||
|
let group = [
|
||||||
|
author,
|
||||||
|
content,
|
||||||
|
media
|
||||||
|
]
|
||||||
|
|
||||||
|
return group
|
||||||
|
.compactMap { $0 }
|
||||||
|
.joined(separator: ", ")
|
||||||
|
}
|
||||||
|
.assign(to: &$groupedAccessibilityLabel)
|
||||||
|
|
||||||
|
$groupedAccessibilityLabel
|
||||||
|
.sink { accessibilityLabel in
|
||||||
|
statusView.accessibilityLabel = accessibilityLabel
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,10 @@ public protocol StatusViewDelegate: AnyObject {
|
||||||
func statusView(_ statusView: StatusView, spoilerOverlayViewDidPressed overlayView: SpoilerOverlayView)
|
func statusView(_ statusView: StatusView, spoilerOverlayViewDidPressed overlayView: SpoilerOverlayView)
|
||||||
func statusView(_ statusView: StatusView, spoilerBannerViewDidPressed bannerView: SpoilerBannerView)
|
func statusView(_ statusView: StatusView, spoilerBannerViewDidPressed bannerView: SpoilerBannerView)
|
||||||
func statusView(_ statusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaSensitiveButtonDidPressed button: UIButton)
|
func statusView(_ statusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaSensitiveButtonDidPressed button: UIButton)
|
||||||
|
|
||||||
|
// a11y
|
||||||
|
func statusView(_ statusView: StatusView, accessibilityActivate: Void)
|
||||||
|
|
||||||
// func statusView(_ statusView: StatusView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
// func statusView(_ statusView: StatusView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
||||||
// func statusView(_ statusView: StatusView, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
// func statusView(_ statusView: StatusView, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue