diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index 59d6d763..d4fdd413 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -66,6 +66,7 @@ EAA5EEB528ECBFB4003B3210 /* ImageLabelAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA5EEB428ECBFB4003B3210 /* ImageLabelAttribute.swift */; }; EAA5EEB728ECC03A003B3210 /* ToolTipLabelAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA5EEB628ECC03A003B3210 /* ToolTipLabelAttribute.swift */; }; EAA5EEB928ECD24B003B3210 /* Icons.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EAA5EEB828ECD24B003B3210 /* Icons.xcassets */; }; + EAA5EEE028F49DB3003B3210 /* Colorable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA5EEDF28F49DB3003B3210 /* Colorable.swift */; }; EAB1D29A28A5611D00DAE764 /* SelectorGroupModelable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB1D29928A5611D00DAE764 /* SelectorGroupModelable.swift */; }; EAB1D29C28A5618900DAE764 /* RadioButtonGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB1D29B28A5618900DAE764 /* RadioButtonGroup.swift */; }; EAB1D29E28A5619500DAE764 /* RadioButtonGroupModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB1D29D28A5619500DAE764 /* RadioButtonGroupModel.swift */; }; @@ -175,6 +176,7 @@ EAA5EEB428ECBFB4003B3210 /* ImageLabelAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageLabelAttribute.swift; sourceTree = ""; }; EAA5EEB628ECC03A003B3210 /* ToolTipLabelAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolTipLabelAttribute.swift; sourceTree = ""; }; EAA5EEB828ECD24B003B3210 /* Icons.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Icons.xcassets; sourceTree = ""; }; + EAA5EEDF28F49DB3003B3210 /* Colorable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Colorable.swift; sourceTree = ""; }; EAB1D29928A5611D00DAE764 /* SelectorGroupModelable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectorGroupModelable.swift; sourceTree = ""; }; EAB1D29B28A5618900DAE764 /* RadioButtonGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButtonGroup.swift; sourceTree = ""; }; EAB1D29D28A5619500DAE764 /* RadioButtonGroupModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButtonGroupModel.swift; sourceTree = ""; }; @@ -356,6 +358,7 @@ children = ( EA3361C4289030FC0071C351 /* Accessable.swift */, EA4DB2FC28D3D0CA00103EE3 /* AnyEquatable.swift */, + EAA5EEDF28F49DB3003B3210 /* Colorable.swift */, EA3361AC288B26190071C351 /* DataTrackable.swift */, EA3361A9288B25E40071C351 /* Disabling.swift */, EAF7F0A1289AFB3900B287F5 /* Errorable.swift */, @@ -677,6 +680,7 @@ EAA5EEB328EB6A66003B3210 /* TextEntryField.swift in Sources */, EA89201328B568D8006B9984 /* RadioBox.swift in Sources */, EA84F6B128B94A2500D67ABC /* CodableColor.swift in Sources */, + EAA5EEE028F49DB3003B3210 /* Colorable.swift in Sources */, EA3362402892EF6C0071C351 /* Label.swift in Sources */, EAF7F0B3289B1ADC00B287F5 /* ActionLabelAttribute.swift in Sources */, EA33622E2891EA3C0071C351 /* DispatchQueue+Once.swift in Sources */, diff --git a/VDS/Components/TextFields/EntryField/EntryField.swift b/VDS/Components/TextFields/EntryField/EntryField.swift index bae1fc76..bb2f6108 100644 --- a/VDS/Components/TextFields/EntryField/EntryField.swift +++ b/VDS/Components/TextFields/EntryField/EntryField.swift @@ -16,27 +16,25 @@ open class EntryField: Control { //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- - private var stackView: UIStackView = { + internal var stackView: UIStackView = { return UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .vertical $0.distribution = .fill } }() + + internal var titleLabel = Label().with { + $0.setContentCompressionResistancePriority(.required, for: .vertical) + } - private var containerStackView: UIStackView = { - return UIStackView().with { - $0.translatesAutoresizingMaskIntoConstraints = false - $0.axis = .horizontal - $0.distribution = .fill - $0.spacing = 12 - } - }() + internal var errorLabel = Label().with { + $0.setContentCompressionResistancePriority(.required, for: .vertical) + } - private var titleLabel = Label() - private var errorLabel = Label() - private var helperLabel = Label() - private var successLabel = Label() + internal var helperLabel = Label().with { + $0.setContentCompressionResistancePriority(.required, for: .vertical) + } internal var containerView: UIView = { return UIView().with { @@ -44,11 +42,13 @@ open class EntryField: Control { } }() + internal var tooltipView: UIView? + //-------------------------------------------------- // MARK: - Configuration Properties //-------------------------------------------------- // Sizes are from InVision design specs. - public let containerSize = CGSize(width: 45, height: 45) + internal let containerSize = CGSize(width: 45, height: 45) internal let primaryColorConfig = DisabledSurfaceColorConfiguration().with { $0.disabled.lightColor = VDSColor.interactiveDisabledOnlight @@ -64,38 +64,14 @@ open class EntryField: Control { $0.enabled.darkColor = VDSColor.elementsSecondaryOndark } - internal var backgroundColorConfiguration: EntryFieldColorConfiguration = { - return EntryFieldColorConfiguration().with { - $0.enabled.lightColor = VDSFormControlsColor.backgroundOnlight - $0.enabled.darkColor = VDSFormControlsColor.backgroundOndark - $0.disabled.lightColor = VDSFormControlsColor.backgroundOnlight - $0.disabled.darkColor = VDSFormControlsColor.backgroundOndark - - //error/success doesn't care enabled/disable - $0.error.lightColor = VDSColor.feedbackErrorBackgroundOnlight - $0.error.darkColor = VDSColor.feedbackErrorBackgroundOndark - $0.success.lightColor = VDSColor.feedbackSuccessBackgroundOnlight - $0.success.darkColor = VDSColor.feedbackSuccessBackgroundOndark - - } - }() - - internal var borderColorConfiguration: EntryFieldColorConfiguration = { - return EntryFieldColorConfiguration().with { - $0.enabled.lightColor = VDSFormControlsColor.borderOnlight - $0.enabled.darkColor = VDSFormControlsColor.borderOnlight - $0.disabled.lightColor = VDSColor.interactiveDisabledOnlight - $0.disabled.darkColor = VDSColor.interactiveDisabledOndark - - //error/success doesn't care enabled/disable - $0.error.lightColor = VDSColor.feedbackErrorOnlight - $0.error.darkColor = VDSColor.feedbackErrorOndark - $0.success.lightColor = VDSColor.feedbackSuccessOnlight - $0.success.darkColor = VDSColor.feedbackSuccessOndark - } + internal lazy var backgroundColorConfiguration: AnyColorable = { + return getBackgroundConfig() + }() + + internal lazy var borderColorConfiguration: AnyColorable = { + return getBorderConfig() }() - //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- @@ -110,16 +86,7 @@ open class EntryField: Control { @Proxy(\.model.errorText) open var errorText: String? - - @Proxy(\.model.showSuccess) - open var showSuccess: Bool - - @Proxy(\.model.successText) - open var successText: String? - - @Proxy(\.model.helperTextPlacement) - open var helperTextPlacement: HelperTextPlacement - + @Proxy(\.model.tooltipTitle) open var tooltipTitle: String? @@ -156,8 +123,7 @@ open class EntryField: Control { //-------------------------------------------------- internal var heightConstraint: NSLayoutConstraint? internal var widthConstraint: NSLayoutConstraint? - internal var minWidthConstraint: NSLayoutConstraint? - + //-------------------------------------------------- // MARK: - Lifecycle //-------------------------------------------------- @@ -178,30 +144,88 @@ open class EntryField: Control { heightConstraint?.isActive = true widthConstraint = containerView.widthAnchor.constraint(equalToConstant: 0) - - minWidthConstraint = containerView.widthAnchor.constraint(greaterThanOrEqualToConstant: 0) - minWidthConstraint?.isActive = true - - containerStackView.addArrangedSubview(containerView) + + let container = getContainer() stackView.addArrangedSubview(titleLabel) - stackView.addArrangedSubview(containerStackView) + stackView.addArrangedSubview(container) stackView.addArrangedSubview(errorLabel) - stackView.addArrangedSubview(successLabel) - + stackView.addArrangedSubview(helperLabel) + stackView.setCustomSpacing(4, after: titleLabel) - stackView.setCustomSpacing(8, after: containerStackView) + stackView.setCustomSpacing(8, after: container) stackView.setCustomSpacing(8, after: errorLabel) - stackView.setCustomSpacing(8, after: successLabel) - + stackView.topAnchor.constraint(equalTo: topAnchor).isActive = true stackView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true stackView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true stackView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true + + titleLabel.textColorConfiguration = primaryColorConfig.eraseToAnyColorable() + errorLabel.textColorConfiguration = primaryColorConfig.eraseToAnyColorable() + helperLabel.textColorConfiguration = secondaryColorConfig.eraseToAnyColorable() + } + + open func getContainer() -> UIView { + return containerView + } + + open func getBackgroundConfig() -> AnyColorable { + return EntryFieldColorConfiguration().with { + $0.enabled.lightColor = VDSFormControlsColor.backgroundOnlight + $0.enabled.darkColor = VDSFormControlsColor.backgroundOndark + $0.disabled.lightColor = VDSFormControlsColor.backgroundOnlight + $0.disabled.darkColor = VDSFormControlsColor.backgroundOndark + + //error doesn't care enabled/disable + $0.error.lightColor = VDSColor.feedbackErrorBackgroundOnlight + $0.error.darkColor = VDSColor.feedbackErrorBackgroundOndark + }.eraseToAnyColorable() + } + + open func getBorderConfig() -> AnyColorable { + return EntryFieldColorConfiguration().with { + $0.enabled.lightColor = VDSFormControlsColor.borderOnlight + $0.enabled.darkColor = VDSFormControlsColor.borderOnlight + $0.disabled.lightColor = VDSColor.interactiveDisabledOnlight + $0.disabled.darkColor = VDSColor.interactiveDisabledOndark + + //error doesn't care enabled/disable + $0.error.lightColor = VDSColor.feedbackErrorOnlight + $0.error.darkColor = VDSColor.feedbackErrorOndark + }.eraseToAnyColorable() + } + + open func getToolTipView(viewModel: ModelType) -> UIView? { + guard let toolTipTitleModel = viewModel.tooltipTitleModel, let toolTipContentModel = viewModel.tooltipContentModel else { + return nil + } + let stack = UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.axis = .vertical + $0.distribution = .fill + $0.spacing = 4 + } + + let title = Label().with { + $0.setContentCompressionResistancePriority(.required, for: .vertical) + } + title.set(with: toolTipTitleModel) + + let content = Label().with { + $0.setContentCompressionResistancePriority(.required, for: .vertical) + } + content.set(with: toolTipContentModel) + + stack.addArrangedSubview(title) + stack.addArrangedSubview(content) + + stack.backgroundColor = backgroundColorConfiguration.getColor(viewModel) + + return stack + } + + open func showToolTipView(){ - titleLabel.textColorConfiguration = primaryColorConfig - successLabel.textColorConfiguration = primaryColorConfig - errorLabel.textColorConfiguration = primaryColorConfig - helperLabel.textColorConfiguration = secondaryColorConfig } public override func reset() { @@ -214,55 +238,58 @@ open class EntryField: Control { //-------------------------------------------------- open override func updateView(viewModel: ModelType) { + containerView.backgroundColor = backgroundColorConfiguration.getColor(viewModel) + containerView.layer.borderColor = borderColorConfiguration.getColor(viewModel).cgColor + containerView.layer.borderWidth = 1 + containerView.layer.cornerRadius = 4 + + updateTitleLabel(viewModel: viewModel) + updateErrorLabel(viewModel: viewModel) + updateHelperLabel(viewModel: viewModel) + + setAccessibilityHint() + backgroundColor = viewModel.surface.color + setNeedsLayout() + layoutIfNeeded() + } + + open func updateTitleLabel(viewModel: ModelType) { //update the title model if the required flag is false var titleLabelModel = viewModel.labelModel .addOptional(required: viewModel.required, colorConfiguration: secondaryColorConfig) //tooltip action - if let toolTipTitle = viewModel.tooltipTitle, let toolTipContent = viewModel.tooltipContent { + if let view = getToolTipView(viewModel: viewModel) { + tooltipView = view let toolTipAction = PassthroughSubject() titleLabelModel = titleLabelModel.addToolTip(action: toolTipAction, colorConfiguration: primaryColorConfig) - toolTipAction.sink { - print("clicked Tool Tip: \rtitle:\(toolTipTitle)\rcontent:\(toolTipContent)") + toolTipAction.sink { [weak self] in + self?.showToolTipView() }.store(in: &subscribers) + } else { + tooltipView = nil } titleLabel.set(with: titleLabelModel) - - //show error or success + } + + open func updateErrorLabel(viewModel: ModelType){ if viewModel.showError, let errorLabelModel = viewModel.errorLabelModel { errorLabel.set(with: errorLabelModel) errorLabel.isHidden = false - successLabel.isHidden = true - } else if viewModel.showSuccess, let successLabelModel = viewModel.successLabelModel { - successLabel.set(with: successLabelModel) - errorLabel.isHidden = true - successLabel.isHidden = false } else { errorLabel.isHidden = true - successLabel.isHidden = true } - + } + + open func updateHelperLabel(viewModel: ModelType){ //set the helper label position if let helperLabelModel = viewModel.helperLabelModel { - helperLabel.removeFromSuperview() - if viewModel.helperTextPlacement == .right { - containerStackView.spacing = 12 - containerStackView.addArrangedSubview(helperLabel) - } else { - containerStackView.spacing = 0 - stackView.addArrangedSubview(helperLabel) - } helperLabel.set(with: helperLabelModel) helperLabel.isHidden = false } else { helperLabel.isHidden = true } - - setAccessibilityHint(!viewModel.disabled) - backgroundColor = viewModel.surface.color - setNeedsLayout() - layoutIfNeeded() } //-------------------------------------------------- @@ -270,19 +297,13 @@ open class EntryField: Control { //-------------------------------------------------- internal class EntryFieldColorConfiguration: DisabledSurfaceColorConfiguration { public let error = SurfaceColorConfiguration() - public let success = SurfaceColorConfiguration() override func getColor(_ viewModel: ModelType) -> UIColor { //only show error is enabled and showError == true let showErrorColor = !viewModel.disabled && viewModel.showError - let showSuccessColor = !viewModel.disabled && viewModel.showSuccess if showErrorColor { return error.getColor(viewModel) - - } else if showSuccessColor { - return success.getColor(viewModel) - } else { return super.getColor(viewModel) } @@ -307,7 +328,7 @@ extension DefaultLabelModel { public func addToolTip(action: PassthroughSubject, colorConfiguration: DisabledSurfaceColorConfiguration) -> DefaultLabelModel { guard let text = text else { return self} var newAttributes = attributes ?? [] - let newText = "\(text) " + let newText = "\(text) " //create a little space between the final character and tooltip image let toolTip = ToolTipLabelAttribute(action: action, location: newText.count, length: 1, diff --git a/VDS/Components/TextFields/EntryField/EntryFieldModel.swift b/VDS/Components/TextFields/EntryField/EntryFieldModel.swift index 86d80427..746ffda4 100644 --- a/VDS/Components/TextFields/EntryField/EntryFieldModel.swift +++ b/VDS/Components/TextFields/EntryField/EntryFieldModel.swift @@ -18,8 +18,6 @@ public protocol EntryFieldModel: Modelable, FormFieldable, Errorable { var labelText: String? { get set } var helperText: String? { get set } var helperTextPlacement: HelperTextPlacement { get set } - var showSuccess: Bool { get set } - var successText: String? { get set } var transparentBackground: Bool { get set } var width: CGFloat? { get set } var maxLength: Int? { get set } @@ -40,6 +38,7 @@ extension EntryFieldModel { } public var helperLabelModel: DefaultLabelModel? { + guard let helperText else { return nil } var model = DefaultLabelModel() model.textPosition = .left model.typograpicalStyle = .BodySmall @@ -50,6 +49,7 @@ extension EntryFieldModel { } public var errorLabelModel: DefaultLabelModel? { + guard let errorText else { return nil } var model = DefaultLabelModel() model.textPosition = .left model.typograpicalStyle = .BodySmall @@ -59,13 +59,26 @@ extension EntryFieldModel { return model } - public var successLabelModel: DefaultLabelModel? { + public var tooltipTitleModel: DefaultLabelModel? { + guard let tooltipTitle else { return nil } var model = DefaultLabelModel() model.textPosition = .left model.typograpicalStyle = .BodySmall - model.text = successText + model.text = tooltipTitle model.surface = surface model.disabled = disabled return model } + + public var tooltipContentModel: DefaultLabelModel? { + guard let tooltipContent else { return nil } + var model = DefaultLabelModel() + model.textPosition = .left + model.typograpicalStyle = .BodySmall + model.text = tooltipContent + model.surface = surface + model.disabled = disabled + return model + } + } diff --git a/VDS/Components/TextFields/TextEntryField/TextEntryField.swift b/VDS/Components/TextFields/TextEntryField/TextEntryField.swift index f2065977..5e337a1d 100644 --- a/VDS/Components/TextFields/TextEntryField/TextEntryField.swift +++ b/VDS/Components/TextFields/TextEntryField/TextEntryField.swift @@ -8,12 +8,25 @@ import Foundation import UIKit import VDSColorTokens +import VDSFormControlsTokens import Combine public class TextEntryField: TextEntryFieldBase{} open class TextEntryFieldBase: EntryField { + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- + internal var containerStackView: UIStackView = { + return UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.axis = .horizontal + $0.distribution = .fill + $0.spacing = 12 + } + }() + //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- @@ -21,16 +34,92 @@ open class TextEntryFieldBase: EntryField UIView { + containerStackView.addArrangedSubview(containerView) + return containerStackView + } + + open override func getBackgroundConfig() -> AnyColorable { + return TextEntryFieldColorConfiguration().with { + $0.enabled.lightColor = VDSFormControlsColor.backgroundOnlight + $0.enabled.darkColor = VDSFormControlsColor.backgroundOndark + $0.disabled.lightColor = VDSFormControlsColor.backgroundOnlight + $0.disabled.darkColor = VDSFormControlsColor.backgroundOndark + + //error/success doesn't care enabled/disable + $0.error.lightColor = VDSColor.feedbackErrorBackgroundOnlight + $0.error.darkColor = VDSColor.feedbackErrorBackgroundOndark + $0.success.lightColor = VDSColor.feedbackSuccessBackgroundOnlight + $0.success.darkColor = VDSColor.feedbackSuccessBackgroundOndark + }.eraseToAnyColorable() + } + + open override func getBorderConfig() -> AnyColorable { + return TextEntryFieldColorConfiguration().with { + $0.enabled.lightColor = VDSFormControlsColor.borderOnlight + $0.enabled.darkColor = VDSFormControlsColor.borderOnlight + $0.disabled.lightColor = VDSColor.interactiveDisabledOnlight + $0.disabled.darkColor = VDSColor.interactiveDisabledOndark + + //error/success doesn't care enabled/disable + $0.error.lightColor = VDSColor.feedbackErrorOnlight + $0.error.darkColor = VDSColor.feedbackErrorOndark + $0.success.lightColor = VDSColor.feedbackSuccessOnlight + $0.success.darkColor = VDSColor.feedbackSuccessOndark + }.eraseToAnyColorable() + } //-------------------------------------------------- // MARK: - State //-------------------------------------------------- open override func updateView(viewModel: ModelType) { super.updateView(viewModel: viewModel) - containerView.backgroundColor = backgroundColorConfiguration.getColor(viewModel) - containerView.layer.borderColor = borderColorConfiguration.getColor(viewModel).cgColor - containerView.layer.borderWidth = 1 - containerView.layer.cornerRadius = 4 + + //show error or success + if viewModel.showError, let _ = viewModel.errorLabelModel { + successLabel.isHidden = true + + } else if viewModel.showSuccess, let successLabelModel = viewModel.successLabelModel { + successLabel.set(with: successLabelModel) + successLabel.isHidden = false + errorLabel.isHidden = true + + } else { + successLabel.isHidden = true + + } + //set the width constraints if let width = viewModel.width, width > viewModel.type.width { widthConstraint?.constant = width @@ -42,6 +131,47 @@ open class TextEntryFieldBase: EntryField() + + override func getColor(_ viewModel: ModelType) -> UIColor { + //only show error is enabled and showError == true + let showErrorColor = !viewModel.disabled && viewModel.showError + let showSuccessColor = !viewModel.disabled && viewModel.showSuccess + + if showErrorColor { + return error.getColor(viewModel) + + } else if showSuccessColor { + return success.getColor(viewModel) + + } else { + return super.getColor(viewModel) + } + } + } } extension TextEntryFieldType { diff --git a/VDS/Components/TextFields/TextEntryField/TextEntryFieldModel.swift b/VDS/Components/TextFields/TextEntryField/TextEntryFieldModel.swift index 4b310b4a..faa05579 100644 --- a/VDS/Components/TextFields/TextEntryField/TextEntryFieldModel.swift +++ b/VDS/Components/TextFields/TextEntryField/TextEntryFieldModel.swift @@ -13,6 +13,22 @@ public enum TextEntryFieldType: String, CaseIterable { public protocol TextEntryFieldModel: EntryFieldModel { var type: TextEntryFieldType { get set } + var showSuccess: Bool { get set } + var successText: String? { get set } +} + +extension TextEntryFieldModel { + + public var successLabelModel: DefaultLabelModel? { + var model = DefaultLabelModel() + model.textPosition = .left + model.typograpicalStyle = .BodySmall + model.text = successText + model.surface = surface + model.disabled = disabled + return model + } + }