diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index 7090dcfd..3ad5de73 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -10,6 +10,10 @@ 180636C72C29B0A400C92D86 /* InputStepper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 180636C62C29B0A400C92D86 /* InputStepper.swift */; }; 180636C92C29B0DF00C92D86 /* InputStepperLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 180636C82C29B0DF00C92D86 /* InputStepperLog.txt */; }; 1808BEBC2BA41C3200129230 /* CarouselScrollbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1808BEBB2BA41C3200129230 /* CarouselScrollbar.swift */; }; + 1818D04D2C9BD2170053E73C /* ModalDialogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1818D04C2C9BD2170053E73C /* ModalDialogViewController.swift */; }; + 1818D04F2C9BD3F60053E73C /* ModalDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1818D04E2C9BD3F60053E73C /* ModalDialog.swift */; }; + 1818D0512C9BD4090053E73C /* ModalLaunchable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1818D0502C9BD4090053E73C /* ModalLaunchable.swift */; }; + 1818D0532C9BD47C0053E73C /* ModalModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1818D0522C9BD47C0053E73C /* ModalModel.swift */; }; 1832AC572BA0791D008AE476 /* BreadcrumbCellItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1832AC562BA0791D008AE476 /* BreadcrumbCellItem.swift */; }; 183B16F32C78CF7C00BA6A10 /* CarouselSlotCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 183B16F22C78CF7C00BA6A10 /* CarouselSlotCell.swift */; }; 183B16F72C80B32200BA6A10 /* FootnoteGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 183B16F62C80B32200BA6A10 /* FootnoteGroup.swift */; }; @@ -30,6 +34,8 @@ 18B42AC62C09D197008D6262 /* CarouselSlotAlignmentModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B42AC52C09D197008D6262 /* CarouselSlotAlignmentModel.swift */; }; 18B463A42BBD3C46005C4528 /* DropdownOptionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B463A32BBD3C46005C4528 /* DropdownOptionModel.swift */; }; 18B9763F2C11BA4A009271DF /* CarouselPaginationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B9763E2C11BA4A009271DF /* CarouselPaginationModel.swift */; }; + 18C0F9462C98175900E1DD71 /* Modal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18C0F9452C98175900E1DD71 /* Modal.swift */; }; + 18C0F94A2C9817C100E1DD71 /* ModalChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 18C0F9492C9817C100E1DD71 /* ModalChangeLog.txt */; }; 18FEA1AD2BDD137500A56439 /* CalendarIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1AC2BDD137500A56439 /* CalendarIndicatorModel.swift */; }; 18FEA1B52BE0E63600A56439 /* Date+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1B42BE0E63600A56439 /* Date+Extension.swift */; }; 445BA07829C07B3D0036A7C5 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 445BA07729C07B3D0036A7C5 /* Notification.swift */; }; @@ -219,6 +225,10 @@ 180636C82C29B0DF00C92D86 /* InputStepperLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = InputStepperLog.txt; sourceTree = ""; }; 1808BEBB2BA41C3200129230 /* CarouselScrollbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselScrollbar.swift; sourceTree = ""; }; 1808BEBF2BA456B700129230 /* CarouselScrollbarChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = CarouselScrollbarChangeLog.txt; sourceTree = ""; }; + 1818D04C2C9BD2170053E73C /* ModalDialogViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalDialogViewController.swift; sourceTree = ""; }; + 1818D04E2C9BD3F60053E73C /* ModalDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalDialog.swift; sourceTree = ""; }; + 1818D0502C9BD4090053E73C /* ModalLaunchable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalLaunchable.swift; sourceTree = ""; }; + 1818D0522C9BD47C0053E73C /* ModalModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalModel.swift; sourceTree = ""; }; 1832AC562BA0791D008AE476 /* BreadcrumbCellItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreadcrumbCellItem.swift; sourceTree = ""; }; 183B16F22C78CF7C00BA6A10 /* CarouselSlotCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselSlotCell.swift; sourceTree = ""; }; 183B16F62C80B32200BA6A10 /* FootnoteGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FootnoteGroup.swift; sourceTree = ""; }; @@ -244,6 +254,8 @@ 18B463A32BBD3C46005C4528 /* DropdownOptionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropdownOptionModel.swift; sourceTree = ""; }; 18B9763E2C11BA4A009271DF /* CarouselPaginationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselPaginationModel.swift; sourceTree = ""; }; 18BDEE812B75316E00452358 /* ButtonIconChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ButtonIconChangeLog.txt; sourceTree = ""; }; + 18C0F9452C98175900E1DD71 /* Modal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modal.swift; sourceTree = ""; }; + 18C0F9492C9817C100E1DD71 /* ModalChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ModalChangeLog.txt; sourceTree = ""; }; 18FEA1AC2BDD137500A56439 /* CalendarIndicatorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarIndicatorModel.swift; sourceTree = ""; }; 18FEA1B42BE0E63600A56439 /* Date+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extension.swift"; sourceTree = ""; }; 18FEA1B82BE1301700A56439 /* CalendarChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = CalendarChangeLog.txt; sourceTree = ""; }; @@ -550,6 +562,19 @@ path = Carousel; sourceTree = ""; }; + 18C0F9442C980CE500E1DD71 /* Modal */ = { + isa = PBXGroup; + children = ( + 18C0F9452C98175900E1DD71 /* Modal.swift */, + 1818D04E2C9BD3F60053E73C /* ModalDialog.swift */, + 1818D04C2C9BD2170053E73C /* ModalDialogViewController.swift */, + 1818D0502C9BD4090053E73C /* ModalLaunchable.swift */, + 1818D0522C9BD47C0053E73C /* ModalModel.swift */, + 18C0F9492C9817C100E1DD71 /* ModalChangeLog.txt */, + ); + path = Modal; + sourceTree = ""; + }; 440B84C82BD8E0CE004A732A /* Table */ = { isa = PBXGroup; children = ( @@ -731,6 +756,7 @@ EA3362412892EF700071C351 /* Label */, 44604AD529CE195300E62B51 /* Line */, EAD0688C2A55F801002E3A2D /* Loader */, + 18C0F9442C980CE500E1DD71 /* Modal */, 445BA07629C07ABA0036A7C5 /* Notification */, 71B23C2B2B91FA510027F7D9 /* Pagination */, 184023432C61E78D00A412C8 /* PriceLockup */, @@ -1239,6 +1265,7 @@ EA3362062891E14D0071C351 /* VerizonNHGeTX-Regular.otf in Resources */, EA3362052891E14D0071C351 /* VerizonNHGeDS-Bold.otf in Resources */, 180636C92C29B0DF00C92D86 /* InputStepperLog.txt in Resources */, + 18C0F94A2C9817C100E1DD71 /* ModalChangeLog.txt in Resources */, EAA5EEB928ECD24B003B3210 /* Icons.xcassets in Resources */, EAA5EEE428F5B855003B3210 /* VerizonNHGDS-Light.otf in Resources */, ); @@ -1324,6 +1351,7 @@ EA8141102A127066004F60D2 /* UIColor+VDSColor.swift in Sources */, EAF7F0AF289B144C00B287F5 /* UnderlineLabelAttribute.swift in Sources */, EA0D1C412A6AD61C00E5C127 /* Typography+Additional.swift in Sources */, + 18C0F9462C98175900E1DD71 /* Modal.swift in Sources */, EAC925842911C63100091998 /* Colorable.swift in Sources */, 18B463A42BBD3C46005C4528 /* DropdownOptionModel.swift in Sources */, EAF2F4762C231EAA007BFEDC /* AccessibilityActionElement.swift in Sources */, @@ -1341,6 +1369,7 @@ EA596ABD2A16B4EC00300C4B /* Tab.swift in Sources */, 71ACE89E2BA1CC1700FB6ADC /* TiletEyebrowModel.swift in Sources */, EAF7F11728A1475A00B287F5 /* RadioButtonItem.swift in Sources */, + 1818D0512C9BD4090053E73C /* ModalLaunchable.swift in Sources */, EA985BEE2968A92400F2FF2E /* TitleLockupSubTitleModel.swift in Sources */, EA2DC9B22BE175E6004F58C5 /* CharacterCountRule.swift in Sources */, EA985BF22968B5BB00F2FF2E /* TitleLockupTextStyle.swift in Sources */, @@ -1392,6 +1421,7 @@ EAF2F4892C2A1075007BFEDC /* AlertViewController.swift in Sources */, EA0D1C3D2A6AD57600E5C127 /* Typography+Enums.swift in Sources */, EAF1FE9B29DB1A6000101452 /* Changeable.swift in Sources */, + 1818D04F2C9BD3F60053E73C /* ModalDialog.swift in Sources */, EAC58C0C2BED01D500BA39FA /* Telephone.swift in Sources */, EAF7F0A2289AFB3900B287F5 /* Errorable.swift in Sources */, EA8E40912A7D3F6300934ED3 /* UIView+Accessibility.swift in Sources */, @@ -1443,6 +1473,7 @@ EAB5FED429267EB300998C17 /* UIView+NSLayoutConstraint.swift in Sources */, EAB2376829E9992800AABE9A /* TooltipAlertViewController.swift in Sources */, EA33623E2892EE950071C351 /* UIDevice.swift in Sources */, + 1818D04D2C9BD2170053E73C /* ModalDialogViewController.swift in Sources */, EA985C692971B90B00F2FF2E /* IconSize.swift in Sources */, 71FC86E02B973AE500700965 /* DropShadowConfiguration.swift in Sources */, EA3362302891EB4A0071C351 /* Font.swift in Sources */, @@ -1451,6 +1482,7 @@ EAB5FEF12927F4AA00998C17 /* SelfSizingCollectionView.swift in Sources */, 184023452C61E7AD00A412C8 /* PriceLockup.swift in Sources */, EA3361B8288B2AAA0071C351 /* ViewProtocol.swift in Sources */, + 1818D0532C9BD47C0053E73C /* ModalModel.swift in Sources */, EA3361A8288B23300071C351 /* UIColor.swift in Sources */, EA2DC9B42BE2C6FE004F58C5 /* TextField.swift in Sources */, EAC58C182BED0E2300BA39FA /* SecurityCode.swift in Sources */, diff --git a/VDS/Components/Modal/Modal.swift b/VDS/Components/Modal/Modal.swift index c0c28392..19696b15 100644 --- a/VDS/Components/Modal/Modal.swift +++ b/VDS/Components/Modal/Modal.swift @@ -10,8 +10,7 @@ import UIKit import VDSCoreTokens import Combine -/// A Modal is an overlay that interrupts the user flow -/// to force the customer to provide information or a response. +/// A Modal is an overlay that interrupts the user flow to force the customer to provide information or a response. /// After the customer interacts with the modal, they can return to the parent content. @objcMembers @objc(VDSModal) @@ -52,7 +51,7 @@ open class Modal: Control, ModalLaunchable { /// UIView rendered for the content area of the modal open var contentView: UIView? { didSet { setNeedsUpdate() } } - + //-------------------------------------------------- // MARK: - Overrides //-------------------------------------------------- @@ -75,7 +74,7 @@ open class Modal: Control, ModalLaunchable { contentView = nil showModalButton.onClick = { _ in self.showModalButtonClick() } - + bridge_accessibilityLabelBlock = { [weak self] in guard let self else { return "" } var label = title @@ -96,7 +95,6 @@ open class Modal: Control, ModalLaunchable { } internal func showModalButtonClick() { - print("Clicked showModalButton") self.presentModal(surface: self.surface, modalModel: .init(closeButtonText: showModalButton.text ?? "", title: title, diff --git a/VDS/Components/Modal/ModalDialog.swift b/VDS/Components/Modal/ModalDialog.swift new file mode 100644 index 00000000..50b4e361 --- /dev/null +++ b/VDS/Components/Modal/ModalDialog.swift @@ -0,0 +1,239 @@ +// +// ModalDialog.swift +// VDS +// +// Created by Kanamarlapudi, Vasavi on 09/09/24. +// + +import Foundation +import UIKit +import VDSCoreTokens + +@objcMembers +@objc(VDSModalDialog) +open class ModalDialog: View, UIScrollViewDelegate, ParentViewProtocol { + + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + required public init() { + super.init(frame: .zero) + } + + public override init(frame: CGRect) { + super.init(frame: .zero) + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- + private var scrollView = UIScrollView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.backgroundColor = .clear + } + + private let contentStackView = UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.axis = .vertical + $0.alignment = .leading + $0.distribution = .fillProportionally + $0.spacing = 0 + } + + lazy var primaryAccessibilityElement = UIAccessibilityElement(accessibilityContainer: self).with { + $0.accessibilityLabel = "Modal" + } + + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + open var children: [any ViewProtocol] { [closeCrossButton, titleLabel, contentLabel, closeButton] } + + open var modalModel = Modal.ModalModel() { didSet { setNeedsUpdate() } } + + open var titleLabel = Label().with { label in + label.isAccessibilityElement = true + label.textStyle = .boldTitleMedium + } + + open var contentLabel = Label().with { label in + label.isAccessibilityElement = true + label.textStyle = .bodyLarge + } + + open lazy var closeCrossButton = ButtonIcon().with { + $0.kind = .ghost + $0.surfaceType = .colorFill + $0.iconName = .close + $0.size = .small + $0.customContainerSize = UIDevice.isIPad ? 48 : 48 + $0.customIconSize = UIDevice.isIPad ? 32 : 32 + } + open lazy var closeButton = Button().with{ $0.use = .secondary; $0.text = "Close"} + + //-------------------------------------------------- + // MARK: - Configuration + //-------------------------------------------------- + private var closeButtonHeight: CGFloat = 44.0 + private var fullWidth: CGFloat = 296 + private var minHeight: CGFloat = 96.0 + private var maxHeight: CGFloat = 312.0 + + private let containerViewInset = VDSLayout.space4X //UIDevice.isIPad ? VDSLayout.space12X : VDSLayout.space4X + private let contentLabelTopSpace = UIDevice.isIPad ? VDSLayout.space8X : VDSLayout.space6X + private let contentLabelBottomSpace = UIDevice.isIPad ? VDSLayout.space8X : VDSLayout.space12X + private let gapBetweenButtonItems = VDSLayout.space3X + + private let backgroundColorConfiguration = SurfaceColorConfiguration(VDSColor.backgroundPrimaryLight, VDSColor.backgroundPrimaryDark) + private let closeButtonTextColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark) + + private var contentStackViewBottomConstraint: NSLayoutConstraint? + private var heightConstraint: NSLayoutConstraint? + + //-------------------------------------------------- + // MARK: - Overrides + //-------------------------------------------------- + /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations. + open override func setup() { + super.setup() + titleLabel.accessibilityTraits = .header + + layer.cornerRadius = 12 + contentStackView.addArrangedSubview(titleLabel) + contentStackView.addArrangedSubview(contentLabel) + scrollView.addSubview(contentStackView) + addSubview(closeCrossButton) + addSubview(scrollView) + addSubview(closeButton) + + // Activate constraints + NSLayoutConstraint.activate([ + widthAnchor.constraint(equalToConstant: fullWidth), + + // Constraints for the scroll view + scrollView.topAnchor.constraint(equalTo: topAnchor, constant: VDSLayout.space4X), + scrollView.leadingAnchor.constraint(equalTo: leadingAnchor), + scrollView.trailingAnchor.constraint(equalTo: trailingAnchor), +// scrollView.bottomAnchor.constraint(equalTo: line.topAnchor), + +// line.leadingAnchor.constraint(equalTo: leadingAnchor), +// line.trailingAnchor.constraint(equalTo: trailingAnchor), + + closeButton.topAnchor.constraint(equalTo: scrollView.bottomAnchor), + closeButton.leadingAnchor.constraint(equalTo: leadingAnchor), + closeButton.trailingAnchor.constraint(equalTo: trailingAnchor), + closeButton.bottomAnchor.constraint(equalTo: bottomAnchor), + closeButton.heightAnchor.constraint(equalToConstant: closeButtonHeight), + + contentStackView.topAnchor.constraint(equalTo: scrollView.topAnchor), + contentStackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: containerViewInset), + contentStackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: -containerViewInset), + contentStackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -(containerViewInset * 2)), + + ]) + contentStackViewBottomConstraint = contentStackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor) + contentStackViewBottomConstraint?.activate() + + heightConstraint = heightAnchor.constraint(equalToConstant: minHeight) + heightConstraint?.activate() + } + + /// Used to make changes to the View based off a change events or from local properties. + open override func updateView() { + super.updateView() + + backgroundColor = backgroundColorConfiguration.getColor(self) + scrollView.indicatorStyle = surface == .light ? .black : .white + + contentStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } + + titleLabel.surface = surface + contentLabel.surface = surface + + titleLabel.text = modalModel.title + contentLabel.text = modalModel.content + + titleLabel.sizeToFit() + contentLabel.sizeToFit() + + var addedTitle = false + + if let titleText = modalModel.title, !titleText.isEmpty { + contentStackView.addArrangedSubview(titleLabel) + addedTitle = true + } + + var addedContent = false + if let contentText = modalModel.content, !contentText.isEmpty { + contentStackView.addArrangedSubview(contentLabel) + addedContent = true + } else if let contentView = modalModel.contentView { + contentView.translatesAutoresizingMaskIntoConstraints = false + if var surfaceable = contentView as? Surfaceable { + surfaceable.surface = surface + } + contentStackView.addArrangedSubview(contentView) + addedContent = true + } + + if addedTitle && addedContent { + contentStackView.setCustomSpacing(VDSLayout.space1X, after: titleLabel) + } + + let closeButtonTextColor = closeButtonTextColorConfiguration.getColor(self) + closeButton.setTitleColor(closeButtonTextColor, for: .normal) + closeButton.setTitleColor(closeButtonTextColor, for: .highlighted) + closeButton.setTitle(modalModel.closeButtonText, for: .normal) + closeButton.accessibilityLabel = modalModel.closeButtonText + + contentStackView.setNeedsLayout() + contentStackView.layoutIfNeeded() + + scrollView.setNeedsLayout() + scrollView.layoutIfNeeded() + + //dealing with height + //we can't really use the minMax height and set constraints for + //greaterThan or lessThan on the heightAnchor due to scrollView/stackView intrinsic size + //therefore we can do a little math and manually set the height based off all of the content + var contentHeight = closeButtonHeight + scrollView.contentSize.height + (containerViewInset * 2) + + //reset the bottomConstraint + contentStackViewBottomConstraint?.constant = 0 + + if contentHeight < minHeight { + contentHeight = minHeight + + } else if contentHeight > maxHeight { + contentHeight = maxHeight + //since we are now scrolling, add padding to the bottom of the + //stackView between the bottom of the scrollView + contentStackViewBottomConstraint?.constant = -containerViewInset + } + + heightConstraint?.constant = contentHeight + } + + /// Used to update any Accessibility properties. + open override func updateAccessibility() { + super.updateAccessibility() + primaryAccessibilityElement.accessibilityHint = "Double tap on the \(modalModel.closeButtonText) button to close." + primaryAccessibilityElement.accessibilityFrameInContainerSpace = .init(origin: .zero, size: frame.size) + } + + open override var accessibilityElements: [Any]? { + get { + var elements: [Any] = [primaryAccessibilityElement] + contentStackView.arrangedSubviews.forEach{ elements.append($0) } + elements.append(closeButton) + + return elements + } + set {} + } + +} diff --git a/VDS/Components/Modal/ModalDialogViewController.swift b/VDS/Components/Modal/ModalDialogViewController.swift new file mode 100644 index 00000000..69103484 --- /dev/null +++ b/VDS/Components/Modal/ModalDialogViewController.swift @@ -0,0 +1,131 @@ +// +// ModalDialogViewController.swift +// VDS +// +// Created by Kanamarlapudi, Vasavi on 09/09/24. +// + +import Foundation +import UIKit +import Combine +import VDSCoreTokens + +@objcMembers +@objc(VDSModalDialogViewController) +open class ModalDialogViewController: 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() + } + } + } + + private var onClickCloseSubscriber: AnyCancellable? { + willSet { + if let onClickCloseSubscriber { + onClickCloseSubscriber.cancel() + } + } + } + + private let modalDialog = ModalDialog() + + //-------------------------------------------------- + // 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 modalModel = Modal.ModalModel() { didSet { updateView() }} + open var presenter: UIView? { didSet { updateView() }} + + //-------------------------------------------------- + // MARK: - Configuration + //-------------------------------------------------- + private let backgroundColorConfiguration = SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteWhite) + + //-------------------------------------------------- + // MARK: - Lifecycle + //-------------------------------------------------- + open override func viewDidLoad() { + super.viewDidLoad() + isModalInPresentation = UIDevice.isIPad ? true : false + setup() + } + open override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + UIAccessibility.post(notification: .screenChanged, argument: modalDialog) + } + + 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() { + view.accessibilityElements = [modalDialog] + + //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) + + //clicking button + onClickSubscriber = modalDialog.closeButton.publisher(for: .touchUpInside) + .sink {[weak self] button in + guard let self else { return } + self.dismiss() + } + + onClickCloseSubscriber = modalDialog.closeCrossButton.publisher(for: .touchUpInside) + .sink {[weak self] button in + guard let self else { return } + self.dismiss() + } + + view.addSubview(modalDialog) + + // Activate constraints + UIDevice.isIPad ? + NSLayoutConstraint.activate([ + // Constraints for the floating modal view + modalDialog.centerXAnchor.constraint(equalTo: view.centerXAnchor), + modalDialog.centerYAnchor.constraint(equalTo: view.centerYAnchor), + modalDialog.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor), + modalDialog.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor), + modalDialog.topAnchor.constraint(greaterThanOrEqualTo: view.topAnchor), + modalDialog.bottomAnchor.constraint(lessThanOrEqualTo: view.bottomAnchor) + ]) : NSLayoutConstraint.activate([ + // Constraints for the floating modal view + modalDialog.leadingAnchor.constraint(equalTo: view.leadingAnchor), + modalDialog.trailingAnchor.constraint(equalTo: view.trailingAnchor), + modalDialog.topAnchor.constraint(equalTo: view.topAnchor), + modalDialog.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) + } + + /// 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.8) + modalDialog.surface = surface + modalDialog.modalModel = modalModel + } +} diff --git a/VDS/Components/Modal/ModalLaunchable.swift b/VDS/Components/Modal/ModalLaunchable.swift new file mode 100644 index 00000000..b99b638c --- /dev/null +++ b/VDS/Components/Modal/ModalLaunchable.swift @@ -0,0 +1,28 @@ +// +// ModalLaunchable.swift +// VDS +// +// Created by Kanamarlapudi, Vasavi on 09/09/24. +// + +import Foundation +import UIKit + +public protocol ModalLaunchable { + func presentModal(surface: Surface, modalModel: Modal.ModalModel, presenter: UIView?) +} + +extension ModalLaunchable { + public func presentModal(surface: Surface, modalModel: Modal.ModalModel, presenter: UIView? = nil) { + if let presenting = UIApplication.topViewController() { + let modalViewController = ModalDialogViewController(nibName: nil, bundle: nil).with { + $0.surface = surface + $0.modalModel = modalModel + $0.presenter = presenter + $0.modalPresentationStyle = UIDevice.isIPad ? .overCurrentContext : .fullScreen + $0.modalTransitionStyle = .crossDissolve + } + presenting.present(modalViewController, animated: true) + } + } +} diff --git a/VDS/Components/Modal/ModalModel.swift b/VDS/Components/Modal/ModalModel.swift new file mode 100644 index 00000000..1e653ab9 --- /dev/null +++ b/VDS/Components/Modal/ModalModel.swift @@ -0,0 +1,36 @@ +// +// ModalModel.swift +// VDS +// +// Created by Kanamarlapudi, Vasavi on 09/09/24. +// + +import Foundation +import UIKit + +extension Modal { + + /// Model used to represent the modal. + public struct ModalModel: Equatable { + /// Current Surface and this is used to pass down to child objects that implement Surfacable + public var closeButtonText: String + public var title: String? + public var content: String? + public var contentView: UIView? + public var accessibleText: String? + public var contentViewAlignment: UIStackView.Alignment? + public init(closeButtonText: String = "Close", + title: String? = nil, + content: String? = nil, + contentView: UIView? = nil, + accessibleText: String? = "Modal", + contentViewAlignment: UIStackView.Alignment = .leading) { + self.closeButtonText = closeButtonText + self.title = title + self.content = content + self.contentView = contentView + self.accessibleText = accessibleText + self.contentViewAlignment = contentViewAlignment + } + } +}