diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index c17e971e..ff8a6735 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -172,6 +172,8 @@ 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 */; }; + 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 */; }; @@ -398,6 +400,8 @@ 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 = ""; }; + 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 = ""; }; @@ -711,6 +715,7 @@ EA3361AB288B25EC0071C351 /* Protocols */ = { isa = PBXGroup; children = ( + EAF2F4772C249D72007BFEDC /* AccessibilityUpdatable.swift */, EA4DB2FC28D3D0CA00103EE3 /* AnyEquatable.swift */, EA297A5629FB0A360031ED56 /* AppleGuidelinesTouchable.swift */, EAF1FE9A29DB1A6000101452 /* Changeable.swift */, @@ -743,6 +748,7 @@ EA985C1C296CD13600F2FF2E /* BundleManager.swift */, EAF7F0B8289C139800B287F5 /* ColorConfiguration.swift */, EAB5FEF02927F4AA00998C17 /* SelfSizingCollectionView.swift */, + EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */, ); path = Classes; sourceTree = ""; @@ -1206,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 */, @@ -1244,6 +1251,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/BaseClasses/Control.swift b/VDS/BaseClasses/Control.swift index 274d7d9b..2a9fe769 100644 --- a/VDS/BaseClasses/Control.swift +++ b/VDS/BaseClasses/Control.swift @@ -46,7 +46,7 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable { // MARK: - Public Properties //-------------------------------------------------- open var shouldUpdateView: Bool = true - + open var userInfo = [String: Primitive]() open var surface: Surface = .light { didSet { setNeedsUpdate() } } @@ -119,17 +119,136 @@ 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. - override open func accessibilityActivate() -> Bool { - // Hold state in case User wanted isAnimated to remain off. - guard isUserInteractionEnabled else { return false } - sendActions(for: .touchUpInside) - return true - } - open override func layoutSubviews() { super.layoutSubviews() setNeedsUpdate() } + + //-------------------------------------------------- + // MARK: - Accessibility + //-------------------------------------------------- + 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 4ea3ebd3..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,12 +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" : "")" - } /// This will change the state of the Selector and execute the actionBlock if provided. open func toggle() { } @@ -133,4 +137,36 @@ open class SelectorBase: Control, SelectorControlable { super.reset() onChange = nil } + + open override func accessibilityActivate() -> Bool { + guard isEnabled, isUserInteractionEnabled else { return false } + 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/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 fd3f6f2b..44ed01b1 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 @@ -145,7 +145,14 @@ open class SelectorItemBase: Control, Errorable, open var hiddenValue: AnyHashable? { didSet { setNeedsUpdate() } } - open var accessibilityValueText: String? + open override var accessibilityAction: ((Control) -> Void)? { + didSet { + selectorView.accessibilityAction = { [weak self] selectorItemBase in + guard let self else { return } + accessibilityAction?(self) + } + } + } //-------------------------------------------------- // MARK: - Overrides @@ -153,21 +160,61 @@ open class SelectorItemBase: Control, Errorable, /// Executed on initialization for this View. open override func initialSetup() { super.initialSetup() - onClick = { control in - control.toggle() + onClick = { [weak self] control in + guard let self, isEnabled else { return } + toggle() + } + + selectorView.accessibilityAction = { [weak self] _ in + 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 = false - isAccessibilityElement = true - accessibilityTraits = .button + selectorView.isAccessibilityElement = true + isAccessibilityElement = false addSubview(mainStackView) - mainStackView.isUserInteractionEnabled = false + mainStackView.isUserInteractionEnabled = false mainStackView.addArrangedSubview(selectorStackView) mainStackView.addArrangedSubview(errorLabel) selectorStackView.addArrangedSubview(selectorView) @@ -191,14 +238,47 @@ open class SelectorItemBase: Control, Errorable, selectorView.isEnabled = isEnabled selectorView.surface = surface } - - /// Used to update any Accessibility properties. - open override func updateAccessibility() { - super.updateAccessibility() - setAccessibilityLabel(for: [selectorView, label, childLabel, errorLabel]) - 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 + } } - + + /// 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) + } + } + /// Resets to default settings. open override func reset() { super.reset() @@ -290,4 +370,34 @@ open class SelectorItemBase: Control, Errorable, /// This will change to state of the Selector. open func toggle() {} + 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 { +// 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 a807c25c..c7df1765 100644 --- a/VDS/BaseClasses/View.swift +++ b/VDS/BaseClasses/View.swift @@ -52,7 +52,7 @@ open class View: UIView, ViewProtocol, UserInfoable { open var surface: Surface = .light { didSet { setNeedsUpdate() } } open var isEnabled: Bool = true { didSet { setNeedsUpdate() } } - + //-------------------------------------------------- // MARK: - Lifecycle //-------------------------------------------------- @@ -91,4 +91,136 @@ open class View: UIView, ViewProtocol, UserInfoable { setNeedsUpdate() } + //-------------------------------------------------- + // MARK: - Accessibility + //-------------------------------------------------- + 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 super.accessibilityActivate() + + } +// } + } + } diff --git a/VDS/Classes/AccessibilityActionElement.swift b/VDS/Classes/AccessibilityActionElement.swift new file mode 100644 index 00000000..7717839c --- /dev/null +++ b/VDS/Classes/AccessibilityActionElement.swift @@ -0,0 +1,21 @@ +// +// 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 { + public var accessibilityAction: AXVoidReturnBlock? + + public override func accessibilityActivate() -> Bool { + 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 b3b5f38d..d7b80c8a 100644 --- a/VDS/Components/Buttons/ButtonBase.swift +++ b/VDS/Components/Buttons/ButtonBase.swift @@ -50,7 +50,7 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable { //-------------------------------------------------- /// Key of whether or not updateView() is called in setNeedsUpdate() open var shouldUpdateView: Bool = true - + open var surface: Surface = .light { didSet { setNeedsUpdate() } } /// Text that will be used in the titleLabel. @@ -75,7 +75,7 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable { /// Whether the Button should handle the isHighlighted state. open var shouldHighlight: Bool { isHighlighting == false } - + /// Whether the Control is highlighted or not. open override var isHighlighted: Bool { didSet { @@ -139,7 +139,7 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable { shouldUpdateView = true setNeedsUpdate() } - + //-------------------------------------------------- // MARK: - Private Methods //-------------------------------------------------- @@ -172,6 +172,133 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable { } } + //-------------------------------------------------- + // MARK: - Accessibility + //-------------------------------------------------- + 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/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/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/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/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/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/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 4f56c272..15ed4b45 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 } } @@ -215,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 @@ -242,7 +245,6 @@ open class Label: UILabel, ViewProtocol, UserInfoable { } open func updateAccessibility() { - accessibilityLabel = text if isEnabled { accessibilityTraits.remove(.notEnabled) } else { @@ -263,24 +265,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,15 +358,27 @@ 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) + + // set the action of the accessibilityElement + customAccessibilityAction?.accessibilityAction = { [weak self] in + guard let self, isEnabled else { return } + 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 } } if let accessibilityElements, !accessibilityElements.isEmpty { - let staticText = UIAccessibilityElement(accessibilityContainer: self) + let staticText = AccessibilityActionElement(accessibilityContainer: self) staticText.accessibilityLabel = text staticText.accessibilityFrameInContainerSpace = bounds @@ -395,14 +392,40 @@ 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) - private func customAccessibilityAction(text: String?, range: NSRange, accessibleText: String? = nil) -> UIAccessibilityCustomAction? { + 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? { guard let text = text, let attributedText else { return nil } @@ -423,31 +446,147 @@ 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 + return element } - @objc private func accessibilityCustomAction(_ action: UIAccessibilityCustomAction) { - - for actionable in actions { - if action.hash == actionable.accessibilityId { - actionable.performAction() - return + + //-------------------------------------------------- + // MARK: - Accessibility + //-------------------------------------------------- + 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 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/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/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/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 829d1a7d..562e1e54 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().with { $0.accessibilityIdentifier = "RadioBox" } /// If provided, the RadioBox text will be rendered. open var text: String? { didSet { setNeedsUpdate() } } @@ -127,12 +125,19 @@ 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 + guard let self else { return } + accessibilityAction?(self) + } + } + } + //-------------------------------------------------- // MARK: - Configuration Properties //-------------------------------------------------- @@ -165,16 +170,51 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable { onClick = { control in control.toggle() } + + selectorView.bridge_accessibilityLabelBlock = { [weak self] in + guard let self else { return "" } + 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 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. open override func setup() { super.setup() - isAccessibilityElement = true - accessibilityTraits = .button + isAccessibilityElement = false + selectorView.isAccessibilityElement = true + selectorView.accessibilityTraits = .button addSubview(selectorView) - selectorView.isUserInteractionEnabled = false + selectorView.isUserInteractionEnabled = true selectorView.addSubview(selectorStackView) @@ -226,6 +266,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) @@ -239,26 +281,51 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable { setNeedsLayout() } - /// 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" + open override var accessibilityElements: [Any]? { + get { + var items = [Any]() + items.append(selectorView) + + 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) + } + return items } - if let accessibilityValueText { - accessibilityValue = strikethrough - ? "\(strikethroughAccessibilityText), \(accessibilityValueText)" - : accessibilityValueText + 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 { - accessibilityValue = strikethrough - ? "\(strikethroughAccessibilityText)" - : accessibilityValueText + guard !UIAccessibility.isVoiceOverRunning else { return nil } + return super.hitTest(point, with: event) } } + open func getSelectorView() -> UIView { + selectorView + } + //-------------------------------------------------- // 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/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/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 { 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 e3d1d312..0108c874 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 surface: Surface = .light { didSet { setNeedsUpdate() } } @@ -74,7 +74,7 @@ open class TextField: UITextField, ViewProtocol, Errorable { open var errorText: String? { didSet { setNeedsUpdate() } } open var lineBreakMode: NSLineBreakMode = .byClipping { didSet { setNeedsUpdate() } } - + open override var isEnabled: Bool { didSet { setNeedsUpdate() } } open var textColorConfiguration: AnyColorable = ViewColorConfiguration().with { @@ -229,7 +229,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 a0edb28c..ed622f90 100644 --- a/VDS/Components/TextFields/TextArea/TextView.swift +++ b/VDS/Components/TextFields/TextArea/TextView.swift @@ -144,6 +144,139 @@ open class TextView: UITextView, ViewProtocol, Errorable { shouldUpdateView = true setNeedsUpdate() } + + + //-------------------------------------------------- + // MARK: - Accessibility + //-------------------------------------------------- + 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 #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() + + } +// } + } //-------------------------------------------------- // MARK: - Private Methods 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/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() } } diff --git a/VDS/Protocols/AccessibilityUpdatable.swift b/VDS/Protocols/AccessibilityUpdatable.swift new file mode 100644 index 00000000..de46f867 --- /dev/null +++ b/VDS/Protocols/AccessibilityUpdatable.swift @@ -0,0 +1,95 @@ +// +// AccessibilityUpdatable.swift +// VDS +// +// Created by Matt Bruce on 6/20/24. +// + +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 da730372..c7cba091 100644 --- a/VDS/Protocols/ViewProtocol.swift +++ b/VDS/Protocols/ViewProtocol.swift @@ -9,13 +9,16 @@ 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 } + /// Used for setting an implementation for the default Accessible Action + var accessibilityAction: ((Self) -> Void)? { get set } + /// Executed on initialization for this View. func initialSetup() @@ -30,6 +33,7 @@ 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 { @@ -49,7 +53,7 @@ extension ViewProtocol where Self: UIView { view.removeFromSuperview() setNeedsDisplay() } - } + } } extension ViewProtocol where Self: UIControl { 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