diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index 01cb6e65..971a0126 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -68,7 +68,6 @@ EA985C692971B90B00F2FF2E /* IconSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA985C682971B90B00F2FF2E /* IconSize.swift */; }; EA985C7D297DAED300F2FF2E /* Primitive.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA985C7C297DAED300F2FF2E /* Primitive.swift */; }; EAA5EEB528ECBFB4003B3210 /* ImageLabelAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA5EEB428ECBFB4003B3210 /* ImageLabelAttribute.swift */; }; - EAA5EEB728ECC03A003B3210 /* ToolTipLabelAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA5EEB628ECC03A003B3210 /* ToolTipLabelAttribute.swift */; }; EAA5EEB928ECD24B003B3210 /* Icons.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EAA5EEB828ECD24B003B3210 /* Icons.xcassets */; }; EAA5EEE428F5B855003B3210 /* VerizonNHGDS-Light.otf in Resources */ = {isa = PBXBuildFile; fileRef = EAA5EEE328F5B855003B3210 /* VerizonNHGDS-Light.otf */; }; EAA5EEEF28F5C908003B3210 /* VDSTypographyTokens.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = EAA5EEEC28F5C908003B3210 /* VDSTypographyTokens.xcframework */; }; @@ -78,6 +77,11 @@ EAB1D2CD28ABE76100DAE764 /* Withable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB1D2CC28ABE76000DAE764 /* Withable.swift */; }; EAB1D2CF28ABEF2B00DAE764 /* Typography.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB1D2CE28ABEF2B00DAE764 /* Typography.swift */; }; EAB1D2EA28AE84AA00DAE764 /* UIControlPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB1D2E928AE84AA00DAE764 /* UIControlPublisher.swift */; }; + EAB2375D29E8789100AABE9A /* Tooltip.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB2375C29E8789100AABE9A /* Tooltip.swift */; }; + EAB2376229E9880400AABE9A /* TrailingTooltipLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB2376129E9880400AABE9A /* TrailingTooltipLabel.swift */; }; + EAB2376629E9952D00AABE9A /* UIApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB2376529E9952D00AABE9A /* UIApplication.swift */; }; + EAB2376829E9992800AABE9A /* TooltipAlertViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB2376729E9992800AABE9A /* TooltipAlertViewController.swift */; }; + EAB2376A29E9E59100AABE9A /* TooltipLaunchable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB2376929E9E59100AABE9A /* TooltipLaunchable.swift */; }; EAB5FED429267EB300998C17 /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB5FED329267EB300998C17 /* UIView.swift */; }; EAB5FEED2927E1B200998C17 /* ButtonGroupPositionLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB5FEEC2927E1B200998C17 /* ButtonGroupPositionLayout.swift */; }; EAB5FEF12927F4AA00998C17 /* SelfSizingCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB5FEF02927F4AA00998C17 /* SelfSizingCollectionView.swift */; }; @@ -185,7 +189,6 @@ EA985C682971B90B00F2FF2E /* IconSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconSize.swift; sourceTree = ""; }; EA985C7C297DAED300F2FF2E /* Primitive.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Primitive.swift; sourceTree = ""; }; EAA5EEB428ECBFB4003B3210 /* ImageLabelAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageLabelAttribute.swift; sourceTree = ""; }; - EAA5EEB628ECC03A003B3210 /* ToolTipLabelAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolTipLabelAttribute.swift; sourceTree = ""; }; EAA5EEB828ECD24B003B3210 /* Icons.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Icons.xcassets; sourceTree = ""; }; EAA5EEDF28F49DB3003B3210 /* Colorable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Colorable.swift; sourceTree = ""; }; EAA5EEE328F5B855003B3210 /* VerizonNHGDS-Light.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "VerizonNHGDS-Light.otf"; sourceTree = ""; }; @@ -196,6 +199,11 @@ EAB1D2CC28ABE76000DAE764 /* Withable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Withable.swift; sourceTree = ""; }; EAB1D2CE28ABEF2B00DAE764 /* Typography.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Typography.swift; sourceTree = ""; }; EAB1D2E928AE84AA00DAE764 /* UIControlPublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIControlPublisher.swift; sourceTree = ""; }; + EAB2375C29E8789100AABE9A /* Tooltip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tooltip.swift; sourceTree = ""; }; + EAB2376129E9880400AABE9A /* TrailingTooltipLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrailingTooltipLabel.swift; sourceTree = ""; }; + EAB2376529E9952D00AABE9A /* UIApplication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplication.swift; sourceTree = ""; }; + EAB2376729E9992800AABE9A /* TooltipAlertViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TooltipAlertViewController.swift; sourceTree = ""; }; + EAB2376929E9E59100AABE9A /* TooltipLaunchable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TooltipLaunchable.swift; sourceTree = ""; }; EAB5FED329267EB300998C17 /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = ""; }; EAB5FEEC2927E1B200998C17 /* ButtonGroupPositionLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonGroupPositionLayout.swift; sourceTree = ""; }; EAB5FEF02927F4AA00998C17 /* SelfSizingCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelfSizingCollectionView.swift; sourceTree = ""; }; @@ -381,6 +389,7 @@ EA5E3056295105930082B959 /* Tilelet */, EA5E30512950DD8D0082B959 /* TitleLockup */, EA3361A0288B1E6F0071C351 /* Toggle */, + EAB2375B29E8786100AABE9A /* Tooltip */, ); path = Components; sourceTree = ""; @@ -398,6 +407,7 @@ children = ( EAF7F0992899B17200B287F5 /* CATransaction.swift */, EA33622D2891EA3C0071C351 /* DispatchQueue+Once.swift */, + EAB2376529E9952D00AABE9A /* UIApplication.swift */, EA3361A7288B23300071C351 /* UIColor.swift */, EA33623D2892EE950071C351 /* UIDevice.swift */, EAF7F0B4289C126F00B287F5 /* UILabel.swift */, @@ -580,6 +590,17 @@ path = Publishers; sourceTree = ""; }; + EAB2375B29E8786100AABE9A /* Tooltip */ = { + isa = PBXGroup; + children = ( + EAB2375C29E8789100AABE9A /* Tooltip.swift */, + EAB2376729E9992800AABE9A /* TooltipAlertViewController.swift */, + EAB2376929E9E59100AABE9A /* TooltipLaunchable.swift */, + EAB2376129E9880400AABE9A /* TrailingTooltipLabel.swift */, + ); + path = Tooltip; + sourceTree = ""; + }; EAC9257E29119B5D00091998 /* TextLink */ = { isa = PBXGroup; children = ( @@ -642,7 +663,6 @@ EAF7F0AA289B13FD00B287F5 /* TextStyleLabelAttribute.swift */, EAA5EEB428ECBFB4003B3210 /* ImageLabelAttribute.swift */, EAF7F0AC289B142900B287F5 /* StrikeThroughLabelAttribute.swift */, - EAA5EEB628ECC03A003B3210 /* ToolTipLabelAttribute.swift */, EAF7F0AE289B144C00B287F5 /* UnderlineLabelAttribute.swift */, ); path = Attributes; @@ -783,12 +803,14 @@ EA89201328B568D8006B9984 /* RadioBox.swift in Sources */, EAC9258C2911C9DE00091998 /* InputField.swift in Sources */, EA3362402892EF6C0071C351 /* Label.swift in Sources */, + EAB2376229E9880400AABE9A /* TrailingTooltipLabel.swift in Sources */, + EAB2376A29E9E59100AABE9A /* TooltipLaunchable.swift in Sources */, + EAB2375D29E8789100AABE9A /* Tooltip.swift in Sources */, EA985C23296E033A00F2FF2E /* TextArea.swift in Sources */, EAF7F0B3289B1ADC00B287F5 /* ActionLabelAttribute.swift in Sources */, EAC925832911B35400091998 /* TextLinkCaret.swift in Sources */, EA33622E2891EA3C0071C351 /* DispatchQueue+Once.swift in Sources */, EA4DB2FD28D3D0CA00103EE3 /* AnyEquatable.swift in Sources */, - EAA5EEB728ECC03A003B3210 /* ToolTipLabelAttribute.swift in Sources */, EA5E305A29510F8B0082B959 /* EnumSubset.swift in Sources */, EA985BF7296C665E00F2FF2E /* IconName.swift in Sources */, EAF7F0AF289B144C00B287F5 /* UnderlineLabelAttribute.swift in Sources */, @@ -848,8 +870,10 @@ EA3361B6288B2A410071C351 /* Control.swift in Sources */, 5F21D7BF28DCEB3D003E7CD6 /* Useable.swift in Sources */, EAF7F0B7289C12A600B287F5 /* UITapGestureRecognizer.swift in Sources */, + EAB2376629E9952D00AABE9A /* UIApplication.swift in Sources */, EA985BF9296C710100F2FF2E /* IconColor.swift in Sources */, EAB5FED429267EB300998C17 /* UIView.swift in Sources */, + EAB2376829E9992800AABE9A /* TooltipAlertViewController.swift in Sources */, EA33623E2892EE950071C351 /* UIDevice.swift in Sources */, EA985C692971B90B00F2FF2E /* IconSize.swift in Sources */, EA985C672970C21600F2FF2E /* VDSLayout.swift in Sources */, diff --git a/VDS/Components/Label/Attributes/ActionLabelAttribute.swift b/VDS/Components/Label/Attributes/ActionLabelAttribute.swift index cb3453ef..1b9ce53a 100644 --- a/VDS/Components/Label/Attributes/ActionLabelAttribute.swift +++ b/VDS/Components/Label/Attributes/ActionLabelAttribute.swift @@ -32,17 +32,18 @@ public struct ActionLabelAttribute: ActionLabelAttributeModel { public var length: Int public var shouldUnderline: Bool public var accessibleText: String? - public var action = PassthroughSubject() + public var action: PassthroughSubject public var subscriber: AnyCancellable? //-------------------------------------------------- // MARK: - Initializer //-------------------------------------------------- - public init(location: Int, length: Int, shouldUnderline: Bool = true, accessibleText: String? = nil) { + public init(location: Int, length: Int, shouldUnderline: Bool = true, accessibleText: String? = nil, action: PassthroughSubject = .init() ) { self.location = location self.length = length self.shouldUnderline = shouldUnderline self.accessibleText = accessibleText + self.action = action } private enum CodingKeys: String, CodingKey { @@ -53,5 +54,10 @@ public struct ActionLabelAttribute: ActionLabelAttributeModel { if(shouldUnderline){ UnderlineLabelAttribute(location: location, length: length).setAttribute(on: attributedString) } + attributedString.addAttribute(NSAttributedString.Key.action, value: "handler", range: range) } } + +extension NSAttributedString.Key { + public static let action = NSAttributedString.Key(rawValue: "action") +} diff --git a/VDS/Components/Label/Attributes/ImageLabelAttribute.swift b/VDS/Components/Label/Attributes/ImageLabelAttribute.swift index f46e2e8c..dcd98629 100644 --- a/VDS/Components/Label/Attributes/ImageLabelAttribute.swift +++ b/VDS/Components/Label/Attributes/ImageLabelAttribute.swift @@ -46,7 +46,9 @@ public struct ImageLabelAttribute: AttachmentLabelAttributeModel { private func imageAttachment(image: UIImage) -> NSTextAttachment { let attachment = NSTextAttachment() attachment.image = tintColor != nil ? image.withTintColor(tintColor!) : image - attachment.bounds = frame ?? .init(x: 0, y: 0, width: image.size.width, height: image.size.height) + if let frame { + attachment.bounds = frame + } return attachment } diff --git a/VDS/Components/Label/Attributes/ToolTipLabelAttribute.swift b/VDS/Components/Label/Attributes/ToolTipLabelAttribute.swift deleted file mode 100644 index f42ac630..00000000 --- a/VDS/Components/Label/Attributes/ToolTipLabelAttribute.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// ToolTipLabelAttribute.swift -// VDS -// -// Created by Matt Bruce on 10/4/22. -// - -import Foundation -import UIKit -import Combine - -public struct ToolTipLabelAttribute: ActionLabelAttributeModel { - public var id = UUID() - public var accessibleText: String? = "Tool Tip" - public var action: PassthroughSubject - public var location: Int - public var length: Int - public var tintColor: UIColor - - public func setAttribute(on attributedString: NSMutableAttributedString) { - let image = ImageLabelAttribute(location: location, - length: length, - imageName: "info", - frame: .init(x: 0, y: -2, width: 13.3, height: 13.3), - tintColor: tintColor) - - image.setAttribute(on: attributedString) - - } - - public init(action: PassthroughSubject = .init(), location: Int, length: Int, tintColor: UIColor = .black, accessibleText: String? = nil){ - self.action = action - self.location = location - self.length = length - self.tintColor = tintColor - self.accessibleText = accessibleText - } - - public static func == (lhs: ToolTipLabelAttribute, rhs: ToolTipLabelAttribute) -> Bool { - lhs.isEqual(rhs) - } - - public func isEqual(_ equatable: ToolTipLabelAttribute) -> Bool { - return id == equatable.id && range == equatable.range - } -} - diff --git a/VDS/Components/Label/Label.swift b/VDS/Components/Label/Label.swift index 535b5726..47bb0c28 100644 --- a/VDS/Components/Label/Label.swift +++ b/VDS/Components/Label/Label.swift @@ -253,7 +253,7 @@ open class Label: UILabel, Handlerable, ViewProtocol, Resettable, UserInfoable { @objc private func textLinkTapped(_ gesture: UITapGestureRecognizer) { for actionable in actions { // This determines if we tapped on the desired range of text. - if gesture.didTapAttributedTextInLabel(self, inRange: actionable.range) { + if gesture.didTapActionInLabel(self, inRange: actionable.range) { actionable.performAction() return } diff --git a/VDS/Components/Line/Line.swift b/VDS/Components/Line/Line.swift index b08f6273..c62ca2e1 100644 --- a/VDS/Components/Line/Line.swift +++ b/VDS/Components/Line/Line.swift @@ -51,7 +51,7 @@ open class Line: View { //-------------------------------------------------- // MARK: - Configuration //-------------------------------------------------- - private var lineViewColorConfig: AnyColorable = { + public var lineViewColorConfig: AnyColorable = { let config = KeyedColorConfiguration(keyPath: \.style) config.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forKey: .primary) config.setSurfaceColors(VDSColor.elementsLowContrastOnLight, VDSColor.elementsLowContrastOnDark, forKey: .secondary) diff --git a/VDS/Components/TextFields/EntryField/EntryField.swift b/VDS/Components/TextFields/EntryField/EntryField.swift index 20f221cb..dfadc9bc 100644 --- a/VDS/Components/TextFields/EntryField/EntryField.swift +++ b/VDS/Components/TextFields/EntryField/EntryField.swift @@ -106,11 +106,12 @@ open class EntryField: Control, Changeable { } } - open var titleLabel = Label().with { + open var titleLabel = TrailingTooltipLabel().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) - $0.attributes = [] - $0.textPosition = .left - $0.textStyle = .bodySmall + $0.labelTextPosition = .left + $0.labelTextStyle = .bodySmall + $0.tooltipSize = .small + $0.tooltipYOffset = -2 } open var errorLabel = Label().with { @@ -228,56 +229,14 @@ open class EntryField: Control, Changeable { return containerView } - open func getToolTipView() -> UIView? { - guard let tooltipTitle, let tooltipContent, !tooltipTitle.isEmpty, !tooltipContent.isEmpty else { - return nil - } - - let stack = UIStackView().with { - $0.translatesAutoresizingMaskIntoConstraints = false - $0.axis = .vertical - $0.distribution = .fill - $0.spacing = 4 - } - - let title = Label().with { - $0.setContentCompressionResistancePriority(.required, for: .vertical) - $0.textPosition = .left - $0.textStyle = .boldBodySmall - $0.text = tooltipTitle - $0.surface = surface - $0.disabled = disabled - } - - let content = Label().with { - $0.setContentCompressionResistancePriority(.required, for: .vertical) - $0.textPosition = .left - $0.textStyle = .boldBodySmall - $0.text = tooltipContent - $0.surface = surface - $0.disabled = disabled - } - - stack.addArrangedSubview(title) - stack.addArrangedSubview(content) - - stack.backgroundColor = backgroundColorConfiguration.getColor(self) - - return stack - } - - open func showToolTipView(){ - print("toolTip clicked: showToolTipView() called") - } - open override func reset() { super.reset() titleLabel.reset() errorLabel.reset() helperLabel.reset() - titleLabel.textPosition = .left - titleLabel.textStyle = .bodySmall + titleLabel.labelTextPosition = .left + titleLabel.labelTextStyle = .bodySmall errorLabel.textPosition = .left errorLabel.textStyle = .bodySmall helperLabel.textPosition = .left @@ -332,29 +291,12 @@ open class EntryField: Control, Changeable { updatedLabelText = "\(oldText) Optional" attributes.append(optionColorAttr) } - - //add the tool tip - if let view = getToolTipView(), let oldText = updatedLabelText { - tooltipView = view - let toolTipAction = PassthroughSubject() - let toolTipUpdateText = "\(oldText) " //create a little space between the final character and tooltip image - - let toolTipAttribute = ToolTipLabelAttribute(action: toolTipAction, - location: toolTipUpdateText.count - 1, - length: 1, - tintColor: primaryColorConfig.getColor(self)) - updatedLabelText = toolTipUpdateText - attributes.append(toolTipAttribute) - toolTipAction.sink { [weak self] in - self?.showToolTipView() - }.store(in: &subscribers) - } else { - tooltipView = nil - } - + //set the titleLabel - titleLabel.text = updatedLabelText - titleLabel.attributes = attributes + titleLabel.labelText = updatedLabelText + titleLabel.labelAttributes = attributes + titleLabel.tooltipTitle = tooltipTitle ?? "" + titleLabel.tooltipContent = tooltipContent ?? "" titleLabel.surface = surface titleLabel.disabled = disabled diff --git a/VDS/Components/Tooltip/Tooltip.swift b/VDS/Components/Tooltip/Tooltip.swift new file mode 100644 index 00000000..a3b3e612 --- /dev/null +++ b/VDS/Components/Tooltip/Tooltip.swift @@ -0,0 +1,152 @@ +// +// Tooltip.swift +// VDS +// +// Created by Matt Bruce on 4/13/23. +// + +import Foundation +import UIKit +import VDSColorTokens +import VDSFormControlsTokens +import Combine + +@objc(VDSTooltip) +open class Tooltip: Control, TooltipLaunchable { + + //-------------------------------------------------- + // MARK: - Enums + //-------------------------------------------------- + public enum FillColor: String, CaseIterable { + case primary, secondary, brandHighlight + } + + public enum Size: String, CaseIterable { + case small + case medium + + public var dimensions: CGSize { + switch self { + + case .small: + return .init(width: 13.33, height: 13.33) + + case .medium: + return .init(width: 16.67, height: 16.67) + + } + } + } + + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- + private var widthConstraint: NSLayoutConstraint? + private var heightConstraint: NSLayoutConstraint? + private var infoImage = UIImage() + + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + open var imageView = UIImageView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.contentMode = .scaleAspectFill + $0.clipsToBounds = true + } + + open var closeButtonText: String = "Close" { didSet { didChange() }} + + open var fillColor: FillColor = .primary { didSet { didChange() }} + + open var size: Size = .medium { didSet { didChange() }} + + open var title: String = "" { didSet { didChange() }} + + open var content: String = "" { didSet { didChange() }} + + //-------------------------------------------------- + // MARK: - Configuration + //-------------------------------------------------- + private var iconColorConfig: AnyColorable = { + let config = KeyedColorConfiguration(keyPath: \.fillColor) + config.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forKey: .primary) + config.setSurfaceColors(VDSColor.elementsSecondaryOnlight, VDSColor.elementsSecondaryOndark, forKey: .secondary) + config.setSurfaceColors(VDSColor.elementsBrandhighlight, VDSColor.elementsBrandhighlight, forKey: .brandHighlight) + return config.eraseToAnyColorable() + }() + + //-------------------------------------------------- + // 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: - Lifecycle + //-------------------------------------------------- + + open override func setup() { + super.setup() + + if let image = BundleManager.shared.image(for: "info") { + infoImage = image + } + + addSubview(imageView) + + imageView.pinToSuperView() + heightConstraint = imageView.heightAnchor.constraint(equalToConstant: size.dimensions.height) + heightConstraint?.isActive = true + widthConstraint = imageView.widthAnchor.constraint(equalToConstant: size.dimensions.width) + widthConstraint?.isActive = true + + backgroundColor = .clear + + isAccessibilityElement = true + accessibilityTraits = .link + + onClickSubscriber = publisher(for: .touchUpInside) + .sink(receiveValue: { [weak self] tooltip in + guard let self else { return} + self.presentTooltip(surface: tooltip.surface, + title: tooltip.title, + content: tooltip.content, + closeButtonText: tooltip.closeButtonText) + }) + } + + open override func reset() { + super.reset() + size = .medium + title = "" + content = "" + fillColor = .primary + closeButtonText = "Close" + imageView.image = nil + } + + open override func updateView() { + super.updateView() + + //set the dimensions + let dimensions = size.dimensions + heightConstraint?.constant = dimensions.height + widthConstraint?.constant = dimensions.width + + //get the color for the image + let imageColor = iconColorConfig.getColor(self) + imageView.image = infoImage.withTintColor(imageColor) + + accessibilityLabel = "Tooltip: \(title)" + } + +} diff --git a/VDS/Components/Tooltip/TooltipAlertViewController.swift b/VDS/Components/Tooltip/TooltipAlertViewController.swift new file mode 100644 index 00000000..a6cbf14a --- /dev/null +++ b/VDS/Components/Tooltip/TooltipAlertViewController.swift @@ -0,0 +1,162 @@ +// +// TooltipAlertViewController.swift +// VDS +// +// Created by Matt Bruce on 4/14/23. +// + +import Foundation +import UIKit +import Combine +import VDSColorTokens + +open class TooltipAlertViewController: UIViewController, Surfaceable { + + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- + private var onClickSubscriber: AnyCancellable? { + willSet { + if let onClickSubscriber { + onClickSubscriber.cancel() + } + } + } + + private var scrollView = UIScrollView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.backgroundColor = .clear + $0.scrollIndicatorInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: -5) + } + + private let containerView = View().with { + $0.layer.cornerRadius = 8 + $0.layer.shadowColor = UIColor.black.cgColor + $0.layer.shadowOpacity = 0.5 + $0.layer.shadowOffset = CGSize.zero + $0.layer.shadowRadius = 5 + } + + private var line = Line().with { instance in + instance.lineViewColorConfig = SurfaceColorConfiguration(VDSColor.elementsLowContrastOnLight, VDSColor.elementsLowContrastOnLight).eraseToAnyColorable() + } + + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + open var surface: Surface = .light { didSet { updateView() }} + open var titleText: String = "" { didSet { updateView() }} + open var titleLabel = Label().with { label in + label.textStyle = .boldTitleMedium + } + + open var contentText: String = "" { didSet { updateView() }} + open var contentLabel = Label().with { label in + label.textStyle = .bodyMedium + } + + open var closeButtonText: String = "Close" { didSet { updateView() }} + + open lazy var closeButton: UIButton = { + let button = UIButton(type: .system) + button.backgroundColor = .clear + button.setTitle("Close", for: .normal) + button.titleLabel?.font = TextStyle.bodyLarge.font + button.translatesAutoresizingMaskIntoConstraints = false + onClickSubscriber = button.publisher(for: .touchUpInside).sink {[weak self] button in + guard let self else { return } + self.dismiss(animated: true, completion: nil) + } + return button + }() + + //-------------------------------------------------- + // MARK: - Configuration + //-------------------------------------------------- + private let containerViewBackgroundColorConfiguration = SurfaceColorConfiguration().with { instance in + instance.lightColor = .white + instance.darkColor = .black + } + + private let backgroundColorConfiguration = SurfaceColorConfiguration(VDSColor.backgroundPrimaryDark, VDSColor.backgroundPrimaryLight) + + private let closeButtonTextColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark) + + //-------------------------------------------------- + // MARK: - Lifecycle + //-------------------------------------------------- + open override func viewDidLoad() { + super.viewDidLoad() + isModalInPresentation = true + setup() + } + + open func setup() { + + scrollView.addSubview(titleLabel) + scrollView.addSubview(contentLabel) + containerView.addSubview(scrollView) + containerView.addSubview(line) + containerView.addSubview(closeButton) + view.addSubview(containerView) + + NSLayoutConstraint.activate([ + containerView.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor, constant: VDSLayout.Spacing.space8X.value), + containerView.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor, constant: -VDSLayout.Spacing.space8X.value), + containerView.topAnchor.constraint(greaterThanOrEqualTo: view.topAnchor), + containerView.bottomAnchor.constraint(lessThanOrEqualTo: view.bottomAnchor), + + containerView.heightAnchor.constraint(equalToConstant: 312), + containerView.widthAnchor.constraint(equalToConstant: 296), + containerView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + containerView.centerYAnchor.constraint(equalTo: view.centerYAnchor), + + scrollView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: VDSLayout.Spacing.space4X.value), + scrollView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: VDSLayout.Spacing.space4X.value), + scrollView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -VDSLayout.Spacing.space4X.value), + scrollView.bottomAnchor.constraint(equalTo: line.topAnchor, constant: -VDSLayout.Spacing.space4X.value), + + titleLabel.topAnchor.constraint(equalTo: scrollView.topAnchor), + titleLabel.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), + titleLabel.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), + titleLabel.widthAnchor.constraint(equalTo: scrollView.widthAnchor), + + contentLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: VDSLayout.Spacing.space1X.value), + contentLabel.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), + contentLabel.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), + contentLabel.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), + contentLabel.widthAnchor.constraint(equalTo: scrollView.widthAnchor), + + line.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), + line.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), + + closeButton.topAnchor.constraint(equalTo: line.bottomAnchor), + closeButton.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), + closeButton.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), + closeButton.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), + closeButton.heightAnchor.constraint(equalToConstant: 44.0) + ]) + + } + + open func updateView() { + view.backgroundColor = backgroundColorConfiguration.getColor(self).withAlphaComponent(0.3) + containerView.backgroundColor = containerViewBackgroundColorConfiguration.getColor(self) + scrollView.indicatorStyle = surface == .light ? .black : .white + + titleLabel.surface = surface + contentLabel.surface = surface + line.surface = surface + + titleLabel.text = titleText + contentLabel.text = contentText + titleLabel.sizeToFit() + contentLabel.sizeToFit() + + let closeButtonTextColor = closeButtonTextColorConfiguration.getColor(self) + closeButton.setTitleColor(closeButtonTextColor, for: .normal) + closeButton.setTitleColor(closeButtonTextColor, for: .highlighted) + closeButton.setTitle(closeButtonText, for: .normal) + } +} + diff --git a/VDS/Components/Tooltip/TooltipLaunchable.swift b/VDS/Components/Tooltip/TooltipLaunchable.swift new file mode 100644 index 00000000..6a79e987 --- /dev/null +++ b/VDS/Components/Tooltip/TooltipLaunchable.swift @@ -0,0 +1,27 @@ +// +// ToolTipLaunchable.swift +// VDS +// +// Created by Matt Bruce on 4/14/23. +// + +import Foundation +import UIKit + +public protocol TooltipLaunchable { } + +extension TooltipLaunchable { + public func presentTooltip(surface: Surface, title: String, content: String, closeButtonText: String = "Close") { + if let presenting = UIApplication.topViewController() { + let tooltipViewController = TooltipAlertViewController(nibName: nil, bundle: nil).with { + $0.surface = surface + $0.titleText = title + $0.contentText = content + $0.closeButtonText = closeButtonText + $0.modalPresentationStyle = .overCurrentContext + $0.modalTransitionStyle = .crossDissolve + } + presenting.present(tooltipViewController, animated: true) + } + } +} diff --git a/VDS/Components/Tooltip/TrailingTooltipLabel.swift b/VDS/Components/Tooltip/TrailingTooltipLabel.swift new file mode 100644 index 00000000..206aa413 --- /dev/null +++ b/VDS/Components/Tooltip/TrailingTooltipLabel.swift @@ -0,0 +1,94 @@ +// +// TrailingTooltipLabel.swift +// VDS +// +// Created by Matt Bruce on 4/14/23. +// + +import Foundation +import UIKit +import Combine + +@objc(VDSTrailingTooltipLabel) +open class TrailingTooltipLabel: View, TooltipLaunchable { + + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- + private let tooltipAction = PassthroughSubject() + + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + open var label = Label() + + open var labelText: String? { didSet { didChange() }} + + open var labelAttributes: [any LabelAttributeModel]? { didSet { didChange() } } + + open var labelTextStyle: TextStyle = .defaultStyle { didSet { didChange() } } + + open var labelTextPosition: TextPosition = .left { didSet { didChange() } } + + public lazy var textColorConfiguration: AnyColorable = { + label.textColorConfiguration + }() { didSet { didChange() }} + + open var tooltipCloseButtonText: String = "Close" { didSet { didChange() } } + + open var tooltipSize: Tooltip.Size = .medium { didSet { didChange() } } + + open var tooltipTitle: String = "" { didSet { didChange() } } + + open var tooltipContent: String = "" { didSet { didChange() } } + + open var tooltipYOffset: CGFloat = 0 { didSet { didChange() } } + //-------------------------------------------------- + // MARK: - Overrides + //-------------------------------------------------- + open override func setup() { + super.setup() + + addSubview(label) + label.pinToSuperView() + + //create the tooltip click event + tooltipAction.sink { [weak self] in + guard let self else { return } + self.presentTooltip(surface: self.surface, + title: self.tooltipTitle, + content: self.tooltipContent, + closeButtonText: self.tooltipCloseButtonText) + }.store(in: &subscribers) + } + + open override func updateView() { + super.updateView() + + var attributes: [any LabelAttributeModel] = [] + if let labelAttributes { + attributes.append(contentsOf: labelAttributes) + } + + var updatedLabelText = labelText + + //add the tool tip + if let oldText = updatedLabelText, !tooltipTitle.isEmpty, !tooltipContent.isEmpty { + let tooltipUpdateText = "\(oldText) " //create a little space between the final character and tooltip image + let frame = CGRect(x: 0, y: tooltipYOffset, width: tooltipSize.dimensions.width, height: tooltipSize.dimensions.width) + let color = textColorConfiguration.getColor(self) + let tooltipAttribute = ImageLabelAttribute(location: tooltipUpdateText.count - 2, imageName: "info", frame: frame, tintColor: color) + let tooltipAction = ActionLabelAttribute(location: tooltipUpdateText.count - 3, length: 3, shouldUnderline: false, action: tooltipAction) + updatedLabelText = tooltipUpdateText + attributes.append(tooltipAttribute) + attributes.append(tooltipAction) + } + //set the titleLabel + label.text = updatedLabelText + label.attributes = attributes + label.textStyle = labelTextStyle + label.textPosition = labelTextPosition + label.surface = surface + label.disabled = disabled + } +} diff --git a/VDS/Extensions/UIApplication.swift b/VDS/Extensions/UIApplication.swift new file mode 100644 index 00000000..52cf69d6 --- /dev/null +++ b/VDS/Extensions/UIApplication.swift @@ -0,0 +1,32 @@ +// +// UIApplication.swift +// VDS +// +// Created by Matt Bruce on 4/14/23. +// + +import Foundation +import UIKit + +extension UIApplication { + + public class func topViewController(controller: UIViewController? = UIApplication.shared.windows.first?.rootViewController) -> UIViewController? { + + if let nav = controller as? UINavigationController { + return topViewController(controller: nav.visibleViewController) + } + + if let tab = controller as? UITabBarController { + if let selected = tab.selectedViewController { + return topViewController(controller: selected) + } + } + + if let presented = controller?.presentedViewController { + return topViewController(controller: presented) + } + + return controller + } +} + diff --git a/VDS/Extensions/UITapGestureRecognizer.swift b/VDS/Extensions/UITapGestureRecognizer.swift index cf4121ba..1993fdec 100644 --- a/VDS/Extensions/UITapGestureRecognizer.swift +++ b/VDS/Extensions/UITapGestureRecognizer.swift @@ -10,38 +10,20 @@ import UIKit extension UITapGestureRecognizer { - public func didTapAttributedTextInLabel(_ label: UILabel, inRange targetRange: NSRange) -> Bool { - - guard let abstractContainer = label.abstractTextContainer() else { return false } - let textContainer = abstractContainer.0 - let layoutManager = abstractContainer.1 + public func didTapActionInLabel(_ label: UILabel, inRange targetRange: NSRange) -> Bool { - let tapLocation = location(in: label) - let indexOfGlyph = layoutManager.glyphIndex(for: tapLocation, in: textContainer) - let intrinsicWidth = label.intrinsicContentSize.width + guard let attributedText = label.attributedText else { return false } - // Assert that tapped occured within acceptable bounds based on alignment. - switch label.textAlignment { - case .right: - if tapLocation.x < label.bounds.width - intrinsicWidth { - return false - } - case .center: - let halfBounds = label.bounds.width / 2 - let halfIntrinsicWidth = intrinsicWidth / 2 - - if tapLocation.x > halfBounds + halfIntrinsicWidth { - return false - } else if tapLocation.x < halfBounds - halfIntrinsicWidth { - return false - } - default: // Left align - if tapLocation.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(tapLocation) && NSLocationInRange(indexOfGlyph, targetRange) + 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 } }