now subclassing EntryFieldBase

Signed-off-by: Matt Bruce <matt.bruce@verizon.com>
This commit is contained in:
Matt Bruce 2024-04-09 15:38:39 -05:00
parent 8311de5409
commit 4f0f611fb6

View File

@ -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. /// A dropdown select is an expandable menu of predefined options that allows a customer to make a single selection.
@objc(VDSDropdownSelect) @objc(VDSDropdownSelect)
open class DropdownSelect: Control { open class DropdownSelect: EntryFieldBase {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Initializers // MARK: - Initializers
//-------------------------------------------------- //--------------------------------------------------
@ -33,86 +32,27 @@ open class DropdownSelect: Control {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Public Properties // 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. /// 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() }} 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. /// Allows unique ID to be passed to the element.
open var selectId: Int? { didSet { setNeedsUpdate() }} 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 /// Array of options to show
open var options: [DropdownOptionModel] = [] { didSet { setNeedsUpdate() }} open var options: [DropdownOptionModel] = [] { didSet { setNeedsUpdate() }}
/// A callback when the selected option changes. Passes parameters (option). /// A callback when the selected option changes. Passes parameters (option).
open var onDropdownItemSelect: ((DropdownOptionModel) -> Void)? 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 // MARK: - Private Properties
//-------------------------------------------------- //--------------------------------------------------
internal var minWidthDefault = 66.0 internal var minWidthDefault = 66.0
internal var minWidthInlineLabel = 102.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 // MARK: - Public Properties
//-------------------------------------------------- //--------------------------------------------------
open var titleLabel = Label().with {
$0.setContentCompressionResistancePriority(.required, for: .vertical)
$0.textAlignment = .left
$0.textStyle = .bodySmall
}
open var inlineDisplayLabel = Label().with { open var inlineDisplayLabel = Label().with {
$0.textAlignment = .left $0.textAlignment = .left
$0.textStyle = .boldBodyLarge $0.textStyle = .boldBodyLarge
@ -126,24 +66,7 @@ open class DropdownSelect: Control {
$0.textStyle = .bodyLarge $0.textStyle = .bodyLarge
$0.lineBreakMode = .byCharWrapping $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 { open var dropdownField = UITextField().with {
$0.translatesAutoresizingMaskIntoConstraints = false $0.translatesAutoresizingMaskIntoConstraints = false
$0.tintColor = UIColor.clear $0.tintColor = UIColor.clear
@ -160,42 +83,14 @@ open class DropdownSelect: Control {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Configuration Properties // MARK: - Configuration Properties
//-------------------------------------------------- //--------------------------------------------------
internal var containerSize: CGSize { CGSize(width: showInlineLabel ? minWidthInlineLabel : width ?? minWidthDefault, height: 44) } internal override var containerSize: CGSize { CGSize(width: showInlineLabel ? minWidthInlineLabel : width ?? minWidthDefault, height: 44) }
internal let primaryColorConfig = ViewColorConfiguration().with { internal let iconColorConfiguration = ControlColorConfiguration().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 {
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .normal) $0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .normal)
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled) $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled)
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .error) $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 // 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. /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
open override func setup() { open override func setup() {
super.setup() super.setup()
isAccessibilityElement = true
accessibilityLabel = "Dropdown Select" accessibilityLabel = "Dropdown Select"
// stackview // stackview for controls in EntryFieldBase.controlContainerView
addSubview(stackView) let controlStackView = UIStackView().with {
stackView.pinToSuperView() $0.translatesAutoresizingMaskIntoConstraints = false
stackView.heightAnchor.constraint(greaterThanOrEqualToConstant: containerSize.height).isActive = true $0.axis = .horizontal
$0.spacing = VDSFormControls.spaceInset
// containerView stack }
containerView.addSubview(containerStackView) controlContainerView.addSubview(controlStackView)
let spacing = VDSFormControls.spaceInset controlStackView.pinToSuperView()
containerStackView.pinToSuperView(.init(top: spacing, left: spacing, bottom: spacing, right: spacing))
containerStackView.addArrangedSubview(dropdownField) controlStackView.addArrangedSubview(dropdownField)
containerStackView.addArrangedSubview(inlineDisplayLabel) controlStackView.addArrangedSubview(inlineDisplayLabel)
containerStackView.addArrangedSubview(selectedOptionLabel) controlStackView.addArrangedSubview(selectedOptionLabel)
containerStackView.addArrangedSubview(icon)
containerStackView.setCustomSpacing(0, after: dropdownField) controlStackView.setCustomSpacing(0, after: dropdownField)
containerStackView.setCustomSpacing(VDSLayout.Spacing.space1X.value, after: inlineDisplayLabel) controlStackView.setCustomSpacing(VDSLayout.Spacing.space1X.value, after: inlineDisplayLabel)
containerStackView.setCustomSpacing(VDSLayout.Spacing.space3X.value, after: selectedOptionLabel) controlStackView.setCustomSpacing(VDSLayout.Spacing.space3X.value, after: selectedOptionLabel)
dropdownField.width(0) dropdownField.width(0)
inlineWidthConstraint = inlineDisplayLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 0) inlineWidthConstraint = inlineDisplayLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 0)
inlineWidthConstraint?.isActive = true 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 // setting color config
titleLabel.textColorConfiguration = primaryColorConfig.eraseToAnyColorable() inlineDisplayLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable()
errorLabel.textColorConfiguration = primaryColorConfig.eraseToAnyColorable() selectedOptionLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable()
helperLabel.textColorConfiguration = secondaryColorConfig.eraseToAnyColorable()
inlineDisplayLabel.textColorConfiguration = primaryColorConfig.eraseToAnyColorable()
selectedOptionLabel.textColorConfiguration = primaryColorConfig.eraseToAnyColorable()
icon.color = iconColorConfig.getColor(self)
// Options PickerView // Options PickerView
optionsPicker.delegate = self optionsPicker.delegate = self
optionsPicker.dataSource = self optionsPicker.dataSource = self
optionsPicker.isHidden = true optionsPicker.isHidden = true
dropdownField.inputView = optionsPicker dropdownField.inputView = optionsPicker
dropdownField.inputAccessoryView = toolBarForPicker() dropdownField.inputAccessoryView = {
containerStackView.publisher(for: UITapGestureRecognizer()).sink { [weak self] _ in let inputToolbar = UIToolbar().with {
self?.launchPicker() $0.barStyle = .default
}.store(in: &subscribers) $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. /// Used to make changes to the View based off a change events or from local properties.
open override func updateView() { open override func updateView() {
super.updateView()
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() updateInlineLabel()
updateErrorLabel()
updateHelperLabel() dropdownField.isUserInteractionEnabled = readOnly ? false : true
if readOnly {
icon.name = nil
}
selectedOptionLabel.surface = surface selectedOptionLabel.surface = surface
selectedOptionLabel.isEnabled = isEnabled selectedOptionLabel.isEnabled = isEnabled
backgroundColor = surface.color
} }
/// Resets to default settings. /// Resets to default settings.
open override func reset() { open override func reset() {
super.reset() super.reset()
titleLabel.reset()
inlineDisplayLabel.reset()
selectedOptionLabel.reset()
errorLabel.reset()
helperLabel.reset()
titleLabel.textStyle = .bodySmall
inlineDisplayLabel.textStyle = .boldBodyLarge inlineDisplayLabel.textStyle = .boldBodyLarge
selectedOptionLabel.textStyle = .bodyLarge selectedOptionLabel.textStyle = .bodyLarge
errorLabel.textStyle = .bodySmall
helperLabel.textStyle = .bodySmall
tooltipModel = nil
labelText = nil
errorText = nil
showError = false
isEnabled = false
readOnly = false
showInlineLabel = false showInlineLabel = false
helperText = nil
transparentBackground = false
required = false
options = [] options = []
selectId = 0 selectId = 0
} }
@ -312,7 +178,7 @@ open class DropdownSelect: Control {
// MARK: - Public Methods // MARK: - Public Methods
//-------------------------------------------------- //--------------------------------------------------
open func updateTitleLabel() { open override func updateTitleLabel() {
//update the local vars for the label since we no long have a model //update the local vars for the label since we no long have a model
var attributes: [any LabelAttributeModel] = [] var attributes: [any LabelAttributeModel] = []
@ -323,7 +189,7 @@ open class DropdownSelect: Control {
if let oldText = updatedLabelText, !required, !oldText.hasSuffix("Optional") { if let oldText = updatedLabelText, !required, !oldText.hasSuffix("Optional") {
let optionColorAttr = ColorLabelAttribute(location: oldText.count + 2, let optionColorAttr = ColorLabelAttribute(location: oldText.count + 2,
length: 8, length: 8,
color: secondaryColorConfig.getColor(self)) color: secondaryColorConfiguration.getColor(self))
updatedLabelText = showInlineLabel ? "Optional" : "\(oldText) Optional" updatedLabelText = showInlineLabel ? "Optional" : "\(oldText) Optional"
attributes.append(optionColorAttr) attributes.append(optionColorAttr)
@ -357,70 +223,37 @@ open class DropdownSelect: Control {
inlineWidthConstraint?.isActive = true inlineWidthConstraint?.isActive = true
if let selectId, selectId < options.count { if let selectId, selectId < options.count {
updateSelectedOptionLabel(text: options[selectId].text) updateSelectedOptionLabel(option: options[selectId])
} }
} }
open func updateSelectedOptionLabel(text: String? = nil) { open func updateSelectedOptionLabel(option: DropdownOptionModel? = nil) {
selectedOptionLabel.text = text ?? "" selectedOptionLabel.text = option?.text ?? ""
value = option?.value
} }
open func updateErrorLabel() { open override func updateErrorLabel() {
if showError, let errorText { super.updateErrorLabel()
errorLabel.text = errorText if !showError && !hasInternalError {
errorLabel.surface = surface
errorLabel.isEnabled = isEnabled
errorLabel.isHidden = false
icon.name = .error
icon.size = .medium
icon.surface = surface
} else {
icon.name = .downCaret 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() { @objc open func pickerDoneClicked() {
optionsPicker.isHidden = true optionsPicker.isHidden = true
dropdownField.resignFirstResponder() 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 { if optionsPicker.isHidden {
dropdownField.becomeFirstResponder() dropdownField.becomeFirstResponder()
} else { } else {
@ -429,9 +262,6 @@ extension DropdownSelect: UIPickerViewDelegate, UIPickerViewDataSource, UITextFi
optionsPicker.isHidden = !optionsPicker.isHidden optionsPicker.isHidden = !optionsPicker.isHidden
} }
//--------------------------------------------------
// MARK: - UIPickerView Delegate & Datasource
//--------------------------------------------------
public func numberOfComponents(in pickerView: UIPickerView) -> Int { public func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1 return 1
} }
@ -448,7 +278,7 @@ extension DropdownSelect: UIPickerViewDelegate, UIPickerViewDataSource, UITextFi
public func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { public func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
guard options.count > row else { return } guard options.count > row else { return }
selectId = row selectId = row
updateSelectedOptionLabel(text: options[row].text) updateSelectedOptionLabel(option: options[row])
self.onDropdownItemSelect?(options[row]) self.onDropdownItemSelect?(options[row])
} }
} }