diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index bf614448..5cc7ae1c 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -11,6 +11,8 @@ 180636C92C29B0DF00C92D86 /* InputStepperLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 180636C82C29B0DF00C92D86 /* InputStepperLog.txt */; }; 1808BEBC2BA41C3200129230 /* CarouselScrollbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1808BEBB2BA41C3200129230 /* CarouselScrollbar.swift */; }; 1832AC572BA0791D008AE476 /* BreadcrumbCellItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1832AC562BA0791D008AE476 /* BreadcrumbCellItem.swift */; }; + 184023452C61E7AD00A412C8 /* PriceLockup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 184023442C61E7AD00A412C8 /* PriceLockup.swift */; }; + 184023472C61E7EC00A412C8 /* PriceLockupChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 184023462C61E7EC00A412C8 /* PriceLockupChangeLog.txt */; }; 1842B1DF2BECE28B0021AFCA /* CalendarDateViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1842B1DE2BECE28B0021AFCA /* CalendarDateViewCell.swift */; }; 1842B1E12BECE7B70021AFCA /* CalendarHeaderReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1842B1E02BECE7B70021AFCA /* CalendarHeaderReusableView.swift */; }; 1842B1E32BECF0A20021AFCA /* CalendarFooterReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1842B1E22BECF0A10021AFCA /* CalendarFooterReusableView.swift */; }; @@ -214,6 +216,8 @@ 1808BEBB2BA41C3200129230 /* CarouselScrollbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselScrollbar.swift; sourceTree = ""; }; 1808BEBF2BA456B700129230 /* CarouselScrollbarChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = CarouselScrollbarChangeLog.txt; sourceTree = ""; }; 1832AC562BA0791D008AE476 /* BreadcrumbCellItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreadcrumbCellItem.swift; sourceTree = ""; }; + 184023442C61E7AD00A412C8 /* PriceLockup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriceLockup.swift; sourceTree = ""; }; + 184023462C61E7EC00A412C8 /* PriceLockupChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = PriceLockupChangeLog.txt; sourceTree = ""; }; 1842B1DE2BECE28B0021AFCA /* CalendarDateViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarDateViewCell.swift; sourceTree = ""; }; 1842B1E02BECE7B70021AFCA /* CalendarHeaderReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarHeaderReusableView.swift; sourceTree = ""; }; 1842B1E22BECF0A10021AFCA /* CalendarFooterReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarFooterReusableView.swift; sourceTree = ""; }; @@ -471,6 +475,15 @@ path = CarouselScrollbar; sourceTree = ""; }; + 184023432C61E78D00A412C8 /* PriceLockup */ = { + isa = PBXGroup; + children = ( + 184023442C61E7AD00A412C8 /* PriceLockup.swift */, + 184023462C61E7EC00A412C8 /* PriceLockupChangeLog.txt */, + ); + path = PriceLockup; + sourceTree = ""; + }; 186D13C92BBA8A3500986B53 /* DropdownSelect */ = { isa = PBXGroup; children = ( @@ -700,6 +713,7 @@ EAD0688C2A55F801002E3A2D /* Loader */, 445BA07629C07ABA0036A7C5 /* Notification */, 71B23C2B2B91FA510027F7D9 /* Pagination */, + 184023432C61E78D00A412C8 /* PriceLockup */, EA89200B28B530F0006B9984 /* RadioBox */, EAF7F11428A1470D00B287F5 /* RadioButton */, 440B84C82BD8E0CE004A732A /* Table */, @@ -1199,6 +1213,7 @@ buildActionMask = 2147483647; files = ( EA3362042891E14D0071C351 /* VerizonNHGeTX-Bold.otf in Resources */, + 184023472C61E7EC00A412C8 /* PriceLockupChangeLog.txt in Resources */, EA3362072891E14D0071C351 /* VerizonNHGeDS-Regular.otf in Resources */, EA3362062891E14D0071C351 /* VerizonNHGeTX-Regular.otf in Resources */, EA3362052891E14D0071C351 /* VerizonNHGeDS-Bold.otf in Resources */, @@ -1410,6 +1425,7 @@ EAC58C0E2BED021600BA39FA /* Password.swift in Sources */, EAF7F0AD289B142900B287F5 /* StrikeThroughLabelAttribute.swift in Sources */, EAB5FEF12927F4AA00998C17 /* SelfSizingCollectionView.swift in Sources */, + 184023452C61E7AD00A412C8 /* PriceLockup.swift in Sources */, EA3361B8288B2AAA0071C351 /* ViewProtocol.swift in Sources */, EA3361A8288B23300071C351 /* UIColor.swift in Sources */, EA2DC9B42BE2C6FE004F58C5 /* TextField.swift in Sources */, diff --git a/VDS/Components/Carousel/Carousel.swift b/VDS/Components/Carousel/Carousel.swift index f8d62520..9e14d3d3 100644 --- a/VDS/Components/Carousel/Carousel.swift +++ b/VDS/Components/Carousel/Carousel.swift @@ -58,7 +58,7 @@ open class Carousel: View { } /// Space between each tile. The default value will be 6X in tablet and 3X in mobile. - public enum Gutter: String, CaseIterable , DefaultValuing { + public enum Gutter: String, CaseIterable , DefaultValuing, Valuing { case gutter3X = "3X" case gutter6X = "6X" diff --git a/VDS/Components/PriceLockup/PriceLockup.swift b/VDS/Components/PriceLockup/PriceLockup.swift new file mode 100644 index 00000000..990e5b76 --- /dev/null +++ b/VDS/Components/PriceLockup/PriceLockup.swift @@ -0,0 +1,333 @@ +// +// PriceLockup.swift +// VDS +// +// Created by Kanamarlapudi, Vasavi on 06/08/24. +// + +import Foundation +import UIKit +import VDSCoreTokens + +@objcMembers +@objc(VDSPriceLockup) +open class PriceLockup: View { + + //-------------------------------------------------- + // 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: - Enums + //-------------------------------------------------- + /// Enum used to describe the term of PriceLockup. + public enum Term: String, DefaultValuing, CaseIterable { + case month, year, biennial, none + + /// The default term is 'month'. + public static var defaultValue : Self { .month } + + /// Text for this term of PriceLockup. + public var type: String { + switch self { + case .month: + return "mo" + case .year: + return "yr" + case .biennial: + return "biennial" + case .none: + return "" + } + } + } + + /// Enum that represents the size availble for PriceLockup. + public enum Size: String, DefaultValuing, CaseIterable { + case xxxsmall = "3XSmall" + case xxsmall = "2XSmall" + case xsmall = "XSmall" + case small + case medium + case large + case xlarge = "XLarge" + case xxlarge = "2XLarge" + + public static var defaultValue: Self { .medium } + } + + /// Enum used to describe the kind of PriceLockup. + public enum Kind: String, DefaultValuing, CaseIterable { + case primary, secondary, savings + + /// The default kind is 'primary'. + public static var defaultValue : Self { .primary } + + /// Color configuation relative to kind. + public var colorConfiguration: SurfaceColorConfiguration { + switch self { + case .primary: + return SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark) + case .secondary: + return SurfaceColorConfiguration(VDSColor.elementsSecondaryOnlight, VDSColor.elementsSecondaryOndark) + case .savings: + return SurfaceColorConfiguration(VDSColor.paletteGreen26, VDSColor.paletteGreen36) + } + } + } + + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + + /// If true, the component will render as bold. + open var bold: Bool = false { didSet { setNeedsUpdate() } } + + /// Currency - If hideCurrency true, the component will render without currency. + open var hideCurrency: Bool = false { didSet { setNeedsUpdate() } } + + /// Leading text for the component. + open var leadingText: String? { didSet { setNeedsUpdate() } } + + /// Value rendered for the component. + open var price: Float? { didSet { setNeedsUpdate() } } + + /// Color to the component. The default kind is primary. + open var kind: Kind = .defaultValue { didSet { setNeedsUpdate() } } + + /// Size of the component. It varies by size and viewport(mobile/Tablet). + /// The default size is medium with viewport mobile. + open var size: Size = .defaultValue { didSet { setNeedsUpdate() } } + + /// If true, the component with a strikethrough. It applies only when uniformSize is true. + /// Does not apply a strikethrough format to leading and trailing text. + open var strikethrough: Bool = false { didSet { setNeedsUpdate() } } + + /// Term text for the component. The default term is 'month'. + /// Superscript placement can vary when term and delimeter are "none". + open var term: Term = .defaultValue { didSet { setNeedsUpdate() } } + + /// Trailing text for the component. + open var trailingText: String? { didSet { setNeedsUpdate() } } + + /// Superscript text for the component. + open var superscript: String? { didSet { setNeedsUpdate() } } + + /// If true, currency and value have the same font text style as delimeter, term label and superscript. + /// This will render the pricing and term sections as a uniform size. + open var uniformSize: Bool = false { didSet { setNeedsUpdate() } } + + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- + internal var priceLockupLabel = Label().with { + $0.isAccessibilityElement = true + $0.lineBreakMode = .byWordWrapping + } + + internal var delimiterIndex = 0 + internal var strikethroughLocation = 0 + internal var strikethroughLength = 0 + + internal var textPosition:TextPosition = .preDelimiter + enum TextPosition: String, CaseIterable { + case preDelimiter, postDelimiter + } + + //-------------------------------------------------- + // MARK: - Configuration Properties + //-------------------------------------------------- + internal var containerSize: CGSize { CGSize(width: 45, height: 44) } + + // TextStyle for the size. + private var textStyle: TextStyle.StandardStyle { + switch (size, textPosition) { + case (.xxxsmall, .preDelimiter), (.xxxsmall, .postDelimiter): + return .micro + + case (.xxsmall, .preDelimiter), (.xxsmall, .postDelimiter): + return .bodySmall + + case (.xsmall, .preDelimiter), (.xsmall, .postDelimiter): + return .bodyMedium + + case (.small, .preDelimiter), (.small, .postDelimiter): + return .bodyLarge + + case (.medium, .preDelimiter): + return UIDevice.isIPad ? .titleSmall : .titleMedium + + case (.medium, .postDelimiter): + return .bodyLarge + + case (.large, .preDelimiter): + return UIDevice.isIPad ? .titleMedium : .titleLarge + + case (.large, .postDelimiter): + return UIDevice.isIPad ? .titleSmall : .titleMedium + + case (.xlarge, .preDelimiter): + return UIDevice.isIPad ? .titleLarge : .titleXLarge + + case (.xlarge, .postDelimiter): + return UIDevice.isIPad ? .titleMedium : .titleLarge + + case (.xxlarge, .preDelimiter): + return UIDevice.isIPad ? .titleXLarge : .featureSmall + + case (.xxlarge, .postDelimiter): + return UIDevice.isIPad ? .titleLarge : .titleXLarge + } + } + + //-------------------------------------------------- + // 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() + + // Price lockup label + addSubview(priceLockupLabel) + priceLockupLabel.pinToSuperView() + } + + /// Used to make changes to the View based off a change events or from local properties. + open override func updateView() { + super.updateView() + + priceLockupLabel.text = formatText() + priceLockupLabel.surface = surface + + // Set the attributed text + updateLabelAttributes() + } + + open override func setDefaults() { + super.setDefaults() + bold = false + hideCurrency = false + leadingText = nil + price = nil + kind = .defaultValue + size = .defaultValue + strikethrough = false + term = .defaultValue + trailingText = nil + superscript = nil + uniformSize = false + } + + /// Resets to default settings. + open override func reset() { + priceLockupLabel.reset() + super.reset() + } + + //-------------------------------------------------- + // MARK: - Private Methods + //-------------------------------------------------- + // Update PriceLockup text attributes + func updateLabelAttributes() { + var attributes: [any LabelAttributeModel] = [] + attributes.append(ColorLabelAttribute(location: 0, + length: priceLockupLabel.text.count, + color: kind.colorConfiguration.getColor(self))) + textPosition = .postDelimiter + if strikethrough { + + // strike applies only when uniformSize true. Does not apply a strikethrough format to leading, trailing, and superscript text. + attributes.append(TextStyleLabelAttribute(location: 0, + length: priceLockupLabel.text.count, + textStyle: bold ? textStyle.bold : textStyle.regular, + textPosition: .left)) + attributes.append(StrikeThroughLabelAttribute(location:strikethroughLocation, length: strikethroughLength)) + + } else if uniformSize { + + // currency and value have the same font text style as delimeter, term, trailing text and superscript. + attributes.append(TextStyleLabelAttribute(location: 0, + length: priceLockupLabel.text.count, + textStyle: bold ? textStyle.bold : textStyle.regular, + textPosition: .left)) + + } else { + + // size updates relative to predelimiter, postdelimiter + if delimiterIndex > 0 { + textPosition = .preDelimiter + attributes.append(TextStyleLabelAttribute(location: 0, + length: delimiterIndex, + textStyle: bold ? textStyle.bold : textStyle.regular, + textPosition: .left)) + + textPosition = .postDelimiter + attributes.append(TextStyleLabelAttribute(location: delimiterIndex, + length: priceLockupLabel.text.count-delimiterIndex, + textStyle: bold ? textStyle.bold : textStyle.regular, + textPosition: .left)) + } + } + priceLockupLabel.attributes = attributes + } + + // Get text for PriceLockup. + private func formatText() -> String { + var text : String = "" + let space = " " + let delimiter = "/" + delimiterIndex = 0 + strikethroughLength = 0 + let currency: String = hideCurrency ? "" : "$" + + if let leadingText { + text.append(leadingText) + text.append(space) + delimiterIndex = delimiterIndex + leadingText.count + space.count + } + + strikethroughLocation = delimiterIndex + + if let price = price?.clean { + text.append(currency) + text.append(price) + delimiterIndex = delimiterIndex + price.count + currency.count + strikethroughLength = price.count + currency.count + } + + if term != .none { + text.append(delimiter) + text.append(term.type) + strikethroughLength = strikethroughLength + delimiter.count + term.type.count + } + + if let trailingText { + text.append(space) + text.append(trailingText) + } + + if let superscript { + text.append(superscript) + } + + return text + } +} + +extension Float { + // remove a decimal from a float if the decimal is equal to 0 + var clean: String { + return self.truncatingRemainder(dividingBy: 1) == 0 ? String(format: "%.0f", self) : String(describing: self) + } +} diff --git a/VDS/Components/PriceLockup/PriceLockupChangeLog.txt b/VDS/Components/PriceLockup/PriceLockupChangeLog.txt new file mode 100644 index 00000000..e06ee9f8 --- /dev/null +++ b/VDS/Components/PriceLockup/PriceLockupChangeLog.txt @@ -0,0 +1,28 @@ +MM/DD/YYYY +---------------- + +11/16/2023 +---------------- +- Added leadingText and trailingText to anatomy +- Added leadingText and trailingText props to configurations +- Added term prop to configurations +- Removed Suspended orange color and corresponding color tokens + +11/27/2023 +---------------- +- Removed “Delimiter” from Anatomy as “Term” includes both delimiter and term +- Added “Figma only” badge to leadingText and trailingText in Configurations +- Added superscript to “none” under term in Configurations +- Added Overflow section to Layout and spacing +- Updated Spacing to allow for leading and trailing text + +12/18/23 +---------------- +- Updated all pages with spec template updates from Doc Utility Expansion Pack +- Added Content props section to Config page + +1/15/24 +---------------- +- Clarified strikethrough does not apply to leading or trailing text +- Clarified and added to text overflow examples +- Correct Success to Savings in the configuration seciton diff --git a/VDS/Components/TileContainer/TileContainer.swift b/VDS/Components/TileContainer/TileContainer.swift index edb369dd..b00d0388 100644 --- a/VDS/Components/TileContainer/TileContainer.swift +++ b/VDS/Components/TileContainer/TileContainer.swift @@ -15,7 +15,7 @@ import Combine open class TileContainer: TileContainerBase { /// Enum used to describe the padding choices used for this component. - public enum Padding: DefaultValuing { + public enum Padding: DefaultValuing, Valuing { case padding3X case padding4X case padding6X @@ -44,7 +44,7 @@ open class TileContainer: TileContainerBase { } } -open class TileContainerBase: View where PaddingType.ValueType == CGFloat { +open class TileContainerBase: View where PaddingType.ValueType == CGFloat { //-------------------------------------------------- // MARK: - Initializers diff --git a/VDS/Components/Tilelet/Tilelet.swift b/VDS/Components/Tilelet/Tilelet.swift index 263d6b77..86dd346a 100644 --- a/VDS/Components/Tilelet/Tilelet.swift +++ b/VDS/Components/Tilelet/Tilelet.swift @@ -20,7 +20,7 @@ import Combine open class Tilelet: TileContainerBase { /// Enum used to describe the padding choices used for this component. - public enum Padding: String, DefaultValuing, CaseIterable { + public enum Padding: String, DefaultValuing, Valuing, CaseIterable { case small case large diff --git a/VDS/Protocols/DefaultValuing.swift b/VDS/Protocols/DefaultValuing.swift index ffeaf0eb..7d156053 100644 --- a/VDS/Protocols/DefaultValuing.swift +++ b/VDS/Protocols/DefaultValuing.swift @@ -7,6 +7,6 @@ import Foundation -public protocol DefaultValuing: Valuing { +public protocol DefaultValuing { static var defaultValue: Self { get } } diff --git a/VDS/VDS.docc/VDS.md b/VDS/VDS.docc/VDS.md index 2860624d..0a494234 100755 --- a/VDS/VDS.docc/VDS.md +++ b/VDS/VDS.docc/VDS.md @@ -40,6 +40,7 @@ Using the system allows designers and developers to collaborate more easily and - ``Loader`` - ``Notification`` - ``Pagination`` +- ``PriceLockup`` - ``RadioBoxItem`` - ``RadioBoxGroup`` - ``RadioButton``