Merge branch 'develop' of https://gitlab.verizon.com/BPHV_MIPS/vds_ios into vasavk/calendar
# Conflicts: # VDS.xcodeproj/project.pbxproj
This commit is contained in:
commit
76022ab6eb
@ -62,6 +62,10 @@
|
||||
EA21C5DB2B600EDE00CFC139 /* VDSTokens.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = EA21C5DA2B600EDD00CFC139 /* VDSTokens.xcframework */; };
|
||||
EA297A5529FB07760031ED56 /* TooltipLabelAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA297A5429FB07760031ED56 /* TooltipLabelAttribute.swift */; };
|
||||
EA297A5729FB0A360031ED56 /* AppleGuidelinesTouchable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA297A5629FB0A360031ED56 /* AppleGuidelinesTouchable.swift */; };
|
||||
EA2DC9B02BE175BA004F58C5 /* RequiredRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA2DC9AF2BE175BA004F58C5 /* RequiredRule.swift */; };
|
||||
EA2DC9B22BE175E6004F58C5 /* CharacterCountRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA2DC9B12BE175E6004F58C5 /* CharacterCountRule.swift */; };
|
||||
EA2DC9B42BE2C6FE004F58C5 /* TextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA2DC9B32BE2C6FE004F58C5 /* TextField.swift */; };
|
||||
EA2DC9B62BE2F4A1004F58C5 /* UITextView+Publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA2DC9B52BE2F4A1004F58C5 /* UITextView+Publisher.swift */; };
|
||||
EA336171288B19200071C351 /* VDS.docc in Sources */ = {isa = PBXBuildFile; fileRef = EA336170288B19200071C351 /* VDS.docc */; };
|
||||
EA336177288B19210071C351 /* VDS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EA33616C288B19200071C351 /* VDS.framework */; };
|
||||
EA33617C288B19210071C351 /* VDSTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA33617B288B19210071C351 /* VDSTests.swift */; };
|
||||
@ -144,6 +148,7 @@
|
||||
EAB5FEF829393A7200998C17 /* ButtonGroupConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB5FEF729393A7200998C17 /* ButtonGroupConstants.swift */; };
|
||||
EAB5FF0129424ACB00998C17 /* UIControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB5FF0029424ACB00998C17 /* UIControl.swift */; };
|
||||
EABFEB642A26473700C4C106 /* NSAttributedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = EABFEB632A26473700C4C106 /* NSAttributedString.swift */; };
|
||||
EAC58BFD2BE935C300BA39FA /* TitleLockupTextColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58BFC2BE935C300BA39FA /* TitleLockupTextColor.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 */; };
|
||||
@ -259,6 +264,10 @@
|
||||
EA21C5DA2B600EDD00CFC139 /* VDSTokens.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = VDSTokens.xcframework; path = ../SharedFrameworks/VDSTokens.xcframework; sourceTree = "<group>"; };
|
||||
EA297A5429FB07760031ED56 /* TooltipLabelAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TooltipLabelAttribute.swift; sourceTree = "<group>"; };
|
||||
EA297A5629FB0A360031ED56 /* AppleGuidelinesTouchable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleGuidelinesTouchable.swift; sourceTree = "<group>"; };
|
||||
EA2DC9AF2BE175BA004F58C5 /* RequiredRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequiredRule.swift; sourceTree = "<group>"; };
|
||||
EA2DC9B12BE175E6004F58C5 /* CharacterCountRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCountRule.swift; sourceTree = "<group>"; };
|
||||
EA2DC9B32BE2C6FE004F58C5 /* TextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextField.swift; sourceTree = "<group>"; };
|
||||
EA2DC9B52BE2F4A1004F58C5 /* UITextView+Publisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextView+Publisher.swift"; sourceTree = "<group>"; };
|
||||
EA33616C288B19200071C351 /* VDS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = VDS.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
EA33616F288B19200071C351 /* VDS.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VDS.h; sourceTree = "<group>"; };
|
||||
EA336170288B19200071C351 /* VDS.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = VDS.docc; sourceTree = "<group>"; };
|
||||
@ -343,6 +352,7 @@
|
||||
EAB5FEF729393A7200998C17 /* ButtonGroupConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonGroupConstants.swift; sourceTree = "<group>"; };
|
||||
EAB5FF0029424ACB00998C17 /* UIControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIControl.swift; sourceTree = "<group>"; };
|
||||
EABFEB632A26473700C4C106 /* NSAttributedString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSAttributedString.swift; sourceTree = "<group>"; };
|
||||
EAC58BFC2BE935C300BA39FA /* TitleLockupTextColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleLockupTextColor.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>"; };
|
||||
@ -543,6 +553,15 @@
|
||||
path = ButtonGroup;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EA2DC9AE2BE175A6004F58C5 /* Rules */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EA2DC9AF2BE175BA004F58C5 /* RequiredRule.swift */,
|
||||
EA2DC9B12BE175E6004F58C5 /* CharacterCountRule.swift */,
|
||||
);
|
||||
path = Rules;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EA336162288B19200071C351 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -784,12 +803,13 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EA5E30522950DDA60082B959 /* TitleLockup.swift */,
|
||||
EAEEECAC2B1FC1A600531FC2 /* TitleLockupChangeLog.txt */,
|
||||
EA985BEF2968A93600F2FF2E /* TitleLockupEyebrowModel.swift */,
|
||||
EA985BED2968A92400F2FF2E /* TitleLockupSubTitleModel.swift */,
|
||||
EA985BEB2968A91200F2FF2E /* TitleLockupTitleModel.swift */,
|
||||
EAC58BFC2BE935C300BA39FA /* TitleLockupTextColor.swift */,
|
||||
EA985BF12968B5BB00F2FF2E /* TitleLockupTextStyle.swift */,
|
||||
EA513A942A4E1F82002A4DFF /* TitleLockupStyleConfiguration.swift */,
|
||||
EAEEECAC2B1FC1A600531FC2 /* TitleLockupChangeLog.txt */,
|
||||
);
|
||||
path = TitleLockup;
|
||||
sourceTree = "<group>";
|
||||
@ -867,6 +887,7 @@
|
||||
children = (
|
||||
EAB1D2E928AE84AA00DAE764 /* UIControlPublisher.swift */,
|
||||
EA89200328AECF4B006B9984 /* UITextField+Publisher.swift */,
|
||||
EA2DC9B52BE2F4A1004F58C5 /* UITextView+Publisher.swift */,
|
||||
EAD8D2C028BFDE8B006EB6A6 /* UIGestureRecognizer+Publisher.swift */,
|
||||
);
|
||||
path = Publishers;
|
||||
@ -907,6 +928,7 @@
|
||||
EAC925852911C9DE00091998 /* TextFields */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EA2DC9AE2BE175A6004F58C5 /* Rules */,
|
||||
EAC9258B2911C9DE00091998 /* EntryFieldBase.swift */,
|
||||
EAC925862911C9DE00091998 /* InputField */,
|
||||
EA985C21296E032000F2FF2E /* TextArea */,
|
||||
@ -918,6 +940,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EAC925872911C9DE00091998 /* InputField.swift */,
|
||||
EA2DC9B32BE2C6FE004F58C5 /* TextField.swift */,
|
||||
EA6642942BCEBF9500D81DC4 /* TextLinkModel.swift */,
|
||||
);
|
||||
path = InputField;
|
||||
@ -1159,6 +1182,7 @@
|
||||
EA0D1C412A6AD61C00E5C127 /* Typography+Additional.swift in Sources */,
|
||||
EAC925842911C63100091998 /* Colorable.swift in Sources */,
|
||||
18B463A42BBD3C46005C4528 /* DropdownOptionModel.swift in Sources */,
|
||||
EAC58BFD2BE935C300BA39FA /* TitleLockupTextColor.swift in Sources */,
|
||||
EAACB89A2B927108006A3869 /* Valuing.swift in Sources */,
|
||||
EAE785312BA0A438009428EA /* UIImage+Helper.swift in Sources */,
|
||||
EAB5FEF5292D371F00998C17 /* ButtonBase.swift in Sources */,
|
||||
@ -1171,6 +1195,7 @@
|
||||
71ACE89E2BA1CC1700FB6ADC /* TiletEyebrowModel.swift in Sources */,
|
||||
EAF7F11728A1475A00B287F5 /* RadioButtonItem.swift in Sources */,
|
||||
EA985BEE2968A92400F2FF2E /* TitleLockupSubTitleModel.swift in Sources */,
|
||||
EA2DC9B22BE175E6004F58C5 /* CharacterCountRule.swift in Sources */,
|
||||
EA985BF22968B5BB00F2FF2E /* TitleLockupTextStyle.swift in Sources */,
|
||||
EAB1D2CD28ABE76100DAE764 /* Withable.swift in Sources */,
|
||||
71FC86DC2B96F4C800700965 /* PaginationCellItem.swift in Sources */,
|
||||
@ -1180,11 +1205,13 @@
|
||||
71FC86DE2B9738B900700965 /* SurfaceConfigurationValue.swift in Sources */,
|
||||
EA297A5529FB07760031ED56 /* TooltipLabelAttribute.swift in Sources */,
|
||||
EA985BEA29689B6D00F2FF2E /* TileletSubTitleModel.swift in Sources */,
|
||||
EA2DC9B02BE175BA004F58C5 /* RequiredRule.swift in Sources */,
|
||||
18FEA1B72BE0EBFE00A56439 /* CalendarHeaderView.swift in Sources */,
|
||||
EA3361C9289054C50071C351 /* Surfaceable.swift in Sources */,
|
||||
EAB5FEED2927E1B200998C17 /* ButtonGroupPositionLayout.swift in Sources */,
|
||||
EA4DB30228DCBCA500103EE3 /* Badge.swift in Sources */,
|
||||
EA33624728931B050071C351 /* Initable.swift in Sources */,
|
||||
EA2DC9B62BE2F4A1004F58C5 /* UITextView+Publisher.swift in Sources */,
|
||||
EAF7F0A4289B017C00B287F5 /* LabelAttributeModel.swift in Sources */,
|
||||
EA0B18022A9E236900F2D0CD /* SelectorGroupBase.swift in Sources */,
|
||||
EA5F86D02A1F936100BC83E4 /* TabsContainer.swift in Sources */,
|
||||
@ -1262,6 +1289,7 @@
|
||||
EAB5FEF12927F4AA00998C17 /* SelfSizingCollectionView.swift in Sources */,
|
||||
EA3361B8288B2AAA0071C351 /* ViewProtocol.swift in Sources */,
|
||||
EA3361A8288B23300071C351 /* UIColor.swift in Sources */,
|
||||
EA2DC9B42BE2C6FE004F58C5 /* TextField.swift in Sources */,
|
||||
EAC9257D29119B5400091998 /* TextLink.swift in Sources */,
|
||||
EA596ABF2A16B4F500300C4B /* Tabs.swift in Sources */,
|
||||
EAD062A72A3B67770015965D /* UIView+CALayer.swift in Sources */,
|
||||
@ -1417,7 +1445,7 @@
|
||||
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 60;
|
||||
CURRENT_PROJECT_VERSION = 62;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@ -1454,7 +1482,7 @@
|
||||
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 60;
|
||||
CURRENT_PROJECT_VERSION = 62;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
|
||||
@ -20,7 +20,7 @@ extension SelectorGroup {
|
||||
public var hasSelectedItem: Bool { items.filter { $0.isSelected == true }.count > 0 }
|
||||
}
|
||||
|
||||
public protocol SelectorGroupMultiSelect: SelectorGroup {}
|
||||
public protocol SelectorGroupMultiSelect: SelectorGroup, FormFieldable {}
|
||||
extension SelectorGroupMultiSelect {
|
||||
/// Current Selected Control for this group.
|
||||
public var selectedItems: [SelectorItemType]? {
|
||||
@ -30,7 +30,7 @@ extension SelectorGroupMultiSelect {
|
||||
}
|
||||
}
|
||||
|
||||
public protocol SelectorGroupSingleSelect: SelectorGroup {}
|
||||
public protocol SelectorGroupSingleSelect: SelectorGroup, FormFieldable {}
|
||||
extension SelectorGroupSingleSelect {
|
||||
/// Current Selected Control for this group.
|
||||
public var selectedItem: SelectorItemType? {
|
||||
@ -39,7 +39,7 @@ extension SelectorGroupSingleSelect {
|
||||
}
|
||||
|
||||
/// Base Class used for any Grouped Form Control of a Selector Type.
|
||||
open class SelectorGroupBase<SelectorItemType: Control>: Control, SelectorGroup, Changeable {
|
||||
open class SelectorGroupBase<SelectorItemType: Control>: Control, SelectorGroup, Changeable {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Properties
|
||||
|
||||
@ -11,7 +11,7 @@ import Combine
|
||||
import VDSTokens
|
||||
|
||||
/// Base Class used to build out a SelectorControlable control.
|
||||
open class SelectorItemBase<Selector: SelectorControlable>: Control, Errorable, Changeable, FormFieldable {
|
||||
open class SelectorItemBase<Selector: SelectorControlable>: Control, Errorable, Changeable {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
@ -141,8 +141,10 @@ open class SelectorItemBase<Selector: SelectorControlable>: Control, Errorable,
|
||||
|
||||
open var inputId: String? { didSet { setNeedsUpdate() } }
|
||||
|
||||
open var value: AnyHashable? { didSet { setNeedsUpdate() } }
|
||||
open var value: AnyHashable? { hiddenValue }
|
||||
|
||||
open var hiddenValue: AnyHashable? { didSet { setNeedsUpdate() } }
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
@ -214,7 +216,6 @@ open class SelectorItemBase<Selector: SelectorControlable>: Control, Errorable,
|
||||
showError = false
|
||||
errorText = nil
|
||||
inputId = nil
|
||||
value = nil
|
||||
isSelected = false
|
||||
|
||||
onChange = nil
|
||||
|
||||
@ -23,12 +23,12 @@ public final class SelfSizingCollectionView: UICollectionView {
|
||||
/// - layout: Layout used for this CollectionView
|
||||
public override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
|
||||
super.init(frame: frame, collectionViewLayout: layout)
|
||||
self.setupContentSizeObservation()
|
||||
self.initialSetup()
|
||||
}
|
||||
|
||||
public required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
self.setupContentSizeObservation()
|
||||
self.initialSetup()
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
@ -69,22 +69,31 @@ public final class SelfSizingCollectionView: UICollectionView {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Methods
|
||||
//--------------------------------------------------
|
||||
private func setupContentSizeObservation() {
|
||||
private func initialSetup() {
|
||||
//ensure this hasn't run before
|
||||
guard anyCancellable == nil else { return }
|
||||
|
||||
//ensure autoLayout uses intrinsic height
|
||||
setContentHuggingPriority(.required, for: .vertical)
|
||||
setContentCompressionResistancePriority(.required, for: .vertical)
|
||||
collectionViewHeight = height(constant: 0, priority: .defaultHigh)
|
||||
|
||||
anyCancellable = self.publisher(for: \.contentSize, options: [.new])
|
||||
anyCancellable = self.publisher(for: \.contentSize, options: [.new, .old])
|
||||
.sink { [weak self] compare in
|
||||
guard let self else { return }
|
||||
if compare.height != self.collectionViewHeight?.constant {
|
||||
self.invalidateIntrinsicContentSize()
|
||||
self.collectionViewHeight?.constant = compare.height
|
||||
self.contentSizeSubject.send(compare)
|
||||
}
|
||||
|
||||
guard let self,
|
||||
let currentHeight = self.collectionViewHeight?.constant,
|
||||
compare.height != currentHeight else { return }
|
||||
|
||||
self.invalidateIntrinsicContentSize()
|
||||
self.collectionViewHeight?.constant = compare.height
|
||||
self.contentSizeSubject.send(compare)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
anyCancellable?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
extension UITraitCollection {
|
||||
|
||||
@ -210,6 +210,7 @@ open class BadgeIndicator: View {
|
||||
/// The Container's height.
|
||||
open var height: CGFloat? { didSet { setNeedsUpdate() } }
|
||||
|
||||
open var accessibilityText: String? { didSet { setNeedsUpdate() } }
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Properties
|
||||
//--------------------------------------------------
|
||||
@ -348,7 +349,9 @@ open class BadgeIndicator: View {
|
||||
|
||||
open override func updateAccessibility() {
|
||||
super.updateAccessibility()
|
||||
if kind == .numbered {
|
||||
if let accessibilityText {
|
||||
accessibilityLabel = accessibilityText
|
||||
} else if kind == .numbered {
|
||||
accessibilityLabel = label.text
|
||||
} else {
|
||||
accessibilityLabel = "Simple"
|
||||
|
||||
@ -96,7 +96,7 @@ open class BreadcrumbItem: ButtonBase {
|
||||
/// Used to update any Accessibility properties.
|
||||
open override func updateAccessibility() {
|
||||
super.updateAccessibility()
|
||||
accessibilityLabel = "Breadcrumb \(text ?? "")"
|
||||
accessibilityLabel = text
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -39,6 +39,13 @@ open class Breadcrumbs: View {
|
||||
}
|
||||
}
|
||||
|
||||
open override var accessibilityElements: [Any]? {
|
||||
get {
|
||||
return [containerView, breadcrumbs]
|
||||
}
|
||||
set {}
|
||||
}
|
||||
|
||||
/// A callback when the selected item changes. Passes parameters (crumb).
|
||||
open var onBreadcrumbDidSelect: ((BreadcrumbItem) -> Void)?
|
||||
|
||||
@ -73,6 +80,11 @@ open class Breadcrumbs: View {
|
||||
return collectionView
|
||||
}()
|
||||
|
||||
private let containerView = View().with {
|
||||
$0.isAccessibilityElement = true
|
||||
$0.accessibilityLabel = "Breadcrumbs"
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Methods
|
||||
//--------------------------------------------------
|
||||
@ -106,8 +118,10 @@ open class Breadcrumbs: View {
|
||||
/// Executed on initialization for this View.
|
||||
open override func initialSetup() {
|
||||
super.initialSetup()
|
||||
addSubview(collectionView)
|
||||
containerView.addSubview(collectionView)
|
||||
collectionView.pinToSuperView()
|
||||
addSubview(containerView)
|
||||
containerView.pinToSuperView()
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
@ -124,7 +138,7 @@ open class Breadcrumbs: View {
|
||||
super.updateView()
|
||||
collectionView.reloadData()
|
||||
}
|
||||
|
||||
|
||||
open override func layoutSubviews() {
|
||||
//Turn off the ability to execute updateView() in the super
|
||||
//since we don't want an infinite loop
|
||||
@ -139,6 +153,7 @@ open class Breadcrumbs: View {
|
||||
}
|
||||
}
|
||||
private var separatorWidth = Label().with { $0.text = "/"; $0.sizeToFit() }.intrinsicContentSize.width
|
||||
|
||||
}
|
||||
|
||||
extension Breadcrumbs: UICollectionViewDelegate, UICollectionViewDataSource, ButtongGroupPositionLayoutDelegate {
|
||||
|
||||
@ -14,6 +14,7 @@ import VDSTokens
|
||||
/// to allow user selection.
|
||||
@objc(VDSCheckboxGroup)
|
||||
open class CheckboxGroup: SelectorGroupBase<CheckboxItem>, SelectorGroupMultiSelect {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
@ -32,6 +33,10 @@ open class CheckboxGroup: SelectorGroupBase<CheckboxItem>, SelectorGroupMultiSel
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
public var inputId: String?
|
||||
|
||||
public var value: [SelectorItemType]? { selectedItems }
|
||||
|
||||
/// Array of ``CheckboxItemModel`` that will be used to build the selectorViews of type ``CheckboxItem``.
|
||||
open var selectorModels: [CheckboxItemModel]? {
|
||||
didSet {
|
||||
@ -41,7 +46,7 @@ open class CheckboxGroup: SelectorGroupBase<CheckboxItem>, SelectorGroupMultiSel
|
||||
$0.isEnabled = !model.disabled
|
||||
$0.surface = model.surface
|
||||
$0.inputId = model.inputId
|
||||
$0.value = model.value
|
||||
$0.hiddenValue = model.value
|
||||
$0.accessibilityLabel = model.accessibileText
|
||||
$0.accessibilityValue = "item \(index+1) of \(selectorModels.count)"
|
||||
$0.labelText = model.labelText
|
||||
@ -97,7 +102,7 @@ open class CheckboxGroup: SelectorGroupBase<CheckboxItem>, SelectorGroupMultiSel
|
||||
}
|
||||
|
||||
extension CheckboxGroup {
|
||||
public struct CheckboxItemModel : Surfaceable, Initable, FormFieldable, Errorable {
|
||||
public struct CheckboxItemModel : Surfaceable, Initable, Errorable {
|
||||
|
||||
/// Whether this object is disabled or not
|
||||
public var disabled: Bool
|
||||
|
||||
@ -37,6 +37,11 @@ open class DropdownSelect: EntryFieldBase {
|
||||
/// Allows unique ID to be passed to the element.
|
||||
open var selectId: Int? { didSet { setNeedsUpdate() }}
|
||||
|
||||
/// Current SelectedItem Value
|
||||
open override var value: String? {
|
||||
selectedItem?.value
|
||||
}
|
||||
|
||||
/// Current SelectedItem
|
||||
open var selectedItem: DropdownOptionModel? {
|
||||
guard let selectId else { return nil }
|
||||
@ -89,12 +94,6 @@ open class DropdownSelect: EntryFieldBase {
|
||||
// MARK: - Configuration Properties
|
||||
//--------------------------------------------------
|
||||
internal override var containerSize: CGSize { CGSize(width: showInlineLabel ? minWidthInlineLabel : width ?? minWidthDefault, height: 44) }
|
||||
|
||||
internal let iconColorConfiguration = ControlColorConfiguration().with {
|
||||
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .normal)
|
||||
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled)
|
||||
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .error)
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Overrides
|
||||
@ -103,8 +102,6 @@ open class DropdownSelect: EntryFieldBase {
|
||||
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
|
||||
accessibilityLabel = "Dropdown Select"
|
||||
|
||||
// stackview for controls in EntryFieldBase.controlContainerView
|
||||
let controlStackView = UIStackView().with {
|
||||
@ -119,6 +116,10 @@ open class DropdownSelect: EntryFieldBase {
|
||||
controlStackView.addArrangedSubview(inlineDisplayLabel)
|
||||
controlStackView.addArrangedSubview(selectedOptionLabel)
|
||||
|
||||
containerStackView.isAccessibilityElement = true
|
||||
containerStackView.accessibilityLabel = "Dropdown Select"
|
||||
inlineDisplayLabel.isAccessibilityElement = true
|
||||
|
||||
controlStackView.setCustomSpacing(0, after: dropdownField)
|
||||
controlStackView.setCustomSpacing(VDSLayout.space1X, after: inlineDisplayLabel)
|
||||
controlStackView.setCustomSpacing(VDSLayout.space3X, after: selectedOptionLabel)
|
||||
@ -163,7 +164,7 @@ open class DropdownSelect: EntryFieldBase {
|
||||
|
||||
updateInlineLabel()
|
||||
|
||||
dropdownField.isUserInteractionEnabled = readOnly ? false : true
|
||||
dropdownField.isUserInteractionEnabled = isReadOnly ? false : true
|
||||
selectedOptionLabel.surface = surface
|
||||
selectedOptionLabel.isEnabled = isEnabled
|
||||
}
|
||||
@ -191,7 +192,7 @@ open class DropdownSelect: EntryFieldBase {
|
||||
|
||||
updatedLabelText = showInlineLabel ? "" : updatedLabelText
|
||||
|
||||
if let oldText = updatedLabelText, !required, !oldText.hasSuffix("Optional") {
|
||||
if let oldText = updatedLabelText, !isRequired, !oldText.hasSuffix("Optional") {
|
||||
let optionColorAttr = ColorLabelAttribute(location: oldText.count + 2,
|
||||
length: 8,
|
||||
color: secondaryColorConfiguration.getColor(self))
|
||||
@ -234,22 +235,53 @@ open class DropdownSelect: EntryFieldBase {
|
||||
|
||||
open func updateSelectedOptionLabel(option: DropdownOptionModel? = nil) {
|
||||
selectedOptionLabel.text = option?.text ?? ""
|
||||
value = option?.value
|
||||
}
|
||||
|
||||
open override func updateErrorLabel() {
|
||||
super.updateErrorLabel()
|
||||
if !showError && !hasInternalError {
|
||||
icon.name = .downCaret
|
||||
statusIcon.name = .downCaret
|
||||
}
|
||||
icon.surface = surface
|
||||
icon.isHidden = readOnly ? true : false
|
||||
icon.color = iconColorConfiguration.getColor(self)
|
||||
statusIcon.surface = surface
|
||||
statusIcon.isHidden = isReadOnly ? true : false
|
||||
statusIcon.color = iconColorConfiguration.getColor(self)
|
||||
}
|
||||
|
||||
open override func updateAccessibility() {
|
||||
super.updateAccessibility()
|
||||
var selectedOption = selectedOptionLabel.text ?? ""
|
||||
containerStackView.accessibilityLabel = "Dropdown Select, \(selectedOption) \(isReadOnly ? ", read only" : "")"
|
||||
containerStackView.accessibilityHint = isReadOnly || !isEnabled ? "" : "Double tap to open."
|
||||
}
|
||||
|
||||
open override var accessibilityElements: [Any]? {
|
||||
get {
|
||||
var elements = [Any]()
|
||||
elements.append(contentsOf: [titleLabel, containerStackView])
|
||||
|
||||
if showError {
|
||||
elements.append(statusIcon)
|
||||
if let errorText, !errorText.isEmpty {
|
||||
elements.append(errorLabel)
|
||||
}
|
||||
}
|
||||
|
||||
if let helperText, !helperText.isEmpty {
|
||||
elements.append(helperLabel)
|
||||
}
|
||||
|
||||
return elements
|
||||
}
|
||||
|
||||
set { super.accessibilityElements = newValue }
|
||||
}
|
||||
|
||||
|
||||
@objc open func pickerDoneClicked() {
|
||||
optionsPicker.isHidden = true
|
||||
dropdownField.resignFirstResponder()
|
||||
setNeedsUpdate()
|
||||
UIAccessibility.post(notification: .layoutChanged, argument: containerStackView)
|
||||
}
|
||||
}
|
||||
|
||||
@ -260,6 +292,7 @@ extension DropdownSelect: UIPickerViewDelegate, UIPickerViewDataSource {
|
||||
|
||||
internal func launchPicker() {
|
||||
if optionsPicker.isHidden {
|
||||
UIAccessibility.post(notification: .layoutChanged, argument: optionsPicker)
|
||||
dropdownField.becomeFirstResponder()
|
||||
} else {
|
||||
dropdownField.resignFirstResponder()
|
||||
@ -284,6 +317,7 @@ extension DropdownSelect: UIPickerViewDelegate, UIPickerViewDataSource {
|
||||
guard options.count > row else { return }
|
||||
selectId = row
|
||||
updateSelectedOptionLabel(option: options[row])
|
||||
sendActions(for: .valueChanged)
|
||||
self.onItemSelected?(row, options[row])
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ import Combine
|
||||
/// It usually represents a supplementary or utilitarian action. A button icon can stand alone, but often
|
||||
/// exists in a group when there are several actions that can be performed.
|
||||
@objc(VDSButtonIcon)
|
||||
open class ButtonIcon: Control, Changeable, FormFieldable {
|
||||
open class ButtonIcon: Control, Changeable {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
@ -173,10 +173,6 @@ open class ButtonIcon: Control, Changeable, FormFieldable {
|
||||
/// Used to move the icon inside the button in both x and y axis.
|
||||
open var iconOffset: CGPoint = .init(x: 0, y: 0) { didSet { setNeedsUpdate() } }
|
||||
|
||||
open var inputId: String? { didSet { setNeedsUpdate() } }
|
||||
|
||||
open var value: AnyHashable? { didSet { setNeedsUpdate() } }
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Configuration
|
||||
//--------------------------------------------------
|
||||
@ -548,6 +544,7 @@ open class ButtonIcon: Control, Changeable, FormFieldable {
|
||||
badgeIndicator.horizontalPadding = badgeIndicatorModel.horizontalPadding
|
||||
badgeIndicator.hideDot = badgeIndicatorModel.hideDot
|
||||
badgeIndicator.hideBorder = badgeIndicatorModel.hideBorder
|
||||
badgeIndicator.accessibilityText = badgeIndicatorModel.accessibilityText
|
||||
}
|
||||
|
||||
private func updateExpandDirectionalConstraints() {
|
||||
|
||||
@ -46,6 +46,9 @@ extension ButtonIcon {
|
||||
/// Trailing Text height that will be used for the badge indicator.
|
||||
public var trailingText: String?
|
||||
|
||||
/// Accessibliity Text
|
||||
public var accessibilityText: String?
|
||||
|
||||
/// Dot Size that will be used for the badge indicator.
|
||||
public var dotSize: CGFloat?
|
||||
|
||||
@ -61,7 +64,7 @@ extension ButtonIcon {
|
||||
/// Hide Border that will be used for the badge indicator.
|
||||
public var hideBorder: Bool = false
|
||||
|
||||
public init(kind: BadgeIndicator.Kind = .simple, fillColor: BadgeIndicator.FillColor = .red, expandDirection: ExpandDirection = .right, size: BadgeIndicator.Size = .xxlarge, maximumDigits: BadgeIndicator.MaximumDigits = .two, width: CGFloat? = nil, height: CGFloat? = nil, number: Int? = nil, leadingCharacter: String? = "", trailingText: String? = "", dotSize: CGFloat? = nil, verticalPadding: CGFloat? = nil, horizontalPadding: CGFloat? = nil, hideDot: Bool = false, hideBorder: Bool = false) {
|
||||
public init(kind: BadgeIndicator.Kind = .simple, fillColor: BadgeIndicator.FillColor = .red, expandDirection: ExpandDirection = .right, size: BadgeIndicator.Size = .xxlarge, maximumDigits: BadgeIndicator.MaximumDigits = .two, width: CGFloat? = nil, height: CGFloat? = nil, number: Int? = nil, leadingCharacter: String? = "", trailingText: String? = "", accessibilityText: String? = nil, dotSize: CGFloat? = nil, verticalPadding: CGFloat? = nil, horizontalPadding: CGFloat? = nil, hideDot: Bool = false, hideBorder: Bool = false) {
|
||||
self.kind = kind
|
||||
self.fillColor = fillColor
|
||||
self.expandDirection = expandDirection
|
||||
@ -70,6 +73,7 @@ extension ButtonIcon {
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.number = number
|
||||
self.accessibilityText = accessibilityText
|
||||
self.leadingCharacter = leadingCharacter
|
||||
self.trailingText = trailingText
|
||||
self.dotSize = dotSize
|
||||
|
||||
@ -37,17 +37,19 @@ public class TooltipLabelAttribute: ActionLabelAttributeModel, TooltipLaunchable
|
||||
}
|
||||
|
||||
var frame = CGRect.zero
|
||||
let ratio: Double = 1.0 //0.80
|
||||
let yPosition: Double = -3
|
||||
if let font = attributedString.attribute(.font, at: 0, effectiveRange: &originalRange) as? UIFont {
|
||||
switch font.pointSize {
|
||||
case 15..<25:
|
||||
size = .medium
|
||||
frame = CGRect(x: 0, y: -1, width: size.value.dimensions.width * 0.80, height: size.value.dimensions.height * 0.80)
|
||||
frame = CGRect(x: 0, y: yPosition, width: size.value.dimensions.width * ratio, height: size.value.dimensions.height * ratio)
|
||||
case 0..<14:
|
||||
size = .small
|
||||
frame = CGRect(x: 0, y: -1, width: size.value.dimensions.width * 0.80 , height: size.value.dimensions.height * 0.80)
|
||||
frame = CGRect(x: 0, y: yPosition, width: size.value.dimensions.width * ratio , height: size.value.dimensions.height * ratio)
|
||||
default:
|
||||
size = .medium
|
||||
frame = CGRect(x: 0, y: -1, width: size.value.dimensions.width, height: size.value.dimensions.height)
|
||||
frame = CGRect(x: 0, y: yPosition, width: size.value.dimensions.width, height: size.value.dimensions.height)
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,7 +77,7 @@ public class TooltipLabelAttribute: ActionLabelAttributeModel, TooltipLaunchable
|
||||
self.subscriber = subscriber
|
||||
self.surface = surface
|
||||
self.model = model
|
||||
self.accessibleText = accessibleText
|
||||
self.accessibleText = accessibleText ?? model.accessibleText
|
||||
self.presenter = presenter
|
||||
|
||||
//create the tooltip click event
|
||||
|
||||
@ -52,7 +52,7 @@ open class Notification: View {
|
||||
}
|
||||
}
|
||||
|
||||
var accessibilityText: String {
|
||||
var accessibleText: String {
|
||||
switch self {
|
||||
case .info:
|
||||
"Information Message"
|
||||
@ -104,6 +104,7 @@ open class Notification: View {
|
||||
open var typeIcon = Icon().with {
|
||||
$0.name = .infoBold
|
||||
$0.size = UIDevice.isIPad ? .medium : .small
|
||||
$0.accessibilityTraits.remove(.image)
|
||||
}
|
||||
|
||||
/// Icon used for the close.
|
||||
@ -374,7 +375,7 @@ open class Notification: View {
|
||||
open override func updateAccessibility() {
|
||||
super.updateAccessibility()
|
||||
closeButton.accessibilityLabel = "Close Notification"
|
||||
typeIcon.accessibilityLabel = style.accessibilityText
|
||||
typeIcon.accessibilityLabel = style.accessibleText
|
||||
}
|
||||
|
||||
private func setConstraints() {
|
||||
|
||||
@ -189,6 +189,7 @@ open class Pagination: View {
|
||||
nextButton.isHidden = _selectedPageIndex == total - 1
|
||||
collectionView.reloadData()
|
||||
verifyIfMaxDigitChanged()
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
///Identifying if there is any change in the digits of upcoming page
|
||||
|
||||
@ -32,6 +32,10 @@ open class RadioBoxGroup: SelectorGroupBase<RadioBoxItem>, SelectorGroupSingleSe
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
public var inputId: String?
|
||||
|
||||
public var value: SelectorItemType? { selectedItem }
|
||||
|
||||
/// Array of ``RadioBoxItemModel`` that will be used to build the selectorViews of type ``RadioBoxItem``.
|
||||
open var selectorModels: [RadioBoxItemModel]? {
|
||||
didSet {
|
||||
@ -48,6 +52,7 @@ open class RadioBoxGroup: SelectorGroupBase<RadioBoxItem>, SelectorGroupSingleSe
|
||||
$0.subTextRightAttributes = model.subTextRightAttributes
|
||||
$0.isEnabled = !model.disabled
|
||||
$0.inputId = model.inputId
|
||||
$0.hiddenValue = model.value
|
||||
$0.isSelected = model.selected
|
||||
$0.strikethrough = model.strikethrough
|
||||
$0.strikethroughAccessibilityText = model.strikethroughAccessibileText
|
||||
|
||||
@ -127,7 +127,9 @@ open class RadioBoxItem: Control, Changeable, FormFieldable {
|
||||
|
||||
open var inputId: String? { didSet { setNeedsUpdate() } }
|
||||
|
||||
open var value: AnyHashable? { didSet { setNeedsUpdate() } }
|
||||
open var value: AnyHashable? { hiddenValue }
|
||||
|
||||
open var hiddenValue: AnyHashable? { didSet { setNeedsUpdate() } }
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Configuration Properties
|
||||
@ -211,7 +213,7 @@ open class RadioBoxItem: Control, Changeable, FormFieldable {
|
||||
subTextRightAttributedText = nil
|
||||
strikethrough = false
|
||||
inputId = nil
|
||||
value = nil
|
||||
hiddenValue = nil
|
||||
|
||||
isSelected = false
|
||||
onChange = nil
|
||||
|
||||
@ -32,6 +32,10 @@ open class RadioButtonGroup: SelectorGroupBase<RadioButtonItem>, SelectorGroupSi
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
public var inputId: String?
|
||||
|
||||
public var value: SelectorItemType? { selectedItem }
|
||||
|
||||
/// Array of ``RadioButtonItemModel`` that will be used to build the selectorViews of type ``RadioButtonItem``.
|
||||
open var selectorModels: [RadioButtonItemModel]? {
|
||||
didSet {
|
||||
@ -41,7 +45,7 @@ open class RadioButtonGroup: SelectorGroupBase<RadioButtonItem>, SelectorGroupSi
|
||||
$0.isEnabled = !model.disabled
|
||||
$0.surface = model.surface
|
||||
$0.inputId = model.inputId
|
||||
$0.value = model.value
|
||||
$0.hiddenValue = model.value
|
||||
$0.accessibilityLabel = model.accessibileText
|
||||
$0.accessibilityValue = "item \(index+1) of \(selectorModels.count)"
|
||||
$0.labelText = model.labelText
|
||||
@ -57,7 +61,7 @@ open class RadioButtonGroup: SelectorGroupBase<RadioButtonItem>, SelectorGroupSi
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var _showError: Bool = false
|
||||
|
||||
/// Whether not to show the error.
|
||||
|
||||
@ -83,6 +83,8 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
}
|
||||
}()
|
||||
|
||||
open var rules = [AnyRule<String>]()
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Configuration Properties
|
||||
//--------------------------------------------------
|
||||
@ -103,15 +105,24 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
$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.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled)
|
||||
$0.setSurfaceColors(VDSColor.feedbackErrorOnlight, VDSColor.feedbackErrorOndark, forState: .error)
|
||||
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: [.disabled,.error])
|
||||
}
|
||||
|
||||
internal let iconColorConfiguration = ControlColorConfiguration().with {
|
||||
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .normal)
|
||||
$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)
|
||||
}
|
||||
@ -137,7 +148,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
$0.textStyle = .bodySmall
|
||||
}
|
||||
|
||||
open var icon: Icon = Icon().with {
|
||||
open var statusIcon: Icon = Icon().with {
|
||||
$0.size = .medium
|
||||
}
|
||||
|
||||
@ -149,10 +160,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
open var showError: Bool = false { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// FormFieldValidator
|
||||
internal var validator: (any FormFieldValidatorable)?
|
||||
|
||||
/// Whether or not to show the internal error
|
||||
open var hasInternalError: Bool { !(validator?.isValid ?? true) }
|
||||
open var validator: (any FormFieldValidatorable)?
|
||||
|
||||
/// Override UIControl state to add the .error state if showError is true.
|
||||
open override var state: UIControl.State {
|
||||
@ -165,22 +173,8 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
}
|
||||
}
|
||||
|
||||
open var errorText: String? {
|
||||
didSet {
|
||||
updateContainerView()
|
||||
updateErrorLabel()
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
open var internalErrorText: String? {
|
||||
didSet {
|
||||
updateContainerView()
|
||||
updateErrorLabel()
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
open var errorText: String? { didSet { setNeedsUpdate() } }
|
||||
|
||||
open var tooltipModel: Tooltip.TooltipModel? { didSet { setNeedsUpdate() } }
|
||||
|
||||
open var transparentBackground: Bool = false { didSet { setNeedsUpdate() } }
|
||||
@ -190,22 +184,15 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
open var inputId: String? { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// The text of this textField.
|
||||
internal var _value: String?
|
||||
open var value: String? {
|
||||
get { _value }
|
||||
set {
|
||||
if let newValue, newValue != _value {
|
||||
_value = newValue
|
||||
sendActions(for: .valueChanged)
|
||||
}
|
||||
}
|
||||
get { fatalError("must be read from subclass")}
|
||||
}
|
||||
|
||||
open var defaultValue: AnyHashable? { didSet { setNeedsUpdate() } }
|
||||
|
||||
open var required: Bool = false { didSet { setNeedsUpdate() } }
|
||||
open var isRequired: Bool = false { didSet { setNeedsUpdate() } }
|
||||
|
||||
open var readOnly: Bool = false { didSet { setNeedsUpdate() } }
|
||||
open var isReadOnly: Bool = false { didSet { setNeedsUpdate() } }
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Constraints
|
||||
@ -220,7 +207,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
|
||||
isAccessibilityElement = true
|
||||
isAccessibilityElement = false
|
||||
addSubview(stackView)
|
||||
|
||||
//create the wrapping view
|
||||
@ -239,11 +226,11 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
//this is the horizontal stack that contains
|
||||
//the left, InputContainer, Icons, Buttons
|
||||
container.addSubview(containerStackView)
|
||||
containerStackView.pinToSuperView(.uniform(12))
|
||||
containerStackView.pinToSuperView(.uniform(VDSLayout.space3X))
|
||||
|
||||
//add the view to add input fields
|
||||
containerStackView.addArrangedSubview(controlContainerView)
|
||||
containerStackView.addArrangedSubview(icon)
|
||||
containerStackView.addArrangedSubview(statusIcon)
|
||||
containerStackView.setCustomSpacing(VDSLayout.space3X, after: controlContainerView)
|
||||
|
||||
//get the container this is what show helper text, error text
|
||||
@ -295,25 +282,26 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
transparentBackground = false
|
||||
width = nil
|
||||
inputId = nil
|
||||
value = nil
|
||||
defaultValue = nil
|
||||
required = false
|
||||
readOnly = false
|
||||
isRequired = false
|
||||
isReadOnly = false
|
||||
onChange = nil
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
open override func updateView() {
|
||||
super.updateView()
|
||||
|
||||
updateContainerView()
|
||||
updateTitleLabel()
|
||||
updateErrorLabel()
|
||||
updateHelperLabel()
|
||||
|
||||
backgroundColor = surface.color
|
||||
}
|
||||
|
||||
open func validate(){
|
||||
updateRules()
|
||||
validator = FormFieldValidator<EntryFieldBase>(field: self, rules: rules)
|
||||
validator?.validate()
|
||||
internalErrorText = validator?.errorMessage
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
@ -321,7 +309,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
//--------------------------------------------------
|
||||
private func updateContainerView() {
|
||||
containerView.backgroundColor = backgroundColorConfiguration.getColor(self)
|
||||
containerView.layer.borderColor = readOnly ? readOnlyBorderColorConfiguration.getColor(self).cgColor : borderColorConfiguration.getColor(self).cgColor
|
||||
containerView.layer.borderColor = isReadOnly ? readOnlyBorderColorConfiguration.getColor(self).cgColor : borderColorConfiguration.getColor(self).cgColor
|
||||
containerView.layer.borderWidth = VDSFormControls.borderWidth
|
||||
containerView.layer.cornerRadius = VDSFormControls.borderRadius
|
||||
}
|
||||
@ -339,6 +327,21 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
return bottomContainerView
|
||||
}
|
||||
|
||||
internal func updateRules() {
|
||||
rules.removeAll()
|
||||
if self.isRequired {
|
||||
let rule = RequiredRule()
|
||||
if let errorText, !errorText.isEmpty {
|
||||
rule.errorMessage = errorText
|
||||
} else if let labelText{
|
||||
rule.errorMessage = "You must enter a \(labelText)"
|
||||
} else {
|
||||
rule.errorMessage = "You must enter a value"
|
||||
}
|
||||
rules.append(.init(rule))
|
||||
}
|
||||
}
|
||||
|
||||
open func updateTitleLabel() {
|
||||
|
||||
//update the local vars for the label since we no
|
||||
@ -347,7 +350,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
var updatedLabelText = labelText
|
||||
|
||||
//dealing with the "Optional" addition to the text
|
||||
if let oldText = updatedLabelText, !required, !oldText.hasSuffix("Optional") {
|
||||
if let oldText = updatedLabelText, !isRequired, !oldText.hasSuffix("Optional") {
|
||||
if isEnabled {
|
||||
let optionColorAttr = ColorLabelAttribute(location: oldText.count + 2,
|
||||
length: 8,
|
||||
@ -370,37 +373,27 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
}
|
||||
|
||||
open func updateErrorLabel(){
|
||||
if showError, hasInternalError, let errorText, let internalErrorText {
|
||||
errorLabel.text = [internalErrorText, errorText].joined(separator: "\n")
|
||||
errorLabel.surface = surface
|
||||
errorLabel.isEnabled = isEnabled
|
||||
errorLabel.isHidden = false
|
||||
icon.name = .error
|
||||
icon.color = VDSColor.paletteBlack
|
||||
icon.surface = surface
|
||||
icon.isHidden = !isEnabled
|
||||
} else if showError, let errorText {
|
||||
if showError, let errorText {
|
||||
errorLabel.text = errorText
|
||||
errorLabel.surface = surface
|
||||
errorLabel.isEnabled = isEnabled
|
||||
errorLabel.isHidden = false
|
||||
icon.name = .error
|
||||
icon.color = VDSColor.paletteBlack
|
||||
icon.surface = surface
|
||||
icon.isHidden = !isEnabled
|
||||
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
|
||||
icon.name = .error
|
||||
icon.color = VDSColor.paletteBlack
|
||||
icon.surface = surface
|
||||
icon.isHidden = !isEnabled
|
||||
statusIcon.name = .error
|
||||
statusIcon.surface = surface
|
||||
statusIcon.isHidden = !isEnabled || state.contains(.focused)
|
||||
} else {
|
||||
icon.isHidden = true
|
||||
statusIcon.isHidden = true
|
||||
errorLabel.isHidden = true
|
||||
}
|
||||
statusIcon.color = iconColorConfiguration.getColor(self)
|
||||
}
|
||||
|
||||
open func updateHelperLabel(){
|
||||
|
||||
@ -14,7 +14,7 @@ import Combine
|
||||
/// Specialized input fields capture credit card numbers, inline actions, passwords, phone numbers,
|
||||
/// dates and security codes in their correct formats.
|
||||
@objc(VDSInputField)
|
||||
open class InputField: EntryFieldBase, UITextFieldDelegate {
|
||||
open class InputField: EntryFieldBase {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
@ -30,19 +30,19 @@ open class InputField: EntryFieldBase, UITextFieldDelegate {
|
||||
public required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
}
|
||||
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Enums
|
||||
//--------------------------------------------------
|
||||
/// Enum used to describe the input type.
|
||||
public enum FieldType: String, CaseIterable {
|
||||
case text, number, calendar, inlineAction, password, creditCard, tel, date, securityCode
|
||||
case text, number, inlineAction, password, creditCard, tel, date, securityCode
|
||||
}
|
||||
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Properties
|
||||
//--------------------------------------------------
|
||||
internal var inputFieldStackView: UIStackView = {
|
||||
internal var inputFieldStackView: UIStackView = {
|
||||
return UIStackView().with {
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
$0.axis = .horizontal
|
||||
@ -50,9 +50,9 @@ open class InputField: EntryFieldBase, UITextFieldDelegate {
|
||||
$0.spacing = 12
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
internal var minWidthConstraint: NSLayoutConstraint?
|
||||
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
@ -63,7 +63,7 @@ open class InputField: EntryFieldBase, UITextFieldDelegate {
|
||||
}
|
||||
|
||||
/// UITextField shown in the InputField.
|
||||
open var textField = UITextField().with {
|
||||
open var textField = TextField().with {
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
$0.font = TextStyle.bodyLarge.font
|
||||
}
|
||||
@ -73,37 +73,30 @@ open class InputField: EntryFieldBase, UITextFieldDelegate {
|
||||
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forDisabled: true)
|
||||
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forDisabled: false)
|
||||
}.eraseToAnyColorable()
|
||||
|
||||
|
||||
/// Representing the type of input.
|
||||
open var fieldType: FieldType = .text { didSet { setNeedsUpdate() } }
|
||||
|
||||
internal var actionTextLink = TextLink().with { $0.contentEdgeInsets = .top(-2) }
|
||||
|
||||
internal var actionTextLinkModel: TextLinkModel? { didSet { setNeedsUpdate() } }
|
||||
|
||||
open var leftIcon: Icon = Icon().with { $0.size = .medium }
|
||||
|
||||
open var actionTextLink = TextLink().with { $0.contentEdgeInsets = .top(-2) }
|
||||
|
||||
open var actionTextLinkModel: TextLinkModel? { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// The text of this TextField.
|
||||
private var _text: String?
|
||||
open var text: String? {
|
||||
get { _text }
|
||||
get { textField.text }
|
||||
set {
|
||||
if let newValue, newValue != _text {
|
||||
_text = newValue
|
||||
textField.text = newValue
|
||||
value = newValue
|
||||
}
|
||||
textField.text = newValue
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
/// The value of this textField.
|
||||
/// Value for the textField
|
||||
open override var value: String? {
|
||||
didSet {
|
||||
if text != value {
|
||||
text = value
|
||||
}
|
||||
}
|
||||
textField.text
|
||||
}
|
||||
|
||||
|
||||
var _showError: Bool = false
|
||||
/// Whether not to show the error.
|
||||
open override var showError: Bool {
|
||||
@ -134,7 +127,11 @@ open class InputField: EntryFieldBase, UITextFieldDelegate {
|
||||
var state = super.state
|
||||
if showSuccess {
|
||||
state.insert(.success)
|
||||
|
||||
} else if textField.isFirstResponder {
|
||||
state.insert(.focused)
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
}
|
||||
@ -151,27 +148,45 @@ open class InputField: EntryFieldBase, UITextFieldDelegate {
|
||||
/// 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
|
||||
|
||||
minWidthConstraint = containerView.widthAnchor.constraint(greaterThanOrEqualToConstant: 0)
|
||||
minWidthConstraint?.isActive = true
|
||||
|
||||
controlContainerView.addSubview(textField)
|
||||
textField
|
||||
.pinTop()
|
||||
.pinLeading()
|
||||
.pinTrailingLessThanOrEqualTo(nil, 0, .defaultHigh)
|
||||
.pinBottom(0, .defaultHigh)
|
||||
// stackview for controls in EntryFieldBase.controlContainerView
|
||||
let controlStackView = UIStackView().with {
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
$0.axis = .horizontal
|
||||
$0.spacing = VDSLayout.space3X
|
||||
}
|
||||
controlContainerView.addSubview(controlStackView)
|
||||
controlStackView.pinToSuperView()
|
||||
|
||||
controlStackView.addArrangedSubview(leftIcon)
|
||||
controlStackView.addArrangedSubview(textField)
|
||||
|
||||
textField.heightAnchor.constraint(equalToConstant: 20).isActive = true
|
||||
textField.delegate = self
|
||||
textField
|
||||
.textPublisher
|
||||
.sink { [weak self] text in
|
||||
self?.value = text
|
||||
.sink { [weak self] newText in
|
||||
print("textPublisher newText: \(newText)")
|
||||
self?.process(text: newText)
|
||||
self?.validate()
|
||||
self?.sendActions(for: .valueChanged)
|
||||
|
||||
}.store(in: &subscribers)
|
||||
|
||||
textField
|
||||
.publisher(for: .editingDidBegin)
|
||||
.sink { [weak self] _ in
|
||||
self?.setNeedsUpdate()
|
||||
}.store(in: &subscribers)
|
||||
|
||||
textField
|
||||
.publisher(for: .editingDidEnd)
|
||||
.sink { [weak self] _ in
|
||||
self?.validate()
|
||||
}.store(in: &subscribers)
|
||||
|
||||
stackView.addArrangedSubview(successLabel)
|
||||
stackView.setCustomSpacing(8, after: successLabel)
|
||||
|
||||
@ -188,7 +203,6 @@ open class InputField: EntryFieldBase, UITextFieldDelegate {
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
textField.text = ""
|
||||
textField.delegate = self
|
||||
|
||||
successLabel.reset()
|
||||
successLabel.textStyle = .bodySmall
|
||||
@ -207,20 +221,18 @@ open class InputField: EntryFieldBase, UITextFieldDelegate {
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
open override func updateView() {
|
||||
|
||||
//update fieldType first
|
||||
updateFieldType()
|
||||
|
||||
super.updateView()
|
||||
|
||||
textField.isEnabled = isEnabled
|
||||
textField.textColor = textFieldTextColorConfiguration.getColor(self)
|
||||
|
||||
if let actionTextLinkModel {
|
||||
actionTextLink.text = actionTextLinkModel.text
|
||||
actionTextLink.onClick = actionTextLinkModel.onClick
|
||||
actionTextLink.isHidden = false
|
||||
containerStackView.setCustomSpacing(VDSLayout.space2X, after: icon)
|
||||
} else {
|
||||
actionTextLink.isHidden = true
|
||||
containerStackView.setCustomSpacing(0, after: icon)
|
||||
}
|
||||
}
|
||||
|
||||
open override func updateErrorLabel() {
|
||||
super.updateErrorLabel()
|
||||
|
||||
//show error or success
|
||||
if showError, let _ = errorText {
|
||||
@ -232,27 +244,15 @@ open class InputField: EntryFieldBase, UITextFieldDelegate {
|
||||
successLabel.isEnabled = isEnabled
|
||||
successLabel.isHidden = false
|
||||
errorLabel.isHidden = true
|
||||
icon.name = .checkmarkAlt
|
||||
icon.color = VDSColor.paletteBlack
|
||||
icon.surface = surface
|
||||
icon.isHidden = !isEnabled
|
||||
statusIcon.name = .checkmarkAlt
|
||||
statusIcon.color = VDSColor.paletteBlack
|
||||
statusIcon.surface = surface
|
||||
statusIcon.isHidden = !isEnabled
|
||||
} else {
|
||||
icon.isHidden = true
|
||||
successLabel.isHidden = true
|
||||
}
|
||||
|
||||
//set the width constraints
|
||||
if let width, width > fieldType.width {
|
||||
widthConstraint?.constant = width
|
||||
widthConstraint?.isActive = true
|
||||
minWidthConstraint?.isActive = false
|
||||
} else {
|
||||
minWidthConstraint?.constant = fieldType.width
|
||||
widthConstraint?.isActive = false
|
||||
minWidthConstraint?.isActive = true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
open override func updateHelperLabel(){
|
||||
//remove first
|
||||
helperLabel.removeFromSuperview()
|
||||
@ -272,16 +272,135 @@ open class InputField: EntryFieldBase, UITextFieldDelegate {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open func updateFieldType() {
|
||||
|
||||
var minWidth: CGFloat = 40.0
|
||||
var leftIconName: Icon.Name?
|
||||
var actionModel: InputField.TextLinkModel?
|
||||
var toolTipModel: Tooltip.TooltipModel? = tooltipModel
|
||||
var isSecureTextEntry = false
|
||||
var rules = [AnyRule<String>]()
|
||||
var placeholderText: String?
|
||||
|
||||
if self.isRequired {
|
||||
let rule = RequiredRule()
|
||||
if let errorText {
|
||||
rule.errorMessage = errorText
|
||||
}
|
||||
rules.append(.init(rule))
|
||||
}
|
||||
|
||||
switch fieldType {
|
||||
case .text:
|
||||
break
|
||||
|
||||
case .number:
|
||||
break
|
||||
|
||||
case .inlineAction:
|
||||
minWidth = 102.0
|
||||
|
||||
case .password:
|
||||
let isHide = passwordActionType == .hide
|
||||
let buttonText = isHide ?
|
||||
hidePasswordButtonText.isEmpty ? "Hide" : hidePasswordButtonText :
|
||||
showPasswordButtonText.isEmpty ? "Show" : showPasswordButtonText
|
||||
|
||||
isSecureTextEntry = !isHide
|
||||
let nextPasswordActionType = passwordActionType.toggle()
|
||||
if let text, !text.isEmpty {
|
||||
actionModel = .init(text: buttonText,
|
||||
onClick: { [weak self] _ in
|
||||
guard let self else { return }
|
||||
self.passwordActionType = nextPasswordActionType
|
||||
})
|
||||
} else {
|
||||
passwordActionType = .show
|
||||
}
|
||||
minWidth = 62.0
|
||||
|
||||
case .creditCard:
|
||||
minWidth = 288.0
|
||||
|
||||
case .tel:
|
||||
minWidth = 176.0
|
||||
|
||||
case .date:
|
||||
minWidth = 114.0
|
||||
placeholderText = dateFormat.placeholderText
|
||||
|
||||
case .securityCode:
|
||||
minWidth = 88.0
|
||||
|
||||
}
|
||||
|
||||
//textField
|
||||
textField.isSecureTextEntry = isSecureTextEntry
|
||||
|
||||
//leftIcon
|
||||
leftIcon.surface = surface
|
||||
leftIcon.color = iconColorConfiguration.getColor(self)
|
||||
leftIcon.name = leftIconName
|
||||
leftIcon.isHidden = leftIconName == nil
|
||||
|
||||
//actionLink
|
||||
actionTextLink.surface = surface
|
||||
if let actionModel {
|
||||
actionTextLink.text = actionModel.text
|
||||
actionTextLink.onClick = actionModel.onClick
|
||||
actionTextLink.isHidden = false
|
||||
containerStackView.setCustomSpacing(VDSLayout.space2X, after: statusIcon)
|
||||
} else {
|
||||
actionTextLink.isHidden = true
|
||||
containerStackView.setCustomSpacing(0, after: statusIcon)
|
||||
}
|
||||
|
||||
//set the width constraints
|
||||
if let width, width > minWidth {
|
||||
widthConstraint?.constant = width
|
||||
widthConstraint?.isActive = true
|
||||
minWidthConstraint?.isActive = false
|
||||
} else {
|
||||
minWidthConstraint?.constant = minWidth
|
||||
widthConstraint?.isActive = false
|
||||
minWidthConstraint?.isActive = true
|
||||
}
|
||||
|
||||
//placeholder
|
||||
textField.placeholder = placeholderText
|
||||
|
||||
//tooltip
|
||||
tooltipModel = toolTipModel
|
||||
}
|
||||
|
||||
/// Used to update any Accessibility properties.
|
||||
open override func updateAccessibility() {
|
||||
super.updateAccessibility()
|
||||
textField.accessibilityLabel = showError ? "error" : nil
|
||||
if showError {
|
||||
accessibilityElements = [titleLabel, textField, icon, errorLabel, helperLabel]
|
||||
} else {
|
||||
accessibilityElements = [titleLabel, textField, helperLabel]
|
||||
}
|
||||
|
||||
open override var accessibilityElements: [Any]? {
|
||||
get {
|
||||
var elements = [Any]()
|
||||
elements.append(contentsOf: [titleLabel, textField])
|
||||
if showError {
|
||||
elements.append(statusIcon)
|
||||
if let errorText, !errorText.isEmpty {
|
||||
elements.append(errorLabel)
|
||||
}
|
||||
} else if showSuccess, let successText, !successText.isEmpty {
|
||||
elements.append(successLabel)
|
||||
}
|
||||
|
||||
if let helperText, !helperText.isEmpty {
|
||||
elements.append(helperLabel)
|
||||
}
|
||||
|
||||
return elements
|
||||
}
|
||||
|
||||
set { super.accessibilityElements = newValue }
|
||||
}
|
||||
|
||||
open override var canBecomeFirstResponder: Bool { true }
|
||||
@ -292,25 +411,114 @@ open class InputField: EntryFieldBase, UITextFieldDelegate {
|
||||
}
|
||||
return super.resignFirstResponder()
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Password
|
||||
//--------------------------------------------------
|
||||
enum PasswordAction {
|
||||
case show, hide
|
||||
|
||||
func toggle() -> PasswordAction {
|
||||
self == .hide ? .show : .hide
|
||||
}
|
||||
}
|
||||
|
||||
internal var passwordActionType: PasswordAction = .show { didSet { setNeedsUpdate() } }
|
||||
|
||||
open var hidePasswordButtonText: String = "Hide" { didSet { setNeedsUpdate() } }
|
||||
open var showPasswordButtonText: String = "Show" { didSet { setNeedsUpdate() } }
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Date
|
||||
//--------------------------------------------------
|
||||
open var dateFormat: DateFormat = .mmddyy { didSet { setNeedsUpdate() } }
|
||||
|
||||
}
|
||||
|
||||
extension InputField.FieldType {
|
||||
var width: CGFloat {
|
||||
switch self {
|
||||
case .inlineAction:
|
||||
return 102
|
||||
case .password:
|
||||
return 62.0
|
||||
case .creditCard:
|
||||
return 288.0
|
||||
case .tel:
|
||||
return 176.0
|
||||
extension InputField: UITextFieldDelegate {
|
||||
public func process(text changedText: String) {
|
||||
var newText: String = changedText
|
||||
switch fieldType {
|
||||
case .date:
|
||||
return 114.0
|
||||
case .securityCode:
|
||||
return 88.0
|
||||
guard newText.count <= dateFormat.maxLength else { return }
|
||||
let numericText = newText.compactMap { $0.isNumber ? $0 : nil }
|
||||
var formattedText = ""
|
||||
|
||||
for (index, char) in numericText.enumerated() {
|
||||
if (index == 2 || (index == 4 && (dateFormat != .mmyy))) && index < dateFormat.maxLength {
|
||||
formattedText += "/"
|
||||
}
|
||||
formattedText.append(char)
|
||||
}
|
||||
newText = formattedText
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
||||
switch fieldType {
|
||||
case .date:
|
||||
// Allow only numbers and limit the length of text.
|
||||
let allowedCharacters = CharacterSet.decimalDigits
|
||||
let characterSet = CharacterSet(charactersIn: string)
|
||||
return allowedCharacters.isSuperset(of: characterSet) && ((textField.text?.count ?? 0) + string.count - range.length) <= dateFormat.maxLength
|
||||
|
||||
default:
|
||||
return 40.0
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension InputField.FieldType {
|
||||
|
||||
public var keyboardType: UIKeyboardType {
|
||||
switch self {
|
||||
case .number:
|
||||
.numberPad
|
||||
case .tel:
|
||||
.phonePad
|
||||
case .creditCard:
|
||||
.numberPad
|
||||
case .date:
|
||||
.numberPad
|
||||
case .securityCode:
|
||||
.numberPad
|
||||
default:
|
||||
.default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension InputField {
|
||||
public enum DateFormat: String, CaseIterable {
|
||||
case mmyy
|
||||
case mmddyy
|
||||
case mmddyyyy
|
||||
|
||||
public var placeholderText: String {
|
||||
switch self {
|
||||
case .mmyy: "MM/YY"
|
||||
case .mmddyy: "MM/DD/YY"
|
||||
case .mmddyyyy: "MM/DD/YYYY"
|
||||
}
|
||||
}
|
||||
|
||||
public var formatString: String {
|
||||
switch self {
|
||||
case .mmyy: "MM/yy"
|
||||
case .mmddyy: "MM/dd/yy"
|
||||
case .mmddyyyy: "MM/dd/yyyy"
|
||||
}
|
||||
}
|
||||
|
||||
public var maxLength: Int {
|
||||
switch self {
|
||||
case .mmyy: 5
|
||||
case .mmddyy: 8
|
||||
case .mmddyyyy: 10
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
84
VDS/Components/TextFields/InputField/TextField.swift
Normal file
84
VDS/Components/TextFields/InputField/TextField.swift
Normal file
@ -0,0 +1,84 @@
|
||||
//
|
||||
// TextField.swift
|
||||
// VDS
|
||||
//
|
||||
// Created by Matt Bruce on 5/1/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
@objc(VDSTextField)
|
||||
open class TextField: UITextField {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
required public init() {
|
||||
super.init(frame: .zero)
|
||||
initialSetup()
|
||||
}
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
super.init(frame: .zero)
|
||||
initialSetup()
|
||||
}
|
||||
|
||||
public required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
initialSetup()
|
||||
}
|
||||
|
||||
var horizontalPadding: CGFloat = 0
|
||||
|
||||
open override func textRect(forBounds bounds: CGRect) -> CGRect {
|
||||
let rect = super.textRect(forBounds: bounds)
|
||||
return rect.insetBy(dx: -horizontalPadding, dy: 0)
|
||||
}
|
||||
|
||||
open override func editingRect(forBounds bounds: CGRect) -> CGRect {
|
||||
let rect = super.editingRect(forBounds: bounds)
|
||||
return rect.insetBy(dx: -horizontalPadding, dy: 0)
|
||||
}
|
||||
|
||||
open override func placeholderRect(forBounds bounds: CGRect) -> CGRect {
|
||||
let rect = super.placeholderRect(forBounds: bounds)
|
||||
return rect.insetBy(dx: -horizontalPadding, dy: 0)
|
||||
}
|
||||
|
||||
open override var isSecureTextEntry: Bool {
|
||||
didSet {
|
||||
if isFirstResponder {
|
||||
_ = becomeFirstResponder()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open func initialSetup() {
|
||||
let doneToolbar: UIToolbar = UIToolbar()
|
||||
doneToolbar.translatesAutoresizingMaskIntoConstraints = false
|
||||
doneToolbar.barStyle = .default
|
||||
|
||||
let flexSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
|
||||
let done: UIBarButtonItem = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(self.doneButtonAction))
|
||||
done.accessibilityHint = "Double tap to finish editing."
|
||||
doneToolbar.items = [flexSpace, done]
|
||||
doneToolbar.sizeToFit()
|
||||
|
||||
inputAccessoryView = doneToolbar
|
||||
}
|
||||
|
||||
@objc func doneButtonAction() {
|
||||
// Resigns the first responder status when 'Done' is tapped
|
||||
resignFirstResponder()
|
||||
}
|
||||
|
||||
open override func becomeFirstResponder() -> Bool {
|
||||
let success = super.becomeFirstResponder()
|
||||
if isSecureTextEntry, let text {
|
||||
self.text?.removeAll()
|
||||
insertText(text)
|
||||
}
|
||||
return success
|
||||
}
|
||||
}
|
||||
@ -12,7 +12,7 @@ extension InputField {
|
||||
|
||||
///Text that goes in the Tab
|
||||
public var text: String
|
||||
|
||||
|
||||
///Click event when you click on a tab
|
||||
public var onClick: ((TextLink) -> Void)?
|
||||
|
||||
|
||||
18
VDS/Components/TextFields/Rules/CharacterCountRule.swift
Normal file
18
VDS/Components/TextFields/Rules/CharacterCountRule.swift
Normal file
@ -0,0 +1,18 @@
|
||||
//
|
||||
// CharacterCountRule.swift
|
||||
// VDS
|
||||
//
|
||||
// Created by Matt Bruce on 4/30/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class CharacterCountRule: Rule {
|
||||
var maxLength: Int?
|
||||
var errorMessage: String = "You have exceeded the character limit."
|
||||
|
||||
func isValid(value: String?) -> Bool {
|
||||
guard let text = value, let maxLength, maxLength > 0 else { return true }
|
||||
return text.count <= maxLength
|
||||
}
|
||||
}
|
||||
19
VDS/Components/TextFields/Rules/RequiredRule.swift
Normal file
19
VDS/Components/TextFields/Rules/RequiredRule.swift
Normal file
@ -0,0 +1,19 @@
|
||||
//
|
||||
// RequiredRule.swift
|
||||
// VDS
|
||||
//
|
||||
// Created by Matt Bruce on 4/30/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class RequiredRule: Rule {
|
||||
var maxLength: Int?
|
||||
var errorMessage: String = "This field is required."
|
||||
|
||||
func isValid(value: String?) -> Bool {
|
||||
guard let value, !value.isEmpty, value.count > 0 else { return false }
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,7 +60,7 @@ open class TextArea: EntryFieldBase {
|
||||
$0.textAlignment = .right
|
||||
$0.numberOfLines = 1
|
||||
}
|
||||
|
||||
|
||||
private var _minHeight: Height = .twoX
|
||||
|
||||
open var minHeight: Height? {
|
||||
@ -79,54 +79,60 @@ open class TextArea: EntryFieldBase {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
override var containerSize: CGSize { CGSize(width: 182, height: 88) }
|
||||
/// 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.
|
||||
public enum Height: String, CaseIterable {
|
||||
case twoX = "2X"
|
||||
case fourX = "4X"
|
||||
case eightX = "8X"
|
||||
var containerVerticalPadding: CGFloat { VDSLayout.space3X * 2 }
|
||||
|
||||
var value: CGFloat {
|
||||
switch self {
|
||||
case .twoX:
|
||||
88
|
||||
88 - containerVerticalPadding
|
||||
case .fourX:
|
||||
176
|
||||
176 - containerVerticalPadding
|
||||
case .eightX:
|
||||
352
|
||||
352 - containerVerticalPadding
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// The text of this TextArea.
|
||||
private var _text: String?
|
||||
open var text: String? {
|
||||
get { _text }
|
||||
get { textView.text }
|
||||
set {
|
||||
if let newValue, newValue != _text {
|
||||
_text = newValue
|
||||
textView.text = newValue
|
||||
value = newValue
|
||||
}
|
||||
textView.text = newValue
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
/// The value of this textField.
|
||||
open override var value: String? {
|
||||
didSet {
|
||||
if text != value {
|
||||
text = value
|
||||
}
|
||||
}
|
||||
return textView.text
|
||||
}
|
||||
|
||||
|
||||
/// UITextView shown in the TextArea.
|
||||
open var textView = TextView().with {
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
$0.sizeToFit()
|
||||
$0.isScrollEnabled = false
|
||||
$0.textContainerInset = .zero
|
||||
$0.textContainer.lineFragmentPadding = 0
|
||||
}
|
||||
|
||||
open var maxLength: Int? {
|
||||
@ -135,20 +141,15 @@ open class TextArea: EntryFieldBase {
|
||||
}
|
||||
|
||||
didSet {
|
||||
setNeedsUpdate()
|
||||
validate()
|
||||
}
|
||||
}
|
||||
|
||||
/// Color configuration for error icon.
|
||||
internal var iconColorConfiguration = ControlColorConfiguration().with {
|
||||
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .normal)
|
||||
}
|
||||
|
||||
/// Color configuration for character counter's highlight background color
|
||||
internal var highlightBackgroundColor = ControlColorConfiguration().with {
|
||||
$0.setSurfaceColors(VDSColor.backgroundPrimaryDark, VDSColor.backgroundPrimaryLight, forState: .normal)
|
||||
}
|
||||
|
||||
|
||||
/// Color configuration for character counter's highlight text color
|
||||
internal var highlightTextColor = ControlColorConfiguration().with {
|
||||
$0.setSurfaceColors(VDSColor.elementsPrimaryOndark, VDSColor.elementsPrimaryOnlight, forState: .normal)
|
||||
@ -160,9 +161,6 @@ open class TextArea: EntryFieldBase {
|
||||
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
isAccessibilityElement = false
|
||||
validator = FormFieldValidator<TextArea>(field: self, rules: [.init(countRule)])
|
||||
|
||||
containerStackView.pinToSuperView(.uniform(VDSFormControls.spaceInset))
|
||||
minWidthConstraint = containerView.widthAnchor.constraint(greaterThanOrEqualToConstant: containerSize.width)
|
||||
minWidthConstraint?.isActive = true
|
||||
@ -172,14 +170,34 @@ open class TextArea: EntryFieldBase {
|
||||
.pinLeading()
|
||||
.pinTrailingLessThanOrEqualTo(nil, 0, .defaultHigh)
|
||||
.pinBottom(0, .defaultHigh)
|
||||
|
||||
textView.isScrollEnabled = true
|
||||
textView.autocorrectionType = .no
|
||||
|
||||
//events
|
||||
textView
|
||||
.publisher(for: .editingChanged)
|
||||
.sink { [weak self] control in
|
||||
self?.textViewDidChange(control)
|
||||
}.store(in: &subscribers)
|
||||
|
||||
textView
|
||||
.publisher(for: .editingDidBegin)
|
||||
.sink { [weak self] _ in
|
||||
self?.setNeedsUpdate()
|
||||
}.store(in: &subscribers)
|
||||
|
||||
textView
|
||||
.publisher(for: .editingDidEnd)
|
||||
.sink { [weak self] _ in
|
||||
self?.validate()
|
||||
}.store(in: &subscribers)
|
||||
|
||||
textViewHeightConstraint = textView.heightAnchor.constraint(greaterThanOrEqualToConstant: containerSize.height)
|
||||
textViewHeightConstraint?.isActive = true
|
||||
backgroundColorConfiguration.setSurfaceColors(VDSColor.feedbackSuccessBackgroundOnlight, VDSColor.feedbackSuccessBackgroundOndark, forState: .success)
|
||||
borderColorConfiguration.setSurfaceColors(VDSColor.feedbackSuccessOnlight, VDSColor.feedbackSuccessOndark, forState: .success)
|
||||
borderColorConfiguration.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .focused)
|
||||
textView.delegate = self
|
||||
characterCounterLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable()
|
||||
bottomContainerStackView.spacing = VDSLayout.space2X
|
||||
}
|
||||
@ -216,39 +234,62 @@ open class TextArea: EntryFieldBase {
|
||||
widthConstraint?.isActive = false
|
||||
minWidthConstraint?.isActive = true
|
||||
}
|
||||
|
||||
|
||||
characterCounterLabel.text = getCharacterCounterText()
|
||||
|
||||
icon.size = .medium
|
||||
icon.color = iconColorConfiguration.getColor(self)
|
||||
containerView.layer.borderColor = readOnly ? readOnlyBorderColorConfiguration.getColor(self).cgColor : borderColorConfiguration.getColor(self).cgColor
|
||||
textView.isEditable = readOnly ? false : true
|
||||
statusIcon.color = iconColorConfiguration.getColor(self)
|
||||
containerView.layer.borderColor = isReadOnly ? readOnlyBorderColorConfiguration.getColor(self).cgColor : borderColorConfiguration.getColor(self).cgColor
|
||||
textView.isEditable = isReadOnly ? false : true
|
||||
textView.backgroundColor = backgroundColorConfiguration.getColor(self)
|
||||
textView.tintColor = iconColorConfiguration.getColor(self)
|
||||
characterCounterLabel.surface = surface
|
||||
highlightCharacterOverflow()
|
||||
}
|
||||
|
||||
override func updateRules() {
|
||||
super.updateRules()
|
||||
|
||||
rules.append(.init(countRule))
|
||||
}
|
||||
|
||||
/// Container for the area showing helper text, error text, character count, maximum length value.
|
||||
open override func getBottomContainer() -> UIView {
|
||||
bottomStackView.addArrangedSubview(bottomContainerView)
|
||||
bottomStackView.addArrangedSubview(characterCounterLabel)
|
||||
return bottomStackView
|
||||
}
|
||||
|
||||
|
||||
/// Used to update any Accessibility properties.
|
||||
open override func updateAccessibility() {
|
||||
super.updateAccessibility()
|
||||
textView.accessibilityLabel = showError ? "error" : nil
|
||||
if showError {
|
||||
accessibilityElements = [titleLabel, textView, icon, errorLabel, helperLabel]
|
||||
} else {
|
||||
accessibilityElements = [titleLabel, textView, helperLabel]
|
||||
}
|
||||
}
|
||||
|
||||
open override var canBecomeFirstResponder: Bool { true }
|
||||
open override var accessibilityElements: [Any]? {
|
||||
get {
|
||||
var elements = [Any]()
|
||||
elements.append(contentsOf: [titleLabel, textView])
|
||||
|
||||
if showError {
|
||||
elements.append(statusIcon)
|
||||
if let errorText, !errorText.isEmpty {
|
||||
elements.append(errorLabel)
|
||||
}
|
||||
}
|
||||
|
||||
if let helperText, !helperText.isEmpty {
|
||||
elements.append(helperLabel)
|
||||
}
|
||||
|
||||
return elements
|
||||
}
|
||||
|
||||
set { super.accessibilityElements = newValue }
|
||||
}
|
||||
|
||||
|
||||
open override var canBecomeFirstResponder: Bool { true }
|
||||
|
||||
open override func resignFirstResponder() -> Bool {
|
||||
if textView.isFirstResponder {
|
||||
textView.resignFirstResponder()
|
||||
@ -273,43 +314,7 @@ open class TextArea: EntryFieldBase {
|
||||
}
|
||||
}
|
||||
|
||||
open func highlightCharacterOverflow() {
|
||||
let count = textView.text.count
|
||||
guard let maxLength, maxLength > 0, count > maxLength else {
|
||||
textView.textAttributes = nil
|
||||
return
|
||||
}
|
||||
|
||||
var textAttributes = [any LabelAttributeModel]()
|
||||
let location = maxLength
|
||||
let length = count - maxLength
|
||||
textAttributes.append(ColorLabelAttribute(location: location, length: length, color: highlightBackgroundColor.getColor(self), isForegroundColor: false))
|
||||
textAttributes.append(ColorLabelAttribute(location: location, length: length, color: highlightTextColor.getColor(self), isForegroundColor: true))
|
||||
|
||||
textView.textAttributes = textAttributes
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Validation
|
||||
//--------------------------------------------------
|
||||
var countRule = CharacterCountRule()
|
||||
|
||||
class CharacterCountRule: Rule {
|
||||
var maxLength: Int?
|
||||
var errorMessage: String = "You have exceeded the character limit."
|
||||
|
||||
func isValid(value: String?) -> Bool {
|
||||
guard let text = value, let maxLength, maxLength > 0 else { return true }
|
||||
return text.count <= maxLength
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TextArea: UITextViewDelegate {
|
||||
//--------------------------------------------------
|
||||
// MARK: - UITextViewDelegate
|
||||
//--------------------------------------------------
|
||||
public func textViewDidChange(_ textView: UITextView) {
|
||||
func textViewDidChange(_ textView: UITextView) {
|
||||
|
||||
//dynamic textView Height sizing based on Figma
|
||||
//if you want it to work "as-is" delete this code
|
||||
@ -347,5 +352,27 @@ extension TextArea: UITextViewDelegate {
|
||||
text = textView.text
|
||||
sendActions(for: .valueChanged)
|
||||
}
|
||||
}
|
||||
validate()
|
||||
}
|
||||
|
||||
private func highlightCharacterOverflow() {
|
||||
let count = textView.text.count
|
||||
guard let maxLength, maxLength > 0, count > maxLength else {
|
||||
textView.textAttributes = nil
|
||||
return
|
||||
}
|
||||
|
||||
var textAttributes = [any LabelAttributeModel]()
|
||||
let location = maxLength
|
||||
let length = count - maxLength
|
||||
textAttributes.append(ColorLabelAttribute(location: location, length: length, color: highlightBackgroundColor.getColor(self), isForegroundColor: false))
|
||||
textAttributes.append(ColorLabelAttribute(location: location, length: length, color: highlightTextColor.getColor(self), isForegroundColor: true))
|
||||
|
||||
textView.textAttributes = textAttributes
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Validation
|
||||
//--------------------------------------------------
|
||||
var countRule = CharacterCountRule()
|
||||
}
|
||||
|
||||
@ -97,7 +97,6 @@ open class TextView: UITextView, ViewProtocol {
|
||||
initialSetupPerformed = true
|
||||
backgroundColor = .clear
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
accessibilityCustomActions = []
|
||||
setup()
|
||||
setNeedsUpdate()
|
||||
}
|
||||
@ -106,8 +105,25 @@ open class TextView: UITextView, ViewProtocol {
|
||||
|
||||
open func setup() {
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
let doneToolbar: UIToolbar = UIToolbar()
|
||||
doneToolbar.translatesAutoresizingMaskIntoConstraints = false
|
||||
doneToolbar.barStyle = .default
|
||||
|
||||
let flexSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
|
||||
let done: UIBarButtonItem = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(self.doneButtonAction))
|
||||
done.accessibilityHint = "Double tap to finish editing."
|
||||
doneToolbar.items = [flexSpace, done]
|
||||
doneToolbar.sizeToFit()
|
||||
|
||||
inputAccessoryView = doneToolbar
|
||||
|
||||
}
|
||||
|
||||
@objc func doneButtonAction() {
|
||||
// Resigns the first responder status when 'Done' is tapped
|
||||
resignFirstResponder()
|
||||
}
|
||||
|
||||
open func updateView() {
|
||||
updateLabel()
|
||||
}
|
||||
@ -118,7 +134,6 @@ open class TextView: UITextView, ViewProtocol {
|
||||
shouldUpdateView = false
|
||||
surface = .light
|
||||
text = nil
|
||||
accessibilityCustomActions = []
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
@ -69,7 +69,7 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
case secondary
|
||||
case white
|
||||
case black
|
||||
case custom(String)
|
||||
case custom(UIColor)
|
||||
|
||||
private var reflectedValue: String { String(reflecting: self) }
|
||||
|
||||
@ -81,7 +81,7 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
/// Enum used to describe the background effect choices used for this component.
|
||||
public enum BackgroundEffect {
|
||||
case transparency
|
||||
case gradient(String, String)
|
||||
case gradient(UIColor, UIColor)
|
||||
case none
|
||||
}
|
||||
|
||||
@ -108,30 +108,17 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
$0.clipsToBounds = true
|
||||
}
|
||||
|
||||
private var containerView = View()
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
public override var onClickSubscriber: AnyCancellable? {
|
||||
didSet {
|
||||
if onClickSubscriber != nil {
|
||||
isAccessibilityElement = true
|
||||
accessibilityTraits = .link
|
||||
accessibilityHint = "Double tap to open."
|
||||
} else {
|
||||
isAccessibilityElement = false
|
||||
accessibilityTraits.remove(.link)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This takes an image source url and applies it as a background image.
|
||||
open var backgroundImage: UIImage? { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// This is the container in which views will be pinned.
|
||||
open var containerView = View().with {
|
||||
$0.isUserInteractionEnabled = false
|
||||
}
|
||||
|
||||
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
|
||||
@ -141,7 +128,7 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
open var aspectRatio: AspectRatio = .ratio1x1 { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// Sets the background color for the component.
|
||||
open var color: BackgroundColor = .secondary { didSet { setNeedsUpdate() } }
|
||||
open var color: BackgroundColor? { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// Sets the background effect for the component.
|
||||
open var backgroundEffect: BackgroundEffect = .none { didSet { setNeedsUpdate() } }
|
||||
@ -201,14 +188,14 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
// MARK: - Configuration
|
||||
//--------------------------------------------------
|
||||
private let cornerRadius = VDSFormControls.borderRadius * 2
|
||||
private var backgroundColorConfiguration = BackgroundColorConfiguration()
|
||||
internal var backgroundColorConfiguration = BackgroundColorConfiguration()
|
||||
private let dropShadowConfiguration = DropShadowConfiguration().with {
|
||||
$0.shadowColorConfiguration = SurfaceColorConfiguration().with {
|
||||
$0.lightColor = VDSColor.elementsPrimaryOnlight
|
||||
}.eraseToAnyColorable()
|
||||
$0.shadowOffsetConfiguration = .init(.init(width: 0, height: 6), .zero)
|
||||
$0.shadowRadiusConfiguration = .init(3.0, 0.0)
|
||||
$0.shadowOpacityConfiguration = .init(0.01, 0.0)
|
||||
$0.shadowRadiusConfiguration = .init(8.0, 0.0)
|
||||
$0.shadowOpacityConfiguration = .init(0.1, 0.0)
|
||||
}
|
||||
|
||||
private var borderColorConfiguration = SurfaceColorConfiguration().with {
|
||||
@ -232,6 +219,7 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where 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()
|
||||
isAccessibilityElement = false
|
||||
|
||||
let layoutGuide = UILayoutGuide()
|
||||
addLayoutGuide(layoutGuide)
|
||||
@ -240,11 +228,13 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
.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)
|
||||
@ -253,7 +243,7 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
heightConstraint = layoutGuide.heightAnchor.constraint(equalToConstant: 0)
|
||||
|
||||
backgroundImageView.pinToSuperView()
|
||||
|
||||
|
||||
backgroundImageView.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||
backgroundImageView.setContentHuggingPriority(.defaultLow, for: .vertical)
|
||||
backgroundImageView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
@ -261,10 +251,10 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
backgroundImageView.isUserInteractionEnabled = false
|
||||
backgroundImageView.isHidden = true
|
||||
|
||||
containerTopConstraint = containerView.pinTop(anchor: layoutGuide.topAnchor, constant: padding.value)
|
||||
containerBottomConstraint = layoutGuide.pinBottom(anchor: containerView.bottomAnchor, constant: padding.value)
|
||||
containerLeadingConstraint = containerView.pinLeading(anchor: layoutGuide.leadingAnchor, constant: padding.value)
|
||||
containerTrailingConstraint = layoutGuide.pinTrailing(anchor: containerView.trailingAnchor, constant: padding.value)
|
||||
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
|
||||
@ -277,6 +267,14 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
clipsToBounds = true
|
||||
}
|
||||
|
||||
/// Overriden to take the hit if there is an onClickSubscriber and the view is not a UIControl
|
||||
open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
let view = super.hitTest(point, with: event)
|
||||
guard let _ = onClickSubscriber else { return view }
|
||||
guard view is UIControl else { return self }
|
||||
return view
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
@ -291,7 +289,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()
|
||||
@ -331,7 +329,7 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
}
|
||||
|
||||
applyBackgroundEffects()
|
||||
|
||||
|
||||
if showDropShadow, surface == .light {
|
||||
addDropShadow(dropShadowConfiguration)
|
||||
} else {
|
||||
@ -339,6 +337,30 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
}
|
||||
}
|
||||
|
||||
open override func updateAccessibility() {
|
||||
super.updateAccessibility()
|
||||
containerView.isAccessibilityElement = onClickSubscriber != nil
|
||||
containerView.accessibilityHint = "Double tap to open."
|
||||
}
|
||||
|
||||
open override var accessibilityElements: [Any]? {
|
||||
get {
|
||||
var items = [Any]()
|
||||
if containerView.isAccessibilityElement {
|
||||
if !accessibilityTraits.contains(.button) && !accessibilityTraits.contains(.link) {
|
||||
containerView.accessibilityTraits = .button
|
||||
} else {
|
||||
containerView.accessibilityTraits = accessibilityTraits
|
||||
}
|
||||
items.append(containerView)
|
||||
}
|
||||
items.append(contentsOf: contentView.subviews.filter({ $0.isAccessibilityElement == true }))
|
||||
return items
|
||||
}
|
||||
|
||||
set {}
|
||||
}
|
||||
|
||||
/// Used to update frames for the added CAlayers to our view
|
||||
open override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
@ -353,7 +375,7 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
/// This will place a view within the contentView of this component.
|
||||
public func addContentView(_ view: UIView, shouldPin: Bool = true) {
|
||||
view.removeFromSuperview()
|
||||
containerView.addSubview(view)
|
||||
contentView.addSubview(view)
|
||||
if shouldPin {
|
||||
view.pinToSuperView()
|
||||
}
|
||||
@ -373,7 +395,7 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
removeGradientLayer()
|
||||
case .gradient(let firstColor, let secondColor):
|
||||
alphaConfiguration = 1.0
|
||||
addGradientLayer(with: UIColor(hexString: firstColor), secondColor: UIColor(hexString: secondColor))
|
||||
addGradientLayer(with: firstColor, secondColor: secondColor)
|
||||
backgroundImageView.isHidden = true
|
||||
backgroundImageView.alpha = 1.0
|
||||
case .none:
|
||||
@ -439,17 +461,22 @@ extension TileContainerBase {
|
||||
required init() { }
|
||||
|
||||
func getColor(_ object: ObjectType) -> UIColor {
|
||||
switch object.color {
|
||||
guard let color = object.color else {
|
||||
let config = object.surface == .light ? blackColorConfig : whiteColorConfig
|
||||
return config.getColor(object.surface)
|
||||
}
|
||||
|
||||
switch color {
|
||||
case .primary:
|
||||
primaryColorConfig.getColor(object.surface)
|
||||
return primaryColorConfig.getColor(object.surface)
|
||||
case .secondary:
|
||||
secondaryColorConfig.getColor(object.surface)
|
||||
return secondaryColorConfig.getColor(object.surface)
|
||||
case .white:
|
||||
whiteColorConfig.getColor(object.surface)
|
||||
return whiteColorConfig.getColor(object.surface)
|
||||
case .black:
|
||||
blackColorConfig.getColor(object.surface)
|
||||
case .custom(let hexCode):
|
||||
UIColor(hexString: hexCode)
|
||||
return blackColorConfig.getColor(object.surface)
|
||||
case .custom(let color):
|
||||
return color
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -102,6 +102,10 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
|
||||
$0.backgroundColor = .clear
|
||||
}
|
||||
|
||||
private var backgroundColorSurface: Surface {
|
||||
backgroundColorConfiguration.getColor(self).surface
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
@ -201,10 +205,13 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
|
||||
}
|
||||
|
||||
/// Descriptive Icon positioned in the contentView.
|
||||
open var descriptiveIcon = Icon()
|
||||
open var descriptiveIcon = Icon().with {
|
||||
$0.isAccessibilityElement = true
|
||||
}
|
||||
|
||||
/// Directional Icon positioned in the contentView.
|
||||
open var directionalIcon = Icon().with {
|
||||
$0.isAccessibilityElement = true
|
||||
$0.name = .rightArrow
|
||||
}
|
||||
|
||||
@ -407,24 +414,30 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
|
||||
}
|
||||
|
||||
/// Used to update any Accessibility properties.
|
||||
open override func updateAccessibility() {
|
||||
super.updateAccessibility()
|
||||
var elements = [Any]()
|
||||
if badgeModel != nil {
|
||||
elements.append(badge)
|
||||
open override var accessibilityElements: [Any]? {
|
||||
|
||||
get {
|
||||
var elements = [Any]()
|
||||
if let superElements = super.accessibilityElements {
|
||||
elements.append(contentsOf: superElements)
|
||||
}
|
||||
if badgeModel != nil {
|
||||
elements.append(badge)
|
||||
}
|
||||
if titleModel != nil || subTitleModel != nil || eyebrowModel != nil {
|
||||
elements.append(titleLockup)
|
||||
}
|
||||
if descriptiveIconModel != nil {
|
||||
elements.append(descriptiveIcon)
|
||||
}
|
||||
if directionalIconModel != nil {
|
||||
elements.append(directionalIcon)
|
||||
}
|
||||
return elements
|
||||
}
|
||||
if titleModel != nil || subTitleModel != nil || eyebrowModel != nil {
|
||||
elements.append(titleLockup)
|
||||
}
|
||||
if descriptiveIconModel != nil {
|
||||
elements.append(descriptiveIcon)
|
||||
}
|
||||
if directionalIconModel != nil {
|
||||
elements.append(directionalIcon)
|
||||
}
|
||||
accessibilityElements = elements.count > 0 ? elements : nil
|
||||
|
||||
set {}
|
||||
|
||||
setAccessibilityLabel(for: [badge.label, titleLockup.eyebrowLabel, titleLockup.titleLabel, titleLockup.subTitleLabel])
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
@ -435,7 +448,7 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
|
||||
badge.text = badgeModel.text
|
||||
badge.fillColor = badgeModel.fillColor
|
||||
badge.numberOfLines = badgeModel.numberOfLines
|
||||
badge.surface = badgeModel.surface
|
||||
badge.surface = backgroundColorSurface
|
||||
badge.maxWidth = badgeModel.maxWidth
|
||||
badgeLabelHeightGreaterThanConstraint?.constant = badge.label.minimumLineHeight
|
||||
if badgeContainerView.superview == nil {
|
||||
@ -465,7 +478,7 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
|
||||
|
||||
if showTitleLockup {
|
||||
//flip the surface for the titleLockup
|
||||
titleLockup.surface = color == .black ? Surface.dark : Surface.light
|
||||
titleLockup.surface = backgroundColorSurface
|
||||
|
||||
//titleLockup
|
||||
if let textWidth {
|
||||
@ -482,7 +495,7 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
|
||||
titleLockupWidthConstraint = NSLayoutConstraint(item: titleLockup,
|
||||
attribute: .width,
|
||||
relatedBy: .equal,
|
||||
toItem: containerView,
|
||||
toItem: contentView,
|
||||
attribute: .width,
|
||||
multiplier: percentage / 100,
|
||||
constant: 0.0)
|
||||
@ -520,13 +533,15 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
|
||||
if let descriptiveIconModel {
|
||||
descriptiveIcon.name = descriptiveIconModel.name
|
||||
descriptiveIcon.size = descriptiveIconModel.size
|
||||
descriptiveIcon.surface = descriptiveIconModel.surface
|
||||
descriptiveIcon.surface = backgroundColorSurface
|
||||
descriptiveIcon.accessibilityLabel = descriptiveIconModel.accessibleText
|
||||
showIconContainerView = true
|
||||
}
|
||||
|
||||
if let directionalIconModel {
|
||||
directionalIcon.size = directionalIconModel.size
|
||||
directionalIcon.surface = directionalIconModel.surface
|
||||
directionalIcon.surface = backgroundColorSurface
|
||||
directionalIcon.accessibilityLabel = "Right arrow"
|
||||
showIconContainerView = true
|
||||
}
|
||||
|
||||
|
||||
@ -18,11 +18,15 @@ extension Tilelet {
|
||||
/// Enum for a preset height and width for the icon.
|
||||
public var size: Icon.Size
|
||||
|
||||
/// Accessible Text for the Icon
|
||||
var accessibleText: String
|
||||
|
||||
/// Current Surface and this is used to pass down to child objects that implement Surfacable
|
||||
public var surface: Surface
|
||||
|
||||
public init(name: Icon.Name = .multipleDocuments, size: Icon.Size = .medium, surface: Surface = .dark) {
|
||||
public init(name: Icon.Name = .multipleDocuments, size: Icon.Size = .medium, accessibleText: String? = nil, surface: Surface = .dark) {
|
||||
self.name = name
|
||||
self.accessibleText = accessibleText ?? name.rawValue
|
||||
self.size = size
|
||||
self.surface = surface
|
||||
}
|
||||
|
||||
@ -36,8 +36,8 @@ extension Tilelet {
|
||||
public var textAttributes: [any LabelAttributeModel]?
|
||||
|
||||
/// Text color that will be used for the subTitle label.
|
||||
public var textColor: Use = .primary
|
||||
|
||||
public var textColor: TitleLockup.TextColor
|
||||
|
||||
/// LineBreakMode used in Badge label.
|
||||
public var lineBreakMode: NSLineBreakMode
|
||||
|
||||
@ -46,7 +46,7 @@ extension Tilelet {
|
||||
//--------------------------------------------------
|
||||
public init(text: String,
|
||||
otherStandardStyle: OtherStandardStyle = .bodySmall,
|
||||
textColor: Use = .primary,
|
||||
textColor: TitleLockup.TextColor = .primary,
|
||||
textAttributes: [any LabelAttributeModel]? = nil,
|
||||
lineBreakMode: NSLineBreakMode = .byTruncatingTail) {
|
||||
self.text = text
|
||||
@ -64,7 +64,8 @@ extension Tilelet {
|
||||
TitleLockup.SubTitleModel(text: text,
|
||||
otherStandardStyle: otherStandardStyle.value,
|
||||
textColor: textColor,
|
||||
textAttributes: textAttributes, lineBreakMode: lineBreakMode)
|
||||
textAttributes: textAttributes,
|
||||
lineBreakMode: lineBreakMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,6 +32,9 @@ extension Tilelet {
|
||||
/// Text that will be used for the title label.
|
||||
public var text: String = ""
|
||||
|
||||
/// TextColor that will be used for the title label.
|
||||
public var textColor: TitleLockup.TitleTextColor
|
||||
|
||||
/// Used in combination with standardStyle to set the textStyle that will be used for the title label.
|
||||
public var isBold: Bool = false
|
||||
/// Text attributes that will be used for the title label.
|
||||
@ -47,11 +50,13 @@ extension Tilelet {
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
public init(text: String,
|
||||
textColor: TitleLockup.TitleTextColor = .primary,
|
||||
textAttributes: [any LabelAttributeModel]? = nil,
|
||||
isBold: Bool = true,
|
||||
standardStyle: StandardStyle = .titleSmall,
|
||||
lineBreakMode: NSLineBreakMode = .byTruncatingTail) {
|
||||
self.text = text
|
||||
self.textColor = textColor
|
||||
self.textAttributes = textAttributes
|
||||
self.standardStyle = standardStyle
|
||||
self.isBold = isBold
|
||||
@ -64,8 +69,11 @@ extension Tilelet {
|
||||
/// Converts this type of model to a TitleLockup.TitleModel.
|
||||
public func toTitleLockupTitleModel() -> TitleLockup.TitleModel {
|
||||
TitleLockup.TitleModel(text: text,
|
||||
textColor: textColor,
|
||||
textAttributes: textAttributes,
|
||||
isBold: isBold, standardStyle: standardStyle.value, lineBreakMode: lineBreakMode)
|
||||
isBold: isBold,
|
||||
standardStyle: standardStyle.value,
|
||||
lineBreakMode: lineBreakMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,6 +18,9 @@ extension Tilelet {
|
||||
/// Text that will be used for the eyebrow label.
|
||||
public var text: String = ""
|
||||
|
||||
/// Text color that will be used for the eyebrow label
|
||||
public var textColor: TitleLockup.TextColor
|
||||
|
||||
/// Used in combination with standardStyle to set the textStyle that will be used for the eyebrow label.
|
||||
public var isBold: Bool = false
|
||||
/// Text attributes that will be used for the eyebrow label.
|
||||
@ -33,11 +36,13 @@ extension Tilelet {
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
public init(text: String,
|
||||
textColor: TitleLockup.TextColor = .primary,
|
||||
textAttributes: [any LabelAttributeModel]? = nil,
|
||||
isBold: Bool = true,
|
||||
standardStyle: Tilelet.SubTitleModel.OtherStandardStyle = .bodySmall,
|
||||
lineBreakMode: NSLineBreakMode = .byTruncatingTail) {
|
||||
self.text = text
|
||||
self.textColor = textColor
|
||||
self.textAttributes = textAttributes
|
||||
self.standardStyle = standardStyle
|
||||
self.isBold = isBold
|
||||
@ -49,7 +54,11 @@ extension Tilelet {
|
||||
//--------------------------------------------------
|
||||
/// Converts this type of model to a TitleLockup.TitleModel.
|
||||
public func toTitleLockupEyebrowModel() -> TitleLockup.EyebrowModel {
|
||||
TitleLockup.EyebrowModel(text: text, isBold: isBold, standardStyle: standardStyle.value, textAttributes: textAttributes)
|
||||
TitleLockup.EyebrowModel(text: text,
|
||||
textColor: textColor,
|
||||
isBold: isBold,
|
||||
standardStyle: standardStyle.value,
|
||||
textAttributes: textAttributes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -258,22 +258,10 @@ open class TitleLockup: View {
|
||||
bottomSpacing: VDSLayout.space6X)
|
||||
]),
|
||||
])
|
||||
|
||||
private var textColorSecondaryConfiguration = SurfaceColorConfiguration(VDSColor.elementsSecondaryOnlight , VDSColor.elementsSecondaryOnlight).eraseToAnyColorable()
|
||||
|
||||
private var textColorPrimaryConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark).eraseToAnyColorable()
|
||||
|
||||
|
||||
//--------------------------------------------------
|
||||
// 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()
|
||||
|
||||
titleLabel.textColorConfiguration = textColorPrimaryConfiguration
|
||||
|
||||
}
|
||||
|
||||
open override func updateAccessibility() {
|
||||
super.updateAccessibility()
|
||||
var elements = [Any]()
|
||||
@ -330,6 +318,7 @@ open class TitleLockup: View {
|
||||
if let eyebrowModel, !eyebrowModel.text.isEmpty {
|
||||
eyebrowLabel.textAlignment = allLabelsTextAlignment
|
||||
eyebrowLabel.text = eyebrowModel.text
|
||||
eyebrowLabel.textColorConfiguration = eyebrowModel.textColor.configuration
|
||||
eyebrowLabel.attributes = eyebrowModel.textAttributes
|
||||
eyebrowLabel.numberOfLines = eyebrowModel.numberOfLines
|
||||
eyebrowLabel.surface = surface
|
||||
@ -339,15 +328,12 @@ open class TitleLockup: View {
|
||||
//When uniform size is true and the title is bold,
|
||||
//the eyebrow is always regular weight and the secondary color.
|
||||
eyebrowLabel.textStyle = otherStandardStyle.value.regular
|
||||
eyebrowLabel.textColorConfiguration = textColorSecondaryConfiguration
|
||||
} else {
|
||||
//When uniform size is true and the title is regular weight
|
||||
//the eyebrow is always bold and uses the primary color.
|
||||
eyebrowLabel.textStyle = otherStandardStyle.value.bold
|
||||
eyebrowLabel.textColorConfiguration = textColorPrimaryConfiguration
|
||||
}
|
||||
} else {
|
||||
eyebrowLabel.textColorConfiguration = textColorPrimaryConfiguration
|
||||
eyebrowLabel.textStyle = eyebrowModel.isBold ? otherStandardStyle.value.bold : otherStandardStyle.value.regular
|
||||
}
|
||||
addSubview(eyebrowLabel)
|
||||
@ -363,6 +349,7 @@ open class TitleLockup: View {
|
||||
if let titleModel, !titleModel.text.isEmpty {
|
||||
titleLabel.textAlignment = allLabelsTextAlignment
|
||||
titleLabel.textStyle = titleModel.textStyle
|
||||
titleLabel.textColorConfiguration = titleModel.textColor.configuration
|
||||
titleLabel.text = titleModel.text
|
||||
titleLabel.attributes = titleModel.textAttributes
|
||||
titleLabel.numberOfLines = titleModel.numberOfLines
|
||||
@ -381,7 +368,7 @@ open class TitleLockup: View {
|
||||
if let subTitleModel, !subTitleModel.text.isEmpty {
|
||||
subTitleLabel.textAlignment = allLabelsTextAlignment
|
||||
subTitleLabel.textStyle = otherStandardStyle.value.regular
|
||||
subTitleLabel.textColorConfiguration = subTitleModel.textColor == .secondary ? textColorSecondaryConfiguration : textColorPrimaryConfiguration
|
||||
subTitleLabel.textColorConfiguration = subTitleModel.textColor.configuration
|
||||
subTitleLabel.text = subTitleModel.text
|
||||
subTitleLabel.attributes = subTitleModel.textAttributes
|
||||
subTitleLabel.numberOfLines = subTitleModel.numberOfLines
|
||||
|
||||
@ -13,6 +13,9 @@ extension TitleLockup {
|
||||
/// Text that will be used for the eyebrow label.
|
||||
public var text: String
|
||||
|
||||
/// Text color that will be used for the eyebrow label
|
||||
public var textColor: TextColor
|
||||
|
||||
/// Used in combination with standardStyle to set the textStyle that will be used for the eyebrow label.
|
||||
public var isBold: Bool
|
||||
|
||||
@ -26,11 +29,13 @@ extension TitleLockup {
|
||||
public var numberOfLines: Int
|
||||
|
||||
public init(text: String,
|
||||
textColor: TextColor = .primary,
|
||||
isBold: Bool = true,
|
||||
standardStyle: OtherStandardStyle = .bodyLarge,
|
||||
textAttributes: [any LabelAttributeModel]? = nil,
|
||||
numberOfLines: Int = 0) {
|
||||
self.text = text
|
||||
self.textColor = textColor
|
||||
self.isBold = isBold
|
||||
self.standardStyle = standardStyle
|
||||
self.textAttributes = textAttributes
|
||||
@ -41,3 +46,4 @@ extension TitleLockup {
|
||||
public var textStyle: TextStyle { isBold ? standardStyle.value.bold : standardStyle.value.regular }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@ extension TitleLockup {
|
||||
public var otherStandardStyle: OtherStandardStyle
|
||||
|
||||
/// Text color used in the subtitle label.
|
||||
public var textColor: Use
|
||||
public var textColor: TextColor
|
||||
|
||||
/// Array of LabelAttributeModel objects used in rendering the text in the subtitle label.
|
||||
public var textAttributes: [any LabelAttributeModel]?
|
||||
@ -31,7 +31,7 @@ extension TitleLockup {
|
||||
|
||||
public init(text: String,
|
||||
otherStandardStyle: OtherStandardStyle = .bodyLarge,
|
||||
textColor: Use = .primary,
|
||||
textColor: TextColor = .primary,
|
||||
textAttributes: [any LabelAttributeModel]? = nil,
|
||||
numberOfLines: Int = 0,
|
||||
lineBreakMode: NSLineBreakMode = .byWordWrapping) {
|
||||
|
||||
59
VDS/Components/TitleLockup/TitleLockupTextColor.swift
Normal file
59
VDS/Components/TitleLockup/TitleLockupTextColor.swift
Normal file
@ -0,0 +1,59 @@
|
||||
//
|
||||
// TitleLockupTextColor.swift
|
||||
// VDS
|
||||
//
|
||||
// Created by Matt Bruce on 5/6/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import VDSTokens
|
||||
import UIKit
|
||||
|
||||
extension TitleLockup {
|
||||
private static var textColorSecondaryConfiguration = SurfaceColorConfiguration(VDSColor.elementsSecondaryOnlight , VDSColor.elementsSecondaryOnlight).eraseToAnyColorable()
|
||||
|
||||
private static var textColorPrimaryConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark).eraseToAnyColorable()
|
||||
|
||||
public enum TextColor: Equatable {
|
||||
case primary
|
||||
case secondary
|
||||
case custom(UIColor, UIColor)
|
||||
|
||||
private var reflectedValue: String { String(reflecting: self) }
|
||||
|
||||
public static func == (lhs: Self, rhs: Self) -> Bool {
|
||||
lhs.reflectedValue == rhs.reflectedValue
|
||||
}
|
||||
|
||||
public var configuration: AnyColorable {
|
||||
switch self {
|
||||
case .primary:
|
||||
TitleLockup.textColorPrimaryConfiguration
|
||||
case .secondary:
|
||||
TitleLockup.textColorSecondaryConfiguration
|
||||
case .custom(let lightColor, let darkColor):
|
||||
SurfaceColorConfiguration(lightColor, darkColor).eraseToAnyColorable()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum TitleTextColor: Equatable {
|
||||
case primary
|
||||
case custom(UIColor, UIColor)
|
||||
|
||||
private var reflectedValue: String { String(reflecting: self) }
|
||||
|
||||
public static func == (lhs: Self, rhs: Self) -> Bool {
|
||||
lhs.reflectedValue == rhs.reflectedValue
|
||||
}
|
||||
|
||||
public var configuration: AnyColorable {
|
||||
switch self {
|
||||
case .primary:
|
||||
TitleLockup.textColorPrimaryConfiguration
|
||||
case .custom(let lightColor, let darkColor):
|
||||
SurfaceColorConfiguration(lightColor, darkColor).eraseToAnyColorable()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -14,7 +14,10 @@ extension TitleLockup {
|
||||
/// Text that will be used for the title label.
|
||||
public var text: String
|
||||
|
||||
/// Array of LabelAttributeModel objects used in rendering the text.
|
||||
/// Text color that will be used for the eyebrow label
|
||||
public var textColor: TitleTextColor
|
||||
|
||||
/// Array of LabelAttributeModel objects used in rendering the text.
|
||||
public var textAttributes: [any LabelAttributeModel]?
|
||||
|
||||
/// Used in combination with standardStyle to set the textStyle that will be used for the title label.
|
||||
@ -30,12 +33,14 @@ extension TitleLockup {
|
||||
public var lineBreakMode: NSLineBreakMode
|
||||
|
||||
public init(text: String,
|
||||
textColor: TitleTextColor = .primary,
|
||||
textAttributes: [any LabelAttributeModel]? = nil,
|
||||
isBold: Bool = true,
|
||||
standardStyle: TitleStandardStyle = .featureXSmall,
|
||||
numberOfLines: Int = 0,
|
||||
lineBreakMode: NSLineBreakMode = .byTruncatingTail) {
|
||||
self.text = text
|
||||
self.textColor = textColor
|
||||
self.isBold = isBold
|
||||
self.textAttributes = textAttributes
|
||||
self.standardStyle = standardStyle
|
||||
|
||||
@ -145,7 +145,7 @@ open class Toggle: Control, Changeable, FormFieldable {
|
||||
|
||||
open var inputId: String? { didSet { setNeedsUpdate() } }
|
||||
|
||||
open var value: AnyHashable? { didSet { setNeedsUpdate() } }
|
||||
open var value: AnyHashable? { isOn }
|
||||
|
||||
/// The natural size for the receiving view, considering only properties of the view itself.
|
||||
open override var intrinsicContentSize: CGSize {
|
||||
@ -224,7 +224,6 @@ open class Toggle: Control, Changeable, FormFieldable {
|
||||
textWeight = .regular
|
||||
textPosition = .left
|
||||
inputId = nil
|
||||
value = nil
|
||||
onChange = nil
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
|
||||
@ -68,7 +68,7 @@ open class ToggleView: Control, Changeable, FormFieldable {
|
||||
|
||||
open var inputId: String? { didSet { setNeedsUpdate() } }
|
||||
|
||||
open var value: AnyHashable? { didSet { setNeedsUpdate() } }
|
||||
open var value: AnyHashable? { isOn }
|
||||
|
||||
/// The natural size for the receiving view, considering only properties of the view itself.
|
||||
open override var intrinsicContentSize: CGSize { toggleSize }
|
||||
@ -163,7 +163,6 @@ open class ToggleView: Control, Changeable, FormFieldable {
|
||||
isOn = false
|
||||
isAnimated = true
|
||||
inputId = nil
|
||||
value = nil
|
||||
toggleView.backgroundColor = toggleColorConfiguration.getColor(self)
|
||||
knobView.backgroundColor = knobColorConfiguration.getColor(self)
|
||||
onChange = nil
|
||||
|
||||
@ -48,7 +48,6 @@ open class TooltipDialog: View, UIScrollViewDelegate {
|
||||
|
||||
lazy var primaryAccessibilityElement = UIAccessibilityElement(accessibilityContainer: self).with {
|
||||
$0.accessibilityLabel = "Modal"
|
||||
$0.accessibilityFrameInContainerSpace = .init(origin: .zero, size: .init(width: fullWidth, height: VDSLayout.space1X))
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
@ -222,7 +221,8 @@ open class TooltipDialog: View, UIScrollViewDelegate {
|
||||
super.updateAccessibility()
|
||||
|
||||
primaryAccessibilityElement.accessibilityHint = "Double tap on the \(tooltipModel.closeButtonText) button to close."
|
||||
|
||||
primaryAccessibilityElement.accessibilityFrameInContainerSpace = .init(origin: .zero, size: frame.size)
|
||||
|
||||
var elements: [Any] = [primaryAccessibilityElement]
|
||||
contentStackView.arrangedSubviews.forEach{ elements.append($0) }
|
||||
elements.append(closeButton)
|
||||
|
||||
@ -17,16 +17,19 @@ extension Tooltip {
|
||||
public var title: String?
|
||||
public var content: String?
|
||||
public var contentView: UIView?
|
||||
public var accessibleText: String?
|
||||
public var contentViewAlignment: UIStackView.Alignment?
|
||||
public init(closeButtonText: String = "Close",
|
||||
title: String? = nil,
|
||||
content: String? = nil,
|
||||
contentView: UIView? = nil,
|
||||
accessibleText: String? = "Tooltip",
|
||||
contentViewAlignment: UIStackView.Alignment = .leading) {
|
||||
self.closeButtonText = closeButtonText
|
||||
self.title = title
|
||||
self.content = content
|
||||
self.contentView = contentView
|
||||
self.accessibleText = accessibleText
|
||||
self.contentViewAlignment = contentViewAlignment
|
||||
}
|
||||
}
|
||||
|
||||
@ -176,4 +176,10 @@ extension UIColor {
|
||||
guard let found else { return nil}
|
||||
return found
|
||||
}
|
||||
|
||||
public var surface: Surface {
|
||||
var greyScale: CGFloat = 0
|
||||
getWhite(&greyScale, alpha: nil)
|
||||
return greyScale < 0.5 ? .dark : .light
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,6 +29,7 @@ extension Changeable {
|
||||
} else {
|
||||
onChangeSubscriber = nil
|
||||
}
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,6 +30,7 @@ extension Clickable {
|
||||
} else {
|
||||
onClickSubscriber = nil
|
||||
}
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,15 +15,36 @@ public protocol FormFieldable {
|
||||
var inputId: String? { get set }
|
||||
|
||||
/// Value for the Form Field.
|
||||
var value: ValueType? { get set }
|
||||
var value: ValueType? { get }
|
||||
}
|
||||
|
||||
/// Protocol for FormFieldable that require internal validation.
|
||||
public protocol FormFieldInternalValidatable: FormFieldable {
|
||||
public protocol FormFieldInternalValidatable: FormFieldable, Errorable {
|
||||
/// Is there an internalError
|
||||
var hasInternalError: Bool { get }
|
||||
/// Internal Error Message that will show.
|
||||
var internalErrorText: String? { get }
|
||||
|
||||
var validator: (any FormFieldValidatorable)? { get set }
|
||||
|
||||
func validate()
|
||||
}
|
||||
|
||||
extension FormFieldInternalValidatable {
|
||||
/// Whether or not to show the internal error
|
||||
public var hasInternalError: Bool {
|
||||
guard let validator, !showError else { return false }
|
||||
return !validator.isValid
|
||||
}
|
||||
|
||||
public var internalErrorText: String? {
|
||||
guard let validator, !validator.isValid else { return nil }
|
||||
if let errorText, !errorText.isEmpty {
|
||||
return errorText
|
||||
} else {
|
||||
return validator.errorMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Struct that will execute the validation.
|
||||
|
||||
68
VDS/Publishers/UITextView+Publisher.swift
Normal file
68
VDS/Publishers/UITextView+Publisher.swift
Normal file
@ -0,0 +1,68 @@
|
||||
//
|
||||
// UITextView+Publisher.swift
|
||||
// VDS
|
||||
//
|
||||
// Created by Matt Bruce on 5/1/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Combine
|
||||
|
||||
extension UITextView {
|
||||
|
||||
public enum Event {
|
||||
case editingChanged
|
||||
case editingDidBegin
|
||||
case editingDidEnd
|
||||
}
|
||||
|
||||
public func publisher(for event: Event) -> AnyPublisher<UITextView, Never> {
|
||||
TextViewPublisher(textView: self).publisher(for: event)
|
||||
}
|
||||
}
|
||||
|
||||
class TextViewPublisher: NSObject, UITextViewDelegate {
|
||||
var textDidChangePublisher: AnyPublisher<UITextView, Never>
|
||||
var didBeginEditingPublisher: AnyPublisher<UITextView, Never>
|
||||
var didEndEditingPublisher: AnyPublisher<UITextView, Never>
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(textView: UITextView) {
|
||||
textDidChangePublisher = NotificationCenter.default.publisher(for: UITextView.textDidChangeNotification, object: textView)
|
||||
.compactMap { notification in
|
||||
(notification.object as? UITextView)
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
|
||||
didBeginEditingPublisher = NotificationCenter.default.publisher(for: UITextView.textDidBeginEditingNotification, object: textView)
|
||||
.compactMap { notification in
|
||||
(notification.object as? UITextView)
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
|
||||
didEndEditingPublisher = NotificationCenter.default.publisher(for: UITextView.textDidEndEditingNotification, object: textView)
|
||||
.compactMap { notification in
|
||||
(notification.object as? UITextView)
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func publisher(for event: UITextView.Event) -> AnyPublisher<UITextView, Never> {
|
||||
switch event {
|
||||
case .editingChanged:
|
||||
return textDidChangePublisher
|
||||
.map { $0 }
|
||||
.eraseToAnyPublisher()
|
||||
case .editingDidBegin:
|
||||
return didBeginEditingPublisher
|
||||
.map { $0 }
|
||||
.eraseToAnyPublisher()
|
||||
case .editingDidEnd:
|
||||
return didEndEditingPublisher
|
||||
.map { $0 }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,21 @@
|
||||
1.0.62
|
||||
----------------
|
||||
- CXTDT-546824 - Notification - Accessibility - Redundant text is provided for the notification icon.
|
||||
- CXTDT-553663 - DropdownSelect - Accessibility - 5 issues
|
||||
- ONEAPP-6308 - Badge Indicator - Accessibility Text
|
||||
|
||||
1.0.61
|
||||
----------------
|
||||
- CXTDT-552068 - Text Area - Text padding
|
||||
- CXTDT-552074 - Text Area - Tooltip
|
||||
- CXTDT-552070 - Text Area - Container heights
|
||||
- CXTDT-552071 - Text Area - Entering text
|
||||
- CXTDT-552060 - Text Area - Placeholder text
|
||||
- CXTDT-552842 - Breadcrumbs - Accessibility
|
||||
- CXTDT-552825 - Tilelet - Accessibility – The role of button is not provided for the tilelet.
|
||||
- CXTDT-552834 - TileContainer - Accessibility – Voice over is not rendering the information present click state.
|
||||
- CXTDT-549888 - Pagination - Accessibility - The pagination bar does not render the correct selected page
|
||||
|
||||
1.0.60
|
||||
----------------
|
||||
- CXTDT-544442 - Button Icon - Selected state needs to allow custom color
|
||||
|
||||
Loading…
Reference in New Issue
Block a user