diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index d6c5f3f5..1d773b71 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 186B2A8A2B88DA7F001AB71F /* TextAreaChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 186B2A892B88DA7F001AB71F /* TextAreaChangeLog.txt */; }; 18792A902B7431F2008C0D29 /* ButtonIconBadgeIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18792A8F2B7431F2008C0D29 /* ButtonIconBadgeIndicatorModel.swift */; }; 18BDEE822B75316E00452358 /* ButtonIconChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 18BDEE812B75316E00452358 /* ButtonIconChangeLog.txt */; }; 445BA07829C07B3D0036A7C5 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 445BA07729C07B3D0036A7C5 /* Notification.swift */; }; @@ -16,8 +17,10 @@ 5FC35BE328D51405004EBEAC /* Button.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FC35BE228D51405004EBEAC /* Button.swift */; }; 710607952B91A99500F2863F /* TitleletChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 710607942B91A99500F2863F /* TitleletChangeLog.txt */; }; 7115BD3C2B84C0C200E0A610 /* TileContainerChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 7115BD3B2B84C0C200E0A610 /* TileContainerChangeLog.txt */; }; - 71BFA70A2B7F70E6000DCE33 /* Dropshadowable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71BFA7092B7F70E6000DCE33 /* Dropshadowable.swift */; }; + 71BFA70A2B7F70E6000DCE33 /* DropShadowable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71BFA7092B7F70E6000DCE33 /* DropShadowable.swift */; }; 71C02B382B7BD98F00E93E66 /* NotificationChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 71C02B372B7BD98F00E93E66 /* NotificationChangeLog.txt */; }; + 71FC86DE2B9738B900700965 /* SurfaceConfigurationValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71FC86DD2B9738B900700965 /* SurfaceConfigurationValue.swift */; }; + 71FC86E02B973AE500700965 /* DropShadowConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71FC86DF2B973AE500700965 /* DropShadowConfiguration.swift */; }; EA0B18022A9E236900F2D0CD /* SelectorGroupBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA0B18012A9E236900F2D0CD /* SelectorGroupBase.swift */; }; EA0B18052A9E2D2D00F2D0CD /* SelectorBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA0B18032A9E2D2D00F2D0CD /* SelectorBase.swift */; }; EA0B18062A9E2D2D00F2D0CD /* SelectorItemBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA0B18042A9E2D2D00F2D0CD /* SelectorItemBase.swift */; }; @@ -68,6 +71,7 @@ EA5F86C82A1BD99100BC83E4 /* TabModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5F86C72A1BD99100BC83E4 /* TabModel.swift */; }; EA5F86CC2A1D28B500BC83E4 /* ReleaseNotes.txt in Resources */ = {isa = PBXBuildFile; fileRef = EA5F86CB2A1D28B500BC83E4 /* ReleaseNotes.txt */; }; EA5F86D02A1F936100BC83E4 /* TabsContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5F86CF2A1F936100BC83E4 /* TabsContainer.swift */; }; + EA6F330E2B911E9000BACAB9 /* TextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA6F330D2B911E9000BACAB9 /* TextView.swift */; }; EA81410B2A0E8E3C004F60D2 /* ButtonIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA81410A2A0E8E3C004F60D2 /* ButtonIcon.swift */; }; EA8141102A127066004F60D2 /* UIColor+VDSColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA81410F2A127066004F60D2 /* UIColor+VDSColor.swift */; }; EA89200428AECF4B006B9984 /* UITextField+Publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA89200328AECF4B006B9984 /* UITextField+Publisher.swift */; }; @@ -176,6 +180,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 186B2A892B88DA7F001AB71F /* TextAreaChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = TextAreaChangeLog.txt; sourceTree = ""; }; 18792A8F2B7431F2008C0D29 /* ButtonIconBadgeIndicatorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonIconBadgeIndicatorModel.swift; sourceTree = ""; }; 18BDEE812B75316E00452358 /* ButtonIconChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ButtonIconChangeLog.txt; sourceTree = ""; }; 445BA07729C07B3D0036A7C5 /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = ""; }; @@ -185,8 +190,10 @@ 5FC35BE228D51405004EBEAC /* Button.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Button.swift; sourceTree = ""; }; 710607942B91A99500F2863F /* TitleletChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = TitleletChangeLog.txt; sourceTree = ""; }; 7115BD3B2B84C0C200E0A610 /* TileContainerChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = TileContainerChangeLog.txt; sourceTree = ""; }; - 71BFA7092B7F70E6000DCE33 /* Dropshadowable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dropshadowable.swift; sourceTree = ""; }; + 71BFA7092B7F70E6000DCE33 /* DropShadowable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropShadowable.swift; sourceTree = ""; }; 71C02B372B7BD98F00E93E66 /* NotificationChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = NotificationChangeLog.txt; sourceTree = ""; }; + 71FC86DD2B9738B900700965 /* SurfaceConfigurationValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceConfigurationValue.swift; sourceTree = ""; }; + 71FC86DF2B973AE500700965 /* DropShadowConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropShadowConfiguration.swift; sourceTree = ""; }; EA0B18012A9E236900F2D0CD /* SelectorGroupBase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectorGroupBase.swift; sourceTree = ""; }; EA0B18032A9E2D2D00F2D0CD /* SelectorBase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectorBase.swift; sourceTree = ""; }; EA0B18042A9E2D2D00F2D0CD /* SelectorItemBase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectorItemBase.swift; sourceTree = ""; }; @@ -238,6 +245,7 @@ EA5F86C72A1BD99100BC83E4 /* TabModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabModel.swift; sourceTree = ""; }; EA5F86CB2A1D28B500BC83E4 /* ReleaseNotes.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ReleaseNotes.txt; sourceTree = ""; }; EA5F86CF2A1F936100BC83E4 /* TabsContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabsContainer.swift; sourceTree = ""; }; + EA6F330D2B911E9000BACAB9 /* TextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextView.swift; sourceTree = ""; }; EA81410A2A0E8E3C004F60D2 /* ButtonIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonIcon.swift; sourceTree = ""; }; EA81410F2A127066004F60D2 /* UIColor+VDSColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+VDSColor.swift"; sourceTree = ""; }; EA89200328AECF4B006B9984 /* UITextField+Publisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextField+Publisher.swift"; sourceTree = ""; }; @@ -568,7 +576,7 @@ EAB1D2CC28ABE76000DAE764 /* Withable.swift */, 5F21D7BE28DCEB3D003E7CD6 /* Useable.swift */, EAACB8992B927108006A3869 /* Valuing.swift */, - 71BFA7092B7F70E6000DCE33 /* Dropshadowable.swift */, + 71BFA7092B7F70E6000DCE33 /* DropShadowable.swift */, ); path = Protocols; sourceTree = ""; @@ -587,6 +595,8 @@ isa = PBXGroup; children = ( EA3361BC288B2C760071C351 /* TypeAlias.swift */, + 71FC86DD2B9738B900700965 /* SurfaceConfigurationValue.swift */, + 71FC86DF2B973AE500700965 /* DropShadowConfiguration.swift */, ); path = Utilities; sourceTree = ""; @@ -724,6 +734,8 @@ isa = PBXGroup; children = ( EA985C22296E033A00F2FF2E /* TextArea.swift */, + EA6F330D2B911E9000BACAB9 /* TextView.swift */, + 186B2A892B88DA7F001AB71F /* TextAreaChangeLog.txt */, ); path = TextArea; sourceTree = ""; @@ -955,6 +967,7 @@ EA3362042891E14D0071C351 /* VerizonNHGeTX-Bold.otf in Resources */, 71C02B382B7BD98F00E93E66 /* NotificationChangeLog.txt in Resources */, EAEEECA72B1F952000531FC2 /* TabsChangeLog.txt in Resources */, + 186B2A8A2B88DA7F001AB71F /* TextAreaChangeLog.txt in Resources */, EAEEEC962B1F893B00531FC2 /* ButtonChangeLog.txt in Resources */, 710607952B91A99500F2863F /* TitleletChangeLog.txt in Resources */, EA5F86CC2A1D28B500BC83E4 /* ReleaseNotes.txt in Resources */, @@ -1010,7 +1023,7 @@ EAACB8982B92706F006A3869 /* DefaultValuing.swift in Sources */, EAB2376A29E9E59100AABE9A /* TooltipLaunchable.swift in Sources */, EAB2375D29E8789100AABE9A /* Tooltip.swift in Sources */, - 71BFA70A2B7F70E6000DCE33 /* Dropshadowable.swift in Sources */, + 71BFA70A2B7F70E6000DCE33 /* DropShadowable.swift in Sources */, EA0D1C452A6AD73000E5C127 /* RawRepresentable.swift in Sources */, EA985C23296E033A00F2FF2E /* TextArea.swift in Sources */, EAF7F0B3289B1ADC00B287F5 /* ActionLabelAttribute.swift in Sources */, @@ -1036,6 +1049,7 @@ EAC846F3294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift in Sources */, EAF7F0952899861000B287F5 /* CheckboxItem.swift in Sources */, EA985BE82968951C00F2FF2E /* TileletTitleModel.swift in Sources */, + 71FC86DE2B9738B900700965 /* SurfaceConfigurationValue.swift in Sources */, EA297A5529FB07760031ED56 /* TooltipLabelAttribute.swift in Sources */, EA985BEA29689B6D00F2FF2E /* TileletSubTitleModel.swift in Sources */, EA3361C9289054C50071C351 /* Surfaceable.swift in Sources */, @@ -1067,6 +1081,7 @@ EAF1FE9B29DB1A6000101452 /* Changeable.swift in Sources */, EAF7F0A2289AFB3900B287F5 /* Errorable.swift in Sources */, EA8E40912A7D3F6300934ED3 /* UIView+Accessibility.swift in Sources */, + EA6F330E2B911E9000BACAB9 /* TextView.swift in Sources */, EA985C7D297DAED300F2FF2E /* Primitive.swift in Sources */, EAF1FE9929D4850E00101452 /* Clickable.swift in Sources */, EAD0688E2A55F819002E3A2D /* Loader.swift in Sources */, @@ -1105,6 +1120,7 @@ EAB2376829E9992800AABE9A /* TooltipAlertViewController.swift in Sources */, EA33623E2892EE950071C351 /* UIDevice.swift in Sources */, EA985C692971B90B00F2FF2E /* IconSize.swift in Sources */, + 71FC86E02B973AE500700965 /* DropShadowConfiguration.swift in Sources */, EA985C672970C21600F2FF2E /* VDSLayout.swift in Sources */, EA3362302891EB4A0071C351 /* Font.swift in Sources */, EAF7F0AD289B142900B287F5 /* StrikeThroughLabelAttribute.swift in Sources */, diff --git a/VDS/BaseClasses/Control.swift b/VDS/BaseClasses/Control.swift index e4ba8cde..cc2246aa 100644 --- a/VDS/BaseClasses/Control.swift +++ b/VDS/BaseClasses/Control.swift @@ -132,4 +132,8 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable { return true } + open override func layoutSubviews() { + super.layoutSubviews() + setNeedsUpdate() + } } diff --git a/VDS/BaseClasses/View.swift b/VDS/BaseClasses/View.swift index 89e5ce4b..a807c25c 100644 --- a/VDS/BaseClasses/View.swift +++ b/VDS/BaseClasses/View.swift @@ -86,4 +86,9 @@ open class View: UIView, ViewProtocol, UserInfoable { isEnabled = true } + open override func layoutSubviews() { + super.layoutSubviews() + setNeedsUpdate() + } + } diff --git a/VDS/Components/Icon/ButtonIcon/ButtonIcon.swift b/VDS/Components/Icon/ButtonIcon/ButtonIcon.swift index 16f0416f..65968214 100644 --- a/VDS/Components/Icon/ButtonIcon/ButtonIcon.swift +++ b/VDS/Components/Icon/ButtonIcon/ButtonIcon.swift @@ -232,19 +232,26 @@ open class ButtonIcon: Control, Changeable, FormFieldable { }() } - private struct LowContrastColorFillFloatingConfiguration: Configuration, Dropshadowable { + private struct LowContrastColorFillFloatingConfiguration: Configuration, DropShadowableConfiguration { var kind: Kind = .lowContrast var surfaceType: SurfaceType = .colorFill var floating: Bool = true var backgroundColorConfiguration: AnyColorable = { SurfaceColorConfiguration(VDSColor.paletteWhite, VDSColor.paletteGray20).eraseToAnyColorable() }() - var shadowColorConfiguration: AnyColorable = { - SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteBlack).eraseToAnyColorable() - }() - var shadowOpacity: CGFloat = 0.16 - var shadowOffset: CGSize = .init(width: 0, height: 2) - var shadowRadius: CGFloat = 4 + private let dropshadow1Configuration = DropShadowConfiguration().with { + $0.shadowColorConfiguration = SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteBlack).eraseToAnyColorable() + $0.shadowOpacityConfiguration = SurfaceConfigurationValue(CGFloat(0.12), CGFloat(0.22)) + $0.shadowOffsetConfiguration = SurfaceConfigurationValue(.init(width: 0, height: 1), .init(width: 0, height: 1)) + $0.shadowRadiusConfiguration = SurfaceConfigurationValue(CGFloat(10), CGFloat(12)) + } + private let dropshadow2Configuration = DropShadowConfiguration().with { + $0.shadowColorConfiguration = SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteBlack).eraseToAnyColorable() + $0.shadowOpacityConfiguration = SurfaceConfigurationValue(CGFloat(0.05), CGFloat(0.15)) + $0.shadowOffsetConfiguration = SurfaceConfigurationValue(.init(width: 0, height: 2), .init(width: 0, height: 2)) + $0.shadowRadiusConfiguration = SurfaceConfigurationValue(CGFloat(4), CGFloat(6)) + } + var configurations: [DropShadowable] { [dropshadow1Configuration, dropshadow2Configuration] } } private struct LowContrastMediaConfiguration: Configuration, Borderable { @@ -260,19 +267,26 @@ open class ButtonIcon: Control, Changeable, FormFieldable { }() } - private struct LowContrastMediaFloatingConfiguration: Configuration, Dropshadowable { + private struct LowContrastMediaFloatingConfiguration: Configuration, DropShadowableConfiguration { var kind: Kind = .lowContrast var surfaceType: SurfaceType = .media var floating: Bool = true var backgroundColorConfiguration: AnyColorable = { SurfaceColorConfiguration(VDSColor.paletteWhite, VDSColor.paletteGray20).eraseToAnyColorable() }() - var shadowColorConfiguration: AnyColorable = { - SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteBlack).eraseToAnyColorable() - }() - var shadowOpacity: CGFloat = 0.16 - var shadowOffset: CGSize = .init(width: 0, height: 2) - var shadowRadius: CGFloat = 4 + private let dropshadow1Configuration = DropShadowConfiguration().with { + $0.shadowColorConfiguration = SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteBlack).eraseToAnyColorable() + $0.shadowOpacityConfiguration = SurfaceConfigurationValue(CGFloat(0.12), CGFloat(0.22)) + $0.shadowOffsetConfiguration = SurfaceConfigurationValue(.init(width: 0, height: 1), .init(width: 0, height: 1)) + $0.shadowRadiusConfiguration = SurfaceConfigurationValue(CGFloat(10), CGFloat(12)) + } + private let dropshadow2Configuration = DropShadowConfiguration().with { + $0.shadowColorConfiguration = SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteBlack).eraseToAnyColorable() + $0.shadowOpacityConfiguration = SurfaceConfigurationValue(CGFloat(0.05), CGFloat(0.15)) + $0.shadowOffsetConfiguration = SurfaceConfigurationValue(.init(width: 0, height: 2), .init(width: 0, height: 2)) + $0.shadowRadiusConfiguration = SurfaceConfigurationValue(CGFloat(4), CGFloat(6)) + } + var configurations: [DropShadowable] { [dropshadow1Configuration, dropshadow2Configuration] } } private struct HighContrastConfiguration: Configuration { @@ -291,7 +305,7 @@ open class ButtonIcon: Control, Changeable, FormFieldable { }() } - private struct HighContrastFloatingConfiguration: Configuration, Dropshadowable { + private struct HighContrastFloatingConfiguration: Configuration, DropShadowableConfiguration { var kind: Kind = .highContrast var surfaceType: SurfaceType = .colorFill var floating: Bool = true @@ -305,12 +319,19 @@ open class ButtonIcon: Control, Changeable, FormFieldable { $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: [.selected, .disabled]) }.eraseToAnyColorable() }() - var shadowColorConfiguration: AnyColorable = { - SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteBlack).eraseToAnyColorable() - }() - var shadowOpacity: CGFloat = 0.16 - var shadowOffset: CGSize = .init(width: 0, height: 2) - var shadowRadius: CGFloat = 6 + private let dropshadow1Configuration = DropShadowConfiguration().with { + $0.shadowColorConfiguration = SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteBlack).eraseToAnyColorable() + $0.shadowOpacityConfiguration = SurfaceConfigurationValue(CGFloat(0.22), CGFloat(0.12)) + $0.shadowOffsetConfiguration = SurfaceConfigurationValue(.init(width: 0, height: 1), .init(width: 0, height: 1)) + $0.shadowRadiusConfiguration = SurfaceConfigurationValue(CGFloat(12), CGFloat(10)) + } + private let dropshadow2Configuration = DropShadowConfiguration().with { + $0.shadowColorConfiguration = SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteBlack).eraseToAnyColorable() + $0.shadowOpacityConfiguration = SurfaceConfigurationValue(CGFloat(0.15), CGFloat(0.05)) + $0.shadowOffsetConfiguration = SurfaceConfigurationValue(.init(width: 0, height: 2), .init(width: 0, height: 2)) + $0.shadowRadiusConfiguration = SurfaceConfigurationValue(CGFloat(6), CGFloat(4)) + } + var configurations: [DropShadowable] { [dropshadow1Configuration, dropshadow2Configuration] } } private var badgeIndicatorDefaultSize: CGSize = .zero @@ -322,7 +343,7 @@ open class ButtonIcon: Control, Changeable, FormFieldable { open override func setup() { super.setup() isAccessibilityElement = true - accessibilityTraits = .image + accessibilityTraits = .button accessibilityElements = [badgeIndicator] //create a layoutGuide for the icon to key off of @@ -452,12 +473,6 @@ open class ButtonIcon: Control, Changeable, FormFieldable { layer.borderColor = nil layer.borderWidth = 0 } - - if let dropshadowable = currentConfig as? Dropshadowable { - addDropShadow(dropshadowable) - } else { - removeDropShadows() - } badgeIndicatorCenterXConstraint?.constant = badgeIndicatorOffset.x + badgeIndicatorDefaultSize.width/2 badgeIndicatorCenterYConstraint?.constant = badgeIndicatorOffset.y + badgeIndicatorDefaultSize.height/2 @@ -467,6 +482,12 @@ open class ButtonIcon: Control, Changeable, FormFieldable { if showBadgeIndicator { updateExpandDirectionalConstraints() } + + if let configurations = (currentConfig as? DropShadowableConfiguration)?.configurations { + addDropShadows(configurations) + } else { + removeDropShadows() + } } //-------------------------------------------------- diff --git a/VDS/Components/Label/Attributes/AnyLabelAttribute.swift b/VDS/Components/Label/Attributes/AnyLabelAttribute.swift index b914c912..50e76ba7 100644 --- a/VDS/Components/Label/Attributes/AnyLabelAttribute.swift +++ b/VDS/Components/Label/Attributes/AnyLabelAttribute.swift @@ -30,6 +30,7 @@ public struct AnyAttribute: LabelAttributeModel { } public func setAttribute(on attributedString: NSMutableAttributedString) { + guard isValidRange(on: attributedString) else { return } attributedString.removeAttribute(key, range: range) attributedString.addAttribute(key, value: value, range: range) } diff --git a/VDS/Components/Label/Attributes/ColorLabelAttribute.swift b/VDS/Components/Label/Attributes/ColorLabelAttribute.swift index 354fbc93..50229ab0 100644 --- a/VDS/Components/Label/Attributes/ColorLabelAttribute.swift +++ b/VDS/Components/Label/Attributes/ColorLabelAttribute.swift @@ -31,6 +31,8 @@ public struct ColorLabelAttribute: LabelAttributeModel { } public func setAttribute(on attributedString: NSMutableAttributedString) { + guard isValidRange(on: attributedString) else { return } + var colorRange = range if length == 0 && location == 0 { colorRange = .init(location: location, length: attributedString.length) diff --git a/VDS/Components/Label/Attributes/LabelAttributeModel.swift b/VDS/Components/Label/Attributes/LabelAttributeModel.swift index 62e1bbd8..ac43472f 100644 --- a/VDS/Components/Label/Attributes/LabelAttributeModel.swift +++ b/VDS/Components/Label/Attributes/LabelAttributeModel.swift @@ -29,6 +29,10 @@ extension LabelAttributeModel { public static func == (lhs: any LabelAttributeModel, rhs: any LabelAttributeModel) -> Bool { lhs.isEqual(rhs) } + + public func isValidRange(on attributedString: NSMutableAttributedString) -> Bool { + range.location + range.length <= attributedString.string.count + } } public extension NSAttributedString { diff --git a/VDS/Components/Label/Attributes/StrikeThroughLabelAttribute.swift b/VDS/Components/Label/Attributes/StrikeThroughLabelAttribute.swift index 93650f2b..ba4c97ac 100644 --- a/VDS/Components/Label/Attributes/StrikeThroughLabelAttribute.swift +++ b/VDS/Components/Label/Attributes/StrikeThroughLabelAttribute.swift @@ -24,6 +24,7 @@ public struct StrikeThroughLabelAttribute: LabelAttributeModel { } public func setAttribute(on attributedString: NSMutableAttributedString) { + guard isValidRange(on: attributedString) else { return } attributedString.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.thick.rawValue, range: range) attributedString.addAttribute(.baselineOffset, value: 0, range: range) } diff --git a/VDS/Components/Label/Attributes/TextStyleLabelAttribute.swift b/VDS/Components/Label/Attributes/TextStyleLabelAttribute.swift index ae3ea1e5..a161d580 100644 --- a/VDS/Components/Label/Attributes/TextStyleLabelAttribute.swift +++ b/VDS/Components/Label/Attributes/TextStyleLabelAttribute.swift @@ -44,6 +44,7 @@ public struct TextStyleLabelAttribute: LabelAttributeModel { } public func setAttribute(on attributedString: NSMutableAttributedString) { + guard isValidRange(on: attributedString) else { return } attributedString.removeAttribute(.font, range: range) attributedString.addAttribute(.font, value: textStyle.font, range: range) if let textColor { diff --git a/VDS/Components/Label/Attributes/UnderlineLabelAttribute.swift b/VDS/Components/Label/Attributes/UnderlineLabelAttribute.swift index 0ca4e2f9..a2ad403e 100644 --- a/VDS/Components/Label/Attributes/UnderlineLabelAttribute.swift +++ b/VDS/Components/Label/Attributes/UnderlineLabelAttribute.swift @@ -52,7 +52,8 @@ public struct UnderlineLabelAttribute: LabelAttributeModel { //-------------------------------------------------- // MARK: - Public Methods //-------------------------------------------------- - public func setAttribute(on attributedString: NSMutableAttributedString) { + public func setAttribute(on attributedString: NSMutableAttributedString) { + guard isValidRange(on: attributedString) else { return } attributedString.addAttribute(.underlineStyle, value: underlineValue.rawValue, range: range) if let color = color { attributedString.addAttribute(.underlineColor, value: color, range: range) diff --git a/VDS/Components/Loader/Loader.swift b/VDS/Components/Loader/Loader.swift index baf6ecdb..1cfb02c4 100644 --- a/VDS/Components/Loader/Loader.swift +++ b/VDS/Components/Loader/Loader.swift @@ -80,7 +80,7 @@ open class Loader: View { super.updateView() icon.color = iconColorConfiguration.getColor(self) icon.customSize = size - if isActive { + if isActive && isVisibleOnScreen { startAnimating() } else { stopAnimating() diff --git a/VDS/Components/TextFields/EntryFieldBase.swift b/VDS/Components/TextFields/EntryFieldBase.swift index aaf03256..ae91cc7a 100644 --- a/VDS/Components/TextFields/EntryFieldBase.swift +++ b/VDS/Components/TextFields/EntryFieldBase.swift @@ -13,7 +13,7 @@ import Combine /// Base Class used to build out a Input controls. @objc(VDSEntryField) -open class EntryFieldBase: Control, Changeable { +open class EntryFieldBase: Control, Changeable, FormFieldable { //-------------------------------------------------- // MARK: - Initializers @@ -59,7 +59,7 @@ open class EntryFieldBase: Control, Changeable { return UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .horizontal - $0.distribution = .fillProportionally + $0.distribution = .fill $0.alignment = .top } }() @@ -70,6 +70,20 @@ open class EntryFieldBase: Control, Changeable { } }() + internal var bottomContainerView: UIView = { + return UIView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + } + }() + + internal var bottomContainerStackView: UIStackView = { + return UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.axis = .vertical + $0.distribution = .fill + } + }() + //-------------------------------------------------- // MARK: - Configuration Properties //-------------------------------------------------- @@ -93,11 +107,15 @@ open class EntryFieldBase: Control, Changeable { } internal var borderColorConfiguration = ControlColorConfiguration().with { - $0.setSurfaceColors(VDSFormControlsColor.borderOnlight, VDSFormControlsColor.borderOnlight, forState: .normal) + $0.setSurfaceColors(VDSFormControlsColor.borderOnlight, VDSFormControlsColor.borderOndark, forState: .normal) $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled) $0.setSurfaceColors(VDSColor.feedbackErrorOnlight, VDSColor.feedbackErrorOndark, forState: .error) } + internal var readOnlyBorderColorConfiguration = ControlColorConfiguration().with { + $0.setSurfaceColors(VDSFormControlsColor.borderReadonlyOnlight, VDSFormControlsColor.borderReadonlyOndark, forState: .normal) + } + //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- @@ -135,19 +153,42 @@ open class EntryFieldBase: Control, Changeable { /// Whether not to show the error. open var showError: Bool = false { didSet { setNeedsUpdate() } } + /// Whether or not to show the internal error + open internal(set) var hasInternalError: Bool = false { didSet { setNeedsUpdate() } } + /// Override UIControl state to add the .error state if showError is true. open override var state: UIControl.State { get { var state = super.state - if showError { + if showError || hasInternalError { state.insert(.error) } return state } } + + open var errorText: String? { + didSet { + updateContainerView() + updateErrorLabel() + setNeedsUpdate() + } + } + + internal var internalErrorText: String? { + didSet { + updateContainerView() + updateErrorLabel() + setNeedsUpdate() + } + } + + /// Override this to conveniently get/set the textfield(s). + open var text: String? { + get { nil } + set { fatalError("You MUST override EntryField's 'text' variable in your subclass.") } + } - open var errorText: String? { didSet { setNeedsUpdate() } } - open var tooltipModel: Tooltip.TooltipModel? { didSet { setNeedsUpdate() } } open var transparentBackground: Bool = false { didSet { setNeedsUpdate() } } @@ -157,8 +198,20 @@ open class EntryFieldBase: Control, Changeable { open var maxLength: Int? { didSet { setNeedsUpdate() } } open var inputId: String? { didSet { setNeedsUpdate() } } + + /// The text of this textField. + private var _value: AnyHashable? + open var value: AnyHashable? { + get { _value } + set { + if let newValue, newValue != _value { + _value = newValue + text = newValue as? String + } + setNeedsUpdate() + } + } - open var value: AnyHashable? { didSet { setNeedsUpdate() } } open var defaultValue: AnyHashable? { didSet { setNeedsUpdate() } } @@ -184,7 +237,7 @@ open class EntryFieldBase: Control, Changeable { //create the wrapping view heightConstraint = containerView.heightAnchor.constraint(greaterThanOrEqualToConstant: containerSize.height) - widthConstraint?.priority = .defaultHigh + heightConstraint?.priority = .defaultHigh heightConstraint?.isActive = true widthConstraint = containerView.widthAnchor.constraint(equalToConstant: 0) @@ -203,15 +256,26 @@ open class EntryFieldBase: Control, Changeable { //add the view to add input fields containerStackView.addArrangedSubview(controlContainerView) containerStackView.addArrangedSubview(icon) + containerStackView.setCustomSpacing(VDSLayout.Spacing.space3X.value, after: controlContainerView) + //get the container this is what show helper text, error text + //can include other for character count, max length + let bottomContainer = getBottomContainer() + + //add bottomContainerStackView + //this is the vertical stack that contains error text, helper text + bottomContainer.addSubview(bottomContainerStackView) + bottomContainerStackView.pinToSuperView() + bottomContainerStackView.addArrangedSubview(errorLabel) + bottomContainerStackView.addArrangedSubview(helperLabel) + stackView.addArrangedSubview(titleLabel) stackView.addArrangedSubview(container) - stackView.addArrangedSubview(errorLabel) - stackView.addArrangedSubview(helperLabel) + stackView.addArrangedSubview(bottomContainer) stackView.setCustomSpacing(4, after: titleLabel) stackView.setCustomSpacing(8, after: container) - stackView.setCustomSpacing(8, after: errorLabel) + stackView.setCustomSpacing(8, after: bottomContainer) stackView .pinTop() @@ -254,11 +318,7 @@ open class EntryFieldBase: Control, Changeable { open override func updateView() { super.updateView() - containerView.backgroundColor = backgroundColorConfiguration.getColor(self) - containerView.layer.borderColor = borderColorConfiguration.getColor(self).cgColor - containerView.layer.borderWidth = VDSFormControls.widthBorder - containerView.layer.cornerRadius = VDSFormControls.borderradius - + updateContainerView() updateTitleLabel() updateErrorLabel() updateHelperLabel() @@ -266,6 +326,16 @@ open class EntryFieldBase: Control, Changeable { backgroundColor = surface.color } + //-------------------------------------------------- + // MARK: - Private Methods + //-------------------------------------------------- + private func updateContainerView() { + containerView.backgroundColor = backgroundColorConfiguration.getColor(self) + containerView.layer.borderColor = borderColorConfiguration.getColor(self).cgColor + containerView.layer.borderWidth = VDSFormControls.widthBorder + containerView.layer.cornerRadius = VDSFormControls.borderradius + } + //-------------------------------------------------- // MARK: - Public Methods //-------------------------------------------------- @@ -273,6 +343,11 @@ open class EntryFieldBase: Control, Changeable { open func getContainer() -> UIView { return containerView } + + /// Container for the area in which helper or error text presents. + open func getBottomContainer() -> UIView { + return bottomContainerView + } open func updateTitleLabel() { @@ -305,7 +380,16 @@ open class EntryFieldBase: Control, Changeable { } open func updateErrorLabel(){ - if showError, let errorText { + if showError, hasInternalError, let errorText, let internalErrorText { + errorLabel.text = [internalErrorText, errorText].joined(separator: "\n") + errorLabel.surface = surface + errorLabel.isEnabled = isEnabled + errorLabel.isHidden = false + icon.name = .error + icon.color = VDSColor.paletteBlack + icon.surface = surface + icon.isHidden = !isEnabled + } else if showError, let errorText { errorLabel.text = errorText errorLabel.surface = surface errorLabel.isEnabled = isEnabled @@ -314,6 +398,15 @@ open class EntryFieldBase: Control, Changeable { icon.color = VDSColor.paletteBlack icon.surface = surface icon.isHidden = !isEnabled + } else if hasInternalError, let internalErrorText { + errorLabel.text = internalErrorText + errorLabel.surface = surface + errorLabel.isEnabled = isEnabled + errorLabel.isHidden = false + icon.name = .error + icon.color = VDSColor.paletteBlack + icon.surface = surface + icon.isHidden = !isEnabled } else { icon.isHidden = true errorLabel.isHidden = true diff --git a/VDS/Components/TextFields/InputField/InputField.swift b/VDS/Components/TextFields/InputField/InputField.swift index 94ef5231..ac350bc1 100644 --- a/VDS/Components/TextFields/InputField/InputField.swift +++ b/VDS/Components/TextFields/InputField/InputField.swift @@ -78,6 +78,18 @@ open class InputField: EntryFieldBase, UITextFieldDelegate { /// Representing the type of input. open var fieldType: FieldType = .text { didSet { setNeedsUpdate() } } + /// The text of this textField. + open override var text: String? { + get { textField.text } + set { + if let newValue, newValue != text { + textField.text = newValue + value = newValue + } + setNeedsUpdate() + } + } + var _showError: Bool = false /// Whether not to show the error. open override var showError: Bool { diff --git a/VDS/Components/TextFields/TextArea/TextArea.swift b/VDS/Components/TextFields/TextArea/TextArea.swift index 7d70cb44..d30c187f 100644 --- a/VDS/Components/TextFields/TextArea/TextArea.swift +++ b/VDS/Components/TextFields/TextArea/TextArea.swift @@ -5,7 +5,6 @@ // Created by Matt Bruce on 1/10/23. // -import Foundation import Foundation import UIKit import VDSColorTokens @@ -36,64 +35,147 @@ open class TextArea: EntryFieldBase { //-------------------------------------------------- internal var minWidthConstraint: NSLayoutConstraint? internal var textViewHeightConstraint: NSLayoutConstraint? - + internal var inputFieldStackView: UIStackView = { return UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .horizontal $0.distribution = .fill - $0.spacing = 12 + $0.spacing = VDSLayout.Spacing.space3X.value } }() + internal var bottomView: UIView = { + return UIView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + } + }() + + internal var bottomStackView: UIStackView = { + return UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.axis = .horizontal + $0.distribution = .fill + $0.alignment = .top + $0.spacing = VDSLayout.Spacing.space2X.value + } + }() + + open var characterCounterLabel = Label().with { + $0.setContentCompressionResistancePriority(.required, for: .vertical) + $0.textStyle = .bodySmall + $0.textAlignment = .right + $0.numberOfLines = 1 + } + + private var _minHeight: Height = .twoX + + open var minHeight: Height? { + get { return _minHeight } + set { + if let newValue { + _minHeight = newValue + } else { + _minHeight = .twoX + } + textViewHeightConstraint?.constant = _minHeight.value + setNeedsUpdate() + } + } + //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- - override var containerSize: CGSize { CGSize(width: 45, height: 88) } - + override var containerSize: CGSize { CGSize(width: 182, height: 88) } + + /// Enum used to describe the the height of TextArea. + public enum Height: String, CaseIterable { + case twoX = "2X" + case fourX = "4X" + case eightX = "8X" + + var value: CGFloat { + switch self { + case .twoX: + 88 + case .fourX: + 176 + case .eightX: + 352 + } + } + } + + /// The text of this textView + open override var text: String? { + get { textView.text } + set { + if let newValue, newValue != text { + textView.text = newValue + value = newValue + } + setNeedsUpdate() + } + } + /// UITextView shown in the TextArea. - open var textView = UITextView().with { + open var textView = TextView().with { $0.translatesAutoresizingMaskIntoConstraints = false - $0.font = TextStyle.bodyLarge.font $0.sizeToFit() $0.isScrollEnabled = false } - /// Color configuration for the textView. - open var textViewTextColorConfiguration: AnyColorable = ViewColorConfiguration().with { - $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forDisabled: true) - $0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forDisabled: false) - }.eraseToAnyColorable() { didSet { setNeedsUpdate() } } - + /// Color configuration for error icon. + internal var iconColorConfiguration = ControlColorConfiguration().with { + $0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .normal) + } + + /// Color configuration for character counter's highlight background color + internal var highlightBackgroundColor = ControlColorConfiguration().with { + $0.setSurfaceColors(VDSColor.backgroundPrimaryDark, VDSColor.backgroundPrimaryLight, forState: .normal) + } + + /// Color configuration for character counter's highlight text color + internal var highlightTextColor = ControlColorConfiguration().with { + $0.setSurfaceColors(VDSColor.elementsPrimaryOndark, VDSColor.elementsPrimaryOnlight, forState: .normal) + } + //-------------------------------------------------- // 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() + accessibilityLabel = "TextArea" - minWidthConstraint = containerView.widthAnchor.constraint(greaterThanOrEqualToConstant: 0) + containerStackView.pinToSuperView(.uniform(VDSFormControls.spaceInset)) + minWidthConstraint = containerView.widthAnchor.constraint(greaterThanOrEqualToConstant: containerSize.width) minWidthConstraint?.isActive = true - controlContainerView.addSubview(textView) textView .pinTop() .pinLeading() .pinTrailingLessThanOrEqualTo(nil, 0, .defaultHigh) .pinBottom(0, .defaultHigh) - - textViewHeightConstraint = textView.heightAnchor.constraint(greaterThanOrEqualToConstant: 64) + textView.isScrollEnabled = true + textView.autocorrectionType = .no + textViewHeightConstraint = textView.heightAnchor.constraint(greaterThanOrEqualToConstant: containerSize.height) textViewHeightConstraint?.isActive = true backgroundColorConfiguration.setSurfaceColors(VDSColor.feedbackSuccessBackgroundOnlight, VDSColor.feedbackSuccessBackgroundOndark, forState: .success) borderColorConfiguration.setSurfaceColors(VDSColor.feedbackSuccessOnlight, VDSColor.feedbackSuccessOndark, forState: .success) - + borderColorConfiguration.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .focused) textView.delegate = self + characterCounterLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable() + bottomContainerStackView.spacing = VDSLayout.Spacing.space2X.value } /// Resets to default settings. open override func reset() { super.reset() textView.text = "" + characterCounterLabel.reset() + characterCounterLabel.textStyle = .bodySmall + setNeedsUpdate() } /// Container for the area in which the user interacts. @@ -107,7 +189,8 @@ open class TextArea: EntryFieldBase { super.updateView() textView.isEditable = isEnabled - textView.textColor = textViewTextColorConfiguration.getColor(self) + textView.isEnabled = isEnabled + textView.surface = surface //set the width constraints if let width { @@ -119,6 +202,80 @@ open class TextArea: EntryFieldBase { widthConstraint?.isActive = false minWidthConstraint?.isActive = true } + + let characterError = getCharacterCounterText() + if let maxLength, maxLength > 0 { + characterCounterLabel.text = characterError + } else { + characterCounterLabel.text = "" + } + + icon.size = .medium + icon.color = iconColorConfiguration.getColor(self) + containerView.layer.borderColor = readOnly ? readOnlyBorderColorConfiguration.getColor(self).cgColor : borderColorConfiguration.getColor(self).cgColor + textView.isEditable = readOnly ? false : true + textView.backgroundColor = backgroundColorConfiguration.getColor(self) + textView.tintColor = iconColorConfiguration.getColor(self) + characterCounterLabel.surface = surface + highlightCharacterOverflow() + } + + /// Container for the area showing helper text, error text, character count, maximum length value. + open override func getBottomContainer() -> UIView { + bottomView.addSubview(bottomStackView) + bottomStackView.pinToSuperView() + bottomStackView.addArrangedSubview(bottomContainerView) + bottomStackView.addArrangedSubview(characterCounterLabel) + return bottomView + } + + /// Used to update any Accessibility properties. + open override func updateAccessibility() { + super.updateAccessibility() + if showError { + setAccessibilityLabel(for: [titleLabel, textView, errorLabel, helperLabel]) + } else { + setAccessibilityLabel(for: [titleLabel, textView, helperLabel]) + } + } + + //-------------------------------------------------- + // MARK: - Private Methods + //-------------------------------------------------- + private func getCharacterCounterText() -> String? { + let count = textView.text.count + let countStr = (count > maxLength ?? 0) ? ("-" + "\(count-(maxLength ?? 0))") : "\(count)" + if let maxLength, maxLength > 0 { + if count > maxLength { + hasInternalError = true + internalErrorText = "You have exceeded the character limit." + return countStr + } else { + hasInternalError = false + internalErrorText = nil + return ("\(countStr)" + "/" + "\(maxLength)") + } + } else { + hasInternalError = false + internalErrorText = nil + return nil + } + } + + open func highlightCharacterOverflow() { + let count = textView.text.count + guard let maxLength, maxLength > 0, count > maxLength else { + textView.textAttributes = nil + return + } + + var textAttributes = [any LabelAttributeModel]() + let location = maxLength + let length = count - maxLength + textAttributes.append(ColorLabelAttribute(location: location, length: length, color: highlightBackgroundColor.getColor(self), isForegroundColor: false)) + textAttributes.append(ColorLabelAttribute(location: location, length: length, color: highlightTextColor.getColor(self), isForegroundColor: true)) + + textView.textAttributes = textAttributes } } @@ -127,27 +284,42 @@ extension TextArea: UITextViewDelegate { // MARK: - UITextViewDelegate //-------------------------------------------------- public func textViewDidChange(_ textView: UITextView) { - + //dynamic textView Height sizing based on Figma //if you want it to work "as-is" delete this code //since it will autogrow with the current settings if let textViewHeightConstraint, textView.isEditable { - let height = textView.frame.size.height - let constraintHeight = textViewHeightConstraint.constant - if height > constraintHeight { - if height > 64 && height < 152 { - textViewHeightConstraint.constant = 152 - } else if height > 152 { - textViewHeightConstraint.constant = 328 - } else { - textViewHeightConstraint.constant = 64 - } + var height = textView.contentSize.height + height = max(height, _minHeight.value) + if height > Height.twoX.value && height <= Height.fourX.value { + textViewHeightConstraint.constant = Height.fourX.value + } else if height > Height.fourX.value { + textViewHeightConstraint.constant = Height.eightX.value + } else { + textViewHeightConstraint.constant = Height.twoX.value } } - //setting the value and firing control event - value = textView.text - sendActions(for: .valueChanged) + //The exceeding characters will be highlighted to help users correct their entry. + if let maxLength, maxLength > 0 { + // allow - 20% of character limit + let overflowLimit = Double(maxLength) * 0.20 + let allowCharCount = Int(overflowLimit) + maxLength - } + if textView.text.count <= allowCharCount { + highlightCharacterOverflow() + + //setting the value and firing control event + text = textView.text + sendActions(for: .valueChanged) + } else { + textView.text.removeLast() + highlightCharacterOverflow() + } + } else { + //setting the value and firing control event + text = textView.text + sendActions(for: .valueChanged) + } + } } diff --git a/VDS/Components/TextFields/TextArea/TextAreaChangeLog.txt b/VDS/Components/TextFields/TextArea/TextAreaChangeLog.txt new file mode 100644 index 00000000..555ad730 --- /dev/null +++ b/VDS/Components/TextFields/TextArea/TextAreaChangeLog.txt @@ -0,0 +1,38 @@ +MM/DD/YYYY +---------------- +- Initial Brand 3.0 handoff + +12/27/2021 +---------------- +- Removed Max idth Updated the SPECS with FormControl tokens + +02/25/2022 +---------------- +- Replaced Info and Error Non-Scaling icons with VDS Icon. +- Removed “weight” and “vector effect” from Anatomy and States. + +07/27/2022 +---------------- +- Added Configurations section with transparentBackground principles. + +08/10/2022 +---------------- +- Updated default and inverted prop to light and dark surface. + +11/30/2022 +---------------- +- Added "(web only)" to any instance of "keyboard focus" + +12/13/2022 +---------------- +- Replaced form border and focus border pixel values and style & spacing with tokens. + +01/18/2023 +---------------- +- Updated Anatomy items: +- Added “Highlight” to item #10 +- Changed item #7 to “Tooltip” from “Tooltip Component” + +04/12/2023 +---------------- +- Updated hex colors for updated feedback tokens in error states. diff --git a/VDS/Components/TextFields/TextArea/TextView.swift b/VDS/Components/TextFields/TextArea/TextView.swift new file mode 100644 index 00000000..ea96ed6e --- /dev/null +++ b/VDS/Components/TextFields/TextArea/TextView.swift @@ -0,0 +1,150 @@ +// +// TextView.swift +// VDS +// +// Created by Matt Bruce on 2/29/24. +// + +import Foundation +import UIKit +import Combine +import VDSColorTokens + +@objc(VDSTextView) +open class TextView: UITextView, ViewProtocol { + + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + required public init() { + super.init(frame: .zero, textContainer: nil) + initialSetup() + } + + public override init(frame: CGRect, textContainer: NSTextContainer?) { + super.init(frame: frame, textContainer: textContainer) + initialSetup() + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + initialSetup() + } + + //-------------------------------------------------- + // MARK: - Combine Properties + //-------------------------------------------------- + /// Set of Subscribers for any Publishers for this Control. + open var subscribers = Set() + + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- + private var initialSetupPerformed = false + + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + /// Key of whether or not updateView() is called in setNeedsUpdate() + open var shouldUpdateView: Bool = true + + open var surface: Surface = .light { didSet { setNeedsUpdate() } } + + /// Array of LabelAttributeModel objects used in rendering the text. + open var textAttributes: [any LabelAttributeModel]? { didSet { setNeedsUpdate() } } + + /// TextStyle used on the titleLabel. + open var textStyle: TextStyle { .defaultStyle } + + /// Will determine if a scaled font should be used for the titleLabel font. + open var useScaledFont: Bool = false { didSet { setNeedsUpdate() } } + + open var isEnabled: Bool = true { didSet { setNeedsUpdate() } } + + open var textColorConfiguration: AnyColorable = ViewColorConfiguration().with { + $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forDisabled: true) + $0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forDisabled: false) + }.eraseToAnyColorable(){ didSet { setNeedsUpdate() }} + + open override var textColor: UIColor? { + get { textColorConfiguration.getColor(self) } + set { } + } + + override public var text: String! { + get { super.text } + set { + super.text = newValue + updateLabel() + } + } + + override public var textAlignment: NSTextAlignment { + didSet { + if textAlignment != oldValue { + // Text alignment can be part of our paragraph style, so we may need to + // re-style when changed + updateLabel() + } + } + } + + //-------------------------------------------------- + // MARK: - Lifecycle + //-------------------------------------------------- + open func initialSetup() { + if !initialSetupPerformed { + backgroundColor = .clear + translatesAutoresizingMaskIntoConstraints = false + accessibilityCustomActions = [] + setup() + setNeedsUpdate() + } + } + + + open func setup() { + translatesAutoresizingMaskIntoConstraints = false + } + + open func updateView() { + updateLabel() + } + + open func updateAccessibility() {} + + open func reset() { + shouldUpdateView = false + surface = .light + text = nil + accessibilityCustomActions = [] + shouldUpdateView = true + setNeedsUpdate() + } + + //-------------------------------------------------- + // MARK: - Private Methods + //-------------------------------------------------- + private func updateLabel() { + + //clear the arrays holding actions + accessibilityCustomActions = [] + if let text, !text.isEmpty { + //create the primary string + let mutableText = NSMutableAttributedString.mutableText(for: text, + textStyle: textStyle, + useScaledFont: useScaledFont, + textColor: textColor!, + alignment: textAlignment, + lineBreakMode: .byWordWrapping) + //apply any attributes + if let attributes = textAttributes { + mutableText.apply(attributes: attributes) + } + attributedText = mutableText + } else { + attributedText = nil + } + } +} + diff --git a/VDS/Components/TileContainer/TileContainer.swift b/VDS/Components/TileContainer/TileContainer.swift index 6cf7d24f..14d38d05 100644 --- a/VDS/Components/TileContainer/TileContainer.swift +++ b/VDS/Components/TileContainer/TileContainer.swift @@ -189,9 +189,15 @@ open class TileContainerBase: Control where Padding // MARK: - Configuration //-------------------------------------------------- private let cornerRadius = VDSFormControls.borderradius * 2 - private var backgroundColorConfiguration = BackgroundColorConfiguration() - private var dropshadowConfiguration = DropshadowConfiguration() + private let dropShadowConfiguration = DropShadowConfiguration().with { + $0.shadowColorConfiguration = SurfaceColorConfiguration().with { + $0.lightColor = VDSColor.elementsPrimaryOnlight + }.eraseToAnyColorable() + $0.shadowOffsetConfiguration = .init(.init(width: 0, height: 6), .zero) + $0.shadowRadiusConfiguration = .init(3.0, 0.0) + $0.shadowOpacityConfiguration = .init(0.01, 0.0) + } private var borderColorConfiguration = SurfaceColorConfiguration().with { $0.lightColor = VDSColor.elementsLowcontrastOnlight @@ -315,7 +321,7 @@ open class TileContainerBase: Control where Padding heightConstraint?.isActive = false } if showDropShadows, surface == .light { - addDropShadow(dropshadowConfiguration) + addDropShadow(dropShadowConfiguration) } else { removeDropShadows() } @@ -401,15 +407,6 @@ open class TileContainerBase: Control where Padding extension TileContainerBase { - struct DropshadowConfiguration: Dropshadowable { - var shadowColorConfiguration: AnyColorable = SurfaceColorConfiguration().with { - $0.lightColor = VDSColor.elementsPrimaryOnlight - }.eraseToAnyColorable() - var shadowOpacity: CGFloat = 0.01 - var shadowOffset: CGSize = .init(width: 0, height: 6) - var shadowRadius: CGFloat = 3 - } - final class BackgroundColorConfiguration: ObjectColorable { typealias ObjectType = TileContainerBase diff --git a/VDS/Components/Tilelet/TileletSubTitleModel.swift b/VDS/Components/Tilelet/TileletSubTitleModel.swift index 63820316..62adda7e 100644 --- a/VDS/Components/Tilelet/TileletSubTitleModel.swift +++ b/VDS/Components/Tilelet/TileletSubTitleModel.swift @@ -15,7 +15,7 @@ extension Tilelet { // MARK: - Enums //-------------------------------------------------- /// Enum used to describe the textStyle of the subTitle label. - public enum StandardStyle: String, EnumSubset { + public enum OtherStandardStyle: String, EnumSubset { case bodyLarge case bodyMedium case bodySmall @@ -30,7 +30,7 @@ extension Tilelet { public var text: String = "" /// Text style that will be used for the subTitle label. - public var standardStyle: StandardStyle = .bodySmall + public var otherStandardStyle: OtherStandardStyle = .bodySmall /// Text attributes that will be used for the subTitle label. public var textAttributes: [any LabelAttributeModel]? @@ -45,11 +45,14 @@ extension Tilelet { // MARK: - Initializers //-------------------------------------------------- public init(text: String, + otherStandardStyle: OtherStandardStyle = .bodySmall, textColor: Use = .primary, textAttributes: [any LabelAttributeModel]? = nil, standardStyle: StandardStyle = .bodySmall, lineBreakMode: NSLineBreakMode = .byTruncatingTail) { + textAttributes: [any LabelAttributeModel]? = nil) { self.text = text + self.otherStandardStyle = otherStandardStyle self.textAttributes = textAttributes self.textColor = textColor self.standardStyle = standardStyle @@ -62,7 +65,7 @@ extension Tilelet { /// Converts this type of model to a TitleLockup.SubTitleModel. public func toTitleLockupSubTitleModel() -> TitleLockup.SubTitleModel { TitleLockup.SubTitleModel(text: text, - standardStyle: standardStyle.value, + otherStandardStyle: otherStandardStyle.value, textColor: textColor, textAttributes: textAttributes, lineBreakMode: lineBreakMode) } diff --git a/VDS/Components/TitleLockup/TitleLockup.swift b/VDS/Components/TitleLockup/TitleLockup.swift index 6dc42e70..7feb86e7 100644 --- a/VDS/Components/TitleLockup/TitleLockup.swift +++ b/VDS/Components/TitleLockup/TitleLockup.swift @@ -45,7 +45,7 @@ open class TitleLockup: View { //-------------------------------------------------- private var otherStandardStyle: OtherStandardStyle { if let subTitleModel, !subTitleModel.text.isEmpty { - return subTitleModel.standardStyle + return subTitleModel.otherStandardStyle } else if let eyebrowModel, !eyebrowModel.text.isEmpty { return eyebrowModel.standardStyle } else { diff --git a/VDS/Components/TitleLockup/TitleLockupSubTitleModel.swift b/VDS/Components/TitleLockup/TitleLockupSubTitleModel.swift index 5ad105ef..f558ffb4 100644 --- a/VDS/Components/TitleLockup/TitleLockupSubTitleModel.swift +++ b/VDS/Components/TitleLockup/TitleLockupSubTitleModel.swift @@ -15,7 +15,7 @@ extension TitleLockup { public var text: String /// Standard style that will be used for the subTitle label. - public var standardStyle: OtherStandardStyle + public var otherStandardStyle: OtherStandardStyle /// Text color used in the subtitle label. public var textColor: Use @@ -30,13 +30,13 @@ extension TitleLockup { public var lineBreakMode: NSLineBreakMode public init(text: String, - standardStyle: OtherStandardStyle = .bodyLarge, + otherStandardStyle: OtherStandardStyle = .bodyLarge, textColor: Use = .primary, textAttributes: [any LabelAttributeModel]? = nil, numberOfLines: Int = 0, lineBreakMode: NSLineBreakMode = .byWordWrapping) { self.text = text - self.standardStyle = standardStyle + self.otherStandardStyle = otherStandardStyle self.textColor = textColor self.textAttributes = textAttributes self.numberOfLines = numberOfLines @@ -44,7 +44,7 @@ extension TitleLockup { } /// TextStyle used to render the text. - public var textStyle: TextStyle { standardStyle.value.regular } + public var textStyle: TextStyle { otherStandardStyle.value.regular } } diff --git a/VDS/Protocols/DropShadowable.swift b/VDS/Protocols/DropShadowable.swift new file mode 100644 index 00000000..b569ecab --- /dev/null +++ b/VDS/Protocols/DropShadowable.swift @@ -0,0 +1,89 @@ +// +// DropShadowable.swift +// VDS +// +// Created by Bandaru, Krishna Kishore on 16/02/24. +// + +import Foundation +import UIKit + +/** + DropShadowable protocol helps with the configuration values for adding drop shadows for light & dark surfaces. +*/ +protocol DropShadowable { + ///Shadow Color configuration for light and dark surfaces + var shadowColorConfiguration: AnyColorable { get set } + ///Shadow Opacity configuration for light and dark surfaces + var shadowOpacityConfiguration: SurfaceConfigurationValue { get set } + ///Shadow Offset configuration for light and dark surfaces + var shadowOffsetConfiguration: SurfaceConfigurationValue { get set } + ///Shadow Radius configuration for light and dark surfaces + var shadowRadiusConfiguration: SurfaceConfigurationValue { get set } +} + +/** + DropShadowableConfiguration protocol helps with multiple drop shadows configurations can be added to a view. + */ +protocol DropShadowableConfiguration { + + ///Configurations are the DropShadowable list, these are applied on the view + var configurations: [DropShadowable] { get } +} + +/** + Extension on ViewProtocol for adding drop shadows & gradient layer on view. + */ +extension ViewProtocol where Self: UIView { + + func addDropShadow(_ config: DropShadowable) { + addDropShadows([config]) + } + + func addDropShadows(_ configs: [DropShadowable]) { + removeDropShadows() + layer.backgroundColor = backgroundColor?.cgColor + layer.masksToBounds = false + for config in configs { + let shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius) + let shadowLayer = CALayer() + shadowLayer.shadowPath = shadowPath.cgPath + shadowLayer.frame = bounds + shadowLayer.position = .init(x: bounds.midX, y: bounds.midY) + shadowLayer.backgroundColor = backgroundColor?.cgColor + shadowLayer.cornerRadius = layer.cornerRadius + shadowLayer.shadowColor = config.shadowColorConfiguration.getColor(self).cgColor + shadowLayer.shadowOpacity = Float(config.shadowOpacityConfiguration.value(for: self)) + shadowLayer.shadowOffset = config.shadowOffsetConfiguration.value(for: self) + shadowLayer.shadowRadius = config.shadowRadiusConfiguration.value(for: self) + shadowLayer.name = "dropShadowLayer" + shadowLayer.shouldRasterize = true + shadowLayer.rasterizationScale = UIScreen.main.scale + layer.insertSublayer(shadowLayer, at: 0) + } + } + + func removeDropShadows() { + layer.sublayers?.removeAll { $0.name == "dropShadowLayer" } + } + + func addGradientLayer(with firstColor: UIColor, secondColor: UIColor) { + removeGradientLayer() + let gradientLayer = CAGradientLayer() + gradientLayer.frame = bounds + gradientLayer.startPoint = CGPoint(x: 0, y: 1) + gradientLayer.endPoint = CGPoint(x: 1, y: 0) + gradientLayer.position = center + gradientLayer.shouldRasterize = true + gradientLayer.backgroundColor = UIColor.clear.cgColor + gradientLayer.rasterizationScale = UIScreen.main.scale + gradientLayer.cornerRadius = layer.cornerRadius + gradientLayer.colors = [firstColor.cgColor, secondColor.cgColor] + gradientLayer.name = "gradientLayer" + layer.insertSublayer(gradientLayer, at: 0) + } + + func removeGradientLayer() { + layer.sublayers?.removeAll { $0.name == "gradientLayer" } + } +} diff --git a/VDS/Protocols/Dropshadowable.swift b/VDS/Protocols/Dropshadowable.swift deleted file mode 100644 index 77f1a475..00000000 --- a/VDS/Protocols/Dropshadowable.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// Dropshadowable.swift -// VDS -// -// Created by Bandaru, Krishna Kishore on 16/02/24. -// - -import Foundation -import UIKit - -protocol Dropshadowable { - - var shadowColorConfiguration: AnyColorable { get set } - var shadowOpacity: CGFloat { get set } - var shadowOffset: CGSize { get set } - var shadowRadius: CGFloat { get set } -} - -extension ViewProtocol where Self: UIView { - - func addDropShadow(_ config: Dropshadowable) { - removeDropShadows() - layer.backgroundColor = backgroundColor?.cgColor - layer.masksToBounds = false - let shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius) - let shadowLayer = CALayer() - shadowLayer.shadowPath = shadowPath.cgPath - shadowLayer.frame = bounds - shadowLayer.position = center - shadowLayer.backgroundColor = UIColor.clear.cgColor - shadowLayer.cornerRadius = layer.cornerRadius - shadowLayer.shadowColor = config.shadowColorConfiguration.getColor(self).cgColor - shadowLayer.shadowOpacity = Float(config.shadowOpacity) - shadowLayer.shadowOffset = .init(width: config.shadowOffset.width, height: config.shadowOffset.height) - shadowLayer.shadowRadius = config.shadowRadius - shadowLayer.name = "dropShadowLayer" - shadowLayer.shouldRasterize = true - shadowLayer.rasterizationScale = UIScreen.main.scale - layer.insertSublayer(shadowLayer, at: 0) - } - - func removeDropShadows() { - layer.sublayers?.removeAll { $0.name == "dropShadowLayer" } - } - - func addGradientLayer(with firstColor: UIColor, secondColor: UIColor) { - removeGradientLayer() - let gradientLayer = CAGradientLayer() - gradientLayer.frame = bounds - gradientLayer.startPoint = CGPoint(x: 0, y: 1) - gradientLayer.endPoint = CGPoint(x: 1, y: 0) - gradientLayer.position = center - gradientLayer.shouldRasterize = true - gradientLayer.rasterizationScale = UIScreen.main.scale - gradientLayer.cornerRadius = layer.cornerRadius - gradientLayer.colors = [firstColor.cgColor, secondColor.cgColor] - gradientLayer.name = "gradientLayer" - layer.insertSublayer(gradientLayer, at: 0) - } - - func removeGradientLayer() { - layer.sublayers?.removeAll { $0.name == "gradientLayer" } - } -} diff --git a/VDS/Utilities/DropShadowConfiguration.swift b/VDS/Utilities/DropShadowConfiguration.swift new file mode 100644 index 00000000..e0cc3dd8 --- /dev/null +++ b/VDS/Utilities/DropShadowConfiguration.swift @@ -0,0 +1,33 @@ +// +// DropShadowConfiguration.swift +// VDS +// +// Created by Bandaru, Krishna Kishore on 05/03/24. +// + +import Foundation + +/** + DropShadowConfiguration confirms to DropShadowable where it has configurable properties required for drop shadow +*/ +final class DropShadowConfiguration: DropShadowable, ObjectWithable { + + typealias CGFloatConfigurationValue = SurfaceConfigurationValue + typealias CGSizeConfigurationValue = SurfaceConfigurationValue + + ///Shadow Color configuration for light and dark surfaces + var shadowColorConfiguration: AnyColorable + ///Shadow Opacity configuration for light and dark surfaces + var shadowOpacityConfiguration: CGFloatConfigurationValue + ///Shadow Offset configuration for light and dark surfaces + var shadowOffsetConfiguration: CGSizeConfigurationValue + ///Shadow Radius configuration for light and dark surfaces + var shadowRadiusConfiguration: CGFloatConfigurationValue + + init(shadowColorConfiguration: AnyColorable = SurfaceColorConfiguration().eraseToAnyColorable(), shadowOpacity: CGFloatConfigurationValue = CGFloatConfigurationValue(1.0, 1.0), shadowOffset: CGSizeConfigurationValue = CGSizeConfigurationValue(.zero, .zero), shadowRadius: CGFloatConfigurationValue = CGFloatConfigurationValue(1.0, 1.0)) { + self.shadowColorConfiguration = shadowColorConfiguration + self.shadowOpacityConfiguration = shadowOpacity + self.shadowOffsetConfiguration = shadowOffset + self.shadowRadiusConfiguration = shadowRadius + } +} diff --git a/VDS/Utilities/SurfaceConfigurationValue.swift b/VDS/Utilities/SurfaceConfigurationValue.swift new file mode 100644 index 00000000..1b304d2a --- /dev/null +++ b/VDS/Utilities/SurfaceConfigurationValue.swift @@ -0,0 +1,31 @@ +// +// SurfaceConfigurationValue.swift +// VDS +// +// Created by Bandaru, Krishna Kishore on 05/03/24. +// + +import Foundation + +/** +SurfaceConfiguration is a type that holds the generic datatype for light surface & dark surface and returns the value based on the surface. +*/ +struct SurfaceConfigurationValue { + + var lightValue: ValueType + var darkValue: ValueType + + public init(_ lightValue: ValueType, _ darkValue: ValueType) { + self.lightValue = lightValue + self.darkValue = darkValue + } + + public init(value: ValueType) { + self.lightValue = value + self.darkValue = value + } + + public func value(for object: Surfaceable) -> ValueType { + object.surface == .light ? lightValue : darkValue + } +}