From 4f0f611fb65ea3fa40b81ae012e0c6d1f3eb835a Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 9 Apr 2024 15:38:39 -0500 Subject: [PATCH] now subclassing EntryFieldBase Signed-off-by: Matt Bruce --- .../DropdownSelect/DropdownSelect.swift | 304 ++++-------------- 1 file changed, 67 insertions(+), 237 deletions(-) diff --git a/VDS/Components/DropdownSelect/DropdownSelect.swift b/VDS/Components/DropdownSelect/DropdownSelect.swift index 55510dd4..94f7112d 100644 --- a/VDS/Components/DropdownSelect/DropdownSelect.swift +++ b/VDS/Components/DropdownSelect/DropdownSelect.swift @@ -13,8 +13,7 @@ import Combine /// A dropdown select is an expandable menu of predefined options that allows a customer to make a single selection. @objc(VDSDropdownSelect) -open class DropdownSelect: Control { - +open class DropdownSelect: EntryFieldBase { //-------------------------------------------------- // MARK: - Initializers //-------------------------------------------------- @@ -33,86 +32,27 @@ open class DropdownSelect: Control { //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- - - /// Boolean value that determines if component should show the error state/error message. - open var showError: Bool = false { didSet { setNeedsUpdate() }} - - /// Message displayed when there is an error. - open var errorText: String? { didSet { setNeedsUpdate() }} - - /// If provided, will be used as text for the helper label. - open var helperText: String? { didSet { setNeedsUpdate() }} - /// If true, the label will be displayed inside the dropdown containerView. Otherwise, the label will be above the dropdown containerView like a normal text input. open var showInlineLabel: Bool = false { didSet { setNeedsUpdate() }} - - /// If provided, will be used as context for the label on the input field. - open var labelText: String? { didSet { setNeedsUpdate() }} - - /// Not allowed the user interaction to select/change input if it is true. - open var readOnly: Bool = false { didSet { setNeedsUpdate() }} - - /// Used to show optional indicator for the label. - open var required: Bool = false { didSet { setNeedsUpdate() }} - + /// Allows unique ID to be passed to the element. open var selectId: Int? { didSet { setNeedsUpdate() }} - /// Config object for tooltip option, is optional. - open var tooltipModel: Tooltip.TooltipModel? { didSet { setNeedsUpdate() } } - - /// If provided, will render with trnasparent background. - open var transparentBackground: Bool = false { didSet { setNeedsUpdate() }} - - /// Used to set width for the Dropdown Select. - open var width: CGFloat? { didSet { setNeedsUpdate() } } - /// Array of options to show open var options: [DropdownOptionModel] = [] { didSet { setNeedsUpdate() }} /// A callback when the selected option changes. Passes parameters (option). open var onDropdownItemSelect: ((DropdownOptionModel) -> Void)? - open override var state: UIControl.State { - get { - var state = super.state - if showError { - state.insert(.error) - } - return state - } - } - //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- internal var minWidthDefault = 66.0 internal var minWidthInlineLabel = 102.0 - - var stackView: UIStackView = UIStackView().with { - $0.translatesAutoresizingMaskIntoConstraints = false - $0.axis = .vertical - } - - var containerView: UIView = UIView().with { - $0.translatesAutoresizingMaskIntoConstraints = false - } - - var containerStackView: UIStackView = UIStackView().with { - $0.translatesAutoresizingMaskIntoConstraints = false - $0.axis = .horizontal - $0.spacing = VDSFormControls.spaceInset - } - + //-------------------------------------------------- // MARK: - Public Properties - //-------------------------------------------------- - open var titleLabel = Label().with { - $0.setContentCompressionResistancePriority(.required, for: .vertical) - $0.textAlignment = .left - $0.textStyle = .bodySmall - } - + //-------------------------------------------------- open var inlineDisplayLabel = Label().with { $0.textAlignment = .left $0.textStyle = .boldBodyLarge @@ -126,24 +66,7 @@ open class DropdownSelect: Control { $0.textStyle = .bodyLarge $0.lineBreakMode = .byCharWrapping } - - open var icon: Icon = Icon().with { - $0.size = .medium - $0.name = Icon.Name(name: "down-caret") - } - - open var errorLabel = Label().with { - $0.setContentCompressionResistancePriority(.required, for: .vertical) - $0.textAlignment = .left - $0.textStyle = .bodySmall - } - - open var helperLabel = Label().with { - $0.setContentCompressionResistancePriority(.required, for: .vertical) - $0.textAlignment = .left - $0.textStyle = .bodySmall - } - + open var dropdownField = UITextField().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.tintColor = UIColor.clear @@ -160,42 +83,14 @@ open class DropdownSelect: Control { //-------------------------------------------------- // MARK: - Configuration Properties //-------------------------------------------------- - internal var containerSize: CGSize { CGSize(width: showInlineLabel ? minWidthInlineLabel : width ?? minWidthDefault, height: 44) } - - 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 let iconColorConfig = ControlColorConfiguration().with { + 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) } - - 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.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: [.disabled,.error]) - $0.setSurfaceColors(VDSColor.feedbackErrorOnlight, VDSColor.feedbackErrorOndark, forState: .error) - $0.setSurfaceColors(VDSFormControlsColor.borderHoverOnlight, VDSFormControlsColor.borderHoverOndark, forState: .focused) - } - - internal var readOnlyBorderColorConfiguration = ControlColorConfiguration().with { - $0.setSurfaceColors(VDSFormControlsColor.borderReadonlyOnlight, VDSFormControlsColor.borderReadonlyOndark, forState: .normal) - } - + //-------------------------------------------------- // MARK: - Overrides //-------------------------------------------------- @@ -203,107 +98,78 @@ open class DropdownSelect: Control { /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations. open override func setup() { super.setup() - isAccessibilityElement = true + accessibilityLabel = "Dropdown Select" - // stackview - addSubview(stackView) - stackView.pinToSuperView() - stackView.heightAnchor.constraint(greaterThanOrEqualToConstant: containerSize.height).isActive = true - - // containerView stack - containerView.addSubview(containerStackView) - let spacing = VDSFormControls.spaceInset - containerStackView.pinToSuperView(.init(top: spacing, left: spacing, bottom: spacing, right: spacing)) + // stackview for controls in EntryFieldBase.controlContainerView + let controlStackView = UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.axis = .horizontal + $0.spacing = VDSFormControls.spaceInset + } + controlContainerView.addSubview(controlStackView) + controlStackView.pinToSuperView() - containerStackView.addArrangedSubview(dropdownField) - containerStackView.addArrangedSubview(inlineDisplayLabel) - containerStackView.addArrangedSubview(selectedOptionLabel) - containerStackView.addArrangedSubview(icon) + controlStackView.addArrangedSubview(dropdownField) + controlStackView.addArrangedSubview(inlineDisplayLabel) + controlStackView.addArrangedSubview(selectedOptionLabel) - containerStackView.setCustomSpacing(0, after: dropdownField) - containerStackView.setCustomSpacing(VDSLayout.Spacing.space1X.value, after: inlineDisplayLabel) - containerStackView.setCustomSpacing(VDSLayout.Spacing.space3X.value, after: selectedOptionLabel) + controlStackView.setCustomSpacing(0, after: dropdownField) + controlStackView.setCustomSpacing(VDSLayout.Spacing.space1X.value, after: inlineDisplayLabel) + controlStackView.setCustomSpacing(VDSLayout.Spacing.space3X.value, after: selectedOptionLabel) dropdownField.width(0) inlineWidthConstraint = inlineDisplayLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 0) inlineWidthConstraint?.isActive = true - - // component stackview subviews - stackView.addArrangedSubview(titleLabel) - stackView.addArrangedSubview(containerView) - stackView.addArrangedSubview(errorLabel) - stackView.addArrangedSubview(helperLabel) - - stackView.setCustomSpacing(4, after: titleLabel) - stackView.setCustomSpacing(8, after: containerView) - stackView.setCustomSpacing(8, after: errorLabel) // setting color config - titleLabel.textColorConfiguration = primaryColorConfig.eraseToAnyColorable() - errorLabel.textColorConfiguration = primaryColorConfig.eraseToAnyColorable() - helperLabel.textColorConfiguration = secondaryColorConfig.eraseToAnyColorable() - inlineDisplayLabel.textColorConfiguration = primaryColorConfig.eraseToAnyColorable() - selectedOptionLabel.textColorConfiguration = primaryColorConfig.eraseToAnyColorable() - icon.color = iconColorConfig.getColor(self) + inlineDisplayLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable() + selectedOptionLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable() // Options PickerView optionsPicker.delegate = self optionsPicker.dataSource = self optionsPicker.isHidden = true dropdownField.inputView = optionsPicker - dropdownField.inputAccessoryView = toolBarForPicker() - containerStackView.publisher(for: UITapGestureRecognizer()).sink { [weak self] _ in - self?.launchPicker() - }.store(in: &subscribers) + dropdownField.inputAccessoryView = { + let inputToolbar = UIToolbar().with { + $0.barStyle = .default + $0.isTranslucent = true + $0.items=[ + UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.flexibleSpace, target: self, action: nil), + UIBarButtonItem(title: "Done", style: UIBarButtonItem.Style.done, target: self, action: #selector(pickerDoneClicked)) + ] + } + inputToolbar.sizeToFit() + return inputToolbar + }() + + // tap gesture + containerStackView + .publisher(for: UITapGestureRecognizer()) + .sink { [weak self] _ in + self?.launchPicker() + } + .store(in: &subscribers) } /// Used to make changes to the View based off a change events or from local properties. open override func updateView() { + super.updateView() - containerView.backgroundColor = backgroundColorConfiguration.getColor(self) - containerView.layer.borderWidth = VDSFormControls.widthBorder - containerView.layer.cornerRadius = VDSFormControls.borderradius - containerView.layer.borderColor = readOnly ? readOnlyBorderColorConfiguration.getColor(self).cgColor : borderColorConfiguration.getColor(self).cgColor - dropdownField.isUserInteractionEnabled = readOnly ? false : true - stackView.backgroundColor = transparentBackground ? .clear : surface.color - - updateTitleLabel() updateInlineLabel() - updateErrorLabel() - updateHelperLabel() - if readOnly { - icon.name = nil - } + + dropdownField.isUserInteractionEnabled = readOnly ? false : true selectedOptionLabel.surface = surface selectedOptionLabel.isEnabled = isEnabled - backgroundColor = surface.color } /// Resets to default settings. open override func reset() { super.reset() - titleLabel.reset() - inlineDisplayLabel.reset() - selectedOptionLabel.reset() - errorLabel.reset() - helperLabel.reset() - - titleLabel.textStyle = .bodySmall inlineDisplayLabel.textStyle = .boldBodyLarge selectedOptionLabel.textStyle = .bodyLarge - errorLabel.textStyle = .bodySmall - helperLabel.textStyle = .bodySmall - tooltipModel = nil - labelText = nil - errorText = nil - showError = false - isEnabled = false - readOnly = false showInlineLabel = false - helperText = nil - transparentBackground = false - required = false options = [] selectId = 0 } @@ -312,7 +178,7 @@ open class DropdownSelect: Control { // MARK: - Public Methods //-------------------------------------------------- - open func updateTitleLabel() { + open override func updateTitleLabel() { //update the local vars for the label since we no long have a model var attributes: [any LabelAttributeModel] = [] @@ -323,7 +189,7 @@ open class DropdownSelect: Control { if let oldText = updatedLabelText, !required, !oldText.hasSuffix("Optional") { let optionColorAttr = ColorLabelAttribute(location: oldText.count + 2, length: 8, - color: secondaryColorConfig.getColor(self)) + color: secondaryColorConfiguration.getColor(self)) updatedLabelText = showInlineLabel ? "Optional" : "\(oldText) Optional" attributes.append(optionColorAttr) @@ -357,70 +223,37 @@ open class DropdownSelect: Control { inlineWidthConstraint?.isActive = true if let selectId, selectId < options.count { - updateSelectedOptionLabel(text: options[selectId].text) + updateSelectedOptionLabel(option: options[selectId]) } } - open func updateSelectedOptionLabel(text: String? = nil) { - selectedOptionLabel.text = text ?? "" + open func updateSelectedOptionLabel(option: DropdownOptionModel? = nil) { + selectedOptionLabel.text = option?.text ?? "" + value = option?.value } - open func updateErrorLabel() { - if showError, let errorText { - errorLabel.text = errorText - errorLabel.surface = surface - errorLabel.isEnabled = isEnabled - errorLabel.isHidden = false - icon.name = .error - icon.size = .medium - icon.surface = surface - } else { + open override func updateErrorLabel() { + super.updateErrorLabel() + if !showError && !hasInternalError { icon.name = .downCaret - icon.surface = surface - errorLabel.isHidden = true - } - icon.color = iconColorConfig.getColor(self) - } - - open func updateHelperLabel() { - if let helperText { - helperLabel.text = helperText - helperLabel.surface = surface - helperLabel.isEnabled = isEnabled - helperLabel.isHidden = false - } else { - helperLabel.isHidden = true } + icon.surface = surface + icon.isHidden = readOnly ? true : false + icon.color = iconColorConfiguration.getColor(self) } @objc open func pickerDoneClicked() { optionsPicker.isHidden = true dropdownField.resignFirstResponder() } - - //-------------------------------------------------- - // MARK: - Private Methods - //-------------------------------------------------- - - private func toolBarForPicker() -> UIToolbar { - - let inputToolbar: UIToolbar = UIToolbar() - inputToolbar.barStyle = .default - inputToolbar.isTranslucent = true - - // add a done button to the toolbar - inputToolbar.items=[ - UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.flexibleSpace, target: self, action: nil), - UIBarButtonItem(title: "Done", style: UIBarButtonItem.Style.done, target: self, action: #selector(pickerDoneClicked)) - ] - inputToolbar.sizeToFit() - return inputToolbar - } } -extension DropdownSelect: UIPickerViewDelegate, UIPickerViewDataSource, UITextFieldDelegate { +//-------------------------------------------------- +// MARK: - UIPickerView Delegate & Datasource +//-------------------------------------------------- +extension DropdownSelect: UIPickerViewDelegate, UIPickerViewDataSource { - func launchPicker() { + internal func launchPicker() { if optionsPicker.isHidden { dropdownField.becomeFirstResponder() } else { @@ -429,9 +262,6 @@ extension DropdownSelect: UIPickerViewDelegate, UIPickerViewDataSource, UITextFi optionsPicker.isHidden = !optionsPicker.isHidden } - //-------------------------------------------------- - // MARK: - UIPickerView Delegate & Datasource - //-------------------------------------------------- public func numberOfComponents(in pickerView: UIPickerView) -> Int { return 1 } @@ -448,7 +278,7 @@ extension DropdownSelect: UIPickerViewDelegate, UIPickerViewDataSource, UITextFi public func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { guard options.count > row else { return } selectId = row - updateSelectedOptionLabel(text: options[row].text) + updateSelectedOptionLabel(option: options[row]) self.onDropdownItemSelect?(options[row]) } }