diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index f05784ea..d185ce6c 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 */; }; @@ -221,6 +227,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 = ""; }; @@ -246,6 +256,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 = ""; }; @@ -554,6 +566,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 = ( @@ -735,6 +760,7 @@ EA3362412892EF700071C351 /* Label */, 44604AD529CE195300E62B51 /* Line */, EAD0688C2A55F801002E3A2D /* Loader */, + 18C0F9442C980CE500E1DD71 /* Modal */, 445BA07629C07ABA0036A7C5 /* Notification */, 71B23C2B2B91FA510027F7D9 /* Pagination */, 184023432C61E78D00A412C8 /* PriceLockup */, @@ -1245,6 +1271,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 */, ); @@ -1330,6 +1357,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 */, @@ -1347,6 +1375,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 */, @@ -1399,6 +1428,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 */, @@ -1451,6 +1481,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 */, @@ -1459,6 +1490,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 new file mode 100644 index 00000000..b99d99a8 --- /dev/null +++ b/VDS/Components/Modal/Modal.swift @@ -0,0 +1,150 @@ +// +// Modal.swift +// VDS +// +// Created by Kanamarlapudi, Vasavi on 05/09/24. +// + +import Foundation +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. +/// After the customer interacts with the modal, they can return to the parent content. +@objcMembers +@objc(VDSModal) +open class Modal: Control, ModalLaunchable { + + //-------------------------------------------------- + // 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 + //-------------------------------------------------- + internal var showModalButton = Button().with { + $0.use = .primary + $0.text = "Show Modal" + } + + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + /// Text rendered for the title of the modal + open var title: String? { didSet { setNeedsUpdate() } } + + /// Text rendered for the content of the modal + open var content: String? { didSet { setNeedsUpdate() } } + + /// UIView rendered for the content area of the modal + open var contentView: UIView? { didSet { setNeedsUpdate() } } + + /// Array of Buttonable Views that are shown as Modal Footer. Primary and Close button data for modal button group. + open var buttonData: [ButtonBase]? { didSet { setNeedsUpdate() } } + + /// If provided, the Modal has the option to be displayed at full screen. + open var fullScreenDialog: Bool = false { didSet { setNeedsUpdate() } } + + /// If provided, close button can not be present. + open var hideCloseButton: Bool = false { didSet { setNeedsUpdate() } } + + //-------------------------------------------------- + // 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() + + addSubview(showModalButton) + showModalButton.pinToSuperView() + backgroundColor = .clear + + isAccessibilityElement = true + accessibilityTraits = .button + } + + open override func setDefaults() { + super.setDefaults() + title = nil + content = nil + contentView = nil + buttonData = nil + fullScreenDialog = false + hideCloseButton = false + + showModalButton.onClick = { _ in self.showModalButtonClick() } + + bridge_accessibilityLabelBlock = { [weak self] in + guard let self else { return "" } + var label = title + if label == nil { + label = content + } + if let label, !label.isEmpty { + return label + } else { + return "Modal" + } + } + + bridge_accessibilityHintBlock = { [weak self] in + guard let self else { return "" } + return isEnabled ? "Double tap to open." : "" + } + } + + internal func showModalButtonClick() { + self.presentModal(surface: self.surface, + modalModel: .init(closeButtonText: showModalButton.text ?? "", + title: title, + content: content, + contentView: contentView, + buttonData: buttonData, + fullScreenDialog: fullScreenDialog, + hideCloseButton: hideCloseButton), + presenter: self) + } + + + /// Used to make changes to the View based off a change events or from local properties. + open override func updateView() { + super.updateView() + showModalButton.surface = surface + } + + public static func accessibleText(for title: String?, content: String?, closeButtonText: String) -> String { + var label = "" + if let title { + label = title + } + if let content { + if !label.isEmpty { + label += "," + } + label += content + } + return label + } +} + + +// MARK: AppleGuidelinesTouchable +extension Modal: AppleGuidelinesTouchable { + /// Overrides to ensure that the touch point meets a minimum of the minimumTappableArea. + override open func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + Self.acceptablyOutsideBounds(point: point, bounds: bounds) + } + +} diff --git a/VDS/Components/Modal/ModalChangeLog.txt b/VDS/Components/Modal/ModalChangeLog.txt new file mode 100644 index 00000000..7716b8b2 --- /dev/null +++ b/VDS/Components/Modal/ModalChangeLog.txt @@ -0,0 +1,67 @@ +MM/DD/YYYY +---------------- +- Initial Brand 3.0 handoff + +12/17/2021 +---------------- +- Replaced focusring colors (previously interactive/onlight/ondark) with accessibility/onlight/ondark colors +- Updated focus border name (previously interactive.focusring.onlight) with focusring.onlight/ondark + +12/31/2021 +---------------- +- Updated Hover and Active state trigger specs. If triggered by mouse, Active same as Hover. If not, Active same as Default. + +03/01/2022 +---------------- +- Replaced Close Non-Scaling icon with VDS Icon. +- Removed “vector effect” from Anatomy. +- Removed “weight” from Configurations. + +08/10/2022 +---------------- +- Updated default and inverted prop to light and dark surface. +- Noted that button is optional within anatomy + +09/06/2022 +---------------- +- Updated Anatomy element names to remove the word “Modal” from text elements, updated Button to be Button Group, +and noted Button Group as optional across all visuals within Anatomy. + +11/30/2022 +---------------- +- Added "(web only)" to any instance of "keyboard focus" + +12/13/2022 +---------------- +- Replaced focus border pixel and style & spacing values with tokens. + +04/24/2023 +---------------- +- Updated all instances of Close Button (VDS Icon) with VDS Button Icon (size small) +- Button Icon placed 8px from top/right edge +- Use the Ghost variant of Button Icon +- Added Button Icon props to Elements spec + +10/17/2023 +---------------- +- Added component tokens table +- Applied component tokens to light, dark surface configurations + +11/22/2023 +---------------- +- Updated tab/desk visuals to reflect new corner radius value - 12px +- Updated border radius value in Anatomy + +11/27/2023 +---------------- +- Updated ‘border radius” to “corner radius” in Anatomy + +12/1/2023 +---------------- +- Applied palette tokens instead of hardcoded values where component tokens included an opacity +- Removed layer opacity annotation for instances where opacity is built into a component token + +07/18/2024 +---------------- +- Added Scrollbar hit area with z-index specifications to the Behaviors page +- Decreased the height of the Grab zone to equal the height of the scrollbar thumb on the Behaviors page diff --git a/VDS/Components/Modal/ModalDialog.swift b/VDS/Components/Modal/ModalDialog.swift new file mode 100644 index 00000000..9c7dd42f --- /dev/null +++ b/VDS/Components/Modal/ModalDialog.swift @@ -0,0 +1,238 @@ +// +// 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: - Public Properties + //-------------------------------------------------- + open var children: [any ViewProtocol] { [closeCrossButton, titleLabel, contentLabel, buttonGroupData] } + + open var modalModel = Modal.ModalModel() { didSet { setNeedsUpdate() } } + + open var titleLabel = Label().with { label in + label.isAccessibilityElement = true + label.textStyle = .boldTitleLarge + } + + 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 var buttonGroupData = ButtonGroup().with { + $0.alignment = .left + } + + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- + private var scrollView = UIScrollView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.backgroundColor = .clear + } + + private var 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" + } + + // close button with the 48 x 48 px + private var closeCrossButtonSize = 48.0 + + private let containerViewInset = 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 + + //-------------------------------------------------- + // MARK: - Configuration Properties + //-------------------------------------------------- + private let backgroundColorConfiguration = SurfaceColorConfiguration(VDSColor.backgroundPrimaryLight, VDSColor.backgroundPrimaryDark) + private let textColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark) + + //-------------------------------------------------- + // MARK: - Constraints + //-------------------------------------------------- + private var contentStackViewBottomConstraint: 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 + + // Add titleLabel, contentLabel to contentStack. + contentStackView.addArrangedSubview(titleLabel) + contentStackView.addArrangedSubview(contentLabel) + contentStackView.setCustomSpacing(contentLabelTopSpace, after: titleLabel) + scrollView.addSubview(contentStackView) + + // Add crossButon, scrollView, buttonsData. + addSubview(closeCrossButton) + addSubview(scrollView) + addSubview(buttonGroupData) + self.bringSubviewToFront(closeCrossButton) + + let crossTopSpace = UIDevice.isIPad && !modalModel.fullScreenDialog ? 0 : VDSLayout.space12X + let scrollTopSpace = UIDevice.isIPad && !modalModel.fullScreenDialog ? containerViewInset : (crossTopSpace + closeCrossButtonSize) + let contentTrailingSpace = UIDevice.isIPad ? (containerViewInset/2) - 6 : containerViewInset + + // Activate constraints + NSLayoutConstraint.activate([ + // Constraints for the closeCrossButton + closeCrossButton.topAnchor.constraint(equalTo: topAnchor, constant: crossTopSpace), + closeCrossButton.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor), + closeCrossButton.trailingAnchor.constraint(equalTo: trailingAnchor), + closeCrossButton.heightAnchor.constraint(equalToConstant: closeCrossButtonSize), + + // Constraints for the bottom button view + buttonGroupData.leadingAnchor.constraint(equalTo: leadingAnchor, constant:containerViewInset), + buttonGroupData.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -containerViewInset), + buttonGroupData.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -containerViewInset), + + // Constraints for the scrollView + scrollView.topAnchor.constraint(equalTo: topAnchor, constant: scrollTopSpace), + scrollView.leadingAnchor.constraint(equalTo: leadingAnchor, constant:containerViewInset), + scrollView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -contentTrailingSpace), + scrollView.bottomAnchor.constraint(equalTo: buttonGroupData.topAnchor, constant: -contentLabelBottomSpace), + + // Constraints for the contentStackView + contentStackView.topAnchor.constraint(equalTo: scrollView.topAnchor), + contentStackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), + contentStackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: -contentTrailingSpace), + contentStackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -contentTrailingSpace), + contentStackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor) + ]) + + contentStackViewBottomConstraint = contentStackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor) + contentStackViewBottomConstraint?.activate() + } + + /// Used to make changes to the View based off a change events or from local properties. + open override func updateView() { + super.updateView() + + // Update surface and background + backgroundColor = backgroundColorConfiguration.getColor(self) + scrollView.indicatorStyle = surface == .light ? .black : .white + closeCrossButton.surface = surface + buttonGroupData.surface = surface + titleLabel.surface = surface + contentLabel.surface = surface + + // Re-arrange contentStack + contentStackView.removeArrangedSubviews() + + titleLabel.text = modalModel.title + contentLabel.text = modalModel.content + titleLabel.textColor = textColorConfiguration.getColor(self) + contentLabel.textColor = textColorConfiguration.getColor(self) + titleLabel.sizeToFit() + contentLabel.sizeToFit() + + // Add buttons data if provided + if let buttons = modalModel.buttonData, buttons.count > 0 { + buttonGroupData.buttons = buttons + let percent = UIDevice.isIPad ? 50.0 : 100.0 + buttonGroupData.rowQuantityTablet = 2 + buttonGroupData.rowQuantityPhone = 1 + buttonGroupData.childWidth = .percentage(percent) + } + + // Update title, content and contentview + 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.spacing = contentLabelTopSpace + } + + closeCrossButton.isHidden = modalModel.hideCloseButton + + contentStackView.setNeedsLayout() + contentStackView.layoutIfNeeded() + scrollView.setNeedsLayout() + scrollView.layoutIfNeeded() + } + + /// Used to update any Accessibility properties. + open override func updateAccessibility() { + super.updateAccessibility() + primaryAccessibilityElement.accessibilityHint = "Double tap on the cross 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(buttonGroupData) + return elements + } + set {} + } + +} diff --git a/VDS/Components/Modal/ModalDialogViewController.swift b/VDS/Components/Modal/ModalDialogViewController.swift new file mode 100644 index 00000000..8e0a2dcf --- /dev/null +++ b/VDS/Components/Modal/ModalDialogViewController.swift @@ -0,0 +1,124 @@ +// +// 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 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 = true + 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.closeCrossButton.publisher(for: .touchUpInside) + .sink {[weak self] button in + guard let self else { return } + self.dismiss() + } + + view.addSubview(modalDialog) + + } + + /// Used to make changes to the View based off a change events or from local properties. + open func updateView() { + modalDialog.surface = surface + modalDialog.modalModel = modalModel + + + // Activate constraints + modalDialog.removeConstraints() + let isFullScreen = UIDevice.isIPad && !modalModel.fullScreenDialog ? false : true + + if isFullScreen { + view.backgroundColor = modalDialog.backgroundColor + modalDialog + .pinLeading() + .pinTrailing() + modalDialog.pinTop(anchor: UIDevice.isIPad ? view.safeAreaLayoutGuide.topAnchor : view.topAnchor) + modalDialog.pinBottom(UIDevice.isIPad ? view.bottomAnchor : view.safeAreaLayoutGuide.bottomAnchor) + + } else { + view.backgroundColor = backgroundColorConfiguration.getColor(self).withAlphaComponent(0.8) + NSLayoutConstraint.activate([ + // Constraints for the floating modal view for Tablet. + modalDialog.centerXAnchor.constraint(equalTo: view.centerXAnchor), + modalDialog.centerYAnchor.constraint(equalTo: view.centerYAnchor), + modalDialog.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.7), + modalDialog.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.7) + ]) + } + + } +} diff --git a/VDS/Components/Modal/ModalLaunchable.swift b/VDS/Components/Modal/ModalLaunchable.swift new file mode 100644 index 00000000..537de6c5 --- /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 && !modalModel.fullScreenDialog ? .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..c0bb6f79 --- /dev/null +++ b/VDS/Components/Modal/ModalModel.swift @@ -0,0 +1,45 @@ +// +// 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 var buttonData: [ButtonBase]? + public var fullScreenDialog: Bool + public var hideCloseButton: Bool + public init(closeButtonText: String = "Close", + title: String? = nil, + content: String? = nil, + contentView: UIView? = nil, + buttonData: [ButtonBase]? = nil, + fullScreenDialog: Bool = false, + hideCloseButton: Bool = false, + 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 + self.buttonData = buttonData + self.fullScreenDialog = fullScreenDialog + self.hideCloseButton = hideCloseButton + } + } +} diff --git a/VDS/VDS.docc/VDS.md b/VDS/VDS.docc/VDS.md index 97036331..e1fda159 100755 --- a/VDS/VDS.docc/VDS.md +++ b/VDS/VDS.docc/VDS.md @@ -40,6 +40,7 @@ Using the system allows designers and developers to collaborate more easily and - ``Label`` - ``Line`` - ``Loader`` +- ``Modal`` - ``Notification`` - ``Pagination`` - ``PriceLockup``