Merge branch 'mbruce/bugfix' into 'develop'
Bugs merged See merge request BPHV_MIPS/vds_ios!264
This commit is contained in:
commit
f4892918e3
@ -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 */; };
|
||||
@ -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 */; };
|
||||
@ -368,7 +369,7 @@
|
||||
EAC58C222BF2824200BA39FA /* DatePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePicker.swift; sourceTree = "<group>"; };
|
||||
EAC58C242BF2A7FB00BA39FA /* DatePickerChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = DatePickerChangeLog.txt; sourceTree = "<group>"; };
|
||||
EAC58C262BF4116200BA39FA /* DatePickerCalendarModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickerCalendarModel.swift; sourceTree = "<group>"; };
|
||||
EAC58C282BF4118C00BA39FA /* DatePickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickerViewController.swift; sourceTree = "<group>"; };
|
||||
EAC58C282BF4118C00BA39FA /* ClearPopoverViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClearPopoverViewController.swift; sourceTree = "<group>"; };
|
||||
EAC71A1C2A2E155A00E47A9F /* Checkbox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Checkbox.swift; sourceTree = "<group>"; };
|
||||
EAC71A1E2A2E173D00E47A9F /* RadioButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButton.swift; sourceTree = "<group>"; };
|
||||
EAC846F2294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonGroupCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
@ -402,6 +403,7 @@
|
||||
EAF1FE9A29DB1A6000101452 /* Changeable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Changeable.swift; sourceTree = "<group>"; };
|
||||
EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityActionElement.swift; sourceTree = "<group>"; };
|
||||
EAF2F4772C249D72007BFEDC /* AccessibilityUpdatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityUpdatable.swift; sourceTree = "<group>"; };
|
||||
EAF2F4882C2A1075007BFEDC /* AlertViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertViewController.swift; sourceTree = "<group>"; };
|
||||
EAF7F0932899861000B287F5 /* CheckboxItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckboxItem.swift; sourceTree = "<group>"; };
|
||||
EAF7F0992899B17200B287F5 /* CATransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CATransaction.swift; sourceTree = "<group>"; };
|
||||
EAF7F09F289AB7EC00B287F5 /* View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = "<group>"; };
|
||||
@ -746,9 +748,11 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EA985C1C296CD13600F2FF2E /* BundleManager.swift */,
|
||||
EAC58C282BF4118C00BA39FA /* ClearPopoverViewController.swift */,
|
||||
EAF7F0B8289C139800B287F5 /* ColorConfiguration.swift */,
|
||||
EAB5FEF02927F4AA00998C17 /* SelfSizingCollectionView.swift */,
|
||||
EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */,
|
||||
EAF2F4882C2A1075007BFEDC /* AlertViewController.swift */,
|
||||
);
|
||||
path = Classes;
|
||||
sourceTree = "<group>";
|
||||
@ -966,7 +970,6 @@
|
||||
children = (
|
||||
EAC58C222BF2824200BA39FA /* DatePicker.swift */,
|
||||
EAC58C262BF4116200BA39FA /* DatePickerCalendarModel.swift */,
|
||||
EAC58C282BF4118C00BA39FA /* DatePickerViewController.swift */,
|
||||
EAC58C242BF2A7FB00BA39FA /* DatePickerChangeLog.txt */,
|
||||
);
|
||||
path = DatePicker;
|
||||
@ -1304,12 +1307,13 @@
|
||||
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 */,
|
||||
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 */,
|
||||
@ -1531,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;
|
||||
@ -1569,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;
|
||||
|
||||
96
VDS/Classes/AlertViewController.swift
Normal file
96
VDS/Classes/AlertViewController.swift
Normal file
@ -0,0 +1,96 @@
|
||||
//
|
||||
// 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<AnyCancellable>()
|
||||
|
||||
//--------------------------------------------------
|
||||
// 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]
|
||||
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)
|
||||
])
|
||||
}
|
||||
|
||||
open override func touchesBegan(_ touches: Set<UITouch>, 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() {
|
||||
view.backgroundColor = backgroundColorConfiguration.getColor(self).withAlphaComponent(0.3)
|
||||
if var dialog = dialog as? Surfaceable {
|
||||
dialog.surface = surface
|
||||
}
|
||||
}
|
||||
}
|
||||
152
VDS/Classes/ClearPopoverViewController.swift
Normal file
152
VDS/Classes/ClearPopoverViewController.swift
Normal file
@ -0,0 +1,152 @@
|
||||
//
|
||||
// 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 maxWidth: CGFloat?
|
||||
|
||||
open var sourceRect: CGRect?
|
||||
|
||||
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
|
||||
self.sourceRect = sourceRect
|
||||
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<CGRect>, in view: AutoreleasingUnsafeMutablePointer<UIView>) {
|
||||
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, let sourceRect {
|
||||
popoverPresentationController.sourceRect = sourceRect
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
||||
}
|
||||
}
|
||||
@ -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() {
|
||||
|
||||
@ -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
|
||||
//--------------------------------------------------
|
||||
@ -26,12 +26,19 @@ open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopov
|
||||
//--------------------------------------------------
|
||||
/// A callback when the selected option changes. Passes parameters (option).
|
||||
open var onDateSelected: ((Date, DatePicker) -> Void)?
|
||||
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Properties
|
||||
//--------------------------------------------------
|
||||
class Responder: UIView {
|
||||
open override var canBecomeFirstResponder: Bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
internal override var responder: UIResponder? { hiddenView }
|
||||
internal var hiddenView = Responder().with { $0.width(0) }
|
||||
internal var minWidthDefault = 186.0
|
||||
|
||||
internal var bottomStackView: UIStackView = {
|
||||
return UIStackView().with {
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
@ -41,6 +48,33 @@ open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopov
|
||||
$0.spacing = VDSLayout.space2X
|
||||
}
|
||||
}()
|
||||
|
||||
//--------------------------------------------------
|
||||
// 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
|
||||
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
|
||||
@ -87,12 +121,12 @@ open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopov
|
||||
}
|
||||
|
||||
open var dateFormat: DateFormat = .shortNumeric { didSet{ setNeedsUpdate() } }
|
||||
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Configuration Properties
|
||||
//--------------------------------------------------
|
||||
internal override var containerSize: CGSize { CGSize(width: minWidthDefault, height: 44) }
|
||||
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
@ -100,20 +134,29 @@ open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopov
|
||||
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
|
||||
|
||||
// setting color config
|
||||
selectedDateLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable()
|
||||
|
||||
|
||||
// tap gesture
|
||||
containerView
|
||||
.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)
|
||||
|
||||
NotificationCenter.default
|
||||
.publisher(for: UIDevice.orientationDidChangeNotification).sink { [weak self] _ in
|
||||
guard let self else { return }
|
||||
hidePopoverView()
|
||||
}
|
||||
.store(in: &subscribers)
|
||||
|
||||
popoverOverlayView.isHidden = true
|
||||
}
|
||||
|
||||
open override func getFieldContainer() -> UIView {
|
||||
@ -125,9 +168,10 @@ open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopov
|
||||
}
|
||||
controlStackView.addArrangedSubview(calendarIcon)
|
||||
controlStackView.addArrangedSubview(selectedDateLabel)
|
||||
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()
|
||||
@ -140,7 +184,7 @@ open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopov
|
||||
selectedDateLabel.isEnabled = isEnabled
|
||||
calendarIcon.color = iconColorConfiguration.getColor(self)
|
||||
}
|
||||
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
@ -152,32 +196,249 @@ open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopov
|
||||
formatter.dateFormat = dateFormat.format
|
||||
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
|
||||
extension DatePicker {
|
||||
|
||||
private func showPopover() {
|
||||
guard let viewController = UIApplication.topViewController(), var parentView = viewController.view, !popoverVisible else {
|
||||
hidePopoverView()
|
||||
return
|
||||
}
|
||||
if let viewController = UIApplication.topViewController() {
|
||||
viewController.present(calendarVC, animated: true, completion: nil)
|
||||
|
||||
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 = calculatePopoverPosition(relativeTo: containerView,
|
||||
in: parentView,
|
||||
size: popoverViewSize,
|
||||
with: popoverSpacing) {
|
||||
calendar.onChange = { [weak self] control in
|
||||
guard let self else { return }
|
||||
selectedDate = control.selectedDate
|
||||
sendActions(for: .valueChanged)
|
||||
UIAccessibility.post(notification: .layoutChanged, argument: containerView)
|
||||
hidePopoverView()
|
||||
}
|
||||
|
||||
// popoverView container
|
||||
popoverView = UIView()
|
||||
popoverView.backgroundColor = .clear
|
||||
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)
|
||||
popoverVisible = true
|
||||
popoverView.addSubview(calendar)
|
||||
|
||||
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()
|
||||
|
||||
// update containerview
|
||||
_ = responder?.becomeFirstResponder()
|
||||
updateContainerView()
|
||||
|
||||
// animate the calendar to show
|
||||
UIView.animate(withDuration: 0.5,
|
||||
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
|
||||
UIAccessibility.post(notification: .layoutChanged, argument: calendar)
|
||||
parentView.layoutIfNeeded()
|
||||
})
|
||||
|
||||
} 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 hidePopoverView() {
|
||||
if topViewController != nil {
|
||||
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 }
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal func didSelectDate(_ controller: DatePickerViewController, date: Date) {
|
||||
selectedDate = date
|
||||
controller.dismiss(animated: true) { [weak self] in
|
||||
guard let self else { return }
|
||||
self.sendActions(for: .valueChanged)
|
||||
UIAccessibility.post(notification: .layoutChanged, argument: self.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
|
||||
}
|
||||
|
||||
public func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
|
||||
return .none
|
||||
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
|
||||
let popoverWidth = size.width
|
||||
let popoverHeight = 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
|
||||
}
|
||||
|
||||
// Ensure the popover is within the parent's bounds horizontally
|
||||
popoverX = max(0, min(popoverX, parentBounds.width - popoverWidth))
|
||||
|
||||
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 let scrollView = parentView as? UIScrollView {
|
||||
// Calculate vertical position and height
|
||||
availableSpaceAbove = sourceFrameInParent.minY - scrollView.bounds.minY - spacing
|
||||
availableSpaceBelow = scrollView.bounds.maxY - sourceFrameInParent.maxY - spacing
|
||||
|
||||
if availableSpaceAbove > availableSpaceBelow {
|
||||
// Show above
|
||||
popoverY = sourceFrameInParent.minY - popoverHeight - spacing
|
||||
} else {
|
||||
// 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 {
|
||||
// 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 since there is no way we can show the popover without a scrollview
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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() }}
|
||||
|
||||
|
||||
@ -389,60 +389,100 @@ 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 abstractContainer = abstractTextContainer() else { return false }
|
||||
let textContainer = abstractContainer.textContainer
|
||||
let layoutManager = abstractContainer.layoutManager
|
||||
|
||||
guard let attributedText else { return false }
|
||||
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 location.x < bounds.width - intrinsicWidth {
|
||||
return false
|
||||
}
|
||||
case .center:
|
||||
let halfBounds = 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)
|
||||
}
|
||||
|
||||
/**
|
||||
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: 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)
|
||||
}
|
||||
|
||||
|
||||
|
||||
//--------------------------------------------------
|
||||
// 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 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
|
||||
@ -456,10 +496,6 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
|
||||
return element
|
||||
}
|
||||
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Accessibility
|
||||
//--------------------------------------------------
|
||||
open var accessibilityAction: ((Label) -> Void)?
|
||||
|
||||
private var _isAccessibilityElement: Bool = false
|
||||
|
||||
@ -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() } }
|
||||
@ -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
|
||||
}
|
||||
@ -341,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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -166,11 +166,7 @@ open class InputField: EntryFieldBase {
|
||||
if showSuccess {
|
||||
state.insert(.success)
|
||||
}
|
||||
|
||||
if textField.isFirstResponder {
|
||||
state.insert(.focused)
|
||||
}
|
||||
|
||||
|
||||
return state
|
||||
}
|
||||
}
|
||||
@ -200,6 +196,54 @@ 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 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)")
|
||||
}
|
||||
|
||||
if let successText, showSuccess {
|
||||
accessibilityLabels.append("success, \(successText)")
|
||||
}
|
||||
|
||||
accessibilityLabels.append("\(Self.self)")
|
||||
|
||||
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 {
|
||||
@ -264,11 +308,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)
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,15 @@
|
||||
1.0.68
|
||||
1.0.69
|
||||
----------------
|
||||
- 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
|
||||
|
||||
1.0.69
|
||||
----------------
|
||||
- Expired Build because of a issue
|
||||
|
||||
1.0.67
|
||||
----------------
|
||||
|
||||
Loading…
Reference in New Issue
Block a user