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

# Conflicts:
#	VDS/Components/TextFields/EntryFieldBase.swift
This commit is contained in:
Vasavi Kanamarlapudi 2024-07-29 16:40:24 +05:30
commit c120c746d2
73 changed files with 2316 additions and 573 deletions

View File

@ -20,7 +20,10 @@
18A3F12A2BD9298900498E4A /* Calendar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A3F1292BD9298900498E4A /* Calendar.swift */; };
18A65A022B96E848006602CC /* Breadcrumbs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A65A012B96E848006602CC /* Breadcrumbs.swift */; };
18A65A042B96F050006602CC /* BreadcrumbItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A65A032B96F050006602CC /* BreadcrumbItem.swift */; };
18AE87502C06FDA60075F181 /* Carousel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18AE874F2C06FDA60075F181 /* Carousel.swift */; };
18B42AC62C09D197008D6262 /* CarouselSlotAlignmentModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B42AC52C09D197008D6262 /* CarouselSlotAlignmentModel.swift */; };
18B463A42BBD3C46005C4528 /* DropdownOptionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B463A32BBD3C46005C4528 /* DropdownOptionModel.swift */; };
18B9763F2C11BA4A009271DF /* CarouselPaginationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B9763E2C11BA4A009271DF /* CarouselPaginationModel.swift */; };
18FEA1AD2BDD137500A56439 /* CalendarIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1AC2BDD137500A56439 /* CalendarIndicatorModel.swift */; };
18FEA1B52BE0E63600A56439 /* Date+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1B42BE0E63600A56439 /* Date+Extension.swift */; };
445BA07829C07B3D0036A7C5 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 445BA07729C07B3D0036A7C5 /* Notification.swift */; };
@ -154,7 +157,7 @@
EAC58C182BED0E2300BA39FA /* SecurityCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C172BED0E2300BA39FA /* SecurityCode.swift */; };
EAC58C232BF2824200BA39FA /* DatePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C222BF2824200BA39FA /* DatePicker.swift */; };
EAC58C272BF4116200BA39FA /* DatePickerCalendarModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C262BF4116200BA39FA /* DatePickerCalendarModel.swift */; };
EAC58C292BF4118C00BA39FA /* DatePickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C282BF4118C00BA39FA /* DatePickerViewController.swift */; };
EAC58C292BF4118C00BA39FA /* ClearPopoverViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C282BF4118C00BA39FA /* ClearPopoverViewController.swift */; };
EAC71A1D2A2E155A00E47A9F /* Checkbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC71A1C2A2E155A00E47A9F /* Checkbox.swift */; };
EAC71A1F2A2E173D00E47A9F /* RadioButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC71A1E2A2E173D00E47A9F /* RadioButton.swift */; };
EAC846F3294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC846F2294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift */; };
@ -176,6 +179,7 @@
EAF1FE9B29DB1A6000101452 /* Changeable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF1FE9A29DB1A6000101452 /* Changeable.swift */; };
EAF2F4762C231EAA007BFEDC /* AccessibilityActionElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */; };
EAF2F4782C249D72007BFEDC /* AccessibilityUpdatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF2F4772C249D72007BFEDC /* AccessibilityUpdatable.swift */; };
EAF2F4892C2A1075007BFEDC /* AlertViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF2F4882C2A1075007BFEDC /* AlertViewController.swift */; };
EAF7F0952899861000B287F5 /* CheckboxItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0932899861000B287F5 /* CheckboxItem.swift */; };
EAF7F09A2899B17200B287F5 /* CATransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0992899B17200B287F5 /* CATransaction.swift */; };
EAF7F0A0289AB7EC00B287F5 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F09F289AB7EC00B287F5 /* View.swift */; };
@ -222,7 +226,11 @@
18A3F1292BD9298900498E4A /* Calendar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Calendar.swift; sourceTree = "<group>"; };
18A65A012B96E848006602CC /* Breadcrumbs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Breadcrumbs.swift; sourceTree = "<group>"; };
18A65A032B96F050006602CC /* BreadcrumbItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreadcrumbItem.swift; sourceTree = "<group>"; };
18AE874F2C06FDA60075F181 /* Carousel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Carousel.swift; sourceTree = "<group>"; };
18AE87532C06FE610075F181 /* CarouselChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = CarouselChangeLog.txt; sourceTree = "<group>"; };
18B42AC52C09D197008D6262 /* CarouselSlotAlignmentModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselSlotAlignmentModel.swift; sourceTree = "<group>"; };
18B463A32BBD3C46005C4528 /* DropdownOptionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropdownOptionModel.swift; sourceTree = "<group>"; };
18B9763E2C11BA4A009271DF /* CarouselPaginationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselPaginationModel.swift; sourceTree = "<group>"; };
18BDEE812B75316E00452358 /* ButtonIconChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ButtonIconChangeLog.txt; sourceTree = "<group>"; };
18FEA1AC2BDD137500A56439 /* CalendarIndicatorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarIndicatorModel.swift; sourceTree = "<group>"; };
18FEA1B42BE0E63600A56439 /* Date+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extension.swift"; sourceTree = "<group>"; };
@ -372,7 +380,7 @@
EAC58C222BF2824200BA39FA /* DatePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePicker.swift; sourceTree = "<group>"; };
EAC58C242BF2A7FB00BA39FA /* DatePickerChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = DatePickerChangeLog.txt; sourceTree = "<group>"; };
EAC58C262BF4116200BA39FA /* DatePickerCalendarModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickerCalendarModel.swift; sourceTree = "<group>"; };
EAC58C282BF4118C00BA39FA /* DatePickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickerViewController.swift; sourceTree = "<group>"; };
EAC58C282BF4118C00BA39FA /* ClearPopoverViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClearPopoverViewController.swift; sourceTree = "<group>"; };
EAC71A1C2A2E155A00E47A9F /* Checkbox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Checkbox.swift; sourceTree = "<group>"; };
EAC71A1E2A2E173D00E47A9F /* RadioButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButton.swift; sourceTree = "<group>"; };
EAC846F2294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonGroupCollectionViewCell.swift; sourceTree = "<group>"; };
@ -406,6 +414,7 @@
EAF1FE9A29DB1A6000101452 /* Changeable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Changeable.swift; sourceTree = "<group>"; };
EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityActionElement.swift; sourceTree = "<group>"; };
EAF2F4772C249D72007BFEDC /* AccessibilityUpdatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityUpdatable.swift; sourceTree = "<group>"; };
EAF2F4882C2A1075007BFEDC /* AlertViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertViewController.swift; sourceTree = "<group>"; };
EAF7F0932899861000B287F5 /* CheckboxItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckboxItem.swift; sourceTree = "<group>"; };
EAF7F0992899B17200B287F5 /* CATransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CATransaction.swift; sourceTree = "<group>"; };
EAF7F09F289AB7EC00B287F5 /* View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = "<group>"; };
@ -498,6 +507,17 @@
path = Breadcrumbs;
sourceTree = "<group>";
};
18AE874E2C06FD610075F181 /* Carousel */ = {
isa = PBXGroup;
children = (
18AE874F2C06FDA60075F181 /* Carousel.swift */,
18B9763E2C11BA4A009271DF /* CarouselPaginationModel.swift */,
18B42AC52C09D197008D6262 /* CarouselSlotAlignmentModel.swift */,
18AE87532C06FE610075F181 /* CarouselChangeLog.txt */,
);
path = Carousel;
sourceTree = "<group>";
};
440B84C82BD8E0CE004A732A /* Table */ = {
isa = PBXGroup;
children = (
@ -668,6 +688,7 @@
18A65A002B96E7E1006602CC /* Breadcrumbs */,
EA0FC2BE2912D18200DF80B4 /* Buttons */,
18A3F1202BD8F5DE00498E4A /* Calendar */,
18AE874E2C06FD610075F181 /* Carousel */,
1808BEBA2BA41B1D00129230 /* CarouselScrollbar */,
EAF7F092289985E200B287F5 /* Checkbox */,
EAC58C1F2BF127F000BA39FA /* DatePicker */,
@ -760,9 +781,11 @@
isa = PBXGroup;
children = (
EA985C1C296CD13600F2FF2E /* BundleManager.swift */,
EAC58C282BF4118C00BA39FA /* ClearPopoverViewController.swift */,
EAF7F0B8289C139800B287F5 /* ColorConfiguration.swift */,
EAB5FEF02927F4AA00998C17 /* SelfSizingCollectionView.swift */,
EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */,
EAF2F4882C2A1075007BFEDC /* AlertViewController.swift */,
);
path = Classes;
sourceTree = "<group>";
@ -980,7 +1003,6 @@
children = (
EAC58C222BF2824200BA39FA /* DatePicker.swift */,
EAC58C262BF4116200BA39FA /* DatePickerCalendarModel.swift */,
EAC58C282BF4118C00BA39FA /* DatePickerViewController.swift */,
EAC58C242BF2A7FB00BA39FA /* DatePickerChangeLog.txt */,
);
path = DatePicker;
@ -1303,6 +1325,7 @@
EA5F86D02A1F936100BC83E4 /* TabsContainer.swift in Sources */,
EAF7F0B1289B177F00B287F5 /* ColorLabelAttribute.swift in Sources */,
EAC9258F2911C9DE00091998 /* EntryFieldBase.swift in Sources */,
18B9763F2C11BA4A009271DF /* CarouselPaginationModel.swift in Sources */,
EAB1D2EA28AE84AA00DAE764 /* UIControlPublisher.swift in Sources */,
EAD068922A560B65002E3A2D /* LoaderViewController.swift in Sources */,
44BD43B62C04866600644F87 /* TableRowModel.swift in Sources */,
@ -1314,18 +1337,20 @@
EA8E40932A82889500934ED3 /* TooltipDialog.swift in Sources */,
44604AD429CE186A00E62B51 /* NotificationButtonModel.swift in Sources */,
EAD8D2C128BFDE8B006EB6A6 /* UIGestureRecognizer+Publisher.swift in Sources */,
18B42AC62C09D197008D6262 /* CarouselSlotAlignmentModel.swift in Sources */,
71B23C2D2B91FA690027F7D9 /* Pagination.swift in Sources */,
EA0D1C372A681CCE00E5C127 /* ToggleView.swift in Sources */,
EAF7F0B9289C139800B287F5 /* ColorConfiguration.swift in Sources */,
EA3361BD288B2C760071C351 /* TypeAlias.swift in Sources */,
EAC58C0A2BED004E00BA39FA /* FieldType.swift in Sources */,
EA471F3A2A95587500CE9E58 /* LayoutConstraintable.swift in Sources */,
EAC58C292BF4118C00BA39FA /* DatePickerViewController.swift in Sources */,
EAC58C292BF4118C00BA39FA /* ClearPopoverViewController.swift in Sources */,
EAF193432C134F3800C68D18 /* TableCellItem.swift in Sources */,
EAB1D2CF28ABEF2B00DAE764 /* Typography+Base.swift in Sources */,
EA0D1C3B2A6AD51B00E5C127 /* Typogprahy+Styles.swift in Sources */,
EAF7F09A2899B17200B287F5 /* CATransaction.swift in Sources */,
EAC58C162BED0E0300BA39FA /* InlineAction.swift in Sources */,
EAF2F4892C2A1075007BFEDC /* AlertViewController.swift in Sources */,
EA0D1C3D2A6AD57600E5C127 /* Typography+Enums.swift in Sources */,
EAF1FE9B29DB1A6000101452 /* Changeable.swift in Sources */,
EAC58C0C2BED01D500BA39FA /* Telephone.swift in Sources */,
@ -1351,6 +1376,7 @@
EA0B18052A9E2D2D00F2D0CD /* SelectorBase.swift in Sources */,
EAC71A1D2A2E155A00E47A9F /* Checkbox.swift in Sources */,
EAF7F0AB289B13FD00B287F5 /* TextStyleLabelAttribute.swift in Sources */,
18AE87502C06FDA60075F181 /* Carousel.swift in Sources */,
EAB1D29C28A5618900DAE764 /* RadioButtonGroup.swift in Sources */,
EA81410B2A0E8E3C004F60D2 /* ButtonIcon.swift in Sources */,
EA985BE629688F6A00F2FF2E /* TileletBadgeModel.swift in Sources */,
@ -1547,7 +1573,7 @@
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 67;
CURRENT_PROJECT_VERSION = 71;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = "";
DYLIB_COMPATIBILITY_VERSION = 1;
@ -1585,7 +1611,7 @@
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 67;
CURRENT_PROJECT_VERSION = 71;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = "";
DYLIB_COMPATIBILITY_VERSION = 1;

View File

@ -10,6 +10,7 @@ import UIKit
import Combine
/// Base Class use to build Controls.
@objcMembers
@objc(VDSControl)
open class Control: UIControl, ViewProtocol, UserInfoable, Clickable {
//--------------------------------------------------
@ -129,7 +130,6 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable {
//--------------------------------------------------
open var accessibilityAction: ((Control) -> Void)?
private var _isAccessibilityElement: Bool = false
open override var isAccessibilityElement: Bool {
get {
var block: AXBoolReturnBlock?
@ -137,7 +137,7 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable {
// if #available(iOS 17, *) {
// block = isAccessibilityElementBlock
// }
if block == nil {
block = bridge_isAccessibilityElementBlock
}
@ -145,15 +145,14 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable {
if let block {
return block()
} else {
return _isAccessibilityElement
return super.isAccessibilityElement
}
}
set {
_isAccessibilityElement = newValue
super.isAccessibilityElement = newValue
}
}
private var _accessibilityLabel: String?
open override var accessibilityLabel: String? {
get {
var block: AXStringReturnBlock?
@ -168,15 +167,14 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable {
if let block {
return block()
} else {
return _accessibilityLabel
return super.accessibilityLabel
}
}
set {
_accessibilityLabel = newValue
super.accessibilityLabel = newValue
}
}
private var _accessibilityHint: String?
open override var accessibilityHint: String? {
get {
var block: AXStringReturnBlock?
@ -191,15 +189,14 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable {
if let block {
return block()
} else {
return _accessibilityHint
return super.accessibilityHint
}
}
set {
_accessibilityHint = newValue
super.accessibilityHint = newValue
}
}
private var _accessibilityValue: String?
open override var accessibilityValue: String? {
get {
var block: AXStringReturnBlock?
@ -215,11 +212,11 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable {
if let block{
return block()
} else {
return _accessibilityValue
return super.accessibilityValue
}
}
set {
_accessibilityValue = newValue
super.accessibilityValue = newValue
}
}

View File

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

View File

@ -10,6 +10,7 @@ import UIKit
import Combine
/// Base Class used to build Views.
@objcMembers
@objc(VDSView)
open class View: UIView, ViewProtocol, UserInfoable {
@ -96,7 +97,6 @@ open class View: UIView, ViewProtocol, UserInfoable {
//--------------------------------------------------
open var accessibilityAction: ((View) -> Void)?
private var _isAccessibilityElement: Bool = false
open override var isAccessibilityElement: Bool {
get {
var block: AXBoolReturnBlock?
@ -112,22 +112,21 @@ open class View: UIView, ViewProtocol, UserInfoable {
if let block {
return block()
} else {
return _isAccessibilityElement
return super.isAccessibilityElement
}
}
set {
_isAccessibilityElement = newValue
super.isAccessibilityElement = newValue
}
}
private var _accessibilityLabel: String?
open override var accessibilityLabel: String? {
get {
var block: AXStringReturnBlock?
// if #available(iOS 17, *) {
// block = accessibilityLabelBlock
// }
//
if block == nil {
block = bridge_accessibilityLabelBlock
}
@ -135,15 +134,14 @@ open class View: UIView, ViewProtocol, UserInfoable {
if let block {
return block()
} else {
return _accessibilityLabel
return super.accessibilityLabel
}
}
set {
_accessibilityLabel = newValue
super.accessibilityLabel = newValue
}
}
private var _accessibilityHint: String?
open override var accessibilityHint: String? {
get {
var block: AXStringReturnBlock?
@ -158,15 +156,14 @@ open class View: UIView, ViewProtocol, UserInfoable {
if let block {
return block()
} else {
return _accessibilityHint
return super.accessibilityHint
}
}
set {
_accessibilityHint = newValue
super.accessibilityHint = newValue
}
}
private var _accessibilityValue: String?
open override var accessibilityValue: String? {
get {
var block: AXStringReturnBlock?
@ -182,11 +179,11 @@ open class View: UIView, ViewProtocol, UserInfoable {
if let block{
return block()
} else {
return _accessibilityValue
return super.accessibilityValue
}
}
set {
_accessibilityValue = newValue
super.accessibilityValue = newValue
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,6 +11,7 @@ import VDSCoreTokens
import Combine
/// Base class used for UIButton type classes.
@objcMembers
@objc(VDSButtonBase)
open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable {
@ -177,7 +178,6 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable {
//--------------------------------------------------
open var accessibilityAction: ((ButtonBase) -> Void)?
private var _isAccessibilityElement: Bool = false
open override var isAccessibilityElement: Bool {
get {
var block: AXBoolReturnBlock?
@ -193,15 +193,14 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable {
if let block {
return block()
} else {
return _isAccessibilityElement
return super.isAccessibilityElement
}
}
set {
_isAccessibilityElement = newValue
super.isAccessibilityElement = newValue
}
}
private var _accessibilityLabel: String?
open override var accessibilityLabel: String? {
get {
var block: AXStringReturnBlock?
@ -216,15 +215,14 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable {
if let block {
return block()
} else {
return _accessibilityLabel
return super.accessibilityLabel
}
}
set {
_accessibilityLabel = newValue
super.accessibilityLabel = newValue
}
}
private var _accessibilityHint: String?
open override var accessibilityHint: String? {
get {
var block: AXStringReturnBlock?
@ -239,15 +237,14 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable {
if let block {
return block()
} else {
return _accessibilityHint
return super.accessibilityHint
}
}
set {
_accessibilityHint = newValue
super.accessibilityHint = newValue
}
}
private var _accessibilityValue: String?
open override var accessibilityValue: String? {
get {
var block: AXStringReturnBlock?
@ -263,11 +260,11 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable {
if let block{
return block()
} else {
return _accessibilityValue
return super.accessibilityValue
}
}
set {
_accessibilityValue = newValue
super.accessibilityValue = newValue
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,566 @@
//
// Carousel.swift
// VDS
//
// Created by Kanamarlapudi, Vasavi on 29/05/24.
//
import Foundation
import UIKit
import VDSCoreTokens
import Combine
/// A carousel is a collection of related content in a row that a customer can navigate through horizontally.
/// Use this component to show content that is supplementary, not essential for task completion.
@objcMembers
@objc(VDSCarousel)
open class Carousel: View {
//--------------------------------------------------
// MARK: - Initializers
//--------------------------------------------------
required public init() {
super.init(frame: .zero)
}
public override init(frame: CGRect) {
super.init(frame: .zero)
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
}
//--------------------------------------------------
// MARK: - Enums
//--------------------------------------------------
/// Enum used to describe the pagination display for this component.
public enum PaginationDisplay: String, CaseIterable {
case persistent, none
}
/// Enum used to describe the peek for this component.
/// This is how much a tile is partially visible. It is measured by the distance between the edge of
/// the tile and the edge of the viewport or carousel container. A peek can appear on the left and/or
/// right edge of the carousel container or viewport, depending on the carousels scroll position.
public enum Peek: String, CaseIterable {
case standard, minimum, none
}
/// Enum used to describe the vertical of slotAlignment.
public enum Vertical: String, CaseIterable {
case top, middle, bottom
}
/// Enum used to describe the horizontal of slotAlignment.
public enum Horizontal: String, CaseIterable {
case left, center, right
}
/// Space between each tile. The default value will be 6X in tablet and 3X in mobile.
public enum Gutter: String, CaseIterable , DefaultValuing {
case gutter3X = "3X"
case gutter6X = "6X"
public static var defaultValue: Self { UIDevice.isIPad ? .gutter6X : .gutter3X }
public var value: CGFloat {
switch self {
case .gutter3X:
VDSLayout.space3X
case .gutter6X:
VDSLayout.space6X
}
}
}
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
/// views used to render view in the carousel slots.
open var views: [UIView] = [] { didSet { setNeedsUpdate() } }
/// Space between each tile. The default value will be 6X in tablet and 3X in mobile.
open var gutter: Gutter = Gutter.defaultValue { didSet { setNeedsUpdate() } }
/// The amount of slides visible in the carousel container at one time.
/// The default value will be 3UP in tablet and 1UP in mobile.
open var layout: CarouselScrollbar.Layout = UIDevice.isIPad ? .threeUP : .oneUP {
didSet {
carouselScrollBar.position = 0
setNeedsUpdate()
}
}
/// A callback when moving the carousel. Returns selectedGroupIndex.
open var onChange: ((Int) -> Void)? {
get { nil }
set {
onChangeCancellable?.cancel()
if let newValue {
onChangeCancellable = onChangePublisher
.sink { c in
newValue(c)
}
}
}
}
/// Config object for pagination.
open var pagination: CarouselPaginationModel = .init(kind: .lowContrast, floating: true) { didSet {setNeedsUpdate() } }
/// If provided, will determine the conditions to render the pagination arrows.
open var paginationDisplay: PaginationDisplay = .none { didSet {setNeedsUpdate() } }
/// If provided, will apply margin to pagination arrows. Can be set to either positive or negative values.
/// The default value will be 3X in tablet and 2X in mobile. These values are the default in order to avoid overlapping content within the carousel.
open var paginationInset: CGFloat = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space2X { didSet { updatePaginationInset() } }
/// Options for user to configure the partially-visible tile in group.
/// Setting peek to 'none' will display arrow navigation icons on mobile devices.
open var peek: Peek = .standard { didSet { setNeedsUpdate() } }
/// The initial visible slide's index in the carousel.
open var groupIndex: Int = 0 { didSet { setNeedsUpdate() } }
/// If provided, will set the alignment for slot content when the slots has different heights.
open var slotAlignment: CarouselSlotAlignmentModel? = .init(vertical: .top, horizontal: .left) { didSet { setNeedsUpdate() } }
//--------------------------------------------------
// MARK: - Private Properties
//--------------------------------------------------
internal var containerSize: CGSize { CGSize(width: frame.size.width, height: 44) }
private let contentStackView = UIStackView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.axis = .vertical
$0.distribution = .fill
$0.spacing = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space1X
$0.backgroundColor = .clear
}
internal var carouselScrollBar = CarouselScrollbar().with {
$0.layout = UIDevice.isIPad ? .threeUP : .oneUP
$0.position = 0
$0.backgroundColor = .clear
}
internal var containerView = View().with {
$0.clipsToBounds = true
$0.backgroundColor = .clear
}
internal var scrollContainerView = View().with {
$0.clipsToBounds = true
$0.backgroundColor = .clear
}
private var scrollView = UIScrollView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.backgroundColor = .clear
}
/// Previous button to show previous slide.
private var previousButton = ButtonIcon().with {
$0.kind = .lowContrast
$0.iconName = .leftCaret
$0.iconOffset = .init(x: -2, y: 0)
$0.customContainerSize = UIDevice.isIPad ? 40 : 28
$0.icon.customSize = UIDevice.isIPad ? 16 : 12
}
/// Next button to show next slide.
private var nextButton = ButtonIcon().with {
$0.kind = .lowContrast
$0.iconName = .rightCaret
$0.iconOffset = .init(x: 2, y: 0)
$0.customContainerSize = UIDevice.isIPad ? 40 : 28
$0.icon.customSize = UIDevice.isIPad ? 16 : 12
}
/// A publisher for when moving the carousel. Passes parameters selectedGroupIndex (position).
open var onChangePublisher = PassthroughSubject<Int, Never>()
private var onChangeCancellable: AnyCancellable?
private var containerStackHeightConstraint: NSLayoutConstraint?
private var containerViewHeightConstraint: NSLayoutConstraint?
private var prevButtonLeadingConstraint: NSLayoutConstraint?
private var nextButtonTrailingConstraint: NSLayoutConstraint?
// The scrollbar has top 5X space. So the expected top space is adjusted for tablet and mobile.
let scrollbarTopSpace = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space1X
var slotDefaultHeight = 50.0
var peekMinimum = 24.0
var minimumSlotWidth = 0.0
//--------------------------------------------------
// MARK: - Lifecycle
//--------------------------------------------------
/// Executed on initialization for this View.
open override func initialSetup() {
super.initialSetup()
}
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
open override func setup() {
super.setup()
isAccessibilityElement = false
// Add containerView
addSubview(containerView)
containerView
.pinTop()
.pinBottom()
.pinLeading()
.pinTrailing()
.heightGreaterThanEqualTo(containerSize.height)
containerView.centerYAnchor.constraint(equalTo: centerYAnchor).activate()
// Add content stackview
containerView.addSubview(contentStackView)
// Add scrollview
scrollContainerView.addSubview(scrollView)
scrollView.pinToSuperView()
// Add pagination button icons
scrollContainerView.addSubview(previousButton)
previousButton
.pinLeadingGreaterThanOrEqualTo()
.pinCenterY()
scrollContainerView.addSubview(nextButton)
nextButton
.pinTrailingLessThanOrEqualTo()
.pinCenterY()
// Add scroll container view & carousel scrollbar
contentStackView.addArrangedSubview(scrollContainerView)
contentStackView.addArrangedSubview(carouselScrollBar)
contentStackView.setCustomSpacing(scrollbarTopSpace, after: scrollContainerView)
contentStackView
.pinTop()
.pinBottom()
.pinLeading()
.pinTrailing()
.heightGreaterThanEqualTo(containerSize.height)
addlisteners()
updatePaginationInset()
}
/// Used to make changes to the View based off a change events or from local properties.
open override func updateView() {
super.updateView()
carouselScrollBar.numberOfSlides = views.count
carouselScrollBar.layout = layout
if (carouselScrollBar.position == 0 || carouselScrollBar.position > carouselScrollBar.numberOfSlides) {
carouselScrollBar.position = 1
}
carouselScrollBar.isHidden = (totalPositions() <= 1) ? true : false
// Mobile/Tablet layouts without peek - must show pagination controls.
// If peek is none, pagination controls should show. So set to persistent.
if peek == .none {
paginationDisplay = .persistent
}
// Minimum (Mobile only) Supported only on Mobile viewports. If a user passes Minimum for tablet carousel, the peek reverts to Standard.
if UIDevice.isIPad && peek == .minimum {
peek = .standard
}
// Standard(Default) Peek - Supported for all Tablet viewports and layouts. Supported only for 1up layouts on Mobile viewports.
if peek == .standard && !UIDevice.isIPad && layout != CarouselScrollbar.Layout.oneUP {
peek = .minimum
}
updatePaginationControls()
addCarouselSlots()
}
/// Resets to default settings.
open override func reset() {
super.reset()
shouldUpdateView = false
layout = UIDevice.isIPad ? .threeUP : .oneUP
pagination = .init(kind: .lowContrast, floating: true)
paginationDisplay = .none
paginationInset = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space2X
gutter = UIDevice.isIPad ? .gutter6X : .gutter3X
peek = .standard
}
//--------------------------------------------------
// MARK: - Private Methods
//--------------------------------------------------
private func addlisteners() {
nextButton.onClick = { _ in self.nextButtonClick() }
previousButton.onClick = { _ in self.previousButtonClick() }
/// Will be called when the scrubber position changes.
carouselScrollBar.onScrubberDrag = { [weak self] scrubberId in
guard let self else { return }
updateScrollPosition(position: scrubberId, callbackText:"onThumbPositionChange")
}
/// Will be called when the scrollbar thumb move forward.
carouselScrollBar.onMoveForward = { [weak self] scrubberId in
guard let self else { return }
updateScrollPosition(position: scrubberId, callbackText:"onMoveForward")
}
/// Will be called when the scrollbar thumb move backward.
carouselScrollBar.onMoveBackward = { [weak self] scrubberId in
guard let self else { return }
updateScrollPosition(position: scrubberId, callbackText:"onMoveBackward")
}
/// Will be called when the scrollbar thumb touch start.
carouselScrollBar.onThumbTouchStart = { [weak self] scrubberId in
guard let self else { return }
updateScrollPosition(position: scrubberId, callbackText:"onThumbTouchStart")
}
/// Will be called when the scrollbar thumb touch end.
carouselScrollBar.onThumbTouchEnd = { [weak self] scrubberId in
guard let self else { return }
updateScrollPosition(position: scrubberId, callbackText:"onThumbTouchEnd")
}
}
// Update pagination buttons with selected surface, kind, floating values
private func updatePaginationControls() {
containerView.surface = surface
showPaginationControls()
previousButton.kind = pagination.kind
previousButton.floating = pagination.floating
nextButton.kind = pagination.kind
nextButton.floating = pagination.floating
previousButton.surface = surface
nextButton.surface = surface
}
// Show/Hide pagination buttons of Carousel based on First or Middle or Last
private func showPaginationControls() {
if carouselScrollBar.numberOfSlides == layout.value {
previousButton.isHidden = true
nextButton.isHidden = true
} else {
previousButton.isHidden = (carouselScrollBar.position == 1) || (paginationDisplay == .none)
nextButton.isHidden = (carouselScrollBar.position == totalPositions()) || (paginationDisplay == .none)
}
}
private func estimateHeightFor(component: UIView, with itemWidth: CGFloat) -> CGFloat {
let maxSize = CGSize(width: itemWidth, height: CGFloat.greatestFiniteMagnitude)
let estItemSize = component.systemLayoutSizeFitting(maxSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
return estItemSize.height
}
private func fetchCarouselHeight() -> CGFloat {
var height = slotDefaultHeight
if views.count > 0 {
for index in 0...views.count - 1 {
let estHeight = estimateHeightFor(component: views[index], with: minimumSlotWidth)
height = estHeight > height ? estHeight : height
}
}
return height
}
// Add carousel slots and load data if any
private func addCarouselSlots() {
getSlotWidth()
if containerView.frame.size.width > 0 {
containerViewHeightConstraint?.isActive = false
containerStackHeightConstraint?.isActive = false
let slotHeight = fetchCarouselHeight()
// Perform a loop to iterate each subView
scrollView.subviews.forEach { subView in
// Removing subView from its parent view
subView.removeFromSuperview()
}
// Add carousel items
if views.count > 0 {
var xPos = 0.0
for index in 0...views.count - 1 {
// Add Carousel Slot
let carouselSlot = View().with {
$0.clipsToBounds = true
}
scrollView.addSubview(carouselSlot)
scrollView.delegate = self
carouselSlot
.pinTop()
.pinBottom()
.pinLeading(xPos)
.width(minimumSlotWidth)
.height(slotHeight)
xPos = xPos + minimumSlotWidth + gutter.value
let component = views[index]
carouselSlot.addSubview(component)
setSlotAlignment(contentView: component)
}
scrollView.contentSize = CGSize(width: xPos - gutter.value, height: slotHeight)
}
let containerHeight = slotHeight + scrollbarTopSpace + containerSize.height
if carouselScrollBar.isHidden {
containerStackHeightConstraint = contentStackView.heightAnchor.constraint(equalToConstant: slotHeight)
containerViewHeightConstraint = containerView.heightAnchor.constraint(equalToConstant: slotHeight)
} else {
containerStackHeightConstraint = contentStackView.heightAnchor.constraint(equalToConstant: containerHeight)
containerViewHeightConstraint = containerView.heightAnchor.constraint(equalToConstant: containerHeight)
}
containerViewHeightConstraint?.isActive = true
containerStackHeightConstraint?.isActive = true
}
}
// Set slot alignment if provided. Used only when slot content have different heights or widths.
private func setSlotAlignment(contentView: UIView) {
switch slotAlignment?.vertical {
case .top:
contentView
.pinTop()
.pinBottomLessThanOrEqualTo()
case .middle:
contentView
.pinTopGreaterThanOrEqualTo()
.pinBottomLessThanOrEqualTo()
.pinCenterY()
case .bottom:
contentView
.pinTopGreaterThanOrEqualTo()
.pinBottom()
default: break
}
switch slotAlignment?.horizontal {
case .left:
contentView
.pinLeading()
.pinTrailingLessThanOrEqualTo()
case .center:
contentView
.pinLeadingGreaterThanOrEqualTo()
.pinTrailingLessThanOrEqualTo()
.pinCenterX()
case .right:
contentView
.pinLeadingGreaterThanOrEqualTo()
.pinTrailing()
default: break
}
}
// Get the slot width relative to the peak
private func getSlotWidth() {
let actualWidth = containerView.frame.size.width
let isScrollbarSuppressed = views.count > 0 && layout.value == views.count
let isPeekMinimumOnTablet = UIDevice.isIPad && peek == .minimum
let isPeekNone: Bool = peek == .none
minimumSlotWidth = isScrollbarSuppressed || isPeekMinimumOnTablet || isPeekNone ? actualWidth - ((CGFloat(layout.value)-1) * gutter.value): actualWidth - (CGFloat(layout.value) * gutter.value)
if !isScrollbarSuppressed {
switch peek {
case .standard:
// Standard(Default) Peek - Supported for all Tablet viewports and layouts. Supported only for 1up layouts on Mobile viewports.
if UIDevice.isIPad {
minimumSlotWidth = minimumSlotWidth - (minimumSlotWidth/(CGFloat(layout.value) + 3))
} else if layout == .oneUP {
minimumSlotWidth = minimumSlotWidth - (minimumSlotWidth/4)
}
case .minimum:
// Peek Mimumum Width: 24px from edge of container (at the default view of the carousel with one peek visible)
// Minimum (Mobile only) Supported only on Mobile viewports. If a user passes Minimum for tablet carousel, the peek reverts to Standard.
minimumSlotWidth = isPeekMinimumOnTablet ? minimumSlotWidth : minimumSlotWidth - peekMinimum - gutter.value
case .none:
break
}
}
minimumSlotWidth = ceil(minimumSlotWidth / CGFloat(layout.value))
}
private func nextButtonClick() {
carouselScrollBar.position = carouselScrollBar.position+1
showPaginationControls()
updateScrollPosition(position: carouselScrollBar.position, callbackText:"pageControlClicks")
}
private func previousButtonClick() {
carouselScrollBar.position = carouselScrollBar.position-1
showPaginationControls()
updateScrollPosition(position: carouselScrollBar.position, callbackText:"pageControlClicks")
}
private func updatePaginationInset() {
prevButtonLeadingConstraint?.isActive = false
nextButtonTrailingConstraint?.isActive = false
prevButtonLeadingConstraint = previousButton.leadingAnchor.constraint(equalTo: scrollContainerView.leadingAnchor, constant: paginationInset)
nextButtonTrailingConstraint = nextButton.trailingAnchor.constraint(equalTo: scrollContainerView.trailingAnchor, constant: -paginationInset)
prevButtonLeadingConstraint?.isActive = true
nextButtonTrailingConstraint?.isActive = true
}
private func updateScrollbarPosition(targetContentOffsetXPos:CGFloat) {
let scrollContentSizeWidth = scrollView.contentSize.width
let totalPositions = totalPositions()
let layoutSpace = Int (floor( Double(scrollContentSizeWidth / Double(totalPositions))))
let remindSpace = Int(targetContentOffsetXPos) % layoutSpace
var contentPos = (Int(targetContentOffsetXPos) / layoutSpace) + 1
contentPos = remindSpace > layoutSpace/2 ? contentPos+1 : contentPos
carouselScrollBar.position = contentPos
updateScrollPosition(position: contentPos, callbackText: "ScrollViewMoved")
}
// Update scrollview offset relative to scrollbar thumb position
private func updateScrollPosition(position: Int, callbackText: String) {
if carouselScrollBar.numberOfSlides > 0 {
let scrollContentSizeWidth = scrollView.contentSize.width
let totalPositions = totalPositions()
var xPos = 0.0
if position == 1 {
xPos = 0.0
} else if position == totalPositions {
xPos = scrollContentSizeWidth - containerView.frame.size.width
} else {
let isScrollbarSuppressed = views.count > 0 && layout.value == views.count
let isPeekMinimumOnTablet = UIDevice.isIPad && peek == .minimum
if !isScrollbarSuppressed {
let slotWidthWithGutter = minimumSlotWidth + gutter.value
let xPosition = CGFloat( Float(position-1) * Float(layout.value) * Float(slotWidthWithGutter))
let peekWidth = (containerView.frame.size.width - gutter.value - (Double(layout.value) * (minimumSlotWidth + gutter.value)))/2
xPos = (peek == .none || isPeekMinimumOnTablet) ? xPosition : xPosition - gutter.value - peekWidth
}
}
carouselScrollBar.scrubberId = position+1
let yPos = scrollView.contentOffset.y
scrollView.setContentOffset(CGPoint(x: xPos, y: yPos), animated: true)
showPaginationControls()
groupIndex = position-1
onChangePublisher.send(groupIndex)
}
}
// Get the overall positions of the carousel scrollbar relative to the slides and selected layout
private func totalPositions() -> Int {
return Int (ceil (Double(carouselScrollBar.numberOfSlides) / Double(layout.value)))
}
}
extension Carousel: UIScrollViewDelegate {
//--------------------------------------------------
// MARK: - UIScrollView Delegate
//--------------------------------------------------
public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
updateScrollbarPosition(targetContentOffsetXPos: targetContentOffset.pointee.x)
}
}

View 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

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

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

View File

@ -12,6 +12,7 @@ import Combine
/// A carousel scrollbar is a control that allows to navigate between items in a carousel.
/// It's also a status indicator that conveys the relative amount of content in a carousel and a location within it.
@objcMembers
@objc(VDSCarouselScrollbar)
open class CarouselScrollbar: View {
@ -45,13 +46,13 @@ open class CarouselScrollbar: View {
}
/// The number of slides that can appear at once in a set in a carousel container.
open var selectedLayout: Layout? {
get { return _selectedLayout }
open var layout: Layout? {
get { return _layout }
set {
if let newValue {
_selectedLayout = newValue
_layout = newValue
} else {
_selectedLayout = .oneUP
_layout = .oneUP
}
setThumbWidth()
scrollThumbToPosition(position)
@ -198,7 +199,7 @@ open class CarouselScrollbar: View {
//--------------------------------------------------
// Sizes are from InVision design specs.
internal var containerSize: CGSize { CGSize(width: 45, height: 44) }
internal var _selectedLayout: Layout = .oneUP
internal var _layout: Layout = .oneUP
internal var _numberOfSlides: Int = 1
internal var totalPositions: Int = 1
internal var _position: Int = 1
@ -329,7 +330,7 @@ open class CarouselScrollbar: View {
// Compute track width and should maintain minimum thumb width if needed
private func setThumbWidth() {
let width = (Float(trackViewWidth) / Float(numberOfSlides)) * Float(_selectedLayout.value)
let width = (Float(trackViewWidth) / Float(numberOfSlides)) * Float(_layout.value)
computedWidth = (width > Float(trackViewWidth)) ? Float(trackViewWidth) : width
thumbWidth = (width <= Float(trackViewWidth) && width > minThumbWidth) ? width : ((width > Float(trackViewWidth)) ? Float(trackViewWidth) : minThumbWidth)
thumbView.frame.size.width = CGFloat(thumbWidth)
@ -362,7 +363,7 @@ open class CarouselScrollbar: View {
}
private func checkPositions() {
totalPositions = Int (ceil (Double(numberOfSlides) / Double(_selectedLayout.value)))
totalPositions = Int (ceil (Double(numberOfSlides) / Double(_layout.value)))
}
private func scrollThumbToPosition(_ position: Int) {

View File

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

View File

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

View File

@ -9,6 +9,7 @@ import Foundation
import UIKit
/// Checkboxes are a multi-select component through which a customer indicates a choice. If a binary choice, the component is a checkbox. If the choice has multiple options, the component is a ``CheckboxGroup``.
@objcMembers
@objc(VDSCheckboxItem)
open class CheckboxItem: SelectorItemBase<Checkbox> {
@ -47,6 +48,13 @@ open class CheckboxItem: SelectorItemBase<Checkbox> {
isSelected.toggle()
sendActions(for: .valueChanged)
}
open override func setup() {
super.setup()
let foo = ConcreteClass(customView: Checkbox())
print(foo.customView.isAnimated)
}
/// Used to make changes to the View based off a change events or from local properties.
open override func updateView() {
@ -54,3 +62,24 @@ open class CheckboxItem: SelectorItemBase<Checkbox> {
super.updateView()
}
}
@objcMembers
open class GenericClass<T: UIView>: NSObject {
public var customView: T
public init(customView: T = T()) {
self.customView = customView
}
}
@objcMembers
@objc(VDSConcreteClass)
open class ConcreteClass: GenericClass<Checkbox> {
}
@objcMembers
@objc(VDSConcreteCheckboxClass)
open class ConcreteCheckboxClass: ConcreteClass {}

View File

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

View File

@ -10,8 +10,6 @@ import UIKit
extension DatePicker {
public struct CalendarModel {
public let surface: Surface
/// If set to true, the calendar will not have a border.
public let hideContainerBorder: Bool
@ -35,15 +33,13 @@ extension DatePicker {
/// Array of ``CalendarIndicatorModel`` you are wanting to show on legend.
public let indicators: [CalendarBase.CalendarIndicatorModel]
public init(surface: Surface = .light,
hideContainerBorder: Bool = false,
public init(hideContainerBorder: Bool = false,
hideCurrentDateIndicator: Bool = false,
activeDates: [Date] = [],
inactiveDates: [Date] = [],
minDate: Date = Date().startOfMonth,
maxDate: Date = Date().endOfMonth,
indicators: [CalendarBase.CalendarIndicatorModel] = []) {
self.surface = surface
self.hideContainerBorder = hideContainerBorder
self.hideCurrentDateIndicator = hideCurrentDateIndicator
self.activeDates = activeDates

View File

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

View File

@ -11,6 +11,7 @@ import VDSCoreTokens
import Combine
/// A dropdown select is an expandable menu of predefined options that allows a customer to make a single selection.
@objcMembers
@objc(VDSDropdownSelect)
open class DropdownSelect: EntryFieldBase {
//--------------------------------------------------
@ -30,19 +31,7 @@ open class DropdownSelect: EntryFieldBase {
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
/// Override UIControl state to add the .error state if showSuccess is true and if showError is true.
open override var state: UIControl.State {
get {
var state = super.state
if dropdownField.isFirstResponder {
state.insert(.focused)
}
return state
}
}
//--------------------------------------------------
/// If true, the label will be displayed inside the dropdown containerView. Otherwise, the label will be above the dropdown containerView like a normal text input.
open var showInlineLabel: Bool = false { didSet { setNeedsUpdate() }}

View File

@ -13,6 +13,7 @@ import Combine
/// An icon is a graphical element that conveys information at a glance. It helps orient
/// a customer, explain functionality and draw attention to interactive elements. Icons
/// should have a functional purpose and should never be used for decoration.
@objcMembers
@objc(VDSIcon)
open class Icon: View {
@ -93,12 +94,15 @@ open class Icon: View {
backgroundColor = .clear
isAccessibilityElement = true
accessibilityTraits = .image
accessibilityTraits = .none
accessibilityHint = "image"
bridge_accessibilityLabelBlock = { [weak self] in
guard let self else { return "" }
return name?.rawValue ?? "icon"
}
}

View File

@ -360,7 +360,7 @@ open class InputStepper: EntryFieldBase {
}
// Set control width to input stepper.
internal func setControlWidth(_ text: String?) {
private func setControlWidth(_ text: String?) {
if let text, text == "auto" {
stepperWidthConstraint?.deactivate()
} else if let controlWidth = Int(text ?? "") {
@ -371,7 +371,7 @@ open class InputStepper: EntryFieldBase {
}
// Handling the controlwidth without going beyond the width of the parent container.
internal func updateStepperContainerWidth(controlWidth: CGFloat, width: CGFloat) {
private func updateStepperContainerWidth(controlWidth: CGFloat, width: CGFloat) {
if controlWidth >= containerSize.width && controlWidth <= width {
stepperWidthConstraint?.deactivate()
stepperWidthConstraint?.constant = controlWidth

View File

@ -12,6 +12,7 @@ import Combine
/// Label is a standard view used to draw text with applying Typography through ``TextStyle`` as well
/// as other attributes using any implemetation of ``LabelAttributeModel``.
@objcMembers
@objc(VDSLabel)
open class Label: UILabel, ViewProtocol, UserInfoable {
@ -214,10 +215,6 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
}
open func setup() {
bridge_accessibilityLabelBlock = { [weak self] in
guard let self else { return "" }
return text
}
}
open func reset() {
@ -389,60 +386,100 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
}
}
//--------------------------------------------------
// MARK: - Touch Events
//--------------------------------------------------
@objc private func textLinkTapped(_ gesture: UITapGestureRecognizer) {
for actionable in actions {
// This determines if we tapped on the desired range of text.
let location = gesture.location(in: self)
if didTapActionInLabel(location, inRange: actionable.range) {
actionable.performAction()
return
}
let location = gesture.location(in: self)
if let action = actions.first(where: { isAction(for: location, inRange: $0.range) }) {
action.performAction()
}
}
public func isAction(for location: CGPoint) -> Bool {
for actionable in actions {
if didTapActionInLabel(location, inRange: actionable.range) {
return true
}
}
return false
actions.contains(where: {isAction(for: location, inRange: $0.range)})
}
private func didTapActionInLabel(_ location: CGPoint, inRange targetRange: NSRange) -> Bool {
public func isAction(for location: CGPoint, inRange targetRange: NSRange) -> Bool {
guard let abstractContainer = abstractTextContainer() else { return false }
let textContainer = abstractContainer.textContainer
let layoutManager = abstractContainer.layoutManager
guard let attributedText else { return false }
let indexOfGlyph = layoutManager.glyphIndex(for: location, in: textContainer)
let intrinsicWidth = intrinsicContentSize.width
// Assert that tapped occured within acceptable bounds based on alignment.
switch textAlignment {
case .right:
if location.x < bounds.width - intrinsicWidth {
return false
}
case .center:
let halfBounds = bounds.width / 2
let halfIntrinsicWidth = intrinsicWidth / 2
if location.x > halfBounds + halfIntrinsicWidth {
return false
} else if location.x < halfBounds - halfIntrinsicWidth {
return false
}
default: // Left align
if location.x > intrinsicWidth {
return false
}
}
// Affirms that the tap occured in the desired rect of provided by the target range.
return layoutManager.boundingRect(forGlyphRange: targetRange, in: textContainer).contains(location)
&& NSLocationInRange(indexOfGlyph, targetRange)
}
/**
Provides a text container and layout manager of how the text would appear on screen.
They are used in tandem to derive low-level TextKit results of the label.
*/
public func abstractTextContainer() -> (textContainer: NSTextContainer, layoutManager: NSLayoutManager, textStorage: NSTextStorage)? {
// Must configure the attributed string to translate what would appear on screen to accurately analyze.
guard let attributedText = attributedText else { return nil }
let paragraph = NSMutableParagraphStyle()
paragraph.alignment = textAlignment
let stagedAttributedString = NSMutableAttributedString(attributedString: attributedText)
stagedAttributedString.addAttributes([NSAttributedString.Key.paragraphStyle: paragraph], range: NSRange(location: 0, length: attributedText.string.count))
let textStorage = NSTextStorage(attributedString: stagedAttributedString)
let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: bounds.size)
let textStorage = NSTextStorage(attributedString: attributedText)
let textContainer = NSTextContainer(size: .zero)
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
let characterIndex = layoutManager.characterIndex(for: location, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
guard let _ = attributedText.attribute(NSAttributedString.Key.action, at: characterIndex, effectiveRange: nil) as? String, characterIndex < attributedText.length else { return false }
return true
textContainer.lineFragmentPadding = 0.0
textContainer.lineBreakMode = lineBreakMode
textContainer.maximumNumberOfLines = numberOfLines
textContainer.size = bounds.size
return (textContainer, layoutManager, textStorage)
}
//--------------------------------------------------
// MARK: - Accessibility
//--------------------------------------------------
private func customAccessibilityElement(text: String?, range: NSRange, accessibleText: String? = nil) -> AccessibilityActionElement? {
guard let text = text, let attributedText else { return nil }
guard let text = text, let abstractContainer = abstractTextContainer() else { return nil }
let textContainer = abstractContainer.textContainer
let layoutManager = abstractContainer.layoutManager
let actionText = accessibleText ?? (text.isValid(range: range) ? NSString(string:text).substring(with: range) : text)
// Calculate the frame of the substring
let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: bounds.size)
let textStorage = NSTextStorage(attributedString: attributedText)
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
var glyphRange = NSRange()
// Convert the range for the substring into a range of glyphs
layoutManager.characterRange(forGlyphRange: range, actualGlyphRange: &glyphRange)
let substringBounds = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
// Create custom accessibility element
@ -456,13 +493,8 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
return element
}
//--------------------------------------------------
// MARK: - Accessibility
//--------------------------------------------------
open var accessibilityAction: ((Label) -> Void)?
private var _isAccessibilityElement: Bool = false
open override var isAccessibilityElement: Bool {
get {
var block: AXBoolReturnBlock?
@ -478,15 +510,14 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
if let block {
return block()
} else {
return _isAccessibilityElement
return super.isAccessibilityElement
}
}
set {
_isAccessibilityElement = newValue
super.isAccessibilityElement = newValue
}
}
private var _accessibilityLabel: String?
open override var accessibilityLabel: String? {
get {
var block: AXStringReturnBlock?
@ -501,15 +532,14 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
if let block {
return block()
} else {
return _accessibilityLabel
return super.accessibilityLabel
}
}
set {
_accessibilityLabel = newValue
super.accessibilityLabel = newValue
}
}
private var _accessibilityHint: String?
open override var accessibilityHint: String? {
get {
var block: AXStringReturnBlock?
@ -524,15 +554,14 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
if let block {
return block()
} else {
return _accessibilityHint
return super.accessibilityHint
}
}
set {
_accessibilityHint = newValue
super.accessibilityHint = newValue
}
}
private var _accessibilityValue: String?
open override var accessibilityValue: String? {
get {
var block: AXStringReturnBlock?
@ -548,11 +577,11 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
if let block{
return block()
} else {
return _accessibilityValue
return super.accessibilityValue
}
}
set {
_accessibilityValue = newValue
super.accessibilityValue = newValue
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,6 +9,7 @@ import UIKit
import VDSCoreTokens
///This is customised button for Pagination view
@objcMembers
@objc(PaginationButton)
open class PaginationButton: ButtonBase {
//--------------------------------------------------

View File

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

View File

@ -12,6 +12,7 @@ import VDSCoreTokens
/// Radio boxes are single-select components through which a customer indicates a choice
/// that are used within a ``RadioBoxGroup``.
@objcMembers
@objc(VDSRadioBoxItem)
open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable {
@ -214,7 +215,7 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable {
selectorView.isAccessibilityElement = true
selectorView.accessibilityTraits = .button
addSubview(selectorView)
selectorView.isUserInteractionEnabled = true
selectorView.isUserInteractionEnabled = false
selectorView.addSubview(selectorStackView)

View File

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

View File

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

View File

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

View File

@ -10,6 +10,7 @@ import UIKit
import VDSCoreTokens
///Table is view composed of rows and columns, which takes any view into each cell and resizes based on the highest cell height.
@objcMembers
@objc(VDSTable)
open class Table: View {
@ -51,10 +52,8 @@ open class Table: View {
func horizontalValue() -> CGFloat {
switch self {
case .standard:
return UIDevice.isIPad ? VDSLayout.space8X : VDSLayout.space6X
case .compact:
return UIDevice.isIPad ? VDSLayout.space8X : VDSLayout.space6X
case .standard, .compact:
return UIDevice.isIPad ? VDSLayout.space4X : VDSLayout.space3X
}
}
@ -147,7 +146,10 @@ extension Table: UICollectionViewDelegate, UICollectionViewDataSource, TableColl
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TableCellItem.Identifier, for: indexPath) as? TableCellItem else { return UICollectionViewCell() }
let currentItem = tableData[indexPath.section].columns[indexPath.row]
let shouldStrip = striped ? (indexPath.section % 2 != 0) : false
cell.updateCell(content: currentItem, surface: surface, striped: shouldStrip, padding: padding)
let isHeader = tableData[indexPath.section].isHeader
var edgePadding = UIEdgeInsets(top: padding.verticalValue(), left: 0, bottom: padding.verticalValue(), right: padding.horizontalValue())
edgePadding.left = (indexPath.row == 0 && !striped) ? VDSLayout.space1X : padding.horizontalValue()
cell.updateCell(content: currentItem, surface: surface, striped: shouldStrip, padding: edgePadding, isHeader: isHeader)
return cell
}

View File

@ -29,10 +29,7 @@ final class TableCellItem: UICollectionViewCell {
/// Color configuration for striped background color
private let stripedColorConfiguration = SurfaceColorConfiguration(VDSColor.backgroundSecondaryLight, VDSColor.backgroundSecondaryDark)
/// Padding parameter to maintain the edge spacing of the containerView
private var padding: Table.Padding = .standard
//--------------------------------------------------
// MARK: - Initializers
//--------------------------------------------------
@ -58,10 +55,10 @@ final class TableCellItem: UICollectionViewCell {
//--------------------------------------------------
/// Updates the cell content with ``TableItemModel`` and styling/padding attributes from other parameters
public func updateCell(content: TableItemModel, surface: Surface, striped: Bool = false, padding: Table.Padding = .standard) {
public func updateCell(content: TableItemModel, surface: Surface, striped: Bool = false, padding: UIEdgeInsets, isHeader: Bool = false) {
containerView.subviews.forEach({ $0.removeFromSuperview() })
self.padding = padding
containerView.surface = surface
containerView.backgroundColor = striped ? stripedColorConfiguration.getColor(surface) : backgroundColorConfiguration.getColor(surface)
@ -82,11 +79,11 @@ final class TableCellItem: UICollectionViewCell {
}
NSLayoutConstraint.activate([
component.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: VDSLayout.space1X),
component.topAnchor.constraint(greaterThanOrEqualTo: containerView.topAnchor, constant: padding.verticalValue()),
containerView.bottomAnchor.constraint(greaterThanOrEqualTo: component.bottomAnchor, constant: padding.verticalValue()),
containerView.trailingAnchor.constraint(greaterThanOrEqualTo: component.trailingAnchor, constant: padding.horizontalValue()),
containerView.centerYAnchor.constraint(equalTo: component.centerYAnchor)
component.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: padding.left),
containerView.trailingAnchor.constraint(greaterThanOrEqualTo: component.trailingAnchor, constant: padding.right)
])
component.topAnchor.constraint(equalTo: containerView.topAnchor, constant: padding.top).isActive = !isHeader
containerView.bottomAnchor.constraint(equalTo: component.bottomAnchor, constant: padding.bottom).isActive = isHeader
}
}

View File

@ -40,6 +40,9 @@ class MatrixFlowLayout : UICollectionViewFlowLayout {
///padding type to be set from Table component, which is used to calculate the size & position of the cell.
var layoutPadding: Table.Padding = .standard
///Striped status of Table, based on this status padding of leading attribute changes.
var striped: Bool = false
//--------------------------------------------------
// MARK: - Overrides
//--------------------------------------------------
@ -77,7 +80,7 @@ class MatrixFlowLayout : UICollectionViewFlowLayout {
let selectedItem = delegate.collectionView(collectionView, dataForItemAt: indexPath)
///Calculate the estimated height of the cell
let itemHeight = estimateHeightFor(item: selectedItem, with: itemWidth)
let itemHeight = estimateHeightFor(item: selectedItem, with: itemWidth, index: indexPath)
layoutWidth += itemWidth
@ -108,8 +111,8 @@ class MatrixFlowLayout : UICollectionViewFlowLayout {
}
/// Fetches estimated height by calling the cell's component estimated height and adding padding
private func estimateHeightFor(item: TableItemModel, with width: CGFloat) -> CGFloat {
private func estimateHeightFor(item: TableItemModel, with width: CGFloat, index: IndexPath) -> CGFloat {
let horizontalPadding = (index.row == 0 && !striped) ? (VDSLayout.space1X + layoutPadding.horizontalValue()) : (2 * layoutPadding.horizontalValue())
let itemWidth = width - layoutPadding.horizontalValue() - defaultLeadingPadding
let maxSize = CGSize(width: itemWidth, height: CGFloat.greatestFiniteMagnitude)
let estItemSize = item.component?.systemLayoutSizeFitting(maxSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel) ?? CGSize(width: itemWidth, height: item.defaultHeight)

View File

@ -11,11 +11,14 @@ public struct TableRowModel {
public var columns: [TableItemModel]
public var isHeader: Bool = false
public var columnsCount: Int {
return columns.count
}
public init(columns: [TableItemModel]) {
public init(columns: [TableItemModel], isHeader: Bool = false) {
self.columns = columns
self.isHeader = isHeader
}
}

View File

@ -11,7 +11,7 @@ import VDSCoreTokens
import Combine
extension Tabs {
@objcMembers
@objc(VDSTab)
open class Tab: Control, Groupable {

View File

@ -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 doesnt need to be compared.
@objcMembers
@objc(VDSTabs)
open class Tabs: View {

View File

@ -8,6 +8,7 @@
import Foundation
import UIKit
@objcMembers
@objc(VDSTabsContainer)
open class TabsContainer: View {

View File

@ -11,6 +11,7 @@ import VDSCoreTokens
import Combine
/// Base Class used to build out a Input controls.
@objcMembers
@objc(VDSEntryField)
open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
@ -28,7 +29,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
public required init?(coder: NSCoder) {
super.init(coder: coder)
}
//--------------------------------------------------
// MARK: - Enums
//--------------------------------------------------
@ -92,12 +93,6 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
}
}()
/// This is the view that will be wrapped with the border for userInteraction.
/// The only subview of this view is the fieldStackView
internal var containerView = View().with {
$0.isAccessibilityElement = true
}
/// This is set by a local method.
internal var bottomContainerView: UIView!
@ -115,7 +110,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
internal var widthConstraint: NSLayoutConstraint?
internal var trailingEqualsConstraint: NSLayoutConstraint?
internal var trailingLessThanEqualsConstraint: NSLayoutConstraint?
//--------------------------------------------------
// MARK: - Configuration Properties
//--------------------------------------------------
@ -133,18 +128,18 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forDisabled: true)
$0.setSurfaceColors(VDSColor.elementsSecondaryOnlight, VDSColor.elementsSecondaryOndark, forDisabled: false)
}
internal var backgroundColorConfiguration = ControlColorConfiguration().with {
$0.setSurfaceColors(VDSFormControlsColor.backgroundOnlight, VDSFormControlsColor.backgroundOndark, forState: .normal)
$0.setSurfaceColors(VDSFormControlsColor.backgroundOnlight, VDSFormControlsColor.backgroundOndark, forState: .disabled)
$0.setSurfaceColors(VDSColor.feedbackErrorBackgroundOnlight, VDSColor.feedbackErrorBackgroundOndark, forState: .error)
$0.setSurfaceColors(VDSColor.feedbackErrorBackgroundOnlight, VDSColor.feedbackErrorBackgroundOndark, forState: [.error, .focused])
}
internal var borderColorConfiguration = ControlColorConfiguration().with {
$0.setSurfaceColors(VDSFormControlsColor.borderOnlight, VDSFormControlsColor.borderOndark, forState: .normal)
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOnlight, forState: .focused)
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOnlight, forState: [.focused, .error])
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .focused)
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: [.focused, .error])
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled)
$0.setSurfaceColors(VDSColor.feedbackErrorOnlight, VDSColor.feedbackErrorOndark, forState: .error)
$0.setSurfaceColors(VDSFormControlsColor.borderReadonlyOnlight, VDSFormControlsColor.borderReadonlyOndark, forState: .readonly)
@ -155,7 +150,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled)
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .error)
}
internal var readOnlyBorderColorConfiguration = ControlColorConfiguration().with {
$0.setSurfaceColors(VDSFormControlsColor.borderReadonlyOnlight, VDSFormControlsColor.borderReadonlyOndark, forState: .normal)
}
@ -163,8 +158,14 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
open var onChangeSubscriber: AnyCancellable?
/// This is the view that will be wrapped with the border for userInteraction.
/// The only subview of this view is the fieldStackView
open var containerView = View().with {
$0.isAccessibilityElement = true
}
open var onChangeSubscriber: AnyCancellable?
open var titleLabel = Label().with {
$0.setContentCompressionResistancePriority(.required, for: .vertical)
$0.textStyle = .bodySmall
@ -183,9 +184,11 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
open var statusIcon: Icon = Icon().with {
$0.size = .medium
$0.isAccessibilityElement = false
$0.isAccessibilityElement = true
}
open var useRequiredRule: Bool = true { didSet { setNeedsUpdate() } }
open var labelText: String? { didSet { setNeedsUpdate() } }
open var helperText: String? { didSet { setNeedsUpdate() } }
@ -195,7 +198,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
/// FormFieldValidator
open var validator: (any FormFieldValidatorable)?
/// Override UIControl state to add the .error state if showError is true.
open override var state: UIControl.State {
get {
@ -207,21 +210,24 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
if isReadOnly {
state.insert(.readonly)
}
if let responder, responder.isFirstResponder {
state.insert(.focused)
}
}
return state
}
}
open var errorText: String? { didSet { setNeedsUpdate() } }
open var tooltipModel: Tooltip.TooltipModel? { didSet { setNeedsUpdate() } }
open var transparentBackground: Bool = false { didSet { setNeedsUpdate() } }
open var width: CGFloat? { didSet { setNeedsUpdate() } }
open var inputId: String? { didSet { setNeedsUpdate() } }
/// The text of this textField.
open var value: String? {
get { fatalError("must be read from subclass")}
@ -232,21 +238,21 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
open var isRequired: Bool = false { didSet { setNeedsUpdate() } }
open var isReadOnly: Bool = false { didSet { setNeedsUpdate() } }
open var helperTextPlacement: HelperTextPlacement = .bottom {
didSet {
updateHelperTextPosition()
}
}
open var rules = [AnyRule<String>]()
open var accessibilityHintText: String = "Double tap to open"
//--------------------------------------------------
// MARK: - Overrides
//--------------------------------------------------
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
open override func setup() {
super.setup()
@ -326,9 +332,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
if let errorText, showError {
accessibilityLabels.append("error, \(errorText)")
}
accessibilityLabels.append("\(Self.self)")
return accessibilityLabels.joined(separator: ", ")
}
@ -341,11 +345,17 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
guard let self else { return "" }
return value
}
statusIcon.bridge_accessibilityLabelBlock = { [weak self] in
guard let self else { return "" }
return showError || hasInternalError ? "error" : nil
}
}
/// Updates the UI
open override func updateView() {
super.updateView()
updateRules()
updateContainerView(flag: true)
updateContainerWidth()
updateTitleLabel()
@ -363,7 +373,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
titleLabel.textStyle = .bodySmall
errorLabel.textStyle = .bodySmall
helperLabel.textStyle = .bodySmall
labelText = nil
helperText = nil
showError = false
@ -381,19 +391,19 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
open override var canBecomeFirstResponder: Bool {
responder?.canBecomeFirstResponder ?? super.canBecomeFirstResponder
}
open override func becomeFirstResponder() -> Bool {
responder?.becomeFirstResponder() ?? super.becomeFirstResponder()
}
open override var canResignFirstResponder: Bool {
responder?.canResignFirstResponder ?? super.canResignFirstResponder
}
open override func resignFirstResponder() -> Bool {
responder?.resignFirstResponder() ?? super.resignFirstResponder()
}
//--------------------------------------------------
// MARK: - Public Methods
//--------------------------------------------------
@ -401,21 +411,20 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
open func getFieldContainer() -> UIView {
fatalError("Subclass must return the view that contains the field/view the user will interact with.")
}
/// Container for the area in which helper or error text presents.
open func getBottomContainer() -> UIView {
return bottomContainerStackView
}
open func validate(){
updateRules()
validator = FormFieldValidator<EntryFieldBase>(field: self, rules: rules)
validator?.validate()
setNeedsUpdate()
}
open func updateTitleLabel() {
//update the local vars for the label since we no
//long have a model
var attributes: [any LabelAttributeModel] = []
@ -436,36 +445,43 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
if let tooltipModel {
attributes.append(TooltipLabelAttribute(surface: surface, model: tooltipModel, presenter: self))
}
//set the titleLabel
titleLabel.text = updatedLabelText
titleLabel.attributes = attributes
titleLabel.surface = surface
titleLabel.isEnabled = isEnabled
}
open func updateErrorLabel(){
if showError, let errorText {
errorLabel.text = errorText
errorLabel.surface = surface
errorLabel.isEnabled = isEnabled
errorLabel.isHidden = false
statusIcon.name = .error
statusIcon.surface = surface
statusIcon.isHidden = !isEnabled || state.contains(.focused)
} else if hasInternalError, let internalErrorText {
errorLabel.text = internalErrorText
errorLabel.surface = surface
errorLabel.isEnabled = isEnabled
errorLabel.isHidden = false
/// always show the errorIcon if there is an error
if showError || hasInternalError {
statusIcon.name = .error
statusIcon.surface = surface
statusIcon.isHidden = !isEnabled || state.contains(.focused)
} else {
statusIcon.isHidden = true
errorLabel.isHidden = true
}
statusIcon.color = iconColorConfiguration.getColor(self)
// only show errorLabel if there is a message
var message: String?
if showError, let errorText {
message = errorText
} else if hasInternalError, let internalErrorText {
message = internalErrorText
}
if let message {
errorLabel.text = message
errorLabel.surface = surface
errorLabel.isEnabled = isEnabled
errorLabel.isHidden = false
} else {
errorLabel.isHidden = true
}
}
open func updateHelperLabel(){
@ -507,7 +523,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
//--------------------------------------------------
internal func updateRules() {
rules.removeAll()
if self.isRequired {
if isRequired && useRequiredRule {
let rule = RequiredRule()
if let errorText, !errorText.isEmpty {
rule.errorMessage = errorText

View File

@ -51,6 +51,14 @@ extension InputField {
}
}
public var accessibilityLabel: String {
switch self {
case .generic, .placeholder: return "credit card"
default: return rawValue
}
}
func separatorIndices(_ length: Int) -> [Int] {
var indices: [Int] = [4, 8, 12]
switch self {
@ -125,7 +133,7 @@ extension InputField {
class CreditCardHandler: FieldTypeHandler {
static let shared = CreditCardHandler()
private override init() {
super.init()
self.validateOnChange = false
@ -135,6 +143,7 @@ extension InputField {
fileprivate func updateLeftImage(_ inputField: InputField) {
let imageName = inputField.cardType.imageName(surface: inputField.surface)
creditCardImageView.image = BundleManager.shared.image(for: imageName)
creditCardImageView.accessibilityLabel = inputField.cardType.accessibilityLabel
}
override func updateView(_ inputField: InputField) {
@ -148,14 +157,14 @@ extension InputField {
inputField.textField.leftView = iconContainerView
inputField.textField.leftViewMode = .always
updateLeftImage(inputField)
}
internal var creditCardImageView = UIImageView().with {
$0.height(20)
$0.width(32)
$0.isAccessibilityElement = false
$0.isAccessibilityElement = true
$0.translatesAutoresizingMaskIntoConstraints = false
$0.contentMode = .scaleAspectFill
$0.clipsToBounds = true

View File

@ -68,6 +68,12 @@ extension InputField {
actionModel.onClick(inputField)
}
inputField.actionTextLink.isHidden = false
// set the accessibilityLabel
if let labelText = inputField.labelText {
inputField.actionTextLink.bridge_accessibilityLabelBlock = {
return "\(actionModel.text) \(labelText)"
}
}
inputField.fieldStackView.setCustomSpacing(VDSLayout.space2X, after: inputField.statusIcon)
} else {
inputField.actionTextLink.isHidden = true

View File

@ -41,7 +41,7 @@ extension InputField {
guard let self else { return }
self.passwordActionType = nextPasswordActionType
inputField.setNeedsUpdate()
})
})
} else {
passwordActionType = .show
}

View File

@ -10,6 +10,35 @@ import UIKit
extension InputField {
public class TelephoneNumberValidator: Rule, Withable {
public var format: String
public var errorMessage: String = "Please enter a valid telephone number"
public init(format: String) {
self.format = format
}
public func isValid(value: String?) -> Bool {
guard let value, !value.isEmpty else { return true }
let regex = createRegex(from: format)
let predicate = NSPredicate(format: "SELF MATCHES %@", regex)
let valid = predicate.evaluate(with: value)
return valid
}
private func createRegex(from format: String) -> String {
// Escape special regex characters in the format string
let escapedFormat = NSRegularExpression.escapedPattern(for: format)
// Replace placeholder characters with regex patterns
let regex = escapedFormat
.replacingOccurrences(of: "X", with: "\\d")
return "^" + regex + "$"
}
}
class TelephoneHandler: FieldTypeHandler {
static let shared = TelephoneHandler()
@ -25,14 +54,7 @@ extension InputField {
}
override func appendRules(_ inputField: InputField) {
if let text = inputField.textField.text, text.count > 0 {
let rule = CharacterCountRule().copyWith {
$0.maxLength = "XXX-XXX-XXXX".count
$0.compareType = .equals
$0.errorMessage = "Enter a valid telephone."
}
inputField.rules.append(.init(rule))
}
inputField.rules.append(.init(TelephoneNumberValidator(format: "XXX-XXX-XXXX")))
}
override func textField(_ inputField: InputField, textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
@ -49,7 +71,7 @@ extension InputField {
let rawNumber = newText.filter { $0.isNumber }
// Format the number with dashes
let formattedNumber = formatUSNumber(rawNumber)
let formattedNumber = rawNumber.formatUSNumber()
// Set the formatted text
textField.text = formattedNumber
@ -62,43 +84,54 @@ extension InputField {
textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition)
}
value = formattedNumber
// Prevent the default behavior
return false
}
internal func formatUSNumber(_ number: String) -> String {
// Format the number in the style XXX-XXX-XXXX
let areaCodeLength = 3
let centralOfficeCodeLength = 3
let lineNumberLength = 4
var formattedNumber = ""
if number.count > 0 {
formattedNumber.append(contentsOf: number.prefix(areaCodeLength))
override func textFieldDidEndEditing(_ inputField: InputField, textField: UITextField) {
if let text = inputField.text {
textField.text = text.formatUSNumber()
value = textField.text
}
if number.count > areaCodeLength {
let startIndex = number.index(number.startIndex, offsetBy: areaCodeLength)
let endIndex = number.index(startIndex, offsetBy: min(centralOfficeCodeLength, number.count - areaCodeLength))
let centralOfficeCode = number[startIndex..<endIndex]
formattedNumber.append("-")
formattedNumber.append(contentsOf: centralOfficeCode)
}
if number.count > areaCodeLength + centralOfficeCodeLength {
let startIndex = number.index(number.startIndex, offsetBy: areaCodeLength + centralOfficeCodeLength)
let endIndex = number.index(startIndex, offsetBy: min(lineNumberLength, number.count - areaCodeLength - centralOfficeCodeLength))
let lineNumber = number[startIndex..<endIndex]
formattedNumber.append("-")
formattedNumber.append(contentsOf: lineNumber)
}
return formattedNumber
}
}
}
extension String {
public func formatUSNumber() -> String {
// Format the number in the style XXX-XXX-XXXX
let areaCodeLength = 3
let centralOfficeCodeLength = 3
let lineNumberLength = 4
var formattedNumber = ""
let number = filter { $0.isNumber }
if number.count > 0 {
formattedNumber.append(contentsOf: number.prefix(areaCodeLength))
}
if number.count > areaCodeLength {
let startIndex = number.index(number.startIndex, offsetBy: areaCodeLength)
let endIndex = number.index(startIndex, offsetBy: min(centralOfficeCodeLength, number.count - areaCodeLength))
let centralOfficeCode = number[startIndex..<endIndex]
formattedNumber.append("-")
formattedNumber.append(contentsOf: centralOfficeCode)
}
if number.count > areaCodeLength + centralOfficeCodeLength {
let startIndex = number.index(number.startIndex, offsetBy: areaCodeLength + centralOfficeCodeLength)
let endIndex = number.index(startIndex, offsetBy: min(lineNumberLength, number.count - areaCodeLength - centralOfficeCodeLength))
let lineNumber = number[startIndex..<endIndex]
formattedNumber.append("-")
formattedNumber.append(contentsOf: lineNumber)
}
return formattedNumber
}
}

View File

@ -13,6 +13,7 @@ import Combine
/// An input field is an input wherein a customer enters information. They typically appear in forms.
/// Specialized input fields capture credit card numbers, inline actions, passwords, phone numbers,
/// dates and security codes in their correct formats.
@objcMembers
@objc(VDSInputField)
open class InputField: EntryFieldBase {
@ -105,6 +106,11 @@ open class InputField: EntryFieldBase {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.textStyle = TextStyle.bodyLarge
$0.isAccessibilityElement = false
$0.autocorrectionType = .no
$0.spellCheckingType = .no
$0.smartQuotesType = .no
$0.smartDashesType = .no
$0.smartInsertDeleteType = .no
}
/// Color configuration for the textField.
@ -166,11 +172,7 @@ open class InputField: EntryFieldBase {
if showSuccess {
state.insert(.success)
}
if textField.isFirstResponder {
state.insert(.focused)
}
return state
}
}
@ -186,6 +188,8 @@ open class InputField: EntryFieldBase {
super.setup()
accessibilityHintText = "Double tap to edit"
actionTextLink.accessibilityTraits = .button
textField.heightAnchor.constraint(equalToConstant: 20).isActive = true
textField.delegate = self
bottomContainerStackView.insertArrangedSubview(successLabel, at: 0)
@ -200,6 +204,59 @@ open class InputField: EntryFieldBase {
borderColorConfiguration.setSurfaceColors(VDSColor.feedbackSuccessOnlight, VDSColor.feedbackSuccessOndark, forState: .success)
textField.textColorConfiguration = textFieldTextColorConfiguration
containerView.bridge_accessibilityLabelBlock = { [weak self] in
guard let self else { return "" }
var accessibilityLabels = [String]()
if let text = titleLabel.text?.trimmingCharacters(in: .whitespaces) {
accessibilityLabels.append(text)
}
if let formatText = textField.formatText, !formatText.isEmpty, textField.text.isEmpty {
accessibilityLabels.append("format, \(formatText)")
}
if let placeholderText = textField.placeholder, !placeholderText.isEmpty, textField.text.isEmpty {
accessibilityLabels.append("placeholder, \(placeholderText)")
}
if isReadOnly {
accessibilityLabels.append("read only")
}
if !isEnabled {
accessibilityLabels.append("dimmed")
}
if let errorText, showError {
accessibilityLabels.append("error, \(errorText)")
}
if let successText, showSuccess {
accessibilityLabels.append("success, \(successText)")
}
accessibilityLabels.append("\(Self.self)")
return accessibilityLabels.joined(separator: ", ")
}
statusIcon.bridge_accessibilityLabelBlock = { [weak self] in
guard let self else { return "" }
if showError {
return "error"
} else if showSuccess {
return "success"
} else {
return nil
}
}
containerView.bridge_accessibilityValueBlock = { [weak self] in
guard let self else { return "" }
return textField.isSecureTextEntry ? "\(textField.text.count) stars" : value
}
}
open override func getFieldContainer() -> UIView {
@ -255,6 +312,24 @@ open class InputField: EntryFieldBase {
}
}
open var widthPercentage: CGFloat? { didSet { setNeedsUpdate() } }
internal override func updateContainerWidth() {
widthConstraint?.deactivate()
trailingLessThanEqualsConstraint?.deactivate()
trailingEqualsConstraint?.deactivate()
//see if there is a widthPercentage and follow the same pattern as done for "width"
let currentWidth = (horizontalPinnedWidth() ?? 0) * (widthPercentage ?? 0)
if currentWidth >= minWidth, currentWidth <= maxWidth {
widthConstraint?.constant = currentWidth
widthConstraint?.activate()
trailingLessThanEqualsConstraint?.activate()
} else {
super.updateContainerWidth()
}
}
override func updateRules() {
super.updateRules()
fieldType.handler().appendRules(self)
@ -264,11 +339,20 @@ open class InputField: EntryFieldBase {
get {
var elements = [Any]()
elements.append(contentsOf: [titleLabel, containerView])
if showError {
if let leftView = textField.leftView {
elements.append(leftView)
}
if !statusIcon.isHidden{
elements.append(statusIcon)
if let errorText, !errorText.isEmpty {
elements.append(errorLabel)
}
}
if !actionTextLink.isHidden {
elements.append(actionTextLink)
}
if let errorText, !errorText.isEmpty, showError || hasInternalError {
elements.append(errorLabel)
} else if showSuccess, let successText, !successText.isEmpty {
elements.append(successLabel)
}
@ -285,29 +369,33 @@ open class InputField: EntryFieldBase {
}
extension InputField: UITextFieldDelegate {
public func textFieldDidBeginEditing(_ textField: UITextField) {
open func textFieldDidBeginEditing(_ textField: UITextField) {
fieldType.handler().textFieldDidBeginEditing(self, textField: textField)
updateContainerView(flag: true)
updateErrorLabel()
}
public func textFieldDidEndEditing(_ textField: UITextField) {
open func textFieldDidEndEditing(_ textField: UITextField) {
fieldType.handler().textFieldDidEndEditing(self, textField: textField)
validate()
UIAccessibility.post(notification: .layoutChanged, argument: self.containerView)
}
public func textFieldDidChangeSelection(_ textField: UITextField) {
open func textFieldDidChangeSelection(_ textField: UITextField) {
fieldType.handler().textFieldDidChangeSelection(self, textField: textField)
text = textField.text
sendActions(for: .valueChanged)
if fieldType.handler().validateOnChange {
validate()
}
sendActions(for: .valueChanged)
setNeedsUpdate()
}
public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
return fieldType.handler().textField(self, textField: textField, shouldChangeCharactersIn: range, replacementString: string)
open func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let shouldChange = fieldType.handler().textField(self, textField: textField, shouldChangeCharactersIn: range, replacementString: string)
if shouldChange {
UIAccessibility.post(notification: .announcement, argument: string)
}
return shouldChange
}
}

View File

@ -10,6 +10,7 @@ import UIKit
import Combine
import VDSCoreTokens
@objcMembers
@objc(VDSTextField)
open class TextField: UITextField, ViewProtocol, Errorable {
@ -47,6 +48,11 @@ open class TextField: UITextField, ViewProtocol, Errorable {
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
/// Set to true to hide the blinking textField cursor.
open var hideBlinkingCaret = false
open var enableClipboardActions: Bool = true
open var onDidDeleteBackwards: (() -> Void)?
/// Key of whether or not updateView() is called in setNeedsUpdate()
open var shouldUpdateView: Bool = true
@ -209,6 +215,23 @@ open class TextField: UITextField, ViewProtocol, Errorable {
return success
}
open override func caretRect(for position: UITextPosition) -> CGRect {
if hideBlinkingCaret {
return .zero
}
let caretRect = super.caretRect(for: position)
return CGRect(origin: caretRect.origin, size: CGSize(width: 1, height: caretRect.height))
}
open override func deleteBackward() {
super.deleteBackward()
onDidDeleteBackwards?()
}
open override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { enableClipboardActions }
//--------------------------------------------------
// MARK: - Private Methods
//--------------------------------------------------
@ -236,7 +259,6 @@ open class TextField: UITextField, ViewProtocol, Errorable {
//--------------------------------------------------
open var accessibilityAction: ((TextField) -> Void)?
private var _isAccessibilityElement: Bool = false
open override var isAccessibilityElement: Bool {
get {
var block: AXBoolReturnBlock?
@ -252,15 +274,14 @@ open class TextField: UITextField, ViewProtocol, Errorable {
if let block {
return block()
} else {
return _isAccessibilityElement
return super.isAccessibilityElement
}
}
set {
_isAccessibilityElement = newValue
super.isAccessibilityElement = newValue
}
}
private var _accessibilityLabel: String?
open override var accessibilityLabel: String? {
get {
var block: AXStringReturnBlock?
@ -275,15 +296,14 @@ open class TextField: UITextField, ViewProtocol, Errorable {
if let block {
return block()
} else {
return _accessibilityLabel
return super.accessibilityLabel
}
}
set {
_accessibilityLabel = newValue
super.accessibilityLabel = newValue
}
}
private var _accessibilityHint: String?
open override var accessibilityHint: String? {
get {
var block: AXStringReturnBlock?
@ -298,15 +318,14 @@ open class TextField: UITextField, ViewProtocol, Errorable {
if let block {
return block()
} else {
return _accessibilityHint
return super.accessibilityHint
}
}
set {
_accessibilityHint = newValue
super.accessibilityHint = newValue
}
}
private var _accessibilityValue: String?
open override var accessibilityValue: String? {
get {
var block: AXStringReturnBlock?
@ -322,11 +341,11 @@ open class TextField: UITextField, ViewProtocol, Errorable {
if let block{
return block()
} else {
return _accessibilityValue
return super.accessibilityValue
}
}
set {
_accessibilityValue = newValue
super.accessibilityValue = newValue
}
}

View File

@ -12,6 +12,7 @@ import Combine
/// A text area is an input wherein a customer enters long-form information.
/// Use a text area when you want customers to enter text thats longer than a single line.
@objcMembers
@objc(VDSTextArea)
open class TextArea: EntryFieldBase {
//--------------------------------------------------
@ -56,18 +57,7 @@ open class TextArea: EntryFieldBase {
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
/// Override UIControl state to add the .error state if showSuccess is true and if showError is true.
open override var state: UIControl.State {
get {
var state = super.state
if textView.isFirstResponder {
state.insert(.focused)
}
return state
}
}
//--------------------------------------------------
override var containerSize: CGSize { CGSize(width: 182, height: Height.twoX.value) }
/// Enum used to describe the the height of TextArea.
@ -112,6 +102,7 @@ open class TextArea: EntryFieldBase {
$0.isScrollEnabled = true
$0.textContainerInset = .zero
$0.autocorrectionType = .no
$0.spellCheckingType = .no
$0.textContainer.lineFragmentPadding = 0
}
@ -121,7 +112,11 @@ open class TextArea: EntryFieldBase {
}
didSet {
validate()
setNeedsUpdate()
if textView.isFirstResponder {
validate()
}
}
}
@ -143,6 +138,7 @@ open class TextArea: EntryFieldBase {
super.setup()
accessibilityHintText = "Double tap to edit"
textView.delegate = self
//events
textView
@ -198,8 +194,9 @@ open class TextArea: EntryFieldBase {
override func updateRules() {
super.updateRules()
rules.append(.init(countRule))
if let maxLength, maxLength > 0 {
rules.append(.init(countRule))
}
}
open override func getFieldContainer() -> UIView {
@ -237,7 +234,7 @@ open class TextArea: EntryFieldBase {
}
}
func textViewDidChange(_ textView: UITextView) {
public func textViewDidChange(_ textView: UITextView) {
//dynamic textView Height sizing based on Figma
//if you want it to work "as-is" delete this code
@ -299,3 +296,10 @@ open class TextArea: EntryFieldBase {
//--------------------------------------------------
var countRule = CharacterCountRule()
}
extension TextArea: UITextViewDelegate {
public func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
UIAccessibility.post(notification: .announcement, argument: text)
return true
}
}

View File

@ -10,6 +10,7 @@ import UIKit
import Combine
import VDSCoreTokens
@objcMembers
@objc(VDSTextView)
open class TextView: UITextView, ViewProtocol, Errorable {
@ -41,10 +42,19 @@ open class TextView: UITextView, ViewProtocol, Errorable {
// MARK: - Private Properties
//--------------------------------------------------
private var initialSetupPerformed = false
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
open var placeholder: String? { didSet { setNeedsUpdate() } }
open var placeholderLabel = Label().with {
$0.textColorConfiguration = ViewColorConfiguration().with {
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forDisabled: true)
$0.setSurfaceColors(VDSColor.elementsSecondaryOnlight, VDSColor.elementsSecondaryOndark, forDisabled: false)
}.eraseToAnyColorable()
}
/// Key of whether or not updateView() is called in setNeedsUpdate()
open var shouldUpdateView: Bool = true
@ -88,6 +98,7 @@ open class TextView: UITextView, ViewProtocol, Errorable {
if textAlignment != oldValue {
// Text alignment can be part of our paragraph style, so we may need to
// re-style when changed
placeholderLabel.textAlignment = textAlignment
updateLabel()
}
}
@ -118,6 +129,9 @@ open class TextView: UITextView, ViewProtocol, Errorable {
done.pinCenterY()
.pinTrailing(16)
inputAccessoryView = accessView
addSubview(placeholderLabel)
placeholderLabel.pinToSuperView()
}
@objc func doneButtonAction() {
@ -145,13 +159,16 @@ open class TextView: UITextView, ViewProtocol, Errorable {
setNeedsUpdate()
}
open override func layoutSubviews() {
super.layoutSubviews()
placeholderLabel.preferredMaxLayoutWidth = textContainer.size.width - textContainer.lineFragmentPadding * 2
}
//--------------------------------------------------
// MARK: - Accessibility
//--------------------------------------------------
open var accessibilityAction: ((TextView) -> Void)?
private var _isAccessibilityElement: Bool = false
open override var isAccessibilityElement: Bool {
get {
var block: AXBoolReturnBlock?
@ -167,15 +184,14 @@ open class TextView: UITextView, ViewProtocol, Errorable {
if let block {
return block()
} else {
return _isAccessibilityElement
return super.isAccessibilityElement
}
}
set {
_isAccessibilityElement = newValue
super.isAccessibilityElement = newValue
}
}
private var _accessibilityLabel: String?
open override var accessibilityLabel: String? {
get {
var block: AXStringReturnBlock?
@ -190,15 +206,14 @@ open class TextView: UITextView, ViewProtocol, Errorable {
if let block {
return block()
} else {
return _accessibilityLabel
return super.accessibilityLabel
}
}
set {
_accessibilityLabel = newValue
super.accessibilityLabel = newValue
}
}
private var _accessibilityHint: String?
open override var accessibilityHint: String? {
get {
var block: AXStringReturnBlock?
@ -213,15 +228,14 @@ open class TextView: UITextView, ViewProtocol, Errorable {
if let block {
return block()
} else {
return _accessibilityHint
return super.accessibilityHint
}
}
set {
_accessibilityHint = newValue
super.accessibilityHint = newValue
}
}
private var _accessibilityValue: String?
open override var accessibilityValue: String? {
get {
var block: AXStringReturnBlock?
@ -237,11 +251,11 @@ open class TextView: UITextView, ViewProtocol, Errorable {
if let block{
return block()
} else {
return _accessibilityValue
return super.accessibilityValue
}
}
set {
_accessibilityValue = newValue
super.accessibilityValue = newValue
}
}
@ -301,6 +315,10 @@ open class TextView: UITextView, ViewProtocol, Errorable {
} else {
attributedText = nil
}
placeholderLabel.textStyle = textStyle
placeholderLabel.surface = surface
placeholderLabel.text = placeholder
placeholderLabel.isHidden = !text.isEmpty
}
}

View File

@ -10,6 +10,7 @@ import VDSCoreTokens
import UIKit
import Combine
@objcMembers
@objc(VDSTileContainer)
open class TileContainer: TileContainerBase<TileContainer.Padding> {
@ -44,6 +45,7 @@ open class TileContainer: TileContainerBase<TileContainer.Padding> {
}
open class TileContainerBase<PaddingType: DefaultValuing>: Control where PaddingType.ValueType == CGFloat {
//--------------------------------------------------
// MARK: - Initializers
//--------------------------------------------------
@ -73,7 +75,7 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
case custom(UIColor)
private var reflectedValue: String { String(reflecting: self) }
public static func == (lhs: Self, rhs: Self) -> Bool {
lhs.reflectedValue == rhs.reflectedValue
}
@ -85,7 +87,7 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
case gradient(UIColor, UIColor)
case none
}
/// Enum used to describe the aspect ratios used for this component.
public enum AspectRatio: String, CaseIterable {
case ratio1x1 = "1:1"
@ -108,9 +110,14 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
$0.contentMode = .scaleAspectFill
$0.clipsToBounds = true
}
internal var containerView = View()
open var containerView = View().with {
$0.setContentHuggingPriority(.defaultLow, for: .horizontal)
$0.setContentHuggingPriority(.defaultLow, for: .vertical)
$0.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
$0.setContentCompressionResistancePriority(.defaultHigh, for: .vertical)
}
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
@ -119,27 +126,27 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
/// This is the container in which views will be pinned.
open var contentView = View()
/// This is the view used to show the high light color for a onClick.
open var highlightView = View().with {
$0.isUserInteractionEnabled = false
}
/// This controls the aspect ratio for the component.
open var aspectRatio: AspectRatio = .ratio1x1 { didSet { setNeedsUpdate() } }
/// Sets the background color for the component.
open var color: BackgroundColor? { didSet { setNeedsUpdate() } }
/// Sets the background effect for the component.
open var backgroundEffect: BackgroundEffect = .none { didSet { setNeedsUpdate() } }
/// Sets the inside padding for the component
open var padding: PaddingType = PaddingType.defaultValue { didSet { setNeedsUpdate() } }
/// Applies a background color if backgroundImage prop fails or has trouble loading.
open var imageFallbackColor: Surface = .light { didSet { setNeedsUpdate() } }
private var _width: CGFloat?
/// Sets the width for the component. Accepts a pixel value.
open var width: CGFloat? {
@ -153,7 +160,7 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
setNeedsUpdate()
}
}
private var _height: CGFloat?
/// Sets the height for the component. Accepts a pixel value.
open var height: CGFloat? {
@ -173,18 +180,14 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
/// Determines if there is a drop shadow or not.
open var showDropShadow: Bool = false { didSet { setNeedsUpdate() } }
//--------------------------------------------------
// MARK: - Constraints
//--------------------------------------------------
internal var widthConstraint: NSLayoutConstraint?
internal var heightConstraint: NSLayoutConstraint?
internal var heightGreaterThanConstraint: NSLayoutConstraint?
internal var containerTopConstraint: NSLayoutConstraint?
internal var containerBottomConstraint: NSLayoutConstraint?
internal var containerLeadingConstraint: NSLayoutConstraint?
internal var containerTrailingConstraint: NSLayoutConstraint?
internal var aspectRatioConstraint: NSLayoutConstraint?
//--------------------------------------------------
// MARK: - Configuration
//--------------------------------------------------
@ -222,29 +225,21 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
super.setup()
isAccessibilityElement = false
let layoutGuide = UILayoutGuide()
addLayoutGuide(layoutGuide)
layoutGuide
.pinTop()
.pinLeading()
.pinTrailing(0, .defaultHigh)
.pinBottom(0, .defaultHigh)
addSubview(backgroundImageView)
addSubview(containerView)
containerView.addSubview(contentView)
addSubview(highlightView)
containerView.pinToSuperView()
widthConstraint = layoutGuide.widthAnchor.constraint(equalToConstant: 0)
heightGreaterThanConstraint = layoutGuide.heightAnchor.constraint(greaterThanOrEqualToConstant: 44.0)
heightGreaterThanConstraint?.isActive = false
heightConstraint = layoutGuide.heightAnchor.constraint(equalToConstant: 0)
containerView.addSubview(backgroundImageView)
backgroundImageView.pinToSuperView()
containerView.addSubview(contentView)
contentView.pinToSuperView()
containerView.addSubview(highlightView)
highlightView.pinToSuperView()
widthConstraint = widthAnchor.constraint(equalToConstant: 0).deactivate()
heightConstraint = heightAnchor.constraint(equalToConstant: 0).deactivate()
backgroundImageView.setContentHuggingPriority(.defaultLow, for: .horizontal)
backgroundImageView.setContentHuggingPriority(.defaultLow, for: .vertical)
backgroundImageView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
@ -252,25 +247,28 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
backgroundImageView.isUserInteractionEnabled = false
backgroundImageView.isHidden = true
containerTopConstraint = contentView.pinTop(anchor: layoutGuide.topAnchor, constant: padding.value)
containerBottomConstraint = layoutGuide.pinBottom(anchor: contentView.bottomAnchor, constant: padding.value)
containerLeadingConstraint = contentView.pinLeading(anchor: layoutGuide.leadingAnchor, constant: padding.value)
containerTrailingConstraint = layoutGuide.pinTrailing(anchor: contentView.trailingAnchor, constant: padding.value)
highlightView.pin(layoutGuide)
highlightView.isHidden = true
highlightView.backgroundColor = .clear
//corner radius
layer.cornerRadius = cornerRadius
containerView.layer.cornerRadius = cornerRadius
backgroundImageView.layer.cornerRadius = cornerRadius
highlightView.layer.cornerRadius = cornerRadius
clipsToBounds = true
containerView.clipsToBounds = true
containerView.bridge_isAccessibilityElementBlock = { [weak self] in self?.onClickSubscriber != nil }
containerView.accessibilityHint = "Double tap to open."
containerView.accessibilityLabel = nil
NotificationCenter.default
.publisher(for: UIDevice.orientationDidChangeNotification)
.sink() { [weak self] _ in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.001) { [weak self] in
guard let self else { return }
setNeedsUpdate()
}
}.store(in: &subscribers)
}
/// Overriden to take the hit if there is an onClickSubscriber and the view is not a UIControl
@ -286,7 +284,7 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
super.reset()
shouldUpdateView = false
color = .white
aspectRatio = .ratio1x1
aspectRatio = .none
imageFallbackColor = .light
width = nil
height = nil
@ -295,7 +293,7 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
shouldUpdateView = true
setNeedsUpdate()
}
/// Used to make changes to the View based off a change events or from local properties.
open override func updateView() {
super.updateView()
@ -303,44 +301,14 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
highlightView.backgroundColor = hightLightViewColorConfiguration.getColor(self)
highlightView.isHidden = !isHighlighted
layer.borderColor = borderColorConfiguration.getColor(self).cgColor
layer.borderWidth = showBorder ? VDSFormControls.borderWidth : 0
containerTopConstraint?.constant = padding.value
containerLeadingConstraint?.constant = padding.value
containerBottomConstraint?.constant = padding.value
containerTrailingConstraint?.constant = padding.value
if let width, aspectRatio == .none && height == nil{
widthConstraint?.constant = width
widthConstraint?.isActive = true
heightConstraint?.isActive = false
heightGreaterThanConstraint?.isActive = true
} else if let height, let width {
widthConstraint?.constant = width
heightConstraint?.constant = height
heightConstraint?.isActive = true
widthConstraint?.isActive = true
heightGreaterThanConstraint?.isActive = false
} else if let width {
let size = ratioSize(for: width)
widthConstraint?.constant = size.width
heightConstraint?.constant = size.height
widthConstraint?.isActive = true
heightConstraint?.isActive = true
heightGreaterThanConstraint?.isActive = false
} else {
widthConstraint?.isActive = false
heightConstraint?.isActive = false
}
applyBackgroundEffects()
containerView.layer.borderColor = borderColorConfiguration.getColor(self).cgColor
containerView.layer.borderWidth = showBorder ? VDSFormControls.borderWidth : 0
contentView.removeConstraints()
contentView.pinToSuperView(.uniform(padding.value))
updateContainerView()
if showDropShadow, surface == .light {
addDropShadow(dropShadowConfiguration)
} else {
removeDropShadows()
}
}
open override var accessibilityElements: [Any]? {
@ -363,23 +331,16 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
//append all children that are accessible
items.append(contentsOf: elements)
return items
}
set {}
}
/// Used to update frames for the added CAlayers to our view
open override func layoutSubviews() {
super.layoutSubviews()
dropShadowLayers?.forEach { $0.frame = bounds }
gradientLayers?.forEach { $0.frame = bounds }
}
//--------------------------------------------------
// MARK: - Public Methods
//--------------------------------------------------
/// This will place a view within the contentView of this component.
public func addContentView(_ view: UIView, shouldPin: Bool = true) {
view.removeFromSuperview()
@ -388,7 +349,7 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
view.pinToSuperView()
}
}
//--------------------------------------------------
// MARK: - Private Methods
//--------------------------------------------------
@ -400,58 +361,134 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
switch backgroundEffect {
case .transparency:
alphaConfiguration = 0.8
removeGradientLayer()
containerView.removeGradientLayer()
case .gradient(let firstColor, let secondColor):
alphaConfiguration = 1.0
addGradientLayer(with: firstColor, secondColor: secondColor)
containerView.addGradientLayer(with: firstColor, secondColor: secondColor)
backgroundImageView.isHidden = true
backgroundImageView.alpha = 1.0
case .none:
alphaConfiguration = 1.0
removeGradientLayer()
containerView.removeGradientLayer()
}
if let backgroundImage {
backgroundImageView.image = backgroundImage
backgroundImageView.isHidden = false
backgroundImageView.alpha = alphaConfiguration
backgroundColor = imageFallbackColor.withAlphaComponent(alphaConfiguration)
containerView.backgroundColor = imageFallbackColor.withAlphaComponent(alphaConfiguration)
} else {
backgroundImageView.isHidden = true
backgroundImageView.alpha = 1.0
backgroundColor = color.withAlphaComponent(alphaConfiguration)
containerView.backgroundColor = color.withAlphaComponent(alphaConfiguration)
}
}
private func updateContainerView() {
applyBackgroundEffects()
if showDropShadow, surface == .light {
containerView.addDropShadow(dropShadowConfiguration)
} else {
containerView.removeDropShadows()
}
containerView.dropShadowLayers?.forEach { $0.frame = containerView.bounds }
containerView.gradientLayers?.forEach { $0.frame = containerView.bounds }
//sizing the container with constraints
//Set local vars
var containerViewWidth: CGFloat? = width
let containerViewHeight: CGFloat? = height
let multiplier = aspectRatio.multiplier
//turn off the constraints
aspectRatioConstraint?.deactivate()
widthConstraint?.deactivate()
heightConstraint?.deactivate()
//-------------------------------------------------------------------------
//Overriding Nil Width Rules
//-------------------------------------------------------------------------
//Rule 1:
//In the scenario where we only have a height but the multiplie is nil, we
//want to set the width with the parent's width which will more or less "fill"
//the container horizontally
//- height is set
//- width is not set
//- aspectRatio is not set
if let superviewWidth, superviewWidth > 0,
containerViewHeight != nil,
containerViewWidth == nil,
multiplier == nil {
containerViewWidth = superviewWidth
}
//Rule 2:
//In the scenario where no width and height is set, want to set the width with the
//parent's width which will more or less "fill" the container horizontally
//- height is not set
//- width is not set
else if let superviewWidth, superviewWidth > 0,
containerViewWidth == nil,
containerViewHeight == nil {
containerViewWidth = superviewWidth
}
//-------------------------------------------------------------------------
//-------------------------------------------------------------------------
//Width + AspectRatio Constraint
//-------------------------------------------------------------------------
if let containerViewWidth,
let multiplier,
containerViewWidth > 0,
containerViewHeight == nil {
widthConstraint?.constant = containerViewWidth
widthConstraint?.activate()
aspectRatioConstraint = heightAnchor.constraint(equalTo: widthAnchor, multiplier: multiplier)
aspectRatioConstraint?.activate()
}
//-------------------------------------------------------------------------
//Height + AspectRatio Constraint
//-------------------------------------------------------------------------
else if let containerViewHeight,
let multiplier,
containerViewHeight > 0,
containerViewWidth == nil {
heightConstraint?.constant = containerViewHeight
heightConstraint?.activate()
aspectRatioConstraint = widthAnchor.constraint(equalTo: heightAnchor, multiplier: multiplier)
aspectRatioConstraint?.activate()
} else {
//-------------------------------------------------------------------------
//Width Constraint
//-------------------------------------------------------------------------
if let containerViewWidth,
containerViewWidth > 0 {
widthConstraint?.constant = containerViewWidth
widthConstraint?.activate()
}
//-------------------------------------------------------------------------
//Height Constraint
//-------------------------------------------------------------------------
if let containerViewHeight,
containerViewHeight > 0 {
heightConstraint?.constant = containerViewHeight
heightConstraint?.activate()
}
}
}
private func ratioSize(for width: CGFloat) -> CGSize {
var height: CGFloat = width
switch aspectRatio {
case .ratio1x1:
break;
case .ratio3x4:
height = (4 / 3) * width
case .ratio4x3:
height = (3 / 4) * width
case .ratio2x3:
height = (3 / 2) * width
case .ratio3x2:
height = (2 / 3) * width
case .ratio9x16:
height = (16 / 9) * width
case .ratio16x9:
height = (9 / 16) * width
case .ratio1x2:
height = (2 / 1) * width
case .ratio2x1:
height = (1 / 2) * width
default:
break
}
return CGSize(width: width, height: height)
/// This is the size of the superview's allowed space for this container first by constrained size which would include padding/inset values an
private var superviewWidth: CGFloat? {
horizontalPinnedWidth() ?? superview?.frame.size.width
}
}
extension TileContainerBase {
@ -491,3 +528,30 @@ extension TileContainerBase {
}
}
}
extension TileContainerBase.AspectRatio {
var multiplier: CGFloat? {
switch self {
case .ratio1x1:
return 1
case .ratio3x4:
return 4 / 3
case .ratio4x3:
return 3 / 4
case .ratio2x3:
return 3 / 2
case .ratio3x2:
return 2 / 3
case .ratio9x16:
return 16 / 9
case .ratio16x9:
return 9 / 16
case .ratio1x2:
return 2 / 1
case .ratio2x1:
return 1 / 2
case .none:
return nil
}
}
}

View File

@ -15,6 +15,7 @@ import Combine
/// support quick scanning and engagement. A Tilelet is fully clickable and
/// while it can include an arrow CTA, it does not require one in order to
/// function.
@objcMembers
@objc(VDSTilelet)
open class Tilelet: TileContainerBase<Tilelet.Padding> {
@ -205,15 +206,10 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
}
/// Descriptive Icon positioned in the contentView.
open var descriptiveIcon = Icon().with {
$0.isAccessibilityElement = false
}
open var descriptiveIcon = Icon()
/// Directional Icon positioned in the contentView.
open var directionalIcon = Icon().with {
$0.isAccessibilityElement = false
$0.name = .rightArrow
}
open var directionalIcon = Icon()
private var _textWidth: TextWidth?
@ -302,8 +298,9 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
open override func setup() {
super.setup()
aspectRatio = .none
color = .black
aspectRatio = .none
addContentView(stackView)
//badge
@ -381,11 +378,22 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
titleLockupSubTitleLabelHeightGreaterThanConstraint = titleLockup.subTitleLabel.heightGreaterThanEqualTo(constant: titleLockup.subTitleLabel.minimumLineHeight)
titleLockupSubTitleLabelHeightGreaterThanConstraint?.priority = .defaultHigh
titleLockupSubTitleLabelHeightGreaterThanConstraint?.activate()
directionalIcon.bridge_accessibilityLabelBlock = { [weak self] in
guard let self, let directionalIconModel else { return nil }
return directionalIconModel.accessibleText
}
descriptiveIcon.bridge_accessibilityLabelBlock = { [weak self] in
guard let self, let descriptiveIconModel else { return nil }
return descriptiveIconModel.accessibleText
}
}
/// Resets to default settings.
open override func reset() {
shouldUpdateView = false
super.reset()
aspectRatio = .none
color = .black
//models
@ -405,18 +413,14 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
updateBadge()
updateTitleLockup()
updateIcons()
///Content-driven height Tilelets - Minimum height is configurable.
///if width != nil && (aspectRatio != .none || height != nil) then tilelet is not self growing, so we can apply text position alignments.
if width != nil && (aspectRatio != .none || height != nil) {
updateTextPositionAlignment()
}
updateTextPositionAlignment()
setNeedsLayout()
}
/// Used to update any Accessibility properties.
open override var accessibilityElements: [Any]? {
get {
var views = [UIView]()
var views = [AnyObject]()
// grab the available views in order
if badgeModel != nil {
@ -424,7 +428,15 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
}
if titleModel != nil || subTitleModel != nil || eyebrowModel != nil {
views.append(titleLockup)
let titleLockupViews = gatherAccessibilityElements(from: titleLockup)
views.append(contentsOf: titleLockupViews)
}
if descriptiveIconModel != nil {
views.append(descriptiveIcon)
} else if directionalIconModel != nil {
views.append(directionalIcon)
}
containerView.setAccessibilityLabel(for: views)
@ -584,6 +596,7 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
}
private func updateTextPositionAlignment() {
guard width != nil && (aspectRatio != .none || height != nil) else { return }
switch textPostion {
case .top:
titleLockupTopConstraint?.activate()

View File

@ -66,6 +66,10 @@ extension Tilelet {
public var iconName: Icon.Name {
return self == .rightArrow ? .rightArrow : .externalLink
}
public var accessibilityLabel: String {
self == .rightArrow ? "Directional right arrow" : "External link"
}
}
public enum IconSize: String, EnumSubset {
@ -80,7 +84,7 @@ extension Tilelet {
public var iconColor: IconColor?
/// Accessible Text for the Icon
public var accessibleText: String
public var accessibleText: String?
/// Enum for a icon type you want shown..
public var iconType: IconType
@ -95,7 +99,7 @@ extension Tilelet {
self.iconType = iconType
self.iconColor = iconColor
self.accessibleText = accessibleText ?? iconType.iconName.rawValue
self.accessibleText = accessibleText ?? iconType.accessibilityLabel
self.size = size
}
}

View File

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

View File

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

View File

@ -12,6 +12,7 @@ import Combine
/// A toggle is a control that lets customers instantly turn on
/// or turn off a single option, setting or function.
@objcMembers
@objc(VDSToggleView)
open class ToggleView: Control, Changeable, FormFieldable {
@ -219,7 +220,7 @@ open class ToggleView: Control, Changeable, FormFieldable {
}
knobTrailingConstraint?.isActive = true
knobLeadingConstraint?.isActive = true
setNeedsLayout()
layoutIfNeeded()
}
private func updateToggle() {

View File

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

View File

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

View File

@ -9,6 +9,8 @@ import Foundation
import UIKit
import VDSCoreTokens
@objcMembers
@objc(VDSTooltipDialog)
open class TooltipDialog: View, UIScrollViewDelegate {
//--------------------------------------------------

View File

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

View File

@ -14,14 +14,14 @@ extension UIView {
/// - views: Array of Views that you want to join the accessibilityLabel.
/// - separator: Separator used between the accessibilityLabel for each UIView.
/// - Returns: Joined String.
public func combineAccessibilityLabel(for views: [UIView], separator: String = ", ") -> String? {
let labels = views.map({($0.accessibilityLabel?.isEmpty ?? true) ? nil : $0.accessibilityLabel}).compactMap({$0})
public func combineAccessibilityLabel(for views: [AnyObject], separator: String = ", ") -> String? {
let labels: [String] = views.map({($0.accessibilityLabel?.isEmpty ?? true) ? nil : $0.accessibilityLabel}).compactMap({$0})
return labels.joined(separator: separator)
}
/// AccessibilityLabel helper for joining the accessibilityLabel property of all views passed in.
/// - Parameter views: Array of Views that you want to join the accessibilityLabel.
public func setAccessibilityLabel(for views: [UIView]) {
public func setAccessibilityLabel(for views: [AnyObject]) {
accessibilityLabel = combineAccessibilityLabel(for: views)
}
@ -50,8 +50,8 @@ extension UIView {
return isIntersecting
}
public func gatherAccessibilityElements(from view: UIView) -> [Any] {
var elements: [Any] = []
public func gatherAccessibilityElements(from view: AnyObject) -> [AnyObject] {
var elements: [AnyObject] = []
for subview in view.subviews {
if subview.isAccessibilityElement && subview.isVisibleOnScreen {

View File

@ -20,6 +20,9 @@ public protocol FormFieldable {
/// Protocol for FormFieldable that require internal validation.
public protocol FormFieldInternalValidatable: FormFieldable, Errorable {
/// Rules that drive the validator
var rules: [AnyRule<ValueType>] { get set }
/// Is there an internalError
var hasInternalError: Bool { get }
/// Internal Error Message that will show.

View File

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

View File

@ -1,6 +1,32 @@
1.0.68
1.0.71
----------------
- CXTDT-581800 - DatePicker - Selected Error state icon
- CXTDT-581801 - DatePicker - border disappears for on dark focus state
- CXTDT-581803 - DatePicker - Calendar does not switch to Dark Mode
- CXTDT-584278 InputField - Accessibility
- CXTDT-586375 - Table - Issue With Stripe
- CXTDT-577463 - InputField - Accessibility - #7
- CXTDT-565796 - DropdownSelect Removed the "Type" from the VoiceOver
1.0.70
----------------
- CXTDT-577463 - InputField - Accessibility - #1 Typing Feedback
- CXTDT-577463 - InputField - Accessibility - #5 Password / Inline Action
- CXTDT-560485 - Tilelet - Accessibility Icons
- DatePicker - Final logic for how the calendar shows.
1.0.69
----------------
- DatePicker - Refactored how this is shown
- Checkbox Item/Group - Accessibility Refactor
- Radiobox Item/Group - Accessibility Refactor
- Radiobutton Item/Group - Accessibility Refactor
- CXTDT-553663 - DropdownSelect - Accessibility - has popup
- CXTDT-577463 - InputField - Accessibility
1.0.69
----------------
- Expired Build because of a issue
1.0.67
----------------

View File

@ -26,6 +26,7 @@ Using the system allows designers and developers to collaborate more easily and
- ``ButtonIcon``
- ``ButtonGroup``
- ``CalendarBase``
- ``Carousel``
- ``CarouselScrollbar``
- ``Checkbox``
- ``CheckboxItem``