Merge branch 'develop' of https://gitlab.verizon.com/BPHV_MIPS/vds_ios into vasavk/inputStepper
# Conflicts: # VDS/Components/TextFields/EntryFieldBase.swift
This commit is contained in:
commit
c120c746d2
@ -20,7 +20,10 @@
|
||||
18A3F12A2BD9298900498E4A /* Calendar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A3F1292BD9298900498E4A /* Calendar.swift */; };
|
||||
18A65A022B96E848006602CC /* Breadcrumbs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A65A012B96E848006602CC /* Breadcrumbs.swift */; };
|
||||
18A65A042B96F050006602CC /* BreadcrumbItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A65A032B96F050006602CC /* BreadcrumbItem.swift */; };
|
||||
18AE87502C06FDA60075F181 /* Carousel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18AE874F2C06FDA60075F181 /* Carousel.swift */; };
|
||||
18B42AC62C09D197008D6262 /* CarouselSlotAlignmentModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B42AC52C09D197008D6262 /* CarouselSlotAlignmentModel.swift */; };
|
||||
18B463A42BBD3C46005C4528 /* DropdownOptionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B463A32BBD3C46005C4528 /* DropdownOptionModel.swift */; };
|
||||
18B9763F2C11BA4A009271DF /* CarouselPaginationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B9763E2C11BA4A009271DF /* CarouselPaginationModel.swift */; };
|
||||
18FEA1AD2BDD137500A56439 /* CalendarIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1AC2BDD137500A56439 /* CalendarIndicatorModel.swift */; };
|
||||
18FEA1B52BE0E63600A56439 /* Date+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1B42BE0E63600A56439 /* Date+Extension.swift */; };
|
||||
445BA07829C07B3D0036A7C5 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 445BA07729C07B3D0036A7C5 /* Notification.swift */; };
|
||||
@ -154,7 +157,7 @@
|
||||
EAC58C182BED0E2300BA39FA /* SecurityCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C172BED0E2300BA39FA /* SecurityCode.swift */; };
|
||||
EAC58C232BF2824200BA39FA /* DatePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C222BF2824200BA39FA /* DatePicker.swift */; };
|
||||
EAC58C272BF4116200BA39FA /* DatePickerCalendarModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C262BF4116200BA39FA /* DatePickerCalendarModel.swift */; };
|
||||
EAC58C292BF4118C00BA39FA /* DatePickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C282BF4118C00BA39FA /* DatePickerViewController.swift */; };
|
||||
EAC58C292BF4118C00BA39FA /* ClearPopoverViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C282BF4118C00BA39FA /* ClearPopoverViewController.swift */; };
|
||||
EAC71A1D2A2E155A00E47A9F /* Checkbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC71A1C2A2E155A00E47A9F /* Checkbox.swift */; };
|
||||
EAC71A1F2A2E173D00E47A9F /* RadioButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC71A1E2A2E173D00E47A9F /* RadioButton.swift */; };
|
||||
EAC846F3294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC846F2294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift */; };
|
||||
@ -176,6 +179,7 @@
|
||||
EAF1FE9B29DB1A6000101452 /* Changeable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF1FE9A29DB1A6000101452 /* Changeable.swift */; };
|
||||
EAF2F4762C231EAA007BFEDC /* AccessibilityActionElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */; };
|
||||
EAF2F4782C249D72007BFEDC /* AccessibilityUpdatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF2F4772C249D72007BFEDC /* AccessibilityUpdatable.swift */; };
|
||||
EAF2F4892C2A1075007BFEDC /* AlertViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF2F4882C2A1075007BFEDC /* AlertViewController.swift */; };
|
||||
EAF7F0952899861000B287F5 /* CheckboxItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0932899861000B287F5 /* CheckboxItem.swift */; };
|
||||
EAF7F09A2899B17200B287F5 /* CATransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0992899B17200B287F5 /* CATransaction.swift */; };
|
||||
EAF7F0A0289AB7EC00B287F5 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F09F289AB7EC00B287F5 /* View.swift */; };
|
||||
@ -222,7 +226,11 @@
|
||||
18A3F1292BD9298900498E4A /* Calendar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Calendar.swift; sourceTree = "<group>"; };
|
||||
18A65A012B96E848006602CC /* Breadcrumbs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Breadcrumbs.swift; sourceTree = "<group>"; };
|
||||
18A65A032B96F050006602CC /* BreadcrumbItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreadcrumbItem.swift; sourceTree = "<group>"; };
|
||||
18AE874F2C06FDA60075F181 /* Carousel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Carousel.swift; sourceTree = "<group>"; };
|
||||
18AE87532C06FE610075F181 /* CarouselChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = CarouselChangeLog.txt; sourceTree = "<group>"; };
|
||||
18B42AC52C09D197008D6262 /* CarouselSlotAlignmentModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselSlotAlignmentModel.swift; sourceTree = "<group>"; };
|
||||
18B463A32BBD3C46005C4528 /* DropdownOptionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropdownOptionModel.swift; sourceTree = "<group>"; };
|
||||
18B9763E2C11BA4A009271DF /* CarouselPaginationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselPaginationModel.swift; sourceTree = "<group>"; };
|
||||
18BDEE812B75316E00452358 /* ButtonIconChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ButtonIconChangeLog.txt; sourceTree = "<group>"; };
|
||||
18FEA1AC2BDD137500A56439 /* CalendarIndicatorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarIndicatorModel.swift; sourceTree = "<group>"; };
|
||||
18FEA1B42BE0E63600A56439 /* Date+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extension.swift"; sourceTree = "<group>"; };
|
||||
@ -372,7 +380,7 @@
|
||||
EAC58C222BF2824200BA39FA /* DatePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePicker.swift; sourceTree = "<group>"; };
|
||||
EAC58C242BF2A7FB00BA39FA /* DatePickerChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = DatePickerChangeLog.txt; sourceTree = "<group>"; };
|
||||
EAC58C262BF4116200BA39FA /* DatePickerCalendarModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickerCalendarModel.swift; sourceTree = "<group>"; };
|
||||
EAC58C282BF4118C00BA39FA /* DatePickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickerViewController.swift; sourceTree = "<group>"; };
|
||||
EAC58C282BF4118C00BA39FA /* ClearPopoverViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClearPopoverViewController.swift; sourceTree = "<group>"; };
|
||||
EAC71A1C2A2E155A00E47A9F /* Checkbox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Checkbox.swift; sourceTree = "<group>"; };
|
||||
EAC71A1E2A2E173D00E47A9F /* RadioButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButton.swift; sourceTree = "<group>"; };
|
||||
EAC846F2294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonGroupCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
@ -406,6 +414,7 @@
|
||||
EAF1FE9A29DB1A6000101452 /* Changeable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Changeable.swift; sourceTree = "<group>"; };
|
||||
EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityActionElement.swift; sourceTree = "<group>"; };
|
||||
EAF2F4772C249D72007BFEDC /* AccessibilityUpdatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityUpdatable.swift; sourceTree = "<group>"; };
|
||||
EAF2F4882C2A1075007BFEDC /* AlertViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertViewController.swift; sourceTree = "<group>"; };
|
||||
EAF7F0932899861000B287F5 /* CheckboxItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckboxItem.swift; sourceTree = "<group>"; };
|
||||
EAF7F0992899B17200B287F5 /* CATransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CATransaction.swift; sourceTree = "<group>"; };
|
||||
EAF7F09F289AB7EC00B287F5 /* View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = "<group>"; };
|
||||
@ -498,6 +507,17 @@
|
||||
path = Breadcrumbs;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
18AE874E2C06FD610075F181 /* Carousel */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
18AE874F2C06FDA60075F181 /* Carousel.swift */,
|
||||
18B9763E2C11BA4A009271DF /* CarouselPaginationModel.swift */,
|
||||
18B42AC52C09D197008D6262 /* CarouselSlotAlignmentModel.swift */,
|
||||
18AE87532C06FE610075F181 /* CarouselChangeLog.txt */,
|
||||
);
|
||||
path = Carousel;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
440B84C82BD8E0CE004A732A /* Table */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -668,6 +688,7 @@
|
||||
18A65A002B96E7E1006602CC /* Breadcrumbs */,
|
||||
EA0FC2BE2912D18200DF80B4 /* Buttons */,
|
||||
18A3F1202BD8F5DE00498E4A /* Calendar */,
|
||||
18AE874E2C06FD610075F181 /* Carousel */,
|
||||
1808BEBA2BA41B1D00129230 /* CarouselScrollbar */,
|
||||
EAF7F092289985E200B287F5 /* Checkbox */,
|
||||
EAC58C1F2BF127F000BA39FA /* DatePicker */,
|
||||
@ -760,9 +781,11 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EA985C1C296CD13600F2FF2E /* BundleManager.swift */,
|
||||
EAC58C282BF4118C00BA39FA /* ClearPopoverViewController.swift */,
|
||||
EAF7F0B8289C139800B287F5 /* ColorConfiguration.swift */,
|
||||
EAB5FEF02927F4AA00998C17 /* SelfSizingCollectionView.swift */,
|
||||
EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */,
|
||||
EAF2F4882C2A1075007BFEDC /* AlertViewController.swift */,
|
||||
);
|
||||
path = Classes;
|
||||
sourceTree = "<group>";
|
||||
@ -980,7 +1003,6 @@
|
||||
children = (
|
||||
EAC58C222BF2824200BA39FA /* DatePicker.swift */,
|
||||
EAC58C262BF4116200BA39FA /* DatePickerCalendarModel.swift */,
|
||||
EAC58C282BF4118C00BA39FA /* DatePickerViewController.swift */,
|
||||
EAC58C242BF2A7FB00BA39FA /* DatePickerChangeLog.txt */,
|
||||
);
|
||||
path = DatePicker;
|
||||
@ -1303,6 +1325,7 @@
|
||||
EA5F86D02A1F936100BC83E4 /* TabsContainer.swift in Sources */,
|
||||
EAF7F0B1289B177F00B287F5 /* ColorLabelAttribute.swift in Sources */,
|
||||
EAC9258F2911C9DE00091998 /* EntryFieldBase.swift in Sources */,
|
||||
18B9763F2C11BA4A009271DF /* CarouselPaginationModel.swift in Sources */,
|
||||
EAB1D2EA28AE84AA00DAE764 /* UIControlPublisher.swift in Sources */,
|
||||
EAD068922A560B65002E3A2D /* LoaderViewController.swift in Sources */,
|
||||
44BD43B62C04866600644F87 /* TableRowModel.swift in Sources */,
|
||||
@ -1314,18 +1337,20 @@
|
||||
EA8E40932A82889500934ED3 /* TooltipDialog.swift in Sources */,
|
||||
44604AD429CE186A00E62B51 /* NotificationButtonModel.swift in Sources */,
|
||||
EAD8D2C128BFDE8B006EB6A6 /* UIGestureRecognizer+Publisher.swift in Sources */,
|
||||
18B42AC62C09D197008D6262 /* CarouselSlotAlignmentModel.swift in Sources */,
|
||||
71B23C2D2B91FA690027F7D9 /* Pagination.swift in Sources */,
|
||||
EA0D1C372A681CCE00E5C127 /* ToggleView.swift in Sources */,
|
||||
EAF7F0B9289C139800B287F5 /* ColorConfiguration.swift in Sources */,
|
||||
EA3361BD288B2C760071C351 /* TypeAlias.swift in Sources */,
|
||||
EAC58C0A2BED004E00BA39FA /* FieldType.swift in Sources */,
|
||||
EA471F3A2A95587500CE9E58 /* LayoutConstraintable.swift in Sources */,
|
||||
EAC58C292BF4118C00BA39FA /* DatePickerViewController.swift in Sources */,
|
||||
EAC58C292BF4118C00BA39FA /* ClearPopoverViewController.swift in Sources */,
|
||||
EAF193432C134F3800C68D18 /* TableCellItem.swift in Sources */,
|
||||
EAB1D2CF28ABEF2B00DAE764 /* Typography+Base.swift in Sources */,
|
||||
EA0D1C3B2A6AD51B00E5C127 /* Typogprahy+Styles.swift in Sources */,
|
||||
EAF7F09A2899B17200B287F5 /* CATransaction.swift in Sources */,
|
||||
EAC58C162BED0E0300BA39FA /* InlineAction.swift in Sources */,
|
||||
EAF2F4892C2A1075007BFEDC /* AlertViewController.swift in Sources */,
|
||||
EA0D1C3D2A6AD57600E5C127 /* Typography+Enums.swift in Sources */,
|
||||
EAF1FE9B29DB1A6000101452 /* Changeable.swift in Sources */,
|
||||
EAC58C0C2BED01D500BA39FA /* Telephone.swift in Sources */,
|
||||
@ -1351,6 +1376,7 @@
|
||||
EA0B18052A9E2D2D00F2D0CD /* SelectorBase.swift in Sources */,
|
||||
EAC71A1D2A2E155A00E47A9F /* Checkbox.swift in Sources */,
|
||||
EAF7F0AB289B13FD00B287F5 /* TextStyleLabelAttribute.swift in Sources */,
|
||||
18AE87502C06FDA60075F181 /* Carousel.swift in Sources */,
|
||||
EAB1D29C28A5618900DAE764 /* RadioButtonGroup.swift in Sources */,
|
||||
EA81410B2A0E8E3C004F60D2 /* ButtonIcon.swift in Sources */,
|
||||
EA985BE629688F6A00F2FF2E /* TileletBadgeModel.swift in Sources */,
|
||||
@ -1547,7 +1573,7 @@
|
||||
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 67;
|
||||
CURRENT_PROJECT_VERSION = 71;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@ -1585,7 +1611,7 @@
|
||||
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 67;
|
||||
CURRENT_PROJECT_VERSION = 71;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
|
||||
@ -10,6 +10,7 @@ import UIKit
|
||||
import Combine
|
||||
|
||||
/// Base Class use to build Controls.
|
||||
@objcMembers
|
||||
@objc(VDSControl)
|
||||
open class Control: UIControl, ViewProtocol, UserInfoable, Clickable {
|
||||
//--------------------------------------------------
|
||||
@ -129,7 +130,6 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable {
|
||||
//--------------------------------------------------
|
||||
open var accessibilityAction: ((Control) -> Void)?
|
||||
|
||||
private var _isAccessibilityElement: Bool = false
|
||||
open override var isAccessibilityElement: Bool {
|
||||
get {
|
||||
var block: AXBoolReturnBlock?
|
||||
@ -137,7 +137,7 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable {
|
||||
// if #available(iOS 17, *) {
|
||||
// block = isAccessibilityElementBlock
|
||||
// }
|
||||
|
||||
|
||||
if block == nil {
|
||||
block = bridge_isAccessibilityElementBlock
|
||||
}
|
||||
@ -145,15 +145,14 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable {
|
||||
if let block {
|
||||
return block()
|
||||
} else {
|
||||
return _isAccessibilityElement
|
||||
return super.isAccessibilityElement
|
||||
}
|
||||
}
|
||||
set {
|
||||
_isAccessibilityElement = newValue
|
||||
super.isAccessibilityElement = newValue
|
||||
}
|
||||
}
|
||||
|
||||
private var _accessibilityLabel: String?
|
||||
open override var accessibilityLabel: String? {
|
||||
get {
|
||||
var block: AXStringReturnBlock?
|
||||
@ -168,15 +167,14 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable {
|
||||
if let block {
|
||||
return block()
|
||||
} else {
|
||||
return _accessibilityLabel
|
||||
return super.accessibilityLabel
|
||||
}
|
||||
}
|
||||
set {
|
||||
_accessibilityLabel = newValue
|
||||
super.accessibilityLabel = newValue
|
||||
}
|
||||
}
|
||||
|
||||
private var _accessibilityHint: String?
|
||||
open override var accessibilityHint: String? {
|
||||
get {
|
||||
var block: AXStringReturnBlock?
|
||||
@ -191,15 +189,14 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable {
|
||||
if let block {
|
||||
return block()
|
||||
} else {
|
||||
return _accessibilityHint
|
||||
return super.accessibilityHint
|
||||
}
|
||||
}
|
||||
set {
|
||||
_accessibilityHint = newValue
|
||||
super.accessibilityHint = newValue
|
||||
}
|
||||
}
|
||||
|
||||
private var _accessibilityValue: String?
|
||||
open override var accessibilityValue: String? {
|
||||
get {
|
||||
var block: AXStringReturnBlock?
|
||||
@ -215,11 +212,11 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable {
|
||||
if let block{
|
||||
return block()
|
||||
} else {
|
||||
return _accessibilityValue
|
||||
return super.accessibilityValue
|
||||
}
|
||||
}
|
||||
set {
|
||||
_accessibilityValue = newValue
|
||||
super.accessibilityValue = newValue
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -28,6 +28,8 @@ public protocol SelectorControlable: Control, Changeable {
|
||||
}
|
||||
|
||||
/// Base Class used to build out a Selector control.
|
||||
@objcMembers
|
||||
@objc(VDSSelectorBase)
|
||||
open class SelectorBase: Control, SelectorControlable {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
|
||||
@ -10,6 +10,7 @@ import UIKit
|
||||
import Combine
|
||||
|
||||
/// Base Class used to build Views.
|
||||
@objcMembers
|
||||
@objc(VDSView)
|
||||
open class View: UIView, ViewProtocol, UserInfoable {
|
||||
|
||||
@ -96,7 +97,6 @@ open class View: UIView, ViewProtocol, UserInfoable {
|
||||
//--------------------------------------------------
|
||||
open var accessibilityAction: ((View) -> Void)?
|
||||
|
||||
private var _isAccessibilityElement: Bool = false
|
||||
open override var isAccessibilityElement: Bool {
|
||||
get {
|
||||
var block: AXBoolReturnBlock?
|
||||
@ -112,22 +112,21 @@ open class View: UIView, ViewProtocol, UserInfoable {
|
||||
if let block {
|
||||
return block()
|
||||
} else {
|
||||
return _isAccessibilityElement
|
||||
return super.isAccessibilityElement
|
||||
}
|
||||
}
|
||||
set {
|
||||
_isAccessibilityElement = newValue
|
||||
super.isAccessibilityElement = newValue
|
||||
}
|
||||
}
|
||||
|
||||
private var _accessibilityLabel: String?
|
||||
open override var accessibilityLabel: String? {
|
||||
get {
|
||||
var block: AXStringReturnBlock?
|
||||
// if #available(iOS 17, *) {
|
||||
// block = accessibilityLabelBlock
|
||||
// }
|
||||
//
|
||||
|
||||
if block == nil {
|
||||
block = bridge_accessibilityLabelBlock
|
||||
}
|
||||
@ -135,15 +134,14 @@ open class View: UIView, ViewProtocol, UserInfoable {
|
||||
if let block {
|
||||
return block()
|
||||
} else {
|
||||
return _accessibilityLabel
|
||||
return super.accessibilityLabel
|
||||
}
|
||||
}
|
||||
set {
|
||||
_accessibilityLabel = newValue
|
||||
super.accessibilityLabel = newValue
|
||||
}
|
||||
}
|
||||
|
||||
private var _accessibilityHint: String?
|
||||
open override var accessibilityHint: String? {
|
||||
get {
|
||||
var block: AXStringReturnBlock?
|
||||
@ -158,15 +156,14 @@ open class View: UIView, ViewProtocol, UserInfoable {
|
||||
if let block {
|
||||
return block()
|
||||
} else {
|
||||
return _accessibilityHint
|
||||
return super.accessibilityHint
|
||||
}
|
||||
}
|
||||
set {
|
||||
_accessibilityHint = newValue
|
||||
super.accessibilityHint = newValue
|
||||
}
|
||||
}
|
||||
|
||||
private var _accessibilityValue: String?
|
||||
open override var accessibilityValue: String? {
|
||||
get {
|
||||
var block: AXStringReturnBlock?
|
||||
@ -182,11 +179,11 @@ open class View: UIView, ViewProtocol, UserInfoable {
|
||||
if let block{
|
||||
return block()
|
||||
} else {
|
||||
return _accessibilityValue
|
||||
return super.accessibilityValue
|
||||
}
|
||||
}
|
||||
set {
|
||||
_accessibilityValue = newValue
|
||||
super.accessibilityValue = newValue
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
98
VDS/Classes/AlertViewController.swift
Normal file
98
VDS/Classes/AlertViewController.swift
Normal file
@ -0,0 +1,98 @@
|
||||
//
|
||||
// AlertViewController.swift
|
||||
// VDS
|
||||
//
|
||||
// Created by Matt Bruce on 6/24/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Combine
|
||||
import VDSCoreTokens
|
||||
|
||||
@objcMembers
|
||||
@objc(VDSAlertViewController)
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
154
VDS/Classes/ClearPopoverViewController.swift
Normal file
154
VDS/Classes/ClearPopoverViewController.swift
Normal file
@ -0,0 +1,154 @@
|
||||
//
|
||||
// DatePickerPopoverViewController.swift
|
||||
// VDS
|
||||
//
|
||||
// Created by Matt Bruce on 5/14/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
@objcMembers
|
||||
@objc(VDSClearPopoverViewController)
|
||||
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 popover’s 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) {
|
||||
|
||||
}
|
||||
}
|
||||
@ -15,6 +15,7 @@ import Combine
|
||||
/// If you are using AutoLayoutConstraints you have a combination of Leading/Left and Trailing/Right NSLayoutConstraints,
|
||||
/// you need to ensure that one of these Horizontal Contraints is not constraint of "equatTo". If you are to pin the left/right edges
|
||||
/// to its parent this object will stretch to the parent's width.
|
||||
@objcMembers
|
||||
@objc(VDSBadge)
|
||||
open class Badge: View {
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ import VDSCoreTokens
|
||||
import Combine
|
||||
|
||||
/// A badge indicator is a visual label used to convey status or highlight supplemental information.
|
||||
@objcMembers
|
||||
@objc(VDSBadgeIndicator)
|
||||
open class BadgeIndicator: View {
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@ import Combine
|
||||
/// A Breadcrumb Item contains href(link) and selected flag.
|
||||
/// Breadcrumb links to its respective page if it is not disabled.
|
||||
/// Breadcrumb contains text with a separator by default, highlights text in bold without a separator if selected.
|
||||
@objcMembers
|
||||
@objc (VDSBreadcrumbItem)
|
||||
open class BreadcrumbItem: ButtonBase {
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@ import Combine
|
||||
/// A Breadcrumbs contains BreadcrumbItems.
|
||||
/// It contains Breadcrumb Item Default, Breadcrumb Item Selected, Separator.
|
||||
/// Breadcrumbs are secondary navigation that use a hierarchy of internal links to tell customers where they are in an experience. Each breadcrumb links to its respective page, except for that of current page.
|
||||
@objcMembers
|
||||
@objc(VDSBreadcrumbs)
|
||||
open class Breadcrumbs: View {
|
||||
|
||||
|
||||
@ -15,6 +15,7 @@ import Combine
|
||||
/// If you are using AutoLayoutConstraints you have a combination of Leading/Left and Trailing/Right NSLayoutConstraints,
|
||||
/// you need to ensure that one of these Horizontal Contraints is not constraint of "equatTo". If you are to pin the left/right edges
|
||||
/// to its parent this object will stretch to the parent's width.
|
||||
@objcMembers
|
||||
@objc(VDSButton)
|
||||
open class Button: ButtonBase, Useable {
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ import VDSCoreTokens
|
||||
import Combine
|
||||
|
||||
/// Base class used for UIButton type classes.
|
||||
@objcMembers
|
||||
@objc(VDSButtonBase)
|
||||
open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable {
|
||||
|
||||
@ -177,7 +178,6 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable {
|
||||
//--------------------------------------------------
|
||||
open var accessibilityAction: ((ButtonBase) -> Void)?
|
||||
|
||||
private var _isAccessibilityElement: Bool = false
|
||||
open override var isAccessibilityElement: Bool {
|
||||
get {
|
||||
var block: AXBoolReturnBlock?
|
||||
@ -193,15 +193,14 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable {
|
||||
if let block {
|
||||
return block()
|
||||
} else {
|
||||
return _isAccessibilityElement
|
||||
return super.isAccessibilityElement
|
||||
}
|
||||
}
|
||||
set {
|
||||
_isAccessibilityElement = newValue
|
||||
super.isAccessibilityElement = newValue
|
||||
}
|
||||
}
|
||||
|
||||
private var _accessibilityLabel: String?
|
||||
open override var accessibilityLabel: String? {
|
||||
get {
|
||||
var block: AXStringReturnBlock?
|
||||
@ -216,15 +215,14 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable {
|
||||
if let block {
|
||||
return block()
|
||||
} else {
|
||||
return _accessibilityLabel
|
||||
return super.accessibilityLabel
|
||||
}
|
||||
}
|
||||
set {
|
||||
_accessibilityLabel = newValue
|
||||
super.accessibilityLabel = newValue
|
||||
}
|
||||
}
|
||||
|
||||
private var _accessibilityHint: String?
|
||||
open override var accessibilityHint: String? {
|
||||
get {
|
||||
var block: AXStringReturnBlock?
|
||||
@ -239,15 +237,14 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable {
|
||||
if let block {
|
||||
return block()
|
||||
} else {
|
||||
return _accessibilityHint
|
||||
return super.accessibilityHint
|
||||
}
|
||||
}
|
||||
set {
|
||||
_accessibilityHint = newValue
|
||||
super.accessibilityHint = newValue
|
||||
}
|
||||
}
|
||||
|
||||
private var _accessibilityValue: String?
|
||||
open override var accessibilityValue: String? {
|
||||
get {
|
||||
var block: AXStringReturnBlock?
|
||||
@ -263,11 +260,11 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable {
|
||||
if let block{
|
||||
return block()
|
||||
} else {
|
||||
return _accessibilityValue
|
||||
return super.accessibilityValue
|
||||
}
|
||||
}
|
||||
set {
|
||||
_accessibilityValue = newValue
|
||||
super.accessibilityValue = newValue
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ import VDSCoreTokens
|
||||
import Combine
|
||||
|
||||
/// A button group contains combinations of related CTAs including ``Button``, ``TextLink``, and ``TextLinkCaret``. This group component controls a combination's orientation, spacing, size and allowable size pairings.
|
||||
@objcMembers
|
||||
@objc(VDSButtonGroup)
|
||||
open class ButtonGroup: View {
|
||||
|
||||
|
||||
@ -16,6 +16,7 @@ import Combine
|
||||
/// If you are using AutoLayoutConstraints you have a combination of Leading/Left and Trailing/Right NSLayoutConstraints,
|
||||
/// you need to ensure that one of these Horizontal Contraints is not constraint of "equatTo". If you are to pin the left/right edges
|
||||
/// to its parent this object will stretch to the parent's width.
|
||||
@objcMembers
|
||||
@objc(VDSTextLink)
|
||||
open class TextLink: ButtonBase {
|
||||
//--------------------------------------------------
|
||||
|
||||
@ -16,6 +16,7 @@ import Combine
|
||||
/// If you are using AutoLayoutConstraints you have a combination of Leading/Left and Trailing/Right NSLayoutConstraints,
|
||||
/// you need to ensure that one of these Horizontal Contraints is not constraint of "equatTo". If you are to pin the left/right edges
|
||||
/// to its parent this object will stretch to the parent's width.
|
||||
@objcMembers
|
||||
@objc(VDSTextLinkCaret)
|
||||
open class TextLinkCaret: ButtonBase {
|
||||
//--------------------------------------------------
|
||||
|
||||
@ -11,6 +11,7 @@ import VDSCoreTokens
|
||||
import Combine
|
||||
|
||||
/// A calendar is a monthly view that lets customers select a single date.
|
||||
@objcMembers
|
||||
@objc(VDSCalendar)
|
||||
open class CalendarBase: Control, Changeable {
|
||||
|
||||
|
||||
566
VDS/Components/Carousel/Carousel.swift
Normal file
566
VDS/Components/Carousel/Carousel.swift
Normal file
@ -0,0 +1,566 @@
|
||||
//
|
||||
// Carousel.swift
|
||||
// VDS
|
||||
//
|
||||
// Created by Kanamarlapudi, Vasavi on 29/05/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import VDSCoreTokens
|
||||
import Combine
|
||||
|
||||
/// A carousel is a collection of related content in a row that a customer can navigate through horizontally.
|
||||
/// Use this component to show content that is supplementary, not essential for task completion.
|
||||
@objcMembers
|
||||
@objc(VDSCarousel)
|
||||
open class Carousel: View {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
required public init() {
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
|
||||
public required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Enums
|
||||
//--------------------------------------------------
|
||||
/// Enum used to describe the pagination display for this component.
|
||||
public enum PaginationDisplay: String, CaseIterable {
|
||||
case persistent, none
|
||||
}
|
||||
|
||||
/// Enum used to describe the peek for this component.
|
||||
/// This is how much a tile is partially visible. It is measured by the distance between the edge of
|
||||
/// the tile and the edge of the viewport or carousel container. A peek can appear on the left and/or
|
||||
/// right edge of the carousel container or viewport, depending on the carousel’s scroll position.
|
||||
public enum Peek: String, CaseIterable {
|
||||
case standard, minimum, none
|
||||
}
|
||||
|
||||
/// Enum used to describe the vertical of slotAlignment.
|
||||
public enum Vertical: String, CaseIterable {
|
||||
case top, middle, bottom
|
||||
}
|
||||
|
||||
/// Enum used to describe the horizontal of slotAlignment.
|
||||
public enum Horizontal: String, CaseIterable {
|
||||
case left, center, right
|
||||
}
|
||||
|
||||
/// Space between each tile. The default value will be 6X in tablet and 3X in mobile.
|
||||
public enum Gutter: String, CaseIterable , DefaultValuing {
|
||||
case gutter3X = "3X"
|
||||
case gutter6X = "6X"
|
||||
|
||||
public static var defaultValue: Self { UIDevice.isIPad ? .gutter6X : .gutter3X }
|
||||
|
||||
public var value: CGFloat {
|
||||
switch self {
|
||||
case .gutter3X:
|
||||
VDSLayout.space3X
|
||||
case .gutter6X:
|
||||
VDSLayout.space6X
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
/// views used to render view in the carousel slots.
|
||||
open var views: [UIView] = [] { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// Space between each tile. The default value will be 6X in tablet and 3X in mobile.
|
||||
open var gutter: Gutter = Gutter.defaultValue { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// The amount of slides visible in the carousel container at one time.
|
||||
/// The default value will be 3UP in tablet and 1UP in mobile.
|
||||
open var layout: CarouselScrollbar.Layout = UIDevice.isIPad ? .threeUP : .oneUP {
|
||||
didSet {
|
||||
carouselScrollBar.position = 0
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
/// A callback when moving the carousel. Returns selectedGroupIndex.
|
||||
open var onChange: ((Int) -> Void)? {
|
||||
get { nil }
|
||||
set {
|
||||
onChangeCancellable?.cancel()
|
||||
if let newValue {
|
||||
onChangeCancellable = onChangePublisher
|
||||
.sink { c in
|
||||
newValue(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Config object for pagination.
|
||||
open var pagination: CarouselPaginationModel = .init(kind: .lowContrast, floating: true) { didSet {setNeedsUpdate() } }
|
||||
|
||||
/// If provided, will determine the conditions to render the pagination arrows.
|
||||
open var paginationDisplay: PaginationDisplay = .none { didSet {setNeedsUpdate() } }
|
||||
|
||||
/// If provided, will apply margin to pagination arrows. Can be set to either positive or negative values.
|
||||
/// The default value will be 3X in tablet and 2X in mobile. These values are the default in order to avoid overlapping content within the carousel.
|
||||
open var paginationInset: CGFloat = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space2X { didSet { updatePaginationInset() } }
|
||||
|
||||
/// Options for user to configure the partially-visible tile in group.
|
||||
/// Setting peek to 'none' will display arrow navigation icons on mobile devices.
|
||||
open var peek: Peek = .standard { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// The initial visible slide's index in the carousel.
|
||||
open var groupIndex: Int = 0 { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// If provided, will set the alignment for slot content when the slots has different heights.
|
||||
open var slotAlignment: CarouselSlotAlignmentModel? = .init(vertical: .top, horizontal: .left) { didSet { setNeedsUpdate() } }
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Properties
|
||||
//--------------------------------------------------
|
||||
internal var containerSize: CGSize { CGSize(width: frame.size.width, height: 44) }
|
||||
private let contentStackView = UIStackView().with {
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
$0.axis = .vertical
|
||||
$0.distribution = .fill
|
||||
$0.spacing = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space1X
|
||||
$0.backgroundColor = .clear
|
||||
}
|
||||
|
||||
internal var carouselScrollBar = CarouselScrollbar().with {
|
||||
$0.layout = UIDevice.isIPad ? .threeUP : .oneUP
|
||||
$0.position = 0
|
||||
$0.backgroundColor = .clear
|
||||
}
|
||||
|
||||
internal var containerView = View().with {
|
||||
$0.clipsToBounds = true
|
||||
$0.backgroundColor = .clear
|
||||
}
|
||||
|
||||
internal var scrollContainerView = View().with {
|
||||
$0.clipsToBounds = true
|
||||
$0.backgroundColor = .clear
|
||||
}
|
||||
|
||||
private var scrollView = UIScrollView().with {
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
$0.backgroundColor = .clear
|
||||
}
|
||||
|
||||
/// Previous button to show previous slide.
|
||||
private var previousButton = ButtonIcon().with {
|
||||
$0.kind = .lowContrast
|
||||
$0.iconName = .leftCaret
|
||||
$0.iconOffset = .init(x: -2, y: 0)
|
||||
$0.customContainerSize = UIDevice.isIPad ? 40 : 28
|
||||
$0.icon.customSize = UIDevice.isIPad ? 16 : 12
|
||||
}
|
||||
|
||||
/// Next button to show next slide.
|
||||
private var nextButton = ButtonIcon().with {
|
||||
$0.kind = .lowContrast
|
||||
$0.iconName = .rightCaret
|
||||
$0.iconOffset = .init(x: 2, y: 0)
|
||||
$0.customContainerSize = UIDevice.isIPad ? 40 : 28
|
||||
$0.icon.customSize = UIDevice.isIPad ? 16 : 12
|
||||
}
|
||||
|
||||
/// A publisher for when moving the carousel. Passes parameters selectedGroupIndex (position).
|
||||
open var onChangePublisher = PassthroughSubject<Int, Never>()
|
||||
private var onChangeCancellable: AnyCancellable?
|
||||
|
||||
private var containerStackHeightConstraint: NSLayoutConstraint?
|
||||
private var containerViewHeightConstraint: NSLayoutConstraint?
|
||||
private var prevButtonLeadingConstraint: NSLayoutConstraint?
|
||||
private var nextButtonTrailingConstraint: NSLayoutConstraint?
|
||||
|
||||
// The scrollbar has top 5X space. So the expected top space is adjusted for tablet and mobile.
|
||||
let scrollbarTopSpace = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space1X
|
||||
|
||||
var slotDefaultHeight = 50.0
|
||||
var peekMinimum = 24.0
|
||||
var minimumSlotWidth = 0.0
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Lifecycle
|
||||
//--------------------------------------------------
|
||||
/// Executed on initialization for this View.
|
||||
open override func initialSetup() {
|
||||
super.initialSetup()
|
||||
}
|
||||
|
||||
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
isAccessibilityElement = false
|
||||
|
||||
// Add containerView
|
||||
addSubview(containerView)
|
||||
containerView
|
||||
.pinTop()
|
||||
.pinBottom()
|
||||
.pinLeading()
|
||||
.pinTrailing()
|
||||
.heightGreaterThanEqualTo(containerSize.height)
|
||||
containerView.centerYAnchor.constraint(equalTo: centerYAnchor).activate()
|
||||
|
||||
// Add content stackview
|
||||
containerView.addSubview(contentStackView)
|
||||
|
||||
// Add scrollview
|
||||
scrollContainerView.addSubview(scrollView)
|
||||
scrollView.pinToSuperView()
|
||||
|
||||
// Add pagination button icons
|
||||
scrollContainerView.addSubview(previousButton)
|
||||
previousButton
|
||||
.pinLeadingGreaterThanOrEqualTo()
|
||||
.pinCenterY()
|
||||
|
||||
scrollContainerView.addSubview(nextButton)
|
||||
nextButton
|
||||
.pinTrailingLessThanOrEqualTo()
|
||||
.pinCenterY()
|
||||
|
||||
// Add scroll container view & carousel scrollbar
|
||||
contentStackView.addArrangedSubview(scrollContainerView)
|
||||
contentStackView.addArrangedSubview(carouselScrollBar)
|
||||
contentStackView.setCustomSpacing(scrollbarTopSpace, after: scrollContainerView)
|
||||
contentStackView
|
||||
.pinTop()
|
||||
.pinBottom()
|
||||
.pinLeading()
|
||||
.pinTrailing()
|
||||
.heightGreaterThanEqualTo(containerSize.height)
|
||||
|
||||
addlisteners()
|
||||
updatePaginationInset()
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
open override func updateView() {
|
||||
super.updateView()
|
||||
|
||||
carouselScrollBar.numberOfSlides = views.count
|
||||
carouselScrollBar.layout = layout
|
||||
if (carouselScrollBar.position == 0 || carouselScrollBar.position > carouselScrollBar.numberOfSlides) {
|
||||
carouselScrollBar.position = 1
|
||||
}
|
||||
carouselScrollBar.isHidden = (totalPositions() <= 1) ? true : false
|
||||
|
||||
// Mobile/Tablet layouts without peek - must show pagination controls.
|
||||
// If peek is ‘none’, pagination controls should show. So set to persistent.
|
||||
if peek == .none {
|
||||
paginationDisplay = .persistent
|
||||
}
|
||||
|
||||
// Minimum (Mobile only) Supported only on Mobile viewports. If a user passes Minimum for tablet carousel, the peek reverts to Standard.
|
||||
if UIDevice.isIPad && peek == .minimum {
|
||||
peek = .standard
|
||||
}
|
||||
|
||||
// Standard(Default) Peek - Supported for all Tablet viewports and layouts. Supported only for 1up layouts on Mobile viewports.
|
||||
if peek == .standard && !UIDevice.isIPad && layout != CarouselScrollbar.Layout.oneUP {
|
||||
peek = .minimum
|
||||
}
|
||||
|
||||
updatePaginationControls()
|
||||
addCarouselSlots()
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
shouldUpdateView = false
|
||||
layout = UIDevice.isIPad ? .threeUP : .oneUP
|
||||
pagination = .init(kind: .lowContrast, floating: true)
|
||||
paginationDisplay = .none
|
||||
paginationInset = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space2X
|
||||
gutter = UIDevice.isIPad ? .gutter6X : .gutter3X
|
||||
peek = .standard
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Methods
|
||||
//--------------------------------------------------
|
||||
private func addlisteners() {
|
||||
nextButton.onClick = { _ in self.nextButtonClick() }
|
||||
previousButton.onClick = { _ in self.previousButtonClick() }
|
||||
|
||||
/// Will be called when the scrubber position changes.
|
||||
carouselScrollBar.onScrubberDrag = { [weak self] scrubberId in
|
||||
guard let self else { return }
|
||||
updateScrollPosition(position: scrubberId, callbackText:"onThumbPositionChange")
|
||||
}
|
||||
|
||||
/// Will be called when the scrollbar thumb move forward.
|
||||
carouselScrollBar.onMoveForward = { [weak self] scrubberId in
|
||||
guard let self else { return }
|
||||
updateScrollPosition(position: scrubberId, callbackText:"onMoveForward")
|
||||
}
|
||||
|
||||
/// Will be called when the scrollbar thumb move backward.
|
||||
carouselScrollBar.onMoveBackward = { [weak self] scrubberId in
|
||||
guard let self else { return }
|
||||
updateScrollPosition(position: scrubberId, callbackText:"onMoveBackward")
|
||||
}
|
||||
|
||||
/// Will be called when the scrollbar thumb touch start.
|
||||
carouselScrollBar.onThumbTouchStart = { [weak self] scrubberId in
|
||||
guard let self else { return }
|
||||
updateScrollPosition(position: scrubberId, callbackText:"onThumbTouchStart")
|
||||
}
|
||||
|
||||
/// Will be called when the scrollbar thumb touch end.
|
||||
carouselScrollBar.onThumbTouchEnd = { [weak self] scrubberId in
|
||||
guard let self else { return }
|
||||
updateScrollPosition(position: scrubberId, callbackText:"onThumbTouchEnd")
|
||||
}
|
||||
}
|
||||
|
||||
// Update pagination buttons with selected surface, kind, floating values
|
||||
private func updatePaginationControls() {
|
||||
containerView.surface = surface
|
||||
showPaginationControls()
|
||||
previousButton.kind = pagination.kind
|
||||
previousButton.floating = pagination.floating
|
||||
nextButton.kind = pagination.kind
|
||||
nextButton.floating = pagination.floating
|
||||
previousButton.surface = surface
|
||||
nextButton.surface = surface
|
||||
}
|
||||
|
||||
// Show/Hide pagination buttons of Carousel based on First or Middle or Last
|
||||
private func showPaginationControls() {
|
||||
if carouselScrollBar.numberOfSlides == layout.value {
|
||||
previousButton.isHidden = true
|
||||
nextButton.isHidden = true
|
||||
} else {
|
||||
previousButton.isHidden = (carouselScrollBar.position == 1) || (paginationDisplay == .none)
|
||||
nextButton.isHidden = (carouselScrollBar.position == totalPositions()) || (paginationDisplay == .none)
|
||||
}
|
||||
}
|
||||
|
||||
private func estimateHeightFor(component: UIView, with itemWidth: CGFloat) -> CGFloat {
|
||||
let maxSize = CGSize(width: itemWidth, height: CGFloat.greatestFiniteMagnitude)
|
||||
let estItemSize = component.systemLayoutSizeFitting(maxSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
|
||||
return estItemSize.height
|
||||
}
|
||||
|
||||
private func fetchCarouselHeight() -> CGFloat {
|
||||
var height = slotDefaultHeight
|
||||
if views.count > 0 {
|
||||
for index in 0...views.count - 1 {
|
||||
let estHeight = estimateHeightFor(component: views[index], with: minimumSlotWidth)
|
||||
height = estHeight > height ? estHeight : height
|
||||
}
|
||||
}
|
||||
return height
|
||||
}
|
||||
|
||||
// Add carousel slots and load data if any
|
||||
private func addCarouselSlots() {
|
||||
getSlotWidth()
|
||||
if containerView.frame.size.width > 0 {
|
||||
containerViewHeightConstraint?.isActive = false
|
||||
containerStackHeightConstraint?.isActive = false
|
||||
let slotHeight = fetchCarouselHeight()
|
||||
|
||||
// Perform a loop to iterate each subView
|
||||
scrollView.subviews.forEach { subView in
|
||||
// Removing subView from its parent view
|
||||
subView.removeFromSuperview()
|
||||
}
|
||||
|
||||
// Add carousel items
|
||||
if views.count > 0 {
|
||||
var xPos = 0.0
|
||||
for index in 0...views.count - 1 {
|
||||
|
||||
// Add Carousel Slot
|
||||
let carouselSlot = View().with {
|
||||
$0.clipsToBounds = true
|
||||
}
|
||||
scrollView.addSubview(carouselSlot)
|
||||
scrollView.delegate = self
|
||||
|
||||
carouselSlot
|
||||
.pinTop()
|
||||
.pinBottom()
|
||||
.pinLeading(xPos)
|
||||
.width(minimumSlotWidth)
|
||||
.height(slotHeight)
|
||||
xPos = xPos + minimumSlotWidth + gutter.value
|
||||
|
||||
let component = views[index]
|
||||
carouselSlot.addSubview(component)
|
||||
setSlotAlignment(contentView: component)
|
||||
}
|
||||
scrollView.contentSize = CGSize(width: xPos - gutter.value, height: slotHeight)
|
||||
}
|
||||
|
||||
let containerHeight = slotHeight + scrollbarTopSpace + containerSize.height
|
||||
if carouselScrollBar.isHidden {
|
||||
containerStackHeightConstraint = contentStackView.heightAnchor.constraint(equalToConstant: slotHeight)
|
||||
containerViewHeightConstraint = containerView.heightAnchor.constraint(equalToConstant: slotHeight)
|
||||
} else {
|
||||
containerStackHeightConstraint = contentStackView.heightAnchor.constraint(equalToConstant: containerHeight)
|
||||
containerViewHeightConstraint = containerView.heightAnchor.constraint(equalToConstant: containerHeight)
|
||||
}
|
||||
containerViewHeightConstraint?.isActive = true
|
||||
containerStackHeightConstraint?.isActive = true
|
||||
}
|
||||
}
|
||||
|
||||
// Set slot alignment if provided. Used only when slot content have different heights or widths.
|
||||
private func setSlotAlignment(contentView: UIView) {
|
||||
switch slotAlignment?.vertical {
|
||||
case .top:
|
||||
contentView
|
||||
.pinTop()
|
||||
.pinBottomLessThanOrEqualTo()
|
||||
case .middle:
|
||||
contentView
|
||||
.pinTopGreaterThanOrEqualTo()
|
||||
.pinBottomLessThanOrEqualTo()
|
||||
.pinCenterY()
|
||||
case .bottom:
|
||||
contentView
|
||||
.pinTopGreaterThanOrEqualTo()
|
||||
.pinBottom()
|
||||
default: break
|
||||
}
|
||||
|
||||
switch slotAlignment?.horizontal {
|
||||
case .left:
|
||||
contentView
|
||||
.pinLeading()
|
||||
.pinTrailingLessThanOrEqualTo()
|
||||
case .center:
|
||||
contentView
|
||||
.pinLeadingGreaterThanOrEqualTo()
|
||||
.pinTrailingLessThanOrEqualTo()
|
||||
.pinCenterX()
|
||||
case .right:
|
||||
contentView
|
||||
.pinLeadingGreaterThanOrEqualTo()
|
||||
.pinTrailing()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
// Get the slot width relative to the peak
|
||||
private func getSlotWidth() {
|
||||
let actualWidth = containerView.frame.size.width
|
||||
let isScrollbarSuppressed = views.count > 0 && layout.value == views.count
|
||||
let isPeekMinimumOnTablet = UIDevice.isIPad && peek == .minimum
|
||||
let isPeekNone: Bool = peek == .none
|
||||
minimumSlotWidth = isScrollbarSuppressed || isPeekMinimumOnTablet || isPeekNone ? actualWidth - ((CGFloat(layout.value)-1) * gutter.value): actualWidth - (CGFloat(layout.value) * gutter.value)
|
||||
if !isScrollbarSuppressed {
|
||||
switch peek {
|
||||
case .standard:
|
||||
// Standard(Default) Peek - Supported for all Tablet viewports and layouts. Supported only for 1up layouts on Mobile viewports.
|
||||
if UIDevice.isIPad {
|
||||
minimumSlotWidth = minimumSlotWidth - (minimumSlotWidth/(CGFloat(layout.value) + 3))
|
||||
} else if layout == .oneUP {
|
||||
minimumSlotWidth = minimumSlotWidth - (minimumSlotWidth/4)
|
||||
}
|
||||
case .minimum:
|
||||
// Peek Mimumum Width: 24px from edge of container (at the default view of the carousel with one peek visible)
|
||||
// Minimum (Mobile only) Supported only on Mobile viewports. If a user passes Minimum for tablet carousel, the peek reverts to Standard.
|
||||
minimumSlotWidth = isPeekMinimumOnTablet ? minimumSlotWidth : minimumSlotWidth - peekMinimum - gutter.value
|
||||
case .none:
|
||||
break
|
||||
}
|
||||
}
|
||||
minimumSlotWidth = ceil(minimumSlotWidth / CGFloat(layout.value))
|
||||
}
|
||||
|
||||
private func nextButtonClick() {
|
||||
carouselScrollBar.position = carouselScrollBar.position+1
|
||||
showPaginationControls()
|
||||
updateScrollPosition(position: carouselScrollBar.position, callbackText:"pageControlClicks")
|
||||
}
|
||||
|
||||
private func previousButtonClick() {
|
||||
carouselScrollBar.position = carouselScrollBar.position-1
|
||||
showPaginationControls()
|
||||
updateScrollPosition(position: carouselScrollBar.position, callbackText:"pageControlClicks")
|
||||
}
|
||||
|
||||
private func updatePaginationInset() {
|
||||
prevButtonLeadingConstraint?.isActive = false
|
||||
nextButtonTrailingConstraint?.isActive = false
|
||||
prevButtonLeadingConstraint = previousButton.leadingAnchor.constraint(equalTo: scrollContainerView.leadingAnchor, constant: paginationInset)
|
||||
nextButtonTrailingConstraint = nextButton.trailingAnchor.constraint(equalTo: scrollContainerView.trailingAnchor, constant: -paginationInset)
|
||||
prevButtonLeadingConstraint?.isActive = true
|
||||
nextButtonTrailingConstraint?.isActive = true
|
||||
}
|
||||
|
||||
private func updateScrollbarPosition(targetContentOffsetXPos:CGFloat) {
|
||||
let scrollContentSizeWidth = scrollView.contentSize.width
|
||||
let totalPositions = totalPositions()
|
||||
let layoutSpace = Int (floor( Double(scrollContentSizeWidth / Double(totalPositions))))
|
||||
let remindSpace = Int(targetContentOffsetXPos) % layoutSpace
|
||||
var contentPos = (Int(targetContentOffsetXPos) / layoutSpace) + 1
|
||||
contentPos = remindSpace > layoutSpace/2 ? contentPos+1 : contentPos
|
||||
carouselScrollBar.position = contentPos
|
||||
updateScrollPosition(position: contentPos, callbackText: "ScrollViewMoved")
|
||||
}
|
||||
|
||||
// Update scrollview offset relative to scrollbar thumb position
|
||||
private func updateScrollPosition(position: Int, callbackText: String) {
|
||||
if carouselScrollBar.numberOfSlides > 0 {
|
||||
let scrollContentSizeWidth = scrollView.contentSize.width
|
||||
let totalPositions = totalPositions()
|
||||
var xPos = 0.0
|
||||
if position == 1 {
|
||||
xPos = 0.0
|
||||
} else if position == totalPositions {
|
||||
xPos = scrollContentSizeWidth - containerView.frame.size.width
|
||||
} else {
|
||||
let isScrollbarSuppressed = views.count > 0 && layout.value == views.count
|
||||
let isPeekMinimumOnTablet = UIDevice.isIPad && peek == .minimum
|
||||
if !isScrollbarSuppressed {
|
||||
let slotWidthWithGutter = minimumSlotWidth + gutter.value
|
||||
let xPosition = CGFloat( Float(position-1) * Float(layout.value) * Float(slotWidthWithGutter))
|
||||
let peekWidth = (containerView.frame.size.width - gutter.value - (Double(layout.value) * (minimumSlotWidth + gutter.value)))/2
|
||||
xPos = (peek == .none || isPeekMinimumOnTablet) ? xPosition : xPosition - gutter.value - peekWidth
|
||||
}
|
||||
}
|
||||
carouselScrollBar.scrubberId = position+1
|
||||
let yPos = scrollView.contentOffset.y
|
||||
scrollView.setContentOffset(CGPoint(x: xPos, y: yPos), animated: true)
|
||||
showPaginationControls()
|
||||
groupIndex = position-1
|
||||
onChangePublisher.send(groupIndex)
|
||||
}
|
||||
}
|
||||
|
||||
// Get the overall positions of the carousel scrollbar relative to the slides and selected layout
|
||||
private func totalPositions() -> Int {
|
||||
return Int (ceil (Double(carouselScrollBar.numberOfSlides) / Double(layout.value)))
|
||||
}
|
||||
}
|
||||
|
||||
extension Carousel: UIScrollViewDelegate {
|
||||
//--------------------------------------------------
|
||||
// MARK: - UIScrollView Delegate
|
||||
//--------------------------------------------------
|
||||
public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
|
||||
updateScrollbarPosition(targetContentOffsetXPos: targetContentOffset.pointee.x)
|
||||
}
|
||||
|
||||
}
|
||||
15
VDS/Components/Carousel/CarouselChangeLog.txt
Normal file
15
VDS/Components/Carousel/CarouselChangeLog.txt
Normal file
@ -0,0 +1,15 @@
|
||||
MM/DD/YYYY
|
||||
----------------
|
||||
|
||||
06/22/2023
|
||||
----------------
|
||||
- Initial Beta Release
|
||||
|
||||
10/02/2023
|
||||
----------------
|
||||
- Removed (Beta) from header. Removed deprecated sections and “New” badge from Kind section.
|
||||
|
||||
11/20/2023
|
||||
----------------
|
||||
- Updated visuals to reflect new corner radius value - 12px
|
||||
- Updated focus border corner radius to 14px
|
||||
26
VDS/Components/Carousel/CarouselPaginationModel.swift
Normal file
26
VDS/Components/Carousel/CarouselPaginationModel.swift
Normal file
@ -0,0 +1,26 @@
|
||||
//
|
||||
// CarouselPaginationModel.swift
|
||||
// VDS
|
||||
//
|
||||
// Created by Kanamarlapudi, Vasavi on 06/06/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
/// Custom data type for pagination prop for 'Carousel' component.
|
||||
extension Carousel {
|
||||
public struct CarouselPaginationModel {
|
||||
|
||||
/// Pagination supports Button icon property 'kind'.
|
||||
public var kind: ButtonIcon.Kind
|
||||
|
||||
/// Pagination supports Button icon property 'floating'.
|
||||
public var floating: Bool
|
||||
|
||||
public init(kind: ButtonIcon.Kind, floating: Bool) {
|
||||
self.kind = kind
|
||||
self.floating = floating
|
||||
}
|
||||
}
|
||||
}
|
||||
27
VDS/Components/Carousel/CarouselSlotAlignmentModel.swift
Normal file
27
VDS/Components/Carousel/CarouselSlotAlignmentModel.swift
Normal file
@ -0,0 +1,27 @@
|
||||
//
|
||||
// CarouselSlotAlignmentModel.swift
|
||||
// VDS
|
||||
//
|
||||
// Created by Kanamarlapudi, Vasavi on 31/05/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Custom data type for the SlotAlignment prop for the 'carousel' component.
|
||||
extension Carousel {
|
||||
|
||||
/// Used only when slot content have different heights or widths.
|
||||
public struct CarouselSlotAlignmentModel {
|
||||
|
||||
/// Used for vertical alignment of slot alignment.
|
||||
public var vertical: Carousel.Vertical
|
||||
|
||||
/// Used for horizontal alignment of slot alignment.
|
||||
public var horizontal: Carousel.Horizontal
|
||||
|
||||
public init(vertical: Carousel.Vertical, horizontal: Carousel.Horizontal) {
|
||||
self.vertical = vertical
|
||||
self.horizontal = horizontal
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -12,6 +12,7 @@ import Combine
|
||||
|
||||
/// A carousel scrollbar is a control that allows to navigate between items in a carousel.
|
||||
/// It's also a status indicator that conveys the relative amount of content in a carousel and a location within it.
|
||||
@objcMembers
|
||||
@objc(VDSCarouselScrollbar)
|
||||
open class CarouselScrollbar: View {
|
||||
|
||||
@ -45,13 +46,13 @@ open class CarouselScrollbar: View {
|
||||
}
|
||||
|
||||
/// The number of slides that can appear at once in a set in a carousel container.
|
||||
open var selectedLayout: Layout? {
|
||||
get { return _selectedLayout }
|
||||
open var layout: Layout? {
|
||||
get { return _layout }
|
||||
set {
|
||||
if let newValue {
|
||||
_selectedLayout = newValue
|
||||
_layout = newValue
|
||||
} else {
|
||||
_selectedLayout = .oneUP
|
||||
_layout = .oneUP
|
||||
}
|
||||
setThumbWidth()
|
||||
scrollThumbToPosition(position)
|
||||
@ -198,7 +199,7 @@ open class CarouselScrollbar: View {
|
||||
//--------------------------------------------------
|
||||
// Sizes are from InVision design specs.
|
||||
internal var containerSize: CGSize { CGSize(width: 45, height: 44) }
|
||||
internal var _selectedLayout: Layout = .oneUP
|
||||
internal var _layout: Layout = .oneUP
|
||||
internal var _numberOfSlides: Int = 1
|
||||
internal var totalPositions: Int = 1
|
||||
internal var _position: Int = 1
|
||||
@ -329,7 +330,7 @@ open class CarouselScrollbar: View {
|
||||
|
||||
// Compute track width and should maintain minimum thumb width if needed
|
||||
private func setThumbWidth() {
|
||||
let width = (Float(trackViewWidth) / Float(numberOfSlides)) * Float(_selectedLayout.value)
|
||||
let width = (Float(trackViewWidth) / Float(numberOfSlides)) * Float(_layout.value)
|
||||
computedWidth = (width > Float(trackViewWidth)) ? Float(trackViewWidth) : width
|
||||
thumbWidth = (width <= Float(trackViewWidth) && width > minThumbWidth) ? width : ((width > Float(trackViewWidth)) ? Float(trackViewWidth) : minThumbWidth)
|
||||
thumbView.frame.size.width = CGFloat(thumbWidth)
|
||||
@ -362,7 +363,7 @@ open class CarouselScrollbar: View {
|
||||
}
|
||||
|
||||
private func checkPositions() {
|
||||
totalPositions = Int (ceil (Double(numberOfSlides) / Double(_selectedLayout.value)))
|
||||
totalPositions = Int (ceil (Double(numberOfSlides) / Double(_layout.value)))
|
||||
}
|
||||
|
||||
private func scrollThumbToPosition(_ position: Int) {
|
||||
|
||||
@ -12,6 +12,7 @@ import VDSCoreTokens
|
||||
|
||||
/// Checkboxes are a multi-select component through which a customer indicates a choice. This is also used within
|
||||
/// ``CheckboxItem`` and ``CheckboxGroup``
|
||||
@objcMembers
|
||||
@objc(VDSCheckbox)
|
||||
open class Checkbox: SelectorBase {
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ import VDSCoreTokens
|
||||
/// When the choice has multiple options, use a checkbox group. For example, use a checkbox group when
|
||||
/// asking a customer which attributes they would like to filter their search by. This uses ``CheckboxItem``
|
||||
/// to allow user selection.
|
||||
@objcMembers
|
||||
@objc(VDSCheckboxGroup)
|
||||
open class CheckboxGroup: SelectorGroupBase<CheckboxItem>, SelectorGroupMultiSelect {
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@ import Foundation
|
||||
import UIKit
|
||||
|
||||
/// Checkboxes are a multi-select component through which a customer indicates a choice. If a binary choice, the component is a checkbox. If the choice has multiple options, the component is a ``CheckboxGroup``.
|
||||
@objcMembers
|
||||
@objc(VDSCheckboxItem)
|
||||
open class CheckboxItem: SelectorItemBase<Checkbox> {
|
||||
|
||||
@ -47,6 +48,13 @@ open class CheckboxItem: SelectorItemBase<Checkbox> {
|
||||
isSelected.toggle()
|
||||
sendActions(for: .valueChanged)
|
||||
}
|
||||
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
let foo = ConcreteClass(customView: Checkbox())
|
||||
|
||||
print(foo.customView.isAnimated)
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
open override func updateView() {
|
||||
@ -54,3 +62,24 @@ open class CheckboxItem: SelectorItemBase<Checkbox> {
|
||||
super.updateView()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objcMembers
|
||||
open class GenericClass<T: UIView>: NSObject {
|
||||
public var customView: T
|
||||
|
||||
public init(customView: T = T()) {
|
||||
self.customView = customView
|
||||
}
|
||||
}
|
||||
|
||||
@objcMembers
|
||||
@objc(VDSConcreteClass)
|
||||
open class ConcreteClass: GenericClass<Checkbox> {
|
||||
|
||||
}
|
||||
|
||||
@objcMembers
|
||||
@objc(VDSConcreteCheckboxClass)
|
||||
open class ConcreteCheckboxClass: ConcreteClass {}
|
||||
|
||||
|
||||
@ -4,8 +4,9 @@ import VDSCoreTokens
|
||||
import Combine
|
||||
|
||||
/// A dropdown select is an expandable menu of predefined options that allows a customer to make a single selection.
|
||||
@objcMembers
|
||||
@objc(VDSDatePicker)
|
||||
open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopoverPresentationControllerDelegate {
|
||||
open class DatePicker: EntryFieldBase {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
@ -26,12 +27,33 @@ open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopov
|
||||
//--------------------------------------------------
|
||||
/// A callback when the selected option changes. Passes parameters (option).
|
||||
open var onDateSelected: ((Date, DatePicker) -> Void)?
|
||||
|
||||
|
||||
/// Override UIControl state to add the .error state if showError is true.
|
||||
open override var state: UIControl.State {
|
||||
get {
|
||||
var state = super.state
|
||||
if isEnabled {
|
||||
if isCalendarShowing {
|
||||
state.insert(.focused)
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Properties
|
||||
//--------------------------------------------------
|
||||
class Responder: UIView {
|
||||
open override var canBecomeFirstResponder: Bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
internal override var responder: UIResponder? { hiddenView }
|
||||
internal var isCalendarShowing: Bool = false { didSet { setNeedsUpdate() } }
|
||||
internal var hiddenView = Responder().with { $0.width(0) }
|
||||
internal var minWidthDefault = 186.0
|
||||
|
||||
internal var bottomStackView: UIStackView = {
|
||||
return UIStackView().with {
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
@ -41,6 +63,33 @@ open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopov
|
||||
$0.spacing = VDSLayout.space2X
|
||||
}
|
||||
}()
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Popover/Alert Properties
|
||||
//--------------------------------------------------
|
||||
/// View shown inline
|
||||
internal var popoverOverlayView = UIView().with {
|
||||
$0.backgroundColor = .clear
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
}
|
||||
|
||||
/// use this to track touch events outside of the popover in the overlay
|
||||
internal var popupOverlayTapGesture: AnyCancellable?
|
||||
|
||||
/// View shown inline
|
||||
internal var popoverView: UIView!
|
||||
/// Size used for the popover
|
||||
internal var popoverViewSize: CGSize = .zero
|
||||
/// Spacing between the popover and the ContainerView when not a AlertViewController
|
||||
internal var popoverSpacing: CGFloat = VDSLayout.space1X
|
||||
/// Whether or not the popover is visible
|
||||
internal var popoverVisible = false
|
||||
/// If the ContainerView exists somewhere in the superview hierarch in a ScrollView.
|
||||
internal var scrollView: UIScrollView?
|
||||
/// Original Found ScrollView ContentSize, this will get reset back to this size when the Popover is removed.
|
||||
internal var scrollViewContentSize: CGSize?
|
||||
/// Presenting ViewController with showing the AlertViewController Version.
|
||||
internal var topViewController: UIViewController?
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
@ -87,12 +136,12 @@ open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopov
|
||||
}
|
||||
|
||||
open var dateFormat: DateFormat = .shortNumeric { didSet{ setNeedsUpdate() } }
|
||||
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Configuration Properties
|
||||
//--------------------------------------------------
|
||||
internal override var containerSize: CGSize { CGSize(width: minWidthDefault, height: 44) }
|
||||
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
@ -100,20 +149,29 @@ open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopov
|
||||
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
|
||||
|
||||
// setting color config
|
||||
selectedDateLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable()
|
||||
|
||||
|
||||
// tap gesture
|
||||
containerView
|
||||
.publisher(for: UITapGestureRecognizer())
|
||||
.sink { [weak self] _ in
|
||||
guard let self else { return }
|
||||
if self.isEnabled && !self.isReadOnly {
|
||||
self.togglePicker()
|
||||
if isEnabled && !isReadOnly {
|
||||
showPopover()
|
||||
}
|
||||
}
|
||||
.store(in: &subscribers)
|
||||
|
||||
NotificationCenter.default
|
||||
.publisher(for: UIDevice.orientationDidChangeNotification).sink { [weak self] _ in
|
||||
guard let self else { return }
|
||||
hidePopoverView()
|
||||
}
|
||||
.store(in: &subscribers)
|
||||
|
||||
popoverOverlayView.isHidden = true
|
||||
}
|
||||
|
||||
open override func getFieldContainer() -> UIView {
|
||||
@ -125,9 +183,10 @@ open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopov
|
||||
}
|
||||
controlStackView.addArrangedSubview(calendarIcon)
|
||||
controlStackView.addArrangedSubview(selectedDateLabel)
|
||||
controlStackView.addArrangedSubview(hiddenView)
|
||||
return controlStackView
|
||||
}
|
||||
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
open override func updateView() {
|
||||
super.updateView()
|
||||
@ -140,7 +199,7 @@ open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopov
|
||||
selectedDateLabel.isEnabled = isEnabled
|
||||
calendarIcon.color = iconColorConfiguration.getColor(self)
|
||||
}
|
||||
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
@ -152,32 +211,253 @@ open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopov
|
||||
formatter.dateFormat = dateFormat.format
|
||||
selectedDateLabel.text = formatter.string(from: date)
|
||||
}
|
||||
}
|
||||
|
||||
internal func togglePicker() {
|
||||
let calendarVC = DatePickerViewController(calendarModel, delegate: self)
|
||||
calendarVC.modalPresentationStyle = .popover
|
||||
calendarVC.selectedDate = selectedDate ?? Date()
|
||||
if let popoverController = calendarVC.popoverPresentationController {
|
||||
popoverController.delegate = self
|
||||
popoverController.sourceView = containerView
|
||||
popoverController.sourceRect = containerView.bounds
|
||||
popoverController.permittedArrowDirections = .up
|
||||
extension DatePicker {
|
||||
|
||||
private func showPopover() {
|
||||
guard let viewController = UIApplication.topViewController(), var parentView = viewController.view, !popoverVisible else {
|
||||
hidePopoverView()
|
||||
return
|
||||
}
|
||||
if let viewController = UIApplication.topViewController() {
|
||||
viewController.present(calendarVC, animated: true, completion: nil)
|
||||
|
||||
let calendar = CalendarBase()
|
||||
calendar.activeDates = calendarModel.activeDates
|
||||
calendar.hideContainerBorder = calendarModel.hideContainerBorder
|
||||
calendar.hideCurrentDateIndicator = calendarModel.hideCurrentDateIndicator
|
||||
calendar.inactiveDates = calendarModel.inactiveDates
|
||||
calendar.indicators = calendarModel.indicators
|
||||
calendar.maxDate = calendarModel.maxDate
|
||||
calendar.minDate = calendarModel.minDate
|
||||
calendar.surface = surface
|
||||
calendar.setNeedsLayout()
|
||||
calendar.layoutIfNeeded()
|
||||
|
||||
//size the popover
|
||||
popoverViewSize = .init(width: calendar.frame.width, height: calendar.frame.height)
|
||||
|
||||
//find scrollView
|
||||
if scrollView == nil {
|
||||
scrollView = containerView.findSuperview(ofType: UIScrollView.self)
|
||||
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(flag: true)
|
||||
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
|
||||
isCalendarShowing = true
|
||||
}
|
||||
|
||||
internal func didSelectDate(_ controller: DatePickerViewController, date: Date) {
|
||||
selectedDate = date
|
||||
controller.dismiss(animated: true) { [weak self] in
|
||||
guard let self else { return }
|
||||
self.sendActions(for: .valueChanged)
|
||||
UIAccessibility.post(notification: .layoutChanged, argument: self.containerView)
|
||||
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)
|
||||
}
|
||||
}
|
||||
isCalendarShowing = false
|
||||
}
|
||||
|
||||
public func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
|
||||
return .none
|
||||
private func calculatePopoverPosition(relativeTo sourceView: UIView, in parentView: UIView, size: CGSize, with spacing: CGFloat) -> CGPoint? {
|
||||
let sourceFrameInParent = sourceView.convert(sourceView.bounds, to: parentView)
|
||||
let parentBounds = parentView.bounds
|
||||
let safeAreaInsets = parentView.safeAreaInsets
|
||||
let popoverWidth = size.width
|
||||
let popoverHeight = size.height
|
||||
|
||||
var popoverX: CGFloat = 0
|
||||
var popoverY: CGFloat = 0
|
||||
|
||||
// Calculate horizontal position
|
||||
if sourceFrameInParent.width <= popoverWidth {
|
||||
if sourceFrameInParent.midX - popoverWidth / 2 < 0 {
|
||||
// Align to left
|
||||
popoverX = sourceFrameInParent.minX
|
||||
} else if sourceFrameInParent.midX + popoverWidth / 2 > parentBounds.width {
|
||||
// Align to right
|
||||
popoverX = sourceFrameInParent.maxX - popoverWidth
|
||||
} else {
|
||||
// Center on source view
|
||||
popoverX = sourceFrameInParent.midX - popoverWidth / 2
|
||||
}
|
||||
} else {
|
||||
popoverX = sourceFrameInParent.minX //sourceFrameInParent.midX - popoverWidth / 2
|
||||
}
|
||||
|
||||
// Ensure the popover is within the parent's bounds horizontally
|
||||
popoverX = max(0, min(popoverX, parentBounds.width - popoverWidth))
|
||||
|
||||
var availableSpaceAbove: CGFloat = 0.0
|
||||
var availableSpaceBelow: CGFloat = 0.0
|
||||
|
||||
/// if the scrollView is set we want to change how we calculate the containerView's position
|
||||
if let scrollView = parentView as? UIScrollView {
|
||||
// Calculate vertical position and height
|
||||
availableSpaceAbove = sourceFrameInParent.minY - scrollView.bounds.minY - spacing
|
||||
availableSpaceBelow = scrollView.bounds.maxY - sourceFrameInParent.maxY - spacing
|
||||
|
||||
if availableSpaceAbove > availableSpaceBelow {
|
||||
// Show above
|
||||
popoverY = sourceFrameInParent.minY - popoverHeight - spacing
|
||||
} else {
|
||||
// Show below
|
||||
popoverY = sourceFrameInParent.maxY + spacing
|
||||
|
||||
// See if we need to expand the contentSize of the ScrollView
|
||||
let diff = scrollView.contentSize.height - sourceFrameInParent.maxY
|
||||
if diff < popoverHeight {
|
||||
scrollView.contentSize.height += popoverHeight - diff + VDSLayout.space4X
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// Calculate vertical position and height
|
||||
availableSpaceAbove = sourceFrameInParent.minY - safeAreaInsets.top - spacing
|
||||
availableSpaceBelow = parentBounds.height - sourceFrameInParent.maxY - safeAreaInsets.bottom - spacing
|
||||
|
||||
if availableSpaceAbove >= popoverHeight {
|
||||
// Show above
|
||||
popoverY = sourceFrameInParent.minY - popoverHeight - spacing
|
||||
|
||||
} else if availableSpaceBelow >= popoverHeight {
|
||||
// Show below
|
||||
popoverY = sourceFrameInParent.maxY + spacing
|
||||
|
||||
} else {
|
||||
|
||||
//return nil since there is no way we can show the popover without a scrollview
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return .init(x: popoverX, y: popoverY)
|
||||
}
|
||||
|
||||
private func gestureEventOccured(_ gesture: UIGestureRecognizer, parentView: UIView) {
|
||||
guard let popoverView, popoverVisible else { return }
|
||||
let location = gesture.location(in: parentView)
|
||||
if !popoverView.frame.contains(location) {
|
||||
hidePopoverView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension UIView {
|
||||
public func findSuperview<T: UIView>(ofType type: T.Type) -> T? {
|
||||
var currentView: UIView? = self
|
||||
while let view = currentView {
|
||||
if let superview = view.superview as? T {
|
||||
return superview
|
||||
}
|
||||
currentView = view.superview
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,8 +10,6 @@ import UIKit
|
||||
|
||||
extension DatePicker {
|
||||
public struct CalendarModel {
|
||||
public let surface: Surface
|
||||
|
||||
/// If set to true, the calendar will not have a border.
|
||||
public let hideContainerBorder: Bool
|
||||
|
||||
@ -35,15 +33,13 @@ extension DatePicker {
|
||||
/// Array of ``CalendarIndicatorModel`` you are wanting to show on legend.
|
||||
public let indicators: [CalendarBase.CalendarIndicatorModel]
|
||||
|
||||
public init(surface: Surface = .light,
|
||||
hideContainerBorder: Bool = false,
|
||||
public init(hideContainerBorder: Bool = false,
|
||||
hideCurrentDateIndicator: Bool = false,
|
||||
activeDates: [Date] = [],
|
||||
inactiveDates: [Date] = [],
|
||||
minDate: Date = Date().startOfMonth,
|
||||
maxDate: Date = Date().endOfMonth,
|
||||
indicators: [CalendarBase.CalendarIndicatorModel] = []) {
|
||||
self.surface = surface
|
||||
self.hideContainerBorder = hideContainerBorder
|
||||
self.hideCurrentDateIndicator = hideCurrentDateIndicator
|
||||
self.activeDates = activeDates
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -11,6 +11,7 @@ import VDSCoreTokens
|
||||
import Combine
|
||||
|
||||
/// A dropdown select is an expandable menu of predefined options that allows a customer to make a single selection.
|
||||
@objcMembers
|
||||
@objc(VDSDropdownSelect)
|
||||
open class DropdownSelect: EntryFieldBase {
|
||||
//--------------------------------------------------
|
||||
@ -30,19 +31,7 @@ open class DropdownSelect: EntryFieldBase {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
/// Override UIControl state to add the .error state if showSuccess is true and if showError is true.
|
||||
open override var state: UIControl.State {
|
||||
get {
|
||||
var state = super.state
|
||||
if dropdownField.isFirstResponder {
|
||||
state.insert(.focused)
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
/// If true, the label will be displayed inside the dropdown containerView. Otherwise, the label will be above the dropdown containerView like a normal text input.
|
||||
open var showInlineLabel: Bool = false { didSet { setNeedsUpdate() }}
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@ import Combine
|
||||
/// An icon is a graphical element that conveys information at a glance. It helps orient
|
||||
/// a customer, explain functionality and draw attention to interactive elements. Icons
|
||||
/// should have a functional purpose and should never be used for decoration.
|
||||
@objcMembers
|
||||
@objc(VDSIcon)
|
||||
open class Icon: View {
|
||||
|
||||
@ -93,12 +94,15 @@ open class Icon: View {
|
||||
backgroundColor = .clear
|
||||
|
||||
isAccessibilityElement = true
|
||||
accessibilityTraits = .image
|
||||
accessibilityTraits = .none
|
||||
accessibilityHint = "image"
|
||||
|
||||
bridge_accessibilityLabelBlock = { [weak self] in
|
||||
guard let self else { return "" }
|
||||
return name?.rawValue ?? "icon"
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -360,7 +360,7 @@ open class InputStepper: EntryFieldBase {
|
||||
}
|
||||
|
||||
// Set control width to input stepper.
|
||||
internal func setControlWidth(_ text: String?) {
|
||||
private func setControlWidth(_ text: String?) {
|
||||
if let text, text == "auto" {
|
||||
stepperWidthConstraint?.deactivate()
|
||||
} else if let controlWidth = Int(text ?? "") {
|
||||
@ -371,7 +371,7 @@ open class InputStepper: EntryFieldBase {
|
||||
}
|
||||
|
||||
// Handling the controlwidth without going beyond the width of the parent container.
|
||||
internal func updateStepperContainerWidth(controlWidth: CGFloat, width: CGFloat) {
|
||||
private func updateStepperContainerWidth(controlWidth: CGFloat, width: CGFloat) {
|
||||
if controlWidth >= containerSize.width && controlWidth <= width {
|
||||
stepperWidthConstraint?.deactivate()
|
||||
stepperWidthConstraint?.constant = controlWidth
|
||||
|
||||
@ -12,6 +12,7 @@ import Combine
|
||||
|
||||
/// Label is a standard view used to draw text with applying Typography through ``TextStyle`` as well
|
||||
/// as other attributes using any implemetation of ``LabelAttributeModel``.
|
||||
@objcMembers
|
||||
@objc(VDSLabel)
|
||||
open class Label: UILabel, ViewProtocol, UserInfoable {
|
||||
|
||||
@ -214,10 +215,6 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
|
||||
}
|
||||
|
||||
open func setup() {
|
||||
bridge_accessibilityLabelBlock = { [weak self] in
|
||||
guard let self else { return "" }
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
open func reset() {
|
||||
@ -389,60 +386,100 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Touch Events
|
||||
//--------------------------------------------------
|
||||
@objc private func textLinkTapped(_ gesture: UITapGestureRecognizer) {
|
||||
for actionable in actions {
|
||||
// This determines if we tapped on the desired range of text.
|
||||
let location = gesture.location(in: self)
|
||||
if didTapActionInLabel(location, inRange: actionable.range) {
|
||||
actionable.performAction()
|
||||
return
|
||||
}
|
||||
let location = gesture.location(in: self)
|
||||
if let action = actions.first(where: { isAction(for: location, inRange: $0.range) }) {
|
||||
action.performAction()
|
||||
}
|
||||
}
|
||||
|
||||
public func isAction(for location: CGPoint) -> Bool {
|
||||
for actionable in actions {
|
||||
if didTapActionInLabel(location, inRange: actionable.range) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
actions.contains(where: {isAction(for: location, inRange: $0.range)})
|
||||
}
|
||||
|
||||
private func didTapActionInLabel(_ location: CGPoint, inRange targetRange: NSRange) -> Bool {
|
||||
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 attributedText else { return false }
|
||||
let indexOfGlyph = layoutManager.glyphIndex(for: location, in: textContainer)
|
||||
let intrinsicWidth = intrinsicContentSize.width
|
||||
|
||||
// Assert that tapped occured within acceptable bounds based on alignment.
|
||||
switch textAlignment {
|
||||
case .right:
|
||||
if location.x < bounds.width - intrinsicWidth {
|
||||
return false
|
||||
}
|
||||
case .center:
|
||||
let halfBounds = bounds.width / 2
|
||||
let halfIntrinsicWidth = intrinsicWidth / 2
|
||||
|
||||
if location.x > halfBounds + halfIntrinsicWidth {
|
||||
return false
|
||||
} else if location.x < halfBounds - halfIntrinsicWidth {
|
||||
return false
|
||||
}
|
||||
default: // Left align
|
||||
if location.x > intrinsicWidth {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Affirms that the tap occured in the desired rect of provided by the target range.
|
||||
return layoutManager.boundingRect(forGlyphRange: targetRange, in: textContainer).contains(location)
|
||||
&& NSLocationInRange(indexOfGlyph, targetRange)
|
||||
}
|
||||
|
||||
/**
|
||||
Provides a text container and layout manager of how the text would appear on screen.
|
||||
They are used in tandem to derive low-level TextKit results of the label.
|
||||
*/
|
||||
public func abstractTextContainer() -> (textContainer: NSTextContainer, layoutManager: NSLayoutManager, textStorage: NSTextStorage)? {
|
||||
|
||||
// Must configure the attributed string to translate what would appear on screen to accurately analyze.
|
||||
guard let attributedText = attributedText else { return nil }
|
||||
|
||||
let paragraph = NSMutableParagraphStyle()
|
||||
paragraph.alignment = textAlignment
|
||||
|
||||
let stagedAttributedString = NSMutableAttributedString(attributedString: attributedText)
|
||||
stagedAttributedString.addAttributes([NSAttributedString.Key.paragraphStyle: paragraph], range: NSRange(location: 0, length: attributedText.string.count))
|
||||
|
||||
let textStorage = NSTextStorage(attributedString: stagedAttributedString)
|
||||
let layoutManager = NSLayoutManager()
|
||||
let textContainer = NSTextContainer(size: bounds.size)
|
||||
let textStorage = NSTextStorage(attributedString: attributedText)
|
||||
let textContainer = NSTextContainer(size: .zero)
|
||||
|
||||
layoutManager.addTextContainer(textContainer)
|
||||
textStorage.addLayoutManager(layoutManager)
|
||||
|
||||
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
|
||||
|
||||
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 attributedText else { return nil }
|
||||
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)
|
||||
|
||||
// Calculate the frame of the substring
|
||||
let layoutManager = NSLayoutManager()
|
||||
let textContainer = NSTextContainer(size: bounds.size)
|
||||
let textStorage = NSTextStorage(attributedString: attributedText)
|
||||
layoutManager.addTextContainer(textContainer)
|
||||
textStorage.addLayoutManager(layoutManager)
|
||||
|
||||
var glyphRange = NSRange()
|
||||
|
||||
// Convert the range for the substring into a range of glyphs
|
||||
layoutManager.characterRange(forGlyphRange: range, actualGlyphRange: &glyphRange)
|
||||
|
||||
let substringBounds = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
|
||||
|
||||
// Create custom accessibility element
|
||||
@ -456,13 +493,8 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
|
||||
return element
|
||||
}
|
||||
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Accessibility
|
||||
//--------------------------------------------------
|
||||
open var accessibilityAction: ((Label) -> Void)?
|
||||
|
||||
private var _isAccessibilityElement: Bool = false
|
||||
open override var isAccessibilityElement: Bool {
|
||||
get {
|
||||
var block: AXBoolReturnBlock?
|
||||
@ -478,15 +510,14 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
|
||||
if let block {
|
||||
return block()
|
||||
} else {
|
||||
return _isAccessibilityElement
|
||||
return super.isAccessibilityElement
|
||||
}
|
||||
}
|
||||
set {
|
||||
_isAccessibilityElement = newValue
|
||||
super.isAccessibilityElement = newValue
|
||||
}
|
||||
}
|
||||
|
||||
private var _accessibilityLabel: String?
|
||||
open override var accessibilityLabel: String? {
|
||||
get {
|
||||
var block: AXStringReturnBlock?
|
||||
@ -501,15 +532,14 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
|
||||
if let block {
|
||||
return block()
|
||||
} else {
|
||||
return _accessibilityLabel
|
||||
return super.accessibilityLabel
|
||||
}
|
||||
}
|
||||
set {
|
||||
_accessibilityLabel = newValue
|
||||
super.accessibilityLabel = newValue
|
||||
}
|
||||
}
|
||||
|
||||
private var _accessibilityHint: String?
|
||||
open override var accessibilityHint: String? {
|
||||
get {
|
||||
var block: AXStringReturnBlock?
|
||||
@ -524,15 +554,14 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
|
||||
if let block {
|
||||
return block()
|
||||
} else {
|
||||
return _accessibilityHint
|
||||
return super.accessibilityHint
|
||||
}
|
||||
}
|
||||
set {
|
||||
_accessibilityHint = newValue
|
||||
super.accessibilityHint = newValue
|
||||
}
|
||||
}
|
||||
|
||||
private var _accessibilityValue: String?
|
||||
open override var accessibilityValue: String? {
|
||||
get {
|
||||
var block: AXStringReturnBlock?
|
||||
@ -548,11 +577,11 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
|
||||
if let block{
|
||||
return block()
|
||||
} else {
|
||||
return _accessibilityValue
|
||||
return super.accessibilityValue
|
||||
}
|
||||
}
|
||||
set {
|
||||
_accessibilityValue = newValue
|
||||
super.accessibilityValue = newValue
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@ import UIKit
|
||||
import VDSCoreTokens
|
||||
|
||||
/// A line visually separates content sections or elements in lists, tables and layouts to indicate content hierarchy.
|
||||
@objcMembers
|
||||
@objc(VDSLine)
|
||||
open class Line: View {
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ import VDSCoreTokens
|
||||
|
||||
|
||||
/// A loader is an indicator that uses animation to show customers that there is an indefinite amount of wait time while a task is ongoing, e.g. a page is loading, a form is being submitted. The component disappears when the task is complete.
|
||||
@objcMembers
|
||||
@objc(VDSLoader)
|
||||
open class Loader: View {
|
||||
|
||||
|
||||
@ -10,6 +10,8 @@ import UIKit
|
||||
import VDSCoreTokens
|
||||
|
||||
/// ViewController to show the Loader, this will be presented using the LoaderLaunchable Protocl.
|
||||
@objcMembers
|
||||
@objc(VDSLoaderViewController)
|
||||
open class LoaderViewController: UIViewController, Surfaceable {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Properties
|
||||
|
||||
@ -14,6 +14,7 @@ import Combine
|
||||
/// in context. There are four types: information, success, warning and error; each
|
||||
/// with different color and content. They may be screen-specific, flow-specific or
|
||||
/// experience-wide.
|
||||
@objcMembers
|
||||
@objc(VDSNotification)
|
||||
open class Notification: View {
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ import VDSCoreTokens
|
||||
import Combine
|
||||
|
||||
///Pagination is a control that enables customers to navigate multiple pages of content by selecting either a specific page or the next or previous set of four pages.
|
||||
@objcMembers
|
||||
@objc(VDSPagination)
|
||||
open class Pagination: View {
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@ import UIKit
|
||||
import VDSCoreTokens
|
||||
|
||||
///This is customised button for Pagination view
|
||||
@objcMembers
|
||||
@objc(PaginationButton)
|
||||
open class PaginationButton: ButtonBase {
|
||||
//--------------------------------------------------
|
||||
|
||||
@ -11,6 +11,7 @@ import UIKit
|
||||
/// Radio boxes are single-select components through which a customer indicates a choice.
|
||||
/// They're stylized ``RadioButtons`` that must always be paired with one or more ``RadioBoxItem``
|
||||
/// in a radio box group. Use radio boxes to display choices like device storage.
|
||||
@objcMembers
|
||||
@objc(VDSRadioBoxGroup)
|
||||
open class RadioBoxGroup: SelectorGroupBase<RadioBoxItem>, SelectorGroupSingleSelect {
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ import VDSCoreTokens
|
||||
|
||||
/// Radio boxes are single-select components through which a customer indicates a choice
|
||||
/// that are used within a ``RadioBoxGroup``.
|
||||
@objcMembers
|
||||
@objc(VDSRadioBoxItem)
|
||||
open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable {
|
||||
|
||||
@ -214,7 +215,7 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable {
|
||||
selectorView.isAccessibilityElement = true
|
||||
selectorView.accessibilityTraits = .button
|
||||
addSubview(selectorView)
|
||||
selectorView.isUserInteractionEnabled = true
|
||||
selectorView.isUserInteractionEnabled = false
|
||||
|
||||
selectorView.addSubview(selectorStackView)
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@ import VDSCoreTokens
|
||||
/// Radio buttons are single-select components through which a customer indicates a choice.
|
||||
/// They must always be paired with one or more ``RadioButtonItem`` within a ``RadioButtonGroup``.
|
||||
/// Use radio buttons to display choices like delivery method.
|
||||
@objcMembers
|
||||
@objc(VDSRadioButton)
|
||||
open class RadioButton: SelectorBase {
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ import UIKit
|
||||
/// Radio buttons items are single-select components through which a customer indicates a choice.
|
||||
/// They must always be paired with one or more other ``RadioButtonItem`` within a radio button group.
|
||||
/// Use radio buttons to display choices like delivery method.
|
||||
@objcMembers
|
||||
@objc(VDSRadioButtonGroup)
|
||||
open class RadioButtonGroup: SelectorGroupBase<RadioButtonItem>, SelectorGroupSingleSelect {
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ import UIKit
|
||||
/// Radio buttons items are single-select components through which a customer indicates a choice.
|
||||
/// They must always be paired with one or more other radio button items within a ``RadioButtonGroup``.
|
||||
/// Use radio buttons to display choices like delivery method.
|
||||
@objcMembers
|
||||
@objc(VDSRadioButtonItem)
|
||||
open class RadioButtonItem: SelectorItemBase<RadioButton> {
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@ import UIKit
|
||||
import VDSCoreTokens
|
||||
|
||||
///Table is view composed of rows and columns, which takes any view into each cell and resizes based on the highest cell height.
|
||||
@objcMembers
|
||||
@objc(VDSTable)
|
||||
open class Table: View {
|
||||
|
||||
@ -51,10 +52,8 @@ open class Table: View {
|
||||
|
||||
func horizontalValue() -> CGFloat {
|
||||
switch self {
|
||||
case .standard:
|
||||
return UIDevice.isIPad ? VDSLayout.space8X : VDSLayout.space6X
|
||||
case .compact:
|
||||
return UIDevice.isIPad ? VDSLayout.space8X : VDSLayout.space6X
|
||||
case .standard, .compact:
|
||||
return UIDevice.isIPad ? VDSLayout.space4X : VDSLayout.space3X
|
||||
}
|
||||
}
|
||||
|
||||
@ -147,7 +146,10 @@ extension Table: UICollectionViewDelegate, UICollectionViewDataSource, TableColl
|
||||
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TableCellItem.Identifier, for: indexPath) as? TableCellItem else { return UICollectionViewCell() }
|
||||
let currentItem = tableData[indexPath.section].columns[indexPath.row]
|
||||
let shouldStrip = striped ? (indexPath.section % 2 != 0) : false
|
||||
cell.updateCell(content: currentItem, surface: surface, striped: shouldStrip, padding: padding)
|
||||
let isHeader = tableData[indexPath.section].isHeader
|
||||
var edgePadding = UIEdgeInsets(top: padding.verticalValue(), left: 0, bottom: padding.verticalValue(), right: padding.horizontalValue())
|
||||
edgePadding.left = (indexPath.row == 0 && !striped) ? VDSLayout.space1X : padding.horizontalValue()
|
||||
cell.updateCell(content: currentItem, surface: surface, striped: shouldStrip, padding: edgePadding, isHeader: isHeader)
|
||||
return cell
|
||||
}
|
||||
|
||||
|
||||
@ -29,10 +29,7 @@ final class TableCellItem: UICollectionViewCell {
|
||||
|
||||
/// Color configuration for striped background color
|
||||
private let stripedColorConfiguration = SurfaceColorConfiguration(VDSColor.backgroundSecondaryLight, VDSColor.backgroundSecondaryDark)
|
||||
|
||||
/// Padding parameter to maintain the edge spacing of the containerView
|
||||
private var padding: Table.Padding = .standard
|
||||
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
@ -58,10 +55,10 @@ final class TableCellItem: UICollectionViewCell {
|
||||
//--------------------------------------------------
|
||||
|
||||
/// Updates the cell content with ``TableItemModel`` and styling/padding attributes from other parameters
|
||||
public func updateCell(content: TableItemModel, surface: Surface, striped: Bool = false, padding: Table.Padding = .standard) {
|
||||
public func updateCell(content: TableItemModel, surface: Surface, striped: Bool = false, padding: UIEdgeInsets, isHeader: Bool = false) {
|
||||
|
||||
containerView.subviews.forEach({ $0.removeFromSuperview() })
|
||||
self.padding = padding
|
||||
|
||||
containerView.surface = surface
|
||||
containerView.backgroundColor = striped ? stripedColorConfiguration.getColor(surface) : backgroundColorConfiguration.getColor(surface)
|
||||
|
||||
@ -82,11 +79,11 @@ final class TableCellItem: UICollectionViewCell {
|
||||
}
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
component.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: VDSLayout.space1X),
|
||||
component.topAnchor.constraint(greaterThanOrEqualTo: containerView.topAnchor, constant: padding.verticalValue()),
|
||||
containerView.bottomAnchor.constraint(greaterThanOrEqualTo: component.bottomAnchor, constant: padding.verticalValue()),
|
||||
containerView.trailingAnchor.constraint(greaterThanOrEqualTo: component.trailingAnchor, constant: padding.horizontalValue()),
|
||||
containerView.centerYAnchor.constraint(equalTo: component.centerYAnchor)
|
||||
component.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: padding.left),
|
||||
containerView.trailingAnchor.constraint(greaterThanOrEqualTo: component.trailingAnchor, constant: padding.right)
|
||||
])
|
||||
|
||||
component.topAnchor.constraint(equalTo: containerView.topAnchor, constant: padding.top).isActive = !isHeader
|
||||
containerView.bottomAnchor.constraint(equalTo: component.bottomAnchor, constant: padding.bottom).isActive = isHeader
|
||||
}
|
||||
}
|
||||
|
||||
@ -40,6 +40,9 @@ class MatrixFlowLayout : UICollectionViewFlowLayout {
|
||||
///padding type to be set from Table component, which is used to calculate the size & position of the cell.
|
||||
var layoutPadding: Table.Padding = .standard
|
||||
|
||||
///Striped status of Table, based on this status padding of leading attribute changes.
|
||||
var striped: Bool = false
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
@ -77,7 +80,7 @@ class MatrixFlowLayout : UICollectionViewFlowLayout {
|
||||
let selectedItem = delegate.collectionView(collectionView, dataForItemAt: indexPath)
|
||||
|
||||
///Calculate the estimated height of the cell
|
||||
let itemHeight = estimateHeightFor(item: selectedItem, with: itemWidth)
|
||||
let itemHeight = estimateHeightFor(item: selectedItem, with: itemWidth, index: indexPath)
|
||||
|
||||
layoutWidth += itemWidth
|
||||
|
||||
@ -108,8 +111,8 @@ class MatrixFlowLayout : UICollectionViewFlowLayout {
|
||||
}
|
||||
|
||||
/// Fetches estimated height by calling the cell's component estimated height and adding padding
|
||||
private func estimateHeightFor(item: TableItemModel, with width: CGFloat) -> CGFloat {
|
||||
|
||||
private func estimateHeightFor(item: TableItemModel, with width: CGFloat, index: IndexPath) -> CGFloat {
|
||||
let horizontalPadding = (index.row == 0 && !striped) ? (VDSLayout.space1X + layoutPadding.horizontalValue()) : (2 * layoutPadding.horizontalValue())
|
||||
let itemWidth = width - layoutPadding.horizontalValue() - defaultLeadingPadding
|
||||
let maxSize = CGSize(width: itemWidth, height: CGFloat.greatestFiniteMagnitude)
|
||||
let estItemSize = item.component?.systemLayoutSizeFitting(maxSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel) ?? CGSize(width: itemWidth, height: item.defaultHeight)
|
||||
|
||||
@ -11,11 +11,14 @@ public struct TableRowModel {
|
||||
|
||||
public var columns: [TableItemModel]
|
||||
|
||||
public var isHeader: Bool = false
|
||||
|
||||
public var columnsCount: Int {
|
||||
return columns.count
|
||||
}
|
||||
|
||||
public init(columns: [TableItemModel]) {
|
||||
public init(columns: [TableItemModel], isHeader: Bool = false) {
|
||||
self.columns = columns
|
||||
self.isHeader = isHeader
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@ import VDSCoreTokens
|
||||
import Combine
|
||||
|
||||
extension Tabs {
|
||||
|
||||
@objcMembers
|
||||
@objc(VDSTab)
|
||||
open class Tab: Control, Groupable {
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@ import UIKit
|
||||
import VDSCoreTokens
|
||||
|
||||
/// Tabs are organizational components that group content and allow customers to navigate its display. Use them to separate content when the content is related but doesn’t need to be compared.
|
||||
@objcMembers
|
||||
@objc(VDSTabs)
|
||||
open class Tabs: View {
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
@objcMembers
|
||||
@objc(VDSTabsContainer)
|
||||
open class TabsContainer: View {
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ import VDSCoreTokens
|
||||
import Combine
|
||||
|
||||
/// Base Class used to build out a Input controls.
|
||||
@objcMembers
|
||||
@objc(VDSEntryField)
|
||||
open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
|
||||
@ -28,7 +29,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
public required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
}
|
||||
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Enums
|
||||
//--------------------------------------------------
|
||||
@ -92,12 +93,6 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
}
|
||||
}()
|
||||
|
||||
/// This is the view that will be wrapped with the border for userInteraction.
|
||||
/// The only subview of this view is the fieldStackView
|
||||
internal var containerView = View().with {
|
||||
$0.isAccessibilityElement = true
|
||||
}
|
||||
|
||||
/// This is set by a local method.
|
||||
internal var bottomContainerView: UIView!
|
||||
|
||||
@ -115,7 +110,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
internal var widthConstraint: NSLayoutConstraint?
|
||||
internal var trailingEqualsConstraint: NSLayoutConstraint?
|
||||
internal var trailingLessThanEqualsConstraint: NSLayoutConstraint?
|
||||
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Configuration Properties
|
||||
//--------------------------------------------------
|
||||
@ -133,18 +128,18 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forDisabled: true)
|
||||
$0.setSurfaceColors(VDSColor.elementsSecondaryOnlight, VDSColor.elementsSecondaryOndark, forDisabled: false)
|
||||
}
|
||||
|
||||
|
||||
internal var backgroundColorConfiguration = ControlColorConfiguration().with {
|
||||
$0.setSurfaceColors(VDSFormControlsColor.backgroundOnlight, VDSFormControlsColor.backgroundOndark, forState: .normal)
|
||||
$0.setSurfaceColors(VDSFormControlsColor.backgroundOnlight, VDSFormControlsColor.backgroundOndark, forState: .disabled)
|
||||
$0.setSurfaceColors(VDSColor.feedbackErrorBackgroundOnlight, VDSColor.feedbackErrorBackgroundOndark, forState: .error)
|
||||
$0.setSurfaceColors(VDSColor.feedbackErrorBackgroundOnlight, VDSColor.feedbackErrorBackgroundOndark, forState: [.error, .focused])
|
||||
}
|
||||
|
||||
|
||||
internal var borderColorConfiguration = ControlColorConfiguration().with {
|
||||
$0.setSurfaceColors(VDSFormControlsColor.borderOnlight, VDSFormControlsColor.borderOndark, forState: .normal)
|
||||
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOnlight, forState: .focused)
|
||||
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOnlight, forState: [.focused, .error])
|
||||
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .focused)
|
||||
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: [.focused, .error])
|
||||
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled)
|
||||
$0.setSurfaceColors(VDSColor.feedbackErrorOnlight, VDSColor.feedbackErrorOndark, forState: .error)
|
||||
$0.setSurfaceColors(VDSFormControlsColor.borderReadonlyOnlight, VDSFormControlsColor.borderReadonlyOndark, forState: .readonly)
|
||||
@ -155,7 +150,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled)
|
||||
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .error)
|
||||
}
|
||||
|
||||
|
||||
internal var readOnlyBorderColorConfiguration = ControlColorConfiguration().with {
|
||||
$0.setSurfaceColors(VDSFormControlsColor.borderReadonlyOnlight, VDSFormControlsColor.borderReadonlyOndark, forState: .normal)
|
||||
}
|
||||
@ -163,8 +158,14 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
open var onChangeSubscriber: AnyCancellable?
|
||||
/// This is the view that will be wrapped with the border for userInteraction.
|
||||
/// The only subview of this view is the fieldStackView
|
||||
open var containerView = View().with {
|
||||
$0.isAccessibilityElement = true
|
||||
}
|
||||
|
||||
open var onChangeSubscriber: AnyCancellable?
|
||||
|
||||
open var titleLabel = Label().with {
|
||||
$0.setContentCompressionResistancePriority(.required, for: .vertical)
|
||||
$0.textStyle = .bodySmall
|
||||
@ -183,9 +184,11 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
|
||||
open var statusIcon: Icon = Icon().with {
|
||||
$0.size = .medium
|
||||
$0.isAccessibilityElement = false
|
||||
$0.isAccessibilityElement = true
|
||||
}
|
||||
|
||||
|
||||
open var useRequiredRule: Bool = true { didSet { setNeedsUpdate() } }
|
||||
|
||||
open var labelText: String? { didSet { setNeedsUpdate() } }
|
||||
|
||||
open var helperText: String? { didSet { setNeedsUpdate() } }
|
||||
@ -195,7 +198,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
|
||||
/// FormFieldValidator
|
||||
open var validator: (any FormFieldValidatorable)?
|
||||
|
||||
|
||||
/// Override UIControl state to add the .error state if showError is true.
|
||||
open override var state: UIControl.State {
|
||||
get {
|
||||
@ -207,21 +210,24 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
if isReadOnly {
|
||||
state.insert(.readonly)
|
||||
}
|
||||
if let responder, responder.isFirstResponder {
|
||||
state.insert(.focused)
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
open var errorText: String? { didSet { setNeedsUpdate() } }
|
||||
|
||||
|
||||
open var tooltipModel: Tooltip.TooltipModel? { didSet { setNeedsUpdate() } }
|
||||
|
||||
|
||||
open var transparentBackground: Bool = false { didSet { setNeedsUpdate() } }
|
||||
|
||||
open var width: CGFloat? { didSet { setNeedsUpdate() } }
|
||||
|
||||
|
||||
open var inputId: String? { didSet { setNeedsUpdate() } }
|
||||
|
||||
|
||||
/// The text of this textField.
|
||||
open var value: String? {
|
||||
get { fatalError("must be read from subclass")}
|
||||
@ -232,21 +238,21 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
open var isRequired: Bool = false { didSet { setNeedsUpdate() } }
|
||||
|
||||
open var isReadOnly: Bool = false { didSet { setNeedsUpdate() } }
|
||||
|
||||
|
||||
open var helperTextPlacement: HelperTextPlacement = .bottom {
|
||||
didSet {
|
||||
updateHelperTextPosition()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
open var rules = [AnyRule<String>]()
|
||||
|
||||
|
||||
open var accessibilityHintText: String = "Double tap to open"
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
|
||||
|
||||
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
@ -326,9 +332,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
if let errorText, showError {
|
||||
accessibilityLabels.append("error, \(errorText)")
|
||||
}
|
||||
|
||||
accessibilityLabels.append("\(Self.self)")
|
||||
|
||||
|
||||
return accessibilityLabels.joined(separator: ", ")
|
||||
}
|
||||
|
||||
@ -341,11 +345,17 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
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
|
||||
open override func updateView() {
|
||||
super.updateView()
|
||||
updateRules()
|
||||
updateContainerView(flag: true)
|
||||
updateContainerWidth()
|
||||
updateTitleLabel()
|
||||
@ -363,7 +373,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
titleLabel.textStyle = .bodySmall
|
||||
errorLabel.textStyle = .bodySmall
|
||||
helperLabel.textStyle = .bodySmall
|
||||
|
||||
|
||||
labelText = nil
|
||||
helperText = nil
|
||||
showError = false
|
||||
@ -381,19 +391,19 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
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
|
||||
//--------------------------------------------------
|
||||
@ -401,21 +411,20 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
open func getFieldContainer() -> UIView {
|
||||
fatalError("Subclass must return the view that contains the field/view the user will interact with.")
|
||||
}
|
||||
|
||||
|
||||
/// Container for the area in which helper or error text presents.
|
||||
open func getBottomContainer() -> UIView {
|
||||
return bottomContainerStackView
|
||||
}
|
||||
|
||||
|
||||
open func validate(){
|
||||
updateRules()
|
||||
validator = FormFieldValidator<EntryFieldBase>(field: self, rules: rules)
|
||||
validator?.validate()
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
|
||||
open func updateTitleLabel() {
|
||||
|
||||
|
||||
//update the local vars for the label since we no
|
||||
//long have a model
|
||||
var attributes: [any LabelAttributeModel] = []
|
||||
@ -436,36 +445,43 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
if let tooltipModel {
|
||||
attributes.append(TooltipLabelAttribute(surface: surface, model: tooltipModel, presenter: self))
|
||||
}
|
||||
|
||||
|
||||
//set the titleLabel
|
||||
titleLabel.text = updatedLabelText
|
||||
titleLabel.attributes = attributes
|
||||
titleLabel.surface = surface
|
||||
titleLabel.isEnabled = isEnabled
|
||||
}
|
||||
|
||||
|
||||
open func updateErrorLabel(){
|
||||
if showError, let errorText {
|
||||
errorLabel.text = errorText
|
||||
errorLabel.surface = surface
|
||||
errorLabel.isEnabled = isEnabled
|
||||
errorLabel.isHidden = false
|
||||
statusIcon.name = .error
|
||||
statusIcon.surface = surface
|
||||
statusIcon.isHidden = !isEnabled || state.contains(.focused)
|
||||
} else if hasInternalError, let internalErrorText {
|
||||
errorLabel.text = internalErrorText
|
||||
errorLabel.surface = surface
|
||||
errorLabel.isEnabled = isEnabled
|
||||
errorLabel.isHidden = false
|
||||
|
||||
/// always show the errorIcon if there is an error
|
||||
if showError || hasInternalError {
|
||||
statusIcon.name = .error
|
||||
statusIcon.surface = surface
|
||||
statusIcon.isHidden = !isEnabled || state.contains(.focused)
|
||||
} else {
|
||||
statusIcon.isHidden = true
|
||||
errorLabel.isHidden = true
|
||||
}
|
||||
statusIcon.color = iconColorConfiguration.getColor(self)
|
||||
|
||||
// only show errorLabel if there is a message
|
||||
var message: String?
|
||||
if showError, let errorText {
|
||||
message = errorText
|
||||
} else if hasInternalError, let internalErrorText {
|
||||
message = internalErrorText
|
||||
}
|
||||
|
||||
if let message {
|
||||
errorLabel.text = message
|
||||
errorLabel.surface = surface
|
||||
errorLabel.isEnabled = isEnabled
|
||||
errorLabel.isHidden = false
|
||||
} else {
|
||||
errorLabel.isHidden = true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
open func updateHelperLabel(){
|
||||
@ -507,7 +523,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
//--------------------------------------------------
|
||||
internal func updateRules() {
|
||||
rules.removeAll()
|
||||
if self.isRequired {
|
||||
if isRequired && useRequiredRule {
|
||||
let rule = RequiredRule()
|
||||
if let errorText, !errorText.isEmpty {
|
||||
rule.errorMessage = errorText
|
||||
|
||||
@ -51,6 +51,14 @@ extension InputField {
|
||||
}
|
||||
}
|
||||
|
||||
public var accessibilityLabel: String {
|
||||
switch self {
|
||||
case .generic, .placeholder: return "credit card"
|
||||
default: return rawValue
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func separatorIndices(_ length: Int) -> [Int] {
|
||||
var indices: [Int] = [4, 8, 12]
|
||||
switch self {
|
||||
@ -125,7 +133,7 @@ extension InputField {
|
||||
|
||||
class CreditCardHandler: FieldTypeHandler {
|
||||
static let shared = CreditCardHandler()
|
||||
|
||||
|
||||
private override init() {
|
||||
super.init()
|
||||
self.validateOnChange = false
|
||||
@ -135,6 +143,7 @@ extension InputField {
|
||||
fileprivate func updateLeftImage(_ inputField: InputField) {
|
||||
let imageName = inputField.cardType.imageName(surface: inputField.surface)
|
||||
creditCardImageView.image = BundleManager.shared.image(for: imageName)
|
||||
creditCardImageView.accessibilityLabel = inputField.cardType.accessibilityLabel
|
||||
}
|
||||
|
||||
override func updateView(_ inputField: InputField) {
|
||||
@ -148,14 +157,14 @@ extension InputField {
|
||||
|
||||
inputField.textField.leftView = iconContainerView
|
||||
inputField.textField.leftViewMode = .always
|
||||
|
||||
|
||||
updateLeftImage(inputField)
|
||||
}
|
||||
|
||||
|
||||
internal var creditCardImageView = UIImageView().with {
|
||||
$0.height(20)
|
||||
$0.width(32)
|
||||
$0.isAccessibilityElement = false
|
||||
$0.isAccessibilityElement = true
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
$0.contentMode = .scaleAspectFill
|
||||
$0.clipsToBounds = true
|
||||
|
||||
@ -68,6 +68,12 @@ extension InputField {
|
||||
actionModel.onClick(inputField)
|
||||
}
|
||||
inputField.actionTextLink.isHidden = false
|
||||
// set the accessibilityLabel
|
||||
if let labelText = inputField.labelText {
|
||||
inputField.actionTextLink.bridge_accessibilityLabelBlock = {
|
||||
return "\(actionModel.text) \(labelText)"
|
||||
}
|
||||
}
|
||||
inputField.fieldStackView.setCustomSpacing(VDSLayout.space2X, after: inputField.statusIcon)
|
||||
} else {
|
||||
inputField.actionTextLink.isHidden = true
|
||||
|
||||
@ -41,7 +41,7 @@ extension InputField {
|
||||
guard let self else { return }
|
||||
self.passwordActionType = nextPasswordActionType
|
||||
inputField.setNeedsUpdate()
|
||||
})
|
||||
})
|
||||
} else {
|
||||
passwordActionType = .show
|
||||
}
|
||||
|
||||
@ -10,6 +10,35 @@ import UIKit
|
||||
|
||||
extension InputField {
|
||||
|
||||
public class TelephoneNumberValidator: Rule, Withable {
|
||||
public var format: String
|
||||
public var errorMessage: String = "Please enter a valid telephone number"
|
||||
|
||||
public init(format: String) {
|
||||
self.format = format
|
||||
}
|
||||
|
||||
public func isValid(value: String?) -> Bool {
|
||||
guard let value, !value.isEmpty else { return true }
|
||||
let regex = createRegex(from: format)
|
||||
let predicate = NSPredicate(format: "SELF MATCHES %@", regex)
|
||||
let valid = predicate.evaluate(with: value)
|
||||
return valid
|
||||
}
|
||||
|
||||
private func createRegex(from format: String) -> String {
|
||||
// Escape special regex characters in the format string
|
||||
let escapedFormat = NSRegularExpression.escapedPattern(for: format)
|
||||
|
||||
// Replace placeholder characters with regex patterns
|
||||
let regex = escapedFormat
|
||||
.replacingOccurrences(of: "X", with: "\\d")
|
||||
|
||||
return "^" + regex + "$"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TelephoneHandler: FieldTypeHandler {
|
||||
static let shared = TelephoneHandler()
|
||||
|
||||
@ -25,14 +54,7 @@ extension InputField {
|
||||
}
|
||||
|
||||
override func appendRules(_ inputField: InputField) {
|
||||
if let text = inputField.textField.text, text.count > 0 {
|
||||
let rule = CharacterCountRule().copyWith {
|
||||
$0.maxLength = "XXX-XXX-XXXX".count
|
||||
$0.compareType = .equals
|
||||
$0.errorMessage = "Enter a valid telephone."
|
||||
}
|
||||
inputField.rules.append(.init(rule))
|
||||
}
|
||||
inputField.rules.append(.init(TelephoneNumberValidator(format: "XXX-XXX-XXXX")))
|
||||
}
|
||||
|
||||
override func textField(_ inputField: InputField, textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
||||
@ -49,7 +71,7 @@ extension InputField {
|
||||
let rawNumber = newText.filter { $0.isNumber }
|
||||
|
||||
// Format the number with dashes
|
||||
let formattedNumber = formatUSNumber(rawNumber)
|
||||
let formattedNumber = rawNumber.formatUSNumber()
|
||||
|
||||
// Set the formatted text
|
||||
textField.text = formattedNumber
|
||||
@ -62,43 +84,54 @@ extension InputField {
|
||||
textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition)
|
||||
}
|
||||
|
||||
value = formattedNumber
|
||||
|
||||
// Prevent the default behavior
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
internal func formatUSNumber(_ number: String) -> String {
|
||||
// Format the number in the style XXX-XXX-XXXX
|
||||
let areaCodeLength = 3
|
||||
let centralOfficeCodeLength = 3
|
||||
let lineNumberLength = 4
|
||||
|
||||
var formattedNumber = ""
|
||||
|
||||
if number.count > 0 {
|
||||
formattedNumber.append(contentsOf: number.prefix(areaCodeLength))
|
||||
override func textFieldDidEndEditing(_ inputField: InputField, textField: UITextField) {
|
||||
if let text = inputField.text {
|
||||
textField.text = text.formatUSNumber()
|
||||
value = textField.text
|
||||
}
|
||||
|
||||
if number.count > areaCodeLength {
|
||||
let startIndex = number.index(number.startIndex, offsetBy: areaCodeLength)
|
||||
let endIndex = number.index(startIndex, offsetBy: min(centralOfficeCodeLength, number.count - areaCodeLength))
|
||||
let centralOfficeCode = number[startIndex..<endIndex]
|
||||
formattedNumber.append("-")
|
||||
formattedNumber.append(contentsOf: centralOfficeCode)
|
||||
}
|
||||
|
||||
if number.count > areaCodeLength + centralOfficeCodeLength {
|
||||
let startIndex = number.index(number.startIndex, offsetBy: areaCodeLength + centralOfficeCodeLength)
|
||||
let endIndex = number.index(startIndex, offsetBy: min(lineNumberLength, number.count - areaCodeLength - centralOfficeCodeLength))
|
||||
let lineNumber = number[startIndex..<endIndex]
|
||||
formattedNumber.append("-")
|
||||
formattedNumber.append(contentsOf: lineNumber)
|
||||
}
|
||||
|
||||
return formattedNumber
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension String {
|
||||
public func formatUSNumber() -> String {
|
||||
// Format the number in the style XXX-XXX-XXXX
|
||||
let areaCodeLength = 3
|
||||
let centralOfficeCodeLength = 3
|
||||
let lineNumberLength = 4
|
||||
|
||||
var formattedNumber = ""
|
||||
let number = filter { $0.isNumber }
|
||||
|
||||
if number.count > 0 {
|
||||
formattedNumber.append(contentsOf: number.prefix(areaCodeLength))
|
||||
}
|
||||
|
||||
if number.count > areaCodeLength {
|
||||
let startIndex = number.index(number.startIndex, offsetBy: areaCodeLength)
|
||||
let endIndex = number.index(startIndex, offsetBy: min(centralOfficeCodeLength, number.count - areaCodeLength))
|
||||
let centralOfficeCode = number[startIndex..<endIndex]
|
||||
formattedNumber.append("-")
|
||||
formattedNumber.append(contentsOf: centralOfficeCode)
|
||||
}
|
||||
|
||||
if number.count > areaCodeLength + centralOfficeCodeLength {
|
||||
let startIndex = number.index(number.startIndex, offsetBy: areaCodeLength + centralOfficeCodeLength)
|
||||
let endIndex = number.index(startIndex, offsetBy: min(lineNumberLength, number.count - areaCodeLength - centralOfficeCodeLength))
|
||||
let lineNumber = number[startIndex..<endIndex]
|
||||
formattedNumber.append("-")
|
||||
formattedNumber.append(contentsOf: lineNumber)
|
||||
}
|
||||
|
||||
return formattedNumber
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@ import Combine
|
||||
/// An input field is an input wherein a customer enters information. They typically appear in forms.
|
||||
/// Specialized input fields capture credit card numbers, inline actions, passwords, phone numbers,
|
||||
/// dates and security codes in their correct formats.
|
||||
@objcMembers
|
||||
@objc(VDSInputField)
|
||||
open class InputField: EntryFieldBase {
|
||||
|
||||
@ -105,6 +106,11 @@ open class InputField: EntryFieldBase {
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
$0.textStyle = TextStyle.bodyLarge
|
||||
$0.isAccessibilityElement = false
|
||||
$0.autocorrectionType = .no
|
||||
$0.spellCheckingType = .no
|
||||
$0.smartQuotesType = .no
|
||||
$0.smartDashesType = .no
|
||||
$0.smartInsertDeleteType = .no
|
||||
}
|
||||
|
||||
/// Color configuration for the textField.
|
||||
@ -166,11 +172,7 @@ open class InputField: EntryFieldBase {
|
||||
if showSuccess {
|
||||
state.insert(.success)
|
||||
}
|
||||
|
||||
if textField.isFirstResponder {
|
||||
state.insert(.focused)
|
||||
}
|
||||
|
||||
|
||||
return state
|
||||
}
|
||||
}
|
||||
@ -186,6 +188,8 @@ open class InputField: EntryFieldBase {
|
||||
super.setup()
|
||||
accessibilityHintText = "Double tap to edit"
|
||||
|
||||
actionTextLink.accessibilityTraits = .button
|
||||
|
||||
textField.heightAnchor.constraint(equalToConstant: 20).isActive = true
|
||||
textField.delegate = self
|
||||
bottomContainerStackView.insertArrangedSubview(successLabel, at: 0)
|
||||
@ -200,6 +204,59 @@ open class InputField: EntryFieldBase {
|
||||
borderColorConfiguration.setSurfaceColors(VDSColor.feedbackSuccessOnlight, VDSColor.feedbackSuccessOndark, forState: .success)
|
||||
|
||||
textField.textColorConfiguration = textFieldTextColorConfiguration
|
||||
|
||||
containerView.bridge_accessibilityLabelBlock = { [weak self] in
|
||||
guard let self else { return "" }
|
||||
var accessibilityLabels = [String]()
|
||||
|
||||
if let text = titleLabel.text?.trimmingCharacters(in: .whitespaces) {
|
||||
accessibilityLabels.append(text)
|
||||
}
|
||||
|
||||
if let formatText = textField.formatText, !formatText.isEmpty, textField.text.isEmpty {
|
||||
accessibilityLabels.append("format, \(formatText)")
|
||||
}
|
||||
|
||||
if let placeholderText = textField.placeholder, !placeholderText.isEmpty, textField.text.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
|
||||
}
|
||||
}
|
||||
|
||||
containerView.bridge_accessibilityValueBlock = { [weak self] in
|
||||
guard let self else { return "" }
|
||||
return textField.isSecureTextEntry ? "\(textField.text.count) stars" : value
|
||||
}
|
||||
}
|
||||
|
||||
open override func getFieldContainer() -> UIView {
|
||||
@ -255,6 +312,24 @@ open class InputField: EntryFieldBase {
|
||||
}
|
||||
}
|
||||
|
||||
open var widthPercentage: CGFloat? { didSet { setNeedsUpdate() } }
|
||||
|
||||
internal override func updateContainerWidth() {
|
||||
widthConstraint?.deactivate()
|
||||
trailingLessThanEqualsConstraint?.deactivate()
|
||||
trailingEqualsConstraint?.deactivate()
|
||||
|
||||
//see if there is a widthPercentage and follow the same pattern as done for "width"
|
||||
let currentWidth = (horizontalPinnedWidth() ?? 0) * (widthPercentage ?? 0)
|
||||
if currentWidth >= minWidth, currentWidth <= maxWidth {
|
||||
widthConstraint?.constant = currentWidth
|
||||
widthConstraint?.activate()
|
||||
trailingLessThanEqualsConstraint?.activate()
|
||||
} else {
|
||||
super.updateContainerWidth()
|
||||
}
|
||||
}
|
||||
|
||||
override func updateRules() {
|
||||
super.updateRules()
|
||||
fieldType.handler().appendRules(self)
|
||||
@ -264,11 +339,20 @@ open class InputField: EntryFieldBase {
|
||||
get {
|
||||
var elements = [Any]()
|
||||
elements.append(contentsOf: [titleLabel, containerView])
|
||||
if showError {
|
||||
if let leftView = textField.leftView {
|
||||
elements.append(leftView)
|
||||
}
|
||||
|
||||
if !statusIcon.isHidden{
|
||||
elements.append(statusIcon)
|
||||
if let errorText, !errorText.isEmpty {
|
||||
elements.append(errorLabel)
|
||||
}
|
||||
}
|
||||
|
||||
if !actionTextLink.isHidden {
|
||||
elements.append(actionTextLink)
|
||||
}
|
||||
|
||||
if let errorText, !errorText.isEmpty, showError || hasInternalError {
|
||||
elements.append(errorLabel)
|
||||
} else if showSuccess, let successText, !successText.isEmpty {
|
||||
elements.append(successLabel)
|
||||
}
|
||||
@ -285,29 +369,33 @@ open class InputField: EntryFieldBase {
|
||||
}
|
||||
|
||||
extension InputField: UITextFieldDelegate {
|
||||
public func textFieldDidBeginEditing(_ textField: UITextField) {
|
||||
open func textFieldDidBeginEditing(_ textField: UITextField) {
|
||||
fieldType.handler().textFieldDidBeginEditing(self, textField: textField)
|
||||
updateContainerView(flag: true)
|
||||
updateErrorLabel()
|
||||
}
|
||||
|
||||
public func textFieldDidEndEditing(_ textField: UITextField) {
|
||||
open func textFieldDidEndEditing(_ textField: UITextField) {
|
||||
fieldType.handler().textFieldDidEndEditing(self, textField: textField)
|
||||
validate()
|
||||
UIAccessibility.post(notification: .layoutChanged, argument: self.containerView)
|
||||
}
|
||||
|
||||
public func textFieldDidChangeSelection(_ textField: UITextField) {
|
||||
open func textFieldDidChangeSelection(_ textField: UITextField) {
|
||||
fieldType.handler().textFieldDidChangeSelection(self, textField: textField)
|
||||
text = textField.text
|
||||
sendActions(for: .valueChanged)
|
||||
if fieldType.handler().validateOnChange {
|
||||
validate()
|
||||
}
|
||||
sendActions(for: .valueChanged)
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
||||
return fieldType.handler().textField(self, textField: textField, shouldChangeCharactersIn: range, replacementString: string)
|
||||
open func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
||||
let shouldChange = fieldType.handler().textField(self, textField: textField, shouldChangeCharactersIn: range, replacementString: string)
|
||||
if shouldChange {
|
||||
UIAccessibility.post(notification: .announcement, argument: string)
|
||||
}
|
||||
return shouldChange
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@ import UIKit
|
||||
import Combine
|
||||
import VDSCoreTokens
|
||||
|
||||
@objcMembers
|
||||
@objc(VDSTextField)
|
||||
open class TextField: UITextField, ViewProtocol, Errorable {
|
||||
|
||||
@ -47,6 +48,11 @@ open class TextField: UITextField, ViewProtocol, Errorable {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//--------------------------------------------------
|
||||
/// Set to true to hide the blinking textField cursor.
|
||||
open var hideBlinkingCaret = false
|
||||
open var enableClipboardActions: Bool = true
|
||||
open var onDidDeleteBackwards: (() -> Void)?
|
||||
|
||||
/// Key of whether or not updateView() is called in setNeedsUpdate()
|
||||
open var shouldUpdateView: Bool = true
|
||||
|
||||
@ -209,6 +215,23 @@ open class TextField: UITextField, ViewProtocol, Errorable {
|
||||
return success
|
||||
}
|
||||
|
||||
open override func caretRect(for position: UITextPosition) -> CGRect {
|
||||
|
||||
if hideBlinkingCaret {
|
||||
return .zero
|
||||
}
|
||||
|
||||
let caretRect = super.caretRect(for: position)
|
||||
return CGRect(origin: caretRect.origin, size: CGSize(width: 1, height: caretRect.height))
|
||||
}
|
||||
|
||||
open override func deleteBackward() {
|
||||
super.deleteBackward()
|
||||
onDidDeleteBackwards?()
|
||||
}
|
||||
|
||||
open override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { enableClipboardActions }
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Methods
|
||||
//--------------------------------------------------
|
||||
@ -236,7 +259,6 @@ open class TextField: UITextField, ViewProtocol, Errorable {
|
||||
//--------------------------------------------------
|
||||
open var accessibilityAction: ((TextField) -> Void)?
|
||||
|
||||
private var _isAccessibilityElement: Bool = false
|
||||
open override var isAccessibilityElement: Bool {
|
||||
get {
|
||||
var block: AXBoolReturnBlock?
|
||||
@ -252,15 +274,14 @@ open class TextField: UITextField, ViewProtocol, Errorable {
|
||||
if let block {
|
||||
return block()
|
||||
} else {
|
||||
return _isAccessibilityElement
|
||||
return super.isAccessibilityElement
|
||||
}
|
||||
}
|
||||
set {
|
||||
_isAccessibilityElement = newValue
|
||||
super.isAccessibilityElement = newValue
|
||||
}
|
||||
}
|
||||
|
||||
private var _accessibilityLabel: String?
|
||||
open override var accessibilityLabel: String? {
|
||||
get {
|
||||
var block: AXStringReturnBlock?
|
||||
@ -275,15 +296,14 @@ open class TextField: UITextField, ViewProtocol, Errorable {
|
||||
if let block {
|
||||
return block()
|
||||
} else {
|
||||
return _accessibilityLabel
|
||||
return super.accessibilityLabel
|
||||
}
|
||||
}
|
||||
set {
|
||||
_accessibilityLabel = newValue
|
||||
super.accessibilityLabel = newValue
|
||||
}
|
||||
}
|
||||
|
||||
private var _accessibilityHint: String?
|
||||
open override var accessibilityHint: String? {
|
||||
get {
|
||||
var block: AXStringReturnBlock?
|
||||
@ -298,15 +318,14 @@ open class TextField: UITextField, ViewProtocol, Errorable {
|
||||
if let block {
|
||||
return block()
|
||||
} else {
|
||||
return _accessibilityHint
|
||||
return super.accessibilityHint
|
||||
}
|
||||
}
|
||||
set {
|
||||
_accessibilityHint = newValue
|
||||
super.accessibilityHint = newValue
|
||||
}
|
||||
}
|
||||
|
||||
private var _accessibilityValue: String?
|
||||
open override var accessibilityValue: String? {
|
||||
get {
|
||||
var block: AXStringReturnBlock?
|
||||
@ -322,11 +341,11 @@ open class TextField: UITextField, ViewProtocol, Errorable {
|
||||
if let block{
|
||||
return block()
|
||||
} else {
|
||||
return _accessibilityValue
|
||||
return super.accessibilityValue
|
||||
}
|
||||
}
|
||||
set {
|
||||
_accessibilityValue = newValue
|
||||
super.accessibilityValue = newValue
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ import Combine
|
||||
|
||||
/// A text area is an input wherein a customer enters long-form information.
|
||||
/// Use a text area when you want customers to enter text that’s longer than a single line.
|
||||
@objcMembers
|
||||
@objc(VDSTextArea)
|
||||
open class TextArea: EntryFieldBase {
|
||||
//--------------------------------------------------
|
||||
@ -56,18 +57,7 @@ open class TextArea: EntryFieldBase {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
/// Override UIControl state to add the .error state if showSuccess is true and if showError is true.
|
||||
open override var state: UIControl.State {
|
||||
get {
|
||||
var state = super.state
|
||||
if textView.isFirstResponder {
|
||||
state.insert(.focused)
|
||||
}
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
override var containerSize: CGSize { CGSize(width: 182, height: Height.twoX.value) }
|
||||
|
||||
/// Enum used to describe the the height of TextArea.
|
||||
@ -112,6 +102,7 @@ open class TextArea: EntryFieldBase {
|
||||
$0.isScrollEnabled = true
|
||||
$0.textContainerInset = .zero
|
||||
$0.autocorrectionType = .no
|
||||
$0.spellCheckingType = .no
|
||||
$0.textContainer.lineFragmentPadding = 0
|
||||
}
|
||||
|
||||
@ -121,7 +112,11 @@ open class TextArea: EntryFieldBase {
|
||||
}
|
||||
|
||||
didSet {
|
||||
validate()
|
||||
setNeedsUpdate()
|
||||
|
||||
if textView.isFirstResponder {
|
||||
validate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -143,6 +138,7 @@ open class TextArea: EntryFieldBase {
|
||||
super.setup()
|
||||
|
||||
accessibilityHintText = "Double tap to edit"
|
||||
textView.delegate = self
|
||||
|
||||
//events
|
||||
textView
|
||||
@ -198,8 +194,9 @@ open class TextArea: EntryFieldBase {
|
||||
|
||||
override func updateRules() {
|
||||
super.updateRules()
|
||||
|
||||
rules.append(.init(countRule))
|
||||
if let maxLength, maxLength > 0 {
|
||||
rules.append(.init(countRule))
|
||||
}
|
||||
}
|
||||
|
||||
open override func getFieldContainer() -> UIView {
|
||||
@ -237,7 +234,7 @@ open class TextArea: EntryFieldBase {
|
||||
}
|
||||
}
|
||||
|
||||
func textViewDidChange(_ textView: UITextView) {
|
||||
public func textViewDidChange(_ textView: UITextView) {
|
||||
|
||||
//dynamic textView Height sizing based on Figma
|
||||
//if you want it to work "as-is" delete this code
|
||||
@ -299,3 +296,10 @@ open class TextArea: EntryFieldBase {
|
||||
//--------------------------------------------------
|
||||
var countRule = CharacterCountRule()
|
||||
}
|
||||
|
||||
extension TextArea: UITextViewDelegate {
|
||||
public func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
|
||||
UIAccessibility.post(notification: .announcement, argument: text)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ import UIKit
|
||||
import Combine
|
||||
import VDSCoreTokens
|
||||
|
||||
@objcMembers
|
||||
@objc(VDSTextView)
|
||||
open class TextView: UITextView, ViewProtocol, Errorable {
|
||||
|
||||
@ -41,10 +42,19 @@ open class TextView: UITextView, ViewProtocol, Errorable {
|
||||
// MARK: - Private Properties
|
||||
//--------------------------------------------------
|
||||
private var initialSetupPerformed = false
|
||||
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Properties
|
||||
//--------------------------------------------------
|
||||
open var placeholder: String? { didSet { setNeedsUpdate() } }
|
||||
|
||||
open var placeholderLabel = Label().with {
|
||||
$0.textColorConfiguration = ViewColorConfiguration().with {
|
||||
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forDisabled: true)
|
||||
$0.setSurfaceColors(VDSColor.elementsSecondaryOnlight, VDSColor.elementsSecondaryOndark, forDisabled: false)
|
||||
}.eraseToAnyColorable()
|
||||
}
|
||||
|
||||
/// Key of whether or not updateView() is called in setNeedsUpdate()
|
||||
open var shouldUpdateView: Bool = true
|
||||
|
||||
@ -88,6 +98,7 @@ open class TextView: UITextView, ViewProtocol, Errorable {
|
||||
if textAlignment != oldValue {
|
||||
// Text alignment can be part of our paragraph style, so we may need to
|
||||
// re-style when changed
|
||||
placeholderLabel.textAlignment = textAlignment
|
||||
updateLabel()
|
||||
}
|
||||
}
|
||||
@ -118,6 +129,9 @@ open class TextView: UITextView, ViewProtocol, Errorable {
|
||||
done.pinCenterY()
|
||||
.pinTrailing(16)
|
||||
inputAccessoryView = accessView
|
||||
|
||||
addSubview(placeholderLabel)
|
||||
placeholderLabel.pinToSuperView()
|
||||
}
|
||||
|
||||
@objc func doneButtonAction() {
|
||||
@ -145,13 +159,16 @@ open class TextView: UITextView, ViewProtocol, Errorable {
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
|
||||
open override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
placeholderLabel.preferredMaxLayoutWidth = textContainer.size.width - textContainer.lineFragmentPadding * 2
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Accessibility
|
||||
//--------------------------------------------------
|
||||
open var accessibilityAction: ((TextView) -> Void)?
|
||||
|
||||
private var _isAccessibilityElement: Bool = false
|
||||
open override var isAccessibilityElement: Bool {
|
||||
get {
|
||||
var block: AXBoolReturnBlock?
|
||||
@ -167,15 +184,14 @@ open class TextView: UITextView, ViewProtocol, Errorable {
|
||||
if let block {
|
||||
return block()
|
||||
} else {
|
||||
return _isAccessibilityElement
|
||||
return super.isAccessibilityElement
|
||||
}
|
||||
}
|
||||
set {
|
||||
_isAccessibilityElement = newValue
|
||||
super.isAccessibilityElement = newValue
|
||||
}
|
||||
}
|
||||
|
||||
private var _accessibilityLabel: String?
|
||||
open override var accessibilityLabel: String? {
|
||||
get {
|
||||
var block: AXStringReturnBlock?
|
||||
@ -190,15 +206,14 @@ open class TextView: UITextView, ViewProtocol, Errorable {
|
||||
if let block {
|
||||
return block()
|
||||
} else {
|
||||
return _accessibilityLabel
|
||||
return super.accessibilityLabel
|
||||
}
|
||||
}
|
||||
set {
|
||||
_accessibilityLabel = newValue
|
||||
super.accessibilityLabel = newValue
|
||||
}
|
||||
}
|
||||
|
||||
private var _accessibilityHint: String?
|
||||
open override var accessibilityHint: String? {
|
||||
get {
|
||||
var block: AXStringReturnBlock?
|
||||
@ -213,15 +228,14 @@ open class TextView: UITextView, ViewProtocol, Errorable {
|
||||
if let block {
|
||||
return block()
|
||||
} else {
|
||||
return _accessibilityHint
|
||||
return super.accessibilityHint
|
||||
}
|
||||
}
|
||||
set {
|
||||
_accessibilityHint = newValue
|
||||
super.accessibilityHint = newValue
|
||||
}
|
||||
}
|
||||
|
||||
private var _accessibilityValue: String?
|
||||
open override var accessibilityValue: String? {
|
||||
get {
|
||||
var block: AXStringReturnBlock?
|
||||
@ -237,11 +251,11 @@ open class TextView: UITextView, ViewProtocol, Errorable {
|
||||
if let block{
|
||||
return block()
|
||||
} else {
|
||||
return _accessibilityValue
|
||||
return super.accessibilityValue
|
||||
}
|
||||
}
|
||||
set {
|
||||
_accessibilityValue = newValue
|
||||
super.accessibilityValue = newValue
|
||||
}
|
||||
}
|
||||
|
||||
@ -301,6 +315,10 @@ open class TextView: UITextView, ViewProtocol, Errorable {
|
||||
} else {
|
||||
attributedText = nil
|
||||
}
|
||||
placeholderLabel.textStyle = textStyle
|
||||
placeholderLabel.surface = surface
|
||||
placeholderLabel.text = placeholder
|
||||
placeholderLabel.isHidden = !text.isEmpty
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ import VDSCoreTokens
|
||||
import UIKit
|
||||
import Combine
|
||||
|
||||
@objcMembers
|
||||
@objc(VDSTileContainer)
|
||||
open class TileContainer: TileContainerBase<TileContainer.Padding> {
|
||||
|
||||
@ -44,6 +45,7 @@ open class TileContainer: TileContainerBase<TileContainer.Padding> {
|
||||
}
|
||||
|
||||
open class TileContainerBase<PaddingType: DefaultValuing>: Control where PaddingType.ValueType == CGFloat {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
@ -73,7 +75,7 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
case custom(UIColor)
|
||||
|
||||
private var reflectedValue: String { String(reflecting: self) }
|
||||
|
||||
|
||||
public static func == (lhs: Self, rhs: Self) -> Bool {
|
||||
lhs.reflectedValue == rhs.reflectedValue
|
||||
}
|
||||
@ -85,7 +87,7 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
case gradient(UIColor, UIColor)
|
||||
case none
|
||||
}
|
||||
|
||||
|
||||
/// Enum used to describe the aspect ratios used for this component.
|
||||
public enum AspectRatio: String, CaseIterable {
|
||||
case ratio1x1 = "1:1"
|
||||
@ -108,9 +110,14 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
$0.contentMode = .scaleAspectFill
|
||||
$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
|
||||
//--------------------------------------------------
|
||||
@ -119,27 +126,27 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
|
||||
/// This is the container in which views will be pinned.
|
||||
open var contentView = View()
|
||||
|
||||
|
||||
/// This is the view used to show the high light color for a onClick.
|
||||
open var highlightView = View().with {
|
||||
$0.isUserInteractionEnabled = false
|
||||
}
|
||||
|
||||
|
||||
/// This controls the aspect ratio for the component.
|
||||
open var aspectRatio: AspectRatio = .ratio1x1 { didSet { setNeedsUpdate() } }
|
||||
|
||||
|
||||
/// Sets the background color for the component.
|
||||
open var color: BackgroundColor? { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// Sets the background effect for the component.
|
||||
open var backgroundEffect: BackgroundEffect = .none { didSet { setNeedsUpdate() } }
|
||||
|
||||
|
||||
/// Sets the inside padding for the component
|
||||
open var padding: PaddingType = PaddingType.defaultValue { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// Applies a background color if backgroundImage prop fails or has trouble loading.
|
||||
open var imageFallbackColor: Surface = .light { didSet { setNeedsUpdate() } }
|
||||
|
||||
|
||||
private var _width: CGFloat?
|
||||
/// Sets the width for the component. Accepts a pixel value.
|
||||
open var width: CGFloat? {
|
||||
@ -153,7 +160,7 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var _height: CGFloat?
|
||||
/// Sets the height for the component. Accepts a pixel value.
|
||||
open var height: CGFloat? {
|
||||
@ -173,18 +180,14 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
|
||||
/// Determines if there is a drop shadow or not.
|
||||
open var showDropShadow: Bool = false { didSet { setNeedsUpdate() } }
|
||||
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Constraints
|
||||
//--------------------------------------------------
|
||||
internal var widthConstraint: NSLayoutConstraint?
|
||||
internal var heightConstraint: NSLayoutConstraint?
|
||||
internal var heightGreaterThanConstraint: NSLayoutConstraint?
|
||||
internal var containerTopConstraint: NSLayoutConstraint?
|
||||
internal var containerBottomConstraint: NSLayoutConstraint?
|
||||
internal var containerLeadingConstraint: NSLayoutConstraint?
|
||||
internal var containerTrailingConstraint: NSLayoutConstraint?
|
||||
|
||||
internal var aspectRatioConstraint: NSLayoutConstraint?
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Configuration
|
||||
//--------------------------------------------------
|
||||
@ -222,29 +225,21 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
super.setup()
|
||||
isAccessibilityElement = false
|
||||
|
||||
let layoutGuide = UILayoutGuide()
|
||||
addLayoutGuide(layoutGuide)
|
||||
layoutGuide
|
||||
.pinTop()
|
||||
.pinLeading()
|
||||
.pinTrailing(0, .defaultHigh)
|
||||
.pinBottom(0, .defaultHigh)
|
||||
addSubview(backgroundImageView)
|
||||
addSubview(containerView)
|
||||
containerView.addSubview(contentView)
|
||||
addSubview(highlightView)
|
||||
|
||||
containerView.pinToSuperView()
|
||||
|
||||
widthConstraint = layoutGuide.widthAnchor.constraint(equalToConstant: 0)
|
||||
|
||||
heightGreaterThanConstraint = layoutGuide.heightAnchor.constraint(greaterThanOrEqualToConstant: 44.0)
|
||||
heightGreaterThanConstraint?.isActive = false
|
||||
|
||||
heightConstraint = layoutGuide.heightAnchor.constraint(equalToConstant: 0)
|
||||
|
||||
containerView.addSubview(backgroundImageView)
|
||||
backgroundImageView.pinToSuperView()
|
||||
|
||||
containerView.addSubview(contentView)
|
||||
contentView.pinToSuperView()
|
||||
|
||||
containerView.addSubview(highlightView)
|
||||
highlightView.pinToSuperView()
|
||||
|
||||
widthConstraint = widthAnchor.constraint(equalToConstant: 0).deactivate()
|
||||
heightConstraint = heightAnchor.constraint(equalToConstant: 0).deactivate()
|
||||
|
||||
backgroundImageView.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||
backgroundImageView.setContentHuggingPriority(.defaultLow, for: .vertical)
|
||||
backgroundImageView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
@ -252,25 +247,28 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
backgroundImageView.isUserInteractionEnabled = false
|
||||
backgroundImageView.isHidden = true
|
||||
|
||||
containerTopConstraint = contentView.pinTop(anchor: layoutGuide.topAnchor, constant: padding.value)
|
||||
containerBottomConstraint = layoutGuide.pinBottom(anchor: contentView.bottomAnchor, constant: padding.value)
|
||||
containerLeadingConstraint = contentView.pinLeading(anchor: layoutGuide.leadingAnchor, constant: padding.value)
|
||||
containerTrailingConstraint = layoutGuide.pinTrailing(anchor: contentView.trailingAnchor, constant: padding.value)
|
||||
|
||||
highlightView.pin(layoutGuide)
|
||||
highlightView.isHidden = true
|
||||
highlightView.backgroundColor = .clear
|
||||
|
||||
//corner radius
|
||||
layer.cornerRadius = cornerRadius
|
||||
containerView.layer.cornerRadius = cornerRadius
|
||||
backgroundImageView.layer.cornerRadius = cornerRadius
|
||||
highlightView.layer.cornerRadius = cornerRadius
|
||||
clipsToBounds = true
|
||||
containerView.clipsToBounds = true
|
||||
|
||||
containerView.bridge_isAccessibilityElementBlock = { [weak self] in self?.onClickSubscriber != nil }
|
||||
containerView.accessibilityHint = "Double tap to open."
|
||||
containerView.accessibilityLabel = nil
|
||||
|
||||
|
||||
NotificationCenter.default
|
||||
.publisher(for: UIDevice.orientationDidChangeNotification)
|
||||
.sink() { [weak self] _ in
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.001) { [weak self] in
|
||||
guard let self else { return }
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}.store(in: &subscribers)
|
||||
|
||||
}
|
||||
|
||||
/// Overriden to take the hit if there is an onClickSubscriber and the view is not a UIControl
|
||||
@ -286,7 +284,7 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
super.reset()
|
||||
shouldUpdateView = false
|
||||
color = .white
|
||||
aspectRatio = .ratio1x1
|
||||
aspectRatio = .none
|
||||
imageFallbackColor = .light
|
||||
width = nil
|
||||
height = nil
|
||||
@ -295,7 +293,7 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
open override func updateView() {
|
||||
super.updateView()
|
||||
@ -303,44 +301,14 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
highlightView.backgroundColor = hightLightViewColorConfiguration.getColor(self)
|
||||
highlightView.isHidden = !isHighlighted
|
||||
|
||||
layer.borderColor = borderColorConfiguration.getColor(self).cgColor
|
||||
layer.borderWidth = showBorder ? VDSFormControls.borderWidth : 0
|
||||
|
||||
containerTopConstraint?.constant = padding.value
|
||||
containerLeadingConstraint?.constant = padding.value
|
||||
containerBottomConstraint?.constant = padding.value
|
||||
containerTrailingConstraint?.constant = padding.value
|
||||
|
||||
if let width, aspectRatio == .none && height == nil{
|
||||
widthConstraint?.constant = width
|
||||
widthConstraint?.isActive = true
|
||||
heightConstraint?.isActive = false
|
||||
heightGreaterThanConstraint?.isActive = true
|
||||
} else if let height, let width {
|
||||
widthConstraint?.constant = width
|
||||
heightConstraint?.constant = height
|
||||
heightConstraint?.isActive = true
|
||||
widthConstraint?.isActive = true
|
||||
heightGreaterThanConstraint?.isActive = false
|
||||
} else if let width {
|
||||
let size = ratioSize(for: width)
|
||||
widthConstraint?.constant = size.width
|
||||
heightConstraint?.constant = size.height
|
||||
widthConstraint?.isActive = true
|
||||
heightConstraint?.isActive = true
|
||||
heightGreaterThanConstraint?.isActive = false
|
||||
} else {
|
||||
widthConstraint?.isActive = false
|
||||
heightConstraint?.isActive = false
|
||||
}
|
||||
|
||||
applyBackgroundEffects()
|
||||
containerView.layer.borderColor = borderColorConfiguration.getColor(self).cgColor
|
||||
containerView.layer.borderWidth = showBorder ? VDSFormControls.borderWidth : 0
|
||||
|
||||
contentView.removeConstraints()
|
||||
contentView.pinToSuperView(.uniform(padding.value))
|
||||
|
||||
updateContainerView()
|
||||
|
||||
if showDropShadow, surface == .light {
|
||||
addDropShadow(dropShadowConfiguration)
|
||||
} else {
|
||||
removeDropShadows()
|
||||
}
|
||||
}
|
||||
|
||||
open override var accessibilityElements: [Any]? {
|
||||
@ -363,23 +331,16 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
|
||||
//append all children that are accessible
|
||||
items.append(contentsOf: elements)
|
||||
|
||||
|
||||
return items
|
||||
}
|
||||
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
|
||||
//--------------------------------------------------
|
||||
|
||||
|
||||
/// This will place a view within the contentView of this component.
|
||||
public func addContentView(_ view: UIView, shouldPin: Bool = true) {
|
||||
view.removeFromSuperview()
|
||||
@ -388,7 +349,7 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
view.pinToSuperView()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Methods
|
||||
//--------------------------------------------------
|
||||
@ -400,58 +361,134 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
switch backgroundEffect {
|
||||
case .transparency:
|
||||
alphaConfiguration = 0.8
|
||||
removeGradientLayer()
|
||||
containerView.removeGradientLayer()
|
||||
case .gradient(let firstColor, let secondColor):
|
||||
alphaConfiguration = 1.0
|
||||
addGradientLayer(with: firstColor, secondColor: secondColor)
|
||||
containerView.addGradientLayer(with: firstColor, secondColor: secondColor)
|
||||
backgroundImageView.isHidden = true
|
||||
backgroundImageView.alpha = 1.0
|
||||
case .none:
|
||||
alphaConfiguration = 1.0
|
||||
removeGradientLayer()
|
||||
containerView.removeGradientLayer()
|
||||
}
|
||||
if let backgroundImage {
|
||||
backgroundImageView.image = backgroundImage
|
||||
backgroundImageView.isHidden = false
|
||||
backgroundImageView.alpha = alphaConfiguration
|
||||
backgroundColor = imageFallbackColor.withAlphaComponent(alphaConfiguration)
|
||||
containerView.backgroundColor = imageFallbackColor.withAlphaComponent(alphaConfiguration)
|
||||
} else {
|
||||
backgroundImageView.isHidden = true
|
||||
backgroundImageView.alpha = 1.0
|
||||
backgroundColor = color.withAlphaComponent(alphaConfiguration)
|
||||
containerView.backgroundColor = color.withAlphaComponent(alphaConfiguration)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateContainerView() {
|
||||
applyBackgroundEffects()
|
||||
|
||||
if showDropShadow, surface == .light {
|
||||
containerView.addDropShadow(dropShadowConfiguration)
|
||||
} else {
|
||||
containerView.removeDropShadows()
|
||||
}
|
||||
|
||||
containerView.dropShadowLayers?.forEach { $0.frame = containerView.bounds }
|
||||
containerView.gradientLayers?.forEach { $0.frame = containerView.bounds }
|
||||
|
||||
//sizing the container with constraints
|
||||
|
||||
//Set local vars
|
||||
var containerViewWidth: CGFloat? = width
|
||||
let containerViewHeight: CGFloat? = height
|
||||
let multiplier = aspectRatio.multiplier
|
||||
|
||||
//turn off the constraints
|
||||
aspectRatioConstraint?.deactivate()
|
||||
widthConstraint?.deactivate()
|
||||
heightConstraint?.deactivate()
|
||||
|
||||
//-------------------------------------------------------------------------
|
||||
//Overriding Nil Width Rules
|
||||
//-------------------------------------------------------------------------
|
||||
//Rule 1:
|
||||
//In the scenario where we only have a height but the multiplie is nil, we
|
||||
//want to set the width with the parent's width which will more or less "fill"
|
||||
//the container horizontally
|
||||
//- height is set
|
||||
//- width is not set
|
||||
//- aspectRatio is not set
|
||||
if let superviewWidth, superviewWidth > 0,
|
||||
containerViewHeight != nil,
|
||||
containerViewWidth == nil,
|
||||
multiplier == nil {
|
||||
containerViewWidth = superviewWidth
|
||||
}
|
||||
|
||||
//Rule 2:
|
||||
//In the scenario where no width and height is set, want to set the width with the
|
||||
//parent's width which will more or less "fill" the container horizontally
|
||||
//- height is not set
|
||||
//- width is not set
|
||||
else if let superviewWidth, superviewWidth > 0,
|
||||
containerViewWidth == nil,
|
||||
containerViewHeight == nil {
|
||||
containerViewWidth = superviewWidth
|
||||
}
|
||||
//-------------------------------------------------------------------------
|
||||
|
||||
|
||||
//-------------------------------------------------------------------------
|
||||
//Width + AspectRatio Constraint
|
||||
//-------------------------------------------------------------------------
|
||||
if let containerViewWidth,
|
||||
let multiplier,
|
||||
containerViewWidth > 0,
|
||||
containerViewHeight == nil {
|
||||
widthConstraint?.constant = containerViewWidth
|
||||
widthConstraint?.activate()
|
||||
aspectRatioConstraint = heightAnchor.constraint(equalTo: widthAnchor, multiplier: multiplier)
|
||||
aspectRatioConstraint?.activate()
|
||||
|
||||
}
|
||||
//-------------------------------------------------------------------------
|
||||
//Height + AspectRatio Constraint
|
||||
//-------------------------------------------------------------------------
|
||||
else if let containerViewHeight,
|
||||
let multiplier,
|
||||
containerViewHeight > 0,
|
||||
containerViewWidth == nil {
|
||||
heightConstraint?.constant = containerViewHeight
|
||||
heightConstraint?.activate()
|
||||
aspectRatioConstraint = widthAnchor.constraint(equalTo: heightAnchor, multiplier: multiplier)
|
||||
aspectRatioConstraint?.activate()
|
||||
|
||||
} else {
|
||||
|
||||
//-------------------------------------------------------------------------
|
||||
//Width Constraint
|
||||
//-------------------------------------------------------------------------
|
||||
if let containerViewWidth,
|
||||
containerViewWidth > 0 {
|
||||
widthConstraint?.constant = containerViewWidth
|
||||
widthConstraint?.activate()
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------------
|
||||
//Height Constraint
|
||||
//-------------------------------------------------------------------------
|
||||
if let containerViewHeight,
|
||||
containerViewHeight > 0 {
|
||||
heightConstraint?.constant = containerViewHeight
|
||||
heightConstraint?.activate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func ratioSize(for width: CGFloat) -> CGSize {
|
||||
var height: CGFloat = width
|
||||
|
||||
switch aspectRatio {
|
||||
case .ratio1x1:
|
||||
break;
|
||||
case .ratio3x4:
|
||||
height = (4 / 3) * width
|
||||
case .ratio4x3:
|
||||
height = (3 / 4) * width
|
||||
case .ratio2x3:
|
||||
height = (3 / 2) * width
|
||||
case .ratio3x2:
|
||||
height = (2 / 3) * width
|
||||
case .ratio9x16:
|
||||
height = (16 / 9) * width
|
||||
case .ratio16x9:
|
||||
height = (9 / 16) * width
|
||||
case .ratio1x2:
|
||||
height = (2 / 1) * width
|
||||
case .ratio2x1:
|
||||
height = (1 / 2) * width
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
return CGSize(width: width, height: height)
|
||||
/// This is the size of the superview's allowed space for this container first by constrained size which would include padding/inset values an
|
||||
private var superviewWidth: CGFloat? {
|
||||
horizontalPinnedWidth() ?? superview?.frame.size.width
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
extension TileContainerBase {
|
||||
@ -491,3 +528,30 @@ extension TileContainerBase {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TileContainerBase.AspectRatio {
|
||||
var multiplier: CGFloat? {
|
||||
switch self {
|
||||
case .ratio1x1:
|
||||
return 1
|
||||
case .ratio3x4:
|
||||
return 4 / 3
|
||||
case .ratio4x3:
|
||||
return 3 / 4
|
||||
case .ratio2x3:
|
||||
return 3 / 2
|
||||
case .ratio3x2:
|
||||
return 2 / 3
|
||||
case .ratio9x16:
|
||||
return 16 / 9
|
||||
case .ratio16x9:
|
||||
return 9 / 16
|
||||
case .ratio1x2:
|
||||
return 2 / 1
|
||||
case .ratio2x1:
|
||||
return 1 / 2
|
||||
case .none:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@ import Combine
|
||||
/// support quick scanning and engagement. A Tilelet is fully clickable and
|
||||
/// while it can include an arrow CTA, it does not require one in order to
|
||||
/// function.
|
||||
@objcMembers
|
||||
@objc(VDSTilelet)
|
||||
open class Tilelet: TileContainerBase<Tilelet.Padding> {
|
||||
|
||||
@ -205,15 +206,10 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
|
||||
}
|
||||
|
||||
/// Descriptive Icon positioned in the contentView.
|
||||
open var descriptiveIcon = Icon().with {
|
||||
$0.isAccessibilityElement = false
|
||||
}
|
||||
open var descriptiveIcon = Icon()
|
||||
|
||||
/// Directional Icon positioned in the contentView.
|
||||
open var directionalIcon = Icon().with {
|
||||
$0.isAccessibilityElement = false
|
||||
$0.name = .rightArrow
|
||||
}
|
||||
open var directionalIcon = Icon()
|
||||
|
||||
private var _textWidth: TextWidth?
|
||||
|
||||
@ -302,8 +298,9 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
|
||||
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
aspectRatio = .none
|
||||
color = .black
|
||||
aspectRatio = .none
|
||||
|
||||
addContentView(stackView)
|
||||
|
||||
//badge
|
||||
@ -381,11 +378,22 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
|
||||
titleLockupSubTitleLabelHeightGreaterThanConstraint = titleLockup.subTitleLabel.heightGreaterThanEqualTo(constant: titleLockup.subTitleLabel.minimumLineHeight)
|
||||
titleLockupSubTitleLabelHeightGreaterThanConstraint?.priority = .defaultHigh
|
||||
titleLockupSubTitleLabelHeightGreaterThanConstraint?.activate()
|
||||
|
||||
directionalIcon.bridge_accessibilityLabelBlock = { [weak self] in
|
||||
guard let self, let directionalIconModel else { return nil }
|
||||
return directionalIconModel.accessibleText
|
||||
}
|
||||
|
||||
descriptiveIcon.bridge_accessibilityLabelBlock = { [weak self] in
|
||||
guard let self, let descriptiveIconModel else { return nil }
|
||||
return descriptiveIconModel.accessibleText
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
shouldUpdateView = false
|
||||
super.reset()
|
||||
aspectRatio = .none
|
||||
color = .black
|
||||
//models
|
||||
@ -405,18 +413,14 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
|
||||
updateBadge()
|
||||
updateTitleLockup()
|
||||
updateIcons()
|
||||
///Content-driven height Tilelets - Minimum height is configurable.
|
||||
///if width != nil && (aspectRatio != .none || height != nil) then tilelet is not self growing, so we can apply text position alignments.
|
||||
if width != nil && (aspectRatio != .none || height != nil) {
|
||||
updateTextPositionAlignment()
|
||||
}
|
||||
updateTextPositionAlignment()
|
||||
setNeedsLayout()
|
||||
}
|
||||
|
||||
/// Used to update any Accessibility properties.
|
||||
open override var accessibilityElements: [Any]? {
|
||||
get {
|
||||
var views = [UIView]()
|
||||
var views = [AnyObject]()
|
||||
|
||||
// grab the available views in order
|
||||
if badgeModel != nil {
|
||||
@ -424,7 +428,15 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
|
||||
}
|
||||
|
||||
if titleModel != nil || subTitleModel != nil || eyebrowModel != nil {
|
||||
views.append(titleLockup)
|
||||
let titleLockupViews = gatherAccessibilityElements(from: titleLockup)
|
||||
views.append(contentsOf: titleLockupViews)
|
||||
}
|
||||
|
||||
if descriptiveIconModel != nil {
|
||||
views.append(descriptiveIcon)
|
||||
|
||||
} else if directionalIconModel != nil {
|
||||
views.append(directionalIcon)
|
||||
}
|
||||
|
||||
containerView.setAccessibilityLabel(for: views)
|
||||
@ -584,6 +596,7 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
|
||||
}
|
||||
|
||||
private func updateTextPositionAlignment() {
|
||||
guard width != nil && (aspectRatio != .none || height != nil) else { return }
|
||||
switch textPostion {
|
||||
case .top:
|
||||
titleLockupTopConstraint?.activate()
|
||||
|
||||
@ -66,6 +66,10 @@ extension Tilelet {
|
||||
public var iconName: Icon.Name {
|
||||
return self == .rightArrow ? .rightArrow : .externalLink
|
||||
}
|
||||
|
||||
public var accessibilityLabel: String {
|
||||
self == .rightArrow ? "Directional right arrow" : "External link"
|
||||
}
|
||||
}
|
||||
|
||||
public enum IconSize: String, EnumSubset {
|
||||
@ -80,7 +84,7 @@ extension Tilelet {
|
||||
public var iconColor: IconColor?
|
||||
|
||||
/// Accessible Text for the Icon
|
||||
public var accessibleText: String
|
||||
public var accessibleText: String?
|
||||
|
||||
/// Enum for a icon type you want shown..
|
||||
public var iconType: IconType
|
||||
@ -95,7 +99,7 @@ extension Tilelet {
|
||||
|
||||
self.iconType = iconType
|
||||
self.iconColor = iconColor
|
||||
self.accessibleText = accessibleText ?? iconType.iconName.rawValue
|
||||
self.accessibleText = accessibleText ?? iconType.accessibilityLabel
|
||||
self.size = size
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,6 +12,7 @@ import Combine
|
||||
|
||||
/// Title Lockup ensures the readability of words on the screen
|
||||
/// with approved built in text size configurations.
|
||||
@objcMembers
|
||||
@objc(VDSTitleLockup)
|
||||
open class TitleLockup: View {
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ import Combine
|
||||
|
||||
/// A toggle is a control that lets customers instantly turn on
|
||||
/// or turn off a single option, setting or function.
|
||||
@objcMembers
|
||||
@objc(VDSToggle)
|
||||
open class Toggle: Control, Changeable, FormFieldable {
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ import Combine
|
||||
|
||||
/// A toggle is a control that lets customers instantly turn on
|
||||
/// or turn off a single option, setting or function.
|
||||
@objcMembers
|
||||
@objc(VDSToggleView)
|
||||
open class ToggleView: Control, Changeable, FormFieldable {
|
||||
|
||||
@ -219,7 +220,7 @@ open class ToggleView: Control, Changeable, FormFieldable {
|
||||
}
|
||||
knobTrailingConstraint?.isActive = true
|
||||
knobLeadingConstraint?.isActive = true
|
||||
setNeedsLayout()
|
||||
layoutIfNeeded()
|
||||
}
|
||||
|
||||
private func updateToggle() {
|
||||
|
||||
@ -13,6 +13,7 @@ import Combine
|
||||
/// A tooltip is an overlay that clarifies another component or content
|
||||
/// element. It is triggered when a customer hovers, clicks or taps
|
||||
/// the tooltip icon.
|
||||
@objcMembers
|
||||
@objc(VDSTooltip)
|
||||
open class Tooltip: Control, TooltipLaunchable {
|
||||
|
||||
|
||||
@ -10,6 +10,8 @@ import UIKit
|
||||
import Combine
|
||||
import VDSCoreTokens
|
||||
|
||||
@objcMembers
|
||||
@objc(VDSTooltipAlertViewController)
|
||||
open class TooltipAlertViewController: UIViewController, Surfaceable {
|
||||
|
||||
/// Set of Subscribers for any Publishers for this Control.
|
||||
|
||||
@ -9,6 +9,8 @@ import Foundation
|
||||
import UIKit
|
||||
import VDSCoreTokens
|
||||
|
||||
@objcMembers
|
||||
@objc(VDSTooltipDialog)
|
||||
open class TooltipDialog: View, UIScrollViewDelegate {
|
||||
|
||||
//--------------------------------------------------
|
||||
|
||||
@ -12,23 +12,11 @@ extension UITapGestureRecognizer {
|
||||
|
||||
/// Determines if the touch event has a action attribute within the range given
|
||||
/// - Parameters:
|
||||
/// - label: UILabel in question
|
||||
/// - label: Label in question
|
||||
/// - targetRange: Range to look within
|
||||
/// - Returns: Wether the range in the label has an action
|
||||
public func didTapActionInLabel(_ label: UILabel, inRange targetRange: NSRange) -> Bool {
|
||||
|
||||
guard let attributedText = label.attributedText else { return false }
|
||||
|
||||
let layoutManager = NSLayoutManager()
|
||||
let textContainer = NSTextContainer(size: label.bounds.size)
|
||||
let textStorage = NSTextStorage(attributedString: attributedText)
|
||||
layoutManager.addTextContainer(textContainer)
|
||||
textStorage.addLayoutManager(layoutManager)
|
||||
|
||||
let location = location(in: label)
|
||||
let characterIndex = layoutManager.characterIndex(for: location, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
|
||||
|
||||
guard let _ = attributedText.attribute(NSAttributedString.Key.action, at: characterIndex, effectiveRange: nil) as? String, characterIndex < attributedText.length else { return false }
|
||||
return true
|
||||
public func didTapActionInLabel(_ label: Label, inRange targetRange: NSRange) -> Bool {
|
||||
let tapLocation = location(in: label)
|
||||
return label.isAction(for: tapLocation, inRange: targetRange)
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,14 +14,14 @@ extension UIView {
|
||||
/// - views: Array of Views that you want to join the accessibilityLabel.
|
||||
/// - separator: Separator used between the accessibilityLabel for each UIView.
|
||||
/// - Returns: Joined String.
|
||||
public func combineAccessibilityLabel(for views: [UIView], separator: String = ", ") -> String? {
|
||||
let labels = views.map({($0.accessibilityLabel?.isEmpty ?? true) ? nil : $0.accessibilityLabel}).compactMap({$0})
|
||||
public func combineAccessibilityLabel(for views: [AnyObject], separator: String = ", ") -> String? {
|
||||
let labels: [String] = views.map({($0.accessibilityLabel?.isEmpty ?? true) ? nil : $0.accessibilityLabel}).compactMap({$0})
|
||||
return labels.joined(separator: separator)
|
||||
}
|
||||
|
||||
/// AccessibilityLabel helper for joining the accessibilityLabel property of all views passed in.
|
||||
/// - Parameter views: Array of Views that you want to join the accessibilityLabel.
|
||||
public func setAccessibilityLabel(for views: [UIView]) {
|
||||
public func setAccessibilityLabel(for views: [AnyObject]) {
|
||||
accessibilityLabel = combineAccessibilityLabel(for: views)
|
||||
}
|
||||
|
||||
@ -50,8 +50,8 @@ extension UIView {
|
||||
return isIntersecting
|
||||
}
|
||||
|
||||
public func gatherAccessibilityElements(from view: UIView) -> [Any] {
|
||||
var elements: [Any] = []
|
||||
public func gatherAccessibilityElements(from view: AnyObject) -> [AnyObject] {
|
||||
var elements: [AnyObject] = []
|
||||
|
||||
for subview in view.subviews {
|
||||
if subview.isAccessibilityElement && subview.isVisibleOnScreen {
|
||||
|
||||
@ -20,6 +20,9 @@ public protocol FormFieldable {
|
||||
|
||||
/// Protocol for FormFieldable that require internal validation.
|
||||
public protocol FormFieldInternalValidatable: FormFieldable, Errorable {
|
||||
/// Rules that drive the validator
|
||||
var rules: [AnyRule<ValueType>] { get set }
|
||||
|
||||
/// Is there an internalError
|
||||
var hasInternalError: Bool { get }
|
||||
/// Internal Error Message that will show.
|
||||
|
||||
@ -631,6 +631,257 @@ extension LayoutConstraintable {
|
||||
return centerYAnchor.constraint(greaterThanOrEqualTo: found, constant: -constant).with { $0.priority = priority; $0.isActive = true }
|
||||
}
|
||||
}
|
||||
|
||||
// alignment
|
||||
public enum LayoutAlignment: String, CaseIterable {
|
||||
case fill
|
||||
case leading
|
||||
case top
|
||||
case center
|
||||
case trailing
|
||||
case bottom
|
||||
}
|
||||
|
||||
public enum LayoutDistribution: String, CaseIterable {
|
||||
case fill
|
||||
case fillProportionally
|
||||
}
|
||||
|
||||
extension LayoutConstraintable {
|
||||
public func removeConstraints() {
|
||||
guard let view = self as? UIView, let superview = view.superview else { return }
|
||||
|
||||
// Remove all existing constraints on the containerView
|
||||
let superviewConstraints = superview.constraints
|
||||
for constraint in superviewConstraints {
|
||||
if constraint.firstItem as? UIView == view
|
||||
|| constraint.secondItem as? UIView == view {
|
||||
superview.removeConstraint(constraint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func applyAlignment(_ alignment: LayoutAlignment, edges: UIEdgeInsets = UIEdgeInsets.zero) {
|
||||
guard let superview = superview else { return }
|
||||
|
||||
removeConstraints()
|
||||
|
||||
switch alignment {
|
||||
case .fill:
|
||||
pinToSuperView(edges)
|
||||
|
||||
case .leading:
|
||||
pinTop(edges.top)
|
||||
pinLeading(edges.left)
|
||||
pinTrailingLessThanOrEqualTo(anchor: superview.trailingAnchor, constant: edges.right)
|
||||
pinBottom(edges.bottom)
|
||||
|
||||
case .trailing:
|
||||
pinTop(edges.top)
|
||||
pinLeadingGreaterThanOrEqualTo(anchor: superview.leadingAnchor, constant: edges.left)
|
||||
pinTrailing(edges.right)
|
||||
pinBottom(edges.bottom)
|
||||
|
||||
case .top:
|
||||
pinTop(edges.top)
|
||||
pinLeadingGreaterThanOrEqualTo(anchor: superview.leadingAnchor, constant: edges.left)
|
||||
pinTrailingLessThanOrEqualTo(anchor: superview.trailingAnchor, constant: edges.right)
|
||||
pinBottomLessThanOrEqualTo(anchor: superview.bottomAnchor, constant: edges.bottom)
|
||||
|
||||
case .bottom:
|
||||
pinTopGreaterThanOrEqualTo(anchor: superview.topAnchor, constant: edges.top)
|
||||
pinLeadingGreaterThanOrEqualTo(anchor: superview.leadingAnchor, constant: edges.left)
|
||||
pinTrailingLessThanOrEqualTo(anchor: superview.trailingAnchor, constant: edges.right)
|
||||
pinBottom(edges.bottom)
|
||||
|
||||
case .center:
|
||||
pinCenterX()
|
||||
pinTop(edges.top)
|
||||
pinLeadingGreaterThanOrEqualTo(anchor: superview.leadingAnchor, constant: edges.left)
|
||||
pinTrailingLessThanOrEqualTo(anchor: superview.trailingAnchor, constant: edges.right)
|
||||
pinBottom(edges.bottom)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Method to check if the view is pinned to its superview
|
||||
public func isPinnedEqual() -> Bool {
|
||||
isPinnedEqualVertically() && isPinnedEqualHorizontally()
|
||||
}
|
||||
|
||||
public func horizontalPinnedWidth() -> CGFloat? {
|
||||
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 trailingPosition - leadingPosition
|
||||
|
||||
} else if let leadingGuide = leadingObject as? UILayoutGuide, let trailingGuide = trailingObject as? UILayoutGuide {
|
||||
let leadingPosition = leadingGuide.layoutFrame.minX
|
||||
let trailingPosition = trailingGuide.layoutFrame.maxX
|
||||
return trailingPosition - leadingPosition
|
||||
|
||||
} 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 trailingPosition - leadingPosition
|
||||
|
||||
} 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 trailingPosition - leadingPosition
|
||||
}
|
||||
|
||||
} else if let pinnedObject = leadingPinnedObject {
|
||||
if let view = pinnedObject as? UIView {
|
||||
return view.bounds.size.width
|
||||
} else if let layoutGuide = pinnedObject as? UILayoutGuide {
|
||||
return layoutGuide.layoutFrame.size.width
|
||||
}
|
||||
|
||||
} else if let pinnedObject = trailingPinnedObject {
|
||||
if let view = pinnedObject as? UIView {
|
||||
return view.bounds.size.width
|
||||
} else if let layoutGuide = pinnedObject as? UILayoutGuide {
|
||||
return layoutGuide.layoutFrame.size.width
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
public func verticalPinnedHeight() -> CGFloat? {
|
||||
guard let view = self as? UIView, let superview = view.superview else { return nil }
|
||||
let constraints = superview.constraints
|
||||
|
||||
var topPinnedObject: AnyObject?
|
||||
var bottomPinnedObject: AnyObject?
|
||||
|
||||
for constraint in constraints {
|
||||
if (constraint.firstItem === view && (constraint.firstAttribute == .top || constraint.firstAttribute == .topMargin)) {
|
||||
topPinnedObject = constraint.secondItem as AnyObject?
|
||||
} else if (constraint.secondItem === view && (constraint.secondAttribute == .top || constraint.secondAttribute == .topMargin)) {
|
||||
topPinnedObject = constraint.firstItem as AnyObject?
|
||||
} else if (constraint.firstItem === view && (constraint.firstAttribute == .bottom || constraint.firstAttribute == .bottomMargin)) {
|
||||
bottomPinnedObject = constraint.secondItem as AnyObject?
|
||||
} else if (constraint.secondItem === view && (constraint.secondAttribute == .bottom || constraint.secondAttribute == .bottomMargin)) {
|
||||
bottomPinnedObject = constraint.firstItem as AnyObject?
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure both top and bottom pinned objects are identified
|
||||
if let topObject = topPinnedObject, let bottomObject = bottomPinnedObject {
|
||||
|
||||
// Calculate the size based on the pinned objects
|
||||
if let topView = topObject as? UIView, let bottomView = bottomObject as? UIView {
|
||||
let topPosition = topView.convert(topView.bounds.origin, to: superview).y
|
||||
let bottomPosition = bottomView.convert(bottomView.bounds.origin, to: superview).y + bottomView.bounds.height
|
||||
return bottomPosition - topPosition
|
||||
|
||||
} else if let topGuide = topObject as? UILayoutGuide, let bottomGuide = bottomObject as? UILayoutGuide {
|
||||
let topPosition = topGuide.layoutFrame.minY
|
||||
let bottomPosition = bottomGuide.layoutFrame.maxY
|
||||
return bottomPosition - topPosition
|
||||
|
||||
} else if let topView = topObject as? UIView, let bottomGuide = bottomObject as? UILayoutGuide {
|
||||
let topPosition = topView.convert(topView.bounds.origin, to: superview).y
|
||||
let bottomPosition = bottomGuide.layoutFrame.maxY
|
||||
return bottomPosition - topPosition
|
||||
|
||||
} else if let topGuide = topObject as? UILayoutGuide, let bottomView = bottomObject as? UIView {
|
||||
let topPosition = topGuide.layoutFrame.minY
|
||||
let bottomPosition = bottomView.convert(bottomView.bounds.origin, to: superview).y + bottomView.bounds.height
|
||||
return bottomPosition - topPosition
|
||||
}
|
||||
|
||||
} else if let pinnedObject = topPinnedObject {
|
||||
if let view = pinnedObject as? UIView {
|
||||
return view.bounds.size.height
|
||||
} else if let layoutGuide = pinnedObject as? UILayoutGuide {
|
||||
return layoutGuide.layoutFrame.size.height
|
||||
}
|
||||
|
||||
} else if let pinnedObject = bottomPinnedObject {
|
||||
if let view = pinnedObject as? UIView {
|
||||
return view.bounds.size.height
|
||||
} else if let layoutGuide = pinnedObject as? UILayoutGuide {
|
||||
return layoutGuide.layoutFrame.size.height
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
public func isPinnedEqualHorizontally() -> 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 isPinnedEqualVertically() -> 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
|
||||
//--------------------------------------------------
|
||||
|
||||
@ -1,6 +1,32 @@
|
||||
1.0.68
|
||||
1.0.71
|
||||
----------------
|
||||
- CXTDT-581800 - DatePicker - Selected Error state icon
|
||||
- CXTDT-581801 - DatePicker - border disappears for on dark focus state
|
||||
- CXTDT-581803 - DatePicker - Calendar does not switch to Dark Mode
|
||||
- CXTDT-584278 – InputField - Accessibility
|
||||
- CXTDT-586375 - Table - Issue With Stripe
|
||||
- CXTDT-577463 - InputField - Accessibility - #7
|
||||
- CXTDT-565796 - DropdownSelect – Removed the "Type" from the VoiceOver
|
||||
|
||||
1.0.70
|
||||
----------------
|
||||
- CXTDT-577463 - InputField - Accessibility - #1 Typing Feedback
|
||||
- CXTDT-577463 - InputField - Accessibility - #5 Password / Inline Action
|
||||
- CXTDT-560485 - Tilelet - Accessibility Icons
|
||||
- DatePicker - Final logic for how the calendar shows.
|
||||
|
||||
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
|
||||
----------------
|
||||
|
||||
@ -26,6 +26,7 @@ Using the system allows designers and developers to collaborate more easily and
|
||||
- ``ButtonIcon``
|
||||
- ``ButtonGroup``
|
||||
- ``CalendarBase``
|
||||
- ``Carousel``
|
||||
- ``CarouselScrollbar``
|
||||
- ``Checkbox``
|
||||
- ``CheckboxItem``
|
||||
|
||||
Loading…
Reference in New Issue
Block a user