diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift index 600016f6b..d78355b12 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift @@ -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 ) } } - ]) + ] } } diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCellDelegate.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCellDelegate.swift index 17ff907df..f2484e494 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCellDelegate.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCellDelegate.swift @@ -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) } diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusThreadRootTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusThreadRootTableViewCell.swift index c8a1fa4ce..ac86e6aa1 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/StatusThreadRootTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/StatusThreadRootTableViewCell.swift @@ -112,6 +112,7 @@ extension StatusThreadRootTableViewCell { statusView.mediaGridContainerView, statusView.pollTableView, statusView.pollStatusStackView, + statusView.statusCardControl, statusView.actionToolbarContainer, statusView.statusMetricView, ] diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift index 4cefb6714..adb12fa0a 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift @@ -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 } diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift index 530ceea20..dc370811f 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift @@ -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 } } diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift index 7082fec32..857b2d316 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift @@ -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) } } diff --git a/MastodonSDK/Sources/MastodonUI/View/Menu/LabeledAction.swift b/MastodonSDK/Sources/MastodonUI/View/Menu/LabeledAction.swift new file mode 100644 index 000000000..a3b1ef4b0 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/View/Menu/LabeledAction.swift @@ -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 + } + +} + + diff --git a/MastodonSDK/Sources/MastodonUI/View/Menu/MastodonMenu.swift b/MastodonSDK/Sources/MastodonUI/View/Menu/MastodonMenu.swift index a2875ba15..e67d11450 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Menu/MastodonMenu.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Menu/MastodonMenu.swift @@ -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 {