Merge branch 'develop' of https://gitlab.verizon.com/BPHV_MIPS/vds_ios into vasavk/carousel

This commit is contained in:
Vasavi Kanamarlapudi 2024-07-01 10:35:04 +05:30
commit 4b016974d2
64 changed files with 2593 additions and 885 deletions

View File

@ -157,7 +157,7 @@
EAC58C182BED0E2300BA39FA /* SecurityCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C172BED0E2300BA39FA /* SecurityCode.swift */; };
EAC58C232BF2824200BA39FA /* DatePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C222BF2824200BA39FA /* DatePicker.swift */; };
EAC58C272BF4116200BA39FA /* DatePickerCalendarModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C262BF4116200BA39FA /* DatePickerCalendarModel.swift */; };
EAC58C292BF4118C00BA39FA /* DatePickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C282BF4118C00BA39FA /* DatePickerViewController.swift */; };
EAC58C292BF4118C00BA39FA /* ClearPopoverViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C282BF4118C00BA39FA /* ClearPopoverViewController.swift */; };
EAC71A1D2A2E155A00E47A9F /* Checkbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC71A1C2A2E155A00E47A9F /* Checkbox.swift */; };
EAC71A1F2A2E173D00E47A9F /* RadioButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC71A1E2A2E173D00E47A9F /* RadioButton.swift */; };
EAC846F3294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC846F2294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift */; };
@ -177,6 +177,9 @@
EAF193432C134F3800C68D18 /* TableCellItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 443DBAF92BDA303F0021497E /* TableCellItem.swift */; };
EAF1FE9929D4850E00101452 /* Clickable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF1FE9829D4850E00101452 /* Clickable.swift */; };
EAF1FE9B29DB1A6000101452 /* Changeable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF1FE9A29DB1A6000101452 /* Changeable.swift */; };
EAF2F4762C231EAA007BFEDC /* AccessibilityActionElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */; };
EAF2F4782C249D72007BFEDC /* AccessibilityUpdatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF2F4772C249D72007BFEDC /* AccessibilityUpdatable.swift */; };
EAF2F4892C2A1075007BFEDC /* AlertViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF2F4882C2A1075007BFEDC /* AlertViewController.swift */; };
EAF7F0952899861000B287F5 /* CheckboxItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0932899861000B287F5 /* CheckboxItem.swift */; };
EAF7F09A2899B17200B287F5 /* CATransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0992899B17200B287F5 /* CATransaction.swift */; };
EAF7F0A0289AB7EC00B287F5 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F09F289AB7EC00B287F5 /* View.swift */; };
@ -375,7 +378,7 @@
EAC58C222BF2824200BA39FA /* DatePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePicker.swift; sourceTree = "<group>"; };
EAC58C242BF2A7FB00BA39FA /* DatePickerChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = DatePickerChangeLog.txt; sourceTree = "<group>"; };
EAC58C262BF4116200BA39FA /* DatePickerCalendarModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickerCalendarModel.swift; sourceTree = "<group>"; };
EAC58C282BF4118C00BA39FA /* DatePickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickerViewController.swift; sourceTree = "<group>"; };
EAC58C282BF4118C00BA39FA /* ClearPopoverViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClearPopoverViewController.swift; sourceTree = "<group>"; };
EAC71A1C2A2E155A00E47A9F /* Checkbox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Checkbox.swift; sourceTree = "<group>"; };
EAC71A1E2A2E173D00E47A9F /* RadioButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButton.swift; sourceTree = "<group>"; };
EAC846F2294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonGroupCollectionViewCell.swift; sourceTree = "<group>"; };
@ -407,6 +410,9 @@
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>"; };
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>"; };
EAF2F4882C2A1075007BFEDC /* AlertViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertViewController.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>"; };
EAF7F09F289AB7EC00B287F5 /* View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = "<group>"; };
@ -732,6 +738,7 @@
EA3361AB288B25EC0071C351 /* Protocols */ = {
isa = PBXGroup;
children = (
EAF2F4772C249D72007BFEDC /* AccessibilityUpdatable.swift */,
EA4DB2FC28D3D0CA00103EE3 /* AnyEquatable.swift */,
EA297A5629FB0A360031ED56 /* AppleGuidelinesTouchable.swift */,
EAF1FE9A29DB1A6000101452 /* Changeable.swift */,
@ -762,8 +769,11 @@
isa = PBXGroup;
children = (
EA985C1C296CD13600F2FF2E /* BundleManager.swift */,
EAC58C282BF4118C00BA39FA /* ClearPopoverViewController.swift */,
EAF7F0B8289C139800B287F5 /* ColorConfiguration.swift */,
EAB5FEF02927F4AA00998C17 /* SelfSizingCollectionView.swift */,
EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */,
EAF2F4882C2A1075007BFEDC /* AlertViewController.swift */,
);
path = Classes;
sourceTree = "<group>";
@ -981,7 +991,6 @@
children = (
EAC58C222BF2824200BA39FA /* DatePicker.swift */,
EAC58C262BF4116200BA39FA /* DatePickerCalendarModel.swift */,
EAC58C282BF4118C00BA39FA /* DatePickerViewController.swift */,
EAC58C242BF2A7FB00BA39FA /* DatePickerChangeLog.txt */,
);
path = DatePicker;
@ -1227,6 +1236,7 @@
EAF7F0A6289B0CE000B287F5 /* Resetable.swift in Sources */,
EA985C2D296F03FE00F2FF2E /* TileletIconModels.swift in Sources */,
EA89200428AECF4B006B9984 /* UITextField+Publisher.swift in Sources */,
EAF2F4782C249D72007BFEDC /* AccessibilityUpdatable.swift in Sources */,
18A65A022B96E848006602CC /* Breadcrumbs.swift in Sources */,
1842B1E12BECE7B70021AFCA /* CalendarHeaderReusableView.swift in Sources */,
EA78C7962C00CAC200430AD1 /* Groupable.swift in Sources */,
@ -1265,6 +1275,7 @@
EA0D1C412A6AD61C00E5C127 /* Typography+Additional.swift in Sources */,
EAC925842911C63100091998 /* Colorable.swift in Sources */,
18B463A42BBD3C46005C4528 /* DropdownOptionModel.swift in Sources */,
EAF2F4762C231EAA007BFEDC /* AccessibilityActionElement.swift in Sources */,
EAC58BFD2BE935C300BA39FA /* TitleLockupTextColor.swift in Sources */,
EAACB89A2B927108006A3869 /* Valuing.swift in Sources */,
EAE785312BA0A438009428EA /* UIImage+Helper.swift in Sources */,
@ -1319,12 +1330,13 @@
EA3361BD288B2C760071C351 /* TypeAlias.swift in Sources */,
EAC58C0A2BED004E00BA39FA /* FieldType.swift in Sources */,
EA471F3A2A95587500CE9E58 /* LayoutConstraintable.swift in Sources */,
EAC58C292BF4118C00BA39FA /* DatePickerViewController.swift in Sources */,
EAC58C292BF4118C00BA39FA /* ClearPopoverViewController.swift in Sources */,
EAF193432C134F3800C68D18 /* TableCellItem.swift in Sources */,
EAB1D2CF28ABEF2B00DAE764 /* Typography+Base.swift in Sources */,
EA0D1C3B2A6AD51B00E5C127 /* Typogprahy+Styles.swift in Sources */,
EAF7F09A2899B17200B287F5 /* CATransaction.swift in Sources */,
EAC58C162BED0E0300BA39FA /* InlineAction.swift in Sources */,
EAF2F4892C2A1075007BFEDC /* AlertViewController.swift in Sources */,
EA0D1C3D2A6AD57600E5C127 /* Typography+Enums.swift in Sources */,
EAF1FE9B29DB1A6000101452 /* Changeable.swift in Sources */,
EAC58C0C2BED01D500BA39FA /* Telephone.swift in Sources */,
@ -1547,7 +1559,7 @@
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 66;
CURRENT_PROJECT_VERSION = 68;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = "";
DYLIB_COMPATIBILITY_VERSION = 1;
@ -1585,7 +1597,7 @@
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 66;
CURRENT_PROJECT_VERSION = 68;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = "";
DYLIB_COMPATIBILITY_VERSION = 1;

View File

@ -46,7 +46,7 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable {
// MARK: - Public Properties
//--------------------------------------------------
open var shouldUpdateView: Bool = true
open var userInfo = [String: Primitive]()
open var surface: Surface = .light { didSet { setNeedsUpdate() } }
@ -119,17 +119,132 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable {
//--------------------------------------------------
// MARK: - Overrides
//--------------------------------------------------
/// Implement accessibilityActivate on an element in order to handle the default action.
/// - Returns: Based on whether the userInteraction is enabled.
override open func accessibilityActivate() -> Bool {
// Hold state in case User wanted isAnimated to remain off.
guard isUserInteractionEnabled else { return false }
sendActions(for: .touchUpInside)
return true
}
open override func layoutSubviews() {
super.layoutSubviews()
setNeedsUpdate()
}
//--------------------------------------------------
// MARK: - Accessibility
//--------------------------------------------------
open var accessibilityAction: ((Control) -> Void)?
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 super.isAccessibilityElement
}
}
set {
super.isAccessibilityElement = newValue
}
}
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 super.accessibilityLabel
}
}
set {
super.accessibilityLabel = newValue
}
}
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 super.accessibilityHint
}
}
set {
super.accessibilityHint = newValue
}
}
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 super.accessibilityValue
}
}
set {
super.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
control.toggle()
}
bridge_accessibilityLabelBlock = { [weak self] in
guard let self else { return "" }
return "\(Self.self)\(showError ? ", error" : "")"
}
bridge_accessibilityHintBlock = { [weak self] in
guard let self else { return "" }
return !isEnabled ? "" : "Double tap to activate."
}
}
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
@ -119,12 +129,6 @@ open class SelectorBase: Control, SelectorControlable {
setNeedsLayout()
layoutIfNeeded()
}
/// Used to update any Accessibility properties.ß
open override func updateAccessibility() {
super.updateAccessibility()
accessibilityLabel = "\(Self.self)\(showError ? ", error" : "")"
}
/// This will change the state of the Selector and execute the actionBlock if provided.
open func toggle() { }
@ -133,4 +137,36 @@ open class SelectorBase: Control, SelectorControlable {
super.reset()
onChange = nil
}
open override func accessibilityActivate() -> Bool {
guard isEnabled, isUserInteractionEnabled else { return false }
guard isEnabled, isUserInteractionEnabled else { return false }
var value = true
// if #available(iOS 17, *) {
// if let block = accessibilityAction {
// block(self)
//
// } else if let block = accessibilityActivateBlock {
// value = block()
//
// } else if let block = bridge_accessibilityActivateBlock {
// value = block()
//
// } else {
// toggle()
// }
// } else {
if let block = accessibilityAction {
block(self)
} else if let block = bridge_accessibilityActivateBlock {
value = block()
} else {
toggle()
}
// }
return value
}
}

View File

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

View File

@ -11,7 +11,7 @@ import Combine
import VDSCoreTokens
/// 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
@ -145,7 +145,14 @@ open class SelectorItemBase<Selector: SelectorControlable>: Control, Errorable,
open var hiddenValue: AnyHashable? { didSet { setNeedsUpdate() } }
open var accessibilityValueText: String?
open override var accessibilityAction: ((Control) -> Void)? {
didSet {
selectorView.accessibilityAction = { [weak self] selectorItemBase in
guard let self else { return }
accessibilityAction?(self)
}
}
}
//--------------------------------------------------
// MARK: - Overrides
@ -153,21 +160,61 @@ open class SelectorItemBase<Selector: SelectorControlable>: Control, Errorable,
/// Executed on initialization for this View.
open override func initialSetup() {
super.initialSetup()
onClick = { control in
control.toggle()
onClick = { [weak self] control in
guard let self, isEnabled else { return }
toggle()
}
selectorView.accessibilityAction = { [weak self] _ in
guard let self, isEnabled else { return }
toggle()
}
selectorView.bridge_accessibilityLabelBlock = { [weak self ] in
guard let self else { return "" }
var accessibilityLabels = [String]()
if isSelected {
accessibilityLabels.append("selected")
}
accessibilityLabels.append("\(Selector.self)")
if let text = labelText, !text.isEmpty {
accessibilityLabels.append(text)
}
if let text = childText, !text.isEmpty {
accessibilityLabels.append(text)
}
if !isEnabled {
accessibilityLabels.append("dimmed")
}
if let errorText, showError, !errorText.isEmpty {
accessibilityLabels.append("error, \(errorText)")
}
return accessibilityLabels.joined(separator: ", ")
}
selectorView.bridge_accessibilityHintBlock = { [weak self] in
guard let self else { return "" }
return !isEnabled ? "" : "Double tap to activate."
}
}
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
open override func setup() {
super.setup()
selectorView.isAccessibilityElement = false
isAccessibilityElement = true
accessibilityTraits = .button
selectorView.isAccessibilityElement = true
isAccessibilityElement = false
addSubview(mainStackView)
mainStackView.isUserInteractionEnabled = false
mainStackView.isUserInteractionEnabled = false
mainStackView.addArrangedSubview(selectorStackView)
mainStackView.addArrangedSubview(errorLabel)
selectorStackView.addArrangedSubview(selectorView)
@ -191,14 +238,47 @@ open class SelectorItemBase<Selector: SelectorControlable>: Control, Errorable,
selectorView.isEnabled = isEnabled
selectorView.surface = surface
}
/// Used to update any Accessibility properties.
open override func updateAccessibility() {
super.updateAccessibility()
setAccessibilityLabel(for: [selectorView, label, childLabel, errorLabel])
accessibilityValue = accessibilityValueText
open override var accessibilityElements: [Any]? {
get {
var elements = [Any]()
elements.append(selectorView)
if let text = labelText, !text.isEmpty {
elements.append(label)
}
if let text = childText, !text.isEmpty {
elements.append(childLabel)
}
if let errorText, showError, !errorText.isEmpty {
elements.append(errorLabel)
}
return elements
}
set {
super.accessibilityElements = newValue
}
}
/// Overriden to take the hit if there is an onClickSubscriber and the view is not a UIControl
open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard isEnabled else { return super.hitTest(point, with: event) }
let labelPoint = convert(point, to: label)
let childLabelPoint = convert(point, to: childLabel)
if label.isAction(for: labelPoint) {
return label
} else if childLabel.isAction(for: childLabelPoint) {
return childLabel
} else {
guard !UIAccessibility.isVoiceOverRunning else { return nil }
return super.hitTest(point, with: event)
}
}
/// Resets to default settings.
open override func reset() {
super.reset()
@ -290,4 +370,34 @@ open class SelectorItemBase<Selector: SelectorControlable>: Control, Errorable,
/// This will change to state of the Selector.
open func toggle() {}
open override func accessibilityActivate() -> Bool {
guard isEnabled, isUserInteractionEnabled else { return false }
var value = true
// if #available(iOS 17, *) {
// if let block = accessibilityAction {
// block(self)
//
// } else if let block = accessibilityActivateBlock {
// value = block()
//
// } else if let block = bridge_accessibilityActivateBlock {
// value = block()
//
// } else {
// toggle()
// }
// } else {
if let block = accessibilityAction {
block(self)
} else if let block = bridge_accessibilityActivateBlock {
value = block()
} else {
toggle()
}
// }
return value
}
}

View File

@ -52,7 +52,7 @@ open class View: UIView, ViewProtocol, UserInfoable {
open var surface: Surface = .light { didSet { setNeedsUpdate() } }
open var isEnabled: Bool = true { didSet { setNeedsUpdate() } }
//--------------------------------------------------
// MARK: - Lifecycle
//--------------------------------------------------
@ -91,4 +91,132 @@ open class View: UIView, ViewProtocol, UserInfoable {
setNeedsUpdate()
}
//--------------------------------------------------
// MARK: - Accessibility
//--------------------------------------------------
open var accessibilityAction: ((View) -> Void)?
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 super.isAccessibilityElement
}
}
set {
super.isAccessibilityElement = newValue
}
}
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 super.accessibilityLabel
}
}
set {
super.accessibilityLabel = newValue
}
}
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 super.accessibilityHint
}
}
set {
super.accessibilityHint = newValue
}
}
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 super.accessibilityValue
}
}
set {
super.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

@ -0,0 +1,96 @@
//
// AlertViewController.swift
// VDS
//
// Created by Matt Bruce on 6/24/24.
//
import Foundation
import UIKit
import Combine
import VDSCoreTokens
open class AlertViewController: UIViewController, Surfaceable {
/// Set of Subscribers for any Publishers for this Control.
open var subscribers = Set<AnyCancellable>()
//--------------------------------------------------
// MARK: - Private Properties
//--------------------------------------------------
private var onClickSubscriber: AnyCancellable? {
willSet {
if let onClickSubscriber {
onClickSubscriber.cancel()
}
}
}
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
/// Current Surface and this is used to pass down to child objects that implement Surfacable
open var surface: Surface = .light { didSet { updateView() }}
open var presenter: UIView? { didSet { updateView() }}
open var dialog: UIView!
//--------------------------------------------------
// MARK: - Configuration
//--------------------------------------------------
private let backgroundColorConfiguration = SurfaceColorConfiguration(VDSColor.backgroundPrimaryDark, VDSColor.backgroundPrimaryLight)
//--------------------------------------------------
// MARK: - Lifecycle
//--------------------------------------------------
open override func viewDidLoad() {
super.viewDidLoad()
isModalInPresentation = true
setup()
}
open override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
UIAccessibility.post(notification: .screenChanged, argument: dialog)
}
private func dismiss() {
dismiss(animated: true) { [weak self] in
guard let self, let presenter else { return }
UIAccessibility.post(notification: .layoutChanged, argument: presenter)
}
}
open func setup() {
guard let dialog else { return }
view.accessibilityElements = [dialog]
view.addSubview(dialog)
// Activate constraints
NSLayoutConstraint.activate([
// Constraints for the floating modal view
dialog.centerXAnchor.constraint(equalTo: view.centerXAnchor),
dialog.centerYAnchor.constraint(equalTo: view.centerYAnchor),
dialog.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor, constant: 10),
dialog.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor, constant: -10),
dialog.topAnchor.constraint(greaterThanOrEqualTo: view.topAnchor, constant: 10),
dialog.bottomAnchor.constraint(lessThanOrEqualTo: view.bottomAnchor, constant: -10)
])
}
open override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location = touch.location(in: view)
if dialog.frame.contains(location) {
super.touchesBegan(touches, with: event)
} else {
dismiss()
}
}
/// Used to make changes to the View based off a change events or from local properties.
open func updateView() {
view.backgroundColor = backgroundColorConfiguration.getColor(self).withAlphaComponent(0.3)
if var dialog = dialog as? Surfaceable {
dialog.surface = surface
}
}
}

View File

@ -0,0 +1,152 @@
//
// DatePickerPopoverViewController.swift
// VDS
//
// Created by Matt Bruce on 5/14/24.
//
import Foundation
import UIKit
open class ClearPopoverViewController: UIViewController, UIPopoverPresentationControllerDelegate {
/// The view to be inserted inside the popover
private var contentView: UIView!
/// An object representing the arrow of the popover.
private var arrow: UIPopoverArrowDirection
/// Popover presentation controller of the popover
private var popOver: UIPopoverPresentationController!
open var maxWidth: CGFloat?
open var sourceRect: CGRect?
open var spacing: CGFloat = 0
/**
A controller that manages the popover.
- Parameter contentView: The view to be inserted inside the popover.
- Parameter design: An object used for defining visual attributes of the popover.
- Parameter arrow: An object representing the arrow in popover.
- Parameter sourceView: The view containing the anchor rectangle for the popover.
- Parameter sourceRect: The rectangle in the specified view in which to anchor the popover.
- Parameter barButtonItem: The bar button item on which to anchor the popover.
Assign a value to `barButton` to anchor the popover to the specified bar button item. When presented, the popovers arrow points to the specified item. Alternatively, you may specify the anchor location for the popover using the `sourceView` and `sourceRect` properties.
*/
public init(contentView: UIView, arrow: UIPopoverArrowDirection, sourceView: UIView? = nil, sourceRect: CGRect? = nil, spacing: CGFloat = 0, barButtonItem: UIBarButtonItem? = nil) {
self.contentView = contentView
self.spacing = spacing
self.arrow = arrow
self.sourceRect = sourceRect
super.init(nibName: nil, bundle: nil)
setupPopover(sourceView, sourceRect, barButtonItem)
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
open override func viewIsAppearing(_ animated: Bool) {
super.viewIsAppearing(animated)
view.superview?.accessibilityIdentifier = "HadCornerRadius"
view.accessibilityIdentifier = "PopoverViewController.View"
contentView.accessibilityIdentifier = "PopoverViewController.ContentView"
view.superview?.layer.cornerRadius = 0
}
open override func viewDidLayoutSubviews() {
contentView.frame.origin = CGPoint(x: 0, y: 0)
}
///Sets up the Popover and starts the timer for its closing.
private func setupPopover(_ sourceView: UIView?, _ sourceRect: CGRect?, _ barButtonItem: UIBarButtonItem?) {
modalPresentationStyle = .popover
view.addSubview(contentView)
popOver = self.popoverPresentationController!
popOver.popoverLayoutMargins = .zero
popOver.popoverBackgroundViewClass = ClearPopoverBackgroundView.self
popOver.sourceView = sourceView
popOver.popoverLayoutMargins = .zero
if let sourceRect = sourceRect {
popOver.sourceRect = sourceRect
}
popOver.barButtonItem = barButtonItem
popOver.delegate = self
popOver.permittedArrowDirections = arrow
popOver.backgroundColor = .clear
}
open func popoverPresentationController(_ popoverPresentationController: UIPopoverPresentationController, willRepositionPopoverTo rect: UnsafeMutablePointer<CGRect>, in view: AutoreleasingUnsafeMutablePointer<UIView>) {
if let presentedView = popoverPresentationController.presentedViewController.view.superview {
presentedView.layer.cornerRadius = 0
}
}
private func updatePopoverPosition() {
guard let popoverPresentationController = popoverPresentationController else { return }
if let sourceView = popoverPresentationController.sourceView, let sourceRect {
popoverPresentationController.sourceRect = sourceRect
}
}
// Ensure to handle rotations
open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: { [weak self] _ in
self?.updatePopoverPosition()
})
}
open func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
return .none
}
// Returns presentation controller of the popover
open func getPopoverPresentationController() -> UIPopoverPresentationController {
return popOver
}
}
open class ClearPopoverBackgroundView: UIPopoverBackgroundView {
open override var arrowOffset: CGFloat {
get { 0 }
set { }
}
open override var arrowDirection: UIPopoverArrowDirection {
get { .any }
set { }
}
open override class var wantsDefaultContentAppearance: Bool {
false
}
open override class func contentViewInsets() -> UIEdgeInsets{
.zero
}
open override class func arrowHeight() -> CGFloat {
0
}
open override class func arrowBase() -> CGFloat{
0
}
open override func layoutSubviews() {
super.layoutSubviews()
layer.shadowOpacity = 0
layer.shadowRadius = 0
layer.cornerRadius = 0
}
open override func draw(_ rect: CGRect) {
}
}

View File

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

View File

@ -292,6 +292,16 @@ open class BadgeIndicator: View {
label.centerYAnchor.constraint(equalTo: badgeView.centerYAnchor).isActive = true
labelContraints.isActive = true
bridge_accessibilityLabelBlock = { [weak self] in
guard let self else { return "" }
if let accessibilityText {
return kind == .numbered ? label.text + " " + accessibilityText : accessibilityText
} else if kind == .numbered {
return label.text
} else {
return "Simple"
}
}
}
/// Resets to default settings.
@ -347,17 +357,6 @@ open class BadgeIndicator: View {
setNeedsLayout()
}
open override func updateAccessibility() {
super.updateAccessibility()
if let accessibilityText {
accessibilityLabel = kind == .numbered ? label.text + " " + accessibilityText : accessibilityText
} else if kind == .numbered {
accessibilityLabel = label.text
} else {
accessibilityLabel = "Simple"
}
}
open override func layoutSubviews() {
super.layoutSubviews()

View File

@ -82,6 +82,15 @@ open class BreadcrumbItem: ButtonBase {
isAccessibilityElement = true
accessibilityTraits = .link
bridge_accessibilityHintBlock = { [weak self] in
guard let self else { return "" }
return !isEnabled ? "" : "Double tap to open."
}
bridge_accessibilityLabelBlock = { [weak self] in
guard let self else { return "" }
return text
}
}
/// Used to make changes to the View based off a change events or from local properties.
@ -134,10 +143,4 @@ open class BreadcrumbItem: ButtonBase {
setNeedsUpdate()
}
/// Used to update any Accessibility properties.
open override func updateAccessibility() {
super.updateAccessibility()
accessibilityLabel = text
}
}

View File

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

View File

@ -105,6 +105,12 @@ open class TextLink: ButtonBase {
lineHeightConstraint = line.height(constant: 1)
lineHeightConstraint?.isActive = true
}
bridge_accessibilityHintBlock = { [weak self] in
guard let self else { return "" }
return !isEnabled ? "" : "Double tap to open."
}
}
/// Used to make changes to the View based off a change events or from local properties.

View File

@ -86,6 +86,12 @@ open class TextLinkCaret: ButtonBase {
accessibilityTraits = .link
titleLabel?.numberOfLines = 0
titleLabel?.lineBreakMode = .byWordWrapping
bridge_accessibilityHintBlock = { [weak self] in
guard let self else { return "" }
return !isEnabled ? "" : "Double tap to open."
}
}
/// Used to make changes to the View based off a change events or from local properties.

View File

@ -160,7 +160,7 @@ open class CalendarBase: Control, Changeable {
if (minDate <= maxDate) {
// Check if current date falls between min & max dates.
let fallsBetween = displayDate.isBetweeen(date: minDate, andDate: maxDate)
displayDate = fallsBetween ? displayDate : minDate
displayDate = fallsBetween ? displayDate : (displayDate.monthInt == minDate.monthInt) ? minDate : maxDate
fetchDates(with: displayDate)
}
containerView.backgroundColor = transparentBackground ? .clear : backgroundColorConfiguration.getColor(self)
@ -201,7 +201,7 @@ open class CalendarBase: Control, Changeable {
}
}
updateViewConstraints()
}
}
func updateViewConstraints() {
collectionView.reloadData()
@ -331,38 +331,28 @@ extension CalendarBase: UICollectionViewDelegate, UICollectionViewDataSource, UI
}
}
public func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool {
if let cell = collectionView.cellForItem(at: indexPath) as? CalendarDateViewCell {
let isEnabled: Bool = cell.isDateEnabled()
if isEnabled {
cell.activeModeStart()
}
}
return true
}
public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// reload selected index, if it is in enabled state.
if let cell = collectionView.cellForItem(at: indexPath) as? CalendarDateViewCell {
let isEnabled: Bool = cell.isDateEnabled()
if isEnabled {
cell.activeModeEnd()
// Callback to pass selected date if it is enabled only.
selectedDate = dates[indexPath.row]
sendActions(for: .valueChanged)
displayDate = selectedDate
var reloadIndexPaths = [indexPath]
// If an cell is already selected, then it needs to be deselected.
// Add its index path to the array of index paths to be reloaded.
if let deselectIndexPath = selectedIndexPath {
reloadIndexPaths.append(deselectIndexPath)
let hasDate: Bool = cell.hasText()
if hasDate {
let isEnabled: Bool = cell.isDateEnabled()
if isEnabled {
// Callback to pass selected date if it is enabled only.
selectedDate = dates[indexPath.row]
sendActions(for: .valueChanged)
displayDate = selectedDate
var reloadIndexPaths = [indexPath]
// If an cell is already selected, then it needs to be deselected.
// Add its index path to the array of index paths to be reloaded.
if let deselectIndexPath = selectedIndexPath {
reloadIndexPaths.append(deselectIndexPath)
}
collectionView.reloadItems(at: reloadIndexPaths)
}
collectionView.reloadItems(at: reloadIndexPaths)
}
}
}

View File

@ -41,6 +41,21 @@ final class CalendarDateViewCell: UICollectionViewCell {
$0.textStyle = .bodySmall
}
override var isHighlighted: Bool {
didSet{
if self.isHighlighted && hasText() && isDateEnabled() {
self.contentView.layer.borderColor = activeBorderColorConfiguration.getColor(surface).cgColor
self.contentView.layer.borderWidth = VDSFormControls.borderWidth
self.contentView.layer.cornerRadius = VDSFormControls.borderRadius
} else {
self.contentView.layer.borderColor = nil
self.contentView.layer.borderWidth = 0
self.contentView.layer.cornerRadius = 0
}
}
}
private var isEnabled = false
private lazy var shapeLayer = CAShapeLayer()
private var surface: Surface = .light
private let selectedTextColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryInverseOnlight, VDSColor.elementsPrimaryInverseOndark)
@ -120,20 +135,21 @@ final class CalendarDateViewCell: UICollectionViewCell {
}
}
// update text color, bg color, corner radius.
if numberLabel.text == selectedDate.getDay()
&& selectedDate.monthInt == displayDate.monthInt
&& selectedDate.yearInt == displayDate.yearInt
&& numberLabel.isEnabled {
numberLabel.textColor = selectedTextColorConfiguration.getColor(surface)
layer.backgroundColor = selectedBackgroundColor.getColor(surface).cgColor
layer.cornerRadius = VDSFormControls.borderRadius
} else {
numberLabel.textColor = unselectedTextColorConfiguration.getColor(surface)
layer.backgroundColor = nil
layer.cornerRadius = 0
// Set selected/unselected state text color, bg color, corner radius if cell is in enabled state.
if isEnabled {
if numberLabel.text == selectedDate.getDay()
&& selectedDate.monthInt == displayDate.monthInt
&& selectedDate.yearInt == displayDate.yearInt {
numberLabel.textColor = selectedTextColorConfiguration.getColor(surface)
layer.backgroundColor = selectedBackgroundColor.getColor(surface).cgColor
layer.cornerRadius = VDSFormControls.borderRadius
} else {
numberLabel.textColor = unselectedTextColorConfiguration.getColor(surface)
layer.backgroundColor = nil
layer.cornerRadius = 0
}
}
// add indicators.
@ -155,26 +171,18 @@ final class CalendarDateViewCell: UICollectionViewCell {
numberLabel.textStyle = .bodySmall
}
}
func hasText() -> Bool {
return !numberLabel.text.isEmpty
}
// returns cell enabled state.
func isDateEnabled() -> Bool {
return numberLabel.isEnabled
}
func activeModeStart() {
numberLabel.layer.borderColor = activeBorderColorConfiguration.getColor(surface).cgColor
numberLabel.layer.borderWidth = VDSFormControls.borderWidth
numberLabel.layer.cornerRadius = VDSFormControls.borderRadius
}
func activeModeEnd() {
numberLabel.layer.borderColor = nil
numberLabel.layer.borderWidth = 0
numberLabel.layer.cornerRadius = 0
return isEnabled
}
func disableLabel(with surface: Surface) {
numberLabel.isEnabled = false
isEnabled = false
numberLabel.textColor = disabledTextColorConfiguration.getColor(surface)
layer.backgroundColor = disabledBackgroundColor.getColor(surface).cgColor
}
@ -183,7 +191,7 @@ final class CalendarDateViewCell: UICollectionViewCell {
for x in 0...activeDates.count-1 {
if activeDates[x].monthInt == displayDate.monthInt && activeDates[x].yearInt == displayDate.yearInt {
if let day:Int = Int(numberLabel.text), day == activeDates[x].dayInt {
numberLabel.isEnabled = true
isEnabled = true
}
}
}
@ -194,7 +202,7 @@ final class CalendarDateViewCell: UICollectionViewCell {
if activeDates.count > 0 && inactiveDates.count == 0 {
showActiveDates(with: displayDate, activeDates: activeDates, inactiveDates: inactiveDates)
} else {
numberLabel.isEnabled = true
isEnabled = true
}
}
@ -204,7 +212,7 @@ final class CalendarDateViewCell: UICollectionViewCell {
disableLabel(with: surface)
showActiveDates(with: displayDate, activeDates: activeDates, inactiveDates: inactiveDates)
} else {
numberLabel.isEnabled = true
isEnabled = true
}
}
@ -213,7 +221,7 @@ final class CalendarDateViewCell: UICollectionViewCell {
if let day = Int(numberLabel.text), day < minDate.dayInt {
disableLabel(with: surface)
} else {
numberLabel.isEnabled = false
isEnabled = false
handleActiveDates(with: displayDate, activeDates: activeDates, inactiveDates: inactiveDates)
}
}
@ -223,7 +231,7 @@ final class CalendarDateViewCell: UICollectionViewCell {
if let day = Int(numberLabel.text), day > maxDate.dayInt {
disableLabel(with: surface)
} else {
numberLabel.isEnabled = false
isEnabled = false
handleActiveDates(with: displayDate, activeDates: activeDates, inactiveDates: inactiveDates)
}
}
@ -233,7 +241,7 @@ final class CalendarDateViewCell: UICollectionViewCell {
if let day = Int(numberLabel.text), day < minDate.dayInt || day > maxDate.dayInt {
disableLabel(with: surface)
} else {
numberLabel.isEnabled = false
isEnabled = false
handleActiveDates(with: displayDate, activeDates: activeDates, inactiveDates: inactiveDates)
}
}

View File

@ -224,7 +224,7 @@ private class LegendCollectionViewCell: UICollectionViewCell {
title.text = text
title.textColor = textColorConfiguration.getColor(surface)
legendIndicator.backgroundColor = drawSemiCircle ? .clear : (clearFullcircle ? .clear : color)
legendIndicator.backgroundColor = drawSemiCircle ? .clear : (clearFullcircle ? .clear : indicatorColorConfiguration.getColor(surface))
legendIndicator.layer.borderColor = indicatorColorConfiguration.getColor(surface).cgColor
self.layoutIfNeeded()
@ -239,7 +239,7 @@ private class LegendCollectionViewCell: UICollectionViewCell {
path.addArc(withCenter: center, radius: center.x, startAngle: 2 * .pi, endAngle: .pi, clockwise: true)
path.close()
shapeLayer.path = path.cgPath
shapeLayer.fillColor = color.cgColor
shapeLayer.fillColor = indicatorColorConfiguration.getColor(surface).cgColor
guard legendIndicator.layer.sublayers?.contains(shapeLayer) ?? true else { return }
legendIndicator.layer.addSublayer(shapeLayer)

View File

@ -68,16 +68,16 @@ class CalendarHeaderReusableView: UICollectionReusableView {
$0.kind = .ghost
$0.iconName = .leftCaret
$0.iconOffset = .init(x: -2, y: 0)
$0.icon.size = .small
$0.size = .small
$0.customContainerSize = 40
$0.icon.customSize = 16
}
internal var nextButton = ButtonIcon().with {
$0.kind = .ghost
$0.iconName = .rightCaret
$0.iconOffset = .init(x: 2, y: 0)
$0.icon.size = .small
$0.size = .small
$0.customContainerSize = 40
$0.icon.customSize = 16
}
internal var headerTitle = Label().with {

View File

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

View File

@ -47,8 +47,6 @@ open class CheckboxGroup: SelectorGroupBase<CheckboxItem>, SelectorGroupMultiSel
$0.surface = model.surface
$0.inputId = model.inputId
$0.hiddenValue = model.value
$0.accessibilityLabel = model.accessibileText
$0.accessibilityValueText = "item \(index+1) of \(selectorModels.count)"
$0.labelText = model.labelText
$0.labelTextAttributes = model.labelTextAttributes
$0.childText = model.childText
@ -56,6 +54,7 @@ open class CheckboxGroup: SelectorGroupBase<CheckboxItem>, SelectorGroupMultiSel
$0.isSelected = model.selected
$0.errorText = model.errorText
$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.
open override func toggle() {
guard isEnabled else { return }
//removed error
if showError && isSelected == false {
showError.toggle()

View File

@ -5,7 +5,7 @@ import Combine
/// A dropdown select is an expandable menu of predefined options that allows a customer to make a single selection.
@objc(VDSDatePicker)
open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopoverPresentationControllerDelegate {
open class DatePicker: EntryFieldBase {
//--------------------------------------------------
// MARK: - Initializers
//--------------------------------------------------
@ -26,12 +26,19 @@ open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopov
//--------------------------------------------------
/// A callback when the selected option changes. Passes parameters (option).
open var onDateSelected: ((Date, DatePicker) -> Void)?
//--------------------------------------------------
// MARK: - Private Properties
//--------------------------------------------------
class Responder: UIView {
open override var canBecomeFirstResponder: Bool {
true
}
}
internal override var responder: UIResponder? { hiddenView }
internal var hiddenView = Responder().with { $0.width(0) }
internal var minWidthDefault = 186.0
internal var bottomStackView: UIStackView = {
return UIStackView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
@ -41,6 +48,33 @@ open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopov
$0.spacing = VDSLayout.space2X
}
}()
//--------------------------------------------------
// MARK: - Private Popover/Alert Properties
//--------------------------------------------------
/// View shown inline
internal var popoverOverlayView = UIView().with {
$0.backgroundColor = .clear
$0.translatesAutoresizingMaskIntoConstraints = false
}
/// use this to track touch events outside of the popover in the overlay
internal var popupOverlayTapGesture: AnyCancellable?
/// View shown inline
internal var popoverView: UIView!
/// Size used for the popover
internal var popoverViewSize: CGSize = .zero
/// Spacing between the popover and the ContainerView when not a AlertViewController
internal var popoverSpacing: CGFloat = VDSLayout.space1X
/// Whether or not the popover is visible
internal var popoverVisible = false
/// If the ContainerView exists somewhere in the superview hierarch in a ScrollView.
internal var scrollView: UIScrollView?
/// Original Found ScrollView ContentSize, this will get reset back to this size when the Popover is removed.
internal var scrollViewContentSize: CGSize?
/// Presenting ViewController with showing the AlertViewController Version.
internal var topViewController: UIViewController?
//--------------------------------------------------
// MARK: - Public Properties
@ -87,12 +121,12 @@ open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopov
}
open var dateFormat: DateFormat = .shortNumeric { didSet{ setNeedsUpdate() } }
//--------------------------------------------------
// MARK: - Configuration Properties
//--------------------------------------------------
internal override var containerSize: CGSize { CGSize(width: minWidthDefault, height: 44) }
//--------------------------------------------------
// MARK: - Overrides
//--------------------------------------------------
@ -100,24 +134,29 @@ open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopov
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
open override func setup() {
super.setup()
fieldStackView.isAccessibilityElement = true
fieldStackView.accessibilityLabel = "Date Picker"
fieldStackView.accessibilityHint = "Double Tap to open"
// setting color config
selectedDateLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable()
// tap gesture
fieldStackView
containerView
.publisher(for: UITapGestureRecognizer())
.sink { [weak self] _ in
guard let self else { return }
if self.isEnabled && !self.isReadOnly {
self.togglePicker()
if isEnabled && !isReadOnly {
showPopover()
}
}
.store(in: &subscribers)
NotificationCenter.default
.publisher(for: UIDevice.orientationDidChangeNotification).sink { [weak self] _ in
guard let self else { return }
hidePopoverView()
}
.store(in: &subscribers)
popoverOverlayView.isHidden = true
}
open override func getFieldContainer() -> UIView {
@ -129,9 +168,10 @@ open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopov
}
controlStackView.addArrangedSubview(calendarIcon)
controlStackView.addArrangedSubview(selectedDateLabel)
controlStackView.addArrangedSubview(hiddenView)
return controlStackView
}
/// Used to make changes to the View based off a change events or from local properties.
open override func updateView() {
super.updateView()
@ -145,13 +185,6 @@ open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopov
calendarIcon.color = iconColorConfiguration.getColor(self)
}
open override func updateAccessibility() {
super.updateAccessibility()
fieldStackView.accessibilityLabel = "Date Picker, \(accessibilityLabelText)"
fieldStackView.accessibilityHint = isReadOnly || !isEnabled ? "" : "Double tap to open."
fieldStackView.accessibilityValue = value
}
/// Resets to default settings.
open override func reset() {
super.reset()
@ -163,32 +196,249 @@ open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopov
formatter.dateFormat = dateFormat.format
selectedDateLabel.text = formatter.string(from: date)
}
}
internal func togglePicker() {
let calendarVC = DatePickerViewController(calendarModel, delegate: self)
calendarVC.modalPresentationStyle = .popover
calendarVC.selectedDate = selectedDate ?? Date()
if let popoverController = calendarVC.popoverPresentationController {
popoverController.delegate = self
popoverController.sourceView = containerView
popoverController.sourceRect = containerView.bounds
popoverController.permittedArrowDirections = .up
extension DatePicker {
private func showPopover() {
guard let viewController = UIApplication.topViewController(), var parentView = viewController.view, !popoverVisible else {
hidePopoverView()
return
}
if let viewController = UIApplication.topViewController() {
viewController.present(calendarVC, animated: true, completion: nil)
let calendar = CalendarBase()
calendar.activeDates = calendarModel.activeDates
calendar.hideContainerBorder = calendarModel.hideContainerBorder
calendar.hideCurrentDateIndicator = calendarModel.hideCurrentDateIndicator
calendar.inactiveDates = calendarModel.inactiveDates
calendar.indicators = calendarModel.indicators
calendar.maxDate = calendarModel.maxDate
calendar.minDate = calendarModel.minDate
calendar.surface = calendarModel.surface
calendar.setNeedsLayout()
calendar.layoutIfNeeded()
//size the popover
popoverViewSize = .init(width: calendar.frame.width, height: calendar.frame.height)
//find scrollView
if scrollView == nil {
scrollView = findScrollView(from: containerView)
scrollViewContentSize = scrollView?.contentSize
}
if let scrollView {
parentView = scrollView
}
// see if you should use the popover or show an alert
if let popoverOrigin = calculatePopoverPosition(relativeTo: containerView,
in: parentView,
size: popoverViewSize,
with: popoverSpacing) {
calendar.onChange = { [weak self] control in
guard let self else { return }
selectedDate = control.selectedDate
sendActions(for: .valueChanged)
UIAccessibility.post(notification: .layoutChanged, argument: containerView)
hidePopoverView()
}
// popoverView container
popoverView = UIView()
popoverView.backgroundColor = .clear
popoverView.frame = CGRect(x: popoverOrigin.x, y: popoverOrigin.y, width: calendar.frame.width, height: calendar.frame.height)
popoverView.alpha = 0
popoverView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
popoverVisible = true
popoverView.addSubview(calendar)
calendar.pinToSuperView()
// add views
popoverOverlayView.isHidden = false
popupOverlayTapGesture = popoverOverlayView
.publisher(for: UITapGestureRecognizer())
.sink(receiveValue: { [weak self] gesture in
guard let self else { return }
gestureEventOccured(gesture, parentView: parentView)
})
parentView.addSubview(popoverOverlayView)
popoverOverlayView.pinToSuperView()
parentView.addSubview(popoverView)
parentView.layoutIfNeeded()
// update containerview
_ = responder?.becomeFirstResponder()
updateContainerView()
// animate the calendar to show
UIView.animate(withDuration: 0.5,
delay: 0,
usingSpringWithDamping: 0.8,
initialSpringVelocity: 0.2,
options: .curveEaseOut,
animations: { [weak self] in
guard let self else { return }
popoverView.alpha = 1
popoverView.transform = CGAffineTransform.identity
UIAccessibility.post(notification: .layoutChanged, argument: calendar)
parentView.layoutIfNeeded()
})
} else {
let dialog = UIScrollView()
dialog.translatesAutoresizingMaskIntoConstraints = false
dialog.addSubview(calendar)
dialog.backgroundColor = .clear
dialog.contentSize = .init(width: calendar.frame.width + 20, height: calendar.frame.width + 20)
dialog.width(calendar.frame.width + 20)
dialog.height(calendar.frame.height + 20)
calendar.pinToSuperView(.uniform(10))
calendar.onChange = { [weak self] control in
guard let self else { return }
selectedDate = control.selectedDate
sendActions(for: .valueChanged)
UIAccessibility.post(notification: .layoutChanged, argument: containerView)
viewController.dismiss(animated: true)
}
let alert = AlertViewController().with {
$0.dialog = dialog
$0.modalPresentationStyle = .overCurrentContext
$0.modalTransitionStyle = .crossDissolve
}
topViewController = viewController
viewController.present(alert, animated: true){
dialog.flashScrollIndicators()
}
}
}
private func hidePopoverView() {
if topViewController != nil {
topViewController?.dismiss(animated: true)
topViewController = nil
} else {
popoverOverlayView.isHidden = true
popoverOverlayView.removeFromSuperview()
popupOverlayTapGesture?.cancel()
popupOverlayTapGesture = nil
UIView.animate(withDuration: 0.2,
animations: {[weak self] in
guard let self, let popoverView else { return }
popoverView.alpha = 0
popoverView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
if let scrollView, let scrollViewContentSize {
scrollView.contentSize = scrollViewContentSize
}
}) { [weak self] _ in
guard let self, let popoverView else { return }
popoverView.isHidden = true
popoverView.removeFromSuperview()
popoverVisible = false
responder?.resignFirstResponder()
setNeedsUpdate()
UIAccessibility.post(notification: .layoutChanged, argument: containerView)
}
}
}
internal func didSelectDate(_ controller: DatePickerViewController, date: Date) {
selectedDate = date
controller.dismiss(animated: true) { [weak self] in
guard let self else { return }
self.sendActions(for: .valueChanged)
UIAccessibility.post(notification: .layoutChanged, argument: self.fieldStackView)
private func findScrollView(from view: UIView) -> UIScrollView? {
var currentView = view
while let superview = currentView.superview {
if let scrollView = superview as? UIScrollView {
return scrollView
}
currentView = superview
}
return nil
}
public func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return .none
private func calculatePopoverPosition(relativeTo sourceView: UIView, in parentView: UIView, size: CGSize, with spacing: CGFloat) -> CGPoint? {
let sourceFrameInParent = sourceView.convert(sourceView.bounds, to: parentView)
let parentBounds = parentView.bounds
let safeAreaInsets = parentView.safeAreaInsets
let popoverWidth = size.width
let popoverHeight = size.height
var popoverX: CGFloat = 0
var popoverY: CGFloat = 0
// Calculate horizontal position
if sourceFrameInParent.width <= popoverWidth {
if sourceFrameInParent.midX - popoverWidth / 2 < 0 {
// Align to left
popoverX = sourceFrameInParent.minX
} else if sourceFrameInParent.midX + popoverWidth / 2 > parentBounds.width {
// Align to right
popoverX = sourceFrameInParent.maxX - popoverWidth
} else {
// Center on source view
popoverX = sourceFrameInParent.midX - popoverWidth / 2
}
} else {
popoverX = sourceFrameInParent.minX //sourceFrameInParent.midX - popoverWidth / 2
}
// Ensure the popover is within the parent's bounds horizontally
popoverX = max(0, min(popoverX, parentBounds.width - popoverWidth))
var availableSpaceAbove: CGFloat = 0.0
var availableSpaceBelow: CGFloat = 0.0
/// if the scrollView is set we want to change how we calculate the containerView's position
if let scrollView = parentView as? UIScrollView {
// Calculate vertical position and height
availableSpaceAbove = sourceFrameInParent.minY - scrollView.bounds.minY - spacing
availableSpaceBelow = scrollView.bounds.maxY - sourceFrameInParent.maxY - spacing
if availableSpaceAbove > availableSpaceBelow {
// Show above
popoverY = sourceFrameInParent.minY - popoverHeight - spacing
} else {
// Show below
popoverY = sourceFrameInParent.maxY + spacing
// See if we need to expand the contentSize of the ScrollView
let diff = scrollView.contentSize.height - sourceFrameInParent.maxY
if diff < popoverHeight {
scrollView.contentSize.height += popoverHeight - diff + VDSLayout.space4X
}
}
} else {
// Calculate vertical position and height
availableSpaceAbove = sourceFrameInParent.minY - safeAreaInsets.top - spacing
availableSpaceBelow = parentBounds.height - sourceFrameInParent.maxY - safeAreaInsets.bottom - spacing
if availableSpaceAbove >= popoverHeight {
// Show above
popoverY = sourceFrameInParent.minY - popoverHeight - spacing
} else if availableSpaceBelow >= popoverHeight {
// Show below
popoverY = sourceFrameInParent.maxY + spacing
} else {
//return nil since there is no way we can show the popover without a scrollview
return nil
}
}
return .init(x: popoverX, y: popoverY)
}
private func gestureEventOccured(_ gesture: UIGestureRecognizer, parentView: UIView) {
guard let popoverView, popoverVisible else { return }
let location = gesture.location(in: parentView)
if !popoverView.frame.contains(location) {
hidePopoverView()
}
}
}

View File

@ -1,71 +0,0 @@
//
// DatePickerPopoverViewController.swift
// VDS
//
// Created by Matt Bruce on 5/14/24.
//
import Foundation
import UIKit
protocol DatePickerViewControllerDelegate: NSObject {
func didSelectDate(_ controller: DatePicker.DatePickerViewController, date: Date)
}
extension DatePicker {
class DatePickerViewController: UIViewController {
private var padding: CGFloat = 15
private var topPadding: CGFloat { 10 + padding }
private var calendarModel: CalendarModel
private let picker = CalendarBase()
weak var delegate: DatePickerViewControllerDelegate?
init(_ calendarModel: CalendarModel, delegate: DatePickerViewControllerDelegate?) {
self.delegate = delegate
self.calendarModel = calendarModel
super.init(nibName: nil, bundle: nil)
self.picker.onChange = { [weak self] control in
guard let self else { return }
self.delegate?.didSelectDate(self, date: control.selectedDate)
}
}
var selectedDate: Date = Date() {
didSet {
picker.selectedDate = selectedDate
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(picker)
picker.surface = calendarModel.surface
picker.hideContainerBorder = calendarModel.hideContainerBorder
picker.hideCurrentDateIndicator = calendarModel.hideCurrentDateIndicator
picker.indicators = calendarModel.indicators
picker.activeDates = calendarModel.activeDates
picker.inactiveDates = calendarModel.inactiveDates
picker.selectedDate = selectedDate
picker.minDate = calendarModel.minDate
picker.maxDate = calendarModel.maxDate
picker.pinToSuperView(.init(top: topPadding, left: padding, bottom: padding, right: padding))
view.backgroundColor = picker.backgroundColor
}
override var preferredContentSize: CGSize {
get {
var size = picker.frame.size
size.height += 40
size.width += 30
return size
}
set {
super.preferredContentSize = newValue
}
}
}
}

View File

@ -30,19 +30,7 @@ open class DropdownSelect: EntryFieldBase {
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
/// Override UIControl state to add the .error state if showSuccess is true and if showError is true.
open override var state: UIControl.State {
get {
var state = super.state
if dropdownField.isFirstResponder {
state.insert(.focused)
}
return state
}
}
//--------------------------------------------------
/// If true, the label will be displayed inside the dropdown containerView. Otherwise, the label will be above the dropdown containerView like a normal text input.
open var showInlineLabel: Bool = false { didSet { setNeedsUpdate() }}
@ -66,6 +54,8 @@ open class DropdownSelect: EntryFieldBase {
//--------------------------------------------------
// MARK: - Private Properties
//--------------------------------------------------
internal override var responder: UIResponder? { dropdownField }
internal var minWidthDefault = 66.0
internal var minWidthInlineLabel = 102.0
internal override var minWidth: CGFloat { showInlineLabel ? minWidthInlineLabel : minWidthDefault }
@ -130,8 +120,8 @@ open class DropdownSelect: EntryFieldBase {
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
open override func setup() {
super.setup()
accessibilityHintText = "has popup, Double tap to open."
fieldStackView.isAccessibilityElement = true
inlineDisplayLabel.isAccessibilityElement = true
dropdownField.width(0)
@ -276,57 +266,11 @@ open class DropdownSelect: EntryFieldBase {
statusIcon.color = iconColorConfiguration.getColor(self)
}
open override func updateAccessibility() {
super.updateAccessibility()
fieldStackView.accessibilityLabel = "Dropdown Select, \(accessibilityLabelText)"
fieldStackView.accessibilityHint = isReadOnly || !isEnabled ? "" : "has popup, Double tap to open."
fieldStackView.accessibilityValue = value
}
open override var accessibilityElements: [Any]? {
get {
var elements = [Any]()
elements.append(contentsOf: [titleLabel, fieldStackView])
if showError {
elements.append(statusIcon)
if let errorText, !errorText.isEmpty {
elements.append(errorLabel)
}
}
if let helperText, !helperText.isEmpty {
elements.append(helperLabel)
}
return elements
}
set { super.accessibilityElements = newValue }
}
@objc open func pickerDoneClicked() {
optionsPicker.isHidden = true
dropdownField.resignFirstResponder()
setNeedsUpdate()
UIAccessibility.post(notification: .layoutChanged, argument: fieldStackView)
}
open override var canBecomeFirstResponder: Bool {
return dropdownField.canBecomeFirstResponder
}
open override func becomeFirstResponder() -> Bool {
return dropdownField.becomeFirstResponder()
}
open override var canResignFirstResponder: Bool {
return dropdownField.canResignFirstResponder
}
open override func resignFirstResponder() -> Bool {
return dropdownField.resignFirstResponder()
UIAccessibility.post(notification: .layoutChanged, argument: containerView)
}
}
@ -337,8 +281,8 @@ extension DropdownSelect: UIPickerViewDelegate, UIPickerViewDataSource {
internal func launchPicker() {
if optionsPicker.isHidden {
UIAccessibility.post(notification: .layoutChanged, argument: optionsPicker)
dropdownField.becomeFirstResponder()
UIAccessibility.post(notification: .layoutChanged, argument: optionsPicker)
} else {
dropdownField.resignFirstResponder()
}

View File

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

View File

@ -94,6 +94,12 @@ open class Icon: View {
isAccessibilityElement = true
accessibilityTraits = .image
bridge_accessibilityLabelBlock = { [weak self] in
guard let self else { return "" }
return name?.rawValue ?? "icon"
}
}
/// Used to make changes to the View based off a change events or from local properties.
@ -118,12 +124,7 @@ open class Icon: View {
super.reset()
color = VDSColor.paletteBlack
imageView.image = nil
}
open override func updateAccessibility() {
super.updateAccessibility()
accessibilityLabel = name?.rawValue ?? "icon"
}
}
}
extension UIImage {

View File

@ -91,16 +91,14 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
private struct LabelAction {
var range: NSRange
var action: PassthroughSubject<Void, Never>
var accessibilityId: Int = 0
var frame: CGRect = .zero
func performAction() {
action.send()
}
init(range: NSRange, action: PassthroughSubject<Void, Never>, accessibilityID: Int = 0) {
init(range: NSRange, action: PassthroughSubject<Void, Never>) {
self.range = range
self.action = action
self.accessibilityId = accessibilityID
}
}
@ -215,7 +213,8 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
}
}
open func setup() {}
open func setup() {
}
open func reset() {
shouldUpdateView = false
@ -242,7 +241,6 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
}
open func updateAccessibility() {
accessibilityLabel = text
if isEnabled {
accessibilityTraits.remove(.notEnabled)
} else {
@ -263,24 +261,7 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
super.layoutSubviews()
applyActions()
}
/// Addig custom accessibillty actions from the collection of attributes.
open override func accessibilityActivate() -> Bool {
guard let accessibleActions = accessibilityCustomActions else { return false }
for actionable in actions {
for action in accessibleActions {
if action.hash == actionable.accessibilityId {
actionable.performAction()
return true
}
}
}
return false
}
//--------------------------------------------------
// MARK: - Private Methods
//--------------------------------------------------
@ -373,15 +354,27 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
//see if the attribute is Actionable
if let actionable = attribute as? any ActionLabelAttributeModel, mutableAttributedString.isValid(range: actionable.range) {
//create a accessibleAction
let customAccessibilityAction = customAccessibilityAction(text: mutableAttributedString.string, range: actionable.range, accessibleText: actionable.accessibleText)
let customAccessibilityAction = customAccessibilityElement(text: mutableAttributedString.string,
range: actionable.range,
accessibleText: actionable.accessibleText)
// creat the action
let labelAction = LabelAction(range: actionable.range, action: actionable.action)
// set the action of the accessibilityElement
customAccessibilityAction?.accessibilityAction = { [weak self] in
guard let self, isEnabled else { return }
labelAction.performAction()
}
//create a wrapper for the attributes range, block and
actions.append(LabelAction(range: actionable.range, action: actionable.action, accessibilityID: customAccessibilityAction?.hashValue ?? -1))
actions.append(labelAction)
isUserInteractionEnabled = true
}
}
if let accessibilityElements, !accessibilityElements.isEmpty {
let staticText = UIAccessibilityElement(accessibilityContainer: self)
let staticText = AccessibilityActionElement(accessibilityContainer: self)
staticText.accessibilityLabel = text
staticText.accessibilityFrameInContainerSpace = bounds
@ -392,62 +385,236 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
}
}
//--------------------------------------------------
// MARK: - Touch Events
//--------------------------------------------------
@objc private func textLinkTapped(_ gesture: UITapGestureRecognizer) {
for actionable in actions {
// This determines if we tapped on the desired range of text.
if gesture.didTapActionInLabel(self, inRange: actionable.range) {
actionable.performAction()
return
}
let location = gesture.location(in: self)
if let action = actions.first(where: { isAction(for: location, inRange: $0.range) }) {
action.performAction()
}
}
private func customAccessibilityAction(text: String?, range: NSRange, accessibleText: String? = nil) -> UIAccessibilityCustomAction? {
public func isAction(for location: CGPoint) -> Bool {
actions.contains(where: {isAction(for: location, inRange: $0.range)})
}
public func isAction(for location: CGPoint, inRange targetRange: NSRange) -> Bool {
guard let abstractContainer = abstractTextContainer() else { return false }
let textContainer = abstractContainer.textContainer
let layoutManager = abstractContainer.layoutManager
guard let text = text, let attributedText else { return nil }
let indexOfGlyph = layoutManager.glyphIndex(for: location, in: textContainer)
let intrinsicWidth = intrinsicContentSize.width
let actionText = accessibleText ?? (text.isValid(range: range) ? NSString(string:text).substring(with: range) : text)
// Calculate the frame of the substring
// Assert that tapped occured within acceptable bounds based on alignment.
switch textAlignment {
case .right:
if location.x < bounds.width - intrinsicWidth {
return false
}
case .center:
let halfBounds = bounds.width / 2
let halfIntrinsicWidth = intrinsicWidth / 2
if location.x > halfBounds + halfIntrinsicWidth {
return false
} else if location.x < halfBounds - halfIntrinsicWidth {
return false
}
default: // Left align
if location.x > intrinsicWidth {
return false
}
}
// Affirms that the tap occured in the desired rect of provided by the target range.
return layoutManager.boundingRect(forGlyphRange: targetRange, in: textContainer).contains(location)
&& NSLocationInRange(indexOfGlyph, targetRange)
}
/**
Provides a text container and layout manager of how the text would appear on screen.
They are used in tandem to derive low-level TextKit results of the label.
*/
public func abstractTextContainer() -> (textContainer: NSTextContainer, layoutManager: NSLayoutManager, textStorage: NSTextStorage)? {
// Must configure the attributed string to translate what would appear on screen to accurately analyze.
guard let attributedText = attributedText else { return nil }
let paragraph = NSMutableParagraphStyle()
paragraph.alignment = textAlignment
let stagedAttributedString = NSMutableAttributedString(attributedString: attributedText)
stagedAttributedString.addAttributes([NSAttributedString.Key.paragraphStyle: paragraph], range: NSRange(location: 0, length: attributedText.string.count))
let textStorage = NSTextStorage(attributedString: stagedAttributedString)
let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: bounds.size)
let textStorage = NSTextStorage(attributedString: attributedText)
let textContainer = NSTextContainer(size: .zero)
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
textContainer.lineFragmentPadding = 0.0
textContainer.lineBreakMode = lineBreakMode
textContainer.maximumNumberOfLines = numberOfLines
textContainer.size = bounds.size
return (textContainer, layoutManager, textStorage)
}
//--------------------------------------------------
// MARK: - Accessibility
//--------------------------------------------------
private func customAccessibilityElement(text: String?, range: NSRange, accessibleText: String? = nil) -> AccessibilityActionElement? {
guard let text = text, let abstractContainer = abstractTextContainer() else { return nil }
let textContainer = abstractContainer.textContainer
let layoutManager = abstractContainer.layoutManager
let actionText = accessibleText ?? (text.isValid(range: range) ? NSString(string:text).substring(with: range) : text)
var glyphRange = NSRange()
// Convert the range for the substring into a range of glyphs
layoutManager.characterRange(forGlyphRange: range, actualGlyphRange: &glyphRange)
let substringBounds = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
// Create custom accessibility element
let element = UIAccessibilityElement(accessibilityContainer: self)
let element = AccessibilityActionElement(accessibilityContainer: self)
element.accessibilityLabel = actionText
element.accessibilityTraits = .link
element.accessibilityHint = "Double tap to open"
element.accessibilityFrameInContainerSpace = substringBounds
//TODO: accessibilityHint for Label
// element.accessibilityHint = MVMCoreUIUtility.hardcodedString(withKey: "swipe_to_select_with_action_hint")
accessibilityElements = (accessibilityElements ?? []).compactMap{$0 as? UIAccessibilityElement}.filter { $0.accessibilityLabel != actionText }
accessibilityElements?.append(element)
let accessibleAction = UIAccessibilityCustomAction(name: actionText, target: self, selector: #selector(accessibilityCustomAction(_:)))
accessibilityCustomActions?.append(accessibleAction)
return accessibleAction
return element
}
@objc private func accessibilityCustomAction(_ action: UIAccessibilityCustomAction) {
for actionable in actions {
if action.hash == actionable.accessibilityId {
actionable.performAction()
return
open var accessibilityAction: ((Label) -> Void)?
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 super.isAccessibilityElement
}
}
set {
super.isAccessibilityElement = newValue
}
}
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 super.accessibilityLabel
}
}
set {
super.accessibilityLabel = newValue
}
}
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 super.accessibilityHint
}
}
set {
super.accessibilityHint = newValue
}
}
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 super.accessibilityValue
}
}
set {
super.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
accessibilityElements = [closeButton, typeIcon, titleLabel, subTitleLabel, buttonGroup]
closeButton.accessibilityTraits = [.button]
closeButton.accessibilityLabel = "Close Notification"
typeIcon.bridge_accessibilityLabelBlock = { [weak self] in
guard let self else { return "" }
return style.accessibleText
}
}
/// Resets to default settings.
@ -372,12 +378,6 @@ open class Notification: View {
}
}
open override func updateAccessibility() {
super.updateAccessibility()
closeButton.accessibilityLabel = "Close Notification"
typeIcon.accessibilityLabel = style.accessibleText
}
private func setConstraints() {
labelViewAndButtonViewConstraint?.deactivate()
labelViewBottomConstraint?.deactivate()

View File

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

View File

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

View File

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

View File

@ -74,9 +74,7 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable {
}
/// Selector for this RadioBox.
open var selectorView = UIView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
}
open var selectorView = View().with { $0.accessibilityIdentifier = "RadioBox" }
/// If provided, the RadioBox text will be rendered.
open var text: String? { didSet { setNeedsUpdate() } }
@ -127,12 +125,19 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable {
open var inputId: String? { didSet { setNeedsUpdate() } }
open var value: AnyHashable? { hiddenValue }
open var value: String? { hiddenValue }
open var hiddenValue: AnyHashable? { didSet { setNeedsUpdate() } }
open var hiddenValue: String? { didSet { setNeedsUpdate() } }
open var accessibilityValueText: String?
open override var accessibilityAction: ((Control) -> Void)? {
didSet {
selectorView.accessibilityAction = { [weak self] selectorItemBase in
guard let self else { return }
accessibilityAction?(self)
}
}
}
//--------------------------------------------------
// MARK: - Configuration Properties
//--------------------------------------------------
@ -165,16 +170,51 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable {
onClick = { control in
control.toggle()
}
selectorView.bridge_accessibilityLabelBlock = { [weak self] in
guard let self else { return "" }
var accessibilityLabels = [String]()
if isSelected {
accessibilityLabels.append("selected")
}
accessibilityLabels.append("Radiobox")
if let text, !text.isEmpty {
accessibilityLabels.append(text)
}
if let text = subText, !text.isEmpty {
accessibilityLabels.append(text)
}
if let text = subTextRight, !text.isEmpty {
accessibilityLabels.append(text)
}
if strikethrough {
accessibilityLabels.append(strikethroughAccessibilityText)
}
if !isEnabled {
accessibilityLabels.append("dimmed")
}
return accessibilityLabels.joined(separator: ", ")
}
}
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
open override func setup() {
super.setup()
isAccessibilityElement = true
accessibilityTraits = .button
isAccessibilityElement = false
selectorView.isAccessibilityElement = true
selectorView.accessibilityTraits = .button
addSubview(selectorView)
selectorView.isUserInteractionEnabled = false
selectorView.isUserInteractionEnabled = true
selectorView.addSubview(selectorStackView)
@ -226,6 +266,8 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable {
/// This will change the state of the Selector and execute the actionBlock if provided.
open func toggle() {
guard isEnabled else { return }
//removed error
isSelected.toggle()
sendActions(for: .valueChanged)
@ -239,26 +281,51 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable {
setNeedsLayout()
}
/// Used to update any Accessibility properties.
open override func updateAccessibility() {
super.updateAccessibility()
setAccessibilityLabel(for: [textLabel, subTextLabel, subTextRightLabel])
if let currentAccessibilityLabel = accessibilityLabel {
accessibilityLabel = "Radiobox, \(currentAccessibilityLabel)"
} else {
accessibilityLabel = "Radiobox"
open override var accessibilityElements: [Any]? {
get {
var items = [Any]()
items.append(selectorView)
if let text = text, !text.isEmpty {
items.append(textLabel)
}
if let text = subText, !text.isEmpty {
items.append(subTextLabel)
}
if let text = subTextRight, !text.isEmpty {
items.append(subTextRightLabel)
}
return items
}
if let accessibilityValueText {
accessibilityValue = strikethrough
? "\(strikethroughAccessibilityText), \(accessibilityValueText)"
: accessibilityValueText
set {}
}
/// Overriden to take the hit if there is an onClickSubscriber and the view is not a UIControl
open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard isEnabled else { return super.hitTest(point, with: event) }
let textPoint = convert(point, to: textLabel)
let subTextPoint = convert(point, to: subTextLabel)
let subTextRightPoint = convert(point, to: subTextRightLabel)
if textLabel.isAction(for: textPoint) {
return textLabel
} else if subTextLabel.isAction(for: subTextPoint) {
return subTextLabel
} else if subTextRightLabel.isAction(for: subTextRightPoint) {
return subTextRightLabel
} else {
accessibilityValue = strikethrough
? "\(strikethroughAccessibilityText)"
: accessibilityValueText
guard !UIAccessibility.isVoiceOverRunning else { return nil }
return super.hitTest(point, with: event)
}
}
open func getSelectorView() -> UIView {
selectorView
}
//--------------------------------------------------
// MARK: - Private Methods
//--------------------------------------------------

View File

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

View File

@ -46,8 +46,6 @@ open class RadioButtonGroup: SelectorGroupBase<RadioButtonItem>, SelectorGroupSi
$0.surface = model.surface
$0.inputId = model.inputId
$0.hiddenValue = model.value
$0.accessibilityLabel = model.accessibileText
$0.accessibilityValueText = "item \(index+1) of \(selectorModels.count)"
$0.labelText = model.labelText
$0.labelTextAttributes = model.labelTextAttributes
$0.childText = model.childText
@ -55,6 +53,7 @@ open class RadioButtonGroup: SelectorGroupBase<RadioButtonItem>, SelectorGroupSi
$0.isSelected = model.selected
$0.errorText = model.errorText
$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.
open override func toggle() {
guard !isSelected else { return }
guard !isSelected, isEnabled else { return }
//removed error
if showError && isSelected == false {

View File

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

View File

@ -305,7 +305,10 @@ open class Tabs: View {
tabItem.orientation = orientation
tabItem.surface = surface
tabItem.indicatorPosition = indicatorPosition
tabItem.accessibilityValueText = "\(index+1) of \(tabViews.count) Tabs"
tabItem.bridge_accessibilityValueBlock = { [weak self] in
guard let self else { return "" }
return "\(index+1) of \(tabViews.count) Tabs"
}
}
}

View File

@ -40,6 +40,8 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
//--------------------------------------------------
// MARK: - Private Properties
//--------------------------------------------------
internal var responder: UIResponder? { return nil }
internal let mainStackView = UIStackView().with {
$0.axis = .vertical
$0.alignment = .fill
@ -92,11 +94,9 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
/// This is the view that will be wrapped with the border for userInteraction.
/// The only subview of this view is the fieldStackView
internal var containerView: UIView = {
return UIView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
}
}()
internal var containerView = View().with {
$0.isAccessibilityElement = true
}
/// This is set by a local method.
internal var bottomContainerView: UIView!
@ -183,7 +183,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
open var statusIcon: Icon = Icon().with {
$0.size = .medium
$0.isAccessibilityElement = false
$0.isAccessibilityElement = true
}
open var labelText: String? { didSet { setNeedsUpdate() } }
@ -207,6 +207,9 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
if isReadOnly {
state.insert(.readonly)
}
if let responder, responder.isFirstResponder {
state.insert(.focused)
}
}
return state
}
@ -241,22 +244,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
open var rules = [AnyRule<String>]()
open var accessibilityLabelText: String {
var accessibilityLabels = [String]()
if let text = titleLabel.text {
accessibilityLabels.append(text)
}
if isReadOnly {
accessibilityLabels.append("read only")
}
if !isEnabled {
accessibilityLabels.append("dimmed")
}
if let errorText, showError {
accessibilityLabels.append("error, \(errorText)")
}
return accessibilityLabels.joined(separator: ", ")
}
open var accessibilityHintText: String = "Double tap to open"
//--------------------------------------------------
// MARK: - Overrides
@ -274,11 +262,11 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
.pinBottom()
trailingEqualsConstraint = layoutGuide.pinTrailing(anchor: trailingAnchor)
// width constraints
trailingLessThanEqualsConstraint = layoutGuide.pinTrailingLessThanOrEqualTo(anchor: trailingAnchor)?.deactivate()
widthConstraint = layoutGuide.widthAnchor.constraint(equalToConstant: 0).deactivate()
// Add mainStackView to the view
addSubview(mainStackView)
@ -292,7 +280,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
//InputContainer, Icons, Buttons
containerView.addSubview(fieldStackView)
fieldStackView.pinToSuperView(.uniform(VDSLayout.space3X))
let fieldContainerView = getFieldContainer()
fieldContainerView.translatesAutoresizingMaskIntoConstraints = false
@ -300,11 +288,11 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
fieldStackView.addArrangedSubview(fieldContainerView)
fieldStackView.addArrangedSubview(statusIcon)
fieldStackView.setCustomSpacing(VDSLayout.space3X, after: fieldContainerView)
//get the container this is what show helper text, error text
//can include other for character count, max length
bottomContainerView = getBottomContainer()
//this is the vertical stack that contains error text, helper text
bottomContainerStackView.addArrangedSubview(errorLabel)
bottomContainerStackView.addArrangedSubview(helperLabel)
@ -312,11 +300,11 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
// Add arranged subviews to textFieldStackView
contentStackView.addArrangedSubview(containerView)
contentStackView.addArrangedSubview(bottomContainerView)
// Add arranged subviews to mainStackView
mainStackView.addArrangedSubview(titleLabel)
mainStackView.addArrangedSubview(contentStackView)
// Initial position of the helper label
updateHelperTextPosition()
@ -324,6 +312,43 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
titleLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable()
errorLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable()
helperLabel.textColorConfiguration = secondaryColorConfiguration.eraseToAnyColorable()
containerView.bridge_accessibilityLabelBlock = { [weak self] in
guard let self else { return "" }
var accessibilityLabels = [String]()
if let text = titleLabel.text?.trimmingCharacters(in: .whitespaces) {
accessibilityLabels.append(text)
}
if isReadOnly {
accessibilityLabels.append("read only")
}
if !isEnabled {
accessibilityLabels.append("dimmed")
}
if let errorText, showError {
accessibilityLabels.append("error, \(errorText)")
}
accessibilityLabels.append("\(Self.self)")
return accessibilityLabels.joined(separator: ", ")
}
containerView.bridge_accessibilityHintBlock = { [weak self] in
guard let self else { return "" }
return isReadOnly || !isEnabled ? "" : accessibilityHintText
}
containerView.bridge_accessibilityValueBlock = { [weak self] in
guard let self else { return "" }
return value
}
statusIcon.bridge_accessibilityLabelBlock = { [weak self] in
guard let self else { return "" }
return showError || hasInternalError ? "error" : nil
}
}
/// Updates the UI
@ -360,6 +385,22 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
isReadOnly = false
onChange = nil
}
open override var canBecomeFirstResponder: Bool {
responder?.canBecomeFirstResponder ?? super.canBecomeFirstResponder
}
open override func becomeFirstResponder() -> Bool {
responder?.becomeFirstResponder() ?? super.becomeFirstResponder()
}
open override var canResignFirstResponder: Bool {
responder?.canResignFirstResponder ?? super.canResignFirstResponder
}
open override func resignFirstResponder() -> Bool {
responder?.resignFirstResponder() ?? super.resignFirstResponder()
}
//--------------------------------------------------
// MARK: - Public Methods
@ -447,6 +488,28 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
}
}
open override var accessibilityElements: [Any]? {
get {
var elements = [Any]()
elements.append(contentsOf: [titleLabel, containerView])
if showError {
elements.append(statusIcon)
if let errorText, !errorText.isEmpty {
elements.append(errorLabel)
}
}
if let helperText, !helperText.isEmpty {
elements.append(helperLabel)
}
return elements
}
set { super.accessibilityElements = newValue }
}
//--------------------------------------------------
// MARK: - Private Methods
//--------------------------------------------------

View File

@ -125,7 +125,7 @@ extension InputField {
class CreditCardHandler: FieldTypeHandler {
static let shared = CreditCardHandler()
private override init() {
super.init()
self.validateOnChange = false
@ -135,6 +135,7 @@ extension InputField {
fileprivate func updateLeftImage(_ inputField: InputField) {
let imageName = inputField.cardType.imageName(surface: inputField.surface)
creditCardImageView.image = BundleManager.shared.image(for: imageName)
creditCardImageView.accessibilityLabel = inputField.cardType.rawValue
}
override func updateView(_ inputField: InputField) {
@ -148,14 +149,14 @@ extension InputField {
inputField.textField.leftView = iconContainerView
inputField.textField.leftViewMode = .always
updateLeftImage(inputField)
}
internal var creditCardImageView = UIImageView().with {
$0.height(20)
$0.width(32)
$0.isAccessibilityElement = false
$0.isAccessibilityElement = true
$0.translatesAutoresizingMaskIntoConstraints = false
$0.contentMode = .scaleAspectFill
$0.clipsToBounds = true

View File

@ -34,6 +34,8 @@ open class InputField: EntryFieldBase {
//--------------------------------------------------
// MARK: - Private Properties
//--------------------------------------------------
internal override var responder: UIResponder? { textField }
internal override var containerBackgroundColor: UIColor {
if showSuccess {
return backgroundColorConfiguration.getColor(self)
@ -102,6 +104,7 @@ open class InputField: EntryFieldBase {
open var textField = TextField().with {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.textStyle = TextStyle.bodyLarge
$0.isAccessibilityElement = false
}
/// Color configuration for the textField.
@ -163,11 +166,7 @@ open class InputField: EntryFieldBase {
if showSuccess {
state.insert(.success)
}
if textField.isFirstResponder {
state.insert(.focused)
}
return state
}
}
@ -181,6 +180,8 @@ open class InputField: EntryFieldBase {
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
open override func setup() {
super.setup()
accessibilityHintText = "Double tap to edit"
textField.heightAnchor.constraint(equalToConstant: 20).isActive = true
textField.delegate = self
bottomContainerStackView.insertArrangedSubview(successLabel, at: 0)
@ -195,6 +196,54 @@ open class InputField: EntryFieldBase {
borderColorConfiguration.setSurfaceColors(VDSColor.feedbackSuccessOnlight, VDSColor.feedbackSuccessOndark, forState: .success)
textField.textColorConfiguration = textFieldTextColorConfiguration
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 let formatText = textField.formatText, !formatText.isEmpty {
accessibilityLabels.append("format, \(formatText)")
}
if let placeholderText = textField.placeholder, !placeholderText.isEmpty {
accessibilityLabels.append("placeholder, \(placeholderText)")
}
if isReadOnly {
accessibilityLabels.append("read only")
}
if !isEnabled {
accessibilityLabels.append("dimmed")
}
if let errorText, showError {
accessibilityLabels.append("error, \(errorText)")
}
if let successText, showSuccess {
accessibilityLabels.append("success, \(successText)")
}
accessibilityLabels.append("\(Self.self)")
return accessibilityLabels.joined(separator: ", ")
}
statusIcon.bridge_accessibilityLabelBlock = { [weak self] in
guard let self else { return "" }
if showError {
return "error"
} else if showSuccess {
return "success"
} else {
return nil
}
}
}
open override func getFieldContainer() -> UIView {
@ -227,13 +276,7 @@ open class InputField: EntryFieldBase {
textField.isEnabled = isEnabled
textField.isUserInteractionEnabled = isEnabled && !isReadOnly
}
open override func updateAccessibility() {
super.updateAccessibility()
textField.accessibilityLabel = accessibilityLabelText
textField.accessibilityHint = isReadOnly || !isEnabled ? "" : "Double tap to open."
}
open override func updateErrorLabel() {
super.updateErrorLabel()
@ -264,12 +307,21 @@ open class InputField: EntryFieldBase {
open override var accessibilityElements: [Any]? {
get {
var elements = [Any]()
elements.append(contentsOf: [titleLabel, textField])
if showError {
elements.append(contentsOf: [titleLabel, containerView])
if let leftView = textField.leftView {
elements.append(leftView)
}
if !statusIcon.isHidden{
elements.append(statusIcon)
if let errorText, !errorText.isEmpty {
elements.append(errorLabel)
}
}
if !actionTextLink.isHidden {
elements.append(actionTextLink)
}
if let errorText, !errorText.isEmpty, showError || hasInternalError {
elements.append(errorLabel)
} else if showSuccess, let successText, !successText.isEmpty {
elements.append(successLabel)
}
@ -283,22 +335,6 @@ open class InputField: EntryFieldBase {
set { super.accessibilityElements = newValue }
}
open override var canBecomeFirstResponder: Bool {
return textField.canBecomeFirstResponder
}
open override func becomeFirstResponder() -> Bool {
return textField.becomeFirstResponder()
}
open override var canResignFirstResponder: Bool {
return textField.canResignFirstResponder
}
open override func resignFirstResponder() -> Bool {
return textField.resignFirstResponder()
}
}
extension InputField: UITextFieldDelegate {
@ -311,6 +347,7 @@ extension InputField: UITextFieldDelegate {
public func textFieldDidEndEditing(_ textField: UITextField) {
fieldType.handler().textFieldDidEndEditing(self, textField: textField)
validate()
UIAccessibility.post(notification: .layoutChanged, argument: self.containerView)
}
public func textFieldDidChangeSelection(_ textField: UITextField) {

View File

@ -47,7 +47,10 @@ open class TextField: UITextField, ViewProtocol, Errorable {
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
private var formatLabel = Label().with {
/// Key of whether or not updateView() is called in setNeedsUpdate()
open var shouldUpdateView: Bool = true
private var formatLabel = Label().with {
$0.tag = 999
$0.textColorConfiguration = ViewColorConfiguration().with {
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forDisabled: true)
@ -63,9 +66,6 @@ open class TextField: UITextField, ViewProtocol, Errorable {
/// Will determine if a scaled font should be used for the titleLabel font.
open var useScaledFont: Bool = false { didSet { setNeedsUpdate() } }
/// Key of whether or not updateView() is called in setNeedsUpdate()
open var shouldUpdateView: Bool = true
open var surface: Surface = .light { didSet { setNeedsUpdate() } }
@ -74,7 +74,7 @@ open class TextField: UITextField, ViewProtocol, Errorable {
open var errorText: String? { didSet { setNeedsUpdate() } }
open var lineBreakMode: NSLineBreakMode = .byClipping { didSet { setNeedsUpdate() } }
open override var isEnabled: Bool { didSet { setNeedsUpdate() } }
open var textColorConfiguration: AnyColorable = ViewColorConfiguration().with {
@ -229,7 +229,135 @@ open class TextField: UITextField, ViewProtocol, Errorable {
attributedText = nil
}
}
//--------------------------------------------------
// MARK: - Accessibility
//--------------------------------------------------
open var accessibilityAction: ((TextField) -> Void)?
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 super.isAccessibilityElement
}
}
set {
super.isAccessibilityElement = newValue
}
}
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 super.accessibilityLabel
}
}
set {
super.accessibilityLabel = newValue
}
}
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 super.accessibilityHint
}
}
set {
super.accessibilityHint = newValue
}
}
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 super.accessibilityValue
}
}
set {
super.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 {

View File

@ -32,6 +32,8 @@ open class TextArea: EntryFieldBase {
//--------------------------------------------------
// MARK: - Private Properties
//--------------------------------------------------
internal override var responder: UIResponder? { textView }
internal var textViewHeightConstraint: NSLayoutConstraint?
internal var inputFieldStackView: UIStackView = {
@ -42,30 +44,19 @@ open class TextArea: EntryFieldBase {
$0.spacing = VDSLayout.space3X
}
}()
open var characterCounterLabel = Label().with {
$0.setContentCompressionResistancePriority(.required, for: .vertical)
$0.textStyle = .bodySmall
$0.textAlignment = .right
$0.numberOfLines = 1
}
open var minHeight: Height = .twoX { didSet { setNeedsUpdate() } }
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
/// Override UIControl state to add the .error state if showSuccess is true and if showError is true.
open override var state: UIControl.State {
get {
var state = super.state
if textView.isFirstResponder {
state.insert(.focused)
}
return state
}
}
//--------------------------------------------------
override var containerSize: CGSize { CGSize(width: 182, height: Height.twoX.value) }
/// Enum used to describe the the height of TextArea.
@ -101,13 +92,15 @@ open class TextArea: EntryFieldBase {
open override var value: String? {
return textView.text
}
/// UITextView shown in the TextArea.
open var textView = TextView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.sizeToFit()
$0.isScrollEnabled = false
$0.isAccessibilityElement = false
$0.isScrollEnabled = true
$0.textContainerInset = .zero
$0.autocorrectionType = .no
$0.textContainer.lineFragmentPadding = 0
}
@ -137,10 +130,8 @@ open class TextArea: EntryFieldBase {
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
open override func setup() {
super.setup()
fieldStackView.pinToSuperView(.uniform(VDSFormControls.spaceInset))
textView.isScrollEnabled = true
textView.autocorrectionType = .no
accessibilityHintText = "Double tap to edit"
//events
textView
@ -159,6 +150,7 @@ open class TextArea: EntryFieldBase {
.publisher(for: .editingDidEnd)
.sink { [weak self] _ in
self?.validate()
UIAccessibility.post(notification: .layoutChanged, argument: self?.containerView)
}.store(in: &subscribers)
textViewHeightConstraint = textView.heightAnchor.constraint(greaterThanOrEqualToConstant: containerSize.height)
@ -192,13 +184,7 @@ open class TextArea: EntryFieldBase {
characterCounterLabel.surface = surface
highlightCharacterOverflow()
}
open override func updateAccessibility() {
super.updateAccessibility()
textView.accessibilityLabel = accessibilityLabelText
textView.accessibilityHint = isReadOnly || !isEnabled ? "" : "Double tap to open."
}
override func updateRules() {
super.updateRules()
@ -222,46 +208,7 @@ open class TextArea: EntryFieldBase {
stackView.addArrangedSubview(characterCounterLabel)
return stackView
}
open override var accessibilityElements: [Any]? {
get {
var elements = [Any]()
elements.append(contentsOf: [titleLabel, textView])
if showError {
elements.append(statusIcon)
if let errorText, !errorText.isEmpty {
elements.append(errorLabel)
}
}
if let helperText, !helperText.isEmpty {
elements.append(helperLabel)
}
return elements
}
set { super.accessibilityElements = newValue }
}
open override var canBecomeFirstResponder: Bool {
return textView.canBecomeFirstResponder
}
open override func becomeFirstResponder() -> Bool {
return textView.becomeFirstResponder()
}
open override var canResignFirstResponder: Bool {
return textView.canResignFirstResponder
}
open override func resignFirstResponder() -> Bool {
return textView.resignFirstResponder()
}
//--------------------------------------------------
// MARK: - Private Methods
//--------------------------------------------------

View File

@ -144,6 +144,135 @@ open class TextView: UITextView, ViewProtocol, Errorable {
shouldUpdateView = true
setNeedsUpdate()
}
//--------------------------------------------------
// MARK: - Accessibility
//--------------------------------------------------
open var accessibilityAction: ((TextView) -> Void)?
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 super.isAccessibilityElement
}
}
set {
super.isAccessibilityElement = newValue
}
}
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 super.accessibilityLabel
}
}
set {
super.accessibilityLabel = newValue
}
}
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 super.accessibilityHint
}
}
set {
super.accessibilityHint = newValue
}
}
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 super.accessibilityValue
}
}
set {
super.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

View File

@ -44,6 +44,7 @@ open class TileContainer: TileContainerBase<TileContainer.Padding> {
}
open class TileContainerBase<PaddingType: DefaultValuing>: Control where PaddingType.ValueType == CGFloat {
//--------------------------------------------------
// MARK: - Initializers
//--------------------------------------------------
@ -69,6 +70,7 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
case secondary
case white
case black
case token(UIColor.VDSColor)
case custom(UIColor)
private var reflectedValue: String { String(reflecting: self) }
@ -108,8 +110,13 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
$0.clipsToBounds = true
}
internal var containerView = View()
open var containerView = View().with {
$0.setContentHuggingPriority(.defaultLow, for: .horizontal)
$0.setContentHuggingPriority(.defaultLow, for: .vertical)
$0.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
$0.setContentCompressionResistancePriority(.defaultHigh, for: .vertical)
}
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
@ -178,12 +185,7 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
//--------------------------------------------------
internal var widthConstraint: NSLayoutConstraint?
internal var heightConstraint: NSLayoutConstraint?
internal var heightGreaterThanConstraint: NSLayoutConstraint?
internal var containerTopConstraint: NSLayoutConstraint?
internal var containerBottomConstraint: NSLayoutConstraint?
internal var containerLeadingConstraint: NSLayoutConstraint?
internal var containerTrailingConstraint: NSLayoutConstraint?
//--------------------------------------------------
// MARK: - Configuration
//--------------------------------------------------
@ -221,28 +223,20 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
super.setup()
isAccessibilityElement = false
let layoutGuide = UILayoutGuide()
addLayoutGuide(layoutGuide)
layoutGuide
.pinTop()
.pinLeading()
.pinTrailing(0, .defaultHigh)
.pinBottom(0, .defaultHigh)
addSubview(backgroundImageView)
addSubview(containerView)
containerView.addSubview(contentView)
addSubview(highlightView)
containerView.pinToSuperView()
widthConstraint = layoutGuide.widthAnchor.constraint(equalToConstant: 0)
heightGreaterThanConstraint = layoutGuide.heightAnchor.constraint(greaterThanOrEqualToConstant: 44.0)
heightGreaterThanConstraint?.isActive = false
heightConstraint = layoutGuide.heightAnchor.constraint(equalToConstant: 0)
containerView.addSubview(backgroundImageView)
backgroundImageView.pinToSuperView()
containerView.addSubview(contentView)
contentView.pinToSuperView()
containerView.addSubview(highlightView)
highlightView.pinToSuperView()
widthConstraint = widthAnchor.constraint(equalToConstant: 0).deactivate()
heightConstraint = heightAnchor.constraint(equalToConstant: 0).deactivate()
backgroundImageView.setContentHuggingPriority(.defaultLow, for: .horizontal)
backgroundImageView.setContentHuggingPriority(.defaultLow, for: .vertical)
@ -251,20 +245,28 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
backgroundImageView.isUserInteractionEnabled = false
backgroundImageView.isHidden = true
containerTopConstraint = contentView.pinTop(anchor: layoutGuide.topAnchor, constant: padding.value)
containerBottomConstraint = layoutGuide.pinBottom(anchor: contentView.bottomAnchor, constant: padding.value)
containerLeadingConstraint = contentView.pinLeading(anchor: layoutGuide.leadingAnchor, constant: padding.value)
containerTrailingConstraint = layoutGuide.pinTrailing(anchor: contentView.trailingAnchor, constant: padding.value)
highlightView.pin(layoutGuide)
highlightView.isHidden = true
highlightView.backgroundColor = .clear
//corner radius
layer.cornerRadius = cornerRadius
containerView.layer.cornerRadius = cornerRadius
backgroundImageView.layer.cornerRadius = cornerRadius
highlightView.layer.cornerRadius = cornerRadius
clipsToBounds = true
containerView.clipsToBounds = true
containerView.bridge_isAccessibilityElementBlock = { [weak self] in self?.onClickSubscriber != nil }
containerView.accessibilityHint = "Double tap to open."
containerView.accessibilityLabel = nil
NotificationCenter.default
.publisher(for: UIDevice.orientationDidChangeNotification)
.sink() { [weak self] _ in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.001) { [weak self] in
guard let self else { return }
setNeedsUpdate()
}
}.store(in: &subscribers)
}
/// Overriden to take the hit if there is an onClickSubscriber and the view is not a UIControl
@ -280,7 +282,7 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
super.reset()
shouldUpdateView = false
color = .white
aspectRatio = .ratio1x1
aspectRatio = .none
imageFallbackColor = .light
width = nil
height = nil
@ -297,53 +299,15 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
highlightView.backgroundColor = hightLightViewColorConfiguration.getColor(self)
highlightView.isHidden = !isHighlighted
layer.borderColor = borderColorConfiguration.getColor(self).cgColor
layer.borderWidth = showBorder ? VDSFormControls.borderWidth : 0
containerView.layer.borderColor = borderColorConfiguration.getColor(self).cgColor
containerView.layer.borderWidth = showBorder ? VDSFormControls.borderWidth : 0
containerTopConstraint?.constant = padding.value
containerLeadingConstraint?.constant = padding.value
containerBottomConstraint?.constant = padding.value
containerTrailingConstraint?.constant = padding.value
if let width, aspectRatio == .none && height == nil{
widthConstraint?.constant = width
widthConstraint?.isActive = true
heightConstraint?.isActive = false
heightGreaterThanConstraint?.isActive = true
} else if let height, let width {
widthConstraint?.constant = width
heightConstraint?.constant = height
heightConstraint?.isActive = true
widthConstraint?.isActive = true
heightGreaterThanConstraint?.isActive = false
} else if let width {
let size = ratioSize(for: width)
widthConstraint?.constant = size.width
heightConstraint?.constant = size.height
widthConstraint?.isActive = true
heightConstraint?.isActive = true
heightGreaterThanConstraint?.isActive = false
} else {
widthConstraint?.isActive = false
heightConstraint?.isActive = false
}
applyBackgroundEffects()
contentView.removeConstraints()
contentView.pinToSuperView(.uniform(padding.value))
if showDropShadow, surface == .light {
addDropShadow(dropShadowConfiguration)
} else {
removeDropShadows()
}
updateContainerView()
}
open override func updateAccessibility() {
super.updateAccessibility()
containerView.isAccessibilityElement = onClickSubscriber != nil
containerView.accessibilityHint = "Double tap to open."
containerView.accessibilityLabel = nil
}
open override var accessibilityElements: [Any]? {
get {
var items = [Any]()
@ -370,13 +334,6 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
set {}
}
/// Used to update frames for the added CAlayers to our view
open override func layoutSubviews() {
super.layoutSubviews()
dropShadowLayers?.forEach { $0.frame = bounds }
gradientLayers?.forEach { $0.frame = bounds }
}
//--------------------------------------------------
// MARK: - Public Methods
//--------------------------------------------------
@ -401,25 +358,25 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
switch backgroundEffect {
case .transparency:
alphaConfiguration = 0.8
removeGradientLayer()
containerView.removeGradientLayer()
case .gradient(let firstColor, let secondColor):
alphaConfiguration = 1.0
addGradientLayer(with: firstColor, secondColor: secondColor)
containerView.addGradientLayer(with: firstColor, secondColor: secondColor)
backgroundImageView.isHidden = true
backgroundImageView.alpha = 1.0
case .none:
alphaConfiguration = 1.0
removeGradientLayer()
containerView.removeGradientLayer()
}
if let backgroundImage {
backgroundImageView.image = backgroundImage
backgroundImageView.isHidden = false
backgroundImageView.alpha = alphaConfiguration
backgroundColor = imageFallbackColor.withAlphaComponent(alphaConfiguration)
containerView.backgroundColor = imageFallbackColor.withAlphaComponent(alphaConfiguration)
} else {
backgroundImageView.isHidden = true
backgroundImageView.alpha = 1.0
backgroundColor = color.withAlphaComponent(alphaConfiguration)
containerView.backgroundColor = color.withAlphaComponent(alphaConfiguration)
}
}
@ -452,7 +409,77 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
return CGSize(width: width, height: height)
}
private func sizeContainerView(width: CGFloat? = nil, height: CGFloat? = nil) {
if let width, width > 0 {
widthConstraint?.constant = width
widthConstraint?.activate()
}
if let height, height > 0 {
heightConstraint?.constant = height
heightConstraint?.activate()
}
}
private func updateContainerView() {
applyBackgroundEffects()
widthConstraint?.deactivate()
heightConstraint?.deactivate()
if showDropShadow, surface == .light {
containerView.addDropShadow(dropShadowConfiguration)
} else {
containerView.removeDropShadows()
}
containerView.dropShadowLayers?.forEach { $0.frame = containerView.bounds }
containerView.gradientLayers?.forEach { $0.frame = containerView.bounds }
if width != nil || height != nil {
var containerViewWidth: CGFloat?
var containerViewHeight: CGFloat?
//run logic to determine which to activate
if let width, aspectRatio == .none && height == nil{
containerViewWidth = width
} else if let height, aspectRatio == .none && width == nil{
containerViewHeight = height
} else if let height, let width {
containerViewWidth = width
containerViewHeight = height
} else if let width {
let size = ratioSize(for: width)
containerViewWidth = size.width
containerViewHeight = size.height
} else if let height {
let size = ratioSize(for: height)
containerViewWidth = size.width
containerViewHeight = size.height
}
sizeContainerView(width: containerViewWidth, height: containerViewHeight)
} else {
if let parentSize = horizontalPinnedSize() {
var containerViewWidth: CGFloat?
var containerViewHeight: CGFloat?
let size = ratioSize(for: parentSize.width)
if aspectRatio == .none {
containerViewWidth = size.width
} else {
containerViewWidth = size.width
containerViewHeight = size.height
}
sizeContainerView(width: containerViewWidth, height: containerViewHeight)
}
}
}
}
extension TileContainerBase {
@ -484,6 +511,8 @@ extension TileContainerBase {
return whiteColorConfig.getColor(object.surface)
case .black:
return blackColorConfig.getColor(object.surface)
case .token(let vdsColor):
return vdsColor.uiColor
case .custom(let color):
return color
}

View File

@ -302,8 +302,9 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
open override func setup() {
super.setup()
aspectRatio = .none
color = .black
aspectRatio = .none
addContentView(stackView)
//badge
@ -386,6 +387,7 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
/// Resets to default settings.
open override func reset() {
shouldUpdateView = false
super.reset()
aspectRatio = .none
color = .black
//models
@ -405,11 +407,7 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
updateBadge()
updateTitleLockup()
updateIcons()
///Content-driven height Tilelets - Minimum height is configurable.
///if width != nil && (aspectRatio != .none || height != nil) then tilelet is not self growing, so we can apply text position alignments.
if width != nil && (aspectRatio != .none || height != nil) {
updateTextPositionAlignment()
}
updateTextPositionAlignment()
setNeedsLayout()
}
@ -584,6 +582,7 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
}
private func updateTextPositionAlignment() {
guard width != nil && (aspectRatio != .none || height != nil) else { return }
switch textPostion {
case .top:
titleLockupTopConstraint?.activate()

View File

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

View File

@ -207,6 +207,14 @@ open class Toggle: Control, Changeable, FormFieldable {
label.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor)
]
bridge_accessibilityValueBlock = { [weak self] in
guard let self else { return "" }
if showText {
return isSelected ? onText : offText
} else {
return isSelected ? "On" : "Off"
}
}
}
/// Resets to default settings.
@ -238,16 +246,6 @@ open class Toggle: Control, Changeable, FormFieldable {
toggleView.isEnabled = isEnabled
toggleView.isOn = isOn
}
/// Used to update any Accessibility properties.
open override func updateAccessibility() {
super.updateAccessibility()
if showText {
accessibilityValue = isSelected ? onText : offText
} else {
accessibilityValue = isSelected ? "On" : "Off"
}
}
/// This will change the state of the Selector and execute the actionBlock if provided.
open func toggle() {

View File

@ -153,7 +153,7 @@ open class ToggleView: Control, Changeable, FormFieldable {
// Update shadow layers frames to match the view's bounds
knobView.layer.insertSublayer(shadowLayer1, at: 0)
knobView.layer.insertSublayer(shadowLayer2, at: 0)
accessibilityLabel = "Toggle"
}
/// Resets to default settings.
@ -176,13 +176,6 @@ open class ToggleView: Control, Changeable, FormFieldable {
updateToggle()
}
/// Used to update any Accessibility properties.
open override func updateAccessibility() {
super.updateAccessibility()
accessibilityLabel = "Toggle"
}
/// This will change the state of the Selector and execute the actionBlock if provided.
open func toggle() {
@ -226,7 +219,7 @@ open class ToggleView: Control, Changeable, FormFieldable {
}
knobTrailingConstraint?.isActive = true
knobLeadingConstraint?.isActive = true
setNeedsLayout()
layoutIfNeeded()
}
private func updateToggle() {

View File

@ -138,6 +138,24 @@ open class Tooltip: Control, TooltipLaunchable {
contentView: tooltip.contentView),
presenter: self)
}
bridge_accessibilityLabelBlock = { [weak self] in
guard let self else { return "" }
var label = title
if label == nil {
label = content
}
if let label, !label.isEmpty {
return label
} else {
return "Modal"
}
}
bridge_accessibilityHintBlock = { [weak self] in
guard let self else { return "" }
return isEnabled ? "Double tap to open." : ""
}
}
/// Resets to default settings.
@ -163,23 +181,7 @@ open class Tooltip: Control, TooltipLaunchable {
//get the color for the image
icon.color = iconColorConfiguration.getColor(self)
}
/// Used to update any Accessibility properties.
open override func updateAccessibility() {
super.updateAccessibility()
var label = title
if label == nil {
label = content
}
if let label, !label.isEmpty {
accessibilityLabel = label
} else {
accessibilityLabel = "Modal"
}
accessibilityHint = isEnabled ? "Double tap to open." : ""
}
public static func accessibleText(for title: String?, content: String?, closeButtonText: String) -> String {
var label = ""
if let title {

View File

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

View File

@ -12,23 +12,11 @@ extension UITapGestureRecognizer {
/// Determines if the touch event has a action attribute within the range given
/// - Parameters:
/// - label: UILabel in question
/// - label: Label in question
/// - targetRange: Range to look within
/// - Returns: Wether the range in the label has an action
public func didTapActionInLabel(_ label: UILabel, inRange targetRange: NSRange) -> Bool {
guard let attributedText = label.attributedText else { return false }
let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: label.bounds.size)
let textStorage = NSTextStorage(attributedString: attributedText)
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
let location = location(in: label)
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
public func didTapActionInLabel(_ label: Label, inRange targetRange: NSRange) -> Bool {
let tapLocation = location(in: label)
return label.isAction(for: tapLocation, inRange: targetRange)
}
}

View File

@ -62,7 +62,7 @@ extension UIView {
} else {
removeDebugBorder()
}
if let view = self as? ViewProtocol {
if let view = self as? (any ViewProtocol) {
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 {
/// Property used to add context to the Grouping of a set.
var accessibilityValueText: String? { get set }
}

View File

@ -631,6 +631,195 @@ extension LayoutConstraintable {
return centerYAnchor.constraint(greaterThanOrEqualTo: found, constant: -constant).with { $0.priority = priority; $0.isActive = true }
}
}
// alignment
public enum LayoutAlignment: String, CaseIterable {
case fill
case leading
case top
case center
case trailing
case bottom
}
public enum LayoutDistribution: String, CaseIterable {
case fill
case fillProportionally
}
extension LayoutConstraintable {
public func removeConstraints() {
guard let view = self as? UIView, let superview = view.superview else { return }
// Remove all existing constraints on the containerView
let superviewConstraints = superview.constraints
for constraint in superviewConstraints {
if constraint.firstItem as? UIView == view
|| constraint.secondItem as? UIView == view {
superview.removeConstraint(constraint)
}
}
}
public func applyAlignment(_ alignment: LayoutAlignment, edges: UIEdgeInsets = UIEdgeInsets.zero) {
guard let superview = superview else { return }
removeConstraints()
switch alignment {
case .fill:
pinToSuperView(edges)
case .leading:
pinTop(edges.top)
pinLeading(edges.left)
pinTrailingLessThanOrEqualTo(anchor: superview.trailingAnchor, constant: edges.right)
pinBottom(edges.bottom)
case .trailing:
pinTop(edges.top)
pinLeadingGreaterThanOrEqualTo(anchor: superview.leadingAnchor, constant: edges.left)
pinTrailing(edges.right)
pinBottom(edges.bottom)
case .top:
pinTop(edges.top)
pinLeadingGreaterThanOrEqualTo(anchor: superview.leadingAnchor, constant: edges.left)
pinTrailingLessThanOrEqualTo(anchor: superview.trailingAnchor, constant: edges.right)
pinBottomLessThanOrEqualTo(anchor: superview.bottomAnchor, constant: edges.bottom)
case .bottom:
pinTopGreaterThanOrEqualTo(anchor: superview.topAnchor, constant: edges.top)
pinLeadingGreaterThanOrEqualTo(anchor: superview.leadingAnchor, constant: edges.left)
pinTrailingLessThanOrEqualTo(anchor: superview.trailingAnchor, constant: edges.right)
pinBottom(edges.bottom)
case .center:
pinCenterX()
pinTop(edges.top)
pinLeadingGreaterThanOrEqualTo(anchor: superview.leadingAnchor, constant: edges.left)
pinTrailingLessThanOrEqualTo(anchor: superview.trailingAnchor, constant: edges.right)
pinBottom(edges.bottom)
}
}
// Method to check if the view is pinned to its superview
public func isPinnedToSuperview() -> Bool {
isPinnedVerticallyToSuperview() && isPinnedHorizontallyToSuperview()
}
public func horizontalPinnedSize() -> CGSize? {
guard let view = self as? UIView, let superview = view.superview else { return nil }
let constraints = superview.constraints
var leadingPinnedObject: AnyObject?
var trailingPinnedObject: AnyObject?
for constraint in constraints {
if (constraint.firstItem === view && (constraint.firstAttribute == .leading || constraint.firstAttribute == .left)) {
leadingPinnedObject = constraint.secondItem as AnyObject?
} else if (constraint.secondItem === view && (constraint.secondAttribute == .leading || constraint.secondAttribute == .left)) {
leadingPinnedObject = constraint.firstItem as AnyObject?
} else if (constraint.firstItem === view && (constraint.firstAttribute == .trailing || constraint.firstAttribute == .right)) {
trailingPinnedObject = constraint.secondItem as AnyObject?
} else if (constraint.secondItem === view && (constraint.secondAttribute == .trailing || constraint.secondAttribute == .right)) {
trailingPinnedObject = constraint.firstItem as AnyObject?
}
}
// Ensure both leading and trailing pinned objects are identified
if let leadingObject = leadingPinnedObject, let trailingObject = trailingPinnedObject {
// Calculate the size based on the pinned objects
if let leadingView = leadingObject as? UIView, let trailingView = trailingObject as? UIView {
let leadingPosition = leadingView.convert(leadingView.bounds.origin, to: superview).x
let trailingPosition = trailingView.convert(trailingView.bounds.origin, to: superview).x + trailingView.bounds.width
return CGSize(width: trailingPosition - leadingPosition, height: view.bounds.size.height)
} else if let leadingGuide = leadingObject as? UILayoutGuide, let trailingGuide = trailingObject as? UILayoutGuide {
let leadingPosition = leadingGuide.layoutFrame.minX
let trailingPosition = trailingGuide.layoutFrame.maxX
return CGSize(width: trailingPosition - leadingPosition, height: view.bounds.size.height)
} else if let leadingView = leadingObject as? UIView, let trailingGuide = trailingObject as? UILayoutGuide {
let leadingPosition = leadingView.convert(leadingView.bounds.origin, to: superview).x
let trailingPosition = trailingGuide.layoutFrame.maxX
return CGSize(width: trailingPosition - leadingPosition, height: view.bounds.size.height)
} else if let leadingGuide = leadingObject as? UILayoutGuide, let trailingView = trailingObject as? UIView {
let leadingPosition = leadingGuide.layoutFrame.minX
let trailingPosition = trailingView.convert(trailingView.bounds.origin, to: superview).x + trailingView.bounds.width
return CGSize(width: trailingPosition - leadingPosition, height: view.bounds.size.height)
}
} else if let pinnedObject = leadingPinnedObject {
if let view = pinnedObject as? UIView {
return view.bounds.size
} else if let layoutGuide = pinnedObject as? UILayoutGuide {
return layoutGuide.layoutFrame.size
}
} else if let pinnedObject = trailingPinnedObject {
if let view = pinnedObject as? UIView {
return view.bounds.size
} else if let layoutGuide = pinnedObject as? UILayoutGuide {
return layoutGuide.layoutFrame.size
}
}
return nil
}
public func isPinnedHorizontallyToSuperview() -> Bool {
guard let view = self as? UIView, let superview = view.superview else { return false }
let constraints = superview.constraints
var leadingPinned = false
var trailingPinned = false
for constraint in constraints {
if (constraint.firstItem as? UIView == view && constraint.firstAttribute == .leading && constraint.relation == .equal) ||
(constraint.secondItem as? UIView == view && constraint.secondAttribute == .leading && constraint.relation == .equal) ||
(constraint.firstItem as? UIView == view && constraint.firstAttribute == .left && constraint.relation == .equal) ||
(constraint.secondItem as? UIView == view && constraint.secondAttribute == .left && constraint.relation == .equal) {
leadingPinned = true
}
if (constraint.firstItem as? UIView == view && constraint.firstAttribute == .trailing && constraint.relation == .equal) ||
(constraint.secondItem as? UIView == view && constraint.secondAttribute == .trailing && constraint.relation == .equal) ||
(constraint.firstItem as? UIView == view && constraint.firstAttribute == .right && constraint.relation == .equal) ||
(constraint.secondItem as? UIView == view && constraint.secondAttribute == .right && constraint.relation == .equal) {
trailingPinned = true
}
}
return leadingPinned && trailingPinned
}
public func isPinnedVerticallyToSuperview() -> Bool {
guard let view = self as? UIView, let superview = view.superview else { return false }
let constraints = superview.constraints
var topPinned = false
var bottomPinned = false
for constraint in constraints {
if (constraint.firstItem as? UIView == view && constraint.firstAttribute == .top && constraint.relation == .equal) ||
(constraint.secondItem as? UIView == view && constraint.secondAttribute == .top && constraint.relation == .equal) {
topPinned = true
}
if (constraint.firstItem as? UIView == view && constraint.firstAttribute == .bottom && constraint.relation == .equal) ||
(constraint.secondItem as? UIView == view && constraint.secondAttribute == .bottom && constraint.relation == .equal) {
bottomPinned = true
}
}
return topPinned && bottomPinned
}
}
//--------------------------------------------------
// MARK: - Implementations
//--------------------------------------------------

View File

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

View File

@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "discover.svg",
"filename" : "Discover-02.svg",
"idiom" : "universal"
}
],

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -1,49 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 20">
<g>
<path fill="#231F20" d="M1.447,7.449H0v5.067h1.447c0.763,0,1.324-0.184,1.806-0.587c0.579-0.482,0.921-1.201,0.921-1.946
C4.164,8.492,3.051,7.449,1.447,7.449z M2.604,11.254c-0.307,0.281-0.71,0.403-1.35,0.403H0.991V8.308h0.263
c0.64,0,1.026,0.114,1.35,0.412c0.342,0.307,0.544,0.772,0.544,1.262C3.147,10.465,2.946,10.947,2.604,11.254z"/>
<rect id="XMLID_422_" x="4.62" y="7.449" fill="#231F20" width="0.991" height="5.067"/>
<path id="XMLID_421_" fill="#231F20" d="M8.022,9.395c-0.596-0.219-0.763-0.368-0.763-0.64c0-0.316,0.307-0.561,0.736-0.561
c0.298,0,0.535,0.123,0.798,0.412l0.517-0.675C8.89,7.563,8.381,7.37,7.82,7.37c-0.894,0-1.578,0.622-1.578,1.447
c0,0.701,0.316,1.052,1.245,1.385c0.386,0.14,0.587,0.228,0.684,0.289c0.202,0.132,0.298,0.316,0.298,0.526
c0,0.412-0.324,0.71-0.763,0.71c-0.473,0-0.85-0.237-1.078-0.675l-0.64,0.614c0.456,0.666,0.999,0.964,1.753,0.964
c1.026,0,1.745-0.684,1.745-1.666C9.486,10.167,9.153,9.807,8.022,9.395z"/>
<path id="XMLID_420_" fill="#231F20" d="M9.793,9.982c0,1.49,1.166,2.639,2.674,2.639c0.421,0,0.789-0.088,1.236-0.298v-1.166
c-0.395,0.395-0.745,0.552-1.192,0.552c-0.991,0-1.701-0.719-1.701-1.745c0-0.973,0.728-1.736,1.657-1.736
c0.473,0,0.824,0.167,1.236,0.57V7.633c-0.43-0.219-0.789-0.307-1.219-0.307C10.994,7.335,9.793,8.51,9.793,9.982z"/>
<polygon id="XMLID_419_" fill="#231F20" points="21.532,10.85 20.182,7.449 19.104,7.449 21.26,12.648 21.786,12.648 23.978,7.449
22.908,7.449 "/>
<polygon id="XMLID_418_" fill="#231F20" points="24.425,12.516 27.222,12.516 27.222,11.657 25.407,11.657 25.407,10.289
27.152,10.289 27.152,9.43 25.407,9.43 25.407,8.308 27.222,8.308 27.222,7.449 24.425,7.449 "/>
<path fill="#231F20" d="M31.132,8.948c0-0.947-0.649-1.499-1.788-1.499h-1.464v5.067h0.991v-2.034h0.132l1.368,2.034h1.219
l-1.596-2.13C30.72,10.228,31.132,9.719,31.132,8.948z M29.151,9.781h-0.289V8.247h0.307c0.614,0,0.947,0.254,0.947,0.754
C30.115,9.509,29.782,9.781,29.151,9.781z"/>
<linearGradient id="XMLID_2_" gradientUnits="userSpaceOnUse" x1="18.0982" y1="592.1596" x2="16.2331" y2="589.2393" gradientTransform="matrix(1 0 0 1 0 -580)">
<stop offset="0" stop-color="#F89F20"/>
<stop offset="0.2502" stop-color="#F79A20"/>
<stop offset="0.5331" stop-color="#F68D20"/>
<stop offset="0.6196" stop-color="#F58720"/>
<stop offset="0.7232" stop-color="#F48120"/>
<stop offset="1" stop-color="#F37521"/>
</linearGradient>
<circle id="XMLID_415_" fill="url(#XMLID_2_)" cx="16.719" cy="10" r="2.692"/>
<linearGradient id="XMLID_3_" gradientUnits="userSpaceOnUse" x1="17.8034" y1="592.1198" x2="15.0775" y2="586.7917" gradientTransform="matrix(1 0 0 1 0 -580)">
<stop offset="0" stop-color="#F58720"/>
<stop offset="0.3587" stop-color="#E16F27"/>
<stop offset="0.703" stop-color="#D4602C"/>
<stop offset="0.9816" stop-color="#D05B2E"/>
</linearGradient>
<circle id="XMLID_414_" opacity="0.65" fill="url(#XMLID_3_)" cx="16.719" cy="10" r="2.692"/>
<g id="XMLID_430_">
<path fill="#231F20" d="M31.763,7.642c0-0.088-0.061-0.14-0.167-0.14h-0.14v0.447h0.105V7.773l0.123,0.175h0.132l-0.149-0.184
C31.728,7.747,31.763,7.703,31.763,7.642z M31.579,7.703h-0.018V7.589h0.018c0.053,0,0.079,0.018,0.079,0.061
C31.658,7.685,31.632,7.703,31.579,7.703z"/>
<path fill="#231F20" d="M31.614,7.335c-0.219,0-0.386,0.175-0.386,0.386c0,0.219,0.175,0.386,0.386,0.386
c0.21,0,0.386-0.175,0.386-0.386C32,7.51,31.825,7.335,31.614,7.335z M31.614,8.045c-0.167,0-0.307-0.14-0.307-0.316
c0-0.175,0.14-0.316,0.307-0.316c0.167,0,0.307,0.149,0.307,0.316C31.921,7.905,31.781,8.045,31.614,8.045z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "jcb.svg",
"filename" : "jcb-emblem-logo.svg",
"idiom" : "universal"
}
],

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="_レイヤー_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 34 26.19"><path d="M34,20.91c0,2.91-2.37,5.28-5.28,5.28H0V5.28C0,2.37,2.37,0,5.28,0h28.72V20.91Z" fill="#fff"/><path d="M24.65,15.55h2.18l.27-.02c.42-.08,.77-.46,.77-.98s-.35-.87-.77-.98l-.27-.02h-2.18v2Z" fill="#469b23"/><path d="M26.58,1.77c-2.08,0-3.78,1.68-3.78,3.78v3.93h5.34c.12,0,.27,0,.37,.02,1.21,.06,2.1,.69,2.1,1.77,0,.85-.6,1.58-1.72,1.72v.04c1.23,.08,2.16,.77,2.16,1.83,0,1.14-1.04,1.89-2.41,1.89h-5.86v7.69h5.55c2.08,0,3.78-1.68,3.78-3.78V1.77h-5.53Z" fill="#469b23"/><path d="M27.6,11.51c0-.5-.35-.83-.77-.89l-.21-.02h-1.97v1.83h1.97l.21-.02c.42-.06,.77-.39,.77-.89Z" fill="#469b23"/><path d="M5.67,1.77c-2.08,0-3.78,1.68-3.78,3.78V14.88c1.06,.52,2.16,.85,3.26,.85,1.31,0,2.02-.79,2.02-1.87v-4.41h3.24v4.39c0,1.7-1.06,3.1-4.66,3.1-2.18,0-3.89-.48-3.89-.48v7.96H7.42c2.08,0,3.78-1.68,3.78-3.78V1.77H5.67Z" fill="#0c2c84"/><path d="M16.13,1.77c-2.08,0-3.78,1.68-3.78,3.78v4.95c.96-.81,2.62-1.33,5.3-1.21,1.43,.06,2.97,.46,2.97,.46v1.6c-.77-.39-1.68-.75-2.87-.83-2.04-.15-3.26,.85-3.26,2.6s1.23,2.76,3.26,2.6c1.18-.08,2.1-.46,2.87-.83v1.6s-1.52,.39-2.97,.46c-2.68,.12-4.34-.39-5.3-1.21v8.73h5.55c2.08,0,3.78-1.68,3.78-3.78V1.77h-5.55Z" fill="#d7182a"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1,51 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 20">
<g>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="-856.4599" y1="503.2267" x2="-855.8029" y2="503.2267" gradientTransform="matrix(12.5258 0 0 -12.5258 10748.9648 6314.5825)">
<stop offset="0" stop-color="#007940"/>
<stop offset="0.229" stop-color="#00873F"/>
<stop offset="0.743" stop-color="#40A737"/>
<stop offset="1" stop-color="#5CB531"/>
</linearGradient>
<path fill="url(#SVGID_1_)" d="M22.7,12.1h1.9c0.1,0,0.2,0,0.2,0c0.4-0.1,0.7-0.4,0.7-0.9c0-0.4-0.3-0.8-0.7-0.9c-0.1,0-0.2,0-0.2,0h-1.9
V12.1z"/>
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="-856.4599" y1="503.3283" x2="-855.8034" y2="503.3283" gradientTransform="matrix(12.5258 0 0 -12.5258 10748.9648 6314.5825)">
<stop offset="0" stop-color="#007940"/>
<stop offset="0.229" stop-color="#00873F"/>
<stop offset="0.743" stop-color="#40A737"/>
<stop offset="1" stop-color="#5CB531"/>
</linearGradient>
<path fill="url(#SVGID_2_)" d="M24.5,0c-1.8,0-3.3,1.5-3.3,3.3v3.5h4.7c0.1,0,0.2,0,0.3,0C27.2,6.9,28,7.4,28,8.4c0,0.8-0.5,1.4-1.5,1.5v0
c1.1,0.1,1.9,0.7,1.9,1.6c0,1-0.9,1.7-2.1,1.7h-5.2V20H26c1.8,0,3.3-1.5,3.3-3.3V0L24.5,0z"/>
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="-856.4599" y1="503.4401" x2="-855.8029" y2="503.4401" gradientTransform="matrix(12.5258 0 0 -12.5258 10748.9648 6314.5825)">
<stop offset="0" stop-color="#007940"/>
<stop offset="0.229" stop-color="#00873F"/>
<stop offset="0.743" stop-color="#40A737"/>
<stop offset="1" stop-color="#5CB531"/>
</linearGradient>
<path fill="url(#SVGID_3_)" d="M25.3,8.6c0-0.4-0.3-0.7-0.7-0.8c0,0-0.1,0-0.2,0h-1.7v1.6h1.7c0.1,0,0.2,0,0.2,0C25,9.3,25.3,9,25.3,8.6
L25.3,8.6z"/>
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="-857.9309" y1="503.329" x2="-857.2637" y2="503.329" gradientTransform="matrix(12.5258 0 0 -12.5258 10748.9648 6314.5825)">
<stop offset="0" stop-color="#1F286F"/>
<stop offset="0.475" stop-color="#004E94"/>
<stop offset="0.826" stop-color="#0066B1"/>
<stop offset="1" stop-color="#006FBC"/>
</linearGradient>
<path fill="url(#SVGID_4_)" d="M6,0C4.2,0,2.7,1.5,2.7,3.3v8.2c0.9,0.5,1.9,0.8,2.9,0.8c1.2,0,1.8-0.7,1.8-1.6V6.8h2.9v3.9
c0,1.5-0.9,2.7-4.1,2.7c-1.9,0-3.4-0.4-3.4-0.4v7h4.9c1.8,0,3.3-1.5,3.3-3.3V0L6,0z"/>
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="-857.1989" y1="503.3275" x2="-856.5508" y2="503.3275" gradientTransform="matrix(12.5258 0 0 -12.5258 10748.9648 6314.5825)">
<stop offset="0" stop-color="#6C2C2F"/>
<stop offset="0.173" stop-color="#882730"/>
<stop offset="0.573" stop-color="#BE1833"/>
<stop offset="0.859" stop-color="#DC0436"/>
<stop offset="1" stop-color="#E60039"/>
</linearGradient>
<path fill="url(#SVGID_5_)" d="M15.2,0c-1.8,0-3.3,1.5-3.3,3.3v4.4c0.8-0.7,2.3-1.2,4.7-1.1C17.8,6.7,19.2,7,19.2,7v1.4
c-0.7-0.3-1.5-0.7-2.5-0.7c-1.8-0.1-2.9,0.8-2.9,2.3c0,1.6,1.1,2.4,2.9,2.3c1-0.1,1.8-0.4,2.5-0.7V13c0,0-1.3,0.3-2.6,0.4
c-2.4,0.1-3.8-0.3-4.7-1.1V20h4.9c1.8,0,3.3-1.5,3.3-3.3V0L15.2,0z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -1,6 +1,26 @@
1.0.69
----------------
- DatePicker - Refactored how this is shown
- Checkbox Item/Group - Accessibility Refactor
- Radiobox Item/Group - Accessibility Refactor
- Radiobutton Item/Group - Accessibility Refactor
- CXTDT-553663 - DropdownSelect - Accessibility - has popup
- CXTDT-577463 - InputField - Accessibility
1.0.69
----------------
- Expired Build because of a issue
1.0.67
----------------
- CXTDT-553663 - DropdownSelect - Accessibility - has popup
- CXTDT-568463 - Calendar - On long press, hover randomizes
- CXTDT-568412 - Calendar - Incorrect side nav icon size
- CXTDT-568422 - Calendar - DarkMode Legend icon fill using Light mode color
- CXTDT-565796 - DropdownSelect - Accessibility
- CXTDT-560458 - Dropdown/TextArea - Different voiceover
- CXTDT-565106 - InputField - CreditCard - Icons
- CXTDT-546821 - TextArea - Accessibility
- CXTDT-560823 - TextArea - Accessibility
1.0.66
----------------