// // 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 = .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 lazy var closeButton = Button().with{ $0.use = .secondary; $0.text = "Close"} //-------------------------------------------------- // MARK: - Configuration //-------------------------------------------------- // full width : viewport width private var fullWidth: CGFloat = UIScreen.main.bounds.size.width // Min height content area 136 px. Total window height 232 px private var minHeight: CGFloat = 232.0 // Max height content area. total window height: 70% of viewport height private var maxHeight: CGFloat = 0.0 // Min default width private var minWidth: CGFloat = 560.0 // Max width: 70% of viewport width private var maxWidth: CGFloat = 0.0 // 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 closeButtonTextColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark) //-------------------------------------------------- // MARK: - Constraints //-------------------------------------------------- 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() // Max Width: 70% of viewport width. // Maximum width is only applied to a provided width and not to the default. maxWidth = fullWidth * (70/100) // Max Height: Total window height 70% of viewport height. // For Tablet: By default the model's height is dynamic and is based on the amount of content // it contains. it can expand between a minimum and maximum height. The minimum height is determined by the minimum // height of the content area plus the top and bottom padding. maxHeight = UIScreen.main.bounds.size.height * (70/100) 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, closeButton. addSubview(closeCrossButton) addSubview(scrollView) addSubview(closeButton) self.bringSubviewToFront(closeCrossButton) let contentTrailingSpace = UIDevice.isIPad ? containerViewInset/2 : containerViewInset let crossTopSpace = UIDevice.isIPad ? 0 : VDSLayout.space12X let scrollTopSpace = UIDevice.isIPad ? containerViewInset : (crossTopSpace + closeCrossButtonSize) // Activate constraints NSLayoutConstraint.activate([ widthAnchor.constraint(equalToConstant: maxWidth), // 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), closeCrossButton.widthAnchor.constraint(equalToConstant: closeCrossButtonSize), // Constraints for the bottom button view closeButton.leadingAnchor.constraint(equalTo: leadingAnchor, constant:containerViewInset), closeButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -containerViewInset), closeButton.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: closeButton.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), contentStackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -contentTrailingSpace), contentStackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor) ]) contentStackViewBottomConstraint = contentStackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor) contentStackViewBottomConstraint?.activate() heightConstraint = heightAnchor.constraint(equalToConstant: maxHeight) heightConstraint?.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 closeButton.surface = surface titleLabel.surface = surface contentLabel.surface = surface // Re-arrange contentStack contentStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } titleLabel.text = modalModel.title contentLabel.text = modalModel.content titleLabel.sizeToFit() contentLabel.sizeToFit() // 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.setCustomSpacing(contentLabelTopSpace, after: titleLabel) } // Update closeButton 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() } /// 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 {} } }