diff --git a/submodules/AlertUI/Sources/TextAlertWithEntitiesController.swift b/submodules/AlertUI/Sources/TextAlertWithEntitiesController.swift index a9e506b81ce..9818396c02e 100644 --- a/submodules/AlertUI/Sources/TextAlertWithEntitiesController.swift +++ b/submodules/AlertUI/Sources/TextAlertWithEntitiesController.swift @@ -7,6 +7,16 @@ import TextNodeWithEntities private let alertWidth: CGFloat = 270.0 +private final class TextAlertWithEntitiesAccessibilityCustomAction: UIAccessibilityCustomAction { + let perform: () -> Void + + init(name: String, target: Any?, selector: Selector, perform: @escaping () -> Void) { + self.perform = perform + + super.init(name: name, target: target, selector: selector) + } +} + final class TextAlertWithEntitiesContentNode: AlertContentNode { private var theme: AlertControllerTheme private let actionLayout: TextAlertContentActionLayout @@ -25,6 +35,10 @@ final class TextAlertWithEntitiesContentNode: AlertContentNode { return self._dismissOnOutsideTap } + override public var accessibilityInitialFocusNode: ASDisplayNode? { + return self.titleNode ?? self.textNode + } + private var highlightedItemIndex: Int? = nil var textAttributeAction: (NSAttributedString.Key, (Any) -> Void)? { @@ -47,6 +61,7 @@ final class TextAlertWithEntitiesContentNode: AlertContentNode { self.textNode.highlightAttributeAction = nil self.textNode.tapAttributeAction = nil } + self.updateTextAccessibilityActions() } } @@ -59,10 +74,11 @@ final class TextAlertWithEntitiesContentNode: AlertContentNode { titleNode.attributedText = title titleNode.displaysAsynchronously = false titleNode.isUserInteractionEnabled = false - titleNode.maximumNumberOfLines = 4 + titleNode.maximumNumberOfLines = 0 titleNode.truncationType = .end titleNode.isAccessibilityElement = true titleNode.accessibilityLabel = title.string + titleNode.accessibilityTraits = [.header] self.titleNode = titleNode } else { self.titleNode = nil @@ -127,6 +143,38 @@ final class TextAlertWithEntitiesContentNode: AlertContentNode { for separatorNode in self.actionVerticalSeparators { self.addSubnode(separatorNode) } + + self.updateTextAccessibilityActions() + } + + private func updateTextAccessibilityActions() { + guard let attributedText = self.textNode.attributedText, let (attribute, textAttributeAction) = self.textAttributeAction, attributedText.length != 0 else { + self.textNode.accessibilityCustomActions = nil + return + } + + var accessibilityActions: [UIAccessibilityCustomAction] = [] + attributedText.enumerateAttribute(attribute, in: NSRange(location: 0, length: attributedText.length), options: []) { [weak self] value, range, _ in + guard let self, let value else { + return + } + let actionName = attributedText.attributedSubstring(from: range).string.trimmingCharacters(in: .whitespacesAndNewlines) + guard !actionName.isEmpty else { + return + } + accessibilityActions.append(TextAlertWithEntitiesAccessibilityCustomAction(name: actionName, target: self, selector: #selector(self.performTextAccessibilityAction(_:)), perform: { + textAttributeAction(value) + })) + } + self.textNode.accessibilityCustomActions = accessibilityActions.isEmpty ? nil : accessibilityActions + } + + @objc private func performTextAccessibilityAction(_ action: UIAccessibilityCustomAction) -> Bool { + guard let action = action as? TextAlertWithEntitiesAccessibilityCustomAction else { + return false + } + action.perform() + return true } func setHighlightedItemIndex(_ index: Int?, update: Bool = false) { @@ -199,6 +247,13 @@ final class TextAlertWithEntitiesContentNode: AlertContentNode { _ = self.updateLayout(size: size, transition: .immediate) } } + + override func contentSizeCategoryUpdated() { + for actionNode in self.actionNodes { + actionNode.updateTheme(self.theme) + } + self.requestLayout?(.immediate) + } override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { self.validLayout = size @@ -214,24 +269,21 @@ final class TextAlertWithEntitiesContentNode: AlertContentNode { } let textSize = self.textNode.updateLayout(CGSize(width: size.width - insets.left - insets.right, height: CGFloat.greatestFiniteMagnitude)) - let actionButtonHeight: CGFloat = 44.0 - - var minActionsWidth: CGFloat = 0.0 - let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) - let actionTitleInsets: CGFloat = 8.0 + let minimumActionButtonHeight: CGFloat = 44.0 + let maxActionWidth: CGFloat = self.actionNodes.isEmpty ? size.width : floor(size.width / CGFloat(self.actionNodes.count)) var effectiveActionLayout = self.actionLayout + if self.traitCollection.preferredContentSizeCategory.isAccessibilityCategory { + effectiveActionLayout = .vertical + } + var actionHeights: [CGFloat] = [] for actionNode in self.actionNodes { - let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight)) - if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 { + let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: max(1.0, maxActionWidth - 16.0), height: CGFloat.greatestFiniteMagnitude)) + let actionHeight = max(minimumActionButtonHeight, actionTitleSize.height + 20.0) + actionHeights.append(actionHeight) + if case .horizontal = effectiveActionLayout, actionHeight > minimumActionButtonHeight { effectiveActionLayout = .vertical } - switch effectiveActionLayout { - case .horizontal: - minActionsWidth += actionTitleSize.width + actionTitleInsets - case .vertical: - minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets) - } } let resultSize: CGSize @@ -239,9 +291,9 @@ final class TextAlertWithEntitiesContentNode: AlertContentNode { var actionsHeight: CGFloat = 0.0 switch effectiveActionLayout { case .horizontal: - actionsHeight = actionButtonHeight + actionsHeight = actionHeights.max() ?? minimumActionButtonHeight case .vertical: - actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) + actionsHeight = actionHeights.reduce(0.0, +) } let contentWidth = alertWidth - insets.left - insets.right @@ -261,10 +313,11 @@ final class TextAlertWithEntitiesContentNode: AlertContentNode { resultSize = CGSize(width: contentWidth + insets.left + insets.right, height: textSize.height + actionsHeight + insets.top + insets.bottom) } + self.actionNodesSeparator.isHidden = self.actionNodes.isEmpty self.actionNodesSeparator.frame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)) var actionOffset: CGFloat = 0.0 - let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count)) + let actionWidth: CGFloat = self.actionNodes.isEmpty ? resultSize.width : floor(resultSize.width / CGFloat(self.actionNodes.count)) var separatorIndex = -1 var nodeIndex = 0 for actionNode in self.actionNodes { @@ -294,11 +347,12 @@ final class TextAlertWithEntitiesContentNode: AlertContentNode { let actionNodeFrame: CGRect switch effectiveActionLayout { case .horizontal: - actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionsHeight)) actionOffset += currentActionWidth case .vertical: - actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) - actionOffset += actionButtonHeight + let actionHeight = actionHeights[nodeIndex] + actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionHeight)) + actionOffset += actionHeight } transition.updateFrame(node: actionNode, frame: actionNodeFrame) diff --git a/submodules/Display/Source/Accessibility.swift b/submodules/Display/Source/Accessibility.swift index 36be42c7ee8..86093e4c3fe 100644 --- a/submodules/Display/Source/Accessibility.swift +++ b/submodules/Display/Source/Accessibility.swift @@ -20,6 +20,32 @@ public func addAccessibilityChildren(of node: ASDisplayNode, container: Any, to } } +public func firstAccessibilityElement(in view: UIView) -> Any? { + guard !view.isHidden, view.alpha > 0.01, !view.accessibilityElementsHidden else { + return nil + } + if view.isAccessibilityElement { + return view + } + if let accessibilityElements = view.accessibilityElements { + for element in accessibilityElements { + if let elementView = element as? UIView { + if let result = firstAccessibilityElement(in: elementView) { + return result + } + } else { + return element + } + } + } + for subview in view.subviews { + if let result = firstAccessibilityElement(in: subview) { + return result + } + } + return nil +} + public func smartInvertColorsEnabled() -> Bool { if #available(iOSApplicationExtension 11.0, iOS 11.0, *), UIAccessibility.isInvertColorsEnabled { return true diff --git a/submodules/Display/Source/AccessibilityAreaNode.swift b/submodules/Display/Source/AccessibilityAreaNode.swift index 5975b2fa319..d7d35607524 100644 --- a/submodules/Display/Source/AccessibilityAreaNode.swift +++ b/submodules/Display/Source/AccessibilityAreaNode.swift @@ -7,9 +7,21 @@ public protocol AccessibilityFocusableNode { } public final class AccessibilityAreaNode: ASDisplayNode { - public var activate: (() -> Bool)? - public var increment: (() -> Void)? - public var decrement: (() -> Void)? + public var activate: (() -> Bool)? { + didSet { + self.updateRespondsToUserInteraction() + } + } + public var increment: (() -> Void)? { + didSet { + self.updateRespondsToUserInteraction() + } + } + public var decrement: (() -> Void)? { + didSet { + self.updateRespondsToUserInteraction() + } + } public var focused: (() -> Void)? override public init() { @@ -17,6 +29,23 @@ public final class AccessibilityAreaNode: ASDisplayNode { self.isAccessibilityElement = true } + + override public func didLoad() { + super.didLoad() + + self.updateRespondsToUserInteraction() + } + + private func updateRespondsToUserInteraction() { + if self.isNodeLoaded { + self.view.accessibilityRespondsToUserInteraction = self.activate != nil + || self.increment != nil + || self.decrement != nil + || self.accessibilityTraits.contains(.button) + || self.accessibilityTraits.contains(.link) + || self.accessibilityTraits.contains(.adjustable) + } + } override public func accessibilityActivate() -> Bool { return self.activate?() ?? false diff --git a/submodules/Display/Source/ActionSheetButtonItem.swift b/submodules/Display/Source/ActionSheetButtonItem.swift index 7a37d67160c..9211d152870 100644 --- a/submodules/Display/Source/ActionSheetButtonItem.swift +++ b/submodules/Display/Source/ActionSheetButtonItem.swift @@ -48,9 +48,6 @@ public class ActionSheetButtonItem: ActionSheetItem { public class ActionSheetButtonNode: ActionSheetItemNode { private let theme: ActionSheetControllerTheme - private let defaultFont: UIFont - private let boldFont: UIFont - private var item: ActionSheetButtonItem? private let button: HighlightTrackingButton @@ -61,16 +58,13 @@ public class ActionSheetButtonNode: ActionSheetItemNode { override public init(theme: ActionSheetControllerTheme) { self.theme = theme - - self.defaultFont = Font.regular(floor(theme.baseFontSize * 20.0 / 17.0)) - self.boldFont = Font.medium(floor(theme.baseFontSize * 20.0 / 17.0)) - + self.button = HighlightTrackingButton() self.button.isAccessibilityElement = false self.label = ImmediateTextNode() self.label.isUserInteractionEnabled = false - self.label.maximumNumberOfLines = 1 + self.label.maximumNumberOfLines = 0 self.label.displaysAsynchronously = false self.label.truncationType = .end @@ -146,9 +140,9 @@ public class ActionSheetButtonNode: ActionSheetItemNode { } switch item.font { case .default: - textFont = Font.regular(floor(theme.baseFontSize * 20.0 / 17.0)) + textFont = Font.regular(UIFontMetrics(forTextStyle: .body).scaledValue(for: floor(theme.baseFontSize * 20.0 / 17.0))) case .bold: - textFont = Font.medium(floor(theme.baseFontSize * 20.0 / 17.0)) + textFont = Font.medium(UIFontMetrics(forTextStyle: .body).scaledValue(for: floor(theme.baseFontSize * 20.0 / 17.0))) } self.label.attributedText = NSAttributedString(string: item.title, font: textFont, textColor: textColor) self.label.isAccessibilityElement = false @@ -165,11 +159,11 @@ public class ActionSheetButtonNode: ActionSheetItemNode { } public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { - let size = CGSize(width: constrainedSize.width, height: 57.0) + let labelSize = self.label.updateLayout(CGSize(width: max(1.0, constrainedSize.width - 32.0), height: constrainedSize.height)) + let size = CGSize(width: constrainedSize.width, height: max(57.0, labelSize.height + 28.0)) self.button.frame = CGRect(origin: CGPoint(), size: size) - let labelSize = self.label.updateLayout(CGSize(width: max(1.0, size.width - 10.0), height: size.height)) self.label.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - labelSize.width) / 2.0), y: floorToScreenPixels((size.height - labelSize.height) / 2.0)), size: labelSize) self.accessibilityArea.frame = CGRect(origin: CGPoint(), size: size) diff --git a/submodules/Display/Source/ActionSheetCheckboxItem.swift b/submodules/Display/Source/ActionSheetCheckboxItem.swift index ec1828d03e0..50eba712dc2 100644 --- a/submodules/Display/Source/ActionSheetCheckboxItem.swift +++ b/submodules/Display/Source/ActionSheetCheckboxItem.swift @@ -40,11 +40,10 @@ public class ActionSheetCheckboxItem: ActionSheetItem { } public class ActionSheetCheckboxItemNode: ActionSheetItemNode { - private let defaultFont: UIFont - private let theme: ActionSheetControllerTheme private var item: ActionSheetCheckboxItem? + private var usesVerticalTextLayout = false private let button: HighlightTrackingButton private let titleNode: ImmediateTextNode @@ -55,19 +54,18 @@ public class ActionSheetCheckboxItemNode: ActionSheetItemNode { override public init(theme: ActionSheetControllerTheme) { self.theme = theme - self.defaultFont = Font.regular(floor(theme.baseFontSize * 20.0 / 17.0)) self.button = HighlightTrackingButton() self.button.isAccessibilityElement = false self.titleNode = ImmediateTextNode() - self.titleNode.maximumNumberOfLines = 1 + self.titleNode.maximumNumberOfLines = 0 self.titleNode.isUserInteractionEnabled = false self.titleNode.displaysAsynchronously = false self.titleNode.isAccessibilityElement = false self.labelNode = ImmediateTextNode() - self.labelNode.maximumNumberOfLines = 1 + self.labelNode.maximumNumberOfLines = 0 self.labelNode.isUserInteractionEnabled = false self.labelNode.displaysAsynchronously = false self.labelNode.isAccessibilityElement = false @@ -120,13 +118,17 @@ public class ActionSheetCheckboxItemNode: ActionSheetItemNode { func setItem(_ item: ActionSheetCheckboxItem) { self.item = item - let defaultFont = Font.regular(floor(theme.baseFontSize * 20.0 / 17.0)) + let baseFontSize = floor(theme.baseFontSize * 20.0 / 17.0) + let scaledFontSize = UIFontMetrics(forTextStyle: .body).scaledValue(for: baseFontSize) + let defaultFont = Font.regular(scaledFontSize) + self.usesVerticalTextLayout = !item.label.isEmpty && scaledFontSize > baseFontSize * 1.2 self.titleNode.attributedText = NSAttributedString(string: item.title, font: defaultFont, textColor: self.theme.primaryTextColor) self.labelNode.attributedText = NSAttributedString(string: item.label, font: defaultFont, textColor: self.theme.secondaryTextColor) self.checkNode.isHidden = !item.value self.accessibilityArea.accessibilityLabel = item.title + self.accessibilityArea.accessibilityValue = item.label.isEmpty ? nil : item.label var accessibilityTraits: UIAccessibilityTraits = [.button] if item.value { @@ -136,21 +138,40 @@ public class ActionSheetCheckboxItemNode: ActionSheetItemNode { } public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { - let size = CGSize(width: constrainedSize.width, height: 57.0) - - self.button.frame = CGRect(origin: CGPoint(), size: size) - var titleOrigin: CGFloat = 50.0 var checkOrigin: CGFloat = 27.0 + var rightInset: CGFloat = 15.0 if let item = self.item, item.style == .alignRight { titleOrigin = 24.0 - checkOrigin = size.width - 22.0 + checkOrigin = constrainedSize.width - 22.0 + rightInset = 50.0 + } + + let contentWidth = max(1.0, constrainedSize.width - titleOrigin - rightInset) + let labelSize: CGSize + let titleSize: CGSize + let textHeight: CGFloat + if self.usesVerticalTextLayout { + titleSize = self.titleNode.updateLayout(CGSize(width: contentWidth, height: constrainedSize.height)) + labelSize = self.labelNode.updateLayout(CGSize(width: contentWidth, height: constrainedSize.height)) + textHeight = titleSize.height + 4.0 + labelSize.height + } else { + labelSize = self.labelNode.updateLayout(CGSize(width: contentWidth * 0.45, height: constrainedSize.height)) + titleSize = self.titleNode.updateLayout(CGSize(width: max(1.0, contentWidth - labelSize.width - 8.0), height: constrainedSize.height)) + textHeight = max(titleSize.height, labelSize.height) } + let size = CGSize(width: constrainedSize.width, height: max(57.0, textHeight + 28.0)) + + self.button.frame = CGRect(origin: CGPoint(), size: size) - let labelSize = self.labelNode.updateLayout(CGSize(width: size.width - 44.0 - 15.0 - 8.0, height: size.height)) - let titleSize = self.titleNode.updateLayout(CGSize(width: size.width - 44.0 - labelSize.width - 15.0 - 8.0, height: size.height)) - self.titleNode.frame = CGRect(origin: CGPoint(x: titleOrigin, y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize) - self.labelNode.frame = CGRect(origin: CGPoint(x: size.width - 15.0 - labelSize.width, y: floorToScreenPixels((size.height - labelSize.height) / 2.0)), size: labelSize) + if self.usesVerticalTextLayout { + let textOrigin = floorToScreenPixels((size.height - textHeight) / 2.0) + self.titleNode.frame = CGRect(origin: CGPoint(x: titleOrigin, y: textOrigin), size: titleSize) + self.labelNode.frame = CGRect(origin: CGPoint(x: titleOrigin, y: textOrigin + titleSize.height + 4.0), size: labelSize) + } else { + self.titleNode.frame = CGRect(origin: CGPoint(x: titleOrigin, y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize) + self.labelNode.frame = CGRect(origin: CGPoint(x: size.width - 15.0 - labelSize.width, y: floorToScreenPixels((size.height - labelSize.height) / 2.0)), size: labelSize) + } if let image = self.checkNode.image { self.checkNode.frame = CGRect(origin: CGPoint(x: floor(checkOrigin - (image.size.width / 2.0)), y: floor((size.height - image.size.height) / 2.0)), size: image.size) diff --git a/submodules/Display/Source/ActionSheetController.swift b/submodules/Display/Source/ActionSheetController.swift index 8727b6e887b..91821ca42cf 100644 --- a/submodules/Display/Source/ActionSheetController.swift +++ b/submodules/Display/Source/ActionSheetController.swift @@ -22,6 +22,9 @@ open class ActionSheetController: ViewController, PresentableController, Standal private var groups: [ActionSheetItemGroup] = [] private var isDismissed: Bool = false + private weak var previousAccessibilityFocus: AnyObject? + private var contentSizeCategoryObserver: NSObjectProtocol? + private var reduceTransparencyObserver: NSObjectProtocol? public var dismissed: ((Bool) -> Void)? @@ -35,11 +38,34 @@ open class ActionSheetController: ViewController, PresentableController, Standal self.statusBar.statusBarStyle = .Ignore self.blocksBackgroundWhenInOverlay = true + + self.contentSizeCategoryObserver = NotificationCenter.default.addObserver(forName: UIContentSizeCategory.didChangeNotification, object: nil, queue: .main, using: { [weak self] _ in + guard let self, self.isViewLoaded else { + return + } + self.actionSheetNode.setGroups(self.groups) + UIAccessibility.post(notification: .layoutChanged, argument: firstAccessibilityElement(in: self.actionSheetNode.view) ?? self.actionSheetNode.view) + }) + self.reduceTransparencyObserver = NotificationCenter.default.addObserver(forName: UIAccessibility.reduceTransparencyStatusDidChangeNotification, object: nil, queue: .main, using: { [weak self] _ in + guard let self, self.isViewLoaded else { + return + } + self.actionSheetNode.setGroups(self.groups) + }) } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + deinit { + if let contentSizeCategoryObserver = self.contentSizeCategoryObserver { + NotificationCenter.default.removeObserver(contentSizeCategoryObserver) + } + if let reduceTransparencyObserver = self.reduceTransparencyObserver { + NotificationCenter.default.removeObserver(reduceTransparencyObserver) + } + } public func dismissAnimated() { if !self.isDismissed { @@ -48,13 +74,27 @@ open class ActionSheetController: ViewController, PresentableController, Standal } } + open override func accessibilityPerformEscape() -> Bool { + if self.isDismissed { + return false + } + self.isDismissed = true + self.actionSheetNode.animateOut(cancelled: true) + return true + } + open override func loadDisplayNode() { self.displayNode = ActionSheetControllerNode(theme: self.theme, allowInputInset: self.allowInputInset) self.displayNodeDidLoad() self.actionSheetNode.dismiss = { [weak self] cancelled in - self?.dismissed?(cancelled) - self?.presentingViewController?.dismiss(animated: false) + guard let self else { + return + } + self.dismissed?(cancelled) + self.presentingViewController?.dismiss(animated: false, completion: { [weak self] in + self?.restoreAccessibilityFocus() + }) } self.actionSheetNode.setGroups(self.groups) @@ -72,8 +112,31 @@ open class ActionSheetController: ViewController, PresentableController, Standal self.viewDidAppear(completion: {}) } + open override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + if self.previousAccessibilityFocus == nil { + self.previousAccessibilityFocus = UIAccessibility.focusedElement(using: .notificationVoiceOver) as AnyObject? + } + } + public func viewDidAppear(completion: @escaping () -> Void) { - self.actionSheetNode.animateIn(completion: completion) + self.actionSheetNode.animateIn { [weak self] in + completion() + + guard let self else { + return + } + UIAccessibility.post(notification: .screenChanged, argument: firstAccessibilityElement(in: self.actionSheetNode.view) ?? self.actionSheetNode.view) + } + } + + private func restoreAccessibilityFocus() { + guard let previousAccessibilityFocus = self.previousAccessibilityFocus else { + return + } + self.previousAccessibilityFocus = nil + UIAccessibility.post(notification: .layoutChanged, argument: previousAccessibilityFocus) } public func setItemGroups(_ groups: [ActionSheetItemGroup]) { diff --git a/submodules/Display/Source/ActionSheetControllerNode.swift b/submodules/Display/Source/ActionSheetControllerNode.swift index fc4d61c7528..e49a8b3eb96 100644 --- a/submodules/Display/Source/ActionSheetControllerNode.swift +++ b/submodules/Display/Source/ActionSheetControllerNode.swift @@ -63,6 +63,14 @@ final class ActionSheetControllerNode: ASDisplayNode, ASScrollViewDelegate { self.itemGroupsContainerNode.isUserInteractionEnabled = false super.init() + + self.view.accessibilityViewIsModal = true + self.dismissTapView.isAccessibilityElement = false + self.dismissTapView.accessibilityElementsHidden = true + self.leftDimView.accessibilityElementsHidden = true + self.rightDimView.accessibilityElementsHidden = true + self.topDimView.accessibilityElementsHidden = true + self.bottomDimView.accessibilityElementsHidden = true self.scrollView.delegate = self.wrappedScrollViewDelegate @@ -150,6 +158,12 @@ final class ActionSheetControllerNode: ASDisplayNode, ASScrollViewDelegate { func animateIn(completion: @escaping () -> Void) { + if UIAccessibility.isReduceMotionEnabled { + self.itemGroupsContainerNode.isUserInteractionEnabled = true + completion() + return + } + let tempDimView = UIView() tempDimView.backgroundColor = self.theme.dimColor tempDimView.frame = self.bounds.offsetBy(dx: 0.0, dy: -self.bounds.size.height) @@ -172,6 +186,11 @@ final class ActionSheetControllerNode: ASDisplayNode, ASScrollViewDelegate { } func animateOut(cancelled: Bool) { + if UIAccessibility.isReduceMotionEnabled { + self.dismiss(cancelled) + return + } + let tempDimView = UIView() tempDimView.backgroundColor = self.theme.dimColor tempDimView.frame = self.bounds.offsetBy(dx: 0.0, dy: -self.bounds.size.height) @@ -235,6 +254,10 @@ final class ActionSheetControllerNode: ASDisplayNode, ASScrollViewDelegate { func setGroups(_ groups: [ActionSheetItemGroup]) { self.itemGroupsContainerNode.setGroups(groups) + + if let validLayout = self.validLayout { + self.containerLayoutUpdated(validLayout, transition: .immediate) + } } func updateItem(groupIndex: Int, itemIndex: Int, _ f: (ActionSheetItem) -> ActionSheetItem) { diff --git a/submodules/Display/Source/ActionSheetItemGroupNode.swift b/submodules/Display/Source/ActionSheetItemGroupNode.swift index af9d79fc277..b4abcbe216f 100644 --- a/submodules/Display/Source/ActionSheetItemGroupNode.swift +++ b/submodules/Display/Source/ActionSheetItemGroupNode.swift @@ -40,8 +40,11 @@ final class ActionSheetItemGroupNode: ASDisplayNode, ASScrollViewDelegate { self.clippingNode = ASDisplayNode() self.clippingNode.clipsToBounds = true self.clippingNode.cornerRadius = 16.0 + if UIAccessibility.isReduceTransparencyEnabled { + self.clippingNode.backgroundColor = self.theme.itemBackgroundColor.withAlphaComponent(1.0) + } - self.backgroundEffectView = UIVisualEffectView(effect: UIBlurEffect(style: self.theme.backgroundType == .light ? .light : .dark)) + self.backgroundEffectView = UIVisualEffectView(effect: UIAccessibility.isReduceTransparencyEnabled ? nil : UIBlurEffect(style: self.theme.backgroundType == .light ? .light : .dark)) self.scrollNode = ASScrollNode() self.scrollNode.canCancelAllTouchesInViews = true diff --git a/submodules/Display/Source/ActionSheetSwitchItem.swift b/submodules/Display/Source/ActionSheetSwitchItem.swift index 75d0651a30f..6763dd1f2d2 100644 --- a/submodules/Display/Source/ActionSheetSwitchItem.swift +++ b/submodules/Display/Source/ActionSheetSwitchItem.swift @@ -49,7 +49,7 @@ public class ActionSheetSwitchNode: ActionSheetItemNode { self.label = ImmediateTextNode() self.label.isUserInteractionEnabled = false - self.label.maximumNumberOfLines = 1 + self.label.maximumNumberOfLines = 0 self.label.displaysAsynchronously = false self.label.truncationType = .end self.label.isAccessibilityElement = false @@ -86,7 +86,7 @@ public class ActionSheetSwitchNode: ActionSheetItemNode { func setItem(_ item: ActionSheetSwitchItem) { self.item = item - let defaultFont = Font.regular(floor(theme.baseFontSize * 20.0 / 17.0)) + let defaultFont = Font.regular(UIFontMetrics(forTextStyle: .body).scaledValue(for: floor(theme.baseFontSize * 20.0 / 17.0))) self.label.attributedText = NSAttributedString(string: item.title, font: defaultFont, textColor: self.theme.primaryTextColor) self.label.isAccessibilityElement = false @@ -103,13 +103,6 @@ public class ActionSheetSwitchNode: ActionSheetItemNode { } public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { - let size = CGSize(width: constrainedSize.width, height: 57.0) - - self.button.frame = CGRect(origin: CGPoint(), size: size) - - let labelSize = self.label.updateLayout(CGSize(width: max(1.0, size.width - 51.0 - 16.0 * 2.0), height: size.height)) - self.label.frame = CGRect(origin: CGPoint(x: 16.0, y: floorToScreenPixels((size.height - labelSize.height) / 2.0)), size: labelSize) - var switchSize = CGSize(width: 51.0, height: 31.0) if let switchView = self.switchNode.view as? UISwitch { if self.switchNode.bounds.size.width.isZero { @@ -117,6 +110,12 @@ public class ActionSheetSwitchNode: ActionSheetItemNode { } switchSize = switchView.bounds.size } + + let labelSize = self.label.updateLayout(CGSize(width: max(1.0, constrainedSize.width - switchSize.width - 16.0 * 3.0), height: constrainedSize.height)) + let size = CGSize(width: constrainedSize.width, height: max(57.0, labelSize.height + 28.0)) + + self.button.frame = CGRect(origin: CGPoint(), size: size) + self.label.frame = CGRect(origin: CGPoint(x: 16.0, y: floorToScreenPixels((size.height - labelSize.height) / 2.0)), size: labelSize) self.switchNode.frame = CGRect(origin: CGPoint(x: size.width - 16.0 - switchSize.width, y: floor((size.height - switchSize.height) / 2.0)), size: switchSize) self.accessibilityArea.frame = CGRect(origin: CGPoint(), size: size) @@ -127,7 +126,7 @@ public class ActionSheetSwitchNode: ActionSheetItemNode { @objc func buttonPressed() { let value = !self.switchNode.isOn - self.switchNode.setOn(value, animated: true) + self.switchNode.setOn(value, animated: !UIAccessibility.isReduceMotionEnabled) self.item?.action(value) } } diff --git a/submodules/Display/Source/ActionSheetTextItem.swift b/submodules/Display/Source/ActionSheetTextItem.swift index 7921a5fe45a..ec7ecbfc750 100644 --- a/submodules/Display/Source/ActionSheetTextItem.swift +++ b/submodules/Display/Source/ActionSheetTextItem.swift @@ -78,8 +78,9 @@ public class ActionSheetTextNode: ActionSheetItemNode { fontSize = 15.0 } - let defaultFont = Font.regular(floor(self.theme.baseFontSize * fontSize / 17.0)) - let boldFont = Font.semibold(floor(self.theme.baseFontSize * fontSize / 17.0)) + let scaledFontSize = UIFontMetrics(forTextStyle: .body).scaledValue(for: floor(self.theme.baseFontSize * fontSize / 17.0)) + let defaultFont = Font.regular(scaledFontSize) + let boldFont = Font.semibold(scaledFontSize) if item.parseMarkdown { let body = MarkdownAttributeSet(font: defaultFont, textColor: self.theme.secondaryTextColor) diff --git a/submodules/Display/Source/AlertContentNode.swift b/submodules/Display/Source/AlertContentNode.swift index b95b6e38ad1..faee3a61f38 100644 --- a/submodules/Display/Source/AlertContentNode.swift +++ b/submodules/Display/Source/AlertContentNode.swift @@ -4,6 +4,10 @@ import AsyncDisplayKit open class AlertContentNode: ASDisplayNode { open var requestLayout: ((ContainedViewLayoutTransition) -> Void)? + + open var accessibilityInitialFocusNode: ASDisplayNode? { + return nil + } open var dismissOnOutsideTap: Bool { return true @@ -19,6 +23,10 @@ open class AlertContentNode: ASDisplayNode { } + open func contentSizeCategoryUpdated() { + + } + open func performHighlightedAction() { } diff --git a/submodules/Display/Source/AlertController.swift b/submodules/Display/Source/AlertController.swift index 19e64a13fd0..fa7c3571d48 100644 --- a/submodules/Display/Source/AlertController.swift +++ b/submodules/Display/Source/AlertController.swift @@ -87,6 +87,9 @@ open class AlertController: ViewController, StandalonePresentableController, Key private let allowInputInset: Bool private weak var existingAlertController: AlertController? + private weak var previousAccessibilityFocus: AnyObject? + private var contentSizeCategoryObserver: NSObjectProtocol? + private var reduceTransparencyObserver: NSObjectProtocol? public var willDismiss: (() -> Void)? public var dismissed: ((Bool) -> Void)? @@ -102,11 +105,30 @@ open class AlertController: ViewController, StandalonePresentableController, Key self.blocksBackgroundWhenInOverlay = true self.statusBar.statusBarStyle = .Ignore + + self.contentSizeCategoryObserver = NotificationCenter.default.addObserver(forName: UIContentSizeCategory.didChangeNotification, object: nil, queue: .main, using: { [weak self] _ in + self?.contentNode.contentSizeCategoryUpdated() + }) + self.reduceTransparencyObserver = NotificationCenter.default.addObserver(forName: UIAccessibility.reduceTransparencyStatusDidChangeNotification, object: nil, queue: .main, using: { [weak self] _ in + guard let self, self.isViewLoaded else { + return + } + self.controllerNode.updateTheme(self.theme) + }) } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + deinit { + if let contentSizeCategoryObserver = self.contentSizeCategoryObserver { + NotificationCenter.default.removeObserver(contentSizeCategoryObserver) + } + if let reduceTransparencyObserver = self.reduceTransparencyObserver { + NotificationCenter.default.removeObserver(reduceTransparencyObserver) + } + } private var isDismissed = false override open func loadDisplayNode() { @@ -130,10 +152,25 @@ open class AlertController: ViewController, StandalonePresentableController, Key override open func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) + self.existingAlertController?.previousAccessibilityFocus = nil self.existingAlertController?.dismiss(completion: nil) self.existingAlertController = nil self.controllerNode.animateIn() + UIAccessibility.post( + notification: .screenChanged, + argument: self.contentNode.accessibilityInitialFocusNode?.view ?? firstAccessibilityElement(in: self.contentNode.view) ?? self.contentNode.view + ) + } + + override open func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + if let existingAlertController = self.existingAlertController { + self.previousAccessibilityFocus = existingAlertController.previousAccessibilityFocus + } else if self.previousAccessibilityFocus == nil { + self.previousAccessibilityFocus = UIAccessibility.focusedElement(using: .notificationVoiceOver) as AnyObject? + } } override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { @@ -147,7 +184,10 @@ open class AlertController: ViewController, StandalonePresentableController, Key self.isDismissed = true self.dismissed?(false) } - self.presentingViewController?.dismiss(animated: false, completion: completion) + self.presentingViewController?.dismiss(animated: false, completion: { [weak self] in + self?.restoreAccessibilityFocus() + completion?() + }) } open func dismissAnimated() { @@ -156,6 +196,27 @@ open class AlertController: ViewController, StandalonePresentableController, Key } } + override open func accessibilityPerformEscape() -> Bool { + guard self.contentNode.dismissOnOutsideTap, !self.isDismissed else { + return false + } + self.willDismiss?() + self.controllerNode.animateOut { [weak self] in + self?.dismissed?(true) + self?.isDismissed = true + self?.dismiss() + } + return true + } + + private func restoreAccessibilityFocus() { + guard let previousAccessibilityFocus = self.previousAccessibilityFocus else { + return + } + self.previousAccessibilityFocus = nil + UIAccessibility.post(notification: .layoutChanged, argument: previousAccessibilityFocus) + } + public var keyShortcuts: [KeyShortcut] { return [ KeyShortcut( diff --git a/submodules/Display/Source/AlertControllerNode.swift b/submodules/Display/Source/AlertControllerNode.swift index 8f4970eaef1..3dfce2befd9 100644 --- a/submodules/Display/Source/AlertControllerNode.swift +++ b/submodules/Display/Source/AlertControllerNode.swift @@ -50,18 +50,22 @@ final class AlertControllerNode: ASDisplayNode { self.containerNode.layer.masksToBounds = true self.backgroundNode = ASDisplayNode() - self.backgroundNode.backgroundColor = theme.backgroundColor + self.backgroundNode.backgroundColor = UIAccessibility.isReduceTransparencyEnabled ? theme.backgroundColor.withAlphaComponent(1.0) : theme.backgroundColor // self.effectNode = ASDisplayNode(viewBlock: { // return UIVisualEffectView(effect: UIBlurEffect(style: theme.backgroundType == .light ? .light : .dark)) // }) - self.effectView = UIVisualEffectView(effect: UIBlurEffect(style: theme.backgroundType == .light ? .light : .dark)) + self.effectView = UIVisualEffectView(effect: UIAccessibility.isReduceTransparencyEnabled ? nil : UIBlurEffect(style: theme.backgroundType == .light ? .light : .dark)) self.contentNode = contentNode super.init() + self.view.accessibilityViewIsModal = true + self.dimContainerView.isAccessibilityElement = false + self.dimContainerView.accessibilityElementsHidden = true + self.view.addSubview(self.dimContainerView) self.dimContainerView.addSubview(self.centerDimView) self.dimContainerView.addSubview(self.topDimView) @@ -104,12 +108,16 @@ final class AlertControllerNode: ASDisplayNode { } func updateTheme(_ theme: AlertControllerTheme) { - self.effectView.effect = UIBlurEffect(style: theme.backgroundType == .light ? .light : .dark) - self.backgroundNode.backgroundColor = theme.backgroundColor + self.effectView.effect = UIAccessibility.isReduceTransparencyEnabled ? nil : UIBlurEffect(style: theme.backgroundType == .light ? .light : .dark) + self.backgroundNode.backgroundColor = UIAccessibility.isReduceTransparencyEnabled ? theme.backgroundColor.withAlphaComponent(1.0) : theme.backgroundColor self.contentNode.updateTheme(theme) } func animateIn() { + if UIAccessibility.isReduceMotionEnabled { + return + } + if let previousNode = self.existingAlertControllerNode { let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .spring) @@ -146,6 +154,11 @@ final class AlertControllerNode: ASDisplayNode { } func animateOut(completion: @escaping () -> Void) { + if UIAccessibility.isReduceMotionEnabled { + completion() + return + } + self.containerNode.layer.removeAllAnimations() //self.centerDimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5) //self.centerDimView.image = nil diff --git a/submodules/Display/Source/ListView.swift b/submodules/Display/Source/ListView.swift index 6e945691f3a..92ff34d3cc6 100644 --- a/submodules/Display/Source/ListView.swift +++ b/submodules/Display/Source/ListView.swift @@ -5442,7 +5442,7 @@ open class ListViewImpl: ASDisplayNode, ListView, ASScrollViewDelegate, ASGestur if let (_, frame) = accessibilityFocusedNode { for itemNode in self.itemNodes { if frame.intersects(itemNode.frame) { - UIAccessibility.post(notification: UIAccessibility.Notification.layoutChanged, argument: itemNode.view) + UIAccessibility.post(notification: UIAccessibility.Notification.layoutChanged, argument: firstAccessibilityElement(in: itemNode.view) ?? itemNode.view) if let index = itemNode.index { let scrollStatus: String if let accessibilityPageScrolledString = self.accessibilityPageScrolledString { @@ -5465,8 +5465,10 @@ open class ListViewImpl: ASDisplayNode, ListView, ASScrollViewDelegate, ASGestur switch direction { case .down: scrollDirection = self.rotated ? .up : .down - default: + case .up: scrollDirection = self.rotated ? .down : .up + default: + return false } return self.scrollWithDirection(scrollDirection, distance: distance) } @@ -5477,8 +5479,17 @@ open class ListViewImpl: ASDisplayNode, ListView, ASScrollViewDelegate, ASGestur } private func findAccessibilityFocus(_ node: ASDisplayNode) -> Bool { - if node.view.accessibilityElementIsFocused() { + return containsAccessibilityFocus(node.view) +} + +private func containsAccessibilityFocus(_ view: UIView) -> Bool { + if view.accessibilityElementIsFocused() { return true } + for subview in view.subviews { + if containsAccessibilityFocus(subview) { + return true + } + } return false } diff --git a/submodules/Display/Source/TextAlertController.swift b/submodules/Display/Source/TextAlertController.swift index 2224e1a4d0c..4b7cc74d0f5 100644 --- a/submodules/Display/Source/TextAlertController.swift +++ b/submodules/Display/Source/TextAlertController.swift @@ -46,7 +46,7 @@ public final class TextAlertContentActionNode: HighlightableButtonNode { super.init() - self.titleNode.maximumNumberOfLines = 2 + self.titleNode.maximumNumberOfLines = 0 self.highligthedChanged = { [weak self] value in if let strongSelf = self { @@ -110,7 +110,8 @@ public final class TextAlertContentActionNode: HighlightableButtonNode { } private func updateTitle() { - var font = Font.regular(theme.baseFontSize) + let fontSize = UIFontMetrics(forTextStyle: .body).scaledValue(for: theme.baseFontSize) + var font = Font.regular(fontSize) var color: UIColor switch self.action.type { case .defaultAction, .genericAction: @@ -120,7 +121,7 @@ public final class TextAlertContentActionNode: HighlightableButtonNode { } switch self.action.type { case .defaultAction, .defaultDestructiveAction: - font = Font.semibold(theme.baseFontSize) + font = Font.semibold(fontSize) case .destructiveAction, .genericAction: break } @@ -134,7 +135,11 @@ public final class TextAlertContentActionNode: HighlightableButtonNode { self.setAttributedTitle(attributedString, for: []) self.accessibilityLabel = self.action.title - self.accessibilityTraits = [.button] + var accessibilityTraits: UIAccessibilityTraits = [.button] + if !self.actionEnabled { + accessibilityTraits.insert(.notEnabled) + } + self.accessibilityTraits = accessibilityTraits } @objc func pressed() { @@ -153,6 +158,16 @@ public enum TextAlertContentActionLayout { case vertical } +private final class TextAlertAccessibilityCustomAction: UIAccessibilityCustomAction { + let perform: () -> Void + + init(name: String, target: Any?, selector: Selector, perform: @escaping () -> Void) { + self.perform = perform + + super.init(name: name, target: target, selector: selector) + } +} + public final class TextAlertContentNode: AlertContentNode { private var theme: AlertControllerTheme private let actionLayout: TextAlertContentActionLayout @@ -163,6 +178,10 @@ public final class TextAlertContentNode: AlertContentNode { private let actionNodesSeparator: ASDisplayNode private let actionNodes: [TextAlertContentActionNode] private let actionVerticalSeparators: [ASDisplayNode] + private let dynamicTypeTitle: String? + private let dynamicTypeText: String? + private let dynamicTypeParseMarkdown: Bool + private let linkAction: (([NSAttributedString.Key: Any], Int) -> Void)? private var validLayout: CGSize? @@ -171,6 +190,10 @@ public final class TextAlertContentNode: AlertContentNode { return self._dismissOnOutsideTap } + override public var accessibilityInitialFocusNode: ASDisplayNode? { + return self.titleNode ?? self.textNode + } + private var highlightedItemIndex: Int? = nil public var textAttributeAction: (NSAttributedString.Key, (Any) -> Void)? { @@ -193,22 +216,28 @@ public final class TextAlertContentNode: AlertContentNode { self.textNode.highlightAttributeAction = nil self.textNode.tapAttributeAction = nil } + self.updateTextAccessibilityActions() } } - public init(theme: AlertControllerTheme, title: NSAttributedString?, text: NSAttributedString, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout, dismissOnOutsideTap: Bool, linkAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil) { + public init(theme: AlertControllerTheme, title: NSAttributedString?, text: NSAttributedString, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout, dismissOnOutsideTap: Bool, linkAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil, dynamicTypeTitle: String? = nil, dynamicTypeText: String? = nil, dynamicTypeParseMarkdown: Bool = false) { self.theme = theme self.actionLayout = actionLayout self._dismissOnOutsideTap = dismissOnOutsideTap + self.dynamicTypeTitle = dynamicTypeTitle + self.dynamicTypeText = dynamicTypeText + self.dynamicTypeParseMarkdown = dynamicTypeParseMarkdown + self.linkAction = linkAction if let title = title { let titleNode = ImmediateTextNode() titleNode.attributedText = title titleNode.displaysAsynchronously = false titleNode.isUserInteractionEnabled = false - titleNode.maximumNumberOfLines = 4 + titleNode.maximumNumberOfLines = 0 titleNode.truncationType = .end titleNode.isAccessibilityElement = true titleNode.accessibilityLabel = title.string + titleNode.accessibilityTraits = [.header] self.titleNode = titleNode } else { self.titleNode = nil @@ -281,6 +310,46 @@ public final class TextAlertContentNode: AlertContentNode { for separatorNode in self.actionVerticalSeparators { self.addSubnode(separatorNode) } + + self.updateTextAccessibilityActions() + } + + private func updateTextAccessibilityActions() { + guard let attributedText = self.textNode.attributedText, attributedText.length != 0 else { + self.textNode.accessibilityCustomActions = nil + return + } + + var accessibilityActions: [UIAccessibilityCustomAction] = [] + attributedText.enumerateAttributes(in: NSRange(location: 0, length: attributedText.length), options: []) { [weak self] attributes, range, _ in + guard let self else { + return + } + let actionName = attributedText.attributedSubstring(from: range).string.trimmingCharacters(in: .whitespacesAndNewlines) + guard !actionName.isEmpty else { + return + } + + if self.textAttributeAction == nil, attributes[NSAttributedString.Key(rawValue: "URL")] != nil, let linkAction = self.linkAction { + accessibilityActions.append(TextAlertAccessibilityCustomAction(name: actionName, target: self, selector: #selector(self.performTextAccessibilityAction(_:)), perform: { + linkAction(attributes, range.location) + })) + } + if let (attribute, textAttributeAction) = self.textAttributeAction, let value = attributes[attribute] { + accessibilityActions.append(TextAlertAccessibilityCustomAction(name: actionName, target: self, selector: #selector(self.performTextAccessibilityAction(_:)), perform: { + textAttributeAction(value) + })) + } + } + self.textNode.accessibilityCustomActions = accessibilityActions.isEmpty ? nil : accessibilityActions + } + + @objc private func performTextAccessibilityAction(_ action: UIAccessibilityCustomAction) -> Bool { + guard let action = action as? TextAlertAccessibilityCustomAction else { + return false + } + action.perform() + return true } func setHighlightedItemIndex(_ index: Int?, update: Bool = false) { @@ -353,6 +422,19 @@ public final class TextAlertContentNode: AlertContentNode { } } + override public func contentSizeCategoryUpdated() { + for actionNode in self.actionNodes { + actionNode.updateTheme(self.theme) + } + if let dynamicTypeText = self.dynamicTypeText { + let attributedStrings = standardTextAlertAttributedStrings(theme: self.theme, title: self.dynamicTypeTitle, text: dynamicTypeText, parseMarkdown: self.dynamicTypeParseMarkdown) + self.titleNode?.attributedText = attributedStrings.title + self.textNode.attributedText = attributedStrings.text + self.updateTextAccessibilityActions() + } + self.requestLayout?(.immediate) + } + override public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { self.validLayout = size @@ -367,24 +449,21 @@ public final class TextAlertContentNode: AlertContentNode { } let textSize = self.textNode.updateLayout(CGSize(width: size.width - insets.left - insets.right, height: CGFloat.greatestFiniteMagnitude)) - let actionButtonHeight: CGFloat = 44.0 - - var minActionsWidth: CGFloat = 0.0 - let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) - let actionTitleInsets: CGFloat = 8.0 + let minimumActionButtonHeight: CGFloat = 44.0 + let maxActionWidth: CGFloat = self.actionNodes.isEmpty ? size.width : floor(size.width / CGFloat(self.actionNodes.count)) var effectiveActionLayout = self.actionLayout + if self.traitCollection.preferredContentSizeCategory.isAccessibilityCategory { + effectiveActionLayout = .vertical + } + var actionHeights: [CGFloat] = [] for actionNode in self.actionNodes { - let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight)) - if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 { + let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: max(1.0, maxActionWidth - 16.0), height: CGFloat.greatestFiniteMagnitude)) + let actionHeight = max(minimumActionButtonHeight, actionTitleSize.height + 20.0) + actionHeights.append(actionHeight) + if case .horizontal = effectiveActionLayout, actionHeight > minimumActionButtonHeight { effectiveActionLayout = .vertical } - switch effectiveActionLayout { - case .horizontal: - minActionsWidth += actionTitleSize.width + actionTitleInsets - case .vertical: - minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets) - } } let resultSize: CGSize @@ -392,9 +471,9 @@ public final class TextAlertContentNode: AlertContentNode { var actionsHeight: CGFloat = 0.0 switch effectiveActionLayout { case .horizontal: - actionsHeight = actionButtonHeight + actionsHeight = actionHeights.max() ?? minimumActionButtonHeight case .vertical: - actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) + actionsHeight = actionHeights.reduce(0.0, +) } let contentWidth = alertWidth - insets.left - insets.right @@ -414,10 +493,11 @@ public final class TextAlertContentNode: AlertContentNode { resultSize = CGSize(width: contentWidth + insets.left + insets.right, height: textSize.height + actionsHeight + insets.top + insets.bottom) } + self.actionNodesSeparator.isHidden = self.actionNodes.isEmpty self.actionNodesSeparator.frame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)) var actionOffset: CGFloat = 0.0 - let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count)) + let actionWidth: CGFloat = self.actionNodes.isEmpty ? resultSize.width : floor(resultSize.width / CGFloat(self.actionNodes.count)) var separatorIndex = -1 var nodeIndex = 0 for actionNode in self.actionNodes { @@ -447,11 +527,12 @@ public final class TextAlertContentNode: AlertContentNode { let actionNodeFrame: CGRect switch effectiveActionLayout { case .horizontal: - actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionsHeight)) actionOffset += currentActionWidth case .vertical: - actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) - actionOffset += actionButtonHeight + let actionHeight = actionHeights[nodeIndex] + actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionHeight)) + actionOffset += actionHeight } transition.updateFrame(node: actionNode, frame: actionNodeFrame) @@ -463,16 +544,18 @@ public final class TextAlertContentNode: AlertContentNode { } } -public func textAlertController(theme: AlertControllerTheme, title: NSAttributedString?, text: NSAttributedString, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout = .horizontal, dismissOnOutsideTap: Bool = true, linkAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil) -> AlertController { - return AlertController(theme: theme, contentNode: TextAlertContentNode(theme: theme, title: title, text: text, actions: actions, actionLayout: actionLayout, dismissOnOutsideTap: dismissOnOutsideTap, linkAction: linkAction)) -} +private func standardTextAlertAttributedStrings(theme: AlertControllerTheme, title: String?, text: String, parseMarkdown: Bool) -> (title: NSAttributedString?, text: NSAttributedString) { + let titleFontSize = UIFontMetrics(forTextStyle: .headline).scaledValue(for: theme.baseFontSize) + let bodyBaseFontSize = title == nil ? theme.baseFontSize : floor(theme.baseFontSize * 13.0 / 17.0) + let bodyFontSize = UIFontMetrics(forTextStyle: .body).scaledValue(for: bodyBaseFontSize) -public func standardTextAlertController(theme: AlertControllerTheme, title: String?, text: String, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout = .horizontal, allowInputInset: Bool = true, parseMarkdown: Bool = false, dismissOnOutsideTap: Bool = true, linkAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil) -> AlertController { - var dismissImpl: (() -> Void)? + let attributedTitle = title.flatMap { + NSAttributedString(string: $0, font: Font.semibold(titleFontSize), textColor: theme.primaryColor, paragraphAlignment: .center) + } let attributedText: NSAttributedString if parseMarkdown { - let font = title == nil ? Font.semibold(theme.baseFontSize) : Font.regular(floor(theme.baseFontSize * 13.0 / 17.0)) - let boldFont = title == nil ? Font.bold(theme.baseFontSize) : Font.semibold(floor(theme.baseFontSize * 13.0 / 17.0)) + let font = title == nil ? Font.semibold(bodyFontSize) : Font.regular(bodyFontSize) + let boldFont = title == nil ? Font.bold(bodyFontSize) : Font.semibold(bodyFontSize) let body = MarkdownAttributeSet(font: font, textColor: theme.primaryColor) let bold = MarkdownAttributeSet(font: boldFont, textColor: theme.primaryColor) let link = MarkdownAttributeSet(font: font, textColor: theme.accentColor) @@ -480,14 +563,25 @@ public func standardTextAlertController(theme: AlertControllerTheme, title: Stri return ("URL", url) }), textAlignment: .center) } else { - attributedText = NSAttributedString(string: text, font: title == nil ? Font.semibold(theme.baseFontSize) : Font.regular(floor(theme.baseFontSize * 13.0 / 17.0)), textColor: theme.primaryColor, paragraphAlignment: .center) + let font = title == nil ? Font.semibold(bodyFontSize) : Font.regular(bodyFontSize) + attributedText = NSAttributedString(string: text, font: font, textColor: theme.primaryColor, paragraphAlignment: .center) } - let controller = AlertController(theme: theme, contentNode: TextAlertContentNode(theme: theme, title: title != nil ? NSAttributedString(string: title!, font: Font.semibold(theme.baseFontSize), textColor: theme.primaryColor, paragraphAlignment: .center) : nil, text: attributedText, actions: actions.map { action in + return (attributedTitle, attributedText) +} + +public func textAlertController(theme: AlertControllerTheme, title: NSAttributedString?, text: NSAttributedString, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout = .horizontal, dismissOnOutsideTap: Bool = true, linkAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil) -> AlertController { + return AlertController(theme: theme, contentNode: TextAlertContentNode(theme: theme, title: title, text: text, actions: actions, actionLayout: actionLayout, dismissOnOutsideTap: dismissOnOutsideTap, linkAction: linkAction)) +} + +public func standardTextAlertController(theme: AlertControllerTheme, title: String?, text: String, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout = .horizontal, allowInputInset: Bool = true, parseMarkdown: Bool = false, dismissOnOutsideTap: Bool = true, linkAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil) -> AlertController { + var dismissImpl: (() -> Void)? + let attributedStrings = standardTextAlertAttributedStrings(theme: theme, title: title, text: text, parseMarkdown: parseMarkdown) + let controller = AlertController(theme: theme, contentNode: TextAlertContentNode(theme: theme, title: attributedStrings.title, text: attributedStrings.text, actions: actions.map { action in return TextAlertAction(type: action.type, title: action.title, action: { dismissImpl?() action.action() }) - }, actionLayout: actionLayout, dismissOnOutsideTap: dismissOnOutsideTap, linkAction: linkAction), allowInputInset: allowInputInset) + }, actionLayout: actionLayout, dismissOnOutsideTap: dismissOnOutsideTap, linkAction: linkAction, dynamicTypeTitle: title, dynamicTypeText: text, dynamicTypeParseMarkdown: parseMarkdown), allowInputInset: allowInputInset) dismissImpl = { [weak controller] in controller?.dismissAnimated() } diff --git a/submodules/ShareController/Sources/ShareActionButtonNode.swift b/submodules/ShareController/Sources/ShareActionButtonNode.swift index 13cf0857c2b..2b1dca4bc76 100644 --- a/submodules/ShareController/Sources/ShareActionButtonNode.swift +++ b/submodules/ShareController/Sources/ShareActionButtonNode.swift @@ -28,6 +28,7 @@ public final class ShareActionButtonNode: HighlightTrackingButtonNode { public var badge: String? { didSet { if self.badge != oldValue { + self.accessibilityValue = self.badge if let badge = self.badge { self.badgeText = NSAttributedString(string: badge, font: Font.regular(14.0), textColor: self.badgeTextColor, paragraphAlignment: .center) self.badgeLabel.isHidden = false @@ -68,6 +69,9 @@ public final class ShareActionButtonNode: HighlightTrackingButtonNode { self.badgeBackground.image = generateStretchableFilledCircleImage(diameter: 22.0, color: badgeBackgroundColor) super.init() + + self.isAccessibilityElement = true + self.accessibilityTraits = .button self.containerNode.addSubnode(self.referenceNode) self.addSubnode(self.containerNode) @@ -147,6 +151,10 @@ public final class ShareStartAtTimestampNode: HighlightTrackingButtonNode { self.titleTextNode.displaysAsynchronously = false super.init() + + self.isAccessibilityElement = true + self.accessibilityLabel = titleText + self.accessibilityTraits = .button self.addSubnode(self.checkNode) self.addSubnode(self.titleTextNode) @@ -156,6 +164,11 @@ public final class ShareStartAtTimestampNode: HighlightTrackingButtonNode { @objc private func pressed() { self.checkNode.setSelected(!self.checkNode.selected, animated: true) + if self.checkNode.selected { + self.accessibilityTraits.insert(.selected) + } else { + self.accessibilityTraits.remove(.selected) + } self.updated?() } diff --git a/submodules/ShareController/Sources/ShareControllerNode.swift b/submodules/ShareController/Sources/ShareControllerNode.swift index 7566da53641..7d69d239609 100644 --- a/submodules/ShareController/Sources/ShareControllerNode.swift +++ b/submodules/ShareController/Sources/ShareControllerNode.swift @@ -706,6 +706,8 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate self.addSubnode(self.wrappingScrollNode) self.cancelButtonNode.setTitle(self.presentationData.strings.Common_Cancel, with: Font.medium(20.0), with: self.presentationData.theme.actionSheet.standardActionTextColor, for: .normal) + self.cancelButtonNode.accessibilityLabel = self.presentationData.strings.Common_Cancel + self.cancelButtonNode.accessibilityTraits = .button self.wrappingScrollNode.addSubnode(self.cancelButtonNode) self.cancelButtonNode.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside) @@ -970,6 +972,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate func setActionNodesHidden(_ hidden: Bool, inputField: Bool = false, actions: Bool = false, animated: Bool = true) { func updateActionNodesAlpha(_ nodes: [ASDisplayNode], alpha: CGFloat) { for node in nodes { + node.accessibilityElementsHidden = alpha.isZero if !node.alpha.isEqual(to: alpha) { let previousAlpha = node.alpha node.alpha = alpha @@ -1005,6 +1008,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate if let previous = previous { previous.setDidBeginDragging(nil) previous.setContentOffsetUpdated(nil) + previous.accessibilityElementsHidden = true if animated { transition = .animated(duration: 0.4, curve: .spring) self.previousContentNode = previous @@ -1032,6 +1036,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate if let (layout, navigationBarHeight, bottomGridInset) = self.containerLayout { if let contentNode = contentNode, let previous = previous { contentNode.frame = previous.frame + contentNode.accessibilityElementsHidden = false contentNode.updateLayout(size: previous.bounds.size, isLandscape: layout.size.width > layout.size.height, bottomInset: bottomGridInset, transition: .immediate) contentNode.setDidBeginDragging({ [weak self] in @@ -1065,6 +1070,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate } } else { if let contentNode = self.contentNode { + contentNode.accessibilityElementsHidden = false contentNode.setDidBeginDragging({ [weak self] in self?.contentNodeDidBeginDragging() }) @@ -1077,6 +1083,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) } } else if let contentNode = contentNode { + contentNode.accessibilityElementsHidden = false contentNode.setContentOffsetUpdated({ [weak self] contentOffset, transition in self?.contentNodeOffsetUpdated(contentOffset, transition: transition) }) @@ -1200,7 +1207,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, - inVoiceOver: false + inVoiceOver: layout.inVoiceOver ) controller.presentationContext.containerLayoutUpdated(subLayout, transition: transition) } @@ -1410,19 +1417,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate } self.inputFieldNode.deactivateInput() - let transition: ContainedViewLayoutTransition - if peerId == nil { - transition = .animated(duration: 0.12, curve: .easeInOut) - } else { - transition = .immediate - } - transition.updateAlpha(node: self.actionButtonNode, alpha: 0.0) - transition.updateAlpha(node: self.inputFieldNode, alpha: 0.0) - transition.updateAlpha(node: self.actionSeparatorNode, alpha: 0.0) - transition.updateAlpha(node: self.actionsBackgroundNode, alpha: 0.0) - if let startAtTimestampNode = self.startAtTimestampNode { - transition.updateAlpha(node: startAtTimestampNode, alpha: 0.0) - } + self.setActionNodesHidden(true, inputField: true, actions: true, animated: peerId == nil) let peerIds: [PeerId] var topicIds: [PeerId: Int64] = [:] @@ -1500,7 +1495,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate if long { strongSelf.transitionToContentNode(ShareProlongedLoadingContainerNode(theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, forceNativeAppearance: true, environment: strongSelf.environment), fastOut: true) } else { - strongSelf.transitionToContentNode(ShareLoadingContainerNode(theme: strongSelf.presentationData.theme, forceNativeAppearance: true), fastOut: true) + strongSelf.transitionToContentNode(ShareLoadingContainerNode(theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, forceNativeAppearance: true), fastOut: true) } } @@ -1715,15 +1710,8 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate case .preparing: if loadingTimestamp == nil { strongSelf.inputFieldNode.deactivateInput() - let transition = ContainedViewLayoutTransition.animated(duration: 0.12, curve: .easeInOut) - transition.updateAlpha(node: strongSelf.actionButtonNode, alpha: 0.0) - transition.updateAlpha(node: strongSelf.inputFieldNode, alpha: 0.0) - transition.updateAlpha(node: strongSelf.actionSeparatorNode, alpha: 0.0) - transition.updateAlpha(node: strongSelf.actionsBackgroundNode, alpha: 0.0) - if let startAtTimestampNode = strongSelf.startAtTimestampNode { - transition.updateAlpha(node: startAtTimestampNode, alpha: 0.0) - } - strongSelf.transitionToContentNode(ShareLoadingContainerNode(theme: strongSelf.presentationData.theme, forceNativeAppearance: true), fastOut: true) + strongSelf.setActionNodesHidden(true, inputField: true, actions: true) + strongSelf.transitionToContentNode(ShareLoadingContainerNode(theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, forceNativeAppearance: true), fastOut: true) loadingTimestamp = CACurrentMediaTime() if reportReady { strongSelf.ready.set(.single(true)) @@ -1840,19 +1828,23 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate if count == 0 { if self.presetText != nil { self.actionButtonNode.setTitle(self.presentationData.strings.ShareMenu_Send, with: Font.medium(20.0), with: self.presentationData.theme.actionSheet.disabledActionTextColor, for: .normal) + self.actionButtonNode.accessibilityLabel = self.presentationData.strings.ShareMenu_Send self.actionButtonNode.isEnabled = false self.actionButtonNode.badge = nil } else if let segmentedValues = self.segmentedValues { let value = segmentedValues[self.selectedSegmentedIndex] self.actionButtonNode.setTitle(value.actionTitle, with: Font.regular(20.0), with: self.presentationData.theme.actionSheet.standardActionTextColor, for: .normal) + self.actionButtonNode.accessibilityLabel = value.actionTitle self.actionButtonNode.isEnabled = true self.actionButtonNode.badge = nil } else if let defaultAction = self.defaultAction { self.actionButtonNode.setTitle(defaultAction.title, with: Font.regular(20.0), with: self.presentationData.theme.actionSheet.standardActionTextColor, for: .normal) + self.actionButtonNode.accessibilityLabel = defaultAction.title self.actionButtonNode.isEnabled = true self.actionButtonNode.badge = nil } else { self.actionButtonNode.setTitle(self.presentationData.strings.ShareMenu_Send, with: Font.medium(20.0), with: self.presentationData.theme.actionSheet.disabledActionTextColor, for: .normal) + self.actionButtonNode.accessibilityLabel = self.presentationData.strings.ShareMenu_Send self.actionButtonNode.isEnabled = false self.actionButtonNode.badge = nil } @@ -1866,20 +1858,14 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate } self.actionButtonNode.isEnabled = true self.actionButtonNode.setTitle(text, with: Font.medium(20.0), with: self.presentationData.theme.actionSheet.standardActionTextColor, for: .normal) + self.actionButtonNode.accessibilityLabel = text self.actionButtonNode.badge = "\(count)" } } func transitionToProgress(signal: Signal) { self.inputFieldNode.deactivateInput() - let transition = ContainedViewLayoutTransition.animated(duration: 0.12, curve: .easeInOut) - transition.updateAlpha(node: self.actionButtonNode, alpha: 0.0) - transition.updateAlpha(node: self.inputFieldNode, alpha: 0.0) - transition.updateAlpha(node: self.actionSeparatorNode, alpha: 0.0) - transition.updateAlpha(node: self.actionsBackgroundNode, alpha: 0.0) - if let startAtTimestampNode = self.startAtTimestampNode { - transition.updateAlpha(node: startAtTimestampNode, alpha: 0.0) - } + self.setActionNodesHidden(true, inputField: true, actions: true) self.transitionToContentNode(ShareProlongedLoadingContainerNode(theme: self.presentationData.theme, strings: self.presentationData.strings, forceNativeAppearance: true, environment: self.environment), fastOut: true) let timestamp = CACurrentMediaTime() @@ -1913,16 +1899,9 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate completion() })) } else { - let transition = ContainedViewLayoutTransition.animated(duration: 0.12, curve: .easeInOut) - transition.updateAlpha(node: self.actionButtonNode, alpha: 0.0) - transition.updateAlpha(node: self.inputFieldNode, alpha: 0.0) - transition.updateAlpha(node: self.actionSeparatorNode, alpha: 0.0) - transition.updateAlpha(node: self.actionsBackgroundNode, alpha: 0.0) - if let startAtTimestampNode = self.startAtTimestampNode { - transition.updateAlpha(node: startAtTimestampNode, alpha: 0.0) - } + self.setActionNodesHidden(true, inputField: true, actions: true) - self.transitionToContentNode(ShareLoadingContainerNode(theme: self.presentationData.theme, forceNativeAppearance: true), fastOut: true) + self.transitionToContentNode(ShareLoadingContainerNode(theme: self.presentationData.theme, strings: self.presentationData.strings, forceNativeAppearance: true), fastOut: true) let timestamp = CACurrentMediaTime() var wasDone = false diff --git a/submodules/ShareController/Sources/ShareControllerPeerGridItem.swift b/submodules/ShareController/Sources/ShareControllerPeerGridItem.swift index 1389f89037b..c4a1463ef47 100644 --- a/submodules/ShareController/Sources/ShareControllerPeerGridItem.swift +++ b/submodules/ShareController/Sources/ShareControllerPeerGridItem.swift @@ -77,6 +77,10 @@ final class ShareControllerGridSectionNode: ASDisplayNode { self.titleNode.truncationMode = .byTruncatingTail super.init() + + self.isAccessibilityElement = true + self.accessibilityTraits = .button + self.peerNode.accessibilityElementsHidden = true self.addSubnode(self.backgroundNode) self.addSubnode(self.titleNode) @@ -255,6 +259,7 @@ final class ShareControllerPeerGridItemNode: GridItemNode { online: isOnline, synchronousLoad: synchronousLoad ) + self.accessibilityLabel = threadData?.info.title ?? peer.compactDisplayTitle if let shimmerNode = self.placeholderNode { self.placeholderNode = nil shimmerNode.removeFromSupernode() @@ -277,7 +282,9 @@ final class ShareControllerPeerGridItemNode: GridItemNode { synchronousLoad: synchronousLoad, storyMode: storyMode ) + self.accessibilityLabel = strings.StoryFeed_ContextAddStory } else { + self.isAccessibilityElement = false let shimmerNode: ShimmerEffectNode if let current = self.placeholderNode { shimmerNode = current @@ -304,6 +311,9 @@ final class ShareControllerPeerGridItemNode: GridItemNode { shimmerNode.update(backgroundColor: theme.list.itemBlocksBackgroundColor, foregroundColor: theme.list.mediaPlaceholderColor, shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: shapes, horizontal: true, size: self.bounds.size) } + if item != nil { + self.isAccessibilityElement = true + } self.currentState = (environment, context, theme, strings, item, search) self.setNeedsLayout() if let effectivePresence { @@ -322,6 +332,29 @@ final class ShareControllerPeerGridItemNode: GridItemNode { } self.peerNode.updateSelection(selected: selected, animated: animated) + self.accessibilityValue = selected ? self.currentState?.strings.VoiceOver_Chat_Selected : nil + if selected { + self.accessibilityTraits.insert(.selected) + } else { + self.accessibilityTraits.remove(.selected) + } + } + + override func accessibilityActivate() -> Bool { + guard let controllerInteraction = self.controllerInteraction, let item = self.currentState?.item else { + return false + } + switch item { + case let .peer(peer, _, _, _, requiresPremiumForMessaging, requiresStars): + if requiresPremiumForMessaging || requiresStars != nil { + controllerInteraction.disabledPeerSelected(peer) + } else { + controllerInteraction.togglePeer(peer, self.currentState?.search ?? false) + } + case .story: + controllerInteraction.shareStory?() + } + return true } override func layout() { diff --git a/submodules/ShareController/Sources/ShareInputFieldNode.swift b/submodules/ShareController/Sources/ShareInputFieldNode.swift index 014c980eb98..826e9fb3ab7 100644 --- a/submodules/ShareController/Sources/ShareInputFieldNode.swift +++ b/submodules/ShareController/Sources/ShareInputFieldNode.swift @@ -135,6 +135,9 @@ private final class ShareInputCopyComponent: Component { textView.mask = self.textMask } textView.frame = textFrame + textView.isAccessibilityElement = true + textView.accessibilityLabel = component.text + textView.accessibilityTraits = [.staticText] } let buttonSize = self.button.update( @@ -159,6 +162,9 @@ private final class ShareInputCopyComponent: Component { self.addSubview(buttonView) } buttonView.frame = buttonFrame + buttonView.isAccessibilityElement = true + buttonView.accessibilityLabel = component.strings.Conversation_LinkDialogCopy + buttonView.accessibilityTraits = [.button] } if self.textMask.image == nil { @@ -217,12 +223,14 @@ public final class ShareInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegat self.textInputNode.attributedText = NSAttributedString(string: newValue, font: Font.regular(17.0), textColor: self.theme.textColor) self.placeholderNode.isHidden = !newValue.isEmpty || self.inputCopyText != nil self.clearButton.isHidden = newValue.isEmpty + self.clearButton.isAccessibilityElement = !newValue.isEmpty } } public var placeholder: String = "" { didSet { self.placeholderNode.attributedText = NSAttributedString(string: self.placeholder, font: Font.regular(17.0), textColor: self.theme.placeholderColor) + self.textInputNode.textView.accessibilityLabel = self.placeholder } } @@ -244,6 +252,7 @@ public final class ShareInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegat self.textInputNode.textContainerInset = UIEdgeInsets(top: self.inputInsets.top, left: 0.0, bottom: self.inputInsets.bottom, right: 0.0) self.textInputNode.keyboardAppearance = theme.keyboard.keyboardAppearance self.textInputNode.tintColor = theme.accentColor + self.textInputNode.textView.accessibilityLabel = placeholder self.placeholderNode = ASTextNode() self.placeholderNode.isUserInteractionEnabled = false @@ -256,6 +265,9 @@ public final class ShareInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegat self.clearButton.displaysAsynchronously = false self.clearButton.setImage(generateClearIcon(color: theme.clearButtonColor), for: []) self.clearButton.isHidden = true + self.clearButton.isAccessibilityElement = false + self.clearButton.accessibilityLabel = strings.WebSearch_RecentSectionClear + self.clearButton.accessibilityTraits = .button super.init() @@ -352,6 +364,7 @@ public final class ShareInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegat public func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) { self.clearButton.isHidden = false + self.clearButton.isAccessibilityElement = true if self.selectTextOnce { self.selectTextOnce = false @@ -364,6 +377,7 @@ public final class ShareInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegat public func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) { self.placeholderNode.isHidden = !(editableTextNode.textView.text ?? "").isEmpty || self.inputCopyText != nil self.clearButton.isHidden = true + self.clearButton.isAccessibilityElement = false } private func calculateTextFieldMetrics(width: CGFloat) -> CGFloat { diff --git a/submodules/ShareController/Sources/ShareLoadingContainerNode.swift b/submodules/ShareController/Sources/ShareLoadingContainerNode.swift index 06ef0c4c471..f1056cf063a 100644 --- a/submodules/ShareController/Sources/ShareLoadingContainerNode.swift +++ b/submodules/ShareController/Sources/ShareLoadingContainerNode.swift @@ -47,6 +47,7 @@ public final class ShareLoadingContainerNode: ASDisplayNode, ShareContentContain private var contentOffsetUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)? private var theme: PresentationTheme + private let strings: PresentationStrings private let activityIndicator: ActivityIndicator private let statusNode: RadialStatusNode private let doneStatusNode: RadialStatusNode @@ -67,22 +68,42 @@ public final class ShareLoadingContainerNode: ASDisplayNode, ShareContentContain self.statusNode.transitionToState(.progress(color: self.theme.actionSheet.controlAccentColor, lineWidth: 2.0, value: 1.0, cancelEnabled: false, animateRotation: true), completion: {}) self.doneStatusNode.transitionToState(.check(self.theme.actionSheet.controlAccentColor), completion: {}) } + self.updateAccessibilityLabel() } } - public init(theme: PresentationTheme, forceNativeAppearance: Bool) { + public init(theme: PresentationTheme, strings: PresentationStrings, forceNativeAppearance: Bool) { self.theme = theme + self.strings = strings self.activityIndicator = ActivityIndicator(type: .custom(theme.actionSheet.controlAccentColor, !forceNativeAppearance ? 22.0 : 50.0, 2.0, forceNativeAppearance)) self.statusNode = RadialStatusNode(backgroundNodeColor: .clear) self.doneStatusNode = RadialStatusNode(backgroundNodeColor: .clear) super.init() + + self.isAccessibilityElement = true + self.accessibilityTraits = [.staticText, .updatesFrequently] + self.updateAccessibilityLabel() + self.activityIndicator.isAccessibilityElement = false + self.statusNode.isAccessibilityElement = false + self.doneStatusNode.isAccessibilityElement = false self.addSubnode(self.activityIndicator) self.addSubnode(self.statusNode) self.addSubnode(self.doneStatusNode) self.doneStatusNode.transitionToState(.progress(color: self.theme.actionSheet.controlAccentColor, lineWidth: 2.0, value: 0.0, cancelEnabled: false, animateRotation: true), completion: {}) } + + private func updateAccessibilityLabel() { + switch self.state { + case .preparing: + self.accessibilityLabel = self.strings.Channel_NotificationLoading + case let .progress(value): + self.accessibilityLabel = self.strings.Share_UploadProgress(Int(value * 100.0)).string + case .done: + self.accessibilityLabel = self.strings.Share_UploadDone + } + } public func activate() { } @@ -249,6 +270,8 @@ public final class ShareProlongedLoadingContainerNode: ASDisplayNode, ShareConte self.progressTextNode = ImmediateTextNode() self.progressTextNode.textAlignment = .center + self.progressTextNode.isAccessibilityElement = true + self.progressTextNode.accessibilityTraits = [.staticText, .updatesFrequently] self.progressBackgroundNode = ASDisplayNode() self.progressBackgroundNode.backgroundColor = theme.actionSheet.controlAccentColor.withMultipliedAlpha(0.2) @@ -350,6 +373,7 @@ public final class ShareProlongedLoadingContainerNode: ASDisplayNode, ShareConte } self.progressTextNode.attributedText = NSAttributedString(string: progressText, font: Font.with(size: 17.0, design: .regular, weight: .semibold, traits: [.monospacedNumbers]), textColor: self.theme.actionSheet.primaryTextColor) + self.progressTextNode.accessibilityLabel = progressText let progressTextSize = self.progressTextNode.updateLayout(size) let progressTextFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - progressTextSize.width) / 2.0), y: progressFrame.minY - spacing - 9.0 - progressTextSize.height), size: progressTextSize) self.progressTextNode.frame = progressTextFrame @@ -359,9 +383,11 @@ public final class ShareProlongedLoadingContainerNode: ASDisplayNode, ShareConte let animationFrame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (progressTextFrame.minY - imageSize.height - 20.0)), size: imageSize) self.animationNode.frame = animationFrame + self.animationNode.isAccessibilityElement = false self.animationNode.updateLayout(size: imageSize) self.doneAnimationNode.frame = animationFrame + self.doneAnimationNode.isAccessibilityElement = false self.doneAnimationNode.updateLayout(size: imageSize) self.contentOffsetUpdated?(-size.height + nodeHeight * 0.5, transition) diff --git a/submodules/ShareController/Sources/SharePeersContainerNode.swift b/submodules/ShareController/Sources/SharePeersContainerNode.swift index 1bc44a39f92..1a41fd845e5 100644 --- a/submodules/ShareController/Sources/SharePeersContainerNode.swift +++ b/submodules/ShareController/Sources/SharePeersContainerNode.swift @@ -236,15 +236,24 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { emptyColor: nil, synchronousLoad: false ) + self.contentTitleAccountNode.isAccessibilityElement = true + self.contentTitleAccountNode.accessibilityLabel = strings.Shortcut_SwitchAccount + self.contentTitleAccountNode.accessibilityValue = info.peer.compactDisplayTitle + self.contentTitleAccountNode.accessibilityTraits = [.button] } else { self.contentTitleAccountNode.isHidden = true + self.contentTitleAccountNode.isAccessibilityElement = false } self.searchButtonNode = HighlightableButtonNode() self.searchButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Share/SearchIcon"), color: self.theme.actionSheet.controlAccentColor), for: []) + self.searchButtonNode.accessibilityLabel = strings.Common_Search + self.searchButtonNode.accessibilityTraits = [.button] self.shareButtonNode = HighlightableButtonNode() self.shareButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Share/ShareIcon"), color: self.theme.actionSheet.controlAccentColor), for: []) + self.shareButtonNode.accessibilityLabel = strings.ShareMenu_ShareTo + self.shareButtonNode.accessibilityTraits = [.button] self.shareReferenceNode = ContextReferenceContentNode() self.shareContainerNode = ContextControllerSourceNode() @@ -272,6 +281,8 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { } super.init() + + self.contentTitleNode.accessibilityTraits = [.header] self.addSubnode(self.contentGridNode) self.addSubnode(self.headerNode) diff --git a/submodules/ShareController/Sources/ShareSearchBarNode.swift b/submodules/ShareController/Sources/ShareSearchBarNode.swift index a9f76440429..67b01a43563 100644 --- a/submodules/ShareController/Sources/ShareSearchBarNode.swift +++ b/submodules/ShareController/Sources/ShareSearchBarNode.swift @@ -19,7 +19,7 @@ final class ShareSearchBarNode: ASDisplayNode, UITextFieldDelegate { var textUpdated: ((String) -> Void)? - init(theme: PresentationTheme, placeholder: String) { + init(theme: PresentationTheme, strings: PresentationStrings, placeholder: String) { self.backgroundNode = ASImageNode() self.backgroundNode.isLayerBacked = true self.backgroundNode.displaysAsynchronously = false @@ -38,6 +38,9 @@ final class ShareSearchBarNode: ASDisplayNode, UITextFieldDelegate { self.clearButton.displaysAsynchronously = false self.clearButton.setImage(generateClearIcon(color: theme.actionSheet.inputClearButtonColor), for: []) self.clearButton.isHidden = true + self.clearButton.isAccessibilityElement = false + self.clearButton.accessibilityLabel = strings.WebSearch_RecentSectionClear + self.clearButton.accessibilityTraits = .button self.textInputNode = TextFieldNode() self.textInputNode.fixOffset = false @@ -108,7 +111,9 @@ final class ShareSearchBarNode: ASDisplayNode, UITextFieldDelegate { } @objc func textFieldDidChangeText() { - self.clearButton.isHidden = self.textInputNode.textField.text?.isEmpty ?? true + let isEmpty = self.textInputNode.textField.text?.isEmpty ?? true + self.clearButton.isHidden = isEmpty + self.clearButton.isAccessibilityElement = !isEmpty self.textUpdated?(self.textInputNode.textField.text ?? "") } diff --git a/submodules/ShareController/Sources/ShareSearchContainerNode.swift b/submodules/ShareController/Sources/ShareSearchContainerNode.swift index faef506e643..29220c1508e 100644 --- a/submodules/ShareController/Sources/ShareSearchContainerNode.swift +++ b/submodules/ShareController/Sources/ShareSearchContainerNode.swift @@ -241,11 +241,13 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { self.contentGridNode = GridNode() self.contentGridNode.isHidden = true - self.searchNode = ShareSearchBarNode(theme: theme, placeholder: strings.Common_Search) + self.searchNode = ShareSearchBarNode(theme: theme, strings: strings, placeholder: strings.Common_Search) self.cancelButtonNode = HighlightableButtonNode() self.cancelButtonNode.setTitle(strings.Common_Cancel, with: cancelFont, with: theme.actionSheet.controlAccentColor, for: []) self.cancelButtonNode.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0) + self.cancelButtonNode.accessibilityLabel = strings.Common_Cancel + self.cancelButtonNode.accessibilityTraits = [.button] self.contentSeparatorNode = ASDisplayNode() self.contentSeparatorNode.isLayerBacked = true diff --git a/submodules/ShareController/Sources/ShareTopicGridItem.swift b/submodules/ShareController/Sources/ShareTopicGridItem.swift index 015d0b76488..e27f92c389b 100644 --- a/submodules/ShareController/Sources/ShareTopicGridItem.swift +++ b/submodules/ShareController/Sources/ShareTopicGridItem.swift @@ -72,6 +72,10 @@ final class ShareTopicGridItemNode: GridItemNode { self.textNode.textAlignment = .center super.init() + + self.isAccessibilityElement = false + self.accessibilityTraits = [.button] + self.textNode.isAccessibilityElement = false self.addSubnode(self.textNode) } @@ -91,6 +95,14 @@ final class ShareTopicGridItemNode: GridItemNode { } } } + + override func accessibilityActivate() -> Bool { + guard self.currentItem?.peer != nil else { + return false + } + self.tapped() + return true + } override func updateAbsoluteRect(_ absoluteRect: CGRect, within containerSize: CGSize) { let rect = absoluteRect @@ -107,8 +119,11 @@ final class ShareTopicGridItemNode: GridItemNode { return } self.currentItem = item + self.isAccessibilityElement = item.peer != nil + self.accessibilityLabel = nil if let threadInfo = item.threadInfo { + self.accessibilityLabel = threadInfo.info.title self.textNode.attributedText = NSAttributedString(string: threadInfo.info.title, font: Font.regular(11.0), textColor: item.theme.actionSheet.primaryTextColor) let iconContent: EmojiStatusComponent.Content @@ -138,9 +153,11 @@ final class ShareTopicGridItemNode: GridItemNode { if iconComponentView.superview == nil { self.view.addSubview(iconComponentView) } + iconComponentView.accessibilityElementsHidden = true iconComponentView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - iconSize.width) / 2.0), y: 7.0), size: iconSize) } } else if let peer = item.peer, let mainPeer = peer.chatMainPeer { + self.accessibilityLabel = mainPeer.compactDisplayTitle self.textNode.attributedText = NSAttributedString(string: mainPeer.compactDisplayTitle, font: Font.regular(11.0), textColor: item.theme.actionSheet.primaryTextColor) let avatarNode: AvatarNode @@ -148,6 +165,7 @@ final class ShareTopicGridItemNode: GridItemNode { avatarNode = current } else { avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 12.0)) + avatarNode.isAccessibilityElement = false self.avatarNode = avatarNode self.addSubnode(avatarNode) } diff --git a/submodules/ShareController/Sources/ShareTopicsContainerNode.swift b/submodules/ShareController/Sources/ShareTopicsContainerNode.swift index 18d5b6e4a23..0989a2c9db7 100644 --- a/submodules/ShareController/Sources/ShareTopicsContainerNode.swift +++ b/submodules/ShareController/Sources/ShareTopicsContainerNode.swift @@ -95,6 +95,8 @@ private class CancelButtonNode: ASDisplayNode { self.strings = strings self.buttonNode = HighlightTrackingButtonNode() + self.buttonNode.accessibilityLabel = strings.Common_Back + self.buttonNode.accessibilityTraits = [.button] self.arrowNode = ASImageNode() self.arrowNode.displaysAsynchronously = false @@ -237,6 +239,8 @@ final class ShareTopicsContainerNode: ASDisplayNode, ShareContentContainerNode { self.backNode = CancelButtonNode(theme: theme, strings: strings) super.init() + + self.contentTitleNode.accessibilityTraits = [.header] self.addSubnode(self.contentGridNode) self.addSubnode(self.headerNode) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift index 06d1369916a..bb8d13d9835 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -801,34 +801,11 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } override public func updateAccessibilityData(_ accessibilityData: ChatMessageAccessibilityData) { - super.updateAccessibilityData(accessibilityData) - - self.messageAccessibilityArea.accessibilityLabel = accessibilityData.label - self.messageAccessibilityArea.accessibilityValue = accessibilityData.value - self.messageAccessibilityArea.accessibilityHint = accessibilityData.hint - self.messageAccessibilityArea.accessibilityTraits = accessibilityData.traits - if let customActions = accessibilityData.customActions { - self.messageAccessibilityArea.accessibilityCustomActions = customActions.map({ action -> UIAccessibilityCustomAction in - return ChatMessageAccessibilityCustomAction(name: action.name, target: self, selector: #selector(self.performLocalAccessibilityCustomAction(_:)), action: action.action) - }) - } else { - self.messageAccessibilityArea.accessibilityCustomActions = nil - } + super.updateAccessibilityData(accessibilityData, accessibilityNode: self.messageAccessibilityArea, customActionTarget: self, customActionSelector: #selector(self.performLocalAccessibilityCustomAction(_:))) } - @objc private func performLocalAccessibilityCustomAction(_ action: UIAccessibilityCustomAction) { - if let action = action as? ChatMessageAccessibilityCustomAction { - switch action.action { - case .reply: - if let item = self.item { - item.controllerInteraction.setupReply(item.message.id) - } - case .options: - if let item = self.item { - item.controllerInteraction.openMessageContextMenu(item.message, false, self, self.imageNode.frame, nil, nil) - } - } - } + @objc private func performLocalAccessibilityCustomAction(_ action: UIAccessibilityCustomAction) -> Bool { + return self.performAccessibilityCustomAction(action, sourceNode: self, sourceRect: self.imageNode.frame) } override public func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: ChatMessageHeaderSpec) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) { @@ -2810,6 +2787,8 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { guard let item = self.item else { return } + let isSelected = item.controllerInteraction.selectionState.map { $0.selectedIds.contains(item.message.id) } + self.updateAccessibilityData(ChatMessageAccessibilityData(item: item, isSelected: isSelected)) if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.effectiveTopId == item.message.id { return diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 2dcfc95ed57..a60e8d488ce 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -5681,43 +5681,20 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } override public func updateAccessibilityData(_ accessibilityData: ChatMessageAccessibilityData) { - super.updateAccessibilityData(accessibilityData) - - self.messageAccessibilityArea.accessibilityLabel = accessibilityData.label - self.messageAccessibilityArea.accessibilityValue = accessibilityData.value - self.messageAccessibilityArea.accessibilityHint = accessibilityData.hint - self.messageAccessibilityArea.accessibilityTraits = accessibilityData.traits - if let customActions = accessibilityData.customActions { - self.messageAccessibilityArea.accessibilityCustomActions = customActions.map({ action -> UIAccessibilityCustomAction in - return ChatMessageAccessibilityCustomAction(name: action.name, target: self, selector: #selector(self.performLocalAccessibilityCustomAction(_:)), action: action.action) - }) - } else { - self.messageAccessibilityArea.accessibilityCustomActions = nil - } + super.updateAccessibilityData(accessibilityData, accessibilityNode: self.messageAccessibilityArea, customActionTarget: self, customActionSelector: #selector(self.performLocalAccessibilityCustomAction(_:))) } - @objc private func performLocalAccessibilityCustomAction(_ action: UIAccessibilityCustomAction) { - if let action = action as? ChatMessageAccessibilityCustomAction { - switch action.action { - case .reply: - if let item = self.item { - item.controllerInteraction.setupReply(item.message.id) - } - case .options: - if let item = self.item { - var subFrame = self.backgroundNode.frame - if case .group = item.content { - for contentNode in self.contentNodes { - if contentNode.item?.message.stableId == item.message.stableId { - subFrame = contentNode.frame.insetBy(dx: 0.0, dy: -4.0) - break - } - } - } - item.controllerInteraction.openMessageContextMenu(item.message, false, self, subFrame, nil, nil) - } + @objc private func performLocalAccessibilityCustomAction(_ action: UIAccessibilityCustomAction) -> Bool { + var subFrame = self.backgroundNode.frame + if let item = self.item, case .group = item.content { + for contentNode in self.contentNodes { + if contentNode.item?.message.stableId == item.message.stableId { + subFrame = contentNode.frame.insetBy(dx: 0.0, dy: -4.0) + break + } } } + return self.performAccessibilityCustomAction(action, sourceNode: self, sourceRect: subFrame) } override public func shouldAnimateHorizontalFrameTransition() -> Bool { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift index f8c3faf6960..1e3104b5476 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift @@ -246,34 +246,11 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, ASGestureReco } override public func updateAccessibilityData(_ accessibilityData: ChatMessageAccessibilityData) { - super.updateAccessibilityData(accessibilityData) - - self.messageAccessibilityArea.accessibilityLabel = accessibilityData.label - self.messageAccessibilityArea.accessibilityValue = accessibilityData.value - self.messageAccessibilityArea.accessibilityHint = accessibilityData.hint - self.messageAccessibilityArea.accessibilityTraits = accessibilityData.traits - if let customActions = accessibilityData.customActions { - self.messageAccessibilityArea.accessibilityCustomActions = customActions.map({ action -> UIAccessibilityCustomAction in - return ChatMessageAccessibilityCustomAction(name: action.name, target: self, selector: #selector(self.performLocalAccessibilityCustomAction(_:)), action: action.action) - }) - } else { - self.messageAccessibilityArea.accessibilityCustomActions = nil - } + super.updateAccessibilityData(accessibilityData, accessibilityNode: self.messageAccessibilityArea, customActionTarget: self, customActionSelector: #selector(self.performLocalAccessibilityCustomAction(_:))) } - @objc private func performLocalAccessibilityCustomAction(_ action: UIAccessibilityCustomAction) { - if let action = action as? ChatMessageAccessibilityCustomAction { - switch action.action { - case .reply: - if let item = self.item { - item.controllerInteraction.setupReply(item.message.id) - } - case .options: - if let item = self.item { - item.controllerInteraction.openMessageContextMenu(item.message, false, self, self.interactiveVideoNode.frame, nil, nil) - } - } - } + @objc private func performLocalAccessibilityCustomAction(_ action: UIAccessibilityCustomAction) -> Bool { + return self.performAccessibilityCustomAction(action, sourceNode: self, sourceRect: self.interactiveVideoNode.frame) } override public func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: ChatMessageHeaderSpec) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) { @@ -1205,6 +1182,8 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, ASGestureReco guard let item = self.item else { return } + let isSelected = item.controllerInteraction.selectionState.map { $0.selectedIds.contains(item.message.id) } + self.updateAccessibilityData(ChatMessageAccessibilityData(item: item, isSelected: isSelected)) if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.effectiveTopId == item.message.id { return diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift index e6d467fbff3..1ef092933d7 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift @@ -66,7 +66,12 @@ private let fileSizeFormatter: ByteCountFormatter = { public enum ChatMessageAccessibilityCustomActionType { case reply + case navigateToReply(EngineMessage.Id) + case react case options + case copy + case forward + case delete } public final class ChatMessageAccessibilityCustomAction: UIAccessibilityCustomAction { @@ -86,6 +91,7 @@ public final class ChatMessageAccessibilityData { public let traits: UIAccessibilityTraits public let customActions: [ChatMessageAccessibilityCustomAction]? public let singleUrl: String? + public let respondsToUserInteraction: Bool public init(item: ChatMessageItem, isSelected: Bool?) { var hint: String? @@ -445,15 +451,16 @@ public final class ChatMessageAccessibilityData { if isSelected { result += item.presentationData.strings.VoiceOver_Chat_Selected result += "\n" + traits.insert(.selected) } - traits.insert(.startsMediaSession) } result += "\(text)" - let dateString = DateFormatter.localizedString(from: Date(timeIntervalSince1970: Double(message.timestamp)), dateStyle: .medium, timeStyle: .short) - - result += "\n\(dateString)" + if !isReply { + let dateString = DateFormatter.localizedString(from: Date(timeIntervalSince1970: Double(message.timestamp)), dateStyle: .medium, timeStyle: .short) + result += "\n\(dateString)" + } if !isIncoming && !isReply { result += "\n" if item.sending { @@ -462,14 +469,13 @@ public final class ChatMessageAccessibilityData { result += item.presentationData.strings.VoiceOver_Chat_Failed } else { if item.read { - if announceIncomingAuthors { - result += item.presentationData.strings.VoiceOver_Chat_SeenByRecipients - } else { - result += item.presentationData.strings.VoiceOver_Chat_SeenByRecipient - } + result += item.presentationData.strings.Conversation_ChecksTooltip_Read + } else { + result += item.presentationData.strings.Conversation_ChecksTooltip_Delivered } for attribute in message.attributes { if let attribute = attribute as? ConsumableContentMessageAttribute { + result += "\n" if !attribute.consumed { if announceIncomingAuthors { result += item.presentationData.strings.VoiceOver_Chat_NotPlayedByRecipients @@ -509,6 +515,7 @@ public final class ChatMessageAccessibilityData { var (label, value) = dataForMessage(item.message, false) var replyValue: String? + var replyMessageId: EngineMessage.Id? for attribute in item.message.attributes { if let attribute = attribute as? TextEntitiesMessageAttribute { @@ -537,7 +544,11 @@ public final class ChatMessageAccessibilityData { break } } - } else if let attribute = attribute as? ReplyMessageAttribute, let replyMessage = item.message.associatedMessages[attribute.messageId] { + } else if let attribute = attribute as? ReplyMessageAttribute { + replyMessageId = attribute.messageId + guard let replyMessage = item.message.associatedMessages[attribute.messageId] else { + continue + } var replyLabel: String if replyMessage.flags.contains(.Incoming) { if let author = replyMessage.author { @@ -600,19 +611,39 @@ public final class ChatMessageAccessibilityData { if canReply { customActions.append(ChatMessageAccessibilityCustomAction(name: item.presentationData.strings.VoiceOver_MessageContextReply, target: nil, selector: #selector(self.noop), action: .reply)) } + if let replyMessageId { + customActions.append(ChatMessageAccessibilityCustomAction(name: item.presentationData.strings.VoiceOver_Chat_GoToOriginalMessage, target: nil, selector: #selector(self.noop), action: .navigateToReply(replyMessageId))) + } + if canAddMessageReactions(message: EngineMessage(item.message)) { + customActions.append(ChatMessageAccessibilityCustomAction(name: item.presentationData.strings.MediaEditor_Shortcut_Reaction, target: nil, selector: #selector(self.noop), action: .react)) + } + if !item.message.text.isEmpty { + customActions.append(ChatMessageAccessibilityCustomAction(name: item.presentationData.strings.Conversation_ContextMenuCopy, target: nil, selector: #selector(self.noop), action: .copy)) + } + if item.controllerInteraction.canPerformAccessibilityMessageActions { + customActions.append(ChatMessageAccessibilityCustomAction(name: item.presentationData.strings.VoiceOver_MessageContextForward, target: nil, selector: #selector(self.noop), action: .forward)) + customActions.append(ChatMessageAccessibilityCustomAction(name: item.presentationData.strings.VoiceOver_MessageContextDelete, target: nil, selector: #selector(self.noop), action: .delete)) + } customActions.append(ChatMessageAccessibilityCustomAction(name: item.presentationData.strings.VoiceOver_MessageContextOpenMessageMenu, target: nil, selector: #selector(self.noop), action: .options)) } if let replyValue { - value = "\(value). \(item.presentationData.strings.VoiceOver_Chat_ReplyingToMessage(replyValue).string)" + let replyHint = item.presentationData.strings.VoiceOver_Chat_ReplyingToMessage(replyValue).string + if let hint, !hint.isEmpty { + self.hint = "\(hint). \(replyHint)" + } else { + self.hint = replyHint + } + } else { + self.hint = hint } self.label = label self.value = value - self.hint = hint self.traits = traits self.customActions = customActions.isEmpty ? nil : customActions self.singleUrl = singleUrl + self.respondsToUserInteraction = singleUrl != nil || !item.message.media.isEmpty } @objc private func noop() { @@ -656,6 +687,7 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol { open var item: ChatMessageItem? open var accessibilityData: ChatMessageAccessibilityData? + private weak var messageAccessibilityNode: AccessibilityAreaNode? open var safeInsets = UIEdgeInsets() open var awaitingAppliedReaction: (MessageReaction.Reaction?, () -> Void)? @@ -694,6 +726,60 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol { open func updateAccessibilityData(_ accessibilityData: ChatMessageAccessibilityData) { self.accessibilityData = accessibilityData } + + public func updateAccessibilityData(_ accessibilityData: ChatMessageAccessibilityData, accessibilityNode: AccessibilityAreaNode, customActionTarget: Any, customActionSelector: Selector) { + self.accessibilityData = accessibilityData + self.messageAccessibilityNode = accessibilityNode + + accessibilityNode.accessibilityLabel = accessibilityData.label + accessibilityNode.accessibilityValue = accessibilityData.value + accessibilityNode.accessibilityHint = accessibilityData.hint + accessibilityNode.accessibilityTraits = accessibilityData.traits + accessibilityNode.view.accessibilityRespondsToUserInteraction = accessibilityData.respondsToUserInteraction + if let customActions = accessibilityData.customActions { + accessibilityNode.accessibilityCustomActions = customActions.map { action in + return ChatMessageAccessibilityCustomAction(name: action.name, target: customActionTarget, selector: customActionSelector, action: action.action) + } + } else { + accessibilityNode.accessibilityCustomActions = nil + } + } + + public func accessibilityContainsFocus() -> Bool { + return self.messageAccessibilityNode?.view.accessibilityElementIsFocused() == true || self.view.accessibilityElementIsFocused() + } + + public func restoreAccessibilityFocus() { + UIAccessibility.post(notification: .layoutChanged, argument: self.messageAccessibilityNode?.view ?? self.view) + } + + public func performAccessibilityCustomAction(_ customAction: UIAccessibilityCustomAction, sourceNode: ASDisplayNode, sourceRect: CGRect) -> Bool { + guard let action = customAction as? ChatMessageAccessibilityCustomAction, let item = self.item else { + return false + } + + switch action.action { + case .reply: + item.controllerInteraction.setupReply(item.message.id) + case let .navigateToReply(messageId): + item.controllerInteraction.accessibilityNavigationTargetMessageId = messageId + item.controllerInteraction.navigateToMessage(item.message.id, messageId, NavigateToMessageParams(timestamp: nil, quote: nil)) + case .react: + item.controllerInteraction.updateMessageReaction(item.message, .default, false, nil) + case .options: + item.controllerInteraction.openMessageContextMenu(item.message, false, sourceNode, sourceRect, nil, nil) + case .copy: + guard !item.message.text.isEmpty else { + return false + } + item.controllerInteraction.copyText(item.message.text) + case .forward: + item.controllerInteraction.accessibilityForwardMessage(item.message) + case .delete: + item.controllerInteraction.accessibilityDeleteMessage(item.message) + } + return true + } override open func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { if let item = item as? ChatMessageItem { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift index 4d5cd55f59b..cd67ee899f8 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift @@ -389,34 +389,11 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { } override public func updateAccessibilityData(_ accessibilityData: ChatMessageAccessibilityData) { - super.updateAccessibilityData(accessibilityData) - - self.messageAccessibilityArea.accessibilityLabel = accessibilityData.label - self.messageAccessibilityArea.accessibilityValue = accessibilityData.value - self.messageAccessibilityArea.accessibilityHint = accessibilityData.hint - self.messageAccessibilityArea.accessibilityTraits = accessibilityData.traits - if let customActions = accessibilityData.customActions { - self.messageAccessibilityArea.accessibilityCustomActions = customActions.map({ action -> UIAccessibilityCustomAction in - return ChatMessageAccessibilityCustomAction(name: action.name, target: self, selector: #selector(self.performLocalAccessibilityCustomAction(_:)), action: action.action) - }) - } else { - self.messageAccessibilityArea.accessibilityCustomActions = nil - } + super.updateAccessibilityData(accessibilityData, accessibilityNode: self.messageAccessibilityArea, customActionTarget: self, customActionSelector: #selector(self.performLocalAccessibilityCustomAction(_:))) } - @objc private func performLocalAccessibilityCustomAction(_ action: UIAccessibilityCustomAction) { - if let action = action as? ChatMessageAccessibilityCustomAction { - switch action.action { - case .reply: - if let item = self.item { - item.controllerInteraction.setupReply(item.message.id) - } - case .options: - if let item = self.item { - item.controllerInteraction.openMessageContextMenu(item.message, false, self, self.imageNode.frame, nil, nil) - } - } - } + @objc private func performLocalAccessibilityCustomAction(_ action: UIAccessibilityCustomAction) -> Bool { + return self.performAccessibilityCustomAction(action, sourceNode: self, sourceRect: self.imageNode.frame) } override public func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: ChatMessageHeaderSpec) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) { @@ -1799,6 +1776,8 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { guard let item = self.item else { return } + let isSelected = item.controllerInteraction.selectionState.map { $0.selectedIds.contains(item.message.id) } + self.updateAccessibilityData(ChatMessageAccessibilityData(item: item, isSelected: isSelected)) if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.effectiveTopId == item.message.id { return diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift index 826bdb4098d..da7c676ff45 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift @@ -71,6 +71,21 @@ public let chatTextInputMinFontSize: CGFloat = 5.0 private let minInputFontSize = chatTextInputMinFontSize +private func accessibilityTextInputView(in view: UIView) -> UIView { + if view is UITextInput { + return view + } + for subview in view.subviews { + if !subview.isHidden && subview.alpha > 0.0 { + let result = accessibilityTextInputView(in: subview) + if result is UITextInput { + return result + } + } + } + return view +} + private func calclulateTextFieldMinHeight(_ presentationInterfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { var baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) if "".isEmpty { @@ -1185,6 +1200,9 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg } old.resignInputFirstResponder() old.asNode.removeFromSupernode() + if wasFirstResponder && UIAccessibility.isVoiceOverRunning { + UIAccessibility.post(notification: .layoutChanged, argument: new.inputView) + } } private func loadTextInputNode(useNative: Bool = false) { @@ -3198,6 +3216,10 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg richTextInputNode.textContainerInset = textInputViewRealInsets richTextInputNode.textFieldFrame = actualTextFieldFrame richTextInputNode.updateLayout(size: textFieldFrame.size) + let accessibilityInputView = accessibilityTextInputView(in: richTextInputNode.inputView) + let accessibilityBounds = richTextInputNode.inputView.bounds.inset(by: richTextInputNode.inputHitTestSlop) + accessibilityInputView.accessibilityFrame = UIAccessibility.convertToScreenCoordinates(accessibilityBounds, in: richTextInputNode.inputView) + accessibilityInputView.accessibilityRespondsToUserInteraction = true self.updateInputField(textInputFrame: textFieldFrame, transition: ComponentTransition(transition)) if shouldUpdateLayout { richTextInputNode.layoutInputField() diff --git a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift index 0546d4be9b9..74b28aa7394 100644 --- a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift @@ -278,6 +278,10 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol public let openMessageStats: (EngineMessage.Id) -> Void public let editMessageMedia: (EngineMessage.Id, Bool) -> Void public let copyText: (String) -> Void + public let accessibilityForwardMessage: (EngineRawMessage) -> Void + public let accessibilityDeleteMessage: (EngineRawMessage) -> Void + public let canPerformAccessibilityMessageActions: Bool + public var accessibilityNavigationTargetMessageId: EngineMessage.Id? public let displayUndo: (UndoOverlayContent) -> Void public let isAnimatingMessage: (UInt32) -> Bool public let getMessageTransitionNode: () -> ChatMessageTransitionProtocol? @@ -504,6 +508,9 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol displayTodoToggleUnavailable: @escaping (EngineMessage.Id) -> Void, canEditMessageRichText: @escaping (EngineRawMessage) -> Bool = { _ in false }, toggleMessageRichTextCheckbox: @escaping (EngineMessage.Id, [Int], Bool) -> Void = { _, _, _ in }, + accessibilityForwardMessage: @escaping (EngineRawMessage) -> Void = { _ in }, + accessibilityDeleteMessage: @escaping (EngineRawMessage) -> Void = { _ in }, + canPerformAccessibilityMessageActions: Bool = false, openStarsPurchase: @escaping (Int64?) -> Void, openRankInfo: @escaping (EnginePeer, ChatRankInfoScreenRole, String) -> Void, openSetPeerAvatar: @escaping () -> Void, @@ -597,6 +604,9 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol self.openMessageStats = openMessageStats self.editMessageMedia = editMessageMedia self.copyText = copyText + self.accessibilityForwardMessage = accessibilityForwardMessage + self.accessibilityDeleteMessage = accessibilityDeleteMessage + self.canPerformAccessibilityMessageActions = canPerformAccessibilityMessageActions self.displayUndo = displayUndo self.isAnimatingMessage = isAnimatingMessage self.getMessageTransitionNode = getMessageTransitionNode diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/Sources/PeerInfoChatListPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/Sources/PeerInfoChatListPaneNode.swift index bdc20e8af83..e11b62000fb 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/Sources/PeerInfoChatListPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/Sources/PeerInfoChatListPaneNode.swift @@ -414,6 +414,7 @@ public final class PeerInfoChatListPaneNode: ASDisplayNode, PeerInfoPaneNode, AS self.insertSubnode(chatController.displayNode, aboveSubnode: self.chatListNode) chatController.displayNode.alpha = 0.0 chatController.displayNode.clipsToBounds = true + chatController.displayNode.accessibilityElementsHidden = true self.updateChatController(transition: .immediate) @@ -442,6 +443,8 @@ public final class PeerInfoChatListPaneNode: ASDisplayNode, PeerInfoPaneNode, AS } if let contentNode = chatController.customNavigationBarContentNode { self.removeChatWhenNotSearching = true + chatController.displayNode.accessibilityElementsHidden = false + self.chatListNode.accessibilityElementsHidden = true chatController.displayNode.layer.allowsGroupOpacity = true if transition.isAnimated { @@ -480,6 +483,8 @@ public final class PeerInfoChatListPaneNode: ASDisplayNode, PeerInfoPaneNode, AS self.chatController = nil let displayNode = chatController.displayNode + displayNode.accessibilityElementsHidden = true + self.chatListNode.accessibilityElementsHidden = false chatController.displayNode.layer.allowsGroupOpacity = true chatController.displayNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak displayNode] _ in displayNode?.removeFromSupernode() @@ -583,7 +588,7 @@ public final class PeerInfoChatListPaneNode: ASDisplayNode, PeerInfoPaneNode, AS let combinedBottomInset = bottomInset transition.updateFrame(node: chatController.displayNode, frame: chatFrame) chatController.updateIsScrollingLockedAtTop(isScrollingLockedAtTop: isScrollingLockedAtTop) - chatController.containerLayoutUpdated(ContainerViewLayout(size: chatFrame.size, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), deviceMetrics: deviceMetrics, intrinsicInsets: UIEdgeInsets(top: topInset + navigationHeight, left: sideInset, bottom: combinedBottomInset, right: sideInset), safeInsets: UIEdgeInsets(top: navigationHeight + topInset + 4.0, left: sideInset, bottom: combinedBottomInset, right: sideInset), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: transition) + chatController.containerLayoutUpdated(ContainerViewLayout(size: chatFrame.size, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), deviceMetrics: deviceMetrics, intrinsicInsets: UIEdgeInsets(top: topInset + navigationHeight, left: sideInset, bottom: combinedBottomInset, right: sideInset), safeInsets: UIEdgeInsets(top: navigationHeight + topInset + 4.0, left: sideInset, bottom: combinedBottomInset, right: sideInset), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: UIAccessibility.isVoiceOverRunning), transition: transition) } public func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatPaneNode/Sources/PeerInfoChatPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatPaneNode/Sources/PeerInfoChatPaneNode.swift index 31a2d6d09df..68c57aaa20f 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatPaneNode/Sources/PeerInfoChatPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatPaneNode/Sources/PeerInfoChatPaneNode.swift @@ -304,7 +304,7 @@ public final class PeerInfoChatPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScro let combinedBottomInset = bottomInset transition.updateFrame(node: self.chatController.displayNode, frame: chatFrame) self.chatController.updateIsScrollingLockedAtTop(isScrollingLockedAtTop: isScrollingLockedAtTop) - self.chatController.containerLayoutUpdated(ContainerViewLayout(size: chatFrame.size, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), deviceMetrics: deviceMetrics, intrinsicInsets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: combinedBottomInset, right: sideInset), safeInsets: UIEdgeInsets(top: 0.0 + 4.0, left: sideInset, bottom: combinedBottomInset, right: sideInset), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: transition) + self.chatController.containerLayoutUpdated(ContainerViewLayout(size: chatFrame.size, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), deviceMetrics: deviceMetrics, intrinsicInsets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: combinedBottomInset, right: sideInset), safeInsets: UIEdgeInsets(top: 0.0 + 4.0, left: sideInset, bottom: combinedBottomInset, right: sideInset), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: UIAccessibility.isVoiceOverRunning), transition: transition) } override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenActionItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenActionItem.swift index 64ab4e9b1d1..872e6ae9e6a 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenActionItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenActionItem.swift @@ -100,6 +100,16 @@ private final class PeerInfoScreenActionItemNode: PeerInfoScreenItemNode { self.item = item self.selectionNode.pressed = item.action + if let action = item.action { + self.activateArea.accessibilityTraits = [.button] + self.activateArea.activate = { + action() + return true + } + } else { + self.activateArea.accessibilityTraits = [.staticText] + self.activateArea.activate = nil + } let sideInset: CGFloat = 16.0 + safeInsets.left var leftInset = (item.icon == nil && item.iconSignal == nil ? sideInset : sideInset + 29.0 + 16.0) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenAddressItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenAddressItem.swift index c67a082d945..46f875cfa6c 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenAddressItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenAddressItem.swift @@ -177,6 +177,17 @@ private final class PeerInfoScreenAddressItemNode: PeerInfoScreenItemNode { self.item = item self.presentationData = presentationData + + if let action = item.action { + self.activateArea.accessibilityTraits = [.button] + self.activateArea.activate = { + action() + return true + } + } else { + self.activateArea.accessibilityTraits = [.staticText] + self.activateArea.activate = nil + } self.containerNode.isGestureEnabled = item.contextAction != nil @@ -231,6 +242,7 @@ private final class PeerInfoScreenAddressItemNode: PeerInfoScreenItemNode { self.activateArea.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: height)) self.activateArea.accessibilityLabel = item.label + self.activateArea.accessibilityValue = item.text let contentSize = CGSize(width: width, height: height) self.containerNode.frame = CGRect(origin: CGPoint(), size: contentSize) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBusinessHoursItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBusinessHoursItem.swift index a4c4781af56..817af7543a4 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBusinessHoursItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBusinessHoursItem.swift @@ -14,6 +14,15 @@ import BundleIconComponent import PlainButtonComponent import AccountContext +private final class PeerInfoBusinessHoursAccessibilityAction: UIAccessibilityCustomAction { + let perform: () -> Void + + init(name: String, target: Any?, selector: Selector, perform: @escaping () -> Void) { + self.perform = perform + super.init(name: name, target: target, selector: selector) + } +} + func businessHoursTextToCopy(businessHours: TelegramBusinessHours, presentationData: PresentationData, displayLocalTimezone: Bool) -> String { var text = "" @@ -297,6 +306,15 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode self.item = item self.presentationData = presentationData self.theme = presentationData.theme + self.activateArea.accessibilityTraits = [.button] + self.activateArea.activate = { [weak self] in + guard let self else { + return false + } + self.isExpanded = !self.isExpanded + self.item?.requestLayout(true) + return true + } self.containerNode.isGestureEnabled = item.contextAction != nil @@ -445,6 +463,7 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode } var timezoneSwitchButtonSize: CGSize? + var accessibilityTimezoneSwitchTitle: String? if hasTimezoneDependentEntries { let timezoneSwitchButton: ComponentView if let current = self.timezoneSwitchButton { @@ -459,6 +478,7 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode } else { timezoneSwitchTitle = presentationData.strings.PeerInfo_BusinessHours_TimezoneSwitchBusiness } + accessibilityTimezoneSwitchTitle = timezoneSwitchTitle timezoneSwitchButtonSize = timezoneSwitchButton.update( transition: .immediate, component: AnyComponent(PlainButtonComponent( @@ -651,6 +671,23 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode self.activateArea.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: height)) self.activateArea.accessibilityLabel = item.label + self.activateArea.accessibilityValue = businessHoursTextToCopy(businessHours: item.businessHours, presentationData: presentationData, displayLocalTimezone: self.displayLocalTimezone) + if let accessibilityTimezoneSwitchTitle { + self.activateArea.accessibilityCustomActions = [ + PeerInfoBusinessHoursAccessibilityAction(name: accessibilityTimezoneSwitchTitle, target: self, selector: #selector(self.performAccessibilityAction(_:)), perform: { [weak self] in + guard let self else { + return + } + self.displayLocalTimezone = !self.displayLocalTimezone + if !self.isExpanded { + self.isExpanded = true + } + self.item?.requestLayout(true) + }) + ] + } else { + self.activateArea.accessibilityCustomActions = nil + } let contentSize = CGSize(width: width, height: height) self.containerNode.frame = CGRect(origin: CGPoint(), size: contentSize) @@ -671,6 +708,14 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode return height } + + @objc private func performAccessibilityAction(_ action: UIAccessibilityCustomAction) -> Bool { + guard let action = action as? PeerInfoBusinessHoursAccessibilityAction else { + return false + } + action.perform() + return true + } private func updateTouchesAtPoint(_ point: CGPoint?) { } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenCommunityItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenCommunityItem.swift index 4ccf64b2ea6..89cfdf443b2 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenCommunityItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenCommunityItem.swift @@ -101,6 +101,11 @@ private final class PeerInfoScreenCommunityItemNode: PeerInfoScreenItemNode { self.item = item self.selectionNode.pressed = item.action + self.activateArea.accessibilityTraits = [.button] + self.activateArea.activate = { + item.action() + return true + } let sideInset: CGFloat = 16.0 + safeInsets.left let avatarSize: CGFloat = 30.0 diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenContactInfoItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenContactInfoItem.swift index a8313a54f2e..5b538a4e2a2 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenContactInfoItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenContactInfoItem.swift @@ -8,6 +8,15 @@ import AppBundle import TelegramStringFormatting import ContextUI +private final class PeerInfoContactAccessibilityAction: UIAccessibilityCustomAction { + let perform: () -> Void + + init(name: String, target: Any?, selector: Selector, perform: @escaping () -> Void) { + self.perform = perform + super.init(name: name, target: target, selector: selector) + } +} + final class PeerInfoScreenContactInfoItem: PeerInfoScreenItem { let id: AnyHashable let username: String @@ -244,6 +253,34 @@ private final class PeerInfoScreenContactInfoItemNode: PeerInfoScreenItemNode { self.item = item self.theme = presentationData.theme + + var accessibilityActions: [UIAccessibilityCustomAction] = [] + if let usernameAction = item.usernameAction, !item.username.isEmpty { + accessibilityActions.append(PeerInfoContactAccessibilityAction(name: item.username, target: self, selector: #selector(self.performAccessibilityAction(_:)), perform: { [weak self] in + guard let self else { + return + } + usernameAction(self.contextSourceNode) + })) + } + if let phoneAction = item.phoneAction, !item.phoneNumber.isEmpty { + accessibilityActions.append(PeerInfoContactAccessibilityAction(name: item.phoneNumber, target: self, selector: #selector(self.performAccessibilityAction(_:)), perform: { [weak self] in + guard let self else { + return + } + phoneAction(self.contextSourceNode) + })) + } + self.activateArea.accessibilityCustomActions = accessibilityActions.isEmpty ? nil : accessibilityActions + self.activateArea.accessibilityTraits = accessibilityActions.isEmpty ? [.staticText] : [.button] + if let primaryAction = accessibilityActions.first as? PeerInfoContactAccessibilityAction { + self.activateArea.activate = { + primaryAction.perform() + return true + } + } else { + self.activateArea.activate = nil + } // if let action = item.action { // self.selectionNode.pressed = { [weak self] in @@ -324,8 +361,15 @@ private final class PeerInfoScreenContactInfoItemNode: PeerInfoScreenItemNode { self.bottomSeparatorNode.isHidden = hasBottomCorners self.activateArea.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: height)) - self.activateArea.accessibilityLabel = item.username - self.activateArea.accessibilityValue = item.phoneNumber + self.activateArea.accessibilityLabel = item.username.isEmpty ? item.phoneNumber : item.username + var accessibilityValues: [String] = [] + if !item.username.isEmpty && !item.phoneNumber.isEmpty { + accessibilityValues.append(item.phoneNumber) + } + if let additionalText = item.additionalText, !additionalText.isEmpty { + accessibilityValues.append(additionalText) + } + self.activateArea.accessibilityValue = accessibilityValues.isEmpty ? nil : accessibilityValues.joined(separator: ". ") let contentSize = CGSize(width: width, height: height) self.containerNode.frame = CGRect(origin: CGPoint(), size: contentSize) @@ -347,6 +391,14 @@ private final class PeerInfoScreenContactInfoItemNode: PeerInfoScreenItemNode { return height } + + @objc private func performAccessibilityAction(_ action: UIAccessibilityCustomAction) -> Bool { + guard let action = action as? PeerInfoContactAccessibilityAction else { + return false + } + action.perform() + return true + } private func updateTouchesAtPoint(_ point: CGPoint?) { guard let _ = self.item, let theme = self.theme else { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureEncryptionKeyItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureEncryptionKeyItem.swift index 4a942dc0bec..224797a7db7 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureEncryptionKeyItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureEncryptionKeyItem.swift @@ -32,6 +32,7 @@ private final class PeerInfoScreenDisclosureEncryptionKeyItemNode: PeerInfoScree private let arrowNode: ASImageNode private let bottomSeparatorNode: ASDisplayNode private let maskNode: ASImageNode + private let activateArea: AccessibilityAreaNode private var item: PeerInfoScreenDisclosureEncryptionKeyItem? @@ -59,6 +60,8 @@ private final class PeerInfoScreenDisclosureEncryptionKeyItemNode: PeerInfoScree self.maskNode = ASImageNode() self.maskNode.isUserInteractionEnabled = false + + self.activateArea = AccessibilityAreaNode() super.init() @@ -72,6 +75,7 @@ private final class PeerInfoScreenDisclosureEncryptionKeyItemNode: PeerInfoScree self.addSubnode(self.keyNode) self.addSubnode(self.arrowNode) self.addSubnode(self.maskNode) + self.addSubnode(self.activateArea) } override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { @@ -86,6 +90,17 @@ private final class PeerInfoScreenDisclosureEncryptionKeyItemNode: PeerInfoScree self.item = item self.selectionNode.pressed = item.action + self.activateArea.accessibilityLabel = item.text + if let action = item.action { + self.activateArea.accessibilityTraits = [.button] + self.activateArea.activate = { + action() + return true + } + } else { + self.activateArea.accessibilityTraits = [.staticText] + self.activateArea.activate = nil + } let sideInset: CGFloat = 16.0 + safeInsets.left @@ -128,6 +143,7 @@ private final class PeerInfoScreenDisclosureEncryptionKeyItemNode: PeerInfoScree transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: sideInset, y: height - UIScreenPixel), size: CGSize(width: width - sideInset, height: UIScreenPixel))) transition.updateAlpha(node: self.bottomSeparatorNode, alpha: bottomItem == nil ? 0.0 : 1.0) + self.activateArea.frame = CGRect(origin: .zero, size: CGSize(width: width, height: height)) return height } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureItem.swift index f788f65067d..9f0f3ca89c4 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureItem.swift @@ -152,6 +152,16 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode { self.item = item self.selectionNode.pressed = item.action + if let action = item.action { + self.activateArea.accessibilityTraits = [.button] + self.activateArea.activate = { + action() + return true + } + } else { + self.activateArea.accessibilityTraits = [.staticText] + self.activateArea.activate = nil + } let sideInset: CGFloat = 16.0 + safeInsets.left let leftInset = (item.icon == nil && item.iconSignal == nil ? sideInset : sideInset + 29.0 + 16.0) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenLabeledValueItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenLabeledValueItem.swift index 37d162fdaa8..9a675ae6c90 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenLabeledValueItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenLabeledValueItem.swift @@ -484,8 +484,18 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { action(strongSelf.contextSourceNode, nil) } } + self.activateArea.accessibilityTraits = [.button] + self.activateArea.activate = { [weak self] in + guard let self else { + return false + } + action(self.contextSourceNode, nil) + return true + } } else { self.selectionNode.pressed = nil + self.activateArea.accessibilityTraits = [.staticText] + self.activateArea.activate = nil } let sideInset: CGFloat = 16.0 + safeInsets.left diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenMemberItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenMemberItem.swift index b6527b86b1c..dfea2cd23b1 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenMemberItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenMemberItem.swift @@ -9,6 +9,15 @@ import AccountContext import TelegramCore import ItemListUI +private final class PeerInfoMemberAccessibilityAction: UIAccessibilityCustomAction { + let perform: () -> Void + + init(name: String, target: Any?, selector: Selector, perform: @escaping () -> Void) { + self.perform = perform + super.init(name: name, target: target, selector: selector) + } +} + enum PeerInfoScreenMemberItemAction { case open case promote @@ -58,6 +67,7 @@ private final class PeerInfoScreenMemberItemNode: PeerInfoScreenItemNode { private let selectionNode: PeerInfoScreenSelectableBackgroundNode private let maskNode: ASImageNode private let bottomSeparatorNode: ASDisplayNode + private let activateArea: AccessibilityAreaNode private var item: PeerInfoScreenMemberItem? private var itemNode: ItemListPeerItemNode? @@ -72,6 +82,8 @@ private final class PeerInfoScreenMemberItemNode: PeerInfoScreenItemNode { self.bottomSeparatorNode = ASDisplayNode() self.bottomSeparatorNode.isLayerBacked = true + + self.activateArea = AccessibilityAreaNode() super.init() @@ -81,6 +93,7 @@ private final class PeerInfoScreenMemberItemNode: PeerInfoScreenItemNode { self.addSubnode(self.bottomSeparatorNode) self.addSubnode(self.selectionNode) + self.addSubnode(self.activateArea) } override func didLoad() { @@ -258,6 +271,42 @@ private final class PeerInfoScreenMemberItemNode: PeerInfoScreenItemNode { } itemNode.visibility = .visible(1.0, .infinite) + itemNode.isAccessibilityElement = false + + self.activateArea.accessibilityLabel = itemNode.accessibilityLabel + self.activateArea.accessibilityValue = itemNode.accessibilityValue + if let action = item.action { + self.activateArea.accessibilityTraits = [.button] + self.activateArea.activate = { + action(.open) + return true + } + } else { + self.activateArea.accessibilityTraits = [.staticText] + self.activateArea.activate = nil + } + + var accessibilityActions: [UIAccessibilityCustomAction] = [] + if let memberAction = item.action, actions.contains(.promote), case .channel = item.enclosingPeer { + accessibilityActions.append(PeerInfoMemberAccessibilityAction(name: presentationData.strings.GroupInfo_ActionPromote, target: self, selector: #selector(self.performAccessibilityAction(_:)), perform: { + memberAction(.promote) + })) + } + if let memberAction = item.action, actions.contains(.restrict), case .channel = item.enclosingPeer { + accessibilityActions.append(PeerInfoMemberAccessibilityAction(name: presentationData.strings.GroupInfo_ActionRestrict, target: self, selector: #selector(self.performAccessibilityAction(_:)), perform: { + memberAction(.restrict) + })) + } + if let memberAction = item.action, actions.contains(.restrict) { + accessibilityActions.append(PeerInfoMemberAccessibilityAction(name: presentationData.strings.Common_Delete, target: self, selector: #selector(self.performAccessibilityAction(_:)), perform: { + memberAction(.remove) + })) + } else if let memberAction = item.action, actions.contains(.logout) { + accessibilityActions.append(PeerInfoMemberAccessibilityAction(name: presentationData.strings.Settings_Context_Logout, target: self, selector: #selector(self.performAccessibilityAction(_:)), perform: { + memberAction(.remove) + })) + } + self.activateArea.accessibilityCustomActions = accessibilityActions.isEmpty ? nil : accessibilityActions let height = itemNode.contentSize.height @@ -279,6 +328,7 @@ private final class PeerInfoScreenMemberItemNode: PeerInfoScreenItemNode { let highlightNodeOffset: CGFloat = topItem == nil ? 0.0 : UIScreenPixel self.selectionNode.update(size: CGSize(width: width, height: height + highlightNodeOffset), theme: presentationData.theme, transition: transition) transition.updateFrame(node: self.selectionNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -highlightNodeOffset), size: CGSize(width: width, height: height + highlightNodeOffset))) + self.activateArea.frame = CGRect(origin: .zero, size: CGSize(width: width, height: height)) var separatorInset: CGFloat = sideInset if bottomItem != nil { @@ -292,6 +342,14 @@ private final class PeerInfoScreenMemberItemNode: PeerInfoScreenItemNode { return height } + + @objc private func performAccessibilityAction(_ action: UIAccessibilityCustomAction) -> Bool { + guard let action = action as? PeerInfoMemberAccessibilityAction else { + return false + } + action.perform() + return true + } private func updateTouchesAtPoint(_ point: CGPoint?) { guard let item = self.item else { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift index 1bf164df4ea..e539bec5076 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift @@ -430,6 +430,17 @@ private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNod self.item = item self.presentationData = presentationData self.theme = presentationData.theme + self.activateArea.isAccessibilityElement = !item.data.isLoading + self.activateArea.accessibilityTraits = [.button] + self.activateArea.activate = { + item.action() + return true + } + if let peer = item.data.peer.chatMainPeer { + self.activateArea.accessibilityLabel = EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + } else { + self.activateArea.accessibilityLabel = nil + } self.selectionNode.pressed = { [weak self] in if let strongSelf = self { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenSwitchItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenSwitchItem.swift index 10cff7c74cc..cd2f9909a0c 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenSwitchItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenSwitchItem.swift @@ -86,7 +86,7 @@ private final class PeerInfoScreenSwitchItemNode: PeerInfoScreenItemNode { guard let strongSelf = self, let item = strongSelf.item else { return false } - let value = !strongSelf.switchNode.isOn + let value = item.isLocked ? strongSelf.switchNode.isOn : !strongSelf.switchNode.isOn item.toggled?(value) return true } @@ -163,7 +163,8 @@ private final class PeerInfoScreenSwitchItemNode: PeerInfoScreenItemNode { self.activateArea.accessibilityLabel = item.text self.activateArea.accessibilityValue = item.value ? presentationData.strings.VoiceOver_Common_On : presentationData.strings.VoiceOver_Common_Off - self.activateArea.accessibilityHint = presentationData.strings.VoiceOver_Common_SwitchHint + self.activateArea.accessibilityHint = item.isLocked ? nil : presentationData.strings.VoiceOver_Common_SwitchHint + self.activateArea.accessibilityTraits = [.button] let textSize = self.textNode.updateLayout(CGSize(width: width - leftInset - rightInset, height: .greatestFiniteMagnitude)) let textFrame = CGRect(origin: CGPoint(x: leftInset, y: 16.0), size: textSize) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoGifPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoGifPaneNode.swift index 5365b2f8942..99c2bbffb30 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoGifPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoGifPaneNode.swift @@ -80,6 +80,9 @@ private final class VisualMediaItemNode: ASDisplayNode { self.mediaBadgeNode.frame = CGRect(origin: CGPoint(x: 6.0, y: 6.0), size: CGSize(width: 50.0, height: 50.0)) super.init() + + self.isAccessibilityElement = true + self.accessibilityTraits = [.button, .image] self.addSubnode(self.containerNode) self.containerNode.addSubnode(self.imageNode) @@ -116,34 +119,38 @@ private final class VisualMediaItemNode: ASDisplayNode { if case .ended = recognizer.state { if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation { if case .tap = gesture { - if let (item, _, _, _) = self.item { - var media: EngineRawMedia? - for value in item.message.effectiveMedia { - if let image = value as? TelegramMediaImage { - media = image - break - } else if let file = value as? TelegramMediaFile { - media = file - break - } - } - - if let media = media { - if let file = media as? TelegramMediaFile { - if isMediaStreamable(message: EngineMessage(item.message), media: file) { - self.interaction.openMessage(item.message) - } else { - self.progressPressed() - } - } else { - self.interaction.openMessage(item.message) - } - } - } + let _ = self.activateMedia() } } } } + + private func activateMedia() -> Bool { + guard let item = self.item?.0 else { + return false + } + + var media: EngineRawMedia? + for value in item.message.effectiveMedia { + if let image = value as? TelegramMediaImage { + media = image + break + } else if let file = value as? TelegramMediaFile { + media = file + break + } + } + + guard let media else { + return false + } + if let file = media as? TelegramMediaFile, !isMediaStreamable(message: EngineMessage(item.message), media: file) { + self.progressPressed() + } else { + self.interaction.openMessage(item.message) + } + return true + } private func progressPressed() { guard let message = self.item?.0.message else { @@ -299,6 +306,15 @@ private final class VisualMediaItemNode: ASDisplayNode { self.mediaBadgeNode.isHidden = true } self.item = (item, media, size, mediaDimensions) + + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + if media is TelegramMediaImage { + self.accessibilityLabel = presentationData.strings.VoiceOver_Chat_Photo + } else { + self.accessibilityLabel = presentationData.strings.VoiceOver_Chat_Video + } + self.accessibilityValue = item.message.text.isEmpty ? nil : item.message.text + self.updateAccessibilityActions(strings: presentationData.strings) self.updateHiddenMedia() } @@ -340,9 +356,16 @@ private final class VisualMediaItemNode: ASDisplayNode { func updateSelectionState(animated: Bool) { if let (item, _, _, _) = self.item, let theme = self.theme { self.containerNode.isGestureEnabled = self.interaction.selectedMessageIds == nil + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + self.updateAccessibilityActions(strings: presentationData.strings) if let selectedIds = self.interaction.selectedMessageIds { let selected = selectedIds.contains(item.message.id) + if selected { + self.accessibilityTraits.insert(.selected) + } else { + self.accessibilityTraits.remove(.selected) + } if let selectionNode = self.selectionNode { selectionNode.updateSelected(selected, animated: animated) @@ -367,6 +390,7 @@ private final class VisualMediaItemNode: ASDisplayNode { } } } else { + self.accessibilityTraits.remove(.selected) if let selectionNode = self.selectionNode { self.selectionNode = nil if animated { @@ -380,6 +404,36 @@ private final class VisualMediaItemNode: ASDisplayNode { } } } + + override func accessibilityActivate() -> Bool { + guard let item = self.item?.0 else { + return false + } + if let selectedMessageIds = self.interaction.selectedMessageIds { + self.interaction.toggleSelection(item.message.id, !selectedMessageIds.contains(item.message.id)) + return true + } else { + return self.activateMedia() + } + } + + private func updateAccessibilityActions(strings: PresentationStrings) { + if self.interaction.selectedMessageIds == nil { + self.accessibilityCustomActions = [ + UIAccessibilityCustomAction(name: strings.VoiceOver_MessageContextOpenMessageMenu, target: self, selector: #selector(self.accessibilityOpenContextMenu(_:))) + ] + } else { + self.accessibilityCustomActions = nil + } + } + + @objc private func accessibilityOpenContextMenu(_ action: UIAccessibilityCustomAction) -> Bool { + guard self.interaction.selectedMessageIds == nil, let item = self.item?.0 else { + return false + } + self.interaction.openMessageContextActions(item.message, self.containerNode, self.containerNode.bounds, nil) + return true + } func transitionNode() -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { let imageNode = self.imageNode diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedPeersPane.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedPeersPane.swift index c518841d851..2998ab90689 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedPeersPane.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedPeersPane.swift @@ -363,12 +363,16 @@ final class PeerInfoRecommendedPeersPaneNode: ASDisplayNode, PeerInfoPaneNode { unlockButton.animationLoopTime = 2.5 unlockButton.animation = "premium_unlock" unlockButton.iconPosition = .right - unlockButton.title = isBots ? presentationData.strings.PeerInfo_SimilarBots_ShowMore : presentationData.strings.Channel_SimilarChannels_ShowMore unlockButton.pressed = { [weak self] in self?.unlockPressed() } } + let unlockTitle = isBots ? presentationData.strings.PeerInfo_SimilarBots_ShowMore : presentationData.strings.Channel_SimilarChannels_ShowMore + unlockButton.title = unlockTitle + unlockButton.isAccessibilityElement = true + unlockButton.accessibilityLabel = unlockTitle + unlockButton.accessibilityTraits = [.button] if themeUpdated { let topColor = presentationData.theme.list.plainBackgroundColor.withAlphaComponent(0.0) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarTransformContainerNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarTransformContainerNode.swift index 89dc09d0d02..30308bb378e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarTransformContainerNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarTransformContainerNode.swift @@ -24,6 +24,7 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { let containerNode: ContextControllerSourceNode let avatarNode: AvatarNode + private let accessibilityArea: AccessibilityAreaNode private(set) var avatarStoryView: ComponentView? var videoNode: UniversalVideoNode? var markupNode: AvatarVideoNode? @@ -60,13 +61,18 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { let avatarFont = avatarPlaceholderFont(size: floor(100.0 * 16.0 / 37.0)) self.avatarNode = AvatarNode(font: avatarFont) + self.accessibilityArea = AccessibilityAreaNode() super.init() self.addSubnode(self.containerNode) self.containerNode.addSubnode(self.avatarNode) + self.containerNode.addSubnode(self.accessibilityArea) self.containerNode.frame = CGRect(origin: CGPoint(x: -50.0, y: -50.0), size: CGSize(width: 100.0, height: 100.0)) self.avatarNode.frame = self.containerNode.bounds + self.accessibilityArea.frame = self.containerNode.bounds + self.accessibilityArea.accessibilityTraits = [.button, .image] + self.avatarNode.isAccessibilityElement = false let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) self.avatarNode.view.addGestureRecognizer(tapGestureRecognizer) @@ -252,8 +258,22 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { func update(peer: EnginePeer?, threadId: Int64?, threadInfo: EngineMessageHistoryThread.Info?, item: PeerInfoAvatarListItem?, theme: PresentationTheme, avatarSize: CGFloat, isExpanded: Bool, isSettings: Bool) { self.params = Params(peer: peer, threadId: threadId, threadInfo: threadInfo, item: item, theme: theme, avatarSize: avatarSize, isExpanded: isExpanded, isSettings: isSettings) + self.accessibilityArea.isAccessibilityElement = peer != nil if let peer = peer { + self.accessibilityArea.accessibilityLabel = threadInfo?.title ?? peer.compactDisplayTitle + self.accessibilityArea.activate = { [weak self] in + guard let self else { + return false + } + if threadInfo != nil { + self.emojiTapped?() + } else { + self.tapped?() + } + return true + } + let previousItem = self.item var item = item self.item = item @@ -343,6 +363,7 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { self.containerNode.frame = CGRect(origin: CGPoint(x: -avatarSize / 2.0, y: -avatarSize / 2.0), size: CGSize(width: avatarSize, height: avatarSize)) self.avatarNode.frame = self.containerNode.bounds + self.accessibilityArea.frame = self.containerNode.bounds self.avatarNode.font = avatarPlaceholderFont(size: floor(avatarSize * 16.0 / 37.0)) if let item = item { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarNode.swift index 98fc8ba70d5..f9761f0759e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarNode.swift @@ -16,6 +16,7 @@ import GalleryUI final class PeerInfoEditingAvatarNode: ASDisplayNode { private let context: AccountContext let avatarNode: AvatarNode + private let accessibilityArea: AccessibilityAreaNode fileprivate var videoNode: UniversalVideoNode? fileprivate var markupNode: AvatarVideoNode? private var videoContent: NativeVideoContent? @@ -30,11 +31,23 @@ final class PeerInfoEditingAvatarNode: ASDisplayNode { self.context = context let avatarFont = avatarPlaceholderFont(size: floor(100.0 * 16.0 / 37.0)) self.avatarNode = AvatarNode(font: avatarFont) + self.accessibilityArea = AccessibilityAreaNode() super.init() self.addSubnode(self.avatarNode) + self.addSubnode(self.accessibilityArea) self.avatarNode.frame = CGRect(origin: CGPoint(x: -50.0, y: -50.0), size: CGSize(width: 100.0, height: 100.0)) + self.accessibilityArea.frame = self.avatarNode.frame + self.accessibilityArea.accessibilityTraits = [.button, .image] + self.accessibilityArea.activate = { [weak self] in + guard let self else { + return false + } + self.tapped?(false) + return true + } + self.avatarNode.isAccessibilityElement = false self.avatarNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } @@ -59,8 +72,11 @@ final class PeerInfoEditingAvatarNode: ASDisplayNode { var removedPhotoResourceIds = Set() func update(peer: EnginePeer?, threadData: MessageHistoryThreadData?, chatLocation: ChatLocation, item: PeerInfoAvatarListItem?, updatingAvatar: PeerInfoUpdatingAvatar?, uploadProgress: AvatarUploadProgress?, theme: PresentationTheme, avatarSize: CGFloat, isEditing: Bool) { guard let peer = peer else { + self.accessibilityArea.isAccessibilityElement = false return } + self.accessibilityArea.isAccessibilityElement = true + self.accessibilityArea.accessibilityLabel = peer.compactDisplayTitle let canEdit = canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData) @@ -86,6 +102,7 @@ final class PeerInfoEditingAvatarNode: ASDisplayNode { self.avatarNode.font = avatarPlaceholderFont(size: floor(avatarSize * 16.0 / 37.0)) self.avatarNode.setPeer(context: self.context, theme: theme, peer: peer, overrideImage: overrideImage, clipStyle: .none, synchronousLoad: false, displayDimensions: CGSize(width: avatarSize, height: avatarSize)) self.avatarNode.frame = CGRect(origin: CGPoint(x: -avatarSize / 2.0, y: -avatarSize / 2.0), size: CGSize(width: avatarSize, height: avatarSize)) + self.accessibilityArea.frame = self.avatarNode.frame var isForum = false let avatarCornerRadius: CGFloat diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderEditingContentNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderEditingContentNode.swift index 8a704e1cb13..950aaac88c9 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderEditingContentNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderEditingContentNode.swift @@ -27,6 +27,8 @@ final class PeerInfoHeaderEditingContentNode: ASDisplayNode { self.avatarTextNode = ImmediateTextNode() self.avatarButtonNode = HighlightableButtonNode() + self.avatarButtonNode.isAccessibilityElement = true + self.avatarButtonNode.accessibilityTraits = [.button] super.init() diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderMultiLineTextFieldNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderMultiLineTextFieldNode.swift index 9d253d3f71c..6834ba5c5e2 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderMultiLineTextFieldNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderMultiLineTextFieldNode.swift @@ -53,6 +53,7 @@ final class PeerInfoHeaderMultiLineTextFieldNode: ASDisplayNode, PeerInfoHeaderT self.clearButtonNode = HighlightableButtonNode() self.clearButtonNode.isHidden = true self.clearButtonNode.isAccessibilityElement = false + self.clearButtonNode.accessibilityTraits = .button self.maskNode = ASImageNode() self.maskNode.isUserInteractionEnabled = false @@ -130,7 +131,9 @@ final class PeerInfoHeaderMultiLineTextFieldNode: ASDisplayNode, PeerInfoHeaderT let attributedPlaceholderText = NSAttributedString(string: placeholder, font: titleFont, textColor: presentationData.theme.list.itemPlaceholderTextColor) if self.textNode.attributedPlaceholderText == nil || !self.textNode.attributedPlaceholderText!.isEqual(to: attributedPlaceholderText) { self.textNode.attributedPlaceholderText = attributedPlaceholderText + self.textNode.textView.accessibilityHint = attributedPlaceholderText.string } + self.clearButtonNode.accessibilityLabel = presentationData.strings.WebSearch_RecentSectionClear if let updateText = updateText { let attributedText = NSAttributedString(string: updateText, font: titleFont, textColor: presentationData.theme.list.itemPrimaryTextColor) @@ -184,7 +187,7 @@ final class PeerInfoHeaderMultiLineTextFieldNode: ASDisplayNode, PeerInfoHeaderT let isHidden = !self.textNode.isFirstResponder() || self.text.isEmpty self.clearIconNode.isHidden = isHidden self.clearButtonNode.isHidden = isHidden - self.clearButtonNode.isAccessibilityElement = isHidden + self.clearButtonNode.isAccessibilityElement = !isHidden } func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift index fdfd1cee79e..51823b4fc88 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift @@ -45,6 +45,15 @@ import BundleIconComponent import MarqueeComponent import EdgeEffect +private final class PeerInfoHeaderAccessibilityAction: UIAccessibilityCustomAction { + let perform: () -> Void + + init(name: String, target: Any?, selector: Selector, perform: @escaping () -> Void) { + self.perform = perform + super.init(name: name, target: target, selector: selector) + } +} + final class PeerInfoHeaderNavigationTransition { let sourceNavigationBar: NavigationBar let sourceTitleView: ChatTitleView @@ -1370,6 +1379,13 @@ final class PeerInfoHeaderNode: ASDisplayNode { TitleNodeStateRegular: MultiScaleTextState(attributes: titleAttributes, constrainedSize: titleConstrainedSize), TitleNodeStateExpanded: MultiScaleTextState(attributes: smallTitleAttributes, constrainedSize: titleConstrainedSize) ], mainState: TitleNodeStateRegular) + if let peer, peer.isScam { + self.titleNode.accessibilityLabel = "\(titleStringText), \(presentationData.strings.Message_ScamAccount)" + } else if let peer, peer.isFake { + self.titleNode.accessibilityLabel = "\(titleStringText), \(presentationData.strings.Message_FakeAccount)" + } else { + self.titleNode.accessibilityLabel = titleStringText + } let subtitleStates: [AnyHashable: MultiScaleTextState] = [ TitleNodeStateRegular: MultiScaleTextState(attributes: subtitleAttributes, constrainedSize: titleConstrainedSize), @@ -1382,6 +1398,27 @@ final class PeerInfoHeaderNode: ASDisplayNode { subtitleNodeLayout = self.subtitleNode.updateLayout(text: subtitleStringText, states: subtitleStates, mainState: TitleNodeStateRegular) } self.subtitleNode.accessibilityLabel = subtitleStringText + self.subtitleNode.isAccessibilityElement = !subtitleIsButton + var subtitleAccessibilityActions: [UIAccessibilityCustomAction] = [] + if self.isSettings, case let .user(user) = peer { + if let phone = user.phone, !phone.isEmpty { + subtitleAccessibilityActions.append(PeerInfoHeaderAccessibilityAction(name: presentationData.strings.Settings_CopyPhoneNumber, target: self, selector: #selector(self.performAccessibilityAction(_:)), perform: { [weak self] in + guard let self else { + return + } + self.displayCopyContextMenu?(self.subtitleNodeRawContainer, true, false) + })) + } + if let username = user.addressName, !username.isEmpty { + subtitleAccessibilityActions.append(PeerInfoHeaderAccessibilityAction(name: presentationData.strings.Settings_CopyUsername, target: self, selector: #selector(self.performAccessibilityAction(_:)), perform: { [weak self] in + guard let self else { + return + } + self.displayCopyContextMenu?(self.subtitleNodeRawContainer, false, true) + })) + } + } + self.subtitleNode.accessibilityCustomActions = subtitleAccessibilityActions.isEmpty ? nil : subtitleAccessibilityActions var subtitleButtonHorizontalOffset: CGFloat = 0.0 if subtitleIsButton { @@ -1401,6 +1438,8 @@ final class PeerInfoHeaderNode: ASDisplayNode { subtitleBackgroundButton = HighlightTrackingButtonNode() self.subtitleBackgroundButton = subtitleBackgroundButton self.subtitleNode.addSubnode(subtitleBackgroundButton) + subtitleBackgroundButton.isAccessibilityElement = true + subtitleBackgroundButton.accessibilityTraits = [.button] subtitleBackgroundButton.addTarget(self, action: #selector(self.subtitleBackgroundPressed), forControlEvents: .touchUpInside) subtitleBackgroundButton.highligthedChanged = { [weak self] highlighted in @@ -1416,6 +1455,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { } } } + subtitleBackgroundButton.accessibilityLabel = subtitleStringText let subtitleArrowNode: ASImageNode if let current = self.subtitleArrowNode { @@ -2707,6 +2747,13 @@ final class PeerInfoHeaderNode: ASDisplayNode { musicView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } } + musicView.isAccessibilityElement = true + musicView.accessibilityLabel = musicString.string + musicView.accessibilityTraits = [.button] + musicView.accessibilityElementsHidden = false + for subview in musicView.subviews { + subview.accessibilityElementsHidden = true + } if additive { musicTransition.updateFrameAdditiveToCenter(view: musicView, frame: musicFrame) } else { @@ -2756,6 +2803,14 @@ final class PeerInfoHeaderNode: ASDisplayNode { private func actionButtonPressed(_ buttonNode: PeerInfoHeaderActionButtonNode, gesture: ContextGesture?) { self.performButtonAction?(buttonNode.key, nil, gesture) } + + @objc private func performAccessibilityAction(_ action: UIAccessibilityCustomAction) -> Bool { + guard let action = action as? PeerInfoHeaderAccessibilityAction else { + return false + } + action.perform() + return true + } override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { var result = super.point(inside: point, with: event) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderSingleLineTextFieldNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderSingleLineTextFieldNode.swift index 762bb6b22a7..7c1bbb7af30 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderSingleLineTextFieldNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderSingleLineTextFieldNode.swift @@ -36,6 +36,7 @@ final class PeerInfoHeaderSingleLineTextFieldNode: ASDisplayNode, PeerInfoHeader self.clearButtonNode = HighlightableButtonNode() self.clearButtonNode.isHidden = true self.clearButtonNode.isAccessibilityElement = false + self.clearButtonNode.accessibilityTraits = .button self.topSeparator = ASDisplayNode() @@ -84,12 +85,13 @@ final class PeerInfoHeaderSingleLineTextFieldNode: ASDisplayNode, PeerInfoHeader let isHidden = !self.textNode.textField.isFirstResponder || self.text.isEmpty self.clearIconNode.isHidden = isHidden self.clearButtonNode.isHidden = isHidden - self.clearButtonNode.isAccessibilityElement = isHidden + self.clearButtonNode.isAccessibilityElement = !isHidden } func update(width: CGFloat, safeInset: CGFloat, isSettings: Bool, hasPrevious: Bool, hasNext: Bool, placeholder: String, isEnabled: Bool, presentationData: PresentationData, updateText: String?) -> CGFloat { let titleFont = Font.regular(presentationData.listsFontSize.itemListBaseFontSize) self.textNode.textField.font = titleFont + self.clearButtonNode.accessibilityLabel = presentationData.strings.WebSearch_RecentSectionClear if self.theme !== presentationData.theme { self.theme = presentationData.theme diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSubtitleBadgeView.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSubtitleBadgeView.swift index 0e194b6af97..94332864224 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSubtitleBadgeView.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSubtitleBadgeView.swift @@ -20,6 +20,9 @@ final class PeerInfoSubtitleBadgeView: HighlightTrackingButton { self.backgroundView.isUserInteractionEnabled = false super.init(frame: CGRect()) + + self.isAccessibilityElement = true + self.accessibilityTraits = [.button] self.addSubview(self.backgroundView) @@ -64,6 +67,8 @@ final class PeerInfoSubtitleBadgeView: HighlightTrackingButton { } func update(title: String, fillColor: UIColor, foregroundColor: UIColor) -> CGSize { + self.accessibilityLabel = title + let labelSize = self.labelView.update( transition: .immediate, component: AnyComponent(Text(text: title, font: Font.regular(11.0), color: foregroundColor)), @@ -80,6 +85,7 @@ final class PeerInfoSubtitleBadgeView: HighlightTrackingButton { if let labelComponentView = self.labelView.view { if labelComponentView.superview == nil { labelComponentView.isUserInteractionEnabled = false + labelComponentView.accessibilityElementsHidden = true self.addSubview(labelComponentView) } labelComponentView.frame = CGRect(origin: CGPoint(x: floor((size.width - labelSize.width) * 0.5), y: floor((size.height - labelSize.height) * 0.5)), size: labelSize) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/AddGiftsScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/AddGiftsScreen.swift index fe41b91c02b..404ac155b22 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/AddGiftsScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/AddGiftsScreen.swift @@ -184,6 +184,7 @@ final class AddGiftsScreenComponent: Component { if buttonPanelView.superview == nil { self.addSubview(buttonPanelView) } + buttonPanelView.accessibilityElementsHidden = giftsListView.selectedItems.isEmpty transition.setFrame(view: buttonPanelView, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelSize.height + bottomPanelOffset), size: bottomPanelSize)) } @@ -266,7 +267,9 @@ public final class AddGiftsScreen: ViewControllerComponentContainer { } self.filterButton.addTarget(self, action: #selector(self.filterPressed), forControlEvents: .touchUpInside) - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "___close", style: .plain, target: self, action: #selector(self.cancelPressed)) + let closeButtonItem = UIBarButtonItem(title: "___close", style: .plain, target: self, action: #selector(self.cancelPressed)) + closeButtonItem.accessibilityLabel = presentationData.strings.Common_Close + self.navigationItem.leftBarButtonItem = closeButtonItem self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: self.filterButton) } @@ -426,6 +429,10 @@ private final class FilterHeaderButton: HighlightableButtonNode { super.init() + self.isAccessibilityElement = true + self.accessibilityLabel = presentationData.strings.Common_More + self.accessibilityTraits = [.button] + self.containerNode.addSubnode(self.referenceNode) self.addSubnode(self.containerNode) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftsListView.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftsListView.swift index e1c98a1ac32..24f2630ba91 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftsListView.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftsListView.swift @@ -747,6 +747,7 @@ final class GiftsListView: UIView { if !validIds.contains(id) { removeIds.append(id) if let itemView = item.1.view { + itemView.accessibilityElementsHidden = true if !transition.animation.isImmediate { itemView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false) itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in @@ -859,6 +860,9 @@ final class GiftsListView: UIView { } view.bounds = CGRect(origin: .zero, size: emptyResultsTitleFrame.size) panelTransition.setPosition(view: view, position: emptyResultsTitleFrame.center) + view.isAccessibilityElement = true + view.accessibilityLabel = presentationData.strings.PeerInfo_Gifts_EmptyCollection_Title + view.accessibilityTraits = [.header] } if let view = self.emptyResultsText.view { if view.superview == nil { @@ -868,6 +872,9 @@ final class GiftsListView: UIView { } view.bounds = CGRect(origin: .zero, size: emptyResultsTextFrame.size) panelTransition.setPosition(view: view, position: emptyResultsTextFrame.center) + view.isAccessibilityElement = true + view.accessibilityLabel = presentationData.strings.PeerInfo_Gifts_EmptyCollection_Text + view.accessibilityTraits = [.staticText] } if let view = self.emptyResultsAction.view { if view.superview == nil { @@ -877,6 +884,12 @@ final class GiftsListView: UIView { } view.bounds = CGRect(origin: .zero, size: emptyResultsActionFrame.size) panelTransition.setPosition(view: view, position: emptyResultsActionFrame.center) + view.isAccessibilityElement = true + view.accessibilityLabel = presentationData.strings.PeerInfo_Gifts_EmptyCollection_Action + view.accessibilityTraits = [.button] + for subview in view.subviews { + subview.accessibilityElementsHidden = true + } } } else if self.filteredResultsAreEmpty { let sideInset: CGFloat = 44.0 @@ -951,6 +964,7 @@ final class GiftsListView: UIView { self.emptyResultsClippingView.addSubview(view) view.playOnce() } + view.accessibilityElementsHidden = true view.bounds = CGRect(origin: .zero, size: emptyResultsAnimationFrame.size) panelTransition.setPosition(view: view, position: emptyResultsAnimationFrame.center) } @@ -962,6 +976,9 @@ final class GiftsListView: UIView { } view.bounds = CGRect(origin: .zero, size: emptyResultsTitleFrame.size) panelTransition.setPosition(view: view, position: emptyResultsTitleFrame.center) + view.isAccessibilityElement = true + view.accessibilityLabel = presentationData.strings.PeerInfo_Gifts_NoResults + view.accessibilityTraits = [.header] } if let view = self.emptyResultsAction.view { if view.superview == nil { @@ -971,8 +988,15 @@ final class GiftsListView: UIView { } view.bounds = CGRect(origin: .zero, size: emptyResultsActionFrame.size) panelTransition.setPosition(view: view, position: emptyResultsActionFrame.center) + view.isAccessibilityElement = true + view.accessibilityLabel = presentationData.strings.PeerInfo_Gifts_NoResults_ViewAll + view.accessibilityTraits = [.button] + for subview in view.subviews { + subview.accessibilityElementsHidden = true + } } } else { + self.emptyResultsClippingView.accessibilityElementsHidden = true if let view = self.emptyResultsAnimation.view { fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in view.removeFromSuperview() @@ -997,6 +1021,7 @@ final class GiftsListView: UIView { } fadeTransition.setAlpha(view: self.emptyResultsClippingView, alpha: visibleHeight < 300.0 ? 0.0 : 1.0) + self.emptyResultsClippingView.accessibilityElementsHidden = (!self.resultsAreEmpty && !self.filteredResultsAreEmpty) || self.emptyResultsClippingView.isHidden || visibleHeight < 300.0 if self.peerId == self.context.account.peerId, !self.canSelect && !self.filteredResultsAreEmpty && self.profileGifts.collectionId == nil && self.emptyResultsClippingView.isHidden { let footerText: ComponentView diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index ee9ea2d9fc6..d893925f24a 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -470,6 +470,8 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr } self.giftsListView.parentController = self.parentController self.giftsListView.frame = previousGiftsListView.frame + previousGiftsListView.accessibilityElementsHidden = true + self.giftsListView.accessibilityElementsHidden = false self.scrollNode.view.insertSubview(self.giftsListView, aboveSubview: previousGiftsListView) @@ -709,6 +711,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr if let tabSelectorView = self.tabSelector.view { if tabSelectorView.superview == nil { tabSelectorView.alpha = 1.0 + tabSelectorView.accessibilityElementsHidden = false self.scrollNode.view.insertSubview(tabSelectorView, at: 0) if !transition.animation.isImmediate { @@ -720,6 +723,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr topInset += tabSelectorSize.height + 15.0 } } else if let tabSelectorView = self.tabSelector.view { + tabSelectorView.accessibilityElementsHidden = true tabSelectorView.alpha = 0.0 tabSelectorView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, completion: { _ in tabSelectorView.removeFromSuperview() @@ -834,6 +838,12 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr panelContentContainer.addSubview(panelButtonView) } panelButtonView.frame = CGRect(origin: CGPoint(x: buttonInsets.left, y: 8.0), size: panelButtonSize) + panelButtonView.isAccessibilityElement = true + panelButtonView.accessibilityLabel = buttonTitle + panelButtonView.accessibilityTraits = [.button] + for subview in panelButtonView.subviews { + subview.accessibilityElementsHidden = true + } } panelTransition.setFrame(view: panelContentContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: size.height - bottomPanelHeight), size: CGSize(width: size.width, height: bottomPanelHeight))) @@ -904,6 +914,15 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr panelContentContainer.addSubview(panelCheckView) } panelCheckView.frame = CGRect(origin: CGPoint(x: floor((size.width - panelCheckSize.width) / 2.0), y: 16.0 + 16.0), size: panelCheckSize) + panelCheckView.isAccessibilityElement = true + panelCheckView.accessibilityLabel = presentationData.strings.PeerInfo_Gifts_ChannelNotify + panelCheckView.accessibilityTraits = [.button] + if self.profileGifts.currentState?.notificationsEnabled == true { + panelCheckView.accessibilityTraits.insert(.selected) + } + for subview in panelCheckView.subviews { + subview.accessibilityElementsHidden = true + } } if let panelButtonView = panelButton.view { panelButtonView.isHidden = true @@ -915,6 +934,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr panelEdgeEffectView.update(content: presentationData.theme.list.blocksBackgroundColor, blur: false, rect: edgeEffectFrame, edge: .bottom, edgeSize: 40.0, transition: panelTransition) ComponentTransition.spring(duration: 0.4).setSublayerTransform(view: panelContentContainer, transform: CATransform3DMakeTranslation(0.0, bottomPanelHeight * (1.0 - panelVisibility), 0.0)) + panelContentContainer.accessibilityElementsHidden = panelVisibility == 0.0 contentHeight += bottomPanelHeight bottomScrollInset = bottomPanelHeight - 40.0 diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index 921303febf3..48d60356684 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -1468,6 +1468,9 @@ private final class StorySearchHeaderComponent: Component { let insets = UIEdgeInsets(top: 7.0, left: 16.0, bottom: 7.0, right: 16.0) let titleString = component.strings.StoryList_GridHeaderLocationSearch(Int32(component.count)) + self.isAccessibilityElement = true + self.accessibilityLabel = titleString + self.accessibilityTraits = [.staticText] let titleSize = self.title.update( transition: .immediate, @@ -1479,6 +1482,7 @@ private final class StorySearchHeaderComponent: Component { ) if let titleView = self.title.view { if titleView.superview == nil { + titleView.accessibilityElementsHidden = true self.addSubview(titleView) } titleView.frame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: titleSize) @@ -1497,6 +1501,27 @@ private final class StorySearchHeaderComponent: Component { } } +private final class StoryGridAccessibilityElement: UIAccessibilityElement { + var activate: () -> Bool = { + return false + } + var openContextMenu: () -> Bool = { + return false + } + + init(accessibilityContainer container: Any) { + super.init(accessibilityContainer: container) + } + + override func accessibilityActivate() -> Bool { + return self.activate() + } + + @objc func accessibilityOpenContextMenu(_ action: UIAccessibilityCustomAction) -> Bool { + return self.openContextMenu() + } +} + public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScrollViewDelegate, ASGestureRecognizerDelegate { public enum Scope { case peer(id: EnginePeer.Id, isSaved: Bool, isArchived: Bool) @@ -1579,6 +1604,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr private let itemGrid: SparseItemGrid private let itemGridBinding: SparseItemGridBindingImpl + private var storyAccessibilityElements: [EngineStoryId: StoryGridAccessibilityElement] = [:] private let directMediaImageCache: DirectMediaImageCache private var items: SparseItemGrid.Items? private var pinnedIds: Set = Set() @@ -2046,6 +2072,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } strongSelf.paneDidScroll?() strongSelf.cancelPreviewGestures() + strongSelf.updateAccessibilityElements() if strongSelf.locationViewState.displayingMapModeOptions { strongSelf.locationViewState.displayingMapModeOptions = false @@ -3500,6 +3527,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } itemLayer.updateSelection(theme: self.itemGridBinding.checkNodeTheme, isSelected: isSelected, animated: animated) } + self.updateAccessibilityElements() var isSelecting = false if let selectedIds = self._itemInteraction?.selectedIds, !selectedIds.isEmpty { @@ -3546,6 +3574,67 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.gridSelectionGesture = nil } } + + private func updateAccessibilityElements() { + var accessibilityElements: [UIAccessibilityElement] = [] + var validStoryIds: Set = [] + self.itemGrid.forEachVisibleItem { [weak self] displayItem in + guard let self, let itemLayer = displayItem.layer as? ItemLayer, let item = itemLayer.item, !itemLayer.isHidden else { + return + } + validStoryIds.insert(item.storyId) + + let accessibilityElement: StoryGridAccessibilityElement + if let current = self.storyAccessibilityElements[item.storyId] { + accessibilityElement = current + } else { + accessibilityElement = StoryGridAccessibilityElement(accessibilityContainer: self.itemGrid.view) + self.storyAccessibilityElements[item.storyId] = accessibilityElement + } + accessibilityElement.activate = { [weak self, weak itemLayer] in + guard let self, let itemLayer else { + return false + } + self.itemGridBinding.onTap(item: item, itemLayer: itemLayer, point: CGPoint(x: itemLayer.bounds.midX, y: itemLayer.bounds.midY)) + return true + } + accessibilityElement.openContextMenu = { [weak self, weak itemLayer] in + guard let self, let itemLayer else { + return false + } + let rect = self.itemGrid.frameForItem(layer: itemLayer) + self.openContextMenu(item: item.story, itemLayer: itemLayer, rect: rect, gesture: nil) + return true + } + var label = item.story.media._asMedia() is TelegramMediaFile ? self.presentationData.strings.VoiceOver_Chat_Video : self.presentationData.strings.VoiceOver_Chat_Photo + if let authorPeer = item.authorPeer { + label += ", \(authorPeer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder))" + } + accessibilityElement.accessibilityLabel = label + accessibilityElement.accessibilityTraits = [.button, .image] + accessibilityElement.accessibilityCustomActions = [ + UIAccessibilityCustomAction(name: self.presentationData.strings.VoiceOver_MessageContextOpenMessageMenu, target: accessibilityElement, selector: #selector(StoryGridAccessibilityElement.accessibilityOpenContextMenu(_:))) + ] + if self.itemInteraction.selectedIds?.contains(item.story.id) == true { + accessibilityElement.accessibilityTraits.insert(.selected) + accessibilityElement.accessibilityValue = self.presentationData.strings.VoiceOver_Chat_Selected + } + accessibilityElement.accessibilityFrameInContainerSpace = self.itemGrid.frameForItem(layer: itemLayer) + accessibilityElements.append(accessibilityElement) + } + accessibilityElements.sort { lhs, rhs in + let lhsFrame = lhs.accessibilityFrameInContainerSpace + let rhsFrame = rhs.accessibilityFrameInContainerSpace + if abs(lhsFrame.minY - rhsFrame.minY) > UIScreenPixel { + return lhsFrame.minY < rhsFrame.minY + } else { + return lhsFrame.minX < rhsFrame.minX + } + } + self.storyAccessibilityElements = self.storyAccessibilityElements.filter { validStoryIds.contains($0.key) } + self.itemGrid.view.isAccessibilityElement = false + self.itemGrid.view.accessibilityElements = accessibilityElements + } private func updateHiddenItems() { self.itemGrid.forEachVisibleItem { itemValue in @@ -3564,6 +3653,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } } } + self.updateAccessibilityElements() } private func presentDeleteConfirmation(ids: Set) { @@ -3708,7 +3798,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, - inVoiceOver: false + inVoiceOver: UIAccessibility.isVoiceOverRunning ), navigationBarHeight: 0.0, topPadding: mapOverscrollInset + self.additionalNavigationHeight, @@ -4854,6 +4944,9 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.itemGrid.pinchEnabled = items.count > 2 && !self.isReordering self.itemGrid.update(size: size, insets: UIEdgeInsets(top: gridTopInset, left: sideInset, bottom: listBottomInset, right: sideInset), useSideInsets: !isList, scrollIndicatorInsets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), lockScrollingAtTop: isScrollingLockedAtTop, fixedItemHeight: fixedItemHeight, fixedItemAspect: fixedItemAspect, adjustForSmallCount: adjustForSmallCount, items: items, theme: self.itemGridBinding.chatPresentationData.theme.theme, synchronous: wasFirstTime ? .full : .none, transition: animateGridItems ? .spring(duration: 0.35) : .immediate) + DispatchQueue.main.async { [weak self] in + self?.updateAccessibilityElements() + } } self.listBottomInset = listBottomInset @@ -5760,6 +5853,12 @@ private final class BottomActionsPanelComponent: Component { if itemComponenView.superview == nil { self.addSubview(itemComponenView) } + itemComponenView.isAccessibilityElement = true + itemComponenView.accessibilityLabel = item.title + itemComponenView.accessibilityTraits = item.isEnabled ? [.button] : [.button, .notEnabled] + for subview in itemComponenView.subviews { + subview.accessibilityElementsHidden = true + } itemComponenView.frame = itemFrame } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift index 40b4fd0bf53..2f4de550bc8 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift @@ -1094,6 +1094,27 @@ public protocol PeerInfoScreenNodeProtocol: AnyObject { func displaySharedMediaFastScrollingTooltip() } +private final class VisualMediaGridAccessibilityElement: UIAccessibilityElement { + var activate: () -> Bool = { + return false + } + var openContextMenu: () -> Bool = { + return false + } + + init(accessibilityContainer container: Any) { + super.init(accessibilityContainer: container) + } + + override func accessibilityActivate() -> Bool { + return self.activate() + } + + @objc func accessibilityOpenContextMenu(_ action: UIAccessibilityCustomAction) -> Bool { + return self.openContextMenu() + } +} + public final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScrollViewDelegate, ASGestureRecognizerDelegate { public enum ContentType { case photoOrVideo @@ -1134,6 +1155,7 @@ public final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, private let contextGestureContainerNode: ContextControllerSourceNode private let itemGrid: SparseItemGrid private let itemGridBinding: SparseItemGridBindingImpl + private var gridAccessibilityElements: [MessageId: VisualMediaGridAccessibilityElement] = [:] private let listBackgroundView: UIImageView private let listMaskView: UIImageView private let directMediaImageCache: DirectMediaImageCache @@ -1352,6 +1374,7 @@ public final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, strongSelf.paneDidScroll?() strongSelf.cancelPreviewGestures() + strongSelf.updateGridAccessibilityElements() } self.itemGridBinding.coveringInsetOffsetUpdatedImpl = { [weak self] transition in @@ -1894,6 +1917,83 @@ public final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, } return nil } + + private func updateGridAccessibilityElements() { + switch self.contentType { + case .files, .voiceAndVideoMessages, .music: + self.gridAccessibilityElements.removeAll() + self.itemGrid.view.accessibilityElements = nil + return + case .photo, .video, .photoOrVideo, .gifs: + break + } + + var accessibilityElements: [UIAccessibilityElement] = [] + var validMessageIds: Set = [] + self.itemGrid.forEachVisibleItem { [weak self] displayItem in + guard let self, let itemLayer = displayItem.layer as? GenericItemLayer, let item = itemLayer.item, !itemLayer.isHidden else { + return + } + validMessageIds.insert(item.message.id) + + let accessibilityElement: VisualMediaGridAccessibilityElement + if let current = self.gridAccessibilityElements[item.message.id] { + accessibilityElement = current + } else { + accessibilityElement = VisualMediaGridAccessibilityElement(accessibilityContainer: self.itemGrid.view) + self.gridAccessibilityElements[item.message.id] = accessibilityElement + } + accessibilityElement.activate = { [weak self, weak itemLayer] in + guard let self, let itemLayer else { + return false + } + self.itemGridBinding.onTap(item: item, itemLayer: itemLayer, point: CGPoint(x: itemLayer.bounds.midX, y: itemLayer.bounds.midY)) + return true + } + accessibilityElement.openContextMenu = { [weak self, weak itemLayer] in + guard let self, let itemLayer else { + return false + } + let rect = self.itemGrid.frameForItem(layer: itemLayer) + self.chatControllerInteraction.openMessageContextActions(item.message, self, rect, nil) + return true + } + + var isVideo = false + for media in item.message.effectiveMedia { + if let image = media as? TelegramMediaImage, image.video != nil { + isVideo = true + break + } else if let file = media as? TelegramMediaFile, file.isVideo || file.isAnimated { + isVideo = true + break + } + } + accessibilityElement.accessibilityLabel = isVideo ? self.presentationData.strings.VoiceOver_Chat_Video : self.presentationData.strings.VoiceOver_Chat_Photo + accessibilityElement.accessibilityValue = item.message.text.isEmpty ? nil : item.message.text + accessibilityElement.accessibilityTraits = [.button, .image] + accessibilityElement.accessibilityCustomActions = [ + UIAccessibilityCustomAction(name: self.presentationData.strings.VoiceOver_MessageContextOpenMessageMenu, target: accessibilityElement, selector: #selector(VisualMediaGridAccessibilityElement.accessibilityOpenContextMenu(_:))) + ] + if self.chatControllerInteraction.selectionState?.selectedIds.contains(item.message.id) == true { + accessibilityElement.accessibilityTraits.insert(.selected) + } + accessibilityElement.accessibilityFrameInContainerSpace = self.itemGrid.frameForItem(layer: itemLayer) + accessibilityElements.append(accessibilityElement) + } + accessibilityElements.sort { lhs, rhs in + let lhsFrame = lhs.accessibilityFrameInContainerSpace + let rhsFrame = rhs.accessibilityFrameInContainerSpace + if abs(lhsFrame.minY - rhsFrame.minY) > UIScreenPixel { + return lhsFrame.minY < rhsFrame.minY + } else { + return lhsFrame.minX < rhsFrame.minX + } + } + self.gridAccessibilityElements = self.gridAccessibilityElements.filter { validMessageIds.contains($0.key) } + self.itemGrid.view.isAccessibilityElement = false + self.itemGrid.view.accessibilityElements = accessibilityElements + } public func updateHiddenMedia() { self.itemGrid.forEachVisibleItem { item in @@ -1912,6 +2012,7 @@ public final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, itemLayer.isHidden = false } } + self.updateGridAccessibilityElements() } public func transferVelocity(_ velocity: CGFloat) { @@ -2173,6 +2274,7 @@ public final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, } itemLayer.updateSelection(theme: self.itemGridBinding.checkNodeTheme, isSelected: self.chatControllerInteraction.selectionState?.selectedIds.contains(item.message.id), animated: animated) } + self.updateGridAccessibilityElements() let isSelecting = self.chatControllerInteraction.selectionState != nil self.itemGrid.pinchEnabled = !isSelecting @@ -2285,6 +2387,9 @@ public final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, let listSideInset = isList ? sideInset + 16.0 : sideInset self.itemGrid.update(size: size, insets: UIEdgeInsets(top: topInset, left: listSideInset, bottom: bottomInset, right: listSideInset), useSideInsets: !isList, scrollIndicatorInsets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), lockScrollingAtTop: isScrollingLockedAtTop, fixedItemHeight: fixedItemHeight, fixedItemAspect: nil, items: items, theme: self.itemGridBinding.chatPresentationData.theme.theme, synchronous: wasFirstTime ? .full : .none) + DispatchQueue.main.async { [weak self] in + self?.updateGridAccessibilityElements() + } if let initialMessageIndexValue = self.initialMessageIndex, items.items.contains(where: { item in if let _ = item as? VisualMediaItem { return true diff --git a/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/Sources/ArchiveInfoContentComponent.swift b/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/Sources/ArchiveInfoContentComponent.swift index fb4c0c5aa38..fcfda7714b5 100644 --- a/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/Sources/ArchiveInfoContentComponent.swift +++ b/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/Sources/ArchiveInfoContentComponent.swift @@ -56,6 +56,7 @@ public final class ArchiveInfoContentComponent: Component { private let title = ComponentView() private let mainText = ComponentView() + private let settingsButton: UIButton private var chevronImage: UIImage? @@ -68,6 +69,7 @@ public final class ArchiveInfoContentComponent: Component { self.iconBackground = UIImageView() self.iconForeground = UIImageView() + self.settingsButton = UIButton(type: .custom) super.init(frame: frame) @@ -86,6 +88,15 @@ public final class ArchiveInfoContentComponent: Component { self.scrollView.addSubview(self.iconBackground) self.scrollView.addSubview(self.iconForeground) + self.scrollView.addSubview(self.settingsButton) + + self.iconBackground.isAccessibilityElement = false + self.iconBackground.accessibilityElementsHidden = true + self.iconForeground.isAccessibilityElement = false + self.iconForeground.accessibilityElementsHidden = true + + self.settingsButton.accessibilityTraits = [.button, .link] + self.settingsButton.addTarget(self, action: #selector(self.openSettingsPressed), for: .touchUpInside) } required init(coder: NSCoder) { @@ -105,6 +116,8 @@ public final class ArchiveInfoContentComponent: Component { let sideInset: CGFloat = 16.0 let sideIconInset: CGFloat = 40.0 + let titleFontSize = UIFontMetrics(forTextStyle: .headline).scaledValue(for: 19.0) + let bodyFontSize = UIFontMetrics(forTextStyle: .body).scaledValue(for: 15.0) var contentHeight: CGFloat = 0.0 @@ -128,7 +141,7 @@ public final class ArchiveInfoContentComponent: Component { contentHeight += 15.0 let titleString = NSMutableAttributedString() - titleString.append(NSAttributedString(string: component.strings.ArchiveInfo_Title, font: Font.semibold(19.0), textColor: component.theme.list.itemPrimaryTextColor)) + titleString.append(NSAttributedString(string: component.strings.ArchiveInfo_Title, font: Font.semibold(titleFontSize), textColor: component.theme.list.itemPrimaryTextColor)) let imageAttachment = NSTextAttachment() imageAttachment.image = self.iconBackground.image titleString.append(NSAttributedString(attachment: imageAttachment)) @@ -137,7 +150,8 @@ public final class ArchiveInfoContentComponent: Component { transition: .immediate, component: AnyComponent(MultilineTextComponent( text: .plain(titleString), - maximumNumberOfLines: 1 + horizontalAlignment: .center, + maximumNumberOfLines: 0 )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) @@ -146,6 +160,9 @@ public final class ArchiveInfoContentComponent: Component { if titleView.superview == nil { self.scrollView.addSubview(titleView) } + titleView.isAccessibilityElement = true + titleView.accessibilityLabel = component.strings.ArchiveInfo_Title + titleView.accessibilityTraits.insert(.header) transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: contentHeight), size: titleSize)) } contentHeight += titleSize.height @@ -161,15 +178,15 @@ public final class ArchiveInfoContentComponent: Component { let mainText = NSMutableAttributedString() mainText.append(parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes( body: MarkdownAttributeSet( - font: Font.regular(15.0), + font: Font.regular(bodyFontSize), textColor: component.theme.list.itemSecondaryTextColor ), bold: MarkdownAttributeSet( - font: Font.semibold(15.0), + font: Font.semibold(bodyFontSize), textColor: component.theme.list.itemSecondaryTextColor ), link: MarkdownAttributeSet( - font: Font.regular(15.0), + font: Font.regular(bodyFontSize), textColor: component.theme.list.itemAccentColor, additionalAttributes: [:] ), @@ -214,7 +231,12 @@ public final class ArchiveInfoContentComponent: Component { if mainTextView.superview == nil { self.scrollView.addSubview(mainTextView) } - transition.setFrame(view: mainTextView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - mainTextSize.width) * 0.5), y: contentHeight), size: mainTextSize)) + mainTextView.accessibilityElementsHidden = true + let mainTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - mainTextSize.width) * 0.5), y: contentHeight), size: mainTextSize) + transition.setFrame(view: mainTextView, frame: mainTextFrame) + self.settingsButton.accessibilityLabel = mainText.string + transition.setFrame(view: self.settingsButton, frame: mainTextFrame) + self.scrollView.bringSubviewToFront(self.settingsButton) } contentHeight += mainTextSize.height @@ -269,7 +291,7 @@ public final class ArchiveInfoContentComponent: Component { let titleSize = item.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: itemDesc.title, font: Font.semibold(15.0), textColor: component.theme.list.itemPrimaryTextColor)), + text: .plain(NSAttributedString(string: itemDesc.title, font: Font.semibold(bodyFontSize), textColor: component.theme.list.itemPrimaryTextColor)), maximumNumberOfLines: 0, lineSpacing: 0.2 )), @@ -279,7 +301,7 @@ public final class ArchiveInfoContentComponent: Component { let textSize = item.text.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: itemDesc.text, font: Font.regular(15.0), textColor: component.theme.list.itemSecondaryTextColor)), + text: .plain(NSAttributedString(string: itemDesc.text, font: Font.regular(bodyFontSize), textColor: component.theme.list.itemSecondaryTextColor)), maximumNumberOfLines: 0, lineSpacing: 0.18 )), @@ -291,6 +313,8 @@ public final class ArchiveInfoContentComponent: Component { if iconView.superview == nil { self.scrollView.addSubview(iconView) } + iconView.isAccessibilityElement = false + iconView.accessibilityElementsHidden = true transition.setFrame(view: iconView, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight + 4.0), size: iconSize)) } @@ -298,7 +322,10 @@ public final class ArchiveInfoContentComponent: Component { if titleView.superview == nil { self.scrollView.addSubview(titleView) } - transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: sideInset + sideIconInset, y: contentHeight), size: titleSize)) + titleView.isAccessibilityElement = true + titleView.accessibilityLabel = "\(itemDesc.title). \(itemDesc.text)" + let titleFrame = CGRect(origin: CGPoint(x: sideInset + sideIconInset, y: contentHeight), size: titleSize) + transition.setFrame(view: titleView, frame: titleFrame) } contentHeight += titleSize.height contentHeight += 2.0 @@ -307,7 +334,15 @@ public final class ArchiveInfoContentComponent: Component { if textView.superview == nil { self.scrollView.addSubview(textView) } - transition.setFrame(view: textView, frame: CGRect(origin: CGPoint(x: sideInset + sideIconInset, y: contentHeight), size: textSize)) + textView.accessibilityElementsHidden = true + let textFrame = CGRect(origin: CGPoint(x: sideInset + sideIconInset, y: contentHeight), size: textSize) + transition.setFrame(view: textView, frame: textFrame) + item.title.view?.accessibilityFrameInContainerSpace = CGRect( + x: sideInset, + y: textFrame.minY - titleSize.height - 2.0, + width: availableSize.width - sideInset * 2.0, + height: titleSize.height + 2.0 + textSize.height + ) } contentHeight += textSize.height } @@ -321,6 +356,10 @@ public final class ArchiveInfoContentComponent: Component { return size } + + @objc private func openSettingsPressed() { + self.component?.openSettings() + } } public func makeView() -> View { diff --git a/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/Sources/ArchiveInfoScreen.swift b/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/Sources/ArchiveInfoScreen.swift index 4c79c49686b..a8252f59bb9 100644 --- a/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/Sources/ArchiveInfoScreen.swift +++ b/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/Sources/ArchiveInfoScreen.swift @@ -36,6 +36,7 @@ private final class ArchiveInfoSheetContentComponent: Component { final class View: UIView { private let content = ComponentView() private let button = ComponentView() + private let accessibilityButton: UIButton fileprivate let playButtonAnimation = ActionSlot() private var didPlayAnimation = false @@ -43,7 +44,15 @@ private final class ArchiveInfoSheetContentComponent: Component { private var component: ArchiveInfoSheetContentComponent? override init(frame: CGRect) { + self.accessibilityButton = UIButton(type: .custom) + super.init(frame: frame) + + self.accessibilityViewIsModal = true + + self.accessibilityButton.accessibilityTraits = .button + self.accessibilityButton.addTarget(self, action: #selector(self.closePressed), for: .touchUpInside) + self.addSubview(self.accessibilityButton) } required init?(coder: NSCoder) { @@ -80,6 +89,9 @@ private final class ArchiveInfoSheetContentComponent: Component { contentHeight += contentSize.height contentHeight += 30.0 + let buttonFontSize = UIFontMetrics(forTextStyle: .headline).scaledValue(for: 17.0) + let buttonHeight = max(52.0, ceil(buttonFontSize + 35.0)) + var buttonTitle: [AnyComponentWithIdentity] = [] buttonTitle.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(LottieComponent( content: LottieComponent.AppBundleContent(name: "anim_ok"), @@ -92,11 +104,12 @@ private final class ArchiveInfoSheetContentComponent: Component { text: environment.strings.ArchiveInfo_CloseAction, badge: 0, textColor: environment.theme.list.itemCheckColors.foregroundColor, + fontSize: buttonFontSize, badgeBackground: environment.theme.list.itemCheckColors.foregroundColor, badgeForeground: environment.theme.list.itemCheckColors.fillColor )))) - let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) + let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: buttonHeight, sideInset: 30.0) let buttonSize = self.button.update( transition: transition, component: AnyComponent(ButtonComponent( @@ -119,7 +132,7 @@ private final class ArchiveInfoSheetContentComponent: Component { } )), environment: {}, - containerSize: CGSize(width: availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0) + containerSize: CGSize(width: availableSize.width - buttonInsets.left - buttonInsets.right, height: buttonHeight) ) let buttonFrame = CGRect(origin: CGPoint(x: buttonInsets.left, y: contentHeight), size: buttonSize) if let buttonView = self.button.view { @@ -130,11 +143,18 @@ private final class ArchiveInfoSheetContentComponent: Component { } transition.setFrame(view: buttonView, frame: buttonFrame) } + self.accessibilityButton.accessibilityLabel = environment.strings.ArchiveInfo_CloseAction + transition.setFrame(view: self.accessibilityButton, frame: buttonFrame) + self.bringSubviewToFront(self.accessibilityButton) contentHeight += buttonSize.height contentHeight += buttonInsets.bottom return CGSize(width: availableSize.width, height: contentHeight) } + + @objc private func closePressed() { + self.component?.dismiss() + } } func makeView() -> View { @@ -260,6 +280,7 @@ private final class ArchiveInfoScreenComponent: Component { if let sheetView = self.sheet.view { if sheetView.superview == nil { self.addSubview(sheetView) + sheetView.accessibilityViewIsModal = true } transition.setFrame(view: sheetView, frame: CGRect(origin: CGPoint(), size: availableSize)) } @@ -278,7 +299,12 @@ private final class ArchiveInfoScreenComponent: Component { } public class ArchiveInfoScreen: ViewControllerComponentContainer { + private let buttonAction: (() -> Void)? + private var isDismissingFromAccessibility = false + public init(context: AccountContext, settings: GlobalPrivacySettings, buttonAction: (() -> Void)? = nil) { + self.buttonAction = buttonAction + super.init(context: context, component: ArchiveInfoScreenComponent( context: context, settings: settings, @@ -305,5 +331,17 @@ public class ArchiveInfoScreen: ViewControllerComponentContainer { super.viewDidAppear(animated) self.view.disablesInteractiveModalDismiss = true + UIAccessibility.post(notification: .screenChanged, argument: self.view) + } + + override public func accessibilityPerformEscape() -> Bool { + if self.isDismissingFromAccessibility { + return false + } + self.isDismissingFromAccessibility = true + self.dismiss(completion: { [buttonAction = self.buttonAction] in + buttonAction?() + }) + return true } } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 23695c21df0..f40282c31c1 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -5820,7 +5820,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }, queue: .mainQueue()) self.richTextCheckboxDebounceTimers[messageId] = timer timer.start() - }, openStarsPurchase: { [weak self] amount in + }, accessibilityForwardMessage: { [weak self] message in + self?.interfaceInteraction?.forwardMessages([message]) + }, accessibilityDeleteMessage: { [weak self] message in + self?.interfaceInteraction?.deleteMessages([message], nil, { _ in }) + }, canPerformAccessibilityMessageActions: true, openStarsPurchase: { [weak self] amount in self?.interfaceInteraction?.openStarsPurchase(amount) }, openRankInfo: { [weak self] peer, role, rank in guard let self, let chatPeer = self.presentationInterfaceState.renderedPeer?.peer else { diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 83c19884c8d..a8f8b31aa37 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -811,8 +811,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } self.inputPanelBackgroundNode.isUserInteractionEnabled = false - self.navigateButtons = ChatHistoryNavigationButtons(theme: self.chatPresentationInterfaceState.theme, preferClearGlass: self.chatPresentationInterfaceState.preferredGlassType == .clear, dateTimeFormat: self.chatPresentationInterfaceState.dateTimeFormat, backgroundNode: self.backgroundNode, isChatRotated: historyNodeRotated) - self.navigateButtons.accessibilityElementsHidden = true + self.navigateButtons = ChatHistoryNavigationButtons(theme: self.chatPresentationInterfaceState.theme, strings: self.chatPresentationInterfaceState.strings, preferClearGlass: self.chatPresentationInterfaceState.preferredGlassType == .clear, dateTimeFormat: self.chatPresentationInterfaceState.dateTimeFormat, backgroundNode: self.backgroundNode, isChatRotated: historyNodeRotated) super.init() diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index b547dd3b6f7..5ac5770692a 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -3873,6 +3873,16 @@ public final class ChatHistoryListNodeImpl: ASDisplayNode, ChatHistoryNode, Chat } self.hasActiveTransition = true let transition = self.enqueuedHistoryViewTransitions.removeFirst() + + let accessibilityNavigationTargetMessageId = UIAccessibility.isVoiceOverRunning ? self.controllerInteraction.accessibilityNavigationTargetMessageId : nil + var accessibilityFocusedMessageId: MessageId? + if UIAccessibility.isVoiceOverRunning { + self.forEachVisibleMessageItemNode { itemNode in + if accessibilityFocusedMessageId == nil, itemNode.accessibilityContainsFocus(), let item = itemNode.item { + accessibilityFocusedMessageId = item.content.first?.0.id + } + } + } var expiredMessageStableIds = Set() if let previousHistoryView = self.historyView, transition.options.contains(.AnimateInsertion) { @@ -4429,6 +4439,32 @@ public final class ChatHistoryListNodeImpl: ASDisplayNode, ChatHistoryNode, Chat } strongSelf.hasActiveTransition = false + + if let accessibilityNavigationTargetMessageId { + var didRestoreAccessibilityNavigationTarget = false + strongSelf.forEachVisibleMessageItemNode { itemNode in + if let item = itemNode.item, item.content.contains(where: { $0.0.id == accessibilityNavigationTargetMessageId }) { + didRestoreAccessibilityNavigationTarget = true + if strongSelf.controllerInteraction.accessibilityNavigationTargetMessageId == accessibilityNavigationTargetMessageId { + strongSelf.controllerInteraction.accessibilityNavigationTargetMessageId = nil + } + itemNode.restoreAccessibilityFocus() + } + } + if !didRestoreAccessibilityNavigationTarget, let accessibilityFocusedMessageId { + strongSelf.forEachVisibleMessageItemNode { itemNode in + if let item = itemNode.item, item.content.contains(where: { $0.0.id == accessibilityFocusedMessageId }) { + itemNode.restoreAccessibilityFocus() + } + } + } + } else if let accessibilityFocusedMessageId { + strongSelf.forEachVisibleMessageItemNode { itemNode in + if let item = itemNode.item, item.content.contains(where: { $0.0.id == accessibilityFocusedMessageId }) { + itemNode.restoreAccessibilityFocus() + } + } + } if let previousCloneView { previousCloneView.transform = strongSelf.view.transform diff --git a/submodules/TelegramUI/Sources/ChatHistoryNavigationButtonNode.swift b/submodules/TelegramUI/Sources/ChatHistoryNavigationButtonNode.swift index 53e1f936985..158651f5d5e 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryNavigationButtonNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryNavigationButtonNode.swift @@ -42,6 +42,7 @@ class ChatHistoryNavigationButtonNode: ContextControllerSourceNode { var badge: String = "" { didSet { if self.badge != oldValue { + self.accessibilityValue = self.badge.isEmpty ? nil : self.badge self.layoutBadge() } } @@ -51,7 +52,7 @@ class ChatHistoryNavigationButtonNode: ContextControllerSourceNode { private var preferClearGlass: Bool private let type: ChatHistoryNavigationButtonType - init(theme: PresentationTheme, preferClearGlass: Bool, backgroundNode: WallpaperBackgroundNode, type: ChatHistoryNavigationButtonType) { + init(theme: PresentationTheme, strings: PresentationStrings, preferClearGlass: Bool, backgroundNode: WallpaperBackgroundNode, type: ChatHistoryNavigationButtonType) { self.theme = theme self.preferClearGlass = preferClearGlass self.type = type @@ -84,6 +85,21 @@ class ChatHistoryNavigationButtonNode: ContextControllerSourceNode { self.badgeTextNode.reverseAnimationDirection = true super.init() + + self.isAccessibilityElement = true + self.accessibilityTraits = .button + switch type { + case .down: + self.accessibilityLabel = strings.KeyCommand_ScrollDown + case .up: + self.accessibilityLabel = strings.KeyCommand_ScrollUp + case .mentions: + self.accessibilityLabel = strings.Conversation_ContextMenuMention + case .reactions: + self.accessibilityLabel = strings.Conversation_ReadAllReactions + case .pollVotes: + self.accessibilityLabel = strings.Conversation_ReadAllPollVotes + } let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.onTapGesture(_:))) self.tapRecognizer = tapRecognizer @@ -159,6 +175,14 @@ class ChatHistoryNavigationButtonNode: ContextControllerSourceNode { } } } + + override func accessibilityActivate() -> Bool { + guard self.isEnabled, let tapped = self.tapped else { + return false + } + tapped() + return true + } private var currentValue: Int = 0 private func layoutBadge() { diff --git a/submodules/TelegramUI/Sources/ChatHistoryNavigationButtons.swift b/submodules/TelegramUI/Sources/ChatHistoryNavigationButtons.swift index de472c88f3e..62b9dd5fd4b 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryNavigationButtons.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryNavigationButtons.swift @@ -112,29 +112,29 @@ final class ChatHistoryNavigationButtons: ASDisplayNode { } } - init(theme: PresentationTheme, preferClearGlass: Bool, dateTimeFormat: PresentationDateTimeFormat, backgroundNode: WallpaperBackgroundNode, isChatRotated: Bool) { + init(theme: PresentationTheme, strings: PresentationStrings, preferClearGlass: Bool, dateTimeFormat: PresentationDateTimeFormat, backgroundNode: WallpaperBackgroundNode, isChatRotated: Bool) { self.isChatRotated = isChatRotated self.theme = theme self.preferClearGlass = preferClearGlass self.dateTimeFormat = dateTimeFormat - self.mentionsButton = ChatHistoryNavigationButtonNode(theme: theme, preferClearGlass: preferClearGlass, backgroundNode: backgroundNode, type: .mentions) + self.mentionsButton = ChatHistoryNavigationButtonNode(theme: theme, strings: strings, preferClearGlass: preferClearGlass, backgroundNode: backgroundNode, type: .mentions) self.mentionsButton.alpha = 0.0 self.mentionsButton.isHidden = true - self.reactionsButton = ChatHistoryNavigationButtonNode(theme: theme, preferClearGlass: preferClearGlass, backgroundNode: backgroundNode, type: .reactions) + self.reactionsButton = ChatHistoryNavigationButtonNode(theme: theme, strings: strings, preferClearGlass: preferClearGlass, backgroundNode: backgroundNode, type: .reactions) self.reactionsButton.alpha = 0.0 self.reactionsButton.isHidden = true - self.pollVotesButton = ChatHistoryNavigationButtonNode(theme: theme, preferClearGlass: preferClearGlass, backgroundNode: backgroundNode, type: .pollVotes) + self.pollVotesButton = ChatHistoryNavigationButtonNode(theme: theme, strings: strings, preferClearGlass: preferClearGlass, backgroundNode: backgroundNode, type: .pollVotes) self.pollVotesButton.alpha = 0.0 self.pollVotesButton.isHidden = true - self.downButton = ChatHistoryNavigationButtonNode(theme: theme, preferClearGlass: preferClearGlass, backgroundNode: backgroundNode, type: isChatRotated ? .down : .up) + self.downButton = ChatHistoryNavigationButtonNode(theme: theme, strings: strings, preferClearGlass: preferClearGlass, backgroundNode: backgroundNode, type: isChatRotated ? .down : .up) self.downButton.alpha = 0.0 self.downButton.isHidden = true - self.upButton = ChatHistoryNavigationButtonNode(theme: theme, preferClearGlass: preferClearGlass, backgroundNode: backgroundNode, type: isChatRotated ? .up : .down) + self.upButton = ChatHistoryNavigationButtonNode(theme: theme, strings: strings, preferClearGlass: preferClearGlass, backgroundNode: backgroundNode, type: isChatRotated ? .up : .down) self.upButton.alpha = 0.0 self.upButton.isHidden = true diff --git a/submodules/TelegramUI/Sources/ChatMessageActionUrlAuthController.swift b/submodules/TelegramUI/Sources/ChatMessageActionUrlAuthController.swift index 1b29e65e763..05431e45d48 100644 --- a/submodules/TelegramUI/Sources/ChatMessageActionUrlAuthController.swift +++ b/submodules/TelegramUI/Sources/ChatMessageActionUrlAuthController.swift @@ -11,14 +11,22 @@ import TextFormat import AccountContext import Markdown -private let textFont = Font.regular(13.0) -private let boldTextFont = Font.semibold(13.0) - private func formattedText(_ text: String, color: UIColor, textAlignment: NSTextAlignment = .natural) -> NSAttributedString { + let textFont = UIFontMetrics(forTextStyle: .footnote).scaledFont(for: Font.regular(13.0)) + let boldTextFont = UIFontMetrics(forTextStyle: .footnote).scaledFont(for: Font.semibold(13.0)) return parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: color), bold: MarkdownAttributeSet(font: boldTextFont, textColor: color), link: MarkdownAttributeSet(font: textFont, textColor: color), linkAttribute: { _ in return nil}), textAlignment: textAlignment) } +private final class ChatMessageActionUrlAuthOptionNode: ASTextNode { + var activate: (() -> Bool)? + + override func accessibilityActivate() -> Bool { + return self.activate?() ?? false + } +} + private final class ChatMessageActionUrlAuthAlertContentNode: AlertContentNode { + private var theme: AlertControllerTheme private let strings: PresentationStrings private let nameDisplayOrder: PresentationPersonNameOrder private let defaultUrl: String @@ -29,9 +37,9 @@ private final class ChatMessageActionUrlAuthAlertContentNode: AlertContentNode { private let titleNode: ASTextNode private let textNode: ASTextNode private let authorizeCheckNode: InteractiveCheckNode - private let authorizeLabelNode: ASTextNode + private let authorizeLabelNode: ChatMessageActionUrlAuthOptionNode private let allowWriteCheckNode: InteractiveCheckNode - private let allowWriteLabelNode: ASTextNode + private let allowWriteLabelNode: ChatMessageActionUrlAuthOptionNode private let actionNodesSeparator: ASDisplayNode private let actionNodes: [TextAlertContentActionNode] @@ -42,6 +50,10 @@ private final class ChatMessageActionUrlAuthAlertContentNode: AlertContentNode { override var dismissOnOutsideTap: Bool { return self.isUserInteractionEnabled } + + override var accessibilityInitialFocusNode: ASDisplayNode? { + return self.titleNode + } var authorize: Bool = true { didSet { @@ -52,16 +64,19 @@ private final class ChatMessageActionUrlAuthAlertContentNode: AlertContentNode { if !self.authorize && self.allowWriteAccess { self.allowWriteAccess = false } + self.updateOptionAccessibility() } } var allowWriteAccess: Bool = true { didSet { self.allowWriteCheckNode.setSelected(self.allowWriteAccess, animated: true) + self.updateOptionAccessibility() } } init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, defaultUrl: String, domain: String, bot: EnginePeer, requestWriteAccess: Bool, displayName: String, actions: [TextAlertAction]) { + self.theme = theme self.strings = strings self.nameDisplayOrder = nameDisplayOrder self.defaultUrl = defaultUrl @@ -70,22 +85,26 @@ private final class ChatMessageActionUrlAuthAlertContentNode: AlertContentNode { self.displayName = displayName self.titleNode = ASTextNode() - self.titleNode.maximumNumberOfLines = 2 + self.titleNode.maximumNumberOfLines = 0 + self.titleNode.isAccessibilityElement = true + self.titleNode.accessibilityTraits = [.header] self.textNode = ASTextNode() self.textNode.maximumNumberOfLines = 0 self.authorizeCheckNode = InteractiveCheckNode(theme: CheckNodeTheme(backgroundColor: theme.accentColor, strokeColor: theme.contrastColor, borderColor: theme.controlBorderColor, overlayBorder: false, hasInset: false, hasShadow: false)) self.authorizeCheckNode.setSelected(true, animated: false) - self.authorizeLabelNode = ASTextNode() - self.authorizeLabelNode.maximumNumberOfLines = 4 + self.authorizeLabelNode = ChatMessageActionUrlAuthOptionNode() + self.authorizeLabelNode.maximumNumberOfLines = 0 self.authorizeLabelNode.isUserInteractionEnabled = true + self.authorizeLabelNode.isAccessibilityElement = true self.allowWriteCheckNode = InteractiveCheckNode(theme: CheckNodeTheme(backgroundColor: theme.accentColor, strokeColor: theme.contrastColor, borderColor: theme.controlBorderColor, overlayBorder: false, hasInset: false, hasShadow: false)) self.allowWriteCheckNode.setSelected(true, animated: false) - self.allowWriteLabelNode = ASTextNode() - self.allowWriteLabelNode.maximumNumberOfLines = 4 + self.allowWriteLabelNode = ChatMessageActionUrlAuthOptionNode() + self.allowWriteLabelNode.maximumNumberOfLines = 0 self.allowWriteLabelNode.isUserInteractionEnabled = true + self.allowWriteLabelNode.isAccessibilityElement = true self.actionNodesSeparator = ASDisplayNode() self.actionNodesSeparator.isLayerBacked = true @@ -105,6 +124,9 @@ private final class ChatMessageActionUrlAuthAlertContentNode: AlertContentNode { self.actionVerticalSeparators = actionVerticalSeparators super.init() + + self.authorizeCheckNode.isAccessibilityElement = false + self.allowWriteCheckNode.isAccessibilityElement = false self.addSubnode(self.titleNode) self.addSubnode(self.textNode) @@ -136,6 +158,20 @@ private final class ChatMessageActionUrlAuthAlertContentNode: AlertContentNode { strongSelf.allowWriteAccess = !strongSelf.allowWriteAccess } } + self.authorizeLabelNode.activate = { [weak self] in + guard let self else { + return false + } + self.authorize = !self.authorize + return true + } + self.allowWriteLabelNode.activate = { [weak self] in + guard let self, self.authorize else { + return false + } + self.allowWriteAccess = !self.allowWriteAccess + return true + } self.updateTheme(theme) } @@ -158,11 +194,19 @@ private final class ChatMessageActionUrlAuthAlertContentNode: AlertContentNode { } override func updateTheme(_ theme: AlertControllerTheme) { - self.titleNode.attributedText = NSAttributedString(string: strings.Conversation_OpenBotLinkTitle, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) + self.theme = theme + let titleFont = UIFontMetrics(forTextStyle: .headline).scaledFont(for: Font.bold(17.0)) + self.titleNode.attributedText = NSAttributedString(string: strings.Conversation_OpenBotLinkTitle, font: titleFont, textColor: theme.primaryColor, paragraphAlignment: .center) self.textNode.attributedText = formattedText(strings.Conversation_OpenBotLinkText(self.defaultUrl).string, color: theme.primaryColor, textAlignment: .center) self.authorizeLabelNode.attributedText = formattedText(strings.Conversation_OpenBotLinkLogin(self.domain, self.displayName).string, color: theme.primaryColor) self.allowWriteLabelNode.attributedText = formattedText(strings.Conversation_OpenBotLinkAllowMessages(self.bot.displayTitle(strings: self.strings, displayOrder: self.nameDisplayOrder)).string, color: theme.primaryColor) + self.titleNode.accessibilityLabel = self.titleNode.attributedText?.string + self.textNode.isAccessibilityElement = true + self.textNode.accessibilityLabel = self.textNode.attributedText?.string + self.authorizeLabelNode.accessibilityLabel = self.authorizeLabelNode.attributedText?.string + self.allowWriteLabelNode.accessibilityLabel = self.allowWriteLabelNode.attributedText?.string + self.updateOptionAccessibility() self.actionNodesSeparator.backgroundColor = theme.separatorColor for actionNode in self.actionNodes { @@ -176,6 +220,26 @@ private final class ChatMessageActionUrlAuthAlertContentNode: AlertContentNode { _ = self.updateLayout(size: size, transition: .immediate) } } + + private func updateOptionAccessibility() { + self.authorizeLabelNode.accessibilityTraits = self.authorize ? [.button, .selected] : [.button] + var allowWriteTraits: UIAccessibilityTraits = [.button] + if self.allowWriteAccess { + allowWriteTraits.insert(.selected) + } + if !self.authorize { + allowWriteTraits.insert(.notEnabled) + } + self.allowWriteLabelNode.accessibilityTraits = allowWriteTraits + } + + override func contentSizeCategoryUpdated() { + self.updateTheme(self.theme) + for actionNode in self.actionNodes { + actionNode.updateTheme(self.theme) + } + self.requestLayout?(.immediate) + } override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { var size = size @@ -200,8 +264,11 @@ private final class ChatMessageActionUrlAuthAlertContentNode: AlertContentNode { var entriesHeight: CGFloat = 0.0 let authorizeSize = self.authorizeLabelNode.measure(condensedSize) - transition.updateFrame(node: self.authorizeLabelNode, frame: CGRect(origin: CGPoint(x: 46.0, y: origin.y), size: authorizeSize)) - transition.updateFrame(node: self.authorizeCheckNode, frame: CGRect(origin: CGPoint(x: 12.0, y: origin.y - 2.0), size: checkSize)) + let authorizeLabelFrame = CGRect(origin: CGPoint(x: 46.0, y: origin.y), size: authorizeSize) + let authorizeCheckFrame = CGRect(origin: CGPoint(x: 12.0, y: origin.y - 2.0), size: checkSize) + transition.updateFrame(node: self.authorizeLabelNode, frame: authorizeLabelFrame) + transition.updateFrame(node: self.authorizeCheckNode, frame: authorizeCheckFrame) + self.authorizeLabelNode.view.accessibilityFrameInContainerSpace = authorizeLabelFrame.union(authorizeCheckFrame) origin.y += authorizeSize.height entriesHeight += authorizeSize.height @@ -210,21 +277,27 @@ private final class ChatMessageActionUrlAuthAlertContentNode: AlertContentNode { entriesHeight += 16.0 let allowWriteSize = self.allowWriteLabelNode.measure(condensedSize) - transition.updateFrame(node: self.allowWriteLabelNode, frame: CGRect(origin: CGPoint(x: 46.0, y: origin.y), size: allowWriteSize)) - transition.updateFrame(node: self.allowWriteCheckNode, frame: CGRect(origin: CGPoint(x: 12.0, y: origin.y - 2.0), size: checkSize)) + let allowWriteLabelFrame = CGRect(origin: CGPoint(x: 46.0, y: origin.y), size: allowWriteSize) + let allowWriteCheckFrame = CGRect(origin: CGPoint(x: 12.0, y: origin.y - 2.0), size: checkSize) + transition.updateFrame(node: self.allowWriteLabelNode, frame: allowWriteLabelFrame) + transition.updateFrame(node: self.allowWriteCheckNode, frame: allowWriteCheckFrame) + self.allowWriteLabelNode.view.accessibilityFrameInContainerSpace = allowWriteLabelFrame.union(allowWriteCheckFrame) origin.y += allowWriteSize.height entriesHeight += allowWriteSize.height } - let actionButtonHeight: CGFloat = 44.0 + let minimumActionButtonHeight: CGFloat = 44.0 var minActionsWidth: CGFloat = 0.0 - let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) + let maxActionWidth: CGFloat = self.actionNodes.isEmpty ? size.width : floor(size.width / CGFloat(self.actionNodes.count)) let actionTitleInsets: CGFloat = 8.0 - var effectiveActionLayout = TextAlertContentActionLayout.horizontal + var effectiveActionLayout: TextAlertContentActionLayout = self.traitCollection.preferredContentSizeCategory.isAccessibilityCategory ? .vertical : .horizontal + var actionHeights: [CGFloat] = [] for actionNode in self.actionNodes { - let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight)) - if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 { + let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: max(1.0, maxActionWidth - 16.0), height: CGFloat.greatestFiniteMagnitude)) + let actionHeight = max(minimumActionButtonHeight, actionTitleSize.height + 20.0) + actionHeights.append(actionHeight) + if case .horizontal = effectiveActionLayout, actionHeight > minimumActionButtonHeight { effectiveActionLayout = .vertical } switch effectiveActionLayout { @@ -243,9 +316,9 @@ private final class ChatMessageActionUrlAuthAlertContentNode: AlertContentNode { var actionsHeight: CGFloat = 0.0 switch effectiveActionLayout { case .horizontal: - actionsHeight = actionButtonHeight + actionsHeight = actionHeights.max() ?? minimumActionButtonHeight case .vertical: - actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) + actionsHeight = actionHeights.reduce(0.0, +) } let resultWidth = contentWidth + insets.left + insets.right @@ -254,7 +327,7 @@ private final class ChatMessageActionUrlAuthAlertContentNode: AlertContentNode { transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) var actionOffset: CGFloat = 0.0 - let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count)) + let actionWidth: CGFloat = self.actionNodes.isEmpty ? resultSize.width : floor(resultSize.width / CGFloat(self.actionNodes.count)) var separatorIndex = -1 var nodeIndex = 0 for actionNode in self.actionNodes { @@ -284,11 +357,12 @@ private final class ChatMessageActionUrlAuthAlertContentNode: AlertContentNode { let actionNodeFrame: CGRect switch effectiveActionLayout { case .horizontal: - actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionsHeight)) actionOffset += currentActionWidth case .vertical: - actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) - actionOffset += actionButtonHeight + let actionHeight = actionHeights[nodeIndex] + actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionHeight)) + actionOffset += actionHeight } transition.updateFrame(node: actionNode, frame: actionNodeFrame) diff --git a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift index b29ac66d01c..2d23d0a97ba 100644 --- a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift @@ -200,11 +200,15 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, ASScrollViewDe self.listNode = ListViewImpl() self.listNode.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor - self.listNode.accessibilityPageScrolledString = { row, count in - return presentationData.strings.VoiceOver_ScrollStatus(row, count).string - } super.init() + + self.listNode.accessibilityPageScrolledString = { [weak self] row, count in + guard let self else { + return "" + } + return self.presentationData.strings.VoiceOver_ScrollStatus(row, count).string + } self.backgroundColor = self.presentationData.theme.chatList.backgroundColor self.isOpaque = false diff --git a/submodules/TelegramUI/Sources/ChatSearchResultsController.swift b/submodules/TelegramUI/Sources/ChatSearchResultsController.swift index 82bb22f0c1c..a30815f552f 100644 --- a/submodules/TelegramUI/Sources/ChatSearchResultsController.swift +++ b/submodules/TelegramUI/Sources/ChatSearchResultsController.swift @@ -43,6 +43,7 @@ final class ChatSearchResultsController: ViewController { if let strongSelf = self { strongSelf.presentationData = presentationData strongSelf.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationTheme: presentationData.theme, presentationStrings: presentationData.strings), transition: .immediate) + strongSelf.navigationItem.rightBarButtonItem?.accessibilityLabel = presentationData.strings.Common_Done strongSelf.controllerNode.updatePresentationData(presentationData) } }) @@ -51,7 +52,9 @@ final class ChatSearchResultsController: ViewController { self.title = searchQuery self.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: UIView()) - self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(donePressed)) + let doneButtonItem = UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(donePressed)) + doneButtonItem.accessibilityLabel = self.presentationData.strings.Common_Done + self.navigationItem.rightBarButtonItem = doneButtonItem } required init(coder aDecoder: NSCoder) { diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift index 908f39f5e6d..0b6ad80c33b 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift @@ -299,16 +299,22 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection self.titleView.title = CounterControllerTitle(title: self.params.title ?? self.presentationData.strings.PrivacyLastSeenSettings_EmpryUsersPlaceholder, counter: "") if self.rightNavigationButton == nil { let rightNavigationButton = UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.rightNavigationButtonPressed)) + rightNavigationButton.accessibilityLabel = self.presentationData.strings.Common_Done self.rightNavigationButton = rightNavigationButton - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "___close", style: .plain, target: self, action: #selector(cancelPressed)) + let closeButtonItem = UIBarButtonItem(title: "___close", style: .plain, target: self, action: #selector(cancelPressed)) + closeButtonItem.accessibilityLabel = self.presentationData.strings.Common_Close + self.navigationItem.leftBarButtonItem = closeButtonItem self.navigationItem.rightBarButtonItem = self.rightNavigationButton } case let .chatSelection(chatSelection): self.titleView.title = CounterControllerTitle(title: self.params.title ?? chatSelection.title, counter: "") if self.rightNavigationButton == nil { let rightNavigationButton = UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.rightNavigationButtonPressed)) + rightNavigationButton.accessibilityLabel = self.presentationData.strings.Common_Done self.rightNavigationButton = rightNavigationButton - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "___close", style: .plain, target: self, action: #selector(cancelPressed)) + let closeButtonItem = UIBarButtonItem(title: "___close", style: .plain, target: self, action: #selector(cancelPressed)) + closeButtonItem.accessibilityLabel = self.presentationData.strings.Common_Close + self.navigationItem.leftBarButtonItem = closeButtonItem self.navigationItem.rightBarButtonItem = self.rightNavigationButton } } diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift index 881bfd5b67d..451938718af 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift @@ -449,6 +449,11 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { func updatePresentationData(_ presentationData: PresentationData) { self.presentationData = presentationData self.backgroundColor = presentationData.theme.chatList.backgroundColor + if case let .chats(chatListNode) = self.contentNode { + chatListNode.accessibilityPageScrolledString = { row, count in + return presentationData.strings.VoiceOver_ScrollStatus(row, count).string + } + } } func scrollToTop() {