diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index 3da4fb0d..7090dcfd 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 1808BEBC2BA41C3200129230 /* CarouselScrollbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1808BEBB2BA41C3200129230 /* CarouselScrollbar.swift */; }; 1832AC572BA0791D008AE476 /* BreadcrumbCellItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1832AC562BA0791D008AE476 /* BreadcrumbCellItem.swift */; }; 183B16F32C78CF7C00BA6A10 /* CarouselSlotCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 183B16F22C78CF7C00BA6A10 /* CarouselSlotCell.swift */; }; + 183B16F72C80B32200BA6A10 /* FootnoteGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 183B16F62C80B32200BA6A10 /* FootnoteGroup.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 */; }; @@ -20,6 +21,8 @@ 1855EC662BAABF2A002ACAC2 /* BreadcrumbItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1855EC652BAABF2A002ACAC2 /* BreadcrumbItemModel.swift */; }; 186D13CB2BBA8B1500986B53 /* DropdownSelect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 186D13CA2BBA8B1500986B53 /* DropdownSelect.swift */; }; 18792A902B7431F2008C0D29 /* ButtonIconBadgeIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18792A8F2B7431F2008C0D29 /* ButtonIconBadgeIndicatorModel.swift */; }; + 18926F5B2C7616A500C55BF6 /* FootnoteItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18926F5A2C7616A500C55BF6 /* FootnoteItem.swift */; }; + 18926F5D2C7616C600C55BF6 /* FootnoteChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 18926F5C2C7616C600C55BF6 /* FootnoteChangeLog.txt */; }; 18A3F12A2BD9298900498E4A /* Calendar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A3F1292BD9298900498E4A /* Calendar.swift */; }; 18A65A022B96E848006602CC /* Breadcrumbs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A65A012B96E848006602CC /* Breadcrumbs.swift */; }; 18A65A042B96F050006602CC /* BreadcrumbItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A65A032B96F050006602CC /* BreadcrumbItem.swift */; }; @@ -218,6 +221,7 @@ 1808BEBF2BA456B700129230 /* CarouselScrollbarChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = CarouselScrollbarChangeLog.txt; sourceTree = ""; }; 1832AC562BA0791D008AE476 /* BreadcrumbCellItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreadcrumbCellItem.swift; sourceTree = ""; }; 183B16F22C78CF7C00BA6A10 /* CarouselSlotCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselSlotCell.swift; sourceTree = ""; }; + 183B16F62C80B32200BA6A10 /* FootnoteGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FootnoteGroup.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 = ""; }; @@ -229,6 +233,8 @@ 186D13CA2BBA8B1500986B53 /* DropdownSelect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropdownSelect.swift; sourceTree = ""; }; 186D13CE2BBC36EE00986B53 /* DropdownSelectChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = DropdownSelectChangeLog.txt; sourceTree = ""; }; 18792A8F2B7431F2008C0D29 /* ButtonIconBadgeIndicatorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonIconBadgeIndicatorModel.swift; sourceTree = ""; }; + 18926F5A2C7616A500C55BF6 /* FootnoteItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FootnoteItem.swift; sourceTree = ""; }; + 18926F5C2C7616C600C55BF6 /* FootnoteChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = FootnoteChangeLog.txt; sourceTree = ""; }; 18A3F1292BD9298900498E4A /* Calendar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Calendar.swift; sourceTree = ""; }; 18A65A012B96E848006602CC /* Breadcrumbs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Breadcrumbs.swift; sourceTree = ""; }; 18A65A032B96F050006602CC /* BreadcrumbItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreadcrumbItem.swift; sourceTree = ""; }; @@ -496,6 +502,16 @@ path = DropdownSelect; sourceTree = ""; }; + 18926F592C76168300C55BF6 /* Footnote */ = { + isa = PBXGroup; + children = ( + 18926F5A2C7616A500C55BF6 /* FootnoteItem.swift */, + 183B16F62C80B32200BA6A10 /* FootnoteGroup.swift */, + 18926F5C2C7616C600C55BF6 /* FootnoteChangeLog.txt */, + ); + path = Footnote; + sourceTree = ""; + }; 18A3F1202BD8F5DE00498E4A /* Calendar */ = { isa = PBXGroup; children = ( @@ -709,6 +725,7 @@ EAF7F092289985E200B287F5 /* Checkbox */, EAC58C1F2BF127F000BA39FA /* DatePicker */, 186D13C92BBA8A3500986B53 /* DropdownSelect */, + 18926F592C76168300C55BF6 /* Footnote */, EA985BF3296C609E00F2FF2E /* Icon */, 180636C52C29B06200C92D86 /* InputStepper */, EA3362412892EF700071C351 /* Label */, @@ -1215,6 +1232,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 18926F5D2C7616C600C55BF6 /* FootnoteChangeLog.txt in Resources */, EA3362042891E14D0071C351 /* VerizonNHGeTX-Bold.otf in Resources */, 184023472C61E7EC00A412C8 /* PriceLockupChangeLog.txt in Resources */, EA3362072891E14D0071C351 /* VerizonNHGeDS-Regular.otf in Resources */, @@ -1279,6 +1297,7 @@ EA3361C328902D960071C351 /* Toggle.swift in Sources */, EAF7F0A0289AB7EC00B287F5 /* View.swift in Sources */, EAC58C232BF2824200BA39FA /* DatePicker.swift in Sources */, + 183B16F72C80B32200BA6A10 /* FootnoteGroup.swift in Sources */, EA89201328B568D8006B9984 /* RadioBoxItem.swift in Sources */, 71FC86E42B9841AC00700965 /* PaginationFlowLayout.swift in Sources */, EAC9258C2911C9DE00091998 /* InputField.swift in Sources */, @@ -1309,6 +1328,7 @@ 18B463A42BBD3C46005C4528 /* DropdownOptionModel.swift in Sources */, EAF2F4762C231EAA007BFEDC /* AccessibilityActionElement.swift in Sources */, EAC58BFD2BE935C300BA39FA /* TitleLockupTextColor.swift in Sources */, + 18926F5B2C7616A500C55BF6 /* FootnoteItem.swift in Sources */, EAACB89A2B927108006A3869 /* Valuing.swift in Sources */, EAE785312BA0A438009428EA /* UIImage+Helper.swift in Sources */, EAF193422C134F3400C68D18 /* Table.swift in Sources */, diff --git a/VDS/BaseClasses/Selector/SelectorItemBase.swift b/VDS/BaseClasses/Selector/SelectorItemBase.swift index 321df2a8..af415c5c 100644 --- a/VDS/BaseClasses/Selector/SelectorItemBase.swift +++ b/VDS/BaseClasses/Selector/SelectorItemBase.swift @@ -43,13 +43,14 @@ open class SelectorItemBase: Control, Errorable, Changea private var mainStackView = UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false - $0.alignment = .top + $0.alignment = .fill $0.axis = .vertical } private var selectorStackView = UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false - $0.alignment = .top + $0.alignment = .fill + $0.axis = .horizontal } @@ -171,10 +172,16 @@ open class SelectorItemBase: Control, Errorable, Changea isAccessibilityElement = false addSubview(mainStackView) - mainStackView.isUserInteractionEnabled = false + //wrap the selectorView in a view that won't stretch it + //do this by not pinning the bottom + let selectorViewWrapper = UIView().with { $0.translatesAutoresizingMaskIntoConstraints = false } + selectorViewWrapper.addSubview(selectorView) + selectorView.pinTop().pinLeading().pinTrailing().pinBottomLessThanOrEqualTo() + + mainStackView.isUserInteractionEnabled = false mainStackView.addArrangedSubview(selectorStackView) mainStackView.addArrangedSubview(errorLabel) - selectorStackView.addArrangedSubview(selectorView) + selectorStackView.addArrangedSubview(selectorViewWrapper) selectorStackView.addArrangedSubview(selectorLabelStackView) selectorLabelStackView.addArrangedSubview(label) selectorLabelStackView.addArrangedSubview(childLabel) diff --git a/VDS/Components/Footnote/FootnoteChangeLog.txt b/VDS/Components/Footnote/FootnoteChangeLog.txt new file mode 100644 index 00000000..5dce6539 --- /dev/null +++ b/VDS/Components/Footnote/FootnoteChangeLog.txt @@ -0,0 +1,17 @@ +MM/DD/YYYY +---------------- +Initial Brand 3.0 handoff + +12/18/2023 +---------------- +- New + +12/28/2023 +---------------- +- hideSymbol updated to showSymbol and default set to True. +- Figma-only properties section added in Footnote Item Configurations section. + +01/16/2024 +---------------- +- hideSymbol reverted to hideSymbol and default set to False. +- Figma-only properties section removed. diff --git a/VDS/Components/Footnote/FootnoteGroup.swift b/VDS/Components/Footnote/FootnoteGroup.swift new file mode 100644 index 00000000..5d325d8b --- /dev/null +++ b/VDS/Components/Footnote/FootnoteGroup.swift @@ -0,0 +1,160 @@ +// +// FootnoteGroup.swift +// VDS +// +// Created by Kanamarlapudi, Vasavi on 29/08/24. +// + +import Foundation +import UIKit +import VDSCoreTokens + +/// This must always be paired with one or more ``Footnote`` in a FootnoteGroup. +@objcMembers +@objc(VDSFootnoteGroup) +open class FootnoteGroup: 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 width of a fixed value or percentage of parent's width. + public enum Width { + case percentage(CGFloat) + case value(CGFloat) + } + + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + /// Array of ``Footnote`` for the Footnote items. + open var footnoteItems: [FootnoteItem] = [] { didSet { updateFootnoteItems() } } + + /// Any percentage or pixel value and cannot exceed container size. + /// If there is a width that is larger than container size, the footnote will resize to container's width. + open var width: Width? { + get { _width } + set { + if let newValue { + switch newValue { + case .percentage(let percentage): + if percentage <= 100.0 { + _width = newValue + } + case .value(let value): + if value > 0 { + _width = newValue + } + } + } else { + _width = nil + } + updateContainerWidth() + } + } + + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- + private var _width: Width? = nil + + private lazy var stackView = UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.axis = .vertical + $0.distribution = .fill + $0.spacing = VDSLayout.space3X + $0.backgroundColor = .clear + } + + //-------------------------------------------------- + // MARK: - Configuration Properties + //-------------------------------------------------- + internal var maxWidth: CGFloat { horizontalPinnedWidth() ?? (superview?.frame.size.width ?? frame.size.width) } + internal var minWidth: CGFloat { containerSize.width } + internal var containerSize: CGSize { CGSize(width: 55, height: 44) } + + //-------------------------------------------------- + // MARK: - Constraints + //-------------------------------------------------- + internal var widthConstraint: NSLayoutConstraint? + + //-------------------------------------------------- + // MARK: - Overrides + //-------------------------------------------------- + + /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations. + open override func setup() { + super.setup() + + // add footnote item stackview. + addSubview(stackView) + stackView.pinToSuperView() + widthConstraint = widthAnchor.constraint(equalToConstant: 0).deactivate() + } + + open override func setDefaults() { + super.setDefaults() + width = nil + footnoteItems = [] + } + + internal func updateFootnoteItems() { + // symbol containers are as wide as the widest symbol container in the group. + var symbolMaxWidth = 0.0 + + footnoteItems.forEach { footnote in + let separatorWidth = Label().with { + $0.text = footnote.symbolType + $0.textStyle = footnote.symbolLabel.textStyle + $0.sizeToFit() + }.intrinsicContentSize.width + symbolMaxWidth = max(separatorWidth, symbolMaxWidth) + } + + stackView.removeArrangedSubviews() + + // add symbol label, text label to stack. + footnoteItems.forEach { footnote in + footnote.symbolWidth = symbolMaxWidth + footnote.surface = surface + stackView.addArrangedSubview(footnote) + } + } + + /// Update container width after updating content. + internal func updateContainerWidth() { + var newWidth = 0.0 + + switch width { + case .percentage(let percentage): + newWidth = max(maxWidth * ((percentage) / 100), minWidth) + + case .value(let value): + newWidth = value > maxWidth ? maxWidth : value + + case nil: break + + } + + widthConstraint?.deactivate() + + if newWidth > minWidth && newWidth < maxWidth { + widthConstraint?.constant = newWidth + widthConstraint?.activate() + } + } +} diff --git a/VDS/Components/Footnote/FootnoteItem.swift b/VDS/Components/Footnote/FootnoteItem.swift new file mode 100644 index 00000000..489c05be --- /dev/null +++ b/VDS/Components/Footnote/FootnoteItem.swift @@ -0,0 +1,257 @@ +// +// Footnote.swift +// VDS +// +// Created by Kanamarlapudi, Vasavi on 21/08/24. +// + +import Foundation +import UIKit +import VDSCoreTokens + +/// A footnote is text that provides supporting details, legal copy and links to related content. +/// It exists at the bottom or "foot" of a page or section. +@objcMembers +@objc(VDSFootnoteItem) +open class FootnoteItem: 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 kind of component. + public enum Kind: String, DefaultValuing, CaseIterable { + case primary, secondary + + /// The default kind is 'primary'. + public static var defaultValue : Self { .secondary } + + /// Color configuation to Symbol and Text 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) + } + } + } + + /// Enum that represents the size availble for component. + public enum Size: String, DefaultValuing, CaseIterable { + case micro + case small + case large + + public static var defaultValue: Self { .micro } + + /// TextStyle relative to Size. + public var textStyle: TextStyle.StandardStyle { + switch self { + case .micro: + return .micro + case .small: + return .bodySmall + case .large: + return .bodyLarge + } + } + } + + /// Enum used to describe the width of a fixed value or percentage of parent's width. + public enum Width { + case percentage(CGFloat) + case value(CGFloat) + } + + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + /// Color to the component. The default kind is Secondary. + open var kind: Kind = .defaultValue { didSet { setNeedsUpdate() } } + + /// Size of the component. The default size is Micro. + open var size: Size = .defaultValue { didSet { setNeedsUpdate() } } + + /// If hideSymbol true, the component will show text without symbol. + open var hideSymbol: Bool = false { didSet { setNeedsUpdate() } } + + /// symbol type will be shown for the footnote item. The default symbolType is 'asterisk'. + open var symbolType: String = "*" { didSet { setNeedsUpdate() } } + + /// Text of the footnote item. + open var text: String? { didSet { setNeedsUpdate() } } + + open var tooltipModel: Tooltip.TooltipModel? { didSet { setNeedsUpdate() } } + + /// Any percentage or pixel value and cannot exceed container size. + /// If there is a width that is larger than container size, the footnote will resize to container's width. + open var width: Width? { + get { _width } + set { + if let newValue { + switch newValue { + case .percentage(let percentage): + if percentage <= 100.0 { + _width = newValue + } + case .value(let value): + if value > 0 { + _width = newValue + } + } + } else { + _width = nil + } + updateContainerWidth() + } + } + + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- + private var _width: Width? = nil + + /// To set the widest symbol width from the symbol container in the group. + internal var symbolWidth: CGFloat? { didSet { setNeedsUpdate() } } + + private lazy var itemStackView = UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.axis = .horizontal + $0.alignment = .leading + $0.distribution = .fill + $0.spacing = VDSLayout.space1X + $0.backgroundColor = .clear + } + + internal var symbolLabel = Label().with { + $0.isAccessibilityElement = true + $0.numberOfLines = 1 + $0.sizeToFit() + } + + internal var textLabel = Label().with { + $0.isAccessibilityElement = true + $0.lineBreakMode = .byWordWrapping + } + + //-------------------------------------------------- + // MARK: - Configuration Properties + //-------------------------------------------------- + internal var maxWidth: CGFloat { horizontalPinnedWidth() ?? (superview?.frame.size.width ?? frame.size.width) } + internal var minWidth: CGFloat { containerSize.width } + internal var containerSize: CGSize { CGSize(width: 45, height: 44) } + + //-------------------------------------------------- + // MARK: - Constraints + //-------------------------------------------------- + internal var symbolWidthConstraint: NSLayoutConstraint? + internal var itemWidthConstraint: NSLayoutConstraint? + + //-------------------------------------------------- + // MARK: - Overrides + //-------------------------------------------------- + + /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations. + open override func setup() { + super.setup() + + // add footnote item stackview. + addSubview(itemStackView) + itemStackView.pinToSuperView() + + // width constraints + itemWidthConstraint = widthAnchor.constraint(equalToConstant: 0).deactivate() + + // add symbol label, text label to stack. + itemStackView.addArrangedSubview(symbolLabel) + itemStackView.addArrangedSubview(textLabel) + itemStackView.setCustomSpacing(VDSLayout.space1X, after: symbolLabel) + + symbolWidthConstraint = symbolLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 0) + symbolWidthConstraint?.isActive = true + } + + open override func setDefaults() { + super.setDefaults() + hideSymbol = false + text = nil + tooltipModel = nil + width = nil + } + + /// Resets to default settings. + open override func reset() { + symbolLabel.reset() + textLabel.reset() + super.reset() + } + + /// Used to make changes to the View based off a change events or from local properties. + open override func updateView() { + super.updateView() + + // Update symbolLabel + symbolLabel.text = symbolType + symbolLabel.isHidden = hideSymbol + symbolLabel.textColor = kind.colorConfiguration.getColor(self) + symbolLabel.textStyle = size.textStyle.regular + symbolLabel.surface = surface + + //Set width to the symbol label + if let symbolWidth, symbolWidth > 0 { + // Set the widest symbol width from the symbol container in the group. + symbolWidthConstraint?.constant = symbolWidth + } else { + symbolWidthConstraint?.constant = symbolLabel.intrinsicContentSize.width + } + + // Update textLabel + textLabel.text = text + textLabel.textColor = kind.colorConfiguration.getColor(self) + textLabel.textStyle = size.textStyle.regular + textLabel.surface = surface + + // Set the textLabel attributes + if let tooltipModel { + var attributes: [any LabelAttributeModel] = [] + attributes.append(TooltipLabelAttribute(surface: surface, model: tooltipModel, presenter: self)) + textLabel.attributes = attributes + } + } + + /// Update container width after updating content. + internal func updateContainerWidth() { + var newWidth = 0.0 + switch width { + case .percentage(let percentage): + newWidth = max(maxWidth * ((percentage) / 100), minWidth) + + case .value(let value): + newWidth = value > maxWidth ? maxWidth : value + + case nil: + break + } + itemWidthConstraint?.deactivate() + + if newWidth > minWidth && newWidth < maxWidth { + itemWidthConstraint?.constant = newWidth + itemWidthConstraint?.activate() + } + } +} diff --git a/VDS/VDS.docc/VDS.md b/VDS/VDS.docc/VDS.md index ac8d39e7..e1fda159 100755 --- a/VDS/VDS.docc/VDS.md +++ b/VDS/VDS.docc/VDS.md @@ -32,6 +32,8 @@ Using the system allows designers and developers to collaborate more easily and - ``CheckboxItem`` - ``CheckboxGroup`` - ``DropdownSelect`` +- ``FootnoteItem`` +- ``FootnoteGroup`` - ``Icon`` - ``InputStepper`` - ``InputField``