diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index 5dc81d47..25cb8392 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -15,6 +15,8 @@ EA0FC2C62914222900DF80B4 /* ButtonGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA0FC2C52914222900DF80B4 /* ButtonGroup.swift */; }; EA1F266528B945070033E859 /* RadioSwatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA1F266128B945070033E859 /* RadioSwatch.swift */; }; EA1F266628B945070033E859 /* RadioSwatchGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA1F266228B945070033E859 /* RadioSwatchGroup.swift */; }; + EA297A5529FB07760031ED56 /* TooltipLabelAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA297A5429FB07760031ED56 /* TooltipLabelAttribute.swift */; }; + EA297A5729FB0A360031ED56 /* AppleGuidlinesTouchable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA297A5629FB0A360031ED56 /* AppleGuidlinesTouchable.swift */; }; EA336171288B19200071C351 /* VDS.docc in Sources */ = {isa = PBXBuildFile; fileRef = EA336170288B19200071C351 /* VDS.docc */; }; EA336177288B19210071C351 /* VDS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EA33616C288B19200071C351 /* VDS.framework */; }; EA33617C288B19210071C351 /* VDSTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA33617B288B19210071C351 /* VDSTests.swift */; }; @@ -134,6 +136,8 @@ EA0FC2C52914222900DF80B4 /* ButtonGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonGroup.swift; sourceTree = ""; }; EA1F266128B945070033E859 /* RadioSwatch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RadioSwatch.swift; sourceTree = ""; }; EA1F266228B945070033E859 /* RadioSwatchGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RadioSwatchGroup.swift; sourceTree = ""; }; + EA297A5429FB07760031ED56 /* TooltipLabelAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TooltipLabelAttribute.swift; sourceTree = ""; }; + EA297A5629FB0A360031ED56 /* AppleGuidlinesTouchable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleGuidlinesTouchable.swift; sourceTree = ""; }; EA33616C288B19200071C351 /* VDS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = VDS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; EA33616F288B19200071C351 /* VDS.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VDS.h; sourceTree = ""; }; EA336170288B19200071C351 /* VDS.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = VDS.docc; sourceTree = ""; }; @@ -421,6 +425,7 @@ isa = PBXGroup; children = ( EA4DB2FC28D3D0CA00103EE3 /* AnyEquatable.swift */, + EA297A5629FB0A360031ED56 /* AppleGuidlinesTouchable.swift */, EAF1FE9A29DB1A6000101452 /* Changeable.swift */, EAF1FE9829D4850E00101452 /* Clickable.swift */, EAA5EEDF28F49DB3003B3210 /* Colorable.swift */, @@ -661,6 +666,7 @@ EAA5EEB428ECBFB4003B3210 /* ImageLabelAttribute.swift */, EAF7F0AC289B142900B287F5 /* StrikeThroughLabelAttribute.swift */, EAF7F0AE289B144C00B287F5 /* UnderlineLabelAttribute.swift */, + EA297A5429FB07760031ED56 /* TooltipLabelAttribute.swift */, ); path = Attributes; sourceTree = ""; @@ -794,6 +800,7 @@ EAF7F0A6289B0CE000B287F5 /* Resetable.swift in Sources */, EA985C2D296F03FE00F2FF2E /* TileletIconModels.swift in Sources */, EA89200428AECF4B006B9984 /* UITextField+Publisher.swift in Sources */, + EA297A5729FB0A360031ED56 /* AppleGuidlinesTouchable.swift in Sources */, EA3361C328902D960071C351 /* Toggle.swift in Sources */, EAF7F0A0289AB7EC00B287F5 /* View.swift in Sources */, EA89201328B568D8006B9984 /* RadioBox.swift in Sources */, @@ -821,6 +828,7 @@ EAC846F3294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift in Sources */, EAF7F0952899861000B287F5 /* Checkbox.swift in Sources */, EA985BE82968951C00F2FF2E /* TileletTitleModel.swift in Sources */, + EA297A5529FB07760031ED56 /* TooltipLabelAttribute.swift in Sources */, EA985BEA29689B6D00F2FF2E /* TileletSubTitleModel.swift in Sources */, EA3361C9289054C50071C351 /* Surfaceable.swift in Sources */, EAB5FEED2927E1B200998C17 /* ButtonGroupPositionLayout.swift in Sources */, diff --git a/VDS/Components/Label/Attributes/TooltipLabelAttribute.swift b/VDS/Components/Label/Attributes/TooltipLabelAttribute.swift new file mode 100644 index 00000000..e6047dd0 --- /dev/null +++ b/VDS/Components/Label/Attributes/TooltipLabelAttribute.swift @@ -0,0 +1,97 @@ +// +// TooltipLabelAttribute.swift +// VDS +// +// Created by Matt Bruce on 4/27/23. +// + +import Foundation +import UIKit +import Combine +import VDSColorTokens + +public class TooltipLabelAttribute: ActionLabelAttributeModel, TooltipLaunchable { + public var id = UUID() + public var action = PassthroughSubject() + private var subscriber: AnyCancellable? + public var location: Int = 0 + public var length: Int = 3 + public var surface: Surface = .light + public var accessibleText: String? = "Tool Tip" + public var closeButtonText: String = "Close" + public var size: Tooltip.Size = .medium + public var title: String + public var content: String + + public func setAttribute(on attributedString: NSMutableAttributedString) { + //update the location + location = attributedString.string.count + + var imageTintColor: UIColor = surface == .light ? VDSColor.elementsPrimaryOnlight : VDSColor.elementsPrimaryOndark + + //see if you can get the current textColor + var originalRange = NSMakeRange(0, attributedString.length) + if let textColor = attributedString.attribute(.foregroundColor, at: 0, effectiveRange: &originalRange) as? UIColor { + imageTintColor = textColor + } + + //create the space in the attirbuted String for the tooltip image and click action + let spaceForTooltip = String(repeating: " ", count: length) + + //find the middle of the space + let middle = (length/2) + + //add the space to the attributed string + attributedString.insert(NSAttributedString(string: spaceForTooltip), at: location) + + //create the frame in which to hold the icon + let frame = CGRect(x: 0, y: 0, width: size.dimensions.width, height: size.dimensions.width) + + //create the image icon and match the color of the text + let tooltipAttribute = ImageLabelAttribute(location: location + middle, + length: 1, + imageName: "info", + frame: frame, + tintColor: imageTintColor) + + //create the action for the tooltip click + let tooltipAction = ActionLabelAttribute(location: location - middle, + length: length + middle, + shouldUnderline: false, + action: action) + + //apply the attribtes to the current attributedString + tooltipAttribute.setAttribute(on: attributedString) + tooltipAction.setAttribute(on: attributedString) + } + + public init(id: UUID = UUID(), action: PassthroughSubject = PassthroughSubject(), subscriber: AnyCancellable? = nil, surface: Surface, accessibleText: String? = nil, closeButtonText: String, size: Tooltip.Size, title: String, content: String) { + self.id = id + self.action = action + self.subscriber = subscriber + self.surface = surface + self.accessibleText = accessibleText + self.closeButtonText = closeButtonText + self.size = size + self.title = title + self.content = content + + //create the tooltip click event + self.subscriber = action.sink { [weak self] in + guard let self else { return } + self.presentTooltip(surface: self.surface, + title: self.title, + content: self.content, + closeButtonText: self.closeButtonText) + } + } + + 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 47bb0c28..f540e8e4 100644 --- a/VDS/Components/Label/Label.swift +++ b/VDS/Components/Label/Label.swift @@ -156,11 +156,11 @@ open class Label: UILabel, Handlerable, ViewProtocol, Resettable, UserInfoable { //add attribute on the string attribute.setAttribute(on: mutableAttributedString) - + //see if the attribute is Actionable if let actionable = attribute as? any ActionLabelAttributeModel{ //create a accessibleAction - let customAccessibilityAction = customAccessibilityAction(range: actionable.range, accessibleText: actionable.accessibleText) + let customAccessibilityAction = customAccessibilityAction(text: mutableAttributedString.string, range: actionable.range, accessibleText: actionable.accessibleText) //create a wrapper for the attributes range, block and actions.append(LabelAction(range: actionable.range, action: actionable.action, accessibilityID: customAccessibilityAction?.hashValue ?? -1)) @@ -263,7 +263,7 @@ open class Label: UILabel, Handlerable, ViewProtocol, Resettable, UserInfoable { //-------------------------------------------------- // MARK: - Accessibility For Actions //-------------------------------------------------- - private func customAccessibilityAction(range: NSRange, accessibleText: String? = nil) -> UIAccessibilityCustomAction? { + private func customAccessibilityAction(text: String?, range: NSRange, accessibleText: String? = nil) -> UIAccessibilityCustomAction? { guard let text = text else { return nil } //TODO: accessibilityHint for Label diff --git a/VDS/Components/Tooltip/Tooltip.swift b/VDS/Components/Tooltip/Tooltip.swift index 41aea54c..022da1d1 100644 --- a/VDS/Components/Tooltip/Tooltip.swift +++ b/VDS/Components/Tooltip/Tooltip.swift @@ -172,3 +172,12 @@ open class Tooltip: Control, TooltipLaunchable { } } + +// MARK: AppleGuidlinesTouchable +extension Tooltip: AppleGuidlinesTouchable { + + override open func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + Self.acceptablyOutsideBounds(point: point, bounds: bounds) + } + +} diff --git a/VDS/Components/Tooltip/TrailingTooltipLabel.swift b/VDS/Components/Tooltip/TrailingTooltipLabel.swift index 206aa413..0d0f8e19 100644 --- a/VDS/Components/Tooltip/TrailingTooltipLabel.swift +++ b/VDS/Components/Tooltip/TrailingTooltipLabel.swift @@ -64,31 +64,67 @@ open class TrailingTooltipLabel: View, TooltipLaunchable { 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.text = labelText label.textStyle = labelTextStyle label.textPosition = labelTextPosition label.surface = surface label.disabled = disabled + + //add tooltip + if let labelText, !labelText.isEmpty, !tooltipTitle.isEmpty, !tooltipContent.isEmpty { + //create the model + let model = Label.TooltipModel(surface: surface, + closeButtonText: tooltipCloseButtonText, + size: tooltipSize, + title: tooltipTitle, + content: tooltipContent) + + //add the model + label.addTooltip(model: model) + } + } +} + +extension Label { + public struct TooltipModel { + public var surface: Surface + public var closeButtonText: String + public var size: Tooltip.Size + public var title: String + public var content: String + + public init(surface: Surface = .light, closeButtonText: String = "Close", size: Tooltip.Size = .medium, title: String, content: String) { + self.surface = surface + self.closeButtonText = closeButtonText + self.size = size + self.title = title + self.content = content + } + } + + public func addTooltip(model: TooltipModel) { + + var newAttributes: [any LabelAttributeModel] = [] + if let attributes { + attributes.forEach { attribute in + if type(of: attribute) != TooltipLabelAttribute.self { + newAttributes.append(attribute) + } + } + } + + if let text = text, !text.isEmpty { + let tooltip = TooltipLabelAttribute(surface: surface, + closeButtonText: model.closeButtonText, + size: model.size, + title: model.title, + content: model.content) + newAttributes.append(tooltip) + } + + if !newAttributes.isEmpty { + attributes = newAttributes + } } } diff --git a/VDS/Protocols/AppleGuidlinesTouchable.swift b/VDS/Protocols/AppleGuidlinesTouchable.swift new file mode 100644 index 00000000..782e57cd --- /dev/null +++ b/VDS/Protocols/AppleGuidlinesTouchable.swift @@ -0,0 +1,31 @@ +// +// AppleGuidlinesTouchable.swift +// VDS +// +// Created by Matt Bruce on 4/27/23. +// + +import Foundation + +public protocol AppleGuidlinesTouchable { + static var minimumTappableArea: CGFloat { get } + static func acceptablyOutsideBounds(point: CGPoint, bounds: CGRect) -> Bool +} + +extension AppleGuidlinesTouchable { + + static public var minimumTappableArea: CGFloat { + return 45.0 + } + + // If the control is smaller than 45pt by width or height, this will compensate. + static public func acceptablyOutsideBounds(point: CGPoint, bounds: CGRect) -> Bool { + + let faultToleranceX: CGFloat = max((minimumTappableArea - bounds.size.width) / 2.0, 0) + let faultToleranceY: CGFloat = max((minimumTappableArea - bounds.size.height) / 2.0, 0) + let area = bounds.insetBy(dx: -faultToleranceX, dy: -faultToleranceY) + + let contains = area.contains(point) + return contains + } +}