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

View File

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

View File

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

View File

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

View File

@ -52,7 +52,7 @@ open class View: UIView, ViewProtocol, UserInfoable {
open var surface: Surface = .light { didSet { setNeedsUpdate() } } open var surface: Surface = .light { didSet { setNeedsUpdate() } }
open var isEnabled: Bool = true { didSet { setNeedsUpdate() } } open var isEnabled: Bool = true { didSet { setNeedsUpdate() } }
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Lifecycle // MARK: - Lifecycle
//-------------------------------------------------- //--------------------------------------------------
@ -91,4 +91,132 @@ open class View: UIView, ViewProtocol, UserInfoable {
setNeedsUpdate() 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) label.widthGreaterThanEqualTo(constant: minWidth)
maxWidthConstraint = label.widthLessThanEqualTo(constant: 0).with { $0.isActive = false } maxWidthConstraint = label.widthLessThanEqualTo(constant: 0).with { $0.isActive = false }
clipsToBounds = true clipsToBounds = true
bridge_accessibilityLabelBlock = { [weak self] in
guard let self else { return "" }
return text
}
} }
/// Resets to default settings. /// Resets to default settings.
@ -179,10 +184,4 @@ open class Badge: View {
label.surface = surface label.surface = surface
label.isEnabled = isEnabled label.isEnabled = isEnabled
} }
open override func updateAccessibility() {
super.updateAccessibility()
accessibilityLabel = text
}
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -68,16 +68,16 @@ class CalendarHeaderReusableView: UICollectionReusableView {
$0.kind = .ghost $0.kind = .ghost
$0.iconName = .leftCaret $0.iconName = .leftCaret
$0.iconOffset = .init(x: -2, y: 0) $0.iconOffset = .init(x: -2, y: 0)
$0.icon.size = .small $0.customContainerSize = 40
$0.size = .small $0.icon.customSize = 16
} }
internal var nextButton = ButtonIcon().with { internal var nextButton = ButtonIcon().with {
$0.kind = .ghost $0.kind = .ghost
$0.iconName = .rightCaret $0.iconName = .rightCaret
$0.iconOffset = .init(x: 2, y: 0) $0.iconOffset = .init(x: 2, y: 0)
$0.icon.size = .small $0.customContainerSize = 40
$0.size = .small $0.icon.customSize = 16
} }
internal var headerTitle = Label().with { 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. /// This will change the state of the Selector and execute the actionBlock if provided.
open override func toggle() { open override func toggle() {
guard isEnabled else { return }
//removed error //removed error
if showError && isSelected == false { if showError && isSelected == false {
showError.toggle() showError.toggle()

View File

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

View File

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

View File

@ -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. /// A dropdown select is an expandable menu of predefined options that allows a customer to make a single selection.
@objc(VDSDatePicker) @objc(VDSDatePicker)
open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopoverPresentationControllerDelegate { open class DatePicker: EntryFieldBase {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Initializers // MARK: - Initializers
//-------------------------------------------------- //--------------------------------------------------
@ -26,12 +26,19 @@ open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopov
//-------------------------------------------------- //--------------------------------------------------
/// A callback when the selected option changes. Passes parameters (option). /// A callback when the selected option changes. Passes parameters (option).
open var onDateSelected: ((Date, DatePicker) -> Void)? open var onDateSelected: ((Date, DatePicker) -> Void)?
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Private Properties // 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 minWidthDefault = 186.0
internal var bottomStackView: UIStackView = { internal var bottomStackView: UIStackView = {
return UIStackView().with { return UIStackView().with {
$0.translatesAutoresizingMaskIntoConstraints = false $0.translatesAutoresizingMaskIntoConstraints = false
@ -41,6 +48,33 @@ open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopov
$0.spacing = VDSLayout.space2X $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 // MARK: - Public Properties
@ -87,12 +121,12 @@ open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopov
} }
open var dateFormat: DateFormat = .shortNumeric { didSet{ setNeedsUpdate() } } open var dateFormat: DateFormat = .shortNumeric { didSet{ setNeedsUpdate() } }
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Configuration Properties // MARK: - Configuration Properties
//-------------------------------------------------- //--------------------------------------------------
internal override var containerSize: CGSize { CGSize(width: minWidthDefault, height: 44) } internal override var containerSize: CGSize { CGSize(width: minWidthDefault, height: 44) }
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Overrides // 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. /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
open override func setup() { open override func setup() {
super.setup() super.setup()
fieldStackView.isAccessibilityElement = true
fieldStackView.accessibilityLabel = "Date Picker"
fieldStackView.accessibilityHint = "Double Tap to open"
// setting color config // setting color config
selectedDateLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable() selectedDateLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable()
// tap gesture // tap gesture
fieldStackView containerView
.publisher(for: UITapGestureRecognizer()) .publisher(for: UITapGestureRecognizer())
.sink { [weak self] _ in .sink { [weak self] _ in
guard let self else { return } guard let self else { return }
if self.isEnabled && !self.isReadOnly { if isEnabled && !isReadOnly {
self.togglePicker() showPopover()
} }
} }
.store(in: &subscribers) .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 { open override func getFieldContainer() -> UIView {
@ -129,9 +168,10 @@ open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopov
} }
controlStackView.addArrangedSubview(calendarIcon) controlStackView.addArrangedSubview(calendarIcon)
controlStackView.addArrangedSubview(selectedDateLabel) controlStackView.addArrangedSubview(selectedDateLabel)
controlStackView.addArrangedSubview(hiddenView)
return controlStackView return controlStackView
} }
/// Used to make changes to the View based off a change events or from local properties. /// Used to make changes to the View based off a change events or from local properties.
open override func updateView() { open override func updateView() {
super.updateView() super.updateView()
@ -145,13 +185,6 @@ open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopov
calendarIcon.color = iconColorConfiguration.getColor(self) 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. /// Resets to default settings.
open override func reset() { open override func reset() {
super.reset() super.reset()
@ -163,32 +196,249 @@ open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopov
formatter.dateFormat = dateFormat.format formatter.dateFormat = dateFormat.format
selectedDateLabel.text = formatter.string(from: date) selectedDateLabel.text = formatter.string(from: date)
} }
}
internal func togglePicker() { extension DatePicker {
let calendarVC = DatePickerViewController(calendarModel, delegate: self)
calendarVC.modalPresentationStyle = .popover private func showPopover() {
calendarVC.selectedDate = selectedDate ?? Date() guard let viewController = UIApplication.topViewController(), var parentView = viewController.view, !popoverVisible else {
if let popoverController = calendarVC.popoverPresentationController { hidePopoverView()
popoverController.delegate = self return
popoverController.sourceView = containerView
popoverController.sourceRect = containerView.bounds
popoverController.permittedArrowDirections = .up
} }
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) { private func findScrollView(from view: UIView) -> UIScrollView? {
selectedDate = date var currentView = view
controller.dismiss(animated: true) { [weak self] in while let superview = currentView.superview {
guard let self else { return } if let scrollView = superview as? UIScrollView {
self.sendActions(for: .valueChanged) return scrollView
UIAccessibility.post(notification: .layoutChanged, argument: self.fieldStackView) }
currentView = superview
} }
return nil
} }
public func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle { private func calculatePopoverPosition(relativeTo sourceView: UIView, in parentView: UIView, size: CGSize, with spacing: CGFloat) -> CGPoint? {
return .none 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 // 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. /// 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() }} open var showInlineLabel: Bool = false { didSet { setNeedsUpdate() }}
@ -66,6 +54,8 @@ open class DropdownSelect: EntryFieldBase {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Private Properties // MARK: - Private Properties
//-------------------------------------------------- //--------------------------------------------------
internal override var responder: UIResponder? { dropdownField }
internal var minWidthDefault = 66.0 internal var minWidthDefault = 66.0
internal var minWidthInlineLabel = 102.0 internal var minWidthInlineLabel = 102.0
internal override var minWidth: CGFloat { showInlineLabel ? minWidthInlineLabel : minWidthDefault } 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. /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
open override func setup() { open override func setup() {
super.setup() super.setup()
accessibilityHintText = "has popup, Double tap to open."
fieldStackView.isAccessibilityElement = true
inlineDisplayLabel.isAccessibilityElement = true inlineDisplayLabel.isAccessibilityElement = true
dropdownField.width(0) dropdownField.width(0)
@ -276,57 +266,11 @@ open class DropdownSelect: EntryFieldBase {
statusIcon.color = iconColorConfiguration.getColor(self) 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() { @objc open func pickerDoneClicked() {
optionsPicker.isHidden = true optionsPicker.isHidden = true
dropdownField.resignFirstResponder() dropdownField.resignFirstResponder()
setNeedsUpdate() setNeedsUpdate()
UIAccessibility.post(notification: .layoutChanged, argument: fieldStackView) UIAccessibility.post(notification: .layoutChanged, argument: containerView)
}
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()
} }
} }
@ -337,8 +281,8 @@ extension DropdownSelect: UIPickerViewDelegate, UIPickerViewDataSource {
internal func launchPicker() { internal func launchPicker() {
if optionsPicker.isHidden { if optionsPicker.isHidden {
UIAccessibility.post(notification: .layoutChanged, argument: optionsPicker)
dropdownField.becomeFirstResponder() dropdownField.becomeFirstResponder()
UIAccessibility.post(notification: .layoutChanged, argument: optionsPicker)
} else { } else {
dropdownField.resignFirstResponder() dropdownField.resignFirstResponder()
} }

View File

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

View File

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

View File

@ -91,16 +91,14 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
private struct LabelAction { private struct LabelAction {
var range: NSRange var range: NSRange
var action: PassthroughSubject<Void, Never> var action: PassthroughSubject<Void, Never>
var accessibilityId: Int = 0 var frame: CGRect = .zero
func performAction() { func performAction() {
action.send() action.send()
} }
init(range: NSRange, action: PassthroughSubject<Void, Never>, accessibilityID: Int = 0) { init(range: NSRange, action: PassthroughSubject<Void, Never>) {
self.range = range self.range = range
self.action = action self.action = action
self.accessibilityId = accessibilityID
} }
} }
@ -215,7 +213,8 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
} }
} }
open func setup() {} open func setup() {
}
open func reset() { open func reset() {
shouldUpdateView = false shouldUpdateView = false
@ -242,7 +241,6 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
} }
open func updateAccessibility() { open func updateAccessibility() {
accessibilityLabel = text
if isEnabled { if isEnabled {
accessibilityTraits.remove(.notEnabled) accessibilityTraits.remove(.notEnabled)
} else { } else {
@ -263,24 +261,7 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
super.layoutSubviews() super.layoutSubviews()
applyActions() applyActions()
} }
/// Addig custom accessibillty actions from the collection of attributes.
open override func accessibilityActivate() -> Bool {
guard let accessibleActions = accessibilityCustomActions else { return false }
for actionable in actions {
for action in accessibleActions {
if action.hash == actionable.accessibilityId {
actionable.performAction()
return true
}
}
}
return false
}
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Private Methods // MARK: - Private Methods
//-------------------------------------------------- //--------------------------------------------------
@ -373,15 +354,27 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
//see if the attribute is Actionable //see if the attribute is Actionable
if let actionable = attribute as? any ActionLabelAttributeModel, mutableAttributedString.isValid(range: actionable.range) { if let actionable = attribute as? any ActionLabelAttributeModel, mutableAttributedString.isValid(range: actionable.range) {
//create a accessibleAction //create a accessibleAction
let customAccessibilityAction = customAccessibilityAction(text: mutableAttributedString.string, range: actionable.range, accessibleText: actionable.accessibleText) let customAccessibilityAction = customAccessibilityElement(text: mutableAttributedString.string,
range: actionable.range,
accessibleText: actionable.accessibleText)
// creat the action
let labelAction = LabelAction(range: actionable.range, action: actionable.action)
// set the action of the accessibilityElement
customAccessibilityAction?.accessibilityAction = { [weak self] in
guard let self, isEnabled else { return }
labelAction.performAction()
}
//create a wrapper for the attributes range, block and //create a wrapper for the attributes range, block and
actions.append(LabelAction(range: actionable.range, action: actionable.action, accessibilityID: customAccessibilityAction?.hashValue ?? -1)) actions.append(labelAction)
isUserInteractionEnabled = true
} }
} }
if let accessibilityElements, !accessibilityElements.isEmpty { if let accessibilityElements, !accessibilityElements.isEmpty {
let staticText = UIAccessibilityElement(accessibilityContainer: self) let staticText = AccessibilityActionElement(accessibilityContainer: self)
staticText.accessibilityLabel = text staticText.accessibilityLabel = text
staticText.accessibilityFrameInContainerSpace = bounds staticText.accessibilityFrameInContainerSpace = bounds
@ -392,62 +385,236 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
} }
} }
//--------------------------------------------------
// MARK: - Touch Events
//--------------------------------------------------
@objc private func textLinkTapped(_ gesture: UITapGestureRecognizer) { @objc private func textLinkTapped(_ gesture: UITapGestureRecognizer) {
for actionable in actions { let location = gesture.location(in: self)
// This determines if we tapped on the desired range of text. if let action = actions.first(where: { isAction(for: location, inRange: $0.range) }) {
if gesture.didTapActionInLabel(self, inRange: actionable.range) { action.performAction()
actionable.performAction()
return
}
} }
} }
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) // Assert that tapped occured within acceptable bounds based on alignment.
switch textAlignment {
// Calculate the frame of the substring 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 layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: bounds.size) let textContainer = NSTextContainer(size: .zero)
let textStorage = NSTextStorage(attributedString: attributedText)
layoutManager.addTextContainer(textContainer) layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager) 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() var glyphRange = NSRange()
// Convert the range for the substring into a range of glyphs // Convert the range for the substring into a range of glyphs
layoutManager.characterRange(forGlyphRange: range, actualGlyphRange: &glyphRange) layoutManager.characterRange(forGlyphRange: range, actualGlyphRange: &glyphRange)
let substringBounds = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer) let substringBounds = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
// Create custom accessibility element // Create custom accessibility element
let element = UIAccessibilityElement(accessibilityContainer: self) let element = AccessibilityActionElement(accessibilityContainer: self)
element.accessibilityLabel = actionText element.accessibilityLabel = actionText
element.accessibilityTraits = .link element.accessibilityTraits = .link
element.accessibilityHint = "Double tap to open"
element.accessibilityFrameInContainerSpace = substringBounds element.accessibilityFrameInContainerSpace = substringBounds
//TODO: accessibilityHint for Label
// element.accessibilityHint = MVMCoreUIUtility.hardcodedString(withKey: "swipe_to_select_with_action_hint")
accessibilityElements = (accessibilityElements ?? []).compactMap{$0 as? UIAccessibilityElement}.filter { $0.accessibilityLabel != actionText } accessibilityElements = (accessibilityElements ?? []).compactMap{$0 as? UIAccessibilityElement}.filter { $0.accessibilityLabel != actionText }
accessibilityElements?.append(element) accessibilityElements?.append(element)
return element
let accessibleAction = UIAccessibilityCustomAction(name: actionText, target: self, selector: #selector(accessibilityCustomAction(_:)))
accessibilityCustomActions?.append(accessibleAction)
return accessibleAction
} }
@objc private func accessibilityCustomAction(_ action: UIAccessibilityCustomAction) { open var accessibilityAction: ((Label) -> Void)?
for actionable in actions { open override var isAccessibilityElement: Bool {
if action.hash == actionable.accessibilityId { get {
actionable.performAction() var block: AXBoolReturnBlock?
return
// 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 isAccessibilityElement = false
accessibilityElements = [closeButton, typeIcon, titleLabel, subTitleLabel, buttonGroup] accessibilityElements = [closeButton, typeIcon, titleLabel, subTitleLabel, buttonGroup]
closeButton.accessibilityTraits = [.button] closeButton.accessibilityTraits = [.button]
closeButton.accessibilityLabel = "Close Notification"
typeIcon.bridge_accessibilityLabelBlock = { [weak self] in
guard let self else { return "" }
return style.accessibleText
}
} }
/// Resets to default settings. /// Resets to default settings.
@ -372,12 +378,6 @@ open class Notification: View {
} }
} }
open override func updateAccessibility() {
super.updateAccessibility()
closeButton.accessibilityLabel = "Close Notification"
typeIcon.accessibilityLabel = style.accessibleText
}
private func setConstraints() { private func setConstraints() {
labelViewAndButtonViewConstraint?.deactivate() labelViewAndButtonViewConstraint?.deactivate()
labelViewBottomConstraint?.deactivate() labelViewBottomConstraint?.deactivate()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -144,6 +144,135 @@ open class TextView: UITextView, ViewProtocol, Errorable {
shouldUpdateView = true shouldUpdateView = true
setNeedsUpdate() 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 // 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 { open class TileContainerBase<PaddingType: DefaultValuing>: Control where PaddingType.ValueType == CGFloat {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Initializers // MARK: - Initializers
//-------------------------------------------------- //--------------------------------------------------
@ -69,6 +70,7 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
case secondary case secondary
case white case white
case black case black
case token(UIColor.VDSColor)
case custom(UIColor) case custom(UIColor)
private var reflectedValue: String { String(reflecting: self) } private var reflectedValue: String { String(reflecting: self) }
@ -108,8 +110,13 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
$0.clipsToBounds = true $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 // MARK: - Public Properties
//-------------------------------------------------- //--------------------------------------------------
@ -178,12 +185,7 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
//-------------------------------------------------- //--------------------------------------------------
internal var widthConstraint: NSLayoutConstraint? internal var widthConstraint: NSLayoutConstraint?
internal var heightConstraint: 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 // MARK: - Configuration
//-------------------------------------------------- //--------------------------------------------------
@ -221,28 +223,20 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
super.setup() super.setup()
isAccessibilityElement = false isAccessibilityElement = false
let layoutGuide = UILayoutGuide()
addLayoutGuide(layoutGuide)
layoutGuide
.pinTop()
.pinLeading()
.pinTrailing(0, .defaultHigh)
.pinBottom(0, .defaultHigh)
addSubview(backgroundImageView)
addSubview(containerView) addSubview(containerView)
containerView.addSubview(contentView)
addSubview(highlightView)
containerView.pinToSuperView() containerView.pinToSuperView()
widthConstraint = layoutGuide.widthAnchor.constraint(equalToConstant: 0) containerView.addSubview(backgroundImageView)
heightGreaterThanConstraint = layoutGuide.heightAnchor.constraint(greaterThanOrEqualToConstant: 44.0)
heightGreaterThanConstraint?.isActive = false
heightConstraint = layoutGuide.heightAnchor.constraint(equalToConstant: 0)
backgroundImageView.pinToSuperView() 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: .horizontal)
backgroundImageView.setContentHuggingPriority(.defaultLow, for: .vertical) backgroundImageView.setContentHuggingPriority(.defaultLow, for: .vertical)
@ -251,20 +245,28 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
backgroundImageView.isUserInteractionEnabled = false backgroundImageView.isUserInteractionEnabled = false
backgroundImageView.isHidden = true 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.isHidden = true
highlightView.backgroundColor = .clear highlightView.backgroundColor = .clear
//corner radius //corner radius
layer.cornerRadius = cornerRadius containerView.layer.cornerRadius = cornerRadius
backgroundImageView.layer.cornerRadius = cornerRadius backgroundImageView.layer.cornerRadius = cornerRadius
highlightView.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 /// 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() super.reset()
shouldUpdateView = false shouldUpdateView = false
color = .white color = .white
aspectRatio = .ratio1x1 aspectRatio = .none
imageFallbackColor = .light imageFallbackColor = .light
width = nil width = nil
height = nil height = nil
@ -297,53 +299,15 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
highlightView.backgroundColor = hightLightViewColorConfiguration.getColor(self) highlightView.backgroundColor = hightLightViewColorConfiguration.getColor(self)
highlightView.isHidden = !isHighlighted highlightView.isHidden = !isHighlighted
layer.borderColor = borderColorConfiguration.getColor(self).cgColor containerView.layer.borderColor = borderColorConfiguration.getColor(self).cgColor
layer.borderWidth = showBorder ? VDSFormControls.borderWidth : 0 containerView.layer.borderWidth = showBorder ? VDSFormControls.borderWidth : 0
containerTopConstraint?.constant = padding.value contentView.removeConstraints()
containerLeadingConstraint?.constant = padding.value contentView.pinToSuperView(.uniform(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()
if showDropShadow, surface == .light { updateContainerView()
addDropShadow(dropShadowConfiguration)
} else {
removeDropShadows()
}
} }
open override func updateAccessibility() {
super.updateAccessibility()
containerView.isAccessibilityElement = onClickSubscriber != nil
containerView.accessibilityHint = "Double tap to open."
containerView.accessibilityLabel = nil
}
open override var accessibilityElements: [Any]? { open override var accessibilityElements: [Any]? {
get { get {
var items = [Any]() var items = [Any]()
@ -370,13 +334,6 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
set {} 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 // MARK: - Public Methods
//-------------------------------------------------- //--------------------------------------------------
@ -401,25 +358,25 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
switch backgroundEffect { switch backgroundEffect {
case .transparency: case .transparency:
alphaConfiguration = 0.8 alphaConfiguration = 0.8
removeGradientLayer() containerView.removeGradientLayer()
case .gradient(let firstColor, let secondColor): case .gradient(let firstColor, let secondColor):
alphaConfiguration = 1.0 alphaConfiguration = 1.0
addGradientLayer(with: firstColor, secondColor: secondColor) containerView.addGradientLayer(with: firstColor, secondColor: secondColor)
backgroundImageView.isHidden = true backgroundImageView.isHidden = true
backgroundImageView.alpha = 1.0 backgroundImageView.alpha = 1.0
case .none: case .none:
alphaConfiguration = 1.0 alphaConfiguration = 1.0
removeGradientLayer() containerView.removeGradientLayer()
} }
if let backgroundImage { if let backgroundImage {
backgroundImageView.image = backgroundImage backgroundImageView.image = backgroundImage
backgroundImageView.isHidden = false backgroundImageView.isHidden = false
backgroundImageView.alpha = alphaConfiguration backgroundImageView.alpha = alphaConfiguration
backgroundColor = imageFallbackColor.withAlphaComponent(alphaConfiguration) containerView.backgroundColor = imageFallbackColor.withAlphaComponent(alphaConfiguration)
} else { } else {
backgroundImageView.isHidden = true backgroundImageView.isHidden = true
backgroundImageView.alpha = 1.0 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) 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 { extension TileContainerBase {
@ -484,6 +511,8 @@ extension TileContainerBase {
return whiteColorConfig.getColor(object.surface) return whiteColorConfig.getColor(object.surface)
case .black: case .black:
return blackColorConfig.getColor(object.surface) return blackColorConfig.getColor(object.surface)
case .token(let vdsColor):
return vdsColor.uiColor
case .custom(let color): case .custom(let color):
return 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. /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
open override func setup() { open override func setup() {
super.setup() super.setup()
aspectRatio = .none
color = .black color = .black
aspectRatio = .none
addContentView(stackView) addContentView(stackView)
//badge //badge
@ -386,6 +387,7 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
/// Resets to default settings. /// Resets to default settings.
open override func reset() { open override func reset() {
shouldUpdateView = false shouldUpdateView = false
super.reset()
aspectRatio = .none aspectRatio = .none
color = .black color = .black
//models //models
@ -405,11 +407,7 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
updateBadge() updateBadge()
updateTitleLockup() updateTitleLockup()
updateIcons() updateIcons()
///Content-driven height Tilelets - Minimum height is configurable. updateTextPositionAlignment()
///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()
}
setNeedsLayout() setNeedsLayout()
} }
@ -584,6 +582,7 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
} }
private func updateTextPositionAlignment() { private func updateTextPositionAlignment() {
guard width != nil && (aspectRatio != .none || height != nil) else { return }
switch textPostion { switch textPostion {
case .top: case .top:
titleLockupTopConstraint?.activate() titleLockupTopConstraint?.activate()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,95 @@
//
// AccessibilityUpdatable.swift
// VDS
//
// Created by Matt Bruce on 6/20/24.
//
import Foundation
import UIKit
public protocol AccessibilityUpdatable {
var bridge_isAccessibilityElementBlock: AXBoolReturnBlock? { get set }
var bridge_accessibilityLabelBlock: AXStringReturnBlock? { get set }
var bridge_accessibilityValueBlock: AXStringReturnBlock? { get set }
var bridge_accessibilityHintBlock: AXStringReturnBlock? { get set }
var bridge_accessibilityActivateBlock: AXBoolReturnBlock? { get set }
}
private struct AccessibilityBridge {
static var isAccessibilityElementBlockKey: UInt8 = 0
static var activateBlockKey: UInt8 = 1
static var valueBlockKey: UInt8 = 2
static var hintBlockKey: UInt8 = 3
static var labelBlockKey: UInt8 = 4
}
extension AccessibilityUpdatable where Self: NSObject {
public var bridge_isAccessibilityElementBlock: AXBoolReturnBlock? {
get {
return objc_getAssociatedObject(self, &AccessibilityBridge.isAccessibilityElementBlockKey) as? AXBoolReturnBlock
}
set {
objc_setAssociatedObject(self, &AccessibilityBridge.isAccessibilityElementBlockKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
// if #available(iOS 17, *) {
// self.isAccessibilityElementBlock = newValue
// }
}
}
public var bridge_accessibilityActivateBlock: AXBoolReturnBlock? {
get {
return objc_getAssociatedObject(self, &AccessibilityBridge.activateBlockKey) as? AXBoolReturnBlock
}
set {
objc_setAssociatedObject(self, &AccessibilityBridge.activateBlockKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
// if #available(iOS 17, *) {
// self.accessibilityActivateBlock = newValue
// }
}
}
public var bridge_accessibilityValueBlock: AXStringReturnBlock? {
get {
return objc_getAssociatedObject(self, &AccessibilityBridge.valueBlockKey) as? AXStringReturnBlock
}
set {
objc_setAssociatedObject(self, &AccessibilityBridge.valueBlockKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
// if #available(iOS 17, *) {
// self.accessibilityValueBlock = newValue
// }
}
}
public var bridge_accessibilityHintBlock: AXStringReturnBlock? {
get {
return objc_getAssociatedObject(self, &AccessibilityBridge.hintBlockKey) as? AXStringReturnBlock
}
set {
objc_setAssociatedObject(self, &AccessibilityBridge.hintBlockKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
// if #available(iOS 17, *) {
// self.accessibilityHintBlock = newValue
// }
}
}
public var bridge_accessibilityLabelBlock: AXStringReturnBlock? {
get {
return objc_getAssociatedObject(self, &AccessibilityBridge.labelBlockKey) as? AXStringReturnBlock
}
set {
objc_setAssociatedObject(self, &AccessibilityBridge.labelBlockKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
// if #available(iOS 17, *) {
// self.accessibilityLabelBlock = newValue
// }
}
}
}

View File

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

View File

@ -631,6 +631,195 @@ extension LayoutConstraintable {
return centerYAnchor.constraint(greaterThanOrEqualTo: found, constant: -constant).with { $0.priority = priority; $0.isActive = true } 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 // MARK: - Implementations
//-------------------------------------------------- //--------------------------------------------------

View File

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

View File

@ -1,7 +1,7 @@
{ {
"images" : [ "images" : [
{ {
"filename" : "discover.svg", "filename" : "Discover-02.svg",
"idiom" : "universal" "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" : [ "images" : [
{ {
"filename" : "jcb.svg", "filename" : "jcb-emblem-logo.svg",
"idiom" : "universal" "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 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 1.0.66
---------------- ----------------