// // EntryField.swift // VDS // // Created by Matt Bruce on 10/3/22. // import Foundation import UIKit import VDSColorTokens import VDSFormControlsTokens import Combine public enum HelperTextPlacement: String, CaseIterable { case bottom, right } @objc(VDSEntryField) open class EntryField: Control, Accessable { //-------------------------------------------------- // MARK: - Initializers //-------------------------------------------------- required public init() { super.init(frame: .zero) } public override init(frame: CGRect) { super.init(frame: .zero) } public required init?(coder: NSCoder) { super.init(coder: coder) } //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- 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) $0.attributes = [] $0.textPosition = .left $0.typograpicalStyle = .BodySmall } internal var errorLabel = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) $0.textPosition = .left $0.typograpicalStyle = .BodySmall } internal var helperLabel = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) $0.textPosition = .left $0.typograpicalStyle = .BodySmall } internal var containerView: UIView = { return UIView().with { $0.translatesAutoresizingMaskIntoConstraints = false } }() internal var tooltipView: UIView? //-------------------------------------------------- // MARK: - Configuration Properties //-------------------------------------------------- // Sizes are from InVision design specs. internal let containerSize = CGSize(width: 45, height: 45) internal let primaryColorConfig = ViewColorConfiguration().with { $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forDisabled: true) $0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forDisabled: false) } internal let secondaryColorConfig = ViewColorConfiguration().with { $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forDisabled: true) $0.setSurfaceColors(VDSColor.elementsSecondaryOnlight, VDSColor.elementsSecondaryOndark, forDisabled: false) } internal var backgroundColorConfiguration = ControlColorConfiguration().with { $0.setSurfaceColors(VDSFormControlsColor.backgroundOnlight, VDSFormControlsColor.backgroundOndark, forState: .normal) $0.setSurfaceColors(VDSFormControlsColor.backgroundOnlight, VDSFormControlsColor.backgroundOndark, forState: .disabled) $0.setSurfaceColors(VDSColor.feedbackErrorBackgroundOnlight, VDSColor.feedbackErrorBackgroundOndark, forState: .error) } internal var borderColorConfiguration = ControlColorConfiguration().with { $0.setSurfaceColors(VDSFormControlsColor.borderOnlight, VDSFormControlsColor.borderOnlight, forState: .normal) $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled) $0.setSurfaceColors(VDSColor.feedbackErrorOnlight, VDSColor.feedbackErrorOndark, forState: .error) } //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- open var labelText: String? { didSet { didChange() }} open var helperText: String? { didSet { didChange() }} open var showError: Bool = false { didSet { didChange() }} open override var state: UIControl.State { get { var state = super.state if showError { state.insert(.error) } return state } } open var errorText: String? { didSet { didChange() }} open var tooltipTitle: String? { didSet { didChange() }} open var tooltipContent: String? { didSet { didChange() }} open var transparentBackground: Bool = false { didSet { didChange() }} open var width: CGFloat? { didSet { didChange() }} open var maxLength: Int? { didSet { didChange() }} open var inputId: String? { didSet { didChange() }} open var value: AnyHashable? { didSet { didChange() }} open var defaultValue: AnyHashable? { didSet { didChange() }} open var required: Bool = false { didSet { didChange() }} open var readOnly: Bool = false { didSet { didChange() }} open var accessibilityHintEnabled: String? { didSet { didChange() }} open var accessibilityHintDisabled: String? { didSet { didChange() }} open var accessibilityValueEnabled: String? { didSet { didChange() }} open var accessibilityValueDisabled: String? { didSet { didChange() }} open var accessibilityLabelEnabled: String? { didSet { didChange() }} open var accessibilityLabelDisabled: String? { didSet { didChange() }} //-------------------------------------------------- // MARK: - Constraints //-------------------------------------------------- internal var heightConstraint: NSLayoutConstraint? internal var widthConstraint: NSLayoutConstraint? //-------------------------------------------------- // MARK: - Lifecycle //-------------------------------------------------- open override func setup() { super.setup() enabledHighlight = false isAccessibilityElement = true accessibilityTraits = .button addSubview(stackView) //create the wrapping view heightConstraint = containerView.heightAnchor.constraint(greaterThanOrEqualToConstant: containerSize.height) heightConstraint?.isActive = true widthConstraint = containerView.widthAnchor.constraint(equalToConstant: 0) let container = getContainer() stackView.addArrangedSubview(titleLabel) stackView.addArrangedSubview(container) stackView.addArrangedSubview(errorLabel) stackView.addArrangedSubview(helperLabel) stackView.setCustomSpacing(4, after: titleLabel) stackView.setCustomSpacing(8, after: container) stackView.setCustomSpacing(8, after: errorLabel) stackView.pinToSuperView() titleLabel.textColorConfiguration = primaryColorConfig.eraseToAnyColorable() errorLabel.textColorConfiguration = primaryColorConfig.eraseToAnyColorable() helperLabel.textColorConfiguration = secondaryColorConfig.eraseToAnyColorable() } open func getContainer() -> UIView { return containerView } open func getToolTipView() -> UIView? { guard let tooltipTitle, let tooltipContent 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) $0.textPosition = .left $0.typograpicalStyle = .BoldBodySmall $0.text = tooltipTitle $0.surface = surface $0.disabled = disabled } let content = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) $0.textPosition = .left $0.typograpicalStyle = .BoldBodySmall $0.text = tooltipContent $0.surface = surface $0.disabled = disabled } stack.addArrangedSubview(title) stack.addArrangedSubview(content) stack.backgroundColor = backgroundColorConfiguration.getColor(self) return stack } open func showToolTipView(){ print("toolTip clicked: showToolTipView() called") } public override func reset() { super.reset() titleLabel.reset() errorLabel.reset() helperLabel.reset() titleLabel.textPosition = .left titleLabel.typograpicalStyle = .BodySmall errorLabel.textPosition = .left errorLabel.typograpicalStyle = .BodySmall helperLabel.textPosition = .left helperLabel.typograpicalStyle = .BodySmall labelText = nil helperText = nil showError = false errorText = nil tooltipTitle = nil tooltipContent = nil transparentBackground = false width = nil maxLength = nil inputId = nil value = nil defaultValue = nil required = false readOnly = false accessibilityHintEnabled = nil accessibilityHintDisabled = nil accessibilityValueEnabled = nil accessibilityValueDisabled = nil accessibilityLabelEnabled = nil accessibilityLabelDisabled = nil setAccessibilityLabel() } //-------------------------------------------------- // MARK: - State //-------------------------------------------------- open override func updateView() { containerView.backgroundColor = backgroundColorConfiguration.getColor(self) containerView.layer.borderColor = borderColorConfiguration.getColor(self).cgColor containerView.layer.borderWidth = 1 containerView.layer.cornerRadius = 4 updateTitleLabel() updateErrorLabel() updateHelperLabel() setAccessibilityHint() backgroundColor = surface.color } open func updateTitleLabel() { //update the local vars for the label since we no //long have a model var attributes: [any LabelAttributeModel] = [] var updatedLabelText = labelText //dealing with the "Optional" addition to the text if let oldText = updatedLabelText, !required, !oldText.hasSuffix("Optional") { let optionColorAttr = ColorLabelAttribute(location: oldText.count + 2, length: 8, color: secondaryColorConfig.getColor(self)) updatedLabelText = "\(oldText) Optional" attributes.append(optionColorAttr) } //add the tool tip if let view = getToolTipView(), let oldText = updatedLabelText { tooltipView = view let toolTipAction = PassthroughSubject() let toolTipUpdateText = "\(oldText) " //create a little space between the final character and tooltip image let toolTipAttribute = ToolTipLabelAttribute(action: toolTipAction, location: toolTipUpdateText.count, length: 1, tintColor: primaryColorConfig.getColor(self)) updatedLabelText = toolTipUpdateText attributes.append(toolTipAttribute) toolTipAction.sink { [weak self] in self?.showToolTipView() }.store(in: &subscribers) } else { tooltipView = nil } //set the titleLabel titleLabel.text = updatedLabelText titleLabel.attributes = attributes titleLabel.surface = surface titleLabel.disabled = disabled } open func updateErrorLabel(){ if showError, let errorText { errorLabel.text = errorText errorLabel.surface = surface errorLabel.disabled = disabled errorLabel.isHidden = false } else { errorLabel.isHidden = true } } open func updateHelperLabel(){ //set the helper label position if let helperText { helperLabel.text = helperText helperLabel.surface = surface helperLabel.disabled = disabled helperLabel.isHidden = false } else { helperLabel.isHidden = true } } }