vds_ios/VDS/Components/TextFields/TextArea/TextArea.swift
Matt Bruce bf4ff94933 moved default values into setDefaults()
Signed-off-by: Matt Bruce <matt.bruce@verizon.com>
2024-08-09 16:45:35 -05:00

311 lines
11 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 VDSCoreTokens
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.
@objcMembers
@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 override var responder: UIResponder? { textView }
internal var textViewHeightConstraint: NSLayoutConstraint?
internal var inputFieldStackView: UIStackView = {
return UIStackView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.axis = .horizontal
$0.distribution = .fill
$0.spacing = VDSLayout.space3X
}
}()
open var characterCounterLabel = Label().with {
$0.setContentCompressionResistancePriority(.required, for: .vertical)
$0.textStyle = .bodySmall
$0.textAlignment = .right
$0.numberOfLines = 1
}
open var minHeight: Height = .twoX { didSet { setNeedsUpdate() } }
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
override var containerSize: CGSize { CGSize(width: 182, height: Height.twoX.value) }
/// Enum used to describe the the height of TextArea.
public enum Height: String, CaseIterable {
case twoX = "2X"
case fourX = "4X"
case eightX = "8X"
var containerVerticalPadding: CGFloat { VDSLayout.space3X * 2 }
var value: CGFloat {
switch self {
case .twoX:
88 - containerVerticalPadding
case .fourX:
176 - containerVerticalPadding
case .eightX:
352 - containerVerticalPadding
}
}
}
/// The text of this TextArea.
private var _text: String?
open var text: String? {
get { textView.text }
set {
textView.text = newValue
setNeedsUpdate()
}
}
/// The value of this textField.
open override var value: String? {
return textView.text
}
/// UITextView shown in the TextArea.
open var textView = TextView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.sizeToFit()
$0.isAccessibilityElement = false
$0.isScrollEnabled = true
$0.textContainerInset = .zero
$0.autocorrectionType = .no
$0.spellCheckingType = .no
$0.textContainer.lineFragmentPadding = 0
}
open var maxLength: Int? {
willSet {
countRule.maxLength = newValue
}
didSet {
setNeedsUpdate()
if textView.isFirstResponder {
validate()
}
}
}
/// 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()
accessibilityHintText = "Double tap to edit"
textView.delegate = self
//events
textView
.publisher(for: .editingChanged)
.sink { [weak self] control in
self?.textViewDidChange(control)
}.store(in: &subscribers)
textView
.publisher(for: .editingDidBegin)
.sink { [weak self] _ in
self?.setNeedsUpdate()
}.store(in: &subscribers)
textView
.publisher(for: .editingDidEnd)
.sink { [weak self] _ in
self?.validate()
UIAccessibility.post(notification: .layoutChanged, argument: self?.containerView)
}.store(in: &subscribers)
textViewHeightConstraint = textView.heightAnchor.constraint(greaterThanOrEqualToConstant: containerSize.height)
textViewHeightConstraint?.isActive = true
characterCounterLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable()
bottomContainerStackView.spacing = VDSLayout.space2X
}
open override func setDefaults() {
super.setDefaults()
minHeight = .twoX
maxLength = nil
textView.text = ""
characterCounterLabel.textStyle = .bodySmall
}
/// Resets to default settings.
open override func reset() {
characterCounterLabel.reset()
super.reset()
}
/// 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
textViewHeightConstraint?.constant = minHeight.value
characterCounterLabel.text = getCharacterCounterText()
textView.isEditable = !isEnabled || isReadOnly ? false : true
textView.tintColor = iconColorConfiguration.getColor(self)
characterCounterLabel.surface = surface
highlightCharacterOverflow()
}
override func updateRules() {
super.updateRules()
if let maxLength, maxLength > 0 {
rules.append(.init(countRule))
}
}
open override func getFieldContainer() -> UIView {
textView
}
/// Container for the area showing helper text, error text, character count, maximum length value.
open override func getBottomContainer() -> UIView {
let stackView = UIStackView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.axis = .horizontal
$0.distribution = .fill
$0.alignment = .top
$0.spacing = VDSLayout.space2X
}
stackView.addArrangedSubview(super.getBottomContainer())
stackView.addArrangedSubview(characterCounterLabel)
return stackView
}
//--------------------------------------------------
// MARK: - Private Methods
//--------------------------------------------------
private func getCharacterCounterText() -> String? {
let count = textView.text.count
let countStr = (count > maxLength ?? 0) ? ("-" + "\(count-(maxLength ?? 0))") : "\(count)"
if let maxLength, maxLength > 0 {
if count > maxLength {
return countStr
} else {
return ("\(countStr)" + "/" + "\(maxLength)")
}
} else {
return nil
}
}
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 let maxLength, maxLength > 0 {
// allow - 20% of character limit
let overflowLimit = Double(maxLength) * 0.20
let allowCharCount = Int(overflowLimit) + maxLength
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)
}
validate()
}
private 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
}
//--------------------------------------------------
// MARK: - Validation
//--------------------------------------------------
var countRule = CharacterCountRule()
}
extension TextArea: UITextViewDelegate {
public func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
UIAccessibility.post(notification: .announcement, argument: text)
return true
}
}