Merge branch 'refactor/accessibilityBlocks' into 'develop'

added shouldUpdateAccessibility

See merge request BPHV_MIPS/vds_ios!261
This commit is contained in:
Bruce, Matt R 2024-06-20 19:14:05 +00:00
commit eeae91e616
44 changed files with 1475 additions and 320 deletions

View File

@ -172,6 +172,8 @@
EAF193432C134F3800C68D18 /* TableCellItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 443DBAF92BDA303F0021497E /* TableCellItem.swift */; }; EAF193432C134F3800C68D18 /* TableCellItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 443DBAF92BDA303F0021497E /* TableCellItem.swift */; };
EAF1FE9929D4850E00101452 /* Clickable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF1FE9829D4850E00101452 /* Clickable.swift */; }; EAF1FE9929D4850E00101452 /* Clickable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF1FE9829D4850E00101452 /* Clickable.swift */; };
EAF1FE9B29DB1A6000101452 /* Changeable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF1FE9A29DB1A6000101452 /* Changeable.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 */; }; EAF7F0952899861000B287F5 /* CheckboxItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0932899861000B287F5 /* CheckboxItem.swift */; };
EAF7F09A2899B17200B287F5 /* CATransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0992899B17200B287F5 /* CATransaction.swift */; }; EAF7F09A2899B17200B287F5 /* CATransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0992899B17200B287F5 /* CATransaction.swift */; };
EAF7F0A0289AB7EC00B287F5 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F09F289AB7EC00B287F5 /* View.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 = "<group>"; }; EAEEECAE2B1FC2BA00531FC2 /* ToggleViewChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ToggleViewChangeLog.txt; sourceTree = "<group>"; };
EAF1FE9829D4850E00101452 /* Clickable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Clickable.swift; sourceTree = "<group>"; }; EAF1FE9829D4850E00101452 /* Clickable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Clickable.swift; sourceTree = "<group>"; };
EAF1FE9A29DB1A6000101452 /* Changeable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Changeable.swift; sourceTree = "<group>"; }; EAF1FE9A29DB1A6000101452 /* Changeable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Changeable.swift; sourceTree = "<group>"; };
EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityActionElement.swift; sourceTree = "<group>"; };
EAF2F4772C249D72007BFEDC /* AccessibilityUpdatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityUpdatable.swift; sourceTree = "<group>"; };
EAF7F0932899861000B287F5 /* CheckboxItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckboxItem.swift; sourceTree = "<group>"; }; EAF7F0932899861000B287F5 /* CheckboxItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckboxItem.swift; sourceTree = "<group>"; };
EAF7F0992899B17200B287F5 /* CATransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CATransaction.swift; sourceTree = "<group>"; }; EAF7F0992899B17200B287F5 /* CATransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CATransaction.swift; sourceTree = "<group>"; };
EAF7F09F289AB7EC00B287F5 /* View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = "<group>"; }; EAF7F09F289AB7EC00B287F5 /* View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = "<group>"; };
@ -711,6 +715,7 @@
EA3361AB288B25EC0071C351 /* Protocols */ = { EA3361AB288B25EC0071C351 /* Protocols */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
EAF2F4772C249D72007BFEDC /* AccessibilityUpdatable.swift */,
EA4DB2FC28D3D0CA00103EE3 /* AnyEquatable.swift */, EA4DB2FC28D3D0CA00103EE3 /* AnyEquatable.swift */,
EA297A5629FB0A360031ED56 /* AppleGuidelinesTouchable.swift */, EA297A5629FB0A360031ED56 /* AppleGuidelinesTouchable.swift */,
EAF1FE9A29DB1A6000101452 /* Changeable.swift */, EAF1FE9A29DB1A6000101452 /* Changeable.swift */,
@ -743,6 +748,7 @@
EA985C1C296CD13600F2FF2E /* BundleManager.swift */, EA985C1C296CD13600F2FF2E /* BundleManager.swift */,
EAF7F0B8289C139800B287F5 /* ColorConfiguration.swift */, EAF7F0B8289C139800B287F5 /* ColorConfiguration.swift */,
EAB5FEF02927F4AA00998C17 /* SelfSizingCollectionView.swift */, EAB5FEF02927F4AA00998C17 /* SelfSizingCollectionView.swift */,
EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */,
); );
path = Classes; path = Classes;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1206,6 +1212,7 @@
EAF7F0A6289B0CE000B287F5 /* Resetable.swift in Sources */, EAF7F0A6289B0CE000B287F5 /* Resetable.swift in Sources */,
EA985C2D296F03FE00F2FF2E /* TileletIconModels.swift in Sources */, EA985C2D296F03FE00F2FF2E /* TileletIconModels.swift in Sources */,
EA89200428AECF4B006B9984 /* UITextField+Publisher.swift in Sources */, EA89200428AECF4B006B9984 /* UITextField+Publisher.swift in Sources */,
EAF2F4782C249D72007BFEDC /* AccessibilityUpdatable.swift in Sources */,
18A65A022B96E848006602CC /* Breadcrumbs.swift in Sources */, 18A65A022B96E848006602CC /* Breadcrumbs.swift in Sources */,
1842B1E12BECE7B70021AFCA /* CalendarHeaderReusableView.swift in Sources */, 1842B1E12BECE7B70021AFCA /* CalendarHeaderReusableView.swift in Sources */,
EA78C7962C00CAC200430AD1 /* Groupable.swift in Sources */, EA78C7962C00CAC200430AD1 /* Groupable.swift in Sources */,
@ -1244,6 +1251,7 @@
EA0D1C412A6AD61C00E5C127 /* Typography+Additional.swift in Sources */, EA0D1C412A6AD61C00E5C127 /* Typography+Additional.swift in Sources */,
EAC925842911C63100091998 /* Colorable.swift in Sources */, EAC925842911C63100091998 /* Colorable.swift in Sources */,
18B463A42BBD3C46005C4528 /* DropdownOptionModel.swift in Sources */, 18B463A42BBD3C46005C4528 /* DropdownOptionModel.swift in Sources */,
EAF2F4762C231EAA007BFEDC /* AccessibilityActionElement.swift in Sources */,
EAC58BFD2BE935C300BA39FA /* TitleLockupTextColor.swift in Sources */, EAC58BFD2BE935C300BA39FA /* TitleLockupTextColor.swift in Sources */,
EAACB89A2B927108006A3869 /* Valuing.swift in Sources */, EAACB89A2B927108006A3869 /* Valuing.swift in Sources */,
EAE785312BA0A438009428EA /* UIImage+Helper.swift in Sources */, EAE785312BA0A438009428EA /* UIImage+Helper.swift in Sources */,

View File

@ -46,7 +46,7 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable {
// MARK: - Public Properties // MARK: - Public Properties
//-------------------------------------------------- //--------------------------------------------------
open var shouldUpdateView: Bool = true open var shouldUpdateView: Bool = true
open var userInfo = [String: Primitive]() open var userInfo = [String: Primitive]()
open var surface: Surface = .light { didSet { setNeedsUpdate() } } open var surface: Surface = .light { didSet { setNeedsUpdate() } }
@ -119,17 +119,136 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Overrides // 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() { open override func layoutSubviews() {
super.layoutSubviews() super.layoutSubviews()
setNeedsUpdate() 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
}
} }

View File

@ -104,6 +104,16 @@ open class SelectorBase: Control, SelectorControlable {
onClick = { control in onClick = { control in
control.toggle() 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. /// 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() setNeedsLayout()
layoutIfNeeded() 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. /// This will change the state of the Selector and execute the actionBlock if provided.
open func toggle() { } open func toggle() { }
@ -133,4 +137,36 @@ open class SelectorBase: Control, SelectorControlable {
super.reset() super.reset()
onChange = nil 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
}
} }

View File

@ -70,6 +70,13 @@ open class SelectorGroupBase<SelectorItemType: Groupable>: Control, SelectorGrou
self?.didSelect(handler) self?.didSelect(handler)
self?.setNeedsUpdate() self?.setNeedsUpdate()
} }
selector.accessibilityAction = { [weak self] handler in
guard let handler = handler as? SelectorItemType else { return }
self?.didSelect(handler)
self?.setNeedsUpdate()
}
mainStackView.addArrangedSubview(selector) mainStackView.addArrangedSubview(selector)
} }
} }

View File

@ -11,7 +11,7 @@ import Combine
import VDSCoreTokens import VDSCoreTokens
/// Base Class used to build out a SelectorControlable control. /// Base Class used to build out a SelectorControlable control.
open class SelectorItemBase<Selector: SelectorControlable>: Control, Errorable, Changeable, Groupable { open class SelectorItemBase<Selector: SelectorBase>: Control, Errorable, Changeable, Groupable {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Initializers // MARK: - Initializers
@ -145,7 +145,14 @@ open class SelectorItemBase<Selector: SelectorControlable>: Control, Errorable,
open var hiddenValue: AnyHashable? { didSet { setNeedsUpdate() } } 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 // MARK: - Overrides
@ -153,21 +160,61 @@ open class SelectorItemBase<Selector: SelectorControlable>: Control, Errorable,
/// Executed on initialization for this View. /// Executed on initialization for this View.
open override func initialSetup() { open override func initialSetup() {
super.initialSetup() super.initialSetup()
onClick = { control in onClick = { [weak self] control in
control.toggle() 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. /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
open override func setup() { open override func setup() {
super.setup() super.setup()
selectorView.isAccessibilityElement = false selectorView.isAccessibilityElement = true
isAccessibilityElement = true isAccessibilityElement = false
accessibilityTraits = .button
addSubview(mainStackView) addSubview(mainStackView)
mainStackView.isUserInteractionEnabled = false
mainStackView.isUserInteractionEnabled = false
mainStackView.addArrangedSubview(selectorStackView) mainStackView.addArrangedSubview(selectorStackView)
mainStackView.addArrangedSubview(errorLabel) mainStackView.addArrangedSubview(errorLabel)
selectorStackView.addArrangedSubview(selectorView) selectorStackView.addArrangedSubview(selectorView)
@ -191,14 +238,47 @@ open class SelectorItemBase<Selector: SelectorControlable>: Control, Errorable,
selectorView.isEnabled = isEnabled selectorView.isEnabled = isEnabled
selectorView.surface = surface selectorView.surface = surface
} }
/// Used to update any Accessibility properties. open override var accessibilityElements: [Any]? {
open override func updateAccessibility() { get {
super.updateAccessibility() var elements = [Any]()
setAccessibilityLabel(for: [selectorView, label, childLabel, errorLabel])
accessibilityValue = accessibilityValueText 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. /// Resets to default settings.
open override func reset() { open override func reset() {
super.reset() super.reset()
@ -290,4 +370,34 @@ open class SelectorItemBase<Selector: SelectorControlable>: Control, Errorable,
/// This will change to state of the Selector. /// This will change to state of the Selector.
open func toggle() {} 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
}
} }

View File

@ -52,7 +52,7 @@ open class View: UIView, ViewProtocol, UserInfoable {
open var surface: Surface = .light { didSet { setNeedsUpdate() } } open var surface: Surface = .light { didSet { setNeedsUpdate() } }
open var isEnabled: Bool = true { didSet { setNeedsUpdate() } } open var isEnabled: Bool = true { didSet { setNeedsUpdate() } }
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Lifecycle // MARK: - Lifecycle
//-------------------------------------------------- //--------------------------------------------------
@ -91,4 +91,136 @@ open class View: UIView, ViewProtocol, UserInfoable {
setNeedsUpdate() 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()
}
// }
}
} }

View File

@ -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
}
}

View File

@ -147,6 +147,11 @@ open class Badge: View {
label.widthGreaterThanEqualTo(constant: minWidth) label.widthGreaterThanEqualTo(constant: minWidth)
maxWidthConstraint = label.widthLessThanEqualTo(constant: 0).with { $0.isActive = false } maxWidthConstraint = label.widthLessThanEqualTo(constant: 0).with { $0.isActive = false }
clipsToBounds = true clipsToBounds = true
bridge_accessibilityLabelBlock = { [weak self] in
guard let self else { return "" }
return text
}
} }
/// Resets to default settings. /// Resets to default settings.
@ -179,10 +184,4 @@ open class Badge: View {
label.surface = surface label.surface = surface
label.isEnabled = isEnabled label.isEnabled = isEnabled
} }
open override func updateAccessibility() {
super.updateAccessibility()
accessibilityLabel = text
}
} }

View File

@ -292,6 +292,16 @@ open class BadgeIndicator: View {
label.centerYAnchor.constraint(equalTo: badgeView.centerYAnchor).isActive = true label.centerYAnchor.constraint(equalTo: badgeView.centerYAnchor).isActive = true
labelContraints.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. /// Resets to default settings.
@ -347,17 +357,6 @@ open class BadgeIndicator: View {
setNeedsLayout() 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() { open override func layoutSubviews() {
super.layoutSubviews() super.layoutSubviews()

View File

@ -82,6 +82,15 @@ open class BreadcrumbItem: ButtonBase {
isAccessibilityElement = true isAccessibilityElement = true
accessibilityTraits = .link 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. /// 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() setNeedsUpdate()
} }
/// Used to update any Accessibility properties.
open override func updateAccessibility() {
super.updateAccessibility()
accessibilityLabel = text
}
} }

View File

@ -50,7 +50,7 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable {
//-------------------------------------------------- //--------------------------------------------------
/// Key of whether or not updateView() is called in setNeedsUpdate() /// Key of whether or not updateView() is called in setNeedsUpdate()
open var shouldUpdateView: Bool = true open var shouldUpdateView: Bool = true
open var surface: Surface = .light { didSet { setNeedsUpdate() } } open var surface: Surface = .light { didSet { setNeedsUpdate() } }
/// Text that will be used in the titleLabel. /// 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. /// Whether the Button should handle the isHighlighted state.
open var shouldHighlight: Bool { isHighlighting == false } open var shouldHighlight: Bool { isHighlighting == false }
/// Whether the Control is highlighted or not. /// Whether the Control is highlighted or not.
open override var isHighlighted: Bool { open override var isHighlighted: Bool {
didSet { didSet {
@ -139,7 +139,7 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable {
shouldUpdateView = true shouldUpdateView = true
setNeedsUpdate() setNeedsUpdate()
} }
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Private Methods // 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 // MARK: AppleGuidelinesTouchable

View File

@ -105,6 +105,12 @@ open class TextLink: ButtonBase {
lineHeightConstraint = line.height(constant: 1) lineHeightConstraint = line.height(constant: 1)
lineHeightConstraint?.isActive = true 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. /// Used to make changes to the View based off a change events or from local properties.

View File

@ -86,6 +86,12 @@ open class TextLinkCaret: ButtonBase {
accessibilityTraits = .link accessibilityTraits = .link
titleLabel?.numberOfLines = 0 titleLabel?.numberOfLines = 0
titleLabel?.lineBreakMode = .byWordWrapping 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. /// Used to make changes to the View based off a change events or from local properties.

View File

@ -63,6 +63,8 @@ open class Checkbox: SelectorBase {
/// This will change the state of the Selector and execute the actionBlock if provided. /// This will change the state of the Selector and execute the actionBlock if provided.
open override func toggle() { open override func toggle() {
guard isEnabled else { return }
//removed error //removed error
if showError && isSelected == false { if showError && isSelected == false {
showError.toggle() showError.toggle()

View File

@ -47,8 +47,6 @@ open class CheckboxGroup: SelectorGroupBase<CheckboxItem>, SelectorGroupMultiSel
$0.surface = model.surface $0.surface = model.surface
$0.inputId = model.inputId $0.inputId = model.inputId
$0.hiddenValue = model.value $0.hiddenValue = model.value
$0.accessibilityLabel = model.accessibileText
$0.accessibilityValueText = "item \(index+1) of \(selectorModels.count)"
$0.labelText = model.labelText $0.labelText = model.labelText
$0.labelTextAttributes = model.labelTextAttributes $0.labelTextAttributes = model.labelTextAttributes
$0.childText = model.childText $0.childText = model.childText
@ -56,6 +54,7 @@ open class CheckboxGroup: SelectorGroupBase<CheckboxItem>, SelectorGroupMultiSel
$0.isSelected = model.selected $0.isSelected = model.selected
$0.errorText = model.errorText $0.errorText = model.errorText
$0.showError = model.showError $0.showError = model.showError
$0.selectorView.bridge_accessibilityValueBlock = { "item \(index+1) of \(selectorModels.count)" }
} }
} }
} }

View File

@ -38,6 +38,8 @@ open class CheckboxItem: SelectorItemBase<Checkbox> {
//-------------------------------------------------- //--------------------------------------------------
/// This will change the state of the Selector and execute the actionBlock if provided. /// This will change the state of the Selector and execute the actionBlock if provided.
open override func toggle() { open override func toggle() {
guard isEnabled else { return }
//removed error //removed error
if showError && isSelected == false { if showError && isSelected == false {
showError.toggle() showError.toggle()

View File

@ -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. /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
open override func setup() { open override func setup() {
super.setup() super.setup()
accessibilityHintText = "has popup, Double tap to open."
inlineDisplayLabel.isAccessibilityElement = true inlineDisplayLabel.isAccessibilityElement = true

View File

@ -30,7 +30,7 @@ open class ButtonIcon: Control, Changeable {
public required init?(coder: NSCoder) { public required init?(coder: NSCoder) {
super.init(coder: coder) super.init(coder: coder)
} }
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Enums // MARK: - Enums
//-------------------------------------------------- //--------------------------------------------------
@ -43,7 +43,7 @@ open class ButtonIcon: Control, Changeable {
public enum SurfaceType: String, CaseIterable { public enum SurfaceType: String, CaseIterable {
case colorFill, media case colorFill, media
} }
/// Enum used to describe the size of button icon. /// Enum used to describe the size of button icon.
public enum Size: String, EnumSubset { public enum Size: String, EnumSubset {
case large case large
@ -105,12 +105,12 @@ open class ButtonIcon: Control, Changeable {
return .init(x: 6, y: 6) return .init(x: 6, y: 6)
} }
} }
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Public Properties // MARK: - Public Properties
//-------------------------------------------------- //--------------------------------------------------
public var onChangeSubscriber: AnyCancellable? public var onChangeSubscriber: AnyCancellable?
///Badge Indicator object used to render for the ButtonIcon. ///Badge Indicator object used to render for the ButtonIcon.
open var badgeIndicator = BadgeIndicator().with { open var badgeIndicator = BadgeIndicator().with {
$0.translatesAutoresizingMaskIntoConstraints = false $0.translatesAutoresizingMaskIntoConstraints = false
@ -140,10 +140,10 @@ open class ButtonIcon: Control, Changeable {
open var selectedIconName: Icon.Name? { didSet { setNeedsUpdate() } } open var selectedIconName: Icon.Name? { didSet { setNeedsUpdate() } }
open var selectedIconColorConfiguration: SurfaceColorConfiguration? { didSet { setNeedsUpdate() } } open var selectedIconColorConfiguration: SurfaceColorConfiguration? { didSet { setNeedsUpdate() } }
/// Sets the size of button icon and icon. /// Sets the size of button icon and icon.
open var size: Size = .large { didSet { setNeedsUpdate() } } open var size: Size = .large { didSet { setNeedsUpdate() } }
/// If provided, the button icon will have a box shadow. /// If provided, the button icon will have a box shadow.
open var floating: Bool = false { didSet { setNeedsUpdate() } } 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. /// If set to true, the button icon will not have a border.
open var hideBorder: Bool = true { didSet { setNeedsUpdate() } } open var hideBorder: Bool = true { didSet { setNeedsUpdate() } }
/// If provided, the badge indicator will present. /// If provided, the badge indicator will present.
open var showBadgeIndicator: Bool = false { didSet { setNeedsUpdate() } } 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. /// 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() } } open var iconOffset: CGPoint = .init(x: 0, y: 0) { didSet { setNeedsUpdate() } }
/// Sets a custom size of button icon container. /// Sets a custom size of button icon container.
open var customContainerSize: Int? { didSet { setNeedsUpdate() } } open var customContainerSize: Int? { didSet { setNeedsUpdate() } }
/// Sets a custom size of the icon. /// Sets a custom size of the icon.
open var customIconSize: Int? { didSet { setNeedsUpdate() } } open var customIconSize: Int? { didSet { setNeedsUpdate() } }
/// Sets a custom badgeIndicator offset /// Sets a custom badgeIndicator offset
open var customBadgeIndicatorOffset: CGPoint? { didSet { setNeedsUpdate() } } open var customBadgeIndicatorOffset: CGPoint? { didSet { setNeedsUpdate() } }
@ -246,7 +246,7 @@ open class ButtonIcon: Control, Changeable {
SurfaceColorConfiguration(.clear, .clear).eraseToAnyColorable() SurfaceColorConfiguration(.clear, .clear).eraseToAnyColorable()
}() }()
} }
private struct LowContrastColorFillConfiguration: Configuration { private struct LowContrastColorFillConfiguration: Configuration {
var kind: Kind = .lowContrast var kind: Kind = .lowContrast
var surfaceType: SurfaceType = .colorFill 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() SurfaceColorConfiguration(VDSColor.paletteGray44.withAlphaComponent(0.06), VDSColor.paletteGray44.withAlphaComponent(0.26)).eraseToAnyColorable()
}() }()
} }
private struct LowContrastColorFillFloatingConfiguration: Configuration, DropShadowableConfiguration { private struct LowContrastColorFillFloatingConfiguration: Configuration, DropShadowableConfiguration {
var kind: Kind = .lowContrast var kind: Kind = .lowContrast
var surfaceType: SurfaceType = .colorFill var surfaceType: SurfaceType = .colorFill
@ -277,7 +277,7 @@ open class ButtonIcon: Control, Changeable {
} }
var configurations: [DropShadowable] { [dropshadow1Configuration, dropshadow2Configuration] } var configurations: [DropShadowable] { [dropshadow1Configuration, dropshadow2Configuration] }
} }
private struct LowContrastMediaConfiguration: Configuration, Borderable { private struct LowContrastMediaConfiguration: Configuration, Borderable {
var kind: Kind = .lowContrast var kind: Kind = .lowContrast
var surfaceType: SurfaceType = .media var surfaceType: SurfaceType = .media
@ -290,7 +290,7 @@ open class ButtonIcon: Control, Changeable {
SurfaceColorConfiguration(VDSColor.elementsLowcontrastOnlight, VDSColor.elementsLowcontrastOndark).eraseToAnyColorable() SurfaceColorConfiguration(VDSColor.elementsLowcontrastOnlight, VDSColor.elementsLowcontrastOndark).eraseToAnyColorable()
}() }()
} }
private struct LowContrastMediaFloatingConfiguration: Configuration, DropShadowableConfiguration { private struct LowContrastMediaFloatingConfiguration: Configuration, DropShadowableConfiguration {
var kind: Kind = .lowContrast var kind: Kind = .lowContrast
var surfaceType: SurfaceType = .media 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: .disabled)
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: [.selected, .disabled]) $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: [.selected, .disabled])
}.eraseToAnyColorable() }.eraseToAnyColorable()
}() }()
} }
private struct HighContrastFloatingConfiguration: Configuration, DropShadowableConfiguration { private struct HighContrastFloatingConfiguration: Configuration, DropShadowableConfiguration {
var kind: Kind = .highContrast var kind: Kind = .highContrast
var surfaceType: SurfaceType = .colorFill var surfaceType: SurfaceType = .colorFill
@ -357,9 +357,9 @@ open class ButtonIcon: Control, Changeable {
} }
var configurations: [DropShadowable] { [dropshadow1Configuration, dropshadow2Configuration] } var configurations: [DropShadowable] { [dropshadow1Configuration, dropshadow2Configuration] }
} }
private var badgeIndicatorDefaultSize: CGSize = .zero private var badgeIndicatorDefaultSize: CGSize = .zero
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Overrides // MARK: - Overrides
//-------------------------------------------------- //--------------------------------------------------
@ -367,11 +367,11 @@ open class ButtonIcon: Control, Changeable {
open override func setup() { open override func setup() {
super.setup() super.setup()
isAccessibilityElement = false isAccessibilityElement = false
//create a layoutGuide for the icon to key off of //create a layoutGuide for the icon to key off of
let iconLayoutGuide = UILayoutGuide() let iconLayoutGuide = UILayoutGuide()
addLayoutGuide(iconLayoutGuide) addLayoutGuide(iconLayoutGuide)
//add the icon //add the icon
addSubview(icon) addSubview(icon)
@ -379,7 +379,7 @@ open class ButtonIcon: Control, Changeable {
addSubview(badgeIndicator) addSubview(badgeIndicator)
badgeIndicator.isHidden = !showBadgeIndicator badgeIndicator.isHidden = !showBadgeIndicator
badgeIndicatorDefaultSize = badgeIndicator.frame.size badgeIndicatorDefaultSize = badgeIndicator.frame.size
//determines the height/width of the icon //determines the height/width of the icon
layoutGuideWidthConstraint = iconLayoutGuide.width(constant: size.containerSize) layoutGuideWidthConstraint = iconLayoutGuide.width(constant: size.containerSize)
layoutGuideHeightConstraint = iconLayoutGuide.height(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) badgeIndicatorCenterXConstraint = badgeIndicator.centerXAnchor.constraint(equalTo: icon.centerXAnchor)
badgeIndicatorCenterYConstraint = icon.centerYAnchor.constraint(equalTo: badgeIndicator.centerYAnchor) badgeIndicatorCenterYConstraint = icon.centerYAnchor.constraint(equalTo: badgeIndicator.centerYAnchor)
badgeIndicatorCenterYConstraint?.isActive = true badgeIndicatorCenterYConstraint?.isActive = true
badgeIndicatorLeadingConstraint?.isActive = true badgeIndicatorLeadingConstraint?.isActive = true
//pin layout guide //pin layout guide
iconLayoutGuide iconLayoutGuide
@ -396,7 +396,7 @@ open class ButtonIcon: Control, Changeable {
.pinLeading() .pinLeading()
.pinTrailing(0, .defaultHigh) .pinTrailing(0, .defaultHigh)
.pinBottom(0, .defaultHigh) .pinBottom(0, .defaultHigh)
//determines the center point of the icon //determines the center point of the icon
centerXConstraint = icon.centerXAnchor.constraint(equalTo: iconLayoutGuide.centerXAnchor, constant: 0) centerXConstraint = icon.centerXAnchor.constraint(equalTo: iconLayoutGuide.centerXAnchor, constant: 0)
centerXConstraint?.activate() 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. /// This will change the state of the Selector and execute the actionBlock if provided.
open func toggle() { open func toggle() {
//removed error //removed error
isSelected.toggle() isSelected.toggle()
sendActions(for: .valueChanged) sendActions(for: .valueChanged)
} }
/// Resets to default settings. /// Resets to default settings.
open override func reset() { open override func reset() {
super.reset() super.reset()
@ -437,7 +437,7 @@ open class ButtonIcon: Control, Changeable {
showBadgeIndicator = false showBadgeIndicator = false
selectable = false selectable = false
badgeIndicatorModel = nil badgeIndicatorModel = nil
onChange = nil onChange = nil
shouldUpdateView = true shouldUpdateView = true
setNeedsUpdate() setNeedsUpdate()
} }
@ -464,16 +464,18 @@ open class ButtonIcon: Control, Changeable {
setNeedsLayout() setNeedsLayout()
} }
open override func updateAccessibility() { open override var accessibilityElements: [Any]? {
super.updateAccessibility() get {
var elements = [Any]() var elements = [Any]()
if iconName != nil { if iconName != nil {
elements.append(icon) elements.append(icon)
}
if badgeIndicatorModel != nil && showBadgeIndicator {
elements.append(badgeIndicator)
}
return elements.count > 0 ? elements : nil
} }
if badgeIndicatorModel != nil && showBadgeIndicator { set { }
elements.append(badgeIndicator)
}
accessibilityElements = elements.count > 0 ? elements : nil
} }
open override func layoutSubviews() { open override func layoutSubviews() {

View File

@ -94,6 +94,12 @@ open class Icon: View {
isAccessibilityElement = true isAccessibilityElement = true
accessibilityTraits = .image 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. /// 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() super.reset()
color = VDSColor.paletteBlack color = VDSColor.paletteBlack
imageView.image = nil imageView.image = nil
} }
open override func updateAccessibility() {
super.updateAccessibility()
accessibilityLabel = name?.rawValue ?? "icon"
}
} }
extension UIImage { extension UIImage {

View File

@ -91,16 +91,14 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
private struct LabelAction { private struct LabelAction {
var range: NSRange var range: NSRange
var action: PassthroughSubject<Void, Never> var action: PassthroughSubject<Void, Never>
var accessibilityId: Int = 0 var frame: CGRect = .zero
func performAction() { func performAction() {
action.send() action.send()
} }
init(range: NSRange, action: PassthroughSubject<Void, Never>, accessibilityID: Int = 0) { init(range: NSRange, action: PassthroughSubject<Void, Never>) {
self.range = range self.range = range
self.action = action 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() { open func reset() {
shouldUpdateView = false shouldUpdateView = false
@ -242,7 +245,6 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
} }
open func updateAccessibility() { open func updateAccessibility() {
accessibilityLabel = text
if isEnabled { if isEnabled {
accessibilityTraits.remove(.notEnabled) accessibilityTraits.remove(.notEnabled)
} else { } else {
@ -263,24 +265,7 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
super.layoutSubviews() super.layoutSubviews()
applyActions() 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 // MARK: - Private Methods
//-------------------------------------------------- //--------------------------------------------------
@ -373,15 +358,27 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
//see if the attribute is Actionable //see if the attribute is Actionable
if let actionable = attribute as? any ActionLabelAttributeModel, mutableAttributedString.isValid(range: actionable.range) { if let actionable = attribute as? any ActionLabelAttributeModel, mutableAttributedString.isValid(range: actionable.range) {
//create a accessibleAction //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 //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 { if let accessibilityElements, !accessibilityElements.isEmpty {
let staticText = UIAccessibilityElement(accessibilityContainer: self) let staticText = AccessibilityActionElement(accessibilityContainer: self)
staticText.accessibilityLabel = text staticText.accessibilityLabel = text
staticText.accessibilityFrameInContainerSpace = bounds staticText.accessibilityFrameInContainerSpace = bounds
@ -395,14 +392,40 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
@objc private func textLinkTapped(_ gesture: UITapGestureRecognizer) { @objc private func textLinkTapped(_ gesture: UITapGestureRecognizer) {
for actionable in actions { for actionable in actions {
// This determines if we tapped on the desired range of text. // 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() actionable.performAction()
return 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 } 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) let substringBounds = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
// Create custom accessibility element // Create custom accessibility element
let element = UIAccessibilityElement(accessibilityContainer: self) let element = AccessibilityActionElement(accessibilityContainer: self)
element.accessibilityLabel = actionText element.accessibilityLabel = actionText
element.accessibilityTraits = .link element.accessibilityTraits = .link
element.accessibilityHint = "Double tap to open"
element.accessibilityFrameInContainerSpace = substringBounds 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 = (accessibilityElements ?? []).compactMap{$0 as? UIAccessibilityElement}.filter { $0.accessibilityLabel != actionText }
accessibilityElements?.append(element) accessibilityElements?.append(element)
return 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 { // MARK: - Accessibility
if action.hash == actionable.accessibilityId { //--------------------------------------------------
actionable.performAction() open var accessibilityAction: ((Label) -> Void)?
return
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
}
// }
}
} }

View File

@ -265,6 +265,12 @@ open class Notification: View {
isAccessibilityElement = false isAccessibilityElement = false
accessibilityElements = [closeButton, typeIcon, titleLabel, subTitleLabel, buttonGroup] accessibilityElements = [closeButton, typeIcon, titleLabel, subTitleLabel, buttonGroup]
closeButton.accessibilityTraits = [.button] 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. /// 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() { private func setConstraints() {
labelViewAndButtonViewConstraint?.deactivate() labelViewAndButtonViewConstraint?.deactivate()
labelViewBottomConstraint?.deactivate() labelViewBottomConstraint?.deactivate()

View File

@ -86,6 +86,10 @@ open class Pagination: View {
} }
} }
private var paginationDescription: String {
"Page \(selectedPage) of \(total) selected"
}
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Overrides // MARK: - Overrides
//-------------------------------------------------- //--------------------------------------------------
@ -148,14 +152,26 @@ open class Pagination: View {
guard let self else { return } guard let self else { return }
self.selectedPage = max(0, self.selectedPage - 1) 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 var accessibilityElements: [Any]? {
open override func updateAccessibility() { get {
super.updateAccessibility() let views: [UIView] = [previousButton, collectionContainerView, nextButton]
accessibilityElements = [previousButton, collectionContainerView, nextButton] return views.filter({ $0.isHidden == false })
collectionContainerView.accessibilityLabel = "Pagination containing \(total) pages" }
collectionContainerView.accessibilityValue = "Page \(selectedPage) of \(total) selected" set {
}
} }
/// Used to make changes to the View based off a change events or from local properties. /// 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() updateSelection()
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
guard let self else { return } guard let self else { return }
UIAccessibility.post(notification: .announcement, argument: "Page \(self.selectedPage) of \(self.total) selected") UIAccessibility.post(notification: .announcement, argument: paginationDescription)
} }
} }

View File

@ -78,11 +78,6 @@ open class PaginationButton: ButtonBase {
tintColor = color tintColor = color
super.updateView() super.updateView()
} }
open override func accessibilityActivate() -> Bool {
sendActions(for: .touchUpInside)
return true
}
} }
extension PaginationButton { extension PaginationButton {

View File

@ -42,8 +42,6 @@ open class RadioBoxGroup: SelectorGroupBase<RadioBoxItem>, SelectorGroupSingleSe
if let selectorModels { if let selectorModels {
items = selectorModels.enumerated().map { index, model in items = selectorModels.enumerated().map { index, model in
return RadioBoxItem().with { return RadioBoxItem().with {
$0.accessibilityLabel = model.accessibileText
$0.accessibilityValue = "item \(index+1) of \(selectorModels.count)"
$0.text = model.text $0.text = model.text
$0.textAttributes = model.textAttributes $0.textAttributes = model.textAttributes
$0.subText = model.subText $0.subText = model.subText
@ -56,7 +54,7 @@ open class RadioBoxGroup: SelectorGroupBase<RadioBoxItem>, SelectorGroupSingleSe
$0.isSelected = model.selected $0.isSelected = model.selected
$0.strikethrough = model.strikethrough $0.strikethrough = model.strikethrough
$0.strikethroughAccessibilityText = model.strikethroughAccessibileText $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 /// Current Surface and this is used to pass down to child objects that implement Surfacable
public var surface: Surface public var surface: Surface
public var inputId: String? public var inputId: String?
public var value: AnyHashable? public var value: String?
public var accessibileText: String? public var accessibileText: String?
public var text: String public var text: String
/// Array of LabelAttributeModel objects used in rendering the text. /// Array of LabelAttributeModel objects used in rendering the text.
@ -126,7 +124,7 @@ extension RadioBoxGroup {
public var strikethrough: Bool = false public var strikethrough: Bool = false
public var strikethroughAccessibileText: String 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, text: String = "", textAttributes: [any LabelAttributeModel]? = nil,
subText: String? = nil, subTextAttributes: [any LabelAttributeModel]? = nil, subText: String? = nil, subTextAttributes: [any LabelAttributeModel]? = nil,
subTextRight: String? = nil, subTextRightAttributes: [any LabelAttributeModel]? = nil, subTextRight: String? = nil, subTextRightAttributes: [any LabelAttributeModel]? = nil,

View File

@ -74,9 +74,7 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable {
} }
/// Selector for this RadioBox. /// Selector for this RadioBox.
open var selectorView = UIView().with { open var selectorView = View().with { $0.accessibilityIdentifier = "RadioBox" }
$0.translatesAutoresizingMaskIntoConstraints = false
}
/// If provided, the RadioBox text will be rendered. /// If provided, the RadioBox text will be rendered.
open var text: String? { didSet { setNeedsUpdate() } } 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 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 // MARK: - Configuration Properties
//-------------------------------------------------- //--------------------------------------------------
@ -165,16 +170,51 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable {
onClick = { control in onClick = { control in
control.toggle() 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. /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
open override func setup() { open override func setup() {
super.setup() super.setup()
isAccessibilityElement = true isAccessibilityElement = false
accessibilityTraits = .button selectorView.isAccessibilityElement = true
selectorView.accessibilityTraits = .button
addSubview(selectorView) addSubview(selectorView)
selectorView.isUserInteractionEnabled = false selectorView.isUserInteractionEnabled = true
selectorView.addSubview(selectorStackView) 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. /// This will change the state of the Selector and execute the actionBlock if provided.
open func toggle() { open func toggle() {
guard isEnabled else { return }
//removed error //removed error
isSelected.toggle() isSelected.toggle()
sendActions(for: .valueChanged) sendActions(for: .valueChanged)
@ -239,26 +281,51 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable {
setNeedsLayout() setNeedsLayout()
} }
/// Used to update any Accessibility properties. open override var accessibilityElements: [Any]? {
open override func updateAccessibility() { get {
super.updateAccessibility() var items = [Any]()
setAccessibilityLabel(for: [textLabel, subTextLabel, subTextRightLabel]) items.append(selectorView)
if let currentAccessibilityLabel = accessibilityLabel {
accessibilityLabel = "Radiobox, \(currentAccessibilityLabel)" if let text = text, !text.isEmpty {
} else { items.append(textLabel)
accessibilityLabel = "Radiobox" }
if let text = subText, !text.isEmpty {
items.append(subTextLabel)
}
if let text = subTextRight, !text.isEmpty {
items.append(subTextRightLabel)
}
return items
} }
if let accessibilityValueText { set {}
accessibilityValue = strikethrough }
? "\(strikethroughAccessibilityText), \(accessibilityValueText)"
: 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? {
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 { } else {
accessibilityValue = strikethrough guard !UIAccessibility.isVoiceOverRunning else { return nil }
? "\(strikethroughAccessibilityText)" return super.hitTest(point, with: event)
: accessibilityValueText
} }
} }
open func getSelectorView() -> UIView {
selectorView
}
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Private Methods // MARK: - Private Methods
//-------------------------------------------------- //--------------------------------------------------

View File

@ -62,7 +62,7 @@ open class RadioButton: SelectorBase {
/// This will change the state of the Selector and execute the actionBlock if provided. /// This will change the state of the Selector and execute the actionBlock if provided.
open override func toggle() { open override func toggle() {
guard !isSelected else { return } guard !isSelected, isEnabled else { return }
//removed error //removed error
if showError && isSelected == false { if showError && isSelected == false {

View File

@ -46,8 +46,6 @@ open class RadioButtonGroup: SelectorGroupBase<RadioButtonItem>, SelectorGroupSi
$0.surface = model.surface $0.surface = model.surface
$0.inputId = model.inputId $0.inputId = model.inputId
$0.hiddenValue = model.value $0.hiddenValue = model.value
$0.accessibilityLabel = model.accessibileText
$0.accessibilityValueText = "item \(index+1) of \(selectorModels.count)"
$0.labelText = model.labelText $0.labelText = model.labelText
$0.labelTextAttributes = model.labelTextAttributes $0.labelTextAttributes = model.labelTextAttributes
$0.childText = model.childText $0.childText = model.childText
@ -55,6 +53,7 @@ open class RadioButtonGroup: SelectorGroupBase<RadioButtonItem>, SelectorGroupSi
$0.isSelected = model.selected $0.isSelected = model.selected
$0.errorText = model.errorText $0.errorText = model.errorText
$0.showError = model.showError $0.showError = model.showError
$0.selectorView.bridge_accessibilityValueBlock = { "item \(index+1) of \(selectorModels.count)" }
} }
} }
} }

View File

@ -34,7 +34,7 @@ open class RadioButtonItem: SelectorItemBase<RadioButton> {
//-------------------------------------------------- //--------------------------------------------------
/// This will change the state of the Selector and execute the actionBlock if provided. /// This will change the state of the Selector and execute the actionBlock if provided.
open override func toggle() { open override func toggle() {
guard !isSelected else { return } guard !isSelected, isEnabled else { return }
//removed error //removed error
if showError && isSelected == false { if showError && isSelected == false {

View File

@ -88,8 +88,6 @@ extension Tabs {
open var minWidth: CGFloat = 44.0 { didSet { setNeedsUpdate() } } open var minWidth: CGFloat = 44.0 { didSet { setNeedsUpdate() } }
open override var shouldHighlight: Bool { false } open override var shouldHighlight: Bool { false }
open var accessibilityValueText: String?
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Configuration // MARK: - Configuration
@ -151,6 +149,11 @@ extension Tabs {
labelTopConstraint = label.pinTop(anchor: layoutGuide.topAnchor) labelTopConstraint = label.pinTop(anchor: layoutGuide.topAnchor)
labelLeadingConstraint = label.pinLeading(anchor: layoutGuide.leadingAnchor) labelLeadingConstraint = label.pinLeading(anchor: layoutGuide.leadingAnchor)
labelBottomConstraint = label.pinBottom(anchor: layoutGuide.bottomAnchor, priority: .defaultHigh) 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. /// Used to make changes to the View based off a change events or from local properties.
@ -176,13 +179,6 @@ extension Tabs {
setNeedsLayout() setNeedsLayout()
} }
/// Used to update any Accessibility properties.
open override func updateAccessibility() {
super.updateAccessibility()
accessibilityLabel = text
accessibilityValue = accessibilityValueText
}
open override func layoutSubviews() { open override func layoutSubviews() {
super.layoutSubviews() super.layoutSubviews()

View File

@ -305,7 +305,10 @@ open class Tabs: View {
tabItem.orientation = orientation tabItem.orientation = orientation
tabItem.surface = surface tabItem.surface = surface
tabItem.indicatorPosition = indicatorPosition 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"
}
} }
} }

View File

@ -94,12 +94,9 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
/// This is the view that will be wrapped with the border for userInteraction. /// This is the view that will be wrapped with the border for userInteraction.
/// The only subview of this view is the fieldStackView /// The only subview of this view is the fieldStackView
internal var containerView: UIView = { internal var containerView = View().with {
return UIView().with { $0.isAccessibilityElement = true
$0.translatesAutoresizingMaskIntoConstraints = false }
$0.isAccessibilityElement = true
}
}()
/// This is set by a local method. /// This is set by a local method.
internal var bottomContainerView: UIView! internal var bottomContainerView: UIView!
@ -244,27 +241,6 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
open var rules = [AnyRule<String>]() open var rules = [AnyRule<String>]()
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" open var accessibilityHintText: String = "Double tap to open"
//-------------------------------------------------- //--------------------------------------------------
@ -283,11 +259,11 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
.pinBottom() .pinBottom()
trailingEqualsConstraint = layoutGuide.pinTrailing(anchor: trailingAnchor) trailingEqualsConstraint = layoutGuide.pinTrailing(anchor: trailingAnchor)
// width constraints // width constraints
trailingLessThanEqualsConstraint = layoutGuide.pinTrailingLessThanOrEqualTo(anchor: trailingAnchor)?.deactivate() trailingLessThanEqualsConstraint = layoutGuide.pinTrailingLessThanOrEqualTo(anchor: trailingAnchor)?.deactivate()
widthConstraint = layoutGuide.widthAnchor.constraint(equalToConstant: 0).deactivate() widthConstraint = layoutGuide.widthAnchor.constraint(equalToConstant: 0).deactivate()
// Add mainStackView to the view // Add mainStackView to the view
addSubview(mainStackView) addSubview(mainStackView)
@ -301,7 +277,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
//InputContainer, Icons, Buttons //InputContainer, Icons, Buttons
containerView.addSubview(fieldStackView) containerView.addSubview(fieldStackView)
fieldStackView.pinToSuperView(.uniform(VDSLayout.space3X)) fieldStackView.pinToSuperView(.uniform(VDSLayout.space3X))
let fieldContainerView = getFieldContainer() let fieldContainerView = getFieldContainer()
fieldContainerView.translatesAutoresizingMaskIntoConstraints = false fieldContainerView.translatesAutoresizingMaskIntoConstraints = false
@ -309,11 +285,11 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
fieldStackView.addArrangedSubview(fieldContainerView) fieldStackView.addArrangedSubview(fieldContainerView)
fieldStackView.addArrangedSubview(statusIcon) fieldStackView.addArrangedSubview(statusIcon)
fieldStackView.setCustomSpacing(VDSLayout.space3X, after: fieldContainerView) fieldStackView.setCustomSpacing(VDSLayout.space3X, after: fieldContainerView)
//get the container this is what show helper text, error text //get the container this is what show helper text, error text
//can include other for character count, max length //can include other for character count, max length
bottomContainerView = getBottomContainer() bottomContainerView = getBottomContainer()
//this is the vertical stack that contains error text, helper text //this is the vertical stack that contains error text, helper text
bottomContainerStackView.addArrangedSubview(errorLabel) bottomContainerStackView.addArrangedSubview(errorLabel)
bottomContainerStackView.addArrangedSubview(helperLabel) bottomContainerStackView.addArrangedSubview(helperLabel)
@ -321,11 +297,11 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
// Add arranged subviews to textFieldStackView // Add arranged subviews to textFieldStackView
contentStackView.addArrangedSubview(containerView) contentStackView.addArrangedSubview(containerView)
contentStackView.addArrangedSubview(bottomContainerView) contentStackView.addArrangedSubview(bottomContainerView)
// Add arranged subviews to mainStackView // Add arranged subviews to mainStackView
mainStackView.addArrangedSubview(titleLabel) mainStackView.addArrangedSubview(titleLabel)
mainStackView.addArrangedSubview(contentStackView) mainStackView.addArrangedSubview(contentStackView)
// Initial position of the helper label // Initial position of the helper label
updateHelperTextPosition() updateHelperTextPosition()
@ -333,6 +309,38 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
titleLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable() titleLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable()
errorLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable() errorLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable()
helperLabel.textColorConfiguration = secondaryColorConfiguration.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 /// 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]? { open override var accessibilityElements: [Any]? {
get { get {
var elements = [Any]() var elements = [Any]()

View File

@ -47,7 +47,10 @@ open class TextField: UITextField, ViewProtocol, Errorable {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Properties // 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.tag = 999
$0.textColorConfiguration = ViewColorConfiguration().with { $0.textColorConfiguration = ViewColorConfiguration().with {
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forDisabled: true) $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. /// Will determine if a scaled font should be used for the titleLabel font.
open var useScaledFont: Bool = false { didSet { setNeedsUpdate() } } 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() } } 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 errorText: String? { didSet { setNeedsUpdate() } }
open var lineBreakMode: NSLineBreakMode = .byClipping { didSet { setNeedsUpdate() } } open var lineBreakMode: NSLineBreakMode = .byClipping { didSet { setNeedsUpdate() } }
open override var isEnabled: Bool { didSet { setNeedsUpdate() } } open override var isEnabled: Bool { didSet { setNeedsUpdate() } }
open var textColorConfiguration: AnyColorable = ViewColorConfiguration().with { open var textColorConfiguration: AnyColorable = ViewColorConfiguration().with {
@ -229,7 +229,139 @@ open class TextField: UITextField, ViewProtocol, Errorable {
attributedText = nil 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 { extension UITextField {

View File

@ -144,6 +144,139 @@ open class TextView: UITextView, ViewProtocol, Errorable {
shouldUpdateView = true shouldUpdateView = true
setNeedsUpdate() 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 // MARK: - Private Methods

View File

@ -266,6 +266,11 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
backgroundImageView.layer.cornerRadius = cornerRadius backgroundImageView.layer.cornerRadius = cornerRadius
highlightView.layer.cornerRadius = cornerRadius highlightView.layer.cornerRadius = cornerRadius
clipsToBounds = true 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 /// Overriden to take the hit if there is an onClickSubscriber and the view is not a UIControl
@ -338,13 +343,6 @@ open class TileContainerBase<PaddingType: DefaultValuing>: 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]? { open override var accessibilityElements: [Any]? {
get { get {
var items = [Any]() var items = [Any]()

View File

@ -262,20 +262,21 @@ open class TitleLockup: View {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Overrides // MARK: - Overrides
//-------------------------------------------------- //--------------------------------------------------
open override func updateAccessibility() { open override var accessibilityElements: [Any]? {
super.updateAccessibility() get {
var elements = [Any]() var elements = [Any]()
if eyebrowModel != nil { if eyebrowModel != nil {
elements.append(eyebrowLabel) elements.append(eyebrowLabel)
}
if titleModel != nil {
elements.append(titleLabel)
}
if subTitleModel != nil {
elements.append(subTitleLabel)
}
return elements.count > 0 ? elements : nil
} }
if titleModel != nil { set {}
elements.append(titleLabel)
}
if subTitleModel != nil {
elements.append(subTitleLabel)
}
setAccessibilityLabel(for: elements.compactMap({$0 as? UIView}))
accessibilityElements = elements.count > 0 ? elements : nil
} }
/// Resets to default settings. /// Resets to default settings.

View File

@ -207,6 +207,14 @@ open class Toggle: Control, Changeable, FormFieldable {
label.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor) 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. /// Resets to default settings.
@ -238,16 +246,6 @@ open class Toggle: Control, Changeable, FormFieldable {
toggleView.isEnabled = isEnabled toggleView.isEnabled = isEnabled
toggleView.isOn = isOn 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. /// This will change the state of the Selector and execute the actionBlock if provided.
open func toggle() { open func toggle() {

View File

@ -153,7 +153,7 @@ open class ToggleView: Control, Changeable, FormFieldable {
// Update shadow layers frames to match the view's bounds // Update shadow layers frames to match the view's bounds
knobView.layer.insertSublayer(shadowLayer1, at: 0) knobView.layer.insertSublayer(shadowLayer1, at: 0)
knobView.layer.insertSublayer(shadowLayer2, at: 0) knobView.layer.insertSublayer(shadowLayer2, at: 0)
accessibilityLabel = "Toggle"
} }
/// Resets to default settings. /// Resets to default settings.
@ -176,13 +176,6 @@ open class ToggleView: Control, Changeable, FormFieldable {
updateToggle() 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. /// This will change the state of the Selector and execute the actionBlock if provided.
open func toggle() { open func toggle() {

View File

@ -138,6 +138,24 @@ open class Tooltip: Control, TooltipLaunchable {
contentView: tooltip.contentView), contentView: tooltip.contentView),
presenter: self) 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. /// Resets to default settings.
@ -163,23 +181,7 @@ open class Tooltip: Control, TooltipLaunchable {
//get the color for the image //get the color for the image
icon.color = iconColorConfiguration.getColor(self) 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 { public static func accessibleText(for title: String?, content: String?, closeButtonText: String) -> String {
var label = "" var label = ""
if let title { if let title {

View File

@ -219,15 +219,19 @@ open class TooltipDialog: View, UIScrollViewDelegate {
/// Used to update any Accessibility properties. /// Used to update any Accessibility properties.
open override func updateAccessibility() { open override func updateAccessibility() {
super.updateAccessibility() super.updateAccessibility()
primaryAccessibilityElement.accessibilityHint = "Double tap on the \(tooltipModel.closeButtonText) button to close." primaryAccessibilityElement.accessibilityHint = "Double tap on the \(tooltipModel.closeButtonText) button to close."
primaryAccessibilityElement.accessibilityFrameInContainerSpace = .init(origin: .zero, size: frame.size) 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] return elements
contentStackView.arrangedSubviews.forEach{ elements.append($0) } }
elements.append(closeButton) set {}
accessibilityElements = elements
} }
} }

View File

@ -62,7 +62,7 @@ extension UIView {
} else { } else {
removeDebugBorder() removeDebugBorder()
} }
if let view = self as? ViewProtocol { if let view = self as? (any ViewProtocol) {
view.updateView() view.updateView()
} }
} }

View File

@ -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
// }
}
}
}

View File

@ -9,6 +9,4 @@ import Foundation
public protocol Groupable: Control { public protocol Groupable: Control {
/// Property used to add context to the Grouping of a set.
var accessibilityValueText: String? { get set }
} }

View File

@ -9,13 +9,16 @@ import Foundation
import UIKit import UIKit
import Combine 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. /// Set of Subscribers for any Publishers for this Control.
var subscribers: Set<AnyCancellable> { get set } var subscribers: Set<AnyCancellable> { get set }
/// Key of whether or not updateView() is called in setNeedsUpdate() /// Key of whether or not updateView() is called in setNeedsUpdate()
var shouldUpdateView: Bool { get set } 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. /// Executed on initialization for this View.
func initialSetup() func initialSetup()
@ -30,6 +33,7 @@ public protocol ViewProtocol: AnyObject, Initable, Resettable, Enabling, Surface
} }
extension ViewProtocol { extension ViewProtocol {
/// Called when there are changes in a View based off a change events or from local properties. /// Called when there are changes in a View based off a change events or from local properties.
public func setNeedsUpdate() { public func setNeedsUpdate() {
if shouldUpdateView { if shouldUpdateView {
@ -49,7 +53,7 @@ extension ViewProtocol where Self: UIView {
view.removeFromSuperview() view.removeFromSuperview()
setNeedsDisplay() setNeedsDisplay()
} }
} }
} }
extension ViewProtocol where Self: UIControl { extension ViewProtocol where Self: UIControl {

View File

@ -1,9 +1,12 @@
1.0.68
----------------
- CXTDT-553663 - DropdownSelect - Accessibility - has popup
1.0.67 1.0.67
---------------- ----------------
- CXTDT-568463 - Calendar - On long press, hover randomizes - CXTDT-568463 - Calendar - On long press, hover randomizes
- CXTDT-568412 - Calendar - Incorrect side nav icon size - CXTDT-568412 - Calendar - Incorrect side nav icon size
- CXTDT-568422 - Calendar - DarkMode Legend icon fill using Light mode color - CXTDT-568422 - Calendar - DarkMode Legend icon fill using Light mode color
- CXTDT-553663 - DropdownSelect - Accessibility - has popup
- CXTDT-565796 - DropdownSelect - Accessibility - CXTDT-565796 - DropdownSelect - Accessibility
- CXTDT-560458 - Dropdown/TextArea - Different voiceover - CXTDT-560458 - Dropdown/TextArea - Different voiceover
- CXTDT-565106 - InputField - CreditCard - Icons - CXTDT-565106 - InputField - CreditCard - Icons