Merge branch 'mbruce/bugfix' into 'develop'

CXTDT-584278 – InputField - Accessibility

See merge request BPHV_MIPS/vds_ios!270
This commit is contained in:
Bruce, Matt R 2024-07-12 13:38:43 +00:00
commit 71c5e444f4
8 changed files with 135 additions and 60 deletions

View File

@ -93,12 +93,15 @@ open class Icon: View {
backgroundColor = .clear
isAccessibilityElement = true
accessibilityTraits = .image
accessibilityTraits = .none
accessibilityHint = "image"
bridge_accessibilityLabelBlock = { [weak self] in
guard let self else { return "" }
return name?.rawValue ?? "icon"
}
}

View File

@ -28,7 +28,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
public required init?(coder: NSCoder) {
super.init(coder: coder)
}
//--------------------------------------------------
// MARK: - Enums
//--------------------------------------------------
@ -92,12 +92,6 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
}
}()
/// This is the view that will be wrapped with the border for userInteraction.
/// The only subview of this view is the fieldStackView
internal var containerView = View().with {
$0.isAccessibilityElement = true
}
/// This is set by a local method.
internal var bottomContainerView: UIView!
@ -115,7 +109,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
internal var widthConstraint: NSLayoutConstraint?
internal var trailingEqualsConstraint: NSLayoutConstraint?
internal var trailingLessThanEqualsConstraint: NSLayoutConstraint?
//--------------------------------------------------
// MARK: - Configuration Properties
//--------------------------------------------------
@ -133,14 +127,14 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forDisabled: true)
$0.setSurfaceColors(VDSColor.elementsSecondaryOnlight, VDSColor.elementsSecondaryOndark, forDisabled: false)
}
internal var backgroundColorConfiguration = ControlColorConfiguration().with {
$0.setSurfaceColors(VDSFormControlsColor.backgroundOnlight, VDSFormControlsColor.backgroundOndark, forState: .normal)
$0.setSurfaceColors(VDSFormControlsColor.backgroundOnlight, VDSFormControlsColor.backgroundOndark, forState: .disabled)
$0.setSurfaceColors(VDSColor.feedbackErrorBackgroundOnlight, VDSColor.feedbackErrorBackgroundOndark, forState: .error)
$0.setSurfaceColors(VDSColor.feedbackErrorBackgroundOnlight, VDSColor.feedbackErrorBackgroundOndark, forState: [.error, .focused])
}
internal var borderColorConfiguration = ControlColorConfiguration().with {
$0.setSurfaceColors(VDSFormControlsColor.borderOnlight, VDSFormControlsColor.borderOndark, forState: .normal)
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOnlight, forState: .focused)
@ -155,7 +149,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled)
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .error)
}
internal var readOnlyBorderColorConfiguration = ControlColorConfiguration().with {
$0.setSurfaceColors(VDSFormControlsColor.borderReadonlyOnlight, VDSFormControlsColor.borderReadonlyOndark, forState: .normal)
}
@ -163,8 +157,14 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
open var onChangeSubscriber: AnyCancellable?
/// This is the view that will be wrapped with the border for userInteraction.
/// The only subview of this view is the fieldStackView
open var containerView = View().with {
$0.isAccessibilityElement = true
}
open var onChangeSubscriber: AnyCancellable?
open var titleLabel = Label().with {
$0.setContentCompressionResistancePriority(.required, for: .vertical)
$0.textStyle = .bodySmall
@ -185,7 +185,9 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
$0.size = .medium
$0.isAccessibilityElement = true
}
open var useRequiredRule: Bool = true { didSet { setNeedsUpdate() } }
open var labelText: String? { didSet { setNeedsUpdate() } }
open var helperText: String? { didSet { setNeedsUpdate() } }
@ -195,7 +197,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
/// FormFieldValidator
open var validator: (any FormFieldValidatorable)?
/// Override UIControl state to add the .error state if showError is true.
open override var state: UIControl.State {
get {
@ -214,17 +216,17 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
return state
}
}
open var errorText: String? { didSet { setNeedsUpdate() } }
open var tooltipModel: Tooltip.TooltipModel? { didSet { setNeedsUpdate() } }
open var transparentBackground: Bool = false { didSet { setNeedsUpdate() } }
open var width: CGFloat? { didSet { setNeedsUpdate() } }
open var inputId: String? { didSet { setNeedsUpdate() } }
/// The text of this textField.
open var value: String? {
get { fatalError("must be read from subclass")}
@ -235,21 +237,21 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
open var isRequired: Bool = false { didSet { setNeedsUpdate() } }
open var isReadOnly: Bool = false { didSet { setNeedsUpdate() } }
open var helperTextPlacement: HelperTextPlacement = .bottom {
didSet {
updateHelperTextPosition()
}
}
open var rules = [AnyRule<String>]()
open var accessibilityHintText: String = "Double tap to open"
//--------------------------------------------------
// 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()
@ -371,7 +373,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
titleLabel.textStyle = .bodySmall
errorLabel.textStyle = .bodySmall
helperLabel.textStyle = .bodySmall
labelText = nil
helperText = nil
showError = false
@ -389,19 +391,19 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
open override var canBecomeFirstResponder: Bool {
responder?.canBecomeFirstResponder ?? super.canBecomeFirstResponder
}
open override func becomeFirstResponder() -> Bool {
responder?.becomeFirstResponder() ?? super.becomeFirstResponder()
}
open override var canResignFirstResponder: Bool {
responder?.canResignFirstResponder ?? super.canResignFirstResponder
}
open override func resignFirstResponder() -> Bool {
responder?.resignFirstResponder() ?? super.resignFirstResponder()
}
//--------------------------------------------------
// MARK: - Public Methods
//--------------------------------------------------
@ -409,21 +411,21 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
open func getFieldContainer() -> UIView {
fatalError("Subclass must return the view that contains the field/view the user will interact with.")
}
/// Container for the area in which helper or error text presents.
open func getBottomContainer() -> UIView {
return bottomContainerStackView
}
open func validate(){
updateRules()
validator = FormFieldValidator<EntryFieldBase>(field: self, rules: rules)
validator?.validate()
setNeedsUpdate()
}
open func updateTitleLabel() {
//update the local vars for the label since we no
//long have a model
var attributes: [any LabelAttributeModel] = []
@ -444,36 +446,43 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
if let tooltipModel {
attributes.append(TooltipLabelAttribute(surface: surface, model: tooltipModel, presenter: self))
}
//set the titleLabel
titleLabel.text = updatedLabelText
titleLabel.attributes = attributes
titleLabel.surface = surface
titleLabel.isEnabled = isEnabled
}
open func updateErrorLabel(){
if showError, let errorText {
errorLabel.text = errorText
errorLabel.surface = surface
errorLabel.isEnabled = isEnabled
errorLabel.isHidden = false
statusIcon.name = .error
statusIcon.surface = surface
statusIcon.isHidden = !isEnabled || state.contains(.focused)
} else if hasInternalError, let internalErrorText {
errorLabel.text = internalErrorText
errorLabel.surface = surface
errorLabel.isEnabled = isEnabled
errorLabel.isHidden = false
/// always show the errorIcon if there is an error
if showError || hasInternalError {
statusIcon.name = .error
statusIcon.surface = surface
statusIcon.isHidden = !isEnabled || state.contains(.focused)
} else {
statusIcon.isHidden = true
errorLabel.isHidden = true
}
statusIcon.color = iconColorConfiguration.getColor(self)
// only show errorLabel if there is a message
var message: String?
if showError, let errorText {
message = errorText
} else if hasInternalError, let internalErrorText {
message = internalErrorText
}
if let message {
errorLabel.text = message
errorLabel.surface = surface
errorLabel.isEnabled = isEnabled
errorLabel.isHidden = false
} else {
errorLabel.isHidden = true
}
}
open func updateHelperLabel(){
@ -515,7 +524,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
//--------------------------------------------------
internal func updateRules() {
rules.removeAll()
if self.isRequired {
if isRequired && useRequiredRule {
let rule = RequiredRule()
if let errorText, !errorText.isEmpty {
rule.errorMessage = errorText

View File

@ -67,7 +67,14 @@ extension InputField {
}
internal func formatUSNumber(_ number: String) -> String {
override func textFieldDidEndEditing(_ inputField: InputField, textField: UITextField) {
if let text = inputField.text {
let rawNumber = text.filter { $0.isNumber }
textField.text = formatUSNumber(rawNumber)
}
}
func formatUSNumber(_ number: String) -> String {
// Format the number in the style XXX-XXX-XXXX
let areaCodeLength = 3
let centralOfficeCodeLength = 3

View File

@ -107,6 +107,9 @@ open class InputField: EntryFieldBase {
$0.isAccessibilityElement = false
$0.autocorrectionType = .no
$0.spellCheckingType = .no
$0.smartQuotesType = .no
$0.smartDashesType = .no
$0.smartInsertDeleteType = .no
}
/// Color configuration for the textField.
@ -184,6 +187,8 @@ open class InputField: EntryFieldBase {
super.setup()
accessibilityHintText = "Double tap to edit"
actionTextLink.accessibilityTraits = .button
textField.heightAnchor.constraint(equalToConstant: 20).isActive = true
textField.delegate = self
bottomContainerStackView.insertArrangedSubview(successLabel, at: 0)
@ -207,11 +212,11 @@ open class InputField: EntryFieldBase {
accessibilityLabels.append(text)
}
if let formatText = textField.formatText, !formatText.isEmpty {
if let formatText = textField.formatText, !formatText.isEmpty, textField.text.isEmpty {
accessibilityLabels.append("format, \(formatText)")
}
if let placeholderText = textField.placeholder, !placeholderText.isEmpty {
if let placeholderText = textField.placeholder, !placeholderText.isEmpty, textField.text.isEmpty {
accessibilityLabels.append("placeholder, \(placeholderText)")
}
@ -246,6 +251,11 @@ open class InputField: EntryFieldBase {
return nil
}
}
containerView.bridge_accessibilityValueBlock = { [weak self] in
guard let self else { return "" }
return textField.isSecureTextEntry ? "\(textField.text.count) stars" : value
}
}
open override func getFieldContainer() -> UIView {
@ -340,19 +350,19 @@ open class InputField: EntryFieldBase {
}
extension InputField: UITextFieldDelegate {
public func textFieldDidBeginEditing(_ textField: UITextField) {
open func textFieldDidBeginEditing(_ textField: UITextField) {
fieldType.handler().textFieldDidBeginEditing(self, textField: textField)
updateContainerView()
updateErrorLabel()
}
public func textFieldDidEndEditing(_ textField: UITextField) {
open func textFieldDidEndEditing(_ textField: UITextField) {
fieldType.handler().textFieldDidEndEditing(self, textField: textField)
validate()
UIAccessibility.post(notification: .layoutChanged, argument: self.containerView)
}
public func textFieldDidChangeSelection(_ textField: UITextField) {
open func textFieldDidChangeSelection(_ textField: UITextField) {
fieldType.handler().textFieldDidChangeSelection(self, textField: textField)
if fieldType.handler().validateOnChange {
validate()
@ -361,7 +371,7 @@ extension InputField: UITextFieldDelegate {
setNeedsUpdate()
}
public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
open func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let shouldChange = fieldType.handler().textField(self, textField: textField, shouldChangeCharactersIn: range, replacementString: string)
if shouldChange {
UIAccessibility.post(notification: .announcement, argument: string)

View File

@ -47,6 +47,11 @@ open class TextField: UITextField, ViewProtocol, Errorable {
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
/// Set to true to hide the blinking textField cursor.
open var hideBlinkingCaret = false
open var enableClipboardActions: Bool = true
open var onDidDeleteBackwards: (() -> Void)?
/// Key of whether or not updateView() is called in setNeedsUpdate()
open var shouldUpdateView: Bool = true
@ -209,6 +214,23 @@ open class TextField: UITextField, ViewProtocol, Errorable {
return success
}
open override func caretRect(for position: UITextPosition) -> CGRect {
if hideBlinkingCaret {
return .zero
}
let caretRect = super.caretRect(for: position)
return CGRect(origin: caretRect.origin, size: CGSize(width: 1, height: caretRect.height))
}
open override func deleteBackward() {
super.deleteBackward()
onDidDeleteBackwards?()
}
open override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { enableClipboardActions }
//--------------------------------------------------
// MARK: - Private Methods
//--------------------------------------------------

View File

@ -111,7 +111,9 @@ open class TextArea: EntryFieldBase {
}
didSet {
validate()
if textView.isFirstResponder {
validate()
}
}
}

View File

@ -41,10 +41,19 @@ open class TextView: UITextView, ViewProtocol, Errorable {
// MARK: - Private Properties
//--------------------------------------------------
private var initialSetupPerformed = false
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
open var placeholder: String? { didSet { setNeedsUpdate() } }
open var placeholderLabel = Label().with {
$0.textColorConfiguration = ViewColorConfiguration().with {
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forDisabled: true)
$0.setSurfaceColors(VDSColor.elementsSecondaryOnlight, VDSColor.elementsSecondaryOndark, forDisabled: false)
}.eraseToAnyColorable()
}
/// Key of whether or not updateView() is called in setNeedsUpdate()
open var shouldUpdateView: Bool = true
@ -88,6 +97,7 @@ open class TextView: UITextView, ViewProtocol, Errorable {
if textAlignment != oldValue {
// Text alignment can be part of our paragraph style, so we may need to
// re-style when changed
placeholderLabel.textAlignment = textAlignment
updateLabel()
}
}
@ -118,6 +128,9 @@ open class TextView: UITextView, ViewProtocol, Errorable {
done.pinCenterY()
.pinTrailing(16)
inputAccessoryView = accessView
addSubview(placeholderLabel)
placeholderLabel.pinToSuperView()
}
@objc func doneButtonAction() {
@ -145,7 +158,11 @@ open class TextView: UITextView, ViewProtocol, Errorable {
setNeedsUpdate()
}
open override func layoutSubviews() {
super.layoutSubviews()
placeholderLabel.preferredMaxLayoutWidth = textContainer.size.width - textContainer.lineFragmentPadding * 2
}
//--------------------------------------------------
// MARK: - Accessibility
//--------------------------------------------------
@ -297,6 +314,10 @@ open class TextView: UITextView, ViewProtocol, Errorable {
} else {
attributedText = nil
}
placeholderLabel.textStyle = textStyle
placeholderLabel.surface = surface
placeholderLabel.text = placeholder
placeholderLabel.isHidden = !text.isEmpty
}
}

View File

@ -1,6 +1,7 @@
1.0.71
----------------
- CXTDT-581803 - DatePicker - Calendar does not switch to Dark Mode
- CXTDT-584278 InputField - Accessibility
1.0.70
----------------