From ec2be58952211549acfc5b88324b690464413aad Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 13 May 2021 14:27:57 +0800 Subject: [PATCH] feat: add accessibility supports for compose scene --- Localization/app.json | 25 +++++++++- .../Section/CustomEmojiPickerSection.swift | 1 + .../Diffiable/Section/StatusSection.swift | 1 + Mastodon/Generated/Strings.swift | 50 +++++++++++++++++++ .../Resources/en.lproj/Localizable.strings | 17 +++++++ ...tomEmojiPickerItemCollectionViewCell.swift | 4 ++ .../Scene/Compose/ComposeViewController.swift | 26 ++++++++++ .../Compose/View/ComposeToolbarView.swift | 6 +++ .../Scene/MainTab/MainTabBarController.swift | 9 ++-- .../Scene/Profile/ProfileViewController.swift | 6 +++ .../Scene/Share/View/Content/StatusView.swift | 8 ++- 11 files changed, 147 insertions(+), 6 deletions(-) diff --git a/Localization/app.json b/Localization/app.json index 98e9accd..327c7262 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -74,10 +74,17 @@ "settings": "Settings", "delete": "Delete" }, + "tabs": { + "home": "Home", + "search": "Search", + "notification": "Notification", + "profile": "Profile" + }, "status": { "user_reblogged": "%s reblogged", "user_replied_to": "Replied to %s", "show_post": "Show Post", + "show_user_profile": "Show user profile", "content_warning": "content warning", "content_warning_text": "cw: %s", "media_content_warning": "Tap to reveal that may be sensitive", @@ -331,6 +338,17 @@ "unlisted": "Unlisted", "private": "Followers only", "direct": "Only people I mention" + }, + "accessibility": { + "append_attachment": "Append attachment", + "append_poll": "Append poll", + "remove_poll": "Remove poll", + "custom_emoji_picker": "Custom emoji picker", + "enable_content_warning": "Enable content warning", + "disable_content_warning": "Disable content warning", + "post_visibility_menu": "Post visibility menu", + "input_limit_remains_count": "Input limit remains %ld", + "input_limit_exceeds_count": "Input limit exceeds %ld" } }, "profile": { @@ -338,7 +356,12 @@ "dashboard": { "posts": "posts", "following": "following", - "followers": "followers" + "followers": "followers", + "accessibility": { + "count_posts": "%ld posts", + "count_following": "%ld following", + "count_followers": "%ld followers" + } }, "segmented_control": { "posts": "Posts", diff --git a/Mastodon/Diffiable/Section/CustomEmojiPickerSection.swift b/Mastodon/Diffiable/Section/CustomEmojiPickerSection.swift index 06b626d0..20dc5b80 100644 --- a/Mastodon/Diffiable/Section/CustomEmojiPickerSection.swift +++ b/Mastodon/Diffiable/Section/CustomEmojiPickerSection.swift @@ -32,6 +32,7 @@ extension CustomEmojiPickerSection { ], completionHandler: nil ) + cell.accessibilityLabel = attribute.emoji.shortcode return cell } } diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index e7ca8182..d139e061 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -102,6 +102,7 @@ extension StatusSection { case .root: cell.statusView.activeTextLabel.isAccessibilityElement = false var accessibilityElements: [Any] = [] + accessibilityElements.append(cell.statusView.avatarView) accessibilityElements.append(cell.statusView.nameLabel) accessibilityElements.append(cell.statusView.dateLabel) accessibilityElements.append(contentsOf: cell.statusView.activeTextLabel.createAccessibilityElements()) diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index 3785a79d..de69f016 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -200,6 +200,8 @@ internal enum L10n { internal static let mediaContentWarning = L10n.tr("Localizable", "Common.Controls.Status.MediaContentWarning") /// Show Post internal static let showPost = L10n.tr("Localizable", "Common.Controls.Status.ShowPost") + /// Show user profile + internal static let showUserProfile = L10n.tr("Localizable", "Common.Controls.Status.ShowUserProfile") /// %@ reblogged internal static func userReblogged(_ p1: Any) -> String { return L10n.tr("Localizable", "Common.Controls.Status.UserReblogged", String(describing: p1)) @@ -267,6 +269,16 @@ internal enum L10n { internal static let url = L10n.tr("Localizable", "Common.Controls.Status.Tag.Url") } } + internal enum Tabs { + /// Home + internal static let home = L10n.tr("Localizable", "Common.Controls.Tabs.Home") + /// Notification + internal static let notification = L10n.tr("Localizable", "Common.Controls.Tabs.Notification") + /// Profile + internal static let profile = L10n.tr("Localizable", "Common.Controls.Tabs.Profile") + /// Search + internal static let search = L10n.tr("Localizable", "Common.Controls.Tabs.Search") + } internal enum Timeline { internal enum Accessibility { /// %@ favorites @@ -326,6 +338,30 @@ internal enum L10n { internal static func replyingToUser(_ p1: Any) -> String { return L10n.tr("Localizable", "Scene.Compose.ReplyingToUser", String(describing: p1)) } + internal enum Accessibility { + /// Append attachment + internal static let appendAttachment = L10n.tr("Localizable", "Scene.Compose.Accessibility.AppendAttachment") + /// Append poll + internal static let appendPoll = L10n.tr("Localizable", "Scene.Compose.Accessibility.AppendPoll") + /// Custom emoji picker + internal static let customEmojiPicker = L10n.tr("Localizable", "Scene.Compose.Accessibility.CustomEmojiPicker") + /// Disable content warning + internal static let disableContentWarning = L10n.tr("Localizable", "Scene.Compose.Accessibility.DisableContentWarning") + /// Enable content warning + internal static let enableContentWarning = L10n.tr("Localizable", "Scene.Compose.Accessibility.EnableContentWarning") + /// Input limit exceeds %ld + internal static func inputLimitExceedsCount(_ p1: Int) -> String { + return L10n.tr("Localizable", "Scene.Compose.Accessibility.InputLimitExceedsCount", p1) + } + /// Input limit remains %ld + internal static func inputLimitRemainsCount(_ p1: Int) -> String { + return L10n.tr("Localizable", "Scene.Compose.Accessibility.InputLimitRemainsCount", p1) + } + /// Post visibility menu + internal static let postVisibilityMenu = L10n.tr("Localizable", "Scene.Compose.Accessibility.PostVisibilityMenu") + /// Remove poll + internal static let removePoll = L10n.tr("Localizable", "Scene.Compose.Accessibility.RemovePoll") + } internal enum Attachment { /// This %@ is broken and can't be\nuploaded to Mastodon. internal static func attachmentBroken(_ p1: Any) -> String { @@ -481,6 +517,20 @@ internal enum L10n { internal static let following = L10n.tr("Localizable", "Scene.Profile.Dashboard.Following") /// posts internal static let posts = L10n.tr("Localizable", "Scene.Profile.Dashboard.Posts") + internal enum Accessibility { + /// %ld followers + internal static func countFollowers(_ p1: Int) -> String { + return L10n.tr("Localizable", "Scene.Profile.Dashboard.Accessibility.CountFollowers", p1) + } + /// %ld following + internal static func countFollowing(_ p1: Int) -> String { + return L10n.tr("Localizable", "Scene.Profile.Dashboard.Accessibility.CountFollowing", p1) + } + /// %ld posts + internal static func countPosts(_ p1: Int) -> String { + return L10n.tr("Localizable", "Scene.Profile.Dashboard.Accessibility.CountPosts", p1) + } + } } internal enum RelationshipActionAlert { internal enum ConfirmUnblockUsre { diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index b685e965..0549c4a3 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -81,6 +81,7 @@ Please check your internet connection."; "Common.Controls.Status.Poll.VoterCount.Multiple" = "%d voters"; "Common.Controls.Status.Poll.VoterCount.Single" = "%d voter"; "Common.Controls.Status.ShowPost" = "Show Post"; +"Common.Controls.Status.ShowUserProfile" = "Show user profile"; "Common.Controls.Status.Tag.Email" = "Email"; "Common.Controls.Status.Tag.Emoji" = "Emoji"; "Common.Controls.Status.Tag.Hashtag" = "Hashtag"; @@ -89,6 +90,10 @@ Please check your internet connection."; "Common.Controls.Status.Tag.Url" = "URL"; "Common.Controls.Status.UserReblogged" = "%@ reblogged"; "Common.Controls.Status.UserRepliedTo" = "Replied to %@"; +"Common.Controls.Tabs.Home" = "Home"; +"Common.Controls.Tabs.Notification" = "Notification"; +"Common.Controls.Tabs.Profile" = "Profile"; +"Common.Controls.Tabs.Search" = "Search"; "Common.Controls.Timeline.Accessibility.CountFavorites" = "%@ favorites"; "Common.Controls.Timeline.Accessibility.CountReblogs" = "%@ reblogs"; "Common.Controls.Timeline.Accessibility.CountReplies" = "%@ replies"; @@ -105,6 +110,15 @@ Your account looks like this to them."; "Common.Controls.Timeline.Loader.ShowMoreReplies" = "Show more replies"; "Common.Countable.Photo.Multiple" = "photos"; "Common.Countable.Photo.Single" = "photo"; +"Scene.Compose.Accessibility.AppendAttachment" = "Append attachment"; +"Scene.Compose.Accessibility.AppendPoll" = "Append poll"; +"Scene.Compose.Accessibility.CustomEmojiPicker" = "Custom emoji picker"; +"Scene.Compose.Accessibility.DisableContentWarning" = "Disable content warning"; +"Scene.Compose.Accessibility.EnableContentWarning" = "Enable content warning"; +"Scene.Compose.Accessibility.InputLimitExceedsCount" = "Input limit exceeds %ld"; +"Scene.Compose.Accessibility.InputLimitRemainsCount" = "Input limit remains %ld"; +"Scene.Compose.Accessibility.PostVisibilityMenu" = "Post visibility menu"; +"Scene.Compose.Accessibility.RemovePoll" = "Remove poll"; "Scene.Compose.Attachment.AttachmentBroken" = "This %@ is broken and can't be uploaded to Mastodon."; "Scene.Compose.Attachment.DescriptionPhoto" = "Describe photo for low vision people..."; @@ -159,6 +173,9 @@ tap the link to confirm your account."; "Scene.Notification.Action.Reblog" = "rebloged your post"; "Scene.Notification.Title.Everything" = "Everything"; "Scene.Notification.Title.Mentions" = "Mentions"; +"Scene.Profile.Dashboard.Accessibility.CountFollowers" = "%ld followers"; +"Scene.Profile.Dashboard.Accessibility.CountFollowing" = "%ld following"; +"Scene.Profile.Dashboard.Accessibility.CountPosts" = "%ld posts"; "Scene.Profile.Dashboard.Followers" = "followers"; "Scene.Profile.Dashboard.Following" = "following"; "Scene.Profile.Dashboard.Posts" = "posts"; diff --git a/Mastodon/Scene/Compose/CollectionViewCell/CustomEmojiPickerItemCollectionViewCell.swift b/Mastodon/Scene/Compose/CollectionViewCell/CustomEmojiPickerItemCollectionViewCell.swift index 7acc49ae..49e6c1fe 100644 --- a/Mastodon/Scene/Compose/CollectionViewCell/CustomEmojiPickerItemCollectionViewCell.swift +++ b/Mastodon/Scene/Compose/CollectionViewCell/CustomEmojiPickerItemCollectionViewCell.swift @@ -47,6 +47,10 @@ extension CustomEmojiPickerItemCollectionViewCell { emojiImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), emojiImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), ]) + + isAccessibilityElement = true + accessibilityTraits = .button + accessibilityHint = "emoji" } } diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index dedcd405..3f43588f 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -260,6 +260,21 @@ extension ComposeViewController { .receive(on: DispatchQueue.main) .assign(to: \.isEnabled, on: composeToolbarView.pollButton) .store(in: &disposeBag) + + Publishers.CombineLatest( + viewModel.isPollComposing, + viewModel.isPollToolbarButtonEnabled + ) + .receive(on: DispatchQueue.main) + .sink { [weak self] isPollComposing, isPollToolbarButtonEnabled in + guard let self = self else { return } + guard isPollToolbarButtonEnabled else { + self.composeToolbarView.pollButton.accessibilityLabel = L10n.Scene.Compose.Accessibility.appendPoll + return + } + self.composeToolbarView.pollButton.accessibilityLabel = isPollComposing ? L10n.Scene.Compose.Accessibility.removePoll : L10n.Scene.Compose.Accessibility.appendPoll + } + .store(in: &disposeBag) // bind image picker toolbar state viewModel.attachmentServices @@ -271,6 +286,15 @@ extension ComposeViewController { } .store(in: &disposeBag) + // bind content warning button state + viewModel.isContentWarningComposing + .receive(on: DispatchQueue.main) + .sink { [weak self] isContentWarningComposing in + guard let self = self else { return } + self.composeToolbarView.contentWarningButton.accessibilityLabel = isContentWarningComposing ? L10n.Scene.Compose.Accessibility.disableContentWarning : L10n.Scene.Compose.Accessibility.enableContentWarning + } + .store(in: &disposeBag) + // bind visibility toolbar UI Publishers.CombineLatest( viewModel.selectedStatusVisibility, @@ -294,9 +318,11 @@ extension ComposeViewController { case _ where count < 0: self.composeToolbarView.characterCountLabel.font = .systemFont(ofSize: 24, weight: .bold) self.composeToolbarView.characterCountLabel.textColor = Asset.Colors.danger.color + self.composeToolbarView.characterCountLabel.accessibilityLabel = L10n.Scene.Compose.Accessibility.inputLimitExceedsCount(abs(count)) default: self.composeToolbarView.characterCountLabel.font = .systemFont(ofSize: 15, weight: .regular) self.composeToolbarView.characterCountLabel.textColor = Asset.Colors.Label.secondary.color + self.composeToolbarView.characterCountLabel.accessibilityLabel = L10n.Scene.Compose.Accessibility.inputLimitRemainsCount(count) } } .store(in: &disposeBag) diff --git a/Mastodon/Scene/Compose/View/ComposeToolbarView.swift b/Mastodon/Scene/Compose/View/ComposeToolbarView.swift index 6aabc457..68f5eed0 100644 --- a/Mastodon/Scene/Compose/View/ComposeToolbarView.swift +++ b/Mastodon/Scene/Compose/View/ComposeToolbarView.swift @@ -28,6 +28,7 @@ final class ComposeToolbarView: UIView { let button = HighlightDimmableButton() ComposeToolbarView.configureToolbarButtonAppearance(button: button) button.setImage(UIImage(systemName: "photo", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)), for: .normal) + button.accessibilityLabel = L10n.Scene.Compose.Accessibility.appendAttachment return button }() @@ -35,6 +36,7 @@ final class ComposeToolbarView: UIView { let button = HighlightDimmableButton(type: .custom) ComposeToolbarView.configureToolbarButtonAppearance(button: button) button.setImage(UIImage(systemName: "list.bullet", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .medium)), for: .normal) + button.accessibilityLabel = L10n.Scene.Compose.Accessibility.appendPoll return button }() @@ -45,6 +47,7 @@ final class ComposeToolbarView: UIView { .af.imageScaled(to: CGSize(width: 20, height: 20)) .withRenderingMode(.alwaysTemplate) button.setImage(image, for: .normal) + button.accessibilityLabel = L10n.Scene.Compose.Accessibility.customEmojiPicker return button }() @@ -52,6 +55,7 @@ final class ComposeToolbarView: UIView { let button = HighlightDimmableButton() ComposeToolbarView.configureToolbarButtonAppearance(button: button) button.setImage(UIImage(systemName: "exclamationmark.shield", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)), for: .normal) + button.accessibilityLabel = L10n.Scene.Compose.Accessibility.enableContentWarning return button }() @@ -59,6 +63,7 @@ final class ComposeToolbarView: UIView { let button = HighlightDimmableButton() ComposeToolbarView.configureToolbarButtonAppearance(button: button) button.setImage(UIImage(systemName: "person.3", withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .medium)), for: .normal) + button.accessibilityLabel = L10n.Scene.Compose.Accessibility.postVisibilityMenu return button }() @@ -67,6 +72,7 @@ final class ComposeToolbarView: UIView { label.font = .systemFont(ofSize: 15, weight: .regular) label.text = "500" label.textColor = Asset.Colors.Label.secondary.color + label.accessibilityLabel = L10n.Scene.Compose.Accessibility.inputLimitRemainsCount(500) return label }() diff --git a/Mastodon/Scene/MainTab/MainTabBarController.swift b/Mastodon/Scene/MainTab/MainTabBarController.swift index 5fd4c825..8b170157 100644 --- a/Mastodon/Scene/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/MainTab/MainTabBarController.swift @@ -25,10 +25,10 @@ class MainTabBarController: UITabBarController { var title: String { switch self { - case .home: return "Home" - case .search: return "Search" - case .notification: return "Notification" - case .me: return "Me" + case .home: return L10n.Common.Controls.Tabs.home + case .search: return L10n.Common.Controls.Tabs.search + case .notification: return L10n.Common.Controls.Tabs.notification + case .me: return L10n.Common.Controls.Tabs.profile } } @@ -99,6 +99,7 @@ extension MainTabBarController { let viewController = tab.viewController(context: context, coordinator: coordinator) viewController.tabBarItem.title = "" // set text to empty string for image only style (SDK failed to layout when set to nil) viewController.tabBarItem.image = tab.image + viewController.tabBarItem.accessibilityLabel = tab.title return viewController } setViewControllers(viewControllers, animated: false) diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index d186d13d..c60c2040 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -479,6 +479,8 @@ extension ProfileViewController { guard let self = self else { return } let text = count.flatMap { MastodonMetricFormatter().string(from: $0) } ?? "-" self.profileHeaderViewController.profileHeaderView.statusDashboardView.postDashboardMeterView.numberLabel.text = text + self.profileHeaderViewController.profileHeaderView.statusDashboardView.postDashboardMeterView.isAccessibilityElement = true + self.profileHeaderViewController.profileHeaderView.statusDashboardView.postDashboardMeterView.accessibilityLabel = L10n.Scene.Profile.Dashboard.Accessibility.countPosts(count ?? 0) } .store(in: &disposeBag) viewModel.followingCount @@ -486,6 +488,8 @@ extension ProfileViewController { guard let self = self else { return } let text = count.flatMap { MastodonMetricFormatter().string(from: $0) } ?? "-" self.profileHeaderViewController.profileHeaderView.statusDashboardView.followingDashboardMeterView.numberLabel.text = text + self.profileHeaderViewController.profileHeaderView.statusDashboardView.followingDashboardMeterView.isAccessibilityElement = true + self.profileHeaderViewController.profileHeaderView.statusDashboardView.followingDashboardMeterView.accessibilityLabel = L10n.Scene.Profile.Dashboard.Accessibility.countFollowing(count ?? 0) } .store(in: &disposeBag) viewModel.followersCount @@ -493,6 +497,8 @@ extension ProfileViewController { guard let self = self else { return } let text = count.flatMap { MastodonMetricFormatter().string(from: $0) } ?? "-" self.profileHeaderViewController.profileHeaderView.statusDashboardView.followersDashboardMeterView.numberLabel.text = text + self.profileHeaderViewController.profileHeaderView.statusDashboardView.followersDashboardMeterView.isAccessibilityElement = true + self.profileHeaderViewController.profileHeaderView.statusDashboardView.followersDashboardMeterView.accessibilityLabel = L10n.Scene.Profile.Dashboard.Accessibility.countFollowers(count ?? 0) } .store(in: &disposeBag) diff --git a/Mastodon/Scene/Share/View/Content/StatusView.swift b/Mastodon/Scene/Share/View/Content/StatusView.swift index ffe56b8b..46930f33 100644 --- a/Mastodon/Scene/Share/View/Content/StatusView.swift +++ b/Mastodon/Scene/Share/View/Content/StatusView.swift @@ -75,7 +75,13 @@ final class StatusView: UIView { return label }() - let avatarView = UIView() + let avatarView: UIView = { + let view = UIView() + view.isAccessibilityElement = true + view.accessibilityTraits = .button + view.accessibilityLabel = L10n.Common.Controls.Status.showUserProfile + return view + }() let avatarButton: UIButton = { let button = HighlightDimmableButton(type: .custom) let placeholderImage = UIImage.placeholder(size: avatarImageSize, color: .systemFill)