From be6bbbc6ae6cda29b9dd0eceb83ecefe82323658 Mon Sep 17 00:00:00 2001 From: Peter <> Date: Tue, 19 Mar 2019 18:54:41 +0400 Subject: [PATCH] Accessibility updates --- Display/ContextMenuAction.swift | 2 +- Display/ContextMenuActionNode.swift | 4 +- Display/TextNode.swift | 147 ++++++++++++++++++++++++++++ 3 files changed, 150 insertions(+), 3 deletions(-) diff --git a/Display/ContextMenuAction.swift b/Display/ContextMenuAction.swift index 1aa73ccc42..235dda12ab 100644 --- a/Display/ContextMenuAction.swift +++ b/Display/ContextMenuAction.swift @@ -1,6 +1,6 @@ public enum ContextMenuActionContent { - case text(String) + case text(title: String, accessibilityLabel: String) case icon(UIImage) } diff --git a/Display/ContextMenuActionNode.swift b/Display/ContextMenuActionNode.swift index d614f7ed8a..7771bd24dd 100644 --- a/Display/ContextMenuActionNode.swift +++ b/Display/ContextMenuActionNode.swift @@ -26,8 +26,8 @@ final class ContextMenuActionNode: ASDisplayNode { self.actionArea.accessibilityTraits = UIAccessibilityTraitButton switch action.content { - case let .text(title): - self.actionArea.accessibilityLabel = title + case let .text(title, accessibilityLabel): + self.actionArea.accessibilityLabel = accessibilityLabel let textNode = ImmediateTextNode() textNode.isUserInteractionEnabled = false diff --git a/Display/TextNode.swift b/Display/TextNode.swift index 2d53782392..191b53391b 100644 --- a/Display/TextNode.swift +++ b/Display/TextNode.swift @@ -275,6 +275,49 @@ public final class TextNodeLayout: NSObject { return nil } + public func allAttributeRects(name: String) -> [(Any, CGRect)] { + guard let attributedString = self.attributedString else { + return [] + } + var result: [(Any, CGRect)] = [] + attributedString.enumerateAttribute(NSAttributedStringKey(rawValue: name), in: NSRange(location: 0, length: attributedString.length), options: []) { (value, range, _) in + if let value = value, range.length != 0 { + var coveringRect = CGRect() + for line in self.lines { + let lineRange = NSIntersectionRange(range, line.range) + if lineRange.length != 0 { + var leftOffset: CGFloat = 0.0 + if lineRange.location != line.range.location { + leftOffset = floor(CTLineGetOffsetForStringIndex(line.line, lineRange.location, nil)) + } + var rightOffset: CGFloat = line.frame.width + if lineRange.location + lineRange.length != line.range.length { + var secondaryOffset: CGFloat = 0.0 + let rawOffset = CTLineGetOffsetForStringIndex(line.line, lineRange.location + lineRange.length, &secondaryOffset) + rightOffset = ceil(rawOffset) + if !rawOffset.isEqual(to: secondaryOffset) { + rightOffset = ceil(secondaryOffset) + } + } + var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + self.firstLineOffset), size: line.frame.size) + lineFrame = displayLineFrame(frame: lineFrame, isRTL: line.isRTL, boundingRect: CGRect(origin: CGPoint(), size: self.size), cutout: self.cutout) + + let rect = CGRect(origin: CGPoint(x: lineFrame.minX + leftOffset + self.insets.left, y: lineFrame.minY + self.insets.top), size: CGSize(width: rightOffset - leftOffset, height: lineFrame.size.height)) + if coveringRect.isEmpty { + coveringRect = rect + } else { + coveringRect = coveringRect.union(rect) + } + } + } + if !coveringRect.isEmpty { + result.append((value, coveringRect)) + } + } + } + return result + } + public func lineAndAttributeRects(name: String, at index: Int) -> [(CGRect, CGRect)]? { if let attributedString = self.attributedString { var range = NSRange() @@ -313,6 +356,110 @@ public final class TextNodeLayout: NSObject { } } +private final class TextAccessibilityOverlayElement: UIAccessibilityElement { + private let url: String + private let openUrl: (String) -> Void + + init(accessibilityContainer: Any, url: String, openUrl: @escaping (String) -> Void) { + self.url = url + self.openUrl = openUrl + + super.init(accessibilityContainer: accessibilityContainer) + } + + override func accessibilityActivate() -> Bool { + self.openUrl(self.url) + return true + } +} + +private final class TextAccessibilityOverlayNodeView: UIView { + fileprivate var cachedLayout: TextNodeLayout? { + didSet { + self.currentAccessibilityNodes?.forEach({ $0.removeFromSupernode() }) + self.currentAccessibilityNodes = nil + } + } + fileprivate let openUrl: (String) -> Void + + private var currentAccessibilityNodes: [AccessibilityAreaNode]? + + override var accessibilityElements: [Any]? { + get { + if let _ = self.currentAccessibilityNodes { + return nil + } + guard let cachedLayout = self.cachedLayout else { + return nil + } + let urlAttributesAndRects = cachedLayout.allAttributeRects(name: "UrlAttributeT") + + var urlElements: [AccessibilityAreaNode] = [] + for (value, rect) in urlAttributesAndRects { + let element = AccessibilityAreaNode() + element.accessibilityLabel = value as? String ?? "" + element.frame = rect + element.accessibilityTraits = UIAccessibilityTraitLink + element.activate = { [weak self] in + self?.openUrl(value as? String ?? "") + return true + } + self.addSubnode(element) + urlElements.append(element) + } + self.currentAccessibilityNodes = urlElements + return nil + } set(value) { + } + } + + init(openUrl: @escaping (String) -> Void) { + self.openUrl = openUrl + + super.init(frame: CGRect()) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +public final class TextAccessibilityOverlayNode: ASDisplayNode { + public var cachedLayout: TextNodeLayout? { + didSet { + if self.isNodeLoaded { + (self.view as? TextAccessibilityOverlayNodeView)?.cachedLayout = self.cachedLayout + } + } + } + + public var openUrl: ((String) -> Void)? + + override public init() { + super.init() + + let openUrl: (String) -> Void = { [weak self] url in + self?.openUrl?(url) + } + + self.isAccessibilityElement = false + + self.setViewBlock({ + return TextAccessibilityOverlayNodeView(openUrl: openUrl) + }) + } + + override public func didLoad() { + super.didLoad() + + (self.view as? TextAccessibilityOverlayNodeView)?.cachedLayout = self.cachedLayout + } + + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return nil + } +} + public class TextNode: ASDisplayNode { public private(set) var cachedLayout: TextNodeLayout?