Merge branch 'feature/TextArea' into 'develop'

VDS Brand 3.0 Text Area for IOS

See merge request BPHV_MIPS/vds_ios!156
This commit is contained in:
Bruce, Matt R 2024-02-29 20:42:48 +00:00
commit 6553810271
12 changed files with 535 additions and 52 deletions

View File

@ -7,6 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
186B2A8A2B88DA7F001AB71F /* TextAreaChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 186B2A892B88DA7F001AB71F /* TextAreaChangeLog.txt */; };
18792A902B7431F2008C0D29 /* ButtonIconBadgeIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18792A8F2B7431F2008C0D29 /* ButtonIconBadgeIndicatorModel.swift */; };
18BDEE822B75316E00452358 /* ButtonIconChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 18BDEE812B75316E00452358 /* ButtonIconChangeLog.txt */; };
445BA07829C07B3D0036A7C5 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 445BA07729C07B3D0036A7C5 /* Notification.swift */; };
@ -67,6 +68,7 @@
EA5F86C82A1BD99100BC83E4 /* TabModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5F86C72A1BD99100BC83E4 /* TabModel.swift */; };
EA5F86CC2A1D28B500BC83E4 /* ReleaseNotes.txt in Resources */ = {isa = PBXBuildFile; fileRef = EA5F86CB2A1D28B500BC83E4 /* ReleaseNotes.txt */; };
EA5F86D02A1F936100BC83E4 /* TabsContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5F86CF2A1F936100BC83E4 /* TabsContainer.swift */; };
EA6F330E2B911E9000BACAB9 /* TextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA6F330D2B911E9000BACAB9 /* TextView.swift */; };
EA81410B2A0E8E3C004F60D2 /* ButtonIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA81410A2A0E8E3C004F60D2 /* ButtonIcon.swift */; };
EA8141102A127066004F60D2 /* UIColor+VDSColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA81410F2A127066004F60D2 /* UIColor+VDSColor.swift */; };
EA89200428AECF4B006B9984 /* UITextField+Publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA89200328AECF4B006B9984 /* UITextField+Publisher.swift */; };
@ -173,6 +175,7 @@
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
186B2A892B88DA7F001AB71F /* TextAreaChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = TextAreaChangeLog.txt; sourceTree = "<group>"; };
18792A8F2B7431F2008C0D29 /* ButtonIconBadgeIndicatorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonIconBadgeIndicatorModel.swift; sourceTree = "<group>"; };
18BDEE812B75316E00452358 /* ButtonIconChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ButtonIconChangeLog.txt; sourceTree = "<group>"; };
445BA07729C07B3D0036A7C5 /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = "<group>"; };
@ -234,6 +237,7 @@
EA5F86C72A1BD99100BC83E4 /* TabModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabModel.swift; sourceTree = "<group>"; };
EA5F86CB2A1D28B500BC83E4 /* ReleaseNotes.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ReleaseNotes.txt; sourceTree = "<group>"; };
EA5F86CF2A1F936100BC83E4 /* TabsContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabsContainer.swift; sourceTree = "<group>"; };
EA6F330D2B911E9000BACAB9 /* TextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextView.swift; sourceTree = "<group>"; };
EA81410A2A0E8E3C004F60D2 /* ButtonIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonIcon.swift; sourceTree = "<group>"; };
EA81410F2A127066004F60D2 /* UIColor+VDSColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+VDSColor.swift"; sourceTree = "<group>"; };
EA89200328AECF4B006B9984 /* UITextField+Publisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextField+Publisher.swift"; sourceTree = "<group>"; };
@ -715,6 +719,8 @@
isa = PBXGroup;
children = (
EA985C22296E033A00F2FF2E /* TextArea.swift */,
EA6F330D2B911E9000BACAB9 /* TextView.swift */,
186B2A892B88DA7F001AB71F /* TextAreaChangeLog.txt */,
);
path = TextArea;
sourceTree = "<group>";
@ -946,6 +952,7 @@
EA3362042891E14D0071C351 /* VerizonNHGeTX-Bold.otf in Resources */,
71C02B382B7BD98F00E93E66 /* NotificationChangeLog.txt in Resources */,
EAEEECA72B1F952000531FC2 /* TabsChangeLog.txt in Resources */,
186B2A8A2B88DA7F001AB71F /* TextAreaChangeLog.txt in Resources */,
EAEEEC962B1F893B00531FC2 /* ButtonChangeLog.txt in Resources */,
EA5F86CC2A1D28B500BC83E4 /* ReleaseNotes.txt in Resources */,
EAEEEC982B1F8DD100531FC2 /* LineChangeLog.txt in Resources */,
@ -1055,6 +1062,7 @@
EAF1FE9B29DB1A6000101452 /* Changeable.swift in Sources */,
EAF7F0A2289AFB3900B287F5 /* Errorable.swift in Sources */,
EA8E40912A7D3F6300934ED3 /* UIView+Accessibility.swift in Sources */,
EA6F330E2B911E9000BACAB9 /* TextView.swift in Sources */,
EA985C7D297DAED300F2FF2E /* Primitive.swift in Sources */,
EAF1FE9929D4850E00101452 /* Clickable.swift in Sources */,
EAD0688E2A55F819002E3A2D /* Loader.swift in Sources */,

View File

@ -30,6 +30,7 @@ public struct AnyAttribute: LabelAttributeModel {
}
public func setAttribute(on attributedString: NSMutableAttributedString) {
guard isValidRange(on: attributedString) else { return }
attributedString.removeAttribute(key, range: range)
attributedString.addAttribute(key, value: value, range: range)
}

View File

@ -31,6 +31,8 @@ public struct ColorLabelAttribute: LabelAttributeModel {
}
public func setAttribute(on attributedString: NSMutableAttributedString) {
guard isValidRange(on: attributedString) else { return }
var colorRange = range
if length == 0 && location == 0 {
colorRange = .init(location: location, length: attributedString.length)

View File

@ -29,6 +29,10 @@ extension LabelAttributeModel {
public static func == (lhs: any LabelAttributeModel, rhs: any LabelAttributeModel) -> Bool {
lhs.isEqual(rhs)
}
public func isValidRange(on attributedString: NSMutableAttributedString) -> Bool {
range.location + range.length <= attributedString.string.count
}
}
public extension NSAttributedString {

View File

@ -24,6 +24,7 @@ public struct StrikeThroughLabelAttribute: LabelAttributeModel {
}
public func setAttribute(on attributedString: NSMutableAttributedString) {
guard isValidRange(on: attributedString) else { return }
attributedString.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.thick.rawValue, range: range)
attributedString.addAttribute(.baselineOffset, value: 0, range: range)
}

View File

@ -44,6 +44,7 @@ public struct TextStyleLabelAttribute: LabelAttributeModel {
}
public func setAttribute(on attributedString: NSMutableAttributedString) {
guard isValidRange(on: attributedString) else { return }
attributedString.removeAttribute(.font, range: range)
attributedString.addAttribute(.font, value: textStyle.font, range: range)
if let textColor {

View File

@ -52,7 +52,8 @@ public struct UnderlineLabelAttribute: LabelAttributeModel {
//--------------------------------------------------
// MARK: - Public Methods
//--------------------------------------------------
public func setAttribute(on attributedString: NSMutableAttributedString) {
public func setAttribute(on attributedString: NSMutableAttributedString) {
guard isValidRange(on: attributedString) else { return }
attributedString.addAttribute(.underlineStyle, value: underlineValue.rawValue, range: range)
if let color = color {
attributedString.addAttribute(.underlineColor, value: color, range: range)

View File

@ -13,7 +13,7 @@ import Combine
/// Base Class used to build out a Input controls.
@objc(VDSEntryField)
open class EntryFieldBase: Control, Changeable {
open class EntryFieldBase: Control, Changeable, FormFieldable {
//--------------------------------------------------
// MARK: - Initializers
@ -59,7 +59,7 @@ open class EntryFieldBase: Control, Changeable {
return UIStackView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.axis = .horizontal
$0.distribution = .fillProportionally
$0.distribution = .fill
$0.alignment = .top
}
}()
@ -70,6 +70,20 @@ open class EntryFieldBase: Control, Changeable {
}
}()
internal var bottomContainerView: UIView = {
return UIView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
}
}()
internal var bottomContainerStackView: UIStackView = {
return UIStackView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.axis = .vertical
$0.distribution = .fill
}
}()
//--------------------------------------------------
// MARK: - Configuration Properties
//--------------------------------------------------
@ -93,11 +107,15 @@ open class EntryFieldBase: Control, Changeable {
}
internal var borderColorConfiguration = ControlColorConfiguration().with {
$0.setSurfaceColors(VDSFormControlsColor.borderOnlight, VDSFormControlsColor.borderOnlight, forState: .normal)
$0.setSurfaceColors(VDSFormControlsColor.borderOnlight, VDSFormControlsColor.borderOndark, forState: .normal)
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled)
$0.setSurfaceColors(VDSColor.feedbackErrorOnlight, VDSColor.feedbackErrorOndark, forState: .error)
}
internal var readOnlyBorderColorConfiguration = ControlColorConfiguration().with {
$0.setSurfaceColors(VDSFormControlsColor.borderReadonlyOnlight, VDSFormControlsColor.borderReadonlyOndark, forState: .normal)
}
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
@ -135,19 +153,42 @@ open class EntryFieldBase: Control, Changeable {
/// Whether not to show the error.
open var showError: Bool = false { didSet { setNeedsUpdate() } }
/// Whether or not to show the internal error
internal var showInternalError: Bool = false { didSet { setNeedsUpdate() } }
/// Override UIControl state to add the .error state if showError is true.
open override var state: UIControl.State {
get {
var state = super.state
if showError {
if showError || showInternalError {
state.insert(.error)
}
return state
}
}
open var errorText: String? {
didSet {
updateContainerView()
updateErrorLabel()
setNeedsUpdate()
}
}
internal var internalErrorText: String? {
didSet {
updateContainerView()
updateErrorLabel()
setNeedsUpdate()
}
}
/// Override this to conveniently get/set the textfield(s).
open var text: String? {
get { nil }
set { fatalError("You MUST override EntryField's 'text' variable in your subclass.") }
}
open var errorText: String? { didSet { setNeedsUpdate() } }
open var tooltipModel: Tooltip.TooltipModel? { didSet { setNeedsUpdate() } }
open var transparentBackground: Bool = false { didSet { setNeedsUpdate() } }
@ -157,8 +198,20 @@ open class EntryFieldBase: Control, Changeable {
open var maxLength: Int? { didSet { setNeedsUpdate() } }
open var inputId: String? { didSet { setNeedsUpdate() } }
/// The text of this textField.
private var _value: AnyHashable?
open var value: AnyHashable? {
get { _value }
set {
if let newValue, newValue != _value {
_value = newValue
text = newValue as? String
}
setNeedsUpdate()
}
}
open var value: AnyHashable? { didSet { setNeedsUpdate() } }
open var defaultValue: AnyHashable? { didSet { setNeedsUpdate() } }
@ -184,7 +237,7 @@ open class EntryFieldBase: Control, Changeable {
//create the wrapping view
heightConstraint = containerView.heightAnchor.constraint(greaterThanOrEqualToConstant: containerSize.height)
widthConstraint?.priority = .defaultHigh
heightConstraint?.priority = .defaultHigh
heightConstraint?.isActive = true
widthConstraint = containerView.widthAnchor.constraint(equalToConstant: 0)
@ -203,15 +256,26 @@ open class EntryFieldBase: Control, Changeable {
//add the view to add input fields
containerStackView.addArrangedSubview(controlContainerView)
containerStackView.addArrangedSubview(icon)
containerStackView.setCustomSpacing(VDSLayout.Spacing.space3X.value, after: controlContainerView)
//get the container this is what show helper text, error text
//can include other for character count, max length
let bottomContainer = getBottomContainer()
//add bottomContainerStackView
//this is the vertical stack that contains error text, helper text
bottomContainer.addSubview(bottomContainerStackView)
bottomContainerStackView.pinToSuperView()
bottomContainerStackView.addArrangedSubview(errorLabel)
bottomContainerStackView.addArrangedSubview(helperLabel)
stackView.addArrangedSubview(titleLabel)
stackView.addArrangedSubview(container)
stackView.addArrangedSubview(errorLabel)
stackView.addArrangedSubview(helperLabel)
stackView.addArrangedSubview(bottomContainer)
stackView.setCustomSpacing(4, after: titleLabel)
stackView.setCustomSpacing(8, after: container)
stackView.setCustomSpacing(8, after: errorLabel)
stackView.setCustomSpacing(8, after: bottomContainer)
stackView
.pinTop()
@ -254,11 +318,7 @@ open class EntryFieldBase: Control, Changeable {
open override func updateView() {
super.updateView()
containerView.backgroundColor = backgroundColorConfiguration.getColor(self)
containerView.layer.borderColor = borderColorConfiguration.getColor(self).cgColor
containerView.layer.borderWidth = VDSFormControls.widthBorder
containerView.layer.cornerRadius = VDSFormControls.borderradius
updateContainerView()
updateTitleLabel()
updateErrorLabel()
updateHelperLabel()
@ -266,6 +326,16 @@ open class EntryFieldBase: Control, Changeable {
backgroundColor = surface.color
}
//--------------------------------------------------
// MARK: - Private Methods
//--------------------------------------------------
private func updateContainerView() {
containerView.backgroundColor = backgroundColorConfiguration.getColor(self)
containerView.layer.borderColor = borderColorConfiguration.getColor(self).cgColor
containerView.layer.borderWidth = VDSFormControls.widthBorder
containerView.layer.cornerRadius = VDSFormControls.borderradius
}
//--------------------------------------------------
// MARK: - Public Methods
//--------------------------------------------------
@ -273,6 +343,11 @@ open class EntryFieldBase: Control, Changeable {
open func getContainer() -> UIView {
return containerView
}
/// Container for the area in which helper or error text presents.
open func getBottomContainer() -> UIView {
return bottomContainerView
}
open func updateTitleLabel() {
@ -305,7 +380,16 @@ open class EntryFieldBase: Control, Changeable {
}
open func updateErrorLabel(){
if showError, let errorText {
if showError, showInternalError, let errorText, let internalErrorText {
errorLabel.text = [internalErrorText, errorText].joined(separator: "\n")
errorLabel.surface = surface
errorLabel.isEnabled = isEnabled
errorLabel.isHidden = false
icon.name = .error
icon.color = VDSColor.paletteBlack
icon.surface = surface
icon.isHidden = !isEnabled
} else if showError, let errorText {
errorLabel.text = errorText
errorLabel.surface = surface
errorLabel.isEnabled = isEnabled
@ -314,6 +398,15 @@ open class EntryFieldBase: Control, Changeable {
icon.color = VDSColor.paletteBlack
icon.surface = surface
icon.isHidden = !isEnabled
} else if showInternalError, let internalErrorText {
errorLabel.text = internalErrorText
errorLabel.surface = surface
errorLabel.isEnabled = isEnabled
errorLabel.isHidden = false
icon.name = .error
icon.color = VDSColor.paletteBlack
icon.surface = surface
icon.isHidden = !isEnabled
} else {
icon.isHidden = true
errorLabel.isHidden = true

View File

@ -78,6 +78,18 @@ open class InputField: EntryFieldBase, UITextFieldDelegate {
/// Representing the type of input.
open var fieldType: FieldType = .text { didSet { setNeedsUpdate() } }
/// The text of this textField.
open override var text: String? {
get { textField.text }
set {
if let newValue, newValue != text {
textField.text = newValue
value = newValue
}
setNeedsUpdate()
}
}
var _showError: Bool = false
/// Whether not to show the error.
open override var showError: Bool {

View File

@ -5,7 +5,6 @@
// Created by Matt Bruce on 1/10/23.
//
import Foundation
import Foundation
import UIKit
import VDSColorTokens
@ -36,64 +35,147 @@ open class TextArea: EntryFieldBase {
//--------------------------------------------------
internal var minWidthConstraint: NSLayoutConstraint?
internal var textViewHeightConstraint: NSLayoutConstraint?
internal var inputFieldStackView: UIStackView = {
return UIStackView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.axis = .horizontal
$0.distribution = .fill
$0.spacing = 12
$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: 45, height: 88) }
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 textView
open override var text: String? {
get { textView.text }
set {
if let newValue, newValue != text {
textView.text = newValue
value = newValue
}
setNeedsUpdate()
}
}
/// UITextView shown in the TextArea.
open var textView = UITextView().with {
open var textView = TextView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.font = TextStyle.bodyLarge.font
$0.sizeToFit()
$0.isScrollEnabled = false
}
/// Color configuration for the textView.
open var textViewTextColorConfiguration: AnyColorable = ViewColorConfiguration().with {
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forDisabled: true)
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forDisabled: false)
}.eraseToAnyColorable() { didSet { setNeedsUpdate() } }
/// 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"
minWidthConstraint = containerView.widthAnchor.constraint(greaterThanOrEqualToConstant: 0)
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)
textViewHeightConstraint = textView.heightAnchor.constraint(greaterThanOrEqualToConstant: 64)
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.
@ -107,7 +189,8 @@ open class TextArea: EntryFieldBase {
super.updateView()
textView.isEditable = isEnabled
textView.textColor = textViewTextColorConfiguration.getColor(self)
textView.isEnabled = isEnabled
textView.surface = surface
//set the width constraints
if let width {
@ -119,6 +202,80 @@ open class TextArea: EntryFieldBase {
widthConstraint?.isActive = false
minWidthConstraint?.isActive = true
}
let characterError = getCharacterCounterText()
if let maxLength, maxLength > 0 {
characterCounterLabel.text = characterError
} 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 let maxLength, maxLength > 0 {
if count > maxLength {
showInternalError = true
internalErrorText = "You have exceeded the character limit."
return countStr
} else {
showInternalError = false
internalErrorText = nil
return ("\(countStr)" + "/" + "\(maxLength)")
}
} else {
showInternalError = false
internalErrorText = nil
return nil
}
}
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
}
}
@ -127,27 +284,42 @@ 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 {
let height = textView.frame.size.height
let constraintHeight = textViewHeightConstraint.constant
if height > constraintHeight {
if height > 64 && height < 152 {
textViewHeightConstraint.constant = 152
} else if height > 152 {
textViewHeightConstraint.constant = 328
} else {
textViewHeightConstraint.constant = 64
}
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
}
}
//setting the value and firing control event
value = textView.text
sendActions(for: .valueChanged)
//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)
}
}
}

View File

@ -0,0 +1,38 @@
MM/DD/YYYY
----------------
- Initial Brand 3.0 handoff
12/27/2021
----------------
- Removed Max idth Updated the SPECS with FormControl tokens
02/25/2022
----------------
- Replaced Info and Error Non-Scaling icons with VDS Icon.
- Removed “weight” and “vector effect” from Anatomy and States.
07/27/2022
----------------
- Added Configurations section with transparentBackground principles.
08/10/2022
----------------
- Updated default and inverted prop to light and dark surface.
11/30/2022
----------------
- Added "(web only)" to any instance of "keyboard focus"
12/13/2022
----------------
- Replaced form border and focus border pixel values and style & spacing with tokens.
01/18/2023
----------------
- Updated Anatomy items:
- Added “Highlight” to item #10
- Changed item #7 to “Tooltip” from “Tooltip Component”
04/12/2023
----------------
- Updated hex colors for updated feedback tokens in error states.

View File

@ -0,0 +1,150 @@
//
// TextView.swift
// VDS
//
// Created by Matt Bruce on 2/29/24.
//
import Foundation
import UIKit
import Combine
import VDSColorTokens
/// 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
}
}
}