369 lines
13 KiB
Swift
369 lines
13 KiB
Swift
//
|
|
// DropdownSelect.swift
|
|
// VDS
|
|
//
|
|
// Created by Kanamarlapudi, Vasavi on 28/03/24.
|
|
//
|
|
|
|
import Foundation
|
|
import UIKit
|
|
import VDSTokens
|
|
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: EntryFieldBase {
|
|
//--------------------------------------------------
|
|
// 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: - Public Properties
|
|
//--------------------------------------------------
|
|
/// 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 dropdownField.isFirstResponder {
|
|
state.insert(.focused)
|
|
}
|
|
|
|
return state
|
|
}
|
|
}
|
|
|
|
/// 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() }}
|
|
|
|
/// 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 }
|
|
return options[selectId]
|
|
}
|
|
|
|
/// Array of options to show
|
|
open var options: [DropdownOptionModel] = [] { didSet { setNeedsUpdate() }}
|
|
|
|
//--------------------------------------------------
|
|
// MARK: - Private Properties
|
|
//--------------------------------------------------
|
|
internal var minWidthDefault = 66.0
|
|
internal var minWidthInlineLabel = 102.0
|
|
internal override var minWidth: CGFloat { showInlineLabel ? minWidthInlineLabel : minWidthDefault }
|
|
internal override var maxWidth: CGFloat {
|
|
let frameWidth = frame.size.width
|
|
return helperTextPlacement == .right ? (frameWidth - horizontalStackView.spacing) / 2 : frameWidth
|
|
}
|
|
|
|
/// The is used for the for adding the helperLabel to the right of the containerView.
|
|
internal var horizontalStackView: UIStackView = {
|
|
return UIStackView().with {
|
|
$0.translatesAutoresizingMaskIntoConstraints = false
|
|
$0.axis = .horizontal
|
|
$0.distribution = .fillEqually
|
|
$0.spacing = VDSLayout.space3X
|
|
$0.alignment = .top
|
|
}
|
|
}()
|
|
|
|
//--------------------------------------------------
|
|
// MARK: - Public Properties
|
|
//--------------------------------------------------
|
|
open var inlineDisplayLabel = Label().with {
|
|
$0.setContentCompressionResistancePriority(.required, for: .vertical)
|
|
$0.setContentCompressionResistancePriority(.required, for: .horizontal)
|
|
$0.textAlignment = .left
|
|
$0.textStyle = .boldBodyLarge
|
|
$0.numberOfLines = 1
|
|
$0.sizeToFit()
|
|
}
|
|
|
|
open var selectedOptionLabel = Label().with {
|
|
$0.setContentCompressionResistancePriority(.required, for: .vertical)
|
|
$0.setContentCompressionResistancePriority(.required, for: .horizontal)
|
|
$0.textAlignment = .left
|
|
$0.textStyle = .bodyLarge
|
|
$0.numberOfLines = 1
|
|
}
|
|
|
|
open var dropdownField = UITextField().with {
|
|
$0.translatesAutoresizingMaskIntoConstraints = false
|
|
$0.tintColor = UIColor.clear
|
|
$0.font = TextStyle.bodyLarge.font
|
|
}
|
|
|
|
open var optionsPicker = UIPickerView()
|
|
|
|
//--------------------------------------------------
|
|
// MARK: - Constraints
|
|
//--------------------------------------------------
|
|
internal var inlineWidthConstraint: NSLayoutConstraint?
|
|
|
|
//--------------------------------------------------
|
|
// MARK: - Configuration Properties
|
|
//--------------------------------------------------
|
|
internal override var containerSize: CGSize { .init(width: minWidthDefault, height: 44) }
|
|
|
|
//--------------------------------------------------
|
|
// MARK: - Overrides
|
|
//--------------------------------------------------
|
|
|
|
/// 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()
|
|
|
|
fieldStackView.isAccessibilityElement = true
|
|
fieldStackView.accessibilityLabel = "Dropdown Select"
|
|
inlineDisplayLabel.isAccessibilityElement = true
|
|
|
|
dropdownField.width(0)
|
|
inlineWidthConstraint = inlineDisplayLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 0)
|
|
inlineWidthConstraint?.isActive = true
|
|
|
|
// setting color config
|
|
inlineDisplayLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable()
|
|
selectedOptionLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable()
|
|
|
|
// Options PickerView
|
|
optionsPicker.delegate = self
|
|
optionsPicker.dataSource = self
|
|
optionsPicker.isHidden = true
|
|
dropdownField.inputView = optionsPicker
|
|
dropdownField.inputAccessoryView = {
|
|
let accessView = UIView(frame: .init(origin: .zero, size: .init(width: UIScreen.main.bounds.width, height: 44)))
|
|
accessView.backgroundColor = .white
|
|
accessView.addBorder(side: .top, width: 1, color: .lightGray)
|
|
let done = UIButton(type: .system)
|
|
done.setTitle("Done", for: .normal)
|
|
done.translatesAutoresizingMaskIntoConstraints = false
|
|
done.addTarget(self, action: #selector(pickerDoneClicked), for: .touchUpInside)
|
|
accessView.addSubview(done)
|
|
done.pinCenterY()
|
|
.pinTrailing(16)
|
|
return accessView
|
|
}()
|
|
|
|
// tap gesture
|
|
containerView
|
|
.publisher(for: UITapGestureRecognizer())
|
|
.sink { [weak self] _ in
|
|
self?.launchPicker()
|
|
}
|
|
.store(in: &subscribers)
|
|
}
|
|
|
|
open override func getFieldContainer() -> UIView {
|
|
let controlStackView = UIStackView().with {
|
|
$0.translatesAutoresizingMaskIntoConstraints = false
|
|
$0.axis = .horizontal
|
|
$0.distribution = .fill
|
|
$0.spacing = VDSFormControls.spaceInset
|
|
}
|
|
controlStackView.addArrangedSubview(dropdownField)
|
|
controlStackView.addArrangedSubview(inlineDisplayLabel)
|
|
controlStackView.addArrangedSubview(selectedOptionLabel)
|
|
controlStackView.setCustomSpacing(0, after: dropdownField)
|
|
controlStackView.setCustomSpacing(VDSLayout.space1X, after: inlineDisplayLabel)
|
|
controlStackView.setCustomSpacing(VDSLayout.space3X, after: selectedOptionLabel)
|
|
return controlStackView
|
|
}
|
|
|
|
/// Used to make changes to the View based off a change events or from local properties.
|
|
open override func updateView() {
|
|
super.updateView()
|
|
|
|
updateInlineLabel()
|
|
|
|
dropdownField.isUserInteractionEnabled = isReadOnly ? false : true
|
|
selectedOptionLabel.surface = surface
|
|
selectedOptionLabel.isEnabled = isEnabled
|
|
}
|
|
|
|
/// Resets to default settings.
|
|
open override func reset() {
|
|
super.reset()
|
|
|
|
inlineDisplayLabel.textStyle = .boldBodyLarge
|
|
selectedOptionLabel.textStyle = .bodyLarge
|
|
showInlineLabel = false
|
|
options = []
|
|
selectId = nil
|
|
}
|
|
|
|
//--------------------------------------------------
|
|
// MARK: - Public Methods
|
|
//--------------------------------------------------
|
|
|
|
open override func updateTitleLabel() {
|
|
|
|
//update the local vars for the label since we no long have a model
|
|
var attributes: [any LabelAttributeModel] = []
|
|
var updatedLabelText = labelText
|
|
|
|
updatedLabelText = showInlineLabel ? "" : updatedLabelText
|
|
|
|
if let oldText = updatedLabelText, !isRequired, !oldText.hasSuffix("Optional") {
|
|
let optionColorAttr = ColorLabelAttribute(location: oldText.count + 1,
|
|
length: 8,
|
|
color: secondaryColorConfiguration.getColor(self))
|
|
|
|
updatedLabelText = showInlineLabel ? "Optional" : "\(oldText) Optional"
|
|
attributes.append(optionColorAttr)
|
|
}
|
|
|
|
if let tooltipModel {
|
|
attributes.append(TooltipLabelAttribute(surface: surface, model: tooltipModel, presenter: self))
|
|
}
|
|
|
|
titleLabel.text = updatedLabelText
|
|
titleLabel.attributes = attributes
|
|
titleLabel.surface = surface
|
|
titleLabel.isEnabled = isEnabled
|
|
}
|
|
|
|
open func updateInlineLabel() {
|
|
|
|
inlineWidthConstraint?.isActive = false
|
|
|
|
/// inline label text and selected option text separated by ':'
|
|
if let labelText, !labelText.isEmpty {
|
|
inlineDisplayLabel.text = showInlineLabel ? (labelText + ":") : ""
|
|
} else {
|
|
inlineDisplayLabel.text = showInlineLabel ? labelText : ""
|
|
}
|
|
inlineDisplayLabel.surface = surface
|
|
inlineDisplayLabel.isEnabled = isEnabled
|
|
|
|
/// Update width as per updated text size
|
|
inlineWidthConstraint = inlineDisplayLabel.widthAnchor.constraint(equalToConstant: inlineDisplayLabel.intrinsicContentSize.width)
|
|
inlineWidthConstraint?.isActive = true
|
|
|
|
if let selectId, selectId < options.count {
|
|
updateSelectedOptionLabel(option: options[selectId])
|
|
}
|
|
}
|
|
|
|
open func updateSelectedOptionLabel(option: DropdownOptionModel? = nil) {
|
|
selectedOptionLabel.text = option?.text ?? ""
|
|
}
|
|
|
|
open override func updateErrorLabel() {
|
|
super.updateErrorLabel()
|
|
if !showError && !hasInternalError {
|
|
statusIcon.name = .downCaret
|
|
}
|
|
statusIcon.surface = surface
|
|
statusIcon.isHidden = isReadOnly ? true : false
|
|
statusIcon.color = iconColorConfiguration.getColor(self)
|
|
}
|
|
|
|
open override func updateAccessibility() {
|
|
super.updateAccessibility()
|
|
let selectedOption = selectedOptionLabel.text ?? ""
|
|
fieldStackView.accessibilityLabel = "Dropdown Select, \(selectedOption) \(isReadOnly ? ", read only" : "")"
|
|
fieldStackView.accessibilityHint = isReadOnly || !isEnabled ? "" : "Double tap to open."
|
|
}
|
|
|
|
open override var accessibilityElements: [Any]? {
|
|
get {
|
|
var elements = [Any]()
|
|
elements.append(contentsOf: [titleLabel, fieldStackView])
|
|
|
|
if showError {
|
|
elements.append(statusIcon)
|
|
if let errorText, !errorText.isEmpty {
|
|
elements.append(errorLabel)
|
|
}
|
|
}
|
|
|
|
if let helperText, !helperText.isEmpty {
|
|
elements.append(helperLabel)
|
|
}
|
|
|
|
return elements
|
|
}
|
|
|
|
set { super.accessibilityElements = newValue }
|
|
}
|
|
|
|
|
|
@objc open func pickerDoneClicked() {
|
|
optionsPicker.isHidden = true
|
|
dropdownField.resignFirstResponder()
|
|
setNeedsUpdate()
|
|
UIAccessibility.post(notification: .layoutChanged, argument: fieldStackView)
|
|
}
|
|
|
|
open override var canBecomeFirstResponder: Bool {
|
|
return dropdownField.canBecomeFirstResponder
|
|
}
|
|
|
|
open override func becomeFirstResponder() -> Bool {
|
|
return dropdownField.becomeFirstResponder()
|
|
}
|
|
|
|
open override var canResignFirstResponder: Bool {
|
|
return dropdownField.canResignFirstResponder
|
|
}
|
|
|
|
open override func resignFirstResponder() -> Bool {
|
|
return dropdownField.resignFirstResponder()
|
|
}
|
|
}
|
|
|
|
//--------------------------------------------------
|
|
// MARK: - UIPickerView Delegate & Datasource
|
|
//--------------------------------------------------
|
|
extension DropdownSelect: UIPickerViewDelegate, UIPickerViewDataSource {
|
|
|
|
internal func launchPicker() {
|
|
if optionsPicker.isHidden {
|
|
UIAccessibility.post(notification: .layoutChanged, argument: optionsPicker)
|
|
dropdownField.becomeFirstResponder()
|
|
} else {
|
|
dropdownField.resignFirstResponder()
|
|
}
|
|
optionsPicker.isHidden = !optionsPicker.isHidden
|
|
updateContainerView()
|
|
}
|
|
|
|
public func numberOfComponents(in pickerView: UIPickerView) -> Int {
|
|
return 1
|
|
}
|
|
|
|
public func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
|
|
return options.count
|
|
}
|
|
|
|
public func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
|
|
guard options.count > row else { return nil }
|
|
return options[row].text
|
|
}
|
|
|
|
public func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
|
|
guard options.count > row else { return }
|
|
selectId = row
|
|
updateSelectedOptionLabel(option: options[row])
|
|
sendActions(for: .valueChanged)
|
|
}
|
|
}
|