IOS-75: Make StatusCardControl an accessibility element (#934)

Co-authored-by: Marcus Kida <marcus.kida@bearologics.com>
This commit is contained in:
Jed Fox 2023-03-13 07:54:40 -04:00 committed by GitHub
parent 4dbf8d35ec
commit e6b8908ca5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 140 additions and 85 deletions

View File

@ -182,24 +182,24 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
_ cell: UITableViewCell,
statusView: StatusView,
cardControlMenu statusCardControl: StatusCardControl
) -> UIMenu? {
) -> [LabeledAction]? {
guard let card = statusView.viewModel.card,
let url = card.url else {
return nil
}
return UIMenu(children: [
UIAction(
return [
LabeledAction(
title: L10n.Common.Controls.Actions.copy,
image: UIImage(systemName: "doc.on.doc")
) { _ in
) {
UIPasteboard.general.url = url
},
UIAction(
LabeledAction(
title: L10n.Common.Controls.Actions.share,
image: Asset.Arrow.squareAndArrowUp.image.withRenderingMode(.alwaysTemplate)
) { _ in
asset: Asset.Arrow.squareAndArrowUp
) {
DispatchQueue.main.async {
let activityViewController = UIActivityViewController(
activityItems: [
@ -224,10 +224,10 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
}
},
UIAction(
LabeledAction(
title: L10n.Common.Controls.Status.Actions.shareLinkInPost,
image: Asset.ObjectsAndTools.squareAndPencil.image.withRenderingMode(.alwaysTemplate)
) { _ in
asset: Asset.ObjectsAndTools.squareAndPencil
) {
DispatchQueue.main.async {
self.coordinator.present(
scene: .compose(viewModel: ComposeViewModel(
@ -242,7 +242,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
)
}
}
])
]
}
}

View File

@ -39,7 +39,7 @@ protocol StatusTableViewCellDelegate: AnyObject, AutoGenerateProtocolDelegate {
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, statusMetricView: StatusMetricView, favoriteButtonDidPressed button: UIButton)
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, statusMetricView: StatusMetricView, showEditHistory button: UIButton)
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, cardControl: StatusCardControl, didTapURL url: URL)
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, cardControlMenu: StatusCardControl) -> UIMenu?
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, cardControlMenu: StatusCardControl) -> [LabeledAction]?
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, accessibilityActivate: Void)
// sourcery:end
}
@ -113,7 +113,7 @@ extension StatusViewDelegate where Self: StatusViewContainerTableViewCell {
delegate?.tableViewCell(self, statusView: statusView, cardControl: cardControl, didTapURL: url)
}
func statusView(_ statusView: StatusView, cardControlMenu: StatusCardControl) -> UIMenu? {
func statusView(_ statusView: StatusView, cardControlMenu: StatusCardControl) -> [LabeledAction]? {
return delegate?.tableViewCell(self, statusView: statusView, cardControlMenu: cardControlMenu)
}

View File

@ -112,6 +112,7 @@ extension StatusThreadRootTableViewCell {
statusView.mediaGridContainerView,
statusView.pollTableView,
statusView.pollStatusStackView,
statusView.statusCardControl,
statusView.actionToolbarContainer,
statusView.statusMetricView,
]

View File

@ -610,7 +610,7 @@ extension NotificationView: StatusViewDelegate {
assertionFailure()
}
public func statusView(_ statusView: StatusView, cardControlMenu: StatusCardControl) -> UIMenu? {
public func statusView(_ statusView: StatusView, cardControlMenu: StatusCardControl) -> [LabeledAction]? {
assertionFailure()
return nil
}

View File

@ -16,7 +16,7 @@ import WebKit
public protocol StatusCardControlDelegate: AnyObject {
func statusCardControl(_ statusCardControl: StatusCardControl, didTapURL url: URL)
func statusCardControlMenu(_ statusCardControl: StatusCardControl) -> UIMenu?
func statusCardControlMenu(_ statusCardControl: StatusCardControl) -> [LabeledAction]?
}
public final class StatusCardControl: UIControl {
@ -129,6 +129,8 @@ public final class StatusCardControl: UIControl {
])
addInteraction(UIContextMenuInteraction(delegate: self))
isAccessibilityElement = true
accessibilityTraits.insert(.link)
}
required init?(coder: NSCoder) {
@ -236,6 +238,13 @@ public final class StatusCardControl: UIControl {
dividerView.backgroundColor = theme.separator
imageView.backgroundColor = UIColor.tertiarySystemFill
}
public override var accessibilityCustomActions: [UIAccessibilityCustomAction]? {
get {
delegate?.statusCardControlMenu(self)?.map(\.accessibilityCustomAction)
}
set {}
}
}
// MARK: WKWebView delegates
@ -295,8 +304,11 @@ extension StatusCardControl: WKNavigationDelegate, WKUIDelegate {
// MARK: UIContextMenuInteractionDelegate
extension StatusCardControl {
public override func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { elements in
self.delegate?.statusCardControlMenu(self)
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { _ in
if let elements = self.delegate?.statusCardControlMenu(self)?.map(\.menuElement) {
return UIMenu(children: elements)
}
return nil
}
}

View File

@ -35,7 +35,7 @@ public protocol StatusViewDelegate: AnyObject {
func statusView(_ statusView: StatusView, statusMetricView: StatusMetricView, favoriteButtonDidPressed button: UIButton)
func statusView(_ statusView: StatusView, statusMetricView: StatusMetricView, showEditHistory button: UIButton)
func statusView(_ statusView: StatusView, cardControl: StatusCardControl, didTapURL url: URL)
func statusView(_ statusView: StatusView, cardControlMenu: StatusCardControl) -> UIMenu?
func statusView(_ statusView: StatusView, cardControlMenu: StatusCardControl) -> [LabeledAction]?
// a11y
func statusView(_ statusView: StatusView, accessibilityActivate: Void)
@ -804,7 +804,7 @@ extension StatusView: StatusCardControlDelegate {
delegate?.statusView(self, cardControl: statusCardControl, didTapURL: url)
}
public func statusCardControlMenu(_ statusCardControl: StatusCardControl) -> UIMenu? {
public func statusCardControlMenu(_ statusCardControl: StatusCardControl) -> [LabeledAction]? {
delegate?.statusView(self, cardControlMenu: statusCardControl)
}
}

View File

@ -0,0 +1,65 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import MastodonAsset
import UIKit
public struct LabeledAction {
public init(
title: String,
image: UIImage? = nil,
attributes: UIMenuElement.Attributes = [],
state: UIMenuElement.State = .off,
handler: @escaping () -> Void
) {
self.title = title
self.image = image
self.attributes = attributes
self.state = state
self.handler = handler
}
private let title: String
private let image: UIImage?
private let attributes: UIMenuElement.Attributes
private let state: UIMenuElement.State
private let handler: () -> Void
public var menuElement: UIMenuElement {
UIAction(
title: title,
image: image,
identifier: nil,
discoverabilityTitle: nil,
attributes: attributes,
state: .off
) { _ in
handler()
}
}
public var accessibilityCustomAction: UIAccessibilityCustomAction {
UIAccessibilityCustomAction(name: title, image: image) { _ in
handler()
return true
}
}
}
extension LabeledAction {
public init(
title: String,
asset: ImageAsset? = nil,
attributes: UIMenuElement.Attributes = [],
state: UIMenuElement.State = .off,
handler: @escaping () -> Void
) {
self.title = title
self.image = asset?.image.withRenderingMode(.alwaysTemplate)
self.attributes = attributes
self.state = state
self.handler = handler
}
}

View File

@ -59,39 +59,48 @@ extension MastodonMenu {
case deleteStatus
case editStatus
func build(delegate: MastodonMenuDelegate) -> BuiltAction {
func build(delegate: MastodonMenuDelegate) -> LabeledAction {
switch self {
case .hideReblogs(let context):
let title = context.showReblogs ? L10n.Common.Controls.Friendship.hideReblogs : L10n.Common.Controls.Friendship.showReblogs
let reblogAction = BuiltAction(
title: title,
image: UIImage(systemName: "arrow.2.squarepath")
) { [weak delegate] in
let reblogAction = LabeledAction(title: title, image: UIImage(systemName: "arrow.2.squarepath")) { [weak delegate] in
guard let delegate = delegate else { return }
delegate.menuAction(self)
}
return reblogAction
case .muteUser(let context):
let muteAction = BuiltAction(
title: context.isMuting ? L10n.Common.Controls.Friendship.unmuteUser(context.name) : L10n.Common.Controls.Friendship.muteUser(context.name),
image: context.isMuting ? UIImage(systemName: "speaker.wave.2") : UIImage(systemName: "speaker.slash")
) { [weak delegate] in
let title: String
let image: UIImage?
if context.isMuting {
title = L10n.Common.Controls.Friendship.unmuteUser(context.name)
image = UIImage(systemName: "speaker.wave.2")
} else {
title = L10n.Common.Controls.Friendship.muteUser(context.name)
image = UIImage(systemName: "speaker.slash")
}
let muteAction = LabeledAction(title: title, image: image) { [weak delegate] in
guard let delegate = delegate else { return }
delegate.menuAction(self)
}
return muteAction
case .blockUser(let context):
let blockAction = BuiltAction(
title: context.isBlocking ? L10n.Common.Controls.Friendship.unblockUser(context.name) : L10n.Common.Controls.Friendship.blockUser(context.name),
image: context.isBlocking ? UIImage(systemName: "hand.raised") : UIImage(systemName: "hand.raised")
) { [weak delegate] in
let title: String
let image: UIImage?
if context.isBlocking {
title = L10n.Common.Controls.Friendship.unblockUser(context.name)
image = UIImage(systemName: "hand.raised.slash")
} else {
title = L10n.Common.Controls.Friendship.blockUser(context.name)
image = UIImage(systemName: "hand.raised")
}
let blockAction = LabeledAction(title: title, image: image) { [weak delegate] in
guard let delegate = delegate else { return }
delegate.menuAction(self)
}
return blockAction
case .reportUser(let context):
let reportAction = BuiltAction(
let reportAction = LabeledAction(
title: L10n.Common.Controls.Actions.reportUser(context.name),
image: UIImage(systemName: "flag")
) { [weak delegate] in
@ -100,7 +109,7 @@ extension MastodonMenu {
}
return reportAction
case .shareUser(let context):
let shareAction = BuiltAction(
let shareAction = LabeledAction(
title: L10n.Common.Controls.Actions.shareUser(context.name),
image: UIImage(systemName: "square.and.arrow.up")
) { [weak delegate] in
@ -109,16 +118,22 @@ extension MastodonMenu {
}
return shareAction
case .bookmarkStatus(let context):
let action = BuiltAction(
title: context.isBookmarking ? "Remove Bookmark" : "Bookmark", // TODO: i18n
image: context.isBookmarking ? UIImage(systemName: "bookmark.slash.fill") : UIImage(systemName: "bookmark")
) { [weak delegate] in
let title: String
let image: UIImage?
if context.isBookmarking {
title = "Remove Bookmark" // TODO: i18n
image = UIImage(systemName: "bookmark.slash.fill")
} else {
title = "Bookmark" // TODO: i18n
image = UIImage(systemName: "bookmark")
}
let action = LabeledAction(title: title, image: image) { [weak delegate] in
guard let delegate = delegate else { return }
delegate.menuAction(self)
}
return action
case .shareStatus:
let action = BuiltAction(
let action = LabeledAction(
title: "Share", // TODO: i18n
image: UIImage(systemName: "square.and.arrow.up")
) { [weak delegate] in
@ -127,7 +142,7 @@ extension MastodonMenu {
}
return action
case .deleteStatus:
let deleteAction = BuiltAction(
let deleteAction = LabeledAction(
title: L10n.Common.Controls.Actions.delete,
image: UIImage(systemName: "minus.circle"),
attributes: .destructive
@ -137,8 +152,9 @@ extension MastodonMenu {
}
return deleteAction
case let .translateStatus(context):
let translateAction = BuiltAction(
title: L10n.Common.Controls.Actions.TranslatePost.title(Locale.current.localizedString(forIdentifier: context.language) ?? L10n.Common.Controls.Actions.TranslatePost.unknownLanguage),
let language = Locale.current.localizedString(forIdentifier: context.language) ?? L10n.Common.Controls.Actions.TranslatePost.unknownLanguage
let translateAction = LabeledAction(
title: L10n.Common.Controls.Actions.TranslatePost.title(language),
image: UIImage(systemName: "character.book.closed")
) { [weak delegate] in
guard let delegate = delegate else { return }
@ -146,7 +162,10 @@ extension MastodonMenu {
}
return translateAction
case .editStatus:
let editStatusAction = BuiltAction(title: L10n.Common.Controls.Actions.editPost, image: UIImage(systemName: "pencil")) {
let editStatusAction = LabeledAction(
title: L10n.Common.Controls.Actions.editPost,
image: UIImage(systemName: "pencil")
) {
[weak delegate] in
guard let delegate else { return }
delegate.menuAction(self)
@ -156,48 +175,6 @@ extension MastodonMenu {
} // end switch
} // end func build
} // end enum Action
struct BuiltAction {
init(
title: String,
image: UIImage? = nil,
attributes: UIMenuElement.Attributes = [],
state: UIMenuElement.State = .off,
handler: @escaping () -> Void
) {
self.title = title
self.image = image
self.attributes = attributes
self.state = state
self.handler = handler
}
let title: String
let image: UIImage?
let attributes: UIMenuElement.Attributes
let state: UIMenuElement.State
let handler: () -> Void
var menuElement: UIMenuElement {
UIAction(
title: title,
image: image,
identifier: nil,
discoverabilityTitle: nil,
attributes: attributes,
state: .off
) { _ in
handler()
}
}
var accessibilityCustomAction: UIAccessibilityCustomAction {
UIAccessibilityCustomAction(name: title, image: image) { _ in
handler()
return true
}
}
}
}
extension MastodonMenu {