IOS-75: Make StatusCardControl an accessibility element (#934)
Co-authored-by: Marcus Kida <marcus.kida@bearologics.com>
This commit is contained in:
parent
4dbf8d35ec
commit
e6b8908ca5
|
@ -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
|
|||
)
|
||||
}
|
||||
}
|
||||
])
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -112,6 +112,7 @@ extension StatusThreadRootTableViewCell {
|
|||
statusView.mediaGridContainerView,
|
||||
statusView.pollTableView,
|
||||
statusView.pollStatusStackView,
|
||||
statusView.statusCardControl,
|
||||
statusView.actionToolbarContainer,
|
||||
statusView.statusMetricView,
|
||||
]
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue