From 40a31dfe259d5a282f143cc8ec9158f76391de68 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 5 Jun 2024 15:18:32 -0500 Subject: [PATCH 1/8] =?UTF-8?q?CXTDT-560823=20=E2=80=93=20TextArea=20?= =?UTF-8?q?=E2=80=93=20Accessibility=20Labels/Error/ReadyOnly/Disabled.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Matt Bruce --- VDS/Components/DatePicker/DatePicker.swift | 7 +------ .../DropdownSelect/DropdownSelect.swift | 7 +------ VDS/Components/TextFields/EntryFieldBase.swift | 17 +++++++++++++++++ .../TextFields/InputField/InputField.swift | 9 ++------- .../TextFields/TextArea/TextArea.swift | 7 +------ VDS/SupportingFiles/ReleaseNotes.txt | 1 + 6 files changed, 23 insertions(+), 25 deletions(-) diff --git a/VDS/Components/DatePicker/DatePicker.swift b/VDS/Components/DatePicker/DatePicker.swift index 7a158ff6..7645cce5 100644 --- a/VDS/Components/DatePicker/DatePicker.swift +++ b/VDS/Components/DatePicker/DatePicker.swift @@ -147,12 +147,7 @@ open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopov open override func updateAccessibility() { super.updateAccessibility() - let label = "Date Picker, \(isReadOnly ? ", read only" : "")" - if let errorText, showError { - fieldStackView.accessibilityLabel = "\(label) ,error, \(errorText)" - } else { - fieldStackView.accessibilityLabel = label - } + fieldStackView.accessibilityLabel = "Date Picker, \(accessibilityLabelText)" fieldStackView.accessibilityHint = isReadOnly || !isEnabled ? "" : "Double tap to open." fieldStackView.accessibilityValue = value } diff --git a/VDS/Components/DropdownSelect/DropdownSelect.swift b/VDS/Components/DropdownSelect/DropdownSelect.swift index bad62536..e001d130 100644 --- a/VDS/Components/DropdownSelect/DropdownSelect.swift +++ b/VDS/Components/DropdownSelect/DropdownSelect.swift @@ -278,12 +278,7 @@ open class DropdownSelect: EntryFieldBase { open override func updateAccessibility() { super.updateAccessibility() - let label = "Dropdown Select, \(isReadOnly ? ", read only" : "")" - if let errorText, showError { - fieldStackView.accessibilityLabel = "\(label) ,error, \(errorText)" - } else { - fieldStackView.accessibilityLabel = label - } + fieldStackView.accessibilityLabel = "Dropdown Select, \(accessibilityLabelText)" fieldStackView.accessibilityHint = isReadOnly || !isEnabled ? "" : "Double tap to open." fieldStackView.accessibilityValue = value } diff --git a/VDS/Components/TextFields/EntryFieldBase.swift b/VDS/Components/TextFields/EntryFieldBase.swift index ea023736..f56f78a2 100644 --- a/VDS/Components/TextFields/EntryFieldBase.swift +++ b/VDS/Components/TextFields/EntryFieldBase.swift @@ -241,6 +241,23 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { open var rules = [AnyRule]() + open var accessibilityLabelText: String { + var accessibilityLabels = [String]() + if let text = titleLabel.text { + accessibilityLabels.append(text) + } + if isReadOnly { + accessibilityLabels.append("read only") + } + if !isEnabled { + accessibilityLabels.append("dimmed") + } + if let errorText, showError { + accessibilityLabels.append("error, \(errorText)") + } + return accessibilityLabels.joined(separator: ", ") + } + //-------------------------------------------------- // MARK: - Overrides //-------------------------------------------------- diff --git a/VDS/Components/TextFields/InputField/InputField.swift b/VDS/Components/TextFields/InputField/InputField.swift index 516954cd..af358dbf 100644 --- a/VDS/Components/TextFields/InputField/InputField.swift +++ b/VDS/Components/TextFields/InputField/InputField.swift @@ -225,15 +225,10 @@ open class InputField: EntryFieldBase { textField.isUserInteractionEnabled = isEnabled && !isReadOnly textField.textColor = textFieldTextColorConfiguration.getColor(self) } - + open override func updateAccessibility() { super.updateAccessibility() - let label = "\(isReadOnly ? "read only" : "")" - if let errorText, showError { - textField.accessibilityLabel = "\(label) ,error, \(errorText)" - } else { - textField.accessibilityLabel = label - } + textField.accessibilityLabel = accessibilityLabelText textField.accessibilityHint = isReadOnly || !isEnabled ? "" : "Double tap to open." } diff --git a/VDS/Components/TextFields/TextArea/TextArea.swift b/VDS/Components/TextFields/TextArea/TextArea.swift index 870cf6b6..b0d858df 100644 --- a/VDS/Components/TextFields/TextArea/TextArea.swift +++ b/VDS/Components/TextFields/TextArea/TextArea.swift @@ -195,12 +195,7 @@ open class TextArea: EntryFieldBase { open override func updateAccessibility() { super.updateAccessibility() - let label = "\(isReadOnly ? "read only" : "")" - if let errorText, showError { - textView.accessibilityLabel = "\(label) ,error, \(errorText)" - } else { - textView.accessibilityLabel = label - } + textView.accessibilityLabel = accessibilityLabelText textView.accessibilityHint = isReadOnly || !isEnabled ? "" : "Double tap to open." } diff --git a/VDS/SupportingFiles/ReleaseNotes.txt b/VDS/SupportingFiles/ReleaseNotes.txt index e39df158..3c1fee48 100644 --- a/VDS/SupportingFiles/ReleaseNotes.txt +++ b/VDS/SupportingFiles/ReleaseNotes.txt @@ -3,6 +3,7 @@ - CXTDT-565087 - Input Field - Text - OnDark colors - CXTDT-565112 - Input Field - Credit Card icons - CXTDT-565117 - Input Field - Overflow not clipped +- CXTDT-560823 – TextArea – Accessibility Labels/Error/ReadyOnly/Disabled 1.0.65 ---------------- From a71085f2b3401579d0802e035584dff2d3f1994f Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 5 Jun 2024 16:16:13 -0500 Subject: [PATCH 2/8] added release notes Signed-off-by: Matt Bruce --- VDS/SupportingFiles/ReleaseNotes.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/VDS/SupportingFiles/ReleaseNotes.txt b/VDS/SupportingFiles/ReleaseNotes.txt index 3c1fee48..2018da35 100644 --- a/VDS/SupportingFiles/ReleaseNotes.txt +++ b/VDS/SupportingFiles/ReleaseNotes.txt @@ -4,6 +4,8 @@ - CXTDT-565112 - Input Field - Credit Card icons - CXTDT-565117 - Input Field - Overflow not clipped - CXTDT-560823 – TextArea – Accessibility Labels/Error/ReadyOnly/Disabled +- CXTDT-553663 - DropdownSelect – Accessibility +- CXTDT-544662 - Breadcrumbs - Text Wrapping 1.0.65 ---------------- From f5380532ff26af9a0c0b24960be986fb243334db Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 5 Jun 2024 17:32:45 -0500 Subject: [PATCH 3/8] fixed bug for Accessibility Elements in Label Signed-off-by: Matt Bruce --- VDS/Components/Label/Label.swift | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/VDS/Components/Label/Label.swift b/VDS/Components/Label/Label.swift index 9e4dd18e..7d14a85f 100644 --- a/VDS/Components/Label/Label.swift +++ b/VDS/Components/Label/Label.swift @@ -314,8 +314,11 @@ open class Label: UILabel, ViewProtocol, UserInfoable { super.text = newValue return } - + + //clear out accessibility + accessibilityElements?.removeAll() accessibilityCustomActions = [] + //create the primary string let mutableText = NSMutableAttributedString.mutableText(for: newValue, textStyle: textStyle, @@ -337,6 +340,10 @@ open class Label: UILabel, ViewProtocol, UserInfoable { return } + //clear out accessibility + accessibilityElements?.removeAll() + accessibilityCustomActions = [] + let mutableText = NSMutableAttributedString(attributedString: newValue) applyAttributes(mutableText) @@ -348,7 +355,7 @@ open class Label: UILabel, ViewProtocol, UserInfoable { private func applyAttributes(_ mutableAttributedString: NSMutableAttributedString) { actions = [] - if let attributes = attributes { + if let attributes { mutableAttributedString.apply(attributes: attributes) } } @@ -359,7 +366,7 @@ open class Label: UILabel, ViewProtocol, UserInfoable { let mutableAttributedString = NSMutableAttributedString(attributedString: attributedText) - if let attributes = attributes { + if let attributes { //loop through the models attributes for attribute in attributes { From 2e46759857e9b93e8f7ec680bd94295675fa70fd Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 6 Jun 2024 12:22:21 -0500 Subject: [PATCH 4/8] fixed credit card issues with formatting for different card types also added an min/max length based on card type. Signed-off-by: Matt Bruce --- .../InputField/FieldTypes/CreditCard.swift | 56 ++++++++++++++----- 1 file changed, 43 insertions(+), 13 deletions(-) diff --git a/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift b/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift index bd512b50..fef4c77a 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift @@ -9,7 +9,20 @@ import Foundation import UIKit extension InputField { - + class CreditCardNumberRule: Rule, Withable { + var cardType: CreditCardType? + var errorMessage: String = "You have exceeded the character limit." + + func isValid(value: String?) -> Bool { + guard let count = value?.count, let min = cardType?.minLength, let max = cardType?.maxLength else { return true } + if min == max { + return count == max + } else { + return count >= min && count <= max + } + } + } + public enum CreditCardType: String, CaseIterable { case generic case visa @@ -38,15 +51,22 @@ extension InputField { } } - var separatorIndices: [Int] { + func separatorIndices(_ length: Int) -> [Int] { + var indices: [Int] = [4, 8, 12] switch self { - case .dinersClub: - return [4, 10] + case .amex, .dinersClub: + indices = [4, 10] + case .unionPay: + if length == 19 { + indices = [5] + } default: - return [4, 8, 12] + break } + + return indices } - + var securityCodeLength: Int { if self == .amex { return 4 @@ -55,9 +75,21 @@ extension InputField { } } + var minLength: Int { + switch self { + case .visa: return 13 + case .amex: return 15 + case .dinersClub: return 14 + default: return 16 + } + } + var maxLength: Int { switch self { + case .visa: return 19 + case .amex: return 15 case .dinersClub: return 14 + case .unionPay: return 19 default: return 16 } } @@ -131,9 +163,8 @@ extension InputField { override func appendRules(_ inputField: InputField) { if let text = inputField.textField.text, text.count > 0 { - let rule = CharacterCountRule().copyWith { - $0.maxLength = inputField.cardType.maxLength - $0.compareType = .equals + let rule = CreditCardNumberRule().copyWith { + $0.cardType = inputField.cardType $0.errorMessage = "Enter a valid credit card." } inputField.rules.append(.init(rule)) @@ -205,8 +236,8 @@ extension InputField { /// Private internal func formatCreditCardNumber(_ cardType: CreditCardType, number: String) -> String { - let formattedInput = number.filter { $0.isNumber } // Remove any existing slashes - return String.format(formattedInput, indices: cardType.separatorIndices, with: " ") + let rawNumber = number.filter { $0.isNumber } // Remove any existing slashes + return String.format(rawNumber, indices: cardType.separatorIndices(rawNumber.count), with: " ") } internal func updateCardTypeIcon(_ inputField: InputField, rawNumber: String) { @@ -224,9 +255,8 @@ extension InputField { guard rawNumber.count == cardType.maxLength else { return formatCreditCardNumber(cardType, number: number) } let lastFourDigits = rawNumber.suffix(4) let maskedSection = String(repeating: "•", count: 12) - let formattedMaskSection = String.format(maskedSection, indices: cardType.separatorIndices, with: " ") + let formattedMaskSection = String.format(maskedSection, indices: cardType.separatorIndices(rawNumber.count), with: " ") return formattedMaskSection + " " + lastFourDigits } } - } From ba0989bb2ba4574d61e19c2b4de45f845229f25c Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 6 Jun 2024 15:54:56 -0500 Subject: [PATCH 5/8] updated credit card rule Signed-off-by: Matt Bruce --- .../TextFields/InputField/FieldTypes/CreditCard.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift b/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift index fef4c77a..bc3a289a 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift @@ -9,11 +9,11 @@ import Foundation import UIKit extension InputField { - class CreditCardNumberRule: Rule, Withable { - var cardType: CreditCardType? - var errorMessage: String = "You have exceeded the character limit." + public class CreditCardNumberRule: Rule, Withable { + public var cardType: CreditCardType? + public var errorMessage: String = "You have exceeded the character limit." - func isValid(value: String?) -> Bool { + public func isValid(value: String?) -> Bool { guard let count = value?.count, let min = cardType?.minLength, let max = cardType?.maxLength else { return true } if min == max { return count == max From 8a686314f515312c1c661198be5dcc42f3aa729e Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 6 Jun 2024 16:18:03 -0500 Subject: [PATCH 6/8] added a format label Signed-off-by: Matt Bruce --- .../TextFields/InputField/TextField.swift | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/VDS/Components/TextFields/InputField/TextField.swift b/VDS/Components/TextFields/InputField/TextField.swift index b054741d..a2980b3b 100644 --- a/VDS/Components/TextFields/InputField/TextField.swift +++ b/VDS/Components/TextFields/InputField/TextField.swift @@ -47,6 +47,17 @@ open class TextField: UITextField, ViewProtocol, Errorable { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- + private var formatLabel = Label().with { + $0.tag = 999 + $0.textColorConfiguration = ViewColorConfiguration().with { + $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forDisabled: true) + $0.setSurfaceColors(VDSColor.elementsSecondaryOnlight, VDSColor.elementsSecondaryOndark, forDisabled: false) + }.eraseToAnyColorable() + } + + /// Format String similar to placeholder + open var formatText: String? + /// TextStyle used on the titleLabel. open var textStyle: TextStyle = .defaultStyle { didSet { setNeedsUpdate() } } @@ -114,6 +125,37 @@ open class TextField: UITextField, ViewProtocol, Errorable { open func updateView() { updateLabel() + updateFormat() + } + + open func updateFormat() { + guard let formatText else { + formatLabel.text = "" + return + } + + if viewWithTag(999) == nil { + addSubview(formatLabel) + formatLabel.pinToSuperView() + } + + var attributes: [any LabelAttributeModel]? + var finalFormatText = formatText + + if let text, !text.isEmpty { + //make the color of the matching text clear + attributes = [ColorLabelAttribute(location: 0, length: text.count, color: .clear)] + + let startIndex = formatText.index(formatText.startIndex, offsetBy: text.count) + if startIndex < formatText.endIndex { + finalFormatText = text + formatText[startIndex...] + } + } + + //set the label + formatLabel.surface = surface + formatLabel.text = finalFormatText + formatLabel.attributes = attributes } open func updateAccessibility() { From f7ded238578468d1b0ea87ae28a5da43bd2fb276 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 6 Jun 2024 16:18:32 -0500 Subject: [PATCH 7/8] CXTDT-565105 - InputField - Date - Typeover text not working. Signed-off-by: Matt Bruce --- .../InputField/FieldTypes/Date.swift | 125 +++++++++++++++++- .../TextFields/InputField/InputField.swift | 11 +- 2 files changed, 125 insertions(+), 11 deletions(-) diff --git a/VDS/Components/TextFields/InputField/FieldTypes/Date.swift b/VDS/Components/TextFields/InputField/FieldTypes/Date.swift index 19656745..17decc96 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/Date.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/Date.swift @@ -10,6 +10,18 @@ import UIKit extension InputField { + public class DateRule: Rule, Withable { + public var dateFormat: DateFormat? + public var errorMessage: String = "Enter a valid date" + private let dateFormatter = DateFormatter() + + public func isValid(value: String?) -> Bool { + guard let value, let dateFormat, !value.isEmpty else { return true } + dateFormatter.dateFormat = dateFormat.formatString + return dateFormatter.date(from: value) != nil + } + } + public enum DateFormat: String, CaseIterable { case mmyy case mmddyy @@ -46,6 +58,102 @@ extension InputField { case .mmddyyyy: [2,4] } } + + public func isValid(_ date: String) -> Bool { + let allowedCharacters = CharacterSet(charactersIn: "0123456789/") + + // Check if the input contains only allowed characters + if date.rangeOfCharacter(from: allowedCharacters.inverted) != nil || date.isEmpty { + return false + } + + let components = date.split(separator: "/") + + + func isMonth(_ month: String) -> Bool { + switch month.count { + case 1: + guard let month = Int(month), (0...1).contains(month) else { return false } + return true + case 2: + guard let month = Int(month), (1...12).contains(month) else { return false } + return true + default: + return false + } + } + + func isDay(_ day: String) -> Bool { + switch day.count { + case 1: + guard let day = Int(day),(1...3).contains(day) else { return false } + return true + case 2: + guard let day = Int(day), (1...31).contains(day) else { return false } + return true + default: + return false + } + } + + func isYear(_ year: String, max: Int) -> Bool { + guard year.count <= max else { + return false + } + return true + } + + switch self { + case .mmyy: + if components.count > 2 { + return false + } + + // Validate month part + if components.count > 0, let monthPart = components.first { + if !isMonth(String(monthPart)) { + return false + } + } + + // Validate year part + if components.count > 1, let yearPart = components.last { + if !isYear(String(yearPart), max: 2) { + return false + } + } + + case .mmddyy, .mmddyyyy: + if components.count > 3 { + return false + } + + // Validate month part + if components.count > 0, let monthPart = components.first { + if !isMonth(String(monthPart)) { + return false + } + } + + // Validate day part + if components.count > 1 { + let dayPart = components[1] + if !isDay(String(dayPart)) { + return false + } + } + + // Validate year part + if components.count > 2, let yearPart = components.last { + if !isYear(String(yearPart), max: self == .mmddyy ? 2 : 4) { + return false + } + } + } + + return true + } + } class DateHandler: FieldTypeHandler { @@ -58,16 +166,15 @@ extension InputField { override func updateView(_ inputField: InputField) { minWidth = 114.0 - placeholderText = inputField.dateFormat.placeholderText - + //placeholderText = inputField.dateFormat.placeholderText + inputField.textField.formatText = inputField.dateFormat.placeholderText super.updateView(inputField) } override func appendRules(_ inputField: InputField) { if let text = inputField.textField.text, text.count > 0 { - let rule = CharacterCountRule().copyWith { - $0.maxLength = inputField.dateFormat.maxLength - $0.compareType = .equals + let rule = DateRule().copyWith { + $0.dateFormat = inputField.dateFormat $0.errorMessage = "Enter a valid date." } inputField.rules.append(.init(rule)) @@ -86,9 +193,13 @@ extension InputField { if newText.count > inputField.dateFormat.maxLength { return false } - + if newText.count <= inputField.dateFormat.maxLength { - textField.text = String.format(newText, indices: inputField.dateFormat.separatorIndices, with: "/") + let rawNumber = newText.filter { $0.isNumber } + let formatted = String.format(rawNumber, indices: inputField.dateFormat.separatorIndices, with: "/") + if inputField.dateFormat.isValid(formatted) || formatted.isEmpty { + textField.text = formatted + } return false } else { return true diff --git a/VDS/Components/TextFields/InputField/InputField.swift b/VDS/Components/TextFields/InputField/InputField.swift index af358dbf..a160420a 100644 --- a/VDS/Components/TextFields/InputField/InputField.swift +++ b/VDS/Components/TextFields/InputField/InputField.swift @@ -190,9 +190,11 @@ open class InputField: EntryFieldBase { successLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable() backgroundColorConfiguration.setSurfaceColors(VDSColor.feedbackSuccessBackgroundOnlight, VDSColor.feedbackSuccessBackgroundOndark, forState: .success) - - borderColorConfiguration.setSurfaceColors(VDSColor.feedbackSuccessOnlight, VDSColor.feedbackSuccessOndark, forState: .success) + backgroundColorConfiguration.setSurfaceColors(VDSColor.feedbackSuccessBackgroundOnlight, VDSColor.feedbackSuccessBackgroundOndark, forState: [.success, .focused]) + borderColorConfiguration.setSurfaceColors(VDSColor.feedbackSuccessOnlight, VDSColor.feedbackSuccessOndark, forState: .success) + + textField.textColorConfiguration = textFieldTextColorConfiguration } open override func getFieldContainer() -> UIView { @@ -221,9 +223,9 @@ open class InputField: EntryFieldBase { super.updateView() + textField.surface = surface textField.isEnabled = isEnabled textField.isUserInteractionEnabled = isEnabled && !isReadOnly - textField.textColor = textFieldTextColorConfiguration.getColor(self) } open override func updateAccessibility() { @@ -248,7 +250,7 @@ open class InputField: EntryFieldBase { statusIcon.name = .checkmarkAlt statusIcon.color = iconColorConfiguration.getColor(self) statusIcon.surface = surface - statusIcon.isHidden = !isEnabled + statusIcon.isHidden = !isEnabled || state.contains(.focused) } else { successLabel.isHidden = true } @@ -303,6 +305,7 @@ extension InputField: UITextFieldDelegate { public func textFieldDidBeginEditing(_ textField: UITextField) { fieldType.handler().textFieldDidBeginEditing(self, textField: textField) updateContainerView() + updateErrorLabel() } public func textFieldDidEndEditing(_ textField: UITextField) { From a90cd23f11d96f8e09750b151c51a98ae9187849 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 6 Jun 2024 16:19:55 -0500 Subject: [PATCH 8/8] updated release notes Signed-off-by: Matt Bruce --- VDS/SupportingFiles/ReleaseNotes.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/VDS/SupportingFiles/ReleaseNotes.txt b/VDS/SupportingFiles/ReleaseNotes.txt index 2018da35..de8c62de 100644 --- a/VDS/SupportingFiles/ReleaseNotes.txt +++ b/VDS/SupportingFiles/ReleaseNotes.txt @@ -6,6 +6,8 @@ - CXTDT-560823 – TextArea – Accessibility Labels/Error/ReadyOnly/Disabled - CXTDT-553663 - DropdownSelect – Accessibility - CXTDT-544662 - Breadcrumbs - Text Wrapping +- CXTDT-565105 - InputField - Date - Typeover text not working +- CXTDT-565115 - InputField - CreditCard - China UnionPay does not allow longer numbers 1.0.65 ----------------