Merge branch 'feature/inputField' into 'develop'

updated icon to statusIcon and iconColorConfiguration

See merge request BPHV_MIPS/vds_ios!220
This commit is contained in:
Bruce, Matt R 2024-05-03 15:05:04 +00:00
commit 9b3a9e0589
32 changed files with 705 additions and 311 deletions

View File

@ -54,6 +54,10 @@
EA21C5DB2B600EDE00CFC139 /* VDSTokens.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = EA21C5DA2B600EDD00CFC139 /* VDSTokens.xcframework */; }; EA21C5DB2B600EDE00CFC139 /* VDSTokens.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = EA21C5DA2B600EDD00CFC139 /* VDSTokens.xcframework */; };
EA297A5529FB07760031ED56 /* TooltipLabelAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA297A5429FB07760031ED56 /* TooltipLabelAttribute.swift */; }; EA297A5529FB07760031ED56 /* TooltipLabelAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA297A5429FB07760031ED56 /* TooltipLabelAttribute.swift */; };
EA297A5729FB0A360031ED56 /* AppleGuidelinesTouchable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA297A5629FB0A360031ED56 /* AppleGuidelinesTouchable.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 */; }; EA336171288B19200071C351 /* VDS.docc in Sources */ = {isa = PBXBuildFile; fileRef = EA336170288B19200071C351 /* VDS.docc */; };
EA336177288B19210071C351 /* VDS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EA33616C288B19200071C351 /* VDS.framework */; }; EA336177288B19210071C351 /* VDS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EA33616C288B19200071C351 /* VDS.framework */; };
EA33617C288B19210071C351 /* VDSTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA33617B288B19210071C351 /* VDSTests.swift */; }; EA33617C288B19210071C351 /* VDSTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA33617B288B19210071C351 /* VDSTests.swift */; };
@ -243,6 +247,10 @@
EA21C5DA2B600EDD00CFC139 /* VDSTokens.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = VDSTokens.xcframework; path = ../SharedFrameworks/VDSTokens.xcframework; sourceTree = "<group>"; }; 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>"; }; 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>"; }; 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; }; 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>"; }; 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>"; }; EA336170288B19200071C351 /* VDS.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = VDS.docc; sourceTree = "<group>"; };
@ -512,6 +520,15 @@
path = ButtonGroup; path = ButtonGroup;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
EA2DC9AE2BE175A6004F58C5 /* Rules */ = {
isa = PBXGroup;
children = (
EA2DC9AF2BE175BA004F58C5 /* RequiredRule.swift */,
EA2DC9B12BE175E6004F58C5 /* CharacterCountRule.swift */,
);
path = Rules;
sourceTree = "<group>";
};
EA336162288B19200071C351 = { EA336162288B19200071C351 = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -835,6 +852,7 @@
children = ( children = (
EAB1D2E928AE84AA00DAE764 /* UIControlPublisher.swift */, EAB1D2E928AE84AA00DAE764 /* UIControlPublisher.swift */,
EA89200328AECF4B006B9984 /* UITextField+Publisher.swift */, EA89200328AECF4B006B9984 /* UITextField+Publisher.swift */,
EA2DC9B52BE2F4A1004F58C5 /* UITextView+Publisher.swift */,
EAD8D2C028BFDE8B006EB6A6 /* UIGestureRecognizer+Publisher.swift */, EAD8D2C028BFDE8B006EB6A6 /* UIGestureRecognizer+Publisher.swift */,
); );
path = Publishers; path = Publishers;
@ -875,6 +893,7 @@
EAC925852911C9DE00091998 /* TextFields */ = { EAC925852911C9DE00091998 /* TextFields */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
EA2DC9AE2BE175A6004F58C5 /* Rules */,
EAC9258B2911C9DE00091998 /* EntryFieldBase.swift */, EAC9258B2911C9DE00091998 /* EntryFieldBase.swift */,
EAC925862911C9DE00091998 /* InputField */, EAC925862911C9DE00091998 /* InputField */,
EA985C21296E032000F2FF2E /* TextArea */, EA985C21296E032000F2FF2E /* TextArea */,
@ -886,6 +905,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
EAC925872911C9DE00091998 /* InputField.swift */, EAC925872911C9DE00091998 /* InputField.swift */,
EA2DC9B32BE2C6FE004F58C5 /* TextField.swift */,
EA6642942BCEBF9500D81DC4 /* TextLinkModel.swift */, EA6642942BCEBF9500D81DC4 /* TextLinkModel.swift */,
); );
path = InputField; path = InputField;
@ -1136,6 +1156,7 @@
71ACE89E2BA1CC1700FB6ADC /* TiletEyebrowModel.swift in Sources */, 71ACE89E2BA1CC1700FB6ADC /* TiletEyebrowModel.swift in Sources */,
EAF7F11728A1475A00B287F5 /* RadioButtonItem.swift in Sources */, EAF7F11728A1475A00B287F5 /* RadioButtonItem.swift in Sources */,
EA985BEE2968A92400F2FF2E /* TitleLockupSubTitleModel.swift in Sources */, EA985BEE2968A92400F2FF2E /* TitleLockupSubTitleModel.swift in Sources */,
EA2DC9B22BE175E6004F58C5 /* CharacterCountRule.swift in Sources */,
EA985BF22968B5BB00F2FF2E /* TitleLockupTextStyle.swift in Sources */, EA985BF22968B5BB00F2FF2E /* TitleLockupTextStyle.swift in Sources */,
EAB1D2CD28ABE76100DAE764 /* Withable.swift in Sources */, EAB1D2CD28ABE76100DAE764 /* Withable.swift in Sources */,
71FC86DC2B96F4C800700965 /* PaginationCellItem.swift in Sources */, 71FC86DC2B96F4C800700965 /* PaginationCellItem.swift in Sources */,
@ -1145,10 +1166,12 @@
71FC86DE2B9738B900700965 /* SurfaceConfigurationValue.swift in Sources */, 71FC86DE2B9738B900700965 /* SurfaceConfigurationValue.swift in Sources */,
EA297A5529FB07760031ED56 /* TooltipLabelAttribute.swift in Sources */, EA297A5529FB07760031ED56 /* TooltipLabelAttribute.swift in Sources */,
EA985BEA29689B6D00F2FF2E /* TileletSubTitleModel.swift in Sources */, EA985BEA29689B6D00F2FF2E /* TileletSubTitleModel.swift in Sources */,
EA2DC9B02BE175BA004F58C5 /* RequiredRule.swift in Sources */,
EA3361C9289054C50071C351 /* Surfaceable.swift in Sources */, EA3361C9289054C50071C351 /* Surfaceable.swift in Sources */,
EAB5FEED2927E1B200998C17 /* ButtonGroupPositionLayout.swift in Sources */, EAB5FEED2927E1B200998C17 /* ButtonGroupPositionLayout.swift in Sources */,
EA4DB30228DCBCA500103EE3 /* Badge.swift in Sources */, EA4DB30228DCBCA500103EE3 /* Badge.swift in Sources */,
EA33624728931B050071C351 /* Initable.swift in Sources */, EA33624728931B050071C351 /* Initable.swift in Sources */,
EA2DC9B62BE2F4A1004F58C5 /* UITextView+Publisher.swift in Sources */,
EAF7F0A4289B017C00B287F5 /* LabelAttributeModel.swift in Sources */, EAF7F0A4289B017C00B287F5 /* LabelAttributeModel.swift in Sources */,
EA0B18022A9E236900F2D0CD /* SelectorGroupBase.swift in Sources */, EA0B18022A9E236900F2D0CD /* SelectorGroupBase.swift in Sources */,
EA5F86D02A1F936100BC83E4 /* TabsContainer.swift in Sources */, EA5F86D02A1F936100BC83E4 /* TabsContainer.swift in Sources */,
@ -1223,6 +1246,7 @@
EAB5FEF12927F4AA00998C17 /* SelfSizingCollectionView.swift in Sources */, EAB5FEF12927F4AA00998C17 /* SelfSizingCollectionView.swift in Sources */,
EA3361B8288B2AAA0071C351 /* ViewProtocol.swift in Sources */, EA3361B8288B2AAA0071C351 /* ViewProtocol.swift in Sources */,
EA3361A8288B23300071C351 /* UIColor.swift in Sources */, EA3361A8288B23300071C351 /* UIColor.swift in Sources */,
EA2DC9B42BE2C6FE004F58C5 /* TextField.swift in Sources */,
EAC9257D29119B5400091998 /* TextLink.swift in Sources */, EAC9257D29119B5400091998 /* TextLink.swift in Sources */,
EA596ABF2A16B4F500300C4B /* Tabs.swift in Sources */, EA596ABF2A16B4F500300C4B /* Tabs.swift in Sources */,
EAD062A72A3B67770015965D /* UIView+CALayer.swift in Sources */, EAD062A72A3B67770015965D /* UIView+CALayer.swift in Sources */,
@ -1377,7 +1401,7 @@
BUILD_LIBRARY_FOR_DISTRIBUTION = YES; BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
CODE_SIGN_IDENTITY = ""; CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 60; CURRENT_PROJECT_VERSION = 61;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
@ -1414,7 +1438,7 @@
BUILD_LIBRARY_FOR_DISTRIBUTION = YES; BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
CODE_SIGN_IDENTITY = ""; CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 60; CURRENT_PROJECT_VERSION = 61;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;

View File

@ -20,7 +20,7 @@ extension SelectorGroup {
public var hasSelectedItem: Bool { items.filter { $0.isSelected == true }.count > 0 } public var hasSelectedItem: Bool { items.filter { $0.isSelected == true }.count > 0 }
} }
public protocol SelectorGroupMultiSelect: SelectorGroup {} public protocol SelectorGroupMultiSelect: SelectorGroup, FormFieldable {}
extension SelectorGroupMultiSelect { extension SelectorGroupMultiSelect {
/// Current Selected Control for this group. /// Current Selected Control for this group.
public var selectedItems: [SelectorItemType]? { public var selectedItems: [SelectorItemType]? {
@ -30,7 +30,7 @@ extension SelectorGroupMultiSelect {
} }
} }
public protocol SelectorGroupSingleSelect: SelectorGroup {} public protocol SelectorGroupSingleSelect: SelectorGroup, FormFieldable {}
extension SelectorGroupSingleSelect { extension SelectorGroupSingleSelect {
/// Current Selected Control for this group. /// Current Selected Control for this group.
public var selectedItem: SelectorItemType? { public var selectedItem: SelectorItemType? {

View File

@ -11,7 +11,7 @@ import Combine
import VDSTokens import VDSTokens
/// Base Class used to build out a SelectorControlable control. /// Base Class used to build out a SelectorControlable control.
open class SelectorItemBase<Selector: SelectorControlable>: Control, Errorable, Changeable, FormFieldable { open class SelectorItemBase<Selector: SelectorControlable>: Control, Errorable, Changeable {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Initializers // MARK: - Initializers
@ -141,7 +141,9 @@ open class SelectorItemBase<Selector: SelectorControlable>: Control, Errorable,
open var inputId: String? { didSet { setNeedsUpdate() } } 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 // MARK: - Overrides
@ -214,7 +216,6 @@ open class SelectorItemBase<Selector: SelectorControlable>: Control, Errorable,
showError = false showError = false
errorText = nil errorText = nil
inputId = nil inputId = nil
value = nil
isSelected = false isSelected = false
onChange = nil onChange = nil

View File

@ -23,12 +23,12 @@ public final class SelfSizingCollectionView: UICollectionView {
/// - layout: Layout used for this CollectionView /// - layout: Layout used for this CollectionView
public override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) { public override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
super.init(frame: frame, collectionViewLayout: layout) super.init(frame: frame, collectionViewLayout: layout)
self.setupContentSizeObservation() self.initialSetup()
} }
public required init?(coder: NSCoder) { public required init?(coder: NSCoder) {
super.init(coder: coder) super.init(coder: coder)
self.setupContentSizeObservation() self.initialSetup()
} }
//-------------------------------------------------- //--------------------------------------------------
@ -69,22 +69,31 @@ public final class SelfSizingCollectionView: UICollectionView {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Private Methods // 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 //ensure autoLayout uses intrinsic height
setContentHuggingPriority(.required, for: .vertical) setContentHuggingPriority(.required, for: .vertical)
setContentCompressionResistancePriority(.required, for: .vertical) setContentCompressionResistancePriority(.required, for: .vertical)
collectionViewHeight = height(constant: 0, priority: .defaultHigh) 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 .sink { [weak self] compare in
guard let self else { return }
if compare.height != self.collectionViewHeight?.constant { guard let self,
self.invalidateIntrinsicContentSize() let currentHeight = self.collectionViewHeight?.constant,
self.collectionViewHeight?.constant = compare.height compare.height != currentHeight else { return }
self.contentSizeSubject.send(compare)
} self.invalidateIntrinsicContentSize()
self.collectionViewHeight?.constant = compare.height
self.contentSizeSubject.send(compare)
} }
} }
deinit {
anyCancellable?.cancel()
}
} }
extension UITraitCollection { extension UITraitCollection {

View File

@ -96,7 +96,7 @@ open class BreadcrumbItem: ButtonBase {
/// Used to update any Accessibility properties. /// Used to update any Accessibility properties.
open override func updateAccessibility() { open override func updateAccessibility() {
super.updateAccessibility() super.updateAccessibility()
accessibilityLabel = "Breadcrumb \(text ?? "")" accessibilityLabel = text
} }
} }

View File

@ -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). /// A callback when the selected item changes. Passes parameters (crumb).
open var onBreadcrumbDidSelect: ((BreadcrumbItem) -> Void)? open var onBreadcrumbDidSelect: ((BreadcrumbItem) -> Void)?
@ -73,6 +80,11 @@ open class Breadcrumbs: View {
return collectionView return collectionView
}() }()
private let containerView = View().with {
$0.isAccessibilityElement = true
$0.accessibilityLabel = "Breadcrumbs"
}
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Private Methods // MARK: - Private Methods
//-------------------------------------------------- //--------------------------------------------------
@ -106,8 +118,10 @@ open class Breadcrumbs: View {
/// Executed on initialization for this View. /// Executed on initialization for this View.
open override func initialSetup() { open override func initialSetup() {
super.initialSetup() super.initialSetup()
addSubview(collectionView) containerView.addSubview(collectionView)
collectionView.pinToSuperView() collectionView.pinToSuperView()
addSubview(containerView)
containerView.pinToSuperView()
} }
/// Resets to default settings. /// Resets to default settings.
@ -139,6 +153,7 @@ open class Breadcrumbs: View {
} }
} }
private var separatorWidth = Label().with { $0.text = "/"; $0.sizeToFit() }.intrinsicContentSize.width private var separatorWidth = Label().with { $0.text = "/"; $0.sizeToFit() }.intrinsicContentSize.width
} }
extension Breadcrumbs: UICollectionViewDelegate, UICollectionViewDataSource, ButtongGroupPositionLayoutDelegate { extension Breadcrumbs: UICollectionViewDelegate, UICollectionViewDataSource, ButtongGroupPositionLayoutDelegate {

View File

@ -14,6 +14,7 @@ import VDSTokens
/// to allow user selection. /// to allow user selection.
@objc(VDSCheckboxGroup) @objc(VDSCheckboxGroup)
open class CheckboxGroup: SelectorGroupBase<CheckboxItem>, SelectorGroupMultiSelect { open class CheckboxGroup: SelectorGroupBase<CheckboxItem>, SelectorGroupMultiSelect {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Initializers // MARK: - Initializers
//-------------------------------------------------- //--------------------------------------------------
@ -32,6 +33,10 @@ open class CheckboxGroup: SelectorGroupBase<CheckboxItem>, SelectorGroupMultiSel
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Public Properties // 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``. /// Array of ``CheckboxItemModel`` that will be used to build the selectorViews of type ``CheckboxItem``.
open var selectorModels: [CheckboxItemModel]? { open var selectorModels: [CheckboxItemModel]? {
didSet { didSet {
@ -41,7 +46,7 @@ open class CheckboxGroup: SelectorGroupBase<CheckboxItem>, SelectorGroupMultiSel
$0.isEnabled = !model.disabled $0.isEnabled = !model.disabled
$0.surface = model.surface $0.surface = model.surface
$0.inputId = model.inputId $0.inputId = model.inputId
$0.value = model.value $0.hiddenValue = model.value
$0.accessibilityLabel = model.accessibileText $0.accessibilityLabel = model.accessibileText
$0.accessibilityValue = "item \(index+1) of \(selectorModels.count)" $0.accessibilityValue = "item \(index+1) of \(selectorModels.count)"
$0.labelText = model.labelText $0.labelText = model.labelText
@ -97,7 +102,7 @@ open class CheckboxGroup: SelectorGroupBase<CheckboxItem>, SelectorGroupMultiSel
} }
extension CheckboxGroup { extension CheckboxGroup {
public struct CheckboxItemModel : Surfaceable, Initable, FormFieldable, Errorable { public struct CheckboxItemModel : Surfaceable, Initable, Errorable {
/// Whether this object is disabled or not /// Whether this object is disabled or not
public var disabled: Bool public var disabled: Bool

View File

@ -37,6 +37,11 @@ open class DropdownSelect: EntryFieldBase {
/// Allows unique ID to be passed to the element. /// Allows unique ID to be passed to the element.
open var selectId: Int? { didSet { setNeedsUpdate() }} open var selectId: Int? { didSet { setNeedsUpdate() }}
/// Current SelectedItem Value
open override var value: String? {
selectedItem?.value
}
/// Current SelectedItem /// Current SelectedItem
open var selectedItem: DropdownOptionModel? { open var selectedItem: DropdownOptionModel? {
guard let selectId else { return nil } guard let selectId else { return nil }
@ -90,12 +95,6 @@ open class DropdownSelect: EntryFieldBase {
//-------------------------------------------------- //--------------------------------------------------
internal override var containerSize: CGSize { CGSize(width: showInlineLabel ? minWidthInlineLabel : width ?? minWidthDefault, height: 44) } 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 // MARK: - Overrides
//-------------------------------------------------- //--------------------------------------------------
@ -163,7 +162,7 @@ open class DropdownSelect: EntryFieldBase {
updateInlineLabel() updateInlineLabel()
dropdownField.isUserInteractionEnabled = readOnly ? false : true dropdownField.isUserInteractionEnabled = isReadOnly ? false : true
selectedOptionLabel.surface = surface selectedOptionLabel.surface = surface
selectedOptionLabel.isEnabled = isEnabled selectedOptionLabel.isEnabled = isEnabled
} }
@ -191,7 +190,7 @@ open class DropdownSelect: EntryFieldBase {
updatedLabelText = showInlineLabel ? "" : updatedLabelText 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, let optionColorAttr = ColorLabelAttribute(location: oldText.count + 2,
length: 8, length: 8,
color: secondaryColorConfiguration.getColor(self)) color: secondaryColorConfiguration.getColor(self))
@ -234,17 +233,16 @@ open class DropdownSelect: EntryFieldBase {
open func updateSelectedOptionLabel(option: DropdownOptionModel? = nil) { open func updateSelectedOptionLabel(option: DropdownOptionModel? = nil) {
selectedOptionLabel.text = option?.text ?? "" selectedOptionLabel.text = option?.text ?? ""
value = option?.value
} }
open override func updateErrorLabel() { open override func updateErrorLabel() {
super.updateErrorLabel() super.updateErrorLabel()
if !showError && !hasInternalError { if !showError && !hasInternalError {
icon.name = .downCaret statusIcon.name = .downCaret
} }
icon.surface = surface statusIcon.surface = surface
icon.isHidden = readOnly ? true : false statusIcon.isHidden = isReadOnly ? true : false
icon.color = iconColorConfiguration.getColor(self) statusIcon.color = iconColorConfiguration.getColor(self)
} }
@objc open func pickerDoneClicked() { @objc open func pickerDoneClicked() {
@ -284,6 +282,7 @@ extension DropdownSelect: UIPickerViewDelegate, UIPickerViewDataSource {
guard options.count > row else { return } guard options.count > row else { return }
selectId = row selectId = row
updateSelectedOptionLabel(option: options[row]) updateSelectedOptionLabel(option: options[row])
sendActions(for: .valueChanged)
self.onItemSelected?(row, options[row]) self.onItemSelected?(row, options[row])
} }
} }

View File

@ -14,7 +14,7 @@ import Combine
/// It usually represents a supplementary or utilitarian action. A button icon can stand alone, but often /// 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. /// exists in a group when there are several actions that can be performed.
@objc(VDSButtonIcon) @objc(VDSButtonIcon)
open class ButtonIcon: Control, Changeable, FormFieldable { open class ButtonIcon: Control, Changeable {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Initializers // 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. /// Used to move the icon inside the button in both x and y axis.
open var iconOffset: CGPoint = .init(x: 0, y: 0) { didSet { setNeedsUpdate() } } open var iconOffset: CGPoint = .init(x: 0, y: 0) { didSet { setNeedsUpdate() } }
open var inputId: String? { didSet { setNeedsUpdate() } }
open var value: AnyHashable? { didSet { setNeedsUpdate() } }
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Configuration // MARK: - Configuration
//-------------------------------------------------- //--------------------------------------------------

View File

@ -37,17 +37,19 @@ public class TooltipLabelAttribute: ActionLabelAttributeModel, TooltipLaunchable
} }
var frame = CGRect.zero 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 { if let font = attributedString.attribute(.font, at: 0, effectiveRange: &originalRange) as? UIFont {
switch font.pointSize { switch font.pointSize {
case 15..<25: case 15..<25:
size = .medium 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: case 0..<14:
size = .small 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: default:
size = .medium 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)
} }
} }

View File

@ -52,7 +52,7 @@ open class Notification: View {
} }
} }
var accessibilityText: String { var accessibleText: String {
switch self { switch self {
case .info: case .info:
"Information Message" "Information Message"
@ -374,7 +374,7 @@ open class Notification: View {
open override func updateAccessibility() { open override func updateAccessibility() {
super.updateAccessibility() super.updateAccessibility()
closeButton.accessibilityLabel = "Close Notification" closeButton.accessibilityLabel = "Close Notification"
typeIcon.accessibilityLabel = style.accessibilityText typeIcon.accessibilityLabel = style.accessibleText
} }
private func setConstraints() { private func setConstraints() {

View File

@ -189,6 +189,7 @@ open class Pagination: View {
nextButton.isHidden = _selectedPageIndex == total - 1 nextButton.isHidden = _selectedPageIndex == total - 1
collectionView.reloadData() collectionView.reloadData()
verifyIfMaxDigitChanged() verifyIfMaxDigitChanged()
setNeedsUpdate()
} }
///Identifying if there is any change in the digits of upcoming page ///Identifying if there is any change in the digits of upcoming page

View File

@ -32,6 +32,10 @@ open class RadioBoxGroup: SelectorGroupBase<RadioBoxItem>, SelectorGroupSingleSe
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Public Properties // 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``. /// Array of ``RadioBoxItemModel`` that will be used to build the selectorViews of type ``RadioBoxItem``.
open var selectorModels: [RadioBoxItemModel]? { open var selectorModels: [RadioBoxItemModel]? {
didSet { didSet {
@ -48,6 +52,7 @@ open class RadioBoxGroup: SelectorGroupBase<RadioBoxItem>, SelectorGroupSingleSe
$0.subTextRightAttributes = model.subTextRightAttributes $0.subTextRightAttributes = model.subTextRightAttributes
$0.isEnabled = !model.disabled $0.isEnabled = !model.disabled
$0.inputId = model.inputId $0.inputId = model.inputId
$0.hiddenValue = model.value
$0.isSelected = model.selected $0.isSelected = model.selected
$0.strikethrough = model.strikethrough $0.strikethrough = model.strikethrough
$0.strikethroughAccessibilityText = model.strikethroughAccessibileText $0.strikethroughAccessibilityText = model.strikethroughAccessibileText

View File

@ -127,7 +127,9 @@ open class RadioBoxItem: Control, Changeable, FormFieldable {
open var inputId: String? { didSet { setNeedsUpdate() } } 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 // MARK: - Configuration Properties
@ -211,7 +213,7 @@ open class RadioBoxItem: Control, Changeable, FormFieldable {
subTextRightAttributedText = nil subTextRightAttributedText = nil
strikethrough = false strikethrough = false
inputId = nil inputId = nil
value = nil hiddenValue = nil
isSelected = false isSelected = false
onChange = nil onChange = nil

View File

@ -32,6 +32,10 @@ open class RadioButtonGroup: SelectorGroupBase<RadioButtonItem>, SelectorGroupSi
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Public Properties // 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``. /// Array of ``RadioButtonItemModel`` that will be used to build the selectorViews of type ``RadioButtonItem``.
open var selectorModels: [RadioButtonItemModel]? { open var selectorModels: [RadioButtonItemModel]? {
didSet { didSet {
@ -41,7 +45,7 @@ open class RadioButtonGroup: SelectorGroupBase<RadioButtonItem>, SelectorGroupSi
$0.isEnabled = !model.disabled $0.isEnabled = !model.disabled
$0.surface = model.surface $0.surface = model.surface
$0.inputId = model.inputId $0.inputId = model.inputId
$0.value = model.value $0.hiddenValue = model.value
$0.accessibilityLabel = model.accessibileText $0.accessibilityLabel = model.accessibileText
$0.accessibilityValue = "item \(index+1) of \(selectorModels.count)" $0.accessibilityValue = "item \(index+1) of \(selectorModels.count)"
$0.labelText = model.labelText $0.labelText = model.labelText

View File

@ -83,6 +83,8 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
} }
}() }()
open var rules = [AnyRule<String>]()
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Configuration Properties // 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: .normal)
$0.setSurfaceColors(VDSFormControlsColor.backgroundOnlight, VDSFormControlsColor.backgroundOndark, forState: .disabled) $0.setSurfaceColors(VDSFormControlsColor.backgroundOnlight, VDSFormControlsColor.backgroundOndark, forState: .disabled)
$0.setSurfaceColors(VDSColor.feedbackErrorBackgroundOnlight, VDSColor.feedbackErrorBackgroundOndark, forState: .error) $0.setSurfaceColors(VDSColor.feedbackErrorBackgroundOnlight, VDSColor.feedbackErrorBackgroundOndark, forState: .error)
$0.setSurfaceColors(VDSColor.feedbackErrorBackgroundOnlight, VDSColor.feedbackErrorBackgroundOndark, forState: [.error, .focused])
} }
internal var borderColorConfiguration = ControlColorConfiguration().with { internal var borderColorConfiguration = ControlColorConfiguration().with {
$0.setSurfaceColors(VDSFormControlsColor.borderOnlight, VDSFormControlsColor.borderOndark, forState: .normal) $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.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled)
$0.setSurfaceColors(VDSColor.feedbackErrorOnlight, VDSColor.feedbackErrorOndark, forState: .error) $0.setSurfaceColors(VDSColor.feedbackErrorOnlight, VDSColor.feedbackErrorOndark, forState: .error)
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: [.disabled,.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 { internal var readOnlyBorderColorConfiguration = ControlColorConfiguration().with {
$0.setSurfaceColors(VDSFormControlsColor.borderReadonlyOnlight, VDSFormControlsColor.borderReadonlyOndark, forState: .normal) $0.setSurfaceColors(VDSFormControlsColor.borderReadonlyOnlight, VDSFormControlsColor.borderReadonlyOndark, forState: .normal)
} }
@ -137,7 +148,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
$0.textStyle = .bodySmall $0.textStyle = .bodySmall
} }
open var icon: Icon = Icon().with { open var statusIcon: Icon = Icon().with {
$0.size = .medium $0.size = .medium
} }
@ -149,10 +160,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
open var showError: Bool = false { didSet { setNeedsUpdate() } } open var showError: Bool = false { didSet { setNeedsUpdate() } }
/// FormFieldValidator /// FormFieldValidator
internal var validator: (any FormFieldValidatorable)? open var validator: (any FormFieldValidatorable)?
/// Whether or not to show the internal error
open var hasInternalError: Bool { !(validator?.isValid ?? true) }
/// Override UIControl state to add the .error state if showError is true. /// Override UIControl state to add the .error state if showError is true.
open override var state: UIControl.State { open override var state: UIControl.State {
@ -165,21 +173,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
} }
} }
open var errorText: String? { open var errorText: String? { didSet { setNeedsUpdate() } }
didSet {
updateContainerView()
updateErrorLabel()
setNeedsUpdate()
}
}
open var internalErrorText: String? {
didSet {
updateContainerView()
updateErrorLabel()
setNeedsUpdate()
}
}
open var tooltipModel: Tooltip.TooltipModel? { didSet { setNeedsUpdate() } } open var tooltipModel: Tooltip.TooltipModel? { didSet { setNeedsUpdate() } }
@ -190,22 +184,15 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
open var inputId: String? { didSet { setNeedsUpdate() } } open var inputId: String? { didSet { setNeedsUpdate() } }
/// The text of this textField. /// The text of this textField.
internal var _value: String?
open var value: String? { open var value: String? {
get { _value } get { fatalError("must be read from subclass")}
set {
if let newValue, newValue != _value {
_value = newValue
sendActions(for: .valueChanged)
}
}
} }
open var defaultValue: AnyHashable? { didSet { setNeedsUpdate() } } 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 // MARK: - Constraints
@ -239,11 +226,11 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
//this is the horizontal stack that contains //this is the horizontal stack that contains
//the left, InputContainer, Icons, Buttons //the left, InputContainer, Icons, Buttons
container.addSubview(containerStackView) container.addSubview(containerStackView)
containerStackView.pinToSuperView(.uniform(12)) containerStackView.pinToSuperView(.uniform(VDSLayout.space3X))
//add the view to add input fields //add the view to add input fields
containerStackView.addArrangedSubview(controlContainerView) containerStackView.addArrangedSubview(controlContainerView)
containerStackView.addArrangedSubview(icon) containerStackView.addArrangedSubview(statusIcon)
containerStackView.setCustomSpacing(VDSLayout.space3X, after: controlContainerView) containerStackView.setCustomSpacing(VDSLayout.space3X, after: controlContainerView)
//get the container this is what show helper text, error text //get the container this is what show helper text, error text
@ -295,25 +282,26 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
transparentBackground = false transparentBackground = false
width = nil width = nil
inputId = nil inputId = nil
value = nil
defaultValue = nil defaultValue = nil
required = false isRequired = false
readOnly = false isReadOnly = false
onChange = nil onChange = nil
} }
/// Used to make changes to the View based off a change events or from local properties. /// Used to make changes to the View based off a change events or from local properties.
open override func updateView() { open override func updateView() {
super.updateView() super.updateView()
updateContainerView() updateContainerView()
updateTitleLabel() updateTitleLabel()
updateErrorLabel() updateErrorLabel()
updateHelperLabel() updateHelperLabel()
}
backgroundColor = surface.color open func validate(){
updateRules()
validator = FormFieldValidator<EntryFieldBase>(field: self, rules: rules)
validator?.validate() validator?.validate()
internalErrorText = validator?.errorMessage setNeedsUpdate()
} }
//-------------------------------------------------- //--------------------------------------------------
@ -321,7 +309,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
//-------------------------------------------------- //--------------------------------------------------
private func updateContainerView() { private func updateContainerView() {
containerView.backgroundColor = backgroundColorConfiguration.getColor(self) 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.borderWidth = VDSFormControls.borderWidth
containerView.layer.cornerRadius = VDSFormControls.borderRadius containerView.layer.cornerRadius = VDSFormControls.borderRadius
} }
@ -339,6 +327,21 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
return bottomContainerView 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() { open func updateTitleLabel() {
//update the local vars for the label since we no //update the local vars for the label since we no
@ -347,7 +350,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
var updatedLabelText = labelText var updatedLabelText = labelText
//dealing with the "Optional" addition to the text //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 { if isEnabled {
let optionColorAttr = ColorLabelAttribute(location: oldText.count + 2, let optionColorAttr = ColorLabelAttribute(location: oldText.count + 2,
length: 8, length: 8,
@ -370,37 +373,27 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
} }
open func updateErrorLabel(){ open func updateErrorLabel(){
if showError, hasInternalError, let errorText, let internalErrorText { if showError, let errorText {
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 {
errorLabel.text = errorText errorLabel.text = errorText
errorLabel.surface = surface errorLabel.surface = surface
errorLabel.isEnabled = isEnabled errorLabel.isEnabled = isEnabled
errorLabel.isHidden = false errorLabel.isHidden = false
icon.name = .error statusIcon.name = .error
icon.color = VDSColor.paletteBlack statusIcon.surface = surface
icon.surface = surface statusIcon.isHidden = !isEnabled || state.contains(.focused)
icon.isHidden = !isEnabled
} else if hasInternalError, let internalErrorText { } else if hasInternalError, let internalErrorText {
errorLabel.text = internalErrorText errorLabel.text = internalErrorText
errorLabel.surface = surface errorLabel.surface = surface
errorLabel.isEnabled = isEnabled errorLabel.isEnabled = isEnabled
errorLabel.isHidden = false errorLabel.isHidden = false
icon.name = .error statusIcon.name = .error
icon.color = VDSColor.paletteBlack statusIcon.surface = surface
icon.surface = surface statusIcon.isHidden = !isEnabled || state.contains(.focused)
icon.isHidden = !isEnabled
} else { } else {
icon.isHidden = true statusIcon.isHidden = true
errorLabel.isHidden = true errorLabel.isHidden = true
} }
statusIcon.color = iconColorConfiguration.getColor(self)
} }
open func updateHelperLabel(){ open func updateHelperLabel(){

View File

@ -14,7 +14,7 @@ import Combine
/// Specialized input fields capture credit card numbers, inline actions, passwords, phone numbers, /// Specialized input fields capture credit card numbers, inline actions, passwords, phone numbers,
/// dates and security codes in their correct formats. /// dates and security codes in their correct formats.
@objc(VDSInputField) @objc(VDSInputField)
open class InputField: EntryFieldBase, UITextFieldDelegate { open class InputField: EntryFieldBase {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Initializers // MARK: - Initializers
@ -36,13 +36,13 @@ open class InputField: EntryFieldBase, UITextFieldDelegate {
//-------------------------------------------------- //--------------------------------------------------
/// Enum used to describe the input type. /// Enum used to describe the input type.
public enum FieldType: String, CaseIterable { 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 // MARK: - Private Properties
//-------------------------------------------------- //--------------------------------------------------
internal var inputFieldStackView: UIStackView = { internal var inputFieldStackView: UIStackView = {
return UIStackView().with { return UIStackView().with {
$0.translatesAutoresizingMaskIntoConstraints = false $0.translatesAutoresizingMaskIntoConstraints = false
$0.axis = .horizontal $0.axis = .horizontal
@ -63,7 +63,7 @@ open class InputField: EntryFieldBase, UITextFieldDelegate {
} }
/// UITextField shown in the InputField. /// UITextField shown in the InputField.
open var textField = UITextField().with { open var textField = TextField().with {
$0.translatesAutoresizingMaskIntoConstraints = false $0.translatesAutoresizingMaskIntoConstraints = false
$0.font = TextStyle.bodyLarge.font $0.font = TextStyle.bodyLarge.font
} }
@ -77,31 +77,24 @@ open class InputField: EntryFieldBase, UITextFieldDelegate {
/// Representing the type of input. /// Representing the type of input.
open var fieldType: FieldType = .text { didSet { setNeedsUpdate() } } open var fieldType: FieldType = .text { didSet { setNeedsUpdate() } }
internal var actionTextLink = TextLink().with { $0.contentEdgeInsets = .top(-2) } open var leftIcon: Icon = Icon().with { $0.size = .medium }
internal var actionTextLinkModel: TextLinkModel? { didSet { setNeedsUpdate() } } open var actionTextLink = TextLink().with { $0.contentEdgeInsets = .top(-2) }
open var actionTextLinkModel: TextLinkModel? { didSet { setNeedsUpdate() } }
/// The text of this TextField. /// The text of this TextField.
private var _text: String?
open var text: String? { open var text: String? {
get { _text } get { textField.text }
set { set {
if let newValue, newValue != _text { textField.text = newValue
_text = newValue
textField.text = newValue
value = newValue
}
setNeedsUpdate() setNeedsUpdate()
} }
} }
/// The value of this textField. /// Value for the textField
open override var value: String? { open override var value: String? {
didSet { textField.text
if text != value {
text = value
}
}
} }
var _showError: Bool = false var _showError: Bool = false
@ -134,7 +127,11 @@ open class InputField: EntryFieldBase, UITextFieldDelegate {
var state = super.state var state = super.state
if showSuccess { if showSuccess {
state.insert(.success) state.insert(.success)
} else if textField.isFirstResponder {
state.insert(.focused)
} }
return state return state
} }
} }
@ -156,20 +153,38 @@ open class InputField: EntryFieldBase, UITextFieldDelegate {
minWidthConstraint = containerView.widthAnchor.constraint(greaterThanOrEqualToConstant: 0) minWidthConstraint = containerView.widthAnchor.constraint(greaterThanOrEqualToConstant: 0)
minWidthConstraint?.isActive = true minWidthConstraint?.isActive = true
controlContainerView.addSubview(textField) // stackview for controls in EntryFieldBase.controlContainerView
textField let controlStackView = UIStackView().with {
.pinTop() $0.translatesAutoresizingMaskIntoConstraints = false
.pinLeading() $0.axis = .horizontal
.pinTrailingLessThanOrEqualTo(nil, 0, .defaultHigh) $0.spacing = VDSLayout.space3X
.pinBottom(0, .defaultHigh) }
controlContainerView.addSubview(controlStackView)
controlStackView.pinToSuperView()
controlStackView.addArrangedSubview(leftIcon)
controlStackView.addArrangedSubview(textField)
textField.heightAnchor.constraint(equalToConstant: 20).isActive = true textField.heightAnchor.constraint(equalToConstant: 20).isActive = true
textField textField
.textPublisher .textPublisher
.sink { [weak self] text in .sink { [weak self] newText in
self?.value = text print("textPublisher newText: \(newText)")
self?.text = newText
self?.validate()
self?.sendActions(for: .valueChanged) 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) }.store(in: &subscribers)
stackView.addArrangedSubview(successLabel) stackView.addArrangedSubview(successLabel)
@ -188,7 +203,6 @@ open class InputField: EntryFieldBase, UITextFieldDelegate {
open override func reset() { open override func reset() {
super.reset() super.reset()
textField.text = "" textField.text = ""
textField.delegate = self
successLabel.reset() successLabel.reset()
successLabel.textStyle = .bodySmall 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. /// Used to make changes to the View based off a change events or from local properties.
open override func updateView() { open override func updateView() {
//update fieldType first
updateFieldType()
super.updateView() super.updateView()
textField.isEnabled = isEnabled textField.isEnabled = isEnabled
textField.textColor = textFieldTextColorConfiguration.getColor(self) textField.textColor = textFieldTextColorConfiguration.getColor(self)
}
if let actionTextLinkModel { open override func updateErrorLabel() {
actionTextLink.text = actionTextLinkModel.text super.updateErrorLabel()
actionTextLink.onClick = actionTextLinkModel.onClick
actionTextLink.isHidden = false
containerStackView.setCustomSpacing(VDSLayout.space2X, after: icon)
} else {
actionTextLink.isHidden = true
containerStackView.setCustomSpacing(0, after: icon)
}
//show error or success //show error or success
if showError, let _ = errorText { if showError, let _ = errorText {
@ -232,27 +244,15 @@ open class InputField: EntryFieldBase, UITextFieldDelegate {
successLabel.isEnabled = isEnabled successLabel.isEnabled = isEnabled
successLabel.isHidden = false successLabel.isHidden = false
errorLabel.isHidden = true errorLabel.isHidden = true
icon.name = .checkmarkAlt statusIcon.name = .checkmarkAlt
icon.color = VDSColor.paletteBlack statusIcon.color = VDSColor.paletteBlack
icon.surface = surface statusIcon.surface = surface
icon.isHidden = !isEnabled statusIcon.isHidden = !isEnabled
} else { } else {
icon.isHidden = true
successLabel.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(){ open override func updateHelperLabel(){
//remove first //remove first
helperLabel.removeFromSuperview() helperLabel.removeFromSuperview()
@ -273,12 +273,108 @@ 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>]()
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
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
}
//tooltip
tooltipModel = toolTipModel
}
/// Used to update any Accessibility properties. /// Used to update any Accessibility properties.
open override func updateAccessibility() { open override func updateAccessibility() {
super.updateAccessibility() super.updateAccessibility()
textField.accessibilityLabel = showError ? "error" : nil textField.accessibilityLabel = showError ? "error" : nil
if showError { if showError {
accessibilityElements = [titleLabel, textField, icon, errorLabel, helperLabel] accessibilityElements = [titleLabel, textField, statusIcon, errorLabel, helperLabel]
} else { } else {
accessibilityElements = [titleLabel, textField, helperLabel] accessibilityElements = [titleLabel, textField, helperLabel]
} }
@ -292,25 +388,40 @@ open class InputField: EntryFieldBase, UITextFieldDelegate {
} }
return super.resignFirstResponder() 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() } }
} }
extension InputField.FieldType { extension InputField.FieldType {
var width: CGFloat {
public var keyboardType: UIKeyboardType {
switch self { switch self {
case .inlineAction: case .number:
return 102 .numberPad
case .password:
return 62.0
case .creditCard:
return 288.0
case .tel: case .tel:
return 176.0 .phonePad
case .creditCard:
.numberPad
case .date: case .date:
return 114.0 .numberPad
case .securityCode: case .securityCode:
return 88.0 .numberPad
default: default:
return 40.0 .default
} }
} }
} }

View File

@ -0,0 +1,46 @@
//
// TextField.swift
// VDS
//
// Created by Matt Bruce on 5/1/24.
//
import Foundation
import UIKit
@objc(VDSTextField)
open class TextField: UITextField {
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 override func becomeFirstResponder() -> Bool {
let success = super.becomeFirstResponder()
if isSecureTextEntry, let text {
self.text?.removeAll()
insertText(text)
}
return success
}
}

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

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

View File

@ -79,22 +79,34 @@ open class TextArea: EntryFieldBase {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Public Properties // 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. /// Enum used to describe the the height of TextArea.
public enum Height: String, CaseIterable { public enum Height: String, CaseIterable {
case twoX = "2X" case twoX = "2X"
case fourX = "4X" case fourX = "4X"
case eightX = "8X" case eightX = "8X"
var containerVerticalPadding: CGFloat { VDSLayout.space3X * 2 }
var value: CGFloat { var value: CGFloat {
switch self { switch self {
case .twoX: case .twoX:
88 88 - containerVerticalPadding
case .fourX: case .fourX:
176 176 - containerVerticalPadding
case .eightX: case .eightX:
352 352 - containerVerticalPadding
} }
} }
} }
@ -102,24 +114,16 @@ open class TextArea: EntryFieldBase {
/// The text of this TextArea. /// The text of this TextArea.
private var _text: String? private var _text: String?
open var text: String? { open var text: String? {
get { _text } get { textView.text }
set { set {
if let newValue, newValue != _text { textView.text = newValue
_text = newValue
textView.text = newValue
value = newValue
}
setNeedsUpdate() setNeedsUpdate()
} }
} }
/// The value of this textField. /// The value of this textField.
open override var value: String? { open override var value: String? {
didSet { return textView.text
if text != value {
text = value
}
}
} }
/// UITextView shown in the TextArea. /// UITextView shown in the TextArea.
@ -127,6 +131,8 @@ open class TextArea: EntryFieldBase {
$0.translatesAutoresizingMaskIntoConstraints = false $0.translatesAutoresizingMaskIntoConstraints = false
$0.sizeToFit() $0.sizeToFit()
$0.isScrollEnabled = false $0.isScrollEnabled = false
$0.textContainerInset = .zero
$0.textContainer.lineFragmentPadding = 0
} }
open var maxLength: Int? { open var maxLength: Int? {
@ -135,15 +141,10 @@ open class TextArea: EntryFieldBase {
} }
didSet { 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 /// Color configuration for character counter's highlight background color
internal var highlightBackgroundColor = ControlColorConfiguration().with { internal var highlightBackgroundColor = ControlColorConfiguration().with {
$0.setSurfaceColors(VDSColor.backgroundPrimaryDark, VDSColor.backgroundPrimaryLight, forState: .normal) $0.setSurfaceColors(VDSColor.backgroundPrimaryDark, VDSColor.backgroundPrimaryLight, forState: .normal)
@ -161,8 +162,6 @@ open class TextArea: EntryFieldBase {
open override func setup() { open override func setup() {
super.setup() super.setup()
isAccessibilityElement = false isAccessibilityElement = false
validator = FormFieldValidator<TextArea>(field: self, rules: [.init(countRule)])
containerStackView.pinToSuperView(.uniform(VDSFormControls.spaceInset)) containerStackView.pinToSuperView(.uniform(VDSFormControls.spaceInset))
minWidthConstraint = containerView.widthAnchor.constraint(greaterThanOrEqualToConstant: containerSize.width) minWidthConstraint = containerView.widthAnchor.constraint(greaterThanOrEqualToConstant: containerSize.width)
minWidthConstraint?.isActive = true minWidthConstraint?.isActive = true
@ -172,14 +171,34 @@ open class TextArea: EntryFieldBase {
.pinLeading() .pinLeading()
.pinTrailingLessThanOrEqualTo(nil, 0, .defaultHigh) .pinTrailingLessThanOrEqualTo(nil, 0, .defaultHigh)
.pinBottom(0, .defaultHigh) .pinBottom(0, .defaultHigh)
textView.isScrollEnabled = true textView.isScrollEnabled = true
textView.autocorrectionType = .no 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 = textView.heightAnchor.constraint(greaterThanOrEqualToConstant: containerSize.height)
textViewHeightConstraint?.isActive = true textViewHeightConstraint?.isActive = true
backgroundColorConfiguration.setSurfaceColors(VDSColor.feedbackSuccessBackgroundOnlight, VDSColor.feedbackSuccessBackgroundOndark, forState: .success) backgroundColorConfiguration.setSurfaceColors(VDSColor.feedbackSuccessBackgroundOnlight, VDSColor.feedbackSuccessBackgroundOndark, forState: .success)
borderColorConfiguration.setSurfaceColors(VDSColor.feedbackSuccessOnlight, VDSColor.feedbackSuccessOndark, forState: .success) borderColorConfiguration.setSurfaceColors(VDSColor.feedbackSuccessOnlight, VDSColor.feedbackSuccessOndark, forState: .success)
borderColorConfiguration.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .focused) borderColorConfiguration.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .focused)
textView.delegate = self
characterCounterLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable() characterCounterLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable()
bottomContainerStackView.spacing = VDSLayout.space2X bottomContainerStackView.spacing = VDSLayout.space2X
} }
@ -219,16 +238,21 @@ open class TextArea: EntryFieldBase {
characterCounterLabel.text = getCharacterCounterText() characterCounterLabel.text = getCharacterCounterText()
icon.size = .medium statusIcon.color = iconColorConfiguration.getColor(self)
icon.color = iconColorConfiguration.getColor(self) containerView.layer.borderColor = isReadOnly ? readOnlyBorderColorConfiguration.getColor(self).cgColor : borderColorConfiguration.getColor(self).cgColor
containerView.layer.borderColor = readOnly ? readOnlyBorderColorConfiguration.getColor(self).cgColor : borderColorConfiguration.getColor(self).cgColor textView.isEditable = isReadOnly ? false : true
textView.isEditable = readOnly ? false : true
textView.backgroundColor = backgroundColorConfiguration.getColor(self) textView.backgroundColor = backgroundColorConfiguration.getColor(self)
textView.tintColor = iconColorConfiguration.getColor(self) textView.tintColor = iconColorConfiguration.getColor(self)
characterCounterLabel.surface = surface characterCounterLabel.surface = surface
highlightCharacterOverflow() highlightCharacterOverflow()
} }
override func updateRules() {
super.updateRules()
rules.append(.init(countRule))
}
/// Container for the area showing helper text, error text, character count, maximum length value. /// Container for the area showing helper text, error text, character count, maximum length value.
open override func getBottomContainer() -> UIView { open override func getBottomContainer() -> UIView {
bottomStackView.addArrangedSubview(bottomContainerView) bottomStackView.addArrangedSubview(bottomContainerView)
@ -241,7 +265,7 @@ open class TextArea: EntryFieldBase {
super.updateAccessibility() super.updateAccessibility()
textView.accessibilityLabel = showError ? "error" : nil textView.accessibilityLabel = showError ? "error" : nil
if showError { if showError {
accessibilityElements = [titleLabel, textView, icon, errorLabel, helperLabel] accessibilityElements = [titleLabel, textView, statusIcon, errorLabel, helperLabel]
} else { } else {
accessibilityElements = [titleLabel, textView, helperLabel] accessibilityElements = [titleLabel, textView, helperLabel]
} }
@ -273,43 +297,7 @@ open class TextArea: EntryFieldBase {
} }
} }
open func highlightCharacterOverflow() { func textViewDidChange(_ textView: UITextView) {
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) {
//dynamic textView Height sizing based on Figma //dynamic textView Height sizing based on Figma
//if you want it to work "as-is" delete this code //if you want it to work "as-is" delete this code
@ -347,5 +335,27 @@ extension TextArea: UITextViewDelegate {
text = textView.text text = textView.text
sendActions(for: .valueChanged) 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()
} }

View File

@ -108,27 +108,18 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
$0.clipsToBounds = true $0.clipsToBounds = true
} }
private var containerView = View().with {
$0.isUserInteractionEnabled = false
}
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Public Properties // 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. /// This takes an image source url and applies it as a background image.
open var backgroundImage: UIImage? { didSet { setNeedsUpdate() } } open var backgroundImage: UIImage? { didSet { setNeedsUpdate() } }
/// This is the container in which views will be pinned. /// This is the container in which views will be pinned.
open var containerView = View().with { open var contentView = View().with {
$0.isUserInteractionEnabled = false $0.isUserInteractionEnabled = false
} }
@ -232,6 +223,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. /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
open override func setup() { open override func setup() {
super.setup() super.setup()
isAccessibilityElement = false
let layoutGuide = UILayoutGuide() let layoutGuide = UILayoutGuide()
addLayoutGuide(layoutGuide) addLayoutGuide(layoutGuide)
@ -240,11 +232,13 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
.pinLeading() .pinLeading()
.pinTrailing(0, .defaultHigh) .pinTrailing(0, .defaultHigh)
.pinBottom(0, .defaultHigh) .pinBottom(0, .defaultHigh)
addSubview(backgroundImageView) addSubview(backgroundImageView)
addSubview(containerView) addSubview(containerView)
containerView.addSubview(contentView)
addSubview(highlightView) addSubview(highlightView)
containerView.pinToSuperView()
widthConstraint = layoutGuide.widthAnchor.constraint(equalToConstant: 0) widthConstraint = layoutGuide.widthAnchor.constraint(equalToConstant: 0)
heightGreaterThanConstraint = layoutGuide.heightAnchor.constraint(greaterThanOrEqualToConstant: 44.0) heightGreaterThanConstraint = layoutGuide.heightAnchor.constraint(greaterThanOrEqualToConstant: 44.0)
@ -261,10 +255,10 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
backgroundImageView.isUserInteractionEnabled = false backgroundImageView.isUserInteractionEnabled = false
backgroundImageView.isHidden = true backgroundImageView.isHidden = true
containerTopConstraint = containerView.pinTop(anchor: layoutGuide.topAnchor, constant: padding.value) containerTopConstraint = contentView.pinTop(anchor: layoutGuide.topAnchor, constant: padding.value)
containerBottomConstraint = layoutGuide.pinBottom(anchor: containerView.bottomAnchor, constant: padding.value) containerBottomConstraint = layoutGuide.pinBottom(anchor: contentView.bottomAnchor, constant: padding.value)
containerLeadingConstraint = containerView.pinLeading(anchor: layoutGuide.leadingAnchor, constant: padding.value) containerLeadingConstraint = contentView.pinLeading(anchor: layoutGuide.leadingAnchor, constant: padding.value)
containerTrailingConstraint = layoutGuide.pinTrailing(anchor: containerView.trailingAnchor, constant: padding.value) containerTrailingConstraint = layoutGuide.pinTrailing(anchor: contentView.trailingAnchor, constant: padding.value)
highlightView.pin(layoutGuide) highlightView.pin(layoutGuide)
highlightView.isHidden = true highlightView.isHidden = true
@ -339,6 +333,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 /// Used to update frames for the added CAlayers to our view
open override func layoutSubviews() { open override func layoutSubviews() {
super.layoutSubviews() super.layoutSubviews()
@ -353,7 +371,7 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
/// This will place a view within the contentView of this component. /// This will place a view within the contentView of this component.
public func addContentView(_ view: UIView, shouldPin: Bool = true) { public func addContentView(_ view: UIView, shouldPin: Bool = true) {
view.removeFromSuperview() view.removeFromSuperview()
containerView.addSubview(view) contentView.addSubview(view)
if shouldPin { if shouldPin {
view.pinToSuperView() view.pinToSuperView()
} }

View File

@ -201,10 +201,13 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
} }
/// Descriptive Icon positioned in the contentView. /// Descriptive Icon positioned in the contentView.
open var descriptiveIcon = Icon() open var descriptiveIcon = Icon().with {
$0.isAccessibilityElement = true
}
/// Directional Icon positioned in the contentView. /// Directional Icon positioned in the contentView.
open var directionalIcon = Icon().with { open var directionalIcon = Icon().with {
$0.isAccessibilityElement = true
$0.name = .rightArrow $0.name = .rightArrow
} }
@ -407,24 +410,30 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
} }
/// Used to update any Accessibility properties. /// Used to update any Accessibility properties.
open override func updateAccessibility() { open override var accessibilityElements: [Any]? {
super.updateAccessibility()
var elements = [Any]() get {
if badgeModel != nil { var elements = [Any]()
elements.append(badge) if let superElements = super.accessibilityElements {
} elements.append(contentsOf: superElements)
if titleModel != nil || subTitleModel != nil || eyebrowModel != nil { }
elements.append(titleLockup) if badgeModel != nil {
} elements.append(badge)
if descriptiveIconModel != nil { }
elements.append(descriptiveIcon) if titleModel != nil || subTitleModel != nil || eyebrowModel != nil {
} elements.append(titleLockup)
if directionalIconModel != nil { }
elements.append(directionalIcon) if descriptiveIconModel != nil {
} elements.append(descriptiveIcon)
accessibilityElements = elements.count > 0 ? elements : nil }
if directionalIconModel != nil {
elements.append(directionalIcon)
}
return elements
}
set {}
setAccessibilityLabel(for: [badge.label, titleLockup.eyebrowLabel, titleLockup.titleLabel, titleLockup.subTitleLabel])
} }
//-------------------------------------------------- //--------------------------------------------------
@ -482,7 +491,7 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
titleLockupWidthConstraint = NSLayoutConstraint(item: titleLockup, titleLockupWidthConstraint = NSLayoutConstraint(item: titleLockup,
attribute: .width, attribute: .width,
relatedBy: .equal, relatedBy: .equal,
toItem: containerView, toItem: contentView,
attribute: .width, attribute: .width,
multiplier: percentage / 100, multiplier: percentage / 100,
constant: 0.0) constant: 0.0)
@ -521,12 +530,14 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
descriptiveIcon.name = descriptiveIconModel.name descriptiveIcon.name = descriptiveIconModel.name
descriptiveIcon.size = descriptiveIconModel.size descriptiveIcon.size = descriptiveIconModel.size
descriptiveIcon.surface = descriptiveIconModel.surface descriptiveIcon.surface = descriptiveIconModel.surface
descriptiveIcon.accessibilityLabel = descriptiveIconModel.accessibleText
showIconContainerView = true showIconContainerView = true
} }
if let directionalIconModel { if let directionalIconModel {
directionalIcon.size = directionalIconModel.size directionalIcon.size = directionalIconModel.size
directionalIcon.surface = directionalIconModel.surface directionalIcon.surface = directionalIconModel.surface
directionalIcon.accessibilityLabel = "Right arrow"
showIconContainerView = true showIconContainerView = true
} }

View File

@ -18,11 +18,15 @@ extension Tilelet {
/// Enum for a preset height and width for the icon. /// Enum for a preset height and width for the icon.
public var size: Icon.Size 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 /// Current Surface and this is used to pass down to child objects that implement Surfacable
public var surface: Surface 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.name = name
self.accessibleText = accessibleText ?? name.rawValue
self.size = size self.size = size
self.surface = surface self.surface = surface
} }

View File

@ -145,7 +145,7 @@ open class Toggle: Control, Changeable, FormFieldable {
open var inputId: String? { didSet { setNeedsUpdate() } } 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. /// The natural size for the receiving view, considering only properties of the view itself.
open override var intrinsicContentSize: CGSize { open override var intrinsicContentSize: CGSize {
@ -224,7 +224,6 @@ open class Toggle: Control, Changeable, FormFieldable {
textWeight = .regular textWeight = .regular
textPosition = .left textPosition = .left
inputId = nil inputId = nil
value = nil
onChange = nil onChange = nil
shouldUpdateView = true shouldUpdateView = true
setNeedsUpdate() setNeedsUpdate()

View File

@ -68,7 +68,7 @@ open class ToggleView: Control, Changeable, FormFieldable {
open var inputId: String? { didSet { setNeedsUpdate() } } 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. /// The natural size for the receiving view, considering only properties of the view itself.
open override var intrinsicContentSize: CGSize { toggleSize } open override var intrinsicContentSize: CGSize { toggleSize }
@ -163,7 +163,6 @@ open class ToggleView: Control, Changeable, FormFieldable {
isOn = false isOn = false
isAnimated = true isAnimated = true
inputId = nil inputId = nil
value = nil
toggleView.backgroundColor = toggleColorConfiguration.getColor(self) toggleView.backgroundColor = toggleColorConfiguration.getColor(self)
knobView.backgroundColor = knobColorConfiguration.getColor(self) knobView.backgroundColor = knobColorConfiguration.getColor(self)
onChange = nil onChange = nil

View File

@ -29,6 +29,7 @@ extension Changeable {
} else { } else {
onChangeSubscriber = nil onChangeSubscriber = nil
} }
setNeedsUpdate()
} }
} }
} }

View File

@ -30,6 +30,7 @@ extension Clickable {
} else { } else {
onClickSubscriber = nil onClickSubscriber = nil
} }
setNeedsUpdate()
} }
} }
} }

View File

@ -15,15 +15,36 @@ public protocol FormFieldable {
var inputId: String? { get set } var inputId: String? { get set }
/// Value for the Form Field. /// Value for the Form Field.
var value: ValueType? { get set } var value: ValueType? { get }
} }
/// Protocol for FormFieldable that require internal validation. /// Protocol for FormFieldable that require internal validation.
public protocol FormFieldInternalValidatable: FormFieldable { public protocol FormFieldInternalValidatable: FormFieldable, Errorable {
/// Is there an internalError /// Is there an internalError
var hasInternalError: Bool { get } var hasInternalError: Bool { get }
/// Internal Error Message that will show. /// Internal Error Message that will show.
var internalErrorText: String? { get } 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. /// Struct that will execute the validation.

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

View File

@ -1,3 +1,15 @@
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 1.0.60
---------------- ----------------
- CXTDT-544442 - Button Icon - Selected state needs to allow custom color - CXTDT-544442 - Button Icon - Selected state needs to allow custom color