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, quoteStatusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton)
|
||||
func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, quoteStatusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta)
|
||||
func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, accessibilityActivate: Void)
|
||||
// sourcery:end
|
||||
}
|
||||
|
||||
|
@ -59,5 +60,9 @@ extension NotificationViewDelegate where Self: NotificationViewContainerTableVie
|
|||
func notificationView(_ notificationView: NotificationView, quoteStatusView: StatusView, metaText: MetaText, didSelectMeta meta: 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
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ final class StatusTableViewCell: UITableViewCell {
|
|||
|
||||
weak var delegate: StatusTableViewCellDelegate?
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
var _disposeBag = Set<AnyCancellable>()
|
||||
|
||||
let statusView = StatusView()
|
||||
let separatorLine = UIView.separatorLine
|
||||
|
@ -89,6 +90,16 @@ extension StatusTableViewCell {
|
|||
])
|
||||
|
||||
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?) {
|
||||
|
@ -96,6 +107,11 @@ extension StatusTableViewCell {
|
|||
|
||||
updateContainerViewMarginConstraints()
|
||||
}
|
||||
|
||||
override func accessibilityActivate() -> Bool {
|
||||
delegate?.tableViewCell(self, statusView: statusView, accessibilityActivate: Void())
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ protocol StatusTableViewCellDelegate: AnyObject, AutoGenerateProtocolDelegate {
|
|||
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, spoilerOverlayViewDidPressed overlayView: SpoilerOverlayView)
|
||||
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, accessibilityActivate: Void)
|
||||
// sourcery:end
|
||||
}
|
||||
|
||||
|
@ -85,5 +86,9 @@ extension StatusViewDelegate where Self: StatusViewContainerTableViewCell {
|
|||
func statusView(_ statusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaSensitiveButtonDidPressed button: UIButton) {
|
||||
delegate?.tableViewCell(self, statusView: statusView, mediaGridContainerView: mediaGridContainerView, mediaSensitiveButtonDidPressed: button)
|
||||
}
|
||||
|
||||
func statusView(_ statusView: StatusView, accessibilityActivate: Void) {
|
||||
delegate?.tableViewCell(self, statusView: statusView, accessibilityActivate: accessibilityActivate)
|
||||
}
|
||||
// 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, metaText: MetaText, didSelectMeta meta: Meta)
|
||||
|
||||
// a11y
|
||||
func notificationView(_ notificationView: NotificationView, accessibilityActivate: Void)
|
||||
}
|
||||
|
||||
public final class NotificationView: UIView {
|
||||
|
@ -392,6 +395,10 @@ extension NotificationView: StatusViewDelegate {
|
|||
assertionFailure()
|
||||
}
|
||||
|
||||
public func statusView(_ statusView: StatusView, accessibilityActivate: Void) {
|
||||
assertionFailure()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - MastodonMenuDelegate
|
||||
|
|
|
@ -43,6 +43,7 @@ extension StatusView {
|
|||
|
||||
@Published public var timestamp: Date?
|
||||
public var timestampFormatter: ((_ date: Date) -> String)?
|
||||
@Published public var timestampText = ""
|
||||
|
||||
// Spoiler
|
||||
@Published public var spoilerContent: MetaContent?
|
||||
|
@ -90,6 +91,8 @@ extension StatusView {
|
|||
|
||||
public let isNeedsTableViewUpdate = PassthroughSubject<Void, Never>()
|
||||
|
||||
@Published public var groupedAccessibilityLabel = ""
|
||||
|
||||
let timestampUpdatePublisher = Timer.publish(every: 1.0, on: .main, in: .common)
|
||||
.autoconnect()
|
||||
.share()
|
||||
|
@ -176,6 +179,7 @@ extension StatusView.ViewModel {
|
|||
bindToolbar(statusView: statusView)
|
||||
bindMetric(statusView: statusView)
|
||||
bindMenu(statusView: statusView)
|
||||
bindAccessibility(statusView: statusView)
|
||||
}
|
||||
|
||||
private func bindHeader(statusView: StatusView) {
|
||||
|
@ -246,11 +250,14 @@ extension StatusView.ViewModel {
|
|||
return text
|
||||
}
|
||||
.removeDuplicates()
|
||||
.sink { [weak self] text in
|
||||
guard let _ = self else { return }
|
||||
statusView.dateLabel.configure(content: PlaintextMetaContent(string: text))
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
.assign(to: &$timestampText)
|
||||
|
||||
$timestampText
|
||||
.sink { [weak self] text in
|
||||
guard let _ = self else { return }
|
||||
statusView.dateLabel.configure(content: PlaintextMetaContent(string: text))
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
private func bindContent(statusView: StatusView) {
|
||||
|
@ -635,6 +642,84 @@ extension StatusView.ViewModel {
|
|||
.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, spoilerBannerViewDidPressed bannerView: SpoilerBannerView)
|
||||
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, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue