diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index a51fe91e..2d2ab447 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 */; }; @@ -141,6 +148,19 @@ EAB5FF0129424ACB00998C17 /* UIControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB5FF0029424ACB00998C17 /* UIControl.swift */; }; EABFEB642A26473700C4C106 /* NSAttributedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = EABFEB632A26473700C4C106 /* NSAttributedString.swift */; }; EAC58BFD2BE935C300BA39FA /* TitleLockupTextColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58BFC2BE935C300BA39FA /* TitleLockupTextColor.swift */; }; + EAC58C062BED000200BA39FA /* CreditCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C052BED000200BA39FA /* CreditCard.swift */; }; + EAC58C082BED002D00BA39FA /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C072BED002D00BA39FA /* Date.swift */; }; + EAC58C0A2BED004E00BA39FA /* FieldType.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C092BED004E00BA39FA /* FieldType.swift */; }; + EAC58C0C2BED01D500BA39FA /* Telephone.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C0B2BED01D500BA39FA /* Telephone.swift */; }; + EAC58C0E2BED021600BA39FA /* Password.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C0D2BED021600BA39FA /* Password.swift */; }; + EAC58C122BED0DDD00BA39FA /* Text.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C112BED0DDD00BA39FA /* Text.swift */; }; + 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 */; }; @@ -204,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 = ""; }; @@ -338,6 +365,19 @@ EAB5FF0029424ACB00998C17 /* UIControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIControl.swift; sourceTree = ""; }; EABFEB632A26473700C4C106 /* NSAttributedString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSAttributedString.swift; sourceTree = ""; }; EAC58BFC2BE935C300BA39FA /* TitleLockupTextColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleLockupTextColor.swift; sourceTree = ""; }; + EAC58C052BED000200BA39FA /* CreditCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreditCard.swift; sourceTree = ""; }; + EAC58C072BED002D00BA39FA /* Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = ""; }; + EAC58C092BED004E00BA39FA /* FieldType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldType.swift; sourceTree = ""; }; + EAC58C0B2BED01D500BA39FA /* Telephone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Telephone.swift; sourceTree = ""; }; + EAC58C0D2BED021600BA39FA /* Password.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Password.swift; sourceTree = ""; }; + EAC58C112BED0DDD00BA39FA /* Text.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Text.swift; sourceTree = ""; }; + 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 = ""; }; @@ -426,6 +466,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 = ( @@ -594,8 +648,10 @@ EAD062AE2A3B87210015965D /* BadgeIndicator */, 18A65A002B96E7E1006602CC /* Breadcrumbs */, EA0FC2BE2912D18200DF80B4 /* Buttons */, + 18A3F1202BD8F5DE00498E4A /* Calendar */, 1808BEBA2BA41B1D00129230 /* CarouselScrollbar */, EAF7F092289985E200B287F5 /* Checkbox */, + EAC58C1F2BF127F000BA39FA /* DatePicker */, 186D13C92BBA8A3500986B53 /* DropdownSelect */, EA985BF3296C609E00F2FF2E /* Icon */, EA3362412892EF700071C351 /* Label */, @@ -877,6 +933,33 @@ path = Tooltip; sourceTree = ""; }; + EAC58C042BECFFEA00BA39FA /* FieldTypes */ = { + isa = PBXGroup; + children = ( + EAC58C092BED004E00BA39FA /* FieldType.swift */, + EAC58C052BED000200BA39FA /* CreditCard.swift */, + EAC58C072BED002D00BA39FA /* Date.swift */, + EAC58C152BED0E0300BA39FA /* InlineAction.swift */, + EAC58C132BED0DEC00BA39FA /* Number.swift */, + EAC58C0D2BED021600BA39FA /* Password.swift */, + EAC58C172BED0E2300BA39FA /* SecurityCode.swift */, + EAC58C0B2BED01D500BA39FA /* Telephone.swift */, + EAC58C112BED0DDD00BA39FA /* Text.swift */, + ); + 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 = ( @@ -909,6 +992,7 @@ EAC925862911C9DE00091998 /* InputField */ = { isa = PBXGroup; children = ( + EAC58C042BECFFEA00BA39FA /* FieldTypes */, EAC925872911C9DE00091998 /* InputField.swift */, EA2DC9B32BE2C6FE004F58C5 /* TextField.swift */, EA6642942BCEBF9500D81DC4 /* TextLinkModel.swift */, @@ -1090,8 +1174,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 */, @@ -1118,12 +1204,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 */, @@ -1131,6 +1219,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 */, @@ -1144,6 +1233,7 @@ EA33622E2891EA3C0071C351 /* DispatchQueue+Once.swift in Sources */, EA4DB2FD28D3D0CA00103EE3 /* AnyEquatable.swift in Sources */, EA5E305A29510F8B0082B959 /* EnumSubset.swift in Sources */, + EAC58C082BED002D00BA39FA /* Date.swift in Sources */, EA985BF7296C665E00F2FF2E /* IconName.swift in Sources */, EA8141102A127066004F60D2 /* UIColor+VDSColor.swift in Sources */, EAF7F0AF289B144C00B287F5 /* UnderlineLabelAttribute.swift in Sources */, @@ -1197,12 +1287,16 @@ EA0D1C372A681CCE00E5C127 /* ToggleView.swift in Sources */, EAF7F0B9289C139800B287F5 /* ColorConfiguration.swift in Sources */, 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 */, + EAC58C162BED0E0300BA39FA /* InlineAction.swift in Sources */, EA0D1C3D2A6AD57600E5C127 /* Typography+Enums.swift in Sources */, EAF1FE9B29DB1A6000101452 /* Changeable.swift in Sources */, + EAC58C0C2BED01D500BA39FA /* Telephone.swift in Sources */, EAF7F0A2289AFB3900B287F5 /* Errorable.swift in Sources */, EA8E40912A7D3F6300934ED3 /* UIView+Accessibility.swift in Sources */, EA6F330E2B911E9000BACAB9 /* TextView.swift in Sources */, @@ -1216,6 +1310,7 @@ 44604AD729CE196600E62B51 /* Line.swift in Sources */, 1808BEBC2BA41C3200129230 /* CarouselScrollbar.swift in Sources */, EAF978212A99035B00C2FEA9 /* Enabling.swift in Sources */, + EAC58C062BED000200BA39FA /* CreditCard.swift in Sources */, EA5E3058295105A40082B959 /* Tilelet.swift in Sources */, 186D13CB2BBA8B1500986B53 /* DropdownSelect.swift in Sources */, EA89201528B56CF4006B9984 /* RadioBoxGroup.swift in Sources */, @@ -1236,11 +1331,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 */, @@ -1248,15 +1348,19 @@ EA985C692971B90B00F2FF2E /* IconSize.swift in Sources */, 71FC86E02B973AE500700965 /* DropShadowConfiguration.swift in Sources */, EA3362302891EB4A0071C351 /* Font.swift in Sources */, + EAC58C0E2BED021600BA39FA /* Password.swift in Sources */, EAF7F0AD289B142900B287F5 /* StrikeThroughLabelAttribute.swift in Sources */, EAB5FEF12927F4AA00998C17 /* SelfSizingCollectionView.swift in Sources */, EA3361B8288B2AAA0071C351 /* ViewProtocol.swift in Sources */, EA3361A8288B23300071C351 /* UIColor.swift in Sources */, EA2DC9B42BE2C6FE004F58C5 /* TextField.swift in Sources */, + EAC58C182BED0E2300BA39FA /* SecurityCode.swift in Sources */, EAC9257D29119B5400091998 /* TextLink.swift in Sources */, + EAC58C142BED0DEC00BA39FA /* Number.swift in Sources */, 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 */, ); @@ -1407,7 +1511,7 @@ BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 61; + CURRENT_PROJECT_VERSION = 63; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1444,7 +1548,7 @@ BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 61; + 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 ebe92725..740538d1 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..fd3e3452 --- /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: 336) } + 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: indicators.count > 0 ? footerHeight : 0) + } + + 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..9547084f --- /dev/null +++ b/VDS/Components/Calendar/CalendarFooterReusableView.swift @@ -0,0 +1,257 @@ +// +// 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 indicatorWidth = 8.0 + + var textLabel: Label = Label().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.textAlignment = .left + $0.textStyle = .bodySmall + $0.numberOfLines = 1 + } + + internal var containerView = View().with { + $0.clipsToBounds = true + } + + private let flowLayout = LeftAlignedCollectionViewFlowLayout().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) + } + + private var topConstraint: NSLayoutConstraint? + + //-------------------------------------------------- + // 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.pinToSuperView() + + // legend Collection View + containerView.addSubview(legendCollectionView) + legendCollectionView + .pinTopLessThanOrEqualTo(topAnchor, VDSLayout.space6X, .defaultLow) + .pinBottom() + .pinLeading(VDSLayout.space3X) + .pinTrailing(VDSLayout.space3X) + .width(containerSize.width - (2 * VDSLayout.space3X)) + .heightGreaterThanEqualTo(16) + } + + /// Updating UI to show legend with titles. + func update(with surface: Surface, indicators: [CalendarBase.CalendarIndicatorModel]) { + self.items = indicators + self.surface = surface + legendCollectionView.reloadData() + + var height = legendCollectionView.collectionViewLayout.collectionViewContentSize.height + if height > 0 { + topConstraint?.isActive = false + height = height > containerSize.height ? containerSize.height : height + let top = containerSize.height - height + topConstraint = legendCollectionView.topAnchor.constraint(equalTo: topAnchor, constant: top) + topConstraint?.isActive = true + } + } + +} + +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 + } + + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + textLabel.text = items[indexPath.row].label + let intrinsicSize = textLabel.intrinsicContentSize + let cellwidth = intrinsicSize.width + indicatorWidth + VDSLayout.space2X + return .init(width: min(cellwidth, collectionView.frame.width), height: intrinsicSize.height) + } + +} + +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.alignment = .fill + $0.distribution = .fill + $0.spacing = VDSLayout.space2X + $0.axis = .horizontal + $0.backgroundColor = .clear + } + + private lazy var shapeLayer = CAShapeLayer() + + internal var indicatorSize: CGSize { CGSize(width: 8, height: 8) } + + //-------------------------------------------------- + // 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() + } + + func updateView() { + stackView.arrangedSubviews.forEach { $0.removeFromSuperview() } + legendIndicator.layer.sublayers?.forEach { $0.removeFromSuperlayer() } + + legendIndicatorWrapper.addSubview(legendIndicator) + legendIndicator.pinLeading().pinTrailing().width(indicatorSize.width).height(indicatorSize.height).pinCenterY() + + stackView.addArrangedSubview(legendIndicatorWrapper) + stackView.addArrangedSubview(title) + } + + func updateTitle(text: String, color: UIColor, surface: Surface, clearFullcircle: Bool, drawSemiCircle: Bool) { + updateView() + 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) + } +} + +class LeftAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout { + + override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { + let attributes = super.layoutAttributesForElements(in: rect) + + var leftMargin = sectionInset.left + var maxY: CGFloat = -1.0 + attributes?.forEach { layoutAttribute in + if layoutAttribute.frame.origin.y >= maxY { + leftMargin = sectionInset.left + } + + layoutAttribute.frame.origin.x = leftMargin + + leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing + maxY = max(layoutAttribute.frame.maxY , maxY) + } + + return attributes + } +} 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 28c019f5..57dbaa5b 100644 --- a/VDS/Components/DropdownSelect/DropdownSelect.swift +++ b/VDS/Components/DropdownSelect/DropdownSelect.swift @@ -31,6 +31,18 @@ open class DropdownSelect: EntryFieldBase { //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- + /// Override UIControl state to add the .error state if showSuccess is true and if showError is true. + open override var state: UIControl.State { + get { + var state = super.state + if dropdownField.isFirstResponder { + state.insert(.focused) + } + + return state + } + } + /// If true, the label will be displayed inside the dropdown containerView. Otherwise, the label will be above the dropdown containerView like a normal text input. open var showInlineLabel: Bool = false { didSet { setNeedsUpdate() }} @@ -53,17 +65,35 @@ open class DropdownSelect: EntryFieldBase { /// A callback when the selected option changes. Passes parameters (option). open var onItemSelected: ((Int, DropdownOptionModel) -> Void)? - + //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- internal var minWidthDefault = 66.0 internal var minWidthInlineLabel = 102.0 + 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 //-------------------------------------------------- open var inlineDisplayLabel = Label().with { + $0.setContentCompressionResistancePriority(.required, for: .vertical) + $0.setContentCompressionResistancePriority(.required, for: .horizontal) $0.textAlignment = .left $0.textStyle = .boldBodyLarge $0.lineBreakMode = .byCharWrapping @@ -72,6 +102,7 @@ open class DropdownSelect: EntryFieldBase { open var selectedOptionLabel = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) + $0.setContentCompressionResistancePriority(.required, for: .horizontal) $0.textAlignment = .left $0.textStyle = .bodyLarge $0.lineBreakMode = .byCharWrapping @@ -83,17 +114,21 @@ open class DropdownSelect: EntryFieldBase { $0.font = TextStyle.bodyLarge.font } + /// Determines the placement of the helper text. + open var helperTextPlacement: HelperTextPlacement = .bottom { didSet { setNeedsUpdate() } } + open var optionsPicker = UIPickerView() //-------------------------------------------------- // MARK: - Constraints //-------------------------------------------------- internal var inlineWidthConstraint: NSLayoutConstraint? + internal var titleLabelWidthConstraint: NSLayoutConstraint? //-------------------------------------------------- // MARK: - Configuration Properties //-------------------------------------------------- - internal override var containerSize: CGSize { CGSize(width: showInlineLabel ? minWidthInlineLabel : width ?? minWidthDefault, height: 44) } + internal override var containerSize: CGSize { .init(width: minWidthDefault, height: 44) } //-------------------------------------------------- // MARK: - Overrides @@ -101,28 +136,17 @@ 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() + super.setup() + widthConstraint?.activate() - // stackview for controls in EntryFieldBase.controlContainerView - let controlStackView = UIStackView().with { - $0.translatesAutoresizingMaskIntoConstraints = false - $0.axis = .horizontal - $0.spacing = VDSFormControls.spaceInset - } - controlContainerView.addSubview(controlStackView) - controlStackView.pinToSuperView() - - controlStackView.addArrangedSubview(dropdownField) - controlStackView.addArrangedSubview(inlineDisplayLabel) - controlStackView.addArrangedSubview(selectedOptionLabel) - - containerStackView.isAccessibilityElement = true - containerStackView.accessibilityLabel = "Dropdown Select" + titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal) + titleLabel.setContentHuggingPriority(.required, for: .horizontal) + titleLabelWidthConstraint = titleLabel.width(constant: 0) + + fieldStackView.isAccessibilityElement = true + fieldStackView.accessibilityLabel = "Dropdown Select" inlineDisplayLabel.isAccessibilityElement = true - controlStackView.setCustomSpacing(0, after: dropdownField) - controlStackView.setCustomSpacing(VDSLayout.space1X, after: inlineDisplayLabel) - controlStackView.setCustomSpacing(VDSLayout.space3X, after: selectedOptionLabel) dropdownField.width(0) inlineWidthConstraint = inlineDisplayLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 0) inlineWidthConstraint?.isActive = true @@ -150,7 +174,7 @@ open class DropdownSelect: EntryFieldBase { }() // tap gesture - containerStackView + fieldStackView .publisher(for: UITapGestureRecognizer()) .sink { [weak self] _ in self?.launchPicker() @@ -158,6 +182,22 @@ open class DropdownSelect: EntryFieldBase { .store(in: &subscribers) } + open override func getFieldContainer() -> UIView { + let controlStackView = UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.axis = .horizontal + $0.distribution = .fill + $0.spacing = VDSFormControls.spaceInset + } + controlStackView.addArrangedSubview(dropdownField) + controlStackView.addArrangedSubview(inlineDisplayLabel) + controlStackView.addArrangedSubview(selectedOptionLabel) + controlStackView.setCustomSpacing(0, after: dropdownField) + controlStackView.setCustomSpacing(VDSLayout.space1X, after: inlineDisplayLabel) + controlStackView.setCustomSpacing(VDSLayout.space3X, after: selectedOptionLabel) + return controlStackView + } + /// Used to make changes to the View based off a change events or from local properties. open override func updateView() { super.updateView() @@ -247,17 +287,39 @@ open class DropdownSelect: EntryFieldBase { statusIcon.color = iconColorConfiguration.getColor(self) } + open override func updateHelperLabel(){ + //remove first + secondaryStackView.removeFromSuperview() + helperLabel.removeFromSuperview() + + super.updateHelperLabel() + + //set the helper label position + if helperText != nil { + if helperTextPlacement == .right { + horizontalStackView.addArrangedSubview(secondaryStackView) + horizontalStackView.addArrangedSubview(helperLabel) + primaryStackView.addArrangedSubview(horizontalStackView) + } else { + bottomContainerStackView.addArrangedSubview(helperLabel) + primaryStackView.addArrangedSubview(secondaryStackView) + } + } else { + primaryStackView.addArrangedSubview(secondaryStackView) + } + } + open override func updateAccessibility() { super.updateAccessibility() - var selectedOption = selectedOptionLabel.text ?? "" - containerStackView.accessibilityLabel = "Dropdown Select, \(selectedOption) \(isReadOnly ? ", read only" : "")" - containerStackView.accessibilityHint = isReadOnly || !isEnabled ? "" : "Double tap to open." + let selectedOption = selectedOptionLabel.text ?? "" + fieldStackView.accessibilityLabel = "Dropdown Select, \(selectedOption) \(isReadOnly ? ", read only" : "")" + fieldStackView.accessibilityHint = isReadOnly || !isEnabled ? "" : "Double tap to open." } open override var accessibilityElements: [Any]? { get { var elements = [Any]() - elements.append(contentsOf: [titleLabel, containerStackView]) + elements.append(contentsOf: [titleLabel, fieldStackView]) if showError { elements.append(statusIcon) @@ -281,7 +343,22 @@ open class DropdownSelect: EntryFieldBase { optionsPicker.isHidden = true dropdownField.resignFirstResponder() setNeedsUpdate() - UIAccessibility.post(notification: .layoutChanged, argument: containerStackView) + UIAccessibility.post(notification: .layoutChanged, argument: fieldStackView) + } + + open override func layoutSubviews() { + super.layoutSubviews() + titleLabelWidthConstraint?.constant = containerView.frame.width + titleLabelWidthConstraint?.isActive = helperTextPlacement == .right + } + + open override var canBecomeFirstResponder: Bool { true } + + open override func resignFirstResponder() -> Bool { + if dropdownField.isFirstResponder { + dropdownField.resignFirstResponder() + } + return super.resignFirstResponder() } } @@ -289,7 +366,7 @@ open class DropdownSelect: EntryFieldBase { // MARK: - UIPickerView Delegate & Datasource //-------------------------------------------------- extension DropdownSelect: UIPickerViewDelegate, UIPickerViewDataSource { - + internal func launchPicker() { if optionsPicker.isHidden { UIAccessibility.post(notification: .layoutChanged, argument: optionsPicker) @@ -298,6 +375,7 @@ extension DropdownSelect: UIPickerViewDelegate, UIPickerViewDataSource { dropdownField.resignFirstResponder() } optionsPicker.isHidden = !optionsPicker.isHidden + setNeedsUpdate() } public func numberOfComponents(in pickerView: UIPickerView) -> Int { diff --git a/VDS/Components/Icon/IconName.swift b/VDS/Components/Icon/IconName.swift index 116c9de6..c3943d64 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 16ccb131..657b724e 100644 --- a/VDS/Components/TextFields/EntryFieldBase.swift +++ b/VDS/Components/TextFields/EntryFieldBase.swift @@ -40,21 +40,35 @@ 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 $0.distribution = .fill + $0.alignment = .leading } }() - + + /// 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 } }() - internal var containerStackView: UIStackView = { + /// 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 $0.axis = .horizontal @@ -63,23 +77,13 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { } }() - internal var controlContainerView: UIView = { - return UIView().with { - $0.translatesAutoresizingMaskIntoConstraints = false - } - }() - - internal var bottomContainerView: UIView = { - return UIView().with { - $0.translatesAutoresizingMaskIntoConstraints = false - } - }() - + /// This is a vertical stack used for the errorLabel and helperLabel. internal var bottomContainerStackView: UIStackView = { return UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .vertical $0.distribution = .fill + $0.spacing = VDSLayout.space2X } }() @@ -89,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) @@ -199,7 +205,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { //-------------------------------------------------- internal var heightConstraint: NSLayoutConstraint? internal var widthConstraint: NSLayoutConstraint? - + //-------------------------------------------------- // MARK: - Overrides //-------------------------------------------------- @@ -208,51 +214,44 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { super.setup() isAccessibilityElement = false - addSubview(stackView) + addSubview(primaryStackView) //create the wrapping view - heightConstraint = containerView.heightAnchor.constraint(greaterThanOrEqualToConstant: containerSize.height) - heightConstraint?.priority = .defaultHigh - heightConstraint?.isActive = true - - widthConstraint = containerView.widthAnchor.constraint(equalToConstant: 0) - widthConstraint?.priority = .defaultHigh - - //get the container this is what is color - //border, internal, etc... - let container = getContainer() + heightConstraint = containerView.heightGreaterThanEqualTo(constant: containerSize.height) + widthConstraint = containerView.width(constant: frame.size.width) + secondaryStackView.addArrangedSubview(containerView) + secondaryStackView.setCustomSpacing(8, after: containerView) + //add ContainerStackView //this is the horizontal stack that contains //the left, InputContainer, Icons, Buttons - container.addSubview(containerStackView) - containerStackView.pinToSuperView(.uniform(VDSLayout.space3X)) + containerView.addSubview(fieldStackView) + fieldStackView.pinToSuperView(.uniform(VDSLayout.space3X)) + let fieldContainerView = getFieldContainer() + fieldContainerView.translatesAutoresizingMaskIntoConstraints = false + //add the view to add input fields - containerStackView.addArrangedSubview(controlContainerView) - containerStackView.addArrangedSubview(statusIcon) - containerStackView.setCustomSpacing(VDSLayout.space3X, after: controlContainerView) + fieldStackView.addArrangedSubview(fieldContainerView) + fieldStackView.addArrangedSubview(statusIcon) + fieldStackView.setCustomSpacing(VDSLayout.space3X, after: fieldContainerView) //get the container this is what show helper text, error text //can include other for character count, max length let bottomContainer = getBottomContainer() - - //add bottomContainerStackView + //this is the vertical stack that contains error text, helper text - bottomContainerView.addSubview(bottomContainerStackView) - bottomContainerStackView.pinToSuperView() bottomContainerStackView.addArrangedSubview(errorLabel) bottomContainerStackView.addArrangedSubview(helperLabel) - stackView.addArrangedSubview(titleLabel) - stackView.addArrangedSubview(container) - stackView.addArrangedSubview(bottomContainer) + primaryStackView.addArrangedSubview(titleLabel) + primaryStackView.addArrangedSubview(secondaryStackView) + secondaryStackView.addArrangedSubview(bottomContainer) - stackView.setCustomSpacing(4, after: titleLabel) - stackView.setCustomSpacing(8, after: container) - stackView.setCustomSpacing(8, after: bottomContainer) + 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,14 +317,23 @@ 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 getContainer() -> UIView { - return containerView + open func getFieldContainer() -> UIView { + fatalError("Subclass must return the view that contains the field/view the user will interact with.") } /// Container for the area in which helper or error text presents. open func getBottomContainer() -> UIView { - return bottomContainerView + return bottomContainerStackView } internal func updateRules() { diff --git a/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift b/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift new file mode 100644 index 00000000..7137d895 --- /dev/null +++ b/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift @@ -0,0 +1,207 @@ +// +// CreditCard.swift +// VDS +// +// Created by Matt Bruce on 5/9/24. +// + +import Foundation +import UIKit + +extension InputField { + + public enum CreditCardType: String, CaseIterable { + case generic + case visa + case mastercard + case amex + case discover + case dinersClub + case jcb + case unionPay + + func imageName(surface: Surface) -> String { + func getImageName(_ surface: Surface, name: String) -> String { + return surface == .light ? name : "\(name)-inverted" + } + switch self { + case .visa: return getImageName(surface, name: "visa") + case .mastercard: return "mastercard" + case .amex: return "amex" + case .discover: return "discover" + case .dinersClub: return "dinersClub"//getImageName(surface, name: "dinersClub") + case .jcb: return "jcb" + case .unionPay: return getImageName(surface, name: "unionPay") + default: return getImageName(surface, name: "generic") + } + } + + var separatorIndices: [Int] { + switch self { + case .dinersClub: + return [4, 10] + default: + return [4, 8, 12] + } + } + + var securityCodeLength: Int { + if self == .amex { + return 4 + } else { + return 3 + } + } + + var maxLength: Int { + switch self { + case .dinersClub: return 14 + default: return 16 + } + } + + static func from(cardNumber: String) -> CreditCardType { + let clean = cardNumber.filter { $0.isNumber } + let firstNum = Int(clean.prefix(1)) ?? 0 + let twoChar = Int(clean.prefix(2)) ?? 0 + let threeChar = Int(clean.prefix(3)) ?? 0 + let fourChar = Int(clean.prefix(4)) ?? 0 + let sixChar = Int(clean.prefix(6)) ?? 0 + + if firstNum == 4 { + return .visa + } else if twoChar == 34 || twoChar == 37 { + return .amex + } else if (twoChar == 62 && !(sixChar >= 622126 && sixChar <= 622925)) || twoChar == 81 || (twoChar == 60 && fourChar != 6011) { + return .unionPay + } else if (threeChar > 299 && threeChar <= 305) || threeChar == 309 || twoChar == 36 || twoChar == 38 || twoChar == 39 { + return .dinersClub + } else if fourChar == 6011 || (sixChar >= 622126 && sixChar <= 622925) || (threeChar >= 644 && threeChar <= 649) || twoChar == 65 { + return .discover + } else if fourChar >= 3528 && fourChar <= 3589 { + return .jcb + } else if (twoChar >= 51 && twoChar <= 55) || (sixChar >= 222100 && sixChar <= 272099) { + return .mastercard + } else { + return .generic + } + } + + } + + class CreditCardHandler: FieldTypeHandler { + static let shared = CreditCardHandler() + + private override init() { + super.init() + self.validateOnChange = false + self.keyboardType = .numberPad + } + + override func updateView(_ inputField: InputField) { + minWidth = 288.0 + leftImageName = inputField.cardType.imageName(surface: inputField.surface) + super.updateView(inputField) + } + + override func appendRules(_ inputField: InputField) { + if let text = inputField.textField.text, text.count > 0 { + let rule = CharacterCountRule().copyWith { + $0.maxLength = inputField.cardType.maxLength + $0.compareType = .equals + $0.errorMessage = "Enter a valid credit card." + } + inputField.rules.append(.init(rule)) + } + } + + override func textFieldDidBeginEditing(_ inputField: InputField, textField: UITextField) { + //reset the textField when you start editing + value = nil + inputField.cardType = .generic + textField.text = "" + } + + override func textFieldDidEndEditing(_ inputField: InputField, textField: UITextField) { + if let value { + textField.text = maskCreditCardNumber(inputField.cardType, number: value) + } + } + + override func textField(_ inputField: InputField, textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + let allowedCharacters = CharacterSet.decimalDigits + if string.rangeOfCharacter(from: allowedCharacters.inverted) != nil && !string.isEmpty { + return false + } + + // Get the current text + let currentText = textField.text ?? "" + + // Calculate the new text + let newText = (currentText as NSString).replacingCharacters(in: range, with: string) + + // Remove any existing formatting + let rawNumber = newText.filter { $0.isNumber } + + if rawNumber.count > inputField.cardType.maxLength { + return false + } + + // Format the number with spaces + let formattedNumber = formatCreditCardNumber(inputField.cardType, number: rawNumber) + + // Update the icon based on the first four digits + updateCardTypeIcon(inputField, rawNumber: rawNumber) + + // Check again + if rawNumber.count > inputField.cardType.maxLength { + return false + } + + // Set the formatted text + textField.text = formattedNumber + + // Calculate the new cursor position + if let newPosition = textField.cursorPosition(range: range, + replacementString: string, + rawNumber: rawNumber, + formattedNumber: formattedNumber) { + textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition) + } + + // if all passes, then set the number1 + value = rawNumber + + // Prevent the default behavior + return false + } + + /// Private + internal func formatCreditCardNumber(_ cardType: CreditCardType, number: String) -> String { + let formattedInput = number.filter { $0.isNumber } // Remove any existing slashes + return String.format(formattedInput, indices: cardType.separatorIndices, with: " ") + } + + internal func updateCardTypeIcon(_ inputField: InputField, rawNumber: String) { + defer { inputField.setNeedsUpdate() } + + guard rawNumber.count >= 4 else { + inputField.cardType = .generic + return + } + + inputField.cardType = CreditCardType.from(cardNumber: rawNumber) + } + + internal func maskCreditCardNumber(_ cardType: CreditCardType, number: String) -> String { + // Mask the first 12 characters if the length is 16 + let rawNumber = number.filter { $0.isNumber } + guard rawNumber.count == cardType.maxLength else { return formatCreditCardNumber(cardType, number: number) } + let lastFourDigits = rawNumber.suffix(4) + let maskedSection = String(repeating: "•", count: 12) + let formattedMaskSection = String.format(maskedSection, indices: cardType.separatorIndices, with: " ") + return formattedMaskSection + " " + lastFourDigits + } + } + +} diff --git a/VDS/Components/TextFields/InputField/FieldTypes/Date.swift b/VDS/Components/TextFields/InputField/FieldTypes/Date.swift new file mode 100644 index 00000000..19656745 --- /dev/null +++ b/VDS/Components/TextFields/InputField/FieldTypes/Date.swift @@ -0,0 +1,99 @@ +// +// Date.swift +// VDS +// +// Created by Matt Bruce on 5/9/24. +// + +import Foundation +import UIKit + +extension InputField { + + public enum DateFormat: String, CaseIterable { + case mmyy + case mmddyy + case mmddyyyy + + public var placeholderText: String { + switch self { + case .mmyy: "MM/YY" + case .mmddyy: "MM/DD/YY" + case .mmddyyyy: "MM/DD/YYYY" + } + } + + public var formatString: String { + switch self { + case .mmyy: "MM/yy" + case .mmddyy: "MM/dd/yy" + case .mmddyyyy: "MM/dd/yyyy" + } + } + + public var maxLength: Int { + switch self { + case .mmyy: 5 + case .mmddyy: 8 + case .mmddyyyy: 10 + } + } + + internal var separatorIndices: [Int] { + switch self { + case .mmyy: [2] + case .mmddyy: [2,4] + case .mmddyyyy: [2,4] + } + } + } + + class DateHandler: FieldTypeHandler { + static let shared = DateHandler() + + private override init() { + super.init() + self.keyboardType = .numberPad + } + + override func updateView(_ inputField: InputField) { + minWidth = 114.0 + placeholderText = inputField.dateFormat.placeholderText + + super.updateView(inputField) + } + + override func appendRules(_ inputField: InputField) { + if let text = inputField.textField.text, text.count > 0 { + let rule = CharacterCountRule().copyWith { + $0.maxLength = inputField.dateFormat.maxLength + $0.compareType = .equals + $0.errorMessage = "Enter a valid date." + } + inputField.rules.append(.init(rule)) + } + } + + override func textField(_ inputField: InputField, textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + // Allow only numbers and limit the length of text. + guard let oldText = textField.text, + let textRange = Range(range, in: oldText), + string.rangeOfCharacter(from: CharacterSet.decimalDigits.inverted) == nil else { + return false + } + + let newText = oldText.replacingCharacters(in: textRange, with: string) + if newText.count > inputField.dateFormat.maxLength { + return false + } + + if newText.count <= inputField.dateFormat.maxLength { + textField.text = String.format(newText, indices: inputField.dateFormat.separatorIndices, with: "/") + return false + } else { + return true + } + } + } + +} diff --git a/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift b/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift new file mode 100644 index 00000000..ebecb17d --- /dev/null +++ b/VDS/Components/TextFields/InputField/FieldTypes/FieldType.swift @@ -0,0 +1,107 @@ +// +// FieldType.swift +// VDS +// +// Created by Matt Bruce on 5/9/24. +// + +import Foundation +import UIKit +import VDSTokens + +extension InputField { + + public enum FieldType: String, CaseIterable { + case text, number, inlineAction, password, creditCard, telephone, date, securityCode + + func handler() -> FieldTypeHandler { + switch self { + case .text: + return TextHandler.shared + case .number: + return NumberHandler.shared + case .inlineAction: + return InlineActionHandler.shared + case .password: + return PasswordHandler.shared + case .creditCard: + return CreditCardHandler.shared + case .telephone: + return TelephoneHandler.shared + case .date: + return DateHandler.shared + case .securityCode: + return SecurityCodeHandler.shared + } + } + } + + class FieldTypeHandler: NSObject { + var keyboardType: UIKeyboardType + var minWidth: CGFloat = 40.0 + var leftImageName: String? + var actionModel: TextLinkModel? + var toolTipModel: Tooltip.TooltipModel? + var isSecureTextEntry = false + var placeholderText: String? + var value: String? + var validateOnChange = false + + internal override init() { + keyboardType = .default + super.init() + } + + func updateView(_ inputField: InputField) { + + //keyboard + inputField.textField.keyboardType = keyboardType + + //textField + inputField.textField.isSecureTextEntry = isSecureTextEntry + + //leftIcon + if let leftImageName { + inputField.leftImageView.image = BundleManager.shared.image(for: leftImageName) + } + inputField.leftImageView.isHidden = leftImageName == nil + + //actionLink + inputField.actionTextLink.surface = inputField.surface + if let actionModel { + inputField.actionTextLink.text = actionModel.text + inputField.actionTextLink.onClick = { _ in + actionModel.onClick(inputField) + } + inputField.actionTextLink.isHidden = false + inputField.fieldStackView.setCustomSpacing(VDSLayout.space2X, after: inputField.statusIcon) + } else { + inputField.actionTextLink.isHidden = true + inputField.fieldStackView.setCustomSpacing(0, after: inputField.statusIcon) + } + + //placeholder + inputField.textField.placeholder = placeholderText + + //tooltip from Types take precedence + //if one was set, it would show as usual. + if let toolTipModel { + inputField.tooltipModel = toolTipModel + } + } + + func appendRules(_ inputField: InputField) {} + + func textFieldDidBeginEditing(_ inputField: InputField, textField: UITextField) { } + + func textFieldDidEndEditing(_ inputField: InputField, textField: UITextField) {} + + func textFieldDidChangeSelection(_ inputField: InputField, textField: UITextField) {} + + func textField(_ inputField: InputField, textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + return true + } + + } + +} diff --git a/VDS/Components/TextFields/InputField/FieldTypes/InlineAction.swift b/VDS/Components/TextFields/InputField/FieldTypes/InlineAction.swift new file mode 100644 index 00000000..68fe8b04 --- /dev/null +++ b/VDS/Components/TextFields/InputField/FieldTypes/InlineAction.swift @@ -0,0 +1,28 @@ +// +// InlineAction.swift +// VDS +// +// Created by Matt Bruce on 5/9/24. +// + +import Foundation +import UIKit + +extension InputField { + + class InlineActionHandler: FieldTypeHandler { + static let shared = InlineActionHandler() + + private override init() { + super.init() + } + + override func updateView(_ inputField: InputField) { + minWidth = 102.0 + actionModel = inputField.actionTextLinkModel + + super.updateView(inputField) + } + } + +} diff --git a/VDS/Components/TextFields/InputField/FieldTypes/Number.swift b/VDS/Components/TextFields/InputField/FieldTypes/Number.swift new file mode 100644 index 00000000..9e653421 --- /dev/null +++ b/VDS/Components/TextFields/InputField/FieldTypes/Number.swift @@ -0,0 +1,29 @@ +// +// Number.swift +// VDS +// +// Created by Matt Bruce on 5/9/24. +// + +import Foundation +import UIKit + +extension InputField { + + class NumberHandler: FieldTypeHandler { + static let shared = NumberHandler() + + private override init() { + super.init() + self.keyboardType = .numberPad + } + + override func textField(_ inputField: InputField, textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + // Allow only numbers + let allowedCharacters = CharacterSet.decimalDigits + let characterSet = CharacterSet(charactersIn: string) + return allowedCharacters.isSuperset(of: characterSet) + } + } + +} diff --git a/VDS/Components/TextFields/InputField/FieldTypes/Password.swift b/VDS/Components/TextFields/InputField/FieldTypes/Password.swift new file mode 100644 index 00000000..00c00a0d --- /dev/null +++ b/VDS/Components/TextFields/InputField/FieldTypes/Password.swift @@ -0,0 +1,68 @@ +// +// Password.swift +// VDS +// +// Created by Matt Bruce on 5/9/24. +// + +import Foundation +import UIKit + +extension InputField { + + enum PasswordAction { + case show, hide + + func toggle() -> PasswordAction { + self == .hide ? .show : .hide + } + } + + class PasswordHandler: FieldTypeHandler { + static let shared = PasswordHandler() + + internal var passwordActionType: PasswordAction = .hide + + private override init() { + super.init() + } + + override func updateView(_ inputField: InputField) { + let isHide = passwordActionType == .hide + let buttonText = isHide ? + inputField.hidePasswordButtonText.isEmpty ? "Hide" : inputField.hidePasswordButtonText : + inputField.showPasswordButtonText.isEmpty ? "Show" : inputField.showPasswordButtonText + + isSecureTextEntry = !isHide + let nextPasswordActionType = passwordActionType.toggle() + if let text = inputField.text, !text.isEmpty { + actionModel = .init(text: buttonText, + onClick: { [weak self] _ in + guard let self else { return } + self.passwordActionType = nextPasswordActionType + inputField.setNeedsUpdate() + }) + } else { + passwordActionType = .show + } + minWidth = 62.0 + + super.updateView(inputField) + + updateLink(inputField) + } + + func updateLink(_ inputField: InputField) { + if let text = inputField.textField.text, !text.isEmpty { + inputField.actionTextLink.isHidden = false + } else { + inputField.actionTextLink.isHidden = true + } + } + + override func textFieldDidChangeSelection(_ inputField: InputField, textField: UITextField) { + updateLink(inputField) + } + } + +} diff --git a/VDS/Components/TextFields/InputField/FieldTypes/SecurityCode.swift b/VDS/Components/TextFields/InputField/FieldTypes/SecurityCode.swift new file mode 100644 index 00000000..cf60eb9a --- /dev/null +++ b/VDS/Components/TextFields/InputField/FieldTypes/SecurityCode.swift @@ -0,0 +1,111 @@ +// +// SecurityCode.swift +// VDS +// +// Created by Matt Bruce on 5/9/24. +// + +import Foundation +import UIKit +import VDSTokens + +extension InputField { + + class SecurityCodeHandler: FieldTypeHandler { + static let shared = SecurityCodeHandler() + + private override init() { + super.init() + self.keyboardType = .numberPad + } + + override func appendRules(_ inputField: InputField) { + if let text = inputField.textField.text, text.count > 0 { + let rule = CharacterCountRule().copyWith { + $0.maxLength = inputField.cardType.securityCodeLength + $0.compareType = .equals + $0.errorMessage = "Enter a valid security code." + } + inputField.rules.append(.init(rule)) + } + } + + override func updateView(_ inputField: InputField) { + minWidth = 88.0 + isSecureTextEntry = true + toolTipModel = getToolTip(inputField) + super.updateView(inputField) + } + + func getToolTip(_ inputField: InputField) -> Tooltip.TooltipModel { + + let surface = inputField.surface + + var contentView: UIView + var code3Label = Label().with { + $0.text = "Most credit or debit cards have a 3-digit security code on the back." + $0.isEnabled = true + $0.surface = surface + } + var code4Label = Label().with { + $0.text = "American Express cards have a 4-digit code on the front." + $0.isEnabled = true + $0.surface = surface + } + + var code3ImageView = UIImageView().with { + $0.image = BundleManager.shared.image(for: "securityCode\(surface == .dark ? "-inverted": "")") + } + var code4ImageView = UIImageView().with { + $0.image = BundleManager.shared.image(for: "securityCodeAmex\(surface == .dark ? "-inverted": "")") + } + + func stack(_ axis: NSLayoutConstraint.Axis = .vertical) -> UIStackView { + UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.axis = axis + $0.distribution = .fill + $0.alignment = .leading + $0.spacing = VDSLayout.space2X + } + } + + func view3() -> UIView { + stack().with { + $0.addArrangedSubview(code3ImageView) + $0.addArrangedSubview(code3Label) + } + } + + func view4() -> UIView { + stack().with { + $0.addArrangedSubview(code4ImageView) + $0.addArrangedSubview(code4Label) + } + } + + let title: String = inputField.cardType.rawValue + switch inputField.cardType { + case .amex: + contentView = view4() + case .generic: + contentView = stack(.horizontal).with { + $0.addArrangedSubview(view3()) + $0.addArrangedSubview(view4()) + } + default: + contentView = view3() + } + + return .init(contentView: contentView) + } + + override func textField(_ inputField: InputField, textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + // Allow only numbers and limit the length of text. + let allowedCharacters = CharacterSet.decimalDigits + let characterSet = CharacterSet(charactersIn: string) + return allowedCharacters.isSuperset(of: characterSet) && ((textField.text?.count ?? 0) + string.count - range.length) <= inputField.cardType.securityCodeLength + } + } + +} diff --git a/VDS/Components/TextFields/InputField/FieldTypes/Telephone.swift b/VDS/Components/TextFields/InputField/FieldTypes/Telephone.swift new file mode 100644 index 00000000..bfcd9ef7 --- /dev/null +++ b/VDS/Components/TextFields/InputField/FieldTypes/Telephone.swift @@ -0,0 +1,104 @@ +// +// Tel.swift +// VDS +// +// Created by Matt Bruce on 5/9/24. +// + +import Foundation +import UIKit + +extension InputField { + + class TelephoneHandler: FieldTypeHandler { + static let shared = TelephoneHandler() + + private override init() { + super.init() + self.keyboardType = .phonePad + } + + override func updateView(_ inputField: InputField) { + minWidth = 176.0 + + super.updateView(inputField) + } + + override func appendRules(_ inputField: InputField) { + if let text = inputField.textField.text, text.count > 0 { + let rule = CharacterCountRule().copyWith { + $0.maxLength = "XXX-XXX-XXXX".count + $0.compareType = .equals + $0.errorMessage = "Enter a valid telephone." + } + inputField.rules.append(.init(rule)) + } + } + + override func textField(_ inputField: InputField, textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + // Allow only numbers and limit the length of text. + let allowedCharacters = CharacterSet(charactersIn: "01233456789") + let characterSet = CharacterSet(charactersIn: string) + let currentText = textField.text ?? "" + if !allowedCharacters.isSuperset(of: characterSet) { return false } + + // Calculate the new text + let newText = (currentText as NSString).replacingCharacters(in: range, with: string) + + // Remove any existing formatting + let rawNumber = newText.filter { $0.isNumber } + + // Format the number with dashes + let formattedNumber = formatUSNumber(rawNumber) + + // Set the formatted text + textField.text = formattedNumber + + // Calculate the new cursor position + if let newPosition = textField.cursorPosition(range: range, + replacementString: string, + rawNumber: rawNumber, + formattedNumber: formattedNumber) { + textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition) + } + + // Prevent the default behavior + return false + + } + + internal func formatUSNumber(_ number: String) -> String { + // Format the number in the style XXX-XXX-XXXX + let areaCodeLength = 3 + let centralOfficeCodeLength = 3 + let lineNumberLength = 4 + + var formattedNumber = "" + + if number.count > 0 { + formattedNumber.append(contentsOf: number.prefix(areaCodeLength)) + } + + if number.count > areaCodeLength { + let startIndex = number.index(number.startIndex, offsetBy: areaCodeLength) + let endIndex = number.index(startIndex, offsetBy: min(centralOfficeCodeLength, number.count - areaCodeLength)) + let centralOfficeCode = number[startIndex.. areaCodeLength + centralOfficeCodeLength { + let startIndex = number.index(number.startIndex, offsetBy: areaCodeLength + centralOfficeCodeLength) + let endIndex = number.index(startIndex, offsetBy: min(lineNumberLength, number.count - areaCodeLength - centralOfficeCodeLength)) + let lineNumber = number[startIndex.. UIView { + // stackview for controls in EntryFieldBase.controlContainerView + let stackView = UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.axis = .horizontal + $0.spacing = VDSLayout.space3X + } + stackView.addArrangedSubview(leftImageView) + stackView.addArrangedSubview(textField) + return stackView + } + /// Resets to default settings. open override func reset() { super.reset() @@ -212,18 +227,12 @@ open class InputField: EntryFieldBase { successText = nil helperTextPlacement = .bottom } - - /// Container for the area in which the user interacts. - open override func getContainer() -> UIView { - inputFieldStackView.addArrangedSubview(containerView) - return inputFieldStackView - } /// Used to make changes to the View based off a change events or from local properties. open override func updateView() { //update fieldType first - updateFieldType() + fieldType.handler().updateView(self) super.updateView() @@ -255,6 +264,7 @@ open class InputField: EntryFieldBase { } open override func updateHelperLabel(){ //remove first + secondaryStackView.removeFromSuperview() helperLabel.removeFromSuperview() super.updateHelperLabel() @@ -262,118 +272,23 @@ open class InputField: EntryFieldBase { //set the helper label position if helperText != nil { if helperTextPlacement == .right { - inputFieldStackView.spacing = 12 - inputFieldStackView.distribution = .fillEqually - inputFieldStackView.addArrangedSubview(helperLabel) + horizontalStackView.addArrangedSubview(secondaryStackView) + horizontalStackView.addArrangedSubview(helperLabel) + primaryStackView.addArrangedSubview(horizontalStackView) } else { - inputFieldStackView.spacing = 0 - inputFieldStackView.distribution = .fill - stackView.addArrangedSubview(helperLabel) + bottomContainerStackView.addArrangedSubview(helperLabel) + primaryStackView.addArrangedSubview(secondaryStackView) } + } else { + primaryStackView.addArrangedSubview(secondaryStackView) } } - open func updateFieldType() { - - var minWidth: CGFloat = 40.0 - var leftIconName: Icon.Name? - var actionModel: InputField.TextLinkModel? - var toolTipModel: Tooltip.TooltipModel? = tooltipModel - var isSecureTextEntry = false - var rules = [AnyRule]() - var placeholderText: String? - - if self.isRequired { - let rule = RequiredRule() - if let errorText { - rule.errorMessage = errorText - } - rules.append(.init(rule)) - } - - switch fieldType { - case .text: - break - - case .number: - break - - case .inlineAction: - minWidth = 102.0 - - case .password: - let isHide = passwordActionType == .hide - let buttonText = isHide ? - hidePasswordButtonText.isEmpty ? "Hide" : hidePasswordButtonText : - showPasswordButtonText.isEmpty ? "Show" : showPasswordButtonText - - isSecureTextEntry = !isHide - let nextPasswordActionType = passwordActionType.toggle() - if let text, !text.isEmpty { - actionModel = .init(text: buttonText, - onClick: { [weak self] _ in - guard let self else { return } - self.passwordActionType = nextPasswordActionType - }) - } else { - passwordActionType = .show - } - minWidth = 62.0 - - case .creditCard: - minWidth = 288.0 - - case .tel: - minWidth = 176.0 - - case .date: - minWidth = 114.0 - placeholderText = dateFormat.placeholderText - - case .securityCode: - minWidth = 88.0 - - } - - //textField - textField.isSecureTextEntry = isSecureTextEntry - - //leftIcon - leftIcon.surface = surface - leftIcon.color = iconColorConfiguration.getColor(self) - leftIcon.name = leftIconName - leftIcon.isHidden = leftIconName == nil - - //actionLink - actionTextLink.surface = surface - if let actionModel { - actionTextLink.text = actionModel.text - actionTextLink.onClick = actionModel.onClick - actionTextLink.isHidden = false - containerStackView.setCustomSpacing(VDSLayout.space2X, after: statusIcon) - } else { - actionTextLink.isHidden = true - containerStackView.setCustomSpacing(0, after: statusIcon) - } - - //set the width constraints - if let width, width > minWidth { - widthConstraint?.constant = width - widthConstraint?.isActive = true - minWidthConstraint?.isActive = false - } else { - minWidthConstraint?.constant = minWidth - widthConstraint?.isActive = false - minWidthConstraint?.isActive = true - } - - //placeholder - textField.placeholder = placeholderText - - //tooltip - tooltipModel = toolTipModel + override func updateRules() { + super.updateRules() + fieldType.handler().appendRules(self) } - + /// Used to update any Accessibility properties. open override func updateAccessibility() { super.updateAccessibility() @@ -403,6 +318,12 @@ open class InputField: EntryFieldBase { set { super.accessibilityElements = newValue } } + open override func layoutSubviews() { + super.layoutSubviews() + titleLabelWidthConstraint?.constant = containerView.frame.width + titleLabelWidthConstraint?.isActive = helperTextPlacement == .right + } + open override var canBecomeFirstResponder: Bool { true } open override func resignFirstResponder() -> Bool { @@ -411,114 +332,49 @@ open class InputField: EntryFieldBase { } return super.resignFirstResponder() } - - //-------------------------------------------------- - // MARK: - Password - //-------------------------------------------------- - enum PasswordAction { - case show, hide - - func toggle() -> PasswordAction { - self == .hide ? .show : .hide - } - } - - internal var passwordActionType: PasswordAction = .show { didSet { setNeedsUpdate() } } - - open var hidePasswordButtonText: String = "Hide" { didSet { setNeedsUpdate() } } - open var showPasswordButtonText: String = "Show" { didSet { setNeedsUpdate() } } - - //-------------------------------------------------- - // MARK: - Date - //-------------------------------------------------- - open var dateFormat: DateFormat = .mmddyy { didSet { setNeedsUpdate() } } - } extension InputField: UITextFieldDelegate { - public func process(text changedText: String) { - var newText: String = changedText - switch fieldType { - case .date: - guard newText.count <= dateFormat.maxLength else { return } - let numericText = newText.compactMap { $0.isNumber ? $0 : nil } - var formattedText = "" - - for (index, char) in numericText.enumerated() { - if (index == 2 || (index == 4 && (dateFormat != .mmyy))) && index < dateFormat.maxLength { - formattedText += "/" - } - formattedText.append(char) - } - newText = formattedText - - default: break + public func textFieldDidBeginEditing(_ textField: UITextField) { + fieldType.handler().textFieldDidBeginEditing(self, textField: textField) + setNeedsUpdate() + } + + public func textFieldDidEndEditing(_ textField: UITextField) { + fieldType.handler().textFieldDidEndEditing(self, textField: textField) + validate() + } + + public func textFieldDidChangeSelection(_ textField: UITextField) { + fieldType.handler().textFieldDidChangeSelection(self, textField: textField) + if fieldType.handler().validateOnChange { + validate() + } else { + setNeedsUpdate() } + sendActions(for: .valueChanged) } public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { - switch fieldType { - case .date: - // Allow only numbers and limit the length of text. - let allowedCharacters = CharacterSet.decimalDigits - let characterSet = CharacterSet(charactersIn: string) - return allowedCharacters.isSuperset(of: characterSet) && ((textField.text?.count ?? 0) + string.count - range.length) <= dateFormat.maxLength - - default: - return true - } + return fieldType.handler().textField(self, textField: textField, shouldChangeCharactersIn: range, replacementString: string) } } -extension InputField.FieldType { +extension String { - public var keyboardType: UIKeyboardType { - switch self { - case .number: - .numberPad - case .tel: - .phonePad - case .creditCard: - .numberPad - case .date: - .numberPad - case .securityCode: - .numberPad - default: - .default + internal static func format(_ value: String, indices: [Int], with separator: String) -> String { + var formattedString = "" + var currentIndex = value.startIndex + + for index in 0.. UITextPosition? { + let start = range.location + let length = string.count + + let newCursorLocation = start + length + + // Adjust the cursor position to skip over formatting characters + var formattedCharacterCount = 0 + for (index, character) in formattedNumber.enumerated() { + if index >= newCursorLocation + formattedCharacterCount { + break + } + if !character.isNumber { + formattedCharacterCount += 1 + } + } + + let finalCursorLocation = min(newCursorLocation + formattedCharacterCount, formattedNumber.count) + return position(from: beginningOfDocument, offset: finalCursorLocation) + } +} diff --git a/VDS/Components/TextFields/InputField/TextLinkModel.swift b/VDS/Components/TextFields/InputField/TextLinkModel.swift index 89235e5e..a9c557a0 100644 --- a/VDS/Components/TextFields/InputField/TextLinkModel.swift +++ b/VDS/Components/TextFields/InputField/TextLinkModel.swift @@ -12,11 +12,11 @@ extension InputField { ///Text that goes in the Tab public var text: String - + ///Click event when you click on a tab - public var onClick: ((TextLink) -> Void)? - - public init(text: String, onClick: ((TextLink) -> Void)? = nil) { + public var onClick: ((InputField) -> Void) + + public init(text: String = "Apply", onClick: @escaping (InputField) -> Void) { self.text = text self.onClick = onClick } diff --git a/VDS/Components/TextFields/Rules/CharacterCountRule.swift b/VDS/Components/TextFields/Rules/CharacterCountRule.swift index f26ccafc..46cbed05 100644 --- a/VDS/Components/TextFields/Rules/CharacterCountRule.swift +++ b/VDS/Components/TextFields/Rules/CharacterCountRule.swift @@ -7,12 +7,28 @@ import Foundation -class CharacterCountRule: Rule { +class CharacterCountRule: Rule, Withable { + enum CompareType { + case equals, greaterThanEquals, lessThan, lessThanEquals + } var maxLength: Int? var errorMessage: String = "You have exceeded the character limit." + var compareType: CompareType = .lessThanEquals func isValid(value: String?) -> Bool { guard let text = value, let maxLength, maxLength > 0 else { return true } - return text.count <= maxLength + switch compareType { + case .equals: + return text.count == maxLength + + case .greaterThanEquals: + return text.count >= maxLength + + case .lessThan: + return text.count < maxLength + + case .lessThanEquals: + return text.count <= maxLength + } } } diff --git a/VDS/Components/TextFields/TextArea/TextArea.swift b/VDS/Components/TextFields/TextArea/TextArea.swift index 433bdd73..f0fd6a85 100644 --- a/VDS/Components/TextFields/TextArea/TextArea.swift +++ b/VDS/Components/TextFields/TextArea/TextArea.swift @@ -32,7 +32,6 @@ open class TextArea: EntryFieldBase { //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- - internal var minWidthConstraint: NSLayoutConstraint? internal var textViewHeightConstraint: NSLayoutConstraint? internal var inputFieldStackView: UIStackView = { @@ -43,38 +42,15 @@ open class TextArea: EntryFieldBase { $0.spacing = VDSLayout.space3X } }() - - internal var bottomStackView: UIStackView = { - return UIStackView().with { - $0.translatesAutoresizingMaskIntoConstraints = false - $0.axis = .horizontal - $0.distribution = .fill - $0.alignment = .top - $0.spacing = VDSLayout.space2X - } - }() - + open var characterCounterLabel = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) $0.textStyle = .bodySmall $0.textAlignment = .right $0.numberOfLines = 1 } - - private var _minHeight: Height = .twoX - - open var minHeight: Height? { - get { return _minHeight } - set { - if let newValue { - _minHeight = newValue - } else { - _minHeight = .twoX - } - textViewHeightConstraint?.constant = _minHeight.value - setNeedsUpdate() - } - } + + open var minHeight: Height = .twoX { didSet { setNeedsUpdate() } } //-------------------------------------------------- // MARK: - Public Properties @@ -161,16 +137,8 @@ open class TextArea: 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() - containerStackView.pinToSuperView(.uniform(VDSFormControls.spaceInset)) - minWidthConstraint = containerView.widthAnchor.constraint(greaterThanOrEqualToConstant: containerSize.width) - minWidthConstraint?.isActive = true - controlContainerView.addSubview(textView) - textView - .pinTop() - .pinLeading() - .pinTrailingLessThanOrEqualTo(nil, 0, .defaultHigh) - .pinBottom(0, .defaultHigh) - + fieldStackView.pinToSuperView(.uniform(VDSFormControls.spaceInset)) + textView.isScrollEnabled = true textView.autocorrectionType = .no @@ -200,6 +168,7 @@ open class TextArea: EntryFieldBase { borderColorConfiguration.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .focused) characterCounterLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable() bottomContainerStackView.spacing = VDSLayout.space2X + } /// Resets to default settings. @@ -210,13 +179,7 @@ open class TextArea: EntryFieldBase { characterCounterLabel.textStyle = .bodySmall setNeedsUpdate() } - - /// Container for the area in which the user interacts. - open override func getContainer() -> UIView { - inputFieldStackView.addArrangedSubview(containerView) - return inputFieldStackView - } - + /// Used to make changes to the View based off a change events or from local properties. open override func updateView() { super.updateView() @@ -224,17 +187,8 @@ open class TextArea: EntryFieldBase { textView.isEnabled = isEnabled textView.surface = surface - //set the width constraints - if let width { - widthConstraint?.constant = width - widthConstraint?.isActive = true - minWidthConstraint?.isActive = false - } else { - minWidthConstraint?.constant = containerSize.width - widthConstraint?.isActive = false - minWidthConstraint?.isActive = true - } - + textViewHeightConstraint?.constant = minHeight.value + characterCounterLabel.text = getCharacterCounterText() statusIcon.color = iconColorConfiguration.getColor(self) @@ -252,11 +206,22 @@ open class TextArea: EntryFieldBase { rules.append(.init(countRule)) } + open override func getFieldContainer() -> UIView { + textView + } + /// Container for the area showing helper text, error text, character count, maximum length value. open override func getBottomContainer() -> UIView { - bottomStackView.addArrangedSubview(bottomContainerView) - bottomStackView.addArrangedSubview(characterCounterLabel) - return bottomStackView + let stackView = UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.axis = .horizontal + $0.distribution = .fill + $0.alignment = .top + $0.spacing = VDSLayout.space2X + } + stackView.addArrangedSubview(super.getBottomContainer()) + stackView.addArrangedSubview(characterCounterLabel) + return stackView } /// Used to update any Accessibility properties. @@ -321,7 +286,7 @@ open class TextArea: EntryFieldBase { //since it will autogrow with the current settings if let textViewHeightConstraint, textView.isEditable { var height = textView.contentSize.height - height = max(height, _minHeight.value) + height = max(height, minHeight.value) if height > Height.twoX.value && height <= Height.fourX.value { textViewHeightConstraint.constant = Height.fourX.value } else if height > Height.fourX.value { diff --git a/VDS/Components/Tilelet/Tilelet.swift b/VDS/Components/Tilelet/Tilelet.swift index 0827b1c7..a96e1248 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/CreditCard/credit-card.imageset/credit-card.svg b/VDS/SupportingFiles/Icons.xcassets/CreditCard/credit-card.imageset/credit-card.svg deleted file mode 100644 index a5975ebf..00000000 --- a/VDS/SupportingFiles/Icons.xcassets/CreditCard/credit-card.imageset/credit-card.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub-inverted.imageset/Contents.json b/VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub-inverted.imageset/Contents.json index 01c460d2..6c8a6fee 100644 --- a/VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub-inverted.imageset/Contents.json +++ b/VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub-inverted.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "dinersClub-inverted.svg", + "filename" : "DCI_Horizontal-2_onlight.svg", "idiom" : "universal" } ], diff --git a/VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub-inverted.imageset/DCI_Horizontal-2_onlight.svg b/VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub-inverted.imageset/DCI_Horizontal-2_onlight.svg new file mode 100644 index 00000000..569a32c0 --- /dev/null +++ b/VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub-inverted.imageset/DCI_Horizontal-2_onlight.svg @@ -0,0 +1,819 @@ + + + + + + + + + + +]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 +AClkpRPQoVUqp5YqpROIVtpgtzHAAIqX+KBcDSYUVthtoMNtWYU1fSwoyJRZztMGP5QGgkIAPVzT +gPcTYWHFodaALzILR1cY5nJJjAaFyToYy2ehVS5MxMkwOIQHFhQmzBMGWmZSWSw46HWbTz0UKY4N +0Aehb2Bh8rGOz0DbqFVKzKgwYZ4oZowWxiAS0G0YxGnJp5NjVmimRqlyMjSwsGJWGDISAYcixbGV +8aB4joipw3Foal0E4wWAMKopBSsMVigMnJhGAwIdoQYtzEJjDiwgAVeBRcGJoDSqcqAVnqlRxXQb +6sUQUIGgVvjRAaluRJMYMvcsu9fHITe0qqEIVBKOGsni4QsdjOMh4MCS2hujaaiICi4lBrWeIsWx +ZWiDHshCaNnuNPBZb0GCwZATAQqorBKxz54GPhs2mN0rwVLI1IYoZkAjDq2/EDWSJXb+MQBt9Iph +KBuxSxjOFNxnf8kPlOaz3SaRrbrc+CyQu8CMr+9iBiUGeghbVx6CwaCUIEAS9kA4SLwEEblAHohQ +gyMIgrryHg4FD+FhoSIIjSY9RLdVHAoegqDxSRB4hImkskfMNryDbilAKO0KpAAsLbPZQnI3BJdY +pUw5hD7Ew2FXCCwLIwtHg0DJ3WZwFiTE2wA7EZKOJoFqPIbMfWCAuiTDMHIaAb5vfuvHfWPtP8hi +XnvnuH6c7+8f14+3Fa4fc7vbX7W3dtePLf9b2D+vH//9c/24b4w9qO+qtzDfurnsd8bV41873la1 +49uzrZ3n36vOdctyv7Xv/LfmlWPsf924bvwzrhv73v2vGnfc+bfcV41v3xakNf/V/1s31pnrjL/1 +9u6PMa8/Y4591Thjfbu9deP6udz1rhtz/uvPmNeNsfW8brytcre2142tvVn3ujG3u/4btD+2v/4t +3L1ujO/9deNub+WY47Dn67PHtmqcse69YpxvvVhfbnnd1t/aMa8e21stznhb6d0rxlrz+nGuGXNd +P+ex95z7mnHH9XMZY78tKPPdQ+6rxv56fqvGfpW19RXja7Gu3/9fL97X++q3lccW1+59r5vzvlrc +ffb1Yq3zvr9iXDOu+2b/6wbpX3fHXleNN5nFv+qNca4a46yx1rXfj/PGtu66K7/c7r5uvn/l+etc +e+V3Z+5z5Xdb1f4//z3Xfr3+vte+ref9a97vBsq6Y2urv1v4d/6V137x5h33Xvv1e3O8ve+29nsv +/phv/3vl+PbOtef81o73313nyiv//+/KtxWuO/N9q797+1773Va88vtt17xqnnWu/HZ//66b89hW +frfw6mt97vpWfje/ddufceX3+7srv5vs2tx71R/b2u/W2sJvva/6dr8997hqj/P/v+brLdcd8207 +yOe8teYZlG3mvf+a88X6317z1xnbrC+v+ea/bee/d6/xrfpuq6u+2+p6z/XdVd/sd7768g7y94Zc +76qvttriXnXW+tqrdd3X68svr/tyG+vcvc6+7ot9t93fu+u+W1+N719pW/fFfmebf9U339otvvlz +7POu+ub9s/Wd5+6rvjx72c35b47rxqstbG2r/P/91d6Oed3byn6vu628a6517fzzaq+9Fd+s++ad +V3xrX22b7+2V13y5/5xXrjHvmP/LPQhjnLffGO98Nf79Vu6t5hvXe++um8v943qv37hrr7XHFd9f +tf791t33vv1/bX3WdXMbf4xrvluoc90153x/1VzW29qar+f3dssrvpus6mwrvq0NFNqbcbX3a8ux +//X//LPvuv4NBOIY17+F3OtdP2gLM//dV45/x5fv7nn92ypza7W29Vvsq//Y5o9r30D2cp+v7T7r +j/2ufgu597ZXn7mu/lp/cfXb6tfe9e2156sz5rr2DaQ3B9mLe84c5107aAuxzRj/yq3X1t+sscad +145r33/XvoH+xz5vof387tr55xzX3vmuXXPba9fb176F//J8L9ae++xBWmfftzB3nGvPuHrr9eW1 +65t59V5v3X/120r7rr3FPttqr64dhD/uvnLsNa722o6tBvm/tedW417tx1trC7/Vn/fKtxXuuHJ8 +vcW2cvw973VvINDeQmuz5nhfbr3Pvm7QFv7Mf+W349r79v3Xra++uXafdfd1b6Cfuc1bb+Hmu/at +L6+b2/1nzmvXPutf9++1651z5VuYf7/+Z54rz/tu7Wu3H19ducd668q3lf9eV47x9rXf/rfGu3K8 +vb0exLvevfYtxNpzW/f92edd+edaY+995RvIW86zl3G75253t7b2u7W2Ve+ad1s57zj7b7fu+/66 ++97Z28q3VeeX12+/tlpXrj/X2+p3fL+t3/LK7d/Wa135JtP32vyrx5UjQLbWBgo3GYazrW3V98a2 +Yr0/r3dbaWs7z/Xi3jmvOvuqvfbV4u+r/t1vIJ7r3sL8Me/V447xz/V3W6/duf6dQVuI9bW92qs9 +5tf/ardVv9jrjevFu36vd72/3m3V/+6b64o/93nXv3vHVW+r7ce5+m31va2d858r57ju++/fvupN +9vvOevOqPd78+lzxvQiQrdxA4eWZa423xzb3ivX1GPuKs9cVW1xz9jp7X3HWv+LLr635Y41rzndf +bXfHtmZ9O/YYY/9vtjVzu1/e+7U1a7x3/+Hvf/+Kr/0V5w2kcdbc8qy/rTnz/7Xu2W9ta/a+a7s7 +B4Ub/9y79RV/jm2322LLNe7+grzWGH/Pb8a84o5t9ncrtxWu2e/cr645e9x/xzXnbeW/3RVvq96t +rRn7XnG+v+b+L7YWw90apNxA/etv/cX9Z59BHvPb68ebDHN+Na/X1o/DvO/Ft+Zse/5W+9x9xX/f +mvPueeebva05Y/v/9zr7ikE825rzJsvY1lwzv5xv7qvN/vp8f78gzfXdeGtd9xZqv7/Wt/Zq8+5b +c3s/tr7azDH+19abt9X/Xf+O681X285117VzPuvcO9b5buzrzd9+rT23nH9vK9d1c9lm/H+uOOe8 +tbcV501WNeet1nfnXG/2Gf+L8cVV45wr3l133+vl/Na2f8s5xxnXa73dVev8sc9V423Vdf+7Wp59 +37xmfLG//++acd9/74zr/X9rzr3Puev7a+9aX6yz7RZ377nuFWNb8/bX6l8x3mT9Wltz9jn/XTNe +4X151rd/q3/VoM65ztbrurdQ65tx97h+rvvP2O9efe764/q5jfnm/Vu7va/334v7tRVvq40tx9rm +iv/1+ub6Ofb8681r5rn6bWUxt7Vv76+tvGec9ddVX921/7/mTaa7vZb77qvmds/af287x9zristt +1b21v/Kc9eccdHneX1d8t3Bbe33lXN78ft5xvdfajvXNtXMeY3t5rvfefffmH+t9va34esxr5zK3 +mvO9Lc5dV3wx1rn2vbX/lV/9v+cV663cQKCrrb+cV8ztnv/21nu8f+V3A2W+Ocbdaqw9rv1usuut +t5rXy218eeV3Qy1c3spt5b3ePfN9d965e+xzvdvyrTvvFfvN/99Wn1+Nd9YVe935/Xlv/H+9nM/f +3u77zd7vevu9+/9/+cVf//7z7p5rmzHPF9t67wayOOd9sd++3i3U/Pp7ed+dd+7v9tVebuOb9+c9 +X875r5jbmNufrf3V3py73l7/au8m69Zmv/uvmPP+dp453vpuWy9q4bbdatto4e4GABNnNwtnN1S3 +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 +zzBg3UkNHuRBDfK3NKwD + + + diff --git a/VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub-inverted.imageset/dinersClub-inverted.svg b/VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub-inverted.imageset/dinersClub-inverted.svg deleted file mode 100644 index 1c8b4ccc..00000000 --- a/VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub-inverted.imageset/dinersClub-inverted.svg +++ /dev/null @@ -1,196 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub.imageset/Contents.json b/VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub.imageset/Contents.json index 32adfd39..52f90f68 100644 --- a/VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub.imageset/Contents.json +++ b/VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "dinersClub.svg", + "filename" : "DCI_Horizontal-2_ondark.svg", "idiom" : "universal" } ], diff --git a/VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub.imageset/DCI_Horizontal-2_ondark.svg b/VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub.imageset/DCI_Horizontal-2_ondark.svg new file mode 100644 index 00000000..98d7569b --- /dev/null +++ b/VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub.imageset/DCI_Horizontal-2_ondark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub.imageset/dinersClub.svg b/VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub.imageset/dinersClub.svg deleted file mode 100644 index 083ed5af..00000000 --- a/VDS/SupportingFiles/Icons.xcassets/CreditCard/dinersClub.imageset/dinersClub.svg +++ /dev/null @@ -1,196 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/VDS/SupportingFiles/Icons.xcassets/CreditCard/placeholder-inverted.imageset/placeholder-inverted.svg b/VDS/SupportingFiles/Icons.xcassets/CreditCard/placeholder-inverted.imageset/placeholder-inverted.svg deleted file mode 100644 index 06f4891e..00000000 --- a/VDS/SupportingFiles/Icons.xcassets/CreditCard/placeholder-inverted.imageset/placeholder-inverted.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/VDS/SupportingFiles/Icons.xcassets/CreditCard/placeholder.imageset/placeholder.svg b/VDS/SupportingFiles/Icons.xcassets/CreditCard/placeholder.imageset/placeholder.svg deleted file mode 100644 index 842da557..00000000 --- a/VDS/SupportingFiles/Icons.xcassets/CreditCard/placeholder.imageset/placeholder.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/VDS/SupportingFiles/Icons.xcassets/CreditCard/placeholder-inverted.imageset/Contents.json b/VDS/SupportingFiles/Icons.xcassets/CreditCard/unionPay-inverted.imageset/Contents.json similarity index 80% rename from VDS/SupportingFiles/Icons.xcassets/CreditCard/placeholder-inverted.imageset/Contents.json rename to VDS/SupportingFiles/Icons.xcassets/CreditCard/unionPay-inverted.imageset/Contents.json index 5b516479..72aa17d1 100644 --- a/VDS/SupportingFiles/Icons.xcassets/CreditCard/placeholder-inverted.imageset/Contents.json +++ b/VDS/SupportingFiles/Icons.xcassets/CreditCard/unionPay-inverted.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "placeholder-inverted.svg", + "filename" : "UnionPay-logo-onDark.svg", "idiom" : "universal" } ], diff --git a/VDS/SupportingFiles/Icons.xcassets/CreditCard/unionPay-inverted.imageset/UnionPay-logo-onDark.svg b/VDS/SupportingFiles/Icons.xcassets/CreditCard/unionPay-inverted.imageset/UnionPay-logo-onDark.svg new file mode 100644 index 00000000..56a83382 --- /dev/null +++ b/VDS/SupportingFiles/Icons.xcassets/CreditCard/unionPay-inverted.imageset/UnionPay-logo-onDark.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/VDS/SupportingFiles/Icons.xcassets/CreditCard/placeholder.imageset/Contents.json b/VDS/SupportingFiles/Icons.xcassets/CreditCard/unionPay.imageset/Contents.json similarity index 80% rename from VDS/SupportingFiles/Icons.xcassets/CreditCard/placeholder.imageset/Contents.json rename to VDS/SupportingFiles/Icons.xcassets/CreditCard/unionPay.imageset/Contents.json index 9b2b69ae..3bf829c6 100644 --- a/VDS/SupportingFiles/Icons.xcassets/CreditCard/placeholder.imageset/Contents.json +++ b/VDS/SupportingFiles/Icons.xcassets/CreditCard/unionPay.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "placeholder.svg", + "filename" : "UnionPay-logo-onLight.svg", "idiom" : "universal" } ], diff --git a/VDS/SupportingFiles/Icons.xcassets/CreditCard/unionPay.imageset/UnionPay-logo-onLight.svg b/VDS/SupportingFiles/Icons.xcassets/CreditCard/unionPay.imageset/UnionPay-logo-onLight.svg new file mode 100644 index 00000000..47bdbd1b --- /dev/null +++ b/VDS/SupportingFiles/Icons.xcassets/CreditCard/unionPay.imageset/UnionPay-logo-onLight.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/VDS/SupportingFiles/Icons.xcassets/CreditCard/credit-card.imageset/Contents.json b/VDS/SupportingFiles/Icons.xcassets/Restricted/external-link.imageset/Contents.json similarity index 82% rename from VDS/SupportingFiles/Icons.xcassets/CreditCard/credit-card.imageset/Contents.json rename to VDS/SupportingFiles/Icons.xcassets/Restricted/external-link.imageset/Contents.json index 24a71037..1e10c5e2 100644 --- a/VDS/SupportingFiles/Icons.xcassets/CreditCard/credit-card.imageset/Contents.json +++ b/VDS/SupportingFiles/Icons.xcassets/Restricted/external-link.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "credit-card.svg", + "filename" : "external-link.svg", "idiom" : "universal" } ], 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 258951db..461efee9 100644 --- a/VDS/SupportingFiles/ReleaseNotes.txt +++ b/VDS/SupportingFiles/ReleaseNotes.txt @@ -1,7 +1,18 @@ +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 ---------------- - CXTDT-546824 - Notification - Accessibility - Redundant text is provided for the notification icon. - CXTDT-553663 - DropdownSelect - Accessibility - 5 issues +- ONEAPP-6308 - Badge Indicator - Accessibility Text 1.0.61 ---------------- 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``