From a043bd33f2b933d50c8aba334d2c1f4c5f81e0cf Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 5 Jun 2024 16:15:35 -0500 Subject: [PATCH 01/83] added shouldUpdateAccessibility Signed-off-by: Matt Bruce --- VDS/BaseClasses/Control.swift | 2 ++ VDS/BaseClasses/View.swift | 2 ++ VDS/Components/Label/Label.swift | 2 ++ VDS/Components/TextFields/InputField/TextField.swift | 2 ++ VDS/Components/TextFields/TextArea/TextView.swift | 2 ++ VDS/Protocols/ViewProtocol.swift | 8 +++++++- 6 files changed, 17 insertions(+), 1 deletion(-) diff --git a/VDS/BaseClasses/Control.swift b/VDS/BaseClasses/Control.swift index 274d7d9b..6d212e22 100644 --- a/VDS/BaseClasses/Control.swift +++ b/VDS/BaseClasses/Control.swift @@ -47,6 +47,8 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable { //-------------------------------------------------- open var shouldUpdateView: Bool = true + open var shouldUpdateAccessibility: Bool = true + open var userInfo = [String: Primitive]() open var surface: Surface = .light { didSet { setNeedsUpdate() } } diff --git a/VDS/BaseClasses/View.swift b/VDS/BaseClasses/View.swift index a807c25c..9ba21d9c 100644 --- a/VDS/BaseClasses/View.swift +++ b/VDS/BaseClasses/View.swift @@ -46,6 +46,8 @@ open class View: UIView, ViewProtocol, UserInfoable { //-------------------------------------------------- open var shouldUpdateView: Bool = true + open var shouldUpdateAccessibility: Bool = true + /// Dictionary for keeping information for this Control use only Primitives. open var userInfo = [String: Primitive]() diff --git a/VDS/Components/Label/Label.swift b/VDS/Components/Label/Label.swift index 9e4dd18e..41a79c5e 100644 --- a/VDS/Components/Label/Label.swift +++ b/VDS/Components/Label/Label.swift @@ -110,6 +110,8 @@ open class Label: UILabel, ViewProtocol, UserInfoable { /// Key of whether or not updateView() is called in setNeedsUpdate() open var shouldUpdateView: Bool = true + open var shouldUpdateAccessibility: Bool = true + /// Will determine if a scaled font should be used for the font. open var useScaledFont: Bool = false { didSet { setNeedsUpdate() }} diff --git a/VDS/Components/TextFields/InputField/TextField.swift b/VDS/Components/TextFields/InputField/TextField.swift index b054741d..e7c32464 100644 --- a/VDS/Components/TextFields/InputField/TextField.swift +++ b/VDS/Components/TextFields/InputField/TextField.swift @@ -56,6 +56,8 @@ open class TextField: UITextField, ViewProtocol, Errorable { /// Key of whether or not updateView() is called in setNeedsUpdate() open var shouldUpdateView: Bool = true + open var shouldUpdateAccessibility: Bool = true + open var surface: Surface = .light { didSet { setNeedsUpdate() } } open var showError: Bool = false { didSet { setNeedsUpdate() } } diff --git a/VDS/Components/TextFields/TextArea/TextView.swift b/VDS/Components/TextFields/TextArea/TextView.swift index f0b64f46..2fe19cb5 100644 --- a/VDS/Components/TextFields/TextArea/TextView.swift +++ b/VDS/Components/TextFields/TextArea/TextView.swift @@ -48,6 +48,8 @@ open class TextView: UITextView, ViewProtocol, Errorable { /// Key of whether or not updateView() is called in setNeedsUpdate() open var shouldUpdateView: Bool = true + open var shouldUpdateAccessibility: Bool = true + open var surface: Surface = .light { didSet { setNeedsUpdate() } } /// Array of LabelAttributeModel objects used in rendering the text. diff --git a/VDS/Protocols/ViewProtocol.swift b/VDS/Protocols/ViewProtocol.swift index da730372..57b93982 100644 --- a/VDS/Protocols/ViewProtocol.swift +++ b/VDS/Protocols/ViewProtocol.swift @@ -16,6 +16,9 @@ public protocol ViewProtocol: AnyObject, Initable, Resettable, Enabling, Surface /// Key of whether or not updateView() is called in setNeedsUpdate() var shouldUpdateView: Bool { get set } + /// Key of whether or not updateAccessibility() is called in setNeedsUpdate() + var shouldUpdateAccessibility: Bool { get set } + /// Executed on initialization for this View. func initialSetup() @@ -30,12 +33,15 @@ public protocol ViewProtocol: AnyObject, Initable, Resettable, Enabling, Surface } extension ViewProtocol { + /// Called when there are changes in a View based off a change events or from local properties. public func setNeedsUpdate() { if shouldUpdateView { shouldUpdateView = false updateView() - updateAccessibility() + if shouldUpdateAccessibility { + updateAccessibility() + } shouldUpdateView = true } } From b3f810500baffd4f48e652f916d9b60a1719c1a5 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 5 Jun 2024 16:15:53 -0500 Subject: [PATCH 02/83] split up accessibility Signed-off-by: Matt Bruce --- VDS/BaseClasses/Selector/SelectorBase.swift | 1 + .../Selector/SelectorItemBase.swift | 36 ++++++++-- VDS/Components/Buttons/ButtonBase.swift | 2 + VDS/Components/DatePicker/DatePicker.swift | 2 - VDS/Components/RadioBox/RadioBoxItem.swift | 67 ++++++++++++++++--- 5 files changed, 94 insertions(+), 14 deletions(-) diff --git a/VDS/BaseClasses/Selector/SelectorBase.swift b/VDS/BaseClasses/Selector/SelectorBase.swift index 0b29f4e2..809cb4aa 100644 --- a/VDS/BaseClasses/Selector/SelectorBase.swift +++ b/VDS/BaseClasses/Selector/SelectorBase.swift @@ -124,6 +124,7 @@ open class SelectorBase: Control, SelectorControlable { open override func updateAccessibility() { super.updateAccessibility() accessibilityLabel = "\(Self.self)\(showError ? ", error" : "")" + accessibilityHint = !isEnabled ? "" : "Double tap to open." } /// This will change the state of the Selector and execute the actionBlock if provided. diff --git a/VDS/BaseClasses/Selector/SelectorItemBase.swift b/VDS/BaseClasses/Selector/SelectorItemBase.swift index 66772ce0..46d54fb5 100644 --- a/VDS/BaseClasses/Selector/SelectorItemBase.swift +++ b/VDS/BaseClasses/Selector/SelectorItemBase.swift @@ -147,6 +147,30 @@ open class SelectorItemBase: Control, Errorable, open var accessibilityValueText: String? + open var accessibilityLabelText: String { + var accessibilityLabels = [String]() + + accessibilityLabels.append("\(Selector.self)") + + if let text = labelText { + accessibilityLabels.append(text) + } + + if let text = childText { + accessibilityLabels.append(text) + } + + if !isEnabled { + accessibilityLabels.append("dimmed") + } + + if let errorText, showError { + accessibilityLabels.append("error, \(errorText)") + } + + return accessibilityLabels.joined(separator: ", ") + } + //-------------------------------------------------- // MARK: - Overrides //-------------------------------------------------- @@ -162,9 +186,11 @@ open class SelectorItemBase: Control, Errorable, open override func setup() { super.setup() - selectorView.isAccessibilityElement = false - isAccessibilityElement = true - accessibilityTraits = .button + selectorView.isAccessibilityElement = true + selectorView.shouldUpdateAccessibility = false + + isAccessibilityElement = false + accessibilityElements = [selectorView, label, childLabel, errorLabel] addSubview(mainStackView) mainStackView.isUserInteractionEnabled = false @@ -195,8 +221,10 @@ open class SelectorItemBase: Control, Errorable, /// Used to update any Accessibility properties. open override func updateAccessibility() { super.updateAccessibility() - setAccessibilityLabel(for: [selectorView, label, childLabel, errorLabel]) + selectorView.accessibilityLabel = accessibilityLabelText + selectorView.accessibilityHint = !isEnabled ? "" : "Double tap to activate." accessibilityValue = accessibilityValueText + } /// Resets to default settings. diff --git a/VDS/Components/Buttons/ButtonBase.swift b/VDS/Components/Buttons/ButtonBase.swift index 80010ee5..9dd60781 100644 --- a/VDS/Components/Buttons/ButtonBase.swift +++ b/VDS/Components/Buttons/ButtonBase.swift @@ -51,6 +51,8 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable { /// Key of whether or not updateView() is called in setNeedsUpdate() open var shouldUpdateView: Bool = true + open var shouldUpdateAccessibility: Bool = true + open var surface: Surface = .light { didSet { setNeedsUpdate() } } /// Text that will be used in the titleLabel. diff --git a/VDS/Components/DatePicker/DatePicker.swift b/VDS/Components/DatePicker/DatePicker.swift index 7645cce5..c514001a 100644 --- a/VDS/Components/DatePicker/DatePicker.swift +++ b/VDS/Components/DatePicker/DatePicker.swift @@ -102,8 +102,6 @@ open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopov super.setup() fieldStackView.isAccessibilityElement = true - fieldStackView.accessibilityLabel = "Date Picker" - fieldStackView.accessibilityHint = "Double Tap to open" // setting color config selectedDateLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable() diff --git a/VDS/Components/RadioBox/RadioBoxItem.swift b/VDS/Components/RadioBox/RadioBoxItem.swift index 15f7c55a..b555a9b6 100644 --- a/VDS/Components/RadioBox/RadioBoxItem.swift +++ b/VDS/Components/RadioBox/RadioBoxItem.swift @@ -133,6 +133,30 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable { open var accessibilityValueText: String? + open var accessibilityLabelText: String { + var accessibilityLabels = [String]() + + accessibilityLabels.append("Radiobox") + + if let text { + accessibilityLabels.append(text) + } + + if let text = subText { + accessibilityLabels.append(text) + } + + if let text = subTextRight { + accessibilityLabels.append(text) + } + + if !isEnabled { + accessibilityLabels.append("dimmed") + } + + return accessibilityLabels.joined(separator: ", ") + } + //-------------------------------------------------- // MARK: - Configuration Properties //-------------------------------------------------- @@ -171,8 +195,10 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable { open override func setup() { super.setup() - isAccessibilityElement = true - accessibilityTraits = .button + isAccessibilityElement = false + selectorView.isAccessibilityElement = true + selectorView.accessibilityTraits = .button + addSubview(selectorView) selectorView.isUserInteractionEnabled = false @@ -242,12 +268,8 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable { /// Used to update any Accessibility properties. open override func updateAccessibility() { super.updateAccessibility() - setAccessibilityLabel(for: [textLabel, subTextLabel, subTextRightLabel]) - if let currentAccessibilityLabel = accessibilityLabel { - accessibilityLabel = "Radiobox, \(currentAccessibilityLabel)" - } else { - accessibilityLabel = "Radiobox" - } + accessibilityLabel = accessibilityLabelText + if let accessibilityValueText { accessibilityValue = strikethrough ? "\(strikethroughAccessibilityText), \(accessibilityValueText)" @@ -259,6 +281,35 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable { } } + open override var accessibilityElements: [Any]? { + get { + var items = [Any]() + items.append(selectorView) + + let elements = gatherAccessibilityElements(from: selectorView) + let views = elements.compactMap({ $0 as? UIView }) + + //update accessibilityLabel + selectorView.setAccessibilityLabel(for: views) + + //disabled + if !isEnabled { + if let label = selectorView.accessibilityLabel, !label.isEmpty { + selectorView.accessibilityLabel = "\(label), dimmed" + } else { + selectorView.accessibilityLabel = "dimmed" + } + } + + //append all children that are accessible + items.append(contentsOf: elements) + + return items + } + set {} + } + + //-------------------------------------------------- // MARK: - Private Methods //-------------------------------------------------- From c02f355847feb9a8170cbd1c44ef2e61d3829614 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 13 Jun 2024 13:22:30 -0500 Subject: [PATCH 03/83] updated icons Signed-off-by: Matt Bruce --- .../discover.imageset/Contents.json | 2 +- .../discover.imageset/Discover-02.svg | 1 + .../CreditCard/discover.imageset/discover.svg | 49 ------------------ .../CreditCard/jcb.imageset/Contents.json | 2 +- .../jcb.imageset/jcb-emblem-logo.svg | 1 + .../CreditCard/jcb.imageset/jcb.svg | 51 ------------------- 6 files changed, 4 insertions(+), 102 deletions(-) create mode 100644 VDS/SupportingFiles/Icons.xcassets/CreditCard/discover.imageset/Discover-02.svg delete mode 100644 VDS/SupportingFiles/Icons.xcassets/CreditCard/discover.imageset/discover.svg create mode 100644 VDS/SupportingFiles/Icons.xcassets/CreditCard/jcb.imageset/jcb-emblem-logo.svg delete mode 100644 VDS/SupportingFiles/Icons.xcassets/CreditCard/jcb.imageset/jcb.svg diff --git a/VDS/SupportingFiles/Icons.xcassets/CreditCard/discover.imageset/Contents.json b/VDS/SupportingFiles/Icons.xcassets/CreditCard/discover.imageset/Contents.json index 28cfbca3..d8a92d8d 100644 --- a/VDS/SupportingFiles/Icons.xcassets/CreditCard/discover.imageset/Contents.json +++ b/VDS/SupportingFiles/Icons.xcassets/CreditCard/discover.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "discover.svg", + "filename" : "Discover-02.svg", "idiom" : "universal" } ], diff --git a/VDS/SupportingFiles/Icons.xcassets/CreditCard/discover.imageset/Discover-02.svg b/VDS/SupportingFiles/Icons.xcassets/CreditCard/discover.imageset/Discover-02.svg new file mode 100644 index 00000000..ea75afb4 --- /dev/null +++ b/VDS/SupportingFiles/Icons.xcassets/CreditCard/discover.imageset/Discover-02.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/VDS/SupportingFiles/Icons.xcassets/CreditCard/discover.imageset/discover.svg b/VDS/SupportingFiles/Icons.xcassets/CreditCard/discover.imageset/discover.svg deleted file mode 100644 index a056ab58..00000000 --- a/VDS/SupportingFiles/Icons.xcassets/CreditCard/discover.imageset/discover.svg +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/VDS/SupportingFiles/Icons.xcassets/CreditCard/jcb.imageset/Contents.json b/VDS/SupportingFiles/Icons.xcassets/CreditCard/jcb.imageset/Contents.json index 8a4c248a..c4aba6fe 100644 --- a/VDS/SupportingFiles/Icons.xcassets/CreditCard/jcb.imageset/Contents.json +++ b/VDS/SupportingFiles/Icons.xcassets/CreditCard/jcb.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "jcb.svg", + "filename" : "jcb-emblem-logo.svg", "idiom" : "universal" } ], diff --git a/VDS/SupportingFiles/Icons.xcassets/CreditCard/jcb.imageset/jcb-emblem-logo.svg b/VDS/SupportingFiles/Icons.xcassets/CreditCard/jcb.imageset/jcb-emblem-logo.svg new file mode 100644 index 00000000..4b8f6dd9 --- /dev/null +++ b/VDS/SupportingFiles/Icons.xcassets/CreditCard/jcb.imageset/jcb-emblem-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/VDS/SupportingFiles/Icons.xcassets/CreditCard/jcb.imageset/jcb.svg b/VDS/SupportingFiles/Icons.xcassets/CreditCard/jcb.imageset/jcb.svg deleted file mode 100644 index ab197f4b..00000000 --- a/VDS/SupportingFiles/Icons.xcassets/CreditCard/jcb.imageset/jcb.svg +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 0f27e8298782030d98a89b57859073ea7203b201 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 14 Jun 2024 08:47:45 -0500 Subject: [PATCH 04/83] removed duplicate code from base class Signed-off-by: Matt Bruce --- VDS/Components/TextFields/TextArea/TextArea.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/VDS/Components/TextFields/TextArea/TextArea.swift b/VDS/Components/TextFields/TextArea/TextArea.swift index b0d858df..87f9a81d 100644 --- a/VDS/Components/TextFields/TextArea/TextArea.swift +++ b/VDS/Components/TextFields/TextArea/TextArea.swift @@ -137,7 +137,6 @@ open class TextArea: EntryFieldBase { /// 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() - fieldStackView.pinToSuperView(.uniform(VDSFormControls.spaceInset)) textView.isScrollEnabled = true textView.autocorrectionType = .no From 9871324c8e876619623af505dac12059fb977f20 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 14 Jun 2024 09:05:22 -0500 Subject: [PATCH 05/83] first cut of accessibility update Signed-off-by: Matt Bruce --- VDS/Components/DatePicker/DatePicker.swift | 38 ++++++++++++++----- .../DropdownSelect/DropdownSelect.swift | 14 +++---- .../TextFields/InputField/InputField.swift | 10 +++-- 3 files changed, 43 insertions(+), 19 deletions(-) diff --git a/VDS/Components/DatePicker/DatePicker.swift b/VDS/Components/DatePicker/DatePicker.swift index 7645cce5..1bd987fd 100644 --- a/VDS/Components/DatePicker/DatePicker.swift +++ b/VDS/Components/DatePicker/DatePicker.swift @@ -100,16 +100,14 @@ open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopov /// 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() - - fieldStackView.isAccessibilityElement = true - fieldStackView.accessibilityLabel = "Date Picker" - fieldStackView.accessibilityHint = "Double Tap to open" + + containerView.isAccessibilityElement = true // setting color config selectedDateLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable() // tap gesture - fieldStackView + containerView .publisher(for: UITapGestureRecognizer()) .sink { [weak self] _ in guard let self else { return } @@ -147,9 +145,31 @@ open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopov open override func updateAccessibility() { super.updateAccessibility() - fieldStackView.accessibilityLabel = "Date Picker, \(accessibilityLabelText)" - fieldStackView.accessibilityHint = isReadOnly || !isEnabled ? "" : "Double tap to open." - fieldStackView.accessibilityValue = value + containerView.accessibilityLabel = "Date Picker, \(accessibilityLabelText)" + containerView.accessibilityHint = isReadOnly || !isEnabled ? "" : "Double tap to open." + containerView.accessibilityValue = value + } + + open override var accessibilityElements: [Any]? { + get { + var elements = [Any]() + elements.append(contentsOf: [titleLabel, containerView]) + + if showError { + elements.append(statusIcon) + if let errorText, !errorText.isEmpty { + elements.append(errorLabel) + } + } + + if let helperText, !helperText.isEmpty { + elements.append(helperLabel) + } + + return elements + } + + set { super.accessibilityElements = newValue } } /// Resets to default settings. @@ -184,7 +204,7 @@ open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopov controller.dismiss(animated: true) { [weak self] in guard let self else { return } self.sendActions(for: .valueChanged) - UIAccessibility.post(notification: .layoutChanged, argument: self.fieldStackView) + UIAccessibility.post(notification: .layoutChanged, argument: self.containerView) } } diff --git a/VDS/Components/DropdownSelect/DropdownSelect.swift b/VDS/Components/DropdownSelect/DropdownSelect.swift index ae07d616..e825e024 100644 --- a/VDS/Components/DropdownSelect/DropdownSelect.swift +++ b/VDS/Components/DropdownSelect/DropdownSelect.swift @@ -131,7 +131,7 @@ open class DropdownSelect: EntryFieldBase { open override func setup() { super.setup() - fieldStackView.isAccessibilityElement = true + containerView.isAccessibilityElement = true inlineDisplayLabel.isAccessibilityElement = true dropdownField.width(0) @@ -278,15 +278,15 @@ open class DropdownSelect: EntryFieldBase { open override func updateAccessibility() { super.updateAccessibility() - fieldStackView.accessibilityLabel = "Dropdown Select, \(accessibilityLabelText)" - fieldStackView.accessibilityHint = isReadOnly || !isEnabled ? "" : "has popup, Double tap to open." - fieldStackView.accessibilityValue = value + containerView.accessibilityLabel = "Dropdown Select, \(accessibilityLabelText)" + containerView.accessibilityHint = isReadOnly || !isEnabled ? "" : "has popup, Double tap to open." + containerView.accessibilityValue = value } open override var accessibilityElements: [Any]? { get { var elements = [Any]() - elements.append(contentsOf: [titleLabel, fieldStackView]) + elements.append(contentsOf: [titleLabel, containerView]) if showError { elements.append(statusIcon) @@ -310,7 +310,7 @@ open class DropdownSelect: EntryFieldBase { optionsPicker.isHidden = true dropdownField.resignFirstResponder() setNeedsUpdate() - UIAccessibility.post(notification: .layoutChanged, argument: fieldStackView) + UIAccessibility.post(notification: .layoutChanged, argument: containerView) } open override var canBecomeFirstResponder: Bool { @@ -337,8 +337,8 @@ extension DropdownSelect: UIPickerViewDelegate, UIPickerViewDataSource { internal func launchPicker() { if optionsPicker.isHidden { - UIAccessibility.post(notification: .layoutChanged, argument: optionsPicker) dropdownField.becomeFirstResponder() + UIAccessibility.post(notification: .layoutChanged, argument: optionsPicker) } else { dropdownField.resignFirstResponder() } diff --git a/VDS/Components/TextFields/InputField/InputField.swift b/VDS/Components/TextFields/InputField/InputField.swift index a160420a..54286fa9 100644 --- a/VDS/Components/TextFields/InputField/InputField.swift +++ b/VDS/Components/TextFields/InputField/InputField.swift @@ -181,6 +181,9 @@ open class InputField: EntryFieldBase { /// 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() + containerView.isAccessibilityElement = true + textField.isAccessibilityElement = false + textField.heightAnchor.constraint(equalToConstant: 20).isActive = true textField.delegate = self bottomContainerStackView.insertArrangedSubview(successLabel, at: 0) @@ -230,8 +233,9 @@ open class InputField: EntryFieldBase { open override func updateAccessibility() { super.updateAccessibility() - textField.accessibilityLabel = accessibilityLabelText - textField.accessibilityHint = isReadOnly || !isEnabled ? "" : "Double tap to open." + containerView.accessibilityLabel = "Input Field, \(accessibilityLabelText)" + containerView.accessibilityHint = isReadOnly || !isEnabled ? "" : "Double tap to edit." + containerView.accessibilityValue = value } open override func updateErrorLabel() { @@ -264,7 +268,7 @@ open class InputField: EntryFieldBase { open override var accessibilityElements: [Any]? { get { var elements = [Any]() - elements.append(contentsOf: [titleLabel, textField]) + elements.append(contentsOf: [titleLabel, containerView]) if showError { elements.append(statusIcon) if let errorText, !errorText.isEmpty { From b8326faa161848132331665c15bad99dc966930a Mon Sep 17 00:00:00 2001 From: vasavk Date: Fri, 14 Jun 2024 21:17:48 +0530 Subject: [PATCH 06/83] Digital ACT-191 CXTDT-568463 defect: Calendar - On long press, hover randomizes --- VDS/Components/Calendar/Calendar.swift | 48 ++++++++----------- .../Calendar/CalendarDateViewCell.swift | 33 ++++++++----- VDS/SupportingFiles/ReleaseNotes.txt | 2 +- 3 files changed, 40 insertions(+), 43 deletions(-) diff --git a/VDS/Components/Calendar/Calendar.swift b/VDS/Components/Calendar/Calendar.swift index 2b0ba57b..3dbf0ba1 100644 --- a/VDS/Components/Calendar/Calendar.swift +++ b/VDS/Components/Calendar/Calendar.swift @@ -201,7 +201,7 @@ open class CalendarBase: Control, Changeable { } } updateViewConstraints() - } + } func updateViewConstraints() { collectionView.reloadData() @@ -331,38 +331,28 @@ extension CalendarBase: UICollectionViewDelegate, UICollectionViewDataSource, UI } } - - public func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool { - if let cell = collectionView.cellForItem(at: indexPath) as? CalendarDateViewCell { - let isEnabled: Bool = cell.isDateEnabled() - if isEnabled { - cell.activeModeStart() - } - } - return true - } - public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { // reload selected index, if it is in enabled state. if let cell = collectionView.cellForItem(at: indexPath) as? CalendarDateViewCell { - let isEnabled: Bool = cell.isDateEnabled() - if isEnabled { - cell.activeModeEnd() - - // Callback to pass selected date if it is enabled only. - selectedDate = dates[indexPath.row] - sendActions(for: .valueChanged) - displayDate = selectedDate - - var reloadIndexPaths = [indexPath] - - // If an cell is already selected, then it needs to be deselected. - // Add its index path to the array of index paths to be reloaded. - if let deselectIndexPath = selectedIndexPath { - reloadIndexPaths.append(deselectIndexPath) + let hasDate: Bool = cell.hasText() + if hasDate { + let isEnabled: Bool = cell.isDateEnabled() + if isEnabled { + // Callback to pass selected date if it is enabled only. + selectedDate = dates[indexPath.row] + sendActions(for: .valueChanged) + displayDate = selectedDate + + var reloadIndexPaths = [indexPath] + + // If an cell is already selected, then it needs to be deselected. + // Add its index path to the array of index paths to be reloaded. + if let deselectIndexPath = selectedIndexPath { + reloadIndexPaths.append(deselectIndexPath) + } + + collectionView.reloadItems(at: reloadIndexPaths) } - - collectionView.reloadItems(at: reloadIndexPaths) } } } diff --git a/VDS/Components/Calendar/CalendarDateViewCell.swift b/VDS/Components/Calendar/CalendarDateViewCell.swift index 4b6f400d..ee2f01c9 100644 --- a/VDS/Components/Calendar/CalendarDateViewCell.swift +++ b/VDS/Components/Calendar/CalendarDateViewCell.swift @@ -41,6 +41,21 @@ final class CalendarDateViewCell: UICollectionViewCell { $0.textStyle = .bodySmall } + override var isHighlighted: Bool { + didSet{ + if self.isHighlighted && hasText() && isDateEnabled() { + self.contentView.layer.borderColor = activeBorderColorConfiguration.getColor(surface).cgColor + self.contentView.layer.borderWidth = VDSFormControls.borderWidth + self.contentView.layer.cornerRadius = VDSFormControls.borderRadius + + } else { + self.contentView.layer.borderColor = nil + self.contentView.layer.borderWidth = 0 + self.contentView.layer.cornerRadius = 0 + } + } + } + private lazy var shapeLayer = CAShapeLayer() private var surface: Surface = .light private let selectedTextColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryInverseOnlight, VDSColor.elementsPrimaryInverseOndark) @@ -155,24 +170,16 @@ final class CalendarDateViewCell: UICollectionViewCell { numberLabel.textStyle = .bodySmall } } - + + func hasText() -> Bool { + return !numberLabel.text.isEmpty + } + // returns cell enabled state. func isDateEnabled() -> Bool { return numberLabel.isEnabled } - func activeModeStart() { - numberLabel.layer.borderColor = activeBorderColorConfiguration.getColor(surface).cgColor - numberLabel.layer.borderWidth = VDSFormControls.borderWidth - numberLabel.layer.cornerRadius = VDSFormControls.borderRadius - } - - func activeModeEnd() { - numberLabel.layer.borderColor = nil - numberLabel.layer.borderWidth = 0 - numberLabel.layer.cornerRadius = 0 - } - func disableLabel(with surface: Surface) { numberLabel.isEnabled = false numberLabel.textColor = disabledTextColorConfiguration.getColor(surface) diff --git a/VDS/SupportingFiles/ReleaseNotes.txt b/VDS/SupportingFiles/ReleaseNotes.txt index 326146fb..a3772a7b 100644 --- a/VDS/SupportingFiles/ReleaseNotes.txt +++ b/VDS/SupportingFiles/ReleaseNotes.txt @@ -1,6 +1,6 @@ 1.0.67 ---------------- -- CXTDT-553663 - DropdownSelect - Accessibility - has popup +- CXTDT-568463 - Calendar - On long press, hover randomizes 1.0.66 ---------------- From 4119a6c1806e350439139e20e4e7ef3f0ce1ee9f Mon Sep 17 00:00:00 2001 From: vasavk Date: Fri, 14 Jun 2024 21:20:11 +0530 Subject: [PATCH 07/83] Using internal enabled var for cell instead of label enabled state --- .../Calendar/CalendarDateViewCell.swift | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/VDS/Components/Calendar/CalendarDateViewCell.swift b/VDS/Components/Calendar/CalendarDateViewCell.swift index ee2f01c9..607eac76 100644 --- a/VDS/Components/Calendar/CalendarDateViewCell.swift +++ b/VDS/Components/Calendar/CalendarDateViewCell.swift @@ -47,7 +47,6 @@ final class CalendarDateViewCell: UICollectionViewCell { self.contentView.layer.borderColor = activeBorderColorConfiguration.getColor(surface).cgColor self.contentView.layer.borderWidth = VDSFormControls.borderWidth self.contentView.layer.cornerRadius = VDSFormControls.borderRadius - } else { self.contentView.layer.borderColor = nil self.contentView.layer.borderWidth = 0 @@ -56,6 +55,7 @@ final class CalendarDateViewCell: UICollectionViewCell { } } + private var isEnabled = false private lazy var shapeLayer = CAShapeLayer() private var surface: Surface = .light private let selectedTextColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryInverseOnlight, VDSColor.elementsPrimaryInverseOndark) @@ -135,20 +135,21 @@ final class CalendarDateViewCell: UICollectionViewCell { } } - // update text color, bg color, corner radius. - if numberLabel.text == selectedDate.getDay() - && selectedDate.monthInt == displayDate.monthInt - && selectedDate.yearInt == displayDate.yearInt - && numberLabel.isEnabled { - - numberLabel.textColor = selectedTextColorConfiguration.getColor(surface) - layer.backgroundColor = selectedBackgroundColor.getColor(surface).cgColor - layer.cornerRadius = VDSFormControls.borderRadius - - } else { - numberLabel.textColor = unselectedTextColorConfiguration.getColor(surface) - layer.backgroundColor = nil - layer.cornerRadius = 0 + // Set selected/unselected state text color, bg color, corner radius if cell is in enabled state. + if isEnabled { + if numberLabel.text == selectedDate.getDay() + && selectedDate.monthInt == displayDate.monthInt + && selectedDate.yearInt == displayDate.yearInt { + + numberLabel.textColor = selectedTextColorConfiguration.getColor(surface) + layer.backgroundColor = selectedBackgroundColor.getColor(surface).cgColor + layer.cornerRadius = VDSFormControls.borderRadius + + } else { + numberLabel.textColor = unselectedTextColorConfiguration.getColor(surface) + layer.backgroundColor = nil + layer.cornerRadius = 0 + } } // add indicators. @@ -177,11 +178,11 @@ final class CalendarDateViewCell: UICollectionViewCell { // returns cell enabled state. func isDateEnabled() -> Bool { - return numberLabel.isEnabled + return isEnabled } func disableLabel(with surface: Surface) { - numberLabel.isEnabled = false + isEnabled = false numberLabel.textColor = disabledTextColorConfiguration.getColor(surface) layer.backgroundColor = disabledBackgroundColor.getColor(surface).cgColor } @@ -190,7 +191,7 @@ final class CalendarDateViewCell: UICollectionViewCell { for x in 0...activeDates.count-1 { if activeDates[x].monthInt == displayDate.monthInt && activeDates[x].yearInt == displayDate.yearInt { if let day:Int = Int(numberLabel.text), day == activeDates[x].dayInt { - numberLabel.isEnabled = true + isEnabled = true } } } @@ -201,7 +202,7 @@ final class CalendarDateViewCell: UICollectionViewCell { if activeDates.count > 0 && inactiveDates.count == 0 { showActiveDates(with: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) } else { - numberLabel.isEnabled = true + isEnabled = true } } @@ -211,7 +212,7 @@ final class CalendarDateViewCell: UICollectionViewCell { disableLabel(with: surface) showActiveDates(with: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) } else { - numberLabel.isEnabled = true + isEnabled = true } } @@ -220,7 +221,7 @@ final class CalendarDateViewCell: UICollectionViewCell { if let day = Int(numberLabel.text), day < minDate.dayInt { disableLabel(with: surface) } else { - numberLabel.isEnabled = false + isEnabled = false handleActiveDates(with: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) } } @@ -230,7 +231,7 @@ final class CalendarDateViewCell: UICollectionViewCell { if let day = Int(numberLabel.text), day > maxDate.dayInt { disableLabel(with: surface) } else { - numberLabel.isEnabled = false + isEnabled = false handleActiveDates(with: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) } } @@ -240,7 +241,7 @@ final class CalendarDateViewCell: UICollectionViewCell { if let day = Int(numberLabel.text), day < minDate.dayInt || day > maxDate.dayInt { disableLabel(with: surface) } else { - numberLabel.isEnabled = false + isEnabled = false handleActiveDates(with: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) } } From ed356299b5048248e3c6be38b1cad675eb63bf0b Mon Sep 17 00:00:00 2001 From: vasavk Date: Fri, 14 Jun 2024 21:21:26 +0530 Subject: [PATCH 08/83] Fixed issue observed: fetching days with wrong date if display date exceeds min/max date --- VDS/Components/Calendar/Calendar.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VDS/Components/Calendar/Calendar.swift b/VDS/Components/Calendar/Calendar.swift index 3dbf0ba1..cd1c783b 100644 --- a/VDS/Components/Calendar/Calendar.swift +++ b/VDS/Components/Calendar/Calendar.swift @@ -160,7 +160,7 @@ open class CalendarBase: Control, Changeable { if (minDate <= maxDate) { // Check if current date falls between min & max dates. let fallsBetween = displayDate.isBetweeen(date: minDate, andDate: maxDate) - displayDate = fallsBetween ? displayDate : minDate + displayDate = fallsBetween ? displayDate : (displayDate.monthInt == minDate.monthInt) ? minDate : maxDate fetchDates(with: displayDate) } containerView.backgroundColor = transparentBackground ? .clear : backgroundColorConfiguration.getColor(self) From c9c3b54484f526f34b63abfff6c64ac6fee8f7c8 Mon Sep 17 00:00:00 2001 From: vasavk Date: Fri, 14 Jun 2024 21:24:53 +0530 Subject: [PATCH 09/83] updated missed note. --- VDS/SupportingFiles/ReleaseNotes.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/VDS/SupportingFiles/ReleaseNotes.txt b/VDS/SupportingFiles/ReleaseNotes.txt index a3772a7b..5014b345 100644 --- a/VDS/SupportingFiles/ReleaseNotes.txt +++ b/VDS/SupportingFiles/ReleaseNotes.txt @@ -1,5 +1,6 @@ 1.0.67 ---------------- +- CXTDT-553663 - DropdownSelect - Accessibility - has popup - CXTDT-568463 - Calendar - On long press, hover randomizes 1.0.66 From bec994e2d486d05f9aedabfbc1e39bceebe30e40 Mon Sep 17 00:00:00 2001 From: vasavk Date: Fri, 14 Jun 2024 21:33:02 +0530 Subject: [PATCH 10/83] Digital ACT-191 CXTDT-568412 defect: Calendar - Incorrect side nav icon size --- VDS/Components/Calendar/CalendarHeaderReusableView.swift | 8 ++++---- VDS/SupportingFiles/ReleaseNotes.txt | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/VDS/Components/Calendar/CalendarHeaderReusableView.swift b/VDS/Components/Calendar/CalendarHeaderReusableView.swift index d3da2dca..218f5471 100644 --- a/VDS/Components/Calendar/CalendarHeaderReusableView.swift +++ b/VDS/Components/Calendar/CalendarHeaderReusableView.swift @@ -68,16 +68,16 @@ class CalendarHeaderReusableView: UICollectionReusableView { $0.kind = .ghost $0.iconName = .leftCaret $0.iconOffset = .init(x: -2, y: 0) - $0.icon.size = .small - $0.size = .small + $0.customContainerSize = 40 + $0.icon.customSize = 16 } internal var nextButton = ButtonIcon().with { $0.kind = .ghost $0.iconName = .rightCaret $0.iconOffset = .init(x: 2, y: 0) - $0.icon.size = .small - $0.size = .small + $0.customContainerSize = 40 + $0.icon.customSize = 16 } internal var headerTitle = Label().with { diff --git a/VDS/SupportingFiles/ReleaseNotes.txt b/VDS/SupportingFiles/ReleaseNotes.txt index 5014b345..e89bb500 100644 --- a/VDS/SupportingFiles/ReleaseNotes.txt +++ b/VDS/SupportingFiles/ReleaseNotes.txt @@ -2,6 +2,7 @@ ---------------- - CXTDT-553663 - DropdownSelect - Accessibility - has popup - CXTDT-568463 - Calendar - On long press, hover randomizes +- CXTDT-568412 - Calendar - Incorrect side nav icon size 1.0.66 ---------------- From c1f2aa65919d28b1cf7c046ea86828432173040b Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 14 Jun 2024 11:33:59 -0500 Subject: [PATCH 11/83] refactored textarea Signed-off-by: Matt Bruce --- VDS/Components/TextFields/TextArea/TextArea.swift | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/VDS/Components/TextFields/TextArea/TextArea.swift b/VDS/Components/TextFields/TextArea/TextArea.swift index 90e797a4..79e7b825 100644 --- a/VDS/Components/TextFields/TextArea/TextArea.swift +++ b/VDS/Components/TextFields/TextArea/TextArea.swift @@ -137,7 +137,8 @@ open class TextArea: EntryFieldBase { /// 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() - + containerView.isAccessibilityElement = true + textView.isAccessibilityElement = false textView.isScrollEnabled = true textView.autocorrectionType = .no @@ -194,8 +195,9 @@ open class TextArea: EntryFieldBase { open override func updateAccessibility() { super.updateAccessibility() - textView.accessibilityLabel = accessibilityLabelText - textView.accessibilityHint = isReadOnly || !isEnabled ? "" : "Double tap to open." + containerView.accessibilityLabel = "\(Self.self), \(accessibilityLabelText)" + containerView.accessibilityHint = isReadOnly || !isEnabled ? "" : "Double tap to open." + containerView.accessibilityValue = value } override func updateRules() { @@ -225,7 +227,7 @@ open class TextArea: EntryFieldBase { open override var accessibilityElements: [Any]? { get { var elements = [Any]() - elements.append(contentsOf: [titleLabel, textView]) + elements.append(contentsOf: [titleLabel, containerView]) if showError { elements.append(statusIcon) From 69cdae83749b60f0debced661db3cc0134c13365 Mon Sep 17 00:00:00 2001 From: vasavk Date: Fri, 14 Jun 2024 22:04:25 +0530 Subject: [PATCH 12/83] Digital ACT-191 CXTDT-568422 defect: Calendar - DarkMode Legend icon fill using Light mode color --- VDS/Components/Calendar/CalendarFooterReusableView.swift | 4 ++-- VDS/SupportingFiles/ReleaseNotes.txt | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/VDS/Components/Calendar/CalendarFooterReusableView.swift b/VDS/Components/Calendar/CalendarFooterReusableView.swift index 263cd83e..020655ea 100644 --- a/VDS/Components/Calendar/CalendarFooterReusableView.swift +++ b/VDS/Components/Calendar/CalendarFooterReusableView.swift @@ -224,7 +224,7 @@ private class LegendCollectionViewCell: UICollectionViewCell { title.text = text title.textColor = textColorConfiguration.getColor(surface) - legendIndicator.backgroundColor = drawSemiCircle ? .clear : (clearFullcircle ? .clear : color) + legendIndicator.backgroundColor = drawSemiCircle ? .clear : (clearFullcircle ? .clear : indicatorColorConfiguration.getColor(surface)) legendIndicator.layer.borderColor = indicatorColorConfiguration.getColor(surface).cgColor self.layoutIfNeeded() @@ -239,7 +239,7 @@ private class LegendCollectionViewCell: UICollectionViewCell { path.addArc(withCenter: center, radius: center.x, startAngle: 2 * .pi, endAngle: .pi, clockwise: true) path.close() shapeLayer.path = path.cgPath - shapeLayer.fillColor = color.cgColor + shapeLayer.fillColor = indicatorColorConfiguration.getColor(surface).cgColor guard legendIndicator.layer.sublayers?.contains(shapeLayer) ?? true else { return } legendIndicator.layer.addSublayer(shapeLayer) diff --git a/VDS/SupportingFiles/ReleaseNotes.txt b/VDS/SupportingFiles/ReleaseNotes.txt index e89bb500..8a038e51 100644 --- a/VDS/SupportingFiles/ReleaseNotes.txt +++ b/VDS/SupportingFiles/ReleaseNotes.txt @@ -3,6 +3,7 @@ - CXTDT-553663 - DropdownSelect - Accessibility - has popup - CXTDT-568463 - Calendar - On long press, hover randomizes - CXTDT-568412 - Calendar - Incorrect side nav icon size +- CXTDT-568422 - Calendar - DarkMode Legend icon fill using Light mode color 1.0.66 ---------------- From 652088bbb186e44da1c8f180f6c662339ef5fbdc Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 14 Jun 2024 12:31:21 -0500 Subject: [PATCH 13/83] refactored to base class and removed more duplicate code Signed-off-by: Matt Bruce --- VDS/Components/DatePicker/DatePicker.swift | 33 +----------- .../DropdownSelect/DropdownSelect.swift | 31 ------------ .../TextFields/EntryFieldBase.swift | 38 +++++++++++++- .../TextFields/InputField/InputField.swift | 13 ++--- .../TextFields/TextArea/TextArea.swift | 50 ++++--------------- 5 files changed, 51 insertions(+), 114 deletions(-) diff --git a/VDS/Components/DatePicker/DatePicker.swift b/VDS/Components/DatePicker/DatePicker.swift index 1040a82c..fdc6e05f 100644 --- a/VDS/Components/DatePicker/DatePicker.swift +++ b/VDS/Components/DatePicker/DatePicker.swift @@ -100,8 +100,6 @@ open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopov /// 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() - - containerView.isAccessibilityElement = true // setting color config selectedDateLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable() @@ -142,36 +140,7 @@ open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopov selectedDateLabel.isEnabled = isEnabled calendarIcon.color = iconColorConfiguration.getColor(self) } - - open override func updateAccessibility() { - super.updateAccessibility() - containerView.accessibilityLabel = "Date Picker, \(accessibilityLabelText)" - containerView.accessibilityHint = isReadOnly || !isEnabled ? "" : "Double tap to open." - containerView.accessibilityValue = value - } - - open override var accessibilityElements: [Any]? { - get { - var elements = [Any]() - elements.append(contentsOf: [titleLabel, containerView]) - - if showError { - elements.append(statusIcon) - if let errorText, !errorText.isEmpty { - elements.append(errorLabel) - } - } - - if let helperText, !helperText.isEmpty { - elements.append(helperLabel) - } - - return elements - } - - set { super.accessibilityElements = newValue } - } - + /// Resets to default settings. open override func reset() { super.reset() diff --git a/VDS/Components/DropdownSelect/DropdownSelect.swift b/VDS/Components/DropdownSelect/DropdownSelect.swift index 37ec7984..4b9026f1 100644 --- a/VDS/Components/DropdownSelect/DropdownSelect.swift +++ b/VDS/Components/DropdownSelect/DropdownSelect.swift @@ -131,7 +131,6 @@ open class DropdownSelect: EntryFieldBase { open override func setup() { super.setup() - containerView.isAccessibilityElement = true inlineDisplayLabel.isAccessibilityElement = true dropdownField.width(0) @@ -276,36 +275,6 @@ open class DropdownSelect: EntryFieldBase { statusIcon.color = iconColorConfiguration.getColor(self) } - open override func updateAccessibility() { - super.updateAccessibility() - containerView.accessibilityLabel = "Dropdown Select, \(accessibilityLabelText)" - containerView.accessibilityHint = isReadOnly || !isEnabled ? "" : "has popup, Double tap to open." - containerView.accessibilityValue = value - } - - open override var accessibilityElements: [Any]? { - get { - var elements = [Any]() - elements.append(contentsOf: [titleLabel, containerView]) - - if showError { - elements.append(statusIcon) - if let errorText, !errorText.isEmpty { - elements.append(errorLabel) - } - } - - if let helperText, !helperText.isEmpty { - elements.append(helperLabel) - } - - return elements - } - - set { super.accessibilityElements = newValue } - } - - @objc open func pickerDoneClicked() { optionsPicker.isHidden = true dropdownField.resignFirstResponder() diff --git a/VDS/Components/TextFields/EntryFieldBase.swift b/VDS/Components/TextFields/EntryFieldBase.swift index 2111a7f0..493aff0f 100644 --- a/VDS/Components/TextFields/EntryFieldBase.swift +++ b/VDS/Components/TextFields/EntryFieldBase.swift @@ -95,6 +95,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { internal var containerView: UIView = { return UIView().with { $0.translatesAutoresizingMaskIntoConstraints = false + $0.isAccessibilityElement = true } }() @@ -243,7 +244,8 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { open var accessibilityLabelText: String { var accessibilityLabels = [String]() - if let text = titleLabel.text { + + if let text = titleLabel.text?.trimmingCharacters(in: .whitespaces) { accessibilityLabels.append(text) } if isReadOnly { @@ -255,9 +257,14 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { if let errorText, showError { accessibilityLabels.append("error, \(errorText)") } + + accessibilityLabels.append("\(Self.self)") + return accessibilityLabels.joined(separator: ", ") } + open var accessibilityHintText: String = "Double tap to open" + //-------------------------------------------------- // MARK: - Overrides //-------------------------------------------------- @@ -447,6 +454,35 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { } } + open override func updateAccessibility() { + super.updateAccessibility() + containerView.accessibilityLabel = accessibilityLabelText + containerView.accessibilityHint = isReadOnly || !isEnabled ? "" : accessibilityHintText + containerView.accessibilityValue = value + } + + open override var accessibilityElements: [Any]? { + get { + var elements = [Any]() + elements.append(contentsOf: [titleLabel, containerView]) + + if showError { + elements.append(statusIcon) + if let errorText, !errorText.isEmpty { + elements.append(errorLabel) + } + } + + if let helperText, !helperText.isEmpty { + elements.append(helperLabel) + } + + return elements + } + + set { super.accessibilityElements = newValue } + } + //-------------------------------------------------- // MARK: - Private Methods //-------------------------------------------------- diff --git a/VDS/Components/TextFields/InputField/InputField.swift b/VDS/Components/TextFields/InputField/InputField.swift index 9635cb5f..e3336b76 100644 --- a/VDS/Components/TextFields/InputField/InputField.swift +++ b/VDS/Components/TextFields/InputField/InputField.swift @@ -102,6 +102,7 @@ open class InputField: EntryFieldBase { open var textField = TextField().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.textStyle = TextStyle.bodyLarge + $0.isAccessibilityElement = false } /// Color configuration for the textField. @@ -181,8 +182,7 @@ open class InputField: EntryFieldBase { /// 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() - containerView.isAccessibilityElement = true - textField.isAccessibilityElement = false + accessibilityHintText = "Double tap to edit" textField.heightAnchor.constraint(equalToConstant: 20).isActive = true textField.delegate = self @@ -230,14 +230,7 @@ open class InputField: EntryFieldBase { textField.isEnabled = isEnabled textField.isUserInteractionEnabled = isEnabled && !isReadOnly } - - open override func updateAccessibility() { - super.updateAccessibility() - containerView.accessibilityLabel = "Input Field, \(accessibilityLabelText)" - containerView.accessibilityHint = isReadOnly || !isEnabled ? "" : "Double tap to edit." - containerView.accessibilityValue = value - } - + open override func updateErrorLabel() { super.updateErrorLabel() diff --git a/VDS/Components/TextFields/TextArea/TextArea.swift b/VDS/Components/TextFields/TextArea/TextArea.swift index 79e7b825..12fbc228 100644 --- a/VDS/Components/TextFields/TextArea/TextArea.swift +++ b/VDS/Components/TextFields/TextArea/TextArea.swift @@ -42,14 +42,14 @@ open class TextArea: EntryFieldBase { $0.spacing = VDSLayout.space3X } }() - + open var characterCounterLabel = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) $0.textStyle = .bodySmall $0.textAlignment = .right $0.numberOfLines = 1 } - + open var minHeight: Height = .twoX { didSet { setNeedsUpdate() } } //-------------------------------------------------- @@ -101,13 +101,15 @@ open class TextArea: EntryFieldBase { open override var value: String? { return textView.text } - + /// UITextView shown in the TextArea. open var textView = TextView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.sizeToFit() - $0.isScrollEnabled = false + $0.isAccessibilityElement = false + $0.isScrollEnabled = true $0.textContainerInset = .zero + $0.autocorrectionType = .no $0.textContainer.lineFragmentPadding = 0 } @@ -137,10 +139,8 @@ open class TextArea: EntryFieldBase { /// 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() - containerView.isAccessibilityElement = true - textView.isAccessibilityElement = false - textView.isScrollEnabled = true - textView.autocorrectionType = .no + + accessibilityHintText = "Double tap to edit" //events textView @@ -192,14 +192,7 @@ open class TextArea: EntryFieldBase { characterCounterLabel.surface = surface highlightCharacterOverflow() } - - open override func updateAccessibility() { - super.updateAccessibility() - containerView.accessibilityLabel = "\(Self.self), \(accessibilityLabelText)" - containerView.accessibilityHint = isReadOnly || !isEnabled ? "" : "Double tap to open." - containerView.accessibilityValue = value - } - + override func updateRules() { super.updateRules() @@ -223,30 +216,7 @@ open class TextArea: EntryFieldBase { stackView.addArrangedSubview(characterCounterLabel) return stackView } - - open override var accessibilityElements: [Any]? { - get { - var elements = [Any]() - elements.append(contentsOf: [titleLabel, containerView]) - - if showError { - elements.append(statusIcon) - if let errorText, !errorText.isEmpty { - elements.append(errorLabel) - } - } - - if let helperText, !helperText.isEmpty { - elements.append(helperLabel) - } - - return elements - } - - set { super.accessibilityElements = newValue } - } - - + open override var canBecomeFirstResponder: Bool { return textView.canBecomeFirstResponder } From e8f225cdc310d78a094f347fedbcb37988393180 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 14 Jun 2024 12:59:13 -0500 Subject: [PATCH 14/83] refactored responder property Signed-off-by: Matt Bruce --- .../DropdownSelect/DropdownSelect.swift | 18 ++-------------- .../TextFields/EntryFieldBase.swift | 18 ++++++++++++++++ .../TextFields/InputField/InputField.swift | 21 ++++--------------- .../TextFields/TextArea/TextArea.swift | 19 +++-------------- 4 files changed, 27 insertions(+), 49 deletions(-) diff --git a/VDS/Components/DropdownSelect/DropdownSelect.swift b/VDS/Components/DropdownSelect/DropdownSelect.swift index 4b9026f1..1496f392 100644 --- a/VDS/Components/DropdownSelect/DropdownSelect.swift +++ b/VDS/Components/DropdownSelect/DropdownSelect.swift @@ -66,6 +66,8 @@ open class DropdownSelect: EntryFieldBase { //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- + internal override var responder: UIResponder? { dropdownField } + internal var minWidthDefault = 66.0 internal var minWidthInlineLabel = 102.0 internal override var minWidth: CGFloat { showInlineLabel ? minWidthInlineLabel : minWidthDefault } @@ -281,22 +283,6 @@ open class DropdownSelect: EntryFieldBase { setNeedsUpdate() UIAccessibility.post(notification: .layoutChanged, argument: containerView) } - - open override var canBecomeFirstResponder: Bool { - return dropdownField.canBecomeFirstResponder - } - - open override func becomeFirstResponder() -> Bool { - return dropdownField.becomeFirstResponder() - } - - open override var canResignFirstResponder: Bool { - return dropdownField.canResignFirstResponder - } - - open override func resignFirstResponder() -> Bool { - return dropdownField.resignFirstResponder() - } } //-------------------------------------------------- diff --git a/VDS/Components/TextFields/EntryFieldBase.swift b/VDS/Components/TextFields/EntryFieldBase.swift index 493aff0f..4e7422ee 100644 --- a/VDS/Components/TextFields/EntryFieldBase.swift +++ b/VDS/Components/TextFields/EntryFieldBase.swift @@ -40,6 +40,8 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- + internal var responder: UIResponder? { return nil } + internal let mainStackView = UIStackView().with { $0.axis = .vertical $0.alignment = .fill @@ -367,6 +369,22 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { isReadOnly = false onChange = nil } + + 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 diff --git a/VDS/Components/TextFields/InputField/InputField.swift b/VDS/Components/TextFields/InputField/InputField.swift index e3336b76..c2f779d4 100644 --- a/VDS/Components/TextFields/InputField/InputField.swift +++ b/VDS/Components/TextFields/InputField/InputField.swift @@ -34,6 +34,8 @@ open class InputField: EntryFieldBase { //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- + internal override var responder: UIResponder? { textField } + internal override var containerBackgroundColor: UIColor { if showSuccess { return backgroundColorConfiguration.getColor(self) @@ -102,7 +104,7 @@ open class InputField: EntryFieldBase { open var textField = TextField().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.textStyle = TextStyle.bodyLarge - $0.isAccessibilityElement = false + $0.isAccessibilityElement = false } /// Color configuration for the textField. @@ -280,22 +282,6 @@ open class InputField: EntryFieldBase { set { super.accessibilityElements = newValue } } - - open override var canBecomeFirstResponder: Bool { - return textField.canBecomeFirstResponder - } - - open override func becomeFirstResponder() -> Bool { - return textField.becomeFirstResponder() - } - - open override var canResignFirstResponder: Bool { - return textField.canResignFirstResponder - } - - open override func resignFirstResponder() -> Bool { - return textField.resignFirstResponder() - } } extension InputField: UITextFieldDelegate { @@ -308,6 +294,7 @@ extension InputField: UITextFieldDelegate { public func textFieldDidEndEditing(_ textField: UITextField) { fieldType.handler().textFieldDidEndEditing(self, textField: textField) validate() + UIAccessibility.post(notification: .layoutChanged, argument: self.containerView) } public func textFieldDidChangeSelection(_ textField: UITextField) { diff --git a/VDS/Components/TextFields/TextArea/TextArea.swift b/VDS/Components/TextFields/TextArea/TextArea.swift index 12fbc228..a487dc53 100644 --- a/VDS/Components/TextFields/TextArea/TextArea.swift +++ b/VDS/Components/TextFields/TextArea/TextArea.swift @@ -32,6 +32,8 @@ open class TextArea: EntryFieldBase { //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- + internal override var responder: UIResponder? { textView } + internal var textViewHeightConstraint: NSLayoutConstraint? internal var inputFieldStackView: UIStackView = { @@ -159,6 +161,7 @@ open class TextArea: EntryFieldBase { .publisher(for: .editingDidEnd) .sink { [weak self] _ in self?.validate() + UIAccessibility.post(notification: .layoutChanged, argument: self?.containerView) }.store(in: &subscribers) textViewHeightConstraint = textView.heightAnchor.constraint(greaterThanOrEqualToConstant: containerSize.height) @@ -217,22 +220,6 @@ open class TextArea: EntryFieldBase { return stackView } - open override var canBecomeFirstResponder: Bool { - return textView.canBecomeFirstResponder - } - - open override func becomeFirstResponder() -> Bool { - return textView.becomeFirstResponder() - } - - open override var canResignFirstResponder: Bool { - return textView.canResignFirstResponder - } - - open override func resignFirstResponder() -> Bool { - return textView.resignFirstResponder() - } - //-------------------------------------------------- // MARK: - Private Methods //-------------------------------------------------- From a2a06f3870fb657c37c57d490e2a7f0f269e65f1 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 14 Jun 2024 15:45:04 -0500 Subject: [PATCH 15/83] added token to enum Signed-off-by: Matt Bruce --- VDS/Components/TileContainer/TileContainer.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/VDS/Components/TileContainer/TileContainer.swift b/VDS/Components/TileContainer/TileContainer.swift index dc3c936e..d600f5dd 100644 --- a/VDS/Components/TileContainer/TileContainer.swift +++ b/VDS/Components/TileContainer/TileContainer.swift @@ -69,6 +69,7 @@ open class TileContainerBase: Control where Padding case secondary case white case black + case token(UIColor.VDSColor) case custom(UIColor) private var reflectedValue: String { String(reflecting: self) } @@ -484,6 +485,8 @@ extension TileContainerBase { return whiteColorConfig.getColor(object.surface) case .black: return blackColorConfig.getColor(object.surface) + case .token(let vdsColor): + return vdsColor.uiColor case .custom(let color): return color } From c697c4a4374440efe8fc38ebf43e7959ee361d41 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 14 Jun 2024 16:11:18 -0500 Subject: [PATCH 16/83] updated release notes Signed-off-by: Matt Bruce --- VDS/SupportingFiles/ReleaseNotes.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/VDS/SupportingFiles/ReleaseNotes.txt b/VDS/SupportingFiles/ReleaseNotes.txt index 8a038e51..69b826f9 100644 --- a/VDS/SupportingFiles/ReleaseNotes.txt +++ b/VDS/SupportingFiles/ReleaseNotes.txt @@ -4,6 +4,9 @@ - CXTDT-568463 - Calendar - On long press, hover randomizes - CXTDT-568412 - Calendar - Incorrect side nav icon size - CXTDT-568422 - Calendar - DarkMode Legend icon fill using Light mode color +- CXTDT-560823 - TextArea - Accessibility +- CXTDT-560458 - Dropdown/TextArea - Different voiceover +- CXTDT-546821 - TextArea - Accessibility 1.0.66 ---------------- From f08fe00a25fe4419205411a58b42cd1381d59702 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 14 Jun 2024 16:14:06 -0500 Subject: [PATCH 17/83] updated release notes with close issues Signed-off-by: Matt Bruce --- VDS/SupportingFiles/ReleaseNotes.txt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/VDS/SupportingFiles/ReleaseNotes.txt b/VDS/SupportingFiles/ReleaseNotes.txt index 69b826f9..cd4712b2 100644 --- a/VDS/SupportingFiles/ReleaseNotes.txt +++ b/VDS/SupportingFiles/ReleaseNotes.txt @@ -1,12 +1,14 @@ 1.0.67 ---------------- -- CXTDT-553663 - DropdownSelect - Accessibility - has popup - CXTDT-568463 - Calendar - On long press, hover randomizes - CXTDT-568412 - Calendar - Incorrect side nav icon size - CXTDT-568422 - Calendar - DarkMode Legend icon fill using Light mode color -- CXTDT-560823 - TextArea - Accessibility +- CXTDT-553663 - DropdownSelect - Accessibility - has popup +- CXTDT-565796 - DropdownSelect - Accessibility - CXTDT-560458 - Dropdown/TextArea - Different voiceover +- CXTDT-565106 - InputField - CreditCard - Icons - CXTDT-546821 - TextArea - Accessibility +- CXTDT-560823 - TextArea - Accessibility 1.0.66 ---------------- From 001c11a1d6c5e7cccaabecc7692a720da02b5879 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 14 Jun 2024 16:17:18 -0500 Subject: [PATCH 18/83] updated version Signed-off-by: Matt Bruce --- VDS.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index fdfe2b15..c17e971e 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -1523,7 +1523,7 @@ BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 66; + CURRENT_PROJECT_VERSION = 67; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1561,7 +1561,7 @@ BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 66; + CURRENT_PROJECT_VERSION = 67; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; From 21595355a9aef9ada4d1755926d1f1ce705b71a3 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Mon, 17 Jun 2024 11:05:29 -0500 Subject: [PATCH 19/83] updated for issue Signed-off-by: Matt Bruce --- VDS/Components/DropdownSelect/DropdownSelect.swift | 1 + VDS/SupportingFiles/ReleaseNotes.txt | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/VDS/Components/DropdownSelect/DropdownSelect.swift b/VDS/Components/DropdownSelect/DropdownSelect.swift index 1496f392..7fc52808 100644 --- a/VDS/Components/DropdownSelect/DropdownSelect.swift +++ b/VDS/Components/DropdownSelect/DropdownSelect.swift @@ -132,6 +132,7 @@ open class DropdownSelect: EntryFieldBase { /// 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() + accessibilityHintText = "has popup, Double tap to open." inlineDisplayLabel.isAccessibilityElement = true diff --git a/VDS/SupportingFiles/ReleaseNotes.txt b/VDS/SupportingFiles/ReleaseNotes.txt index cd4712b2..8f13a9b0 100644 --- a/VDS/SupportingFiles/ReleaseNotes.txt +++ b/VDS/SupportingFiles/ReleaseNotes.txt @@ -1,9 +1,12 @@ +1.0.68 +---------------- +- CXTDT-553663 - DropdownSelect - Accessibility - has popup + 1.0.67 ---------------- - CXTDT-568463 - Calendar - On long press, hover randomizes - CXTDT-568412 - Calendar - Incorrect side nav icon size - CXTDT-568422 - Calendar - DarkMode Legend icon fill using Light mode color -- CXTDT-553663 - DropdownSelect - Accessibility - has popup - CXTDT-565796 - DropdownSelect - Accessibility - CXTDT-560458 - Dropdown/TextArea - Different voiceover - CXTDT-565106 - InputField - CreditCard - Icons From 225c44a84a1aa6e2568f10ad4a4690aede979e05 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 19 Jun 2024 09:35:51 -0500 Subject: [PATCH 20/83] added action element Signed-off-by: Matt Bruce --- VDS.xcodeproj/project.pbxproj | 4 ++++ VDS/Classes/AccessibilityActionElement.swift | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 VDS/Classes/AccessibilityActionElement.swift diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index c17e971e..b44980ae 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -172,6 +172,7 @@ EAF193432C134F3800C68D18 /* TableCellItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 443DBAF92BDA303F0021497E /* TableCellItem.swift */; }; EAF1FE9929D4850E00101452 /* Clickable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF1FE9829D4850E00101452 /* Clickable.swift */; }; EAF1FE9B29DB1A6000101452 /* Changeable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF1FE9A29DB1A6000101452 /* Changeable.swift */; }; + EAF2F4762C231EAA007BFEDC /* AccessibilityActionElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */; }; EAF7F0952899861000B287F5 /* CheckboxItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0932899861000B287F5 /* CheckboxItem.swift */; }; EAF7F09A2899B17200B287F5 /* CATransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0992899B17200B287F5 /* CATransaction.swift */; }; EAF7F0A0289AB7EC00B287F5 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F09F289AB7EC00B287F5 /* View.swift */; }; @@ -398,6 +399,7 @@ EAEEECAE2B1FC2BA00531FC2 /* ToggleViewChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ToggleViewChangeLog.txt; sourceTree = ""; }; EAF1FE9829D4850E00101452 /* Clickable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Clickable.swift; sourceTree = ""; }; EAF1FE9A29DB1A6000101452 /* Changeable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Changeable.swift; sourceTree = ""; }; + EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityActionElement.swift; sourceTree = ""; }; EAF7F0932899861000B287F5 /* CheckboxItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckboxItem.swift; sourceTree = ""; }; EAF7F0992899B17200B287F5 /* CATransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CATransaction.swift; sourceTree = ""; }; EAF7F09F289AB7EC00B287F5 /* View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = ""; }; @@ -743,6 +745,7 @@ EA985C1C296CD13600F2FF2E /* BundleManager.swift */, EAF7F0B8289C139800B287F5 /* ColorConfiguration.swift */, EAB5FEF02927F4AA00998C17 /* SelfSizingCollectionView.swift */, + EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */, ); path = Classes; sourceTree = ""; @@ -1244,6 +1247,7 @@ EA0D1C412A6AD61C00E5C127 /* Typography+Additional.swift in Sources */, EAC925842911C63100091998 /* Colorable.swift in Sources */, 18B463A42BBD3C46005C4528 /* DropdownOptionModel.swift in Sources */, + EAF2F4762C231EAA007BFEDC /* AccessibilityActionElement.swift in Sources */, EAC58BFD2BE935C300BA39FA /* TitleLockupTextColor.swift in Sources */, EAACB89A2B927108006A3869 /* Valuing.swift in Sources */, EAE785312BA0A438009428EA /* UIImage+Helper.swift in Sources */, diff --git a/VDS/Classes/AccessibilityActionElement.swift b/VDS/Classes/AccessibilityActionElement.swift new file mode 100644 index 00000000..df3b6ed9 --- /dev/null +++ b/VDS/Classes/AccessibilityActionElement.swift @@ -0,0 +1,20 @@ +// +// AccessibilityActionElement.swift +// VDS +// +// Created by Matt Bruce on 6/19/24. +// + +import Foundation +import UIKit + +/// Custom UIAccessibilityElement that allows you to set the default action used in accessibilityActivate. +public class AccessibilityActionElement: UIAccessibilityElement { + var action: (() -> Void)? + + public override func accessibilityActivate() -> Bool { + action?() + return true + } +} + From 6d42ec599cd0f834b6ff993ca8cc12798d494f0d Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 19 Jun 2024 09:36:11 -0500 Subject: [PATCH 21/83] updated accessibility elements Signed-off-by: Matt Bruce --- .../Selector/SelectorItemBase.swift | 38 ++++++++++++++----- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/VDS/BaseClasses/Selector/SelectorItemBase.swift b/VDS/BaseClasses/Selector/SelectorItemBase.swift index cee8d9c2..24e949e7 100644 --- a/VDS/BaseClasses/Selector/SelectorItemBase.swift +++ b/VDS/BaseClasses/Selector/SelectorItemBase.swift @@ -149,14 +149,13 @@ open class SelectorItemBase: Control, Errorable, open var accessibilityLabelText: String { var accessibilityLabels = [String]() - accessibilityLabels.append("\(Selector.self)") - if let text = labelText { + if let text = labelText, !text.isEmpty { accessibilityLabels.append(text) } - if let text = childText { + if let text = childText, !text.isEmpty { accessibilityLabels.append(text) } @@ -164,7 +163,7 @@ open class SelectorItemBase: Control, Errorable, accessibilityLabels.append("dimmed") } - if let errorText, showError { + if let errorText, showError, !errorText.isEmpty { accessibilityLabels.append("error, \(errorText)") } @@ -177,9 +176,7 @@ open class SelectorItemBase: Control, Errorable, /// Executed on initialization for this View. open override func initialSetup() { super.initialSetup() - onClick = { control in - control.toggle() - } + } /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations. @@ -190,7 +187,6 @@ open class SelectorItemBase: Control, Errorable, selectorView.shouldUpdateAccessibility = false isAccessibilityElement = false - accessibilityElements = [selectorView, label, childLabel, errorLabel] addSubview(mainStackView) mainStackView.isUserInteractionEnabled = false @@ -211,6 +207,7 @@ open class SelectorItemBase: Control, Errorable, open override func updateView() { super.updateView() updateLabels() + selectorView.isUserInteractionEnabled = true selectorView.showError = showError selectorView.isSelected = isSelected selectorView.isHighlighted = isHighlighted @@ -224,7 +221,30 @@ open class SelectorItemBase: Control, Errorable, selectorView.accessibilityLabel = accessibilityLabelText selectorView.accessibilityHint = !isEnabled ? "" : "Double tap to activate." accessibilityValue = accessibilityValueText - + } + + open override var accessibilityElements: [Any]? { + get { + var elements = [Any]() + + elements.append(selectorView) + + if let text = labelText, !text.isEmpty { + elements.append(label) + } + + if let text = childText, !text.isEmpty { + elements.append(childLabel) + } + + if let errorText, showError, !errorText.isEmpty { + elements.append(errorLabel) + } + return elements + } + set { + super.accessibilityElements = newValue + } } /// Resets to default settings. From 4b06ef2e31d0dda57520a45967b7e65e118cc50d Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 19 Jun 2024 09:38:34 -0500 Subject: [PATCH 22/83] updated to use new accessibility Signed-off-by: Matt Bruce --- VDS/Components/Label/Label.swift | 62 +++++++++----------------------- 1 file changed, 16 insertions(+), 46 deletions(-) diff --git a/VDS/Components/Label/Label.swift b/VDS/Components/Label/Label.swift index 8c995ca1..c95e27a8 100644 --- a/VDS/Components/Label/Label.swift +++ b/VDS/Components/Label/Label.swift @@ -91,16 +91,14 @@ open class Label: UILabel, ViewProtocol, UserInfoable { private struct LabelAction { var range: NSRange var action: PassthroughSubject - var accessibilityId: Int = 0 - + var frame: CGRect = .zero func performAction() { action.send() } - init(range: NSRange, action: PassthroughSubject, accessibilityID: Int = 0) { + init(range: NSRange, action: PassthroughSubject) { self.range = range self.action = action - self.accessibilityId = accessibilityID } } @@ -265,24 +263,7 @@ open class Label: UILabel, ViewProtocol, UserInfoable { super.layoutSubviews() applyActions() } - - /// Addig custom accessibillty actions from the collection of attributes. - open override func accessibilityActivate() -> Bool { - guard let accessibleActions = accessibilityCustomActions else { return false } - - for actionable in actions { - for action in accessibleActions { - if action.hash == actionable.accessibilityId { - actionable.performAction() - return true - } - } - } - - return false - } - //-------------------------------------------------- // MARK: - Private Methods //-------------------------------------------------- @@ -375,10 +356,17 @@ open class Label: UILabel, ViewProtocol, UserInfoable { //see if the attribute is Actionable if let actionable = attribute as? any ActionLabelAttributeModel, mutableAttributedString.isValid(range: actionable.range) { //create a accessibleAction - let customAccessibilityAction = customAccessibilityAction(text: mutableAttributedString.string, range: actionable.range, accessibleText: actionable.accessibleText) - + let customAccessibilityAction = customAccessibilityElement(text: mutableAttributedString.string, + range: actionable.range, + accessibleText: actionable.accessibleText) + + // creat the action + let labelAction = LabelAction(range: actionable.range, action: actionable.action) + + customAccessibilityAction?.action = labelAction.performAction //create a wrapper for the attributes range, block and - actions.append(LabelAction(range: actionable.range, action: actionable.action, accessibilityID: customAccessibilityAction?.hashValue ?? -1)) + actions.append(labelAction) + isUserInteractionEnabled = true } } @@ -404,7 +392,7 @@ open class Label: UILabel, ViewProtocol, UserInfoable { } } - private func customAccessibilityAction(text: String?, range: NSRange, accessibleText: String? = nil) -> UIAccessibilityCustomAction? { + private func customAccessibilityElement(text: String?, range: NSRange, accessibleText: String? = nil) -> AccessibilityActionElement? { guard let text = text, let attributedText else { return nil } @@ -425,31 +413,13 @@ open class Label: UILabel, ViewProtocol, UserInfoable { let substringBounds = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer) // Create custom accessibility element - let element = UIAccessibilityElement(accessibilityContainer: self) + let element = AccessibilityActionElement(accessibilityContainer: self) element.accessibilityLabel = actionText element.accessibilityTraits = .link + element.accessibilityHint = "Double tap to open" element.accessibilityFrameInContainerSpace = substringBounds - - //TODO: accessibilityHint for Label -// element.accessibilityHint = MVMCoreUIUtility.hardcodedString(withKey: "swipe_to_select_with_action_hint") - accessibilityElements = (accessibilityElements ?? []).compactMap{$0 as? UIAccessibilityElement}.filter { $0.accessibilityLabel != actionText } accessibilityElements?.append(element) - - let accessibleAction = UIAccessibilityCustomAction(name: actionText, target: self, selector: #selector(accessibilityCustomAction(_:))) - accessibilityCustomActions?.append(accessibleAction) - return accessibleAction - } - - @objc private func accessibilityCustomAction(_ action: UIAccessibilityCustomAction) { - - for actionable in actions { - if action.hash == actionable.accessibilityId { - actionable.performAction() - return - } - } + return element } } - - From f9799386d62b6b41ca504baca6199c24255bb1f7 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 19 Jun 2024 09:35:51 -0500 Subject: [PATCH 23/83] added action element Signed-off-by: Matt Bruce --- VDS.xcodeproj/project.pbxproj | 4 ++++ VDS/Classes/AccessibilityActionElement.swift | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 VDS/Classes/AccessibilityActionElement.swift diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index c17e971e..b44980ae 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -172,6 +172,7 @@ EAF193432C134F3800C68D18 /* TableCellItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 443DBAF92BDA303F0021497E /* TableCellItem.swift */; }; EAF1FE9929D4850E00101452 /* Clickable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF1FE9829D4850E00101452 /* Clickable.swift */; }; EAF1FE9B29DB1A6000101452 /* Changeable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF1FE9A29DB1A6000101452 /* Changeable.swift */; }; + EAF2F4762C231EAA007BFEDC /* AccessibilityActionElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */; }; EAF7F0952899861000B287F5 /* CheckboxItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0932899861000B287F5 /* CheckboxItem.swift */; }; EAF7F09A2899B17200B287F5 /* CATransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0992899B17200B287F5 /* CATransaction.swift */; }; EAF7F0A0289AB7EC00B287F5 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F09F289AB7EC00B287F5 /* View.swift */; }; @@ -398,6 +399,7 @@ EAEEECAE2B1FC2BA00531FC2 /* ToggleViewChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ToggleViewChangeLog.txt; sourceTree = ""; }; EAF1FE9829D4850E00101452 /* Clickable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Clickable.swift; sourceTree = ""; }; EAF1FE9A29DB1A6000101452 /* Changeable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Changeable.swift; sourceTree = ""; }; + EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityActionElement.swift; sourceTree = ""; }; EAF7F0932899861000B287F5 /* CheckboxItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckboxItem.swift; sourceTree = ""; }; EAF7F0992899B17200B287F5 /* CATransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CATransaction.swift; sourceTree = ""; }; EAF7F09F289AB7EC00B287F5 /* View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = ""; }; @@ -743,6 +745,7 @@ EA985C1C296CD13600F2FF2E /* BundleManager.swift */, EAF7F0B8289C139800B287F5 /* ColorConfiguration.swift */, EAB5FEF02927F4AA00998C17 /* SelfSizingCollectionView.swift */, + EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */, ); path = Classes; sourceTree = ""; @@ -1244,6 +1247,7 @@ EA0D1C412A6AD61C00E5C127 /* Typography+Additional.swift in Sources */, EAC925842911C63100091998 /* Colorable.swift in Sources */, 18B463A42BBD3C46005C4528 /* DropdownOptionModel.swift in Sources */, + EAF2F4762C231EAA007BFEDC /* AccessibilityActionElement.swift in Sources */, EAC58BFD2BE935C300BA39FA /* TitleLockupTextColor.swift in Sources */, EAACB89A2B927108006A3869 /* Valuing.swift in Sources */, EAE785312BA0A438009428EA /* UIImage+Helper.swift in Sources */, diff --git a/VDS/Classes/AccessibilityActionElement.swift b/VDS/Classes/AccessibilityActionElement.swift new file mode 100644 index 00000000..df3b6ed9 --- /dev/null +++ b/VDS/Classes/AccessibilityActionElement.swift @@ -0,0 +1,20 @@ +// +// AccessibilityActionElement.swift +// VDS +// +// Created by Matt Bruce on 6/19/24. +// + +import Foundation +import UIKit + +/// Custom UIAccessibilityElement that allows you to set the default action used in accessibilityActivate. +public class AccessibilityActionElement: UIAccessibilityElement { + var action: (() -> Void)? + + public override func accessibilityActivate() -> Bool { + action?() + return true + } +} + From 5efcc624e004ed1bb1de389fe0e880474b1ea838 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 19 Jun 2024 09:38:34 -0500 Subject: [PATCH 24/83] updated to use new accessibility Signed-off-by: Matt Bruce --- VDS/Components/Label/Label.swift | 62 +++++++++----------------------- 1 file changed, 16 insertions(+), 46 deletions(-) diff --git a/VDS/Components/Label/Label.swift b/VDS/Components/Label/Label.swift index 4f56c272..c094f775 100644 --- a/VDS/Components/Label/Label.swift +++ b/VDS/Components/Label/Label.swift @@ -91,16 +91,14 @@ open class Label: UILabel, ViewProtocol, UserInfoable { private struct LabelAction { var range: NSRange var action: PassthroughSubject - var accessibilityId: Int = 0 - + var frame: CGRect = .zero func performAction() { action.send() } - init(range: NSRange, action: PassthroughSubject, accessibilityID: Int = 0) { + init(range: NSRange, action: PassthroughSubject) { self.range = range self.action = action - self.accessibilityId = accessibilityID } } @@ -263,24 +261,7 @@ open class Label: UILabel, ViewProtocol, UserInfoable { super.layoutSubviews() applyActions() } - - /// Addig custom accessibillty actions from the collection of attributes. - open override func accessibilityActivate() -> Bool { - guard let accessibleActions = accessibilityCustomActions else { return false } - - for actionable in actions { - for action in accessibleActions { - if action.hash == actionable.accessibilityId { - actionable.performAction() - return true - } - } - } - - return false - } - //-------------------------------------------------- // MARK: - Private Methods //-------------------------------------------------- @@ -373,10 +354,17 @@ open class Label: UILabel, ViewProtocol, UserInfoable { //see if the attribute is Actionable if let actionable = attribute as? any ActionLabelAttributeModel, mutableAttributedString.isValid(range: actionable.range) { //create a accessibleAction - let customAccessibilityAction = customAccessibilityAction(text: mutableAttributedString.string, range: actionable.range, accessibleText: actionable.accessibleText) - + let customAccessibilityAction = customAccessibilityElement(text: mutableAttributedString.string, + range: actionable.range, + accessibleText: actionable.accessibleText) + + // creat the action + let labelAction = LabelAction(range: actionable.range, action: actionable.action) + + customAccessibilityAction?.action = labelAction.performAction //create a wrapper for the attributes range, block and - actions.append(LabelAction(range: actionable.range, action: actionable.action, accessibilityID: customAccessibilityAction?.hashValue ?? -1)) + actions.append(labelAction) + isUserInteractionEnabled = true } } @@ -402,7 +390,7 @@ open class Label: UILabel, ViewProtocol, UserInfoable { } } - private func customAccessibilityAction(text: String?, range: NSRange, accessibleText: String? = nil) -> UIAccessibilityCustomAction? { + private func customAccessibilityElement(text: String?, range: NSRange, accessibleText: String? = nil) -> AccessibilityActionElement? { guard let text = text, let attributedText else { return nil } @@ -423,31 +411,13 @@ open class Label: UILabel, ViewProtocol, UserInfoable { let substringBounds = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer) // Create custom accessibility element - let element = UIAccessibilityElement(accessibilityContainer: self) + let element = AccessibilityActionElement(accessibilityContainer: self) element.accessibilityLabel = actionText element.accessibilityTraits = .link + element.accessibilityHint = "Double tap to open" element.accessibilityFrameInContainerSpace = substringBounds - - //TODO: accessibilityHint for Label -// element.accessibilityHint = MVMCoreUIUtility.hardcodedString(withKey: "swipe_to_select_with_action_hint") - accessibilityElements = (accessibilityElements ?? []).compactMap{$0 as? UIAccessibilityElement}.filter { $0.accessibilityLabel != actionText } accessibilityElements?.append(element) - - let accessibleAction = UIAccessibilityCustomAction(name: actionText, target: self, selector: #selector(accessibilityCustomAction(_:))) - accessibilityCustomActions?.append(accessibleAction) - return accessibleAction - } - - @objc private func accessibilityCustomAction(_ action: UIAccessibilityCustomAction) { - - for actionable in actions { - if action.hash == actionable.accessibilityId { - actionable.performAction() - return - } - } + return element } } - - From 4aa081f6deb2d7741445e745b7c4fc1ba1d1de92 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 19 Jun 2024 09:49:19 -0500 Subject: [PATCH 25/83] updated for checkbox only Signed-off-by: Matt Bruce --- VDS/BaseClasses/Selector/SelectorItemBase.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/VDS/BaseClasses/Selector/SelectorItemBase.swift b/VDS/BaseClasses/Selector/SelectorItemBase.swift index 24e949e7..3de2b9db 100644 --- a/VDS/BaseClasses/Selector/SelectorItemBase.swift +++ b/VDS/BaseClasses/Selector/SelectorItemBase.swift @@ -176,7 +176,9 @@ open class SelectorItemBase: Control, Errorable, /// Executed on initialization for this View. open override func initialSetup() { super.initialSetup() - + selectorView.onClick = { [weak self] control in + self?.toggle() + } } /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations. @@ -188,7 +190,6 @@ open class SelectorItemBase: Control, Errorable, isAccessibilityElement = false addSubview(mainStackView) - mainStackView.isUserInteractionEnabled = false mainStackView.addArrangedSubview(selectorStackView) mainStackView.addArrangedSubview(errorLabel) From c20027dd2c6aadae02d626942f6fe099fc4caf18 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 19 Jun 2024 10:53:19 -0500 Subject: [PATCH 26/83] updated for label click Signed-off-by: Matt Bruce --- .../Selector/SelectorItemBase.swift | 16 ++++++++- VDS/Components/Label/Label.swift | 35 +++++++++++++++++-- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/VDS/BaseClasses/Selector/SelectorItemBase.swift b/VDS/BaseClasses/Selector/SelectorItemBase.swift index 3de2b9db..bc92eb07 100644 --- a/VDS/BaseClasses/Selector/SelectorItemBase.swift +++ b/VDS/BaseClasses/Selector/SelectorItemBase.swift @@ -176,7 +176,7 @@ open class SelectorItemBase: Control, Errorable, /// Executed on initialization for this View. open override func initialSetup() { super.initialSetup() - selectorView.onClick = { [weak self] control in + onClick = { [weak self] control in self?.toggle() } } @@ -247,7 +247,21 @@ open class SelectorItemBase: Control, Errorable, super.accessibilityElements = newValue } } + + /// Overriden to take the hit if there is an onClickSubscriber and the view is not a UIControl + open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let labelPoint = convert(point, to: label) + let childLabelPoint = convert(point, to: childLabel) + if label.isAction(for: labelPoint) { + return label + } else if childLabel.isAction(for: childLabelPoint) { + return childLabel + } else { + return super.hitTest(point, with: event) + } + } + /// Resets to default settings. open override func reset() { super.reset() diff --git a/VDS/Components/Label/Label.swift b/VDS/Components/Label/Label.swift index c95e27a8..090215c2 100644 --- a/VDS/Components/Label/Label.swift +++ b/VDS/Components/Label/Label.swift @@ -371,7 +371,7 @@ open class Label: UILabel, ViewProtocol, UserInfoable { } if let accessibilityElements, !accessibilityElements.isEmpty { - let staticText = UIAccessibilityElement(accessibilityContainer: self) + let staticText = AccessibilityActionElement(accessibilityContainer: self) staticText.accessibilityLabel = text staticText.accessibilityFrameInContainerSpace = bounds @@ -385,12 +385,38 @@ open class Label: UILabel, ViewProtocol, UserInfoable { @objc private func textLinkTapped(_ gesture: UITapGestureRecognizer) { for actionable in actions { // This determines if we tapped on the desired range of text. - if gesture.didTapActionInLabel(self, inRange: actionable.range) { + let location = gesture.location(in: self) + if didTapActionInLabel(location, inRange: actionable.range) { actionable.performAction() return } } } + + public func isAction(for location: CGPoint) -> Bool { + for actionable in actions { + if didTapActionInLabel(location, inRange: actionable.range) { + return true + } + } + return false + } + + private func didTapActionInLabel(_ location: CGPoint, inRange targetRange: NSRange) -> Bool { + + guard let attributedText else { return false } + let layoutManager = NSLayoutManager() + let textContainer = NSTextContainer(size: bounds.size) + let textStorage = NSTextStorage(attributedString: attributedText) + layoutManager.addTextContainer(textContainer) + textStorage.addLayoutManager(layoutManager) + + let characterIndex = layoutManager.characterIndex(for: location, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil) + + guard let _ = attributedText.attribute(NSAttributedString.Key.action, at: characterIndex, effectiveRange: nil) as? String, characterIndex < attributedText.length else { return false } + return true + } + private func customAccessibilityElement(text: String?, range: NSRange, accessibleText: String? = nil) -> AccessibilityActionElement? { @@ -422,4 +448,9 @@ open class Label: UILabel, ViewProtocol, UserInfoable { accessibilityElements?.append(element) return element } + + public override func accessibilityActivate() -> Bool { + return false + } + } From bdd481109afb40bb231f71bfcdb586fbab49163e Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 19 Jun 2024 10:58:48 -0500 Subject: [PATCH 27/83] allow touch events for labels to bleed through Signed-off-by: Matt Bruce --- .../Selector/SelectorItemBase.swift | 14 ++++++++++ VDS/Components/Label/Label.swift | 28 ++++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/VDS/BaseClasses/Selector/SelectorItemBase.swift b/VDS/BaseClasses/Selector/SelectorItemBase.swift index fd3f6f2b..35c77922 100644 --- a/VDS/BaseClasses/Selector/SelectorItemBase.swift +++ b/VDS/BaseClasses/Selector/SelectorItemBase.swift @@ -198,7 +198,21 @@ open class SelectorItemBase: Control, Errorable, setAccessibilityLabel(for: [selectorView, label, childLabel, errorLabel]) accessibilityValue = accessibilityValueText } + + /// Overriden to take the hit if there is an onClickSubscriber and the view is not a UIControl + open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let labelPoint = convert(point, to: label) + let childLabelPoint = convert(point, to: childLabel) + if label.isAction(for: labelPoint) { + return label + } else if childLabel.isAction(for: childLabelPoint) { + return childLabel + } else { + return super.hitTest(point, with: event) + } + } + /// Resets to default settings. open override func reset() { super.reset() diff --git a/VDS/Components/Label/Label.swift b/VDS/Components/Label/Label.swift index c094f775..9dd9bf1d 100644 --- a/VDS/Components/Label/Label.swift +++ b/VDS/Components/Label/Label.swift @@ -383,12 +383,38 @@ open class Label: UILabel, ViewProtocol, UserInfoable { @objc private func textLinkTapped(_ gesture: UITapGestureRecognizer) { for actionable in actions { // This determines if we tapped on the desired range of text. - if gesture.didTapActionInLabel(self, inRange: actionable.range) { + let location = gesture.location(in: self) + if didTapActionInLabel(location, inRange: actionable.range) { actionable.performAction() return } } } + + public func isAction(for location: CGPoint) -> Bool { + for actionable in actions { + if didTapActionInLabel(location, inRange: actionable.range) { + return true + } + } + return false + } + + private func didTapActionInLabel(_ location: CGPoint, inRange targetRange: NSRange) -> Bool { + + guard let attributedText else { return false } + let layoutManager = NSLayoutManager() + let textContainer = NSTextContainer(size: bounds.size) + let textStorage = NSTextStorage(attributedString: attributedText) + layoutManager.addTextContainer(textContainer) + textStorage.addLayoutManager(layoutManager) + + let characterIndex = layoutManager.characterIndex(for: location, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil) + + guard let _ = attributedText.attribute(NSAttributedString.Key.action, at: characterIndex, effectiveRange: nil) as? String, characterIndex < attributedText.length else { return false } + return true + } + private func customAccessibilityElement(text: String?, range: NSRange, accessibleText: String? = nil) -> AccessibilityActionElement? { From 8d2125a91cafbd5f7634188b64c5056fb4985a01 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 19 Jun 2024 11:57:49 -0500 Subject: [PATCH 28/83] updated mainStackView Signed-off-by: Matt Bruce --- VDS/BaseClasses/Selector/SelectorItemBase.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/VDS/BaseClasses/Selector/SelectorItemBase.swift b/VDS/BaseClasses/Selector/SelectorItemBase.swift index bc92eb07..555102a2 100644 --- a/VDS/BaseClasses/Selector/SelectorItemBase.swift +++ b/VDS/BaseClasses/Selector/SelectorItemBase.swift @@ -191,6 +191,7 @@ open class SelectorItemBase: Control, Errorable, isAccessibilityElement = false addSubview(mainStackView) + mainStackView.isUserInteractionEnabled = false mainStackView.addArrangedSubview(selectorStackView) mainStackView.addArrangedSubview(errorLabel) selectorStackView.addArrangedSubview(selectorView) From ac816f07f0041ce05c53486a6f8d2e3e9c3591a6 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 19 Jun 2024 13:27:50 -0500 Subject: [PATCH 29/83] added accessibility override for main click Signed-off-by: Matt Bruce --- VDS/BaseClasses/Selector/SelectorBase.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/VDS/BaseClasses/Selector/SelectorBase.swift b/VDS/BaseClasses/Selector/SelectorBase.swift index f8c9650d..31d317cf 100644 --- a/VDS/BaseClasses/Selector/SelectorBase.swift +++ b/VDS/BaseClasses/Selector/SelectorBase.swift @@ -134,4 +134,15 @@ open class SelectorBase: Control, SelectorControlable { super.reset() onChange = nil } + + public var accessibilityDefaultAction: (() -> Void)? + + public override func accessibilityActivate() -> Bool { + if let accessibilityDefaultAction { + accessibilityDefaultAction() + } else { + toggle() + } + return true + } } From ec8d4ba1d66ce91d8481cb90e39442a4bf3793a9 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 19 Jun 2024 13:28:04 -0500 Subject: [PATCH 30/83] updated base with fix --- .../Selector/SelectorItemBase.swift | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/VDS/BaseClasses/Selector/SelectorItemBase.swift b/VDS/BaseClasses/Selector/SelectorItemBase.swift index 555102a2..3a04b86d 100644 --- a/VDS/BaseClasses/Selector/SelectorItemBase.swift +++ b/VDS/BaseClasses/Selector/SelectorItemBase.swift @@ -11,7 +11,7 @@ import Combine import VDSCoreTokens /// Base Class used to build out a SelectorControlable control. -open class SelectorItemBase: Control, Errorable, Changeable, Groupable { +open class SelectorItemBase: Control, Errorable, Changeable, Groupable { //-------------------------------------------------- // MARK: - Initializers @@ -149,8 +149,13 @@ open class SelectorItemBase: Control, Errorable, open var accessibilityLabelText: String { var accessibilityLabels = [String]() - accessibilityLabels.append("\(Selector.self)") + + if isSelected { + accessibilityLabels.append("selected") + } + accessibilityLabels.append("\(Selector.self)") + if let text = labelText, !text.isEmpty { accessibilityLabels.append(text) } @@ -162,7 +167,7 @@ open class SelectorItemBase: Control, Errorable, if !isEnabled { accessibilityLabels.append("dimmed") } - + if let errorText, showError, !errorText.isEmpty { accessibilityLabels.append("error, \(errorText)") } @@ -177,8 +182,14 @@ open class SelectorItemBase: Control, Errorable, open override func initialSetup() { super.initialSetup() onClick = { [weak self] control in - self?.toggle() + guard let self, isEnabled else { return } + toggle() } + + selectorView.accessibilityDefaultAction = { [weak self] in + guard let self, isEnabled else { return } + toggle() + } } /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations. @@ -209,7 +220,6 @@ open class SelectorItemBase: Control, Errorable, open override func updateView() { super.updateView() updateLabels() - selectorView.isUserInteractionEnabled = true selectorView.showError = showError selectorView.isSelected = isSelected selectorView.isHighlighted = isHighlighted @@ -251,14 +261,16 @@ open class SelectorItemBase: Control, Errorable, /// Overriden to take the hit if there is an onClickSubscriber and the view is not a UIControl open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + guard isEnabled else { return super.hitTest(point, with: event) } + let labelPoint = convert(point, to: label) let childLabelPoint = convert(point, to: childLabel) - if label.isAction(for: labelPoint) { return label } else if childLabel.isAction(for: childLabelPoint) { return childLabel } else { + guard !UIAccessibility.isVoiceOverRunning else { return nil } return super.hitTest(point, with: event) } } From f25aee83de6099dda05ab2e19aa821a004a75016 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 19 Jun 2024 14:02:11 -0500 Subject: [PATCH 31/83] only toggle when enabled Signed-off-by: Matt Bruce --- VDS/Components/Checkbox/Checkbox.swift | 2 + VDS/Components/Checkbox/CheckboxItem.swift | 2 + VDS/Components/Label/Label.swift | 9 ++- VDS/Components/RadioBox/RadioBoxItem.swift | 55 ++++++++++++------- VDS/Components/RadioButton/RadioButton.swift | 2 +- .../RadioButton/RadioButtonItem.swift | 2 +- 6 files changed, 47 insertions(+), 25 deletions(-) diff --git a/VDS/Components/Checkbox/Checkbox.swift b/VDS/Components/Checkbox/Checkbox.swift index a999e74a..3369ebcb 100644 --- a/VDS/Components/Checkbox/Checkbox.swift +++ b/VDS/Components/Checkbox/Checkbox.swift @@ -63,6 +63,8 @@ open class Checkbox: SelectorBase { /// This will change the state of the Selector and execute the actionBlock if provided. open override func toggle() { + guard isEnabled else { return } + //removed error if showError && isSelected == false { showError.toggle() diff --git a/VDS/Components/Checkbox/CheckboxItem.swift b/VDS/Components/Checkbox/CheckboxItem.swift index aa5dea5a..21943636 100644 --- a/VDS/Components/Checkbox/CheckboxItem.swift +++ b/VDS/Components/Checkbox/CheckboxItem.swift @@ -38,6 +38,8 @@ open class CheckboxItem: SelectorItemBase { //-------------------------------------------------- /// This will change the state of the Selector and execute the actionBlock if provided. open override func toggle() { + guard isEnabled else { return } + //removed error if showError && isSelected == false { showError.toggle() diff --git a/VDS/Components/Label/Label.swift b/VDS/Components/Label/Label.swift index 090215c2..1ec1de82 100644 --- a/VDS/Components/Label/Label.swift +++ b/VDS/Components/Label/Label.swift @@ -363,7 +363,12 @@ open class Label: UILabel, ViewProtocol, UserInfoable { // creat the action let labelAction = LabelAction(range: actionable.range, action: actionable.action) - customAccessibilityAction?.action = labelAction.performAction + // set the action of the accessibilityElement + customAccessibilityAction?.action = { [weak self] in + guard let self, isEnabled else { return } + labelAction.performAction() + } + //create a wrapper for the attributes range, block and actions.append(labelAction) isUserInteractionEnabled = true @@ -449,7 +454,7 @@ open class Label: UILabel, ViewProtocol, UserInfoable { return element } - public override func accessibilityActivate() -> Bool { + open override func accessibilityActivate() -> Bool { return false } diff --git a/VDS/Components/RadioBox/RadioBoxItem.swift b/VDS/Components/RadioBox/RadioBoxItem.swift index 8ca46b26..417df508 100644 --- a/VDS/Components/RadioBox/RadioBoxItem.swift +++ b/VDS/Components/RadioBox/RadioBoxItem.swift @@ -198,7 +198,6 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable { isAccessibilityElement = false selectorView.isAccessibilityElement = true selectorView.accessibilityTraits = .button - addSubview(selectorView) selectorView.isUserInteractionEnabled = false @@ -252,6 +251,8 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable { /// This will change the state of the Selector and execute the actionBlock if provided. open func toggle() { + guard isEnabled else { return } + //removed error isSelected.toggle() sendActions(for: .valueChanged) @@ -268,14 +269,14 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable { /// Used to update any Accessibility properties. open override func updateAccessibility() { super.updateAccessibility() - accessibilityLabel = accessibilityLabelText + selectorView.accessibilityLabel = accessibilityLabelText if let accessibilityValueText { - accessibilityValue = strikethrough + selectorView.accessibilityValue = strikethrough ? "\(strikethroughAccessibilityText), \(accessibilityValueText)" : accessibilityValueText } else { - accessibilityValue = strikethrough + selectorView.accessibilityValue = strikethrough ? "\(strikethroughAccessibilityText)" : accessibilityValueText } @@ -286,29 +287,41 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable { var items = [Any]() items.append(selectorView) - let elements = gatherAccessibilityElements(from: selectorView) - let views = elements.compactMap({ $0 as? UIView }) - - //update accessibilityLabel - selectorView.setAccessibilityLabel(for: views) - - //disabled - if !isEnabled { - if let label = selectorView.accessibilityLabel, !label.isEmpty { - selectorView.accessibilityLabel = "\(label), dimmed" - } else { - selectorView.accessibilityLabel = "dimmed" - } + if let text = text, !text.isEmpty { + items.append(textLabel) + } + + if let text = subText, !text.isEmpty { + items.append(subTextLabel) + } + + if let text = subTextRight, !text.isEmpty { + items.append(subTextRightLabel) } - - //append all children that are accessible - items.append(contentsOf: elements) - return items } set {} } + /// Overriden to take the hit if there is an onClickSubscriber and the view is not a UIControl + open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + guard isEnabled else { return super.hitTest(point, with: event) } + + let textPoint = convert(point, to: textLabel) + let subTextPoint = convert(point, to: subTextLabel) + let subTextRightPoint = convert(point, to: subTextRightLabel) + + if textLabel.isAction(for: textPoint) { + return textLabel + } else if subTextLabel.isAction(for: subTextPoint) { + return subTextLabel + } else if subTextRightLabel.isAction(for: subTextRightPoint) { + return subTextRightLabel + } else { + guard !UIAccessibility.isVoiceOverRunning else { return nil } + return super.hitTest(point, with: event) + } + } //-------------------------------------------------- // MARK: - Private Methods diff --git a/VDS/Components/RadioButton/RadioButton.swift b/VDS/Components/RadioButton/RadioButton.swift index 36a2cedf..f19e7b2f 100644 --- a/VDS/Components/RadioButton/RadioButton.swift +++ b/VDS/Components/RadioButton/RadioButton.swift @@ -62,7 +62,7 @@ open class RadioButton: SelectorBase { /// This will change the state of the Selector and execute the actionBlock if provided. open override func toggle() { - guard !isSelected else { return } + guard !isSelected, isEnabled else { return } //removed error if showError && isSelected == false { diff --git a/VDS/Components/RadioButton/RadioButtonItem.swift b/VDS/Components/RadioButton/RadioButtonItem.swift index ebde90c8..bc15531d 100644 --- a/VDS/Components/RadioButton/RadioButtonItem.swift +++ b/VDS/Components/RadioButton/RadioButtonItem.swift @@ -34,7 +34,7 @@ open class RadioButtonItem: SelectorItemBase { //-------------------------------------------------- /// This will change the state of the Selector and execute the actionBlock if provided. open override func toggle() { - guard !isSelected else { return } + guard !isSelected, isEnabled else { return } //removed error if showError && isSelected == false { From 4b8d0d985ae1cb32b67d5d1aca72e15b5dc7c7bf Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 19 Jun 2024 17:16:27 -0500 Subject: [PATCH 32/83] added accessibility var Signed-off-by: Matt Bruce --- VDS/Protocols/ViewProtocol.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/VDS/Protocols/ViewProtocol.swift b/VDS/Protocols/ViewProtocol.swift index 57b93982..ba912684 100644 --- a/VDS/Protocols/ViewProtocol.swift +++ b/VDS/Protocols/ViewProtocol.swift @@ -19,6 +19,9 @@ public protocol ViewProtocol: AnyObject, Initable, Resettable, Enabling, Surface /// Key of whether or not updateAccessibility() is called in setNeedsUpdate() var shouldUpdateAccessibility: Bool { get set } + /// Used for setting an implementation for the default Accessible Action + var accessibilityDefaultAction: ((Self) -> Void)? { get set } + /// Executed on initialization for this View. func initialSetup() @@ -55,7 +58,7 @@ extension ViewProtocol where Self: UIView { view.removeFromSuperview() setNeedsDisplay() } - } + } } extension ViewProtocol where Self: UIControl { From a2f7c03b8fbc2dd77bbbe2b719e22ab30c96dea1 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 19 Jun 2024 17:17:19 -0500 Subject: [PATCH 33/83] added to base classes the new accessiblity implmenation Signed-off-by: Matt Bruce --- VDS/BaseClasses/Control.swift | 11 +++++++++-- VDS/BaseClasses/View.swift | 12 +++++++++++- VDS/Components/Buttons/ButtonBase.swift | 11 +++++++++++ VDS/Components/Label/Label.swift | 6 +++++- .../TextFields/InputField/TextField.swift | 13 +++++++++++++ VDS/Components/TextFields/TextArea/TextView.swift | 13 +++++++++++++ VDS/Protocols/ViewProtocol.swift | 2 +- 7 files changed, 63 insertions(+), 5 deletions(-) diff --git a/VDS/BaseClasses/Control.swift b/VDS/BaseClasses/Control.swift index 6d212e22..434a1a88 100644 --- a/VDS/BaseClasses/Control.swift +++ b/VDS/BaseClasses/Control.swift @@ -53,6 +53,8 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable { open var surface: Surface = .light { didSet { setNeedsUpdate() } } + open var accessibilityAction: ((Control) -> Void)? + /// Whether the Control is selected or not. open override var isSelected: Bool { didSet { setNeedsUpdate() } } @@ -123,9 +125,14 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable { //-------------------------------------------------- /// Implement accessibilityActivate on an element in order to handle the default action. /// - Returns: Based on whether the userInteraction is enabled. - override open func accessibilityActivate() -> Bool { + open override func accessibilityActivate() -> Bool { // Hold state in case User wanted isAnimated to remain off. - guard isUserInteractionEnabled else { return false } + guard isEnabled, isUserInteractionEnabled else { return false } + + if let accessibilityAction { + accessibilityAction(self) + } + sendActions(for: .touchUpInside) return true } diff --git a/VDS/BaseClasses/View.swift b/VDS/BaseClasses/View.swift index 9ba21d9c..ec3f8dd0 100644 --- a/VDS/BaseClasses/View.swift +++ b/VDS/BaseClasses/View.swift @@ -55,6 +55,8 @@ open class View: UIView, ViewProtocol, UserInfoable { open var isEnabled: Bool = true { didSet { setNeedsUpdate() } } + open var accessibilityAction: ((View) -> Void)? + //-------------------------------------------------- // MARK: - Lifecycle //-------------------------------------------------- @@ -87,7 +89,15 @@ open class View: UIView, ViewProtocol, UserInfoable { surface = .light isEnabled = true } - + + open override func accessibilityActivate() -> Bool { + guard isEnabled, isUserInteractionEnabled else { return false } + if let accessibilityAction { + accessibilityAction(self) + } + return true + } + open override func layoutSubviews() { super.layoutSubviews() setNeedsUpdate() diff --git a/VDS/Components/Buttons/ButtonBase.swift b/VDS/Components/Buttons/ButtonBase.swift index a8443f58..56a23011 100644 --- a/VDS/Components/Buttons/ButtonBase.swift +++ b/VDS/Components/Buttons/ButtonBase.swift @@ -78,6 +78,8 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable { /// Whether the Button should handle the isHighlighted state. open var shouldHighlight: Bool { isHighlighting == false } + open var accessibilityAction: ((ButtonBase) -> Void)? + /// Whether the Control is highlighted or not. open override var isHighlighted: Bool { didSet { @@ -141,6 +143,15 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable { shouldUpdateView = true setNeedsUpdate() } + + open override func accessibilityActivate() -> Bool { + guard isEnabled, isUserInteractionEnabled else { return false } + if let accessibilityAction { + accessibilityAction(self) + } + sendActions(for: .touchUpInside) + return true + } //-------------------------------------------------- // MARK: - Private Methods diff --git a/VDS/Components/Label/Label.swift b/VDS/Components/Label/Label.swift index 1ec1de82..c5b760a7 100644 --- a/VDS/Components/Label/Label.swift +++ b/VDS/Components/Label/Label.swift @@ -132,6 +132,8 @@ open class Label: UILabel, ViewProtocol, UserInfoable { /// Line break mode for the label, default is set to word wrapping. open override var lineBreakMode: NSLineBreakMode { didSet { setNeedsUpdate() }} + open var accessibilityAction: ((Label) -> Void)? + /// Text that will be used in the label. private var _text: String! override open var text: String! { @@ -455,7 +457,9 @@ open class Label: UILabel, ViewProtocol, UserInfoable { } open override func accessibilityActivate() -> Bool { - return false + guard let accessibilityAction, isEnabled, isUserInteractionEnabled else { return false } + accessibilityAction(self) + return true } } diff --git a/VDS/Components/TextFields/InputField/TextField.swift b/VDS/Components/TextFields/InputField/TextField.swift index da109148..800d836b 100644 --- a/VDS/Components/TextFields/InputField/TextField.swift +++ b/VDS/Components/TextFields/InputField/TextField.swift @@ -77,6 +77,8 @@ open class TextField: UITextField, ViewProtocol, Errorable { open var lineBreakMode: NSLineBreakMode = .byClipping { didSet { setNeedsUpdate() } } + open var accessibilityAction: ((TextField) -> Void)? + open override var isEnabled: Bool { didSet { setNeedsUpdate() } } open var textColorConfiguration: AnyColorable = ViewColorConfiguration().with { @@ -211,6 +213,17 @@ open class TextField: UITextField, ViewProtocol, Errorable { return success } + open override func accessibilityActivate() -> Bool { + guard isEnabled, isUserInteractionEnabled else { return false } + + if let accessibilityAction { + accessibilityAction(self) + return true + } else { + return super.accessibilityActivate() + } + } + //-------------------------------------------------- // MARK: - Private Methods //-------------------------------------------------- diff --git a/VDS/Components/TextFields/TextArea/TextView.swift b/VDS/Components/TextFields/TextArea/TextView.swift index 6c9e92ab..3832d2e5 100644 --- a/VDS/Components/TextFields/TextArea/TextView.swift +++ b/VDS/Components/TextFields/TextArea/TextView.swift @@ -68,6 +68,8 @@ open class TextView: UITextView, ViewProtocol, Errorable { $0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forDisabled: false) }.eraseToAnyColorable(){ didSet { setNeedsUpdate() }} + open var accessibilityAction: ((TextView) -> Void)? + open var showError: Bool = false { didSet { setNeedsUpdate() } } open var errorText: String? { didSet { setNeedsUpdate() } } @@ -146,6 +148,17 @@ open class TextView: UITextView, ViewProtocol, Errorable { shouldUpdateView = true setNeedsUpdate() } + + open override func accessibilityActivate() -> Bool { + guard isEnabled, isUserInteractionEnabled else { return false } + + if let accessibilityAction { + accessibilityAction(self) + return true + } else { + return super.accessibilityActivate() + } + } //-------------------------------------------------- // MARK: - Private Methods diff --git a/VDS/Protocols/ViewProtocol.swift b/VDS/Protocols/ViewProtocol.swift index ba912684..4c2aa694 100644 --- a/VDS/Protocols/ViewProtocol.swift +++ b/VDS/Protocols/ViewProtocol.swift @@ -20,7 +20,7 @@ public protocol ViewProtocol: AnyObject, Initable, Resettable, Enabling, Surface var shouldUpdateAccessibility: Bool { get set } /// Used for setting an implementation for the default Accessible Action - var accessibilityDefaultAction: ((Self) -> Void)? { get set } + var accessibilityAction: ((Self) -> Void)? { get set } /// Executed on initialization for this View. func initialSetup() From 6c4d42898449e8f6cb848b5d39b1acb1cbb208b4 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 19 Jun 2024 17:17:46 -0500 Subject: [PATCH 34/83] override for accessibility new impl Signed-off-by: Matt Bruce --- VDS/BaseClasses/Selector/SelectorBase.swift | 11 ++--- .../Selector/SelectorGroupBase.swift | 7 +++ .../Selector/SelectorItemBase.swift | 22 +++++++++- .../Pagination/PaginationButton.swift | 5 --- VDS/Components/RadioBox/RadioBoxItem.swift | 43 +++++++++++++++---- 5 files changed, 68 insertions(+), 20 deletions(-) diff --git a/VDS/BaseClasses/Selector/SelectorBase.swift b/VDS/BaseClasses/Selector/SelectorBase.swift index 31d317cf..35999cf6 100644 --- a/VDS/BaseClasses/Selector/SelectorBase.swift +++ b/VDS/BaseClasses/Selector/SelectorBase.swift @@ -135,14 +135,15 @@ open class SelectorBase: Control, SelectorControlable { onChange = nil } - public var accessibilityDefaultAction: (() -> Void)? - - public override func accessibilityActivate() -> Bool { - if let accessibilityDefaultAction { - accessibilityDefaultAction() + open override func accessibilityActivate() -> Bool { + guard isEnabled, isUserInteractionEnabled else { return false } + + if let accessibilityAction { + accessibilityAction(self) } else { toggle() } + return true } } diff --git a/VDS/BaseClasses/Selector/SelectorGroupBase.swift b/VDS/BaseClasses/Selector/SelectorGroupBase.swift index 184f8e07..4df1ca5c 100644 --- a/VDS/BaseClasses/Selector/SelectorGroupBase.swift +++ b/VDS/BaseClasses/Selector/SelectorGroupBase.swift @@ -70,6 +70,13 @@ open class SelectorGroupBase: Control, SelectorGrou self?.didSelect(handler) self?.setNeedsUpdate() } + + selector.accessibilityAction = { [weak self] handler in + guard let handler = handler as? SelectorItemType else { return } + self?.didSelect(handler) + self?.setNeedsUpdate() + } + mainStackView.addArrangedSubview(selector) } } diff --git a/VDS/BaseClasses/Selector/SelectorItemBase.swift b/VDS/BaseClasses/Selector/SelectorItemBase.swift index 3a04b86d..2cd7eead 100644 --- a/VDS/BaseClasses/Selector/SelectorItemBase.swift +++ b/VDS/BaseClasses/Selector/SelectorItemBase.swift @@ -147,6 +147,15 @@ open class SelectorItemBase: Control, Errorable, Changea open var accessibilityValueText: String? + open override var accessibilityDefaultAction: ((Control) -> Void)? { + didSet { + selectorView.accessibilityAction = { [weak self] selectorItemBase in + guard let self else { return } + accessibilityAction?(self) + } + } + } + open var accessibilityLabelText: String { var accessibilityLabels = [String]() @@ -186,7 +195,7 @@ open class SelectorItemBase: Control, Errorable, Changea toggle() } - selectorView.accessibilityDefaultAction = { [weak self] in + selectorView.accessibilityAction = { [weak self] _ in guard let self, isEnabled else { return } toggle() } @@ -366,4 +375,15 @@ open class SelectorItemBase: Control, Errorable, Changea /// This will change to state of the Selector. open func toggle() {} + open override func accessibilityActivate() -> Bool { + guard isEnabled, isUserInteractionEnabled else { return false } + + if let accessibilityAction { + accessibilityAction(self) + } else { + toggle() + } + + return true + } } diff --git a/VDS/Components/Pagination/PaginationButton.swift b/VDS/Components/Pagination/PaginationButton.swift index 05ef64ee..d7aaf8e2 100644 --- a/VDS/Components/Pagination/PaginationButton.swift +++ b/VDS/Components/Pagination/PaginationButton.swift @@ -78,11 +78,6 @@ open class PaginationButton: ButtonBase { tintColor = color super.updateView() } - - open override func accessibilityActivate() -> Bool { - sendActions(for: .touchUpInside) - return true - } } extension PaginationButton { diff --git a/VDS/Components/RadioBox/RadioBoxItem.swift b/VDS/Components/RadioBox/RadioBoxItem.swift index 417df508..71b05494 100644 --- a/VDS/Components/RadioBox/RadioBoxItem.swift +++ b/VDS/Components/RadioBox/RadioBoxItem.swift @@ -74,9 +74,7 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable { } /// Selector for this RadioBox. - open var selectorView = UIView().with { - $0.translatesAutoresizingMaskIntoConstraints = false - } + open var selectorView = View() /// If provided, the RadioBox text will be rendered. open var text: String? { didSet { setNeedsUpdate() } } @@ -133,20 +131,33 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable { open var accessibilityValueText: String? + open override var accessibilityAction: ((Control) -> Void)? { + didSet { + selectorView.accessibilityAction = { [weak self] selectorItemBase in + guard let self else { return } + accessibilityAction?(self) + } + } + } + open var accessibilityLabelText: String { var accessibilityLabels = [String]() + if isSelected { + accessibilityLabels.append("selected") + } + accessibilityLabels.append("Radiobox") - if let text { + if let text, !text.isEmpty { accessibilityLabels.append(text) } - if let text = subText { + if let text = subText, !text.isEmpty { accessibilityLabels.append(text) } - if let text = subTextRight { + if let text = subTextRight, !text.isEmpty { accessibilityLabels.append(text) } @@ -189,6 +200,16 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable { onClick = { control in control.toggle() } + + if #available(iOS 17.0, *) { + accessibilityHintBlock = { [weak self] in + + return "foo" + } + } else { + // Fallback on earlier versions + } + } /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations. @@ -199,7 +220,7 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable { selectorView.isAccessibilityElement = true selectorView.accessibilityTraits = .button addSubview(selectorView) - selectorView.isUserInteractionEnabled = false + selectorView.isUserInteractionEnabled = true selectorView.addSubview(selectorStackView) @@ -281,7 +302,7 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable { : accessibilityValueText } } - + open override var accessibilityElements: [Any]? { get { var items = [Any]() @@ -322,7 +343,11 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable { return super.hitTest(point, with: event) } } - + + open func getSelectorView() -> UIView { + selectorView + } + //-------------------------------------------------- // MARK: - Private Methods //-------------------------------------------------- From b18cabb680e458c88c1854f4cdd28b7563ab7bf6 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 19 Jun 2024 17:17:51 -0500 Subject: [PATCH 35/83] added any Signed-off-by: Matt Bruce --- VDS/Extensions/UIView+CALayer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VDS/Extensions/UIView+CALayer.swift b/VDS/Extensions/UIView+CALayer.swift index 3c1db6a5..05924211 100644 --- a/VDS/Extensions/UIView+CALayer.swift +++ b/VDS/Extensions/UIView+CALayer.swift @@ -62,7 +62,7 @@ extension UIView { } else { removeDebugBorder() } - if let view = self as? ViewProtocol { + if let view = self as? (any ViewProtocol) { view.updateView() } } From 2700c39632d62b060aaff8e383650fa54dab5610 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 19 Jun 2024 17:27:29 -0500 Subject: [PATCH 36/83] fixed rename Signed-off-by: Matt Bruce --- VDS/BaseClasses/Selector/SelectorItemBase.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VDS/BaseClasses/Selector/SelectorItemBase.swift b/VDS/BaseClasses/Selector/SelectorItemBase.swift index 2cd7eead..63159d93 100644 --- a/VDS/BaseClasses/Selector/SelectorItemBase.swift +++ b/VDS/BaseClasses/Selector/SelectorItemBase.swift @@ -147,7 +147,7 @@ open class SelectorItemBase: Control, Errorable, Changea open var accessibilityValueText: String? - open override var accessibilityDefaultAction: ((Control) -> Void)? { + open override var accessibilityAction: ((Control) -> Void)? { didSet { selectorView.accessibilityAction = { [weak self] selectorItemBase in guard let self else { return } From d013f07db3d8a4c12574f22b71d3b1d2d791e05e Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 20 Jun 2024 10:11:41 -0500 Subject: [PATCH 37/83] first cut with just inline replace Signed-off-by: Matt Bruce --- VDS/BaseClasses/Control.swift | 150 ++++++++++++++--- VDS/BaseClasses/Selector/SelectorBase.swift | 36 ++++- .../Selector/SelectorItemBase.swift | 37 +++-- VDS/BaseClasses/View.swift | 150 +++++++++++++++-- VDS/Classes/AccessibilityActionElement.swift | 4 +- VDS/Components/Buttons/ButtonBase.swift | 144 +++++++++++++++-- VDS/Components/Label/Label.swift | 141 +++++++++++++++- .../TextFields/InputField/TextField.swift | 153 ++++++++++++++++-- .../TextFields/TextArea/TextView.swift | 140 ++++++++++++++-- VDS/Protocols/ViewProtocol.swift | 83 ++++++++++ 10 files changed, 937 insertions(+), 101 deletions(-) diff --git a/VDS/BaseClasses/Control.swift b/VDS/BaseClasses/Control.swift index 434a1a88..aa7443a2 100644 --- a/VDS/BaseClasses/Control.swift +++ b/VDS/BaseClasses/Control.swift @@ -46,15 +46,11 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable { // MARK: - Public Properties //-------------------------------------------------- open var shouldUpdateView: Bool = true - - open var shouldUpdateAccessibility: Bool = true - + open var userInfo = [String: Primitive]() open var surface: Surface = .light { didSet { setNeedsUpdate() } } - open var accessibilityAction: ((Control) -> Void)? - /// Whether the Control is selected or not. open override var isSelected: Bool { didSet { setNeedsUpdate() } } @@ -123,22 +119,138 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable { //-------------------------------------------------- // MARK: - Overrides //-------------------------------------------------- - /// Implement accessibilityActivate on an element in order to handle the default action. - /// - Returns: Based on whether the userInteraction is enabled. - open override func accessibilityActivate() -> Bool { - // Hold state in case User wanted isAnimated to remain off. - guard isEnabled, isUserInteractionEnabled else { return false } - - if let accessibilityAction { - accessibilityAction(self) - } - - sendActions(for: .touchUpInside) - return true - } - open override func layoutSubviews() { super.layoutSubviews() setNeedsUpdate() } + + //-------------------------------------------------- + // MARK: - Accessibility + //-------------------------------------------------- + open var shouldUpdateAccessibility: Bool = true + + open var accessibilityAction: ((Control) -> Void)? + + private var _isAccessibilityElement: Bool = false + open override var isAccessibilityElement: Bool { + get { + var block: AXBoolReturnBlock? + +// if #available(iOS 17, *) { +// block = isAccessibilityElementBlock +// } + + if block == nil { + block = bridge_isAccessibilityElementBlock + } + + if let block { + return block() + } else { + return _isAccessibilityElement + } + } + set { + _isAccessibilityElement = newValue + } + } + + private var _accessibilityLabel: String? + open override var accessibilityLabel: String? { + get { + var block: AXStringReturnBlock? +// if #available(iOS 17, *) { +// block = accessibilityLabelBlock +// } + + if block == nil { + block = bridge_accessibilityLabelBlock + } + + if let block { + return block() + } else { + return _accessibilityLabel + } + } + set { + _accessibilityLabel = newValue + } + } + + private var _accessibilityHint: String? + open override var accessibilityHint: String? { + get { + var block: AXStringReturnBlock? +// if #available(iOS 17, *) { +// block = accessibilityHintBlock +// } + + if block == nil { + block = bridge_accessibilityHintBlock + } + + if let block { + return block() + } else { + return _accessibilityHint + } + } + set { + _accessibilityHint = newValue + } + } + + private var _accessibilityValue: String? + open override var accessibilityValue: String? { + get { + var block: AXStringReturnBlock? + +// if #available(iOS 17, *) { +// block = accessibilityHintBlock +// } + + if block == nil { + block = bridge_accessibilityValueBlock + } + + if let block{ + return block() + } else { + return _accessibilityValue + } + } + set { + _accessibilityValue = newValue + } + } + + open override func accessibilityActivate() -> Bool { + guard isEnabled, isUserInteractionEnabled else { return false } + var value = true + +// if #available(iOS 17, *) { +// if let block = accessibilityAction { +// block(self) +// } else if let block = accessibilityActivateBlock { +// value = block() +// +// } else if let block = bridge_accessibilityActivateBlock { +// value = block() +// } +// +// } else { + if let block = accessibilityAction { + block(self) + + } else if let block = bridge_accessibilityActivateBlock { + value = block() + } +// } + + sendActions(for: .touchUpInside) + return value + + } + } diff --git a/VDS/BaseClasses/Selector/SelectorBase.swift b/VDS/BaseClasses/Selector/SelectorBase.swift index 35999cf6..d1683aa3 100644 --- a/VDS/BaseClasses/Selector/SelectorBase.swift +++ b/VDS/BaseClasses/Selector/SelectorBase.swift @@ -137,13 +137,33 @@ open class SelectorBase: Control, SelectorControlable { open override func accessibilityActivate() -> Bool { guard isEnabled, isUserInteractionEnabled else { return false } - - if let accessibilityAction { - accessibilityAction(self) - } else { - toggle() - } - - return true + guard isEnabled, isUserInteractionEnabled else { return false } + var value = true + +// if #available(iOS 17, *) { +// if let block = accessibilityAction { +// block(self) +// +// } else if let block = accessibilityActivateBlock { +// value = block() +// +// } else if let block = bridge_accessibilityActivateBlock { +// value = block() +// +// } else { +// toggle() +// } +// } else { + if let block = accessibilityAction { + block(self) + + } else if let block = bridge_accessibilityActivateBlock { + value = block() + + } else { + toggle() + } +// } + return value } } diff --git a/VDS/BaseClasses/Selector/SelectorItemBase.swift b/VDS/BaseClasses/Selector/SelectorItemBase.swift index 63159d93..0326ac52 100644 --- a/VDS/BaseClasses/Selector/SelectorItemBase.swift +++ b/VDS/BaseClasses/Selector/SelectorItemBase.swift @@ -147,7 +147,7 @@ open class SelectorItemBase: Control, Errorable, Changea open var accessibilityValueText: String? - open override var accessibilityAction: ((Control) -> Void)? { + open override var accessibilityAction: ((Control) -> Void)? { didSet { selectorView.accessibilityAction = { [weak self] selectorItemBase in guard let self else { return } @@ -377,13 +377,32 @@ open class SelectorItemBase: Control, Errorable, Changea open override func accessibilityActivate() -> Bool { guard isEnabled, isUserInteractionEnabled else { return false } - - if let accessibilityAction { - accessibilityAction(self) - } else { - toggle() - } - - return true + var value = true + +// if #available(iOS 17, *) { +// if let block = accessibilityAction { +// block(self) +// +// } else if let block = accessibilityActivateBlock { +// value = block() +// +// } else if let block = bridge_accessibilityActivateBlock { +// value = block() +// +// } else { +// toggle() +// } +// } else { + if let block = accessibilityAction { + block(self) + + } else if let block = bridge_accessibilityActivateBlock { + value = block() + + } else { + toggle() + } +// } + return value } } diff --git a/VDS/BaseClasses/View.swift b/VDS/BaseClasses/View.swift index ec3f8dd0..288a8cdc 100644 --- a/VDS/BaseClasses/View.swift +++ b/VDS/BaseClasses/View.swift @@ -46,17 +46,13 @@ open class View: UIView, ViewProtocol, UserInfoable { //-------------------------------------------------- open var shouldUpdateView: Bool = true - open var shouldUpdateAccessibility: Bool = true - /// Dictionary for keeping information for this Control use only Primitives. open var userInfo = [String: Primitive]() open var surface: Surface = .light { didSet { setNeedsUpdate() } } open var isEnabled: Bool = true { didSet { setNeedsUpdate() } } - - open var accessibilityAction: ((View) -> Void)? - + //-------------------------------------------------- // MARK: - Lifecycle //-------------------------------------------------- @@ -89,18 +85,144 @@ open class View: UIView, ViewProtocol, UserInfoable { surface = .light isEnabled = true } - - open override func accessibilityActivate() -> Bool { - guard isEnabled, isUserInteractionEnabled else { return false } - if let accessibilityAction { - accessibilityAction(self) - } - return true - } - + open override func layoutSubviews() { super.layoutSubviews() setNeedsUpdate() } + //-------------------------------------------------- + // MARK: - Accessibility + //-------------------------------------------------- + open var shouldUpdateAccessibility: Bool = true + + open var accessibilityAction: ((View) -> Void)? + + private var _isAccessibilityElement: Bool = false + open override var isAccessibilityElement: Bool { + get { + var block: AXBoolReturnBlock? + +// if #available(iOS 17, *) { +// block = isAccessibilityElementBlock +// } + + if block == nil { + block = bridge_isAccessibilityElementBlock + } + + if let block { + return block() + } else { + return _isAccessibilityElement + } + } + set { + _isAccessibilityElement = newValue + } + } + + private var _accessibilityLabel: String? + open override var accessibilityLabel: String? { + get { + var block: AXStringReturnBlock? +// if #available(iOS 17, *) { +// block = accessibilityLabelBlock +// } +// + if block == nil { + block = bridge_accessibilityLabelBlock + } + + if let block { + return block() + } else { + return _accessibilityLabel + } + } + set { + _accessibilityLabel = newValue + } + } + + private var _accessibilityHint: String? + open override var accessibilityHint: String? { + get { + var block: AXStringReturnBlock? +// if #available(iOS 17, *) { +// block = accessibilityHintBlock +// } + + if block == nil { + block = bridge_accessibilityHintBlock + } + + if let block { + return block() + } else { + return _accessibilityHint + } + } + set { + _accessibilityHint = newValue + } + } + + private var _accessibilityValue: String? + open override var accessibilityValue: String? { + get { + var block: AXStringReturnBlock? + +// if #available(iOS 17, *) { +// block = accessibilityHintBlock +// } + + if block == nil { + block = bridge_accessibilityValueBlock + } + + if let block{ + return block() + } else { + return _accessibilityValue + } + } + set { + _accessibilityValue = newValue + } + } + + open override func accessibilityActivate() -> Bool { + guard isEnabled, isUserInteractionEnabled else { return false } + +// if #available(iOS 17, *) { +// if let block = accessibilityAction { +// block(self) +// return true +// } else if let block = accessibilityActivateBlock { +// return block() +// +// } else if let block = bridge_accessibilityActivateBlock { +// return block() +// +// } else { +// return true +// +// } +// +// } else { + if let block = accessibilityAction { + block(self) + return true + + } else if let block = bridge_accessibilityActivateBlock { + return block() + + } else { + return true + + } +// } + } + } diff --git a/VDS/Classes/AccessibilityActionElement.swift b/VDS/Classes/AccessibilityActionElement.swift index df3b6ed9..cc3487f9 100644 --- a/VDS/Classes/AccessibilityActionElement.swift +++ b/VDS/Classes/AccessibilityActionElement.swift @@ -10,10 +10,10 @@ import UIKit /// Custom UIAccessibilityElement that allows you to set the default action used in accessibilityActivate. public class AccessibilityActionElement: UIAccessibilityElement { - var action: (() -> Void)? + public var accessibilityAction: AXVoidReturnBlock? public override func accessibilityActivate() -> Bool { - action?() + accessibilityAction?() return true } } diff --git a/VDS/Components/Buttons/ButtonBase.swift b/VDS/Components/Buttons/ButtonBase.swift index 56a23011..e5a80e2e 100644 --- a/VDS/Components/Buttons/ButtonBase.swift +++ b/VDS/Components/Buttons/ButtonBase.swift @@ -50,8 +50,6 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable { //-------------------------------------------------- /// Key of whether or not updateView() is called in setNeedsUpdate() open var shouldUpdateView: Bool = true - - open var shouldUpdateAccessibility: Bool = true open var surface: Surface = .light { didSet { setNeedsUpdate() } } @@ -77,8 +75,6 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable { /// Whether the Button should handle the isHighlighted state. open var shouldHighlight: Bool { isHighlighting == false } - - open var accessibilityAction: ((ButtonBase) -> Void)? /// Whether the Control is highlighted or not. open override var isHighlighted: Bool { @@ -143,16 +139,7 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable { shouldUpdateView = true setNeedsUpdate() } - - open override func accessibilityActivate() -> Bool { - guard isEnabled, isUserInteractionEnabled else { return false } - if let accessibilityAction { - accessibilityAction(self) - } - sendActions(for: .touchUpInside) - return true - } - + //-------------------------------------------------- // MARK: - Private Methods //-------------------------------------------------- @@ -185,6 +172,135 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable { } } + //-------------------------------------------------- + // MARK: - Accessibility + //-------------------------------------------------- + open var shouldUpdateAccessibility: Bool = true + + open var accessibilityAction: ((ButtonBase) -> Void)? + + private var _isAccessibilityElement: Bool = false + open override var isAccessibilityElement: Bool { + get { + var block: AXBoolReturnBlock? + +// if #available(iOS 17, *) { +// block = isAccessibilityElementBlock +// } + + if block == nil { + block = bridge_isAccessibilityElementBlock + } + + if let block { + return block() + } else { + return _isAccessibilityElement + } + } + set { + _isAccessibilityElement = newValue + } + } + + private var _accessibilityLabel: String? + open override var accessibilityLabel: String? { + get { + var block: AXStringReturnBlock? +// if #available(iOS 17, *) { +// block = accessibilityLabelBlock +// } + + if block == nil { + block = bridge_accessibilityLabelBlock + } + + if let block { + return block() + } else { + return _accessibilityLabel + } + } + set { + _accessibilityLabel = newValue + } + } + + private var _accessibilityHint: String? + open override var accessibilityHint: String? { + get { + var block: AXStringReturnBlock? +// if #available(iOS 17, *) { +// block = accessibilityHintBlock +// } + + if block == nil { + block = bridge_accessibilityHintBlock + } + + if let block { + return block() + } else { + return _accessibilityHint + } + } + set { + _accessibilityHint = newValue + } + } + + private var _accessibilityValue: String? + open override var accessibilityValue: String? { + get { + var block: AXStringReturnBlock? + +// if #available(iOS 17, *) { +// block = accessibilityHintBlock +// } + + if block == nil { + block = bridge_accessibilityValueBlock + } + + if let block{ + return block() + } else { + return _accessibilityValue + } + } + set { + _accessibilityValue = newValue + } + } + + open override func accessibilityActivate() -> Bool { + guard isEnabled, isUserInteractionEnabled else { return false } + var value = true + +// if #available(iOS 17, *) { +// if let block = accessibilityAction { +// block(self) +// } else if let block = accessibilityActivateBlock { +// value = block() +// +// } else if let block = bridge_accessibilityActivateBlock { +// value = block() +// } +// +// } else { + if let block = accessibilityAction { + block(self) + + } else if let block = bridge_accessibilityActivateBlock { + value = block() + } +// } + + sendActions(for: .touchUpInside) + return value + + } + } // MARK: AppleGuidelinesTouchable diff --git a/VDS/Components/Label/Label.swift b/VDS/Components/Label/Label.swift index c5b760a7..b0a85e7e 100644 --- a/VDS/Components/Label/Label.swift +++ b/VDS/Components/Label/Label.swift @@ -108,8 +108,6 @@ open class Label: UILabel, ViewProtocol, UserInfoable { /// Key of whether or not updateView() is called in setNeedsUpdate() open var shouldUpdateView: Bool = true - open var shouldUpdateAccessibility: Bool = true - /// Will determine if a scaled font should be used for the font. open var useScaledFont: Bool = false { didSet { setNeedsUpdate() }} @@ -132,8 +130,6 @@ open class Label: UILabel, ViewProtocol, UserInfoable { /// Line break mode for the label, default is set to word wrapping. open override var lineBreakMode: NSLineBreakMode { didSet { setNeedsUpdate() }} - open var accessibilityAction: ((Label) -> Void)? - /// Text that will be used in the label. private var _text: String! override open var text: String! { @@ -366,7 +362,7 @@ open class Label: UILabel, ViewProtocol, UserInfoable { let labelAction = LabelAction(range: actionable.range, action: actionable.action) // set the action of the accessibilityElement - customAccessibilityAction?.action = { [weak self] in + customAccessibilityAction?.accessibilityAction = { [weak self] in guard let self, isEnabled else { return } labelAction.performAction() } @@ -456,10 +452,139 @@ open class Label: UILabel, ViewProtocol, UserInfoable { return element } + + //-------------------------------------------------- + // MARK: - Accessibility + //-------------------------------------------------- + open var shouldUpdateAccessibility: Bool = true + + open var accessibilityAction: ((Label) -> Void)? + + private var _isAccessibilityElement: Bool = false + open override var isAccessibilityElement: Bool { + get { + var block: AXBoolReturnBlock? + +// if #available(iOS 17, *) { +// block = isAccessibilityElementBlock +// } + + if block == nil { + block = bridge_isAccessibilityElementBlock + } + + if let block { + return block() + } else { + return _isAccessibilityElement + } + } + set { + _isAccessibilityElement = newValue + } + } + + private var _accessibilityLabel: String? + open override var accessibilityLabel: String? { + get { + var block: AXStringReturnBlock? +// if #available(iOS 17, *) { +// block = accessibilityLabelBlock +// } + + if block == nil { + block = bridge_accessibilityLabelBlock + } + + if let block { + return block() + } else { + return _accessibilityLabel + } + } + set { + _accessibilityLabel = newValue + } + } + + private var _accessibilityHint: String? + open override var accessibilityHint: String? { + get { + var block: AXStringReturnBlock? +// if #available(iOS 17, *) { +// block = accessibilityHintBlock +// } + + if block == nil { + block = bridge_accessibilityHintBlock + } + + if let block { + return block() + } else { + return _accessibilityHint + } + } + set { + _accessibilityHint = newValue + } + } + + private var _accessibilityValue: String? + open override var accessibilityValue: String? { + get { + var block: AXStringReturnBlock? + +// if #available(iOS 17, *) { +// block = accessibilityHintBlock +// } + + if block == nil { + block = bridge_accessibilityValueBlock + } + + if let block{ + return block() + } else { + return _accessibilityValue + } + } + set { + _accessibilityValue = newValue + } + } + open override func accessibilityActivate() -> Bool { - guard let accessibilityAction, isEnabled, isUserInteractionEnabled else { return false } - accessibilityAction(self) - return true + guard isEnabled, isUserInteractionEnabled else { return false } + +// if #available(iOS 17, *) { +// if let block = accessibilityAction { +// block(self) +// return true +// } else if let block = accessibilityActivateBlock { +// return block() +// +// } else if let block = bridge_accessibilityActivateBlock { +// return block() +// +// } else { +// return true +// +// } +// +// } else { + if let block = accessibilityAction { + block(self) + return true + + } else if let block = bridge_accessibilityActivateBlock { + return block() + + } else { + return true + + } +// } } } diff --git a/VDS/Components/TextFields/InputField/TextField.swift b/VDS/Components/TextFields/InputField/TextField.swift index 800d836b..81e749b2 100644 --- a/VDS/Components/TextFields/InputField/TextField.swift +++ b/VDS/Components/TextFields/InputField/TextField.swift @@ -47,7 +47,10 @@ open class TextField: UITextField, ViewProtocol, Errorable { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- - private var formatLabel = Label().with { + /// Key of whether or not updateView() is called in setNeedsUpdate() + open var shouldUpdateView: Bool = true + + private var formatLabel = Label().with { $0.tag = 999 $0.textColorConfiguration = ViewColorConfiguration().with { $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forDisabled: true) @@ -63,9 +66,6 @@ open class TextField: UITextField, ViewProtocol, Errorable { /// Will determine if a scaled font should be used for the titleLabel font. open var useScaledFont: Bool = false { didSet { setNeedsUpdate() } } - - /// Key of whether or not updateView() is called in setNeedsUpdate() - open var shouldUpdateView: Bool = true open var shouldUpdateAccessibility: Bool = true @@ -76,8 +76,6 @@ open class TextField: UITextField, ViewProtocol, Errorable { open var errorText: String? { didSet { setNeedsUpdate() } } open var lineBreakMode: NSLineBreakMode = .byClipping { didSet { setNeedsUpdate() } } - - open var accessibilityAction: ((TextField) -> Void)? open override var isEnabled: Bool { didSet { setNeedsUpdate() } } @@ -213,17 +211,6 @@ open class TextField: UITextField, ViewProtocol, Errorable { return success } - open override func accessibilityActivate() -> Bool { - guard isEnabled, isUserInteractionEnabled else { return false } - - if let accessibilityAction { - accessibilityAction(self) - return true - } else { - return super.accessibilityActivate() - } - } - //-------------------------------------------------- // MARK: - Private Methods //-------------------------------------------------- @@ -244,7 +231,139 @@ open class TextField: UITextField, ViewProtocol, Errorable { attributedText = nil } } + + + //-------------------------------------------------- + // MARK: - Accessibility + //-------------------------------------------------- + open var accessibilityAction: ((TextField) -> Void)? + private var _isAccessibilityElement: Bool = false + open override var isAccessibilityElement: Bool { + get { + var block: AXBoolReturnBlock? + +// if #available(iOS 17, *) { +// block = isAccessibilityElementBlock +// } + + if block == nil { + block = bridge_isAccessibilityElementBlock + } + + if let block { + return block() + } else { + return _isAccessibilityElement + } + } + set { + _isAccessibilityElement = newValue + } + } + + private var _accessibilityLabel: String? + open override var accessibilityLabel: String? { + get { + var block: AXStringReturnBlock? +// if #available(iOS 17, *) { +// block = accessibilityLabelBlock +// } + + if block == nil { + block = bridge_accessibilityLabelBlock + } + + if let block { + return block() + } else { + return _accessibilityLabel + } + } + set { + _accessibilityLabel = newValue + } + } + + private var _accessibilityHint: String? + open override var accessibilityHint: String? { + get { + var block: AXStringReturnBlock? +// if #available(iOS 17, *) { +// block = accessibilityHintBlock +// } + + if block == nil { + block = bridge_accessibilityHintBlock + } + + if let block { + return block() + } else { + return _accessibilityHint + } + } + set { + _accessibilityHint = newValue + } + } + + private var _accessibilityValue: String? + open override var accessibilityValue: String? { + get { + var block: AXStringReturnBlock? + +// if #available(iOS 17, *) { +// block = accessibilityHintBlock +// } + + if block == nil { + block = bridge_accessibilityValueBlock + } + + if let block{ + return block() + } else { + return _accessibilityValue + } + } + set { + _accessibilityValue = newValue + } + } + + open override func accessibilityActivate() -> Bool { + guard isEnabled, isUserInteractionEnabled else { return false } + +// if #available(iOS 17, *) { +// if let block = accessibilityAction { +// block(self) +// return true +// } else if let block = accessibilityActivateBlock { +// return block() +// +// } else if let block = bridge_accessibilityActivateBlock { +// return block() +// +// } else { +// return super.accessibilityActivate() +// +// } +// +// } else { + if let block = accessibilityAction { + block(self) + return true + + } else if let block = bridge_accessibilityActivateBlock { + return block() + + } else { + return super.accessibilityActivate() + + } +// } + } } extension UITextField { diff --git a/VDS/Components/TextFields/TextArea/TextView.swift b/VDS/Components/TextFields/TextArea/TextView.swift index 3832d2e5..bbb7b329 100644 --- a/VDS/Components/TextFields/TextArea/TextView.swift +++ b/VDS/Components/TextFields/TextArea/TextView.swift @@ -48,8 +48,6 @@ open class TextView: UITextView, ViewProtocol, Errorable { /// Key of whether or not updateView() is called in setNeedsUpdate() open var shouldUpdateView: Bool = true - open var shouldUpdateAccessibility: Bool = true - open var surface: Surface = .light { didSet { setNeedsUpdate() } } /// Array of LabelAttributeModel objects used in rendering the text. @@ -68,8 +66,6 @@ open class TextView: UITextView, ViewProtocol, Errorable { $0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forDisabled: false) }.eraseToAnyColorable(){ didSet { setNeedsUpdate() }} - open var accessibilityAction: ((TextView) -> Void)? - open var showError: Bool = false { didSet { setNeedsUpdate() } } open var errorText: String? { didSet { setNeedsUpdate() } } @@ -149,15 +145,139 @@ open class TextView: UITextView, ViewProtocol, Errorable { setNeedsUpdate() } + + //-------------------------------------------------- + // MARK: - Accessibility + //-------------------------------------------------- + open var shouldUpdateAccessibility: Bool = true + + open var accessibilityAction: ((TextView) -> Void)? + + private var _isAccessibilityElement: Bool = false + open override var isAccessibilityElement: Bool { + get { + var block: AXBoolReturnBlock? + +// if #available(iOS 17, *) { +// block = isAccessibilityElementBlock +// } + + if block == nil { + block = bridge_isAccessibilityElementBlock + } + + if let block { + return block() + } else { + return _isAccessibilityElement + } + } + set { + _isAccessibilityElement = newValue + } + } + + private var _accessibilityLabel: String? + open override var accessibilityLabel: String? { + get { + var block: AXStringReturnBlock? +// if #available(iOS 17, *) { +// block = accessibilityLabelBlock +// } + + if block == nil { + block = bridge_accessibilityLabelBlock + } + + if let block { + return block() + } else { + return _accessibilityLabel + } + } + set { + _accessibilityLabel = newValue + } + } + + private var _accessibilityHint: String? + open override var accessibilityHint: String? { + get { + var block: AXStringReturnBlock? +// if #available(iOS 17, *) { +// block = accessibilityHintBlock +// } + + if block == nil { + block = bridge_accessibilityHintBlock + } + + if let block { + return block() + } else { + return _accessibilityHint + } + } + set { + _accessibilityHint = newValue + } + } + + private var _accessibilityValue: String? + open override var accessibilityValue: String? { + get { + var block: AXStringReturnBlock? + +// if #available(iOS 17, *) { +// block = accessibilityHintBlock +// } + + if block == nil { + block = bridge_accessibilityValueBlock + } + + if let block{ + return block() + } else { + return _accessibilityValue + } + } + set { + _accessibilityValue = newValue + } + } + open override func accessibilityActivate() -> Bool { guard isEnabled, isUserInteractionEnabled else { return false } - if let accessibilityAction { - accessibilityAction(self) - return true - } else { - return super.accessibilityActivate() - } +// if #available(iOS 17, *) { +// if let block = accessibilityAction { +// block(self) +// return true +// } else if let block = accessibilityActivateBlock { +// return block() +// +// } else if let block = bridge_accessibilityActivateBlock { +// return block() +// +// } else { +// return super.accessibilityActivate() +// +// } +// +// } else { + if let block = accessibilityAction { + block(self) + return true + + } else if let block = bridge_accessibilityActivateBlock { + return block() + + } else { + return super.accessibilityActivate() + + } +// } } //-------------------------------------------------- diff --git a/VDS/Protocols/ViewProtocol.swift b/VDS/Protocols/ViewProtocol.swift index 4c2aa694..8018e260 100644 --- a/VDS/Protocols/ViewProtocol.swift +++ b/VDS/Protocols/ViewProtocol.swift @@ -70,3 +70,86 @@ extension ViewProtocol where Self: UIControl { }).store(in: &subscribers) } } + +public protocol AccessibilityUpdatable { + // Basic accessibility + var bridge_isAccessibilityElementBlock: AXBoolReturnBlock? { get set } + + var bridge_accessibilityLabelBlock: AXStringReturnBlock? { get set } + + var bridge_accessibilityValueBlock: AXStringReturnBlock? { get set } + + var bridge_accessibilityHintBlock: AXStringReturnBlock? { get set } + + var bridge_accessibilityActivateBlock: AXBoolReturnBlock? { get set } + +} + +extension NSObject: AccessibilityUpdatable { + static var isAccessibilityElementBlockKey: UInt8 = 0 + static var activateBlockKey: UInt8 = 1 + static var valueBlockKey: UInt8 = 2 + static var hintBlockKey: UInt8 = 3 + static var labelBlockKey: UInt8 = 4 + + public var bridge_isAccessibilityElementBlock: AXBoolReturnBlock? { + get { + return objc_getAssociatedObject(self, &NSObject.isAccessibilityElementBlockKey) as? AXBoolReturnBlock + } + set { + objc_setAssociatedObject(self, &NSObject.isAccessibilityElementBlockKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) +// if #available(iOS 17, *) { +// self.isAccessibilityElementBlock = newValue +// } + } + } + + public var bridge_accessibilityActivateBlock: AXBoolReturnBlock? { + get { + return objc_getAssociatedObject(self, &NSObject.activateBlockKey) as? AXBoolReturnBlock + } + set { + objc_setAssociatedObject(self, &NSObject.activateBlockKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) +// if #available(iOS 17, *) { +// self.accessibilityActivateBlock = newValue +// } + } + } + + public var bridge_accessibilityValueBlock: AXStringReturnBlock? { + get { + return objc_getAssociatedObject(self, &NSObject.valueBlockKey) as? AXStringReturnBlock + } + set { + objc_setAssociatedObject(self, &NSObject.valueBlockKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) +// if #available(iOS 17, *) { +// self.accessibilityValueBlock = newValue +// } + } + } + + public var bridge_accessibilityHintBlock: AXStringReturnBlock? { + get { + return objc_getAssociatedObject(self, &NSObject.hintBlockKey) as? AXStringReturnBlock + } + set { + objc_setAssociatedObject(self, &NSObject.hintBlockKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) +// if #available(iOS 17, *) { +// self.accessibilityHintBlock = newValue +// } + } + } + + public var bridge_accessibilityLabelBlock: AXStringReturnBlock? { + get { + return objc_getAssociatedObject(self, &NSObject.labelBlockKey) as? AXStringReturnBlock + } + set { + objc_setAssociatedObject(self, &NSObject.labelBlockKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) +// if #available(iOS 17, *) { +// self.accessibilityLabelBlock = newValue +// } + } + } + +} From d8c3ba2e33ec704604f5e7e2a2d8072269a2996a Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 20 Jun 2024 12:36:15 -0500 Subject: [PATCH 38/83] added new protocol Signed-off-by: Matt Bruce --- VDS.xcodeproj/project.pbxproj | 4 ++++ VDS/Protocols/AccessibilityUpdatable.swift | 8 ++++++++ 2 files changed, 12 insertions(+) create mode 100644 VDS/Protocols/AccessibilityUpdatable.swift diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index b44980ae..ff8a6735 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -173,6 +173,7 @@ EAF1FE9929D4850E00101452 /* Clickable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF1FE9829D4850E00101452 /* Clickable.swift */; }; EAF1FE9B29DB1A6000101452 /* Changeable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF1FE9A29DB1A6000101452 /* Changeable.swift */; }; EAF2F4762C231EAA007BFEDC /* AccessibilityActionElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */; }; + EAF2F4782C249D72007BFEDC /* AccessibilityUpdatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF2F4772C249D72007BFEDC /* AccessibilityUpdatable.swift */; }; EAF7F0952899861000B287F5 /* CheckboxItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0932899861000B287F5 /* CheckboxItem.swift */; }; EAF7F09A2899B17200B287F5 /* CATransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0992899B17200B287F5 /* CATransaction.swift */; }; EAF7F0A0289AB7EC00B287F5 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F09F289AB7EC00B287F5 /* View.swift */; }; @@ -400,6 +401,7 @@ EAF1FE9829D4850E00101452 /* Clickable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Clickable.swift; sourceTree = ""; }; EAF1FE9A29DB1A6000101452 /* Changeable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Changeable.swift; sourceTree = ""; }; EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityActionElement.swift; sourceTree = ""; }; + EAF2F4772C249D72007BFEDC /* AccessibilityUpdatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityUpdatable.swift; sourceTree = ""; }; EAF7F0932899861000B287F5 /* CheckboxItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckboxItem.swift; sourceTree = ""; }; EAF7F0992899B17200B287F5 /* CATransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CATransaction.swift; sourceTree = ""; }; EAF7F09F289AB7EC00B287F5 /* View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = ""; }; @@ -713,6 +715,7 @@ EA3361AB288B25EC0071C351 /* Protocols */ = { isa = PBXGroup; children = ( + EAF2F4772C249D72007BFEDC /* AccessibilityUpdatable.swift */, EA4DB2FC28D3D0CA00103EE3 /* AnyEquatable.swift */, EA297A5629FB0A360031ED56 /* AppleGuidelinesTouchable.swift */, EAF1FE9A29DB1A6000101452 /* Changeable.swift */, @@ -1209,6 +1212,7 @@ EAF7F0A6289B0CE000B287F5 /* Resetable.swift in Sources */, EA985C2D296F03FE00F2FF2E /* TileletIconModels.swift in Sources */, EA89200428AECF4B006B9984 /* UITextField+Publisher.swift in Sources */, + EAF2F4782C249D72007BFEDC /* AccessibilityUpdatable.swift in Sources */, 18A65A022B96E848006602CC /* Breadcrumbs.swift in Sources */, 1842B1E12BECE7B70021AFCA /* CalendarHeaderReusableView.swift in Sources */, EA78C7962C00CAC200430AD1 /* Groupable.swift in Sources */, diff --git a/VDS/Protocols/AccessibilityUpdatable.swift b/VDS/Protocols/AccessibilityUpdatable.swift new file mode 100644 index 00000000..07bd429b --- /dev/null +++ b/VDS/Protocols/AccessibilityUpdatable.swift @@ -0,0 +1,8 @@ +// +// AccessibilityUpdatable.swift +// VDS +// +// Created by Matt Bruce on 6/20/24. +// + +import Foundation From 73f27d1e8ba94d225f11396aa07c845bb137149d Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 20 Jun 2024 12:37:19 -0500 Subject: [PATCH 39/83] more code refactoring Signed-off-by: Matt Bruce --- VDS/BaseClasses/Control.swift | 2 - VDS/BaseClasses/Selector/SelectorBase.swift | 17 ++-- .../Selector/SelectorItemBase.swift | 77 ++++++++-------- VDS/BaseClasses/View.swift | 4 +- VDS/Classes/AccessibilityActionElement.swift | 3 +- VDS/Components/Badge/Badge.swift | 11 +-- .../BadgeIndicator/BadgeIndicator.swift | 21 ++--- .../Breadcrumbs/BreadcrumbItem.swift | 15 +-- VDS/Components/Buttons/ButtonBase.swift | 2 - .../Buttons/TextLink/TextLink.swift | 6 ++ .../Buttons/TextLinkCaret/TextLinkCaret.swift | 6 ++ VDS/Components/Checkbox/CheckboxGroup.swift | 3 +- .../Icon/ButtonIcon/ButtonIcon.swift | 72 ++++++++------- VDS/Components/Icon/Icon.swift | 13 +-- VDS/Components/Label/Label.swift | 10 +- .../Notification/Notification.swift | 12 +-- VDS/Components/Pagination/Pagination.swift | 30 ++++-- VDS/Components/RadioBox/RadioBoxGroup.swift | 8 +- VDS/Components/RadioBox/RadioBoxItem.swift | 90 +++++++----------- .../RadioButton/RadioButtonGroup.swift | 3 +- VDS/Components/Tabs/Tab.swift | 14 +-- VDS/Components/Tabs/Tabs.swift | 5 +- .../TextFields/EntryFieldBase.swift | 83 ++++++++--------- .../TextFields/InputField/TextField.swift | 2 - .../TextFields/TextArea/TextView.swift | 2 - .../TileContainer/TileContainer.swift | 12 +-- VDS/Components/TitleLockup/TitleLockup.swift | 27 +++--- VDS/Components/Toggle/Toggle.swift | 18 ++-- VDS/Components/Toggle/ToggleView.swift | 9 +- VDS/Components/Tooltip/Tooltip.swift | 34 +++---- VDS/Components/Tooltip/TooltipDialog.swift | 16 ++-- VDS/Protocols/AccessibilityUpdatable.swift | 87 ++++++++++++++++++ VDS/Protocols/Groupable.swift | 2 - VDS/Protocols/ViewProtocol.swift | 92 +------------------ 34 files changed, 399 insertions(+), 409 deletions(-) diff --git a/VDS/BaseClasses/Control.swift b/VDS/BaseClasses/Control.swift index aa7443a2..2a9fe769 100644 --- a/VDS/BaseClasses/Control.swift +++ b/VDS/BaseClasses/Control.swift @@ -127,8 +127,6 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable { //-------------------------------------------------- // MARK: - Accessibility //-------------------------------------------------- - open var shouldUpdateAccessibility: Bool = true - open var accessibilityAction: ((Control) -> Void)? private var _isAccessibilityElement: Bool = false diff --git a/VDS/BaseClasses/Selector/SelectorBase.swift b/VDS/BaseClasses/Selector/SelectorBase.swift index d1683aa3..fb8d771e 100644 --- a/VDS/BaseClasses/Selector/SelectorBase.swift +++ b/VDS/BaseClasses/Selector/SelectorBase.swift @@ -104,6 +104,16 @@ open class SelectorBase: Control, SelectorControlable { onClick = { control in control.toggle() } + + bridge_accessibilityLabelBlock = { [weak self] in + guard let self else { return "" } + return "\(Self.self)\(showError ? ", error" : "")" + } + + bridge_accessibilityHintBlock = { [weak self] in + guard let self else { return "" } + return !isEnabled ? "" : "Double tap to activate." + } } /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations. @@ -119,13 +129,6 @@ open class SelectorBase: Control, SelectorControlable { setNeedsLayout() layoutIfNeeded() } - - /// Used to update any Accessibility properties.ß - open override func updateAccessibility() { - super.updateAccessibility() - accessibilityLabel = "\(Self.self)\(showError ? ", error" : "")" - accessibilityHint = !isEnabled ? "" : "Double tap to open." - } /// This will change the state of the Selector and execute the actionBlock if provided. open func toggle() { } diff --git a/VDS/BaseClasses/Selector/SelectorItemBase.swift b/VDS/BaseClasses/Selector/SelectorItemBase.swift index 0326ac52..44ed01b1 100644 --- a/VDS/BaseClasses/Selector/SelectorItemBase.swift +++ b/VDS/BaseClasses/Selector/SelectorItemBase.swift @@ -145,8 +145,6 @@ open class SelectorItemBase: Control, Errorable, Changea open var hiddenValue: AnyHashable? { didSet { setNeedsUpdate() } } - open var accessibilityValueText: String? - open override var accessibilityAction: ((Control) -> Void)? { didSet { selectorView.accessibilityAction = { [weak self] selectorItemBase in @@ -156,34 +154,6 @@ open class SelectorItemBase: Control, Errorable, Changea } } - open var accessibilityLabelText: String { - var accessibilityLabels = [String]() - - if isSelected { - accessibilityLabels.append("selected") - } - - accessibilityLabels.append("\(Selector.self)") - - if let text = labelText, !text.isEmpty { - accessibilityLabels.append(text) - } - - if let text = childText, !text.isEmpty { - accessibilityLabels.append(text) - } - - if !isEnabled { - accessibilityLabels.append("dimmed") - } - - if let errorText, showError, !errorText.isEmpty { - accessibilityLabels.append("error, \(errorText)") - } - - return accessibilityLabels.joined(separator: ", ") - } - //-------------------------------------------------- // MARK: - Overrides //-------------------------------------------------- @@ -199,15 +169,48 @@ open class SelectorItemBase: Control, Errorable, Changea guard let self, isEnabled else { return } toggle() } + + selectorView.bridge_accessibilityLabelBlock = { [weak self ] in + guard let self else { return "" } + var accessibilityLabels = [String]() + + if isSelected { + accessibilityLabels.append("selected") + } + + accessibilityLabels.append("\(Selector.self)") + + if let text = labelText, !text.isEmpty { + accessibilityLabels.append(text) + } + + if let text = childText, !text.isEmpty { + accessibilityLabels.append(text) + } + + if !isEnabled { + accessibilityLabels.append("dimmed") + } + + if let errorText, showError, !errorText.isEmpty { + accessibilityLabels.append("error, \(errorText)") + } + + return accessibilityLabels.joined(separator: ", ") + } + + selectorView.bridge_accessibilityHintBlock = { [weak self] in + guard let self else { return "" } + return !isEnabled ? "" : "Double tap to activate." + } + } /// 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() - selectorView.isAccessibilityElement = true - selectorView.shouldUpdateAccessibility = false - + selectorView.isAccessibilityElement = true isAccessibilityElement = false addSubview(mainStackView) @@ -235,14 +238,6 @@ open class SelectorItemBase: Control, Errorable, Changea selectorView.isEnabled = isEnabled selectorView.surface = surface } - - /// Used to update any Accessibility properties. - open override func updateAccessibility() { - super.updateAccessibility() - selectorView.accessibilityLabel = accessibilityLabelText - selectorView.accessibilityHint = !isEnabled ? "" : "Double tap to activate." - accessibilityValue = accessibilityValueText - } open override var accessibilityElements: [Any]? { get { diff --git a/VDS/BaseClasses/View.swift b/VDS/BaseClasses/View.swift index 288a8cdc..c7df1765 100644 --- a/VDS/BaseClasses/View.swift +++ b/VDS/BaseClasses/View.swift @@ -94,8 +94,6 @@ open class View: UIView, ViewProtocol, UserInfoable { //-------------------------------------------------- // MARK: - Accessibility //-------------------------------------------------- - open var shouldUpdateAccessibility: Bool = true - open var accessibilityAction: ((View) -> Void)? private var _isAccessibilityElement: Bool = false @@ -219,7 +217,7 @@ open class View: UIView, ViewProtocol, UserInfoable { return block() } else { - return true + return super.accessibilityActivate() } // } diff --git a/VDS/Classes/AccessibilityActionElement.swift b/VDS/Classes/AccessibilityActionElement.swift index cc3487f9..7717839c 100644 --- a/VDS/Classes/AccessibilityActionElement.swift +++ b/VDS/Classes/AccessibilityActionElement.swift @@ -13,7 +13,8 @@ public class AccessibilityActionElement: UIAccessibilityElement { public var accessibilityAction: AXVoidReturnBlock? public override func accessibilityActivate() -> Bool { - accessibilityAction?() + guard let accessibilityAction else { return super.accessibilityActivate() } + accessibilityAction() return true } } diff --git a/VDS/Components/Badge/Badge.swift b/VDS/Components/Badge/Badge.swift index 43f702fa..5753428e 100644 --- a/VDS/Components/Badge/Badge.swift +++ b/VDS/Components/Badge/Badge.swift @@ -147,6 +147,11 @@ open class Badge: View { label.widthGreaterThanEqualTo(constant: minWidth) maxWidthConstraint = label.widthLessThanEqualTo(constant: 0).with { $0.isActive = false } clipsToBounds = true + + bridge_accessibilityLabelBlock = { [weak self] in + guard let self else { return "" } + return text + } } /// Resets to default settings. @@ -179,10 +184,4 @@ open class Badge: View { label.surface = surface label.isEnabled = isEnabled } - - open override func updateAccessibility() { - super.updateAccessibility() - - accessibilityLabel = text - } } diff --git a/VDS/Components/BadgeIndicator/BadgeIndicator.swift b/VDS/Components/BadgeIndicator/BadgeIndicator.swift index 740538d1..60025390 100644 --- a/VDS/Components/BadgeIndicator/BadgeIndicator.swift +++ b/VDS/Components/BadgeIndicator/BadgeIndicator.swift @@ -292,6 +292,16 @@ open class BadgeIndicator: View { label.centerYAnchor.constraint(equalTo: badgeView.centerYAnchor).isActive = true labelContraints.isActive = true + bridge_accessibilityLabelBlock = { [weak self] in + guard let self else { return "" } + if let accessibilityText { + return kind == .numbered ? label.text + " " + accessibilityText : accessibilityText + } else if kind == .numbered { + return label.text + } else { + return "Simple" + } + } } /// Resets to default settings. @@ -347,17 +357,6 @@ open class BadgeIndicator: View { setNeedsLayout() } - open override func updateAccessibility() { - super.updateAccessibility() - if let accessibilityText { - accessibilityLabel = kind == .numbered ? label.text + " " + accessibilityText : accessibilityText - } else if kind == .numbered { - accessibilityLabel = label.text - } else { - accessibilityLabel = "Simple" - } - } - open override func layoutSubviews() { super.layoutSubviews() diff --git a/VDS/Components/Breadcrumbs/BreadcrumbItem.swift b/VDS/Components/Breadcrumbs/BreadcrumbItem.swift index 08e58e60..bf6d4ed1 100644 --- a/VDS/Components/Breadcrumbs/BreadcrumbItem.swift +++ b/VDS/Components/Breadcrumbs/BreadcrumbItem.swift @@ -82,6 +82,15 @@ open class BreadcrumbItem: ButtonBase { isAccessibilityElement = true accessibilityTraits = .link + bridge_accessibilityHintBlock = { [weak self] in + guard let self else { return "" } + return !isEnabled ? "" : "Double tap to open." + } + + bridge_accessibilityLabelBlock = { [weak self] in + guard let self else { return "" } + return text + } } /// Used to make changes to the View based off a change events or from local properties. @@ -134,10 +143,4 @@ open class BreadcrumbItem: ButtonBase { setNeedsUpdate() } - /// Used to update any Accessibility properties. - open override func updateAccessibility() { - super.updateAccessibility() - accessibilityLabel = text - } - } diff --git a/VDS/Components/Buttons/ButtonBase.swift b/VDS/Components/Buttons/ButtonBase.swift index e5a80e2e..d7b80c8a 100644 --- a/VDS/Components/Buttons/ButtonBase.swift +++ b/VDS/Components/Buttons/ButtonBase.swift @@ -175,8 +175,6 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable { //-------------------------------------------------- // MARK: - Accessibility //-------------------------------------------------- - open var shouldUpdateAccessibility: Bool = true - open var accessibilityAction: ((ButtonBase) -> Void)? private var _isAccessibilityElement: Bool = false diff --git a/VDS/Components/Buttons/TextLink/TextLink.swift b/VDS/Components/Buttons/TextLink/TextLink.swift index e0aac99c..f77dfa79 100644 --- a/VDS/Components/Buttons/TextLink/TextLink.swift +++ b/VDS/Components/Buttons/TextLink/TextLink.swift @@ -105,6 +105,12 @@ open class TextLink: ButtonBase { lineHeightConstraint = line.height(constant: 1) lineHeightConstraint?.isActive = true } + + bridge_accessibilityHintBlock = { [weak self] in + guard let self else { return "" } + return !isEnabled ? "" : "Double tap to open." + } + } /// Used to make changes to the View based off a change events or from local properties. diff --git a/VDS/Components/Buttons/TextLinkCaret/TextLinkCaret.swift b/VDS/Components/Buttons/TextLinkCaret/TextLinkCaret.swift index 83d057c1..d1522d02 100644 --- a/VDS/Components/Buttons/TextLinkCaret/TextLinkCaret.swift +++ b/VDS/Components/Buttons/TextLinkCaret/TextLinkCaret.swift @@ -86,6 +86,12 @@ open class TextLinkCaret: ButtonBase { accessibilityTraits = .link titleLabel?.numberOfLines = 0 titleLabel?.lineBreakMode = .byWordWrapping + + bridge_accessibilityHintBlock = { [weak self] in + guard let self else { return "" } + return !isEnabled ? "" : "Double tap to open." + } + } /// Used to make changes to the View based off a change events or from local properties. diff --git a/VDS/Components/Checkbox/CheckboxGroup.swift b/VDS/Components/Checkbox/CheckboxGroup.swift index 242e193e..b450d4d5 100644 --- a/VDS/Components/Checkbox/CheckboxGroup.swift +++ b/VDS/Components/Checkbox/CheckboxGroup.swift @@ -47,8 +47,6 @@ open class CheckboxGroup: SelectorGroupBase, SelectorGroupMultiSel $0.surface = model.surface $0.inputId = model.inputId $0.hiddenValue = model.value - $0.accessibilityLabel = model.accessibileText - $0.accessibilityValueText = "item \(index+1) of \(selectorModels.count)" $0.labelText = model.labelText $0.labelTextAttributes = model.labelTextAttributes $0.childText = model.childText @@ -56,6 +54,7 @@ open class CheckboxGroup: SelectorGroupBase, SelectorGroupMultiSel $0.isSelected = model.selected $0.errorText = model.errorText $0.showError = model.showError + $0.selectorView.bridge_accessibilityValueBlock = { "item \(index+1) of \(selectorModels.count)" } } } } diff --git a/VDS/Components/Icon/ButtonIcon/ButtonIcon.swift b/VDS/Components/Icon/ButtonIcon/ButtonIcon.swift index 64ce0da5..bf8a50f7 100644 --- a/VDS/Components/Icon/ButtonIcon/ButtonIcon.swift +++ b/VDS/Components/Icon/ButtonIcon/ButtonIcon.swift @@ -30,7 +30,7 @@ open class ButtonIcon: Control, Changeable { public required init?(coder: NSCoder) { super.init(coder: coder) } - + //-------------------------------------------------- // MARK: - Enums //-------------------------------------------------- @@ -43,7 +43,7 @@ open class ButtonIcon: Control, Changeable { public enum SurfaceType: String, CaseIterable { case colorFill, media } - + /// Enum used to describe the size of button icon. public enum Size: String, EnumSubset { case large @@ -105,12 +105,12 @@ open class ButtonIcon: Control, Changeable { return .init(x: 6, y: 6) } } - + //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- public var onChangeSubscriber: AnyCancellable? - + ///Badge Indicator object used to render for the ButtonIcon. open var badgeIndicator = BadgeIndicator().with { $0.translatesAutoresizingMaskIntoConstraints = false @@ -140,10 +140,10 @@ open class ButtonIcon: Control, Changeable { open var selectedIconName: Icon.Name? { didSet { setNeedsUpdate() } } open var selectedIconColorConfiguration: SurfaceColorConfiguration? { didSet { setNeedsUpdate() } } - + /// Sets the size of button icon and icon. open var size: Size = .large { didSet { setNeedsUpdate() } } - + /// If provided, the button icon will have a box shadow. open var floating: Bool = false { didSet { setNeedsUpdate() } } @@ -152,7 +152,7 @@ open class ButtonIcon: Control, Changeable { /// If set to true, the button icon will not have a border. open var hideBorder: Bool = true { didSet { setNeedsUpdate() } } - + /// If provided, the badge indicator will present. open var showBadgeIndicator: Bool = false { didSet { setNeedsUpdate() } } @@ -169,14 +169,14 @@ open class ButtonIcon: Control, Changeable { /// Used to move the icon inside the button in both x and y axis. open var iconOffset: CGPoint = .init(x: 0, y: 0) { didSet { setNeedsUpdate() } } - + /// Sets a custom size of button icon container. open var customContainerSize: Int? { didSet { setNeedsUpdate() } } - + /// Sets a custom size of the icon. open var customIconSize: Int? { didSet { setNeedsUpdate() } } - + /// Sets a custom badgeIndicator offset open var customBadgeIndicatorOffset: CGPoint? { didSet { setNeedsUpdate() } } @@ -246,7 +246,7 @@ open class ButtonIcon: Control, Changeable { SurfaceColorConfiguration(.clear, .clear).eraseToAnyColorable() }() } - + private struct LowContrastColorFillConfiguration: Configuration { var kind: Kind = .lowContrast var surfaceType: SurfaceType = .colorFill @@ -255,7 +255,7 @@ open class ButtonIcon: Control, Changeable { SurfaceColorConfiguration(VDSColor.paletteGray44.withAlphaComponent(0.06), VDSColor.paletteGray44.withAlphaComponent(0.26)).eraseToAnyColorable() }() } - + private struct LowContrastColorFillFloatingConfiguration: Configuration, DropShadowableConfiguration { var kind: Kind = .lowContrast var surfaceType: SurfaceType = .colorFill @@ -277,7 +277,7 @@ open class ButtonIcon: Control, Changeable { } var configurations: [DropShadowable] { [dropshadow1Configuration, dropshadow2Configuration] } } - + private struct LowContrastMediaConfiguration: Configuration, Borderable { var kind: Kind = .lowContrast var surfaceType: SurfaceType = .media @@ -290,7 +290,7 @@ open class ButtonIcon: Control, Changeable { SurfaceColorConfiguration(VDSColor.elementsLowcontrastOnlight, VDSColor.elementsLowcontrastOndark).eraseToAnyColorable() }() } - + private struct LowContrastMediaFloatingConfiguration: Configuration, DropShadowableConfiguration { var kind: Kind = .lowContrast var surfaceType: SurfaceType = .media @@ -325,10 +325,10 @@ open class ButtonIcon: Control, Changeable { $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled) $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: [.selected, .disabled]) }.eraseToAnyColorable() - + }() } - + private struct HighContrastFloatingConfiguration: Configuration, DropShadowableConfiguration { var kind: Kind = .highContrast var surfaceType: SurfaceType = .colorFill @@ -357,9 +357,9 @@ open class ButtonIcon: Control, Changeable { } var configurations: [DropShadowable] { [dropshadow1Configuration, dropshadow2Configuration] } } - + private var badgeIndicatorDefaultSize: CGSize = .zero - + //-------------------------------------------------- // MARK: - Overrides //-------------------------------------------------- @@ -367,11 +367,11 @@ open class ButtonIcon: Control, Changeable { open override func setup() { super.setup() isAccessibilityElement = false - + //create a layoutGuide for the icon to key off of let iconLayoutGuide = UILayoutGuide() addLayoutGuide(iconLayoutGuide) - + //add the icon addSubview(icon) @@ -379,7 +379,7 @@ open class ButtonIcon: Control, Changeable { addSubview(badgeIndicator) badgeIndicator.isHidden = !showBadgeIndicator badgeIndicatorDefaultSize = badgeIndicator.frame.size - + //determines the height/width of the icon layoutGuideWidthConstraint = iconLayoutGuide.width(constant: size.containerSize) layoutGuideHeightConstraint = iconLayoutGuide.height(constant: size.containerSize) @@ -388,7 +388,7 @@ open class ButtonIcon: Control, Changeable { badgeIndicatorCenterXConstraint = badgeIndicator.centerXAnchor.constraint(equalTo: icon.centerXAnchor) badgeIndicatorCenterYConstraint = icon.centerYAnchor.constraint(equalTo: badgeIndicator.centerYAnchor) badgeIndicatorCenterYConstraint?.isActive = true - + badgeIndicatorLeadingConstraint?.isActive = true //pin layout guide iconLayoutGuide @@ -396,7 +396,7 @@ open class ButtonIcon: Control, Changeable { .pinLeading() .pinTrailing(0, .defaultHigh) .pinBottom(0, .defaultHigh) - + //determines the center point of the icon centerXConstraint = icon.centerXAnchor.constraint(equalTo: iconLayoutGuide.centerXAnchor, constant: 0) centerXConstraint?.activate() @@ -414,14 +414,14 @@ open class ButtonIcon: Control, Changeable { } } } - + /// This will change the state of the Selector and execute the actionBlock if provided. open func toggle() { //removed error isSelected.toggle() sendActions(for: .valueChanged) } - + /// Resets to default settings. open override func reset() { super.reset() @@ -437,7 +437,7 @@ open class ButtonIcon: Control, Changeable { showBadgeIndicator = false selectable = false badgeIndicatorModel = nil - onChange = nil + onChange = nil shouldUpdateView = true setNeedsUpdate() } @@ -464,16 +464,18 @@ open class ButtonIcon: Control, Changeable { setNeedsLayout() } - open override func updateAccessibility() { - super.updateAccessibility() - var elements = [Any]() - if iconName != nil { - elements.append(icon) + open override var accessibilityElements: [Any]? { + get { + var elements = [Any]() + if iconName != nil { + elements.append(icon) + } + if badgeIndicatorModel != nil && showBadgeIndicator { + elements.append(badgeIndicator) + } + return elements.count > 0 ? elements : nil } - if badgeIndicatorModel != nil && showBadgeIndicator { - elements.append(badgeIndicator) - } - accessibilityElements = elements.count > 0 ? elements : nil + set { } } open override func layoutSubviews() { diff --git a/VDS/Components/Icon/Icon.swift b/VDS/Components/Icon/Icon.swift index ac4a5818..2ac22ac8 100644 --- a/VDS/Components/Icon/Icon.swift +++ b/VDS/Components/Icon/Icon.swift @@ -94,6 +94,12 @@ open class Icon: View { isAccessibilityElement = true accessibilityTraits = .image + + bridge_accessibilityLabelBlock = { [weak self] in + guard let self else { return "" } + return name?.rawValue ?? "icon" + } + } /// Used to make changes to the View based off a change events or from local properties. @@ -118,12 +124,7 @@ open class Icon: View { super.reset() color = VDSColor.paletteBlack imageView.image = nil - } - - open override func updateAccessibility() { - super.updateAccessibility() - accessibilityLabel = name?.rawValue ?? "icon" - } + } } extension UIImage { diff --git a/VDS/Components/Label/Label.swift b/VDS/Components/Label/Label.swift index b0a85e7e..15ed4b45 100644 --- a/VDS/Components/Label/Label.swift +++ b/VDS/Components/Label/Label.swift @@ -213,7 +213,12 @@ open class Label: UILabel, ViewProtocol, UserInfoable { } } - open func setup() {} + open func setup() { + bridge_accessibilityLabelBlock = { [weak self] in + guard let self else { return "" } + return text + } + } open func reset() { shouldUpdateView = false @@ -240,7 +245,6 @@ open class Label: UILabel, ViewProtocol, UserInfoable { } open func updateAccessibility() { - accessibilityLabel = text if isEnabled { accessibilityTraits.remove(.notEnabled) } else { @@ -456,8 +460,6 @@ open class Label: UILabel, ViewProtocol, UserInfoable { //-------------------------------------------------- // MARK: - Accessibility //-------------------------------------------------- - open var shouldUpdateAccessibility: Bool = true - open var accessibilityAction: ((Label) -> Void)? private var _isAccessibilityElement: Bool = false diff --git a/VDS/Components/Notification/Notification.swift b/VDS/Components/Notification/Notification.swift index 5f180b71..66be33c4 100644 --- a/VDS/Components/Notification/Notification.swift +++ b/VDS/Components/Notification/Notification.swift @@ -265,6 +265,12 @@ open class Notification: View { isAccessibilityElement = false accessibilityElements = [closeButton, typeIcon, titleLabel, subTitleLabel, buttonGroup] closeButton.accessibilityTraits = [.button] + closeButton.accessibilityLabel = "Close Notification" + + typeIcon.bridge_accessibilityLabelBlock = { [weak self] in + guard let self else { return "" } + return style.accessibleText + } } /// Resets to default settings. @@ -372,12 +378,6 @@ open class Notification: View { } } - open override func updateAccessibility() { - super.updateAccessibility() - closeButton.accessibilityLabel = "Close Notification" - typeIcon.accessibilityLabel = style.accessibleText - } - private func setConstraints() { labelViewAndButtonViewConstraint?.deactivate() labelViewBottomConstraint?.deactivate() diff --git a/VDS/Components/Pagination/Pagination.swift b/VDS/Components/Pagination/Pagination.swift index 13478d85..8988aaa4 100644 --- a/VDS/Components/Pagination/Pagination.swift +++ b/VDS/Components/Pagination/Pagination.swift @@ -86,6 +86,10 @@ open class Pagination: View { } } + private var paginationDescription: String { + "Page \(selectedPage) of \(total) selected" + } + //-------------------------------------------------- // MARK: - Overrides //-------------------------------------------------- @@ -148,14 +152,26 @@ open class Pagination: View { guard let self else { return } self.selectedPage = max(0, self.selectedPage - 1) } + + collectionContainerView.bridge_accessibilityLabelBlock = { [weak self] in + guard let self else { return "" } + return "Pagination containing \(total) pages" + } + + collectionContainerView.bridge_accessibilityValueBlock = { [weak self] in + guard let self else { return "" } + return paginationDescription + } } - ///Updating the accessiblity values i.e elements, label, value other items for the component. - open override func updateAccessibility() { - super.updateAccessibility() - accessibilityElements = [previousButton, collectionContainerView, nextButton] - collectionContainerView.accessibilityLabel = "Pagination containing \(total) pages" - collectionContainerView.accessibilityValue = "Page \(selectedPage) of \(total) selected" + open override var accessibilityElements: [Any]? { + get { + let views: [UIView] = [previousButton, collectionContainerView, nextButton] + return views.filter({ $0.isHidden == false }) + } + set { + + } } /// Used to make changes to the View based off a change events or from local properties. @@ -176,7 +192,7 @@ open class Pagination: View { updateSelection() DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in guard let self else { return } - UIAccessibility.post(notification: .announcement, argument: "Page \(self.selectedPage) of \(self.total) selected") + UIAccessibility.post(notification: .announcement, argument: paginationDescription) } } diff --git a/VDS/Components/RadioBox/RadioBoxGroup.swift b/VDS/Components/RadioBox/RadioBoxGroup.swift index 296ea8ed..c58802c0 100644 --- a/VDS/Components/RadioBox/RadioBoxGroup.swift +++ b/VDS/Components/RadioBox/RadioBoxGroup.swift @@ -42,8 +42,6 @@ open class RadioBoxGroup: SelectorGroupBase, SelectorGroupSingleSe if let selectorModels { items = selectorModels.enumerated().map { index, model in return RadioBoxItem().with { - $0.accessibilityLabel = model.accessibileText - $0.accessibilityValue = "item \(index+1) of \(selectorModels.count)" $0.text = model.text $0.textAttributes = model.textAttributes $0.subText = model.subText @@ -56,7 +54,7 @@ open class RadioBoxGroup: SelectorGroupBase, SelectorGroupSingleSe $0.isSelected = model.selected $0.strikethrough = model.strikethrough $0.strikethroughAccessibilityText = model.strikethroughAccessibileText - $0.accessibilityValueText = "item \(index+1) of \(selectorModels.count)" + $0.selectorView.bridge_accessibilityValueBlock = { "item \(index+1) of \(selectorModels.count)" } } } } @@ -111,7 +109,7 @@ extension RadioBoxGroup { /// Current Surface and this is used to pass down to child objects that implement Surfacable public var surface: Surface public var inputId: String? - public var value: AnyHashable? + public var value: String? public var accessibileText: String? public var text: String /// Array of LabelAttributeModel objects used in rendering the text. @@ -126,7 +124,7 @@ extension RadioBoxGroup { public var strikethrough: Bool = false public var strikethroughAccessibileText: String - public init(disabled: Bool, surface: Surface = .light, inputId: String? = nil, value: AnyHashable? = nil, + public init(disabled: Bool, surface: Surface = .light, inputId: String? = nil, value: String? = nil, text: String = "", textAttributes: [any LabelAttributeModel]? = nil, subText: String? = nil, subTextAttributes: [any LabelAttributeModel]? = nil, subTextRight: String? = nil, subTextRightAttributes: [any LabelAttributeModel]? = nil, diff --git a/VDS/Components/RadioBox/RadioBoxItem.swift b/VDS/Components/RadioBox/RadioBoxItem.swift index 71b05494..562e1e54 100644 --- a/VDS/Components/RadioBox/RadioBoxItem.swift +++ b/VDS/Components/RadioBox/RadioBoxItem.swift @@ -74,7 +74,7 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable { } /// Selector for this RadioBox. - open var selectorView = View() + open var selectorView = View().with { $0.accessibilityIdentifier = "RadioBox" } /// If provided, the RadioBox text will be rendered. open var text: String? { didSet { setNeedsUpdate() } } @@ -125,12 +125,10 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable { open var inputId: String? { didSet { setNeedsUpdate() } } - open var value: AnyHashable? { hiddenValue } + open var value: String? { hiddenValue } - open var hiddenValue: AnyHashable? { didSet { setNeedsUpdate() } } + open var hiddenValue: String? { didSet { setNeedsUpdate() } } - open var accessibilityValueText: String? - open override var accessibilityAction: ((Control) -> Void)? { didSet { selectorView.accessibilityAction = { [weak self] selectorItemBase in @@ -140,34 +138,6 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable { } } - open var accessibilityLabelText: String { - var accessibilityLabels = [String]() - - if isSelected { - accessibilityLabels.append("selected") - } - - accessibilityLabels.append("Radiobox") - - if let text, !text.isEmpty { - accessibilityLabels.append(text) - } - - if let text = subText, !text.isEmpty { - accessibilityLabels.append(text) - } - - if let text = subTextRight, !text.isEmpty { - accessibilityLabels.append(text) - } - - if !isEnabled { - accessibilityLabels.append("dimmed") - } - - return accessibilityLabels.joined(separator: ", ") - } - //-------------------------------------------------- // MARK: - Configuration Properties //-------------------------------------------------- @@ -201,15 +171,39 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable { control.toggle() } - if #available(iOS 17.0, *) { - accessibilityHintBlock = { [weak self] in - - return "foo" + selectorView.bridge_accessibilityLabelBlock = { [weak self] in + guard let self else { return "" } + var accessibilityLabels = [String]() + + if isSelected { + accessibilityLabels.append("selected") } - } else { - // Fallback on earlier versions + + accessibilityLabels.append("Radiobox") + + if let text, !text.isEmpty { + accessibilityLabels.append(text) + } + + if let text = subText, !text.isEmpty { + accessibilityLabels.append(text) + } + + if let text = subTextRight, !text.isEmpty { + accessibilityLabels.append(text) + } + + if strikethrough { + accessibilityLabels.append(strikethroughAccessibilityText) + } + + if !isEnabled { + accessibilityLabels.append("dimmed") + } + + return accessibilityLabels.joined(separator: ", ") } - + } /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations. @@ -287,22 +281,6 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable { setNeedsLayout() } - /// Used to update any Accessibility properties. - open override func updateAccessibility() { - super.updateAccessibility() - selectorView.accessibilityLabel = accessibilityLabelText - - if let accessibilityValueText { - selectorView.accessibilityValue = strikethrough - ? "\(strikethroughAccessibilityText), \(accessibilityValueText)" - : accessibilityValueText - } else { - selectorView.accessibilityValue = strikethrough - ? "\(strikethroughAccessibilityText)" - : accessibilityValueText - } - } - open override var accessibilityElements: [Any]? { get { var items = [Any]() diff --git a/VDS/Components/RadioButton/RadioButtonGroup.swift b/VDS/Components/RadioButton/RadioButtonGroup.swift index ca91f3e5..f44f5587 100644 --- a/VDS/Components/RadioButton/RadioButtonGroup.swift +++ b/VDS/Components/RadioButton/RadioButtonGroup.swift @@ -46,8 +46,6 @@ open class RadioButtonGroup: SelectorGroupBase, SelectorGroupSi $0.surface = model.surface $0.inputId = model.inputId $0.hiddenValue = model.value - $0.accessibilityLabel = model.accessibileText - $0.accessibilityValueText = "item \(index+1) of \(selectorModels.count)" $0.labelText = model.labelText $0.labelTextAttributes = model.labelTextAttributes $0.childText = model.childText @@ -55,6 +53,7 @@ open class RadioButtonGroup: SelectorGroupBase, SelectorGroupSi $0.isSelected = model.selected $0.errorText = model.errorText $0.showError = model.showError + $0.selectorView.bridge_accessibilityValueBlock = { "item \(index+1) of \(selectorModels.count)" } } } } diff --git a/VDS/Components/Tabs/Tab.swift b/VDS/Components/Tabs/Tab.swift index 3b22c8f5..4c0b55cd 100644 --- a/VDS/Components/Tabs/Tab.swift +++ b/VDS/Components/Tabs/Tab.swift @@ -88,8 +88,6 @@ extension Tabs { open var minWidth: CGFloat = 44.0 { didSet { setNeedsUpdate() } } open override var shouldHighlight: Bool { false } - - open var accessibilityValueText: String? //-------------------------------------------------- // MARK: - Configuration @@ -151,6 +149,11 @@ extension Tabs { labelTopConstraint = label.pinTop(anchor: layoutGuide.topAnchor) labelLeadingConstraint = label.pinLeading(anchor: layoutGuide.leadingAnchor) labelBottomConstraint = label.pinBottom(anchor: layoutGuide.bottomAnchor, priority: .defaultHigh) + + bridge_accessibilityLabelBlock = { [weak self] in + guard let self else { return "" } + return text + } } /// Used to make changes to the View based off a change events or from local properties. @@ -176,13 +179,6 @@ extension Tabs { setNeedsLayout() } - /// Used to update any Accessibility properties. - open override func updateAccessibility() { - super.updateAccessibility() - accessibilityLabel = text - accessibilityValue = accessibilityValueText - } - open override func layoutSubviews() { super.layoutSubviews() diff --git a/VDS/Components/Tabs/Tabs.swift b/VDS/Components/Tabs/Tabs.swift index 4c463900..88ae02e7 100644 --- a/VDS/Components/Tabs/Tabs.swift +++ b/VDS/Components/Tabs/Tabs.swift @@ -305,7 +305,10 @@ open class Tabs: View { tabItem.orientation = orientation tabItem.surface = surface tabItem.indicatorPosition = indicatorPosition - tabItem.accessibilityValueText = "\(index+1) of \(tabViews.count) Tabs" + tabItem.bridge_accessibilityValueBlock = { [weak self] in + guard let self else { return "" } + return "\(index+1) of \(tabViews.count) Tabs" + } } } diff --git a/VDS/Components/TextFields/EntryFieldBase.swift b/VDS/Components/TextFields/EntryFieldBase.swift index 4e7422ee..2ef89480 100644 --- a/VDS/Components/TextFields/EntryFieldBase.swift +++ b/VDS/Components/TextFields/EntryFieldBase.swift @@ -94,12 +94,9 @@ 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: UIView = { - return UIView().with { - $0.translatesAutoresizingMaskIntoConstraints = false - $0.isAccessibilityElement = true - } - }() + internal var containerView = View().with { + $0.isAccessibilityElement = true + } /// This is set by a local method. internal var bottomContainerView: UIView! @@ -244,27 +241,6 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { open var rules = [AnyRule]() - open var accessibilityLabelText: String { - var accessibilityLabels = [String]() - - if let text = titleLabel.text?.trimmingCharacters(in: .whitespaces) { - accessibilityLabels.append(text) - } - if isReadOnly { - accessibilityLabels.append("read only") - } - if !isEnabled { - accessibilityLabels.append("dimmed") - } - if let errorText, showError { - accessibilityLabels.append("error, \(errorText)") - } - - accessibilityLabels.append("\(Self.self)") - - return accessibilityLabels.joined(separator: ", ") - } - open var accessibilityHintText: String = "Double tap to open" //-------------------------------------------------- @@ -283,11 +259,11 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { .pinBottom() trailingEqualsConstraint = layoutGuide.pinTrailing(anchor: trailingAnchor) - + // width constraints trailingLessThanEqualsConstraint = layoutGuide.pinTrailingLessThanOrEqualTo(anchor: trailingAnchor)?.deactivate() widthConstraint = layoutGuide.widthAnchor.constraint(equalToConstant: 0).deactivate() - + // Add mainStackView to the view addSubview(mainStackView) @@ -301,7 +277,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { //InputContainer, Icons, Buttons containerView.addSubview(fieldStackView) fieldStackView.pinToSuperView(.uniform(VDSLayout.space3X)) - + let fieldContainerView = getFieldContainer() fieldContainerView.translatesAutoresizingMaskIntoConstraints = false @@ -309,11 +285,11 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { fieldStackView.addArrangedSubview(fieldContainerView) fieldStackView.addArrangedSubview(statusIcon) fieldStackView.setCustomSpacing(VDSLayout.space3X, after: fieldContainerView) - + //get the container this is what show helper text, error text //can include other for character count, max length bottomContainerView = getBottomContainer() - + //this is the vertical stack that contains error text, helper text bottomContainerStackView.addArrangedSubview(errorLabel) bottomContainerStackView.addArrangedSubview(helperLabel) @@ -321,11 +297,11 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { // Add arranged subviews to textFieldStackView contentStackView.addArrangedSubview(containerView) contentStackView.addArrangedSubview(bottomContainerView) - + // Add arranged subviews to mainStackView mainStackView.addArrangedSubview(titleLabel) mainStackView.addArrangedSubview(contentStackView) - + // Initial position of the helper label updateHelperTextPosition() @@ -333,6 +309,38 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { titleLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable() errorLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable() helperLabel.textColorConfiguration = secondaryColorConfiguration.eraseToAnyColorable() + + containerView.bridge_accessibilityLabelBlock = { [weak self] in + guard let self else { return "" } + var accessibilityLabels = [String]() + + if let text = titleLabel.text?.trimmingCharacters(in: .whitespaces) { + accessibilityLabels.append(text) + } + if isReadOnly { + accessibilityLabels.append("read only") + } + if !isEnabled { + accessibilityLabels.append("dimmed") + } + if let errorText, showError { + accessibilityLabels.append("error, \(errorText)") + } + + accessibilityLabels.append("\(Self.self)") + + return accessibilityLabels.joined(separator: ", ") + } + + containerView.bridge_accessibilityHintBlock = { [weak self] in + guard let self else { return "" } + return isReadOnly || !isEnabled ? "" : accessibilityHintText + } + + containerView.bridge_accessibilityValueBlock = { [weak self] in + guard let self else { return "" } + return value + } } /// Updates the UI @@ -472,13 +480,6 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { } } - open override func updateAccessibility() { - super.updateAccessibility() - containerView.accessibilityLabel = accessibilityLabelText - containerView.accessibilityHint = isReadOnly || !isEnabled ? "" : accessibilityHintText - containerView.accessibilityValue = value - } - open override var accessibilityElements: [Any]? { get { var elements = [Any]() diff --git a/VDS/Components/TextFields/InputField/TextField.swift b/VDS/Components/TextFields/InputField/TextField.swift index 81e749b2..0108c874 100644 --- a/VDS/Components/TextFields/InputField/TextField.swift +++ b/VDS/Components/TextFields/InputField/TextField.swift @@ -67,8 +67,6 @@ open class TextField: UITextField, ViewProtocol, Errorable { /// Will determine if a scaled font should be used for the titleLabel font. open var useScaledFont: Bool = false { didSet { setNeedsUpdate() } } - open var shouldUpdateAccessibility: Bool = true - open var surface: Surface = .light { didSet { setNeedsUpdate() } } open var showError: Bool = false { didSet { setNeedsUpdate() } } diff --git a/VDS/Components/TextFields/TextArea/TextView.swift b/VDS/Components/TextFields/TextArea/TextView.swift index bbb7b329..ed622f90 100644 --- a/VDS/Components/TextFields/TextArea/TextView.swift +++ b/VDS/Components/TextFields/TextArea/TextView.swift @@ -149,8 +149,6 @@ open class TextView: UITextView, ViewProtocol, Errorable { //-------------------------------------------------- // MARK: - Accessibility //-------------------------------------------------- - open var shouldUpdateAccessibility: Bool = true - open var accessibilityAction: ((TextView) -> Void)? private var _isAccessibilityElement: Bool = false diff --git a/VDS/Components/TileContainer/TileContainer.swift b/VDS/Components/TileContainer/TileContainer.swift index d600f5dd..546d8d05 100644 --- a/VDS/Components/TileContainer/TileContainer.swift +++ b/VDS/Components/TileContainer/TileContainer.swift @@ -266,6 +266,11 @@ open class TileContainerBase: Control where Padding backgroundImageView.layer.cornerRadius = cornerRadius highlightView.layer.cornerRadius = cornerRadius clipsToBounds = true + + containerView.bridge_isAccessibilityElementBlock = { [weak self] in self?.onClickSubscriber != nil } + containerView.accessibilityHint = "Double tap to open." + containerView.accessibilityLabel = nil + } /// Overriden to take the hit if there is an onClickSubscriber and the view is not a UIControl @@ -338,13 +343,6 @@ open class TileContainerBase: Control where Padding } } - open override func updateAccessibility() { - super.updateAccessibility() - containerView.isAccessibilityElement = onClickSubscriber != nil - containerView.accessibilityHint = "Double tap to open." - containerView.accessibilityLabel = nil - } - open override var accessibilityElements: [Any]? { get { var items = [Any]() diff --git a/VDS/Components/TitleLockup/TitleLockup.swift b/VDS/Components/TitleLockup/TitleLockup.swift index af699b5c..bc5c4c3a 100644 --- a/VDS/Components/TitleLockup/TitleLockup.swift +++ b/VDS/Components/TitleLockup/TitleLockup.swift @@ -262,20 +262,21 @@ open class TitleLockup: View { //-------------------------------------------------- // MARK: - Overrides //-------------------------------------------------- - open override func updateAccessibility() { - super.updateAccessibility() - var elements = [Any]() - if eyebrowModel != nil { - elements.append(eyebrowLabel) + open override var accessibilityElements: [Any]? { + get { + var elements = [Any]() + if eyebrowModel != nil { + elements.append(eyebrowLabel) + } + if titleModel != nil { + elements.append(titleLabel) + } + if subTitleModel != nil { + elements.append(subTitleLabel) + } + return elements.count > 0 ? elements : nil } - if titleModel != nil { - elements.append(titleLabel) - } - if subTitleModel != nil { - elements.append(subTitleLabel) - } - setAccessibilityLabel(for: elements.compactMap({$0 as? UIView})) - accessibilityElements = elements.count > 0 ? elements : nil + set {} } /// Resets to default settings. diff --git a/VDS/Components/Toggle/Toggle.swift b/VDS/Components/Toggle/Toggle.swift index 6518e8db..aaa411de 100644 --- a/VDS/Components/Toggle/Toggle.swift +++ b/VDS/Components/Toggle/Toggle.swift @@ -207,6 +207,14 @@ open class Toggle: Control, Changeable, FormFieldable { label.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor) ] + bridge_accessibilityValueBlock = { [weak self] in + guard let self else { return "" } + if showText { + return isSelected ? onText : offText + } else { + return isSelected ? "On" : "Off" + } + } } /// Resets to default settings. @@ -238,16 +246,6 @@ open class Toggle: Control, Changeable, FormFieldable { toggleView.isEnabled = isEnabled toggleView.isOn = isOn } - - /// Used to update any Accessibility properties. - open override func updateAccessibility() { - super.updateAccessibility() - if showText { - accessibilityValue = isSelected ? onText : offText - } else { - accessibilityValue = isSelected ? "On" : "Off" - } - } /// This will change the state of the Selector and execute the actionBlock if provided. open func toggle() { diff --git a/VDS/Components/Toggle/ToggleView.swift b/VDS/Components/Toggle/ToggleView.swift index 4889ed40..935ed519 100644 --- a/VDS/Components/Toggle/ToggleView.swift +++ b/VDS/Components/Toggle/ToggleView.swift @@ -153,7 +153,7 @@ open class ToggleView: Control, Changeable, FormFieldable { // Update shadow layers frames to match the view's bounds knobView.layer.insertSublayer(shadowLayer1, at: 0) knobView.layer.insertSublayer(shadowLayer2, at: 0) - + accessibilityLabel = "Toggle" } /// Resets to default settings. @@ -176,13 +176,6 @@ open class ToggleView: Control, Changeable, FormFieldable { updateToggle() } - - /// Used to update any Accessibility properties. - open override func updateAccessibility() { - super.updateAccessibility() - - accessibilityLabel = "Toggle" - } /// This will change the state of the Selector and execute the actionBlock if provided. open func toggle() { diff --git a/VDS/Components/Tooltip/Tooltip.swift b/VDS/Components/Tooltip/Tooltip.swift index 0875ee64..f07fb1be 100644 --- a/VDS/Components/Tooltip/Tooltip.swift +++ b/VDS/Components/Tooltip/Tooltip.swift @@ -138,6 +138,24 @@ open class Tooltip: Control, TooltipLaunchable { contentView: tooltip.contentView), presenter: self) } + + bridge_accessibilityLabelBlock = { [weak self] in + guard let self else { return "" } + var label = title + if label == nil { + label = content + } + if let label, !label.isEmpty { + return label + } else { + return "Modal" + } + } + + bridge_accessibilityHintBlock = { [weak self] in + guard let self else { return "" } + return isEnabled ? "Double tap to open." : "" + } } /// Resets to default settings. @@ -163,23 +181,7 @@ open class Tooltip: Control, TooltipLaunchable { //get the color for the image icon.color = iconColorConfiguration.getColor(self) } - - /// Used to update any Accessibility properties. - open override func updateAccessibility() { - super.updateAccessibility() - var label = title - if label == nil { - label = content - } - if let label, !label.isEmpty { - accessibilityLabel = label - } else { - accessibilityLabel = "Modal" - } - accessibilityHint = isEnabled ? "Double tap to open." : "" - } - public static func accessibleText(for title: String?, content: String?, closeButtonText: String) -> String { var label = "" if let title { diff --git a/VDS/Components/Tooltip/TooltipDialog.swift b/VDS/Components/Tooltip/TooltipDialog.swift index 1a6e192d..0650a808 100644 --- a/VDS/Components/Tooltip/TooltipDialog.swift +++ b/VDS/Components/Tooltip/TooltipDialog.swift @@ -219,15 +219,19 @@ open class TooltipDialog: View, UIScrollViewDelegate { /// Used to update any Accessibility properties. open override func updateAccessibility() { super.updateAccessibility() - primaryAccessibilityElement.accessibilityHint = "Double tap on the \(tooltipModel.closeButtonText) button to close." primaryAccessibilityElement.accessibilityFrameInContainerSpace = .init(origin: .zero, size: frame.size) + } + + open override var accessibilityElements: [Any]? { + get { + var elements: [Any] = [primaryAccessibilityElement] + contentStackView.arrangedSubviews.forEach{ elements.append($0) } + elements.append(closeButton) - var elements: [Any] = [primaryAccessibilityElement] - contentStackView.arrangedSubviews.forEach{ elements.append($0) } - elements.append(closeButton) - - accessibilityElements = elements + return elements + } + set {} } } diff --git a/VDS/Protocols/AccessibilityUpdatable.swift b/VDS/Protocols/AccessibilityUpdatable.swift index 07bd429b..de46f867 100644 --- a/VDS/Protocols/AccessibilityUpdatable.swift +++ b/VDS/Protocols/AccessibilityUpdatable.swift @@ -6,3 +6,90 @@ // import Foundation +import UIKit + +public protocol AccessibilityUpdatable { + var bridge_isAccessibilityElementBlock: AXBoolReturnBlock? { get set } + + var bridge_accessibilityLabelBlock: AXStringReturnBlock? { get set } + + var bridge_accessibilityValueBlock: AXStringReturnBlock? { get set } + + var bridge_accessibilityHintBlock: AXStringReturnBlock? { get set } + + var bridge_accessibilityActivateBlock: AXBoolReturnBlock? { get set } + +} + +private struct AccessibilityBridge { + static var isAccessibilityElementBlockKey: UInt8 = 0 + static var activateBlockKey: UInt8 = 1 + static var valueBlockKey: UInt8 = 2 + static var hintBlockKey: UInt8 = 3 + static var labelBlockKey: UInt8 = 4 +} + +extension AccessibilityUpdatable where Self: NSObject { + + public var bridge_isAccessibilityElementBlock: AXBoolReturnBlock? { + get { + return objc_getAssociatedObject(self, &AccessibilityBridge.isAccessibilityElementBlockKey) as? AXBoolReturnBlock + } + set { + objc_setAssociatedObject(self, &AccessibilityBridge.isAccessibilityElementBlockKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) +// if #available(iOS 17, *) { +// self.isAccessibilityElementBlock = newValue +// } + } + } + + public var bridge_accessibilityActivateBlock: AXBoolReturnBlock? { + get { + return objc_getAssociatedObject(self, &AccessibilityBridge.activateBlockKey) as? AXBoolReturnBlock + } + set { + objc_setAssociatedObject(self, &AccessibilityBridge.activateBlockKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) +// if #available(iOS 17, *) { +// self.accessibilityActivateBlock = newValue +// } + } + } + + public var bridge_accessibilityValueBlock: AXStringReturnBlock? { + get { + return objc_getAssociatedObject(self, &AccessibilityBridge.valueBlockKey) as? AXStringReturnBlock + } + set { + objc_setAssociatedObject(self, &AccessibilityBridge.valueBlockKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) +// if #available(iOS 17, *) { +// self.accessibilityValueBlock = newValue +// } + } + } + + public var bridge_accessibilityHintBlock: AXStringReturnBlock? { + get { + return objc_getAssociatedObject(self, &AccessibilityBridge.hintBlockKey) as? AXStringReturnBlock + } + set { + objc_setAssociatedObject(self, &AccessibilityBridge.hintBlockKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) +// if #available(iOS 17, *) { +// self.accessibilityHintBlock = newValue +// } + } + } + + public var bridge_accessibilityLabelBlock: AXStringReturnBlock? { + get { + return objc_getAssociatedObject(self, &AccessibilityBridge.labelBlockKey) as? AXStringReturnBlock + } + set { + objc_setAssociatedObject(self, &AccessibilityBridge.labelBlockKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) +// if #available(iOS 17, *) { +// self.accessibilityLabelBlock = newValue +// } + } + } + +} + diff --git a/VDS/Protocols/Groupable.swift b/VDS/Protocols/Groupable.swift index 773362b7..b2f95782 100644 --- a/VDS/Protocols/Groupable.swift +++ b/VDS/Protocols/Groupable.swift @@ -9,6 +9,4 @@ import Foundation public protocol Groupable: Control { - /// Property used to add context to the Grouping of a set. - var accessibilityValueText: String? { get set } } diff --git a/VDS/Protocols/ViewProtocol.swift b/VDS/Protocols/ViewProtocol.swift index 8018e260..c7cba091 100644 --- a/VDS/Protocols/ViewProtocol.swift +++ b/VDS/Protocols/ViewProtocol.swift @@ -9,16 +9,13 @@ import Foundation import UIKit import Combine -public protocol ViewProtocol: AnyObject, Initable, Resettable, Enabling, Surfaceable { +public protocol ViewProtocol: AnyObject, Initable, Resettable, Enabling, Surfaceable, AccessibilityUpdatable { /// Set of Subscribers for any Publishers for this Control. var subscribers: Set { get set } /// Key of whether or not updateView() is called in setNeedsUpdate() var shouldUpdateView: Bool { get set } - /// Key of whether or not updateAccessibility() is called in setNeedsUpdate() - var shouldUpdateAccessibility: Bool { get set } - /// Used for setting an implementation for the default Accessible Action var accessibilityAction: ((Self) -> Void)? { get set } @@ -42,9 +39,7 @@ extension ViewProtocol { if shouldUpdateView { shouldUpdateView = false updateView() - if shouldUpdateAccessibility { - updateAccessibility() - } + updateAccessibility() shouldUpdateView = true } } @@ -70,86 +65,3 @@ extension ViewProtocol where Self: UIControl { }).store(in: &subscribers) } } - -public protocol AccessibilityUpdatable { - // Basic accessibility - var bridge_isAccessibilityElementBlock: AXBoolReturnBlock? { get set } - - var bridge_accessibilityLabelBlock: AXStringReturnBlock? { get set } - - var bridge_accessibilityValueBlock: AXStringReturnBlock? { get set } - - var bridge_accessibilityHintBlock: AXStringReturnBlock? { get set } - - var bridge_accessibilityActivateBlock: AXBoolReturnBlock? { get set } - -} - -extension NSObject: AccessibilityUpdatable { - static var isAccessibilityElementBlockKey: UInt8 = 0 - static var activateBlockKey: UInt8 = 1 - static var valueBlockKey: UInt8 = 2 - static var hintBlockKey: UInt8 = 3 - static var labelBlockKey: UInt8 = 4 - - public var bridge_isAccessibilityElementBlock: AXBoolReturnBlock? { - get { - return objc_getAssociatedObject(self, &NSObject.isAccessibilityElementBlockKey) as? AXBoolReturnBlock - } - set { - objc_setAssociatedObject(self, &NSObject.isAccessibilityElementBlockKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) -// if #available(iOS 17, *) { -// self.isAccessibilityElementBlock = newValue -// } - } - } - - public var bridge_accessibilityActivateBlock: AXBoolReturnBlock? { - get { - return objc_getAssociatedObject(self, &NSObject.activateBlockKey) as? AXBoolReturnBlock - } - set { - objc_setAssociatedObject(self, &NSObject.activateBlockKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) -// if #available(iOS 17, *) { -// self.accessibilityActivateBlock = newValue -// } - } - } - - public var bridge_accessibilityValueBlock: AXStringReturnBlock? { - get { - return objc_getAssociatedObject(self, &NSObject.valueBlockKey) as? AXStringReturnBlock - } - set { - objc_setAssociatedObject(self, &NSObject.valueBlockKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) -// if #available(iOS 17, *) { -// self.accessibilityValueBlock = newValue -// } - } - } - - public var bridge_accessibilityHintBlock: AXStringReturnBlock? { - get { - return objc_getAssociatedObject(self, &NSObject.hintBlockKey) as? AXStringReturnBlock - } - set { - objc_setAssociatedObject(self, &NSObject.hintBlockKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) -// if #available(iOS 17, *) { -// self.accessibilityHintBlock = newValue -// } - } - } - - public var bridge_accessibilityLabelBlock: AXStringReturnBlock? { - get { - return objc_getAssociatedObject(self, &NSObject.labelBlockKey) as? AXStringReturnBlock - } - set { - objc_setAssociatedObject(self, &NSObject.labelBlockKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) -// if #available(iOS 17, *) { -// self.accessibilityLabelBlock = newValue -// } - } - } - -} From b302077f994fe98539a389dd5471ca82783ded4e Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 20 Jun 2024 18:23:54 -0500 Subject: [PATCH 40/83] added popover Signed-off-by: Matt Bruce --- VDS.xcodeproj/project.pbxproj | 8 +- VDS/Classes/ClearPopoverViewController.swift | 151 +++++++++++++++++++ 2 files changed, 155 insertions(+), 4 deletions(-) create mode 100644 VDS/Classes/ClearPopoverViewController.swift diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index ff8a6735..7c9565f7 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -152,7 +152,7 @@ EAC58C182BED0E2300BA39FA /* SecurityCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C172BED0E2300BA39FA /* SecurityCode.swift */; }; EAC58C232BF2824200BA39FA /* DatePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C222BF2824200BA39FA /* DatePicker.swift */; }; EAC58C272BF4116200BA39FA /* DatePickerCalendarModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C262BF4116200BA39FA /* DatePickerCalendarModel.swift */; }; - EAC58C292BF4118C00BA39FA /* DatePickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C282BF4118C00BA39FA /* DatePickerViewController.swift */; }; + EAC58C292BF4118C00BA39FA /* ClearPopoverViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C282BF4118C00BA39FA /* ClearPopoverViewController.swift */; }; EAC71A1D2A2E155A00E47A9F /* Checkbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC71A1C2A2E155A00E47A9F /* Checkbox.swift */; }; EAC71A1F2A2E173D00E47A9F /* RadioButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC71A1E2A2E173D00E47A9F /* RadioButton.swift */; }; EAC846F3294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC846F2294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift */; }; @@ -368,7 +368,7 @@ EAC58C222BF2824200BA39FA /* DatePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePicker.swift; sourceTree = ""; }; EAC58C242BF2A7FB00BA39FA /* DatePickerChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = DatePickerChangeLog.txt; sourceTree = ""; }; EAC58C262BF4116200BA39FA /* DatePickerCalendarModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickerCalendarModel.swift; sourceTree = ""; }; - EAC58C282BF4118C00BA39FA /* DatePickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickerViewController.swift; sourceTree = ""; }; + EAC58C282BF4118C00BA39FA /* ClearPopoverViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClearPopoverViewController.swift; sourceTree = ""; }; EAC71A1C2A2E155A00E47A9F /* Checkbox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Checkbox.swift; sourceTree = ""; }; EAC71A1E2A2E173D00E47A9F /* RadioButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButton.swift; sourceTree = ""; }; EAC846F2294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonGroupCollectionViewCell.swift; sourceTree = ""; }; @@ -746,6 +746,7 @@ isa = PBXGroup; children = ( EA985C1C296CD13600F2FF2E /* BundleManager.swift */, + EAC58C282BF4118C00BA39FA /* ClearPopoverViewController.swift */, EAF7F0B8289C139800B287F5 /* ColorConfiguration.swift */, EAB5FEF02927F4AA00998C17 /* SelfSizingCollectionView.swift */, EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */, @@ -966,7 +967,6 @@ children = ( EAC58C222BF2824200BA39FA /* DatePicker.swift */, EAC58C262BF4116200BA39FA /* DatePickerCalendarModel.swift */, - EAC58C282BF4118C00BA39FA /* DatePickerViewController.swift */, EAC58C242BF2A7FB00BA39FA /* DatePickerChangeLog.txt */, ); path = DatePicker; @@ -1304,7 +1304,7 @@ EA3361BD288B2C760071C351 /* TypeAlias.swift in Sources */, EAC58C0A2BED004E00BA39FA /* FieldType.swift in Sources */, EA471F3A2A95587500CE9E58 /* LayoutConstraintable.swift in Sources */, - EAC58C292BF4118C00BA39FA /* DatePickerViewController.swift in Sources */, + EAC58C292BF4118C00BA39FA /* ClearPopoverViewController.swift in Sources */, EAF193432C134F3800C68D18 /* TableCellItem.swift in Sources */, EAB1D2CF28ABEF2B00DAE764 /* Typography+Base.swift in Sources */, EA0D1C3B2A6AD51B00E5C127 /* Typogprahy+Styles.swift in Sources */, diff --git a/VDS/Classes/ClearPopoverViewController.swift b/VDS/Classes/ClearPopoverViewController.swift new file mode 100644 index 00000000..33531a75 --- /dev/null +++ b/VDS/Classes/ClearPopoverViewController.swift @@ -0,0 +1,151 @@ +// +// DatePickerPopoverViewController.swift +// VDS +// +// Created by Matt Bruce on 5/14/24. +// + +import Foundation +import UIKit + +open class ClearPopoverViewController: UIViewController, UIPopoverPresentationControllerDelegate { + + /// The view to be inserted inside the popover + private var contentView: UIView! + + /// An object representing the arrow of the popover. + private var arrow: UIPopoverArrowDirection + + /// Popover presentation controller of the popover + private var popOver: UIPopoverPresentationController! + + open var spacing: CGFloat = 0 + /** + A controller that manages the popover. + - Parameter contentView: The view to be inserted inside the popover. + - Parameter design: An object used for defining visual attributes of the popover. + - Parameter arrow: An object representing the arrow in popover. + - Parameter sourceView: The view containing the anchor rectangle for the popover. + - Parameter sourceRect: The rectangle in the specified view in which to anchor the popover. + - Parameter barButtonItem: The bar button item on which to anchor the popover. + + Assign a value to `barButton` to anchor the popover to the specified bar button item. When presented, the popover’s arrow points to the specified item. Alternatively, you may specify the anchor location for the popover using the `sourceView` and `sourceRect` properties. + */ + public init(contentView: UIView, arrow: UIPopoverArrowDirection, sourceView: UIView? = nil, sourceRect: CGRect? = nil, spacing: CGFloat = 0, barButtonItem: UIBarButtonItem? = nil) { + self.contentView = contentView + self.spacing = spacing + self.arrow = arrow + super.init(nibName: nil, bundle: nil) + setupPopover(sourceView, sourceRect, barButtonItem) + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + open override func viewIsAppearing(_ animated: Bool) { + super.viewIsAppearing(animated) + view.superview?.accessibilityIdentifier = "HadCornerRadius" + view.accessibilityIdentifier = "PopoverViewController.View" + contentView.accessibilityIdentifier = "PopoverViewController.ContentView" + view.superview?.layer.cornerRadius = 0 + } + + open override func viewDidLayoutSubviews() { + contentView.frame.origin = CGPoint(x: 0, y: 0) + } + + ///Sets up the Popover and starts the timer for its closing. + private func setupPopover(_ sourceView: UIView?, _ sourceRect: CGRect?, _ barButtonItem: UIBarButtonItem?) { + modalPresentationStyle = .popover + view.addSubview(contentView) + + popOver = self.popoverPresentationController! + popOver.popoverLayoutMargins = .zero + popOver.popoverBackgroundViewClass = ClearPopoverBackgroundView.self + popOver.sourceView = sourceView + popOver.popoverLayoutMargins = .zero + + if let sourceRect = sourceRect { + popOver.sourceRect = sourceRect + } + + popOver.barButtonItem = barButtonItem + popOver.delegate = self + popOver.permittedArrowDirections = arrow + popOver.backgroundColor = .clear + + } + + open func popoverPresentationController(_ popoverPresentationController: UIPopoverPresentationController, willRepositionPopoverTo rect: UnsafeMutablePointer, in view: AutoreleasingUnsafeMutablePointer) { + if let presentedView = popoverPresentationController.presentedViewController.view.superview { + presentedView.layer.cornerRadius = 0 + } + } + + private func updatePopoverPosition() { + guard let popoverPresentationController = popoverPresentationController else { return } + if let sourceView = popoverPresentationController.sourceView { + popoverPresentationController.sourceRect = .init(x: sourceView.bounds.origin.x, + y: sourceView.bounds.origin.y, + width: sourceView.bounds.width, + height: sourceView.bounds.height + spacing) + } + } + + // Ensure to handle rotations + open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + coordinator.animate(alongsideTransition: { [weak self] _ in + self?.updatePopoverPosition() + }) + } + + open func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle { + return .none + } + + // Returns presentation controller of the popover + open func getPopoverPresentationController() -> UIPopoverPresentationController { + return popOver + } +} + +open class ClearPopoverBackgroundView: UIPopoverBackgroundView { + open override var arrowOffset: CGFloat { + get { 0 } + set { } + } + + open override var arrowDirection: UIPopoverArrowDirection { + get { .any } + set { } + } + + open override class var wantsDefaultContentAppearance: Bool { + false + } + + open override class func contentViewInsets() -> UIEdgeInsets{ + .zero + } + + open override class func arrowHeight() -> CGFloat { + 0 + } + + open override class func arrowBase() -> CGFloat{ + 0 + } + + open override func layoutSubviews() { + super.layoutSubviews() + layer.shadowOpacity = 0 + layer.shadowRadius = 0 + layer.cornerRadius = 0 + } + + open override func draw(_ rect: CGRect) { + + } +} From 0a2e6c88b6a5421ab883fe45c3c8b0a835963b8b Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 20 Jun 2024 18:24:09 -0500 Subject: [PATCH 41/83] deleted file no longer needed Signed-off-by: Matt Bruce --- .../DatePicker/DatePickerViewController.swift | 71 ------------------- 1 file changed, 71 deletions(-) delete mode 100644 VDS/Components/DatePicker/DatePickerViewController.swift diff --git a/VDS/Components/DatePicker/DatePickerViewController.swift b/VDS/Components/DatePicker/DatePickerViewController.swift deleted file mode 100644 index f8a6e2f0..00000000 --- a/VDS/Components/DatePicker/DatePickerViewController.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// DatePickerPopoverViewController.swift -// VDS -// -// Created by Matt Bruce on 5/14/24. -// - -import Foundation -import UIKit - -protocol DatePickerViewControllerDelegate: NSObject { - func didSelectDate(_ controller: DatePicker.DatePickerViewController, date: Date) -} - -extension DatePicker { - class DatePickerViewController: UIViewController { - private var padding: CGFloat = 15 - private var topPadding: CGFloat { 10 + padding } - private var calendarModel: CalendarModel - private let picker = CalendarBase() - weak var delegate: DatePickerViewControllerDelegate? - - init(_ calendarModel: CalendarModel, delegate: DatePickerViewControllerDelegate?) { - self.delegate = delegate - self.calendarModel = calendarModel - super.init(nibName: nil, bundle: nil) - self.picker.onChange = { [weak self] control in - guard let self else { return } - self.delegate?.didSelectDate(self, date: control.selectedDate) - } - } - - var selectedDate: Date = Date() { - didSet { - picker.selectedDate = selectedDate - } - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - view.addSubview(picker) - picker.surface = calendarModel.surface - picker.hideContainerBorder = calendarModel.hideContainerBorder - picker.hideCurrentDateIndicator = calendarModel.hideCurrentDateIndicator - picker.indicators = calendarModel.indicators - picker.activeDates = calendarModel.activeDates - picker.inactiveDates = calendarModel.inactiveDates - picker.selectedDate = selectedDate - picker.minDate = calendarModel.minDate - picker.maxDate = calendarModel.maxDate - picker.pinToSuperView(.init(top: topPadding, left: padding, bottom: padding, right: padding)) - view.backgroundColor = picker.backgroundColor - } - - override var preferredContentSize: CGSize { - get { - var size = picker.frame.size - size.height += 40 - size.width += 30 - return size - } - set { - super.preferredContentSize = newValue - } - } - } -} From ce6aad5540788e3b637139488093d223e5d600f3 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 20 Jun 2024 18:24:21 -0500 Subject: [PATCH 42/83] refactored to use new popover Signed-off-by: Matt Bruce --- VDS/Components/DatePicker/DatePicker.swift | 69 +++++++++++++++------- 1 file changed, 47 insertions(+), 22 deletions(-) diff --git a/VDS/Components/DatePicker/DatePicker.swift b/VDS/Components/DatePicker/DatePicker.swift index fdc6e05f..0e985176 100644 --- a/VDS/Components/DatePicker/DatePicker.swift +++ b/VDS/Components/DatePicker/DatePicker.swift @@ -5,7 +5,7 @@ import Combine /// A dropdown select is an expandable menu of predefined options that allows a customer to make a single selection. @objc(VDSDatePicker) -open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopoverPresentationControllerDelegate { +open class DatePicker: EntryFieldBase { //-------------------------------------------------- // MARK: - Initializers //-------------------------------------------------- @@ -31,7 +31,7 @@ open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopov // MARK: - Private Properties //-------------------------------------------------- internal var minWidthDefault = 186.0 - + internal var bottomStackView: UIStackView = { return UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false @@ -114,6 +114,16 @@ open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopov } } .store(in: &subscribers) + + NotificationCenter.default + .publisher(for: UIDevice.orientationDidChangeNotification).sink { [weak self] _ in + guard let self, let popoverController else { return } + popoverController.dismiss(animated: true){ [weak self] in + guard let self else { return } + } + } + .store(in: &subscribers) + } open override func getFieldContainer() -> UIView { @@ -153,31 +163,46 @@ open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopov selectedDateLabel.text = formatter.string(from: date) } - internal func togglePicker() { - let calendarVC = DatePickerViewController(calendarModel, delegate: self) - calendarVC.modalPresentationStyle = .popover - calendarVC.selectedDate = selectedDate ?? Date() - if let popoverController = calendarVC.popoverPresentationController { - popoverController.delegate = self - popoverController.sourceView = containerView - popoverController.sourceRect = containerView.bounds - popoverController.permittedArrowDirections = .up - } - if let viewController = UIApplication.topViewController() { - viewController.present(calendarVC, animated: true, completion: nil) - } - } + internal var popoverController: UIViewController? - internal func didSelectDate(_ controller: DatePickerViewController, date: Date) { + func didSelect(_ date: Date) { selectedDate = date - controller.dismiss(animated: true) { [weak self] in + sendActions(for: .valueChanged) + UIAccessibility.post(notification: .layoutChanged, argument: self.containerView) + popoverController?.dismiss(animated: true){ [weak self] in guard let self else { return } - self.sendActions(for: .valueChanged) - UIAccessibility.post(notification: .layoutChanged, argument: self.containerView) + popoverController = nil } } - public func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle { - return .none + internal func togglePicker() { + let calendar = CalendarBase() + calendar.activeDates = calendarModel.activeDates + calendar.hideContainerBorder = calendarModel.hideContainerBorder + calendar.hideCurrentDateIndicator = calendarModel.hideCurrentDateIndicator + calendar.inactiveDates = calendarModel.inactiveDates + calendar.indicators = calendarModel.indicators + calendar.maxDate = calendarModel.maxDate + calendar.minDate = calendarModel.minDate + calendar.surface = calendarModel.surface + calendar.setNeedsLayout() + calendar.layoutIfNeeded() + calendar.onChange = { [weak self] control in + guard let self else { return } + didSelect(control.selectedDate) + } + + popoverController = ClearPopoverViewController(contentView: calendar, + arrow: .up, + sourceView: containerView, + sourceRect: containerView.bounds, + spacing: VDSLayout.space1X) + + if let viewController = UIApplication.topViewController(), let popoverController { + viewController.present(popoverController, + animated: true, + completion: nil) + } } } + From 31b9704163e64fd444759d51630254df0c1271ab Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 21 Jun 2024 12:20:39 -0500 Subject: [PATCH 43/83] first attempt to get link clicks working again Signed-off-by: Matt Bruce --- VDS/Components/Label/Label.swift | 85 ++++++++++++++++++++++++++++---- 1 file changed, 75 insertions(+), 10 deletions(-) diff --git a/VDS/Components/Label/Label.swift b/VDS/Components/Label/Label.swift index 15ed4b45..c622b694 100644 --- a/VDS/Components/Label/Label.swift +++ b/VDS/Components/Label/Label.swift @@ -411,20 +411,85 @@ open class Label: UILabel, ViewProtocol, UserInfoable { private func didTapActionInLabel(_ location: CGPoint, inRange targetRange: NSRange) -> Bool { - guard let attributedText else { return false } +// guard let attributedText else { return false } +// let layoutManager = NSLayoutManager() +// let textContainer = NSTextContainer(size: bounds.size) +// let textStorage = NSTextStorage(attributedString: attributedText) + // layoutManager.addTextContainer(textContainer) + // textStorage.addLayoutManager(layoutManager) + // + // let characterIndex = layoutManager.characterIndex(for: location, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil) + // + // guard let _ = attributedText.attribute(NSAttributedString.Key.action, at: characterIndex, effectiveRange: nil) as? String, + // characterIndex < attributedText.length else { + // return false + // } + // return true + + // There would only ever be one clause to act on. + guard let abstractContainer = abstractTextContainer() else { return false } + let textContainer = abstractContainer.0 + let layoutManager = abstractContainer.1 + + let tapLocation = location + let indexOfGlyph = layoutManager.glyphIndex(for: tapLocation, in: textContainer) + let intrinsicWidth = intrinsicContentSize.width + + // Assert that tapped occured within acceptable bounds based on alignment. + switch textAlignment { + case .right: + if tapLocation.x < bounds.width - intrinsicWidth { + return false + } + case .center: + let halfBounds = bounds.width / 2 + let halfIntrinsicWidth = intrinsicWidth / 2 + + if tapLocation.x > halfBounds + halfIntrinsicWidth { + return false + } else if tapLocation.x < halfBounds - halfIntrinsicWidth { + return false + } + default: // Left align + if tapLocation.x > intrinsicWidth { + return false + } + } + + // Affirms that the tap occured in the desired rect of provided by the target range. + return layoutManager.boundingRect(forGlyphRange: targetRange, in: textContainer).contains(tapLocation) && NSLocationInRange(indexOfGlyph, targetRange) + } + + /** + Provides a text container and layout manager of how the text would appear on screen. + They are used in tandem to derive low-level TextKit results of the label. + */ + public func abstractTextContainer() -> (NSTextContainer, NSLayoutManager, NSTextStorage)? { + + // Must configure the attributed string to translate what would appear on screen to accurately analyze. + guard let attributedText = attributedText else { return nil } + + let paragraph = NSMutableParagraphStyle() + paragraph.alignment = textAlignment + + let stagedAttributedString = NSMutableAttributedString(attributedString: attributedText) + stagedAttributedString.addAttributes([NSAttributedString.Key.paragraphStyle: paragraph], range: NSRange(location: 0, length: attributedText.string.count)) + + let textStorage = NSTextStorage(attributedString: stagedAttributedString) let layoutManager = NSLayoutManager() - let textContainer = NSTextContainer(size: bounds.size) - let textStorage = NSTextStorage(attributedString: attributedText) + let textContainer = NSTextContainer(size: .zero) + layoutManager.addTextContainer(textContainer) textStorage.addLayoutManager(layoutManager) - - let characterIndex = layoutManager.characterIndex(for: location, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil) - - guard let _ = attributedText.attribute(NSAttributedString.Key.action, at: characterIndex, effectiveRange: nil) as? String, characterIndex < attributedText.length else { return false } - return true + + textContainer.lineFragmentPadding = 0.0 + textContainer.lineBreakMode = lineBreakMode + textContainer.maximumNumberOfLines = numberOfLines + textContainer.size = bounds.size + + return (textContainer, layoutManager, textStorage) } - - + private func customAccessibilityElement(text: String?, range: NSRange, accessibleText: String? = nil) -> AccessibilityActionElement? { guard let text = text, let attributedText else { return nil } From 8b37986b400622da5d6c7b03c7ece80543d91d0c Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 21 Jun 2024 13:19:15 -0500 Subject: [PATCH 44/83] reverted back to original MVA code for getting the location of links and such. refactored Signed-off-by: Matt Bruce --- VDS/Components/Label/Label.swift | 84 +++++++-------------- VDS/Extensions/UITapGestureRecognizer.swift | 20 +---- 2 files changed, 32 insertions(+), 72 deletions(-) diff --git a/VDS/Components/Label/Label.swift b/VDS/Components/Label/Label.swift index c622b694..78d34574 100644 --- a/VDS/Components/Label/Label.swift +++ b/VDS/Components/Label/Label.swift @@ -389,82 +389,59 @@ open class Label: UILabel, ViewProtocol, UserInfoable { } } + //-------------------------------------------------- + // MARK: - Touch Events + //-------------------------------------------------- @objc private func textLinkTapped(_ gesture: UITapGestureRecognizer) { - for actionable in actions { - // This determines if we tapped on the desired range of text. - let location = gesture.location(in: self) - if didTapActionInLabel(location, inRange: actionable.range) { - actionable.performAction() - return - } + let location = gesture.location(in: self) + if let action = actions.first(where: { isAction(for: location, inRange: $0.range) }) { + action.performAction() } } public func isAction(for location: CGPoint) -> Bool { - for actionable in actions { - if didTapActionInLabel(location, inRange: actionable.range) { - return true - } - } - return false + actions.contains(where: {isAction(for: location, inRange: $0.range)}) } - private func didTapActionInLabel(_ location: CGPoint, inRange targetRange: NSRange) -> Bool { + public func isAction(for location: CGPoint, inRange targetRange: NSRange) -> Bool { + guard let attributedText = attributedText, let abstractContainer = abstractTextContainer() else { return false } + let textContainer = abstractContainer.textContainer + let layoutManager = abstractContainer.layoutManager -// guard let attributedText else { return false } -// let layoutManager = NSLayoutManager() -// let textContainer = NSTextContainer(size: bounds.size) -// let textStorage = NSTextStorage(attributedString: attributedText) - // layoutManager.addTextContainer(textContainer) - // textStorage.addLayoutManager(layoutManager) - // - // let characterIndex = layoutManager.characterIndex(for: location, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil) - // - // guard let _ = attributedText.attribute(NSAttributedString.Key.action, at: characterIndex, effectiveRange: nil) as? String, - // characterIndex < attributedText.length else { - // return false - // } - // return true - - // There would only ever be one clause to act on. - guard let abstractContainer = abstractTextContainer() else { return false } - let textContainer = abstractContainer.0 - let layoutManager = abstractContainer.1 - - let tapLocation = location - let indexOfGlyph = layoutManager.glyphIndex(for: tapLocation, in: textContainer) + let indexOfGlyph = layoutManager.glyphIndex(for: location, in: textContainer) let intrinsicWidth = intrinsicContentSize.width // Assert that tapped occured within acceptable bounds based on alignment. switch textAlignment { case .right: - if tapLocation.x < bounds.width - intrinsicWidth { + if location.x < bounds.width - intrinsicWidth { return false } case .center: let halfBounds = bounds.width / 2 let halfIntrinsicWidth = intrinsicWidth / 2 - if tapLocation.x > halfBounds + halfIntrinsicWidth { + if location.x > halfBounds + halfIntrinsicWidth { return false - } else if tapLocation.x < halfBounds - halfIntrinsicWidth { + } else if location.x < halfBounds - halfIntrinsicWidth { return false } default: // Left align - if tapLocation.x > intrinsicWidth { + if location.x > intrinsicWidth { return false } } // Affirms that the tap occured in the desired rect of provided by the target range. - return layoutManager.boundingRect(forGlyphRange: targetRange, in: textContainer).contains(tapLocation) && NSLocationInRange(indexOfGlyph, targetRange) + return layoutManager.boundingRect(forGlyphRange: targetRange, in: textContainer).contains(location) + && NSLocationInRange(indexOfGlyph, targetRange) } /** Provides a text container and layout manager of how the text would appear on screen. They are used in tandem to derive low-level TextKit results of the label. */ - public func abstractTextContainer() -> (NSTextContainer, NSLayoutManager, NSTextStorage)? { + public func abstractTextContainer() -> (textContainer: NSTextContainer, layoutManager: NSLayoutManager, textStorage: NSTextStorage)? { // Must configure the attributed string to translate what would appear on screen to accurately analyze. guard let attributedText = attributedText else { return nil } @@ -489,25 +466,23 @@ open class Label: UILabel, ViewProtocol, UserInfoable { return (textContainer, layoutManager, textStorage) } - + + //-------------------------------------------------- + // MARK: - Accessibility + //-------------------------------------------------- private func customAccessibilityElement(text: String?, range: NSRange, accessibleText: String? = nil) -> AccessibilityActionElement? { - guard let text = text, let attributedText else { return nil } + guard let text = text, let attributedText, let abstractContainer = abstractTextContainer() else { return nil } + let textContainer = abstractContainer.textContainer + let layoutManager = abstractContainer.layoutManager + let actionText = accessibleText ?? (text.isValid(range: range) ? NSString(string:text).substring(with: range) : text) - // Calculate the frame of the substring - let layoutManager = NSLayoutManager() - let textContainer = NSTextContainer(size: bounds.size) - let textStorage = NSTextStorage(attributedString: attributedText) - layoutManager.addTextContainer(textContainer) - textStorage.addLayoutManager(layoutManager) - var glyphRange = NSRange() // Convert the range for the substring into a range of glyphs layoutManager.characterRange(forGlyphRange: range, actualGlyphRange: &glyphRange) - let substringBounds = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer) // Create custom accessibility element @@ -520,11 +495,8 @@ open class Label: UILabel, ViewProtocol, UserInfoable { accessibilityElements?.append(element) return element } + - - //-------------------------------------------------- - // MARK: - Accessibility - //-------------------------------------------------- open var accessibilityAction: ((Label) -> Void)? private var _isAccessibilityElement: Bool = false diff --git a/VDS/Extensions/UITapGestureRecognizer.swift b/VDS/Extensions/UITapGestureRecognizer.swift index 612a11ca..4461ae06 100644 --- a/VDS/Extensions/UITapGestureRecognizer.swift +++ b/VDS/Extensions/UITapGestureRecognizer.swift @@ -12,23 +12,11 @@ extension UITapGestureRecognizer { /// Determines if the touch event has a action attribute within the range given /// - Parameters: - /// - label: UILabel in question + /// - label: Label in question /// - targetRange: Range to look within /// - Returns: Wether the range in the label has an action - public func didTapActionInLabel(_ label: UILabel, inRange targetRange: NSRange) -> Bool { - - guard let attributedText = label.attributedText else { return false } - - let layoutManager = NSLayoutManager() - let textContainer = NSTextContainer(size: label.bounds.size) - let textStorage = NSTextStorage(attributedString: attributedText) - layoutManager.addTextContainer(textContainer) - textStorage.addLayoutManager(layoutManager) - - let location = location(in: label) - let characterIndex = layoutManager.characterIndex(for: location, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil) - - guard let _ = attributedText.attribute(NSAttributedString.Key.action, at: characterIndex, effectiveRange: nil) as? String, characterIndex < attributedText.length else { return false } - return true + public func didTapActionInLabel(_ label: Label, inRange targetRange: NSRange) -> Bool { + let tapLocation = location(in: label) + return label.isAction(for: tapLocation, inRange: targetRange) } } From e7f5d4ee94699689c8c5b594161df7ccfc20be99 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 21 Jun 2024 13:21:36 -0500 Subject: [PATCH 45/83] removed code not needed. Signed-off-by: Matt Bruce --- VDS/Components/Label/Label.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/VDS/Components/Label/Label.swift b/VDS/Components/Label/Label.swift index 78d34574..c8427f2e 100644 --- a/VDS/Components/Label/Label.swift +++ b/VDS/Components/Label/Label.swift @@ -404,7 +404,7 @@ open class Label: UILabel, ViewProtocol, UserInfoable { } public func isAction(for location: CGPoint, inRange targetRange: NSRange) -> Bool { - guard let attributedText = attributedText, let abstractContainer = abstractTextContainer() else { return false } + guard let abstractContainer = abstractTextContainer() else { return false } let textContainer = abstractContainer.textContainer let layoutManager = abstractContainer.layoutManager @@ -472,7 +472,7 @@ open class Label: UILabel, ViewProtocol, UserInfoable { //-------------------------------------------------- private func customAccessibilityElement(text: String?, range: NSRange, accessibleText: String? = nil) -> AccessibilityActionElement? { - guard let text = text, let attributedText, let abstractContainer = abstractTextContainer() else { return nil } + guard let text = text, let abstractContainer = abstractTextContainer() else { return nil } let textContainer = abstractContainer.textContainer let layoutManager = abstractContainer.layoutManager From aad70d2e409aadc36bc0ea84b68541902dc6d26f Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 21 Jun 2024 13:30:19 -0500 Subject: [PATCH 46/83] testing not using popover viewcontroller Signed-off-by: Matt Bruce --- VDS/Classes/ClearPopoverViewController.swift | 13 +- VDS/Components/DatePicker/DatePicker.swift | 204 ++++++++++++++++--- 2 files changed, 182 insertions(+), 35 deletions(-) diff --git a/VDS/Classes/ClearPopoverViewController.swift b/VDS/Classes/ClearPopoverViewController.swift index 33531a75..6f9bcb67 100644 --- a/VDS/Classes/ClearPopoverViewController.swift +++ b/VDS/Classes/ClearPopoverViewController.swift @@ -19,6 +19,10 @@ open class ClearPopoverViewController: UIViewController, UIPopoverPresentationCo /// Popover presentation controller of the popover private var popOver: UIPopoverPresentationController! + open var maxWidth: CGFloat? + + open var sourceRect: CGRect? + open var spacing: CGFloat = 0 /** A controller that manages the popover. @@ -35,6 +39,7 @@ open class ClearPopoverViewController: UIViewController, UIPopoverPresentationCo self.contentView = contentView self.spacing = spacing self.arrow = arrow + self.sourceRect = sourceRect super.init(nibName: nil, bundle: nil) setupPopover(sourceView, sourceRect, barButtonItem) } @@ -65,7 +70,6 @@ open class ClearPopoverViewController: UIViewController, UIPopoverPresentationCo popOver.popoverBackgroundViewClass = ClearPopoverBackgroundView.self popOver.sourceView = sourceView popOver.popoverLayoutMargins = .zero - if let sourceRect = sourceRect { popOver.sourceRect = sourceRect } @@ -85,11 +89,8 @@ open class ClearPopoverViewController: UIViewController, UIPopoverPresentationCo private func updatePopoverPosition() { guard let popoverPresentationController = popoverPresentationController else { return } - if let sourceView = popoverPresentationController.sourceView { - popoverPresentationController.sourceRect = .init(x: sourceView.bounds.origin.x, - y: sourceView.bounds.origin.y, - width: sourceView.bounds.width, - height: sourceView.bounds.height + spacing) + if let sourceView = popoverPresentationController.sourceView, let sourceRect { + popoverPresentationController.sourceRect = sourceRect } } diff --git a/VDS/Components/DatePicker/DatePicker.swift b/VDS/Components/DatePicker/DatePicker.swift index 0e985176..3ab83be1 100644 --- a/VDS/Components/DatePicker/DatePicker.swift +++ b/VDS/Components/DatePicker/DatePicker.swift @@ -117,10 +117,11 @@ open class DatePicker: EntryFieldBase { NotificationCenter.default .publisher(for: UIDevice.orientationDidChangeNotification).sink { [weak self] _ in - guard let self, let popoverController else { return } - popoverController.dismiss(animated: true){ [weak self] in + guard let self else { return } + popoverController?.dismiss(animated: true){ [weak self] in guard let self else { return } } + hidePopoverView() } .store(in: &subscribers) @@ -163,7 +164,7 @@ open class DatePicker: EntryFieldBase { selectedDateLabel.text = formatter.string(from: date) } - internal var popoverController: UIViewController? + internal var popoverController: ClearPopoverViewController? func didSelect(_ date: Date) { selectedDate = date @@ -175,34 +176,179 @@ open class DatePicker: EntryFieldBase { } } - internal func togglePicker() { - let calendar = CalendarBase() - calendar.activeDates = calendarModel.activeDates - calendar.hideContainerBorder = calendarModel.hideContainerBorder - calendar.hideCurrentDateIndicator = calendarModel.hideCurrentDateIndicator - calendar.inactiveDates = calendarModel.inactiveDates - calendar.indicators = calendarModel.indicators - calendar.maxDate = calendarModel.maxDate - calendar.minDate = calendarModel.minDate - calendar.surface = calendarModel.surface - calendar.setNeedsLayout() - calendar.layoutIfNeeded() - calendar.onChange = { [weak self] control in - guard let self else { return } - didSelect(control.selectedDate) - } + private var overlayView = UIView().with { + $0.backgroundColor = .clear; + $0.isHidden = true + } + private var popoverView: UIView! + private var popoverVisible = false + private var outsideTapGesture: UITapGestureRecognizer? + private var outsidePanGesture: UIPanGestureRecognizer? + +// internal func togglePicker() { +// calendar.activeDates = calendarModel.activeDates +// calendar.hideContainerBorder = calendarModel.hideContainerBorder +// calendar.hideCurrentDateIndicator = calendarModel.hideCurrentDateIndicator +// calendar.inactiveDates = calendarModel.inactiveDates +// calendar.indicators = calendarModel.indicators +// calendar.maxDate = calendarModel.maxDate +// calendar.minDate = calendarModel.minDate +// calendar.surface = calendarModel.surface +// calendar.setNeedsLayout() +// calendar.layoutIfNeeded() +// calendar.onChange = { [weak self] control in +// guard let self else { return } +// didSelect(control.selectedDate) +// } +// +// popoverController = ClearPopoverViewController(contentView: calendar, +// arrow: .any, +// sourceView: containerView, +// sourceRect: .init(x: 0, y: 0, width: 320, height: 45), +// spacing: VDSLayout.space1X) +// popoverController?.maxWidth = 320 +// if let viewController = UIApplication.topViewController(), let popoverController { +// viewController.present(popoverController, +// animated: true, +// completion: nil) +// } +// } +} + +extension DatePicker { + + private func togglePicker() { + guard let viewController = UIApplication.topViewController(), let parentView = viewController.view else { return } + + if popoverVisible { + hidePopoverView() + } else { + let calendar = CalendarBase() + calendar.activeDates = calendarModel.activeDates + calendar.hideContainerBorder = calendarModel.hideContainerBorder + calendar.hideCurrentDateIndicator = calendarModel.hideCurrentDateIndicator + calendar.inactiveDates = calendarModel.inactiveDates + calendar.indicators = calendarModel.indicators + calendar.maxDate = calendarModel.maxDate + calendar.minDate = calendarModel.minDate + calendar.surface = calendarModel.surface + calendar.setNeedsLayout() + calendar.layoutIfNeeded() + calendar.onChange = { [weak self] control in + guard let self else { return } + selectedDate = control.selectedDate + sendActions(for: .valueChanged) + UIAccessibility.post(notification: .layoutChanged, argument: containerView) + hidePopoverView() + } + + outsideTapGesture = UITapGestureRecognizer() + outsidePanGesture = UIPanGestureRecognizer() + + overlayView.publisher(for: outsideTapGesture!).sink { [weak self] _ in + guard let self else { return } + hidePopoverView() + }.store(in: &subscribers) + parentView.publisher(for: outsidePanGesture!).sink { [weak self] _ in + guard let self else { return } + hidePopoverView() + }.store(in: &subscribers) + + overlayView.frame = parentView.bounds + overlayView.isHidden = false + + parentView.addSubview(overlayView) + + popoverView = UIView() + popoverView.backgroundColor = .white + popoverView.layer.cornerRadius = 10 + popoverView.layer.shadowColor = UIColor.black.cgColor + popoverView.layer.shadowOpacity = 0.2 + popoverView.layer.shadowOffset = CGSize(width: 0, height: 5) + popoverView.layer.shadowRadius = 10 + popoverView.isHidden = true + popoverView.addSubview(calendar) + calendar.pinToSuperView() + popoverView.translatesAutoresizingMaskIntoConstraints = false + parentView.addSubview(popoverView) + + popoverView.width(calendar.frame.width) + popoverView.height(calendar.frame.height) + + let spacing: CGFloat = 4 + let (popoverX, popoverY) = calculatePopoverPosition(relativeTo: containerView, in: parentView, size: calendar.frame.size, with: spacing) + popoverView.leadingAnchor.constraint(equalTo: parentView.leadingAnchor, constant: popoverX).isActive = true + popoverView.topAnchor.constraint(equalTo: parentView.topAnchor, constant: popoverY).isActive = true + + parentView.layoutIfNeeded() + popoverView.alpha = 0 + popoverView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9) + popoverView.isHidden = false + popoverVisible = true - popoverController = ClearPopoverViewController(contentView: calendar, - arrow: .up, - sourceView: containerView, - sourceRect: containerView.bounds, - spacing: VDSLayout.space1X) - - if let viewController = UIApplication.topViewController(), let popoverController { - viewController.present(popoverController, - animated: true, - completion: nil) + UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.2, options: .curveEaseOut, animations: { [weak self] in + guard let self else { return } + popoverView.alpha = 1 + popoverView.transform = CGAffineTransform.identity + parentView.layoutIfNeeded() + }) } } + + private func hidePopoverView() { + overlayView.isHidden = true + overlayView.removeFromSuperview() + + outsideTapGesture = nil + outsidePanGesture = nil + UIView.animate(withDuration: 0.2, animations: { + self.popoverView.alpha = 0 + self.popoverView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9) + }) { _ in + self.popoverView.isHidden = true + self.popoverView.removeFromSuperview() + self.popoverVisible = false + } + } + + private func calculatePopoverPosition(relativeTo sourceView: UIView, in parentView: UIView, size: CGSize, with spacing: CGFloat) -> (CGFloat, CGFloat) { + let sourceFrameInParent = sourceView.convert(sourceView.bounds, to: parentView) + let parentBounds = parentView.bounds + let popoverWidth: CGFloat = size.width + let popoverHeight: CGFloat = size.height + + var popoverX: CGFloat = 0 + var popoverY: CGFloat = 0 + + // Calculate horizontal position + if sourceFrameInParent.width < popoverWidth { + if sourceFrameInParent.midX - popoverWidth / 2 < 0 { + // Align to left + popoverX = sourceFrameInParent.minX + } else if sourceFrameInParent.midX + popoverWidth / 2 > parentBounds.width { + // Align to right + popoverX = sourceFrameInParent.maxX - popoverWidth + } else { + // Center on source view + popoverX = sourceFrameInParent.midX - popoverWidth / 2 + } + } else { + popoverX = sourceFrameInParent.midX - popoverWidth / 2 + } + + // Calculate vertical position + if sourceFrameInParent.origin.y > parentBounds.height / 2 { + // Show above + popoverY = sourceFrameInParent.minY - popoverHeight - spacing + } else { + // Show below + popoverY = sourceFrameInParent.maxY + spacing + } + + // Ensure the popover is within the parent's bounds + popoverX = max(0, min(popoverX, parentBounds.width - popoverWidth)) + + return (popoverX, popoverY) + } } From eaf6d68ab7f426baf4bd376ce5fb939e22f696d2 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 21 Jun 2024 14:10:16 -0500 Subject: [PATCH 47/83] reverted to using kevin's old code for actions Signed-off-by: Matt Bruce --- VDS/Components/Label/Label.swift | 30 +++++++++++++ VDS/Extensions/UITapGestureRecognizer.swift | 47 +++++++++++++++------ 2 files changed, 63 insertions(+), 14 deletions(-) diff --git a/VDS/Components/Label/Label.swift b/VDS/Components/Label/Label.swift index 4f56c272..eafd440a 100644 --- a/VDS/Components/Label/Label.swift +++ b/VDS/Components/Label/Label.swift @@ -402,6 +402,36 @@ open class Label: UILabel, ViewProtocol, UserInfoable { } } + /** + Provides a text container and layout manager of how the text would appear on screen. + They are used in tandem to derive low-level TextKit results of the label. + */ + public func abstractTextContainer() -> (textContainer: NSTextContainer, layoutManager: NSLayoutManager, textStorage: NSTextStorage)? { + + // Must configure the attributed string to translate what would appear on screen to accurately analyze. + guard let attributedText = attributedText else { return nil } + + let paragraph = NSMutableParagraphStyle() + paragraph.alignment = textAlignment + + let stagedAttributedString = NSMutableAttributedString(attributedString: attributedText) + stagedAttributedString.addAttributes([NSAttributedString.Key.paragraphStyle: paragraph], range: NSRange(location: 0, length: attributedText.string.count)) + + let textStorage = NSTextStorage(attributedString: stagedAttributedString) + let layoutManager = NSLayoutManager() + let textContainer = NSTextContainer(size: .zero) + + layoutManager.addTextContainer(textContainer) + textStorage.addLayoutManager(layoutManager) + + textContainer.lineFragmentPadding = 0.0 + textContainer.lineBreakMode = lineBreakMode + textContainer.maximumNumberOfLines = numberOfLines + textContainer.size = bounds.size + + return (textContainer, layoutManager, textStorage) + } + private func customAccessibilityAction(text: String?, range: NSRange, accessibleText: String? = nil) -> UIAccessibilityCustomAction? { guard let text = text, let attributedText else { return nil } diff --git a/VDS/Extensions/UITapGestureRecognizer.swift b/VDS/Extensions/UITapGestureRecognizer.swift index 612a11ca..93fd0d8f 100644 --- a/VDS/Extensions/UITapGestureRecognizer.swift +++ b/VDS/Extensions/UITapGestureRecognizer.swift @@ -15,20 +15,39 @@ extension UITapGestureRecognizer { /// - label: UILabel in question /// - targetRange: Range to look within /// - Returns: Wether the range in the label has an action - public func didTapActionInLabel(_ label: UILabel, inRange targetRange: NSRange) -> Bool { - - guard let attributedText = label.attributedText else { return false } - - let layoutManager = NSLayoutManager() - let textContainer = NSTextContainer(size: label.bounds.size) - let textStorage = NSTextStorage(attributedString: attributedText) - layoutManager.addTextContainer(textContainer) - textStorage.addLayoutManager(layoutManager) - + public func didTapActionInLabel(_ label: Label, inRange targetRange: NSRange) -> Bool { + guard let abstractContainer = label.abstractTextContainer() else { return false } let location = location(in: label) - let characterIndex = layoutManager.characterIndex(for: location, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil) - - guard let _ = attributedText.attribute(NSAttributedString.Key.action, at: characterIndex, effectiveRange: nil) as? String, characterIndex < attributedText.length else { return false } - return true + + let textContainer = abstractContainer.textContainer + let layoutManager = abstractContainer.layoutManager + + let indexOfGlyph = layoutManager.glyphIndex(for: location, in: textContainer) + let intrinsicWidth = label.intrinsicContentSize.width + + // Assert that tapped occured within acceptable bounds based on alignment. + switch label.textAlignment { + case .right: + if location.x < label.bounds.width - intrinsicWidth { + return false + } + case .center: + let halfBounds = label.bounds.width / 2 + let halfIntrinsicWidth = intrinsicWidth / 2 + + if location.x > halfBounds + halfIntrinsicWidth { + return false + } else if location.x < halfBounds - halfIntrinsicWidth { + return false + } + default: // Left align + if location.x > intrinsicWidth { + return false + } + } + + // Affirms that the tap occured in the desired rect of provided by the target range. + return layoutManager.boundingRect(forGlyphRange: targetRange, in: textContainer).contains(location) + && NSLocationInRange(indexOfGlyph, targetRange) } } From beaa2b3a82ba33afb79cc6614edc48c4f865eb6a Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 21 Jun 2024 14:43:55 -0500 Subject: [PATCH 48/83] refactored out old code and reorganized Signed-off-by: Matt Bruce --- VDS/Components/DatePicker/DatePicker.swift | 79 ++++++++-------------- 1 file changed, 27 insertions(+), 52 deletions(-) diff --git a/VDS/Components/DatePicker/DatePicker.swift b/VDS/Components/DatePicker/DatePicker.swift index 3ab83be1..e44c2a76 100644 --- a/VDS/Components/DatePicker/DatePicker.swift +++ b/VDS/Components/DatePicker/DatePicker.swift @@ -31,6 +31,14 @@ open class DatePicker: EntryFieldBase { // MARK: - Private Properties //-------------------------------------------------- internal var minWidthDefault = 186.0 + internal var popoverView: UIView! + internal var popoverVisible = false + internal var outsideTapGesture: UITapGestureRecognizer? + internal var outsidePanGesture: UIPanGestureRecognizer? + internal var overlayView = UIView().with { + $0.backgroundColor = .clear; + $0.isHidden = true + } internal var bottomStackView: UIStackView = { return UIStackView().with { @@ -109,8 +117,8 @@ open class DatePicker: EntryFieldBase { .publisher(for: UITapGestureRecognizer()) .sink { [weak self] _ in guard let self else { return } - if self.isEnabled && !self.isReadOnly { - self.togglePicker() + if isEnabled && !isReadOnly { + showPopover() } } .store(in: &subscribers) @@ -118,9 +126,6 @@ open class DatePicker: EntryFieldBase { NotificationCenter.default .publisher(for: UIDevice.orientationDidChangeNotification).sink { [weak self] _ in guard let self else { return } - popoverController?.dismiss(animated: true){ [weak self] in - guard let self else { return } - } hidePopoverView() } .store(in: &subscribers) @@ -175,49 +180,11 @@ open class DatePicker: EntryFieldBase { popoverController = nil } } - - private var overlayView = UIView().with { - $0.backgroundColor = .clear; - $0.isHidden = true - } - private var popoverView: UIView! - private var popoverVisible = false - private var outsideTapGesture: UITapGestureRecognizer? - private var outsidePanGesture: UIPanGestureRecognizer? - -// internal func togglePicker() { -// calendar.activeDates = calendarModel.activeDates -// calendar.hideContainerBorder = calendarModel.hideContainerBorder -// calendar.hideCurrentDateIndicator = calendarModel.hideCurrentDateIndicator -// calendar.inactiveDates = calendarModel.inactiveDates -// calendar.indicators = calendarModel.indicators -// calendar.maxDate = calendarModel.maxDate -// calendar.minDate = calendarModel.minDate -// calendar.surface = calendarModel.surface -// calendar.setNeedsLayout() -// calendar.layoutIfNeeded() -// calendar.onChange = { [weak self] control in -// guard let self else { return } -// didSelect(control.selectedDate) -// } -// -// popoverController = ClearPopoverViewController(contentView: calendar, -// arrow: .any, -// sourceView: containerView, -// sourceRect: .init(x: 0, y: 0, width: 320, height: 45), -// spacing: VDSLayout.space1X) -// popoverController?.maxWidth = 320 -// if let viewController = UIApplication.topViewController(), let popoverController { -// viewController.present(popoverController, -// animated: true, -// completion: nil) -// } -// } } extension DatePicker { - private func togglePicker() { + private func showPopover() { guard let viewController = UIApplication.topViewController(), let parentView = viewController.view else { return } if popoverVisible { @@ -286,7 +253,12 @@ extension DatePicker { popoverView.isHidden = false popoverVisible = true - UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.2, options: .curveEaseOut, animations: { [weak self] in + UIView.animate(withDuration: 0.3, + delay: 0, + usingSpringWithDamping: 0.8, + initialSpringVelocity: 0.2, + options: .curveEaseOut, + animations: { [weak self] in guard let self else { return } popoverView.alpha = 1 popoverView.transform = CGAffineTransform.identity @@ -301,13 +273,16 @@ extension DatePicker { outsideTapGesture = nil outsidePanGesture = nil - UIView.animate(withDuration: 0.2, animations: { - self.popoverView.alpha = 0 - self.popoverView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9) - }) { _ in - self.popoverView.isHidden = true - self.popoverView.removeFromSuperview() - self.popoverVisible = false + UIView.animate(withDuration: 0.2, + animations: {[weak self] in + guard let self else { return } + popoverView.alpha = 0 + popoverView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9) + }) { [weak self] _ in + guard let self else { return } + popoverView.isHidden = true + popoverView.removeFromSuperview() + popoverVisible = false } } From 891a816f557dbc0e16932853c5cb7fe190f3d4eb Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Mon, 24 Jun 2024 15:33:47 -0500 Subject: [PATCH 49/83] refactored base for responder Signed-off-by: Matt Bruce --- VDS/Components/DropdownSelect/DropdownSelect.swift | 14 +------------- VDS/Components/TextFields/EntryFieldBase.swift | 3 +++ .../TextFields/InputField/InputField.swift | 6 +----- VDS/Components/TextFields/TextArea/TextArea.swift | 13 +------------ 4 files changed, 6 insertions(+), 30 deletions(-) diff --git a/VDS/Components/DropdownSelect/DropdownSelect.swift b/VDS/Components/DropdownSelect/DropdownSelect.swift index 7fc52808..832349d2 100644 --- a/VDS/Components/DropdownSelect/DropdownSelect.swift +++ b/VDS/Components/DropdownSelect/DropdownSelect.swift @@ -30,19 +30,7 @@ open class DropdownSelect: EntryFieldBase { //-------------------------------------------------- // MARK: - Public Properties - //-------------------------------------------------- - /// Override UIControl state to add the .error state if showSuccess is true and if showError is true. - open override var state: UIControl.State { - get { - var state = super.state - if dropdownField.isFirstResponder { - state.insert(.focused) - } - - return state - } - } - + //-------------------------------------------------- /// If true, the label will be displayed inside the dropdown containerView. Otherwise, the label will be above the dropdown containerView like a normal text input. open var showInlineLabel: Bool = false { didSet { setNeedsUpdate() }} diff --git a/VDS/Components/TextFields/EntryFieldBase.swift b/VDS/Components/TextFields/EntryFieldBase.swift index 2ef89480..e4aeddc2 100644 --- a/VDS/Components/TextFields/EntryFieldBase.swift +++ b/VDS/Components/TextFields/EntryFieldBase.swift @@ -207,6 +207,9 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { if isReadOnly { state.insert(.readonly) } + if let responder, responder.isFirstResponder { + state.insert(.focused) + } } return state } diff --git a/VDS/Components/TextFields/InputField/InputField.swift b/VDS/Components/TextFields/InputField/InputField.swift index c2f779d4..2ca4c207 100644 --- a/VDS/Components/TextFields/InputField/InputField.swift +++ b/VDS/Components/TextFields/InputField/InputField.swift @@ -166,11 +166,7 @@ open class InputField: EntryFieldBase { if showSuccess { state.insert(.success) } - - if textField.isFirstResponder { - state.insert(.focused) - } - + return state } } diff --git a/VDS/Components/TextFields/TextArea/TextArea.swift b/VDS/Components/TextFields/TextArea/TextArea.swift index a487dc53..d0396d7a 100644 --- a/VDS/Components/TextFields/TextArea/TextArea.swift +++ b/VDS/Components/TextFields/TextArea/TextArea.swift @@ -56,18 +56,7 @@ open class TextArea: EntryFieldBase { //-------------------------------------------------- // MARK: - Public Properties - //-------------------------------------------------- - /// Override UIControl state to add the .error state if showSuccess is true and if showError is true. - open override var state: UIControl.State { - get { - var state = super.state - if textView.isFirstResponder { - state.insert(.focused) - } - return state - } - } - + //-------------------------------------------------- override var containerSize: CGSize { CGSize(width: 182, height: Height.twoX.value) } /// Enum used to describe the the height of TextArea. From 32a44a51b5dbd967f80d0584a1e88b56aa4874f0 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Mon, 24 Jun 2024 15:34:02 -0500 Subject: [PATCH 50/83] updated date picker Signed-off-by: Matt Bruce --- VDS/Components/DatePicker/DatePicker.swift | 106 ++++++++++++--------- 1 file changed, 59 insertions(+), 47 deletions(-) diff --git a/VDS/Components/DatePicker/DatePicker.swift b/VDS/Components/DatePicker/DatePicker.swift index e44c2a76..991167e1 100644 --- a/VDS/Components/DatePicker/DatePicker.swift +++ b/VDS/Components/DatePicker/DatePicker.swift @@ -30,8 +30,10 @@ open class DatePicker: EntryFieldBase { //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- + internal override var responder: UIResponder? { hiddenView } + internal var hiddenView = UITextView().with { $0.width(0) } internal var minWidthDefault = 186.0 - internal var popoverView: UIView! + internal var popoverView: UIScrollView! internal var popoverVisible = false internal var outsideTapGesture: UITapGestureRecognizer? internal var outsidePanGesture: UIPanGestureRecognizer? @@ -141,6 +143,7 @@ open class DatePicker: EntryFieldBase { } controlStackView.addArrangedSubview(calendarIcon) controlStackView.addArrangedSubview(selectedDateLabel) + controlStackView.addArrangedSubview(hiddenView) return controlStackView } @@ -168,25 +171,11 @@ open class DatePicker: EntryFieldBase { formatter.dateFormat = dateFormat.format selectedDateLabel.text = formatter.string(from: date) } - - internal var popoverController: ClearPopoverViewController? - - func didSelect(_ date: Date) { - selectedDate = date - sendActions(for: .valueChanged) - UIAccessibility.post(notification: .layoutChanged, argument: self.containerView) - popoverController?.dismiss(animated: true){ [weak self] in - guard let self else { return } - popoverController = nil - } - } } extension DatePicker { - private func showPopover() { guard let viewController = UIApplication.topViewController(), let parentView = viewController.view else { return } - if popoverVisible { hidePopoverView() } else { @@ -226,34 +215,33 @@ extension DatePicker { parentView.addSubview(overlayView) - popoverView = UIView() - popoverView.backgroundColor = .white - popoverView.layer.cornerRadius = 10 - popoverView.layer.shadowColor = UIColor.black.cgColor - popoverView.layer.shadowOpacity = 0.2 - popoverView.layer.shadowOffset = CGSize(width: 0, height: 5) - popoverView.layer.shadowRadius = 10 + popoverView = UIScrollView() + popoverView.backgroundColor = .green + popoverView.clipsToBounds = true + popoverView.backgroundColor = .clear popoverView.isHidden = true popoverView.addSubview(calendar) calendar.pinToSuperView() - popoverView.translatesAutoresizingMaskIntoConstraints = false parentView.addSubview(popoverView) - popoverView.width(calendar.frame.width) - popoverView.height(calendar.frame.height) let spacing: CGFloat = 4 - let (popoverX, popoverY) = calculatePopoverPosition(relativeTo: containerView, in: parentView, size: calendar.frame.size, with: spacing) - popoverView.leadingAnchor.constraint(equalTo: parentView.leadingAnchor, constant: popoverX).isActive = true - popoverView.topAnchor.constraint(equalTo: parentView.topAnchor, constant: popoverY).isActive = true + let popoverSize: CGSize = .init(width: calendar.frame.width, height: calendar.frame.height) + popoverView.contentSize = CGSize(width: popoverSize.width, height: popoverSize.height) + + let (popoverX, popoverY, adjustedHeight) = calculatePopoverPosition(relativeTo: containerView, in: parentView, size: popoverSize, with: spacing) + //let adjustedX = adjustedHeight != popoverSize.height ? popoverX - 10 : popoverX + let adjustedWidth = adjustedHeight != popoverSize.height ? popoverSize.width + 10 : popoverSize.width + popoverView.frame = CGRect(x: popoverX, y: popoverY, width: adjustedWidth, height: adjustedHeight) parentView.layoutIfNeeded() popoverView.alpha = 0 popoverView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9) popoverView.isHidden = false popoverVisible = true - - UIView.animate(withDuration: 0.3, + _ = responder?.becomeFirstResponder() + updateContainerView() + UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.2, @@ -262,6 +250,10 @@ extension DatePicker { guard let self else { return } popoverView.alpha = 1 popoverView.transform = CGAffineTransform.identity + if popoverSize.height > adjustedHeight { + popoverView.flashScrollIndicators() + } + UIAccessibility.post(notification: .layoutChanged, argument: calendar) parentView.layoutIfNeeded() }) } @@ -270,10 +262,9 @@ extension DatePicker { private func hidePopoverView() { overlayView.isHidden = true overlayView.removeFromSuperview() - outsideTapGesture = nil outsidePanGesture = nil - UIView.animate(withDuration: 0.2, + UIView.animate(withDuration: 0.2, animations: {[weak self] in guard let self else { return } popoverView.alpha = 0 @@ -283,17 +274,23 @@ extension DatePicker { popoverView.isHidden = true popoverView.removeFromSuperview() popoverVisible = false + responder?.resignFirstResponder() + setNeedsUpdate() + UIAccessibility.post(notification: .layoutChanged, argument: containerView) } + } - - private func calculatePopoverPosition(relativeTo sourceView: UIView, in parentView: UIView, size: CGSize, with spacing: CGFloat) -> (CGFloat, CGFloat) { + + private func calculatePopoverPosition(relativeTo sourceView: UIView, in parentView: UIView, size: CGSize, with spacing: CGFloat) -> (CGFloat, CGFloat, CGFloat) { let sourceFrameInParent = sourceView.convert(sourceView.bounds, to: parentView) let parentBounds = parentView.bounds - let popoverWidth: CGFloat = size.width - let popoverHeight: CGFloat = size.height + let safeAreaInsets = parentView.safeAreaInsets + let popoverWidth = size.width + let popoverHeight = size.height var popoverX: CGFloat = 0 var popoverY: CGFloat = 0 + var adjustedHeight = popoverHeight // Calculate horizontal position if sourceFrameInParent.width < popoverWidth { @@ -311,19 +308,34 @@ extension DatePicker { popoverX = sourceFrameInParent.midX - popoverWidth / 2 } - // Calculate vertical position - if sourceFrameInParent.origin.y > parentBounds.height / 2 { - // Show above - popoverY = sourceFrameInParent.minY - popoverHeight - spacing - } else { - // Show below - popoverY = sourceFrameInParent.maxY + spacing - } - - // Ensure the popover is within the parent's bounds + // Ensure the popover is within the parent's bounds horizontally popoverX = max(0, min(popoverX, parentBounds.width - popoverWidth)) - return (popoverX, popoverY) + // Calculate vertical position and height + let availableSpaceAbove = sourceFrameInParent.minY - safeAreaInsets.top - spacing + let availableSpaceBelow = parentBounds.height - sourceFrameInParent.maxY - safeAreaInsets.bottom - spacing + let totalAvailableHeight = parentBounds.height - safeAreaInsets.top - safeAreaInsets.bottom + + if availableSpaceAbove >= popoverHeight { + // Show above without adjusting height + popoverY = sourceFrameInParent.minY - popoverHeight - spacing + } else if availableSpaceBelow >= popoverHeight { + // Show below without adjusting height + popoverY = sourceFrameInParent.maxY + spacing + + } else if totalAvailableHeight >= popoverHeight { + // check if the total + if availableSpaceAbove > availableSpaceBelow { + popoverY = safeAreaInsets.top + } else { + popoverY = parentBounds.height - safeAreaInsets.bottom - popoverHeight + } + } else { + popoverY = safeAreaInsets.top + adjustedHeight = totalAvailableHeight + } + + return (popoverX, popoverY, adjustedHeight) } } From 7cd9a7d1a93c0ed9b10d14e97a3006b08b9cec43 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 25 Jun 2024 11:08:28 -0500 Subject: [PATCH 51/83] added alertViewController Signed-off-by: Matt Bruce --- VDS.xcodeproj/project.pbxproj | 4 + VDS/Classes/AlertViewController.swift | 101 ++++++++++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 VDS/Classes/AlertViewController.swift diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index 7c9565f7..bf8fbc94 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -174,6 +174,7 @@ EAF1FE9B29DB1A6000101452 /* Changeable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF1FE9A29DB1A6000101452 /* Changeable.swift */; }; EAF2F4762C231EAA007BFEDC /* AccessibilityActionElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */; }; EAF2F4782C249D72007BFEDC /* AccessibilityUpdatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF2F4772C249D72007BFEDC /* AccessibilityUpdatable.swift */; }; + EAF2F4892C2A1075007BFEDC /* AlertViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF2F4882C2A1075007BFEDC /* AlertViewController.swift */; }; EAF7F0952899861000B287F5 /* CheckboxItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0932899861000B287F5 /* CheckboxItem.swift */; }; EAF7F09A2899B17200B287F5 /* CATransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0992899B17200B287F5 /* CATransaction.swift */; }; EAF7F0A0289AB7EC00B287F5 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F09F289AB7EC00B287F5 /* View.swift */; }; @@ -402,6 +403,7 @@ EAF1FE9A29DB1A6000101452 /* Changeable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Changeable.swift; sourceTree = ""; }; EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityActionElement.swift; sourceTree = ""; }; EAF2F4772C249D72007BFEDC /* AccessibilityUpdatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityUpdatable.swift; sourceTree = ""; }; + EAF2F4882C2A1075007BFEDC /* AlertViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertViewController.swift; sourceTree = ""; }; EAF7F0932899861000B287F5 /* CheckboxItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckboxItem.swift; sourceTree = ""; }; EAF7F0992899B17200B287F5 /* CATransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CATransaction.swift; sourceTree = ""; }; EAF7F09F289AB7EC00B287F5 /* View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = ""; }; @@ -750,6 +752,7 @@ EAF7F0B8289C139800B287F5 /* ColorConfiguration.swift */, EAB5FEF02927F4AA00998C17 /* SelfSizingCollectionView.swift */, EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */, + EAF2F4882C2A1075007BFEDC /* AlertViewController.swift */, ); path = Classes; sourceTree = ""; @@ -1310,6 +1313,7 @@ EA0D1C3B2A6AD51B00E5C127 /* Typogprahy+Styles.swift in Sources */, EAF7F09A2899B17200B287F5 /* CATransaction.swift in Sources */, EAC58C162BED0E0300BA39FA /* InlineAction.swift in Sources */, + EAF2F4892C2A1075007BFEDC /* AlertViewController.swift in Sources */, EA0D1C3D2A6AD57600E5C127 /* Typography+Enums.swift in Sources */, EAF1FE9B29DB1A6000101452 /* Changeable.swift in Sources */, EAC58C0C2BED01D500BA39FA /* Telephone.swift in Sources */, diff --git a/VDS/Classes/AlertViewController.swift b/VDS/Classes/AlertViewController.swift new file mode 100644 index 00000000..71a1c6d9 --- /dev/null +++ b/VDS/Classes/AlertViewController.swift @@ -0,0 +1,101 @@ +// +// AlertViewController.swift +// VDS +// +// Created by Matt Bruce on 6/24/24. +// + +import Foundation +import UIKit +import Combine +import VDSCoreTokens + +open class AlertViewController: UIViewController, Surfaceable { + + /// Set of Subscribers for any Publishers for this Control. + open var subscribers = Set() + + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- + private var onClickSubscriber: AnyCancellable? { + willSet { + if let onClickSubscriber { + onClickSubscriber.cancel() + } + } + } + + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + /// Current Surface and this is used to pass down to child objects that implement Surfacable + open var surface: Surface = .light { didSet { updateView() }} + open var presenter: UIView? { didSet { updateView() }} + open var dialog: UIView! + + //-------------------------------------------------- + // MARK: - Configuration + //-------------------------------------------------- + private let backgroundColorConfiguration = SurfaceColorConfiguration(VDSColor.backgroundPrimaryDark, VDSColor.backgroundPrimaryLight) + + //-------------------------------------------------- + // MARK: - Lifecycle + //-------------------------------------------------- + open override func viewDidLoad() { + super.viewDidLoad() + isModalInPresentation = true + setup() + } + open override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + UIAccessibility.post(notification: .screenChanged, argument: dialog) + } + + private func dismiss() { + dismiss(animated: true) { [weak self] in + guard let self, let presenter else { return } + UIAccessibility.post(notification: .layoutChanged, argument: presenter) + } + } + + open func setup() { + guard let dialog else { return } + view.accessibilityElements = [dialog] + + //left-right swipe + view.publisher(for: UISwipeGestureRecognizer().with{ $0.direction = .right }) + .sink { [weak self] swipe in + guard let self, !UIAccessibility.isVoiceOverRunning else { return } + self.dismiss() + }.store(in: &subscribers) + + //tapping in background + view.publisher(for: UITapGestureRecognizer().with{ $0.numberOfTapsRequired = 1 }) + .sink { [weak self] swipe in + guard let self, !UIAccessibility.isVoiceOverRunning else { return } + self.dismiss() + }.store(in: &subscribers) + + view.addSubview(dialog) + + // Activate constraints + NSLayoutConstraint.activate([ + // Constraints for the floating modal view + dialog.centerXAnchor.constraint(equalTo: view.centerXAnchor), + dialog.centerYAnchor.constraint(equalTo: view.centerYAnchor), + dialog.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor, constant: 10), + dialog.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor, constant: -10), + dialog.topAnchor.constraint(greaterThanOrEqualTo: view.topAnchor, constant: 10), + dialog.bottomAnchor.constraint(lessThanOrEqualTo: view.bottomAnchor, constant: -10) + ]) + } + + /// Used to make changes to the View based off a change events or from local properties. + open func updateView() { + view.backgroundColor = backgroundColorConfiguration.getColor(self).withAlphaComponent(0.3) + if var dialog = dialog as? Surfaceable { + dialog.surface = surface + } + } +} From 8757fe6147a40979bab21603d2f285e80249cab7 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 25 Jun 2024 11:28:40 -0500 Subject: [PATCH 52/83] refactored popover controlling algo Signed-off-by: Matt Bruce --- VDS/Components/DatePicker/DatePicker.swift | 304 +++++++++++++-------- 1 file changed, 183 insertions(+), 121 deletions(-) diff --git a/VDS/Components/DatePicker/DatePicker.swift b/VDS/Components/DatePicker/DatePicker.swift index 991167e1..e8d13b27 100644 --- a/VDS/Components/DatePicker/DatePicker.swift +++ b/VDS/Components/DatePicker/DatePicker.swift @@ -26,22 +26,13 @@ open class DatePicker: EntryFieldBase { //-------------------------------------------------- /// A callback when the selected option changes. Passes parameters (option). open var onDateSelected: ((Date, DatePicker) -> Void)? - + //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- internal override var responder: UIResponder? { hiddenView } internal var hiddenView = UITextView().with { $0.width(0) } internal var minWidthDefault = 186.0 - internal var popoverView: UIScrollView! - internal var popoverVisible = false - internal var outsideTapGesture: UITapGestureRecognizer? - internal var outsidePanGesture: UIPanGestureRecognizer? - internal var overlayView = UIView().with { - $0.backgroundColor = .clear; - $0.isHidden = true - } - internal var bottomStackView: UIStackView = { return UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false @@ -51,6 +42,24 @@ open class DatePicker: EntryFieldBase { $0.spacing = VDSLayout.space2X } }() + + //-------------------------------------------------- + // MARK: - Private Popover/Alert Properties + //-------------------------------------------------- + /// View shown inline + internal var popoverView: UIView! + /// Size used for the popover + internal var popoverViewSize: CGSize = .zero + /// Spacing between the popover and the ContainerView when not a AlertViewController + internal var popoverSpacing: CGFloat = VDSLayout.space1X + /// Whether or not the popover is visible + internal var popoverVisible = false + /// If the ContainerView exists somewhere in the superview hierarch in a ScrollView. + internal var scrollView: UIScrollView? + /// Original Found ScrollView ContentSize, this will get reset back to this size when the Popover is removed. + internal var scrollViewContentSize: CGSize? + /// Presenting ViewController with showing the AlertViewController Version. + internal var topViewController: UIViewController? //-------------------------------------------------- // MARK: - Public Properties @@ -97,12 +106,12 @@ open class DatePicker: EntryFieldBase { } open var dateFormat: DateFormat = .shortNumeric { didSet{ setNeedsUpdate() } } - + //-------------------------------------------------- // MARK: - Configuration Properties //-------------------------------------------------- internal override var containerSize: CGSize { CGSize(width: minWidthDefault, height: 44) } - + //-------------------------------------------------- // MARK: - Overrides //-------------------------------------------------- @@ -110,10 +119,10 @@ open class DatePicker: EntryFieldBase { /// 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() - + // setting color config selectedDateLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable() - + // tap gesture containerView .publisher(for: UITapGestureRecognizer()) @@ -131,7 +140,7 @@ open class DatePicker: EntryFieldBase { hidePopoverView() } .store(in: &subscribers) - + } open override func getFieldContainer() -> UIView { @@ -146,7 +155,7 @@ open class DatePicker: EntryFieldBase { controlStackView.addArrangedSubview(hiddenView) return controlStackView } - + /// Used to make changes to the View based off a change events or from local properties. open override func updateView() { super.updateView() @@ -159,7 +168,7 @@ open class DatePicker: EntryFieldBase { selectedDateLabel.isEnabled = isEnabled calendarIcon.color = iconColorConfiguration.getColor(self) } - + /// Resets to default settings. open override func reset() { super.reset() @@ -172,24 +181,45 @@ open class DatePicker: EntryFieldBase { selectedDateLabel.text = formatter.string(from: date) } } - + extension DatePicker { + private func showPopover() { - guard let viewController = UIApplication.topViewController(), let parentView = viewController.view else { return } - if popoverVisible { + guard let viewController = UIApplication.topViewController(), var parentView = viewController.view, !popoverVisible else { hidePopoverView() - } else { - let calendar = CalendarBase() - calendar.activeDates = calendarModel.activeDates - calendar.hideContainerBorder = calendarModel.hideContainerBorder - calendar.hideCurrentDateIndicator = calendarModel.hideCurrentDateIndicator - calendar.inactiveDates = calendarModel.inactiveDates - calendar.indicators = calendarModel.indicators - calendar.maxDate = calendarModel.maxDate - calendar.minDate = calendarModel.minDate - calendar.surface = calendarModel.surface - calendar.setNeedsLayout() - calendar.layoutIfNeeded() + return + } + + let calendar = CalendarBase() + calendar.activeDates = calendarModel.activeDates + calendar.hideContainerBorder = calendarModel.hideContainerBorder + calendar.hideCurrentDateIndicator = calendarModel.hideCurrentDateIndicator + calendar.inactiveDates = calendarModel.inactiveDates + calendar.indicators = calendarModel.indicators + calendar.maxDate = calendarModel.maxDate + calendar.minDate = calendarModel.minDate + calendar.surface = calendarModel.surface + calendar.setNeedsLayout() + calendar.layoutIfNeeded() + + //size the popover + popoverViewSize = .init(width: calendar.frame.width, height: calendar.frame.height) + + //find scrollView + if scrollView == nil { + scrollView = findScrollView(from: containerView) + scrollViewContentSize = scrollView?.contentSize + } + + if let scrollView { + parentView = scrollView + } + + // see if you should use the popover or show an alert + if let popoverOrigin = try? calculatePopoverPosition(relativeTo: containerView, + in: parentView, + size: popoverViewSize, + with: popoverSpacing) { calendar.onChange = { [weak self] control in guard let self else { return } selectedDate = control.selectedDate @@ -197,51 +227,28 @@ extension DatePicker { UIAccessibility.post(notification: .layoutChanged, argument: containerView) hidePopoverView() } - - outsideTapGesture = UITapGestureRecognizer() - outsidePanGesture = UIPanGestureRecognizer() - - overlayView.publisher(for: outsideTapGesture!).sink { [weak self] _ in - guard let self else { return } - hidePopoverView() - }.store(in: &subscribers) - parentView.publisher(for: outsidePanGesture!).sink { [weak self] _ in - guard let self else { return } - hidePopoverView() - }.store(in: &subscribers) - - overlayView.frame = parentView.bounds - overlayView.isHidden = false - - parentView.addSubview(overlayView) - - popoverView = UIScrollView() - popoverView.backgroundColor = .green - popoverView.clipsToBounds = true + + // popoverView container + popoverView = UIView() popoverView.backgroundColor = .clear - popoverView.isHidden = true - popoverView.addSubview(calendar) - calendar.pinToSuperView() - parentView.addSubview(popoverView) - - - let spacing: CGFloat = 4 - let popoverSize: CGSize = .init(width: calendar.frame.width, height: calendar.frame.height) - popoverView.contentSize = CGSize(width: popoverSize.width, height: popoverSize.height) - - let (popoverX, popoverY, adjustedHeight) = calculatePopoverPosition(relativeTo: containerView, in: parentView, size: popoverSize, with: spacing) - //let adjustedX = adjustedHeight != popoverSize.height ? popoverX - 10 : popoverX - let adjustedWidth = adjustedHeight != popoverSize.height ? popoverSize.width + 10 : popoverSize.width - popoverView.frame = CGRect(x: popoverX, y: popoverY, width: adjustedWidth, height: adjustedHeight) - - parentView.layoutIfNeeded() + popoverView.frame = CGRect(x: popoverOrigin.x, y: popoverOrigin.y, width: calendar.frame.width, height: calendar.frame.height) popoverView.alpha = 0 popoverView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9) - popoverView.isHidden = false popoverVisible = true + popoverView.addSubview(calendar) + + calendar.pinToSuperView() + + // add views + parentView.addSubview(popoverView) + parentView.layoutIfNeeded() + + // update containerview _ = responder?.becomeFirstResponder() updateContainerView() - UIView.animate(withDuration: 0.3, + + // animate the calendar to show + UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.2, @@ -250,48 +257,87 @@ extension DatePicker { guard let self else { return } popoverView.alpha = 1 popoverView.transform = CGAffineTransform.identity - if popoverSize.height > adjustedHeight { - popoverView.flashScrollIndicators() - } UIAccessibility.post(notification: .layoutChanged, argument: calendar) parentView.layoutIfNeeded() }) - } - } - - private func hidePopoverView() { - overlayView.isHidden = true - overlayView.removeFromSuperview() - outsideTapGesture = nil - outsidePanGesture = nil - UIView.animate(withDuration: 0.2, - animations: {[weak self] in - guard let self else { return } - popoverView.alpha = 0 - popoverView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9) - }) { [weak self] _ in - guard let self else { return } - popoverView.isHidden = true - popoverView.removeFromSuperview() - popoverVisible = false - responder?.resignFirstResponder() - setNeedsUpdate() - UIAccessibility.post(notification: .layoutChanged, argument: containerView) + + } else { + let dialog = UIScrollView() + dialog.translatesAutoresizingMaskIntoConstraints = false + dialog.addSubview(calendar) + dialog.backgroundColor = .clear + dialog.contentSize = .init(width: calendar.frame.width + 20, height: calendar.frame.width + 20) + dialog.width(calendar.frame.width + 20) + dialog.height(calendar.frame.height + 20) + calendar.pinToSuperView(.uniform(10)) + calendar.onChange = { [weak self] control in + guard let self else { return } + selectedDate = control.selectedDate + sendActions(for: .valueChanged) + UIAccessibility.post(notification: .layoutChanged, argument: containerView) + viewController.dismiss(animated: true) + } + + let alert = AlertViewController().with { + $0.dialog = dialog + $0.modalPresentationStyle = .overCurrentContext + $0.modalTransitionStyle = .crossDissolve + } + topViewController = viewController + viewController.present(alert, animated: true){ + dialog.flashScrollIndicators() + } } } - - private func calculatePopoverPosition(relativeTo sourceView: UIView, in parentView: UIView, size: CGSize, with spacing: CGFloat) -> (CGFloat, CGFloat, CGFloat) { + + private func hidePopoverView() { + if topViewController != nil { + topViewController?.dismiss(animated: true) + topViewController = nil + } else { + UIView.animate(withDuration: 0.2, + animations: {[weak self] in + guard let self, let popoverView else { return } + popoverView.alpha = 0 + popoverView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9) + if let scrollView, let scrollViewContentSize { + scrollView.contentSize = scrollViewContentSize + } + + }) { [weak self] _ in + guard let self, let popoverView else { return } + popoverView.isHidden = true + popoverView.removeFromSuperview() + popoverVisible = false + responder?.resignFirstResponder() + setNeedsUpdate() + UIAccessibility.post(notification: .layoutChanged, argument: containerView) + } + } + } + + private func findScrollView(from view: UIView) -> UIScrollView? { + var currentView = view + while let superview = currentView.superview { + if let scrollView = superview as? UIScrollView { + return scrollView + } + currentView = superview + } + return nil + } + + private func calculatePopoverPosition(relativeTo sourceView: UIView, in parentView: UIView, size: CGSize, with spacing: CGFloat) throws -> CGPoint? { let sourceFrameInParent = sourceView.convert(sourceView.bounds, to: parentView) let parentBounds = parentView.bounds let safeAreaInsets = parentView.safeAreaInsets let popoverWidth = size.width let popoverHeight = size.height - + var popoverX: CGFloat = 0 var popoverY: CGFloat = 0 - var adjustedHeight = popoverHeight - + // Calculate horizontal position if sourceFrameInParent.width < popoverWidth { if sourceFrameInParent.midX - popoverWidth / 2 < 0 { @@ -307,35 +353,51 @@ extension DatePicker { } else { popoverX = sourceFrameInParent.midX - popoverWidth / 2 } - + // Ensure the popover is within the parent's bounds horizontally popoverX = max(0, min(popoverX, parentBounds.width - popoverWidth)) - - // Calculate vertical position and height - let availableSpaceAbove = sourceFrameInParent.minY - safeAreaInsets.top - spacing - let availableSpaceBelow = parentBounds.height - sourceFrameInParent.maxY - safeAreaInsets.bottom - spacing - let totalAvailableHeight = parentBounds.height - safeAreaInsets.top - safeAreaInsets.bottom - if availableSpaceAbove >= popoverHeight { - // Show above without adjusting height - popoverY = sourceFrameInParent.minY - popoverHeight - spacing - } else if availableSpaceBelow >= popoverHeight { - // Show below without adjusting height - popoverY = sourceFrameInParent.maxY + spacing + var availableSpaceAbove: CGFloat = 0.0 + var availableSpaceBelow: CGFloat = 0.0 + + /// if the scrollView is set we want to change how we calculate the containerView's position + if var scrollView = parentView as? UIScrollView { + // Calculate vertical position and height + availableSpaceAbove = sourceFrameInParent.minY - scrollView.bounds.minY - spacing + availableSpaceBelow = scrollView.bounds.maxY - sourceFrameInParent.maxY - spacing - } else if totalAvailableHeight >= popoverHeight { - // check if the total if availableSpaceAbove > availableSpaceBelow { - popoverY = safeAreaInsets.top + // Show above + popoverY = sourceFrameInParent.minY - popoverHeight - spacing } else { - popoverY = parentBounds.height - safeAreaInsets.bottom - popoverHeight + // Show below + popoverY = sourceFrameInParent.maxY + spacing + + // See if we need to expand the contentSize of the ScrollView + let diff = scrollView.contentSize.height - sourceFrameInParent.maxY + if diff < popoverHeight { + scrollView.contentSize.height += popoverHeight - diff + VDSLayout.space4X + } } + } else { - popoverY = safeAreaInsets.top - adjustedHeight = totalAvailableHeight + // Calculate vertical position and height + availableSpaceAbove = sourceFrameInParent.minY - safeAreaInsets.top - spacing + availableSpaceBelow = parentBounds.height - sourceFrameInParent.maxY - safeAreaInsets.bottom - spacing + + if availableSpaceAbove >= popoverHeight { + // Show above + popoverY = sourceFrameInParent.minY - popoverHeight - spacing + + } else if availableSpaceBelow >= popoverHeight { + // Show below + popoverY = sourceFrameInParent.maxY + spacing + + } else { + return nil + } } - - return (popoverX, popoverY, adjustedHeight) + + return .init(x: popoverX, y: popoverY) } } - From 67e055878d1be6d7dd2d0915b9b50fc71d49e2d3 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 25 Jun 2024 13:36:56 -0500 Subject: [PATCH 53/83] fixed bug in accessibilityLabel Signed-off-by: Matt Bruce --- VDS/Components/Buttons/ButtonBase.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/VDS/Components/Buttons/ButtonBase.swift b/VDS/Components/Buttons/ButtonBase.swift index d7b80c8a..720fca36 100644 --- a/VDS/Components/Buttons/ButtonBase.swift +++ b/VDS/Components/Buttons/ButtonBase.swift @@ -114,6 +114,11 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable { titleLabel?.adjustsFontSizeToFitWidth = false titleLabel?.lineBreakMode = .byTruncatingTail titleLabel?.numberOfLines = 1 + + bridge_accessibilityLabelBlock = { [weak self] in + guard let self else { return nil } + return text + } } open func updateView() { From 3589e1230a7ab990e8a1badb58a0c0839aa02096 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 25 Jun 2024 13:37:45 -0500 Subject: [PATCH 54/83] removed throws Signed-off-by: Matt Bruce --- VDS/Components/DatePicker/DatePicker.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/VDS/Components/DatePicker/DatePicker.swift b/VDS/Components/DatePicker/DatePicker.swift index e8d13b27..0ae5f47c 100644 --- a/VDS/Components/DatePicker/DatePicker.swift +++ b/VDS/Components/DatePicker/DatePicker.swift @@ -328,7 +328,7 @@ extension DatePicker { return nil } - private func calculatePopoverPosition(relativeTo sourceView: UIView, in parentView: UIView, size: CGSize, with spacing: CGFloat) throws -> CGPoint? { + private func calculatePopoverPosition(relativeTo sourceView: UIView, in parentView: UIView, size: CGSize, with spacing: CGFloat) -> CGPoint? { let sourceFrameInParent = sourceView.convert(sourceView.bounds, to: parentView) let parentBounds = parentView.bounds let safeAreaInsets = parentView.safeAreaInsets @@ -394,6 +394,8 @@ extension DatePicker { popoverY = sourceFrameInParent.maxY + spacing } else { + + //return nil since there is no way we can show the popover without a scrollview return nil } } From 1f0ba0cee6958851d9214760b012bc30f2ad8660 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 25 Jun 2024 13:37:59 -0500 Subject: [PATCH 55/83] made icon accessible Signed-off-by: Matt Bruce --- VDS/Components/TextFields/EntryFieldBase.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VDS/Components/TextFields/EntryFieldBase.swift b/VDS/Components/TextFields/EntryFieldBase.swift index e4aeddc2..bd9155d1 100644 --- a/VDS/Components/TextFields/EntryFieldBase.swift +++ b/VDS/Components/TextFields/EntryFieldBase.swift @@ -183,7 +183,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { open var statusIcon: Icon = Icon().with { $0.size = .medium - $0.isAccessibilityElement = false + $0.isAccessibilityElement = true } open var labelText: String? { didSet { setNeedsUpdate() } } From 67a5663fc34f867d758821a82fef1afce9573580 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 25 Jun 2024 13:38:16 -0500 Subject: [PATCH 56/83] made credit card image accessible Signed-off-by: Matt Bruce --- .../TextFields/InputField/FieldTypes/CreditCard.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift b/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift index ed457446..429b68be 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift @@ -125,7 +125,7 @@ extension InputField { class CreditCardHandler: FieldTypeHandler { static let shared = CreditCardHandler() - + private override init() { super.init() self.validateOnChange = false @@ -135,6 +135,7 @@ extension InputField { fileprivate func updateLeftImage(_ inputField: InputField) { let imageName = inputField.cardType.imageName(surface: inputField.surface) creditCardImageView.image = BundleManager.shared.image(for: imageName) + creditCardImageView.accessibilityLabel = inputField.cardType.rawValue } override func updateView(_ inputField: InputField) { @@ -148,14 +149,14 @@ extension InputField { inputField.textField.leftView = iconContainerView inputField.textField.leftViewMode = .always - + updateLeftImage(inputField) } - + internal var creditCardImageView = UIImageView().with { $0.height(20) $0.width(32) - $0.isAccessibilityElement = false + $0.isAccessibilityElement = true $0.translatesAutoresizingMaskIntoConstraints = false $0.contentMode = .scaleAspectFill $0.clipsToBounds = true From 0ec165701bb776add76d89d873f4d6f1b86eec5e Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 25 Jun 2024 13:39:18 -0500 Subject: [PATCH 57/83] CXTDT-577463 - InputField - Accessible Elements for actionLink/credit Card image, status image - fixed issue with success text Signed-off-by: Matt Bruce --- .../TextFields/InputField/InputField.swift | 44 +++++++++++++++++-- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/VDS/Components/TextFields/InputField/InputField.swift b/VDS/Components/TextFields/InputField/InputField.swift index 2ca4c207..575e843f 100644 --- a/VDS/Components/TextFields/InputField/InputField.swift +++ b/VDS/Components/TextFields/InputField/InputField.swift @@ -196,6 +196,33 @@ open class InputField: EntryFieldBase { borderColorConfiguration.setSurfaceColors(VDSColor.feedbackSuccessOnlight, VDSColor.feedbackSuccessOndark, forState: .success) textField.textColorConfiguration = textFieldTextColorConfiguration + + containerView.bridge_accessibilityLabelBlock = { [weak self] in + guard let self else { return "" } + var accessibilityLabels = [String]() + + if let text = titleLabel.text?.trimmingCharacters(in: .whitespaces) { + accessibilityLabels.append(text) + } + if isReadOnly { + accessibilityLabels.append("read only") + } + if !isEnabled { + accessibilityLabels.append("dimmed") + } + if let errorText, showError { + accessibilityLabels.append("error, \(errorText)") + } + + if let successText, showSuccess { + accessibilityLabels.append("success, \(successText)") + } + + accessibilityLabels.append("\(Self.self)") + + return accessibilityLabels.joined(separator: ", ") + } + } open override func getFieldContainer() -> UIView { @@ -260,11 +287,20 @@ open class InputField: EntryFieldBase { get { var elements = [Any]() elements.append(contentsOf: [titleLabel, containerView]) - if showError { + if let leftView = textField.leftView { + elements.append(leftView) + } + + if !statusIcon.isHidden{ elements.append(statusIcon) - if let errorText, !errorText.isEmpty { - elements.append(errorLabel) - } + } + + if !actionTextLink.isHidden { + elements.append(actionTextLink) + } + + if let errorText, !errorText.isEmpty, showError || hasInternalError { + elements.append(errorLabel) } else if showSuccess, let successText, !successText.isEmpty { elements.append(successLabel) } From 0981bf77297b6bf9451b5d8decfe59eb3f8ea70a Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 25 Jun 2024 13:46:00 -0500 Subject: [PATCH 58/83] updated logic for statusIcon label Signed-off-by: Matt Bruce --- VDS/Components/TextFields/EntryFieldBase.swift | 5 +++++ VDS/Components/TextFields/InputField/InputField.swift | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/VDS/Components/TextFields/EntryFieldBase.swift b/VDS/Components/TextFields/EntryFieldBase.swift index bd9155d1..5bd50351 100644 --- a/VDS/Components/TextFields/EntryFieldBase.swift +++ b/VDS/Components/TextFields/EntryFieldBase.swift @@ -344,6 +344,11 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { guard let self else { return "" } return value } + + statusIcon.bridge_accessibilityLabelBlock = { [weak self] in + guard let self else { return "" } + return showError || hasInternalError ? "error" : nil + } } /// Updates the UI diff --git a/VDS/Components/TextFields/InputField/InputField.swift b/VDS/Components/TextFields/InputField/InputField.swift index 575e843f..9adf34f2 100644 --- a/VDS/Components/TextFields/InputField/InputField.swift +++ b/VDS/Components/TextFields/InputField/InputField.swift @@ -223,6 +223,16 @@ open class InputField: EntryFieldBase { return accessibilityLabels.joined(separator: ", ") } + statusIcon.bridge_accessibilityLabelBlock = { [weak self] in + guard let self else { return "" } + if showError { + return "error" + } else if showSuccess { + return "success" + } else { + return nil + } + } } open override func getFieldContainer() -> UIView { From 649acb77af9417cfe6a9401b155cd2c6a4a2a92a Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 25 Jun 2024 13:56:03 -0500 Subject: [PATCH 59/83] CXTDT-577463 - InputField - Accessibility - Format Text Signed-off-by: Matt Bruce --- VDS/Components/TextFields/InputField/InputField.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/VDS/Components/TextFields/InputField/InputField.swift b/VDS/Components/TextFields/InputField/InputField.swift index 9adf34f2..f7f54896 100644 --- a/VDS/Components/TextFields/InputField/InputField.swift +++ b/VDS/Components/TextFields/InputField/InputField.swift @@ -204,12 +204,23 @@ open class InputField: EntryFieldBase { if let text = titleLabel.text?.trimmingCharacters(in: .whitespaces) { accessibilityLabels.append(text) } + + if let formatText = textField.formatText, !formatText.isEmpty { + accessibilityLabels.append("format, \(formatText)") + } + + if let placeholderText = textField.placeholder, !placeholderText.isEmpty { + accessibilityLabels.append("placeholder, \(placeholderText)") + } + if isReadOnly { accessibilityLabels.append("read only") } + if !isEnabled { accessibilityLabels.append("dimmed") } + if let errorText, showError { accessibilityLabels.append("error, \(errorText)") } From 6408d97841bfae778027a54ee02e54e0c51593f3 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 25 Jun 2024 13:57:41 -0500 Subject: [PATCH 60/83] updted release notes Signed-off-by: Matt Bruce --- VDS/SupportingFiles/ReleaseNotes.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/VDS/SupportingFiles/ReleaseNotes.txt b/VDS/SupportingFiles/ReleaseNotes.txt index 8f13a9b0..484621ea 100644 --- a/VDS/SupportingFiles/ReleaseNotes.txt +++ b/VDS/SupportingFiles/ReleaseNotes.txt @@ -1,6 +1,7 @@ 1.0.68 ---------------- - CXTDT-553663 - DropdownSelect - Accessibility - has popup +- CXTDT-577463 - InputField - Accessibility 1.0.67 ---------------- From 4c4cac92c750cee19f2c5ac726a755359b24e2dd Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 25 Jun 2024 13:58:07 -0500 Subject: [PATCH 61/83] more notes Signed-off-by: Matt Bruce --- VDS/SupportingFiles/ReleaseNotes.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/VDS/SupportingFiles/ReleaseNotes.txt b/VDS/SupportingFiles/ReleaseNotes.txt index 484621ea..cd86edbc 100644 --- a/VDS/SupportingFiles/ReleaseNotes.txt +++ b/VDS/SupportingFiles/ReleaseNotes.txt @@ -1,5 +1,6 @@ 1.0.68 ---------------- +- DatePicker - Refactored how this is shown - CXTDT-553663 - DropdownSelect - Accessibility - has popup - CXTDT-577463 - InputField - Accessibility From 83a0ff07b8b9b6f947b1451065c2d72a25a4c7ee Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 25 Jun 2024 14:01:40 -0500 Subject: [PATCH 62/83] updated notes version Signed-off-by: Matt Bruce --- VDS.xcodeproj/project.pbxproj | 4 ++-- VDS/SupportingFiles/ReleaseNotes.txt | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index bf8fbc94..a5403c49 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -1535,7 +1535,7 @@ BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 67; + CURRENT_PROJECT_VERSION = 68; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1573,7 +1573,7 @@ BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 67; + CURRENT_PROJECT_VERSION = 68; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; diff --git a/VDS/SupportingFiles/ReleaseNotes.txt b/VDS/SupportingFiles/ReleaseNotes.txt index cd86edbc..063d9422 100644 --- a/VDS/SupportingFiles/ReleaseNotes.txt +++ b/VDS/SupportingFiles/ReleaseNotes.txt @@ -1,6 +1,9 @@ 1.0.68 ---------------- - DatePicker - Refactored how this is shown +- Checkbox Item/Group - Accessibility Refactor +- Radiobox Item/Group - Accessibility Refactor +- Radiobutton Item/Group - Accessibility Refactor - CXTDT-553663 - DropdownSelect - Accessibility - has popup - CXTDT-577463 - InputField - Accessibility From 3ec982d45c546b254d4794b0d094a4c7c1d02465 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 25 Jun 2024 14:29:55 -0500 Subject: [PATCH 63/83] fixed responder issue Signed-off-by: Matt Bruce --- VDS/Components/DatePicker/DatePicker.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/VDS/Components/DatePicker/DatePicker.swift b/VDS/Components/DatePicker/DatePicker.swift index 0ae5f47c..b5231154 100644 --- a/VDS/Components/DatePicker/DatePicker.swift +++ b/VDS/Components/DatePicker/DatePicker.swift @@ -30,8 +30,14 @@ open class DatePicker: EntryFieldBase { //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- + class Responder: UIView { + open override var canBecomeFirstResponder: Bool { + true + } + } + internal override var responder: UIResponder? { hiddenView } - internal var hiddenView = UITextView().with { $0.width(0) } + internal var hiddenView = Responder().with { $0.width(0) } internal var minWidthDefault = 186.0 internal var bottomStackView: UIStackView = { return UIStackView().with { From 77fbec8bedb4783ffa45921d9715dc2cbb354a37 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 25 Jun 2024 15:18:19 -0500 Subject: [PATCH 64/83] fixeds for the datePicker Signed-off-by: Matt Bruce --- VDS/Classes/AlertViewController.swift | 25 +++++------- VDS/Components/DatePicker/DatePicker.swift | 47 ++++++++++++++++++---- 2 files changed, 50 insertions(+), 22 deletions(-) diff --git a/VDS/Classes/AlertViewController.swift b/VDS/Classes/AlertViewController.swift index 71a1c6d9..30a5d5c6 100644 --- a/VDS/Classes/AlertViewController.swift +++ b/VDS/Classes/AlertViewController.swift @@ -62,21 +62,6 @@ open class AlertViewController: UIViewController, Surfaceable { open func setup() { guard let dialog else { return } view.accessibilityElements = [dialog] - - //left-right swipe - view.publisher(for: UISwipeGestureRecognizer().with{ $0.direction = .right }) - .sink { [weak self] swipe in - guard let self, !UIAccessibility.isVoiceOverRunning else { return } - self.dismiss() - }.store(in: &subscribers) - - //tapping in background - view.publisher(for: UITapGestureRecognizer().with{ $0.numberOfTapsRequired = 1 }) - .sink { [weak self] swipe in - guard let self, !UIAccessibility.isVoiceOverRunning else { return } - self.dismiss() - }.store(in: &subscribers) - view.addSubview(dialog) // Activate constraints @@ -90,6 +75,16 @@ open class AlertViewController: UIViewController, Surfaceable { dialog.bottomAnchor.constraint(lessThanOrEqualTo: view.bottomAnchor, constant: -10) ]) } + + open override func touchesBegan(_ touches: Set, with event: UIEvent?) { + guard let touch = touches.first else { return } + let location = touch.location(in: view) + if dialog.frame.contains(location) { + super.touchesBegan(touches, with: event) + } else { + dismiss() + } + } /// Used to make changes to the View based off a change events or from local properties. open func updateView() { diff --git a/VDS/Components/DatePicker/DatePicker.swift b/VDS/Components/DatePicker/DatePicker.swift index b5231154..cb2b5b88 100644 --- a/VDS/Components/DatePicker/DatePicker.swift +++ b/VDS/Components/DatePicker/DatePicker.swift @@ -52,6 +52,15 @@ open class DatePicker: EntryFieldBase { //-------------------------------------------------- // MARK: - Private Popover/Alert Properties //-------------------------------------------------- + /// View shown inline + internal var popoverOverlayView = UIView().with { + $0.backgroundColor = .clear + $0.translatesAutoresizingMaskIntoConstraints = false + } + + /// use this to track touch events outside of the popover in the overlay + internal var popupOverlayTapGesture: AnyCancellable? + /// View shown inline internal var popoverView: UIView! /// Size used for the popover @@ -147,6 +156,7 @@ open class DatePicker: EntryFieldBase { } .store(in: &subscribers) + popoverOverlayView.isHidden = true } open override func getFieldContainer() -> UIView { @@ -189,7 +199,7 @@ open class DatePicker: EntryFieldBase { } extension DatePicker { - + private func showPopover() { guard let viewController = UIApplication.topViewController(), var parentView = viewController.view, !popoverVisible else { hidePopoverView() @@ -220,12 +230,12 @@ extension DatePicker { if let scrollView { parentView = scrollView } - + // see if you should use the popover or show an alert - if let popoverOrigin = try? calculatePopoverPosition(relativeTo: containerView, - in: parentView, - size: popoverViewSize, - with: popoverSpacing) { + if let popoverOrigin = calculatePopoverPosition(relativeTo: containerView, + in: parentView, + size: popoverViewSize, + with: popoverSpacing) { calendar.onChange = { [weak self] control in guard let self else { return } selectedDate = control.selectedDate @@ -246,6 +256,16 @@ extension DatePicker { calendar.pinToSuperView() // add views + popoverOverlayView.isHidden = false + popupOverlayTapGesture = popoverOverlayView + .publisher(for: UITapGestureRecognizer()) + .sink(receiveValue: { [weak self] gesture in + guard let self else { return } + gestureEventOccured(gesture, parentView: parentView) + }) + + parentView.addSubview(popoverOverlayView) + popoverOverlayView.pinToSuperView() parentView.addSubview(popoverView) parentView.layoutIfNeeded() @@ -302,6 +322,11 @@ extension DatePicker { topViewController?.dismiss(animated: true) topViewController = nil } else { + popoverOverlayView.isHidden = true + popoverOverlayView.removeFromSuperview() + popupOverlayTapGesture?.cancel() + popupOverlayTapGesture = nil + UIView.animate(withDuration: 0.2, animations: {[weak self] in guard let self, let popoverView else { return } @@ -367,7 +392,7 @@ extension DatePicker { var availableSpaceBelow: CGFloat = 0.0 /// if the scrollView is set we want to change how we calculate the containerView's position - if var scrollView = parentView as? UIScrollView { + if let scrollView = parentView as? UIScrollView { // Calculate vertical position and height availableSpaceAbove = sourceFrameInParent.minY - scrollView.bounds.minY - spacing availableSpaceBelow = scrollView.bounds.maxY - sourceFrameInParent.maxY - spacing @@ -408,4 +433,12 @@ extension DatePicker { return .init(x: popoverX, y: popoverY) } + + private func gestureEventOccured(_ gesture: UIGestureRecognizer, parentView: UIView) { + guard let popoverView, popoverVisible else { return } + let location = gesture.location(in: parentView) + if !popoverView.frame.contains(location) { + hidePopoverView() + } + } } From 738dee681fc40e07d8010dee3cc5d8daaf3afde6 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 25 Jun 2024 15:20:44 -0500 Subject: [PATCH 65/83] updated release notes Signed-off-by: Matt Bruce --- VDS/SupportingFiles/ReleaseNotes.txt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/VDS/SupportingFiles/ReleaseNotes.txt b/VDS/SupportingFiles/ReleaseNotes.txt index 063d9422..54be0815 100644 --- a/VDS/SupportingFiles/ReleaseNotes.txt +++ b/VDS/SupportingFiles/ReleaseNotes.txt @@ -1,12 +1,16 @@ -1.0.68 +1.0.69 ---------------- - DatePicker - Refactored how this is shown -- Checkbox Item/Group - Accessibility Refactor +- Checkbox Item/Group - Accessibility Refactor - Radiobox Item/Group - Accessibility Refactor - Radiobutton Item/Group - Accessibility Refactor - CXTDT-553663 - DropdownSelect - Accessibility - has popup - CXTDT-577463 - InputField - Accessibility +1.0.69 +---------------- +- Expired Build because of a issue + 1.0.67 ---------------- - CXTDT-568463 - Calendar - On long press, hover randomizes From 9c32ea4870ed483c41e36f8f89f95d384455f595 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 26 Jun 2024 09:50:06 -0500 Subject: [PATCH 66/83] redo constraints Signed-off-by: Matt Bruce --- .../TileContainer/TileContainer.swift | 146 +++++++++++------- 1 file changed, 89 insertions(+), 57 deletions(-) diff --git a/VDS/Components/TileContainer/TileContainer.swift b/VDS/Components/TileContainer/TileContainer.swift index 546d8d05..b6014e8f 100644 --- a/VDS/Components/TileContainer/TileContainer.swift +++ b/VDS/Components/TileContainer/TileContainer.swift @@ -179,11 +179,16 @@ open class TileContainerBase: Control where Padding //-------------------------------------------------- internal var widthConstraint: NSLayoutConstraint? internal var heightConstraint: NSLayoutConstraint? - internal var heightGreaterThanConstraint: NSLayoutConstraint? - internal var containerTopConstraint: NSLayoutConstraint? - internal var containerBottomConstraint: NSLayoutConstraint? - internal var containerLeadingConstraint: NSLayoutConstraint? - internal var containerTrailingConstraint: NSLayoutConstraint? + + internal var containerViewBottomConstraint: NSLayoutConstraint? + internal var containerViewTrailingConstraint: NSLayoutConstraint? + internal var containerViewBottomLessThanOrEqualConstraint: NSLayoutConstraint? + internal var containerViewTrailingLessThanOrEqualConstraint: NSLayoutConstraint? + + internal var contentViewTopConstraint: NSLayoutConstraint? + internal var contentViewBottomConstraint: NSLayoutConstraint? + internal var contentViewLeadingConstraint: NSLayoutConstraint? + internal var contentViewTrailingConstraint: NSLayoutConstraint? //-------------------------------------------------- // MARK: - Configuration @@ -221,29 +226,25 @@ open class TileContainerBase: Control where Padding open override func setup() { super.setup() isAccessibilityElement = false - - let layoutGuide = UILayoutGuide() - addLayoutGuide(layoutGuide) - layoutGuide + addSubview(containerView) + containerView.addSubview(backgroundImageView) + containerView.addSubview(contentView) + containerView.addSubview(highlightView) + + containerView .pinTop() .pinLeading() - .pinTrailing(0, .defaultHigh) - .pinBottom(0, .defaultHigh) - addSubview(backgroundImageView) - addSubview(containerView) - containerView.addSubview(contentView) - addSubview(highlightView) - - containerView.pinToSuperView() - widthConstraint = layoutGuide.widthAnchor.constraint(equalToConstant: 0) - - heightGreaterThanConstraint = layoutGuide.heightAnchor.constraint(greaterThanOrEqualToConstant: 44.0) - heightGreaterThanConstraint?.isActive = false - - heightConstraint = layoutGuide.heightAnchor.constraint(equalToConstant: 0) - + containerViewTrailingConstraint = containerView.pinTrailing(anchor: trailingAnchor)?.deactivate() + containerViewBottomConstraint = containerView.pinBottom(anchor: bottomAnchor)?.deactivate() + + containerViewTrailingLessThanOrEqualConstraint = containerView.pinTrailingLessThanOrEqualTo(anchor: trailingAnchor)?.deactivate() + containerViewBottomLessThanOrEqualConstraint = containerView.pinBottomLessThanOrEqualTo(anchor: bottomAnchor)?.deactivate() + backgroundImageView.pinToSuperView() + + widthConstraint = containerView.widthAnchor.constraint(equalToConstant: 0).deactivate() + heightConstraint = containerView.heightAnchor.constraint(equalToConstant: 0).deactivate() backgroundImageView.setContentHuggingPriority(.defaultLow, for: .horizontal) backgroundImageView.setContentHuggingPriority(.defaultLow, for: .vertical) @@ -252,20 +253,20 @@ open class TileContainerBase: Control where Padding backgroundImageView.isUserInteractionEnabled = false backgroundImageView.isHidden = true - containerTopConstraint = contentView.pinTop(anchor: layoutGuide.topAnchor, constant: padding.value) - containerBottomConstraint = layoutGuide.pinBottom(anchor: contentView.bottomAnchor, constant: padding.value) - containerLeadingConstraint = contentView.pinLeading(anchor: layoutGuide.leadingAnchor, constant: padding.value) - containerTrailingConstraint = layoutGuide.pinTrailing(anchor: contentView.trailingAnchor, constant: padding.value) + contentViewTopConstraint = contentView.pinTop(anchor: containerView.topAnchor, constant: padding.value) + contentViewBottomConstraint = containerView.pinBottom(anchor: contentView.bottomAnchor, constant: padding.value) + contentViewLeadingConstraint = contentView.pinLeading(anchor: containerView.leadingAnchor, constant: padding.value) + contentViewTrailingConstraint = containerView.pinTrailing(anchor: contentView.trailingAnchor, constant: padding.value) - highlightView.pin(layoutGuide) + highlightView.pin(containerView) highlightView.isHidden = true highlightView.backgroundColor = .clear //corner radius - layer.cornerRadius = cornerRadius + containerView.layer.cornerRadius = cornerRadius backgroundImageView.layer.cornerRadius = cornerRadius highlightView.layer.cornerRadius = cornerRadius - clipsToBounds = true + containerView.clipsToBounds = true containerView.bridge_isAccessibilityElementBlock = { [weak self] in self?.onClickSubscriber != nil } containerView.accessibilityHint = "Double tap to open." @@ -303,43 +304,74 @@ open class TileContainerBase: Control where Padding highlightView.backgroundColor = hightLightViewColorConfiguration.getColor(self) highlightView.isHidden = !isHighlighted - layer.borderColor = borderColorConfiguration.getColor(self).cgColor - layer.borderWidth = showBorder ? VDSFormControls.borderWidth : 0 + containerView.layer.borderColor = borderColorConfiguration.getColor(self).cgColor + containerView.layer.borderWidth = showBorder ? VDSFormControls.borderWidth : 0 - containerTopConstraint?.constant = padding.value - containerLeadingConstraint?.constant = padding.value - containerBottomConstraint?.constant = padding.value - containerTrailingConstraint?.constant = padding.value + contentViewTopConstraint?.constant = padding.value + contentViewLeadingConstraint?.constant = padding.value + contentViewBottomConstraint?.constant = padding.value + contentViewTrailingConstraint?.constant = padding.value + //deactivate everything + widthConstraint?.deactivate() + heightConstraint?.deactivate() + containerViewTrailingLessThanOrEqualConstraint?.deactivate() + containerViewTrailingLessThanOrEqualConstraint?.deactivate() + containerViewBottomConstraint?.deactivate() + containerViewTrailingConstraint?.deactivate() + + //run logic to determine which to activate if let width, aspectRatio == .none && height == nil{ widthConstraint?.constant = width - widthConstraint?.isActive = true - heightConstraint?.isActive = false - heightGreaterThanConstraint?.isActive = true + widthConstraint?.activate() + containerViewTrailingConstraint?.activate() + containerViewBottomConstraint?.activate() + + } else if let height, aspectRatio == .none && width == nil{ + heightConstraint?.constant = height + heightConstraint?.activate() + containerViewTrailingConstraint?.activate() + containerViewBottomConstraint?.activate() + } else if let height, let width { widthConstraint?.constant = width heightConstraint?.constant = height - heightConstraint?.isActive = true - widthConstraint?.isActive = true - heightGreaterThanConstraint?.isActive = false + heightConstraint?.activate() + widthConstraint?.activate() + containerViewTrailingLessThanOrEqualConstraint?.activate() + containerViewBottomLessThanOrEqualConstraint?.activate() + } else if let width { let size = ratioSize(for: width) widthConstraint?.constant = size.width heightConstraint?.constant = size.height - widthConstraint?.isActive = true - heightConstraint?.isActive = true - heightGreaterThanConstraint?.isActive = false + widthConstraint?.activate() + heightConstraint?.activate() + containerViewTrailingLessThanOrEqualConstraint?.activate() + containerViewBottomLessThanOrEqualConstraint?.activate() + + } else if let height { + let size = ratioSize(for: height) + //enforce exact height/width + widthConstraint?.constant = size.width + heightConstraint?.constant = size.height + widthConstraint?.activate() + heightConstraint?.activate() + containerViewTrailingLessThanOrEqualConstraint?.activate() + containerViewBottomLessThanOrEqualConstraint?.activate() + } else { - widthConstraint?.isActive = false - heightConstraint?.isActive = false + //set to the parent view + containerViewBottomConstraint?.activate() + containerViewTrailingConstraint?.activate() } applyBackgroundEffects() if showDropShadow, surface == .light { - addDropShadow(dropShadowConfiguration) + containerView.addDropShadow(dropShadowConfiguration) } else { - removeDropShadows() + containerView.removeDropShadows() } } @@ -372,8 +404,8 @@ open class TileContainerBase: Control where Padding /// Used to update frames for the added CAlayers to our view open override func layoutSubviews() { super.layoutSubviews() - dropShadowLayers?.forEach { $0.frame = bounds } - gradientLayers?.forEach { $0.frame = bounds } + containerView.dropShadowLayers?.forEach { $0.frame = bounds } + containerView.gradientLayers?.forEach { $0.frame = bounds } } //-------------------------------------------------- @@ -400,25 +432,25 @@ open class TileContainerBase: Control where Padding switch backgroundEffect { case .transparency: alphaConfiguration = 0.8 - removeGradientLayer() + containerView.removeGradientLayer() case .gradient(let firstColor, let secondColor): alphaConfiguration = 1.0 - addGradientLayer(with: firstColor, secondColor: secondColor) + containerView.addGradientLayer(with: firstColor, secondColor: secondColor) backgroundImageView.isHidden = true backgroundImageView.alpha = 1.0 case .none: alphaConfiguration = 1.0 - removeGradientLayer() + containerView.removeGradientLayer() } if let backgroundImage { backgroundImageView.image = backgroundImage backgroundImageView.isHidden = false backgroundImageView.alpha = alphaConfiguration - backgroundColor = imageFallbackColor.withAlphaComponent(alphaConfiguration) + containerView.backgroundColor = imageFallbackColor.withAlphaComponent(alphaConfiguration) } else { backgroundImageView.isHidden = true backgroundImageView.alpha = 1.0 - backgroundColor = color.withAlphaComponent(alphaConfiguration) + containerView.backgroundColor = color.withAlphaComponent(alphaConfiguration) } } From 9c6009e4ac4b3f3d8188c922100f1a8d80a89b7b Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 26 Jun 2024 10:14:48 -0500 Subject: [PATCH 67/83] fixed bug in not using super for overrides Signed-off-by: Matt Bruce --- VDS/BaseClasses/Control.swift | 22 +++++++--------- VDS/BaseClasses/View.swift | 22 +++++++--------- VDS/Components/Buttons/ButtonBase.swift | 25 ++++++------------- VDS/Components/Label/Label.swift | 24 ++++++------------ .../TextFields/InputField/TextField.swift | 20 ++++++--------- .../TextFields/TextArea/TextView.swift | 20 ++++++--------- 6 files changed, 50 insertions(+), 83 deletions(-) diff --git a/VDS/BaseClasses/Control.swift b/VDS/BaseClasses/Control.swift index 2a9fe769..7aa08717 100644 --- a/VDS/BaseClasses/Control.swift +++ b/VDS/BaseClasses/Control.swift @@ -129,7 +129,6 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable { //-------------------------------------------------- open var accessibilityAction: ((Control) -> Void)? - private var _isAccessibilityElement: Bool = false open override var isAccessibilityElement: Bool { get { var block: AXBoolReturnBlock? @@ -137,7 +136,7 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable { // if #available(iOS 17, *) { // block = isAccessibilityElementBlock // } - + if block == nil { block = bridge_isAccessibilityElementBlock } @@ -145,15 +144,14 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable { if let block { return block() } else { - return _isAccessibilityElement + return super.isAccessibilityElement } } set { - _isAccessibilityElement = newValue + super.isAccessibilityElement = newValue } } - private var _accessibilityLabel: String? open override var accessibilityLabel: String? { get { var block: AXStringReturnBlock? @@ -168,15 +166,14 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable { if let block { return block() } else { - return _accessibilityLabel + return super.accessibilityLabel } } set { - _accessibilityLabel = newValue + super.accessibilityLabel = newValue } } - private var _accessibilityHint: String? open override var accessibilityHint: String? { get { var block: AXStringReturnBlock? @@ -191,15 +188,14 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable { if let block { return block() } else { - return _accessibilityHint + return super.accessibilityHint } } set { - _accessibilityHint = newValue + super.accessibilityHint = newValue } } - private var _accessibilityValue: String? open override var accessibilityValue: String? { get { var block: AXStringReturnBlock? @@ -215,11 +211,11 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable { if let block{ return block() } else { - return _accessibilityValue + return super.accessibilityValue } } set { - _accessibilityValue = newValue + super.accessibilityValue = newValue } } diff --git a/VDS/BaseClasses/View.swift b/VDS/BaseClasses/View.swift index c7df1765..7e88df8e 100644 --- a/VDS/BaseClasses/View.swift +++ b/VDS/BaseClasses/View.swift @@ -96,7 +96,6 @@ open class View: UIView, ViewProtocol, UserInfoable { //-------------------------------------------------- open var accessibilityAction: ((View) -> Void)? - private var _isAccessibilityElement: Bool = false open override var isAccessibilityElement: Bool { get { var block: AXBoolReturnBlock? @@ -112,22 +111,21 @@ open class View: UIView, ViewProtocol, UserInfoable { if let block { return block() } else { - return _isAccessibilityElement + return super.isAccessibilityElement } } set { - _isAccessibilityElement = newValue + super.isAccessibilityElement = newValue } } - private var _accessibilityLabel: String? open override var accessibilityLabel: String? { get { var block: AXStringReturnBlock? // if #available(iOS 17, *) { // block = accessibilityLabelBlock // } -// + if block == nil { block = bridge_accessibilityLabelBlock } @@ -135,15 +133,14 @@ open class View: UIView, ViewProtocol, UserInfoable { if let block { return block() } else { - return _accessibilityLabel + return super.accessibilityLabel } } set { - _accessibilityLabel = newValue + super.accessibilityLabel = newValue } } - private var _accessibilityHint: String? open override var accessibilityHint: String? { get { var block: AXStringReturnBlock? @@ -158,15 +155,14 @@ open class View: UIView, ViewProtocol, UserInfoable { if let block { return block() } else { - return _accessibilityHint + return super.accessibilityHint } } set { - _accessibilityHint = newValue + super.accessibilityHint = newValue } } - private var _accessibilityValue: String? open override var accessibilityValue: String? { get { var block: AXStringReturnBlock? @@ -182,11 +178,11 @@ open class View: UIView, ViewProtocol, UserInfoable { if let block{ return block() } else { - return _accessibilityValue + return super.accessibilityValue } } set { - _accessibilityValue = newValue + super.accessibilityValue = newValue } } diff --git a/VDS/Components/Buttons/ButtonBase.swift b/VDS/Components/Buttons/ButtonBase.swift index 720fca36..eebf3a68 100644 --- a/VDS/Components/Buttons/ButtonBase.swift +++ b/VDS/Components/Buttons/ButtonBase.swift @@ -114,11 +114,6 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable { titleLabel?.adjustsFontSizeToFitWidth = false titleLabel?.lineBreakMode = .byTruncatingTail titleLabel?.numberOfLines = 1 - - bridge_accessibilityLabelBlock = { [weak self] in - guard let self else { return nil } - return text - } } open func updateView() { @@ -182,7 +177,6 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable { //-------------------------------------------------- open var accessibilityAction: ((ButtonBase) -> Void)? - private var _isAccessibilityElement: Bool = false open override var isAccessibilityElement: Bool { get { var block: AXBoolReturnBlock? @@ -198,15 +192,14 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable { if let block { return block() } else { - return _isAccessibilityElement + return super.isAccessibilityElement } } set { - _isAccessibilityElement = newValue + super.isAccessibilityElement = newValue } } - private var _accessibilityLabel: String? open override var accessibilityLabel: String? { get { var block: AXStringReturnBlock? @@ -221,15 +214,14 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable { if let block { return block() } else { - return _accessibilityLabel + return super.accessibilityLabel } } set { - _accessibilityLabel = newValue + super.accessibilityLabel = newValue } } - private var _accessibilityHint: String? open override var accessibilityHint: String? { get { var block: AXStringReturnBlock? @@ -244,15 +236,14 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable { if let block { return block() } else { - return _accessibilityHint + return super.accessibilityHint } } set { - _accessibilityHint = newValue + super.accessibilityHint = newValue } } - private var _accessibilityValue: String? open override var accessibilityValue: String? { get { var block: AXStringReturnBlock? @@ -268,11 +259,11 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable { if let block{ return block() } else { - return _accessibilityValue + return super.accessibilityValue } } set { - _accessibilityValue = newValue + super.accessibilityValue = newValue } } diff --git a/VDS/Components/Label/Label.swift b/VDS/Components/Label/Label.swift index dccc0353..e408953b 100644 --- a/VDS/Components/Label/Label.swift +++ b/VDS/Components/Label/Label.swift @@ -214,10 +214,6 @@ open class Label: UILabel, ViewProtocol, UserInfoable { } open func setup() { - bridge_accessibilityLabelBlock = { [weak self] in - guard let self else { return "" } - return text - } } open func reset() { @@ -498,7 +494,6 @@ open class Label: UILabel, ViewProtocol, UserInfoable { open var accessibilityAction: ((Label) -> Void)? - private var _isAccessibilityElement: Bool = false open override var isAccessibilityElement: Bool { get { var block: AXBoolReturnBlock? @@ -514,15 +509,14 @@ open class Label: UILabel, ViewProtocol, UserInfoable { if let block { return block() } else { - return _isAccessibilityElement + return super.isAccessibilityElement } } set { - _isAccessibilityElement = newValue + super.isAccessibilityElement = newValue } } - private var _accessibilityLabel: String? open override var accessibilityLabel: String? { get { var block: AXStringReturnBlock? @@ -537,15 +531,14 @@ open class Label: UILabel, ViewProtocol, UserInfoable { if let block { return block() } else { - return _accessibilityLabel + return super.accessibilityLabel } } set { - _accessibilityLabel = newValue + super.accessibilityLabel = newValue } } - private var _accessibilityHint: String? open override var accessibilityHint: String? { get { var block: AXStringReturnBlock? @@ -560,15 +553,14 @@ open class Label: UILabel, ViewProtocol, UserInfoable { if let block { return block() } else { - return _accessibilityHint + return super.accessibilityHint } } set { - _accessibilityHint = newValue + super.accessibilityHint = newValue } } - private var _accessibilityValue: String? open override var accessibilityValue: String? { get { var block: AXStringReturnBlock? @@ -584,11 +576,11 @@ open class Label: UILabel, ViewProtocol, UserInfoable { if let block{ return block() } else { - return _accessibilityValue + return super.accessibilityValue } } set { - _accessibilityValue = newValue + super.accessibilityValue = newValue } } diff --git a/VDS/Components/TextFields/InputField/TextField.swift b/VDS/Components/TextFields/InputField/TextField.swift index 0108c874..05dc30f2 100644 --- a/VDS/Components/TextFields/InputField/TextField.swift +++ b/VDS/Components/TextFields/InputField/TextField.swift @@ -236,7 +236,6 @@ open class TextField: UITextField, ViewProtocol, Errorable { //-------------------------------------------------- open var accessibilityAction: ((TextField) -> Void)? - private var _isAccessibilityElement: Bool = false open override var isAccessibilityElement: Bool { get { var block: AXBoolReturnBlock? @@ -252,15 +251,14 @@ open class TextField: UITextField, ViewProtocol, Errorable { if let block { return block() } else { - return _isAccessibilityElement + return super.isAccessibilityElement } } set { - _isAccessibilityElement = newValue + super.isAccessibilityElement = newValue } } - private var _accessibilityLabel: String? open override var accessibilityLabel: String? { get { var block: AXStringReturnBlock? @@ -275,15 +273,14 @@ open class TextField: UITextField, ViewProtocol, Errorable { if let block { return block() } else { - return _accessibilityLabel + return super.accessibilityLabel } } set { - _accessibilityLabel = newValue + super.accessibilityLabel = newValue } } - private var _accessibilityHint: String? open override var accessibilityHint: String? { get { var block: AXStringReturnBlock? @@ -298,15 +295,14 @@ open class TextField: UITextField, ViewProtocol, Errorable { if let block { return block() } else { - return _accessibilityHint + return super.accessibilityHint } } set { - _accessibilityHint = newValue + super.accessibilityHint = newValue } } - private var _accessibilityValue: String? open override var accessibilityValue: String? { get { var block: AXStringReturnBlock? @@ -322,11 +318,11 @@ open class TextField: UITextField, ViewProtocol, Errorable { if let block{ return block() } else { - return _accessibilityValue + return super.accessibilityValue } } set { - _accessibilityValue = newValue + super.accessibilityValue = newValue } } diff --git a/VDS/Components/TextFields/TextArea/TextView.swift b/VDS/Components/TextFields/TextArea/TextView.swift index ed622f90..54fa9452 100644 --- a/VDS/Components/TextFields/TextArea/TextView.swift +++ b/VDS/Components/TextFields/TextArea/TextView.swift @@ -151,7 +151,6 @@ open class TextView: UITextView, ViewProtocol, Errorable { //-------------------------------------------------- open var accessibilityAction: ((TextView) -> Void)? - private var _isAccessibilityElement: Bool = false open override var isAccessibilityElement: Bool { get { var block: AXBoolReturnBlock? @@ -167,15 +166,14 @@ open class TextView: UITextView, ViewProtocol, Errorable { if let block { return block() } else { - return _isAccessibilityElement + return super.isAccessibilityElement } } set { - _isAccessibilityElement = newValue + super.isAccessibilityElement = newValue } } - private var _accessibilityLabel: String? open override var accessibilityLabel: String? { get { var block: AXStringReturnBlock? @@ -190,15 +188,14 @@ open class TextView: UITextView, ViewProtocol, Errorable { if let block { return block() } else { - return _accessibilityLabel + return super.accessibilityLabel } } set { - _accessibilityLabel = newValue + super.accessibilityLabel = newValue } } - private var _accessibilityHint: String? open override var accessibilityHint: String? { get { var block: AXStringReturnBlock? @@ -213,15 +210,14 @@ open class TextView: UITextView, ViewProtocol, Errorable { if let block { return block() } else { - return _accessibilityHint + return super.accessibilityHint } } set { - _accessibilityHint = newValue + super.accessibilityHint = newValue } } - private var _accessibilityValue: String? open override var accessibilityValue: String? { get { var block: AXStringReturnBlock? @@ -237,11 +233,11 @@ open class TextView: UITextView, ViewProtocol, Errorable { if let block{ return block() } else { - return _accessibilityValue + return super.accessibilityValue } } set { - _accessibilityValue = newValue + super.accessibilityValue = newValue } } From a432534e1a095953c1336003460abcb9d51dc87a Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 26 Jun 2024 13:28:39 -0500 Subject: [PATCH 68/83] updated constraints Signed-off-by: Matt Bruce --- .../TileContainer/TileContainer.swift | 49 ++++++++++++------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/VDS/Components/TileContainer/TileContainer.swift b/VDS/Components/TileContainer/TileContainer.swift index b6014e8f..3e3b2ddf 100644 --- a/VDS/Components/TileContainer/TileContainer.swift +++ b/VDS/Components/TileContainer/TileContainer.swift @@ -179,10 +179,12 @@ open class TileContainerBase: Control where Padding //-------------------------------------------------- internal var widthConstraint: NSLayoutConstraint? internal var heightConstraint: NSLayoutConstraint? - - internal var containerViewBottomConstraint: NSLayoutConstraint? + + internal var containerViewCenterXConstraint: NSLayoutConstraint? + internal var containerViewLeadingConstraint: NSLayoutConstraint? + internal var containerViewLeadingGreaterThanOrEqualConstraint: NSLayoutConstraint? + internal var containerViewTrailingConstraint: NSLayoutConstraint? - internal var containerViewBottomLessThanOrEqualConstraint: NSLayoutConstraint? internal var containerViewTrailingLessThanOrEqualConstraint: NSLayoutConstraint? internal var contentViewTopConstraint: NSLayoutConstraint? @@ -233,14 +235,15 @@ open class TileContainerBase: Control where Padding containerView .pinTop() - .pinLeading() + .pinBottom() + containerViewCenterXConstraint = containerView.pinCenterX(anchor: centerXAnchor)?.deactivate() + containerViewLeadingConstraint = containerView.pinLeading(anchor: leadingAnchor)?.deactivate() + containerViewLeadingGreaterThanOrEqualConstraint = containerView.pinLeadingGreaterThanOrEqualTo(anchor: leadingAnchor)?.deactivate() + containerViewTrailingConstraint = containerView.pinTrailing(anchor: trailingAnchor)?.deactivate() - containerViewBottomConstraint = containerView.pinBottom(anchor: bottomAnchor)?.deactivate() - containerViewTrailingLessThanOrEqualConstraint = containerView.pinTrailingLessThanOrEqualTo(anchor: trailingAnchor)?.deactivate() - containerViewBottomLessThanOrEqualConstraint = containerView.pinBottomLessThanOrEqualTo(anchor: bottomAnchor)?.deactivate() - + backgroundImageView.pinToSuperView() widthConstraint = containerView.widthAnchor.constraint(equalToConstant: 0).deactivate() @@ -315,31 +318,40 @@ open class TileContainerBase: Control where Padding //deactivate everything widthConstraint?.deactivate() heightConstraint?.deactivate() + + containerViewCenterXConstraint?.deactivate() + + containerViewLeadingGreaterThanOrEqualConstraint?.deactivate() + containerViewLeadingConstraint?.deactivate() + containerViewTrailingLessThanOrEqualConstraint?.deactivate() - containerViewTrailingLessThanOrEqualConstraint?.deactivate() - containerViewBottomConstraint?.deactivate() containerViewTrailingConstraint?.deactivate() + print("Width: \(width?.description ?? "none")") + print("Height: \(height?.description ?? "none")") + //run logic to determine which to activate if let width, aspectRatio == .none && height == nil{ widthConstraint?.constant = width widthConstraint?.activate() - containerViewTrailingConstraint?.activate() - containerViewBottomConstraint?.activate() + containerViewCenterXConstraint?.activate() + containerViewLeadingGreaterThanOrEqualConstraint?.activate() + containerViewTrailingLessThanOrEqualConstraint?.activate() } else if let height, aspectRatio == .none && width == nil{ heightConstraint?.constant = height heightConstraint?.activate() + containerViewLeadingConstraint?.activate() containerViewTrailingConstraint?.activate() - containerViewBottomConstraint?.activate() } else if let height, let width { widthConstraint?.constant = width heightConstraint?.constant = height heightConstraint?.activate() widthConstraint?.activate() + containerViewCenterXConstraint?.activate() + containerViewLeadingGreaterThanOrEqualConstraint?.activate() containerViewTrailingLessThanOrEqualConstraint?.activate() - containerViewBottomLessThanOrEqualConstraint?.activate() } else if let width { let size = ratioSize(for: width) @@ -347,22 +359,23 @@ open class TileContainerBase: Control where Padding heightConstraint?.constant = size.height widthConstraint?.activate() heightConstraint?.activate() + containerViewCenterXConstraint?.activate() + containerViewLeadingGreaterThanOrEqualConstraint?.activate() containerViewTrailingLessThanOrEqualConstraint?.activate() - containerViewBottomLessThanOrEqualConstraint?.activate() } else if let height { let size = ratioSize(for: height) - //enforce exact height/width widthConstraint?.constant = size.width heightConstraint?.constant = size.height widthConstraint?.activate() heightConstraint?.activate() + containerViewCenterXConstraint?.activate() + containerViewLeadingGreaterThanOrEqualConstraint?.activate() containerViewTrailingLessThanOrEqualConstraint?.activate() - containerViewBottomLessThanOrEqualConstraint?.activate() } else { //set to the parent view - containerViewBottomConstraint?.activate() + containerViewLeadingConstraint?.activate() containerViewTrailingConstraint?.activate() } From db3ba4f1a1726d869d5fc1dabffbe10e6895066e Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 26 Jun 2024 13:29:45 -0500 Subject: [PATCH 69/83] reset to start with none Signed-off-by: Matt Bruce --- VDS/Components/TileContainer/TileContainer.swift | 4 ++-- VDS/Components/Tilelet/Tilelet.swift | 9 ++------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/VDS/Components/TileContainer/TileContainer.swift b/VDS/Components/TileContainer/TileContainer.swift index 3e3b2ddf..d2425464 100644 --- a/VDS/Components/TileContainer/TileContainer.swift +++ b/VDS/Components/TileContainer/TileContainer.swift @@ -126,7 +126,7 @@ open class TileContainerBase: Control where Padding } /// This controls the aspect ratio for the component. - open var aspectRatio: AspectRatio = .ratio1x1 { didSet { setNeedsUpdate() } } + open var aspectRatio: AspectRatio = .none { didSet { setNeedsUpdate() } } /// Sets the background color for the component. open var color: BackgroundColor? { didSet { setNeedsUpdate() } } @@ -290,7 +290,7 @@ open class TileContainerBase: Control where Padding super.reset() shouldUpdateView = false color = .white - aspectRatio = .ratio1x1 + aspectRatio = .none imageFallbackColor = .light width = nil height = nil diff --git a/VDS/Components/Tilelet/Tilelet.swift b/VDS/Components/Tilelet/Tilelet.swift index 95acf895..0a7178e2 100644 --- a/VDS/Components/Tilelet/Tilelet.swift +++ b/VDS/Components/Tilelet/Tilelet.swift @@ -302,7 +302,6 @@ open class Tilelet: TileContainerBase { /// 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() - aspectRatio = .none color = .black addContentView(stackView) @@ -386,7 +385,6 @@ open class Tilelet: TileContainerBase { /// Resets to default settings. open override func reset() { shouldUpdateView = false - aspectRatio = .none color = .black //models badgeModel = nil @@ -405,11 +403,7 @@ open class Tilelet: TileContainerBase { updateBadge() updateTitleLockup() updateIcons() - ///Content-driven height Tilelets - Minimum height is configurable. - ///if width != nil && (aspectRatio != .none || height != nil) then tilelet is not self growing, so we can apply text position alignments. - if width != nil && (aspectRatio != .none || height != nil) { - updateTextPositionAlignment() - } + updateTextPositionAlignment() setNeedsLayout() } @@ -584,6 +578,7 @@ open class Tilelet: TileContainerBase { } private func updateTextPositionAlignment() { + guard width != nil && (aspectRatio != .none || height != nil) else { return } switch textPostion { case .top: titleLockupTopConstraint?.activate() From bd908aeb471559b44a03cc3a22a0d93774a9177b Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 26 Jun 2024 13:31:29 -0500 Subject: [PATCH 70/83] removed logic Signed-off-by: Matt Bruce --- VDS/Components/TileContainer/TileContainer.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/VDS/Components/TileContainer/TileContainer.swift b/VDS/Components/TileContainer/TileContainer.swift index d2425464..ad356065 100644 --- a/VDS/Components/TileContainer/TileContainer.swift +++ b/VDS/Components/TileContainer/TileContainer.swift @@ -326,10 +326,7 @@ open class TileContainerBase: Control where Padding containerViewTrailingLessThanOrEqualConstraint?.deactivate() containerViewTrailingConstraint?.deactivate() - - print("Width: \(width?.description ?? "none")") - print("Height: \(height?.description ?? "none")") - + //run logic to determine which to activate if let width, aspectRatio == .none && height == nil{ widthConstraint?.constant = width From 0f368b5132fb0108b387092c6fd1e8d0d31c01c4 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 26 Jun 2024 15:46:37 -0500 Subject: [PATCH 71/83] updated constraints Signed-off-by: Matt Bruce --- VDS/Components/TileContainer/TileContainer.swift | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/VDS/Components/TileContainer/TileContainer.swift b/VDS/Components/TileContainer/TileContainer.swift index ad356065..c2d03aea 100644 --- a/VDS/Components/TileContainer/TileContainer.swift +++ b/VDS/Components/TileContainer/TileContainer.swift @@ -180,7 +180,6 @@ open class TileContainerBase: Control where Padding internal var widthConstraint: NSLayoutConstraint? internal var heightConstraint: NSLayoutConstraint? - internal var containerViewCenterXConstraint: NSLayoutConstraint? internal var containerViewLeadingConstraint: NSLayoutConstraint? internal var containerViewLeadingGreaterThanOrEqualConstraint: NSLayoutConstraint? @@ -235,9 +234,9 @@ open class TileContainerBase: Control where Padding containerView .pinTop() + .pinCenterX() .pinBottom() - containerViewCenterXConstraint = containerView.pinCenterX(anchor: centerXAnchor)?.deactivate() containerViewLeadingConstraint = containerView.pinLeading(anchor: leadingAnchor)?.deactivate() containerViewLeadingGreaterThanOrEqualConstraint = containerView.pinLeadingGreaterThanOrEqualTo(anchor: leadingAnchor)?.deactivate() @@ -318,9 +317,7 @@ open class TileContainerBase: Control where Padding //deactivate everything widthConstraint?.deactivate() heightConstraint?.deactivate() - - containerViewCenterXConstraint?.deactivate() - + containerViewLeadingGreaterThanOrEqualConstraint?.deactivate() containerViewLeadingConstraint?.deactivate() @@ -331,7 +328,6 @@ open class TileContainerBase: Control where Padding if let width, aspectRatio == .none && height == nil{ widthConstraint?.constant = width widthConstraint?.activate() - containerViewCenterXConstraint?.activate() containerViewLeadingGreaterThanOrEqualConstraint?.activate() containerViewTrailingLessThanOrEqualConstraint?.activate() @@ -346,7 +342,6 @@ open class TileContainerBase: Control where Padding heightConstraint?.constant = height heightConstraint?.activate() widthConstraint?.activate() - containerViewCenterXConstraint?.activate() containerViewLeadingGreaterThanOrEqualConstraint?.activate() containerViewTrailingLessThanOrEqualConstraint?.activate() @@ -356,7 +351,6 @@ open class TileContainerBase: Control where Padding heightConstraint?.constant = size.height widthConstraint?.activate() heightConstraint?.activate() - containerViewCenterXConstraint?.activate() containerViewLeadingGreaterThanOrEqualConstraint?.activate() containerViewTrailingLessThanOrEqualConstraint?.activate() @@ -366,7 +360,6 @@ open class TileContainerBase: Control where Padding heightConstraint?.constant = size.height widthConstraint?.activate() heightConstraint?.activate() - containerViewCenterXConstraint?.activate() containerViewLeadingGreaterThanOrEqualConstraint?.activate() containerViewTrailingLessThanOrEqualConstraint?.activate() From 742bae9169e9f95ff3a6fd3f427f7062bda82dab Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 26 Jun 2024 17:19:17 -0500 Subject: [PATCH 72/83] Signed-off-by: Matt Bruce --- VDS/Components/TileContainer/TileContainer.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/VDS/Components/TileContainer/TileContainer.swift b/VDS/Components/TileContainer/TileContainer.swift index c2d03aea..03e320e7 100644 --- a/VDS/Components/TileContainer/TileContainer.swift +++ b/VDS/Components/TileContainer/TileContainer.swift @@ -364,7 +364,6 @@ open class TileContainerBase: Control where Padding containerViewTrailingLessThanOrEqualConstraint?.activate() } else { - //set to the parent view containerViewLeadingConstraint?.activate() containerViewTrailingConstraint?.activate() } From 5fcc59d8af7bff1bbacd6e62a82176001509595e Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 27 Jun 2024 09:48:52 -0500 Subject: [PATCH 73/83] added helpers methods to layoutconstrainable Signed-off-by: Matt Bruce --- VDS/Protocols/LayoutConstraintable.swift | 124 +++++++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/VDS/Protocols/LayoutConstraintable.swift b/VDS/Protocols/LayoutConstraintable.swift index 3e40885b..3661bae0 100644 --- a/VDS/Protocols/LayoutConstraintable.swift +++ b/VDS/Protocols/LayoutConstraintable.swift @@ -631,6 +631,130 @@ extension LayoutConstraintable { return centerYAnchor.constraint(greaterThanOrEqualTo: found, constant: -constant).with { $0.priority = priority; $0.isActive = true } } } + +// alignment +public enum LayoutAlignment: String, CaseIterable { + case fill + case leading + case top + case center + case trailing + case bottom +} + +public enum LayoutDistribution: String, CaseIterable { + case fill + case fillProportionally +} + +extension LayoutConstraintable { + public func removeConstraints() { + guard let view = self as? UIView, let superview = view.superview else { return } + + // Remove all existing constraints on the containerView + let superviewConstraints = superview.constraints + for constraint in superviewConstraints { + if constraint.firstItem as? UIView == view + || constraint.secondItem as? UIView == view { + superview.removeConstraint(constraint) + } + } + } + + public func applyAlignment(_ alignment: LayoutAlignment, edges: UIEdgeInsets = UIEdgeInsets.zero) { + guard let superview = superview else { return } + + removeConstraints() + + switch alignment { + case .fill: + pinToSuperView(edges) + + case .leading: + pinTop(edges.top) + pinLeading(edges.left) + pinTrailingLessThanOrEqualTo(anchor: superview.trailingAnchor, constant: edges.right) + pinBottom(edges.bottom) + + case .trailing: + pinTop(edges.top) + pinLeadingGreaterThanOrEqualTo(anchor: superview.leadingAnchor, constant: edges.left) + pinTrailing(edges.right) + pinBottom(edges.bottom) + + case .top: + pinTop(edges.top) + pinLeadingGreaterThanOrEqualTo(anchor: superview.leadingAnchor, constant: edges.left) + pinTrailingLessThanOrEqualTo(anchor: superview.trailingAnchor, constant: edges.right) + pinBottomLessThanOrEqualTo(anchor: superview.bottomAnchor, constant: edges.bottom) + + case .bottom: + pinTopGreaterThanOrEqualTo(anchor: superview.topAnchor, constant: edges.top) + pinLeadingGreaterThanOrEqualTo(anchor: superview.leadingAnchor, constant: edges.left) + pinTrailingLessThanOrEqualTo(anchor: superview.trailingAnchor, constant: edges.right) + pinBottom(edges.bottom) + + case .center: + pinCenterX() + pinTop(edges.top) + pinLeadingGreaterThanOrEqualTo(anchor: superview.leadingAnchor, constant: edges.left) + pinTrailingLessThanOrEqualTo(anchor: superview.trailingAnchor, constant: edges.right) + pinBottom(edges.bottom) + + } + } + + // Method to check if the view is pinned to its superview + public func isPinnedToSuperview() -> Bool { + isPinnedVerticallyToSuperview() && isPinnedHorizontallyToSuperview() + } + + public func isPinnedHorizontallyToSuperview() -> Bool { + guard let view = self as? UIView, let superview = view.superview else { return false } + let constraints = superview.constraints + var leadingPinned = false + var trailingPinned = false + + for constraint in constraints { + if (constraint.firstItem as? UIView == view && constraint.firstAttribute == .leading && constraint.relation == .equal && constraint.secondItem as? UIView == superview) || + (constraint.secondItem as? UIView == view && constraint.secondAttribute == .leading && constraint.relation == .equal && constraint.firstItem as? UIView == superview) || + (constraint.firstItem as? UIView == view && constraint.firstAttribute == .left && constraint.relation == .equal && constraint.secondItem as? UIView == superview) || + (constraint.secondItem as? UIView == view && constraint.secondAttribute == .left && constraint.relation == .equal && constraint.firstItem as? UIView == superview) { + leadingPinned = true + } + if (constraint.firstItem as? UIView == view && constraint.firstAttribute == .trailing && constraint.relation == .equal && constraint.secondItem as? UIView == superview) || + (constraint.secondItem as? UIView == view && constraint.secondAttribute == .trailing && constraint.relation == .equal && constraint.firstItem as? UIView == superview) || + (constraint.firstItem as? UIView == view && constraint.firstAttribute == .right && constraint.relation == .equal && constraint.secondItem as? UIView == superview) || + (constraint.secondItem as? UIView == view && constraint.secondAttribute == .right && constraint.relation == .equal && constraint.firstItem as? UIView == superview) { + trailingPinned = true + } + } + + return leadingPinned && trailingPinned + } + + public func isPinnedVerticallyToSuperview() -> Bool { + guard let view = self as? UIView, let superview = view.superview else { return false } + let constraints = superview.constraints + var topPinned = false + var bottomPinned = false + + for constraint in constraints { + if (constraint.firstItem as? UIView == view && constraint.firstAttribute == .top && constraint.relation == .equal && constraint.secondItem as? UIView == superview) || + (constraint.secondItem as? UIView == view && constraint.secondAttribute == .top && constraint.relation == .equal && constraint.firstItem as? UIView == superview) { + topPinned = true + } + if (constraint.firstItem as? UIView == view && constraint.firstAttribute == .bottom && constraint.relation == .equal && constraint.secondItem as? UIView == superview) || + (constraint.secondItem as? UIView == view && constraint.secondAttribute == .bottom && constraint.relation == .equal && constraint.firstItem as? UIView == superview) { + bottomPinned = true + } + } + + return topPinned && bottomPinned + } +} + + //-------------------------------------------------- // MARK: - Implementations //-------------------------------------------------- From 289353a4432dbb774122f8ff75a5237e6eaf4a83 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 27 Jun 2024 09:55:37 -0500 Subject: [PATCH 74/83] refactored to final fix, hopefully :D Signed-off-by: Matt Bruce --- .../TileContainer/TileContainer.swift | 162 ++++++++---------- 1 file changed, 75 insertions(+), 87 deletions(-) diff --git a/VDS/Components/TileContainer/TileContainer.swift b/VDS/Components/TileContainer/TileContainer.swift index 03e320e7..9b121aa1 100644 --- a/VDS/Components/TileContainer/TileContainer.swift +++ b/VDS/Components/TileContainer/TileContainer.swift @@ -44,6 +44,7 @@ open class TileContainer: TileContainerBase { } open class TileContainerBase: Control where PaddingType.ValueType == CGFloat { + //-------------------------------------------------- // MARK: - Initializers //-------------------------------------------------- @@ -109,7 +110,16 @@ open class TileContainerBase: Control where Padding $0.clipsToBounds = true } - internal var containerView = View() + open var containerView = View().with { + $0.setContentHuggingPriority(.defaultLow, for: .horizontal) + $0.setContentHuggingPriority(.defaultLow, for: .vertical) + $0.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + $0.setContentCompressionResistancePriority(.defaultHigh, for: .vertical) + } + + open var distribution: LayoutDistribution = .fill { didSet { setNeedsUpdate() } } + + open var alignment: LayoutAlignment = .center { didSet { setNeedsUpdate() } } //-------------------------------------------------- // MARK: - Public Properties @@ -180,12 +190,6 @@ open class TileContainerBase: Control where Padding internal var widthConstraint: NSLayoutConstraint? internal var heightConstraint: NSLayoutConstraint? - internal var containerViewLeadingConstraint: NSLayoutConstraint? - internal var containerViewLeadingGreaterThanOrEqualConstraint: NSLayoutConstraint? - - internal var containerViewTrailingConstraint: NSLayoutConstraint? - internal var containerViewTrailingLessThanOrEqualConstraint: NSLayoutConstraint? - internal var contentViewTopConstraint: NSLayoutConstraint? internal var contentViewBottomConstraint: NSLayoutConstraint? internal var contentViewLeadingConstraint: NSLayoutConstraint? @@ -232,17 +236,6 @@ open class TileContainerBase: Control where Padding containerView.addSubview(contentView) containerView.addSubview(highlightView) - containerView - .pinTop() - .pinCenterX() - .pinBottom() - - containerViewLeadingConstraint = containerView.pinLeading(anchor: leadingAnchor)?.deactivate() - containerViewLeadingGreaterThanOrEqualConstraint = containerView.pinLeadingGreaterThanOrEqualTo(anchor: leadingAnchor)?.deactivate() - - containerViewTrailingConstraint = containerView.pinTrailing(anchor: trailingAnchor)?.deactivate() - containerViewTrailingLessThanOrEqualConstraint = containerView.pinTrailingLessThanOrEqualTo(anchor: trailingAnchor)?.deactivate() - backgroundImageView.pinToSuperView() widthConstraint = containerView.widthAnchor.constraint(equalToConstant: 0).deactivate() @@ -313,70 +306,10 @@ open class TileContainerBase: Control where Padding contentViewLeadingConstraint?.constant = padding.value contentViewBottomConstraint?.constant = padding.value contentViewTrailingConstraint?.constant = padding.value - - //deactivate everything - widthConstraint?.deactivate() - heightConstraint?.deactivate() - containerViewLeadingGreaterThanOrEqualConstraint?.deactivate() - containerViewLeadingConstraint?.deactivate() - - containerViewTrailingLessThanOrEqualConstraint?.deactivate() - containerViewTrailingConstraint?.deactivate() - - //run logic to determine which to activate - if let width, aspectRatio == .none && height == nil{ - widthConstraint?.constant = width - widthConstraint?.activate() - containerViewLeadingGreaterThanOrEqualConstraint?.activate() - containerViewTrailingLessThanOrEqualConstraint?.activate() - - } else if let height, aspectRatio == .none && width == nil{ - heightConstraint?.constant = height - heightConstraint?.activate() - containerViewLeadingConstraint?.activate() - containerViewTrailingConstraint?.activate() - - } else if let height, let width { - widthConstraint?.constant = width - heightConstraint?.constant = height - heightConstraint?.activate() - widthConstraint?.activate() - containerViewLeadingGreaterThanOrEqualConstraint?.activate() - containerViewTrailingLessThanOrEqualConstraint?.activate() - - } else if let width { - let size = ratioSize(for: width) - widthConstraint?.constant = size.width - heightConstraint?.constant = size.height - widthConstraint?.activate() - heightConstraint?.activate() - containerViewLeadingGreaterThanOrEqualConstraint?.activate() - containerViewTrailingLessThanOrEqualConstraint?.activate() - - } else if let height { - let size = ratioSize(for: height) - widthConstraint?.constant = size.width - heightConstraint?.constant = size.height - widthConstraint?.activate() - heightConstraint?.activate() - containerViewLeadingGreaterThanOrEqualConstraint?.activate() - containerViewTrailingLessThanOrEqualConstraint?.activate() - - } else { - containerViewLeadingConstraint?.activate() - containerViewTrailingConstraint?.activate() - } - - applyBackgroundEffects() - - if showDropShadow, surface == .light { - containerView.addDropShadow(dropShadowConfiguration) - } else { - containerView.removeDropShadows() - } + updateContainerView() } - + open override var accessibilityElements: [Any]? { get { var items = [Any]() @@ -403,13 +336,6 @@ open class TileContainerBase: Control where Padding set {} } - /// Used to update frames for the added CAlayers to our view - open override func layoutSubviews() { - super.layoutSubviews() - containerView.dropShadowLayers?.forEach { $0.frame = bounds } - containerView.gradientLayers?.forEach { $0.frame = bounds } - } - //-------------------------------------------------- // MARK: - Public Methods //-------------------------------------------------- @@ -485,7 +411,69 @@ open class TileContainerBase: Control where Padding return CGSize(width: width, height: height) } + + private func sizeContainerView(width: CGFloat? = nil, height: CGFloat? = nil) { + if let width, width > 0 { + widthConstraint?.constant = width + widthConstraint?.activate() + } + + if let height, height > 0 { + heightConstraint?.constant = height + heightConstraint?.activate() + } + } + + private func updateContainerView() { + applyBackgroundEffects() + + widthConstraint?.deactivate() + heightConstraint?.deactivate() + if showDropShadow, surface == .light { + containerView.addDropShadow(dropShadowConfiguration) + } else { + containerView.removeDropShadows() + } + + containerView.dropShadowLayers?.forEach { $0.frame = bounds } + containerView.gradientLayers?.forEach { $0.frame = bounds } + + if width != nil || height != nil { + var containerViewWidth: CGFloat? + var containerViewHeight: CGFloat? + //run logic to determine which to activate + if let width, aspectRatio == .none && height == nil{ + containerViewWidth = width + + } else if let height, aspectRatio == .none && width == nil{ + containerViewHeight = height + + } else if let height, let width { + containerViewWidth = width + containerViewHeight = height + + } else if let width { + let size = ratioSize(for: width) + containerViewWidth = size.width + containerViewHeight = size.height + + } else if let height { + let size = ratioSize(for: height) + containerViewWidth = size.width + containerViewHeight = size.height + } + + sizeContainerView(width: containerViewWidth, height: containerViewHeight) + containerView.applyAlignment(alignment) + + } else { + containerView.applyAlignment(distribution == .fill ? .fill : alignment) + if let superview, distribution == .fill, !isPinnedHorizontallyToSuperview() { + sizeContainerView(width: superview.frame.size.width) + } + } + } } extension TileContainerBase { From 1044282c33f8c90ee757736ad8fa3071eb5afb65 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 27 Jun 2024 10:24:18 -0500 Subject: [PATCH 75/83] fixed bug Signed-off-by: Matt Bruce --- VDS/Components/TileContainer/TileContainer.swift | 6 +++--- VDS/Protocols/LayoutConstraintable.swift | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/VDS/Components/TileContainer/TileContainer.swift b/VDS/Components/TileContainer/TileContainer.swift index 9b121aa1..72ef86d0 100644 --- a/VDS/Components/TileContainer/TileContainer.swift +++ b/VDS/Components/TileContainer/TileContainer.swift @@ -436,9 +436,9 @@ open class TileContainerBase: Control where Padding containerView.removeDropShadows() } - containerView.dropShadowLayers?.forEach { $0.frame = bounds } - containerView.gradientLayers?.forEach { $0.frame = bounds } - + containerView.dropShadowLayers?.forEach { $0.frame = containerView.bounds } + containerView.gradientLayers?.forEach { $0.frame = containerView.bounds } + if width != nil || height != nil { var containerViewWidth: CGFloat? var containerViewHeight: CGFloat? diff --git a/VDS/Protocols/LayoutConstraintable.swift b/VDS/Protocols/LayoutConstraintable.swift index 3661bae0..18c89c14 100644 --- a/VDS/Protocols/LayoutConstraintable.swift +++ b/VDS/Protocols/LayoutConstraintable.swift @@ -714,7 +714,7 @@ extension LayoutConstraintable { let constraints = superview.constraints var leadingPinned = false var trailingPinned = false - + for constraint in constraints { if (constraint.firstItem as? UIView == view && constraint.firstAttribute == .leading && constraint.relation == .equal && constraint.secondItem as? UIView == superview) || (constraint.secondItem as? UIView == view && constraint.secondAttribute == .leading && constraint.relation == .equal && constraint.firstItem as? UIView == superview) || From 25ebcff2d904cbf7fb1c83958751ea87af14bc11 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 27 Jun 2024 12:52:38 -0500 Subject: [PATCH 76/83] rotation checking update size Signed-off-by: Matt Bruce --- VDS/Components/TileContainer/TileContainer.swift | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/VDS/Components/TileContainer/TileContainer.swift b/VDS/Components/TileContainer/TileContainer.swift index 72ef86d0..62e96890 100644 --- a/VDS/Components/TileContainer/TileContainer.swift +++ b/VDS/Components/TileContainer/TileContainer.swift @@ -266,6 +266,15 @@ open class TileContainerBase: Control where Padding containerView.bridge_isAccessibilityElementBlock = { [weak self] in self?.onClickSubscriber != nil } containerView.accessibilityHint = "Double tap to open." containerView.accessibilityLabel = nil + + NotificationCenter.default + .publisher(for: UIDevice.orientationDidChangeNotification) + .sink() { [weak self] _ in + DispatchQueue.main.asyncAfter(deadline: .now() + 0.001) { [weak self] in + guard let self else { return } + setNeedsUpdate() + } + }.store(in: &subscribers) } @@ -423,7 +432,7 @@ open class TileContainerBase: Control where Padding heightConstraint?.activate() } } - + private func updateContainerView() { applyBackgroundEffects() @@ -469,8 +478,8 @@ open class TileContainerBase: Control where Padding } else { containerView.applyAlignment(distribution == .fill ? .fill : alignment) - if let superview, distribution == .fill, !isPinnedHorizontallyToSuperview() { - sizeContainerView(width: superview.frame.size.width) + if distribution == .fill, !isPinnedHorizontallyToSuperview(), let size = horizontalPinnedSize() { + sizeContainerView(width: size.width) } } } From 8dfcb416914921bb055a5623fbd4ee326be6a423 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 27 Jun 2024 12:52:52 -0500 Subject: [PATCH 77/83] refactor for layoutguide Signed-off-by: Matt Bruce --- VDS/Protocols/LayoutConstraintable.swift | 89 ++++++++++++++++++++---- 1 file changed, 77 insertions(+), 12 deletions(-) diff --git a/VDS/Protocols/LayoutConstraintable.swift b/VDS/Protocols/LayoutConstraintable.swift index 18c89c14..ba08e3c2 100644 --- a/VDS/Protocols/LayoutConstraintable.swift +++ b/VDS/Protocols/LayoutConstraintable.swift @@ -709,6 +709,69 @@ extension LayoutConstraintable { isPinnedVerticallyToSuperview() && isPinnedHorizontallyToSuperview() } + public func horizontalPinnedSize() -> CGSize? { + guard let view = self as? UIView, let superview = view.superview else { return nil } + let constraints = superview.constraints + + var leadingPinnedObject: AnyObject? + var trailingPinnedObject: AnyObject? + + for constraint in constraints { + if (constraint.firstItem === view && (constraint.firstAttribute == .leading || constraint.firstAttribute == .left)) { + leadingPinnedObject = constraint.secondItem as AnyObject? + } else if (constraint.secondItem === view && (constraint.secondAttribute == .leading || constraint.secondAttribute == .left)) { + leadingPinnedObject = constraint.firstItem as AnyObject? + } else if (constraint.firstItem === view && (constraint.firstAttribute == .trailing || constraint.firstAttribute == .right)) { + trailingPinnedObject = constraint.secondItem as AnyObject? + } else if (constraint.secondItem === view && (constraint.secondAttribute == .trailing || constraint.secondAttribute == .right)) { + trailingPinnedObject = constraint.firstItem as AnyObject? + } + } + + // Ensure both leading and trailing pinned objects are identified + if let leadingObject = leadingPinnedObject, let trailingObject = trailingPinnedObject { + + // Calculate the size based on the pinned objects + if let leadingView = leadingObject as? UIView, let trailingView = trailingObject as? UIView { + let leadingPosition = leadingView.convert(leadingView.bounds.origin, to: superview).x + let trailingPosition = trailingView.convert(trailingView.bounds.origin, to: superview).x + trailingView.bounds.width + return CGSize(width: trailingPosition - leadingPosition, height: view.bounds.size.height) + + } else if let leadingGuide = leadingObject as? UILayoutGuide, let trailingGuide = trailingObject as? UILayoutGuide { + let leadingPosition = leadingGuide.layoutFrame.minX + let trailingPosition = trailingGuide.layoutFrame.maxX + return CGSize(width: trailingPosition - leadingPosition, height: view.bounds.size.height) + + } else if let leadingView = leadingObject as? UIView, let trailingGuide = trailingObject as? UILayoutGuide { + let leadingPosition = leadingView.convert(leadingView.bounds.origin, to: superview).x + let trailingPosition = trailingGuide.layoutFrame.maxX + return CGSize(width: trailingPosition - leadingPosition, height: view.bounds.size.height) + + } else if let leadingGuide = leadingObject as? UILayoutGuide, let trailingView = trailingObject as? UIView { + let leadingPosition = leadingGuide.layoutFrame.minX + let trailingPosition = trailingView.convert(trailingView.bounds.origin, to: superview).x + trailingView.bounds.width + return CGSize(width: trailingPosition - leadingPosition, height: view.bounds.size.height) + } + + } else if let pinnedObject = leadingPinnedObject { + if let view = pinnedObject as? UIView { + return view.bounds.size + } else if let layoutGuide = pinnedObject as? UILayoutGuide { + return layoutGuide.layoutFrame.size + } + + } else if let pinnedObject = trailingPinnedObject { + if let view = pinnedObject as? UIView { + return view.bounds.size + } else if let layoutGuide = pinnedObject as? UILayoutGuide { + return layoutGuide.layoutFrame.size + } + + } + + return nil + } + public func isPinnedHorizontallyToSuperview() -> Bool { guard let view = self as? UIView, let superview = view.superview else { return false } let constraints = superview.constraints @@ -716,16 +779,16 @@ extension LayoutConstraintable { var trailingPinned = false for constraint in constraints { - if (constraint.firstItem as? UIView == view && constraint.firstAttribute == .leading && constraint.relation == .equal && constraint.secondItem as? UIView == superview) || - (constraint.secondItem as? UIView == view && constraint.secondAttribute == .leading && constraint.relation == .equal && constraint.firstItem as? UIView == superview) || - (constraint.firstItem as? UIView == view && constraint.firstAttribute == .left && constraint.relation == .equal && constraint.secondItem as? UIView == superview) || - (constraint.secondItem as? UIView == view && constraint.secondAttribute == .left && constraint.relation == .equal && constraint.firstItem as? UIView == superview) { + if (constraint.firstItem as? UIView == view && constraint.firstAttribute == .leading && constraint.relation == .equal) || + (constraint.secondItem as? UIView == view && constraint.secondAttribute == .leading && constraint.relation == .equal) || + (constraint.firstItem as? UIView == view && constraint.firstAttribute == .left && constraint.relation == .equal) || + (constraint.secondItem as? UIView == view && constraint.secondAttribute == .left && constraint.relation == .equal) { leadingPinned = true } - if (constraint.firstItem as? UIView == view && constraint.firstAttribute == .trailing && constraint.relation == .equal && constraint.secondItem as? UIView == superview) || - (constraint.secondItem as? UIView == view && constraint.secondAttribute == .trailing && constraint.relation == .equal && constraint.firstItem as? UIView == superview) || - (constraint.firstItem as? UIView == view && constraint.firstAttribute == .right && constraint.relation == .equal && constraint.secondItem as? UIView == superview) || - (constraint.secondItem as? UIView == view && constraint.secondAttribute == .right && constraint.relation == .equal && constraint.firstItem as? UIView == superview) { + if (constraint.firstItem as? UIView == view && constraint.firstAttribute == .trailing && constraint.relation == .equal) || + (constraint.secondItem as? UIView == view && constraint.secondAttribute == .trailing && constraint.relation == .equal) || + (constraint.firstItem as? UIView == view && constraint.firstAttribute == .right && constraint.relation == .equal) || + (constraint.secondItem as? UIView == view && constraint.secondAttribute == .right && constraint.relation == .equal) { trailingPinned = true } } @@ -740,18 +803,20 @@ extension LayoutConstraintable { var bottomPinned = false for constraint in constraints { - if (constraint.firstItem as? UIView == view && constraint.firstAttribute == .top && constraint.relation == .equal && constraint.secondItem as? UIView == superview) || - (constraint.secondItem as? UIView == view && constraint.secondAttribute == .top && constraint.relation == .equal && constraint.firstItem as? UIView == superview) { + if (constraint.firstItem as? UIView == view && constraint.firstAttribute == .top && constraint.relation == .equal) || + (constraint.secondItem as? UIView == view && constraint.secondAttribute == .top && constraint.relation == .equal) { topPinned = true } - if (constraint.firstItem as? UIView == view && constraint.firstAttribute == .bottom && constraint.relation == .equal && constraint.secondItem as? UIView == superview) || - (constraint.secondItem as? UIView == view && constraint.secondAttribute == .bottom && constraint.relation == .equal && constraint.firstItem as? UIView == superview) { + if (constraint.firstItem as? UIView == view && constraint.firstAttribute == .bottom && constraint.relation == .equal) || + (constraint.secondItem as? UIView == view && constraint.secondAttribute == .bottom && constraint.relation == .equal) { bottomPinned = true } } return topPinned && bottomPinned } + + } From 9db5257132e4d5d1c0c9760c96730a6f9d81c28a Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 27 Jun 2024 13:16:52 -0500 Subject: [PATCH 78/83] align left Signed-off-by: Matt Bruce --- VDS/Components/DatePicker/DatePicker.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/VDS/Components/DatePicker/DatePicker.swift b/VDS/Components/DatePicker/DatePicker.swift index cb2b5b88..17ededf4 100644 --- a/VDS/Components/DatePicker/DatePicker.swift +++ b/VDS/Components/DatePicker/DatePicker.swift @@ -370,7 +370,7 @@ extension DatePicker { var popoverY: CGFloat = 0 // Calculate horizontal position - if sourceFrameInParent.width < popoverWidth { + if sourceFrameInParent.width <= popoverWidth { if sourceFrameInParent.midX - popoverWidth / 2 < 0 { // Align to left popoverX = sourceFrameInParent.minX @@ -382,7 +382,7 @@ extension DatePicker { popoverX = sourceFrameInParent.midX - popoverWidth / 2 } } else { - popoverX = sourceFrameInParent.midX - popoverWidth / 2 + popoverX = sourceFrameInParent.minX //sourceFrameInParent.midX - popoverWidth / 2 } // Ensure the popover is within the parent's bounds horizontally From 7d399a42ef216f5474fd85a561266c9f980f09fa Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 27 Jun 2024 15:10:30 -0500 Subject: [PATCH 79/83] fixed animation Signed-off-by: Matt Bruce --- VDS/Components/Toggle/ToggleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VDS/Components/Toggle/ToggleView.swift b/VDS/Components/Toggle/ToggleView.swift index 935ed519..dc5e9570 100644 --- a/VDS/Components/Toggle/ToggleView.swift +++ b/VDS/Components/Toggle/ToggleView.swift @@ -219,7 +219,7 @@ open class ToggleView: Control, Changeable, FormFieldable { } knobTrailingConstraint?.isActive = true knobLeadingConstraint?.isActive = true - setNeedsLayout() + layoutIfNeeded() } private func updateToggle() { From 532cb6c6809914cca81aec2e6641c15362f83cbb Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 27 Jun 2024 15:10:30 -0500 Subject: [PATCH 80/83] fixed animation Signed-off-by: Matt Bruce --- VDS/Components/Toggle/ToggleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VDS/Components/Toggle/ToggleView.swift b/VDS/Components/Toggle/ToggleView.swift index 935ed519..dc5e9570 100644 --- a/VDS/Components/Toggle/ToggleView.swift +++ b/VDS/Components/Toggle/ToggleView.swift @@ -219,7 +219,7 @@ open class ToggleView: Control, Changeable, FormFieldable { } knobTrailingConstraint?.isActive = true knobLeadingConstraint?.isActive = true - setNeedsLayout() + layoutIfNeeded() } private func updateToggle() { From 99db2d3b1c1ebb48754e334e3ae8eb9a3f61f436 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 27 Jun 2024 13:16:52 -0500 Subject: [PATCH 81/83] align left Signed-off-by: Matt Bruce --- VDS/Components/DatePicker/DatePicker.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/VDS/Components/DatePicker/DatePicker.swift b/VDS/Components/DatePicker/DatePicker.swift index cb2b5b88..17ededf4 100644 --- a/VDS/Components/DatePicker/DatePicker.swift +++ b/VDS/Components/DatePicker/DatePicker.swift @@ -370,7 +370,7 @@ extension DatePicker { var popoverY: CGFloat = 0 // Calculate horizontal position - if sourceFrameInParent.width < popoverWidth { + if sourceFrameInParent.width <= popoverWidth { if sourceFrameInParent.midX - popoverWidth / 2 < 0 { // Align to left popoverX = sourceFrameInParent.minX @@ -382,7 +382,7 @@ extension DatePicker { popoverX = sourceFrameInParent.midX - popoverWidth / 2 } } else { - popoverX = sourceFrameInParent.midX - popoverWidth / 2 + popoverX = sourceFrameInParent.minX //sourceFrameInParent.midX - popoverWidth / 2 } // Ensure the popover is within the parent's bounds horizontally From 842bb6f0ad55c36cf7fadaf5c3ff40205db3e04e Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 28 Jun 2024 18:08:50 -0500 Subject: [PATCH 82/83] updated to work at 100% width of parent if not set as well as calculating height if there is an aspect ration. --- .../TileContainer/TileContainer.swift | 63 +++++++++---------- VDS/Components/Tilelet/Tilelet.swift | 4 ++ 2 files changed, 33 insertions(+), 34 deletions(-) diff --git a/VDS/Components/TileContainer/TileContainer.swift b/VDS/Components/TileContainer/TileContainer.swift index 62e96890..d7c3f74f 100644 --- a/VDS/Components/TileContainer/TileContainer.swift +++ b/VDS/Components/TileContainer/TileContainer.swift @@ -117,10 +117,6 @@ open class TileContainerBase: Control where Padding $0.setContentCompressionResistancePriority(.defaultHigh, for: .vertical) } - open var distribution: LayoutDistribution = .fill { didSet { setNeedsUpdate() } } - - open var alignment: LayoutAlignment = .center { didSet { setNeedsUpdate() } } - //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- @@ -136,7 +132,7 @@ open class TileContainerBase: Control where Padding } /// This controls the aspect ratio for the component. - open var aspectRatio: AspectRatio = .none { didSet { setNeedsUpdate() } } + open var aspectRatio: AspectRatio = .ratio1x1 { didSet { setNeedsUpdate() } } /// Sets the background color for the component. open var color: BackgroundColor? { didSet { setNeedsUpdate() } } @@ -190,11 +186,6 @@ open class TileContainerBase: Control where Padding internal var widthConstraint: NSLayoutConstraint? internal var heightConstraint: NSLayoutConstraint? - internal var contentViewTopConstraint: NSLayoutConstraint? - internal var contentViewBottomConstraint: NSLayoutConstraint? - internal var contentViewLeadingConstraint: NSLayoutConstraint? - internal var contentViewTrailingConstraint: NSLayoutConstraint? - //-------------------------------------------------- // MARK: - Configuration //-------------------------------------------------- @@ -231,15 +222,21 @@ open class TileContainerBase: Control where Padding open override func setup() { super.setup() isAccessibilityElement = false + addSubview(containerView) + containerView.pinToSuperView() + containerView.addSubview(backgroundImageView) - containerView.addSubview(contentView) - containerView.addSubview(highlightView) - backgroundImageView.pinToSuperView() - widthConstraint = containerView.widthAnchor.constraint(equalToConstant: 0).deactivate() - heightConstraint = containerView.heightAnchor.constraint(equalToConstant: 0).deactivate() + containerView.addSubview(contentView) + contentView.pinToSuperView() + + containerView.addSubview(highlightView) + highlightView.pinToSuperView() + + widthConstraint = widthAnchor.constraint(equalToConstant: 0).deactivate() + heightConstraint = heightAnchor.constraint(equalToConstant: 0).deactivate() backgroundImageView.setContentHuggingPriority(.defaultLow, for: .horizontal) backgroundImageView.setContentHuggingPriority(.defaultLow, for: .vertical) @@ -248,19 +245,11 @@ open class TileContainerBase: Control where Padding backgroundImageView.isUserInteractionEnabled = false backgroundImageView.isHidden = true - contentViewTopConstraint = contentView.pinTop(anchor: containerView.topAnchor, constant: padding.value) - contentViewBottomConstraint = containerView.pinBottom(anchor: contentView.bottomAnchor, constant: padding.value) - contentViewLeadingConstraint = contentView.pinLeading(anchor: containerView.leadingAnchor, constant: padding.value) - contentViewTrailingConstraint = containerView.pinTrailing(anchor: contentView.trailingAnchor, constant: padding.value) - - highlightView.pin(containerView) highlightView.isHidden = true highlightView.backgroundColor = .clear //corner radius containerView.layer.cornerRadius = cornerRadius - backgroundImageView.layer.cornerRadius = cornerRadius - highlightView.layer.cornerRadius = cornerRadius containerView.clipsToBounds = true containerView.bridge_isAccessibilityElementBlock = { [weak self] in self?.onClickSubscriber != nil } @@ -311,11 +300,9 @@ open class TileContainerBase: Control where Padding containerView.layer.borderColor = borderColorConfiguration.getColor(self).cgColor containerView.layer.borderWidth = showBorder ? VDSFormControls.borderWidth : 0 - contentViewTopConstraint?.constant = padding.value - contentViewLeadingConstraint?.constant = padding.value - contentViewBottomConstraint?.constant = padding.value - contentViewTrailingConstraint?.constant = padding.value - + contentView.removeConstraints() + contentView.pinToSuperView(.uniform(padding.value)) + updateContainerView() } @@ -472,14 +459,22 @@ open class TileContainerBase: Control where Padding containerViewWidth = size.width containerViewHeight = size.height } - sizeContainerView(width: containerViewWidth, height: containerViewHeight) - containerView.applyAlignment(alignment) - } else { - containerView.applyAlignment(distribution == .fill ? .fill : alignment) - if distribution == .fill, !isPinnedHorizontallyToSuperview(), let size = horizontalPinnedSize() { - sizeContainerView(width: size.width) + if let parentSize = horizontalPinnedSize() { + + var containerViewWidth: CGFloat? + var containerViewHeight: CGFloat? + + let size = ratioSize(for: parentSize.width) + if aspectRatio == .none { + containerViewWidth = size.width + } else { + containerViewWidth = size.width + containerViewHeight = size.height + } + + sizeContainerView(width: containerViewWidth, height: containerViewHeight) } } } diff --git a/VDS/Components/Tilelet/Tilelet.swift b/VDS/Components/Tilelet/Tilelet.swift index 0a7178e2..0da20709 100644 --- a/VDS/Components/Tilelet/Tilelet.swift +++ b/VDS/Components/Tilelet/Tilelet.swift @@ -303,6 +303,8 @@ open class Tilelet: TileContainerBase { open override func setup() { super.setup() color = .black + aspectRatio = .none + addContentView(stackView) //badge @@ -385,6 +387,8 @@ open class Tilelet: TileContainerBase { /// Resets to default settings. open override func reset() { shouldUpdateView = false + super.reset() + aspectRatio = .none color = .black //models badgeModel = nil From 540dc35e2f88e292a2f08711d255fff0a60f6beb Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Sat, 29 Jun 2024 09:45:14 -0500 Subject: [PATCH 83/83] added cornerRadius back Signed-off-by: Matt Bruce --- VDS/Components/TileContainer/TileContainer.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/VDS/Components/TileContainer/TileContainer.swift b/VDS/Components/TileContainer/TileContainer.swift index d7c3f74f..6eeaf316 100644 --- a/VDS/Components/TileContainer/TileContainer.swift +++ b/VDS/Components/TileContainer/TileContainer.swift @@ -250,6 +250,8 @@ open class TileContainerBase: Control where Padding //corner radius containerView.layer.cornerRadius = cornerRadius + backgroundImageView.layer.cornerRadius = cornerRadius + highlightView.layer.cornerRadius = cornerRadius containerView.clipsToBounds = true containerView.bridge_isAccessibilityElementBlock = { [weak self] in self?.onClickSubscriber != nil }