vds_ios/VDS/Components/TextFields/TextArea/TextArea.swift
Matt Bruce b907e0e001 implemented new internal error handling
Signed-off-by: Matt Bruce <matt.bruce@verizon.com>
2024-02-29 12:14:27 -06:00

462 lines
17 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// TextArea.swift
// VDS
//
// Created by Matt Bruce on 1/10/23.
//
import Foundation
import UIKit
import VDSColorTokens
import VDSFormControlsTokens
import Combine
/// A text area is an input wherein a customer enters long-form information.
/// Use a text area when you want customers to enter text thats longer than a single line.
@objc(VDSTextArea)
open class TextArea: 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: - Private Properties
//--------------------------------------------------
internal var minWidthConstraint: NSLayoutConstraint?
internal var textViewHeightConstraint: NSLayoutConstraint?
internal var allowCharCount: Int = 0
internal var inputFieldStackView: UIStackView = {
return UIStackView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.axis = .horizontal
$0.distribution = .fill
$0.spacing = VDSLayout.Spacing.space3X.value
}
}()
internal var bottomView: UIView = {
return UIView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
}
}()
internal var bottomStackView: UIStackView = {
return UIStackView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.axis = .horizontal
$0.distribution = .fill
$0.alignment = .top
$0.spacing = VDSLayout.Spacing.space2X.value
}
}()
open var characterCounterLabel = Label().with {
$0.setContentCompressionResistancePriority(.required, for: .vertical)
$0.textStyle = .bodySmall
$0.textAlignment = .right
$0.numberOfLines = 1
}
private var _minHeight: Height = .twoX
open var minHeight: Height? {
get { return _minHeight }
set {
if let newValue {
_minHeight = newValue
} else {
_minHeight = .twoX
}
textViewHeightConstraint?.constant = _minHeight.value
setNeedsUpdate()
}
}
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
override var containerSize: CGSize { CGSize(width: 182, height: 88) }
/// Enum used to describe the the height of TextArea.
public enum Height: String, CaseIterable {
case twoX = "2X"
case fourX = "4X"
case eightX = "8X"
var value: CGFloat {
switch self {
case .twoX:
88
case .fourX:
176
case .eightX:
352
}
}
}
/// The text of this textField.
open override var text: String? {
get { textView.text }
set {
textView.text = newValue
value = newValue
}
}
/// UITextView shown in the TextArea.
open var textView = TextView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.sizeToFit()
$0.isScrollEnabled = false
}
/// Color configuration for error icon.
internal var iconColorConfiguration = ControlColorConfiguration().with {
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .normal)
}
/// Color configuration for character counter's highlight background color
internal var highlightBackgroundColor = ControlColorConfiguration().with {
$0.setSurfaceColors(VDSColor.backgroundPrimaryDark, VDSColor.backgroundPrimaryLight, forState: .normal)
}
/// Color configuration for character counter's highlight text color
internal var highlightTextColor = ControlColorConfiguration().with {
$0.setSurfaceColors(VDSColor.elementsPrimaryOndark, VDSColor.elementsPrimaryOnlight, forState: .normal)
}
//--------------------------------------------------
// 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()
accessibilityLabel = "TextArea"
containerStackView.pinToSuperView(.uniform(VDSFormControls.spaceInset))
minWidthConstraint = containerView.widthAnchor.constraint(greaterThanOrEqualToConstant: containerSize.width)
minWidthConstraint?.isActive = true
controlContainerView.addSubview(textView)
textView
.pinTop()
.pinLeading()
.pinTrailingLessThanOrEqualTo(nil, 0, .defaultHigh)
.pinBottom(0, .defaultHigh)
textView.isScrollEnabled = true
textView.autocorrectionType = .no
textViewHeightConstraint = textView.heightAnchor.constraint(greaterThanOrEqualToConstant: containerSize.height)
textViewHeightConstraint?.isActive = true
backgroundColorConfiguration.setSurfaceColors(VDSColor.feedbackSuccessBackgroundOnlight, VDSColor.feedbackSuccessBackgroundOndark, forState: .success)
borderColorConfiguration.setSurfaceColors(VDSColor.feedbackSuccessOnlight, VDSColor.feedbackSuccessOndark, forState: .success)
borderColorConfiguration.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .focused)
textView.delegate = self
characterCounterLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable()
bottomContainerStackView.spacing = VDSLayout.Spacing.space2X.value
}
/// Resets to default settings.
open override func reset() {
super.reset()
textView.text = ""
characterCounterLabel.reset()
characterCounterLabel.textStyle = .bodySmall
setNeedsUpdate()
}
/// Container for the area in which the user interacts.
open override func getContainer() -> UIView {
inputFieldStackView.addArrangedSubview(containerView)
return inputFieldStackView
}
/// Used to make changes to the View based off a change events or from local properties.
open override func updateView() {
super.updateView()
textView.isEditable = isEnabled
textView.isEnabled = isEnabled
textView.surface = surface
//set the width constraints
if let width {
widthConstraint?.constant = width
widthConstraint?.isActive = true
minWidthConstraint?.isActive = false
} else {
minWidthConstraint?.constant = containerSize.width
widthConstraint?.isActive = false
minWidthConstraint?.isActive = true
}
if let maxLength, maxLength > 0 {
// allow - 20% of character limit
let overflowLimit = Double(maxLength) * 0.20
allowCharCount = Int(overflowLimit) + maxLength
characterCounterLabel.text = getCharacterCounterText()
} else {
characterCounterLabel.text = ""
}
icon.size = .medium
icon.color = iconColorConfiguration.getColor(self)
containerView.layer.borderColor = readOnly ? readOnlyBorderColorConfiguration.getColor(self).cgColor : borderColorConfiguration.getColor(self).cgColor
textView.isEditable = readOnly ? false : true
textView.backgroundColor = backgroundColorConfiguration.getColor(self)
textView.tintColor = iconColorConfiguration.getColor(self)
characterCounterLabel.surface = surface
highlightCharacterOverflow()
}
/// Container for the area showing helper text, error text, character count, maximum length value.
open override func getBottomContainer() -> UIView {
bottomView.addSubview(bottomStackView)
bottomStackView.pinToSuperView()
bottomStackView.addArrangedSubview(bottomContainerView)
bottomStackView.addArrangedSubview(characterCounterLabel)
return bottomView
}
/// Used to update any Accessibility properties.
open override func updateAccessibility() {
super.updateAccessibility()
if showError {
setAccessibilityLabel(for: [titleLabel, textView, errorLabel, helperLabel])
} else {
setAccessibilityLabel(for: [titleLabel, textView, helperLabel])
}
}
//--------------------------------------------------
// MARK: - Private Methods
//--------------------------------------------------
private func getCharacterCounterText() -> String {
let count = textView.text.count
let countStr = (count > maxLength ?? 0) ? ("-" + "\(count-(maxLength ?? 0))") : "\(count)"
if ((maxLength ?? 0) > 0) {
if (count > (maxLength ?? 0)) {
showInternalError = true
internalErrorText = "You have exceeded the character limit."
return countStr
} else {
showInternalError = false
internalErrorText = nil
return ("\(countStr)" + "/" + "\(maxLength ?? 0)")
}
} else {
return ""
}
}
open func highlightCharacterOverflow() {
let count = textView.text.count
guard let maxLength, maxLength > 0, count > maxLength else {
textView.textAttributes = nil
return
}
var textAttributes = [any LabelAttributeModel]()
let location = maxLength
let length = count - maxLength
textAttributes.append(ColorLabelAttribute(location: location, length: length, color: highlightBackgroundColor.getColor(self), isForegroundColor: false))
textAttributes.append(ColorLabelAttribute(location: location, length: length, color: highlightTextColor.getColor(self), isForegroundColor: true))
textView.textAttributes = textAttributes
}
}
extension TextArea: UITextViewDelegate {
//--------------------------------------------------
// MARK: - UITextViewDelegate
//--------------------------------------------------
public func textViewDidChange(_ textView: UITextView) {
//dynamic textView Height sizing based on Figma
//if you want it to work "as-is" delete this code
//since it will autogrow with the current settings
if let textViewHeightConstraint, textView.isEditable {
var height = textView.contentSize.height
height = max(height, _minHeight.value)
if height > Height.twoX.value && height <= Height.fourX.value {
textViewHeightConstraint.constant = Height.fourX.value
} else if height > Height.fourX.value {
textViewHeightConstraint.constant = Height.eightX.value
} else {
textViewHeightConstraint.constant = Height.twoX.value
}
}
//The exceeding characters will be highlighted to help users correct their entry.
if ((maxLength ?? 0) > 0) {
if textView.text.count <= allowCharCount {
highlightCharacterOverflow()
//setting the value and firing control event
text = textView.text
sendActions(for: .valueChanged)
} else {
textView.text.removeLast()
highlightCharacterOverflow()
}
} else {
//setting the value and firing control event
text = textView.text
sendActions(for: .valueChanged)
}
}
}
/// Will move this into a new file, need to talk with Scott/Kyle
open class TextView: UITextView, ViewProtocol {
//--------------------------------------------------
// MARK: - Initializers
//--------------------------------------------------
required public init() {
super.init(frame: .zero, textContainer: nil)
initialSetup()
}
public override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
initialSetup()
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
initialSetup()
}
//--------------------------------------------------
// MARK: - Combine Properties
//--------------------------------------------------
/// Set of Subscribers for any Publishers for this Control.
open var subscribers = Set<AnyCancellable>()
//--------------------------------------------------
// MARK: - Private Properties
//--------------------------------------------------
private var initialSetupPerformed = false
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
/// Key of whether or not updateView() is called in setNeedsUpdate()
open var shouldUpdateView: Bool = true
open var surface: Surface = .light { didSet { setNeedsUpdate() } }
/// Array of LabelAttributeModel objects used in rendering the text.
open var textAttributes: [any LabelAttributeModel]? { didSet { setNeedsUpdate() } }
/// TextStyle used on the titleLabel.
open var textStyle: TextStyle { .defaultStyle }
/// Will determine if a scaled font should be used for the titleLabel font.
open var useScaledFont: Bool = false { didSet { setNeedsUpdate() } }
open var isEnabled: Bool = true { didSet { setNeedsUpdate() } }
open var textColorConfiguration: AnyColorable = ViewColorConfiguration().with {
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forDisabled: true)
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forDisabled: false)
}.eraseToAnyColorable(){ didSet { setNeedsUpdate() }}
open override var textColor: UIColor? {
get { textColorConfiguration.getColor(self) }
set { }
}
override public var text: String! {
get { super.text }
set {
super.text = newValue
updateLabel()
}
}
override public var textAlignment: NSTextAlignment {
didSet {
if textAlignment != oldValue {
// Text alignment can be part of our paragraph style, so we may need to
// re-style when changed
updateLabel()
}
}
}
//--------------------------------------------------
// MARK: - Lifecycle
//--------------------------------------------------
open func initialSetup() {
if !initialSetupPerformed {
backgroundColor = .clear
translatesAutoresizingMaskIntoConstraints = false
accessibilityCustomActions = []
setup()
setNeedsUpdate()
}
}
open func setup() {
translatesAutoresizingMaskIntoConstraints = false
}
open func updateView() {
updateLabel()
}
open func updateAccessibility() {}
open func reset() {
shouldUpdateView = false
surface = .light
text = nil
accessibilityCustomActions = []
shouldUpdateView = true
setNeedsUpdate()
}
//--------------------------------------------------
// MARK: - Private Methods
//--------------------------------------------------
private func updateLabel() {
//clear the arrays holding actions
accessibilityCustomActions = []
if let text, !text.isEmpty {
//create the primary string
let mutableText = NSMutableAttributedString.mutableText(for: text,
textStyle: textStyle,
useScaledFont: useScaledFont,
textColor: textColor!,
alignment: textAlignment,
lineBreakMode: .byWordWrapping)
//apply any attributes
if let attributes = textAttributes {
mutableText.apply(attributes: attributes)
}
attributedText = mutableText
} else {
attributedText = nil
}
}
}