Merge branch 'develop' of https://gitlab.verizon.com/BPHV_MIPS/vds_ios into bugfix/Table
# Conflicts: # VDS/SupportingFiles/ReleaseNotes.txt
This commit is contained in:
commit
0951a48467
@ -7,8 +7,12 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
180636C72C29B0A400C92D86 /* InputStepper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 180636C62C29B0A400C92D86 /* InputStepper.swift */; };
|
||||
180636C92C29B0DF00C92D86 /* InputStepperLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 180636C82C29B0DF00C92D86 /* InputStepperLog.txt */; };
|
||||
1808BEBC2BA41C3200129230 /* CarouselScrollbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1808BEBB2BA41C3200129230 /* CarouselScrollbar.swift */; };
|
||||
1832AC572BA0791D008AE476 /* BreadcrumbCellItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1832AC562BA0791D008AE476 /* BreadcrumbCellItem.swift */; };
|
||||
184023452C61E7AD00A412C8 /* PriceLockup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 184023442C61E7AD00A412C8 /* PriceLockup.swift */; };
|
||||
184023472C61E7EC00A412C8 /* PriceLockupChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 184023462C61E7EC00A412C8 /* PriceLockupChangeLog.txt */; };
|
||||
1842B1DF2BECE28B0021AFCA /* CalendarDateViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1842B1DE2BECE28B0021AFCA /* CalendarDateViewCell.swift */; };
|
||||
1842B1E12BECE7B70021AFCA /* CalendarHeaderReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1842B1E02BECE7B70021AFCA /* CalendarHeaderReusableView.swift */; };
|
||||
1842B1E32BECF0A20021AFCA /* CalendarFooterReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1842B1E22BECF0A10021AFCA /* CalendarFooterReusableView.swift */; };
|
||||
@ -18,7 +22,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 */; };
|
||||
@ -204,9 +211,13 @@
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
180636C62C29B0A400C92D86 /* InputStepper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputStepper.swift; sourceTree = "<group>"; };
|
||||
180636C82C29B0DF00C92D86 /* InputStepperLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = InputStepperLog.txt; sourceTree = "<group>"; };
|
||||
1808BEBB2BA41C3200129230 /* CarouselScrollbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselScrollbar.swift; sourceTree = "<group>"; };
|
||||
1808BEBF2BA456B700129230 /* CarouselScrollbarChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = CarouselScrollbarChangeLog.txt; sourceTree = "<group>"; };
|
||||
1832AC562BA0791D008AE476 /* BreadcrumbCellItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreadcrumbCellItem.swift; sourceTree = "<group>"; };
|
||||
184023442C61E7AD00A412C8 /* PriceLockup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriceLockup.swift; sourceTree = "<group>"; };
|
||||
184023462C61E7EC00A412C8 /* PriceLockupChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = PriceLockupChangeLog.txt; sourceTree = "<group>"; };
|
||||
1842B1DE2BECE28B0021AFCA /* CalendarDateViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarDateViewCell.swift; sourceTree = "<group>"; };
|
||||
1842B1E02BECE7B70021AFCA /* CalendarHeaderReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarHeaderReusableView.swift; sourceTree = "<group>"; };
|
||||
1842B1E22BECF0A10021AFCA /* CalendarFooterReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarFooterReusableView.swift; sourceTree = "<group>"; };
|
||||
@ -219,7 +230,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>"; };
|
||||
@ -442,6 +457,15 @@
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
180636C52C29B06200C92D86 /* InputStepper */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
180636C62C29B0A400C92D86 /* InputStepper.swift */,
|
||||
180636C82C29B0DF00C92D86 /* InputStepperLog.txt */,
|
||||
);
|
||||
path = InputStepper;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
1808BEBA2BA41B1D00129230 /* CarouselScrollbar */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -451,6 +475,15 @@
|
||||
path = CarouselScrollbar;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
184023432C61E78D00A412C8 /* PriceLockup */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
184023442C61E7AD00A412C8 /* PriceLockup.swift */,
|
||||
184023462C61E7EC00A412C8 /* PriceLockupChangeLog.txt */,
|
||||
);
|
||||
path = PriceLockup;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
186D13C92BBA8A3500986B53 /* DropdownSelect */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -487,6 +520,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 = (
|
||||
@ -657,16 +701,19 @@
|
||||
18A65A002B96E7E1006602CC /* Breadcrumbs */,
|
||||
EA0FC2BE2912D18200DF80B4 /* Buttons */,
|
||||
18A3F1202BD8F5DE00498E4A /* Calendar */,
|
||||
18AE874E2C06FD610075F181 /* Carousel */,
|
||||
1808BEBA2BA41B1D00129230 /* CarouselScrollbar */,
|
||||
EAF7F092289985E200B287F5 /* Checkbox */,
|
||||
EAC58C1F2BF127F000BA39FA /* DatePicker */,
|
||||
186D13C92BBA8A3500986B53 /* DropdownSelect */,
|
||||
EA985BF3296C609E00F2FF2E /* Icon */,
|
||||
180636C52C29B06200C92D86 /* InputStepper */,
|
||||
EA3362412892EF700071C351 /* Label */,
|
||||
44604AD529CE195300E62B51 /* Line */,
|
||||
EAD0688C2A55F801002E3A2D /* Loader */,
|
||||
445BA07629C07ABA0036A7C5 /* Notification */,
|
||||
71B23C2B2B91FA510027F7D9 /* Pagination */,
|
||||
184023432C61E78D00A412C8 /* PriceLockup */,
|
||||
EA89200B28B530F0006B9984 /* RadioBox */,
|
||||
EAF7F11428A1470D00B287F5 /* RadioButton */,
|
||||
440B84C82BD8E0CE004A732A /* Table */,
|
||||
@ -1166,9 +1213,11 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
EA3362042891E14D0071C351 /* VerizonNHGeTX-Bold.otf in Resources */,
|
||||
184023472C61E7EC00A412C8 /* PriceLockupChangeLog.txt in Resources */,
|
||||
EA3362072891E14D0071C351 /* VerizonNHGeDS-Regular.otf in Resources */,
|
||||
EA3362062891E14D0071C351 /* VerizonNHGeTX-Regular.otf in Resources */,
|
||||
EA3362052891E14D0071C351 /* VerizonNHGeDS-Bold.otf in Resources */,
|
||||
180636C92C29B0DF00C92D86 /* InputStepperLog.txt in Resources */,
|
||||
EAA5EEB928ECD24B003B3210 /* Icons.xcassets in Resources */,
|
||||
EAA5EEE428F5B855003B3210 /* VerizonNHGDS-Light.otf in Resources */,
|
||||
);
|
||||
@ -1212,6 +1261,7 @@
|
||||
files = (
|
||||
445BA07829C07B3D0036A7C5 /* Notification.swift in Sources */,
|
||||
EA5E304C294CBDD00082B959 /* TileContainer.swift in Sources */,
|
||||
180636C72C29B0A400C92D86 /* InputStepper.swift in Sources */,
|
||||
EAF7F0A6289B0CE000B287F5 /* Resetable.swift in Sources */,
|
||||
EA985C2D296F03FE00F2FF2E /* TileletIconModels.swift in Sources */,
|
||||
EA89200428AECF4B006B9984 /* UITextField+Publisher.swift in Sources */,
|
||||
@ -1290,6 +1340,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 */,
|
||||
@ -1301,6 +1352,7 @@
|
||||
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 */,
|
||||
@ -1339,6 +1391,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 */,
|
||||
@ -1372,6 +1425,7 @@
|
||||
EAC58C0E2BED021600BA39FA /* Password.swift in Sources */,
|
||||
EAF7F0AD289B142900B287F5 /* StrikeThroughLabelAttribute.swift in Sources */,
|
||||
EAB5FEF12927F4AA00998C17 /* SelfSizingCollectionView.swift in Sources */,
|
||||
184023452C61E7AD00A412C8 /* PriceLockup.swift in Sources */,
|
||||
EA3361B8288B2AAA0071C351 /* ViewProtocol.swift in Sources */,
|
||||
EA3361A8288B23300071C351 /* UIColor.swift in Sources */,
|
||||
EA2DC9B42BE2C6FE004F58C5 /* TextField.swift in Sources */,
|
||||
@ -1535,7 +1589,7 @@
|
||||
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 70;
|
||||
CURRENT_PROJECT_VERSION = 72;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@ -1573,7 +1627,7 @@
|
||||
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 70;
|
||||
CURRENT_PROJECT_VERSION = 72;
|
||||
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 {
|
||||
//--------------------------------------------------
|
||||
@ -77,20 +78,30 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Lifecycle
|
||||
//--------------------------------------------------
|
||||
open func initialSetup() {
|
||||
private func initialSetup() {
|
||||
if !initialSetupPerformed {
|
||||
initialSetupPerformed = true
|
||||
shouldUpdateView = false
|
||||
setup()
|
||||
setDefaults()
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
open func setup() {
|
||||
backgroundColor = .clear
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
insetsLayoutMarginsFromSafeArea = false
|
||||
}
|
||||
|
||||
|
||||
open func setDefaults() {
|
||||
backgroundColor = .clear
|
||||
surface = .light
|
||||
isEnabled = true
|
||||
onClick = nil
|
||||
userInfo.removeAll()
|
||||
}
|
||||
|
||||
open func updateView() { }
|
||||
|
||||
open func updateAccessibility() {
|
||||
@ -107,13 +118,12 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
open func reset() {
|
||||
backgroundColor = .clear
|
||||
surface = .light
|
||||
isEnabled = true
|
||||
onClick = nil
|
||||
userInfo.removeAll()
|
||||
shouldUpdateView = false
|
||||
setDefaults()
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
|
||||
@ -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
|
||||
@ -98,13 +100,25 @@ open class SelectorBase: Control, SelectorControlable {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
/// 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 = true
|
||||
accessibilityTraits = .button
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
|
||||
showError = false
|
||||
|
||||
onClick = { control in
|
||||
control.toggle()
|
||||
}
|
||||
|
||||
onChange = nil
|
||||
|
||||
bridge_accessibilityLabelBlock = { [weak self] in
|
||||
guard let self else { return "" }
|
||||
return "\(Self.self)\(showError ? ", error" : "")"
|
||||
@ -116,14 +130,6 @@ open class SelectorBase: Control, SelectorControlable {
|
||||
}
|
||||
}
|
||||
|
||||
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
|
||||
isAccessibilityElement = true
|
||||
accessibilityTraits = .button
|
||||
}
|
||||
|
||||
open override func updateView() {
|
||||
super.updateView()
|
||||
setNeedsLayout()
|
||||
|
||||
@ -65,25 +65,30 @@ open class SelectorGroupBase<SelectorItemType: Groupable>: Control, SelectorGrou
|
||||
}
|
||||
|
||||
didSet {
|
||||
setItemsActions()
|
||||
for selector in items {
|
||||
selector.onClick = { [weak self] handler in
|
||||
self?.didSelect(handler)
|
||||
self?.setNeedsUpdate()
|
||||
}
|
||||
|
||||
selector.accessibilityAction = { [weak self] handler in
|
||||
guard let handler = handler as? SelectorItemType else { return }
|
||||
self?.didSelect(handler)
|
||||
self?.setNeedsUpdate()
|
||||
}
|
||||
|
||||
mainStackView.addArrangedSubview(selector)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open var onChangeSubscriber: AnyCancellable?
|
||||
|
||||
|
||||
private func setItemsActions() {
|
||||
for selector in items {
|
||||
selector.onClick = { [weak self] handler in
|
||||
self?.didSelect(handler)
|
||||
self?.setNeedsUpdate()
|
||||
}
|
||||
|
||||
selector.accessibilityAction = { [weak self] handler in
|
||||
guard let handler = handler as? SelectorItemType else { return }
|
||||
self?.didSelect(handler)
|
||||
self?.setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the Control is enabled or not.
|
||||
override open var isEnabled: Bool {
|
||||
didSet {
|
||||
@ -115,6 +120,12 @@ open class SelectorGroupBase<SelectorItemType: Groupable>: Control, SelectorGrou
|
||||
.pinBottom(0, .defaultHigh)
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
onChange = nil
|
||||
items = []
|
||||
}
|
||||
|
||||
/// Handler for the Group to override on a select event.
|
||||
/// - Parameter selectedControl: Selected Control the user interacted.
|
||||
open func didSelect(_ selectedControl: SelectorItemType) {
|
||||
@ -127,13 +138,6 @@ open class SelectorGroupBase<SelectorItemType: Groupable>: Control, SelectorGrou
|
||||
self?.sendActions(for: .valueChanged)
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
onChange = nil
|
||||
items.forEach{ $0.reset() }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -66,18 +66,21 @@ open class SelectorItemBase<Selector: SelectorBase>: Control, Errorable, Changea
|
||||
/// Label used to render labelText.
|
||||
open var label = Label().with {
|
||||
$0.setContentCompressionResistancePriority(.required, for: .vertical)
|
||||
$0.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
$0.textStyle = .boldBodyLarge
|
||||
}
|
||||
|
||||
/// Label used to render childText.
|
||||
open var childLabel = Label().with {
|
||||
$0.setContentCompressionResistancePriority(.required, for: .vertical)
|
||||
$0.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
$0.textStyle = .bodyLarge
|
||||
}
|
||||
|
||||
/// Label used to render errorText.
|
||||
open var errorLabel = Label().with {
|
||||
$0.setContentCompressionResistancePriority(.required, for: .vertical)
|
||||
$0.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
$0.textStyle = .bodyMedium
|
||||
}
|
||||
|
||||
@ -157,9 +160,32 @@ open class SelectorItemBase<Selector: SelectorBase>: Control, Errorable, Changea
|
||||
//--------------------------------------------------
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
/// 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()
|
||||
|
||||
selectorView.isAccessibilityElement = true
|
||||
isAccessibilityElement = false
|
||||
addSubview(mainStackView)
|
||||
|
||||
mainStackView.isUserInteractionEnabled = false
|
||||
mainStackView.addArrangedSubview(selectorStackView)
|
||||
mainStackView.addArrangedSubview(errorLabel)
|
||||
selectorStackView.addArrangedSubview(selectorView)
|
||||
selectorStackView.addArrangedSubview(selectorLabelStackView)
|
||||
selectorLabelStackView.addArrangedSubview(label)
|
||||
selectorLabelStackView.addArrangedSubview(childLabel)
|
||||
mainStackView
|
||||
.pinTop()
|
||||
.pinLeading()
|
||||
.pinTrailing()
|
||||
.pinBottom(0, .defaultHigh)
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
|
||||
onClick = { [weak self] control in
|
||||
guard let self, isEnabled else { return }
|
||||
toggle()
|
||||
@ -203,29 +229,23 @@ open class SelectorItemBase<Selector: SelectorBase>: Control, Errorable, Changea
|
||||
guard let self else { return "" }
|
||||
return !isEnabled ? "" : "Double tap to activate."
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
label.textStyle = .boldBodyLarge
|
||||
childLabel.textStyle = .bodyLarge
|
||||
errorLabel.textStyle = .bodyMedium
|
||||
|
||||
selectorView.isAccessibilityElement = true
|
||||
isAccessibilityElement = false
|
||||
addSubview(mainStackView)
|
||||
labelText = nil
|
||||
labelTextAttributes = nil
|
||||
labelAttributedText = nil
|
||||
childText = nil
|
||||
childTextAttributes = nil
|
||||
childAttributedText = nil
|
||||
showError = false
|
||||
errorText = nil
|
||||
inputId = nil
|
||||
isSelected = false
|
||||
|
||||
mainStackView.isUserInteractionEnabled = false
|
||||
mainStackView.addArrangedSubview(selectorStackView)
|
||||
mainStackView.addArrangedSubview(errorLabel)
|
||||
selectorStackView.addArrangedSubview(selectorView)
|
||||
selectorStackView.addArrangedSubview(selectorLabelStackView)
|
||||
selectorLabelStackView.addArrangedSubview(label)
|
||||
selectorLabelStackView.addArrangedSubview(childLabel)
|
||||
mainStackView
|
||||
.pinTop()
|
||||
.pinLeading()
|
||||
.pinTrailing()
|
||||
.pinBottom(0, .defaultHigh)
|
||||
onChange = nil
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
@ -281,30 +301,10 @@ open class SelectorItemBase<Selector: SelectorBase>: Control, Errorable, Changea
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
shouldUpdateView = false
|
||||
label.reset()
|
||||
childLabel.reset()
|
||||
errorLabel.reset()
|
||||
|
||||
label.textStyle = .boldBodyLarge
|
||||
childLabel.textStyle = .bodyLarge
|
||||
errorLabel.textStyle = .bodyMedium
|
||||
|
||||
labelText = nil
|
||||
labelTextAttributes = nil
|
||||
labelAttributedText = nil
|
||||
childText = nil
|
||||
childTextAttributes = nil
|
||||
childAttributedText = nil
|
||||
showError = false
|
||||
errorText = nil
|
||||
inputId = nil
|
||||
isSelected = false
|
||||
|
||||
onChange = nil
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
super.reset()
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
|
||||
@ -10,8 +10,9 @@ import UIKit
|
||||
import Combine
|
||||
|
||||
/// Base Class used to build Views.
|
||||
@objcMembers
|
||||
@objc(VDSView)
|
||||
open class View: UIView, ViewProtocol, UserInfoable {
|
||||
open class View: UIView, ViewProtocol, UserInfoable, Clickable {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
@ -36,6 +37,7 @@ open class View: UIView, ViewProtocol, UserInfoable {
|
||||
//--------------------------------------------------
|
||||
open var subscribers = Set<AnyCancellable>()
|
||||
|
||||
open var onClickSubscriber: AnyCancellable?
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Properties
|
||||
//--------------------------------------------------
|
||||
@ -56,20 +58,30 @@ open class View: UIView, ViewProtocol, UserInfoable {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Lifecycle
|
||||
//--------------------------------------------------
|
||||
open func initialSetup() {
|
||||
private func initialSetup() {
|
||||
if !initialSetupPerformed {
|
||||
initialSetupPerformed = true
|
||||
shouldUpdateView = false
|
||||
setup()
|
||||
setDefaults()
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
open func setup() {
|
||||
backgroundColor = .clear
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
insetsLayoutMarginsFromSafeArea = false
|
||||
}
|
||||
|
||||
open func setDefaults() {
|
||||
backgroundColor = .clear
|
||||
surface = .light
|
||||
isEnabled = true
|
||||
onClick = nil
|
||||
userInfo.removeAll()
|
||||
}
|
||||
|
||||
open func updateView() { }
|
||||
|
||||
open func updateAccessibility() {
|
||||
@ -81,9 +93,10 @@ open class View: UIView, ViewProtocol, UserInfoable {
|
||||
}
|
||||
|
||||
open func reset() {
|
||||
backgroundColor = .clear
|
||||
surface = .light
|
||||
isEnabled = true
|
||||
shouldUpdateView = false
|
||||
setDefaults()
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
open override func layoutSubviews() {
|
||||
|
||||
@ -10,6 +10,8 @@ import UIKit
|
||||
import Combine
|
||||
import VDSCoreTokens
|
||||
|
||||
@objcMembers
|
||||
@objc(VDSAlertViewController)
|
||||
open class AlertViewController: UIViewController, Surfaceable {
|
||||
|
||||
/// Set of Subscribers for any Publishers for this Control.
|
||||
|
||||
@ -8,6 +8,8 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
@objcMembers
|
||||
@objc(VDSClearPopoverViewController)
|
||||
open class ClearPopoverViewController: UIViewController, UIPopoverPresentationControllerDelegate {
|
||||
|
||||
/// The view to be inserted inside the popover
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -148,25 +149,28 @@ open class Badge: View {
|
||||
maxWidthConstraint = label.widthLessThanEqualTo(constant: 0).with { $0.isActive = false }
|
||||
clipsToBounds = true
|
||||
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
|
||||
bridge_accessibilityLabelBlock = { [weak self] in
|
||||
guard let self else { return "" }
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
shouldUpdateView = false
|
||||
label.reset()
|
||||
|
||||
label.lineBreakMode = .byTruncatingTail
|
||||
label.textStyle = .boldBodySmall
|
||||
fillColor = .red
|
||||
text = ""
|
||||
maxWidth = nil
|
||||
numberOfLines = 1
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
label.reset()
|
||||
super.reset()
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -292,6 +293,28 @@ open class BadgeIndicator: View {
|
||||
label.centerYAnchor.constraint(equalTo: badgeView.centerYAnchor).isActive = true
|
||||
labelContraints.isActive = true
|
||||
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
label.lineBreakMode = .byTruncatingTail
|
||||
label.textAlignment = .center
|
||||
fillColor = .red
|
||||
number = nil
|
||||
kind = .simple
|
||||
leadingCharacter = nil
|
||||
trailingText = nil
|
||||
size = .xxlarge
|
||||
dotSize = nil
|
||||
verticalPadding = nil
|
||||
horizontalPadding = nil
|
||||
hideDot = false
|
||||
hideBorder = false
|
||||
width = nil
|
||||
height = nil
|
||||
accessibilityText = nil
|
||||
maximumDigits = .two
|
||||
|
||||
bridge_accessibilityLabelBlock = { [weak self] in
|
||||
guard let self else { return "" }
|
||||
if let accessibilityText {
|
||||
@ -306,15 +329,8 @@ open class BadgeIndicator: View {
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
shouldUpdateView = false
|
||||
label.reset()
|
||||
label.lineBreakMode = .byTruncatingTail
|
||||
label.textAlignment = .center
|
||||
fillColor = .red
|
||||
number = nil
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
super.reset()
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -71,17 +72,15 @@ open class BreadcrumbItem: ButtonBase {
|
||||
//--------------------------------------------------
|
||||
// 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()
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
isAccessibilityElement = true
|
||||
accessibilityTraits = .link
|
||||
|
||||
titleLabel?.numberOfLines = 0
|
||||
titleLabel?.lineBreakMode = .byWordWrapping
|
||||
contentHorizontalAlignment = .left
|
||||
|
||||
isAccessibilityElement = true
|
||||
accessibilityTraits = .link
|
||||
|
||||
bridge_accessibilityHintBlock = { [weak self] in
|
||||
guard let self else { return "" }
|
||||
return !isEnabled ? "" : "Double tap to open."
|
||||
@ -130,17 +129,4 @@ open class BreadcrumbItem: ButtonBase {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
shouldUpdateView = false
|
||||
text = nil
|
||||
accessibilityCustomActions = []
|
||||
isAccessibilityElement = true
|
||||
accessibilityTraits = .button
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -108,21 +109,19 @@ open class Breadcrumbs: View {
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
/// Executed on initialization for this View.
|
||||
open override func initialSetup() {
|
||||
super.initialSetup()
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
containerView.addSubview(collectionView)
|
||||
collectionView.pinToSuperView()
|
||||
addSubview(containerView)
|
||||
containerView.pinToSuperView()
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
shouldUpdateView = false
|
||||
breadcrumbs.forEach { $0.reset() }
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
breadcrumbs = []
|
||||
breadcrumbModels = []
|
||||
isEnabled = true
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -222,16 +223,12 @@ open class Button: ButtonBase, Useable {
|
||||
isAccessibilityElement = true
|
||||
accessibilityTraits = .button
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
shouldUpdateView = false
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
use = .primary
|
||||
width = nil
|
||||
size = .large
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -96,13 +97,13 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Lifecycle
|
||||
//--------------------------------------------------
|
||||
open func initialSetup() {
|
||||
private func initialSetup() {
|
||||
if !initialSetupPerformed {
|
||||
initialSetupPerformed = true
|
||||
backgroundColor = .clear
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
accessibilityCustomActions = []
|
||||
shouldUpdateView = false
|
||||
setup()
|
||||
setDefaults()
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
@ -110,10 +111,19 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable {
|
||||
|
||||
open func setup() {
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
}
|
||||
|
||||
open func setDefaults() {
|
||||
backgroundColor = .clear
|
||||
accessibilityCustomActions = []
|
||||
titleLabel?.adjustsFontSizeToFitWidth = false
|
||||
titleLabel?.lineBreakMode = .byTruncatingTail
|
||||
titleLabel?.numberOfLines = 1
|
||||
surface = .light
|
||||
isEnabled = true
|
||||
text = nil
|
||||
onClick = nil
|
||||
userInfo.removeAll()
|
||||
}
|
||||
|
||||
open func updateView() {
|
||||
@ -130,12 +140,7 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable {
|
||||
|
||||
open func reset() {
|
||||
shouldUpdateView = false
|
||||
surface = .light
|
||||
isEnabled = true
|
||||
text = nil
|
||||
accessibilityCustomActions = []
|
||||
onClick = nil
|
||||
userInfo.removeAll()
|
||||
setDefaults()
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -166,15 +167,18 @@ open class ButtonGroup: View {
|
||||
collectionView.reloadData()
|
||||
}
|
||||
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
shouldUpdateView = false
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
rowQuantityPhone = 0
|
||||
rowQuantityTablet = 0
|
||||
alignment = .center
|
||||
childWidth = nil
|
||||
buttons = []
|
||||
}
|
||||
|
||||
open override func reset() {
|
||||
buttons.forEach { $0.reset() }
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
super.reset()
|
||||
}
|
||||
|
||||
open override func layoutSubviews() {
|
||||
|
||||
@ -49,56 +49,53 @@ class ButtonCollectionViewRow {
|
||||
func layout(for position: ButtonGroup.Alignment, with collectionViewWidth: CGFloat){
|
||||
var offset = 0.0
|
||||
let height = rowHeight
|
||||
|
||||
attributes.last?.spacing = 0
|
||||
|
||||
//filter only the buttons since this is the only
|
||||
//object we can change the frames for.
|
||||
let buttonAttributes = attributes.filter{$0.isButton}
|
||||
|
||||
//check to see if you have buttons and there is a percentage
|
||||
if let buttonPercentage, hasButtons, buttonPercentage > 0 {
|
||||
if !buttonAttributes.isEmpty {
|
||||
let buttonCount = CGFloat(buttonAttributes.count)
|
||||
|
||||
var usedSpace = 0.0
|
||||
//get the width for the buttons
|
||||
for attribute in attributes {
|
||||
if !attribute.isButton {
|
||||
usedSpace += attribute.frame.width
|
||||
}
|
||||
usedSpace += attribute.spacing
|
||||
}
|
||||
let buttonAvailableSpace = collectionViewWidth - usedSpace
|
||||
let realPercentage = (buttonPercentage / 100)
|
||||
let buttonWidth = realPercentage * buttonAvailableSpace
|
||||
// print("buttonPercentage :\(realPercentage)")
|
||||
// print("collectionView width:\(collectionViewWidth)")
|
||||
// print("usedSpace width:\(usedSpace)")
|
||||
// print("button available width:\(buttonAvailableSpace)")
|
||||
// print("each button width:\(buttonWidth)\n")
|
||||
// print("minimum widht:\(ButtonSize.large.minimumWidth)")
|
||||
// test sizing
|
||||
var testSize = 0.0
|
||||
var buttonCount = 0.0
|
||||
for attribute in attributes {
|
||||
if attribute.isButton {
|
||||
testSize += buttonWidth
|
||||
buttonCount += 1
|
||||
}
|
||||
///Calculate the spaces between items in the row
|
||||
let totalSpacingBetweenAttributes = attributes.reduce(0.0) { $0 + $1.spacing }
|
||||
|
||||
//see how much of the rows width is used for
|
||||
//non-buttons that are BaseButton Subclasses that are not "Button"
|
||||
let nonButtonSpace = attributes.filter { !$0.isButton }.reduce(0.0) { $0 + $1.frame.width }
|
||||
|
||||
//getting available button space since textlinks need their space
|
||||
let buttonsAvailableSpace = collectionViewWidth - nonButtonSpace - totalSpacingBetweenAttributes
|
||||
let buttonEqualSpacing = buttonsAvailableSpace / buttonCount
|
||||
var maxButtonWidth = buttonEqualSpacing //default to equal spacing
|
||||
var buttonCalculatedPercentage: CGFloat = 0.0
|
||||
|
||||
//check to see if you have buttons and there is a percentage
|
||||
if let buttonPercentage, hasButtons, buttonPercentage > 0 {
|
||||
buttonCalculatedPercentage = CGFloat(buttonPercentage / 100.0)
|
||||
let buttonPercentageWidth = buttonCalculatedPercentage * buttonsAvailableSpace
|
||||
maxButtonWidth = min(max(buttonPercentageWidth, Button.Size.large.minimumWidth), maxButtonWidth)
|
||||
}
|
||||
|
||||
if buttonWidth >= Button.Size.large.minimumWidth {
|
||||
if testSize <= buttonAvailableSpace {
|
||||
for attribute in attributes {
|
||||
if attribute.isButton {
|
||||
attribute.frame.size.width = buttonWidth
|
||||
}
|
||||
//resize the buttonAttributes
|
||||
if maxButtonWidth >= Button.Size.large.minimumWidth {
|
||||
//if there is enough room for all buttons
|
||||
if maxButtonWidth * buttonCount <= buttonsAvailableSpace {
|
||||
for attribute in buttonAttributes {
|
||||
attribute.frame.size.width = buttonCalculatedPercentage.isZero ? min(attribute.frame.size.width, maxButtonWidth) : maxButtonWidth
|
||||
}
|
||||
} else {
|
||||
let distributedSize = buttonAvailableSpace / buttonCount
|
||||
for attribute in attributes {
|
||||
if attribute.isButton {
|
||||
attribute.frame.size.width = distributedSize
|
||||
}
|
||||
//if not enough room, give all buttons the same width
|
||||
for attribute in buttonAttributes {
|
||||
attribute.frame.size.width = buttonEqualSpacing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//update the offset based on position
|
||||
switch position {
|
||||
case .left:
|
||||
break
|
||||
@ -181,7 +178,7 @@ class ButtonGroupPositionLayout: UICollectionViewLayout {
|
||||
var rows = [ButtonCollectionViewRow]()
|
||||
rows.append(ButtonCollectionViewRow())
|
||||
|
||||
let collectionViewWidth = collectionView.frame.width
|
||||
let collectionViewWidth = collectionView.horizontalPinnedWidth() ?? collectionView.frame.width
|
||||
|
||||
for item in 0..<totalItems {
|
||||
|
||||
@ -200,7 +197,8 @@ class ButtonGroupPositionLayout: UICollectionViewLayout {
|
||||
// determine if the current button will fit in the row
|
||||
let rowItemCount = rows.last?.attributes.count ?? 0
|
||||
|
||||
if (layoutWidthIterator + itemSize.width) > collectionViewWidth || (rowQuantity > 0 && rowItemCount == rowQuantity) {
|
||||
if (layoutWidthIterator + itemSize.width) > collectionViewWidth && rowQuantity == 0
|
||||
|| (rowQuantity > 0 && rowItemCount == rowQuantity) {
|
||||
|
||||
// If the current row width (after this item being laid out) is exceeding
|
||||
// the width of the collection view content, put it in the next line
|
||||
@ -318,3 +316,5 @@ class ButtonGroupPositionLayout: UICollectionViewLayout {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@ -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 {
|
||||
//--------------------------------------------------
|
||||
@ -90,12 +91,7 @@ open class TextLink: ButtonBase {
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
isAccessibilityElement = true
|
||||
accessibilityTraits = .link
|
||||
|
||||
//left align titleLabel in case this is pinned leading/trailing
|
||||
//default is always set to center
|
||||
contentHorizontalAlignment = .left
|
||||
|
||||
|
||||
if let titleLabel {
|
||||
addSubview(line)
|
||||
line.pinLeading(titleLabel.leadingAnchor)
|
||||
@ -105,12 +101,21 @@ open class TextLink: ButtonBase {
|
||||
lineHeightConstraint = line.height(constant: 1)
|
||||
lineHeightConstraint?.isActive = true
|
||||
}
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
size = .large
|
||||
accessibilityTraits = .link
|
||||
|
||||
//left align titleLabel in case this is pinned leading/trailing
|
||||
//default is always set to center
|
||||
contentHorizontalAlignment = .left
|
||||
|
||||
bridge_accessibilityHintBlock = { [weak self] in
|
||||
guard let self else { return "" }
|
||||
return !isEnabled ? "" : "Double tap to open."
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
@ -122,18 +127,5 @@ open class TextLink: ButtonBase {
|
||||
//always call last so the label is rendered
|
||||
super.updateView()
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
shouldUpdateView = false
|
||||
text = nil
|
||||
size = .large
|
||||
accessibilityCustomActions = []
|
||||
isAccessibilityElement = true
|
||||
accessibilityTraits = .link
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
//--------------------------------------------------
|
||||
@ -75,10 +76,8 @@ open class TextLinkCaret: ButtonBase {
|
||||
//--------------------------------------------------
|
||||
// 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()
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
//left align titleLabel in case this is pinned leading/trailing
|
||||
//default is always set to center
|
||||
contentHorizontalAlignment = .left
|
||||
@ -87,11 +86,12 @@ open class TextLinkCaret: ButtonBase {
|
||||
titleLabel?.numberOfLines = 0
|
||||
titleLabel?.lineBreakMode = .byWordWrapping
|
||||
|
||||
iconPosition = .right
|
||||
|
||||
bridge_accessibilityHintBlock = { [weak self] in
|
||||
guard let self else { return "" }
|
||||
return !isEnabled ? "" : "Double tap to open."
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
@ -99,14 +99,7 @@ open class TextLinkCaret: ButtonBase {
|
||||
imageAttribute = CaretLabelAttribute(tintColor: textColor, position: iconPosition)
|
||||
super.updateView()
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
iconPosition = .right
|
||||
text = nil
|
||||
}
|
||||
|
||||
|
||||
/// The natural size for the receiving view, considering only properties of the view itself.
|
||||
open override var intrinsicContentSize: CGSize {
|
||||
guard let titleLabel else { return super.intrinsicContentSize }
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -124,10 +125,6 @@ open class CalendarBase: Control, Changeable {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
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()
|
||||
@ -153,6 +150,19 @@ open class CalendarBase: Control, Changeable {
|
||||
|
||||
collectionView.pinCenterX(anchor: containerView.centerXAnchor)
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
hideContainerBorder = false
|
||||
hideCurrentDateIndicator = false
|
||||
transparentBackground = false
|
||||
activeDates = []
|
||||
inactiveDates = []
|
||||
indicators = []
|
||||
minDate = Date()
|
||||
maxDate = Date()
|
||||
selectedDate = Date()
|
||||
}
|
||||
|
||||
open override func updateView() {
|
||||
super.updateView()
|
||||
@ -173,17 +183,6 @@ open class CalendarBase: Control, Changeable {
|
||||
containerView.layer.borderWidth = VDSFormControls.borderWidth
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
hideContainerBorder = false
|
||||
hideCurrentDateIndicator = false
|
||||
transparentBackground = false
|
||||
activeDates = []
|
||||
inactiveDates = []
|
||||
indicators = []
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Methods
|
||||
|
||||
561
VDS/Components/Carousel/Carousel.swift
Normal file
561
VDS/Components/Carousel/Carousel.swift
Normal file
@ -0,0 +1,561 @@
|
||||
//
|
||||
// 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, Valuing {
|
||||
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
|
||||
//--------------------------------------------------
|
||||
/// 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()
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
gutter = UIDevice.isIPad ? .gutter6X : .gutter3X
|
||||
layout = UIDevice.isIPad ? .threeUP : .oneUP
|
||||
onChange = nil
|
||||
pagination = .init(kind: .lowContrast, floating: true)
|
||||
paginationDisplay = .none
|
||||
paginationInset = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space2X
|
||||
peek = .standard
|
||||
groupIndex = 0
|
||||
}
|
||||
|
||||
/// 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()
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// 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)
|
||||
@ -105,14 +106,15 @@ open class CarouselScrollbar: View {
|
||||
|
||||
/// A callback when the scrubber position changes. Passes parameters (position).
|
||||
open var onScrubberDrag: ((Int) -> Void)? {
|
||||
get { nil }
|
||||
set {
|
||||
didSet {
|
||||
onScrubberDragCancellable?.cancel()
|
||||
if let newValue {
|
||||
if let onScrubberDrag {
|
||||
onScrubberDragCancellable = onScrubberDragPublisher
|
||||
.sink { c in
|
||||
newValue(c)
|
||||
onScrubberDrag(c)
|
||||
}
|
||||
} else {
|
||||
onScrubberDragCancellable = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -123,14 +125,15 @@ open class CarouselScrollbar: View {
|
||||
|
||||
/// A callback when the thumb move forward. Passes parameters (position).
|
||||
open var onMoveForward: ((Int) -> Void)? {
|
||||
get { nil }
|
||||
set {
|
||||
didSet {
|
||||
onMoveForwardCancellable?.cancel()
|
||||
if let newValue {
|
||||
if let onMoveForward {
|
||||
onMoveForwardCancellable = onMoveForwardPublisher
|
||||
.sink { c in
|
||||
newValue(c)
|
||||
onMoveForward(c)
|
||||
}
|
||||
} else {
|
||||
onMoveForwardCancellable = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -141,14 +144,15 @@ open class CarouselScrollbar: View {
|
||||
|
||||
/// A callback when the thumb move backward. Passes parameters (position).
|
||||
open var onMoveBackward: ((Int) -> Void)? {
|
||||
get { nil }
|
||||
set {
|
||||
didSet {
|
||||
onMoveBackwardCancellable?.cancel()
|
||||
if let newValue {
|
||||
if let onMoveBackward {
|
||||
onMoveBackwardCancellable = onMoveBackwardPublisher
|
||||
.sink { c in
|
||||
newValue(c)
|
||||
onMoveBackward(c)
|
||||
}
|
||||
} else {
|
||||
onMoveBackwardCancellable = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -159,14 +163,15 @@ open class CarouselScrollbar: View {
|
||||
|
||||
/// A callback when the thumb touch start. Passes parameters (position).
|
||||
open var onThumbTouchStart: ((Int) -> Void)? {
|
||||
get { nil }
|
||||
set {
|
||||
didSet {
|
||||
onThumbTouchStartCancellable?.cancel()
|
||||
if let newValue {
|
||||
if let onThumbTouchStart {
|
||||
onThumbTouchStartCancellable = onThumbTouchStartPublisher
|
||||
.sink { c in
|
||||
newValue(c)
|
||||
onThumbTouchStart(c)
|
||||
}
|
||||
} else {
|
||||
onThumbTouchStartCancellable = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -177,14 +182,15 @@ open class CarouselScrollbar: View {
|
||||
|
||||
/// A callback when the thumb touch end. Passes parameters (position).
|
||||
open var onThumbTouchEnd: ((Int) -> Void)? {
|
||||
get { nil }
|
||||
set {
|
||||
didSet {
|
||||
onThumbTouchEndCancellable?.cancel()
|
||||
if let newValue {
|
||||
if let onThumbTouchEnd {
|
||||
onThumbTouchEndCancellable = onThumbTouchEndPublisher
|
||||
.sink { c in
|
||||
newValue(c)
|
||||
onThumbTouchEnd(c)
|
||||
}
|
||||
} else {
|
||||
onThumbTouchEndCancellable = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -198,7 +204,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
|
||||
@ -234,10 +240,6 @@ open class CarouselScrollbar: View {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Lifecycle
|
||||
//--------------------------------------------------
|
||||
open override func initialSetup() {
|
||||
super.initialSetup()
|
||||
}
|
||||
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
isAccessibilityElement = false
|
||||
@ -297,6 +299,19 @@ open class CarouselScrollbar: View {
|
||||
thumbView.layer.addSublayer(thumbViewLayer)
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
onMoveForward = nil
|
||||
onMoveBackward = nil
|
||||
onScrubberDrag = nil
|
||||
onThumbTouchEnd = nil
|
||||
onThumbTouchStart = nil
|
||||
layout = .oneUP
|
||||
numberOfSlides = 1
|
||||
totalPositions = 1
|
||||
position = 1
|
||||
}
|
||||
|
||||
open override func updateView() {
|
||||
super.updateView()
|
||||
trackView.backgroundColor = trackColorConfiguration.getColor(surface)
|
||||
@ -329,7 +344,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 +377,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 {
|
||||
|
||||
@ -61,6 +62,11 @@ open class Checkbox: SelectorBase {
|
||||
selectorColorConfiguration.setSurfaceColors(VDSColor.elementsPrimaryOndark, VDSColor.elementsPrimaryOnlight, forState: .selected)
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
isAnimated = false
|
||||
}
|
||||
|
||||
/// This will change the state of the Selector and execute the actionBlock if provided.
|
||||
open override func toggle() {
|
||||
guard isEnabled else { return }
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -86,6 +87,12 @@ open class CheckboxGroup: SelectorGroupBase<CheckboxItem>, SelectorGroupMultiSel
|
||||
mainStackView.spacing = VDSLayout.space6X
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
showError = false
|
||||
inputId = nil
|
||||
}
|
||||
|
||||
public override func didSelect(_ selectedControl: CheckboxItem) {
|
||||
selectedControl.toggle()
|
||||
if selectedControl.isSelected, showError{
|
||||
|
||||
@ -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,11 @@ open class CheckboxItem: SelectorItemBase<Checkbox> {
|
||||
isSelected.toggle()
|
||||
sendActions(for: .valueChanged)
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
isAnimated = false
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
open override func updateView() {
|
||||
|
||||
@ -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 {
|
||||
open class DatePicker: EntryFieldBase<String> {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
@ -27,6 +28,19 @@ open class DatePicker: EntryFieldBase {
|
||||
/// 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
|
||||
//--------------------------------------------------
|
||||
@ -35,8 +49,9 @@ open class DatePicker: EntryFieldBase {
|
||||
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 = {
|
||||
@ -137,18 +152,7 @@ open class DatePicker: EntryFieldBase {
|
||||
|
||||
// setting color config
|
||||
selectedDateLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable()
|
||||
|
||||
// tap gesture
|
||||
containerView
|
||||
.publisher(for: UITapGestureRecognizer())
|
||||
.sink { [weak self] _ in
|
||||
guard let self else { return }
|
||||
if isEnabled && !isReadOnly {
|
||||
showPopover()
|
||||
}
|
||||
}
|
||||
.store(in: &subscribers)
|
||||
|
||||
|
||||
NotificationCenter.default
|
||||
.publisher(for: UIDevice.orientationDidChangeNotification).sink { [weak self] _ in
|
||||
guard let self else { return }
|
||||
@ -159,6 +163,22 @@ open class DatePicker: EntryFieldBase {
|
||||
popoverOverlayView.isHidden = true
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
selectedDate = nil
|
||||
calendarModel = .init()
|
||||
dateFormat = .shortNumeric
|
||||
selectedDateLabel.textStyle = .bodyLarge
|
||||
|
||||
// tap gesture
|
||||
containerView.onClick = { [weak self] _ in
|
||||
guard let self else { return }
|
||||
if isEnabled && !isReadOnly {
|
||||
showPopover()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open override func getFieldContainer() -> UIView {
|
||||
// stackview for controls in EntryFieldBase.controlContainerView
|
||||
let controlStackView = UIStackView().with {
|
||||
@ -185,12 +205,6 @@ open class DatePicker: EntryFieldBase {
|
||||
calendarIcon.color = iconColorConfiguration.getColor(self)
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
selectedDateLabel.textStyle = .bodyLarge
|
||||
}
|
||||
|
||||
internal func formatDate(_ date: Date) {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = dateFormat.format
|
||||
@ -315,6 +329,7 @@ extension DatePicker {
|
||||
}
|
||||
}
|
||||
|
||||
isCalendarShowing = true
|
||||
}
|
||||
|
||||
private func hidePopoverView() {
|
||||
@ -346,6 +361,7 @@ extension DatePicker {
|
||||
UIAccessibility.post(notification: .layoutChanged, argument: containerView)
|
||||
}
|
||||
}
|
||||
isCalendarShowing = false
|
||||
}
|
||||
|
||||
private func calculatePopoverPosition(relativeTo sourceView: UIView, in parentView: UIView, size: CGSize, with spacing: CGFloat) -> CGPoint? {
|
||||
|
||||
@ -11,8 +11,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(VDSDropdownSelect)
|
||||
open class DropdownSelect: EntryFieldBase {
|
||||
open class DropdownSelect: EntryFieldBase<String> {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
@ -81,24 +82,15 @@ open class DropdownSelect: EntryFieldBase {
|
||||
open var inlineDisplayLabel = Label().with {
|
||||
$0.setContentCompressionResistancePriority(.required, for: .vertical)
|
||||
$0.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
$0.textAlignment = .left
|
||||
$0.textStyle = .boldBodyLarge
|
||||
$0.numberOfLines = 1
|
||||
$0.sizeToFit()
|
||||
}
|
||||
|
||||
open var selectedOptionLabel = Label().with {
|
||||
$0.setContentCompressionResistancePriority(.required, for: .vertical)
|
||||
$0.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
$0.textAlignment = .left
|
||||
$0.textStyle = .bodyLarge
|
||||
$0.numberOfLines = 1
|
||||
}
|
||||
|
||||
open var dropdownField = UITextField().with {
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
$0.tintColor = UIColor.clear
|
||||
$0.font = TextStyle.bodyLarge.font
|
||||
}
|
||||
|
||||
open var optionsPicker = UIPickerView()
|
||||
@ -152,15 +144,35 @@ open class DropdownSelect: EntryFieldBase {
|
||||
}()
|
||||
|
||||
// tap gesture
|
||||
containerView
|
||||
.publisher(for: UITapGestureRecognizer())
|
||||
.sink { [weak self] _ in
|
||||
self?.launchPicker()
|
||||
}
|
||||
.store(in: &subscribers)
|
||||
containerView.onClick = { [weak self] _ in
|
||||
self?.launchPicker()
|
||||
}
|
||||
containerView.height(44)
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
showInlineLabel = false
|
||||
selectId = nil
|
||||
inlineDisplayLabel.textAlignment = .left
|
||||
inlineDisplayLabel.textStyle = .boldBodyLarge
|
||||
inlineDisplayLabel.numberOfLines = 1
|
||||
selectedOptionLabel.textAlignment = .left
|
||||
selectedOptionLabel.textStyle = .bodyLarge
|
||||
selectedOptionLabel.numberOfLines = 1
|
||||
dropdownField.tintColor = UIColor.clear
|
||||
dropdownField.font = TextStyle.bodyLarge.font
|
||||
showInlineLabel = false
|
||||
options = []
|
||||
selectId = nil
|
||||
}
|
||||
|
||||
open override func reset() {
|
||||
inlineDisplayLabel.reset()
|
||||
selectedOptionLabel.reset()
|
||||
super.reset()
|
||||
}
|
||||
|
||||
open override func getFieldContainer() -> UIView {
|
||||
let controlStackView = UIStackView().with {
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
@ -187,17 +199,6 @@ open class DropdownSelect: EntryFieldBase {
|
||||
selectedOptionLabel.surface = surface
|
||||
selectedOptionLabel.isEnabled = isEnabled
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
|
||||
inlineDisplayLabel.textStyle = .boldBodyLarge
|
||||
selectedOptionLabel.textStyle = .bodyLarge
|
||||
showInlineLabel = false
|
||||
options = []
|
||||
selectId = nil
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Methods
|
||||
|
||||
@ -402,17 +402,36 @@ open class ButtonIcon: Control, Changeable {
|
||||
centerXConstraint?.activate()
|
||||
centerYConstraint = icon.centerYAnchor.constraint(equalTo: iconLayoutGuide.centerYAnchor, constant: 0)
|
||||
centerYConstraint?.activate()
|
||||
|
||||
publisher(for: .touchUpInside)
|
||||
.sink(receiveValue: { [weak self] _ in
|
||||
guard let self, isEnabled,
|
||||
selectedIconName != nil,
|
||||
selectable else { return }
|
||||
toggle()
|
||||
})
|
||||
.store(in: &subscribers)
|
||||
}
|
||||
|
||||
/// Executed on initialization for this View.
|
||||
open override func initialSetup() {
|
||||
super.initialSetup()
|
||||
onClick = { control in
|
||||
guard control.isEnabled else { return }
|
||||
if control.selectedIconName != nil && control.selectable {
|
||||
control.toggle()
|
||||
}
|
||||
}
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
badgeIndicatorModel = nil
|
||||
kind = .ghost
|
||||
surfaceType = .colorFill
|
||||
iconName = nil
|
||||
selectedIconName = nil
|
||||
selectedIconColorConfiguration = nil
|
||||
size = .large
|
||||
floating = false
|
||||
fitToIcon = false
|
||||
hideBorder = true
|
||||
showBadgeIndicator = false
|
||||
selectable = false
|
||||
iconOffset = .init(x: 0, y: 0)
|
||||
customContainerSize = nil
|
||||
customIconSize = nil
|
||||
customBadgeIndicatorOffset = nil
|
||||
onChange = nil
|
||||
}
|
||||
|
||||
/// This will change the state of the Selector and execute the actionBlock if provided.
|
||||
@ -422,26 +441,6 @@ open class ButtonIcon: Control, Changeable {
|
||||
sendActions(for: .valueChanged)
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
shouldUpdateView = false
|
||||
kind = .ghost
|
||||
surfaceType = .colorFill
|
||||
size = .large
|
||||
floating = false
|
||||
hideBorder = true
|
||||
iconOffset = .init(x: 0, y: 0)
|
||||
iconName = nil
|
||||
selectedIconName = nil
|
||||
showBadgeIndicator = false
|
||||
selectable = false
|
||||
badgeIndicatorModel = nil
|
||||
onChange = nil
|
||||
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()
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -89,22 +90,29 @@ open class Icon: View {
|
||||
|
||||
addSubview(imageView)
|
||||
imageView.pinToSuperView()
|
||||
|
||||
backgroundColor = .clear
|
||||
|
||||
|
||||
isAccessibilityElement = true
|
||||
accessibilityTraits = .none
|
||||
accessibilityHint = "image"
|
||||
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
backgroundColor = .clear
|
||||
color = VDSColor.paletteBlack
|
||||
size = .medium
|
||||
name = nil
|
||||
customSize = nil
|
||||
imageView.image = nil
|
||||
|
||||
accessibilityHint = "image"
|
||||
bridge_accessibilityLabelBlock = { [weak self] in
|
||||
guard let self else { return "" }
|
||||
return name?.rawValue ?? "icon"
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
open override func updateView() {
|
||||
super.updateView()
|
||||
@ -122,12 +130,6 @@ open class Icon: View {
|
||||
invalidateIntrinsicContentSize()
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
color = VDSColor.paletteBlack
|
||||
imageView.image = nil
|
||||
}
|
||||
}
|
||||
|
||||
extension UIImage {
|
||||
|
||||
422
VDS/Components/InputStepper/InputStepper.swift
Normal file
422
VDS/Components/InputStepper/InputStepper.swift
Normal file
@ -0,0 +1,422 @@
|
||||
//
|
||||
// InputStepper.swift
|
||||
// VDS
|
||||
//
|
||||
// Created by Kanamarlapudi, Vasavi on 24/06/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import VDSCoreTokens
|
||||
import Combine
|
||||
|
||||
/// A stepper is a two-segment control that people use to increase or decrease an incremental value.'
|
||||
@objcMembers
|
||||
@objc(VDSInputStepper)
|
||||
open class InputStepper: EntryFieldBase<Int> {
|
||||
|
||||
//--------------------------------------------------
|
||||
// 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 size of Input Stepper.
|
||||
public enum Size: String, CaseIterable {
|
||||
case large, small
|
||||
|
||||
var minWidth: CGFloat {
|
||||
self == .large ? 121 : 90
|
||||
}
|
||||
|
||||
var minHeight: CGFloat {
|
||||
self == .large ? 44 : 32
|
||||
}
|
||||
|
||||
var space: CGFloat {
|
||||
self == .large ? VDSLayout.space3X : VDSLayout.space2X
|
||||
}
|
||||
var padding: CGFloat {
|
||||
self == .large ? 6.0 : VDSLayout.space1X
|
||||
}
|
||||
|
||||
var buttonContainerSize: Int {
|
||||
self == .large ? 32 : 24
|
||||
}
|
||||
|
||||
var textStyle: TextStyle {
|
||||
self == .large ? .boldBodyLarge : .boldBodySmall
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum used to describe the width of a fixed value or percentage of the input stepper control.
|
||||
public enum ControlWidth {
|
||||
case percentage(CGFloat)
|
||||
case value(CGFloat)
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
/// If there is a width that is larger than this size's minimumWidth, the input stepper will resize to this width.
|
||||
open var controlWidth: ControlWidth? {
|
||||
get { _controlWidth }
|
||||
set {
|
||||
if let newValue {
|
||||
switch newValue {
|
||||
case .percentage(let percentage):
|
||||
if percentage <= 100.0 {
|
||||
_controlWidth = newValue
|
||||
}
|
||||
case .value(let value):
|
||||
if value > 0 && value > containerSize.width {
|
||||
_controlWidth = newValue
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_controlWidth = nil
|
||||
}
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
/// Accepts percentage value to width of parent container.
|
||||
open var widthPercentage: CGFloat? {
|
||||
didSet {
|
||||
if let percentage = widthPercentage, percentage > 100 {
|
||||
widthPercentage = 100
|
||||
}
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
private var _defaultValue: Int = 0
|
||||
open override var defaultValue: Int? {
|
||||
get { _defaultValue }
|
||||
set {
|
||||
if let newValue {
|
||||
_defaultValue = newValue > maxValue ? maxValue : newValue < minValue ? minValue : newValue
|
||||
} else {
|
||||
_defaultValue = 0
|
||||
}
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
open override var value: Int? { return defaultValue }
|
||||
|
||||
/// Maximum value of the input stepper, defaults to '99'.
|
||||
lazy open var maxValue: Int = { _defaultMaxValue }() {
|
||||
didSet {
|
||||
if maxValue > _defaultMaxValue || maxValue < _defaultMinValue && maxValue > minValue {
|
||||
maxValue = _defaultMaxValue
|
||||
}
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
/// Minimum value of the input stepper, defaults to '0'.
|
||||
lazy open var minValue: Int = { _defaultMinValue }() {
|
||||
didSet {
|
||||
if minValue < _defaultMinValue && minValue >= _defaultMaxValue && minValue < maxValue {
|
||||
minValue = _defaultMinValue
|
||||
}
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
/// The size of the input stepper. Defaults to 'large'.
|
||||
open var size: Size = .large { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// Accepts any text or character to appear next to input stepper value.
|
||||
open var trailingText: String? { didSet { setNeedsUpdate() } }
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Properties
|
||||
//--------------------------------------------------
|
||||
private var _controlWidth: ControlWidth? = nil
|
||||
private var _defaultMinValue: Int = 0
|
||||
private var _defaultMaxValue: Int = 99
|
||||
|
||||
/// This is the view that will be wrapped with the border for userInteraction.
|
||||
/// The only subview of this view is the stepperStackView.
|
||||
internal var stepperContainerView = View().with {
|
||||
$0.isAccessibilityElement = true
|
||||
$0.accessibilityLabel = "Input Stepper"
|
||||
}
|
||||
|
||||
internal var stepperStackView = UIStackView().with {
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
$0.axis = .horizontal
|
||||
$0.distribution = .fill
|
||||
$0.alignment = .fill
|
||||
}
|
||||
|
||||
internal var decrementButton = ButtonIcon().with {
|
||||
$0.kind = .ghost
|
||||
$0.iconName = Icon.Name(name: "minus")
|
||||
$0.iconOffset = .init(x: -2, y: 0)
|
||||
$0.customContainerSize = 32
|
||||
$0.icon.customSize = 16
|
||||
$0.backgroundColor = .clear
|
||||
}
|
||||
|
||||
internal var incrementButton = ButtonIcon().with {
|
||||
$0.kind = .ghost
|
||||
$0.iconName = Icon.Name(name: "plus")
|
||||
$0.iconOffset = .init(x: 2, y: 0)
|
||||
$0.customContainerSize = 32
|
||||
$0.icon.customSize = 16
|
||||
$0.backgroundColor = .clear
|
||||
}
|
||||
|
||||
internal var textLabel = Label().with {
|
||||
$0.setContentCompressionResistancePriority(.required, for: .vertical)
|
||||
$0.textStyle = .boldBodyLarge
|
||||
$0.numberOfLines = 1
|
||||
$0.lineBreakMode = .byTruncatingTail
|
||||
$0.textAlignment = .center
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Constraints
|
||||
//--------------------------------------------------
|
||||
internal var stepperWidthConstraint: NSLayoutConstraint?
|
||||
internal var stepperHeightConstraint: NSLayoutConstraint?
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Configuration Properties
|
||||
//--------------------------------------------------
|
||||
internal override var containerSize: CGSize { CGSize(width: size.minWidth, height: size.minHeight) }
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Lifecycle
|
||||
//--------------------------------------------------
|
||||
|
||||
/// 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()
|
||||
// Set initial states
|
||||
defaultValue = 0
|
||||
containerView.isEnabled = false
|
||||
statusIcon.isHidden = true
|
||||
|
||||
//override the default settings since the containerView
|
||||
//fieldStackView relationShip needs to be updated
|
||||
//we are not applying spacing either in the edges since this
|
||||
//is the view that will take place of the containerView for the
|
||||
//design of the original "containerView". This will get refactored at
|
||||
//some point.
|
||||
fieldStackView.applyAlignment(.leading)
|
||||
|
||||
// Add listeners
|
||||
decrementButton.onClick = { _ in self.decrementButtonClick() }
|
||||
incrementButton.onClick = { _ in self.incrementButtonClick() }
|
||||
|
||||
// setting color config
|
||||
textLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable()
|
||||
}
|
||||
|
||||
open override func getFieldContainer() -> UIView {
|
||||
stepperStackView.addArrangedSubview(decrementButton)
|
||||
stepperStackView.addArrangedSubview(textLabel)
|
||||
stepperStackView.addArrangedSubview(incrementButton)
|
||||
|
||||
// Set space between decrement button, label, and increment button relative to input Stepper size.
|
||||
stepperStackView.setCustomSpacing(size.space, after: decrementButton)
|
||||
stepperStackView.setCustomSpacing(size.space, after: textLabel)
|
||||
|
||||
// stepperContainerView for controls in EntryFieldBase.controlContainerView
|
||||
stepperContainerView.addSubview(stepperStackView)
|
||||
|
||||
// Update Edge insets relative to input Stepper size.
|
||||
stepperStackView.pinToSuperView(.uniform(size.padding))
|
||||
|
||||
stepperWidthConstraint = stepperContainerView.width(constant: containerSize.width, priority: .required)
|
||||
stepperHeightConstraint = stepperContainerView.height(constant: containerSize.height, priority: .required)
|
||||
|
||||
return stepperContainerView
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
open override func updateView() {
|
||||
super.updateView()
|
||||
statusIcon.isHidden = true
|
||||
|
||||
// Update label
|
||||
textLabel.isEnabled = isEnabled
|
||||
textLabel.surface = surface
|
||||
textLabel.text = "\(_defaultValue) " + (trailingText ?? "")
|
||||
textLabel.textStyle = size.textStyle
|
||||
|
||||
updateButtonStates()
|
||||
}
|
||||
|
||||
open override var accessibilityElements: [Any]? {
|
||||
get {
|
||||
var elements = [Any]()
|
||||
|
||||
if !isReadOnly || isEnabled {
|
||||
elements.append(contentsOf: [titleLabel, containerView, decrementButton, textLabel, incrementButton])
|
||||
} else {
|
||||
elements.append(contentsOf: [titleLabel, containerView, textLabel])
|
||||
}
|
||||
|
||||
if showError {
|
||||
if let errorText, !errorText.isEmpty {
|
||||
elements.append(errorLabel)
|
||||
}
|
||||
}
|
||||
if let helperText, !helperText.isEmpty {
|
||||
elements.append(helperLabel)
|
||||
}
|
||||
return elements
|
||||
}
|
||||
|
||||
set { super.accessibilityElements = newValue }
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
textLabel.reset()
|
||||
controlWidth = nil
|
||||
widthPercentage = nil
|
||||
defaultValue = 0
|
||||
minValue = _defaultMinValue
|
||||
maxValue = _defaultMaxValue
|
||||
trailingText = nil
|
||||
size = .large
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
override func updateContainerView() {
|
||||
//we are not calling super since we
|
||||
//are using the fieldStackView as the "containerView"
|
||||
//which will get the look/feel of the containerView.
|
||||
//this will get refactored in the future.
|
||||
fieldStackView.backgroundColor = containerBackgroundColor
|
||||
fieldStackView.layer.borderColor = borderColorConfiguration.getColor(self).cgColor
|
||||
fieldStackView.layer.borderWidth = VDSFormControls.borderWidth
|
||||
}
|
||||
|
||||
internal override func updateContainerWidth() {
|
||||
//we are not calling super here since
|
||||
//we are changing how the widths are getting calculated
|
||||
//now by including a percentage.
|
||||
|
||||
defer {
|
||||
fieldStackView.layer.cornerRadius = containerSize.height / 2
|
||||
}
|
||||
|
||||
stepperWidthConstraint?.deactivate()
|
||||
widthConstraint?.deactivate()
|
||||
trailingLessThanEqualsConstraint?.deactivate()
|
||||
trailingEqualsConstraint?.deactivate()
|
||||
|
||||
var widthConstraintConstant: CGFloat?
|
||||
|
||||
if let widthPercentage, let superWidth = horizontalPinnedWidth() {
|
||||
// test value vs minimum width and take the greater value
|
||||
widthConstraintConstant = max(superWidth * (widthPercentage / 100), minWidth)
|
||||
} else if let width, width >= minWidth, width <= maxWidth {
|
||||
widthConstraintConstant = width
|
||||
} else if let parentWidth = width, parentWidth >= maxWidth {
|
||||
widthConstraintConstant = maxWidth
|
||||
} else if let parentWidth = width, parentWidth <= minWidth {
|
||||
widthConstraintConstant = minWidth
|
||||
}
|
||||
|
||||
if let widthConstraintConstant {
|
||||
widthConstraint?.constant = widthConstraintConstant
|
||||
widthConstraint?.activate()
|
||||
trailingLessThanEqualsConstraint?.activate()
|
||||
} else {
|
||||
trailingEqualsConstraint?.activate()
|
||||
}
|
||||
|
||||
// Update Edge insets if size changes applied.
|
||||
stepperStackView.applyAlignment(.fill, edges: .uniform(size.padding))
|
||||
|
||||
// Update height if size changes applied.
|
||||
stepperHeightConstraint?.constant = containerSize.height
|
||||
|
||||
//update the stepper's widthConstraint if
|
||||
//controlWidth was set
|
||||
guard let controlWidth else {
|
||||
return
|
||||
}
|
||||
|
||||
// Set the inputStepper's controlWidth based on percentage received relative to its parentView's frame.
|
||||
let containerWidth: CGFloat = widthConstraintConstant ?? containerView.frame.size.width
|
||||
var stepperWidthConstant: CGFloat?
|
||||
var stepperWidth: CGFloat
|
||||
|
||||
switch controlWidth {
|
||||
case .percentage(let percentage):
|
||||
stepperWidth = max(containerWidth * ((percentage) / 100), minWidth)
|
||||
|
||||
case .value(let value):
|
||||
stepperWidth = value
|
||||
}
|
||||
|
||||
//get the value of the stepperWidthConstant
|
||||
if stepperWidth >= containerSize.width && stepperWidth <= containerWidth {
|
||||
stepperWidthConstant = stepperWidth
|
||||
} else if stepperWidth >= containerWidth {
|
||||
stepperWidthConstant = containerWidth
|
||||
}
|
||||
|
||||
if let stepperWidthConstant {
|
||||
stepperWidthConstraint?.constant = stepperWidthConstant
|
||||
stepperWidthConstraint?.activate()
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Methods
|
||||
//--------------------------------------------------
|
||||
internal func decrementButtonClick() {
|
||||
if _defaultValue > minValue {
|
||||
defaultValue = _defaultValue - 1
|
||||
sendActions(for: .valueChanged)
|
||||
}
|
||||
}
|
||||
|
||||
internal func incrementButtonClick() {
|
||||
if _defaultValue < maxValue {
|
||||
defaultValue = _defaultValue + 1
|
||||
sendActions(for: .valueChanged)
|
||||
}
|
||||
}
|
||||
|
||||
internal func updateButtonStates() {
|
||||
decrementButton.customContainerSize = size.buttonContainerSize
|
||||
incrementButton.customContainerSize = size.buttonContainerSize
|
||||
decrementButton.surface = surface
|
||||
incrementButton.surface = surface
|
||||
|
||||
if isReadOnly || !isEnabled {
|
||||
decrementButton.isEnabled = false
|
||||
incrementButton.isEnabled = false
|
||||
} else {
|
||||
decrementButton.isEnabled = (defaultValue ?? _defaultMaxValue ) > minValue ? true : false
|
||||
incrementButton.isEnabled = (defaultValue ?? _defaultMinValue) < maxValue ? true : false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
44
VDS/Components/InputStepper/InputStepperLog.txt
Normal file
44
VDS/Components/InputStepper/InputStepperLog.txt
Normal file
@ -0,0 +1,44 @@
|
||||
|
||||
MM/DD/YYYY
|
||||
----------------
|
||||
|
||||
02/2024
|
||||
----------------
|
||||
- New component
|
||||
|
||||
02/15/2024
|
||||
----------------
|
||||
- Added Border align: Inside to Anatomy
|
||||
- Removed leadingText property values from States.
|
||||
- Added Read-only to States.
|
||||
- Created a section for Minimum width in Layout and spacing and updated the minWidth to 145px.
|
||||
- Added trailingText spacing to Layout and spacing.
|
||||
- Reduced space between Button Icons and text to 12px in Layout and spacing.
|
||||
- Added top/bottom padding values to Layout and spacing.
|
||||
|
||||
03/01/2024
|
||||
----------------
|
||||
- Removed Leading Text from “Content and other properties” section.
|
||||
|
||||
03/20/2024
|
||||
----------------
|
||||
- Updated Anatomy artwork and items
|
||||
- Added width and controlWidth to Configurations
|
||||
- Updated Width under Layout and spacing to show layout examples
|
||||
|
||||
04/12/2024
|
||||
----------------
|
||||
- Added a new configuration property (size) that includes large and small
|
||||
- Reduced details from Anatomy page and added them to Configurations/Size
|
||||
- Added Hit area, Small Input Stepper spacing properties to Layout and spacing
|
||||
- Updated the Behavior page to display disabled button icon when value is at max and min
|
||||
|
||||
04/29/2024
|
||||
----------------
|
||||
- Updated the Behavior page to display disabled button icon when value is at max and min
|
||||
|
||||
05/10/2024
|
||||
----------------
|
||||
- Added helperTextPlacement property to Configurations
|
||||
- Added Layout examples for right Helper Text placement in Layout and Spacing
|
||||
- Added overflow examples in Overflow section of Layout and Spacing
|
||||
@ -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 {
|
||||
|
||||
@ -191,42 +192,46 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Lifecycle
|
||||
//--------------------------------------------------
|
||||
open func initialSetup() {
|
||||
private func initialSetup() {
|
||||
if !initialSetupPerformed {
|
||||
initialSetupPerformed = true
|
||||
//register for ContentSizeChanges
|
||||
NotificationCenter
|
||||
.Publisher(center: .default, name: UIContentSizeCategory.didChangeNotification)
|
||||
.sink { [weak self] notification in
|
||||
self?.setNeedsUpdate()
|
||||
}.store(in: &subscribers)
|
||||
backgroundColor = .clear
|
||||
numberOfLines = 0
|
||||
lineBreakMode = .byTruncatingTail
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
accessibilityCustomActions = []
|
||||
isAccessibilityElement = true
|
||||
accessibilityTraits = .staticText
|
||||
textAlignment = .left
|
||||
shouldUpdateView = false
|
||||
setup()
|
||||
setDefaults()
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
open func setup() {
|
||||
//register for ContentSizeChanges
|
||||
NotificationCenter
|
||||
.Publisher(center: .default, name: UIContentSizeCategory.didChangeNotification)
|
||||
.sink { [weak self] notification in
|
||||
self?.setNeedsUpdate()
|
||||
}.store(in: &subscribers)
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
isAccessibilityElement = true
|
||||
}
|
||||
|
||||
open func reset() {
|
||||
shouldUpdateView = false
|
||||
open func setDefaults() {
|
||||
backgroundColor = .clear
|
||||
accessibilityTraits = .staticText
|
||||
accessibilityCustomActions = []
|
||||
surface = .light
|
||||
isEnabled = true
|
||||
attributes = nil
|
||||
textStyle = .defaultStyle
|
||||
lineBreakMode = .byTruncatingTail
|
||||
textAlignment = .left
|
||||
text = nil
|
||||
attributedText = nil
|
||||
numberOfLines = 0
|
||||
backgroundColor = .clear
|
||||
}
|
||||
|
||||
open func reset() {
|
||||
shouldUpdateView = false
|
||||
setDefaults()
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -80,11 +81,6 @@ open class Line: View {
|
||||
//--------------------------------------------------
|
||||
// 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()
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
open override func updateView() {
|
||||
super.updateView()
|
||||
@ -93,8 +89,8 @@ open class Line: View {
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
style = .primary
|
||||
orientation = .horizontal
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -267,26 +268,15 @@ open class Notification: View {
|
||||
closeButton.accessibilityTraits = [.button]
|
||||
closeButton.accessibilityLabel = "Close Notification"
|
||||
|
||||
typeIcon.bridge_accessibilityLabelBlock = { [weak self] in
|
||||
guard let self else { return "" }
|
||||
return style.accessibleText
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
|
||||
shouldUpdateView = false
|
||||
|
||||
titleLabel.reset()
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
titleLabel.text = ""
|
||||
titleLabel.textStyle = UIDevice.isIPad ? .boldBodyLarge : .boldBodySmall
|
||||
|
||||
subTitleLabel.reset()
|
||||
subTitleLabel.textStyle = UIDevice.isIPad ? .bodyLarge : .bodySmall
|
||||
|
||||
buttonGroup.reset()
|
||||
buttonGroup.alignment = .left
|
||||
|
||||
primaryButtonModel = nil
|
||||
@ -301,9 +291,19 @@ open class Notification: View {
|
||||
closeButton.name = .close
|
||||
|
||||
hideCloseButton = false
|
||||
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
|
||||
typeIcon.bridge_accessibilityLabelBlock = { [weak self] in
|
||||
guard let self else { return "" }
|
||||
return style.accessibleText
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
titleLabel.reset()
|
||||
subTitleLabel.reset()
|
||||
buttonGroup.reset()
|
||||
super.reset()
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -56,7 +57,7 @@ open class Pagination: View {
|
||||
///Next button to select next page
|
||||
public let nextButton: PaginationButton = .init(type: .next)
|
||||
/// A callback when the page changes. Passes parameters (selectedPage).
|
||||
public var onPageDidSelect: ((Int) -> Void)?
|
||||
open var onPageDidSelect: ((Int) -> Void)?
|
||||
/// Total number of pages, allows limit ranging from 0 to 9999.
|
||||
@Clamping(range: 0...9999)
|
||||
public var total: Int {
|
||||
@ -69,7 +70,7 @@ open class Pagination: View {
|
||||
}
|
||||
}
|
||||
///Selected active page number and clips to total pages if selected index is greater than the total pages.
|
||||
public var selectedPage: Int {
|
||||
open var selectedPage: Int {
|
||||
set {
|
||||
if newValue >= total {
|
||||
_selectedPageIndex = total - 1
|
||||
@ -94,8 +95,8 @@ open class Pagination: View {
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
/// Executed on initialization for this View.
|
||||
open override func initialSetup() {
|
||||
super.initialSetup()
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
|
||||
collectionContainerView.addSubview(collectionView)
|
||||
containerView.addSubview(previousButton)
|
||||
@ -144,6 +145,10 @@ open class Pagination: View {
|
||||
.sink { [weak self] value in
|
||||
self?.collectionViewWidthAnchor?.constant = value //As cell width is dynamic i.e cell may contain 2 or 3 or 4 charcters. Make sure that all the visible cells are displayed.
|
||||
}.store(in: &subscribers)
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
collectionContainerView.onAccessibilityIncrement = { [weak self] in
|
||||
guard let self else { return }
|
||||
self.selectedPage = max(0, self.selectedPage + 1)
|
||||
|
||||
@ -9,6 +9,7 @@ import UIKit
|
||||
import VDSCoreTokens
|
||||
|
||||
///This is customised button for Pagination view
|
||||
@objcMembers
|
||||
@objc(PaginationButton)
|
||||
open class PaginationButton: ButtonBase {
|
||||
//--------------------------------------------------
|
||||
@ -59,8 +60,8 @@ open class PaginationButton: ButtonBase {
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
/// Executed on initialization for this View.
|
||||
open override func initialSetup() {
|
||||
super.initialSetup()
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
if #available(iOS 15.0, *) {
|
||||
configuration = buttonConfiguration
|
||||
} else {
|
||||
|
||||
333
VDS/Components/PriceLockup/PriceLockup.swift
Normal file
333
VDS/Components/PriceLockup/PriceLockup.swift
Normal file
@ -0,0 +1,333 @@
|
||||
//
|
||||
// PriceLockup.swift
|
||||
// VDS
|
||||
//
|
||||
// Created by Kanamarlapudi, Vasavi on 06/08/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import VDSCoreTokens
|
||||
|
||||
@objcMembers
|
||||
@objc(VDSPriceLockup)
|
||||
open class PriceLockup: 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 term of PriceLockup.
|
||||
public enum Term: String, DefaultValuing, CaseIterable {
|
||||
case month, year, biennial, none
|
||||
|
||||
/// The default term is 'month'.
|
||||
public static var defaultValue : Self { .month }
|
||||
|
||||
/// Text for this term of PriceLockup.
|
||||
public var type: String {
|
||||
switch self {
|
||||
case .month:
|
||||
return "mo"
|
||||
case .year:
|
||||
return "yr"
|
||||
case .biennial:
|
||||
return "biennial"
|
||||
case .none:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum that represents the size availble for PriceLockup.
|
||||
public enum Size: String, DefaultValuing, CaseIterable {
|
||||
case xxxsmall = "3XSmall"
|
||||
case xxsmall = "2XSmall"
|
||||
case xsmall = "XSmall"
|
||||
case small
|
||||
case medium
|
||||
case large
|
||||
case xlarge = "XLarge"
|
||||
case xxlarge = "2XLarge"
|
||||
|
||||
public static var defaultValue: Self { .medium }
|
||||
}
|
||||
|
||||
/// Enum used to describe the kind of PriceLockup.
|
||||
public enum Kind: String, DefaultValuing, CaseIterable {
|
||||
case primary, secondary, savings
|
||||
|
||||
/// The default kind is 'primary'.
|
||||
public static var defaultValue : Self { .primary }
|
||||
|
||||
/// Color configuation relative to kind.
|
||||
public var colorConfiguration: SurfaceColorConfiguration {
|
||||
switch self {
|
||||
case .primary:
|
||||
return SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark)
|
||||
case .secondary:
|
||||
return SurfaceColorConfiguration(VDSColor.elementsSecondaryOnlight, VDSColor.elementsSecondaryOndark)
|
||||
case .savings:
|
||||
return SurfaceColorConfiguration(VDSColor.paletteGreen26, VDSColor.paletteGreen36)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
|
||||
/// If true, the component will render as bold.
|
||||
open var bold: Bool = false { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// Currency - If hideCurrency true, the component will render without currency.
|
||||
open var hideCurrency: Bool = false { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// Leading text for the component.
|
||||
open var leadingText: String? { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// Value rendered for the component.
|
||||
open var price: Float? { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// Color to the component. The default kind is primary.
|
||||
open var kind: Kind = .defaultValue { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// Size of the component. It varies by size and viewport(mobile/Tablet).
|
||||
/// The default size is medium with viewport mobile.
|
||||
open var size: Size = .defaultValue { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// If true, the component with a strikethrough. It applies only when uniformSize is true.
|
||||
/// Does not apply a strikethrough format to leading and trailing text.
|
||||
open var strikethrough: Bool = false { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// Term text for the component. The default term is 'month'.
|
||||
/// Superscript placement can vary when term and delimeter are "none".
|
||||
open var term: Term = .defaultValue { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// Trailing text for the component.
|
||||
open var trailingText: String? { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// Superscript text for the component.
|
||||
open var superscript: String? { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// If true, currency and value have the same font text style as delimeter, term label and superscript.
|
||||
/// This will render the pricing and term sections as a uniform size.
|
||||
open var uniformSize: Bool = false { didSet { setNeedsUpdate() } }
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Properties
|
||||
//--------------------------------------------------
|
||||
internal var priceLockupLabel = Label().with {
|
||||
$0.isAccessibilityElement = true
|
||||
$0.lineBreakMode = .byWordWrapping
|
||||
}
|
||||
|
||||
internal var delimiterIndex = 0
|
||||
internal var strikethroughLocation = 0
|
||||
internal var strikethroughLength = 0
|
||||
|
||||
internal var textPosition:TextPosition = .preDelimiter
|
||||
enum TextPosition: String, CaseIterable {
|
||||
case preDelimiter, postDelimiter
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Configuration Properties
|
||||
//--------------------------------------------------
|
||||
internal var containerSize: CGSize { CGSize(width: 45, height: 44) }
|
||||
|
||||
// TextStyle for the size.
|
||||
private var textStyle: TextStyle.StandardStyle {
|
||||
switch (size, textPosition) {
|
||||
case (.xxxsmall, .preDelimiter), (.xxxsmall, .postDelimiter):
|
||||
return .micro
|
||||
|
||||
case (.xxsmall, .preDelimiter), (.xxsmall, .postDelimiter):
|
||||
return .bodySmall
|
||||
|
||||
case (.xsmall, .preDelimiter), (.xsmall, .postDelimiter):
|
||||
return .bodyMedium
|
||||
|
||||
case (.small, .preDelimiter), (.small, .postDelimiter):
|
||||
return .bodyLarge
|
||||
|
||||
case (.medium, .preDelimiter):
|
||||
return UIDevice.isIPad ? .titleSmall : .titleMedium
|
||||
|
||||
case (.medium, .postDelimiter):
|
||||
return .bodyLarge
|
||||
|
||||
case (.large, .preDelimiter):
|
||||
return UIDevice.isIPad ? .titleMedium : .titleLarge
|
||||
|
||||
case (.large, .postDelimiter):
|
||||
return UIDevice.isIPad ? .titleSmall : .titleMedium
|
||||
|
||||
case (.xlarge, .preDelimiter):
|
||||
return UIDevice.isIPad ? .titleLarge : .titleXLarge
|
||||
|
||||
case (.xlarge, .postDelimiter):
|
||||
return UIDevice.isIPad ? .titleMedium : .titleLarge
|
||||
|
||||
case (.xxlarge, .preDelimiter):
|
||||
return UIDevice.isIPad ? .titleXLarge : .featureSmall
|
||||
|
||||
case (.xxlarge, .postDelimiter):
|
||||
return UIDevice.isIPad ? .titleLarge : .titleXLarge
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// 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()
|
||||
|
||||
// Price lockup label
|
||||
addSubview(priceLockupLabel)
|
||||
priceLockupLabel.pinToSuperView()
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
open override func updateView() {
|
||||
super.updateView()
|
||||
|
||||
priceLockupLabel.text = formatText()
|
||||
priceLockupLabel.surface = surface
|
||||
|
||||
// Set the attributed text
|
||||
updateLabelAttributes()
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
bold = false
|
||||
hideCurrency = false
|
||||
leadingText = nil
|
||||
price = nil
|
||||
kind = .defaultValue
|
||||
size = .defaultValue
|
||||
strikethrough = false
|
||||
term = .defaultValue
|
||||
trailingText = nil
|
||||
superscript = nil
|
||||
uniformSize = false
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
priceLockupLabel.reset()
|
||||
super.reset()
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Methods
|
||||
//--------------------------------------------------
|
||||
// Update PriceLockup text attributes
|
||||
func updateLabelAttributes() {
|
||||
var attributes: [any LabelAttributeModel] = []
|
||||
attributes.append(ColorLabelAttribute(location: 0,
|
||||
length: priceLockupLabel.text.count,
|
||||
color: kind.colorConfiguration.getColor(self)))
|
||||
textPosition = .postDelimiter
|
||||
if strikethrough {
|
||||
|
||||
// strike applies only when uniformSize true. Does not apply a strikethrough format to leading, trailing, and superscript text.
|
||||
attributes.append(TextStyleLabelAttribute(location: 0,
|
||||
length: priceLockupLabel.text.count,
|
||||
textStyle: bold ? textStyle.bold : textStyle.regular,
|
||||
textPosition: .left))
|
||||
attributes.append(StrikeThroughLabelAttribute(location:strikethroughLocation, length: strikethroughLength))
|
||||
|
||||
} else if uniformSize {
|
||||
|
||||
// currency and value have the same font text style as delimeter, term, trailing text and superscript.
|
||||
attributes.append(TextStyleLabelAttribute(location: 0,
|
||||
length: priceLockupLabel.text.count,
|
||||
textStyle: bold ? textStyle.bold : textStyle.regular,
|
||||
textPosition: .left))
|
||||
|
||||
} else {
|
||||
|
||||
// size updates relative to predelimiter, postdelimiter
|
||||
if delimiterIndex > 0 {
|
||||
textPosition = .preDelimiter
|
||||
attributes.append(TextStyleLabelAttribute(location: 0,
|
||||
length: delimiterIndex,
|
||||
textStyle: bold ? textStyle.bold : textStyle.regular,
|
||||
textPosition: .left))
|
||||
|
||||
textPosition = .postDelimiter
|
||||
attributes.append(TextStyleLabelAttribute(location: delimiterIndex,
|
||||
length: priceLockupLabel.text.count-delimiterIndex,
|
||||
textStyle: bold ? textStyle.bold : textStyle.regular,
|
||||
textPosition: .left))
|
||||
}
|
||||
}
|
||||
priceLockupLabel.attributes = attributes
|
||||
}
|
||||
|
||||
// Get text for PriceLockup.
|
||||
private func formatText() -> String {
|
||||
var text : String = ""
|
||||
let space = " "
|
||||
let delimiter = "/"
|
||||
delimiterIndex = 0
|
||||
strikethroughLength = 0
|
||||
let currency: String = hideCurrency ? "" : "$"
|
||||
|
||||
if let leadingText {
|
||||
text.append(leadingText)
|
||||
text.append(space)
|
||||
delimiterIndex = delimiterIndex + leadingText.count + space.count
|
||||
}
|
||||
|
||||
strikethroughLocation = delimiterIndex
|
||||
|
||||
if let price = price?.clean {
|
||||
text.append(currency)
|
||||
text.append(price)
|
||||
delimiterIndex = delimiterIndex + price.count + currency.count
|
||||
strikethroughLength = price.count + currency.count
|
||||
}
|
||||
|
||||
if term != .none {
|
||||
text.append(delimiter)
|
||||
text.append(term.type)
|
||||
strikethroughLength = strikethroughLength + delimiter.count + term.type.count
|
||||
}
|
||||
|
||||
if let trailingText {
|
||||
text.append(space)
|
||||
text.append(trailingText)
|
||||
}
|
||||
|
||||
if let superscript {
|
||||
text.append(superscript)
|
||||
}
|
||||
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
extension Float {
|
||||
// remove a decimal from a float if the decimal is equal to 0
|
||||
var clean: String {
|
||||
return self.truncatingRemainder(dividingBy: 1) == 0 ? String(format: "%.0f", self) : String(describing: self)
|
||||
}
|
||||
}
|
||||
28
VDS/Components/PriceLockup/PriceLockupChangeLog.txt
Normal file
28
VDS/Components/PriceLockup/PriceLockupChangeLog.txt
Normal file
@ -0,0 +1,28 @@
|
||||
MM/DD/YYYY
|
||||
----------------
|
||||
|
||||
11/16/2023
|
||||
----------------
|
||||
- Added leadingText and trailingText to anatomy
|
||||
- Added leadingText and trailingText props to configurations
|
||||
- Added term prop to configurations
|
||||
- Removed Suspended orange color and corresponding color tokens
|
||||
|
||||
11/27/2023
|
||||
----------------
|
||||
- Removed “Delimiter” from Anatomy as “Term” includes both delimiter and term
|
||||
- Added “Figma only” badge to leadingText and trailingText in Configurations
|
||||
- Added superscript to “none” under term in Configurations
|
||||
- Added Overflow section to Layout and spacing
|
||||
- Updated Spacing to allow for leading and trailing text
|
||||
|
||||
12/18/23
|
||||
----------------
|
||||
- Updated all pages with spec template updates from Doc Utility Expansion Pack
|
||||
- Added Content props section to Config page
|
||||
|
||||
1/15/24
|
||||
----------------
|
||||
- Clarified strikethrough does not apply to leading or trailing text
|
||||
- Clarified and added to text overflow examples
|
||||
- Correct Success to Savings in the configuration seciton
|
||||
@ -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 {
|
||||
|
||||
@ -66,17 +67,26 @@ open class RadioBoxGroup: SelectorGroupBase<RadioBoxItem>, SelectorGroupSingleSe
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
private func ensureDevice() {
|
||||
var axis: NSLayoutConstraint.Axis = .vertical
|
||||
var distribution: UIStackView.Distribution = .fill
|
||||
|
||||
defer {
|
||||
mainStackView.axis = axis
|
||||
mainStackView.distribution = distribution
|
||||
}
|
||||
|
||||
if UIDevice.isIPad {
|
||||
mainStackView.axis = .horizontal
|
||||
mainStackView.distribution = .fillEqually
|
||||
axis = .horizontal
|
||||
distribution = .fillEqually
|
||||
} else {
|
||||
if UIDevice.current.orientation.isPortrait || UIDevice.current.orientation == .unknown {
|
||||
mainStackView.axis = .vertical
|
||||
mainStackView.distribution = .fill
|
||||
|
||||
} else {
|
||||
mainStackView.axis = .horizontal
|
||||
mainStackView.distribution = .fillEqually
|
||||
guard let supportedOrientations = UIApplication.shared.windows.first?.rootViewController?.supportedInterfaceOrientations else {
|
||||
return
|
||||
}
|
||||
|
||||
let orientation = UIDevice.current.orientation
|
||||
if supportedOrientations.contains(.landscape) && (orientation == .landscapeLeft || orientation == .landscapeRight) {
|
||||
axis = .horizontal
|
||||
distribution = .fillEqually
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -164,11 +165,38 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
/// Executed on initialization for this View.
|
||||
open override func initialSetup() {
|
||||
super.initialSetup()
|
||||
onClick = { control in
|
||||
control.toggle()
|
||||
/// 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
|
||||
selectorView.isAccessibilityElement = true
|
||||
selectorView.accessibilityTraits = .button
|
||||
addSubview(selectorView)
|
||||
selectorView.isUserInteractionEnabled = false
|
||||
|
||||
selectorView.addSubview(selectorStackView)
|
||||
|
||||
selectorStackView.addArrangedSubview(selectorLeftLabelStackView)
|
||||
selectorStackView.addArrangedSubview(subTextRightLabel)
|
||||
selectorLeftLabelStackView.addArrangedSubview(textLabel)
|
||||
selectorLeftLabelStackView.addArrangedSubview(subTextLabel)
|
||||
|
||||
selectorView
|
||||
.pinTop()
|
||||
.pinLeading()
|
||||
.pinTrailing(0, .defaultHigh)
|
||||
.pinBottom(0, .defaultHigh)
|
||||
|
||||
selectorStackView.pinToSuperView(.uniform(VDSLayout.space3X))
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
|
||||
onClick = { [weak self] _ in
|
||||
guard let self, isEnabled else { return }
|
||||
toggle()
|
||||
}
|
||||
|
||||
selectorView.bridge_accessibilityLabelBlock = { [weak self] in
|
||||
@ -203,43 +231,7 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable {
|
||||
|
||||
return accessibilityLabels.joined(separator: ", ")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
|
||||
isAccessibilityElement = false
|
||||
selectorView.isAccessibilityElement = true
|
||||
selectorView.accessibilityTraits = .button
|
||||
addSubview(selectorView)
|
||||
selectorView.isUserInteractionEnabled = false
|
||||
|
||||
selectorView.addSubview(selectorStackView)
|
||||
|
||||
selectorStackView.addArrangedSubview(selectorLeftLabelStackView)
|
||||
selectorStackView.addArrangedSubview(subTextRightLabel)
|
||||
selectorLeftLabelStackView.addArrangedSubview(textLabel)
|
||||
selectorLeftLabelStackView.addArrangedSubview(subTextLabel)
|
||||
|
||||
selectorView
|
||||
.pinTop()
|
||||
.pinLeading()
|
||||
.pinTrailing(0, .defaultHigh)
|
||||
.pinBottom(0, .defaultHigh)
|
||||
|
||||
selectorStackView.pinToSuperView(.uniform(VDSLayout.space3X))
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
shouldUpdateView = false
|
||||
textLabel.reset()
|
||||
subTextLabel.reset()
|
||||
subTextRightLabel.reset()
|
||||
|
||||
textLabel.textStyle = .boldBodyLarge
|
||||
subTextLabel.textStyle = .bodyLarge
|
||||
subTextRightLabel.textStyle = .bodyLarge
|
||||
@ -259,9 +251,14 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable {
|
||||
|
||||
isSelected = false
|
||||
onChange = nil
|
||||
}
|
||||
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
textLabel.reset()
|
||||
subTextLabel.reset()
|
||||
subTextRightLabel.reset()
|
||||
super.reset()
|
||||
}
|
||||
|
||||
/// This will change the state of the Selector and execute the actionBlock if provided.
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -76,12 +77,13 @@ open class RadioButtonGroup: SelectorGroupBase<RadioButtonItem>, SelectorGroupSi
|
||||
}
|
||||
}
|
||||
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
inputId = nil
|
||||
showError = false
|
||||
}
|
||||
|
||||
public override func didSelect(_ selectedControl: RadioButtonItem) {
|
||||
|
||||
open override func didSelect(_ selectedControl: RadioButtonItem) {
|
||||
if let selectedItem {
|
||||
updateToggle(selectedItem)
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -91,8 +92,8 @@ open class Table: View {
|
||||
//--------------------------------------------------
|
||||
|
||||
///Called upon initializing the table view
|
||||
open override func initialSetup() {
|
||||
super.initialSetup()
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
addSubview(matrixView)
|
||||
matrixView.pinToSuperView()
|
||||
}
|
||||
@ -108,18 +109,16 @@ open class Table: View {
|
||||
matrixView.collectionViewLayout.invalidateLayout()
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
striped = false
|
||||
padding = .standard
|
||||
tableHeader = []
|
||||
tableRows = []
|
||||
fillContainer = true
|
||||
columnWidths = nil
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
|
||||
func calculateColumnWidths() -> [CGFloat] {
|
||||
guard let noOfColumns = tableData.first?.columnsCount else { return [] }
|
||||
let itemWidth = floor(matrixView.safeAreaLayoutGuide.layoutFrame.width / CGFloat(noOfColumns))
|
||||
|
||||
@ -11,7 +11,7 @@ import VDSCoreTokens
|
||||
import Combine
|
||||
|
||||
extension Tabs {
|
||||
|
||||
@objcMembers
|
||||
@objc(VDSTab)
|
||||
open class Tab: Control, Groupable {
|
||||
|
||||
@ -150,6 +150,10 @@ extension Tabs {
|
||||
labelLeadingConstraint = label.pinLeading(anchor: layoutGuide.leadingAnchor)
|
||||
labelBottomConstraint = label.pinBottom(anchor: layoutGuide.bottomAnchor, priority: .defaultHigh)
|
||||
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
bridge_accessibilityLabelBlock = { [weak self] in
|
||||
guard let self else { return "" }
|
||||
return text
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -220,10 +221,11 @@ open class Tabs: View {
|
||||
super.layoutSubviews()
|
||||
updateContentView()
|
||||
}
|
||||
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
shouldUpdateView = false
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
onTabDidSelect = nil
|
||||
onTabShouldSelect = nil
|
||||
orientation = .horizontal
|
||||
borderLine = true
|
||||
fillContainer = false
|
||||
@ -234,11 +236,9 @@ open class Tabs: View {
|
||||
selectedIndex = 0
|
||||
size = .medium
|
||||
sticky = false
|
||||
tabViews.forEach{ $0.reset() }
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
tabModels = []
|
||||
}
|
||||
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Methods
|
||||
//--------------------------------------------------
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
@objcMembers
|
||||
@objc(VDSTabsContainer)
|
||||
open class TabsContainer: View {
|
||||
|
||||
|
||||
@ -11,9 +11,7 @@ import VDSCoreTokens
|
||||
import Combine
|
||||
|
||||
/// Base Class used to build out a Input controls.
|
||||
@objc(VDSEntryField)
|
||||
open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
|
||||
open class EntryFieldBase<ValueType>: Control, Changeable, FormFieldInternalValidatable {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
@ -137,8 +135,8 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
|
||||
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)
|
||||
@ -228,11 +226,11 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
open var inputId: String? { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// The text of this textField.
|
||||
open var value: String? {
|
||||
open var value: ValueType? {
|
||||
get { fatalError("must be read from subclass")}
|
||||
}
|
||||
|
||||
open var defaultValue: AnyHashable? { didSet { setNeedsUpdate() } }
|
||||
open var defaultValue: ValueType? { didSet { setNeedsUpdate() } }
|
||||
|
||||
open var isRequired: Bool = false { didSet { setNeedsUpdate() } }
|
||||
|
||||
@ -244,7 +242,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
}
|
||||
}
|
||||
|
||||
open var rules = [AnyRule<String>]()
|
||||
open var rules = [AnyRule<ValueType>]()
|
||||
|
||||
open var accessibilityHintText: String = "Double tap to open"
|
||||
|
||||
@ -315,6 +313,41 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
errorLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable()
|
||||
helperLabel.textColorConfiguration = secondaryColorConfiguration.eraseToAnyColorable()
|
||||
|
||||
}
|
||||
|
||||
/// Updates the UI
|
||||
open override func updateView() {
|
||||
super.updateView()
|
||||
updateRules()
|
||||
updateContainerView()
|
||||
updateContainerWidth()
|
||||
updateTitleLabel()
|
||||
updateErrorLabel()
|
||||
updateHelperLabel()
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
|
||||
titleLabel.textStyle = .bodySmall
|
||||
errorLabel.textStyle = .bodySmall
|
||||
helperLabel.textStyle = .bodySmall
|
||||
|
||||
labelText = nil
|
||||
helperText = nil
|
||||
showError = false
|
||||
errorText = nil
|
||||
tooltipModel = nil
|
||||
transparentBackground = false
|
||||
width = nil
|
||||
inputId = nil
|
||||
defaultValue = nil
|
||||
isRequired = false
|
||||
isReadOnly = false
|
||||
helperTextPlacement = .bottom
|
||||
rules = []
|
||||
onChange = nil
|
||||
|
||||
containerView.bridge_accessibilityLabelBlock = { [weak self] in
|
||||
guard let self else { return "" }
|
||||
var accessibilityLabels = [String]()
|
||||
@ -331,9 +364,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
if let errorText, showError {
|
||||
accessibilityLabels.append("error, \(errorText)")
|
||||
}
|
||||
|
||||
accessibilityLabels.append("\(Self.self)")
|
||||
|
||||
|
||||
return accessibilityLabels.joined(separator: ", ")
|
||||
}
|
||||
|
||||
@ -343,49 +374,23 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
}
|
||||
|
||||
containerView.bridge_accessibilityValueBlock = { [weak self] in
|
||||
guard let self else { return "" }
|
||||
return value
|
||||
guard let self, let value 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()
|
||||
updateContainerView()
|
||||
updateContainerWidth()
|
||||
updateTitleLabel()
|
||||
updateErrorLabel()
|
||||
updateHelperLabel()
|
||||
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
titleLabel.reset()
|
||||
errorLabel.reset()
|
||||
helperLabel.reset()
|
||||
|
||||
titleLabel.textStyle = .bodySmall
|
||||
errorLabel.textStyle = .bodySmall
|
||||
helperLabel.textStyle = .bodySmall
|
||||
|
||||
labelText = nil
|
||||
helperText = nil
|
||||
showError = false
|
||||
errorText = nil
|
||||
tooltipModel = nil
|
||||
transparentBackground = false
|
||||
width = nil
|
||||
inputId = nil
|
||||
defaultValue = nil
|
||||
isRequired = false
|
||||
isReadOnly = false
|
||||
onChange = nil
|
||||
super.reset()
|
||||
}
|
||||
|
||||
open override var canBecomeFirstResponder: Bool {
|
||||
@ -418,7 +423,6 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
}
|
||||
|
||||
open func validate(){
|
||||
updateRules()
|
||||
validator = FormFieldValidator<EntryFieldBase>(field: self, rules: rules)
|
||||
validator?.validate()
|
||||
setNeedsUpdate()
|
||||
@ -524,8 +528,8 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
//--------------------------------------------------
|
||||
internal func updateRules() {
|
||||
rules.removeAll()
|
||||
if isRequired && useRequiredRule {
|
||||
let rule = RequiredRule()
|
||||
if isRequired && useRequiredRule && ValueType.self == String.self {
|
||||
let rule = RequiredRule<ValueType>()
|
||||
if let errorText, !errorText.isEmpty {
|
||||
rule.errorMessage = errorText
|
||||
} else if let labelText{
|
||||
@ -543,7 +547,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
containerView.layer.borderWidth = VDSFormControls.borderWidth
|
||||
containerView.layer.cornerRadius = VDSFormControls.borderRadius
|
||||
}
|
||||
|
||||
|
||||
internal func updateContainerWidth() {
|
||||
widthConstraint?.deactivate()
|
||||
trailingLessThanEqualsConstraint?.deactivate()
|
||||
|
||||
@ -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,6 +84,8 @@ extension InputField {
|
||||
textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition)
|
||||
}
|
||||
|
||||
value = formattedNumber
|
||||
|
||||
// Prevent the default behavior
|
||||
return false
|
||||
|
||||
@ -69,43 +93,45 @@ extension InputField {
|
||||
|
||||
override func textFieldDidEndEditing(_ inputField: InputField, textField: UITextField) {
|
||||
if let text = inputField.text {
|
||||
let rawNumber = text.filter { $0.isNumber }
|
||||
textField.text = formatUSNumber(rawNumber)
|
||||
textField.text = text.formatUSNumber()
|
||||
value = textField.text
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
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,8 +13,9 @@ 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 {
|
||||
open class InputField: EntryFieldBase<String> {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
@ -204,6 +205,22 @@ open class InputField: EntryFieldBase {
|
||||
|
||||
textField.textColorConfiguration = textFieldTextColorConfiguration
|
||||
|
||||
}
|
||||
|
||||
open override func getFieldContainer() -> UIView {
|
||||
return textField
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
textField.text = ""
|
||||
|
||||
successLabel.textStyle = .bodySmall
|
||||
|
||||
fieldType = .text
|
||||
showSuccess = false
|
||||
successText = nil
|
||||
|
||||
containerView.bridge_accessibilityLabelBlock = { [weak self] in
|
||||
guard let self else { return "" }
|
||||
var accessibilityLabels = [String]()
|
||||
@ -258,22 +275,10 @@ open class InputField: EntryFieldBase {
|
||||
}
|
||||
}
|
||||
|
||||
open override func getFieldContainer() -> UIView {
|
||||
return textField
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
textField.text = ""
|
||||
|
||||
successLabel.reset()
|
||||
successLabel.textStyle = .bodySmall
|
||||
|
||||
fieldType = .text
|
||||
showSuccess = false
|
||||
successText = nil
|
||||
helperTextPlacement = .bottom
|
||||
super.reset()
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
@ -311,6 +316,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)
|
||||
@ -364,11 +387,11 @@ extension InputField: UITextFieldDelegate {
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
open func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
||||
|
||||
@ -10,6 +10,7 @@ import UIKit
|
||||
import Combine
|
||||
import VDSCoreTokens
|
||||
|
||||
@objcMembers
|
||||
@objc(VDSTextField)
|
||||
open class TextField: UITextField, ViewProtocol, Errorable {
|
||||
|
||||
@ -97,19 +98,22 @@ open class TextField: UITextField, ViewProtocol, Errorable {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Lifecycle
|
||||
//--------------------------------------------------
|
||||
open func initialSetup() {
|
||||
private func initialSetup() {
|
||||
if !initialSetupPerformed {
|
||||
initialSetupPerformed = true
|
||||
backgroundColor = .clear
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
clipsToBounds = true
|
||||
shouldUpdateView = false
|
||||
setup()
|
||||
setDefaults()
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
open func setup() {
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
clipsToBounds = true
|
||||
|
||||
let accessView = UIView(frame: .init(origin: .zero, size: .init(width: UIScreen.main.bounds.width, height: 44)))
|
||||
accessView.backgroundColor = .white
|
||||
accessView.addBorder(side: .top, width: 1, color: .lightGray)
|
||||
@ -123,6 +127,17 @@ open class TextField: UITextField, ViewProtocol, Errorable {
|
||||
inputAccessoryView = accessView
|
||||
}
|
||||
|
||||
open func setDefaults() {
|
||||
backgroundColor = .clear
|
||||
surface = .light
|
||||
text = nil
|
||||
formatText = nil
|
||||
useScaledFont = false
|
||||
showError = false
|
||||
errorText = nil
|
||||
textStyle = .defaultStyle
|
||||
}
|
||||
|
||||
@objc func doneButtonAction() {
|
||||
// Resigns the first responder status when 'Done' is tapped
|
||||
let _ = resignFirstResponder()
|
||||
@ -173,8 +188,7 @@ open class TextField: UITextField, ViewProtocol, Errorable {
|
||||
|
||||
open func reset() {
|
||||
shouldUpdateView = false
|
||||
surface = .light
|
||||
text = nil
|
||||
setDefaults()
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
@ -7,12 +7,14 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
class RequiredRule: Rule {
|
||||
class RequiredRule<ValueType>: Rule {
|
||||
var maxLength: Int?
|
||||
var errorMessage: String = "This field is required."
|
||||
|
||||
func isValid(value: String?) -> Bool {
|
||||
guard let value, !value.isEmpty, value.count > 0 else { return false }
|
||||
func isValid(value: ValueType?) -> Bool {
|
||||
guard let value,
|
||||
!"\(value)".isEmpty,
|
||||
"\(value)".count > 0 else { return false }
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,8 +12,9 @@ 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 {
|
||||
open class TextArea: EntryFieldBase<String> {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
@ -111,6 +112,8 @@ open class TextArea: EntryFieldBase {
|
||||
}
|
||||
|
||||
didSet {
|
||||
setNeedsUpdate()
|
||||
|
||||
if textView.isFirstResponder {
|
||||
validate()
|
||||
}
|
||||
@ -163,14 +166,19 @@ open class TextArea: EntryFieldBase {
|
||||
bottomContainerStackView.spacing = VDSLayout.space2X
|
||||
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
minHeight = .twoX
|
||||
maxLength = nil
|
||||
textView.text = ""
|
||||
characterCounterLabel.textStyle = .bodySmall
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
textView.text = ""
|
||||
characterCounterLabel.reset()
|
||||
characterCounterLabel.textStyle = .bodySmall
|
||||
setNeedsUpdate()
|
||||
super.reset()
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
@ -191,8 +199,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 {
|
||||
|
||||
@ -10,6 +10,7 @@ import UIKit
|
||||
import Combine
|
||||
import VDSCoreTokens
|
||||
|
||||
@objcMembers
|
||||
@objc(VDSTextView)
|
||||
open class TextView: UITextView, ViewProtocol, Errorable {
|
||||
|
||||
@ -106,17 +107,20 @@ open class TextView: UITextView, ViewProtocol, Errorable {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Lifecycle
|
||||
//--------------------------------------------------
|
||||
open func initialSetup() {
|
||||
private func initialSetup() {
|
||||
if !initialSetupPerformed {
|
||||
initialSetupPerformed = true
|
||||
backgroundColor = .clear
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
shouldUpdateView = false
|
||||
setup()
|
||||
setDefaults()
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
open func setup() {
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let accessView = UIView(frame: .init(origin: .zero, size: .init(width: UIScreen.main.bounds.width, height: 44)))
|
||||
accessView.backgroundColor = .white
|
||||
accessView.addBorder(side: .top, width: 1, color: .lightGray)
|
||||
@ -133,6 +137,15 @@ open class TextView: UITextView, ViewProtocol, Errorable {
|
||||
placeholderLabel.pinToSuperView()
|
||||
}
|
||||
|
||||
open func setDefaults() {
|
||||
backgroundColor = .clear
|
||||
surface = .light
|
||||
text = nil
|
||||
placeholder = nil
|
||||
errorText = nil
|
||||
showError = false
|
||||
}
|
||||
|
||||
@objc func doneButtonAction() {
|
||||
// Resigns the first responder status when 'Done' is tapped
|
||||
resignFirstResponder()
|
||||
@ -152,8 +165,7 @@ open class TextView: UITextView, ViewProtocol, Errorable {
|
||||
|
||||
open func reset() {
|
||||
shouldUpdateView = false
|
||||
surface = .light
|
||||
text = nil
|
||||
setDefaults()
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
@ -10,11 +10,12 @@ import VDSCoreTokens
|
||||
import UIKit
|
||||
import Combine
|
||||
|
||||
@objcMembers
|
||||
@objc(VDSTileContainer)
|
||||
open class TileContainer: TileContainerBase<TileContainer.Padding> {
|
||||
|
||||
/// Enum used to describe the padding choices used for this component.
|
||||
public enum Padding: DefaultValuing {
|
||||
public enum Padding: DefaultValuing, Valuing {
|
||||
case padding3X
|
||||
case padding4X
|
||||
case padding6X
|
||||
@ -43,7 +44,7 @@ open class TileContainer: TileContainerBase<TileContainer.Padding> {
|
||||
}
|
||||
}
|
||||
|
||||
open class TileContainerBase<PaddingType: DefaultValuing>: Control where PaddingType.ValueType == CGFloat {
|
||||
open class TileContainerBase<PaddingType: DefaultValuing & Valuing>: View where PaddingType.ValueType == CGFloat {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
@ -74,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
|
||||
}
|
||||
@ -86,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"
|
||||
@ -109,7 +110,7 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
$0.contentMode = .scaleAspectFill
|
||||
$0.clipsToBounds = true
|
||||
}
|
||||
|
||||
|
||||
open var containerView = View().with {
|
||||
$0.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||
$0.setContentHuggingPriority(.defaultLow, for: .vertical)
|
||||
@ -117,6 +118,8 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
$0.setContentCompressionResistancePriority(.defaultHigh, for: .vertical)
|
||||
}
|
||||
|
||||
private var isHighlighted: Bool = false { didSet { setNeedsUpdate() } }
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
@ -125,27 +128,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? {
|
||||
@ -159,7 +162,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? {
|
||||
@ -179,13 +182,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 aspectRatioConstraint: NSLayoutConstraint?
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Configuration
|
||||
//--------------------------------------------------
|
||||
@ -228,13 +232,13 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
|
||||
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()
|
||||
|
||||
@ -266,7 +270,7 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}.store(in: &subscribers)
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// Overriden to take the hit if there is an onClickSubscriber and the view is not a UIControl
|
||||
@ -277,21 +281,20 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
return view
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
shouldUpdateView = false
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
backgroundImage = nil
|
||||
color = .white
|
||||
aspectRatio = .none
|
||||
backgroundEffect = .none
|
||||
padding = .defaultValue
|
||||
aspectRatio = .ratio1x1
|
||||
imageFallbackColor = .light
|
||||
width = nil
|
||||
height = nil
|
||||
showBorder = false
|
||||
showDropShadow = false
|
||||
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()
|
||||
@ -301,13 +304,14 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
|
||||
containerView.layer.borderColor = borderColorConfiguration.getColor(self).cgColor
|
||||
containerView.layer.borderWidth = showBorder ? VDSFormControls.borderWidth : 0
|
||||
|
||||
|
||||
contentView.removeConstraints()
|
||||
contentView.pinToSuperView(.uniform(padding.value))
|
||||
|
||||
updateContainerView()
|
||||
|
||||
}
|
||||
|
||||
|
||||
open override var accessibilityElements: [Any]? {
|
||||
get {
|
||||
var items = [Any]()
|
||||
@ -328,16 +332,37 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
|
||||
//append all children that are accessible
|
||||
items.append(contentsOf: elements)
|
||||
|
||||
|
||||
return items
|
||||
}
|
||||
set {}
|
||||
}
|
||||
|
||||
open override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
if let onClickSubscriber {
|
||||
isHighlighted = true
|
||||
}
|
||||
}
|
||||
|
||||
open override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
super.touchesEnded(touches, with: event)
|
||||
if let onClickSubscriber {
|
||||
isHighlighted = false
|
||||
}
|
||||
}
|
||||
|
||||
open override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
super.touchesCancelled(touches, with: event)
|
||||
if let onClickSubscriber {
|
||||
isHighlighted = false
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Methods
|
||||
//--------------------------------------------------
|
||||
|
||||
|
||||
/// This will place a view within the contentView of this component.
|
||||
public func addContentView(_ view: UIView, shouldPin: Bool = true) {
|
||||
view.removeFromSuperview()
|
||||
@ -346,7 +371,7 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
view.pinToSuperView()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Methods
|
||||
//--------------------------------------------------
|
||||
@ -379,55 +404,10 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
containerView.backgroundColor = color.withAlphaComponent(alphaConfiguration)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
private func sizeContainerView(width: CGFloat? = nil, height: CGFloat? = nil) {
|
||||
if let width, width > 0 {
|
||||
widthConstraint?.constant = width
|
||||
widthConstraint?.activate()
|
||||
}
|
||||
|
||||
if let height, height > 0 {
|
||||
heightConstraint?.constant = height
|
||||
heightConstraint?.activate()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateContainerView() {
|
||||
applyBackgroundEffects()
|
||||
|
||||
widthConstraint?.deactivate()
|
||||
heightConstraint?.deactivate()
|
||||
|
||||
|
||||
if showDropShadow, surface == .light {
|
||||
containerView.addDropShadow(dropShadowConfiguration)
|
||||
} else {
|
||||
@ -436,50 +416,101 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
|
||||
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
|
||||
|
||||
if width != nil || height != nil {
|
||||
var containerViewWidth: CGFloat?
|
||||
var containerViewHeight: CGFloat?
|
||||
//run logic to determine which to activate
|
||||
if let width, aspectRatio == .none && height == nil{
|
||||
containerViewWidth = width
|
||||
|
||||
} else if let height, aspectRatio == .none && width == nil{
|
||||
containerViewHeight = height
|
||||
|
||||
} else if let height, let width {
|
||||
containerViewWidth = width
|
||||
containerViewHeight = height
|
||||
|
||||
} else if let width {
|
||||
let size = ratioSize(for: width)
|
||||
containerViewWidth = size.width
|
||||
containerViewHeight = size.height
|
||||
//turn off the constraints
|
||||
aspectRatioConstraint?.deactivate()
|
||||
widthConstraint?.deactivate()
|
||||
heightConstraint?.deactivate()
|
||||
|
||||
} else if let height {
|
||||
let size = ratioSize(for: height)
|
||||
containerViewWidth = size.width
|
||||
containerViewHeight = size.height
|
||||
}
|
||||
sizeContainerView(width: containerViewWidth, height: containerViewHeight)
|
||||
//-------------------------------------------------------------------------
|
||||
//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 {
|
||||
if let parentSize = horizontalPinnedSize() {
|
||||
|
||||
var containerViewWidth: CGFloat?
|
||||
var containerViewHeight: CGFloat?
|
||||
|
||||
let size = ratioSize(for: parentSize.width)
|
||||
if aspectRatio == .none {
|
||||
containerViewWidth = size.width
|
||||
} else {
|
||||
containerViewWidth = size.width
|
||||
containerViewHeight = size.height
|
||||
}
|
||||
|
||||
sizeContainerView(width: containerViewWidth, height: containerViewHeight)
|
||||
|
||||
//-------------------------------------------------------------------------
|
||||
//Width Constraint
|
||||
//-------------------------------------------------------------------------
|
||||
if let containerViewWidth,
|
||||
containerViewWidth > 0 {
|
||||
widthConstraint?.constant = containerViewWidth
|
||||
widthConstraint?.activate()
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------------
|
||||
//Height Constraint
|
||||
//-------------------------------------------------------------------------
|
||||
if let containerViewHeight,
|
||||
containerViewHeight > 0 {
|
||||
heightConstraint?.constant = containerViewHeight
|
||||
heightConstraint?.activate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
@ -519,3 +550,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,11 +15,12 @@ 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> {
|
||||
|
||||
/// Enum used to describe the padding choices used for this component.
|
||||
public enum Padding: String, DefaultValuing, CaseIterable {
|
||||
public enum Padding: String, DefaultValuing, Valuing, CaseIterable {
|
||||
case small
|
||||
case large
|
||||
|
||||
@ -378,6 +379,22 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
|
||||
titleLockupSubTitleLabelHeightGreaterThanConstraint?.priority = .defaultHigh
|
||||
titleLockupSubTitleLabelHeightGreaterThanConstraint?.activate()
|
||||
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
aspectRatio = .none
|
||||
color = .black
|
||||
textWidth = nil
|
||||
textPostion = .top
|
||||
|
||||
//models
|
||||
badgeModel = nil
|
||||
titleModel = nil
|
||||
subTitleModel = nil
|
||||
descriptiveIconModel = nil
|
||||
directionalIconModel = nil
|
||||
|
||||
directionalIcon.bridge_accessibilityLabelBlock = { [weak self] in
|
||||
guard let self, let directionalIconModel else { return nil }
|
||||
return directionalIconModel.accessibleText
|
||||
@ -389,22 +406,6 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
shouldUpdateView = false
|
||||
super.reset()
|
||||
aspectRatio = .none
|
||||
color = .black
|
||||
//models
|
||||
badgeModel = nil
|
||||
titleModel = nil
|
||||
subTitleModel = nil
|
||||
descriptiveIconModel = nil
|
||||
directionalIconModel = nil
|
||||
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()
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -279,18 +280,14 @@ open class TitleLockup: View {
|
||||
set {}
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
shouldUpdateView = false
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
textAlignment = .left
|
||||
eyebrowModel = nil
|
||||
titleModel = nil
|
||||
subTitleModel = nil
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
var labelViews = [UIView]()
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
open override func updateView() {
|
||||
super.updateView()
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -54,6 +55,7 @@ open class Toggle: Control, Changeable, FormFieldable {
|
||||
private var leftConstraints: [NSLayoutConstraint] = []
|
||||
private var rightConstraints: [NSLayoutConstraint] = []
|
||||
private var labelConstraints: [NSLayoutConstraint] = []
|
||||
private var toggleConstraints: [NSLayoutConstraint] = []
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Configuration
|
||||
@ -94,7 +96,7 @@ open class Toggle: Control, Changeable, FormFieldable {
|
||||
open var toggleView = ToggleView().with {
|
||||
$0.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
|
||||
$0.isUserInteractionEnabled = false
|
||||
$0.isAccessibilityElement = false
|
||||
$0.isAccessibilityElement = false
|
||||
}
|
||||
|
||||
/// Used in showing the on/off text.
|
||||
@ -147,35 +149,15 @@ open class Toggle: Control, Changeable, FormFieldable {
|
||||
|
||||
open var value: AnyHashable? { isOn }
|
||||
|
||||
/// The natural size for the receiving view, considering only properties of the view itself.
|
||||
open override var intrinsicContentSize: CGSize {
|
||||
if showLabel {
|
||||
label.sizeToFit()
|
||||
let size = CGSize(width: label.frame.width + spacingBetween + toggleContainerSize.width,
|
||||
height: max(toggleContainerSize.height, label.frame.height))
|
||||
return size
|
||||
} else {
|
||||
return toggleContainerSize
|
||||
}
|
||||
}
|
||||
|
||||
open override var shouldHighlight: Bool { false }
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
/// Executed on initialization for this View.
|
||||
open override func initialSetup() {
|
||||
super.initialSetup()
|
||||
onClick = { control in
|
||||
control.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
|
||||
|
||||
isAccessibilityElement = true
|
||||
if #available(iOS 17.0, *) {
|
||||
accessibilityTraits = .toggleButton
|
||||
@ -207,6 +189,59 @@ open class Toggle: Control, Changeable, FormFieldable {
|
||||
label.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor)
|
||||
]
|
||||
|
||||
// Set content hugging priority
|
||||
setContentHuggingPriority(.required, for: .horizontal)
|
||||
|
||||
isAccessibilityElement = true
|
||||
if #available(iOS 17.0, *) {
|
||||
accessibilityTraits = .toggleButton
|
||||
} else {
|
||||
accessibilityTraits = .button
|
||||
}
|
||||
addSubview(label)
|
||||
addSubview(toggleView)
|
||||
|
||||
// Set up initial constraints for label and switch
|
||||
toggleView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
|
||||
|
||||
//toggle
|
||||
toggleConstraints = [
|
||||
toggleView.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor),
|
||||
toggleView.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor)
|
||||
]
|
||||
|
||||
//toggle and label variants
|
||||
labelConstraints = [
|
||||
height(constant: toggleContainerSize.height, priority: .defaultLow),
|
||||
heightGreaterThanEqualTo(constant: toggleContainerSize.height, priority: .defaultHigh),
|
||||
label.topAnchor.constraint(equalTo: topAnchor),
|
||||
label.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
]
|
||||
|
||||
//label-toggle
|
||||
leftConstraints = [
|
||||
label.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
toggleView.leadingAnchor.constraint(equalTo: label.trailingAnchor, constant: spacingBetween),
|
||||
toggleView.trailingAnchor.constraint(equalTo: trailingAnchor)
|
||||
]
|
||||
|
||||
//toggle-label
|
||||
rightConstraints = [
|
||||
toggleView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
label.leadingAnchor.constraint(equalTo: toggleView.trailingAnchor, constant: spacingBetween),
|
||||
label.trailingAnchor.constraint(equalTo: trailingAnchor)
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
|
||||
onClick = { [weak self] _ in
|
||||
guard let self else { return }
|
||||
toggle()
|
||||
}
|
||||
|
||||
bridge_accessibilityValueBlock = { [weak self] in
|
||||
guard let self else { return "" }
|
||||
if showText {
|
||||
@ -215,13 +250,7 @@ open class Toggle: Control, Changeable, FormFieldable {
|
||||
return isSelected ? "On" : "Off"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
shouldUpdateView = false
|
||||
label.reset()
|
||||
|
||||
isEnabled = true
|
||||
isOn = false
|
||||
isAnimated = true
|
||||
@ -233,8 +262,12 @@ open class Toggle: Control, Changeable, FormFieldable {
|
||||
textPosition = .left
|
||||
inputId = nil
|
||||
onChange = nil
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
label.reset()
|
||||
super.reset()
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
@ -260,6 +293,8 @@ open class Toggle: Control, Changeable, FormFieldable {
|
||||
label.isHidden = !showLabel
|
||||
|
||||
if showLabel {
|
||||
NSLayoutConstraint.deactivate(toggleConstraints)
|
||||
|
||||
label.textAlignment = textPosition == .left ? .right : .left
|
||||
label.textStyle = textStyle
|
||||
label.text = statusText
|
||||
@ -278,6 +313,7 @@ open class Toggle: Control, Changeable, FormFieldable {
|
||||
NSLayoutConstraint.deactivate(leftConstraints)
|
||||
NSLayoutConstraint.deactivate(rightConstraints)
|
||||
NSLayoutConstraint.deactivate(labelConstraints)
|
||||
NSLayoutConstraint.activate(toggleConstraints)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -104,18 +105,10 @@ open class ToggleView: Control, Changeable, FormFieldable {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
/// Executed on initialization for this View.
|
||||
open override func initialSetup() {
|
||||
super.initialSetup()
|
||||
onClick = { control in
|
||||
control.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
|
||||
|
||||
isAccessibilityElement = true
|
||||
if #available(iOS 17.0, *) {
|
||||
accessibilityTraits = .toggleButton
|
||||
@ -156,20 +149,21 @@ open class ToggleView: Control, Changeable, FormFieldable {
|
||||
accessibilityLabel = "Toggle"
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
shouldUpdateView = false
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
isOn = false
|
||||
isAnimated = true
|
||||
inputId = nil
|
||||
toggleView.backgroundColor = toggleColorConfiguration.getColor(self)
|
||||
knobView.backgroundColor = knobColorConfiguration.getColor(self)
|
||||
onChange = nil
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
onChange = nil
|
||||
|
||||
onClick = { [weak self] _ in
|
||||
guard let self else { return }
|
||||
toggle()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
open override func updateView() {
|
||||
super.updateView()
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -128,6 +129,16 @@ open class Tooltip: Control, TooltipLaunchable {
|
||||
|
||||
isAccessibilityElement = true
|
||||
accessibilityTraits = .button
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
closeButtonText = "Close"
|
||||
fillColor = .primary
|
||||
size = .medium
|
||||
title = nil
|
||||
content = nil
|
||||
contentView = nil
|
||||
|
||||
onClick = { [weak self] tooltip in
|
||||
guard let self else { return}
|
||||
@ -157,19 +168,7 @@ open class Tooltip: Control, TooltipLaunchable {
|
||||
return isEnabled ? "Double tap to open." : ""
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
shouldUpdateView = false
|
||||
size = .medium
|
||||
title = ""
|
||||
content = ""
|
||||
fillColor = .primary
|
||||
closeButtonText = "Close"
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
open override func updateView() {
|
||||
|
||||
@ -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 {
|
||||
|
||||
//--------------------------------------------------
|
||||
|
||||
@ -84,17 +84,13 @@ open class TrailingTooltipLabel: View, TooltipLaunchable {
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
shouldUpdateView = false
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
labelText = nil
|
||||
labelAttributes = nil
|
||||
labelTextStyle = .defaultStyle
|
||||
labelTextAlignment = .left
|
||||
tooltipModel = nil
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -9,12 +9,12 @@ import Foundation
|
||||
import UIKit
|
||||
import Combine
|
||||
|
||||
public protocol Clickable: ViewProtocol where Self: UIControl {
|
||||
public protocol Clickable: ViewProtocol {
|
||||
/// Sets the primary Subscriber used for the UIControl event .touchUpInside.
|
||||
var onClickSubscriber: AnyCancellable? { get set }
|
||||
}
|
||||
|
||||
extension Clickable {
|
||||
extension Clickable where Self: UIControl {
|
||||
/// Allows the setting of a completion block against the onClickSubscriber cancellable. This will
|
||||
/// completion block will get executed against the UIControl publisher for the 'touchUpInside' action.
|
||||
public var onClick: ((Self) -> ())? {
|
||||
@ -23,7 +23,7 @@ extension Clickable {
|
||||
onClickSubscriber?.cancel()
|
||||
if let newValue {
|
||||
onClickSubscriber = publisher(for: .touchUpInside)
|
||||
.sink { [weak self] c in
|
||||
.sink { [weak self] c in
|
||||
guard let self, self.isEnabled else { return }
|
||||
newValue(c)
|
||||
}
|
||||
@ -34,3 +34,24 @@ extension Clickable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Clickable where Self: UIView {
|
||||
/// Allows the setting of a completion block against the onClickSubscriber cancellable. This will
|
||||
/// completion block will get executed against the UIControl publisher for the 'touchUpInside' action.
|
||||
public var onClick: ((Self) -> ())? {
|
||||
get { return nil }
|
||||
set {
|
||||
onClickSubscriber?.cancel()
|
||||
if let newValue {
|
||||
onClickSubscriber = publisher(for: UITapGestureRecognizer())
|
||||
.sink { [weak self] _ in
|
||||
guard let self, self.isEnabled else { return }
|
||||
newValue(self)
|
||||
}
|
||||
} else {
|
||||
onClickSubscriber = nil
|
||||
}
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,6 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
public protocol DefaultValuing: Valuing {
|
||||
public protocol DefaultValuing {
|
||||
static var defaultValue: Self { get }
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
import Foundation
|
||||
|
||||
/// Protocol used for a FormField object.
|
||||
public protocol FormFieldable {
|
||||
public protocol FormFieldable<ValueType> {
|
||||
associatedtype ValueType = AnyHashable
|
||||
|
||||
/// Unique Id for the Form Field object within a Form.
|
||||
@ -19,7 +19,10 @@ public protocol FormFieldable {
|
||||
}
|
||||
|
||||
/// Protocol for FormFieldable that require internal validation.
|
||||
public protocol FormFieldInternalValidatable: FormFieldable, Errorable {
|
||||
public protocol FormFieldInternalValidatable<ValueType>: 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.
|
||||
@ -69,16 +72,23 @@ public protocol Rule<ValueType> {
|
||||
func isValid(value: ValueType?) -> Bool
|
||||
/// Error Message to be show if the value is invalid.
|
||||
var errorMessage: String { get }
|
||||
/// type of rule
|
||||
var ruleType: String { get }
|
||||
}
|
||||
|
||||
extension Rule {
|
||||
public var ruleType: String { "\(Self.self)" }
|
||||
}
|
||||
|
||||
/// Type Erased Rule for a specific ValueType.
|
||||
public struct AnyRule<ValueType>: Rule {
|
||||
private let _isValid: (ValueType?) -> Bool
|
||||
|
||||
public var ruleType: String
|
||||
public let errorMessage: String
|
||||
|
||||
public init<R: Rule>(_ rule: R) where R.ValueType == ValueType {
|
||||
self._isValid = rule.isValid
|
||||
self.ruleType = rule.ruleType
|
||||
self.errorMessage = rule.errorMessage
|
||||
}
|
||||
|
||||
|
||||
@ -705,11 +705,11 @@ extension LayoutConstraintable {
|
||||
}
|
||||
|
||||
// Method to check if the view is pinned to its superview
|
||||
public func isPinnedToSuperview() -> Bool {
|
||||
isPinnedVerticallyToSuperview() && isPinnedHorizontallyToSuperview()
|
||||
public func isPinnedEqual() -> Bool {
|
||||
isPinnedEqualVertically() && isPinnedEqualHorizontally()
|
||||
}
|
||||
|
||||
public func horizontalPinnedSize() -> CGSize? {
|
||||
public func horizontalPinnedWidth() -> CGFloat? {
|
||||
guard let view = self as? UIView, let superview = view.superview else { return nil }
|
||||
let constraints = superview.constraints
|
||||
|
||||
@ -735,44 +735,106 @@ extension LayoutConstraintable {
|
||||
if let leadingView = leadingObject as? UIView, let trailingView = trailingObject as? UIView {
|
||||
let leadingPosition = leadingView.convert(leadingView.bounds.origin, to: superview).x
|
||||
let trailingPosition = trailingView.convert(trailingView.bounds.origin, to: superview).x + trailingView.bounds.width
|
||||
return CGSize(width: trailingPosition - leadingPosition, height: view.bounds.size.height)
|
||||
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 CGSize(width: trailingPosition - leadingPosition, height: view.bounds.size.height)
|
||||
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 CGSize(width: trailingPosition - leadingPosition, height: view.bounds.size.height)
|
||||
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 CGSize(width: trailingPosition - leadingPosition, height: view.bounds.size.height)
|
||||
return trailingPosition - leadingPosition
|
||||
}
|
||||
|
||||
} else if let pinnedObject = leadingPinnedObject {
|
||||
if let view = pinnedObject as? UIView {
|
||||
return view.bounds.size
|
||||
return view.bounds.size.width
|
||||
} else if let layoutGuide = pinnedObject as? UILayoutGuide {
|
||||
return layoutGuide.layoutFrame.size
|
||||
return layoutGuide.layoutFrame.size.width
|
||||
}
|
||||
|
||||
} else if let pinnedObject = trailingPinnedObject {
|
||||
if let view = pinnedObject as? UIView {
|
||||
return view.bounds.size
|
||||
return view.bounds.size.width
|
||||
} else if let layoutGuide = pinnedObject as? UILayoutGuide {
|
||||
return layoutGuide.layoutFrame.size
|
||||
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 isPinnedHorizontallyToSuperview() -> Bool {
|
||||
public func isPinnedEqualHorizontally() -> Bool {
|
||||
guard let view = self as? UIView, let superview = view.superview else { return false }
|
||||
let constraints = superview.constraints
|
||||
var leadingPinned = false
|
||||
@ -796,7 +858,7 @@ extension LayoutConstraintable {
|
||||
return leadingPinned && trailingPinned
|
||||
}
|
||||
|
||||
public func isPinnedVerticallyToSuperview() -> Bool {
|
||||
public func isPinnedEqualVertically() -> Bool {
|
||||
guard let view = self as? UIView, let superview = view.superview else { return false }
|
||||
let constraints = superview.constraints
|
||||
var topPinned = false
|
||||
|
||||
@ -19,12 +19,12 @@ public protocol ViewProtocol: AnyObject, Initable, Resettable, Enabling, Surface
|
||||
/// Used for setting an implementation for the default Accessible Action
|
||||
var accessibilityAction: ((Self) -> Void)? { get set }
|
||||
|
||||
/// Executed on initialization for this View.
|
||||
func initialSetup()
|
||||
|
||||
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
|
||||
func setup()
|
||||
|
||||
|
||||
/// Default configurations for values and properties. This is called in the setup() and reset().
|
||||
func setDefaults()
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
func updateView()
|
||||
|
||||
|
||||
@ -1,9 +1,18 @@
|
||||
1.0.72
|
||||
----------------
|
||||
- ONEAPP-9311 - InputStepper - Finished
|
||||
- ONEAPP-9314 - PriceLockup - Finished
|
||||
- CXTDT-599736 - All classes refactored workflow setup(), reset(), setDefaults()
|
||||
|
||||
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 - Default to compact padding, when striped is selected.
|
||||
- CXTDT-586497 - Table - Contents alignment based on the row/header
|
||||
- CXTDT-586375 - Table - Issue With Stripe
|
||||
- CXTDT-577463 - InputField - Accessibility - #7
|
||||
- CXTDT-565796 - DropdownSelect – Removed the "Type" from the VoiceOver
|
||||
|
||||
1.0.70
|
||||
----------------
|
||||
|
||||
@ -26,18 +26,21 @@ Using the system allows designers and developers to collaborate more easily and
|
||||
- ``ButtonIcon``
|
||||
- ``ButtonGroup``
|
||||
- ``CalendarBase``
|
||||
- ``Carousel``
|
||||
- ``CarouselScrollbar``
|
||||
- ``Checkbox``
|
||||
- ``CheckboxItem``
|
||||
- ``CheckboxGroup``
|
||||
- ``DropdownSelect``
|
||||
- ``Icon``
|
||||
- ``InputStepper``
|
||||
- ``InputField``
|
||||
- ``Label``
|
||||
- ``Line``
|
||||
- ``Loader``
|
||||
- ``Notification``
|
||||
- ``Pagination``
|
||||
- ``PriceLockup``
|
||||
- ``RadioBoxItem``
|
||||
- ``RadioBoxGroup``
|
||||
- ``RadioButton``
|
||||
|
||||
Loading…
Reference in New Issue
Block a user