Merge branch 'mbruce/bugfix' into 'develop'

Bugs galore

See merge request BPHV_MIPS/vds_ios!246
This commit is contained in:
Bruce, Matt R 2024-06-06 21:38:31 +00:00
commit d25d4d0fc9
10 changed files with 247 additions and 52 deletions

View File

@ -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
}

View File

@ -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
}

View File

@ -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 {

View File

@ -241,6 +241,23 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
open var rules = [AnyRule<String>]()
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
//--------------------------------------------------

View File

@ -9,7 +9,20 @@ import Foundation
import UIKit
extension InputField {
public class CreditCardNumberRule: Rule, Withable {
public var cardType: CreditCardType?
public var errorMessage: String = "You have exceeded the character limit."
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
} 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
}
}
}

View File

@ -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

View File

@ -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,19 +223,14 @@ 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() {
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."
}
@ -253,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
}
@ -308,6 +305,7 @@ extension InputField: UITextFieldDelegate {
public func textFieldDidBeginEditing(_ textField: UITextField) {
fieldType.handler().textFieldDidBeginEditing(self, textField: textField)
updateContainerView()
updateErrorLabel()
}
public func textFieldDidEndEditing(_ textField: UITextField) {

View File

@ -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() {

View File

@ -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."
}

View File

@ -3,6 +3,11 @@
- 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
- 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
----------------