From a043bd33f2b933d50c8aba334d2c1f4c5f81e0cf Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 5 Jun 2024 16:15:35 -0500 Subject: [PATCH 01/23] added shouldUpdateAccessibility Signed-off-by: Matt Bruce --- VDS/BaseClasses/Control.swift | 2 ++ VDS/BaseClasses/View.swift | 2 ++ VDS/Components/Label/Label.swift | 2 ++ VDS/Components/TextFields/InputField/TextField.swift | 2 ++ VDS/Components/TextFields/TextArea/TextView.swift | 2 ++ VDS/Protocols/ViewProtocol.swift | 8 +++++++- 6 files changed, 17 insertions(+), 1 deletion(-) diff --git a/VDS/BaseClasses/Control.swift b/VDS/BaseClasses/Control.swift index 274d7d9b..6d212e22 100644 --- a/VDS/BaseClasses/Control.swift +++ b/VDS/BaseClasses/Control.swift @@ -47,6 +47,8 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable { //-------------------------------------------------- open var shouldUpdateView: Bool = true + open var shouldUpdateAccessibility: Bool = true + open var userInfo = [String: Primitive]() open var surface: Surface = .light { didSet { setNeedsUpdate() } } diff --git a/VDS/BaseClasses/View.swift b/VDS/BaseClasses/View.swift index a807c25c..9ba21d9c 100644 --- a/VDS/BaseClasses/View.swift +++ b/VDS/BaseClasses/View.swift @@ -46,6 +46,8 @@ open class View: UIView, ViewProtocol, UserInfoable { //-------------------------------------------------- open var shouldUpdateView: Bool = true + open var shouldUpdateAccessibility: Bool = true + /// Dictionary for keeping information for this Control use only Primitives. open var userInfo = [String: Primitive]() diff --git a/VDS/Components/Label/Label.swift b/VDS/Components/Label/Label.swift index 9e4dd18e..41a79c5e 100644 --- a/VDS/Components/Label/Label.swift +++ b/VDS/Components/Label/Label.swift @@ -110,6 +110,8 @@ open class Label: UILabel, ViewProtocol, UserInfoable { /// Key of whether or not updateView() is called in setNeedsUpdate() open var shouldUpdateView: Bool = true + open var shouldUpdateAccessibility: Bool = true + /// Will determine if a scaled font should be used for the font. open var useScaledFont: Bool = false { didSet { setNeedsUpdate() }} diff --git a/VDS/Components/TextFields/InputField/TextField.swift b/VDS/Components/TextFields/InputField/TextField.swift index b054741d..e7c32464 100644 --- a/VDS/Components/TextFields/InputField/TextField.swift +++ b/VDS/Components/TextFields/InputField/TextField.swift @@ -56,6 +56,8 @@ open class TextField: UITextField, ViewProtocol, Errorable { /// Key of whether or not updateView() is called in setNeedsUpdate() open var shouldUpdateView: Bool = true + open var shouldUpdateAccessibility: Bool = true + open var surface: Surface = .light { didSet { setNeedsUpdate() } } open var showError: Bool = false { didSet { setNeedsUpdate() } } diff --git a/VDS/Components/TextFields/TextArea/TextView.swift b/VDS/Components/TextFields/TextArea/TextView.swift index f0b64f46..2fe19cb5 100644 --- a/VDS/Components/TextFields/TextArea/TextView.swift +++ b/VDS/Components/TextFields/TextArea/TextView.swift @@ -48,6 +48,8 @@ open class TextView: UITextView, ViewProtocol, Errorable { /// Key of whether or not updateView() is called in setNeedsUpdate() open var shouldUpdateView: Bool = true + open var shouldUpdateAccessibility: Bool = true + open var surface: Surface = .light { didSet { setNeedsUpdate() } } /// Array of LabelAttributeModel objects used in rendering the text. diff --git a/VDS/Protocols/ViewProtocol.swift b/VDS/Protocols/ViewProtocol.swift index da730372..57b93982 100644 --- a/VDS/Protocols/ViewProtocol.swift +++ b/VDS/Protocols/ViewProtocol.swift @@ -16,6 +16,9 @@ public protocol ViewProtocol: AnyObject, Initable, Resettable, Enabling, Surface /// Key of whether or not updateView() is called in setNeedsUpdate() var shouldUpdateView: Bool { get set } + /// Key of whether or not updateAccessibility() is called in setNeedsUpdate() + var shouldUpdateAccessibility: Bool { get set } + /// Executed on initialization for this View. func initialSetup() @@ -30,12 +33,15 @@ public protocol ViewProtocol: AnyObject, Initable, Resettable, Enabling, Surface } extension ViewProtocol { + /// Called when there are changes in a View based off a change events or from local properties. public func setNeedsUpdate() { if shouldUpdateView { shouldUpdateView = false updateView() - updateAccessibility() + if shouldUpdateAccessibility { + updateAccessibility() + } shouldUpdateView = true } } From b3f810500baffd4f48e652f916d9b60a1719c1a5 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 5 Jun 2024 16:15:53 -0500 Subject: [PATCH 02/23] split up accessibility Signed-off-by: Matt Bruce --- VDS/BaseClasses/Selector/SelectorBase.swift | 1 + .../Selector/SelectorItemBase.swift | 36 ++++++++-- VDS/Components/Buttons/ButtonBase.swift | 2 + VDS/Components/DatePicker/DatePicker.swift | 2 - VDS/Components/RadioBox/RadioBoxItem.swift | 67 ++++++++++++++++--- 5 files changed, 94 insertions(+), 14 deletions(-) diff --git a/VDS/BaseClasses/Selector/SelectorBase.swift b/VDS/BaseClasses/Selector/SelectorBase.swift index 0b29f4e2..809cb4aa 100644 --- a/VDS/BaseClasses/Selector/SelectorBase.swift +++ b/VDS/BaseClasses/Selector/SelectorBase.swift @@ -124,6 +124,7 @@ open class SelectorBase: Control, SelectorControlable { open override func updateAccessibility() { super.updateAccessibility() accessibilityLabel = "\(Self.self)\(showError ? ", error" : "")" + accessibilityHint = !isEnabled ? "" : "Double tap to open." } /// This will change the state of the Selector and execute the actionBlock if provided. diff --git a/VDS/BaseClasses/Selector/SelectorItemBase.swift b/VDS/BaseClasses/Selector/SelectorItemBase.swift index 66772ce0..46d54fb5 100644 --- a/VDS/BaseClasses/Selector/SelectorItemBase.swift +++ b/VDS/BaseClasses/Selector/SelectorItemBase.swift @@ -147,6 +147,30 @@ open class SelectorItemBase: Control, Errorable, open var accessibilityValueText: String? + open var accessibilityLabelText: String { + var accessibilityLabels = [String]() + + accessibilityLabels.append("\(Selector.self)") + + if let text = labelText { + accessibilityLabels.append(text) + } + + if let text = childText { + accessibilityLabels.append(text) + } + + if !isEnabled { + accessibilityLabels.append("dimmed") + } + + if let errorText, showError { + accessibilityLabels.append("error, \(errorText)") + } + + return accessibilityLabels.joined(separator: ", ") + } + //-------------------------------------------------- // MARK: - Overrides //-------------------------------------------------- @@ -162,9 +186,11 @@ open class SelectorItemBase: Control, Errorable, open override func setup() { super.setup() - selectorView.isAccessibilityElement = false - isAccessibilityElement = true - accessibilityTraits = .button + selectorView.isAccessibilityElement = true + selectorView.shouldUpdateAccessibility = false + + isAccessibilityElement = false + accessibilityElements = [selectorView, label, childLabel, errorLabel] addSubview(mainStackView) mainStackView.isUserInteractionEnabled = false @@ -195,8 +221,10 @@ open class SelectorItemBase: Control, Errorable, /// Used to update any Accessibility properties. open override func updateAccessibility() { super.updateAccessibility() - setAccessibilityLabel(for: [selectorView, label, childLabel, errorLabel]) + selectorView.accessibilityLabel = accessibilityLabelText + selectorView.accessibilityHint = !isEnabled ? "" : "Double tap to activate." accessibilityValue = accessibilityValueText + } /// Resets to default settings. diff --git a/VDS/Components/Buttons/ButtonBase.swift b/VDS/Components/Buttons/ButtonBase.swift index 80010ee5..9dd60781 100644 --- a/VDS/Components/Buttons/ButtonBase.swift +++ b/VDS/Components/Buttons/ButtonBase.swift @@ -51,6 +51,8 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable { /// Key of whether or not updateView() is called in setNeedsUpdate() open var shouldUpdateView: Bool = true + open var shouldUpdateAccessibility: Bool = true + open var surface: Surface = .light { didSet { setNeedsUpdate() } } /// Text that will be used in the titleLabel. diff --git a/VDS/Components/DatePicker/DatePicker.swift b/VDS/Components/DatePicker/DatePicker.swift index 7645cce5..c514001a 100644 --- a/VDS/Components/DatePicker/DatePicker.swift +++ b/VDS/Components/DatePicker/DatePicker.swift @@ -102,8 +102,6 @@ open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopov super.setup() fieldStackView.isAccessibilityElement = true - fieldStackView.accessibilityLabel = "Date Picker" - fieldStackView.accessibilityHint = "Double Tap to open" // setting color config selectedDateLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable() diff --git a/VDS/Components/RadioBox/RadioBoxItem.swift b/VDS/Components/RadioBox/RadioBoxItem.swift index 15f7c55a..b555a9b6 100644 --- a/VDS/Components/RadioBox/RadioBoxItem.swift +++ b/VDS/Components/RadioBox/RadioBoxItem.swift @@ -133,6 +133,30 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable { open var accessibilityValueText: String? + open var accessibilityLabelText: String { + var accessibilityLabels = [String]() + + accessibilityLabels.append("Radiobox") + + if let text { + accessibilityLabels.append(text) + } + + if let text = subText { + accessibilityLabels.append(text) + } + + if let text = subTextRight { + accessibilityLabels.append(text) + } + + if !isEnabled { + accessibilityLabels.append("dimmed") + } + + return accessibilityLabels.joined(separator: ", ") + } + //-------------------------------------------------- // MARK: - Configuration Properties //-------------------------------------------------- @@ -171,8 +195,10 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable { open override func setup() { super.setup() - isAccessibilityElement = true - accessibilityTraits = .button + isAccessibilityElement = false + selectorView.isAccessibilityElement = true + selectorView.accessibilityTraits = .button + addSubview(selectorView) selectorView.isUserInteractionEnabled = false @@ -242,12 +268,8 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable { /// Used to update any Accessibility properties. open override func updateAccessibility() { super.updateAccessibility() - setAccessibilityLabel(for: [textLabel, subTextLabel, subTextRightLabel]) - if let currentAccessibilityLabel = accessibilityLabel { - accessibilityLabel = "Radiobox, \(currentAccessibilityLabel)" - } else { - accessibilityLabel = "Radiobox" - } + accessibilityLabel = accessibilityLabelText + if let accessibilityValueText { accessibilityValue = strikethrough ? "\(strikethroughAccessibilityText), \(accessibilityValueText)" @@ -259,6 +281,35 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable { } } + open override var accessibilityElements: [Any]? { + get { + var items = [Any]() + items.append(selectorView) + + let elements = gatherAccessibilityElements(from: selectorView) + let views = elements.compactMap({ $0 as? UIView }) + + //update accessibilityLabel + selectorView.setAccessibilityLabel(for: views) + + //disabled + if !isEnabled { + if let label = selectorView.accessibilityLabel, !label.isEmpty { + selectorView.accessibilityLabel = "\(label), dimmed" + } else { + selectorView.accessibilityLabel = "dimmed" + } + } + + //append all children that are accessible + items.append(contentsOf: elements) + + return items + } + set {} + } + + //-------------------------------------------------- // MARK: - Private Methods //-------------------------------------------------- From 21595355a9aef9ada4d1755926d1f1ce705b71a3 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Mon, 17 Jun 2024 11:05:29 -0500 Subject: [PATCH 03/23] updated for issue Signed-off-by: Matt Bruce --- VDS/Components/DropdownSelect/DropdownSelect.swift | 1 + VDS/SupportingFiles/ReleaseNotes.txt | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/VDS/Components/DropdownSelect/DropdownSelect.swift b/VDS/Components/DropdownSelect/DropdownSelect.swift index 1496f392..7fc52808 100644 --- a/VDS/Components/DropdownSelect/DropdownSelect.swift +++ b/VDS/Components/DropdownSelect/DropdownSelect.swift @@ -132,6 +132,7 @@ open class DropdownSelect: EntryFieldBase { /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations. open override func setup() { super.setup() + accessibilityHintText = "has popup, Double tap to open." inlineDisplayLabel.isAccessibilityElement = true diff --git a/VDS/SupportingFiles/ReleaseNotes.txt b/VDS/SupportingFiles/ReleaseNotes.txt index cd4712b2..8f13a9b0 100644 --- a/VDS/SupportingFiles/ReleaseNotes.txt +++ b/VDS/SupportingFiles/ReleaseNotes.txt @@ -1,9 +1,12 @@ +1.0.68 +---------------- +- CXTDT-553663 - DropdownSelect - Accessibility - has popup + 1.0.67 ---------------- - CXTDT-568463 - Calendar - On long press, hover randomizes - CXTDT-568412 - Calendar - Incorrect side nav icon size - CXTDT-568422 - Calendar - DarkMode Legend icon fill using Light mode color -- CXTDT-553663 - DropdownSelect - Accessibility - has popup - CXTDT-565796 - DropdownSelect - Accessibility - CXTDT-560458 - Dropdown/TextArea - Different voiceover - CXTDT-565106 - InputField - CreditCard - Icons From 225c44a84a1aa6e2568f10ad4a4690aede979e05 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 19 Jun 2024 09:35:51 -0500 Subject: [PATCH 04/23] added action element Signed-off-by: Matt Bruce --- VDS.xcodeproj/project.pbxproj | 4 ++++ VDS/Classes/AccessibilityActionElement.swift | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 VDS/Classes/AccessibilityActionElement.swift diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index c17e971e..b44980ae 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -172,6 +172,7 @@ EAF193432C134F3800C68D18 /* TableCellItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 443DBAF92BDA303F0021497E /* TableCellItem.swift */; }; EAF1FE9929D4850E00101452 /* Clickable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF1FE9829D4850E00101452 /* Clickable.swift */; }; EAF1FE9B29DB1A6000101452 /* Changeable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF1FE9A29DB1A6000101452 /* Changeable.swift */; }; + EAF2F4762C231EAA007BFEDC /* AccessibilityActionElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */; }; EAF7F0952899861000B287F5 /* CheckboxItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0932899861000B287F5 /* CheckboxItem.swift */; }; EAF7F09A2899B17200B287F5 /* CATransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0992899B17200B287F5 /* CATransaction.swift */; }; EAF7F0A0289AB7EC00B287F5 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F09F289AB7EC00B287F5 /* View.swift */; }; @@ -398,6 +399,7 @@ EAEEECAE2B1FC2BA00531FC2 /* ToggleViewChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ToggleViewChangeLog.txt; sourceTree = ""; }; EAF1FE9829D4850E00101452 /* Clickable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Clickable.swift; sourceTree = ""; }; EAF1FE9A29DB1A6000101452 /* Changeable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Changeable.swift; sourceTree = ""; }; + EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityActionElement.swift; sourceTree = ""; }; EAF7F0932899861000B287F5 /* CheckboxItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckboxItem.swift; sourceTree = ""; }; EAF7F0992899B17200B287F5 /* CATransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CATransaction.swift; sourceTree = ""; }; EAF7F09F289AB7EC00B287F5 /* View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = ""; }; @@ -743,6 +745,7 @@ EA985C1C296CD13600F2FF2E /* BundleManager.swift */, EAF7F0B8289C139800B287F5 /* ColorConfiguration.swift */, EAB5FEF02927F4AA00998C17 /* SelfSizingCollectionView.swift */, + EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */, ); path = Classes; sourceTree = ""; @@ -1244,6 +1247,7 @@ EA0D1C412A6AD61C00E5C127 /* Typography+Additional.swift in Sources */, EAC925842911C63100091998 /* Colorable.swift in Sources */, 18B463A42BBD3C46005C4528 /* DropdownOptionModel.swift in Sources */, + EAF2F4762C231EAA007BFEDC /* AccessibilityActionElement.swift in Sources */, EAC58BFD2BE935C300BA39FA /* TitleLockupTextColor.swift in Sources */, EAACB89A2B927108006A3869 /* Valuing.swift in Sources */, EAE785312BA0A438009428EA /* UIImage+Helper.swift in Sources */, diff --git a/VDS/Classes/AccessibilityActionElement.swift b/VDS/Classes/AccessibilityActionElement.swift new file mode 100644 index 00000000..df3b6ed9 --- /dev/null +++ b/VDS/Classes/AccessibilityActionElement.swift @@ -0,0 +1,20 @@ +// +// AccessibilityActionElement.swift +// VDS +// +// Created by Matt Bruce on 6/19/24. +// + +import Foundation +import UIKit + +/// Custom UIAccessibilityElement that allows you to set the default action used in accessibilityActivate. +public class AccessibilityActionElement: UIAccessibilityElement { + var action: (() -> Void)? + + public override func accessibilityActivate() -> Bool { + action?() + return true + } +} + From 6d42ec599cd0f834b6ff993ca8cc12798d494f0d Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 19 Jun 2024 09:36:11 -0500 Subject: [PATCH 05/23] updated accessibility elements Signed-off-by: Matt Bruce --- .../Selector/SelectorItemBase.swift | 38 ++++++++++++++----- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/VDS/BaseClasses/Selector/SelectorItemBase.swift b/VDS/BaseClasses/Selector/SelectorItemBase.swift index cee8d9c2..24e949e7 100644 --- a/VDS/BaseClasses/Selector/SelectorItemBase.swift +++ b/VDS/BaseClasses/Selector/SelectorItemBase.swift @@ -149,14 +149,13 @@ open class SelectorItemBase: Control, Errorable, open var accessibilityLabelText: String { var accessibilityLabels = [String]() - accessibilityLabels.append("\(Selector.self)") - if let text = labelText { + if let text = labelText, !text.isEmpty { accessibilityLabels.append(text) } - if let text = childText { + if let text = childText, !text.isEmpty { accessibilityLabels.append(text) } @@ -164,7 +163,7 @@ open class SelectorItemBase: Control, Errorable, accessibilityLabels.append("dimmed") } - if let errorText, showError { + if let errorText, showError, !errorText.isEmpty { accessibilityLabels.append("error, \(errorText)") } @@ -177,9 +176,7 @@ open class SelectorItemBase: Control, Errorable, /// Executed on initialization for this View. open override func initialSetup() { super.initialSetup() - onClick = { control in - control.toggle() - } + } /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations. @@ -190,7 +187,6 @@ open class SelectorItemBase: Control, Errorable, selectorView.shouldUpdateAccessibility = false isAccessibilityElement = false - accessibilityElements = [selectorView, label, childLabel, errorLabel] addSubview(mainStackView) mainStackView.isUserInteractionEnabled = false @@ -211,6 +207,7 @@ open class SelectorItemBase: Control, Errorable, open override func updateView() { super.updateView() updateLabels() + selectorView.isUserInteractionEnabled = true selectorView.showError = showError selectorView.isSelected = isSelected selectorView.isHighlighted = isHighlighted @@ -224,7 +221,30 @@ open class SelectorItemBase: Control, Errorable, selectorView.accessibilityLabel = accessibilityLabelText selectorView.accessibilityHint = !isEnabled ? "" : "Double tap to activate." accessibilityValue = accessibilityValueText - + } + + open override var accessibilityElements: [Any]? { + get { + var elements = [Any]() + + elements.append(selectorView) + + if let text = labelText, !text.isEmpty { + elements.append(label) + } + + if let text = childText, !text.isEmpty { + elements.append(childLabel) + } + + if let errorText, showError, !errorText.isEmpty { + elements.append(errorLabel) + } + return elements + } + set { + super.accessibilityElements = newValue + } } /// Resets to default settings. From 4b06ef2e31d0dda57520a45967b7e65e118cc50d Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 19 Jun 2024 09:38:34 -0500 Subject: [PATCH 06/23] updated to use new accessibility Signed-off-by: Matt Bruce --- VDS/Components/Label/Label.swift | 62 +++++++++----------------------- 1 file changed, 16 insertions(+), 46 deletions(-) diff --git a/VDS/Components/Label/Label.swift b/VDS/Components/Label/Label.swift index 8c995ca1..c95e27a8 100644 --- a/VDS/Components/Label/Label.swift +++ b/VDS/Components/Label/Label.swift @@ -91,16 +91,14 @@ open class Label: UILabel, ViewProtocol, UserInfoable { private struct LabelAction { var range: NSRange var action: PassthroughSubject - var accessibilityId: Int = 0 - + var frame: CGRect = .zero func performAction() { action.send() } - init(range: NSRange, action: PassthroughSubject, accessibilityID: Int = 0) { + init(range: NSRange, action: PassthroughSubject) { self.range = range self.action = action - self.accessibilityId = accessibilityID } } @@ -265,24 +263,7 @@ open class Label: UILabel, ViewProtocol, UserInfoable { super.layoutSubviews() applyActions() } - - /// Addig custom accessibillty actions from the collection of attributes. - open override func accessibilityActivate() -> Bool { - guard let accessibleActions = accessibilityCustomActions else { return false } - - for actionable in actions { - for action in accessibleActions { - if action.hash == actionable.accessibilityId { - actionable.performAction() - return true - } - } - } - - return false - } - //-------------------------------------------------- // MARK: - Private Methods //-------------------------------------------------- @@ -375,10 +356,17 @@ open class Label: UILabel, ViewProtocol, UserInfoable { //see if the attribute is Actionable if let actionable = attribute as? any ActionLabelAttributeModel, mutableAttributedString.isValid(range: actionable.range) { //create a accessibleAction - let customAccessibilityAction = customAccessibilityAction(text: mutableAttributedString.string, range: actionable.range, accessibleText: actionable.accessibleText) - + let customAccessibilityAction = customAccessibilityElement(text: mutableAttributedString.string, + range: actionable.range, + accessibleText: actionable.accessibleText) + + // creat the action + let labelAction = LabelAction(range: actionable.range, action: actionable.action) + + customAccessibilityAction?.action = labelAction.performAction //create a wrapper for the attributes range, block and - actions.append(LabelAction(range: actionable.range, action: actionable.action, accessibilityID: customAccessibilityAction?.hashValue ?? -1)) + actions.append(labelAction) + isUserInteractionEnabled = true } } @@ -404,7 +392,7 @@ open class Label: UILabel, ViewProtocol, UserInfoable { } } - private func customAccessibilityAction(text: String?, range: NSRange, accessibleText: String? = nil) -> UIAccessibilityCustomAction? { + private func customAccessibilityElement(text: String?, range: NSRange, accessibleText: String? = nil) -> AccessibilityActionElement? { guard let text = text, let attributedText else { return nil } @@ -425,31 +413,13 @@ open class Label: UILabel, ViewProtocol, UserInfoable { let substringBounds = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer) // Create custom accessibility element - let element = UIAccessibilityElement(accessibilityContainer: self) + let element = AccessibilityActionElement(accessibilityContainer: self) element.accessibilityLabel = actionText element.accessibilityTraits = .link + element.accessibilityHint = "Double tap to open" 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 private func accessibilityCustomAction(_ action: UIAccessibilityCustomAction) { - - for actionable in actions { - if action.hash == actionable.accessibilityId { - actionable.performAction() - return - } - } + return element } } - - From f9799386d62b6b41ca504baca6199c24255bb1f7 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 19 Jun 2024 09:35:51 -0500 Subject: [PATCH 07/23] added action element Signed-off-by: Matt Bruce --- VDS.xcodeproj/project.pbxproj | 4 ++++ VDS/Classes/AccessibilityActionElement.swift | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 VDS/Classes/AccessibilityActionElement.swift diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index c17e971e..b44980ae 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -172,6 +172,7 @@ EAF193432C134F3800C68D18 /* TableCellItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 443DBAF92BDA303F0021497E /* TableCellItem.swift */; }; EAF1FE9929D4850E00101452 /* Clickable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF1FE9829D4850E00101452 /* Clickable.swift */; }; EAF1FE9B29DB1A6000101452 /* Changeable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF1FE9A29DB1A6000101452 /* Changeable.swift */; }; + EAF2F4762C231EAA007BFEDC /* AccessibilityActionElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */; }; EAF7F0952899861000B287F5 /* CheckboxItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0932899861000B287F5 /* CheckboxItem.swift */; }; EAF7F09A2899B17200B287F5 /* CATransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0992899B17200B287F5 /* CATransaction.swift */; }; EAF7F0A0289AB7EC00B287F5 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F09F289AB7EC00B287F5 /* View.swift */; }; @@ -398,6 +399,7 @@ EAEEECAE2B1FC2BA00531FC2 /* ToggleViewChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ToggleViewChangeLog.txt; sourceTree = ""; }; EAF1FE9829D4850E00101452 /* Clickable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Clickable.swift; sourceTree = ""; }; EAF1FE9A29DB1A6000101452 /* Changeable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Changeable.swift; sourceTree = ""; }; + EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityActionElement.swift; sourceTree = ""; }; EAF7F0932899861000B287F5 /* CheckboxItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckboxItem.swift; sourceTree = ""; }; EAF7F0992899B17200B287F5 /* CATransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CATransaction.swift; sourceTree = ""; }; EAF7F09F289AB7EC00B287F5 /* View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = ""; }; @@ -743,6 +745,7 @@ EA985C1C296CD13600F2FF2E /* BundleManager.swift */, EAF7F0B8289C139800B287F5 /* ColorConfiguration.swift */, EAB5FEF02927F4AA00998C17 /* SelfSizingCollectionView.swift */, + EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */, ); path = Classes; sourceTree = ""; @@ -1244,6 +1247,7 @@ EA0D1C412A6AD61C00E5C127 /* Typography+Additional.swift in Sources */, EAC925842911C63100091998 /* Colorable.swift in Sources */, 18B463A42BBD3C46005C4528 /* DropdownOptionModel.swift in Sources */, + EAF2F4762C231EAA007BFEDC /* AccessibilityActionElement.swift in Sources */, EAC58BFD2BE935C300BA39FA /* TitleLockupTextColor.swift in Sources */, EAACB89A2B927108006A3869 /* Valuing.swift in Sources */, EAE785312BA0A438009428EA /* UIImage+Helper.swift in Sources */, diff --git a/VDS/Classes/AccessibilityActionElement.swift b/VDS/Classes/AccessibilityActionElement.swift new file mode 100644 index 00000000..df3b6ed9 --- /dev/null +++ b/VDS/Classes/AccessibilityActionElement.swift @@ -0,0 +1,20 @@ +// +// AccessibilityActionElement.swift +// VDS +// +// Created by Matt Bruce on 6/19/24. +// + +import Foundation +import UIKit + +/// Custom UIAccessibilityElement that allows you to set the default action used in accessibilityActivate. +public class AccessibilityActionElement: UIAccessibilityElement { + var action: (() -> Void)? + + public override func accessibilityActivate() -> Bool { + action?() + return true + } +} + From 5efcc624e004ed1bb1de389fe0e880474b1ea838 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 19 Jun 2024 09:38:34 -0500 Subject: [PATCH 08/23] updated to use new accessibility Signed-off-by: Matt Bruce --- VDS/Components/Label/Label.swift | 62 +++++++++----------------------- 1 file changed, 16 insertions(+), 46 deletions(-) diff --git a/VDS/Components/Label/Label.swift b/VDS/Components/Label/Label.swift index 4f56c272..c094f775 100644 --- a/VDS/Components/Label/Label.swift +++ b/VDS/Components/Label/Label.swift @@ -91,16 +91,14 @@ open class Label: UILabel, ViewProtocol, UserInfoable { private struct LabelAction { var range: NSRange var action: PassthroughSubject - var accessibilityId: Int = 0 - + var frame: CGRect = .zero func performAction() { action.send() } - init(range: NSRange, action: PassthroughSubject, accessibilityID: Int = 0) { + init(range: NSRange, action: PassthroughSubject) { self.range = range self.action = action - self.accessibilityId = accessibilityID } } @@ -263,24 +261,7 @@ open class Label: UILabel, ViewProtocol, UserInfoable { super.layoutSubviews() applyActions() } - - /// Addig custom accessibillty actions from the collection of attributes. - open override func accessibilityActivate() -> Bool { - guard let accessibleActions = accessibilityCustomActions else { return false } - - for actionable in actions { - for action in accessibleActions { - if action.hash == actionable.accessibilityId { - actionable.performAction() - return true - } - } - } - - return false - } - //-------------------------------------------------- // MARK: - Private Methods //-------------------------------------------------- @@ -373,10 +354,17 @@ open class Label: UILabel, ViewProtocol, UserInfoable { //see if the attribute is Actionable if let actionable = attribute as? any ActionLabelAttributeModel, mutableAttributedString.isValid(range: actionable.range) { //create a accessibleAction - let customAccessibilityAction = customAccessibilityAction(text: mutableAttributedString.string, range: actionable.range, accessibleText: actionable.accessibleText) - + let customAccessibilityAction = customAccessibilityElement(text: mutableAttributedString.string, + range: actionable.range, + accessibleText: actionable.accessibleText) + + // creat the action + let labelAction = LabelAction(range: actionable.range, action: actionable.action) + + customAccessibilityAction?.action = labelAction.performAction //create a wrapper for the attributes range, block and - actions.append(LabelAction(range: actionable.range, action: actionable.action, accessibilityID: customAccessibilityAction?.hashValue ?? -1)) + actions.append(labelAction) + isUserInteractionEnabled = true } } @@ -402,7 +390,7 @@ open class Label: UILabel, ViewProtocol, UserInfoable { } } - private func customAccessibilityAction(text: String?, range: NSRange, accessibleText: String? = nil) -> UIAccessibilityCustomAction? { + private func customAccessibilityElement(text: String?, range: NSRange, accessibleText: String? = nil) -> AccessibilityActionElement? { guard let text = text, let attributedText else { return nil } @@ -423,31 +411,13 @@ open class Label: UILabel, ViewProtocol, UserInfoable { let substringBounds = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer) // Create custom accessibility element - let element = UIAccessibilityElement(accessibilityContainer: self) + let element = AccessibilityActionElement(accessibilityContainer: self) element.accessibilityLabel = actionText element.accessibilityTraits = .link + element.accessibilityHint = "Double tap to open" 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 private func accessibilityCustomAction(_ action: UIAccessibilityCustomAction) { - - for actionable in actions { - if action.hash == actionable.accessibilityId { - actionable.performAction() - return - } - } + return element } } - - From 4aa081f6deb2d7741445e745b7c4fc1ba1d1de92 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 19 Jun 2024 09:49:19 -0500 Subject: [PATCH 09/23] updated for checkbox only Signed-off-by: Matt Bruce --- VDS/BaseClasses/Selector/SelectorItemBase.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/VDS/BaseClasses/Selector/SelectorItemBase.swift b/VDS/BaseClasses/Selector/SelectorItemBase.swift index 24e949e7..3de2b9db 100644 --- a/VDS/BaseClasses/Selector/SelectorItemBase.swift +++ b/VDS/BaseClasses/Selector/SelectorItemBase.swift @@ -176,7 +176,9 @@ open class SelectorItemBase: Control, Errorable, /// Executed on initialization for this View. open override func initialSetup() { super.initialSetup() - + selectorView.onClick = { [weak self] control in + self?.toggle() + } } /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations. @@ -188,7 +190,6 @@ open class SelectorItemBase: Control, Errorable, isAccessibilityElement = false addSubview(mainStackView) - mainStackView.isUserInteractionEnabled = false mainStackView.addArrangedSubview(selectorStackView) mainStackView.addArrangedSubview(errorLabel) From c20027dd2c6aadae02d626942f6fe099fc4caf18 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 19 Jun 2024 10:53:19 -0500 Subject: [PATCH 10/23] updated for label click Signed-off-by: Matt Bruce --- .../Selector/SelectorItemBase.swift | 16 ++++++++- VDS/Components/Label/Label.swift | 35 +++++++++++++++++-- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/VDS/BaseClasses/Selector/SelectorItemBase.swift b/VDS/BaseClasses/Selector/SelectorItemBase.swift index 3de2b9db..bc92eb07 100644 --- a/VDS/BaseClasses/Selector/SelectorItemBase.swift +++ b/VDS/BaseClasses/Selector/SelectorItemBase.swift @@ -176,7 +176,7 @@ open class SelectorItemBase: Control, Errorable, /// Executed on initialization for this View. open override func initialSetup() { super.initialSetup() - selectorView.onClick = { [weak self] control in + onClick = { [weak self] control in self?.toggle() } } @@ -247,7 +247,21 @@ open class SelectorItemBase: Control, Errorable, super.accessibilityElements = newValue } } + + /// Overriden to take the hit if there is an onClickSubscriber and the view is not a UIControl + open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let labelPoint = convert(point, to: label) + let childLabelPoint = convert(point, to: childLabel) + if label.isAction(for: labelPoint) { + return label + } else if childLabel.isAction(for: childLabelPoint) { + return childLabel + } else { + return super.hitTest(point, with: event) + } + } + /// Resets to default settings. open override func reset() { super.reset() diff --git a/VDS/Components/Label/Label.swift b/VDS/Components/Label/Label.swift index c95e27a8..090215c2 100644 --- a/VDS/Components/Label/Label.swift +++ b/VDS/Components/Label/Label.swift @@ -371,7 +371,7 @@ open class Label: UILabel, ViewProtocol, UserInfoable { } if let accessibilityElements, !accessibilityElements.isEmpty { - let staticText = UIAccessibilityElement(accessibilityContainer: self) + let staticText = AccessibilityActionElement(accessibilityContainer: self) staticText.accessibilityLabel = text staticText.accessibilityFrameInContainerSpace = bounds @@ -385,12 +385,38 @@ open class Label: UILabel, ViewProtocol, UserInfoable { @objc private func textLinkTapped(_ gesture: UITapGestureRecognizer) { for actionable in actions { // This determines if we tapped on the desired range of text. - if gesture.didTapActionInLabel(self, inRange: actionable.range) { + let location = gesture.location(in: self) + if didTapActionInLabel(location, inRange: actionable.range) { actionable.performAction() return } } } + + public func isAction(for location: CGPoint) -> Bool { + for actionable in actions { + if didTapActionInLabel(location, inRange: actionable.range) { + return true + } + } + return false + } + + private func didTapActionInLabel(_ location: CGPoint, inRange targetRange: NSRange) -> Bool { + + guard let attributedText else { return false } + let layoutManager = NSLayoutManager() + let textContainer = NSTextContainer(size: bounds.size) + let textStorage = NSTextStorage(attributedString: attributedText) + layoutManager.addTextContainer(textContainer) + textStorage.addLayoutManager(layoutManager) + + let characterIndex = layoutManager.characterIndex(for: location, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil) + + guard let _ = attributedText.attribute(NSAttributedString.Key.action, at: characterIndex, effectiveRange: nil) as? String, characterIndex < attributedText.length else { return false } + return true + } + private func customAccessibilityElement(text: String?, range: NSRange, accessibleText: String? = nil) -> AccessibilityActionElement? { @@ -422,4 +448,9 @@ open class Label: UILabel, ViewProtocol, UserInfoable { accessibilityElements?.append(element) return element } + + public override func accessibilityActivate() -> Bool { + return false + } + } From bdd481109afb40bb231f71bfcdb586fbab49163e Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 19 Jun 2024 10:58:48 -0500 Subject: [PATCH 11/23] allow touch events for labels to bleed through Signed-off-by: Matt Bruce --- .../Selector/SelectorItemBase.swift | 14 ++++++++++ VDS/Components/Label/Label.swift | 28 ++++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/VDS/BaseClasses/Selector/SelectorItemBase.swift b/VDS/BaseClasses/Selector/SelectorItemBase.swift index fd3f6f2b..35c77922 100644 --- a/VDS/BaseClasses/Selector/SelectorItemBase.swift +++ b/VDS/BaseClasses/Selector/SelectorItemBase.swift @@ -198,7 +198,21 @@ open class SelectorItemBase: Control, Errorable, setAccessibilityLabel(for: [selectorView, label, childLabel, errorLabel]) accessibilityValue = accessibilityValueText } + + /// Overriden to take the hit if there is an onClickSubscriber and the view is not a UIControl + open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let labelPoint = convert(point, to: label) + let childLabelPoint = convert(point, to: childLabel) + if label.isAction(for: labelPoint) { + return label + } else if childLabel.isAction(for: childLabelPoint) { + return childLabel + } else { + return super.hitTest(point, with: event) + } + } + /// Resets to default settings. open override func reset() { super.reset() diff --git a/VDS/Components/Label/Label.swift b/VDS/Components/Label/Label.swift index c094f775..9dd9bf1d 100644 --- a/VDS/Components/Label/Label.swift +++ b/VDS/Components/Label/Label.swift @@ -383,12 +383,38 @@ open class Label: UILabel, ViewProtocol, UserInfoable { @objc private func textLinkTapped(_ gesture: UITapGestureRecognizer) { for actionable in actions { // This determines if we tapped on the desired range of text. - if gesture.didTapActionInLabel(self, inRange: actionable.range) { + let location = gesture.location(in: self) + if didTapActionInLabel(location, inRange: actionable.range) { actionable.performAction() return } } } + + public func isAction(for location: CGPoint) -> Bool { + for actionable in actions { + if didTapActionInLabel(location, inRange: actionable.range) { + return true + } + } + return false + } + + private func didTapActionInLabel(_ location: CGPoint, inRange targetRange: NSRange) -> Bool { + + guard let attributedText else { return false } + let layoutManager = NSLayoutManager() + let textContainer = NSTextContainer(size: bounds.size) + let textStorage = NSTextStorage(attributedString: attributedText) + layoutManager.addTextContainer(textContainer) + textStorage.addLayoutManager(layoutManager) + + let characterIndex = layoutManager.characterIndex(for: location, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil) + + guard let _ = attributedText.attribute(NSAttributedString.Key.action, at: characterIndex, effectiveRange: nil) as? String, characterIndex < attributedText.length else { return false } + return true + } + private func customAccessibilityElement(text: String?, range: NSRange, accessibleText: String? = nil) -> AccessibilityActionElement? { From 8d2125a91cafbd5f7634188b64c5056fb4985a01 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 19 Jun 2024 11:57:49 -0500 Subject: [PATCH 12/23] updated mainStackView Signed-off-by: Matt Bruce --- VDS/BaseClasses/Selector/SelectorItemBase.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/VDS/BaseClasses/Selector/SelectorItemBase.swift b/VDS/BaseClasses/Selector/SelectorItemBase.swift index bc92eb07..555102a2 100644 --- a/VDS/BaseClasses/Selector/SelectorItemBase.swift +++ b/VDS/BaseClasses/Selector/SelectorItemBase.swift @@ -191,6 +191,7 @@ open class SelectorItemBase: Control, Errorable, isAccessibilityElement = false addSubview(mainStackView) + mainStackView.isUserInteractionEnabled = false mainStackView.addArrangedSubview(selectorStackView) mainStackView.addArrangedSubview(errorLabel) selectorStackView.addArrangedSubview(selectorView) From ac816f07f0041ce05c53486a6f8d2e3e9c3591a6 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 19 Jun 2024 13:27:50 -0500 Subject: [PATCH 13/23] added accessibility override for main click Signed-off-by: Matt Bruce --- VDS/BaseClasses/Selector/SelectorBase.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/VDS/BaseClasses/Selector/SelectorBase.swift b/VDS/BaseClasses/Selector/SelectorBase.swift index f8c9650d..31d317cf 100644 --- a/VDS/BaseClasses/Selector/SelectorBase.swift +++ b/VDS/BaseClasses/Selector/SelectorBase.swift @@ -134,4 +134,15 @@ open class SelectorBase: Control, SelectorControlable { super.reset() onChange = nil } + + public var accessibilityDefaultAction: (() -> Void)? + + public override func accessibilityActivate() -> Bool { + if let accessibilityDefaultAction { + accessibilityDefaultAction() + } else { + toggle() + } + return true + } } From ec8d4ba1d66ce91d8481cb90e39442a4bf3793a9 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 19 Jun 2024 13:28:04 -0500 Subject: [PATCH 14/23] updated base with fix --- .../Selector/SelectorItemBase.swift | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/VDS/BaseClasses/Selector/SelectorItemBase.swift b/VDS/BaseClasses/Selector/SelectorItemBase.swift index 555102a2..3a04b86d 100644 --- a/VDS/BaseClasses/Selector/SelectorItemBase.swift +++ b/VDS/BaseClasses/Selector/SelectorItemBase.swift @@ -11,7 +11,7 @@ import Combine import VDSCoreTokens /// Base Class used to build out a SelectorControlable control. -open class SelectorItemBase: Control, Errorable, Changeable, Groupable { +open class SelectorItemBase: Control, Errorable, Changeable, Groupable { //-------------------------------------------------- // MARK: - Initializers @@ -149,8 +149,13 @@ open class SelectorItemBase: Control, Errorable, open var accessibilityLabelText: String { var accessibilityLabels = [String]() - accessibilityLabels.append("\(Selector.self)") + + if isSelected { + accessibilityLabels.append("selected") + } + accessibilityLabels.append("\(Selector.self)") + if let text = labelText, !text.isEmpty { accessibilityLabels.append(text) } @@ -162,7 +167,7 @@ open class SelectorItemBase: Control, Errorable, if !isEnabled { accessibilityLabels.append("dimmed") } - + if let errorText, showError, !errorText.isEmpty { accessibilityLabels.append("error, \(errorText)") } @@ -177,8 +182,14 @@ open class SelectorItemBase: Control, Errorable, open override func initialSetup() { super.initialSetup() onClick = { [weak self] control in - self?.toggle() + guard let self, isEnabled else { return } + toggle() } + + selectorView.accessibilityDefaultAction = { [weak self] in + guard let self, isEnabled else { return } + toggle() + } } /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations. @@ -209,7 +220,6 @@ open class SelectorItemBase: Control, Errorable, open override func updateView() { super.updateView() updateLabels() - selectorView.isUserInteractionEnabled = true selectorView.showError = showError selectorView.isSelected = isSelected selectorView.isHighlighted = isHighlighted @@ -251,14 +261,16 @@ open class SelectorItemBase: Control, Errorable, /// Overriden to take the hit if there is an onClickSubscriber and the view is not a UIControl open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + guard isEnabled else { return super.hitTest(point, with: event) } + let labelPoint = convert(point, to: label) let childLabelPoint = convert(point, to: childLabel) - if label.isAction(for: labelPoint) { return label } else if childLabel.isAction(for: childLabelPoint) { return childLabel } else { + guard !UIAccessibility.isVoiceOverRunning else { return nil } return super.hitTest(point, with: event) } } From f25aee83de6099dda05ab2e19aa821a004a75016 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 19 Jun 2024 14:02:11 -0500 Subject: [PATCH 15/23] only toggle when enabled Signed-off-by: Matt Bruce --- VDS/Components/Checkbox/Checkbox.swift | 2 + VDS/Components/Checkbox/CheckboxItem.swift | 2 + VDS/Components/Label/Label.swift | 9 ++- VDS/Components/RadioBox/RadioBoxItem.swift | 55 ++++++++++++------- VDS/Components/RadioButton/RadioButton.swift | 2 +- .../RadioButton/RadioButtonItem.swift | 2 +- 6 files changed, 47 insertions(+), 25 deletions(-) diff --git a/VDS/Components/Checkbox/Checkbox.swift b/VDS/Components/Checkbox/Checkbox.swift index a999e74a..3369ebcb 100644 --- a/VDS/Components/Checkbox/Checkbox.swift +++ b/VDS/Components/Checkbox/Checkbox.swift @@ -63,6 +63,8 @@ open class Checkbox: SelectorBase { /// This will change the state of the Selector and execute the actionBlock if provided. open override func toggle() { + guard isEnabled else { return } + //removed error if showError && isSelected == false { showError.toggle() diff --git a/VDS/Components/Checkbox/CheckboxItem.swift b/VDS/Components/Checkbox/CheckboxItem.swift index aa5dea5a..21943636 100644 --- a/VDS/Components/Checkbox/CheckboxItem.swift +++ b/VDS/Components/Checkbox/CheckboxItem.swift @@ -38,6 +38,8 @@ open class CheckboxItem: SelectorItemBase { //-------------------------------------------------- /// This will change the state of the Selector and execute the actionBlock if provided. open override func toggle() { + guard isEnabled else { return } + //removed error if showError && isSelected == false { showError.toggle() diff --git a/VDS/Components/Label/Label.swift b/VDS/Components/Label/Label.swift index 090215c2..1ec1de82 100644 --- a/VDS/Components/Label/Label.swift +++ b/VDS/Components/Label/Label.swift @@ -363,7 +363,12 @@ open class Label: UILabel, ViewProtocol, UserInfoable { // creat the action let labelAction = LabelAction(range: actionable.range, action: actionable.action) - customAccessibilityAction?.action = labelAction.performAction + // set the action of the accessibilityElement + customAccessibilityAction?.action = { [weak self] in + guard let self, isEnabled else { return } + labelAction.performAction() + } + //create a wrapper for the attributes range, block and actions.append(labelAction) isUserInteractionEnabled = true @@ -449,7 +454,7 @@ open class Label: UILabel, ViewProtocol, UserInfoable { return element } - public override func accessibilityActivate() -> Bool { + open override func accessibilityActivate() -> Bool { return false } diff --git a/VDS/Components/RadioBox/RadioBoxItem.swift b/VDS/Components/RadioBox/RadioBoxItem.swift index 8ca46b26..417df508 100644 --- a/VDS/Components/RadioBox/RadioBoxItem.swift +++ b/VDS/Components/RadioBox/RadioBoxItem.swift @@ -198,7 +198,6 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable { isAccessibilityElement = false selectorView.isAccessibilityElement = true selectorView.accessibilityTraits = .button - addSubview(selectorView) selectorView.isUserInteractionEnabled = false @@ -252,6 +251,8 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable { /// This will change the state of the Selector and execute the actionBlock if provided. open func toggle() { + guard isEnabled else { return } + //removed error isSelected.toggle() sendActions(for: .valueChanged) @@ -268,14 +269,14 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable { /// Used to update any Accessibility properties. open override func updateAccessibility() { super.updateAccessibility() - accessibilityLabel = accessibilityLabelText + selectorView.accessibilityLabel = accessibilityLabelText if let accessibilityValueText { - accessibilityValue = strikethrough + selectorView.accessibilityValue = strikethrough ? "\(strikethroughAccessibilityText), \(accessibilityValueText)" : accessibilityValueText } else { - accessibilityValue = strikethrough + selectorView.accessibilityValue = strikethrough ? "\(strikethroughAccessibilityText)" : accessibilityValueText } @@ -286,29 +287,41 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable { var items = [Any]() items.append(selectorView) - let elements = gatherAccessibilityElements(from: selectorView) - let views = elements.compactMap({ $0 as? UIView }) - - //update accessibilityLabel - selectorView.setAccessibilityLabel(for: views) - - //disabled - if !isEnabled { - if let label = selectorView.accessibilityLabel, !label.isEmpty { - selectorView.accessibilityLabel = "\(label), dimmed" - } else { - selectorView.accessibilityLabel = "dimmed" - } + if let text = text, !text.isEmpty { + items.append(textLabel) + } + + if let text = subText, !text.isEmpty { + items.append(subTextLabel) + } + + if let text = subTextRight, !text.isEmpty { + items.append(subTextRightLabel) } - - //append all children that are accessible - items.append(contentsOf: elements) - return items } set {} } + /// Overriden to take the hit if there is an onClickSubscriber and the view is not a UIControl + open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + guard isEnabled else { return super.hitTest(point, with: event) } + + let textPoint = convert(point, to: textLabel) + let subTextPoint = convert(point, to: subTextLabel) + let subTextRightPoint = convert(point, to: subTextRightLabel) + + if textLabel.isAction(for: textPoint) { + return textLabel + } else if subTextLabel.isAction(for: subTextPoint) { + return subTextLabel + } else if subTextRightLabel.isAction(for: subTextRightPoint) { + return subTextRightLabel + } else { + guard !UIAccessibility.isVoiceOverRunning else { return nil } + return super.hitTest(point, with: event) + } + } //-------------------------------------------------- // MARK: - Private Methods diff --git a/VDS/Components/RadioButton/RadioButton.swift b/VDS/Components/RadioButton/RadioButton.swift index 36a2cedf..f19e7b2f 100644 --- a/VDS/Components/RadioButton/RadioButton.swift +++ b/VDS/Components/RadioButton/RadioButton.swift @@ -62,7 +62,7 @@ open class RadioButton: SelectorBase { /// This will change the state of the Selector and execute the actionBlock if provided. open override func toggle() { - guard !isSelected else { return } + guard !isSelected, isEnabled else { return } //removed error if showError && isSelected == false { diff --git a/VDS/Components/RadioButton/RadioButtonItem.swift b/VDS/Components/RadioButton/RadioButtonItem.swift index ebde90c8..bc15531d 100644 --- a/VDS/Components/RadioButton/RadioButtonItem.swift +++ b/VDS/Components/RadioButton/RadioButtonItem.swift @@ -34,7 +34,7 @@ open class RadioButtonItem: SelectorItemBase { //-------------------------------------------------- /// This will change the state of the Selector and execute the actionBlock if provided. open override func toggle() { - guard !isSelected else { return } + guard !isSelected, isEnabled else { return } //removed error if showError && isSelected == false { From 4b8d0d985ae1cb32b67d5d1aca72e15b5dc7c7bf Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 19 Jun 2024 17:16:27 -0500 Subject: [PATCH 16/23] added accessibility var Signed-off-by: Matt Bruce --- VDS/Protocols/ViewProtocol.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/VDS/Protocols/ViewProtocol.swift b/VDS/Protocols/ViewProtocol.swift index 57b93982..ba912684 100644 --- a/VDS/Protocols/ViewProtocol.swift +++ b/VDS/Protocols/ViewProtocol.swift @@ -19,6 +19,9 @@ public protocol ViewProtocol: AnyObject, Initable, Resettable, Enabling, Surface /// Key of whether or not updateAccessibility() is called in setNeedsUpdate() var shouldUpdateAccessibility: Bool { get set } + /// Used for setting an implementation for the default Accessible Action + var accessibilityDefaultAction: ((Self) -> Void)? { get set } + /// Executed on initialization for this View. func initialSetup() @@ -55,7 +58,7 @@ extension ViewProtocol where Self: UIView { view.removeFromSuperview() setNeedsDisplay() } - } + } } extension ViewProtocol where Self: UIControl { From a2f7c03b8fbc2dd77bbbe2b719e22ab30c96dea1 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 19 Jun 2024 17:17:19 -0500 Subject: [PATCH 17/23] added to base classes the new accessiblity implmenation Signed-off-by: Matt Bruce --- VDS/BaseClasses/Control.swift | 11 +++++++++-- VDS/BaseClasses/View.swift | 12 +++++++++++- VDS/Components/Buttons/ButtonBase.swift | 11 +++++++++++ VDS/Components/Label/Label.swift | 6 +++++- .../TextFields/InputField/TextField.swift | 13 +++++++++++++ VDS/Components/TextFields/TextArea/TextView.swift | 13 +++++++++++++ VDS/Protocols/ViewProtocol.swift | 2 +- 7 files changed, 63 insertions(+), 5 deletions(-) diff --git a/VDS/BaseClasses/Control.swift b/VDS/BaseClasses/Control.swift index 6d212e22..434a1a88 100644 --- a/VDS/BaseClasses/Control.swift +++ b/VDS/BaseClasses/Control.swift @@ -53,6 +53,8 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable { open var surface: Surface = .light { didSet { setNeedsUpdate() } } + open var accessibilityAction: ((Control) -> Void)? + /// Whether the Control is selected or not. open override var isSelected: Bool { didSet { setNeedsUpdate() } } @@ -123,9 +125,14 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable { //-------------------------------------------------- /// Implement accessibilityActivate on an element in order to handle the default action. /// - Returns: Based on whether the userInteraction is enabled. - override open func accessibilityActivate() -> Bool { + open override func accessibilityActivate() -> Bool { // Hold state in case User wanted isAnimated to remain off. - guard isUserInteractionEnabled else { return false } + guard isEnabled, isUserInteractionEnabled else { return false } + + if let accessibilityAction { + accessibilityAction(self) + } + sendActions(for: .touchUpInside) return true } diff --git a/VDS/BaseClasses/View.swift b/VDS/BaseClasses/View.swift index 9ba21d9c..ec3f8dd0 100644 --- a/VDS/BaseClasses/View.swift +++ b/VDS/BaseClasses/View.swift @@ -55,6 +55,8 @@ open class View: UIView, ViewProtocol, UserInfoable { open var isEnabled: Bool = true { didSet { setNeedsUpdate() } } + open var accessibilityAction: ((View) -> Void)? + //-------------------------------------------------- // MARK: - Lifecycle //-------------------------------------------------- @@ -87,7 +89,15 @@ open class View: UIView, ViewProtocol, UserInfoable { surface = .light isEnabled = true } - + + open override func accessibilityActivate() -> Bool { + guard isEnabled, isUserInteractionEnabled else { return false } + if let accessibilityAction { + accessibilityAction(self) + } + return true + } + open override func layoutSubviews() { super.layoutSubviews() setNeedsUpdate() diff --git a/VDS/Components/Buttons/ButtonBase.swift b/VDS/Components/Buttons/ButtonBase.swift index a8443f58..56a23011 100644 --- a/VDS/Components/Buttons/ButtonBase.swift +++ b/VDS/Components/Buttons/ButtonBase.swift @@ -78,6 +78,8 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable { /// Whether the Button should handle the isHighlighted state. open var shouldHighlight: Bool { isHighlighting == false } + open var accessibilityAction: ((ButtonBase) -> Void)? + /// Whether the Control is highlighted or not. open override var isHighlighted: Bool { didSet { @@ -141,6 +143,15 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable { shouldUpdateView = true setNeedsUpdate() } + + open override func accessibilityActivate() -> Bool { + guard isEnabled, isUserInteractionEnabled else { return false } + if let accessibilityAction { + accessibilityAction(self) + } + sendActions(for: .touchUpInside) + return true + } //-------------------------------------------------- // MARK: - Private Methods diff --git a/VDS/Components/Label/Label.swift b/VDS/Components/Label/Label.swift index 1ec1de82..c5b760a7 100644 --- a/VDS/Components/Label/Label.swift +++ b/VDS/Components/Label/Label.swift @@ -132,6 +132,8 @@ open class Label: UILabel, ViewProtocol, UserInfoable { /// Line break mode for the label, default is set to word wrapping. open override var lineBreakMode: NSLineBreakMode { didSet { setNeedsUpdate() }} + open var accessibilityAction: ((Label) -> Void)? + /// Text that will be used in the label. private var _text: String! override open var text: String! { @@ -455,7 +457,9 @@ open class Label: UILabel, ViewProtocol, UserInfoable { } open override func accessibilityActivate() -> Bool { - return false + guard let accessibilityAction, isEnabled, isUserInteractionEnabled else { return false } + accessibilityAction(self) + return true } } diff --git a/VDS/Components/TextFields/InputField/TextField.swift b/VDS/Components/TextFields/InputField/TextField.swift index da109148..800d836b 100644 --- a/VDS/Components/TextFields/InputField/TextField.swift +++ b/VDS/Components/TextFields/InputField/TextField.swift @@ -77,6 +77,8 @@ open class TextField: UITextField, ViewProtocol, Errorable { open var lineBreakMode: NSLineBreakMode = .byClipping { didSet { setNeedsUpdate() } } + open var accessibilityAction: ((TextField) -> Void)? + open override var isEnabled: Bool { didSet { setNeedsUpdate() } } open var textColorConfiguration: AnyColorable = ViewColorConfiguration().with { @@ -211,6 +213,17 @@ open class TextField: UITextField, ViewProtocol, Errorable { return success } + open override func accessibilityActivate() -> Bool { + guard isEnabled, isUserInteractionEnabled else { return false } + + if let accessibilityAction { + accessibilityAction(self) + return true + } else { + return super.accessibilityActivate() + } + } + //-------------------------------------------------- // MARK: - Private Methods //-------------------------------------------------- diff --git a/VDS/Components/TextFields/TextArea/TextView.swift b/VDS/Components/TextFields/TextArea/TextView.swift index 6c9e92ab..3832d2e5 100644 --- a/VDS/Components/TextFields/TextArea/TextView.swift +++ b/VDS/Components/TextFields/TextArea/TextView.swift @@ -68,6 +68,8 @@ open class TextView: UITextView, ViewProtocol, Errorable { $0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forDisabled: false) }.eraseToAnyColorable(){ didSet { setNeedsUpdate() }} + open var accessibilityAction: ((TextView) -> Void)? + open var showError: Bool = false { didSet { setNeedsUpdate() } } open var errorText: String? { didSet { setNeedsUpdate() } } @@ -146,6 +148,17 @@ open class TextView: UITextView, ViewProtocol, Errorable { shouldUpdateView = true setNeedsUpdate() } + + open override func accessibilityActivate() -> Bool { + guard isEnabled, isUserInteractionEnabled else { return false } + + if let accessibilityAction { + accessibilityAction(self) + return true + } else { + return super.accessibilityActivate() + } + } //-------------------------------------------------- // MARK: - Private Methods diff --git a/VDS/Protocols/ViewProtocol.swift b/VDS/Protocols/ViewProtocol.swift index ba912684..4c2aa694 100644 --- a/VDS/Protocols/ViewProtocol.swift +++ b/VDS/Protocols/ViewProtocol.swift @@ -20,7 +20,7 @@ public protocol ViewProtocol: AnyObject, Initable, Resettable, Enabling, Surface var shouldUpdateAccessibility: Bool { get set } /// Used for setting an implementation for the default Accessible Action - var accessibilityDefaultAction: ((Self) -> Void)? { get set } + var accessibilityAction: ((Self) -> Void)? { get set } /// Executed on initialization for this View. func initialSetup() From 6c4d42898449e8f6cb848b5d39b1acb1cbb208b4 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 19 Jun 2024 17:17:46 -0500 Subject: [PATCH 18/23] override for accessibility new impl Signed-off-by: Matt Bruce --- VDS/BaseClasses/Selector/SelectorBase.swift | 11 ++--- .../Selector/SelectorGroupBase.swift | 7 +++ .../Selector/SelectorItemBase.swift | 22 +++++++++- .../Pagination/PaginationButton.swift | 5 --- VDS/Components/RadioBox/RadioBoxItem.swift | 43 +++++++++++++++---- 5 files changed, 68 insertions(+), 20 deletions(-) diff --git a/VDS/BaseClasses/Selector/SelectorBase.swift b/VDS/BaseClasses/Selector/SelectorBase.swift index 31d317cf..35999cf6 100644 --- a/VDS/BaseClasses/Selector/SelectorBase.swift +++ b/VDS/BaseClasses/Selector/SelectorBase.swift @@ -135,14 +135,15 @@ open class SelectorBase: Control, SelectorControlable { onChange = nil } - public var accessibilityDefaultAction: (() -> Void)? - - public override func accessibilityActivate() -> Bool { - if let accessibilityDefaultAction { - accessibilityDefaultAction() + open override func accessibilityActivate() -> Bool { + guard isEnabled, isUserInteractionEnabled else { return false } + + if let accessibilityAction { + accessibilityAction(self) } else { toggle() } + return true } } diff --git a/VDS/BaseClasses/Selector/SelectorGroupBase.swift b/VDS/BaseClasses/Selector/SelectorGroupBase.swift index 184f8e07..4df1ca5c 100644 --- a/VDS/BaseClasses/Selector/SelectorGroupBase.swift +++ b/VDS/BaseClasses/Selector/SelectorGroupBase.swift @@ -70,6 +70,13 @@ open class SelectorGroupBase: Control, SelectorGrou self?.didSelect(handler) self?.setNeedsUpdate() } + + selector.accessibilityAction = { [weak self] handler in + guard let handler = handler as? SelectorItemType else { return } + self?.didSelect(handler) + self?.setNeedsUpdate() + } + mainStackView.addArrangedSubview(selector) } } diff --git a/VDS/BaseClasses/Selector/SelectorItemBase.swift b/VDS/BaseClasses/Selector/SelectorItemBase.swift index 3a04b86d..2cd7eead 100644 --- a/VDS/BaseClasses/Selector/SelectorItemBase.swift +++ b/VDS/BaseClasses/Selector/SelectorItemBase.swift @@ -147,6 +147,15 @@ open class SelectorItemBase: Control, Errorable, Changea open var accessibilityValueText: String? + open override var accessibilityDefaultAction: ((Control) -> Void)? { + didSet { + selectorView.accessibilityAction = { [weak self] selectorItemBase in + guard let self else { return } + accessibilityAction?(self) + } + } + } + open var accessibilityLabelText: String { var accessibilityLabels = [String]() @@ -186,7 +195,7 @@ open class SelectorItemBase: Control, Errorable, Changea toggle() } - selectorView.accessibilityDefaultAction = { [weak self] in + selectorView.accessibilityAction = { [weak self] _ in guard let self, isEnabled else { return } toggle() } @@ -366,4 +375,15 @@ open class SelectorItemBase: Control, Errorable, Changea /// This will change to state of the Selector. open func toggle() {} + open override func accessibilityActivate() -> Bool { + guard isEnabled, isUserInteractionEnabled else { return false } + + if let accessibilityAction { + accessibilityAction(self) + } else { + toggle() + } + + return true + } } diff --git a/VDS/Components/Pagination/PaginationButton.swift b/VDS/Components/Pagination/PaginationButton.swift index 05ef64ee..d7aaf8e2 100644 --- a/VDS/Components/Pagination/PaginationButton.swift +++ b/VDS/Components/Pagination/PaginationButton.swift @@ -78,11 +78,6 @@ open class PaginationButton: ButtonBase { tintColor = color super.updateView() } - - open override func accessibilityActivate() -> Bool { - sendActions(for: .touchUpInside) - return true - } } extension PaginationButton { diff --git a/VDS/Components/RadioBox/RadioBoxItem.swift b/VDS/Components/RadioBox/RadioBoxItem.swift index 417df508..71b05494 100644 --- a/VDS/Components/RadioBox/RadioBoxItem.swift +++ b/VDS/Components/RadioBox/RadioBoxItem.swift @@ -74,9 +74,7 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable { } /// Selector for this RadioBox. - open var selectorView = UIView().with { - $0.translatesAutoresizingMaskIntoConstraints = false - } + open var selectorView = View() /// If provided, the RadioBox text will be rendered. open var text: String? { didSet { setNeedsUpdate() } } @@ -133,20 +131,33 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable { open var accessibilityValueText: String? + open override var accessibilityAction: ((Control) -> Void)? { + didSet { + selectorView.accessibilityAction = { [weak self] selectorItemBase in + guard let self else { return } + accessibilityAction?(self) + } + } + } + open var accessibilityLabelText: String { var accessibilityLabels = [String]() + if isSelected { + accessibilityLabels.append("selected") + } + accessibilityLabels.append("Radiobox") - if let text { + if let text, !text.isEmpty { accessibilityLabels.append(text) } - if let text = subText { + if let text = subText, !text.isEmpty { accessibilityLabels.append(text) } - if let text = subTextRight { + if let text = subTextRight, !text.isEmpty { accessibilityLabels.append(text) } @@ -189,6 +200,16 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable { onClick = { control in control.toggle() } + + if #available(iOS 17.0, *) { + accessibilityHintBlock = { [weak self] in + + return "foo" + } + } else { + // Fallback on earlier versions + } + } /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations. @@ -199,7 +220,7 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable { selectorView.isAccessibilityElement = true selectorView.accessibilityTraits = .button addSubview(selectorView) - selectorView.isUserInteractionEnabled = false + selectorView.isUserInteractionEnabled = true selectorView.addSubview(selectorStackView) @@ -281,7 +302,7 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable { : accessibilityValueText } } - + open override var accessibilityElements: [Any]? { get { var items = [Any]() @@ -322,7 +343,11 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable { return super.hitTest(point, with: event) } } - + + open func getSelectorView() -> UIView { + selectorView + } + //-------------------------------------------------- // MARK: - Private Methods //-------------------------------------------------- From b18cabb680e458c88c1854f4cdd28b7563ab7bf6 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 19 Jun 2024 17:17:51 -0500 Subject: [PATCH 19/23] added any Signed-off-by: Matt Bruce --- VDS/Extensions/UIView+CALayer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VDS/Extensions/UIView+CALayer.swift b/VDS/Extensions/UIView+CALayer.swift index 3c1db6a5..05924211 100644 --- a/VDS/Extensions/UIView+CALayer.swift +++ b/VDS/Extensions/UIView+CALayer.swift @@ -62,7 +62,7 @@ extension UIView { } else { removeDebugBorder() } - if let view = self as? ViewProtocol { + if let view = self as? (any ViewProtocol) { view.updateView() } } From 2700c39632d62b060aaff8e383650fa54dab5610 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 19 Jun 2024 17:27:29 -0500 Subject: [PATCH 20/23] fixed rename Signed-off-by: Matt Bruce --- VDS/BaseClasses/Selector/SelectorItemBase.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VDS/BaseClasses/Selector/SelectorItemBase.swift b/VDS/BaseClasses/Selector/SelectorItemBase.swift index 2cd7eead..63159d93 100644 --- a/VDS/BaseClasses/Selector/SelectorItemBase.swift +++ b/VDS/BaseClasses/Selector/SelectorItemBase.swift @@ -147,7 +147,7 @@ open class SelectorItemBase: Control, Errorable, Changea open var accessibilityValueText: String? - open override var accessibilityDefaultAction: ((Control) -> Void)? { + open override var accessibilityAction: ((Control) -> Void)? { didSet { selectorView.accessibilityAction = { [weak self] selectorItemBase in guard let self else { return } From d013f07db3d8a4c12574f22b71d3b1d2d791e05e Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 20 Jun 2024 10:11:41 -0500 Subject: [PATCH 21/23] first cut with just inline replace Signed-off-by: Matt Bruce --- VDS/BaseClasses/Control.swift | 150 ++++++++++++++--- VDS/BaseClasses/Selector/SelectorBase.swift | 36 ++++- .../Selector/SelectorItemBase.swift | 37 +++-- VDS/BaseClasses/View.swift | 150 +++++++++++++++-- VDS/Classes/AccessibilityActionElement.swift | 4 +- VDS/Components/Buttons/ButtonBase.swift | 144 +++++++++++++++-- VDS/Components/Label/Label.swift | 141 +++++++++++++++- .../TextFields/InputField/TextField.swift | 153 ++++++++++++++++-- .../TextFields/TextArea/TextView.swift | 140 ++++++++++++++-- VDS/Protocols/ViewProtocol.swift | 83 ++++++++++ 10 files changed, 937 insertions(+), 101 deletions(-) diff --git a/VDS/BaseClasses/Control.swift b/VDS/BaseClasses/Control.swift index 434a1a88..aa7443a2 100644 --- a/VDS/BaseClasses/Control.swift +++ b/VDS/BaseClasses/Control.swift @@ -46,15 +46,11 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable { // MARK: - Public Properties //-------------------------------------------------- open var shouldUpdateView: Bool = true - - open var shouldUpdateAccessibility: Bool = true - + open var userInfo = [String: Primitive]() open var surface: Surface = .light { didSet { setNeedsUpdate() } } - open var accessibilityAction: ((Control) -> Void)? - /// Whether the Control is selected or not. open override var isSelected: Bool { didSet { setNeedsUpdate() } } @@ -123,22 +119,138 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable { //-------------------------------------------------- // MARK: - Overrides //-------------------------------------------------- - /// Implement accessibilityActivate on an element in order to handle the default action. - /// - Returns: Based on whether the userInteraction is enabled. - open override func accessibilityActivate() -> Bool { - // Hold state in case User wanted isAnimated to remain off. - guard isEnabled, isUserInteractionEnabled else { return false } - - if let accessibilityAction { - accessibilityAction(self) - } - - sendActions(for: .touchUpInside) - return true - } - open override func layoutSubviews() { super.layoutSubviews() setNeedsUpdate() } + + //-------------------------------------------------- + // MARK: - Accessibility + //-------------------------------------------------- + open var shouldUpdateAccessibility: Bool = true + + open var accessibilityAction: ((Control) -> Void)? + + private var _isAccessibilityElement: Bool = false + open override var isAccessibilityElement: Bool { + get { + var block: AXBoolReturnBlock? + +// if #available(iOS 17, *) { +// block = isAccessibilityElementBlock +// } + + if block == nil { + block = bridge_isAccessibilityElementBlock + } + + if let block { + return block() + } else { + return _isAccessibilityElement + } + } + set { + _isAccessibilityElement = newValue + } + } + + private var _accessibilityLabel: String? + open override var accessibilityLabel: String? { + get { + var block: AXStringReturnBlock? +// if #available(iOS 17, *) { +// block = accessibilityLabelBlock +// } + + if block == nil { + block = bridge_accessibilityLabelBlock + } + + if let block { + return block() + } else { + return _accessibilityLabel + } + } + set { + _accessibilityLabel = newValue + } + } + + private var _accessibilityHint: String? + open override var accessibilityHint: String? { + get { + var block: AXStringReturnBlock? +// if #available(iOS 17, *) { +// block = accessibilityHintBlock +// } + + if block == nil { + block = bridge_accessibilityHintBlock + } + + if let block { + return block() + } else { + return _accessibilityHint + } + } + set { + _accessibilityHint = newValue + } + } + + private var _accessibilityValue: String? + open override var accessibilityValue: String? { + get { + var block: AXStringReturnBlock? + +// if #available(iOS 17, *) { +// block = accessibilityHintBlock +// } + + if block == nil { + block = bridge_accessibilityValueBlock + } + + if let block{ + return block() + } else { + return _accessibilityValue + } + } + set { + _accessibilityValue = newValue + } + } + + open override func accessibilityActivate() -> Bool { + guard isEnabled, isUserInteractionEnabled else { return false } + var value = true + +// if #available(iOS 17, *) { +// if let block = accessibilityAction { +// block(self) +// } else if let block = accessibilityActivateBlock { +// value = block() +// +// } else if let block = bridge_accessibilityActivateBlock { +// value = block() +// } +// +// } else { + if let block = accessibilityAction { + block(self) + + } else if let block = bridge_accessibilityActivateBlock { + value = block() + } +// } + + sendActions(for: .touchUpInside) + return value + + } + } diff --git a/VDS/BaseClasses/Selector/SelectorBase.swift b/VDS/BaseClasses/Selector/SelectorBase.swift index 35999cf6..d1683aa3 100644 --- a/VDS/BaseClasses/Selector/SelectorBase.swift +++ b/VDS/BaseClasses/Selector/SelectorBase.swift @@ -137,13 +137,33 @@ open class SelectorBase: Control, SelectorControlable { open override func accessibilityActivate() -> Bool { guard isEnabled, isUserInteractionEnabled else { return false } - - if let accessibilityAction { - accessibilityAction(self) - } else { - toggle() - } - - return true + guard isEnabled, isUserInteractionEnabled else { return false } + var value = true + +// if #available(iOS 17, *) { +// if let block = accessibilityAction { +// block(self) +// +// } else if let block = accessibilityActivateBlock { +// value = block() +// +// } else if let block = bridge_accessibilityActivateBlock { +// value = block() +// +// } else { +// toggle() +// } +// } else { + if let block = accessibilityAction { + block(self) + + } else if let block = bridge_accessibilityActivateBlock { + value = block() + + } else { + toggle() + } +// } + return value } } diff --git a/VDS/BaseClasses/Selector/SelectorItemBase.swift b/VDS/BaseClasses/Selector/SelectorItemBase.swift index 63159d93..0326ac52 100644 --- a/VDS/BaseClasses/Selector/SelectorItemBase.swift +++ b/VDS/BaseClasses/Selector/SelectorItemBase.swift @@ -147,7 +147,7 @@ open class SelectorItemBase: Control, Errorable, Changea open var accessibilityValueText: String? - open override var accessibilityAction: ((Control) -> Void)? { + open override var accessibilityAction: ((Control) -> Void)? { didSet { selectorView.accessibilityAction = { [weak self] selectorItemBase in guard let self else { return } @@ -377,13 +377,32 @@ open class SelectorItemBase: Control, Errorable, Changea open override func accessibilityActivate() -> Bool { guard isEnabled, isUserInteractionEnabled else { return false } - - if let accessibilityAction { - accessibilityAction(self) - } else { - toggle() - } - - return true + var value = true + +// if #available(iOS 17, *) { +// if let block = accessibilityAction { +// block(self) +// +// } else if let block = accessibilityActivateBlock { +// value = block() +// +// } else if let block = bridge_accessibilityActivateBlock { +// value = block() +// +// } else { +// toggle() +// } +// } else { + if let block = accessibilityAction { + block(self) + + } else if let block = bridge_accessibilityActivateBlock { + value = block() + + } else { + toggle() + } +// } + return value } } diff --git a/VDS/BaseClasses/View.swift b/VDS/BaseClasses/View.swift index ec3f8dd0..288a8cdc 100644 --- a/VDS/BaseClasses/View.swift +++ b/VDS/BaseClasses/View.swift @@ -46,17 +46,13 @@ open class View: UIView, ViewProtocol, UserInfoable { //-------------------------------------------------- open var shouldUpdateView: Bool = true - open var shouldUpdateAccessibility: Bool = true - /// Dictionary for keeping information for this Control use only Primitives. open var userInfo = [String: Primitive]() open var surface: Surface = .light { didSet { setNeedsUpdate() } } open var isEnabled: Bool = true { didSet { setNeedsUpdate() } } - - open var accessibilityAction: ((View) -> Void)? - + //-------------------------------------------------- // MARK: - Lifecycle //-------------------------------------------------- @@ -89,18 +85,144 @@ open class View: UIView, ViewProtocol, UserInfoable { surface = .light isEnabled = true } - - open override func accessibilityActivate() -> Bool { - guard isEnabled, isUserInteractionEnabled else { return false } - if let accessibilityAction { - accessibilityAction(self) - } - return true - } - + open override func layoutSubviews() { super.layoutSubviews() setNeedsUpdate() } + //-------------------------------------------------- + // MARK: - Accessibility + //-------------------------------------------------- + open var shouldUpdateAccessibility: Bool = true + + open var accessibilityAction: ((View) -> Void)? + + private var _isAccessibilityElement: Bool = false + open override var isAccessibilityElement: Bool { + get { + var block: AXBoolReturnBlock? + +// if #available(iOS 17, *) { +// block = isAccessibilityElementBlock +// } + + if block == nil { + block = bridge_isAccessibilityElementBlock + } + + if let block { + return block() + } else { + return _isAccessibilityElement + } + } + set { + _isAccessibilityElement = newValue + } + } + + private var _accessibilityLabel: String? + open override var accessibilityLabel: String? { + get { + var block: AXStringReturnBlock? +// if #available(iOS 17, *) { +// block = accessibilityLabelBlock +// } +// + if block == nil { + block = bridge_accessibilityLabelBlock + } + + if let block { + return block() + } else { + return _accessibilityLabel + } + } + set { + _accessibilityLabel = newValue + } + } + + private var _accessibilityHint: String? + open override var accessibilityHint: String? { + get { + var block: AXStringReturnBlock? +// if #available(iOS 17, *) { +// block = accessibilityHintBlock +// } + + if block == nil { + block = bridge_accessibilityHintBlock + } + + if let block { + return block() + } else { + return _accessibilityHint + } + } + set { + _accessibilityHint = newValue + } + } + + private var _accessibilityValue: String? + open override var accessibilityValue: String? { + get { + var block: AXStringReturnBlock? + +// if #available(iOS 17, *) { +// block = accessibilityHintBlock +// } + + if block == nil { + block = bridge_accessibilityValueBlock + } + + if let block{ + return block() + } else { + return _accessibilityValue + } + } + set { + _accessibilityValue = newValue + } + } + + open override func accessibilityActivate() -> Bool { + guard isEnabled, isUserInteractionEnabled else { return false } + +// if #available(iOS 17, *) { +// if let block = accessibilityAction { +// block(self) +// return true +// } else if let block = accessibilityActivateBlock { +// return block() +// +// } else if let block = bridge_accessibilityActivateBlock { +// return block() +// +// } else { +// return true +// +// } +// +// } else { + if let block = accessibilityAction { + block(self) + return true + + } else if let block = bridge_accessibilityActivateBlock { + return block() + + } else { + return true + + } +// } + } + } diff --git a/VDS/Classes/AccessibilityActionElement.swift b/VDS/Classes/AccessibilityActionElement.swift index df3b6ed9..cc3487f9 100644 --- a/VDS/Classes/AccessibilityActionElement.swift +++ b/VDS/Classes/AccessibilityActionElement.swift @@ -10,10 +10,10 @@ import UIKit /// Custom UIAccessibilityElement that allows you to set the default action used in accessibilityActivate. public class AccessibilityActionElement: UIAccessibilityElement { - var action: (() -> Void)? + public var accessibilityAction: AXVoidReturnBlock? public override func accessibilityActivate() -> Bool { - action?() + accessibilityAction?() return true } } diff --git a/VDS/Components/Buttons/ButtonBase.swift b/VDS/Components/Buttons/ButtonBase.swift index 56a23011..e5a80e2e 100644 --- a/VDS/Components/Buttons/ButtonBase.swift +++ b/VDS/Components/Buttons/ButtonBase.swift @@ -50,8 +50,6 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable { //-------------------------------------------------- /// Key of whether or not updateView() is called in setNeedsUpdate() open var shouldUpdateView: Bool = true - - open var shouldUpdateAccessibility: Bool = true open var surface: Surface = .light { didSet { setNeedsUpdate() } } @@ -77,8 +75,6 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable { /// Whether the Button should handle the isHighlighted state. open var shouldHighlight: Bool { isHighlighting == false } - - open var accessibilityAction: ((ButtonBase) -> Void)? /// Whether the Control is highlighted or not. open override var isHighlighted: Bool { @@ -143,16 +139,7 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable { shouldUpdateView = true setNeedsUpdate() } - - open override func accessibilityActivate() -> Bool { - guard isEnabled, isUserInteractionEnabled else { return false } - if let accessibilityAction { - accessibilityAction(self) - } - sendActions(for: .touchUpInside) - return true - } - + //-------------------------------------------------- // MARK: - Private Methods //-------------------------------------------------- @@ -185,6 +172,135 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable { } } + //-------------------------------------------------- + // MARK: - Accessibility + //-------------------------------------------------- + open var shouldUpdateAccessibility: Bool = true + + open var accessibilityAction: ((ButtonBase) -> Void)? + + private var _isAccessibilityElement: Bool = false + open override var isAccessibilityElement: Bool { + get { + var block: AXBoolReturnBlock? + +// if #available(iOS 17, *) { +// block = isAccessibilityElementBlock +// } + + if block == nil { + block = bridge_isAccessibilityElementBlock + } + + if let block { + return block() + } else { + return _isAccessibilityElement + } + } + set { + _isAccessibilityElement = newValue + } + } + + private var _accessibilityLabel: String? + open override var accessibilityLabel: String? { + get { + var block: AXStringReturnBlock? +// if #available(iOS 17, *) { +// block = accessibilityLabelBlock +// } + + if block == nil { + block = bridge_accessibilityLabelBlock + } + + if let block { + return block() + } else { + return _accessibilityLabel + } + } + set { + _accessibilityLabel = newValue + } + } + + private var _accessibilityHint: String? + open override var accessibilityHint: String? { + get { + var block: AXStringReturnBlock? +// if #available(iOS 17, *) { +// block = accessibilityHintBlock +// } + + if block == nil { + block = bridge_accessibilityHintBlock + } + + if let block { + return block() + } else { + return _accessibilityHint + } + } + set { + _accessibilityHint = newValue + } + } + + private var _accessibilityValue: String? + open override var accessibilityValue: String? { + get { + var block: AXStringReturnBlock? + +// if #available(iOS 17, *) { +// block = accessibilityHintBlock +// } + + if block == nil { + block = bridge_accessibilityValueBlock + } + + if let block{ + return block() + } else { + return _accessibilityValue + } + } + set { + _accessibilityValue = newValue + } + } + + open override func accessibilityActivate() -> Bool { + guard isEnabled, isUserInteractionEnabled else { return false } + var value = true + +// if #available(iOS 17, *) { +// if let block = accessibilityAction { +// block(self) +// } else if let block = accessibilityActivateBlock { +// value = block() +// +// } else if let block = bridge_accessibilityActivateBlock { +// value = block() +// } +// +// } else { + if let block = accessibilityAction { + block(self) + + } else if let block = bridge_accessibilityActivateBlock { + value = block() + } +// } + + sendActions(for: .touchUpInside) + return value + + } + } // MARK: AppleGuidelinesTouchable diff --git a/VDS/Components/Label/Label.swift b/VDS/Components/Label/Label.swift index c5b760a7..b0a85e7e 100644 --- a/VDS/Components/Label/Label.swift +++ b/VDS/Components/Label/Label.swift @@ -108,8 +108,6 @@ open class Label: UILabel, ViewProtocol, UserInfoable { /// Key of whether or not updateView() is called in setNeedsUpdate() open var shouldUpdateView: Bool = true - open var shouldUpdateAccessibility: Bool = true - /// Will determine if a scaled font should be used for the font. open var useScaledFont: Bool = false { didSet { setNeedsUpdate() }} @@ -132,8 +130,6 @@ open class Label: UILabel, ViewProtocol, UserInfoable { /// Line break mode for the label, default is set to word wrapping. open override var lineBreakMode: NSLineBreakMode { didSet { setNeedsUpdate() }} - open var accessibilityAction: ((Label) -> Void)? - /// Text that will be used in the label. private var _text: String! override open var text: String! { @@ -366,7 +362,7 @@ open class Label: UILabel, ViewProtocol, UserInfoable { let labelAction = LabelAction(range: actionable.range, action: actionable.action) // set the action of the accessibilityElement - customAccessibilityAction?.action = { [weak self] in + customAccessibilityAction?.accessibilityAction = { [weak self] in guard let self, isEnabled else { return } labelAction.performAction() } @@ -456,10 +452,139 @@ open class Label: UILabel, ViewProtocol, UserInfoable { return element } + + //-------------------------------------------------- + // MARK: - Accessibility + //-------------------------------------------------- + open var shouldUpdateAccessibility: Bool = true + + open var accessibilityAction: ((Label) -> Void)? + + private var _isAccessibilityElement: Bool = false + open override var isAccessibilityElement: Bool { + get { + var block: AXBoolReturnBlock? + +// if #available(iOS 17, *) { +// block = isAccessibilityElementBlock +// } + + if block == nil { + block = bridge_isAccessibilityElementBlock + } + + if let block { + return block() + } else { + return _isAccessibilityElement + } + } + set { + _isAccessibilityElement = newValue + } + } + + private var _accessibilityLabel: String? + open override var accessibilityLabel: String? { + get { + var block: AXStringReturnBlock? +// if #available(iOS 17, *) { +// block = accessibilityLabelBlock +// } + + if block == nil { + block = bridge_accessibilityLabelBlock + } + + if let block { + return block() + } else { + return _accessibilityLabel + } + } + set { + _accessibilityLabel = newValue + } + } + + private var _accessibilityHint: String? + open override var accessibilityHint: String? { + get { + var block: AXStringReturnBlock? +// if #available(iOS 17, *) { +// block = accessibilityHintBlock +// } + + if block == nil { + block = bridge_accessibilityHintBlock + } + + if let block { + return block() + } else { + return _accessibilityHint + } + } + set { + _accessibilityHint = newValue + } + } + + private var _accessibilityValue: String? + open override var accessibilityValue: String? { + get { + var block: AXStringReturnBlock? + +// if #available(iOS 17, *) { +// block = accessibilityHintBlock +// } + + if block == nil { + block = bridge_accessibilityValueBlock + } + + if let block{ + return block() + } else { + return _accessibilityValue + } + } + set { + _accessibilityValue = newValue + } + } + open override func accessibilityActivate() -> Bool { - guard let accessibilityAction, isEnabled, isUserInteractionEnabled else { return false } - accessibilityAction(self) - return true + guard isEnabled, isUserInteractionEnabled else { return false } + +// if #available(iOS 17, *) { +// if let block = accessibilityAction { +// block(self) +// return true +// } else if let block = accessibilityActivateBlock { +// return block() +// +// } else if let block = bridge_accessibilityActivateBlock { +// return block() +// +// } else { +// return true +// +// } +// +// } else { + if let block = accessibilityAction { + block(self) + return true + + } else if let block = bridge_accessibilityActivateBlock { + return block() + + } else { + return true + + } +// } } } diff --git a/VDS/Components/TextFields/InputField/TextField.swift b/VDS/Components/TextFields/InputField/TextField.swift index 800d836b..81e749b2 100644 --- a/VDS/Components/TextFields/InputField/TextField.swift +++ b/VDS/Components/TextFields/InputField/TextField.swift @@ -47,7 +47,10 @@ open class TextField: UITextField, ViewProtocol, Errorable { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- - private var formatLabel = Label().with { + /// Key of whether or not updateView() is called in setNeedsUpdate() + open var shouldUpdateView: Bool = true + + private var formatLabel = Label().with { $0.tag = 999 $0.textColorConfiguration = ViewColorConfiguration().with { $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forDisabled: true) @@ -63,9 +66,6 @@ open class TextField: UITextField, ViewProtocol, Errorable { /// Will determine if a scaled font should be used for the titleLabel font. open var useScaledFont: Bool = false { didSet { setNeedsUpdate() } } - - /// Key of whether or not updateView() is called in setNeedsUpdate() - open var shouldUpdateView: Bool = true open var shouldUpdateAccessibility: Bool = true @@ -76,8 +76,6 @@ open class TextField: UITextField, ViewProtocol, Errorable { open var errorText: String? { didSet { setNeedsUpdate() } } open var lineBreakMode: NSLineBreakMode = .byClipping { didSet { setNeedsUpdate() } } - - open var accessibilityAction: ((TextField) -> Void)? open override var isEnabled: Bool { didSet { setNeedsUpdate() } } @@ -213,17 +211,6 @@ open class TextField: UITextField, ViewProtocol, Errorable { return success } - open override func accessibilityActivate() -> Bool { - guard isEnabled, isUserInteractionEnabled else { return false } - - if let accessibilityAction { - accessibilityAction(self) - return true - } else { - return super.accessibilityActivate() - } - } - //-------------------------------------------------- // MARK: - Private Methods //-------------------------------------------------- @@ -244,7 +231,139 @@ open class TextField: UITextField, ViewProtocol, Errorable { attributedText = nil } } + + + //-------------------------------------------------- + // MARK: - Accessibility + //-------------------------------------------------- + open var accessibilityAction: ((TextField) -> Void)? + private var _isAccessibilityElement: Bool = false + open override var isAccessibilityElement: Bool { + get { + var block: AXBoolReturnBlock? + +// if #available(iOS 17, *) { +// block = isAccessibilityElementBlock +// } + + if block == nil { + block = bridge_isAccessibilityElementBlock + } + + if let block { + return block() + } else { + return _isAccessibilityElement + } + } + set { + _isAccessibilityElement = newValue + } + } + + private var _accessibilityLabel: String? + open override var accessibilityLabel: String? { + get { + var block: AXStringReturnBlock? +// if #available(iOS 17, *) { +// block = accessibilityLabelBlock +// } + + if block == nil { + block = bridge_accessibilityLabelBlock + } + + if let block { + return block() + } else { + return _accessibilityLabel + } + } + set { + _accessibilityLabel = newValue + } + } + + private var _accessibilityHint: String? + open override var accessibilityHint: String? { + get { + var block: AXStringReturnBlock? +// if #available(iOS 17, *) { +// block = accessibilityHintBlock +// } + + if block == nil { + block = bridge_accessibilityHintBlock + } + + if let block { + return block() + } else { + return _accessibilityHint + } + } + set { + _accessibilityHint = newValue + } + } + + private var _accessibilityValue: String? + open override var accessibilityValue: String? { + get { + var block: AXStringReturnBlock? + +// if #available(iOS 17, *) { +// block = accessibilityHintBlock +// } + + if block == nil { + block = bridge_accessibilityValueBlock + } + + if let block{ + return block() + } else { + return _accessibilityValue + } + } + set { + _accessibilityValue = newValue + } + } + + open override func accessibilityActivate() -> Bool { + guard isEnabled, isUserInteractionEnabled else { return false } + +// if #available(iOS 17, *) { +// if let block = accessibilityAction { +// block(self) +// return true +// } else if let block = accessibilityActivateBlock { +// return block() +// +// } else if let block = bridge_accessibilityActivateBlock { +// return block() +// +// } else { +// return super.accessibilityActivate() +// +// } +// +// } else { + if let block = accessibilityAction { + block(self) + return true + + } else if let block = bridge_accessibilityActivateBlock { + return block() + + } else { + return super.accessibilityActivate() + + } +// } + } } extension UITextField { diff --git a/VDS/Components/TextFields/TextArea/TextView.swift b/VDS/Components/TextFields/TextArea/TextView.swift index 3832d2e5..bbb7b329 100644 --- a/VDS/Components/TextFields/TextArea/TextView.swift +++ b/VDS/Components/TextFields/TextArea/TextView.swift @@ -48,8 +48,6 @@ open class TextView: UITextView, ViewProtocol, Errorable { /// Key of whether or not updateView() is called in setNeedsUpdate() open var shouldUpdateView: Bool = true - open var shouldUpdateAccessibility: Bool = true - open var surface: Surface = .light { didSet { setNeedsUpdate() } } /// Array of LabelAttributeModel objects used in rendering the text. @@ -68,8 +66,6 @@ open class TextView: UITextView, ViewProtocol, Errorable { $0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forDisabled: false) }.eraseToAnyColorable(){ didSet { setNeedsUpdate() }} - open var accessibilityAction: ((TextView) -> Void)? - open var showError: Bool = false { didSet { setNeedsUpdate() } } open var errorText: String? { didSet { setNeedsUpdate() } } @@ -149,15 +145,139 @@ open class TextView: UITextView, ViewProtocol, Errorable { setNeedsUpdate() } + + //-------------------------------------------------- + // MARK: - Accessibility + //-------------------------------------------------- + open var shouldUpdateAccessibility: Bool = true + + open var accessibilityAction: ((TextView) -> Void)? + + private var _isAccessibilityElement: Bool = false + open override var isAccessibilityElement: Bool { + get { + var block: AXBoolReturnBlock? + +// if #available(iOS 17, *) { +// block = isAccessibilityElementBlock +// } + + if block == nil { + block = bridge_isAccessibilityElementBlock + } + + if let block { + return block() + } else { + return _isAccessibilityElement + } + } + set { + _isAccessibilityElement = newValue + } + } + + private var _accessibilityLabel: String? + open override var accessibilityLabel: String? { + get { + var block: AXStringReturnBlock? +// if #available(iOS 17, *) { +// block = accessibilityLabelBlock +// } + + if block == nil { + block = bridge_accessibilityLabelBlock + } + + if let block { + return block() + } else { + return _accessibilityLabel + } + } + set { + _accessibilityLabel = newValue + } + } + + private var _accessibilityHint: String? + open override var accessibilityHint: String? { + get { + var block: AXStringReturnBlock? +// if #available(iOS 17, *) { +// block = accessibilityHintBlock +// } + + if block == nil { + block = bridge_accessibilityHintBlock + } + + if let block { + return block() + } else { + return _accessibilityHint + } + } + set { + _accessibilityHint = newValue + } + } + + private var _accessibilityValue: String? + open override var accessibilityValue: String? { + get { + var block: AXStringReturnBlock? + +// if #available(iOS 17, *) { +// block = accessibilityHintBlock +// } + + if block == nil { + block = bridge_accessibilityValueBlock + } + + if let block{ + return block() + } else { + return _accessibilityValue + } + } + set { + _accessibilityValue = newValue + } + } + open override func accessibilityActivate() -> Bool { guard isEnabled, isUserInteractionEnabled else { return false } - if let accessibilityAction { - accessibilityAction(self) - return true - } else { - return super.accessibilityActivate() - } +// if #available(iOS 17, *) { +// if let block = accessibilityAction { +// block(self) +// return true +// } else if let block = accessibilityActivateBlock { +// return block() +// +// } else if let block = bridge_accessibilityActivateBlock { +// return block() +// +// } else { +// return super.accessibilityActivate() +// +// } +// +// } else { + if let block = accessibilityAction { + block(self) + return true + + } else if let block = bridge_accessibilityActivateBlock { + return block() + + } else { + return super.accessibilityActivate() + + } +// } } //-------------------------------------------------- diff --git a/VDS/Protocols/ViewProtocol.swift b/VDS/Protocols/ViewProtocol.swift index 4c2aa694..8018e260 100644 --- a/VDS/Protocols/ViewProtocol.swift +++ b/VDS/Protocols/ViewProtocol.swift @@ -70,3 +70,86 @@ extension ViewProtocol where Self: UIControl { }).store(in: &subscribers) } } + +public protocol AccessibilityUpdatable { + // Basic accessibility + var bridge_isAccessibilityElementBlock: AXBoolReturnBlock? { get set } + + var bridge_accessibilityLabelBlock: AXStringReturnBlock? { get set } + + var bridge_accessibilityValueBlock: AXStringReturnBlock? { get set } + + var bridge_accessibilityHintBlock: AXStringReturnBlock? { get set } + + var bridge_accessibilityActivateBlock: AXBoolReturnBlock? { get set } + +} + +extension NSObject: AccessibilityUpdatable { + static var isAccessibilityElementBlockKey: UInt8 = 0 + static var activateBlockKey: UInt8 = 1 + static var valueBlockKey: UInt8 = 2 + static var hintBlockKey: UInt8 = 3 + static var labelBlockKey: UInt8 = 4 + + public var bridge_isAccessibilityElementBlock: AXBoolReturnBlock? { + get { + return objc_getAssociatedObject(self, &NSObject.isAccessibilityElementBlockKey) as? AXBoolReturnBlock + } + set { + objc_setAssociatedObject(self, &NSObject.isAccessibilityElementBlockKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) +// if #available(iOS 17, *) { +// self.isAccessibilityElementBlock = newValue +// } + } + } + + public var bridge_accessibilityActivateBlock: AXBoolReturnBlock? { + get { + return objc_getAssociatedObject(self, &NSObject.activateBlockKey) as? AXBoolReturnBlock + } + set { + objc_setAssociatedObject(self, &NSObject.activateBlockKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) +// if #available(iOS 17, *) { +// self.accessibilityActivateBlock = newValue +// } + } + } + + public var bridge_accessibilityValueBlock: AXStringReturnBlock? { + get { + return objc_getAssociatedObject(self, &NSObject.valueBlockKey) as? AXStringReturnBlock + } + set { + objc_setAssociatedObject(self, &NSObject.valueBlockKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) +// if #available(iOS 17, *) { +// self.accessibilityValueBlock = newValue +// } + } + } + + public var bridge_accessibilityHintBlock: AXStringReturnBlock? { + get { + return objc_getAssociatedObject(self, &NSObject.hintBlockKey) as? AXStringReturnBlock + } + set { + objc_setAssociatedObject(self, &NSObject.hintBlockKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) +// if #available(iOS 17, *) { +// self.accessibilityHintBlock = newValue +// } + } + } + + public var bridge_accessibilityLabelBlock: AXStringReturnBlock? { + get { + return objc_getAssociatedObject(self, &NSObject.labelBlockKey) as? AXStringReturnBlock + } + set { + objc_setAssociatedObject(self, &NSObject.labelBlockKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) +// if #available(iOS 17, *) { +// self.accessibilityLabelBlock = newValue +// } + } + } + +} From d8c3ba2e33ec704604f5e7e2a2d8072269a2996a Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 20 Jun 2024 12:36:15 -0500 Subject: [PATCH 22/23] added new protocol Signed-off-by: Matt Bruce --- VDS.xcodeproj/project.pbxproj | 4 ++++ VDS/Protocols/AccessibilityUpdatable.swift | 8 ++++++++ 2 files changed, 12 insertions(+) create mode 100644 VDS/Protocols/AccessibilityUpdatable.swift diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index b44980ae..ff8a6735 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -173,6 +173,7 @@ EAF1FE9929D4850E00101452 /* Clickable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF1FE9829D4850E00101452 /* Clickable.swift */; }; EAF1FE9B29DB1A6000101452 /* Changeable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF1FE9A29DB1A6000101452 /* Changeable.swift */; }; EAF2F4762C231EAA007BFEDC /* AccessibilityActionElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */; }; + EAF2F4782C249D72007BFEDC /* AccessibilityUpdatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF2F4772C249D72007BFEDC /* AccessibilityUpdatable.swift */; }; EAF7F0952899861000B287F5 /* CheckboxItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0932899861000B287F5 /* CheckboxItem.swift */; }; EAF7F09A2899B17200B287F5 /* CATransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0992899B17200B287F5 /* CATransaction.swift */; }; EAF7F0A0289AB7EC00B287F5 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F09F289AB7EC00B287F5 /* View.swift */; }; @@ -400,6 +401,7 @@ EAF1FE9829D4850E00101452 /* Clickable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Clickable.swift; sourceTree = ""; }; EAF1FE9A29DB1A6000101452 /* Changeable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Changeable.swift; sourceTree = ""; }; EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityActionElement.swift; sourceTree = ""; }; + EAF2F4772C249D72007BFEDC /* AccessibilityUpdatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityUpdatable.swift; sourceTree = ""; }; EAF7F0932899861000B287F5 /* CheckboxItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckboxItem.swift; sourceTree = ""; }; EAF7F0992899B17200B287F5 /* CATransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CATransaction.swift; sourceTree = ""; }; EAF7F09F289AB7EC00B287F5 /* View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = ""; }; @@ -713,6 +715,7 @@ EA3361AB288B25EC0071C351 /* Protocols */ = { isa = PBXGroup; children = ( + EAF2F4772C249D72007BFEDC /* AccessibilityUpdatable.swift */, EA4DB2FC28D3D0CA00103EE3 /* AnyEquatable.swift */, EA297A5629FB0A360031ED56 /* AppleGuidelinesTouchable.swift */, EAF1FE9A29DB1A6000101452 /* Changeable.swift */, @@ -1209,6 +1212,7 @@ EAF7F0A6289B0CE000B287F5 /* Resetable.swift in Sources */, EA985C2D296F03FE00F2FF2E /* TileletIconModels.swift in Sources */, EA89200428AECF4B006B9984 /* UITextField+Publisher.swift in Sources */, + EAF2F4782C249D72007BFEDC /* AccessibilityUpdatable.swift in Sources */, 18A65A022B96E848006602CC /* Breadcrumbs.swift in Sources */, 1842B1E12BECE7B70021AFCA /* CalendarHeaderReusableView.swift in Sources */, EA78C7962C00CAC200430AD1 /* Groupable.swift in Sources */, diff --git a/VDS/Protocols/AccessibilityUpdatable.swift b/VDS/Protocols/AccessibilityUpdatable.swift new file mode 100644 index 00000000..07bd429b --- /dev/null +++ b/VDS/Protocols/AccessibilityUpdatable.swift @@ -0,0 +1,8 @@ +// +// AccessibilityUpdatable.swift +// VDS +// +// Created by Matt Bruce on 6/20/24. +// + +import Foundation From 73f27d1e8ba94d225f11396aa07c845bb137149d Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 20 Jun 2024 12:37:19 -0500 Subject: [PATCH 23/23] more code refactoring Signed-off-by: Matt Bruce --- VDS/BaseClasses/Control.swift | 2 - VDS/BaseClasses/Selector/SelectorBase.swift | 17 ++-- .../Selector/SelectorItemBase.swift | 77 ++++++++-------- VDS/BaseClasses/View.swift | 4 +- VDS/Classes/AccessibilityActionElement.swift | 3 +- VDS/Components/Badge/Badge.swift | 11 +-- .../BadgeIndicator/BadgeIndicator.swift | 21 ++--- .../Breadcrumbs/BreadcrumbItem.swift | 15 +-- VDS/Components/Buttons/ButtonBase.swift | 2 - .../Buttons/TextLink/TextLink.swift | 6 ++ .../Buttons/TextLinkCaret/TextLinkCaret.swift | 6 ++ VDS/Components/Checkbox/CheckboxGroup.swift | 3 +- .../Icon/ButtonIcon/ButtonIcon.swift | 72 ++++++++------- VDS/Components/Icon/Icon.swift | 13 +-- VDS/Components/Label/Label.swift | 10 +- .../Notification/Notification.swift | 12 +-- VDS/Components/Pagination/Pagination.swift | 30 ++++-- VDS/Components/RadioBox/RadioBoxGroup.swift | 8 +- VDS/Components/RadioBox/RadioBoxItem.swift | 90 +++++++----------- .../RadioButton/RadioButtonGroup.swift | 3 +- VDS/Components/Tabs/Tab.swift | 14 +-- VDS/Components/Tabs/Tabs.swift | 5 +- .../TextFields/EntryFieldBase.swift | 83 ++++++++--------- .../TextFields/InputField/TextField.swift | 2 - .../TextFields/TextArea/TextView.swift | 2 - .../TileContainer/TileContainer.swift | 12 +-- VDS/Components/TitleLockup/TitleLockup.swift | 27 +++--- VDS/Components/Toggle/Toggle.swift | 18 ++-- VDS/Components/Toggle/ToggleView.swift | 9 +- VDS/Components/Tooltip/Tooltip.swift | 34 +++---- VDS/Components/Tooltip/TooltipDialog.swift | 16 ++-- VDS/Protocols/AccessibilityUpdatable.swift | 87 ++++++++++++++++++ VDS/Protocols/Groupable.swift | 2 - VDS/Protocols/ViewProtocol.swift | 92 +------------------ 34 files changed, 399 insertions(+), 409 deletions(-) diff --git a/VDS/BaseClasses/Control.swift b/VDS/BaseClasses/Control.swift index aa7443a2..2a9fe769 100644 --- a/VDS/BaseClasses/Control.swift +++ b/VDS/BaseClasses/Control.swift @@ -127,8 +127,6 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable { //-------------------------------------------------- // MARK: - Accessibility //-------------------------------------------------- - open var shouldUpdateAccessibility: Bool = true - open var accessibilityAction: ((Control) -> Void)? private var _isAccessibilityElement: Bool = false diff --git a/VDS/BaseClasses/Selector/SelectorBase.swift b/VDS/BaseClasses/Selector/SelectorBase.swift index d1683aa3..fb8d771e 100644 --- a/VDS/BaseClasses/Selector/SelectorBase.swift +++ b/VDS/BaseClasses/Selector/SelectorBase.swift @@ -104,6 +104,16 @@ open class SelectorBase: Control, SelectorControlable { onClick = { control in control.toggle() } + + bridge_accessibilityLabelBlock = { [weak self] in + guard let self else { return "" } + return "\(Self.self)\(showError ? ", error" : "")" + } + + bridge_accessibilityHintBlock = { [weak self] in + guard let self else { return "" } + return !isEnabled ? "" : "Double tap to activate." + } } /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations. @@ -119,13 +129,6 @@ open class SelectorBase: Control, SelectorControlable { setNeedsLayout() layoutIfNeeded() } - - /// Used to update any Accessibility properties.ß - open override func updateAccessibility() { - super.updateAccessibility() - accessibilityLabel = "\(Self.self)\(showError ? ", error" : "")" - accessibilityHint = !isEnabled ? "" : "Double tap to open." - } /// This will change the state of the Selector and execute the actionBlock if provided. open func toggle() { } diff --git a/VDS/BaseClasses/Selector/SelectorItemBase.swift b/VDS/BaseClasses/Selector/SelectorItemBase.swift index 0326ac52..44ed01b1 100644 --- a/VDS/BaseClasses/Selector/SelectorItemBase.swift +++ b/VDS/BaseClasses/Selector/SelectorItemBase.swift @@ -145,8 +145,6 @@ open class SelectorItemBase: Control, Errorable, Changea open var hiddenValue: AnyHashable? { didSet { setNeedsUpdate() } } - open var accessibilityValueText: String? - open override var accessibilityAction: ((Control) -> Void)? { didSet { selectorView.accessibilityAction = { [weak self] selectorItemBase in @@ -156,34 +154,6 @@ open class SelectorItemBase: Control, Errorable, Changea } } - open var accessibilityLabelText: String { - var accessibilityLabels = [String]() - - if isSelected { - accessibilityLabels.append("selected") - } - - accessibilityLabels.append("\(Selector.self)") - - if let text = labelText, !text.isEmpty { - accessibilityLabels.append(text) - } - - if let text = childText, !text.isEmpty { - accessibilityLabels.append(text) - } - - if !isEnabled { - accessibilityLabels.append("dimmed") - } - - if let errorText, showError, !errorText.isEmpty { - accessibilityLabels.append("error, \(errorText)") - } - - return accessibilityLabels.joined(separator: ", ") - } - //-------------------------------------------------- // MARK: - Overrides //-------------------------------------------------- @@ -199,15 +169,48 @@ open class SelectorItemBase: Control, Errorable, Changea guard let self, isEnabled else { return } toggle() } + + selectorView.bridge_accessibilityLabelBlock = { [weak self ] in + guard let self else { return "" } + var accessibilityLabels = [String]() + + if isSelected { + accessibilityLabels.append("selected") + } + + accessibilityLabels.append("\(Selector.self)") + + if let text = labelText, !text.isEmpty { + accessibilityLabels.append(text) + } + + if let text = childText, !text.isEmpty { + accessibilityLabels.append(text) + } + + if !isEnabled { + accessibilityLabels.append("dimmed") + } + + if let errorText, showError, !errorText.isEmpty { + accessibilityLabels.append("error, \(errorText)") + } + + return accessibilityLabels.joined(separator: ", ") + } + + selectorView.bridge_accessibilityHintBlock = { [weak self] in + guard let self else { return "" } + return !isEnabled ? "" : "Double tap to activate." + } + } /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations. open override func setup() { super.setup() - selectorView.isAccessibilityElement = true - selectorView.shouldUpdateAccessibility = false - + selectorView.isAccessibilityElement = true isAccessibilityElement = false addSubview(mainStackView) @@ -235,14 +238,6 @@ open class SelectorItemBase: Control, Errorable, Changea selectorView.isEnabled = isEnabled selectorView.surface = surface } - - /// Used to update any Accessibility properties. - open override func updateAccessibility() { - super.updateAccessibility() - selectorView.accessibilityLabel = accessibilityLabelText - selectorView.accessibilityHint = !isEnabled ? "" : "Double tap to activate." - accessibilityValue = accessibilityValueText - } open override var accessibilityElements: [Any]? { get { diff --git a/VDS/BaseClasses/View.swift b/VDS/BaseClasses/View.swift index 288a8cdc..c7df1765 100644 --- a/VDS/BaseClasses/View.swift +++ b/VDS/BaseClasses/View.swift @@ -94,8 +94,6 @@ open class View: UIView, ViewProtocol, UserInfoable { //-------------------------------------------------- // MARK: - Accessibility //-------------------------------------------------- - open var shouldUpdateAccessibility: Bool = true - open var accessibilityAction: ((View) -> Void)? private var _isAccessibilityElement: Bool = false @@ -219,7 +217,7 @@ open class View: UIView, ViewProtocol, UserInfoable { return block() } else { - return true + return super.accessibilityActivate() } // } diff --git a/VDS/Classes/AccessibilityActionElement.swift b/VDS/Classes/AccessibilityActionElement.swift index cc3487f9..7717839c 100644 --- a/VDS/Classes/AccessibilityActionElement.swift +++ b/VDS/Classes/AccessibilityActionElement.swift @@ -13,7 +13,8 @@ public class AccessibilityActionElement: UIAccessibilityElement { public var accessibilityAction: AXVoidReturnBlock? public override func accessibilityActivate() -> Bool { - accessibilityAction?() + guard let accessibilityAction else { return super.accessibilityActivate() } + accessibilityAction() return true } } diff --git a/VDS/Components/Badge/Badge.swift b/VDS/Components/Badge/Badge.swift index 43f702fa..5753428e 100644 --- a/VDS/Components/Badge/Badge.swift +++ b/VDS/Components/Badge/Badge.swift @@ -147,6 +147,11 @@ open class Badge: View { label.widthGreaterThanEqualTo(constant: minWidth) maxWidthConstraint = label.widthLessThanEqualTo(constant: 0).with { $0.isActive = false } clipsToBounds = true + + bridge_accessibilityLabelBlock = { [weak self] in + guard let self else { return "" } + return text + } } /// Resets to default settings. @@ -179,10 +184,4 @@ open class Badge: View { label.surface = surface label.isEnabled = isEnabled } - - open override func updateAccessibility() { - super.updateAccessibility() - - accessibilityLabel = text - } } diff --git a/VDS/Components/BadgeIndicator/BadgeIndicator.swift b/VDS/Components/BadgeIndicator/BadgeIndicator.swift index 740538d1..60025390 100644 --- a/VDS/Components/BadgeIndicator/BadgeIndicator.swift +++ b/VDS/Components/BadgeIndicator/BadgeIndicator.swift @@ -292,6 +292,16 @@ open class BadgeIndicator: View { label.centerYAnchor.constraint(equalTo: badgeView.centerYAnchor).isActive = true labelContraints.isActive = true + bridge_accessibilityLabelBlock = { [weak self] in + guard let self else { return "" } + if let accessibilityText { + return kind == .numbered ? label.text + " " + accessibilityText : accessibilityText + } else if kind == .numbered { + return label.text + } else { + return "Simple" + } + } } /// Resets to default settings. @@ -347,17 +357,6 @@ open class BadgeIndicator: View { setNeedsLayout() } - open override func updateAccessibility() { - super.updateAccessibility() - if let accessibilityText { - accessibilityLabel = kind == .numbered ? label.text + " " + accessibilityText : accessibilityText - } else if kind == .numbered { - accessibilityLabel = label.text - } else { - accessibilityLabel = "Simple" - } - } - open override func layoutSubviews() { super.layoutSubviews() diff --git a/VDS/Components/Breadcrumbs/BreadcrumbItem.swift b/VDS/Components/Breadcrumbs/BreadcrumbItem.swift index 08e58e60..bf6d4ed1 100644 --- a/VDS/Components/Breadcrumbs/BreadcrumbItem.swift +++ b/VDS/Components/Breadcrumbs/BreadcrumbItem.swift @@ -82,6 +82,15 @@ open class BreadcrumbItem: ButtonBase { isAccessibilityElement = true accessibilityTraits = .link + bridge_accessibilityHintBlock = { [weak self] in + guard let self else { return "" } + return !isEnabled ? "" : "Double tap to open." + } + + bridge_accessibilityLabelBlock = { [weak self] in + guard let self else { return "" } + return text + } } /// Used to make changes to the View based off a change events or from local properties. @@ -134,10 +143,4 @@ open class BreadcrumbItem: ButtonBase { setNeedsUpdate() } - /// Used to update any Accessibility properties. - open override func updateAccessibility() { - super.updateAccessibility() - accessibilityLabel = text - } - } diff --git a/VDS/Components/Buttons/ButtonBase.swift b/VDS/Components/Buttons/ButtonBase.swift index e5a80e2e..d7b80c8a 100644 --- a/VDS/Components/Buttons/ButtonBase.swift +++ b/VDS/Components/Buttons/ButtonBase.swift @@ -175,8 +175,6 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable { //-------------------------------------------------- // MARK: - Accessibility //-------------------------------------------------- - open var shouldUpdateAccessibility: Bool = true - open var accessibilityAction: ((ButtonBase) -> Void)? private var _isAccessibilityElement: Bool = false diff --git a/VDS/Components/Buttons/TextLink/TextLink.swift b/VDS/Components/Buttons/TextLink/TextLink.swift index e0aac99c..f77dfa79 100644 --- a/VDS/Components/Buttons/TextLink/TextLink.swift +++ b/VDS/Components/Buttons/TextLink/TextLink.swift @@ -105,6 +105,12 @@ open class TextLink: ButtonBase { lineHeightConstraint = line.height(constant: 1) lineHeightConstraint?.isActive = true } + + bridge_accessibilityHintBlock = { [weak self] in + guard let self else { return "" } + return !isEnabled ? "" : "Double tap to open." + } + } /// Used to make changes to the View based off a change events or from local properties. diff --git a/VDS/Components/Buttons/TextLinkCaret/TextLinkCaret.swift b/VDS/Components/Buttons/TextLinkCaret/TextLinkCaret.swift index 83d057c1..d1522d02 100644 --- a/VDS/Components/Buttons/TextLinkCaret/TextLinkCaret.swift +++ b/VDS/Components/Buttons/TextLinkCaret/TextLinkCaret.swift @@ -86,6 +86,12 @@ open class TextLinkCaret: ButtonBase { accessibilityTraits = .link titleLabel?.numberOfLines = 0 titleLabel?.lineBreakMode = .byWordWrapping + + bridge_accessibilityHintBlock = { [weak self] in + guard let self else { return "" } + return !isEnabled ? "" : "Double tap to open." + } + } /// Used to make changes to the View based off a change events or from local properties. diff --git a/VDS/Components/Checkbox/CheckboxGroup.swift b/VDS/Components/Checkbox/CheckboxGroup.swift index 242e193e..b450d4d5 100644 --- a/VDS/Components/Checkbox/CheckboxGroup.swift +++ b/VDS/Components/Checkbox/CheckboxGroup.swift @@ -47,8 +47,6 @@ open class CheckboxGroup: SelectorGroupBase, SelectorGroupMultiSel $0.surface = model.surface $0.inputId = model.inputId $0.hiddenValue = model.value - $0.accessibilityLabel = model.accessibileText - $0.accessibilityValueText = "item \(index+1) of \(selectorModels.count)" $0.labelText = model.labelText $0.labelTextAttributes = model.labelTextAttributes $0.childText = model.childText @@ -56,6 +54,7 @@ open class CheckboxGroup: SelectorGroupBase, SelectorGroupMultiSel $0.isSelected = model.selected $0.errorText = model.errorText $0.showError = model.showError + $0.selectorView.bridge_accessibilityValueBlock = { "item \(index+1) of \(selectorModels.count)" } } } } diff --git a/VDS/Components/Icon/ButtonIcon/ButtonIcon.swift b/VDS/Components/Icon/ButtonIcon/ButtonIcon.swift index 64ce0da5..bf8a50f7 100644 --- a/VDS/Components/Icon/ButtonIcon/ButtonIcon.swift +++ b/VDS/Components/Icon/ButtonIcon/ButtonIcon.swift @@ -30,7 +30,7 @@ open class ButtonIcon: Control, Changeable { public required init?(coder: NSCoder) { super.init(coder: coder) } - + //-------------------------------------------------- // MARK: - Enums //-------------------------------------------------- @@ -43,7 +43,7 @@ open class ButtonIcon: Control, Changeable { public enum SurfaceType: String, CaseIterable { case colorFill, media } - + /// Enum used to describe the size of button icon. public enum Size: String, EnumSubset { case large @@ -105,12 +105,12 @@ open class ButtonIcon: Control, Changeable { return .init(x: 6, y: 6) } } - + //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- public var onChangeSubscriber: AnyCancellable? - + ///Badge Indicator object used to render for the ButtonIcon. open var badgeIndicator = BadgeIndicator().with { $0.translatesAutoresizingMaskIntoConstraints = false @@ -140,10 +140,10 @@ open class ButtonIcon: Control, Changeable { open var selectedIconName: Icon.Name? { didSet { setNeedsUpdate() } } open var selectedIconColorConfiguration: SurfaceColorConfiguration? { didSet { setNeedsUpdate() } } - + /// Sets the size of button icon and icon. open var size: Size = .large { didSet { setNeedsUpdate() } } - + /// If provided, the button icon will have a box shadow. open var floating: Bool = false { didSet { setNeedsUpdate() } } @@ -152,7 +152,7 @@ open class ButtonIcon: Control, Changeable { /// If set to true, the button icon will not have a border. open var hideBorder: Bool = true { didSet { setNeedsUpdate() } } - + /// If provided, the badge indicator will present. open var showBadgeIndicator: Bool = false { didSet { setNeedsUpdate() } } @@ -169,14 +169,14 @@ open class ButtonIcon: Control, Changeable { /// Used to move the icon inside the button in both x and y axis. open var iconOffset: CGPoint = .init(x: 0, y: 0) { didSet { setNeedsUpdate() } } - + /// Sets a custom size of button icon container. open var customContainerSize: Int? { didSet { setNeedsUpdate() } } - + /// Sets a custom size of the icon. open var customIconSize: Int? { didSet { setNeedsUpdate() } } - + /// Sets a custom badgeIndicator offset open var customBadgeIndicatorOffset: CGPoint? { didSet { setNeedsUpdate() } } @@ -246,7 +246,7 @@ open class ButtonIcon: Control, Changeable { SurfaceColorConfiguration(.clear, .clear).eraseToAnyColorable() }() } - + private struct LowContrastColorFillConfiguration: Configuration { var kind: Kind = .lowContrast var surfaceType: SurfaceType = .colorFill @@ -255,7 +255,7 @@ open class ButtonIcon: Control, Changeable { SurfaceColorConfiguration(VDSColor.paletteGray44.withAlphaComponent(0.06), VDSColor.paletteGray44.withAlphaComponent(0.26)).eraseToAnyColorable() }() } - + private struct LowContrastColorFillFloatingConfiguration: Configuration, DropShadowableConfiguration { var kind: Kind = .lowContrast var surfaceType: SurfaceType = .colorFill @@ -277,7 +277,7 @@ open class ButtonIcon: Control, Changeable { } var configurations: [DropShadowable] { [dropshadow1Configuration, dropshadow2Configuration] } } - + private struct LowContrastMediaConfiguration: Configuration, Borderable { var kind: Kind = .lowContrast var surfaceType: SurfaceType = .media @@ -290,7 +290,7 @@ open class ButtonIcon: Control, Changeable { SurfaceColorConfiguration(VDSColor.elementsLowcontrastOnlight, VDSColor.elementsLowcontrastOndark).eraseToAnyColorable() }() } - + private struct LowContrastMediaFloatingConfiguration: Configuration, DropShadowableConfiguration { var kind: Kind = .lowContrast var surfaceType: SurfaceType = .media @@ -325,10 +325,10 @@ open class ButtonIcon: Control, Changeable { $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled) $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: [.selected, .disabled]) }.eraseToAnyColorable() - + }() } - + private struct HighContrastFloatingConfiguration: Configuration, DropShadowableConfiguration { var kind: Kind = .highContrast var surfaceType: SurfaceType = .colorFill @@ -357,9 +357,9 @@ open class ButtonIcon: Control, Changeable { } var configurations: [DropShadowable] { [dropshadow1Configuration, dropshadow2Configuration] } } - + private var badgeIndicatorDefaultSize: CGSize = .zero - + //-------------------------------------------------- // MARK: - Overrides //-------------------------------------------------- @@ -367,11 +367,11 @@ open class ButtonIcon: Control, Changeable { open override func setup() { super.setup() isAccessibilityElement = false - + //create a layoutGuide for the icon to key off of let iconLayoutGuide = UILayoutGuide() addLayoutGuide(iconLayoutGuide) - + //add the icon addSubview(icon) @@ -379,7 +379,7 @@ open class ButtonIcon: Control, Changeable { addSubview(badgeIndicator) badgeIndicator.isHidden = !showBadgeIndicator badgeIndicatorDefaultSize = badgeIndicator.frame.size - + //determines the height/width of the icon layoutGuideWidthConstraint = iconLayoutGuide.width(constant: size.containerSize) layoutGuideHeightConstraint = iconLayoutGuide.height(constant: size.containerSize) @@ -388,7 +388,7 @@ open class ButtonIcon: Control, Changeable { badgeIndicatorCenterXConstraint = badgeIndicator.centerXAnchor.constraint(equalTo: icon.centerXAnchor) badgeIndicatorCenterYConstraint = icon.centerYAnchor.constraint(equalTo: badgeIndicator.centerYAnchor) badgeIndicatorCenterYConstraint?.isActive = true - + badgeIndicatorLeadingConstraint?.isActive = true //pin layout guide iconLayoutGuide @@ -396,7 +396,7 @@ open class ButtonIcon: Control, Changeable { .pinLeading() .pinTrailing(0, .defaultHigh) .pinBottom(0, .defaultHigh) - + //determines the center point of the icon centerXConstraint = icon.centerXAnchor.constraint(equalTo: iconLayoutGuide.centerXAnchor, constant: 0) centerXConstraint?.activate() @@ -414,14 +414,14 @@ open class ButtonIcon: Control, Changeable { } } } - + /// This will change the state of the Selector and execute the actionBlock if provided. open func toggle() { //removed error isSelected.toggle() sendActions(for: .valueChanged) } - + /// Resets to default settings. open override func reset() { super.reset() @@ -437,7 +437,7 @@ open class ButtonIcon: Control, Changeable { showBadgeIndicator = false selectable = false badgeIndicatorModel = nil - onChange = nil + onChange = nil shouldUpdateView = true setNeedsUpdate() } @@ -464,16 +464,18 @@ open class ButtonIcon: Control, Changeable { setNeedsLayout() } - open override func updateAccessibility() { - super.updateAccessibility() - var elements = [Any]() - if iconName != nil { - elements.append(icon) + open override var accessibilityElements: [Any]? { + get { + var elements = [Any]() + if iconName != nil { + elements.append(icon) + } + if badgeIndicatorModel != nil && showBadgeIndicator { + elements.append(badgeIndicator) + } + return elements.count > 0 ? elements : nil } - if badgeIndicatorModel != nil && showBadgeIndicator { - elements.append(badgeIndicator) - } - accessibilityElements = elements.count > 0 ? elements : nil + set { } } open override func layoutSubviews() { diff --git a/VDS/Components/Icon/Icon.swift b/VDS/Components/Icon/Icon.swift index ac4a5818..2ac22ac8 100644 --- a/VDS/Components/Icon/Icon.swift +++ b/VDS/Components/Icon/Icon.swift @@ -94,6 +94,12 @@ open class Icon: View { isAccessibilityElement = true accessibilityTraits = .image + + bridge_accessibilityLabelBlock = { [weak self] in + guard let self else { return "" } + return name?.rawValue ?? "icon" + } + } /// Used to make changes to the View based off a change events or from local properties. @@ -118,12 +124,7 @@ open class Icon: View { super.reset() color = VDSColor.paletteBlack imageView.image = nil - } - - open override func updateAccessibility() { - super.updateAccessibility() - accessibilityLabel = name?.rawValue ?? "icon" - } + } } extension UIImage { diff --git a/VDS/Components/Label/Label.swift b/VDS/Components/Label/Label.swift index b0a85e7e..15ed4b45 100644 --- a/VDS/Components/Label/Label.swift +++ b/VDS/Components/Label/Label.swift @@ -213,7 +213,12 @@ open class Label: UILabel, ViewProtocol, UserInfoable { } } - open func setup() {} + open func setup() { + bridge_accessibilityLabelBlock = { [weak self] in + guard let self else { return "" } + return text + } + } open func reset() { shouldUpdateView = false @@ -240,7 +245,6 @@ open class Label: UILabel, ViewProtocol, UserInfoable { } open func updateAccessibility() { - accessibilityLabel = text if isEnabled { accessibilityTraits.remove(.notEnabled) } else { @@ -456,8 +460,6 @@ open class Label: UILabel, ViewProtocol, UserInfoable { //-------------------------------------------------- // MARK: - Accessibility //-------------------------------------------------- - open var shouldUpdateAccessibility: Bool = true - open var accessibilityAction: ((Label) -> Void)? private var _isAccessibilityElement: Bool = false diff --git a/VDS/Components/Notification/Notification.swift b/VDS/Components/Notification/Notification.swift index 5f180b71..66be33c4 100644 --- a/VDS/Components/Notification/Notification.swift +++ b/VDS/Components/Notification/Notification.swift @@ -265,6 +265,12 @@ open class Notification: View { isAccessibilityElement = false accessibilityElements = [closeButton, typeIcon, titleLabel, subTitleLabel, buttonGroup] closeButton.accessibilityTraits = [.button] + closeButton.accessibilityLabel = "Close Notification" + + typeIcon.bridge_accessibilityLabelBlock = { [weak self] in + guard let self else { return "" } + return style.accessibleText + } } /// Resets to default settings. @@ -372,12 +378,6 @@ open class Notification: View { } } - open override func updateAccessibility() { - super.updateAccessibility() - closeButton.accessibilityLabel = "Close Notification" - typeIcon.accessibilityLabel = style.accessibleText - } - private func setConstraints() { labelViewAndButtonViewConstraint?.deactivate() labelViewBottomConstraint?.deactivate() diff --git a/VDS/Components/Pagination/Pagination.swift b/VDS/Components/Pagination/Pagination.swift index 13478d85..8988aaa4 100644 --- a/VDS/Components/Pagination/Pagination.swift +++ b/VDS/Components/Pagination/Pagination.swift @@ -86,6 +86,10 @@ open class Pagination: View { } } + private var paginationDescription: String { + "Page \(selectedPage) of \(total) selected" + } + //-------------------------------------------------- // MARK: - Overrides //-------------------------------------------------- @@ -148,14 +152,26 @@ open class Pagination: View { guard let self else { return } self.selectedPage = max(0, self.selectedPage - 1) } + + collectionContainerView.bridge_accessibilityLabelBlock = { [weak self] in + guard let self else { return "" } + return "Pagination containing \(total) pages" + } + + collectionContainerView.bridge_accessibilityValueBlock = { [weak self] in + guard let self else { return "" } + return paginationDescription + } } - ///Updating the accessiblity values i.e elements, label, value other items for the component. - open override func updateAccessibility() { - super.updateAccessibility() - accessibilityElements = [previousButton, collectionContainerView, nextButton] - collectionContainerView.accessibilityLabel = "Pagination containing \(total) pages" - collectionContainerView.accessibilityValue = "Page \(selectedPage) of \(total) selected" + open override var accessibilityElements: [Any]? { + get { + let views: [UIView] = [previousButton, collectionContainerView, nextButton] + return views.filter({ $0.isHidden == false }) + } + set { + + } } /// Used to make changes to the View based off a change events or from local properties. @@ -176,7 +192,7 @@ open class Pagination: View { updateSelection() DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in guard let self else { return } - UIAccessibility.post(notification: .announcement, argument: "Page \(self.selectedPage) of \(self.total) selected") + UIAccessibility.post(notification: .announcement, argument: paginationDescription) } } diff --git a/VDS/Components/RadioBox/RadioBoxGroup.swift b/VDS/Components/RadioBox/RadioBoxGroup.swift index 296ea8ed..c58802c0 100644 --- a/VDS/Components/RadioBox/RadioBoxGroup.swift +++ b/VDS/Components/RadioBox/RadioBoxGroup.swift @@ -42,8 +42,6 @@ open class RadioBoxGroup: SelectorGroupBase, SelectorGroupSingleSe if let selectorModels { items = selectorModels.enumerated().map { index, model in return RadioBoxItem().with { - $0.accessibilityLabel = model.accessibileText - $0.accessibilityValue = "item \(index+1) of \(selectorModels.count)" $0.text = model.text $0.textAttributes = model.textAttributes $0.subText = model.subText @@ -56,7 +54,7 @@ open class RadioBoxGroup: SelectorGroupBase, SelectorGroupSingleSe $0.isSelected = model.selected $0.strikethrough = model.strikethrough $0.strikethroughAccessibilityText = model.strikethroughAccessibileText - $0.accessibilityValueText = "item \(index+1) of \(selectorModels.count)" + $0.selectorView.bridge_accessibilityValueBlock = { "item \(index+1) of \(selectorModels.count)" } } } } @@ -111,7 +109,7 @@ extension RadioBoxGroup { /// Current Surface and this is used to pass down to child objects that implement Surfacable public var surface: Surface public var inputId: String? - public var value: AnyHashable? + public var value: String? public var accessibileText: String? public var text: String /// Array of LabelAttributeModel objects used in rendering the text. @@ -126,7 +124,7 @@ extension RadioBoxGroup { public var strikethrough: Bool = false public var strikethroughAccessibileText: String - public init(disabled: Bool, surface: Surface = .light, inputId: String? = nil, value: AnyHashable? = nil, + public init(disabled: Bool, surface: Surface = .light, inputId: String? = nil, value: String? = nil, text: String = "", textAttributes: [any LabelAttributeModel]? = nil, subText: String? = nil, subTextAttributes: [any LabelAttributeModel]? = nil, subTextRight: String? = nil, subTextRightAttributes: [any LabelAttributeModel]? = nil, diff --git a/VDS/Components/RadioBox/RadioBoxItem.swift b/VDS/Components/RadioBox/RadioBoxItem.swift index 71b05494..562e1e54 100644 --- a/VDS/Components/RadioBox/RadioBoxItem.swift +++ b/VDS/Components/RadioBox/RadioBoxItem.swift @@ -74,7 +74,7 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable { } /// Selector for this RadioBox. - open var selectorView = View() + open var selectorView = View().with { $0.accessibilityIdentifier = "RadioBox" } /// If provided, the RadioBox text will be rendered. open var text: String? { didSet { setNeedsUpdate() } } @@ -125,12 +125,10 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable { open var inputId: String? { didSet { setNeedsUpdate() } } - open var value: AnyHashable? { hiddenValue } + open var value: String? { hiddenValue } - open var hiddenValue: AnyHashable? { didSet { setNeedsUpdate() } } + open var hiddenValue: String? { didSet { setNeedsUpdate() } } - open var accessibilityValueText: String? - open override var accessibilityAction: ((Control) -> Void)? { didSet { selectorView.accessibilityAction = { [weak self] selectorItemBase in @@ -140,34 +138,6 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable { } } - open var accessibilityLabelText: String { - var accessibilityLabels = [String]() - - if isSelected { - accessibilityLabels.append("selected") - } - - accessibilityLabels.append("Radiobox") - - if let text, !text.isEmpty { - accessibilityLabels.append(text) - } - - if let text = subText, !text.isEmpty { - accessibilityLabels.append(text) - } - - if let text = subTextRight, !text.isEmpty { - accessibilityLabels.append(text) - } - - if !isEnabled { - accessibilityLabels.append("dimmed") - } - - return accessibilityLabels.joined(separator: ", ") - } - //-------------------------------------------------- // MARK: - Configuration Properties //-------------------------------------------------- @@ -201,15 +171,39 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable { control.toggle() } - if #available(iOS 17.0, *) { - accessibilityHintBlock = { [weak self] in - - return "foo" + selectorView.bridge_accessibilityLabelBlock = { [weak self] in + guard let self else { return "" } + var accessibilityLabels = [String]() + + if isSelected { + accessibilityLabels.append("selected") } - } else { - // Fallback on earlier versions + + accessibilityLabels.append("Radiobox") + + if let text, !text.isEmpty { + accessibilityLabels.append(text) + } + + if let text = subText, !text.isEmpty { + accessibilityLabels.append(text) + } + + if let text = subTextRight, !text.isEmpty { + accessibilityLabels.append(text) + } + + if strikethrough { + accessibilityLabels.append(strikethroughAccessibilityText) + } + + if !isEnabled { + accessibilityLabels.append("dimmed") + } + + return accessibilityLabels.joined(separator: ", ") } - + } /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations. @@ -287,22 +281,6 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable { setNeedsLayout() } - /// Used to update any Accessibility properties. - open override func updateAccessibility() { - super.updateAccessibility() - selectorView.accessibilityLabel = accessibilityLabelText - - if let accessibilityValueText { - selectorView.accessibilityValue = strikethrough - ? "\(strikethroughAccessibilityText), \(accessibilityValueText)" - : accessibilityValueText - } else { - selectorView.accessibilityValue = strikethrough - ? "\(strikethroughAccessibilityText)" - : accessibilityValueText - } - } - open override var accessibilityElements: [Any]? { get { var items = [Any]() diff --git a/VDS/Components/RadioButton/RadioButtonGroup.swift b/VDS/Components/RadioButton/RadioButtonGroup.swift index ca91f3e5..f44f5587 100644 --- a/VDS/Components/RadioButton/RadioButtonGroup.swift +++ b/VDS/Components/RadioButton/RadioButtonGroup.swift @@ -46,8 +46,6 @@ open class RadioButtonGroup: SelectorGroupBase, SelectorGroupSi $0.surface = model.surface $0.inputId = model.inputId $0.hiddenValue = model.value - $0.accessibilityLabel = model.accessibileText - $0.accessibilityValueText = "item \(index+1) of \(selectorModels.count)" $0.labelText = model.labelText $0.labelTextAttributes = model.labelTextAttributes $0.childText = model.childText @@ -55,6 +53,7 @@ open class RadioButtonGroup: SelectorGroupBase, SelectorGroupSi $0.isSelected = model.selected $0.errorText = model.errorText $0.showError = model.showError + $0.selectorView.bridge_accessibilityValueBlock = { "item \(index+1) of \(selectorModels.count)" } } } } diff --git a/VDS/Components/Tabs/Tab.swift b/VDS/Components/Tabs/Tab.swift index 3b22c8f5..4c0b55cd 100644 --- a/VDS/Components/Tabs/Tab.swift +++ b/VDS/Components/Tabs/Tab.swift @@ -88,8 +88,6 @@ extension Tabs { open var minWidth: CGFloat = 44.0 { didSet { setNeedsUpdate() } } open override var shouldHighlight: Bool { false } - - open var accessibilityValueText: String? //-------------------------------------------------- // MARK: - Configuration @@ -151,6 +149,11 @@ extension Tabs { labelTopConstraint = label.pinTop(anchor: layoutGuide.topAnchor) labelLeadingConstraint = label.pinLeading(anchor: layoutGuide.leadingAnchor) labelBottomConstraint = label.pinBottom(anchor: layoutGuide.bottomAnchor, priority: .defaultHigh) + + bridge_accessibilityLabelBlock = { [weak self] in + guard let self else { return "" } + return text + } } /// Used to make changes to the View based off a change events or from local properties. @@ -176,13 +179,6 @@ extension Tabs { setNeedsLayout() } - /// Used to update any Accessibility properties. - open override func updateAccessibility() { - super.updateAccessibility() - accessibilityLabel = text - accessibilityValue = accessibilityValueText - } - open override func layoutSubviews() { super.layoutSubviews() diff --git a/VDS/Components/Tabs/Tabs.swift b/VDS/Components/Tabs/Tabs.swift index 4c463900..88ae02e7 100644 --- a/VDS/Components/Tabs/Tabs.swift +++ b/VDS/Components/Tabs/Tabs.swift @@ -305,7 +305,10 @@ open class Tabs: View { tabItem.orientation = orientation tabItem.surface = surface tabItem.indicatorPosition = indicatorPosition - tabItem.accessibilityValueText = "\(index+1) of \(tabViews.count) Tabs" + tabItem.bridge_accessibilityValueBlock = { [weak self] in + guard let self else { return "" } + return "\(index+1) of \(tabViews.count) Tabs" + } } } diff --git a/VDS/Components/TextFields/EntryFieldBase.swift b/VDS/Components/TextFields/EntryFieldBase.swift index 4e7422ee..2ef89480 100644 --- a/VDS/Components/TextFields/EntryFieldBase.swift +++ b/VDS/Components/TextFields/EntryFieldBase.swift @@ -94,12 +94,9 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { /// This is the view that will be wrapped with the border for userInteraction. /// The only subview of this view is the fieldStackView - internal var containerView: UIView = { - return UIView().with { - $0.translatesAutoresizingMaskIntoConstraints = false - $0.isAccessibilityElement = true - } - }() + internal var containerView = View().with { + $0.isAccessibilityElement = true + } /// This is set by a local method. internal var bottomContainerView: UIView! @@ -244,27 +241,6 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { open var rules = [AnyRule]() - open var accessibilityLabelText: String { - var accessibilityLabels = [String]() - - if let text = titleLabel.text?.trimmingCharacters(in: .whitespaces) { - accessibilityLabels.append(text) - } - if isReadOnly { - accessibilityLabels.append("read only") - } - if !isEnabled { - accessibilityLabels.append("dimmed") - } - if let errorText, showError { - accessibilityLabels.append("error, \(errorText)") - } - - accessibilityLabels.append("\(Self.self)") - - return accessibilityLabels.joined(separator: ", ") - } - open var accessibilityHintText: String = "Double tap to open" //-------------------------------------------------- @@ -283,11 +259,11 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { .pinBottom() trailingEqualsConstraint = layoutGuide.pinTrailing(anchor: trailingAnchor) - + // width constraints trailingLessThanEqualsConstraint = layoutGuide.pinTrailingLessThanOrEqualTo(anchor: trailingAnchor)?.deactivate() widthConstraint = layoutGuide.widthAnchor.constraint(equalToConstant: 0).deactivate() - + // Add mainStackView to the view addSubview(mainStackView) @@ -301,7 +277,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { //InputContainer, Icons, Buttons containerView.addSubview(fieldStackView) fieldStackView.pinToSuperView(.uniform(VDSLayout.space3X)) - + let fieldContainerView = getFieldContainer() fieldContainerView.translatesAutoresizingMaskIntoConstraints = false @@ -309,11 +285,11 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { fieldStackView.addArrangedSubview(fieldContainerView) fieldStackView.addArrangedSubview(statusIcon) fieldStackView.setCustomSpacing(VDSLayout.space3X, after: fieldContainerView) - + //get the container this is what show helper text, error text //can include other for character count, max length bottomContainerView = getBottomContainer() - + //this is the vertical stack that contains error text, helper text bottomContainerStackView.addArrangedSubview(errorLabel) bottomContainerStackView.addArrangedSubview(helperLabel) @@ -321,11 +297,11 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { // Add arranged subviews to textFieldStackView contentStackView.addArrangedSubview(containerView) contentStackView.addArrangedSubview(bottomContainerView) - + // Add arranged subviews to mainStackView mainStackView.addArrangedSubview(titleLabel) mainStackView.addArrangedSubview(contentStackView) - + // Initial position of the helper label updateHelperTextPosition() @@ -333,6 +309,38 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { titleLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable() errorLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable() helperLabel.textColorConfiguration = secondaryColorConfiguration.eraseToAnyColorable() + + containerView.bridge_accessibilityLabelBlock = { [weak self] in + guard let self else { return "" } + var accessibilityLabels = [String]() + + if let text = titleLabel.text?.trimmingCharacters(in: .whitespaces) { + accessibilityLabels.append(text) + } + if isReadOnly { + accessibilityLabels.append("read only") + } + if !isEnabled { + accessibilityLabels.append("dimmed") + } + if let errorText, showError { + accessibilityLabels.append("error, \(errorText)") + } + + accessibilityLabels.append("\(Self.self)") + + return accessibilityLabels.joined(separator: ", ") + } + + containerView.bridge_accessibilityHintBlock = { [weak self] in + guard let self else { return "" } + return isReadOnly || !isEnabled ? "" : accessibilityHintText + } + + containerView.bridge_accessibilityValueBlock = { [weak self] in + guard let self else { return "" } + return value + } } /// Updates the UI @@ -472,13 +480,6 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { } } - open override func updateAccessibility() { - super.updateAccessibility() - containerView.accessibilityLabel = accessibilityLabelText - containerView.accessibilityHint = isReadOnly || !isEnabled ? "" : accessibilityHintText - containerView.accessibilityValue = value - } - open override var accessibilityElements: [Any]? { get { var elements = [Any]() diff --git a/VDS/Components/TextFields/InputField/TextField.swift b/VDS/Components/TextFields/InputField/TextField.swift index 81e749b2..0108c874 100644 --- a/VDS/Components/TextFields/InputField/TextField.swift +++ b/VDS/Components/TextFields/InputField/TextField.swift @@ -67,8 +67,6 @@ open class TextField: UITextField, ViewProtocol, Errorable { /// Will determine if a scaled font should be used for the titleLabel font. open var useScaledFont: Bool = false { didSet { setNeedsUpdate() } } - open var shouldUpdateAccessibility: Bool = true - open var surface: Surface = .light { didSet { setNeedsUpdate() } } open var showError: Bool = false { didSet { setNeedsUpdate() } } diff --git a/VDS/Components/TextFields/TextArea/TextView.swift b/VDS/Components/TextFields/TextArea/TextView.swift index bbb7b329..ed622f90 100644 --- a/VDS/Components/TextFields/TextArea/TextView.swift +++ b/VDS/Components/TextFields/TextArea/TextView.swift @@ -149,8 +149,6 @@ open class TextView: UITextView, ViewProtocol, Errorable { //-------------------------------------------------- // MARK: - Accessibility //-------------------------------------------------- - open var shouldUpdateAccessibility: Bool = true - open var accessibilityAction: ((TextView) -> Void)? private var _isAccessibilityElement: Bool = false diff --git a/VDS/Components/TileContainer/TileContainer.swift b/VDS/Components/TileContainer/TileContainer.swift index d600f5dd..546d8d05 100644 --- a/VDS/Components/TileContainer/TileContainer.swift +++ b/VDS/Components/TileContainer/TileContainer.swift @@ -266,6 +266,11 @@ open class TileContainerBase: Control where Padding backgroundImageView.layer.cornerRadius = cornerRadius highlightView.layer.cornerRadius = cornerRadius clipsToBounds = true + + containerView.bridge_isAccessibilityElementBlock = { [weak self] in self?.onClickSubscriber != nil } + containerView.accessibilityHint = "Double tap to open." + containerView.accessibilityLabel = nil + } /// Overriden to take the hit if there is an onClickSubscriber and the view is not a UIControl @@ -338,13 +343,6 @@ open class TileContainerBase: Control where Padding } } - open override func updateAccessibility() { - super.updateAccessibility() - containerView.isAccessibilityElement = onClickSubscriber != nil - containerView.accessibilityHint = "Double tap to open." - containerView.accessibilityLabel = nil - } - open override var accessibilityElements: [Any]? { get { var items = [Any]() diff --git a/VDS/Components/TitleLockup/TitleLockup.swift b/VDS/Components/TitleLockup/TitleLockup.swift index af699b5c..bc5c4c3a 100644 --- a/VDS/Components/TitleLockup/TitleLockup.swift +++ b/VDS/Components/TitleLockup/TitleLockup.swift @@ -262,20 +262,21 @@ open class TitleLockup: View { //-------------------------------------------------- // MARK: - Overrides //-------------------------------------------------- - open override func updateAccessibility() { - super.updateAccessibility() - var elements = [Any]() - if eyebrowModel != nil { - elements.append(eyebrowLabel) + open override var accessibilityElements: [Any]? { + get { + var elements = [Any]() + if eyebrowModel != nil { + elements.append(eyebrowLabel) + } + if titleModel != nil { + elements.append(titleLabel) + } + if subTitleModel != nil { + elements.append(subTitleLabel) + } + return elements.count > 0 ? elements : nil } - if titleModel != nil { - elements.append(titleLabel) - } - if subTitleModel != nil { - elements.append(subTitleLabel) - } - setAccessibilityLabel(for: elements.compactMap({$0 as? UIView})) - accessibilityElements = elements.count > 0 ? elements : nil + set {} } /// Resets to default settings. diff --git a/VDS/Components/Toggle/Toggle.swift b/VDS/Components/Toggle/Toggle.swift index 6518e8db..aaa411de 100644 --- a/VDS/Components/Toggle/Toggle.swift +++ b/VDS/Components/Toggle/Toggle.swift @@ -207,6 +207,14 @@ open class Toggle: Control, Changeable, FormFieldable { label.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor) ] + bridge_accessibilityValueBlock = { [weak self] in + guard let self else { return "" } + if showText { + return isSelected ? onText : offText + } else { + return isSelected ? "On" : "Off" + } + } } /// Resets to default settings. @@ -238,16 +246,6 @@ open class Toggle: Control, Changeable, FormFieldable { toggleView.isEnabled = isEnabled toggleView.isOn = isOn } - - /// Used to update any Accessibility properties. - open override func updateAccessibility() { - super.updateAccessibility() - if showText { - accessibilityValue = isSelected ? onText : offText - } else { - accessibilityValue = isSelected ? "On" : "Off" - } - } /// This will change the state of the Selector and execute the actionBlock if provided. open func toggle() { diff --git a/VDS/Components/Toggle/ToggleView.swift b/VDS/Components/Toggle/ToggleView.swift index 4889ed40..935ed519 100644 --- a/VDS/Components/Toggle/ToggleView.swift +++ b/VDS/Components/Toggle/ToggleView.swift @@ -153,7 +153,7 @@ open class ToggleView: Control, Changeable, FormFieldable { // Update shadow layers frames to match the view's bounds knobView.layer.insertSublayer(shadowLayer1, at: 0) knobView.layer.insertSublayer(shadowLayer2, at: 0) - + accessibilityLabel = "Toggle" } /// Resets to default settings. @@ -176,13 +176,6 @@ open class ToggleView: Control, Changeable, FormFieldable { updateToggle() } - - /// Used to update any Accessibility properties. - open override func updateAccessibility() { - super.updateAccessibility() - - accessibilityLabel = "Toggle" - } /// This will change the state of the Selector and execute the actionBlock if provided. open func toggle() { diff --git a/VDS/Components/Tooltip/Tooltip.swift b/VDS/Components/Tooltip/Tooltip.swift index 0875ee64..f07fb1be 100644 --- a/VDS/Components/Tooltip/Tooltip.swift +++ b/VDS/Components/Tooltip/Tooltip.swift @@ -138,6 +138,24 @@ open class Tooltip: Control, TooltipLaunchable { contentView: tooltip.contentView), presenter: self) } + + bridge_accessibilityLabelBlock = { [weak self] in + guard let self else { return "" } + var label = title + if label == nil { + label = content + } + if let label, !label.isEmpty { + return label + } else { + return "Modal" + } + } + + bridge_accessibilityHintBlock = { [weak self] in + guard let self else { return "" } + return isEnabled ? "Double tap to open." : "" + } } /// Resets to default settings. @@ -163,23 +181,7 @@ open class Tooltip: Control, TooltipLaunchable { //get the color for the image icon.color = iconColorConfiguration.getColor(self) } - - /// Used to update any Accessibility properties. - open override func updateAccessibility() { - super.updateAccessibility() - var label = title - if label == nil { - label = content - } - if let label, !label.isEmpty { - accessibilityLabel = label - } else { - accessibilityLabel = "Modal" - } - accessibilityHint = isEnabled ? "Double tap to open." : "" - } - public static func accessibleText(for title: String?, content: String?, closeButtonText: String) -> String { var label = "" if let title { diff --git a/VDS/Components/Tooltip/TooltipDialog.swift b/VDS/Components/Tooltip/TooltipDialog.swift index 1a6e192d..0650a808 100644 --- a/VDS/Components/Tooltip/TooltipDialog.swift +++ b/VDS/Components/Tooltip/TooltipDialog.swift @@ -219,15 +219,19 @@ open class TooltipDialog: View, UIScrollViewDelegate { /// Used to update any Accessibility properties. open override func updateAccessibility() { super.updateAccessibility() - primaryAccessibilityElement.accessibilityHint = "Double tap on the \(tooltipModel.closeButtonText) button to close." primaryAccessibilityElement.accessibilityFrameInContainerSpace = .init(origin: .zero, size: frame.size) + } + + open override var accessibilityElements: [Any]? { + get { + var elements: [Any] = [primaryAccessibilityElement] + contentStackView.arrangedSubviews.forEach{ elements.append($0) } + elements.append(closeButton) - var elements: [Any] = [primaryAccessibilityElement] - contentStackView.arrangedSubviews.forEach{ elements.append($0) } - elements.append(closeButton) - - accessibilityElements = elements + return elements + } + set {} } } diff --git a/VDS/Protocols/AccessibilityUpdatable.swift b/VDS/Protocols/AccessibilityUpdatable.swift index 07bd429b..de46f867 100644 --- a/VDS/Protocols/AccessibilityUpdatable.swift +++ b/VDS/Protocols/AccessibilityUpdatable.swift @@ -6,3 +6,90 @@ // import Foundation +import UIKit + +public protocol AccessibilityUpdatable { + var bridge_isAccessibilityElementBlock: AXBoolReturnBlock? { get set } + + var bridge_accessibilityLabelBlock: AXStringReturnBlock? { get set } + + var bridge_accessibilityValueBlock: AXStringReturnBlock? { get set } + + var bridge_accessibilityHintBlock: AXStringReturnBlock? { get set } + + var bridge_accessibilityActivateBlock: AXBoolReturnBlock? { get set } + +} + +private struct AccessibilityBridge { + static var isAccessibilityElementBlockKey: UInt8 = 0 + static var activateBlockKey: UInt8 = 1 + static var valueBlockKey: UInt8 = 2 + static var hintBlockKey: UInt8 = 3 + static var labelBlockKey: UInt8 = 4 +} + +extension AccessibilityUpdatable where Self: NSObject { + + public var bridge_isAccessibilityElementBlock: AXBoolReturnBlock? { + get { + return objc_getAssociatedObject(self, &AccessibilityBridge.isAccessibilityElementBlockKey) as? AXBoolReturnBlock + } + set { + objc_setAssociatedObject(self, &AccessibilityBridge.isAccessibilityElementBlockKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) +// if #available(iOS 17, *) { +// self.isAccessibilityElementBlock = newValue +// } + } + } + + public var bridge_accessibilityActivateBlock: AXBoolReturnBlock? { + get { + return objc_getAssociatedObject(self, &AccessibilityBridge.activateBlockKey) as? AXBoolReturnBlock + } + set { + objc_setAssociatedObject(self, &AccessibilityBridge.activateBlockKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) +// if #available(iOS 17, *) { +// self.accessibilityActivateBlock = newValue +// } + } + } + + public var bridge_accessibilityValueBlock: AXStringReturnBlock? { + get { + return objc_getAssociatedObject(self, &AccessibilityBridge.valueBlockKey) as? AXStringReturnBlock + } + set { + objc_setAssociatedObject(self, &AccessibilityBridge.valueBlockKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) +// if #available(iOS 17, *) { +// self.accessibilityValueBlock = newValue +// } + } + } + + public var bridge_accessibilityHintBlock: AXStringReturnBlock? { + get { + return objc_getAssociatedObject(self, &AccessibilityBridge.hintBlockKey) as? AXStringReturnBlock + } + set { + objc_setAssociatedObject(self, &AccessibilityBridge.hintBlockKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) +// if #available(iOS 17, *) { +// self.accessibilityHintBlock = newValue +// } + } + } + + public var bridge_accessibilityLabelBlock: AXStringReturnBlock? { + get { + return objc_getAssociatedObject(self, &AccessibilityBridge.labelBlockKey) as? AXStringReturnBlock + } + set { + objc_setAssociatedObject(self, &AccessibilityBridge.labelBlockKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) +// if #available(iOS 17, *) { +// self.accessibilityLabelBlock = newValue +// } + } + } + +} + diff --git a/VDS/Protocols/Groupable.swift b/VDS/Protocols/Groupable.swift index 773362b7..b2f95782 100644 --- a/VDS/Protocols/Groupable.swift +++ b/VDS/Protocols/Groupable.swift @@ -9,6 +9,4 @@ import Foundation public protocol Groupable: Control { - /// Property used to add context to the Grouping of a set. - var accessibilityValueText: String? { get set } } diff --git a/VDS/Protocols/ViewProtocol.swift b/VDS/Protocols/ViewProtocol.swift index 8018e260..c7cba091 100644 --- a/VDS/Protocols/ViewProtocol.swift +++ b/VDS/Protocols/ViewProtocol.swift @@ -9,16 +9,13 @@ import Foundation import UIKit import Combine -public protocol ViewProtocol: AnyObject, Initable, Resettable, Enabling, Surfaceable { +public protocol ViewProtocol: AnyObject, Initable, Resettable, Enabling, Surfaceable, AccessibilityUpdatable { /// Set of Subscribers for any Publishers for this Control. var subscribers: Set { get set } /// Key of whether or not updateView() is called in setNeedsUpdate() var shouldUpdateView: Bool { get set } - /// Key of whether or not updateAccessibility() is called in setNeedsUpdate() - var shouldUpdateAccessibility: Bool { get set } - /// Used for setting an implementation for the default Accessible Action var accessibilityAction: ((Self) -> Void)? { get set } @@ -42,9 +39,7 @@ extension ViewProtocol { if shouldUpdateView { shouldUpdateView = false updateView() - if shouldUpdateAccessibility { - updateAccessibility() - } + updateAccessibility() shouldUpdateView = true } } @@ -70,86 +65,3 @@ extension ViewProtocol where Self: UIControl { }).store(in: &subscribers) } } - -public protocol AccessibilityUpdatable { - // Basic accessibility - var bridge_isAccessibilityElementBlock: AXBoolReturnBlock? { get set } - - var bridge_accessibilityLabelBlock: AXStringReturnBlock? { get set } - - var bridge_accessibilityValueBlock: AXStringReturnBlock? { get set } - - var bridge_accessibilityHintBlock: AXStringReturnBlock? { get set } - - var bridge_accessibilityActivateBlock: AXBoolReturnBlock? { get set } - -} - -extension NSObject: AccessibilityUpdatable { - static var isAccessibilityElementBlockKey: UInt8 = 0 - static var activateBlockKey: UInt8 = 1 - static var valueBlockKey: UInt8 = 2 - static var hintBlockKey: UInt8 = 3 - static var labelBlockKey: UInt8 = 4 - - public var bridge_isAccessibilityElementBlock: AXBoolReturnBlock? { - get { - return objc_getAssociatedObject(self, &NSObject.isAccessibilityElementBlockKey) as? AXBoolReturnBlock - } - set { - objc_setAssociatedObject(self, &NSObject.isAccessibilityElementBlockKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) -// if #available(iOS 17, *) { -// self.isAccessibilityElementBlock = newValue -// } - } - } - - public var bridge_accessibilityActivateBlock: AXBoolReturnBlock? { - get { - return objc_getAssociatedObject(self, &NSObject.activateBlockKey) as? AXBoolReturnBlock - } - set { - objc_setAssociatedObject(self, &NSObject.activateBlockKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) -// if #available(iOS 17, *) { -// self.accessibilityActivateBlock = newValue -// } - } - } - - public var bridge_accessibilityValueBlock: AXStringReturnBlock? { - get { - return objc_getAssociatedObject(self, &NSObject.valueBlockKey) as? AXStringReturnBlock - } - set { - objc_setAssociatedObject(self, &NSObject.valueBlockKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) -// if #available(iOS 17, *) { -// self.accessibilityValueBlock = newValue -// } - } - } - - public var bridge_accessibilityHintBlock: AXStringReturnBlock? { - get { - return objc_getAssociatedObject(self, &NSObject.hintBlockKey) as? AXStringReturnBlock - } - set { - objc_setAssociatedObject(self, &NSObject.hintBlockKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) -// if #available(iOS 17, *) { -// self.accessibilityHintBlock = newValue -// } - } - } - - public var bridge_accessibilityLabelBlock: AXStringReturnBlock? { - get { - return objc_getAssociatedObject(self, &NSObject.labelBlockKey) as? AXStringReturnBlock - } - set { - objc_setAssociatedObject(self, &NSObject.labelBlockKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) -// if #available(iOS 17, *) { -// self.accessibilityLabelBlock = newValue -// } - } - } - -}