diff --git a/MVMCoreUI/Atoms/Views/Label.swift b/MVMCoreUI/Atoms/Views/Label.swift index 4346dfa0..ed99fcac 100644 --- a/MVMCoreUI/Atoms/Views/Label.swift +++ b/MVMCoreUI/Atoms/Views/Label.swift @@ -14,7 +14,7 @@ public typealias ActionBlock = () -> () @objcMembers open class Label: UILabel, MVMCoreViewProtocol, MVMCoreUIMoleculeViewProtocol, MVMCoreUIViewConstrainingProtocol, MFButtonProtocol { //------------------------------------------------------ - // MARK: - General Properties + // MARK: - Properties //------------------------------------------------------ public var makeWholeViewClickable = false @@ -56,12 +56,22 @@ public typealias ActionBlock = () -> () public struct ActionableClause { var range: NSRange? var actionBlock: ActionBlock? + var accessibilityID: Int = 0 func performAction() { actionBlock?() } } + //------------------------------------------------------ + // MARK: - Convenience Setter For objective-C + //------------------------------------------------------ + + /// Sets the clauses array to empty. + @objc public func setEmptyClauses() { + clauses = [] + } + //------------------------------------------------------ // MARK: - Initialization //------------------------------------------------------ @@ -72,6 +82,8 @@ public typealias ActionBlock = () -> () numberOfLines = 0 lineBreakMode = .byWordWrapping translatesAutoresizingMaskIntoConstraints = false + clauses = [] + accessibilityCustomActions = [] let tapGesture = UITapGestureRecognizer(target: self, action: #selector(textLinkTapped(_:))) tapGesture.numberOfTapsRequired = 1 @@ -164,7 +176,7 @@ public typealias ActionBlock = () -> () } //------------------------------------------------------ - // MARK: - Functions + // MARK: - Style //------------------------------------------------------ @objc public static func setLabel(_ label: UILabel?, withHTML html: String?) { @@ -293,10 +305,9 @@ public typealias ActionBlock = () -> () guard let actionLabel = label as? Label else { continue } actionLabel.addActionAttributes(range: range, string: attributedString) - actionLabel.clauses.append(ActionableClause(range: range, - actionBlock: actionLabel.createActionBlockFrom(actionMap: json, - additionalData: additionalData, - delegateObject: delegate))) + let actionBlock = actionLabel.createActionBlockFrom(actionMap: json, additionalData: additionalData, delegateObject: delegate) + actionLabel.appendActionableClause(range: range, actionBlock: actionBlock) + default: continue } @@ -400,7 +411,7 @@ public typealias ActionBlock = () -> () /** Appends an external link image to the end of the attributed string. - Will provide one whitespace to the left of the icon + Will provide one whitespace to the left of the icon; adds 2 chars to the end of the string. */ @objc public func appendExternalLinkIcon() { @@ -417,7 +428,7 @@ public typealias ActionBlock = () -> () Insert external link icon anywhere within text of Label. - Note: Each icon insertion adds 1 additional characters to the overall text length. - Therefore, you MUST insert icons and links in the order they would appear. + Therefore, you **MUST** insert icons and links in the order they would appear. - parameter index: Location within the associated text to insert an external Link Icon */ public func insertExternalLinkIcon(at index: Int) { @@ -485,6 +496,12 @@ public typealias ActionBlock = () -> () return containsAttachment } + + func appendActionableClause(range: NSRange, actionBlock: @escaping ActionBlock) { + + let accessibleAction = customAccessibilityAction(range: range) + clauses.append(ActionableClause(range: range, actionBlock: actionBlock, accessibilityID: accessibleAction?.hash ?? -1)) + } } // MARK: - Atomization @@ -496,6 +513,8 @@ extension Label { textAlignment = .left originalAttributedString = nil styleB2(true) + accessibilityCustomActions = [] + clauses = [] } @objc public func setWithJSON(_ json: [AnyHashable: Any]?, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) { @@ -524,7 +543,7 @@ extension Label { // MARK: - Multi-Action Functionality extension Label { - /// Reseting to default Label values. + /// Applied to existing text. Removes underlines of tappable links and assoated actionable clauses. @objc public func clearActionableClauses() { guard let attributedText = attributedText else { return } @@ -536,6 +555,7 @@ extension Label { } self.attributedText = mutableAttributedString + accessibilityElements = [] clauses = [] } @@ -572,7 +592,7 @@ extension Label { @objc public func addTappableLinkAttribute(range: NSRange, actionBlock: @escaping ActionBlock) { setActionAttributes(range: range) - clauses.append(ActionableClause(range: range, actionBlock: actionBlock)) + appendActionableClause(range: range, actionBlock: actionBlock) } /** @@ -587,10 +607,8 @@ extension Label { @objc public func addTappableLinkAttribute(range: NSRange, actionMap: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?, delegateObject: DelegateObject?) { setActionAttributes(range: range) - clauses.append(ActionableClause(range: range, - actionBlock: createActionBlockFrom(actionMap: actionMap, - additionalData: additionalData, - delegateObject: delegateObject))) + let actionBlock = createActionBlockFrom(actionMap: actionMap, additionalData: additionalData, delegateObject: delegateObject) + appendActionableClause(range: range, actionBlock: actionBlock) } @objc private func textLinkTapped(_ gesture: UITapGestureRecognizer) { @@ -642,3 +660,32 @@ extension UITapGestureRecognizer { return NSLocationInRange(indexOfGlyph, targetRange) } } + +// MARK: - Accessibility +extension Label { + + func customAccessibilityAction(range: NSRange) -> UIAccessibilityCustomAction? { + + guard let text = text else { return nil } + + if accessibilityHint == nil { + accessibilityHint = MVMCoreUIUtility.hardcodedString(withKey: "swipe_to_select_with_action_hint") + } + + let actionText = NSString(string: text).substring(with: range) + let accessibleAction = UIAccessibilityCustomAction(name: actionText, target: self, selector: #selector(accessibilityCustomAction(_:))) + accessibilityCustomActions?.append(accessibleAction) + + return accessibleAction + } + + @objc public func accessibilityCustomAction(_ action: UIAccessibilityCustomAction) { + + for clause in clauses { + if action.hash == clause.accessibilityID { + clause.performAction() + return + } + } + } +} diff --git a/MVMCoreUI/Atoms/Views/LabelWithInternalButton.swift b/MVMCoreUI/Atoms/Views/LabelWithInternalButton.swift index 5cc6d50b..bc1c2219 100644 --- a/MVMCoreUI/Atoms/Views/LabelWithInternalButton.swift +++ b/MVMCoreUI/Atoms/Views/LabelWithInternalButton.swift @@ -64,7 +64,15 @@ public typealias CoreObjectActionLoadPresentDelegate = MVMCoreActionDelegateProt if newActionBlock == nil { label?.clearActionableClauses() } else { - label?.clauses = [Label.ActionableClause(range: actionRange, actionBlock: newActionBlock)] + guard let label = label else { return } + + let accessibleAction = UIAccessibilityCustomAction(name: actionText ?? "", target: label, selector: #selector(label.accessibilityCustomAction(_:))) + label.clauses = [Label.ActionableClause(range: actionRange, actionBlock: newActionBlock, accessibilityID: accessibleAction.hash)] + label.accessibilityCustomActions = [accessibleAction] + + if label.accessibilityHint == nil { + label.accessibilityHint = MVMCoreUIUtility.hardcodedString(withKey: "swipe_to_select_with_action_hint") + } } } } diff --git a/MVMCoreUI/SupportingFiles/Strings/en.lproj/Localizable.strings b/MVMCoreUI/SupportingFiles/Strings/en.lproj/Localizable.strings index 2e7677f3..4812d697 100644 --- a/MVMCoreUI/SupportingFiles/Strings/en.lproj/Localizable.strings +++ b/MVMCoreUI/SupportingFiles/Strings/en.lproj/Localizable.strings @@ -8,6 +8,7 @@ //// Accessibility "AccCloseButton" = "Close"; +"swipe_to_select_with_action_hint" = "swipe up or down to select action, then double tap to select."; // Tab "AccTab" = ", tab"; "AccTabHint" = "Double tap to select."; diff --git a/MVMCoreUI/SupportingFiles/Strings/es-MX.lproj/Localizable.strings b/MVMCoreUI/SupportingFiles/Strings/es-MX.lproj/Localizable.strings index bf4f4e47..d6505503 100644 --- a/MVMCoreUI/SupportingFiles/Strings/es-MX.lproj/Localizable.strings +++ b/MVMCoreUI/SupportingFiles/Strings/es-MX.lproj/Localizable.strings @@ -6,6 +6,9 @@ Copyright © 2017 myverizon. All rights reserved. */ +// Accessibility +"swipe_to_select_with_action_hint" = "deslízate hacia arriba o hacia abajo para seleccionar la acción, luego toca dos veces para seleccionar."; + "AccCloseButton" = "Cerrar"; // Tab "AccTab" = ", pestaña"; diff --git a/MVMCoreUI/SupportingFiles/Strings/es.lproj/Localizable.strings b/MVMCoreUI/SupportingFiles/Strings/es.lproj/Localizable.strings index bf4f4e47..d6505503 100644 --- a/MVMCoreUI/SupportingFiles/Strings/es.lproj/Localizable.strings +++ b/MVMCoreUI/SupportingFiles/Strings/es.lproj/Localizable.strings @@ -6,6 +6,9 @@ Copyright © 2017 myverizon. All rights reserved. */ +// Accessibility +"swipe_to_select_with_action_hint" = "deslízate hacia arriba o hacia abajo para seleccionar la acción, luego toca dos veces para seleccionar."; + "AccCloseButton" = "Cerrar"; // Tab "AccTab" = ", pestaña";