From 304416afdb639ed32959f42d4371a9d3ebc4a917 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Sat, 26 Aug 2023 10:45:15 -0500 Subject: [PATCH] update label for accessibility elements Signed-off-by: Matt Bruce --- VDS/Components/Label/Label.swift | 76 +++++++++++++++++++++++++++----- 1 file changed, 66 insertions(+), 10 deletions(-) diff --git a/VDS/Components/Label/Label.swift b/VDS/Components/Label/Label.swift index 45777f5b..dc69bfea 100644 --- a/VDS/Components/Label/Label.swift +++ b/VDS/Components/Label/Label.swift @@ -95,7 +95,7 @@ open class Label: UILabel, ViewProtocol, UserInfoable { .sink { [weak self] notification in self?.setNeedsUpdate() }.store(in: &subscribers) - + backgroundColor = .clear numberOfLines = 0 lineBreakMode = .byWordWrapping @@ -106,7 +106,7 @@ open class Label: UILabel, ViewProtocol, UserInfoable { setNeedsUpdate() } } - + open func setup() {} /// Resets to default settings. @@ -156,6 +156,9 @@ open class Label: UILabel, ViewProtocol, UserInfoable { //force a drawText setNeedsDisplay() + + setNeedsLayout() + layoutIfNeeded() } } } @@ -174,7 +177,25 @@ open class Label: UILabel, ViewProtocol, UserInfoable { //add attribute on the string attribute.setAttribute(on: mutableAttributedString) + } + } + } + open override func layoutSubviews() { + super.layoutSubviews() + applyActions() + } + + private func applyActions() { + actions = [] + guard let attributedText else { return } + + let mutableAttributedString = NSMutableAttributedString(attributedString: attributedText) + + if let attributes = attributes { + //loop through the models attributes + for attribute in attributes { + //see if the attribute is Actionable if let actionable = attribute as? any ActionLabelAttributeModel{ //create a accessibleAction @@ -184,9 +205,20 @@ open class Label: UILabel, ViewProtocol, UserInfoable { actions.append(LabelAction(range: actionable.range, action: actionable.action, accessibilityID: customAccessibilityAction?.hashValue ?? -1)) } } + + if let accessibilityElements, !accessibilityElements.isEmpty { + let staticText = UIAccessibilityElement(accessibilityContainer: self) + staticText.accessibilityLabel = text + staticText.accessibilityTraits = .staticText + staticText.accessibilityFrameInContainerSpace = bounds + + isAccessibilityElement = false + self.accessibilityElements = accessibilityElements.compactMap{$0 as? UIAccessibilityElement}.filter { $0.accessibilityLabel != text } + self.accessibilityElements?.insert(staticText, at: 0) + } } } - + //-------------------------------------------------- // MARK: - Actionable //-------------------------------------------------- @@ -218,10 +250,10 @@ open class Label: UILabel, ViewProtocol, UserInfoable { private var actions: [LabelAction] = [] { didSet { - isUserInteractionEnabled = !actions.isEmpty - accessibilityTraits = !actions.isEmpty ? .link : .staticText if actions.isEmpty { tapGesture = nil + isUserInteractionEnabled = true + accessibilityTraits = .staticText } else { //add tap gesture if tapGesture == nil { @@ -253,18 +285,42 @@ open class Label: UILabel, ViewProtocol, UserInfoable { //-------------------------------------------------- private func customAccessibilityAction(text: String?, range: NSRange, accessibleText: String? = nil) -> UIAccessibilityCustomAction? { - guard let text = text else { return nil } - //TODO: accessibilityHint for Label -// if accessibilityHint == nil { -// accessibilityHint = MVMCoreUIUtility.hardcodedString(withKey: "swipe_to_select_with_action_hint") -// } + guard let text = text, let attributedText else { return nil } let actionText = accessibleText ?? NSString(string:text).substring(with: range) + + // Calculate the frame of the substring + let layoutManager = NSLayoutManager() + let textContainer = NSTextContainer(size: bounds.size) + let textStorage = NSTextStorage(attributedString: attributedText) + layoutManager.addTextContainer(textContainer) + textStorage.addLayoutManager(layoutManager) + + var glyphRange = NSRange() + + // Convert the range for the substring into a range of glyphs + layoutManager.characterRange(forGlyphRange: range, actualGlyphRange: &glyphRange) + + let substringBounds = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer) + + // Create custom accessibility element + let element = UIAccessibilityElement(accessibilityContainer: self) + element.accessibilityLabel = actionText + element.accessibilityTraits = .link + element.accessibilityFrameInContainerSpace = substringBounds + //TODO: accessibilityHint for Label +// element.accessibilityHint = MVMCoreUIUtility.hardcodedString(withKey: "swipe_to_select_with_action_hint") + + accessibilityElements = (accessibilityElements ?? []).compactMap{$0 as? UIAccessibilityElement}.filter { $0.accessibilityLabel != actionText } + accessibilityElements?.append(element) + let accessibleAction = UIAccessibilityCustomAction(name: actionText, target: self, selector: #selector(accessibilityCustomAction(_:))) accessibilityCustomActions?.append(accessibleAction) return accessibleAction } + + @objc public func accessibilityCustomAction(_ action: UIAccessibilityCustomAction) { for actionable in actions {