// // TooltipDialog.swift // VDS // // Created by Matt Bruce on 8/8/23. // import Foundation import UIKit import VDSTokens open class TooltipDialog: View, UIScrollViewDelegate { //-------------------------------------------------- // 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 } private var line = Line().with { instance in instance.lineViewColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsLowcontrastOnlight, VDSColor.elementsLowcontrastOndark).eraseToAnyColorable() } lazy var primaryAccessibilityElement = UIAccessibilityElement(accessibilityContainer: self).with { $0.accessibilityLabel = "Modal" $0.accessibilityFrameInContainerSpace = .init(origin: .zero, size: .init(width: fullWidth, height: VDSLayout.space1X)) } //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- open var tooltipModel = Tooltip.TooltipModel() { 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 closeButton: UIButton = { let button = UIButton(type: .system) button.isAccessibilityElement = true button.backgroundColor = .clear button.setTitle("Close", for: .normal) button.titleLabel?.font = TextStyle.bodyLarge.font button.translatesAutoresizingMaskIntoConstraints = false return button }() //-------------------------------------------------- // 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 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 = 8 contentStackView.addArrangedSubview(titleLabel) contentStackView.addArrangedSubview(contentLabel) scrollView.addSubview(contentStackView) addSubview(scrollView) addSubview(line) 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: line.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 line.surface = surface titleLabel.text = tooltipModel.title contentLabel.text = tooltipModel.content titleLabel.sizeToFit() contentLabel.sizeToFit() var addedTitle = false if let titleText = tooltipModel.title, !titleText.isEmpty { contentStackView.addArrangedSubview(titleLabel) addedTitle = true } var addedContent = false if let contentText = tooltipModel.content, !contentText.isEmpty { contentStackView.addArrangedSubview(contentLabel) addedContent = true } else if let contentView = tooltipModel.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(tooltipModel.closeButtonText, for: .normal) closeButton.accessibilityLabel = tooltipModel.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 \(tooltipModel.closeButtonText) button to close." var elements: [Any] = [primaryAccessibilityElement] contentStackView.arrangedSubviews.forEach{ elements.append($0) } elements.append(closeButton) accessibilityElements = elements } }