diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index 308c7f47..cd08241d 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -54,6 +54,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 */; }; @@ -243,6 +247,10 @@ EA21C5DA2B600EDD00CFC139 /* VDSTokens.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = VDSTokens.xcframework; path = ../SharedFrameworks/VDSTokens.xcframework; sourceTree = ""; }; EA297A5429FB07760031ED56 /* TooltipLabelAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TooltipLabelAttribute.swift; sourceTree = ""; }; EA297A5629FB0A360031ED56 /* AppleGuidelinesTouchable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleGuidelinesTouchable.swift; sourceTree = ""; }; + EA2DC9AF2BE175BA004F58C5 /* RequiredRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequiredRule.swift; sourceTree = ""; }; + EA2DC9B12BE175E6004F58C5 /* CharacterCountRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCountRule.swift; sourceTree = ""; }; + EA2DC9B32BE2C6FE004F58C5 /* TextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextField.swift; sourceTree = ""; }; + EA2DC9B52BE2F4A1004F58C5 /* UITextView+Publisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextView+Publisher.swift"; sourceTree = ""; }; 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 = ""; }; EA336170288B19200071C351 /* VDS.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = VDS.docc; sourceTree = ""; }; @@ -512,6 +520,15 @@ path = ButtonGroup; sourceTree = ""; }; + EA2DC9AE2BE175A6004F58C5 /* Rules */ = { + isa = PBXGroup; + children = ( + EA2DC9AF2BE175BA004F58C5 /* RequiredRule.swift */, + EA2DC9B12BE175E6004F58C5 /* CharacterCountRule.swift */, + ); + path = Rules; + sourceTree = ""; + }; EA336162288B19200071C351 = { isa = PBXGroup; children = ( @@ -835,6 +852,7 @@ children = ( EAB1D2E928AE84AA00DAE764 /* UIControlPublisher.swift */, EA89200328AECF4B006B9984 /* UITextField+Publisher.swift */, + EA2DC9B52BE2F4A1004F58C5 /* UITextView+Publisher.swift */, EAD8D2C028BFDE8B006EB6A6 /* UIGestureRecognizer+Publisher.swift */, ); path = Publishers; @@ -875,6 +893,7 @@ EAC925852911C9DE00091998 /* TextFields */ = { isa = PBXGroup; children = ( + EA2DC9AE2BE175A6004F58C5 /* Rules */, EAC9258B2911C9DE00091998 /* EntryFieldBase.swift */, EAC925862911C9DE00091998 /* InputField */, EA985C21296E032000F2FF2E /* TextArea */, @@ -886,6 +905,7 @@ isa = PBXGroup; children = ( EAC925872911C9DE00091998 /* InputField.swift */, + EA2DC9B32BE2C6FE004F58C5 /* TextField.swift */, EA6642942BCEBF9500D81DC4 /* TextLinkModel.swift */, ); path = InputField; @@ -1136,6 +1156,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 */, @@ -1145,10 +1166,12 @@ 71FC86DE2B9738B900700965 /* SurfaceConfigurationValue.swift in Sources */, EA297A5529FB07760031ED56 /* TooltipLabelAttribute.swift in Sources */, EA985BEA29689B6D00F2FF2E /* TileletSubTitleModel.swift in Sources */, + EA2DC9B02BE175BA004F58C5 /* RequiredRule.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 */, @@ -1223,6 +1246,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 */, @@ -1377,7 +1401,7 @@ BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 60; + CURRENT_PROJECT_VERSION = 61; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1414,7 +1438,7 @@ BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 60; + CURRENT_PROJECT_VERSION = 61; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; diff --git a/VDS/BaseClasses/Selector/SelectorGroupBase.swift b/VDS/BaseClasses/Selector/SelectorGroupBase.swift index 997a71b2..1639ab86 100644 --- a/VDS/BaseClasses/Selector/SelectorGroupBase.swift +++ b/VDS/BaseClasses/Selector/SelectorGroupBase.swift @@ -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: Control, SelectorGroup, Changeable { +open class SelectorGroupBase: Control, SelectorGroup, Changeable { //-------------------------------------------------- // MARK: - Private Properties diff --git a/VDS/BaseClasses/Selector/SelectorItemBase.swift b/VDS/BaseClasses/Selector/SelectorItemBase.swift index 495ffaf3..3dd31dda 100644 --- a/VDS/BaseClasses/Selector/SelectorItemBase.swift +++ b/VDS/BaseClasses/Selector/SelectorItemBase.swift @@ -11,7 +11,7 @@ import Combine import VDSTokens /// Base Class used to build out a SelectorControlable control. -open class SelectorItemBase: Control, Errorable, Changeable, FormFieldable { +open class SelectorItemBase: Control, Errorable, Changeable { //-------------------------------------------------- // MARK: - Initializers @@ -141,8 +141,10 @@ open class SelectorItemBase: 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: Control, Errorable, showError = false errorText = nil inputId = nil - value = nil isSelected = false onChange = nil diff --git a/VDS/Classes/SelfSizingCollectionView.swift b/VDS/Classes/SelfSizingCollectionView.swift index ad5d5661..ff1f9cb8 100644 --- a/VDS/Classes/SelfSizingCollectionView.swift +++ b/VDS/Classes/SelfSizingCollectionView.swift @@ -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 { diff --git a/VDS/Components/Breadcrumbs/BreadcrumbItem.swift b/VDS/Components/Breadcrumbs/BreadcrumbItem.swift index a7b9bef4..f12d9646 100644 --- a/VDS/Components/Breadcrumbs/BreadcrumbItem.swift +++ b/VDS/Components/Breadcrumbs/BreadcrumbItem.swift @@ -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 } } diff --git a/VDS/Components/Breadcrumbs/Breadcrumbs.swift b/VDS/Components/Breadcrumbs/Breadcrumbs.swift index 63084192..7c88e18a 100644 --- a/VDS/Components/Breadcrumbs/Breadcrumbs.swift +++ b/VDS/Components/Breadcrumbs/Breadcrumbs.swift @@ -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 { diff --git a/VDS/Components/Checkbox/CheckboxGroup.swift b/VDS/Components/Checkbox/CheckboxGroup.swift index c7e424a1..1df8d9cd 100644 --- a/VDS/Components/Checkbox/CheckboxGroup.swift +++ b/VDS/Components/Checkbox/CheckboxGroup.swift @@ -14,6 +14,7 @@ import VDSTokens /// to allow user selection. @objc(VDSCheckboxGroup) open class CheckboxGroup: SelectorGroupBase, SelectorGroupMultiSelect { + //-------------------------------------------------- // MARK: - Initializers //-------------------------------------------------- @@ -32,6 +33,10 @@ open class CheckboxGroup: SelectorGroupBase, 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, 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, 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 diff --git a/VDS/Components/DropdownSelect/DropdownSelect.swift b/VDS/Components/DropdownSelect/DropdownSelect.swift index 11550937..e86b3792 100644 --- a/VDS/Components/DropdownSelect/DropdownSelect.swift +++ b/VDS/Components/DropdownSelect/DropdownSelect.swift @@ -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 @@ -163,7 +162,7 @@ open class DropdownSelect: EntryFieldBase { updateInlineLabel() - dropdownField.isUserInteractionEnabled = readOnly ? false : true + dropdownField.isUserInteractionEnabled = isReadOnly ? false : true selectedOptionLabel.surface = surface selectedOptionLabel.isEnabled = isEnabled } @@ -191,7 +190,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,17 +233,16 @@ 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) } @objc open func pickerDoneClicked() { @@ -284,6 +282,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]) } } diff --git a/VDS/Components/Icon/ButtonIcon/ButtonIcon.swift b/VDS/Components/Icon/ButtonIcon/ButtonIcon.swift index 658cb05e..b97a1fac 100644 --- a/VDS/Components/Icon/ButtonIcon/ButtonIcon.swift +++ b/VDS/Components/Icon/ButtonIcon/ButtonIcon.swift @@ -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 //-------------------------------------------------- diff --git a/VDS/Components/Label/Attributes/TooltipLabelAttribute.swift b/VDS/Components/Label/Attributes/TooltipLabelAttribute.swift index 187d4eda..20666b10 100644 --- a/VDS/Components/Label/Attributes/TooltipLabelAttribute.swift +++ b/VDS/Components/Label/Attributes/TooltipLabelAttribute.swift @@ -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) } } diff --git a/VDS/Components/Notification/Notification.swift b/VDS/Components/Notification/Notification.swift index 241efb34..bd4df763 100644 --- a/VDS/Components/Notification/Notification.swift +++ b/VDS/Components/Notification/Notification.swift @@ -52,7 +52,7 @@ open class Notification: View { } } - var accessibilityText: String { + var accessibleText: String { switch self { case .info: "Information Message" @@ -374,7 +374,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() { diff --git a/VDS/Components/Pagination/Pagination.swift b/VDS/Components/Pagination/Pagination.swift index f5a4e2c7..d3c0298a 100644 --- a/VDS/Components/Pagination/Pagination.swift +++ b/VDS/Components/Pagination/Pagination.swift @@ -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 diff --git a/VDS/Components/RadioBox/RadioBoxGroup.swift b/VDS/Components/RadioBox/RadioBoxGroup.swift index 18d3c26b..e284840b 100644 --- a/VDS/Components/RadioBox/RadioBoxGroup.swift +++ b/VDS/Components/RadioBox/RadioBoxGroup.swift @@ -32,6 +32,10 @@ open class RadioBoxGroup: SelectorGroupBase, 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, 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 diff --git a/VDS/Components/RadioBox/RadioBoxItem.swift b/VDS/Components/RadioBox/RadioBoxItem.swift index 23dd2b41..1a9d613f 100644 --- a/VDS/Components/RadioBox/RadioBoxItem.swift +++ b/VDS/Components/RadioBox/RadioBoxItem.swift @@ -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 diff --git a/VDS/Components/RadioButton/RadioButtonGroup.swift b/VDS/Components/RadioButton/RadioButtonGroup.swift index 6b12f60a..aecb8034 100644 --- a/VDS/Components/RadioButton/RadioButtonGroup.swift +++ b/VDS/Components/RadioButton/RadioButtonGroup.swift @@ -32,6 +32,10 @@ open class RadioButtonGroup: SelectorGroupBase, 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, 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, SelectorGroupSi setNeedsUpdate() } } - + private var _showError: Bool = false /// Whether not to show the error. diff --git a/VDS/Components/TextFields/EntryFieldBase.swift b/VDS/Components/TextFields/EntryFieldBase.swift index a8ad917a..644a8de1 100644 --- a/VDS/Components/TextFields/EntryFieldBase.swift +++ b/VDS/Components/TextFields/EntryFieldBase.swift @@ -83,6 +83,8 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { } }() + open var rules = [AnyRule]() + //-------------------------------------------------- // 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 @@ -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(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(){ diff --git a/VDS/Components/TextFields/InputField/InputField.swift b/VDS/Components/TextFields/InputField/InputField.swift index e34ee01c..640538fe 100644 --- a/VDS/Components/TextFields/InputField/InputField.swift +++ b/VDS/Components/TextFields/InputField/InputField.swift @@ -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 } } @@ -156,22 +153,40 @@ open class InputField: EntryFieldBase, UITextFieldDelegate { 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 .textPublisher - .sink { [weak self] text in - self?.value = text + .sink { [weak self] newText in + print("textPublisher newText: \(newText)") + self?.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,13 +272,109 @@ 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]() + + 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. open override func updateAccessibility() { super.updateAccessibility() textField.accessibilityLabel = showError ? "error" : nil if showError { - accessibilityElements = [titleLabel, textField, icon, errorLabel, helperLabel] + accessibilityElements = [titleLabel, textField, statusIcon, errorLabel, helperLabel] } else { accessibilityElements = [titleLabel, textField, helperLabel] } @@ -292,25 +388,40 @@ 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() } } } extension InputField.FieldType { - var width: CGFloat { + + public var keyboardType: UIKeyboardType { switch self { - case .inlineAction: - return 102 - case .password: - return 62.0 - case .creditCard: - return 288.0 + case .number: + .numberPad case .tel: - return 176.0 + .phonePad + case .creditCard: + .numberPad case .date: - return 114.0 + .numberPad case .securityCode: - return 88.0 + .numberPad default: - return 40.0 + .default } } } diff --git a/VDS/Components/TextFields/InputField/TextField.swift b/VDS/Components/TextFields/InputField/TextField.swift new file mode 100644 index 00000000..14178020 --- /dev/null +++ b/VDS/Components/TextFields/InputField/TextField.swift @@ -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 + } +} diff --git a/VDS/Components/TextFields/InputField/TextLinkModel.swift b/VDS/Components/TextFields/InputField/TextLinkModel.swift index d5d8c7ba..89235e5e 100644 --- a/VDS/Components/TextFields/InputField/TextLinkModel.swift +++ b/VDS/Components/TextFields/InputField/TextLinkModel.swift @@ -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)? diff --git a/VDS/Components/TextFields/Rules/CharacterCountRule.swift b/VDS/Components/TextFields/Rules/CharacterCountRule.swift new file mode 100644 index 00000000..f26ccafc --- /dev/null +++ b/VDS/Components/TextFields/Rules/CharacterCountRule.swift @@ -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 + } +} diff --git a/VDS/Components/TextFields/Rules/RequiredRule.swift b/VDS/Components/TextFields/Rules/RequiredRule.swift new file mode 100644 index 00000000..df00bc59 --- /dev/null +++ b/VDS/Components/TextFields/Rules/RequiredRule.swift @@ -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 + } +} + diff --git a/VDS/Components/TextFields/TextArea/TextArea.swift b/VDS/Components/TextFields/TextArea/TextArea.swift index d057333b..7229c4a9 100644 --- a/VDS/Components/TextFields/TextArea/TextArea.swift +++ b/VDS/Components/TextFields/TextArea/TextArea.swift @@ -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) @@ -161,8 +162,6 @@ open class TextArea: EntryFieldBase { open override func setup() { super.setup() isAccessibilityElement = false - validator = FormFieldValidator