From b302077f994fe98539a389dd5471ca82783ded4e Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 20 Jun 2024 18:23:54 -0500 Subject: [PATCH 01/26] 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 02/26] 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 03/26] 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 04/26] 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 05/26] 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 06/26] 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 07/26] 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 08/26] 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 09/26] 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 10/26] 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 11/26] 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 12/26] 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 13/26] 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 14/26] 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 15/26] 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 16/26] 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 17/26] 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 18/26] 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 19/26] 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 20/26] 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 21/26] 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 22/26] 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 23/26] 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 24/26] 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 25/26] 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 26/26] 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