diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index c60f125a..cca7c7f7 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -10,16 +10,23 @@ 1808BEBC2BA41C3200129230 /* CarouselScrollbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1808BEBB2BA41C3200129230 /* CarouselScrollbar.swift */; }; 1808BEC02BA456B700129230 /* CarouselScrollbarChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 1808BEBF2BA456B700129230 /* CarouselScrollbarChangeLog.txt */; }; 1832AC572BA0791D008AE476 /* BreadcrumbCellItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1832AC562BA0791D008AE476 /* BreadcrumbCellItem.swift */; }; + 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 */; }; 18450CF12BA1B19C009FDF2A /* BreadcrumbsChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 18450CF02BA1B19C009FDF2A /* BreadcrumbsChangeLog.txt */; }; 1855EC662BAABF2A002ACAC2 /* BreadcrumbItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1855EC652BAABF2A002ACAC2 /* BreadcrumbItemModel.swift */; }; 186B2A8A2B88DA7F001AB71F /* TextAreaChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 186B2A892B88DA7F001AB71F /* TextAreaChangeLog.txt */; }; 186D13CB2BBA8B1500986B53 /* DropdownSelect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 186D13CA2BBA8B1500986B53 /* DropdownSelect.swift */; }; 186D13CF2BBC36EF00986B53 /* DropdownSelectChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 186D13CE2BBC36EE00986B53 /* DropdownSelectChangeLog.txt */; }; 18792A902B7431F2008C0D29 /* ButtonIconBadgeIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18792A8F2B7431F2008C0D29 /* ButtonIconBadgeIndicatorModel.swift */; }; + 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 */; }; 18B463A42BBD3C46005C4528 /* DropdownOptionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B463A32BBD3C46005C4528 /* DropdownOptionModel.swift */; }; 18BDEE822B75316E00452358 /* ButtonIconChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 18BDEE812B75316E00452358 /* ButtonIconChangeLog.txt */; }; + 18FEA1AD2BDD137500A56439 /* CalendarIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1AC2BDD137500A56439 /* CalendarIndicatorModel.swift */; }; + 18FEA1B52BE0E63600A56439 /* Date+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1B42BE0E63600A56439 /* Date+Extension.swift */; }; + 18FEA1B92BE1301700A56439 /* CalendarChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 18FEA1B82BE1301700A56439 /* CalendarChangeLog.txt */; }; 445BA07829C07B3D0036A7C5 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 445BA07729C07B3D0036A7C5 /* Notification.swift */; }; 44604AD429CE186A00E62B51 /* NotificationButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44604AD329CE186A00E62B51 /* NotificationButtonModel.swift */; }; 44604AD729CE196600E62B51 /* Line.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44604AD629CE196600E62B51 /* Line.swift */; }; @@ -150,6 +157,10 @@ EAC58C142BED0DEC00BA39FA /* Number.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C132BED0DEC00BA39FA /* Number.swift */; }; EAC58C162BED0E0300BA39FA /* InlineAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C152BED0E0300BA39FA /* InlineAction.swift */; }; EAC58C182BED0E2300BA39FA /* SecurityCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C172BED0E2300BA39FA /* SecurityCode.swift */; }; + EAC58C232BF2824200BA39FA /* DatePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C222BF2824200BA39FA /* DatePicker.swift */; }; + EAC58C252BF2A7FB00BA39FA /* DatePickerChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = EAC58C242BF2A7FB00BA39FA /* DatePickerChangeLog.txt */; }; + EAC58C272BF4116200BA39FA /* DatePickerCalendarModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C262BF4116200BA39FA /* DatePickerCalendarModel.swift */; }; + EAC58C292BF4118C00BA39FA /* DatePickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C282BF4118C00BA39FA /* DatePickerViewController.swift */; }; EAC71A1D2A2E155A00E47A9F /* Checkbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC71A1C2A2E155A00E47A9F /* Checkbox.swift */; }; EAC71A1F2A2E173D00E47A9F /* RadioButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC71A1E2A2E173D00E47A9F /* RadioButton.swift */; }; EAC846F3294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC846F2294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift */; }; @@ -213,16 +224,23 @@ 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 = ""; }; + 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 = ""; }; 18450CF02BA1B19C009FDF2A /* BreadcrumbsChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = BreadcrumbsChangeLog.txt; sourceTree = ""; }; 1855EC652BAABF2A002ACAC2 /* BreadcrumbItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreadcrumbItemModel.swift; sourceTree = ""; }; 186B2A892B88DA7F001AB71F /* TextAreaChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = TextAreaChangeLog.txt; sourceTree = ""; }; 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 = ""; }; + 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 = ""; }; 18B463A32BBD3C46005C4528 /* DropdownOptionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropdownOptionModel.swift; sourceTree = ""; }; 18BDEE812B75316E00452358 /* ButtonIconChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ButtonIconChangeLog.txt; sourceTree = ""; }; + 18FEA1AC2BDD137500A56439 /* CalendarIndicatorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarIndicatorModel.swift; sourceTree = ""; }; + 18FEA1B42BE0E63600A56439 /* Date+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extension.swift"; sourceTree = ""; }; + 18FEA1B82BE1301700A56439 /* CalendarChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = CalendarChangeLog.txt; sourceTree = ""; }; 445BA07729C07B3D0036A7C5 /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = ""; }; 44604AD329CE186A00E62B51 /* NotificationButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationButtonModel.swift; sourceTree = ""; }; 44604AD629CE196600E62B51 /* Line.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Line.swift; sourceTree = ""; }; @@ -355,6 +373,10 @@ EAC58C132BED0DEC00BA39FA /* Number.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Number.swift; sourceTree = ""; }; EAC58C152BED0E0300BA39FA /* InlineAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineAction.swift; sourceTree = ""; }; EAC58C172BED0E2300BA39FA /* SecurityCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityCode.swift; sourceTree = ""; }; + EAC58C222BF2824200BA39FA /* DatePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePicker.swift; sourceTree = ""; }; + EAC58C242BF2A7FB00BA39FA /* DatePickerChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = DatePickerChangeLog.txt; sourceTree = ""; }; + EAC58C262BF4116200BA39FA /* DatePickerCalendarModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickerCalendarModel.swift; sourceTree = ""; }; + EAC58C282BF4118C00BA39FA /* DatePickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickerViewController.swift; sourceTree = ""; }; EAC71A1C2A2E155A00E47A9F /* Checkbox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Checkbox.swift; sourceTree = ""; }; EAC71A1E2A2E173D00E47A9F /* RadioButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButton.swift; sourceTree = ""; }; EAC846F2294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonGroupCollectionViewCell.swift; sourceTree = ""; }; @@ -443,6 +465,20 @@ path = DropdownSelect; sourceTree = ""; }; + 18A3F1202BD8F5DE00498E4A /* Calendar */ = { + isa = PBXGroup; + children = ( + 18A3F1292BD9298900498E4A /* Calendar.swift */, + 1842B1DE2BECE28B0021AFCA /* CalendarDateViewCell.swift */, + 1842B1E22BECF0A10021AFCA /* CalendarFooterReusableView.swift */, + 1842B1E02BECE7B70021AFCA /* CalendarHeaderReusableView.swift */, + 18FEA1AC2BDD137500A56439 /* CalendarIndicatorModel.swift */, + 18FEA1B42BE0E63600A56439 /* Date+Extension.swift */, + 18FEA1B82BE1301700A56439 /* CalendarChangeLog.txt */, + ); + path = Calendar; + sourceTree = ""; + }; 18A65A002B96E7E1006602CC /* Breadcrumbs */ = { isa = PBXGroup; children = ( @@ -610,8 +646,10 @@ EAD062AE2A3B87210015965D /* BadgeIndicator */, 18A65A002B96E7E1006602CC /* Breadcrumbs */, EA0FC2BE2912D18200DF80B4 /* Buttons */, + 18A3F1202BD8F5DE00498E4A /* Calendar */, 1808BEBA2BA41B1D00129230 /* CarouselScrollbar */, EAF7F092289985E200B287F5 /* Checkbox */, + EAC58C1F2BF127F000BA39FA /* DatePicker */, 186D13C92BBA8A3500986B53 /* DropdownSelect */, EA985BF3296C609E00F2FF2E /* Icon */, EA3362412892EF700071C351 /* Label */, @@ -909,6 +947,17 @@ path = FieldTypes; sourceTree = ""; }; + EAC58C1F2BF127F000BA39FA /* DatePicker */ = { + isa = PBXGroup; + children = ( + EAC58C222BF2824200BA39FA /* DatePicker.swift */, + EAC58C262BF4116200BA39FA /* DatePickerCalendarModel.swift */, + EAC58C282BF4118C00BA39FA /* DatePickerViewController.swift */, + EAC58C242BF2A7FB00BA39FA /* DatePickerChangeLog.txt */, + ); + path = DatePicker; + sourceTree = ""; + }; EAC9257E29119B5D00091998 /* TextLink */ = { isa = PBXGroup; children = ( @@ -1123,8 +1172,10 @@ EAA5EEB928ECD24B003B3210 /* Icons.xcassets in Resources */, EAEEECA92B1F969700531FC2 /* TooltipChangeLog.txt in Resources */, 186D13CF2BBC36EF00986B53 /* DropdownSelectChangeLog.txt in Resources */, + 18FEA1B92BE1301700A56439 /* CalendarChangeLog.txt in Resources */, EAEEEC9C2B1F8F0700531FC2 /* TextLinkCaretChangeLog.txt in Resources */, EAA5EEE428F5B855003B3210 /* VerizonNHGDS-Light.otf in Resources */, + EAC58C252BF2A7FB00BA39FA /* DatePickerChangeLog.txt in Resources */, 71B5FCBB2B95A0CA00269BCC /* PaginationChangeLog.txt in Resources */, EAEEECAD2B1FC1A600531FC2 /* TitleLockupChangeLog.txt in Resources */, EAEEECAB2B1FBF2A00531FC2 /* ToggleChangeLog.txt in Resources */, @@ -1151,12 +1202,14 @@ EA985C2D296F03FE00F2FF2E /* TileletIconModels.swift in Sources */, EA89200428AECF4B006B9984 /* UITextField+Publisher.swift in Sources */, 18A65A022B96E848006602CC /* Breadcrumbs.swift in Sources */, + 1842B1E12BECE7B70021AFCA /* CalendarHeaderReusableView.swift in Sources */, EA0D1C3F2A6AD5E200E5C127 /* Typography+ContentSizeCategory.swift in Sources */, EA5F86C82A1BD99100BC83E4 /* TabModel.swift in Sources */, EA297A5729FB0A360031ED56 /* AppleGuidelinesTouchable.swift in Sources */, 1832AC572BA0791D008AE476 /* BreadcrumbCellItem.swift in Sources */, EA3361C328902D960071C351 /* Toggle.swift in Sources */, EAF7F0A0289AB7EC00B287F5 /* View.swift in Sources */, + EAC58C232BF2824200BA39FA /* DatePicker.swift in Sources */, EA89201328B568D8006B9984 /* RadioBoxItem.swift in Sources */, 71FC86E42B9841AC00700965 /* PaginationFlowLayout.swift in Sources */, EAC9258C2911C9DE00091998 /* InputField.swift in Sources */, @@ -1164,6 +1217,7 @@ EAB2376229E9880400AABE9A /* TrailingTooltipLabel.swift in Sources */, EAACB8982B92706F006A3869 /* DefaultValuing.swift in Sources */, EAB2376A29E9E59100AABE9A /* TooltipLaunchable.swift in Sources */, + 18A3F12A2BD9298900498E4A /* Calendar.swift in Sources */, 18A65A042B96F050006602CC /* BreadcrumbItem.swift in Sources */, EAB2375D29E8789100AABE9A /* Tooltip.swift in Sources */, 71BFA70A2B7F70E6000DCE33 /* DropShadowable.swift in Sources */, @@ -1233,6 +1287,7 @@ EA3361BD288B2C760071C351 /* TypeAlias.swift in Sources */, EAC58C0A2BED004E00BA39FA /* FieldType.swift in Sources */, EA471F3A2A95587500CE9E58 /* LayoutConstraintable.swift in Sources */, + EAC58C292BF4118C00BA39FA /* DatePickerViewController.swift in Sources */, EAB1D2CF28ABEF2B00DAE764 /* Typography+Base.swift in Sources */, EA0D1C3B2A6AD51B00E5C127 /* Typogprahy+Styles.swift in Sources */, EAF7F09A2899B17200B287F5 /* CATransaction.swift in Sources */, @@ -1274,12 +1329,16 @@ EA0B18062A9E2D2D00F2D0CD /* SelectorItemBase.swift in Sources */, EAB5FF0129424ACB00998C17 /* UIControl.swift in Sources */, EA985BF5296C60C000F2FF2E /* Icon.swift in Sources */, + 1842B1E32BECF0A20021AFCA /* CalendarFooterReusableView.swift in Sources */, EA3361AA288B25E40071C351 /* Disabling.swift in Sources */, EA3361B6288B2A410071C351 /* Control.swift in Sources */, EAC58C122BED0DDD00BA39FA /* Text.swift in Sources */, 5F21D7BF28DCEB3D003E7CD6 /* Useable.swift in Sources */, + EAC58C272BF4116200BA39FA /* DatePickerCalendarModel.swift in Sources */, EAF7F0B7289C12A600B287F5 /* UITapGestureRecognizer.swift in Sources */, + 1842B1DF2BECE28B0021AFCA /* CalendarDateViewCell.swift in Sources */, EA0D1C392A6AD4DF00E5C127 /* Typography+SpacingConfig.swift in Sources */, + 18FEA1B52BE0E63600A56439 /* Date+Extension.swift in Sources */, EAB2376629E9952D00AABE9A /* UIApplication.swift in Sources */, EAB5FED429267EB300998C17 /* UIView+NSLayoutConstraint.swift in Sources */, EAB2376829E9992800AABE9A /* TooltipAlertViewController.swift in Sources */, @@ -1299,6 +1358,7 @@ EA596ABF2A16B4F500300C4B /* Tabs.swift in Sources */, EAD062A72A3B67770015965D /* UIView+CALayer.swift in Sources */, EAD068942A560C13002E3A2D /* LoaderLaunchable.swift in Sources */, + 18FEA1AD2BDD137500A56439 /* CalendarIndicatorModel.swift in Sources */, EA985BEC2968A91200F2FF2E /* TitleLockupTitleModel.swift in Sources */, 5FC35BE328D51405004EBEAC /* Button.swift in Sources */, ); @@ -1449,7 +1509,7 @@ BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 62; + CURRENT_PROJECT_VERSION = 63; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1486,7 +1546,7 @@ BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 62; + CURRENT_PROJECT_VERSION = 63; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; diff --git a/VDS/Components/BadgeIndicator/BadgeIndicator.swift b/VDS/Components/BadgeIndicator/BadgeIndicator.swift index aae927a8..463e5df8 100644 --- a/VDS/Components/BadgeIndicator/BadgeIndicator.swift +++ b/VDS/Components/BadgeIndicator/BadgeIndicator.swift @@ -350,7 +350,7 @@ open class BadgeIndicator: View { open override func updateAccessibility() { super.updateAccessibility() if let accessibilityText { - accessibilityLabel = accessibilityText + accessibilityLabel = kind == .numbered ? label.text + " " + accessibilityText : accessibilityText } else if kind == .numbered { accessibilityLabel = label.text } else { diff --git a/VDS/Components/Calendar/Calendar.swift b/VDS/Components/Calendar/Calendar.swift new file mode 100644 index 00000000..f7c1940c --- /dev/null +++ b/VDS/Components/Calendar/Calendar.swift @@ -0,0 +1,375 @@ +// +// Calendar.swift +// VDS +// +// Created by Kanamarlapudi, Vasavi on 19/04/24. +// + +import Foundation +import UIKit +import VDSTokens +import Combine + +/// A calendar is a monthly view that lets customers select a single date. +@objc(VDSCalendar) +open class CalendarBase: Control, Changeable { + + //-------------------------------------------------- + // 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: - Public Properties + //-------------------------------------------------- + open var onChangeSubscriber: AnyCancellable? + + /// If set to true, the calendar will not have a border. + open var hideContainerBorder: Bool = false { didSet { setNeedsUpdate() } } + + /// If set to true, the calendar will not have current date indication. + open var hideCurrentDateIndicator: Bool = false { didSet { setNeedsUpdate() } } + + /// Enable specific days. Pass an array of string value in date format e.g. ['07/21/2024', '07/24/2024', 07/28/2024']. + /// All other dates will be inactive. + open var activeDates: [Date] = [] { didSet { setNeedsUpdate() } } + + /// Disable specific days. Pass an array of string value in date format e.g. ['07/21/2024', '07/24/2024', 07/28/2024']. + /// All other dates will be active. + open var inactiveDates: [Date] = [] { didSet { setNeedsUpdate() } } + + /// If provided, the calendar will allow a selection to be made from this date forward. Defaults to today. + open var minDate: Date = Date() { didSet { setNeedsUpdate() } } + + /// If provided, the calendar will allow a selection to be made up to this date. + open var maxDate: Date = Date() { didSet { setNeedsUpdate() } } + + /// If provided, this is the date that will show as selected by the Calendar. + /// If no value is provided, the current date will be used. If null is provided, no date will be selected. + open var selectedDate: Date = Date() { didSet { setNeedsUpdate() } } + + /// If provided, the calendar will be rendered with transparent background. + open var transparentBackground: Bool = false { didSet { setNeedsUpdate() } } + + /// Array of ``CalendarIndicatorModel`` you are wanting to show on legend. + open var indicators: [CalendarIndicatorModel] = [] { didSet { setNeedsUpdate() } } + + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- + internal var containerSize: CGSize { CGSize(width: 328, height: 376) } + internal var calendar = Calendar.current + + private let cellItemSize = CGSize(width: 40, height: 40) + private let headerHeight = 88.0 + private let footerHeight = 40.0 + private let calendarWidth = 304.0 + + private var heightConstraint: NSLayoutConstraint? + private var containerHeightConstraint: NSLayoutConstraint? + private var selectedIndexPath : IndexPath? + private var dates: [Date] = [] + private var days: [String] = [] + private var displayDate: Date = Date() + + internal var containerView = View().with { + $0.clipsToBounds = true + } + + /// Collectionview to load calendar month view + private lazy var collectionView: UICollectionView = { + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) + collectionView.isScrollEnabled = false + collectionView.translatesAutoresizingMaskIntoConstraints = false + collectionView.delegate = self + collectionView.dataSource = self + collectionView.showsHorizontalScrollIndicator = false + collectionView.showsVerticalScrollIndicator = false + collectionView.backgroundColor = .clear + + collectionView.register(CalendarDateViewCell.self, + forCellWithReuseIdentifier: CalendarDateViewCell.identifier) + + collectionView.register(CalendarHeaderReusableView.self, + forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, + withReuseIdentifier: CalendarHeaderReusableView.identifier) + + collectionView.register(CalendarFooterReusableView.self, + forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, + withReuseIdentifier: CalendarFooterReusableView.identifier) + return collectionView + }() + + //-------------------------------------------------- + // MARK: - Configuration + //-------------------------------------------------- + internal var containerBorderColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight , VDSColor.elementsPrimaryOndark) + internal var backgroundColorConfiguration = SurfaceColorConfiguration(VDSFormControlsColor.backgroundOnlight, VDSFormControlsColor.backgroundOndark) + + //-------------------------------------------------- + // MARK: - Overrides + //-------------------------------------------------- + open override func initialSetup() { + super.initialSetup() + } + + /// 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() + isAccessibilityElement = true + accessibilityLabel = "Calendar" + addSubview(containerView) + containerView + .pinTop() + .pinBottom() + .pinLeadingGreaterThanOrEqualTo() + .pinTrailingLessThanOrEqualTo() + .width(containerSize.width) + .heightGreaterThanEqualTo(containerSize.height) + containerView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() + + // Calendar View + containerView.addSubview(collectionView) + let calendarHeight = containerSize.height - (2 * VDSLayout.space4X) + let spacing = (containerSize.width - calendarWidth) / 2 + + collectionView + .pinTop(VDSLayout.space4X) + .pinBottom(VDSLayout.space4X) + .pinLeading(spacing) + .pinTrailing(spacing) + .width(calendarWidth) + .heightGreaterThanEqualTo(calendarHeight) + + collectionView.pinCenterX(anchor: containerView.centerXAnchor) + } + + open override func updateView() { + super.updateView() + // range check between min & max dates + if (minDate <= maxDate) { + // Check if current date falls between min & max dates. + let fallsBetween = displayDate.isBetweeen(date: minDate, andDate: maxDate) + displayDate = fallsBetween ? displayDate : minDate + fetchDates(with: displayDate) + } + + containerView.layer.backgroundColor = backgroundColorConfiguration.getColor(self).cgColor + if hideContainerBorder { + containerView.layer.borderColor = nil + containerView.layer.borderWidth = 0 + containerView.layer.cornerRadius = 0 + } else { + containerView.layer.borderColor = containerBorderColorConfiguration.getColor(self).cgColor + containerView.layer.borderWidth = VDSFormControls.borderWidth + containerView.layer.cornerRadius = VDSFormControls.borderRadius + } + } + + /// Resets to default settings. + open override func reset() { + super.reset() + hideContainerBorder = false + hideCurrentDateIndicator = false + transparentBackground = false + activeDates = [] + inactiveDates = [] + indicators = [] + } + + //-------------------------------------------------- + // MARK: - Private Methods + //-------------------------------------------------- + func fetchDates(with aDate: Date) { + heightConstraint?.isActive = false + containerHeightConstraint?.isActive = false + days.removeAll() + dates = aDate.calendarDisplayDays + + for date in dates { + // code to be executed + if date.monthInt != aDate.monthInt { + days.append("") + } else { + days.append(date.getDay()) + } + } + + collectionView.reloadData() + + var height = collectionView.collectionViewLayout.collectionViewContentSize.height + height = height > 0 ? height : containerSize.height + heightConstraint = collectionView.heightAnchor.constraint(equalToConstant: height) + containerHeightConstraint = containerView.heightAnchor.constraint(equalToConstant: height + (2 * VDSLayout.space4X)) + heightConstraint?.isActive = true + containerHeightConstraint?.isActive = true + layoutIfNeeded() + } +} + +extension CalendarBase: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { + //-------------------------------------------------- + // MARK: - UICollectionView Delegate & Datasource + //-------------------------------------------------- + public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + days.count + } + + public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CalendarDateViewCell.identifier, for: indexPath) as? CalendarDateViewCell else { return UICollectionViewCell() } + + var indicatorCount = 0 + + if indicators.count > 0 { + for x in 0...indicators.count - 1 { + if days[indexPath.row] == indicators[x].date.getDay() { + indicatorCount += 1 + } + } + } + + cell.update(with: surface, + indicators: indicators, + text: days[indexPath.row], + indicatorCount: indicatorCount, + selectedDate: selectedDate, + displayDate: displayDate, + hideDate: hideCurrentDateIndicator, + minDate: minDate, + maxDate: maxDate, + activeDates: activeDates, + inactiveDates: inactiveDates) + + if days[indexPath.row] == selectedDate.getDay() { + selectedIndexPath = indexPath + } + + return cell + } + + public func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { + + switch kind { + case UICollectionView.elementKindSectionHeader: + + // Header + guard let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: CalendarHeaderReusableView.identifier, for: indexPath) as? CalendarHeaderReusableView else { + return UICollectionReusableView() + } + var nextEnabled = false + var prevEnabled = false + + // check the interval between min date, max date.. set enable/disable flag for next / previous buttons. + if displayDate.monthInt < maxDate.monthInt && displayDate.yearInt == maxDate.yearInt + || displayDate.yearInt < maxDate.yearInt { + nextEnabled = true + } + if minDate.monthInt < displayDate.monthInt && minDate.yearInt == displayDate.yearInt + || minDate.yearInt < displayDate.yearInt { + prevEnabled = true + } + + header.nextClicked = { [weak self] in + guard let self = self else { return } + let date = calendar.date(byAdding: .month, value: 1, to:displayDate)! + if date.monthInt <= maxDate.monthInt && date.yearInt == maxDate.yearInt + || date.yearInt < maxDate.yearInt { + displayDate = date + fetchDates(with: displayDate) + } + } + + header.previousClicked = { [weak self] in + guard let self = self else { return } + let date = calendar.date(byAdding: .month, value: -1, to:displayDate)! + if minDate.monthInt <= date.monthInt && minDate.yearInt == date.yearInt || (minDate.yearInt < date.yearInt) { + displayDate = date + fetchDates(with: displayDate) + } + } + header.update(with: surface, date: displayDate, nextEnabled: nextEnabled, previousEnabled: prevEnabled) + + return header + + case UICollectionView.elementKindSectionFooter: + + guard let footer = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: CalendarFooterReusableView.identifier, for: indexPath) as? CalendarFooterReusableView else { + return UICollectionReusableView() + } + footer.update(with: surface, indicators: indicators) + + return footer + + default: + + return UICollectionReusableView() + + } + } + + + public func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool { + if let cell = collectionView.cellForItem(at: indexPath) as? CalendarDateViewCell { + let isEnabled: Bool = cell.isDateEnabled() + if isEnabled { + cell.activeModeStart() + } + } + return true + } + + public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + // reload selected index, if it is in enabled state. + if let cell = collectionView.cellForItem(at: indexPath) as? CalendarDateViewCell { + let isEnabled: Bool = cell.isDateEnabled() + if isEnabled { + cell.activeModeEnd() + + // Callback to pass selected date if it is enabled only. + selectedDate = dates[indexPath.row] + sendActions(for: .valueChanged) + displayDate = selectedDate + + var reloadIndexPaths = [indexPath] + + // If an cell is already selected, then it needs to be deselected. + // Add its index path to the array of index paths to be reloaded. + if let deselectIndexPath = selectedIndexPath { + reloadIndexPaths.append(deselectIndexPath) + } + + collectionView.reloadItems(at: reloadIndexPaths) + } + } + } + + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { + return CGSize(width: collectionView.frame.size.width, height: headerHeight) + } + + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { + return CGSize(width: collectionView.frame.size.width, height: footerHeight) + } + + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { + return VDSLayout.space1X + } + + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { + return VDSLayout.space1X + } + + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + return cellItemSize + } +} diff --git a/VDS/Components/Calendar/CalendarChangeLog.txt b/VDS/Components/Calendar/CalendarChangeLog.txt new file mode 100644 index 00000000..ee6928a3 --- /dev/null +++ b/VDS/Components/Calendar/CalendarChangeLog.txt @@ -0,0 +1,84 @@ +MM/DD/YYYY +---------------- +- Initial Brand 3.0 handoff + +12/24/2021 +---------------- +- Replaced focusring colors (previously interactive/onlight/ondark) with accessibility/onlight/ondark colors +- Updated focus border name (previously interactive.focusring.onlight) with focusring.onlight/ondark +- Updated the SPECS with FormControl tokens and corner radius tokens + +02/24/2022 +---------------- +- Replaced Caret Left and Right icons with bold assets. + +02/28/2022 +---------------- +- Removed dev note from Hover state and added a dev note to Active state. + +03/11/2022 +---------------- +- Update Hover and Active states triggers for carets. Icon swap and color change for mouse-only, and color change for touch. + +05/25/2022 +---------------- +- Added date indicator feature (including legend), today’s date indicator, and size/spacing adjustments to support this. + +07/20/2022 +---------------- +- Added configuration page. +- To configuration added transparent background and border suppression. + +08/10/2022 +---------------- +- Updated inverted and default to light and dark surface. Also, updated dark to selected. + +08/16/2022 +---------------- +- Updated default date background to be transparent. Updated border configuration prop name to hideContainerBorder. +- Moved Width from Configurations to Layout and Spacing, and Calendar Indicator specs under Elements. + +11/30/2022 +---------------- +- Added "(web only)" to any instance of "keyboard focus" + +12/13/2022 +---------------- +- Replaced form border and focus border pixel values, styles & spacing with tokens. + +01/09/2023 +---------------- +- Updated Specs to use new SPEC Templates and SPEC DOC Components. + +01/10/2023 +---------------- +- Updated Anatomy item #8 to “Current Date” to match design doc (originally “Today’s Date) + +04/12/2023 +---------------- +- Updated palette colors for Current date. + +05/01/2023 +---------------- +- Updated Day Header Text Style from Bold to Regular +- Updated Date Disabled Text Style from Bold to Regular +- Updated Date Text Style from Bold to Regular +- Updated Current Date font color from blue to black on light and white on dark. +- Updated all frames to reflect new designs. + +05/09/2023 +---------------- +- Replaced Previous and Next carets with Button Icon in: +Anatomy +Configurations +States + +11/09/2023 +---------------- +- Added component tokens +- Applied component tokens to selected states on light and dark surfaces +- Removed redundant color specifications from other sections + +11/30/2023 +---------------- +- Revised selected container background inverse tokens from onlight/ondark to light/dark diff --git a/VDS/Components/Calendar/CalendarDateViewCell.swift b/VDS/Components/Calendar/CalendarDateViewCell.swift new file mode 100644 index 00000000..d66522d0 --- /dev/null +++ b/VDS/Components/Calendar/CalendarDateViewCell.swift @@ -0,0 +1,327 @@ +// +// CalendarDateViewCell.swift +// VDS +// +// Created by Kanamarlapudi, Vasavi on 24/04/24. +// + +import UIKit +import VDSTokens + +final class CalendarDateViewCell: UICollectionViewCell { + + ///Identifier for the Calendar Date Cell. + static let identifier: String = String(describing: CalendarDateViewCell.self) + + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- + internal var containerSize: CGSize { CGSize(width: 40, height: 40) } + + internal var containerView = View().with { + $0.clipsToBounds = true + } + + private lazy var stackView: UIStackView = { + return UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.axis = .horizontal + $0.distribution = .fill + $0.alignment = .center + $0.spacing = VDSLayout.space1X + $0.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + $0.setContentHuggingPriority(.defaultHigh, for: .horizontal) + $0.backgroundColor = .clear + } + }() + + private var numberLabel = Label().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.textAlignment = .center + $0.textStyle = .bodySmall + } + + private lazy var shapeLayer = CAShapeLayer() + private var surface: Surface = .light + private let selectedTextColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryInverseOnlight, VDSColor.elementsPrimaryInverseOndark) + private let selectedBackgroundColor = SurfaceColorConfiguration(VDSColor.backgroundPrimaryInverseLight, VDSColor.backgroundPrimaryInverseDark) + private let selectedCellIndicatorColorConfiguration = SurfaceColorConfiguration(VDSColor.paletteGray65, VDSColor.paletteGray44) + private let unselectedTextColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark) + private let unselectedCellIndicatorColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsSecondaryOnlight, VDSColor.elementsSecondaryOndark) + private let disabledTextColorConfiguration = SurfaceColorConfiguration(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark) + private let disabledBackgroundColor = SurfaceColorConfiguration(VDSFormControlsColor.backgroundOnlight, VDSFormControlsColor.backgroundOndark) + private var activeBorderColorConfiguration = SurfaceColorConfiguration(VDSFormControlsColor.borderHoverOnlight , VDSFormControlsColor.borderHoverOndark) + private let currentDate = Date() + + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + override init(frame: CGRect) { + super.init(frame: frame) + setUp() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setUp() + } + + //-------------------------------------------------- + // MARK: - Private Methods + //-------------------------------------------------- + /// Configuring the cell with default setup. + private func setUp() { + isAccessibilityElement = false + contentView.addSubview(containerView) + containerView + .pinTop() + .pinBottom() + .pinLeadingGreaterThanOrEqualTo() + .pinTrailingLessThanOrEqualTo() + .height(containerSize.height) + .width(containerSize.width) + .pinCenterX() + + // Number label + containerView.addSubview(numberLabel) + numberLabel.pinToSuperView() + + // Indicators + containerView.addSubview(stackView) + let topPos = containerSize.height * 0.7 + stackView + .pinTop(topPos) + .pinBottom() + .pinTopGreaterThanOrEqualTo() + .pinTrailingLessThanOrEqualTo() + .pinCenterX() + } + + /// Updating UI based on selected date, modified indicators data along with surface. + /// Enable/disable cell based on min date, max date, active dates, inactive dates. + func update(with surface: Surface, indicators: [CalendarBase.CalendarIndicatorModel], text: String, indicatorCount: Int, selectedDate: Date, displayDate: Date, hideDate: Bool, minDate: Date, maxDate: Date, activeDates: [Date], inactiveDates: [Date]) { + + stackView.arrangedSubviews.forEach { $0.removeFromSuperview() } + numberLabel.surface = surface + numberLabel.text = text + self.surface = surface + + // enable/disable cells based on min date, max date and active/inactive dates. + updateLabel(with:surface, displayDate: displayDate, minDate: minDate, maxDate: maxDate, activeDates: activeDates, inactiveDates: inactiveDates) + + // handling inactive dates. + if inactiveDates.count > 0 { + for x in 0...inactiveDates.count-1 { + if inactiveDates[x].monthInt == displayDate.monthInt && inactiveDates[x].yearInt == displayDate.yearInt { + if let day:Int = Int(numberLabel.text), day == inactiveDates[x].dayInt { + disableLabel(with: surface) + } + } + } + } + + // update text color, bg color, corner radius. + if numberLabel.text == selectedDate.getDay() + && selectedDate.monthInt == displayDate.monthInt + && selectedDate.yearInt == displayDate.yearInt + && numberLabel.isEnabled { + + numberLabel.textColor = selectedTextColorConfiguration.getColor(surface) + layer.backgroundColor = selectedBackgroundColor.getColor(surface).cgColor + layer.cornerRadius = VDSFormControls.borderRadius + + } else { + numberLabel.textColor = unselectedTextColorConfiguration.getColor(surface) + layer.backgroundColor = nil + layer.cornerRadius = 0 + } + + // add indicators. + if indicatorCount > 0 { + for x in 0...indicators.count-1 { + if numberLabel.text == indicators[x].date.getDay() { + let color = numberLabel.text == selectedDate.getDay() ? selectedCellIndicatorColorConfiguration.getColor(surface) : unselectedCellIndicatorColorConfiguration.getColor(surface) + addIndicator(with: color, surface: surface, clearFullCircle: x == 1, drawSemiCircle: x == 2) + } + } + } + + // update text style for current date. + if numberLabel.text == currentDate.getDay() + && currentDate.monthInt == displayDate.monthInt + && currentDate.yearInt == displayDate.yearInt { + numberLabel.textStyle = hideDate ? .bodySmall : .boldBodySmall + } else { + numberLabel.textStyle = .bodySmall + } + } + + // returns cell enabled state. + func isDateEnabled() -> Bool { + return numberLabel.isEnabled + } + + func activeModeStart() { + numberLabel.layer.borderColor = activeBorderColorConfiguration.getColor(surface).cgColor + numberLabel.layer.borderWidth = VDSFormControls.borderWidth + numberLabel.layer.cornerRadius = VDSFormControls.borderRadius + } + + func activeModeEnd() { + numberLabel.layer.borderColor = nil + numberLabel.layer.borderWidth = 0 + numberLabel.layer.cornerRadius = 0 + } + + func disableLabel(with surface: Surface) { + numberLabel.isEnabled = false + numberLabel.textColor = disabledTextColorConfiguration.getColor(surface) + layer.backgroundColor = disabledBackgroundColor.getColor(surface).cgColor + } + + func showActiveDates(with displayDate: Date, activeDates: [Date], inactiveDates: [Date]) { + for x in 0...activeDates.count-1 { + if activeDates[x].monthInt == displayDate.monthInt && activeDates[x].yearInt == displayDate.yearInt { + if let day:Int = Int(numberLabel.text), day == activeDates[x].dayInt { + numberLabel.isEnabled = true + } + } + } + } + + // handing active dates if exist, else enable numberLabel to display day. + func handleActiveDates(with displayDate: Date, activeDates: [Date], inactiveDates: [Date]) { + if activeDates.count > 0 && inactiveDates.count == 0 { + showActiveDates(with: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) + } else { + numberLabel.isEnabled = true + } + } + + // enable all days if no active dates, handing active dates if exist. + func enableAllDaysAndCheckActiveDates(with surface:Surface, displayDate: Date, activeDates: [Date], inactiveDates: [Date]) { + if activeDates.count > 0 && inactiveDates.count == 0 { + disableLabel(with: surface) + showActiveDates(with: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) + } else { + numberLabel.isEnabled = true + } + } + + func minDateValidation(with surface:Surface, minDate: Date, displayDate: Date, activeDates: [Date], inactiveDates: [Date]) { + // validate days to enable/disable with min date only. + if let day = Int(numberLabel.text), day < minDate.dayInt { + disableLabel(with: surface) + } else { + numberLabel.isEnabled = false + handleActiveDates(with: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) + } + } + + func maxDateValidation(with surface:Surface, maxDate: Date, displayDate: Date, activeDates: [Date], inactiveDates: [Date]) { + // validate days to enable/disable with max date only. + if let day = Int(numberLabel.text), day > maxDate.dayInt { + disableLabel(with: surface) + } else { + numberLabel.isEnabled = false + handleActiveDates(with: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) + } + } + + func minAndMaxDateValidation(with surface:Surface, minDate: Date, maxDate: Date, displayDate: Date, activeDates: [Date], inactiveDates: [Date]) { + // validate days to enable/disable with min and max date. + if let day = Int(numberLabel.text), day < minDate.dayInt || day > maxDate.dayInt { + disableLabel(with: surface) + } else { + numberLabel.isEnabled = false + handleActiveDates(with: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) + } + } + + // enable/disable cells based on min date, max date and active/inactive dates. + func updateLabel(with surface: Surface, displayDate: Date, minDate: Date, maxDate: Date, activeDates: [Date], inactiveDates: [Date]) { + + if minDate.yearInt == displayDate.yearInt && !(maxDate.yearInt == displayDate.yearInt) { + // min year and max year are different, and matched to min year. + if minDate.monthInt == displayDate.monthInt { + // min year and max year are different, and matched to min year and min month. + minDateValidation(with: surface, minDate: minDate, displayDate: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) + } else { + // handing active dates - enable all days if no active dates. + enableAllDaysAndCheckActiveDates(with: surface, displayDate: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) + } + } else if maxDate.yearInt == displayDate.yearInt && !(minDate.yearInt == displayDate.yearInt) { + // min year and max year are different, and matched to max year. + if maxDate.monthInt == displayDate.monthInt { + // min year and max year are different, and matched to max year and max month. + maxDateValidation(with: surface, maxDate: maxDate, displayDate: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) + } else { + // handing active dates - enable all days if no active dates. + enableAllDaysAndCheckActiveDates(with: surface, displayDate: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) + } + } else if minDate.yearInt == displayDate.yearInt && maxDate.yearInt == displayDate.yearInt { + // min year and max year same + if minDate.monthInt == displayDate.monthInt && maxDate.monthInt == displayDate.monthInt { + // min year and max year same, when choose dates in same month. + minAndMaxDateValidation(with: surface, minDate: minDate, maxDate: maxDate, displayDate: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) + + } else if minDate.monthInt == displayDate.monthInt || maxDate.monthInt == displayDate.monthInt { + // min year and max year same, and choose dates in different months. + if minDate.monthInt == displayDate.monthInt { + // min year and max year same, and matched to min month. + minDateValidation(with: surface, minDate: minDate, displayDate: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) + } else if (maxDate.monthInt == displayDate.monthInt) { + // min year and max year same, and matched to max month. + maxDateValidation(with: surface, maxDate: maxDate, displayDate: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) + } + } else { + // min year and max year same, and not matched to min or max month. + // handing active dates - enable all days if no active dates. + enableAllDaysAndCheckActiveDates(with: surface, displayDate: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) + } + } else { + // min year and max year are different, and not matched to min or max year. + // handing active dates - enable all days if no active dates. + enableAllDaysAndCheckActiveDates(with: surface, displayDate: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) + } + } + + func addIndicator(with color: UIColor, surface: Surface, clearFullCircle: Bool, drawSemiCircle: Bool) { + // add indicator + let indicatorView: View = View().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.backgroundColor = .clear + $0.layer.borderWidth = 1.0 + } + + indicatorView + .pinLeading() + .pinTrailing() + .width(8) + .height(8) + .pinCenterY() + + stackView.addArrangedSubview(indicatorView) + + // update indicator + indicatorView.backgroundColor = drawSemiCircle ? .clear : (clearFullCircle ? .clear : color) + indicatorView.layer.borderColor = color.cgColor + + layoutIfNeeded() + + indicatorView.layer.cornerRadius = indicatorView.frame.size.height / 2.0 + + guard drawSemiCircle else { return } + + let center = CGPoint(x: indicatorView.frame.size.width/2, y: indicatorView.frame.size.height/2) + let path = UIBezierPath() + path.move(to: center) + path.addArc(withCenter: center, radius: center.x, startAngle: 2 * .pi, endAngle: .pi, clockwise: true) + path.close() + shapeLayer.path = path.cgPath + shapeLayer.fillColor = color.cgColor + + guard indicatorView.layer.sublayers?.contains(shapeLayer) ?? true else { return } + indicatorView.layer.addSublayer(shapeLayer) + } +} diff --git a/VDS/Components/Calendar/CalendarFooterReusableView.swift b/VDS/Components/Calendar/CalendarFooterReusableView.swift new file mode 100644 index 00000000..f8432bda --- /dev/null +++ b/VDS/Components/Calendar/CalendarFooterReusableView.swift @@ -0,0 +1,196 @@ +// +// CalendarFooterReusableView.swift +// VDS +// +// Created by Kanamarlapudi, Vasavi on 29/04/24. +// + +import UIKit +import VDSTokens + +/// Footer view to show indicators data. +class CalendarFooterReusableView: UICollectionReusableView { + + ///Identifier for the Calendar Footer Reusable View. + static let identifier: String = String(describing: CalendarFooterReusableView.self) + + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- + private var surface: Surface = .light + private var items: [CalendarBase.CalendarIndicatorModel] = [] + internal var containerSize: CGSize { CGSize(width: 304, height: 40) } + + internal var containerView = View().with { + $0.clipsToBounds = true + } + + private let flowLayout = UICollectionViewFlowLayout().with { + $0.estimatedItemSize = UICollectionViewFlowLayout.automaticSize + $0.minimumLineSpacing = VDSLayout.space1X + $0.minimumInteritemSpacing = VDSLayout.space4X + $0.scrollDirection = .vertical + } + open lazy var legendCollectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout).with { + $0.isScrollEnabled = false + $0.translatesAutoresizingMaskIntoConstraints = false + $0.showsVerticalScrollIndicator = false + $0.showsHorizontalScrollIndicator = false + $0.isAccessibilityElement = true + $0.backgroundColor = .clear + $0.delegate = self + $0.dataSource = self + $0.register(LegendCollectionViewCell.self, forCellWithReuseIdentifier: LegendCollectionViewCell.identifier) + } + + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + override init(frame: CGRect) { + super.init(frame: frame) + setUp() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setUp() + } + + //-------------------------------------------------- + // MARK: - Private Methods + //-------------------------------------------------- + /// Configuring the cell with default setup. + private func setUp() { + isAccessibilityElement = false + + addSubview(containerView) + containerView + .pinTopLessThanOrEqualTo(topAnchor, VDSLayout.space6X, .defaultLow) + .pinBottom() + .pinLeadingGreaterThanOrEqualTo(leadingAnchor, VDSLayout.space3X, .defaultHigh) + .pinTrailingLessThanOrEqualTo(trailingAnchor, VDSLayout.space3X, .defaultHigh) + .width(containerSize.width - (2*VDSLayout.space3X)) + .heightLessThanEqualTo(containerSize.height) + + // legend Collection View + containerView.addSubview(legendCollectionView) + legendCollectionView.pinTop().pinBottom().pinLeading().pinTrailing().pinCenterY().pinCenterX() + + } + + /// Updating UI to show legend with titles. + func update(with surface: Surface, indicators: [CalendarBase.CalendarIndicatorModel]) { + self.items = indicators + self.surface = surface + legendCollectionView.reloadData() + } + +} + +extension CalendarFooterReusableView: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { + + public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return items.count + } + + public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard collectionView == legendCollectionView, + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: LegendCollectionViewCell.identifier, for: indexPath) as? LegendCollectionViewCell, + indexPath.row <= items.count else { return UICollectionViewCell() } + let text = items[indexPath.row].label + cell.updateTitle(text: text, + color: VDSColor.elementsSecondaryOnlight, + surface: self.surface, + clearFullcircle: indexPath.row == 1, + drawSemiCircle: indexPath.row == 2) + return cell + } +} + +private class LegendCollectionViewCell: UICollectionViewCell { + + static let identifier: String = String(describing: LegendCollectionViewCell.self) + + private let textColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark) + + private let indicatorColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsSecondaryOnlight, VDSColor.elementsSecondaryOndark) + + private var title: Label = Label().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.textAlignment = .left + $0.numberOfLines = 1 + $0.textStyle = .bodySmall + $0.isAccessibilityElement = false + $0.backgroundColor = .clear + } + + private var legendIndicatorWrapper: View = View().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.backgroundColor = .clear + } + private var legendIndicator: View = View().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.backgroundColor = .clear + $0.layer.borderWidth = 1.0 + } + + private lazy var stackView = UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.distribution = .equalSpacing + $0.spacing = VDSLayout.space2X + $0.axis = .horizontal + $0.backgroundColor = .clear + } + + private lazy var shapeLayer = CAShapeLayer() + + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + override init(frame: CGRect) { + super.init(frame: frame) + setupCell() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setupCell() + } + + func setupCell() { + addSubview(stackView) + stackView.pinToSuperView() + + legendIndicatorWrapper.addSubview(legendIndicator) + legendIndicator.pinLeading().pinTrailing().width(8).height(8).pinCenterY() + + stackView.addArrangedSubview(legendIndicatorWrapper) + stackView.addArrangedSubview(title) + } + + func updateTitle(text: String, color: UIColor, surface: Surface, clearFullcircle: Bool, drawSemiCircle: Bool) { + title.surface = surface + title.text = text + title.textColor = textColorConfiguration.getColor(surface) + + legendIndicator.backgroundColor = drawSemiCircle ? .clear : (clearFullcircle ? .clear : color) + legendIndicator.layer.borderColor = indicatorColorConfiguration.getColor(surface).cgColor + + self.layoutIfNeeded() + + legendIndicator.layer.cornerRadius = legendIndicator.frame.size.height / 2.0 + + guard drawSemiCircle else { return } + + let center = CGPoint(x: legendIndicator.frame.size.width/2, y: legendIndicator.frame.size.height/2) + let path = UIBezierPath() + path.move(to: center) + path.addArc(withCenter: center, radius: center.x, startAngle: 2 * .pi, endAngle: .pi, clockwise: true) + path.close() + shapeLayer.path = path.cgPath + shapeLayer.fillColor = color.cgColor + + guard legendIndicator.layer.sublayers?.contains(shapeLayer) ?? true else { return } + legendIndicator.layer.addSublayer(shapeLayer) + } +} diff --git a/VDS/Components/Calendar/CalendarHeaderReusableView.swift b/VDS/Components/Calendar/CalendarHeaderReusableView.swift new file mode 100644 index 00000000..d0f7f247 --- /dev/null +++ b/VDS/Components/Calendar/CalendarHeaderReusableView.swift @@ -0,0 +1,241 @@ +// +// CalendarHeaderReusableView.swift +// VDS +// +// Created by Kanamarlapudi, Vasavi on 30/04/24. +// + +import UIKit +import VDSTokens + +/// Header view to display month and year along with days of week. +class CalendarHeaderReusableView: UICollectionReusableView { + + ///Identifier for the Calendar Header Reusable View. + static let identifier: String = String(describing: CalendarHeaderReusableView.self) + + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + /// A callback when the next button clicked. + public var nextClicked: (() -> (Void))? + + /// A callback when the previous button clicked. + public var previousClicked: (() -> (Void))? + + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- + internal var containerSize: CGSize { CGSize(width: 304, height: 88) } + + private lazy var stackView = UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.distribution = .fill + $0.spacing = VDSLayout.space1X + $0.axis = .vertical + $0.backgroundColor = .clear + } + + private lazy var topHeaderView = UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.distribution = .fill + $0.spacing = VDSLayout.space1X + $0.axis = .horizontal + $0.backgroundColor = .clear + } + + private lazy var daysCollectionView: UICollectionView = { + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) + collectionView.isScrollEnabled = false + collectionView.translatesAutoresizingMaskIntoConstraints = false + collectionView.delegate = self + collectionView.dataSource = self + collectionView.showsHorizontalScrollIndicator = false + collectionView.showsVerticalScrollIndicator = false + collectionView.backgroundColor = .clear + collectionView.register(collectionViewCell.self, forCellWithReuseIdentifier: collectionViewCell.identifier) + return collectionView + }() + + private var surface: Surface = .light + private var displayDate: Date = Date() + + internal var previousMonthView = View() + internal var nextMonthView = View() + let viewSize = 40.0 + + internal var previousButton = ButtonIcon().with { + $0.kind = .ghost + $0.iconName = .leftCaret + $0.iconOffset = .init(x: -2, y: 0) + $0.icon.size = .small + $0.size = .small + } + + internal var nextButton = ButtonIcon().with { + $0.kind = .ghost + $0.iconName = .rightCaret + $0.iconOffset = .init(x: 2, y: 0) + $0.icon.size = .small + $0.size = .small + } + + internal var headerTitle = Label().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.textAlignment = .center + $0.numberOfLines = 1 + $0.textStyle = .boldBodySmall + $0.backgroundColor = .clear + $0.isAccessibilityElement = false + } + + internal let daysOfWeek = Date.capitalizedFirstLettersOfWeekdays + internal let headerTitleTextColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark) + + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + override init(frame: CGRect) { + super.init(frame: frame) + setUp() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setUp() + } + + //-------------------------------------------------- + // MARK: - Private Methods + //-------------------------------------------------- + /// Configuring the cell with default setup. + private func setUp() { + isAccessibilityElement = false + + // stackview + addSubview(stackView) + stackView + .pinTop() + .pinBottom(VDSLayout.space1X) + .pinLeading() + .pinTrailing() + .height(containerSize.height - VDSLayout.space1X) + .width(containerSize.width) + stackView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() + + // top header stack view + stackView.addArrangedSubview(topHeaderView) + topHeaderView.heightAnchor.constraint(equalToConstant: viewSize).isActive = true + topHeaderView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() + + // previous button + topHeaderView.addArrangedSubview(previousMonthView) + previousMonthView.widthAnchor.constraint(equalToConstant: viewSize).activate() + previousMonthView.addSubview(previousButton) + previousButton.pinCenterY().pinCenterX() + previousButton.onClick = { _ in self.previousButtonClick() } + + // month year label + topHeaderView.addArrangedSubview(headerTitle) + + // next button + topHeaderView.addArrangedSubview(nextMonthView) + nextMonthView.widthAnchor.constraint(equalToConstant: viewSize).activate() + nextMonthView.addSubview(nextButton) + nextButton.pinCenterY().pinCenterX() + nextButton.onClick = { _ in self.nextButtonClick() } + + // days Collection View + stackView.addArrangedSubview(daysCollectionView) + topHeaderView.heightAnchor.constraint(equalToConstant: viewSize).isActive = true + daysCollectionView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() + } + + /// Updating UI based on next/previous clicks along with surface. + /// Updating UI to enable/disable the next & previous buttons, updating header title. + func update(with surface: Surface, date: Date, nextEnabled: Bool, previousEnabled: Bool) { + self.surface = surface + headerTitle.surface = surface + previousButton.surface = surface + nextButton.surface = surface + nextButton.isEnabled = nextEnabled + previousButton.isEnabled = previousEnabled + daysCollectionView.reloadData() + let labelText = date.getMonthName() + " \(date.yearInt)" + headerTitle.text = labelText + headerTitle.textColor = headerTitleTextColorConfiguration.getColor(surface) + } + + func nextButtonClick() { + nextClicked?() + } + + func previousButtonClick() { + previousClicked?() + } +} + +extension CalendarHeaderReusableView: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { + + public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return daysOfWeek.count + } + + public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: collectionViewCell.identifier, for: indexPath) as? collectionViewCell else { return UICollectionViewCell() } + cell.updateTitle(text: daysOfWeek[indexPath.row], surface: self.surface) + return cell + } + + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + return CGSize(width: viewSize, height: viewSize) + } + + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { + return VDSLayout.space1X + } + + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { + return VDSLayout.space1X + } +} + +private class collectionViewCell: UICollectionViewCell { + + static let identifier: String = String(describing: collectionViewCell.self) + + private let textColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsSecondaryOnlight, VDSColor.elementsSecondaryOndark) + + private var title = Label().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.textAlignment = .center + $0.numberOfLines = 1 + $0.textStyle = .bodySmall + $0.backgroundColor = .clear + $0.isAccessibilityElement = false + } + + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + override init(frame: CGRect) { + super.init(frame: frame) + setupCell() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setupCell() + } + + func setupCell() { + addSubview(title) + title.pinToSuperView() + } + + func updateTitle(text: String, surface: Surface) { + title.surface = surface + title.text = text + title.textColor = textColorConfiguration.getColor(surface) + } +} diff --git a/VDS/Components/Calendar/CalendarIndicatorModel.swift b/VDS/Components/Calendar/CalendarIndicatorModel.swift new file mode 100644 index 00000000..04ad9714 --- /dev/null +++ b/VDS/Components/Calendar/CalendarIndicatorModel.swift @@ -0,0 +1,25 @@ +// +// CalendarIndicatorModel.swift +// VDS +// +// Created by Kanamarlapudi, Vasavi on 26/04/24. +// + +import Foundation + +/// Custom data type for indicators prop +extension CalendarBase { + public struct CalendarIndicatorModel { + + /// Text that shown to an indicator for legend + public var label: String + + /// Date to an indicator + public var date: Date + + public init(label: String, date: Date) { + self.label = label + self.date = date + } + } +} diff --git a/VDS/Components/Calendar/Date+Extension.swift b/VDS/Components/Calendar/Date+Extension.swift new file mode 100644 index 00000000..25d8c9de --- /dev/null +++ b/VDS/Components/Calendar/Date+Extension.swift @@ -0,0 +1,107 @@ +// +// Date+Extension.swift +// VDS +// +// Created by Kanamarlapudi, Vasavi on 26/04/24. +// + +import Foundation + +public extension Date { + + static var firstDayOfWeek = Calendar.current.firstWeekday + + /// Capitalizes the first letter of the day of the week + static var capitalizedFirstLettersOfWeekdays: [String] { + let calendar = Calendar.current + let weekdays = calendar.shortWeekdaySymbols + + return weekdays.map { weekday in + guard let firstLetter = weekday.first else { return "" } + return String(firstLetter).capitalized + } + } + + var startOfMonth: Date { + Calendar.current.dateInterval(of: .month, for: self)!.start + } + + var endOfMonth: Date { + let lastDay = Calendar.current.dateInterval(of: .month, for: self)!.end + return Calendar.current.date(byAdding: .day, value: -1, to: lastDay)! + } + + /// Get the number of days of the month + var numberOfDaysInMonth: Int { + Calendar.current.range(of: .day, in: .month, for: self)!.count + } + + var firstWeekDayBeforeStart: Date { + let startOfMonthWeekday = Calendar.current.component(.weekday, from: startOfMonth) + var numberFromPreviousMonth = startOfMonthWeekday - Self.firstDayOfWeek + if numberFromPreviousMonth < 0 { + numberFromPreviousMonth += 7 // Adjust to a 0-6 range if negative + } + return Calendar.current.date(byAdding: .day, value: -numberFromPreviousMonth, to: startOfMonth)! + } + + /// Get the days of the month to display + var calendarDisplayDays: [Date] { + var days: [Date] = [] + // Start with days from the previous month to fill the grid + let firstDisplayDay = firstWeekDayBeforeStart + var day = firstDisplayDay + while day < startOfMonth { + days.append(day) + day = Calendar.current.date(byAdding: .day, value: 1, to: day)! + } + // Add days of the current month + for dayOffset in 0.. Bool { + let from = Calendar.current.date(byAdding: .day, value: -1, to: date1)! + let to = Calendar.current.date(byAdding: .day, value: 1, to: date2)! + return from.compare(self) == self.compare(to) + } + + /// Returns the month name of the given date + func getMonthName() -> String { + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale.current + dateFormatter.setLocalizedDateFormatFromTemplate("MMMM") + return dateFormatter.string(from: self) + } + + func getDay() -> String { + if #available(iOS 15.0, *) { + return formatted(.dateTime.day()) + } else { + // Fallback on earlier versions + let dateFormatter: DateFormatter = DateFormatter() + dateFormatter.dateFormat = "d" + let day: String = dateFormatter.string(from: self) + return day + } + } +} diff --git a/VDS/Components/DatePicker/DatePicker.swift b/VDS/Components/DatePicker/DatePicker.swift new file mode 100644 index 00000000..ff9fbb06 --- /dev/null +++ b/VDS/Components/DatePicker/DatePicker.swift @@ -0,0 +1,182 @@ +import Foundation +import UIKit +import VDSTokens +import Combine + +/// A dropdown select is an expandable menu of predefined options that allows a customer to make a single selection. +@objc(VDSDatePicker) +open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopoverPresentationControllerDelegate { + //-------------------------------------------------- + // 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: - Public Properties + //-------------------------------------------------- + /// A callback when the selected option changes. Passes parameters (option). + open var onDateSelected: ((Date, DatePicker) -> Void)? + + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- + internal var minWidthDefault = 186.0 + + internal var bottomStackView: UIStackView = { + return UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.axis = .vertical + $0.distribution = .fill + $0.alignment = .top + $0.spacing = VDSLayout.space2X + } + }() + + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + open var calendarIcon = Icon().with { + $0.name = .calendar + $0.size = .medium + } + + open var selectedDate: Date? { didSet { setNeedsUpdate() } } + + open var calendarModel: CalendarModel = .init() { didSet { setNeedsUpdate() } } + + open override var value: String? { + get { selectedDateLabel.text } + set { } + } + + open var selectedDateLabel = Label().with { + $0.setContentCompressionResistancePriority(.required, for: .vertical) + $0.textAlignment = .left + $0.textStyle = .bodyLarge + $0.lineBreakMode = .byCharWrapping + } + + public enum DateFormat: String, CaseIterable, CustomStringConvertible { + case shortNumeric + case longAlphabetic + case mediumNumeric + case consiseNumeric + + public var format: String { + switch self { + case .shortNumeric: "MM/dd/yy" + case .longAlphabetic: "MMMM d, yyyy" + case .mediumNumeric: "MM/dd/yyyy" + case .consiseNumeric: "M/d/yyyy" + } + } + + public var description: String { + return format + } + } + + open var dateFormat: DateFormat = .shortNumeric { didSet{ setNeedsUpdate() } } + + //-------------------------------------------------- + // MARK: - Configuration Properties + //-------------------------------------------------- + internal override var containerSize: CGSize { CGSize(width: minWidthDefault, height: 44) } + + //-------------------------------------------------- + // 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 = "Dropdown Select" + + // setting color config + selectedDateLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable() + + // tap gesture + fieldStackView + .publisher(for: UITapGestureRecognizer()) + .sink { [weak self] _ in + guard let self else { return } + if self.isEnabled && !self.isReadOnly { + self.togglePicker() + } + } + .store(in: &subscribers) + } + + open override func getFieldContainer() -> UIView { + // stackview for controls in EntryFieldBase.controlContainerView + let controlStackView = UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.axis = .horizontal + $0.spacing = VDSLayout.space3X + } + controlStackView.addArrangedSubview(calendarIcon) + controlStackView.addArrangedSubview(selectedDateLabel) + return controlStackView + } + + /// Used to make changes to the View based off a change events or from local properties. + open override func updateView() { + super.updateView() + + if let selectedDate { + formatDate(selectedDate) + } + + selectedDateLabel.surface = surface + selectedDateLabel.isEnabled = isEnabled + calendarIcon.color = iconColorConfiguration.getColor(self) + } + + /// Resets to default settings. + open override func reset() { + super.reset() + selectedDateLabel.textStyle = .bodyLarge + } + + internal func formatDate(_ date: Date) { + let formatter = DateFormatter() + formatter.dateFormat = dateFormat.format + selectedDateLabel.text = formatter.string(from: date) + } + + internal func togglePicker() { + let calendarVC = DatePickerViewController(calendarModel, delegate: self) + calendarVC.modalPresentationStyle = .popover + calendarVC.selectedDate = selectedDate ?? Date() + if let popoverController = calendarVC.popoverPresentationController { + popoverController.delegate = self + popoverController.sourceView = containerView + popoverController.sourceRect = containerView.bounds + popoverController.permittedArrowDirections = .up + } + if let viewController = UIApplication.topViewController() { + viewController.present(calendarVC, animated: true, completion: nil) + } + } + + internal func didSelectDate(_ controller: DatePickerViewController, date: Date) { + selectedDate = date + controller.dismiss(animated: true) + sendActions(for: .valueChanged) + } + + public func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle { + return .none + } +} diff --git a/VDS/Components/DatePicker/DatePickerCalendarModel.swift b/VDS/Components/DatePicker/DatePickerCalendarModel.swift new file mode 100644 index 00000000..ccceb5c9 --- /dev/null +++ b/VDS/Components/DatePicker/DatePickerCalendarModel.swift @@ -0,0 +1,56 @@ +// +// DatePicker-CalendarModel.swift +// VDS +// +// Created by Matt Bruce on 5/14/24. +// + +import Foundation +import UIKit + +extension DatePicker { + public struct CalendarModel { + public let surface: Surface + + /// If set to true, the calendar will not have a border. + public let hideContainerBorder: Bool + + /// If set to true, the calendar will not have current date indication. + public let hideCurrentDateIndicator: Bool + + /// Enable specific days. Pass an array of string value in date format e.g. ['07/21/2024', '07/24/2024', 07/28/2024']. + /// All other dates will be inactive. + public let activeDates: [Date] + + /// Disable specific days. Pass an array of string value in date format e.g. ['07/21/2024', '07/24/2024', 07/28/2024']. + /// All other dates will be active. + public let inactiveDates: [Date] + + /// If provided, the calendar will allow a selection to be made from this date forward. Defaults to today. + public let minDate: Date + + /// If provided, the calendar will allow a selection to be made up to this date. + public let maxDate: Date + + /// Array of ``CalendarIndicatorModel`` you are wanting to show on legend. + public let indicators: [CalendarBase.CalendarIndicatorModel] + + public init(surface: Surface = .light, + hideContainerBorder: Bool = false, + hideCurrentDateIndicator: Bool = false, + activeDates: [Date] = [], + inactiveDates: [Date] = [], + minDate: Date = Date().startOfMonth, + maxDate: Date = Date().endOfMonth, + indicators: [CalendarBase.CalendarIndicatorModel] = []) { + self.surface = surface + self.hideContainerBorder = hideContainerBorder + self.hideCurrentDateIndicator = hideCurrentDateIndicator + self.activeDates = activeDates + self.inactiveDates = inactiveDates + self.minDate = minDate + self.maxDate = maxDate + self.indicators = indicators + } + } +} diff --git a/VDS/Components/DatePicker/DatePickerChangeLog.txt b/VDS/Components/DatePicker/DatePickerChangeLog.txt new file mode 100644 index 00000000..e588b709 --- /dev/null +++ b/VDS/Components/DatePicker/DatePickerChangeLog.txt @@ -0,0 +1,48 @@ +MM/DD/YYYY +---------------- +Initial Brand 3.0 handoff + +01/06/2021 +---------------- +Removed Max Width, Increased Min width. +Updated the tokens with FormControl tokens + +02/24/2022 +---------------- +Replaced Calendar, Info, Error, Caret Left, and Caret Right Non-Scaling icons with VDS Icon. + +06/22/2022 +---------------- +Updated Calendar instances with new Calendar (with Indicators) + +07/27/2022 +---------------- +Updated Calendar Date Picker Configurations to include Background Transparent Boolean. +Moved Default Selection Date, Always opened and Date formats to configurartions page. + +08/04/2022 +---------------- +Updated default and inverted prop to light and dark surface. + +09/09/2022 +---------------- +Added missing helper text from disabled state visuals. + +12/13/2022 +---------------- +Updated supported date formats +Added "(web only)" to any instance of "keyboard focus" +Replaced form border and focus border pixel values, styles & spacing with tokens. + +01/20/2023 +---------------- +Updated Specs to use new SPEC Templates and SPEC DOC Components. +Tweaked written out date format so Mo becomes Mon + +04/12/2023 +---------------- +Updated hex colors for updated feedback tokens in error states. + +02/08/2024 +---------------- +Added Calendar position section to Behaviors. diff --git a/VDS/Components/DatePicker/DatePickerViewController.swift b/VDS/Components/DatePicker/DatePickerViewController.swift new file mode 100644 index 00000000..f8a6e2f0 --- /dev/null +++ b/VDS/Components/DatePicker/DatePickerViewController.swift @@ -0,0 +1,71 @@ +// +// DatePickerPopoverViewController.swift +// VDS +// +// Created by Matt Bruce on 5/14/24. +// + +import Foundation +import UIKit + +protocol DatePickerViewControllerDelegate: NSObject { + func didSelectDate(_ controller: DatePicker.DatePickerViewController, date: Date) +} + +extension DatePicker { + class DatePickerViewController: UIViewController { + private var padding: CGFloat = 15 + private var topPadding: CGFloat { 10 + padding } + private var calendarModel: CalendarModel + private let picker = CalendarBase() + weak var delegate: DatePickerViewControllerDelegate? + + init(_ calendarModel: CalendarModel, delegate: DatePickerViewControllerDelegate?) { + self.delegate = delegate + self.calendarModel = calendarModel + super.init(nibName: nil, bundle: nil) + self.picker.onChange = { [weak self] control in + guard let self else { return } + self.delegate?.didSelectDate(self, date: control.selectedDate) + } + } + + var selectedDate: Date = Date() { + didSet { + picker.selectedDate = selectedDate + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + view.addSubview(picker) + picker.surface = calendarModel.surface + picker.hideContainerBorder = calendarModel.hideContainerBorder + picker.hideCurrentDateIndicator = calendarModel.hideCurrentDateIndicator + picker.indicators = calendarModel.indicators + picker.activeDates = calendarModel.activeDates + picker.inactiveDates = calendarModel.inactiveDates + picker.selectedDate = selectedDate + picker.minDate = calendarModel.minDate + picker.maxDate = calendarModel.maxDate + picker.pinToSuperView(.init(top: topPadding, left: padding, bottom: padding, right: padding)) + view.backgroundColor = picker.backgroundColor + } + + override var preferredContentSize: CGSize { + get { + var size = picker.frame.size + size.height += 40 + size.width += 30 + return size + } + set { + super.preferredContentSize = newValue + } + } + } +} diff --git a/VDS/Components/DropdownSelect/DropdownSelect.swift b/VDS/Components/DropdownSelect/DropdownSelect.swift index bf1603a9..9c30ea03 100644 --- a/VDS/Components/DropdownSelect/DropdownSelect.swift +++ b/VDS/Components/DropdownSelect/DropdownSelect.swift @@ -71,7 +71,23 @@ open class DropdownSelect: EntryFieldBase { //-------------------------------------------------- internal var minWidthDefault = 66.0 internal var minWidthInlineLabel = 102.0 - internal var minWidth: CGFloat { showInlineLabel ? minWidthInlineLabel : minWidthDefault } + internal override var minWidth: CGFloat { showInlineLabel ? minWidthInlineLabel : minWidthDefault } + internal override var maxWidth: CGFloat { + let frameWidth = frame.size.width + return helperTextPlacement == .right ? (frameWidth - horizontalStackView.spacing) / 2 : frameWidth + } + + /// The is used for the for adding the helperLabel to the right of the containerView. + internal var horizontalStackView: UIStackView = { + return UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.axis = .horizontal + $0.distribution = .fillEqually + $0.spacing = VDSLayout.space3X + $0.alignment = .top + } + }() + //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- @@ -121,7 +137,8 @@ open class DropdownSelect: EntryFieldBase { /// 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() - + widthConstraint?.activate() + titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal) titleLabel.setContentHuggingPriority(.required, for: .horizontal) titleLabelWidthConstraint = titleLabel.width(constant: 0) @@ -190,14 +207,6 @@ open class DropdownSelect: EntryFieldBase { dropdownField.isUserInteractionEnabled = isReadOnly ? false : true selectedOptionLabel.surface = surface selectedOptionLabel.isEnabled = isEnabled - - //set the width constraints - let maxwidth = frame.size.width - if let width, width > minWidth && width < maxwidth { - widthConstraint?.constant = width - } else { - widthConstraint?.constant = maxwidth >= minWidth ? maxwidth : minWidth - } } /// Resets to default settings. @@ -280,6 +289,7 @@ open class DropdownSelect: EntryFieldBase { open override func updateHelperLabel(){ //remove first + secondaryStackView.removeFromSuperview() helperLabel.removeFromSuperview() super.updateHelperLabel() @@ -287,17 +297,18 @@ open class DropdownSelect: EntryFieldBase { //set the helper label position if helperText != nil { if helperTextPlacement == .right { - middleStackView.spacing = VDSLayout.space3X - middleStackView.distribution = .fillEqually - middleStackView.addArrangedSubview(helperLabel) + horizontalStackView.addArrangedSubview(secondaryStackView) + horizontalStackView.addArrangedSubview(helperLabel) + primaryStackView.addArrangedSubview(horizontalStackView) } else { - middleStackView.spacing = 0 - middleStackView.distribution = .fill bottomContainerStackView.addArrangedSubview(helperLabel) + primaryStackView.addArrangedSubview(secondaryStackView) } + } else { + primaryStackView.addArrangedSubview(secondaryStackView) } } - + open override func updateAccessibility() { super.updateAccessibility() let selectedOption = selectedOptionLabel.text ?? "" diff --git a/VDS/Components/Icon/IconName.swift b/VDS/Components/Icon/IconName.swift index 3a17f686..6af40d3d 100644 --- a/VDS/Components/Icon/IconName.swift +++ b/VDS/Components/Icon/IconName.swift @@ -48,12 +48,14 @@ extension Icon { internal static let paginationRightCaret = Name(name: "pagination-right-caret") internal static let verizonUp = Name(name: "verizon-up") internal static let warningBold = Name(name: "warning-bold") + public static let calendar = Name(name: "calendar") public static let checkmark = Name(name: "checkmark") public static let checkmarkAlt = Name(name: "checkmark-alt") public static let close = Name(name: "close") public static let downCaret = Name(name: "down-caret") public static let downCaretBold = Name(name: "down-caret-bold") public static let error = Name(name: "error") + public static let externalLink = Icon.Name(name: "external-link") public static let info = Name(name: "info") public static let multipleDocuments = Name(name: "multiple-documents") public static let leftArrow = Name(name: "left-arrow") @@ -61,5 +63,6 @@ extension Icon { public static let rightArrow = Name(name: "right-arrow") public static let rightCaret = Name(name: "right-caret") public static let warning = Name(name: "warning") + } } diff --git a/VDS/Components/TextFields/EntryFieldBase.swift b/VDS/Components/TextFields/EntryFieldBase.swift index f34a2259..c7cd766c 100644 --- a/VDS/Components/TextFields/EntryFieldBase.swift +++ b/VDS/Components/TextFields/EntryFieldBase.swift @@ -40,7 +40,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- - internal var stackView: UIStackView = { + internal var primaryStackView: UIStackView = { return UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .vertical @@ -48,22 +48,26 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { $0.alignment = .leading } }() - - internal var middleStackView: UIStackView = { - return UIStackView().with { - $0.translatesAutoresizingMaskIntoConstraints = false - $0.axis = .horizontal - $0.distribution = .fill - $0.alignment = .top - } - }() + /// This is the veritcal stack view that has 2 rows, the containerView and the return view + /// of the getBottomContainer() method, by default returns the bottomContainerStackView. + internal let secondaryStackView = UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.axis = .vertical + $0.distribution = .fill + } + + /// This is the view that will be wrapped with the border for userInteraction. + /// The only subview of this view is the fieldStackView internal var containerView: UIView = { return UIView().with { $0.translatesAutoresizingMaskIntoConstraints = false } }() + /// This is a horizontal Stack View that is placed inside the containterView (bordered view) + /// The first arrangedView will be the view from getFieldContainer() + /// The second view is the statusIcon. internal var fieldStackView: UIStackView = { return UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false @@ -73,6 +77,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { } }() + /// This is a vertical stack used for the errorLabel and helperLabel. internal var bottomContainerStackView: UIStackView = { return UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false @@ -88,7 +93,9 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { // MARK: - Configuration Properties //-------------------------------------------------- // Sizes are from InVision design specs. - internal var containerSize: CGSize { CGSize(width: 45, height: 44) } + internal var maxWidth: CGFloat { frame.size.width } + internal var minWidth: CGFloat { containerSize.width } + internal var containerSize: CGSize { CGSize(width: minWidth, height: 44) } internal let primaryColorConfiguration = ViewColorConfiguration().with { $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forDisabled: true) @@ -207,22 +214,14 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { super.setup() isAccessibilityElement = false - addSubview(stackView) + addSubview(primaryStackView) //create the wrapping view heightConstraint = containerView.heightGreaterThanEqualTo(constant: containerSize.height) - widthConstraint = containerView.width(constant: 0) + widthConstraint = containerView.width(constant: frame.size.width) - let leftStackView = UIStackView().with { - $0.translatesAutoresizingMaskIntoConstraints = false - $0.axis = .vertical - $0.distribution = .fill - } - leftStackView.addArrangedSubview(containerView) - leftStackView.setCustomSpacing(8, after: containerView) - - //add the containerView to the middleStack - middleStackView.addArrangedSubview(leftStackView) + secondaryStackView.addArrangedSubview(containerView) + secondaryStackView.setCustomSpacing(8, after: containerView) //add ContainerStackView //this is the horizontal stack that contains @@ -246,13 +245,13 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { bottomContainerStackView.addArrangedSubview(errorLabel) bottomContainerStackView.addArrangedSubview(helperLabel) - stackView.addArrangedSubview(titleLabel) - stackView.addArrangedSubview(middleStackView) - leftStackView.addArrangedSubview(bottomContainer) + primaryStackView.addArrangedSubview(titleLabel) + primaryStackView.addArrangedSubview(secondaryStackView) + secondaryStackView.addArrangedSubview(bottomContainer) - stackView.setCustomSpacing(4, after: titleLabel) + primaryStackView.setCustomSpacing(4, after: titleLabel) - stackView + primaryStackView .pinTop() .pinLeading() .pinTrailing(0, .defaultHigh) @@ -295,6 +294,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { updateTitleLabel() updateErrorLabel() updateHelperLabel() + updateContainerWidth() } open func validate(){ @@ -317,6 +317,15 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { //-------------------------------------------------- // MARK: - Public Methods //-------------------------------------------------- + open func updateContainerWidth() { + if let width, width > minWidth && width < maxWidth { + widthConstraint?.constant = width + } else { + widthConstraint?.constant = maxWidth >= minWidth ? maxWidth : minWidth + } + widthConstraint?.activate() + } + /// Container for the area in which the user interacts. open func getFieldContainer() -> UIView { fatalError("Subclass must return the view that contains the field/view the user will interact with.") diff --git a/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift b/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift index db45d767..ebecb17d 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift @@ -80,14 +80,6 @@ extension InputField { inputField.fieldStackView.setCustomSpacing(0, after: inputField.statusIcon) } - //set the width constraints - let maxwidth = inputField.frame.size.width - if let width = inputField.width, width > minWidth && width < maxwidth { - inputField.widthConstraint?.constant = width - } else { - inputField.widthConstraint?.constant = maxwidth >= minWidth ? maxwidth : minWidth - } - //placeholder inputField.textField.placeholder = placeholderText diff --git a/VDS/Components/TextFields/InputField/InputField.swift b/VDS/Components/TextFields/InputField/InputField.swift index e11d1253..1886a9ab 100644 --- a/VDS/Components/TextFields/InputField/InputField.swift +++ b/VDS/Components/TextFields/InputField/InputField.swift @@ -35,6 +35,22 @@ open class InputField: EntryFieldBase { // MARK: - Private Properties //-------------------------------------------------- internal var titleLabelWidthConstraint: NSLayoutConstraint? + internal override var minWidth: CGFloat { fieldType.handler().minWidth } + internal override var maxWidth: CGFloat { + let frameWidth = frame.size.width + return helperTextPlacement == .right ? (frameWidth - horizontalStackView.spacing) / 2 : frameWidth + } + + /// The is used for the for adding the helperLabel to the right of the containerView. + internal var horizontalStackView: UIStackView = { + return UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.axis = .horizontal + $0.distribution = .fillEqually + $0.spacing = VDSLayout.space3X + $0.alignment = .top + } + }() //-------------------------------------------------- // MARK: - Public FieldType Properties @@ -173,8 +189,8 @@ open class InputField: EntryFieldBase { textField.heightAnchor.constraint(equalToConstant: 20).isActive = true textField.delegate = self - stackView.addArrangedSubview(successLabel) - stackView.setCustomSpacing(8, after: successLabel) + primaryStackView.addArrangedSubview(successLabel) + primaryStackView.setCustomSpacing(8, after: successLabel) fieldStackView.addArrangedSubview(actionTextLink) @@ -248,6 +264,7 @@ open class InputField: EntryFieldBase { } open override func updateHelperLabel(){ //remove first + secondaryStackView.removeFromSuperview() helperLabel.removeFromSuperview() super.updateHelperLabel() @@ -255,14 +272,15 @@ open class InputField: EntryFieldBase { //set the helper label position if helperText != nil { if helperTextPlacement == .right { - middleStackView.spacing = VDSLayout.space3X - middleStackView.distribution = .fillEqually - middleStackView.addArrangedSubview(helperLabel) + horizontalStackView.addArrangedSubview(secondaryStackView) + horizontalStackView.addArrangedSubview(helperLabel) + primaryStackView.addArrangedSubview(horizontalStackView) } else { - middleStackView.spacing = 0 - middleStackView.distribution = .fill bottomContainerStackView.addArrangedSubview(helperLabel) + primaryStackView.addArrangedSubview(secondaryStackView) } + } else { + primaryStackView.addArrangedSubview(secondaryStackView) } } diff --git a/VDS/Components/TextFields/TextArea/TextArea.swift b/VDS/Components/TextFields/TextArea/TextArea.swift index 3913f2e6..eccd228e 100644 --- a/VDS/Components/TextFields/TextArea/TextArea.swift +++ b/VDS/Components/TextFields/TextArea/TextArea.swift @@ -187,10 +187,6 @@ open class TextArea: EntryFieldBase { textView.isEnabled = isEnabled textView.surface = surface - //set the width constraints - let maxwidth = frame.size.width - let minWidth = containerSize.width - widthConstraint?.constant = maxwidth >= minWidth ? maxwidth : minWidth textViewHeightConstraint?.constant = minHeight.value characterCounterLabel.text = getCharacterCounterText() diff --git a/VDS/Components/TileContainer/TileContainer.swift b/VDS/Components/TileContainer/TileContainer.swift index 08b87a7f..ffd16496 100644 --- a/VDS/Components/TileContainer/TileContainer.swift +++ b/VDS/Components/TileContainer/TileContainer.swift @@ -187,7 +187,7 @@ open class TileContainerBase: Control where Padding //-------------------------------------------------- // MARK: - Configuration //-------------------------------------------------- - private let cornerRadius = VDSFormControls.borderRadius * 2 + private let cornerRadius = VDSLayout.shapeCornerradiusTiles internal var backgroundColorConfiguration = BackgroundColorConfiguration() private let dropShadowConfiguration = DropShadowConfiguration().with { $0.shadowColorConfiguration = SurfaceColorConfiguration().with { diff --git a/VDS/Components/Tilelet/Tilelet.swift b/VDS/Components/Tilelet/Tilelet.swift index f0814e11..98ce49aa 100644 --- a/VDS/Components/Tilelet/Tilelet.swift +++ b/VDS/Components/Tilelet/Tilelet.swift @@ -532,6 +532,7 @@ open class Tilelet: TileContainerBase { var showIconContainerView = false if let descriptiveIconModel { descriptiveIcon.name = descriptiveIconModel.name + descriptiveIcon.color = descriptiveIconModel.color descriptiveIcon.size = descriptiveIconModel.size descriptiveIcon.surface = backgroundColorSurface descriptiveIcon.accessibilityLabel = descriptiveIconModel.accessibleText @@ -539,9 +540,11 @@ open class Tilelet: TileContainerBase { } if let directionalIconModel { + directionalIcon.name = directionalIconModel.iconType.iconName + directionalIcon.color = directionalIconModel.color directionalIcon.size = directionalIconModel.size directionalIcon.surface = backgroundColorSurface - directionalIcon.accessibilityLabel = "Right arrow" + directionalIcon.accessibilityLabel = directionalIconModel.accessibleText showIconContainerView = true } diff --git a/VDS/Components/Tilelet/TileletIconModels.swift b/VDS/Components/Tilelet/TileletIconModels.swift index 457cd907..788a2155 100644 --- a/VDS/Components/Tilelet/TileletIconModels.swift +++ b/VDS/Components/Tilelet/TileletIconModels.swift @@ -7,6 +7,7 @@ import Foundation import UIKit +import VDSTokens extension Tilelet { @@ -15,17 +16,21 @@ extension Tilelet { /// A representation that will be used to render the icon with corresponding name. public var name: Icon.Name + /// Color of the icon. + public var color: UIColor + /// Enum for a preset height and width for the icon. public var size: Icon.Size /// Accessible Text for the Icon - var accessibleText: String + public var accessibleText: String /// Current Surface and this is used to pass down to child objects that implement Surfacable public var surface: Surface - public init(name: Icon.Name = .multipleDocuments, size: Icon.Size = .medium, accessibleText: String? = nil, surface: Surface = .dark) { + public init(name: Icon.Name = .multipleDocuments, color: UIColor = VDSColor.paletteBlack, size: Icon.Size = .medium, accessibleText: String? = nil, surface: Surface = .dark) { self.name = name + self.color = color self.accessibleText = accessibleText ?? name.rawValue self.size = size self.surface = surface @@ -34,13 +39,34 @@ extension Tilelet { /// Model that represents the options available for the directional icon. public struct DirectionalIcon { + public enum IconType { + case rightArrow + case externalLink + + public var iconName: Icon.Name { + return self == .rightArrow ? .rightArrow : .externalLink + } + } + + /// Color of the icon. + public var color: UIColor + + /// Accessible Text for the Icon + public var accessibleText: String + + /// Enum for a icon type you want shown.. + public var iconType: IconType + /// Enum for a preset height and width for the icon. public var size: Icon.Size - + /// Current Surface and this is used to pass down to child objects that implement Surfacable public var surface: Surface - public init(size: Icon.Size = .medium, surface: Surface = .dark) { + public init(iconType: IconType = .rightArrow, color: UIColor = VDSColor.paletteBlack, size: Icon.Size = .medium, accessibleText: String? = nil, surface: Surface = .dark) { + self.iconType = iconType + self.color = color + self.accessibleText = accessibleText ?? iconType.iconName.rawValue self.size = size self.surface = surface } diff --git a/VDS/SupportingFiles/Icons.xcassets/Restricted/external-link.imageset/Contents.json b/VDS/SupportingFiles/Icons.xcassets/Restricted/external-link.imageset/Contents.json new file mode 100644 index 00000000..1e10c5e2 --- /dev/null +++ b/VDS/SupportingFiles/Icons.xcassets/Restricted/external-link.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "external-link.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/VDS/SupportingFiles/Icons.xcassets/Restricted/external-link.imageset/external-link.svg b/VDS/SupportingFiles/Icons.xcassets/Restricted/external-link.imageset/external-link.svg new file mode 100644 index 00000000..5c545b6e --- /dev/null +++ b/VDS/SupportingFiles/Icons.xcassets/Restricted/external-link.imageset/external-link.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/VDS/SupportingFiles/ReleaseNotes.txt b/VDS/SupportingFiles/ReleaseNotes.txt index 39447d3e..461efee9 100644 --- a/VDS/SupportingFiles/ReleaseNotes.txt +++ b/VDS/SupportingFiles/ReleaseNotes.txt @@ -1,7 +1,12 @@ 1.0.63 ---------------- - CXTDT-555860 - Dropdown Select - Form Elements layout option missing - +- ONEAPP-7963 - InputField - Finished Development +- ONEAPP-7958 - Calendar - Finished Development +- ONEAPP-7010 - DatePicker - Finished Development +- CXTDT-555860 - Dropdown Select - Form Elements layout option missing +- CXTDT-557216 - BadgeIndicator - Accessibility +- CXTDT-555854 - Dropdown Select - spacing issues 1.0.62 ---------------- diff --git a/VDS/VDS.docc/VDS.md b/VDS/VDS.docc/VDS.md index 607e09f5..d145ea0a 100755 --- a/VDS/VDS.docc/VDS.md +++ b/VDS/VDS.docc/VDS.md @@ -25,6 +25,7 @@ Using the system allows designers and developers to collaborate more easily and - ``Button`` - ``ButtonIcon`` - ``ButtonGroup`` +- ``CalendarBase`` - ``CarouselScrollbar`` - ``Checkbox`` - ``CheckboxItem``