Merge branch 'mbruce/bugfix' into 'develop'
Bugs galore See merge request BPHV_MIPS/vds_ios!246
This commit is contained in:
commit
d25d4d0fc9
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
|
||||
@ -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
|
||||
//--------------------------------------------------
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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."
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
----------------
|
||||
|
||||
Loading…
Reference in New Issue
Block a user