Merge branch 'develop' of https://gitlab.verizon.com/BPHV_MIPS/vds_ios.git into bugfix/badgeIndicator
This commit is contained in:
commit
11be1c35f0
@ -7,19 +7,35 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
1832AC572BA0791D008AE476 /* BreadcrumbCellItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1832AC562BA0791D008AE476 /* BreadcrumbCellItem.swift */; };
|
||||
18450CF12BA1B19C009FDF2A /* BreadcrumbsChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 18450CF02BA1B19C009FDF2A /* BreadcrumbsChangeLog.txt */; };
|
||||
1855EC662BAABF2A002ACAC2 /* BreadcrumbItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1855EC652BAABF2A002ACAC2 /* BreadcrumbItemModel.swift */; };
|
||||
1808BEBC2BA41C3200129230 /* CarouselScrollbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1808BEBB2BA41C3200129230 /* CarouselScrollbar.swift */; };
|
||||
1808BEC02BA456B700129230 /* CarouselScrollbarChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 1808BEBF2BA456B700129230 /* CarouselScrollbarChangeLog.txt */; };
|
||||
186B2A8A2B88DA7F001AB71F /* TextAreaChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 186B2A892B88DA7F001AB71F /* TextAreaChangeLog.txt */; };
|
||||
18792A902B7431F2008C0D29 /* ButtonIconBadgeIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18792A8F2B7431F2008C0D29 /* ButtonIconBadgeIndicatorModel.swift */; };
|
||||
18A65A022B96E848006602CC /* Breadcrumbs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A65A012B96E848006602CC /* Breadcrumbs.swift */; };
|
||||
18A65A042B96F050006602CC /* BreadcrumbItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A65A032B96F050006602CC /* BreadcrumbItem.swift */; };
|
||||
18BDEE822B75316E00452358 /* ButtonIconChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 18BDEE812B75316E00452358 /* ButtonIconChangeLog.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 */; };
|
||||
5F21D7BF28DCEB3D003E7CD6 /* Useable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F21D7BE28DCEB3D003E7CD6 /* Useable.swift */; };
|
||||
5FC35BE328D51405004EBEAC /* Button.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FC35BE228D51405004EBEAC /* Button.swift */; };
|
||||
710607952B91A99500F2863F /* TitleletChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 710607942B91A99500F2863F /* TitleletChangeLog.txt */; };
|
||||
7115BD3C2B84C0C200E0A610 /* TileContainerChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 7115BD3B2B84C0C200E0A610 /* TileContainerChangeLog.txt */; };
|
||||
71ACE89C2BA0451200FB6ADC /* PaginationContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71ACE89B2BA0451200FB6ADC /* PaginationContainer.swift */; };
|
||||
71ACE89E2BA1CC1700FB6ADC /* TiletEyebrowModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71ACE89D2BA1CC1700FB6ADC /* TiletEyebrowModel.swift */; };
|
||||
71B23C2D2B91FA690027F7D9 /* Pagination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71B23C2C2B91FA690027F7D9 /* Pagination.swift */; };
|
||||
71B5FCBB2B95A0CA00269BCC /* PaginationChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 71B5FCBA2B95A0CA00269BCC /* PaginationChangeLog.txt */; };
|
||||
71BFA70A2B7F70E6000DCE33 /* DropShadowable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71BFA7092B7F70E6000DCE33 /* DropShadowable.swift */; };
|
||||
71C02B382B7BD98F00E93E66 /* NotificationChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 71C02B372B7BD98F00E93E66 /* NotificationChangeLog.txt */; };
|
||||
71FC86DA2B96F44C00700965 /* PaginationButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71FC86D92B96F44C00700965 /* PaginationButton.swift */; };
|
||||
71FC86DC2B96F4C800700965 /* PaginationCellItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71FC86DB2B96F4C800700965 /* PaginationCellItem.swift */; };
|
||||
71FC86DE2B9738B900700965 /* SurfaceConfigurationValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71FC86DD2B9738B900700965 /* SurfaceConfigurationValue.swift */; };
|
||||
71FC86E02B973AE500700965 /* DropShadowConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71FC86DF2B973AE500700965 /* DropShadowConfiguration.swift */; };
|
||||
71FC86E22B97483000700965 /* Clamping.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71FC86E12B97483000700965 /* Clamping.swift */; };
|
||||
71FC86E42B9841AC00700965 /* PaginationFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71FC86E32B9841AC00700965 /* PaginationFlowLayout.swift */; };
|
||||
EA0B18022A9E236900F2D0CD /* SelectorGroupBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA0B18012A9E236900F2D0CD /* SelectorGroupBase.swift */; };
|
||||
EA0B18052A9E2D2D00F2D0CD /* SelectorBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA0B18032A9E2D2D00F2D0CD /* SelectorBase.swift */; };
|
||||
EA0B18062A9E2D2D00F2D0CD /* SelectorItemBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA0B18042A9E2D2D00F2D0CD /* SelectorItemBase.swift */; };
|
||||
@ -102,6 +118,8 @@
|
||||
EAA5EEF128F5C909003B3210 /* VDSColorTokens.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = EAA5EEED28F5C908003B3210 /* VDSColorTokens.xcframework */; };
|
||||
EAA5EEF328F5C909003B3210 /* VDSFormControlsTokens.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = EAA5EEEE28F5C908003B3210 /* VDSFormControlsTokens.xcframework */; };
|
||||
EAA7456C2AB23E2000C1841F /* TooltipModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA7456B2AB23E2000C1841F /* TooltipModel.swift */; };
|
||||
EAACB8982B92706F006A3869 /* DefaultValuing.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAACB8972B92706F006A3869 /* DefaultValuing.swift */; };
|
||||
EAACB89A2B927108006A3869 /* Valuing.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAACB8992B927108006A3869 /* Valuing.swift */; };
|
||||
EAB1D29C28A5618900DAE764 /* RadioButtonGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB1D29B28A5618900DAE764 /* RadioButtonGroup.swift */; };
|
||||
EAB1D2CD28ABE76100DAE764 /* Withable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB1D2CC28ABE76000DAE764 /* Withable.swift */; };
|
||||
EAB1D2CF28ABEF2B00DAE764 /* Typography+Base.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB1D2CE28ABEF2B00DAE764 /* Typography+Base.swift */; };
|
||||
@ -132,6 +150,7 @@
|
||||
EAD068922A560B65002E3A2D /* LoaderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAD068912A560B65002E3A2D /* LoaderViewController.swift */; };
|
||||
EAD068942A560C13002E3A2D /* LoaderLaunchable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAD068932A560C13002E3A2D /* LoaderLaunchable.swift */; };
|
||||
EAD8D2C128BFDE8B006EB6A6 /* UIGestureRecognizer+Publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAD8D2C028BFDE8B006EB6A6 /* UIGestureRecognizer+Publisher.swift */; };
|
||||
EAE785312BA0A438009428EA /* UIImage+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAE785302BA0A438009428EA /* UIImage+Helper.swift */; };
|
||||
EAEEEC922B1F807300531FC2 /* BadgeChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = EAEEEC912B1F807300531FC2 /* BadgeChangeLog.txt */; };
|
||||
EAEEEC962B1F893B00531FC2 /* ButtonChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = EAEEEC952B1F893B00531FC2 /* ButtonChangeLog.txt */; };
|
||||
EAEEEC982B1F8DD100531FC2 /* LineChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = EAEEEC972B1F8DD100531FC2 /* LineChangeLog.txt */; };
|
||||
@ -177,19 +196,35 @@
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
1832AC562BA0791D008AE476 /* BreadcrumbCellItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreadcrumbCellItem.swift; sourceTree = "<group>"; };
|
||||
18450CF02BA1B19C009FDF2A /* BreadcrumbsChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = BreadcrumbsChangeLog.txt; sourceTree = "<group>"; };
|
||||
1855EC652BAABF2A002ACAC2 /* BreadcrumbItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreadcrumbItemModel.swift; sourceTree = "<group>"; };
|
||||
1808BEBB2BA41C3200129230 /* CarouselScrollbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselScrollbar.swift; sourceTree = "<group>"; };
|
||||
1808BEBF2BA456B700129230 /* CarouselScrollbarChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = CarouselScrollbarChangeLog.txt; sourceTree = "<group>"; };
|
||||
186B2A892B88DA7F001AB71F /* TextAreaChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = TextAreaChangeLog.txt; sourceTree = "<group>"; };
|
||||
18792A8F2B7431F2008C0D29 /* ButtonIconBadgeIndicatorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonIconBadgeIndicatorModel.swift; sourceTree = "<group>"; };
|
||||
18A65A012B96E848006602CC /* Breadcrumbs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Breadcrumbs.swift; sourceTree = "<group>"; };
|
||||
18A65A032B96F050006602CC /* BreadcrumbItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreadcrumbItem.swift; sourceTree = "<group>"; };
|
||||
18BDEE812B75316E00452358 /* ButtonIconChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ButtonIconChangeLog.txt; sourceTree = "<group>"; };
|
||||
445BA07729C07B3D0036A7C5 /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = "<group>"; };
|
||||
44604AD329CE186A00E62B51 /* NotificationButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationButtonModel.swift; sourceTree = "<group>"; };
|
||||
44604AD629CE196600E62B51 /* Line.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Line.swift; sourceTree = "<group>"; };
|
||||
5F21D7BE28DCEB3D003E7CD6 /* Useable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Useable.swift; sourceTree = "<group>"; };
|
||||
5FC35BE228D51405004EBEAC /* Button.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Button.swift; sourceTree = "<group>"; };
|
||||
710607942B91A99500F2863F /* TitleletChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = TitleletChangeLog.txt; sourceTree = "<group>"; };
|
||||
7115BD3B2B84C0C200E0A610 /* TileContainerChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = TileContainerChangeLog.txt; sourceTree = "<group>"; };
|
||||
71ACE89B2BA0451200FB6ADC /* PaginationContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationContainer.swift; sourceTree = "<group>"; };
|
||||
71ACE89D2BA1CC1700FB6ADC /* TiletEyebrowModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TiletEyebrowModel.swift; sourceTree = "<group>"; };
|
||||
71B23C2C2B91FA690027F7D9 /* Pagination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pagination.swift; sourceTree = "<group>"; };
|
||||
71B5FCBA2B95A0CA00269BCC /* PaginationChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = PaginationChangeLog.txt; sourceTree = "<group>"; };
|
||||
71BFA7092B7F70E6000DCE33 /* DropShadowable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropShadowable.swift; sourceTree = "<group>"; };
|
||||
71C02B372B7BD98F00E93E66 /* NotificationChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = NotificationChangeLog.txt; sourceTree = "<group>"; };
|
||||
71FC86D92B96F44C00700965 /* PaginationButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationButton.swift; sourceTree = "<group>"; };
|
||||
71FC86DB2B96F4C800700965 /* PaginationCellItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationCellItem.swift; sourceTree = "<group>"; };
|
||||
71FC86DD2B9738B900700965 /* SurfaceConfigurationValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceConfigurationValue.swift; sourceTree = "<group>"; };
|
||||
71FC86DF2B973AE500700965 /* DropShadowConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropShadowConfiguration.swift; sourceTree = "<group>"; };
|
||||
71FC86E12B97483000700965 /* Clamping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Clamping.swift; sourceTree = "<group>"; };
|
||||
71FC86E32B9841AC00700965 /* PaginationFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationFlowLayout.swift; sourceTree = "<group>"; };
|
||||
EA0B18012A9E236900F2D0CD /* SelectorGroupBase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectorGroupBase.swift; sourceTree = "<group>"; };
|
||||
EA0B18032A9E2D2D00F2D0CD /* SelectorBase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectorBase.swift; sourceTree = "<group>"; };
|
||||
EA0B18042A9E2D2D00F2D0CD /* SelectorItemBase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectorItemBase.swift; sourceTree = "<group>"; };
|
||||
@ -274,6 +309,8 @@
|
||||
EAA5EEED28F5C908003B3210 /* VDSColorTokens.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = VDSColorTokens.xcframework; path = ../SharedFrameworks/VDSColorTokens.xcframework; sourceTree = "<group>"; };
|
||||
EAA5EEEE28F5C908003B3210 /* VDSFormControlsTokens.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = VDSFormControlsTokens.xcframework; path = ../SharedFrameworks/VDSFormControlsTokens.xcframework; sourceTree = "<group>"; };
|
||||
EAA7456B2AB23E2000C1841F /* TooltipModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TooltipModel.swift; sourceTree = "<group>"; };
|
||||
EAACB8972B92706F006A3869 /* DefaultValuing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultValuing.swift; sourceTree = "<group>"; };
|
||||
EAACB8992B927108006A3869 /* Valuing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Valuing.swift; sourceTree = "<group>"; };
|
||||
EAB1D29B28A5618900DAE764 /* RadioButtonGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButtonGroup.swift; sourceTree = "<group>"; };
|
||||
EAB1D2CC28ABE76000DAE764 /* Withable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Withable.swift; sourceTree = "<group>"; };
|
||||
EAB1D2CE28ABEF2B00DAE764 /* Typography+Base.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Typography+Base.swift"; sourceTree = "<group>"; };
|
||||
@ -303,6 +340,7 @@
|
||||
EAD068912A560B65002E3A2D /* LoaderViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoaderViewController.swift; sourceTree = "<group>"; };
|
||||
EAD068932A560C13002E3A2D /* LoaderLaunchable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoaderLaunchable.swift; sourceTree = "<group>"; };
|
||||
EAD8D2C028BFDE8B006EB6A6 /* UIGestureRecognizer+Publisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIGestureRecognizer+Publisher.swift"; sourceTree = "<group>"; };
|
||||
EAE785302BA0A438009428EA /* UIImage+Helper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImage+Helper.swift"; sourceTree = "<group>"; };
|
||||
EAEEEC912B1F807300531FC2 /* BadgeChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = BadgeChangeLog.txt; sourceTree = "<group>"; };
|
||||
EAEEEC952B1F893B00531FC2 /* ButtonChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ButtonChangeLog.txt; sourceTree = "<group>"; };
|
||||
EAEEEC972B1F8DD100531FC2 /* LineChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = LineChangeLog.txt; sourceTree = "<group>"; };
|
||||
@ -360,6 +398,27 @@
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
18A65A002B96E7E1006602CC /* Breadcrumbs */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
18A65A012B96E848006602CC /* Breadcrumbs.swift */,
|
||||
18A65A032B96F050006602CC /* BreadcrumbItem.swift */,
|
||||
1855EC652BAABF2A002ACAC2 /* BreadcrumbItemModel.swift */,
|
||||
1832AC562BA0791D008AE476 /* BreadcrumbCellItem.swift */,
|
||||
18450CF02BA1B19C009FDF2A /* BreadcrumbsChangeLog.txt */,
|
||||
);
|
||||
path = Breadcrumbs;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
1808BEBA2BA41B1D00129230 /* CarouselScrollbar */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1808BEBB2BA41C3200129230 /* CarouselScrollbar.swift */,
|
||||
1808BEBF2BA456B700129230 /* CarouselScrollbarChangeLog.txt */,
|
||||
);
|
||||
path = CarouselScrollbar;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
445BA07629C07ABA0036A7C5 /* Notification */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -388,6 +447,19 @@
|
||||
path = Button;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
71B23C2B2B91FA510027F7D9 /* Pagination */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
71B23C2C2B91FA690027F7D9 /* Pagination.swift */,
|
||||
71ACE89B2BA0451200FB6ADC /* PaginationContainer.swift */,
|
||||
71FC86D92B96F44C00700965 /* PaginationButton.swift */,
|
||||
71FC86DB2B96F4C800700965 /* PaginationCellItem.swift */,
|
||||
71FC86E32B9841AC00700965 /* PaginationFlowLayout.swift */,
|
||||
71B5FCBA2B95A0CA00269BCC /* PaginationChangeLog.txt */,
|
||||
);
|
||||
path = Pagination;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EA0B17FF2A9E21CA00F2D0CD /* Selector */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -493,13 +565,16 @@
|
||||
children = (
|
||||
EA4DB2FE28DCBC1900103EE3 /* Badge */,
|
||||
EAD062AE2A3B87210015965D /* BadgeIndicator */,
|
||||
18A65A002B96E7E1006602CC /* Breadcrumbs */,
|
||||
EA0FC2BE2912D18200DF80B4 /* Buttons */,
|
||||
1808BEBA2BA41B1D00129230 /* CarouselScrollbar */,
|
||||
EAF7F092289985E200B287F5 /* Checkbox */,
|
||||
EA985BF3296C609E00F2FF2E /* Icon */,
|
||||
EA3362412892EF700071C351 /* Label */,
|
||||
44604AD529CE195300E62B51 /* Line */,
|
||||
EAD0688C2A55F801002E3A2D /* Loader */,
|
||||
445BA07629C07ABA0036A7C5 /* Notification */,
|
||||
71B23C2B2B91FA510027F7D9 /* Pagination */,
|
||||
EA89200B28B530F0006B9984 /* RadioBox */,
|
||||
EAF7F11428A1470D00B287F5 /* RadioButton */,
|
||||
EA596ABB2A16B4D500300C4B /* Tabs */,
|
||||
@ -536,6 +611,7 @@
|
||||
EA81410F2A127066004F60D2 /* UIColor+VDSColor.swift */,
|
||||
EA33623D2892EE950071C351 /* UIDevice.swift */,
|
||||
EA0B18092AA78F9000F2D0CD /* UIEdgeInsets.swift */,
|
||||
EAE785302BA0A438009428EA /* UIImage+Helper.swift */,
|
||||
EAF7F0B6289C12A600B287F5 /* UITapGestureRecognizer.swift */,
|
||||
EA8E40902A7D3F6300934ED3 /* UIView+Accessibility.swift */,
|
||||
EAB5FED329267EB300998C17 /* UIView+NSLayoutConstraint.swift */,
|
||||
@ -555,6 +631,7 @@
|
||||
EAF1FE9A29DB1A6000101452 /* Changeable.swift */,
|
||||
EAF1FE9829D4850E00101452 /* Clickable.swift */,
|
||||
EAA5EEDF28F49DB3003B3210 /* Colorable.swift */,
|
||||
EAACB8972B92706F006A3869 /* DefaultValuing.swift */,
|
||||
EA3361A9288B25E40071C351 /* Disabling.swift */,
|
||||
EAF978202A99035B00C2FEA9 /* Enabling.swift */,
|
||||
EA5E305929510F8B0082B959 /* EnumSubset.swift */,
|
||||
@ -568,6 +645,7 @@
|
||||
EA3361B7288B2AAA0071C351 /* ViewProtocol.swift */,
|
||||
EAB1D2CC28ABE76000DAE764 /* Withable.swift */,
|
||||
5F21D7BE28DCEB3D003E7CD6 /* Useable.swift */,
|
||||
EAACB8992B927108006A3869 /* Valuing.swift */,
|
||||
71BFA7092B7F70E6000DCE33 /* DropShadowable.swift */,
|
||||
);
|
||||
path = Protocols;
|
||||
@ -587,6 +665,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EA3361BC288B2C760071C351 /* TypeAlias.swift */,
|
||||
71FC86E12B97483000700965 /* Clamping.swift */,
|
||||
71FC86DD2B9738B900700965 /* SurfaceConfigurationValue.swift */,
|
||||
71FC86DF2B973AE500700965 /* DropShadowConfiguration.swift */,
|
||||
);
|
||||
@ -683,9 +762,11 @@
|
||||
children = (
|
||||
EA5E3057295105A40082B959 /* Tilelet.swift */,
|
||||
EA985BE529688F6A00F2FF2E /* TileletBadgeModel.swift */,
|
||||
EA985BE929689B6D00F2FF2E /* TileletSubTitleModel.swift */,
|
||||
71ACE89D2BA1CC1700FB6ADC /* TiletEyebrowModel.swift */,
|
||||
EA985BE72968951C00F2FF2E /* TileletTitleModel.swift */,
|
||||
EA985BE929689B6D00F2FF2E /* TileletSubTitleModel.swift */,
|
||||
EA985C2C296F03FE00F2FF2E /* TileletIconModels.swift */,
|
||||
710607942B91A99500F2863F /* TitleletChangeLog.txt */,
|
||||
);
|
||||
path = Tilelet;
|
||||
sourceTree = "<group>";
|
||||
@ -960,22 +1041,26 @@
|
||||
EAEEECA72B1F952000531FC2 /* TabsChangeLog.txt in Resources */,
|
||||
186B2A8A2B88DA7F001AB71F /* TextAreaChangeLog.txt in Resources */,
|
||||
EAEEEC962B1F893B00531FC2 /* ButtonChangeLog.txt in Resources */,
|
||||
710607952B91A99500F2863F /* TitleletChangeLog.txt in Resources */,
|
||||
EA5F86CC2A1D28B500BC83E4 /* ReleaseNotes.txt in Resources */,
|
||||
EAEEEC982B1F8DD100531FC2 /* LineChangeLog.txt in Resources */,
|
||||
EAEEECA22B1F92AD00531FC2 /* LabelChangeLog.txt in Resources */,
|
||||
EA3362072891E14D0071C351 /* VerizonNHGeDS-Regular.otf in Resources */,
|
||||
EAEEEC9A2B1F8E4400531FC2 /* TextLinkChangeLog.txt in Resources */,
|
||||
1808BEC02BA456B700129230 /* CarouselScrollbarChangeLog.txt in Resources */,
|
||||
EAEEECAF2B1FC2BA00531FC2 /* ToggleViewChangeLog.txt in Resources */,
|
||||
EAEEEC922B1F807300531FC2 /* BadgeChangeLog.txt in Resources */,
|
||||
EAEEEC9E2B1F8F7700531FC2 /* ButtonGroupChangeLog.txt in Resources */,
|
||||
18BDEE822B75316E00452358 /* ButtonIconChangeLog.txt in Resources */,
|
||||
EA3362062891E14D0071C351 /* VerizonNHGeTX-Regular.otf in Resources */,
|
||||
EA3362052891E14D0071C351 /* VerizonNHGeDS-Bold.otf in Resources */,
|
||||
18450CF12BA1B19C009FDF2A /* BreadcrumbsChangeLog.txt in Resources */,
|
||||
EAEEECA02B1F908200531FC2 /* BadgeIndicatorChangeLog.txt in Resources */,
|
||||
EAA5EEB928ECD24B003B3210 /* Icons.xcassets in Resources */,
|
||||
EAEEECA92B1F969700531FC2 /* TooltipChangeLog.txt in Resources */,
|
||||
EAEEEC9C2B1F8F0700531FC2 /* TextLinkCaretChangeLog.txt in Resources */,
|
||||
EAA5EEE428F5B855003B3210 /* VerizonNHGDS-Light.otf in Resources */,
|
||||
71B5FCBB2B95A0CA00269BCC /* PaginationChangeLog.txt in Resources */,
|
||||
EAEEECAD2B1FC1A600531FC2 /* TitleLockupChangeLog.txt in Resources */,
|
||||
EAEEECAB2B1FBF2A00531FC2 /* ToggleChangeLog.txt in Resources */,
|
||||
);
|
||||
@ -1000,22 +1085,29 @@
|
||||
EAF7F0A6289B0CE000B287F5 /* Resetable.swift in Sources */,
|
||||
EA985C2D296F03FE00F2FF2E /* TileletIconModels.swift in Sources */,
|
||||
EA89200428AECF4B006B9984 /* UITextField+Publisher.swift in Sources */,
|
||||
18A65A022B96E848006602CC /* Breadcrumbs.swift in Sources */,
|
||||
EA0D1C3F2A6AD5E200E5C127 /* Typography+ContentSizeCategory.swift in Sources */,
|
||||
EA5F86C82A1BD99100BC83E4 /* TabModel.swift in Sources */,
|
||||
EA0D1C432A6AD70900E5C127 /* VDSTypography.swift in Sources */,
|
||||
EA297A5729FB0A360031ED56 /* AppleGuidelinesTouchable.swift in Sources */,
|
||||
1832AC572BA0791D008AE476 /* BreadcrumbCellItem.swift in Sources */,
|
||||
EA3361C328902D960071C351 /* Toggle.swift in Sources */,
|
||||
EAF7F0A0289AB7EC00B287F5 /* View.swift in Sources */,
|
||||
EA89201328B568D8006B9984 /* RadioBoxItem.swift in Sources */,
|
||||
71FC86E42B9841AC00700965 /* PaginationFlowLayout.swift in Sources */,
|
||||
EAC9258C2911C9DE00091998 /* InputField.swift in Sources */,
|
||||
EA3362402892EF6C0071C351 /* Label.swift in Sources */,
|
||||
EAB2376229E9880400AABE9A /* TrailingTooltipLabel.swift in Sources */,
|
||||
EAACB8982B92706F006A3869 /* DefaultValuing.swift in Sources */,
|
||||
EAB2376A29E9E59100AABE9A /* TooltipLaunchable.swift in Sources */,
|
||||
18A65A042B96F050006602CC /* BreadcrumbItem.swift in Sources */,
|
||||
EAB2375D29E8789100AABE9A /* Tooltip.swift in Sources */,
|
||||
71BFA70A2B7F70E6000DCE33 /* DropShadowable.swift in Sources */,
|
||||
EA0D1C452A6AD73000E5C127 /* RawRepresentable.swift in Sources */,
|
||||
EA985C23296E033A00F2FF2E /* TextArea.swift in Sources */,
|
||||
71FC86E22B97483000700965 /* Clamping.swift in Sources */,
|
||||
EAF7F0B3289B1ADC00B287F5 /* ActionLabelAttribute.swift in Sources */,
|
||||
1855EC662BAABF2A002ACAC2 /* BreadcrumbItemModel.swift in Sources */,
|
||||
EAC925832911B35400091998 /* TextLinkCaret.swift in Sources */,
|
||||
EA33622E2891EA3C0071C351 /* DispatchQueue+Once.swift in Sources */,
|
||||
EA4DB2FD28D3D0CA00103EE3 /* AnyEquatable.swift in Sources */,
|
||||
@ -1025,15 +1117,20 @@
|
||||
EAF7F0AF289B144C00B287F5 /* UnderlineLabelAttribute.swift in Sources */,
|
||||
EA0D1C412A6AD61C00E5C127 /* Typography+Additional.swift in Sources */,
|
||||
EAC925842911C63100091998 /* Colorable.swift in Sources */,
|
||||
EAACB89A2B927108006A3869 /* Valuing.swift in Sources */,
|
||||
EAE785312BA0A438009428EA /* UIImage+Helper.swift in Sources */,
|
||||
EAB5FEF5292D371F00998C17 /* ButtonBase.swift in Sources */,
|
||||
EA978EC5291D6AFE00ACC883 /* AnyLabelAttribute.swift in Sources */,
|
||||
71ACE89C2BA0451200FB6ADC /* PaginationContainer.swift in Sources */,
|
||||
EAC71A1F2A2E173D00E47A9F /* RadioButton.swift in Sources */,
|
||||
EA33622C2891E73B0071C351 /* FontProtocol.swift in Sources */,
|
||||
EA596ABD2A16B4EC00300C4B /* Tab.swift in Sources */,
|
||||
71ACE89E2BA1CC1700FB6ADC /* TiletEyebrowModel.swift in Sources */,
|
||||
EAF7F11728A1475A00B287F5 /* RadioButtonItem.swift in Sources */,
|
||||
EA985BEE2968A92400F2FF2E /* TitleLockupSubTitleModel.swift in Sources */,
|
||||
EA985BF22968B5BB00F2FF2E /* TitleLockupTextStyle.swift in Sources */,
|
||||
EAB1D2CD28ABE76100DAE764 /* Withable.swift in Sources */,
|
||||
71FC86DC2B96F4C800700965 /* PaginationCellItem.swift in Sources */,
|
||||
EAC846F3294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift in Sources */,
|
||||
EAF7F0952899861000B287F5 /* CheckboxItem.swift in Sources */,
|
||||
EA985BE82968951C00F2FF2E /* TileletTitleModel.swift in Sources */,
|
||||
@ -1051,6 +1148,7 @@
|
||||
EAC9258F2911C9DE00091998 /* EntryFieldBase.swift in Sources */,
|
||||
EAB1D2EA28AE84AA00DAE764 /* UIControlPublisher.swift in Sources */,
|
||||
EAD068922A560B65002E3A2D /* LoaderViewController.swift in Sources */,
|
||||
71FC86DA2B96F44C00700965 /* PaginationButton.swift in Sources */,
|
||||
EABFEB642A26473700C4C106 /* NSAttributedString.swift in Sources */,
|
||||
EAF7F13328A2A16500B287F5 /* AttachmentLabelAttributeModel.swift in Sources */,
|
||||
EA0FC2C62914222900DF80B4 /* ButtonGroup.swift in Sources */,
|
||||
@ -1058,6 +1156,7 @@
|
||||
EA8E40932A82889500934ED3 /* TooltipDialog.swift in Sources */,
|
||||
44604AD429CE186A00E62B51 /* NotificationButtonModel.swift in Sources */,
|
||||
EAD8D2C128BFDE8B006EB6A6 /* UIGestureRecognizer+Publisher.swift in Sources */,
|
||||
71B23C2D2B91FA690027F7D9 /* Pagination.swift in Sources */,
|
||||
EA0D1C372A681CCE00E5C127 /* ToggleView.swift in Sources */,
|
||||
EAF7F0B9289C139800B287F5 /* ColorConfiguration.swift in Sources */,
|
||||
EA3361BD288B2C760071C351 /* TypeAlias.swift in Sources */,
|
||||
@ -1078,6 +1177,7 @@
|
||||
EA3361AF288B26310071C351 /* FormFieldable.swift in Sources */,
|
||||
EA513A952A4E1F82002A4DFF /* TitleLockupStyleConfiguration.swift in Sources */,
|
||||
44604AD729CE196600E62B51 /* Line.swift in Sources */,
|
||||
1808BEBC2BA41C3200129230 /* CarouselScrollbar.swift in Sources */,
|
||||
EAF978212A99035B00C2FEA9 /* Enabling.swift in Sources */,
|
||||
EA5E3058295105A40082B959 /* Tilelet.swift in Sources */,
|
||||
EA89201528B56CF4006B9984 /* RadioBoxGroup.swift in Sources */,
|
||||
|
||||
@ -102,7 +102,6 @@ open class SelectorItemBase<Selector: SelectorControlable>: Control, Errorable,
|
||||
/// Instead of use labelText and labelTextAttirbutes, this is a fully baked NSAttributedString with both text and attributes.
|
||||
open var labelAttributedText: NSAttributedString? {
|
||||
didSet {
|
||||
label.useAttributedText = !(labelAttributedText?.string.isEmpty ?? true)
|
||||
label.attributedText = labelAttributedText
|
||||
setNeedsUpdate()
|
||||
}
|
||||
@ -117,7 +116,6 @@ open class SelectorItemBase<Selector: SelectorControlable>: Control, Errorable,
|
||||
/// Instead of use childText and childTextAttirbutes, this is a fully baked NSAttributedString with both text and attributes.
|
||||
open var childAttributedText: NSAttributedString? {
|
||||
didSet {
|
||||
childLabel.useAttributedText = !(childAttributedText?.string.isEmpty ?? true)
|
||||
childLabel.attributedText = childAttributedText
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
@ -34,10 +34,17 @@ public final class SelfSizingCollectionView: UICollectionView {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Properties
|
||||
//--------------------------------------------------
|
||||
private var contentSizeObservation: NSKeyValueObservation?
|
||||
private var collectionViewHeight: NSLayoutConstraint?
|
||||
private var anyCancellable: AnyCancellable?
|
||||
|
||||
private var contentSizeSubject = CurrentValueSubject<CGSize, Never>(.zero)
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
public var contentSizePublisher: AnyPublisher<CGSize, Never> {
|
||||
contentSizeSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
@ -45,7 +52,6 @@ public final class SelfSizingCollectionView: UICollectionView {
|
||||
/// The natural size for the receiving view, considering only properties of the view itself.
|
||||
public override var intrinsicContentSize: CGSize {
|
||||
let contentSize = self.contentSize
|
||||
//print(#function, contentSize)
|
||||
return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height)
|
||||
}
|
||||
|
||||
@ -67,18 +73,17 @@ public final class SelfSizingCollectionView: UICollectionView {
|
||||
//ensure autoLayout uses intrinsic height
|
||||
setContentHuggingPriority(.required, for: .vertical)
|
||||
setContentCompressionResistancePriority(.required, for: .vertical)
|
||||
collectionViewHeight = heightAnchor.constraint(equalToConstant: 0).activate()
|
||||
|
||||
// Observing the value of contentSize seems to be the only reliable way to get the contentSize after the collection view lays out its subviews.
|
||||
self.contentSizeObservation = self.observe(\.contentSize, options: [.old, .new]) { [weak self] _, change in
|
||||
// If we don't specify `options: [.old, .new]`, the change.oldValue and .newValue will always be `nil`.
|
||||
if change.newValue != change.oldValue {
|
||||
self?.invalidateIntrinsicContentSize()
|
||||
if let height = change.newValue?.height {
|
||||
self?.collectionViewHeight?.constant = height
|
||||
collectionViewHeight = height(constant: 0, priority: .defaultHigh)
|
||||
|
||||
anyCancellable = self.publisher(for: \.contentSize, options: [.new])
|
||||
.sink { [weak self] compare in
|
||||
guard let self else { return }
|
||||
if compare.height != self.collectionViewHeight?.constant {
|
||||
self.invalidateIntrinsicContentSize()
|
||||
self.collectionViewHeight?.constant = compare.height
|
||||
self.contentSizeSubject.send(compare)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -147,6 +147,7 @@ open class Badge: View {
|
||||
|
||||
label.widthGreaterThanEqualTo(constant: minWidth)
|
||||
maxWidthConstraint = label.widthLessThanEqualTo(constant: 0).with { $0.isActive = false }
|
||||
clipsToBounds = true
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
|
||||
90
VDS/Components/Breadcrumbs/BreadcrumbCellItem.swift
Normal file
90
VDS/Components/Breadcrumbs/BreadcrumbCellItem.swift
Normal file
@ -0,0 +1,90 @@
|
||||
//
|
||||
// BreadcrumbCellItem.swift
|
||||
// VDS
|
||||
//
|
||||
// Created by Kanamarlapudi, Vasavi on 11/03/24.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import VDSColorTokens
|
||||
|
||||
///This is customised view for Breadcrumb cell item
|
||||
final class BreadcrumbCellItem: UICollectionViewCell {
|
||||
|
||||
///Identifier for the BreadcrumbCellItem
|
||||
static let identifier: String = String(describing: BreadcrumbCellItem.self)
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Properties
|
||||
//--------------------------------------------------
|
||||
internal var stackView: UIStackView = {
|
||||
return UIStackView().with {
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
$0.axis = .horizontal
|
||||
$0.distribution = .fill
|
||||
$0.alignment = .fill
|
||||
$0.spacing = VDSLayout.Spacing.space1X.value
|
||||
$0.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
|
||||
$0.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
||||
}
|
||||
}()
|
||||
|
||||
internal var breadCrumbItem: BreadcrumbItem?
|
||||
|
||||
///separator label
|
||||
private var separator: Label = Label().with {
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
$0.textAlignment = .left
|
||||
$0.numberOfLines = 1
|
||||
$0.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
|
||||
$0.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
||||
$0.text = "/"
|
||||
}
|
||||
|
||||
private let textColorConfiguration = 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()
|
||||
}
|
||||
|
||||
///Configuring the cell with default setup
|
||||
private func setUp() {
|
||||
separator.textColorConfiguration = textColorConfiguration.eraseToAnyColorable()
|
||||
contentView.addSubview(stackView)
|
||||
stackView.pinToSuperView()
|
||||
separator.backgroundColor = .clear
|
||||
}
|
||||
|
||||
///Updating the breadCrumbItem and UI based on the selected flag along with the surface
|
||||
func update(surface: Surface, hideSlash: Bool, breadCrumbItem: BreadcrumbItem) {
|
||||
//update surface
|
||||
separator.surface = surface
|
||||
breadCrumbItem.surface = surface
|
||||
breadCrumbItem.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
breadCrumbItem.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||
|
||||
//remove previous views
|
||||
stackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||
|
||||
//add to stack
|
||||
stackView.addArrangedSubview(separator)
|
||||
stackView.addArrangedSubview(breadCrumbItem)
|
||||
stackView.setCustomSpacing(VDSLayout.Spacing.space1X.value, after: separator)
|
||||
|
||||
//update separator
|
||||
separator.textColor = textColorConfiguration.getColor(surface)
|
||||
separator.isHidden = hideSlash
|
||||
self.breadCrumbItem = breadCrumbItem
|
||||
layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
102
VDS/Components/Breadcrumbs/BreadcrumbItem.swift
Normal file
102
VDS/Components/Breadcrumbs/BreadcrumbItem.swift
Normal file
@ -0,0 +1,102 @@
|
||||
//
|
||||
// BreadcrumbItem.swift
|
||||
// VDS
|
||||
//
|
||||
// Created by Kanamarlapudi, Vasavi on 05/03/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import VDSColorTokens
|
||||
import VDSFormControlsTokens
|
||||
import Combine
|
||||
|
||||
/// A Breadcrumb Item contains href(link) and selected flag.
|
||||
/// Breadcrumb links to its respective page if it is not disabled.
|
||||
/// Breadcrumb contains text with a separator by default, highlights text in bold without a separator if selected.
|
||||
@objc (VDSBreadcrumbItem)
|
||||
open class BreadcrumbItem: ButtonBase {
|
||||
|
||||
//--------------------------------------------------
|
||||
// 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
|
||||
//--------------------------------------------------
|
||||
/// TextStyle used on the titleLabel.
|
||||
open override var textStyle: TextStyle { isSelected ? TextStyle.boldBodySmall : TextStyle.bodySmall }
|
||||
|
||||
/// UIColor used on the titleLabel text.
|
||||
open override var textColor: UIColor {
|
||||
textColorConfiguration.getColor(self)
|
||||
}
|
||||
|
||||
/// The natural size for the receiving view, considering only properties of the view itself.
|
||||
open override var intrinsicContentSize: CGSize {
|
||||
guard let titleLabel else { return super.intrinsicContentSize }
|
||||
// Calculate the titleLabel's intrinsic content size
|
||||
let labelSize = titleLabel.sizeThatFits(CGSize(width: self.frame.width, height: CGFloat.greatestFiniteMagnitude))
|
||||
// Adjust the size if needed (add any additional padding if your design requires)
|
||||
let adjustedSize = CGSize(width: labelSize.width + contentEdgeInsets.left + contentEdgeInsets.right,
|
||||
height: labelSize.height + contentEdgeInsets.top + contentEdgeInsets.bottom)
|
||||
return adjustedSize
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Properties
|
||||
//--------------------------------------------------
|
||||
private var textColorConfiguration = ControlColorConfiguration().with {
|
||||
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .normal)
|
||||
$0.setSurfaceColors(VDSColor.interactiveActiveOnlight, VDSColor.interactiveActiveOndark, forState: .disabled)
|
||||
$0.setSurfaceColors(VDSColor.interactiveActiveOnlight, VDSColor.interactiveActiveOndark, forState: .highlighted)
|
||||
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .selected)
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// 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()
|
||||
isAccessibilityElement = true
|
||||
accessibilityTraits = .link
|
||||
contentHorizontalAlignment = .leading
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
open override func updateView() {
|
||||
//always call last so the label is rendered
|
||||
super.updateView()
|
||||
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
shouldUpdateView = false
|
||||
text = nil
|
||||
accessibilityCustomActions = []
|
||||
isAccessibilityElement = true
|
||||
accessibilityTraits = .button
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
/// Used to update any Accessibility properties.
|
||||
open override func updateAccessibility() {
|
||||
super.updateAccessibility()
|
||||
accessibilityLabel = "Breadcrumb \(text ?? "")"
|
||||
}
|
||||
|
||||
}
|
||||
32
VDS/Components/Breadcrumbs/BreadcrumbItemModel.swift
Normal file
32
VDS/Components/Breadcrumbs/BreadcrumbItemModel.swift
Normal file
@ -0,0 +1,32 @@
|
||||
//
|
||||
// BreadcrumbItemModel.swift
|
||||
// VDS
|
||||
//
|
||||
// Created by Kanamarlapudi, Vasavi on 20/03/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Breadcrumbs {
|
||||
public struct BreadcrumbItemModel {
|
||||
|
||||
///Text that goes in the breadcrumb item
|
||||
public var text: String
|
||||
|
||||
/// Whether the Item can be clicked.
|
||||
public var enabled: Bool
|
||||
|
||||
/// The Breadcrumb link to links to its respective page.
|
||||
public var selected: Bool
|
||||
|
||||
///Click event when you click on a breadcrumb item
|
||||
public var onClick: ((BreadcrumbItem) -> Void)?
|
||||
|
||||
public init(text: String, enabeled: Bool = true, selected: Bool = false, onClick: ((BreadcrumbItem) -> Void)? = nil) {
|
||||
self.text = text
|
||||
self.enabled = enabeled
|
||||
self.selected = selected
|
||||
self.onClick = onClick
|
||||
}
|
||||
}
|
||||
}
|
||||
169
VDS/Components/Breadcrumbs/Breadcrumbs.swift
Normal file
169
VDS/Components/Breadcrumbs/Breadcrumbs.swift
Normal file
@ -0,0 +1,169 @@
|
||||
//
|
||||
// Breadcrumbs.swift
|
||||
// VDS
|
||||
//
|
||||
// Created by Kanamarlapudi, Vasavi on 11/03/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import VDSColorTokens
|
||||
import Combine
|
||||
|
||||
/// A Breadcrumbs contains BreadcrumbItems.
|
||||
/// It contains Breadcrumb Item Default, Breadcrumb Item Selected, Separator.
|
||||
/// Breadcrumbs are secondary navigation that use a hierarchy of internal links to tell customers where they are in an experience. Each breadcrumb links to its respective page, except for that of current page.
|
||||
@objc(VDSBreadcrumbs)
|
||||
open class Breadcrumbs: View {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
/// Array of ``BreadcrumbItem`` views for the Breadcrumbs.
|
||||
open var breadcrumbs: [BreadcrumbItem] = [] { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// Array of ``BreadcurmbItemModel`` you are wanting to show.
|
||||
open var breadcrumbModels: [BreadcrumbItemModel] = [] { didSet { updateBreadcrumbItems() } }
|
||||
|
||||
/// Whether this object is enabled or not
|
||||
override open var isEnabled: Bool {
|
||||
didSet {
|
||||
breadcrumbs.forEach { $0.isEnabled = isEnabled }
|
||||
}
|
||||
}
|
||||
|
||||
/// Current Surface and this is used to pass down to child objects that implement Surfacable
|
||||
override open var surface: Surface {
|
||||
didSet {
|
||||
breadcrumbs.forEach { $0.surface = surface }
|
||||
}
|
||||
}
|
||||
|
||||
/// A callback when the selected item changes. Passes parameters (crumb).
|
||||
open var onBreadcrumbDidSelect: ((BreadcrumbItem) -> Void)?
|
||||
|
||||
/// A callback when the Tab determine if a item should be selected.
|
||||
open var onBreadcrumbShouldSelect:((BreadcrumbItem) -> Bool)?
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Properties
|
||||
//--------------------------------------------------
|
||||
fileprivate lazy var layout = ButtonGroupPositionLayout().with {
|
||||
$0.position = .left
|
||||
$0.delegate = self
|
||||
$0.axisSpacer = { _, _, _ in
|
||||
return VDSLayout.Spacing.space1X.value
|
||||
}
|
||||
$0.verticalSpacer = { _, _ in
|
||||
return VDSLayout.Spacing.space1X.value
|
||||
}
|
||||
}
|
||||
|
||||
///Collectionview to render Breadcrumb Items
|
||||
private lazy var collectionView: SelfSizingCollectionView = {
|
||||
let collectionView = SelfSizingCollectionView(frame: .zero, collectionViewLayout: layout)
|
||||
collectionView.isScrollEnabled = false
|
||||
collectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||
collectionView.delegate = self
|
||||
collectionView.dataSource = self
|
||||
collectionView.showsHorizontalScrollIndicator = false
|
||||
collectionView.showsVerticalScrollIndicator = false
|
||||
collectionView.register(BreadcrumbCellItem.self, forCellWithReuseIdentifier: BreadcrumbCellItem.identifier)
|
||||
collectionView.backgroundColor = .clear
|
||||
return collectionView
|
||||
}()
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Methods
|
||||
//--------------------------------------------------
|
||||
/// Removes all of the Breadcrumbs and creates new ones from the Breadcrumb Models property.
|
||||
private func updateBreadcrumbItems() {
|
||||
// Clear existing breadcrumbs
|
||||
for breadcrumbItem in breadcrumbs {
|
||||
breadcrumbItem.removeFromSuperview()
|
||||
}
|
||||
|
||||
// Create new breadcrumb items from the models
|
||||
breadcrumbs = breadcrumbModels.compactMap({ model in
|
||||
let breadcrumbItem = BreadcrumbItem()
|
||||
breadcrumbItem.text = model.text
|
||||
breadcrumbItem.isEnabled = model.enabled
|
||||
breadcrumbItem.isSelected = model.selected
|
||||
breadcrumbItem.onClick = { [weak self] breadcrumb in
|
||||
guard let self, breadcrumb.isEnabled else { return }
|
||||
if self.onBreadcrumbShouldSelect?(breadcrumb) ?? true {
|
||||
model.onClick?(breadcrumb)
|
||||
self.onBreadcrumbDidSelect?(breadcrumb)
|
||||
}
|
||||
}
|
||||
return breadcrumbItem
|
||||
})
|
||||
}
|
||||
|
||||
//------------------------------------------s--------
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
/// Executed on initialization for this View.
|
||||
open override func initialSetup() {
|
||||
super.initialSetup()
|
||||
addSubview(collectionView)
|
||||
collectionView.pinToSuperView()
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
shouldUpdateView = false
|
||||
breadcrumbs.forEach { $0.reset() }
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
open override func updateView() {
|
||||
super.updateView()
|
||||
collectionView.reloadData()
|
||||
}
|
||||
|
||||
open override func layoutSubviews() {
|
||||
//Turn off the ability to execute updateView() in the super
|
||||
//since we don't want an infinite loop
|
||||
shouldUpdateView = false
|
||||
super.layoutSubviews()
|
||||
shouldUpdateView = true
|
||||
|
||||
// Accounts for any collection size changes
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self else { return }
|
||||
self.collectionView.collectionViewLayout.invalidateLayout()
|
||||
}
|
||||
}
|
||||
private var separatorWidth = Label().with { $0.text = "/"; $0.sizeToFit() }.intrinsicContentSize.width
|
||||
}
|
||||
|
||||
extension Breadcrumbs: UICollectionViewDelegate, UICollectionViewDataSource, ButtongGroupPositionLayoutDelegate {
|
||||
//--------------------------------------------------
|
||||
// MARK: - UICollectionView Delegate & Datasource
|
||||
//--------------------------------------------------
|
||||
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
breadcrumbs.count
|
||||
}
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: BreadcrumbCellItem.identifier, for: indexPath) as? BreadcrumbCellItem else { return UICollectionViewCell() }
|
||||
let hideSlash = indexPath.row == 0
|
||||
cell.update(surface: surface, hideSlash: hideSlash, breadCrumbItem: breadcrumbs[indexPath.row])
|
||||
return cell
|
||||
}
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, sizeForItemAtIndexPath indexPath: IndexPath) -> CGSize {
|
||||
let breadcrumb = breadcrumbs[indexPath.row]
|
||||
let intrinsicSize = breadcrumb.intrinsicContentSize
|
||||
let separatorFullWidth: CGFloat = indexPath.row == 0 ? 0 : VDSLayout.Spacing.space1X.value + separatorWidth
|
||||
let cellwidth = intrinsicSize.width + separatorFullWidth
|
||||
return .init(width: min(cellwidth, collectionView.frame.width), height: intrinsicSize.height)
|
||||
}
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, buttonBaseAtIndexPath indexPath: IndexPath) -> ButtonBase {
|
||||
breadcrumbs[indexPath.row]
|
||||
}
|
||||
}
|
||||
36
VDS/Components/Breadcrumbs/BreadcrumbsChangeLog.txt
Normal file
36
VDS/Components/Breadcrumbs/BreadcrumbsChangeLog.txt
Normal file
@ -0,0 +1,36 @@
|
||||
MM/DD/YYYY
|
||||
----------------
|
||||
- Initial Brand 3.0 handoff
|
||||
|
||||
12/17/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
|
||||
|
||||
2/28/2022
|
||||
----------------
|
||||
- Changed Last Breadcrumb Item to Selected Item
|
||||
|
||||
03/08/2022
|
||||
----------------
|
||||
- Added dev note for Active and Hover states.
|
||||
|
||||
08/04/2022
|
||||
----------------
|
||||
- Updated default and inverted prop to light and dark surface.
|
||||
|
||||
11/30/2022
|
||||
----------------
|
||||
- Added "(web only)" to any instance of "keyboard focus"
|
||||
|
||||
12/13/2022
|
||||
----------------
|
||||
- Replaced focus border pixel and style & spacing values with tokens.
|
||||
|
||||
01/03/2022
|
||||
----------------
|
||||
- Updated Specs to use new SPEC Templates and SPEC DOC Components.
|
||||
|
||||
01/06/2023
|
||||
----------------
|
||||
- Tweaked anatomy element naming to align with design doc and dev doc
|
||||
@ -97,11 +97,15 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable {
|
||||
/// Whether the Control is enabled or not.
|
||||
open override var isEnabled: Bool { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// Whether the Control is selected or not.
|
||||
open override var isSelected: Bool { didSet { setNeedsUpdate() } }
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Lifecycle
|
||||
//--------------------------------------------------
|
||||
open func initialSetup() {
|
||||
if !initialSetupPerformed {
|
||||
initialSetupPerformed = true
|
||||
backgroundColor = .clear
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
accessibilityCustomActions = []
|
||||
@ -140,22 +144,13 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable {
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
override open var intrinsicContentSize: CGSize {
|
||||
let intrinsicContentSize = super.intrinsicContentSize
|
||||
let adjustedWidth = intrinsicContentSize.width + titleEdgeInsets.left + titleEdgeInsets.right
|
||||
let adjustedHeight = intrinsicContentSize.height + titleEdgeInsets.top + titleEdgeInsets.bottom
|
||||
return CGSize(width: adjustedWidth, height: adjustedHeight)
|
||||
}
|
||||
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Methods
|
||||
//--------------------------------------------------
|
||||
private func updateLabel() {
|
||||
|
||||
defer { invalidateIntrinsicContentSize() }
|
||||
|
||||
//clear the arrays holding actions
|
||||
accessibilityCustomActions = []
|
||||
if let text, !text.isEmpty {
|
||||
|
||||
@ -9,6 +9,7 @@ import Foundation
|
||||
import UIKit
|
||||
import VDSColorTokens
|
||||
import VDSFormControlsTokens
|
||||
import Combine
|
||||
|
||||
/// A button group contains combinations of related CTAs including ``Button``, ``TextLink``, and ``TextLinkCaret``. This group component controls a combination's orientation, spacing, size and allowable size pairings.
|
||||
@objc(VDSButtonGroup)
|
||||
@ -98,6 +99,8 @@ open class ButtonGroup: View {
|
||||
buttons.forEach { $0.surface = surface }
|
||||
}
|
||||
}
|
||||
|
||||
open var contentSizePublisher: AnyPublisher<CGSize, Never> { collectionView.contentSizePublisher }
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Properties
|
||||
@ -108,6 +111,7 @@ open class ButtonGroup: View {
|
||||
$0.delegate = self
|
||||
}
|
||||
|
||||
/// CollectionView that renders the array of buttonBase obects.
|
||||
fileprivate lazy var collectionView: SelfSizingCollectionView = {
|
||||
|
||||
return SelfSizingCollectionView(frame: .zero, collectionViewLayout: positionLayout).with {
|
||||
|
||||
@ -6,21 +6,18 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
struct ButtonGroupConstants {
|
||||
static let defaultSpace = 12.0
|
||||
|
||||
enum ButtonSpacingAxis {
|
||||
case horizontal, vertical
|
||||
}
|
||||
|
||||
/// This will determine the spacing that will go between 2 ButtonBases either horizontally or vertically
|
||||
/// - Parameters:
|
||||
/// - axis: horizontal/vertical
|
||||
/// - primary: first ButtonBase
|
||||
/// - neighboring: next ButtonBase based off of axis
|
||||
/// - Returns: float value
|
||||
static func getSpacing(for axis: ButtonSpacingAxis, with primary: ButtonBase, neighboring: ButtonBase) -> CGFloat {
|
||||
static func getSpacing(for axis: NSLayoutConstraint.Axis, with primary: ButtonBase, neighboring: ButtonBase) -> CGFloat {
|
||||
|
||||
//large button
|
||||
if let button = primary as? Button, button.size == .large {
|
||||
|
||||
@ -146,6 +146,8 @@ class ButtonLayoutAttributes: UICollectionViewLayoutAttributes{
|
||||
class ButtonGroupPositionLayout: UICollectionViewLayout {
|
||||
|
||||
weak var delegate: ButtongGroupPositionLayoutDelegate?
|
||||
var verticalSpacer: ((ButtonCollectionViewRow, ButtonCollectionViewRow?) -> CGFloat)?
|
||||
var axisSpacer: ((NSLayoutConstraint.Axis, ButtonBase, ButtonBase) -> CGFloat)?
|
||||
|
||||
// Total height of the content. Will be used to configure the scrollview content
|
||||
var layoutHeight: CGFloat = 0.0
|
||||
@ -154,7 +156,7 @@ class ButtonGroupPositionLayout: UICollectionViewLayout {
|
||||
var buttonPercentage: CGFloat?
|
||||
|
||||
private var itemCache: [ButtonLayoutAttributes] = []
|
||||
|
||||
|
||||
override func prepare() {
|
||||
super.prepare()
|
||||
|
||||
@ -226,7 +228,7 @@ class ButtonGroupPositionLayout: UICollectionViewLayout {
|
||||
let neighbor = delegate.collectionView(collectionView, buttonBaseAtIndexPath: IndexPath(item: nextItem, section: section))
|
||||
|
||||
// get the spacing to go between the current and next button
|
||||
itemSpacing = ButtonGroupConstants.getSpacing(for: .horizontal, with: itemButtonBase, neighboring: neighbor)
|
||||
itemSpacing = getAxisSpacing(for: .horizontal, with: itemButtonBase, neighboring: neighbor)
|
||||
}
|
||||
|
||||
// create the custom layout attribute
|
||||
@ -255,7 +257,7 @@ class ButtonGroupPositionLayout: UICollectionViewLayout {
|
||||
|
||||
if item > 0 {
|
||||
let prevRow = rows[item - 1]
|
||||
rowSpacing = ButtonGroupConstants.getVerticalSpacing(for: prevRow, neighboringRow: row)
|
||||
rowSpacing = getVerticalSpacing(for: prevRow, neighboringRow: row)
|
||||
row.rowY = layoutHeight + rowSpacing
|
||||
layoutHeight += rowSpacing
|
||||
}
|
||||
@ -300,5 +302,19 @@ class ButtonGroupPositionLayout: UICollectionViewLayout {
|
||||
}
|
||||
return collectionView.bounds.width
|
||||
}
|
||||
|
||||
private func getAxisSpacing(for axis: NSLayoutConstraint.Axis, with primary: ButtonBase, neighboring: ButtonBase) -> CGFloat {
|
||||
guard let axisSpacer else {
|
||||
return ButtonGroupConstants.getSpacing(for: axis, with: primary, neighboring: neighboring)
|
||||
}
|
||||
return axisSpacer(axis, primary, neighboring)
|
||||
}
|
||||
|
||||
private func getVerticalSpacing(for row: ButtonCollectionViewRow, neighboringRow: ButtonCollectionViewRow?) -> CGFloat {
|
||||
guard let verticalSpacer else {
|
||||
return ButtonGroupConstants.getVerticalSpacing(for: row, neighboringRow: neighboringRow)
|
||||
}
|
||||
return verticalSpacer(row, neighboringRow)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -75,7 +75,13 @@ open class TextLink: ButtonBase {
|
||||
|
||||
/// The natural size for the receiving view, considering only properties of the view itself.
|
||||
open override var intrinsicContentSize: CGSize {
|
||||
return titleLabel?.intrinsicContentSize ?? super.intrinsicContentSize
|
||||
guard let titleLabel else { return super.intrinsicContentSize }
|
||||
// Calculate the titleLabel's intrinsic content size
|
||||
let labelSize = titleLabel.sizeThatFits(CGSize(width: self.frame.width, height: CGFloat.greatestFiniteMagnitude))
|
||||
// Adjust the size if needed (add any additional padding if your design requires)
|
||||
let adjustedSize = CGSize(width: labelSize.width + contentEdgeInsets.left + contentEdgeInsets.right,
|
||||
height: labelSize.height + contentEdgeInsets.top + contentEdgeInsets.bottom)
|
||||
return adjustedSize
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
@ -87,6 +93,10 @@ open class TextLink: ButtonBase {
|
||||
isAccessibilityElement = true
|
||||
accessibilityTraits = .link
|
||||
|
||||
//left align titleLabel in case this is pinned leading/trailing
|
||||
//default is always set to center
|
||||
contentHorizontalAlignment = .left
|
||||
|
||||
if let titleLabel {
|
||||
addSubview(line)
|
||||
line.pinLeading(titleLabel.leadingAnchor)
|
||||
|
||||
@ -79,6 +79,11 @@ open class TextLinkCaret: ButtonBase {
|
||||
/// 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()
|
||||
|
||||
//left align titleLabel in case this is pinned leading/trailing
|
||||
//default is always set to center
|
||||
contentHorizontalAlignment = .left
|
||||
|
||||
accessibilityTraits = .link
|
||||
titleLabel?.numberOfLines = 0
|
||||
titleLabel?.lineBreakMode = .byWordWrapping
|
||||
@ -98,10 +103,23 @@ open class TextLinkCaret: ButtonBase {
|
||||
}
|
||||
|
||||
/// The natural size for the receiving view, considering only properties of the view itself.
|
||||
override open var intrinsicContentSize: CGSize {
|
||||
//get the labels size, if not the button
|
||||
return titleLabel?.intrinsicContentSize ?? super.intrinsicContentSize
|
||||
open override var intrinsicContentSize: CGSize {
|
||||
guard let titleLabel else { return super.intrinsicContentSize }
|
||||
// Calculate the titleLabel's intrinsic content size
|
||||
let labelSize = titleLabel.sizeThatFits(CGSize(width: self.frame.width - (contentEdgeInsets.left + contentEdgeInsets.right), height: CGFloat.greatestFiniteMagnitude))
|
||||
// Adjust the size if needed (add any additional padding if your design requires)
|
||||
let adjustedSize = CGSize(width: labelSize.width + contentEdgeInsets.left + contentEdgeInsets.right,
|
||||
height: labelSize.height + contentEdgeInsets.top + contentEdgeInsets.bottom)
|
||||
return adjustedSize
|
||||
}
|
||||
|
||||
open override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
// This ensures the titleLabel is correctly positioned within the button
|
||||
titleLabel?.preferredMaxLayoutWidth = self.frame.width - (contentEdgeInsets.left + contentEdgeInsets.right)
|
||||
super.layoutSubviews() // Calling super again to ensure layout is updated with preferredMaxLayoutWidth
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension TextLinkCaret {
|
||||
|
||||
436
VDS/Components/CarouselScrollbar/CarouselScrollbar.swift
Normal file
436
VDS/Components/CarouselScrollbar/CarouselScrollbar.swift
Normal file
@ -0,0 +1,436 @@
|
||||
//
|
||||
// CarouselScrollbar.swift
|
||||
// VDS
|
||||
//
|
||||
// Created by Kanamarlapudi, Vasavi on 12/03/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import VDSColorTokens
|
||||
import VDSFormControlsTokens
|
||||
import Combine
|
||||
|
||||
/// A carousel scrollbar is a control that allows to navigate between items in a carousel.
|
||||
/// It's also a status indicator that conveys the relative amount of content in a carousel and a location within it.
|
||||
@objc(VDSCarouselScrollbar)
|
||||
open class CarouselScrollbar: View {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
required public init() {
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
|
||||
public required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
/// Used to set total number of slides within carousel
|
||||
open var numberOfSlides: Int {
|
||||
get { return _numberOfSlides }
|
||||
set {
|
||||
_numberOfSlides = newValue
|
||||
setThumbWidth()
|
||||
scrollThumbToPosition(position)
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
/// The number of slides that can appear at once in a set in a carousel container.
|
||||
open var selectedLayout: Layout? {
|
||||
get { return _selectedLayout }
|
||||
set {
|
||||
if let newValue {
|
||||
_selectedLayout = newValue
|
||||
} else {
|
||||
_selectedLayout = .oneUP
|
||||
}
|
||||
setThumbWidth()
|
||||
scrollThumbToPosition(position)
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum used to describe the number of slides that can appear at once in a set in a carousel container.
|
||||
public enum Layout: String, CaseIterable {
|
||||
case oneUP = "1UP"
|
||||
case twoUP = "2UP"
|
||||
case threeUP = "3UP"
|
||||
case fourUP = "4UP"
|
||||
case fiveUP = "5UP"
|
||||
case sixUP = "6UP"
|
||||
case eightUP = "8UP"
|
||||
|
||||
var value: Int {
|
||||
switch self {
|
||||
case .oneUP:
|
||||
1
|
||||
case .twoUP:
|
||||
2
|
||||
case .threeUP:
|
||||
3
|
||||
case .fourUP:
|
||||
4
|
||||
case .fiveUP:
|
||||
5
|
||||
case .sixUP:
|
||||
6
|
||||
case .eightUP:
|
||||
8
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Used to set the position of the thumb(scrubber). This is used when the carousel container changes position, it will align the position of thumb(scrubber).
|
||||
open var position: Int {
|
||||
get { return _position }
|
||||
set {
|
||||
checkPositions()
|
||||
_position = (newValue > totalPositions) ? totalPositions : newValue
|
||||
scrollThumbToPosition(position)
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
/// Allows a unique id to be passed into the thumb and track of the thumb(scrubber).
|
||||
open var scrubberId: Int? { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// A callback when the scrubber position changes. Passes parameters (position).
|
||||
open var onScrubberDrag: ((Int) -> Void)? {
|
||||
get { nil }
|
||||
set {
|
||||
onScrubberDragCancellable?.cancel()
|
||||
if let newValue {
|
||||
onScrubberDragCancellable = onScrubberDragPublisher
|
||||
.sink { c in
|
||||
newValue(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher for when the scrubber position changes. Passes parameters (position).
|
||||
open var onScrubberDragPublisher = PassthroughSubject<Int, Never>()
|
||||
private var onScrubberDragCancellable: AnyCancellable?
|
||||
|
||||
/// A callback when the thumb move forward. Passes parameters (position).
|
||||
open var onMoveForward: ((Int) -> Void)? {
|
||||
get { nil }
|
||||
set {
|
||||
onMoveForwardCancellable?.cancel()
|
||||
if let newValue {
|
||||
onMoveForwardCancellable = onMoveForwardPublisher
|
||||
.sink { c in
|
||||
newValue(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher for when the thumb move forward. Passes parameters (position).
|
||||
open var onMoveForwardPublisher = PassthroughSubject<Int, Never>()
|
||||
private var onMoveForwardCancellable: AnyCancellable?
|
||||
|
||||
/// A callback when the thumb move backward. Passes parameters (position).
|
||||
open var onMoveBackward: ((Int) -> Void)? {
|
||||
get { nil }
|
||||
set {
|
||||
onMoveBackwardCancellable?.cancel()
|
||||
if let newValue {
|
||||
onMoveBackwardCancellable = onMoveBackwardPublisher
|
||||
.sink { c in
|
||||
newValue(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher for when the thumb move backward. Passes parameters (position).
|
||||
open var onMoveBackwardPublisher = PassthroughSubject<Int, Never>()
|
||||
private var onMoveBackwardCancellable: AnyCancellable?
|
||||
|
||||
/// A callback when the thumb touch start. Passes parameters (position).
|
||||
open var onThumbTouchStart: ((Int) -> Void)? {
|
||||
get { nil }
|
||||
set {
|
||||
onThumbTouchStartCancellable?.cancel()
|
||||
if let newValue {
|
||||
onThumbTouchStartCancellable = onThumbTouchStartPublisher
|
||||
.sink { c in
|
||||
newValue(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher for when the thumb touch start. Passes parameters (position).
|
||||
open var onThumbTouchStartPublisher = PassthroughSubject<Int, Never>()
|
||||
private var onThumbTouchStartCancellable: AnyCancellable?
|
||||
|
||||
/// A callback when the thumb touch end. Passes parameters (position).
|
||||
open var onThumbTouchEnd: ((Int) -> Void)? {
|
||||
get { nil }
|
||||
set {
|
||||
onThumbTouchEndCancellable?.cancel()
|
||||
if let newValue {
|
||||
onThumbTouchEndCancellable = onThumbTouchEndPublisher
|
||||
.sink { c in
|
||||
newValue(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher for when the thumb touch end. Passes parameters (position).
|
||||
open var onThumbTouchEndPublisher = PassthroughSubject<Int, Never>()
|
||||
private var onThumbTouchEndCancellable: AnyCancellable?
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Properties
|
||||
//--------------------------------------------------
|
||||
// Sizes are from InVision design specs.
|
||||
internal var containerSize: CGSize { CGSize(width: 45, height: 44) }
|
||||
internal var _selectedLayout: Layout = .oneUP
|
||||
internal var _numberOfSlides: Int = 1
|
||||
internal var totalPositions: Int = 1
|
||||
internal var _position: Int = 1
|
||||
internal var trayOriginalCenter: CGPoint!
|
||||
|
||||
private let trackViewWidth = 96
|
||||
private let trackViewHeight: CGFloat = 4
|
||||
private let minThumbWidth: Float = 16.0
|
||||
private var thumbWidth: Float = 16.0
|
||||
private var computedWidth: Float = 0.0
|
||||
private let cornerRadius: CGFloat = 2.0
|
||||
private let activeOpacity: Float = 0.15
|
||||
private let defaultOpacity: Float = 1
|
||||
|
||||
internal var containerView = View().with {
|
||||
$0.clipsToBounds = true
|
||||
}
|
||||
internal var trackView = View()
|
||||
internal var leftActiveOverlay = View()
|
||||
internal var rightActiveOverlay = View()
|
||||
internal var thumbView = View()
|
||||
|
||||
internal var rightActiveOverlayLayer: CALayer = CALayer()
|
||||
internal var leftActiveOverlayLayer: CALayer = CALayer()
|
||||
internal var thumbViewLayer: CALayer = CALayer()
|
||||
//--------------------------------------------------
|
||||
// MARK: - Configuration
|
||||
//--------------------------------------------------
|
||||
private var thumbColorConfiguration = SurfaceColorConfiguration(VDSColor.interactiveScrollthumbOnlight , VDSColor.interactiveScrollthumbOndark)
|
||||
private var trackColorConfiguration = SurfaceColorConfiguration(VDSColor.interactiveScrolltrackOnlight , VDSColor.interactiveScrolltrackOndark)
|
||||
private var activeOverlayColorConfiguration = SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteWhite)
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Lifecycle
|
||||
//--------------------------------------------------
|
||||
open override func initialSetup() {
|
||||
super.initialSetup()
|
||||
}
|
||||
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
isAccessibilityElement = true
|
||||
accessibilityLabel = "Carousel Scrollbar"
|
||||
|
||||
addSubview(containerView)
|
||||
containerView
|
||||
.pinTop()
|
||||
.pinBottom()
|
||||
.pinLeadingGreaterThanOrEqualTo()
|
||||
.pinTrailingLessThanOrEqualTo()
|
||||
.height(containerSize.height)
|
||||
.width(CGFloat(trackViewWidth))
|
||||
|
||||
containerView.centerXAnchor.constraint(equalTo: centerXAnchor).activate()
|
||||
|
||||
//Trackview
|
||||
trackView.frame = CGRectMake(0, 20, CGFloat(trackViewWidth), trackViewHeight)
|
||||
trackView.layer.cornerRadius = cornerRadius
|
||||
containerView.addSubview(trackView)
|
||||
|
||||
///Left active overlay
|
||||
leftActiveOverlay.frame = CGRectMake(trackView.frame.origin.x, 0, CGFloat(trackViewWidth), containerSize.height)
|
||||
leftActiveOverlay.isUserInteractionEnabled = true
|
||||
leftActiveOverlay.backgroundColor = .clear
|
||||
let leftPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(onLeftViewLongPressRecognizer(_:)))
|
||||
leftPressRecognizer.minimumPressDuration = 0
|
||||
leftActiveOverlay.addGestureRecognizer(leftPressRecognizer)
|
||||
containerView.addSubview(leftActiveOverlay)
|
||||
|
||||
leftActiveOverlay.layer.addSublayer(leftActiveOverlayLayer)
|
||||
leftActiveOverlayLayer.cornerRadius = cornerRadius
|
||||
leftActiveOverlayLayer.frame = .init(origin: .zero, size: .init(width: leftActiveOverlayLayer.frame.size.width, height: trackViewHeight))
|
||||
|
||||
///Right active overlay
|
||||
rightActiveOverlay.frame = CGRectMake(thumbView.frame.origin.x + thumbView.frame.size.width, 0, CGFloat(trackViewWidth), containerSize.height)
|
||||
rightActiveOverlay.isUserInteractionEnabled = true
|
||||
rightActiveOverlay.backgroundColor = .clear
|
||||
let rightPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(onRightViewLongPressRecognizer(_:)))
|
||||
rightPressRecognizer.minimumPressDuration = 0
|
||||
rightActiveOverlay.addGestureRecognizer(rightPressRecognizer)
|
||||
containerView.addSubview(rightActiveOverlay)
|
||||
|
||||
rightActiveOverlay.layer.addSublayer(rightActiveOverlayLayer)
|
||||
rightActiveOverlayLayer.cornerRadius = cornerRadius
|
||||
rightActiveOverlayLayer.frame = .init(origin: .zero, size: .init(width: rightActiveOverlay.frame.size.width, height: trackViewHeight))
|
||||
|
||||
//Thumbview
|
||||
thumbView.frame = CGRectMake(0, 0, CGFloat(thumbWidth), containerSize.height)
|
||||
thumbView.backgroundColor = .clear
|
||||
thumbView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action:(#selector(onScrubberChange(_:)))))
|
||||
containerView.addSubview(thumbView)
|
||||
updateActiveOverlays()
|
||||
|
||||
thumbViewLayer.cornerRadius = cornerRadius
|
||||
thumbViewLayer.backgroundColor = thumbColorConfiguration.getColor(surface).cgColor
|
||||
thumbViewLayer.frame = .init(origin: .init(x: 0, y: 20), size: .init(width: CGFloat(thumbWidth), height: trackViewHeight))
|
||||
thumbView.layer.addSublayer(thumbViewLayer)
|
||||
}
|
||||
|
||||
open override func updateView() {
|
||||
super.updateView()
|
||||
trackView.backgroundColor = trackColorConfiguration.getColor(surface)
|
||||
thumbViewLayer.backgroundColor = thumbColorConfiguration.getColor(surface).cgColor
|
||||
}
|
||||
|
||||
open override func updateAccessibility() {
|
||||
super.updateAccessibility()
|
||||
}
|
||||
|
||||
open override func reset() {
|
||||
for subview in subviews {
|
||||
for recognizer in subview.gestureRecognizers ?? [] {
|
||||
subview.removeGestureRecognizer(recognizer)
|
||||
}
|
||||
}
|
||||
super.reset()
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Methods
|
||||
//--------------------------------------------------
|
||||
func movePositionBackward() {
|
||||
position = position - 1
|
||||
scrollThumbToPosition(position)
|
||||
onMoveBackwardPublisher.send(position)
|
||||
}
|
||||
|
||||
func movePositionForward() {
|
||||
position = position + 1
|
||||
scrollThumbToPosition(position)
|
||||
onMoveForwardPublisher.send(position)
|
||||
}
|
||||
|
||||
// Compute track width and should maintain minimum thumb width if needed
|
||||
private func setThumbWidth() {
|
||||
let width = (Float(trackViewWidth) / Float(numberOfSlides)) * Float(_selectedLayout.value)
|
||||
computedWidth = (width > Float(trackViewWidth)) ? Float(trackViewWidth) : width
|
||||
thumbWidth = (width <= Float(trackViewWidth) && width > minThumbWidth) ? width : ((width > Float(trackViewWidth)) ? Float(trackViewWidth) : minThumbWidth)
|
||||
thumbView.frame.size.width = CGFloat(thumbWidth)
|
||||
thumbView.frame.origin.x = trackView.frame.origin.x
|
||||
thumbViewLayer.frame.size.width = thumbView.frame.size.width
|
||||
checkPositions()
|
||||
updateActiveOverlays()
|
||||
}
|
||||
|
||||
// Incomplete set moves a shorter distance than the standard increment value.
|
||||
// Update active overlay frames according to thumb position.
|
||||
private func updateActiveOverlays() {
|
||||
// adjusting thumb position if it goes beyond trackView on left/right.
|
||||
let thumbPosition = thumbView.frame.origin.x + thumbView.frame.size.width
|
||||
let trackPosition = trackView.frame.origin.x + trackView.frame.size.width
|
||||
if thumbPosition > trackPosition {
|
||||
thumbView.frame.origin.x = trackPosition - thumbView.frame.size.width
|
||||
} else if thumbView.frame.origin.x < trackView.frame.origin.x {
|
||||
thumbView.frame.origin.x = trackView.frame.origin.x
|
||||
}
|
||||
|
||||
//left active overlay position update
|
||||
leftActiveOverlay.frame.size.width = thumbView.frame.origin.x - trackView.frame.origin.x + cornerRadius
|
||||
leftActiveOverlayLayer.frame = .init(origin: .init(x: 0, y: 20), size: .init(width: leftActiveOverlay.frame.size.width, height: trackViewHeight))
|
||||
|
||||
//right active overlay position update
|
||||
rightActiveOverlay.frame.origin.x = thumbView.frame.origin.x + thumbView.frame.size.width - cornerRadius
|
||||
rightActiveOverlay.frame.size.width = (trackView.frame.origin.x + trackView.frame.size.width) - (thumbView.frame.origin.x + thumbView.frame.size.width) + cornerRadius
|
||||
rightActiveOverlayLayer.frame = .init(origin: .init(x: 0, y: 20), size: .init(width: rightActiveOverlay.frame.size.width, height: trackViewHeight))
|
||||
}
|
||||
|
||||
private func checkPositions() {
|
||||
totalPositions = Int (ceil (Double(numberOfSlides) / Double(_selectedLayout.value)))
|
||||
}
|
||||
|
||||
private func scrollThumbToPosition(_ position: Int) {
|
||||
setThumb(at: position)
|
||||
onScrubberDragPublisher.send(position)
|
||||
}
|
||||
|
||||
private func setThumb(at position: Int) {
|
||||
UIView.animate(withDuration: 0.1, delay: 0.0, options: .curveLinear, animations: { [weak self] in
|
||||
guard let self else { return }
|
||||
self.thumbView.frame.origin.x = CGFloat(Float((position) - 1) * self.computedWidth) + self.trackView.frame.origin.x
|
||||
self.updateActiveOverlays()
|
||||
})
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Gesture Methods
|
||||
//--------------------------------------------------
|
||||
// Drag scrollbar thumb to move it to the left or right.
|
||||
// Upon releases of drag, the scrollbar thumb snaps to the closest full position and the scrollbar returns to original size without delay.
|
||||
@objc func onScrubberChange(_ sender: UIPanGestureRecognizer) {
|
||||
let translation = sender.translation(in: thumbView)
|
||||
if sender.state == UIGestureRecognizer.State.began {
|
||||
trayOriginalCenter = thumbView.center
|
||||
onThumbTouchStartPublisher.send(position)
|
||||
} else if sender.state == UIGestureRecognizer.State.changed {
|
||||
let draggedPositions = Int (ceil (Double(translation.x) / Double(computedWidth)))
|
||||
setThumb(at: position + draggedPositions)
|
||||
}
|
||||
else if sender.state == UIGestureRecognizer.State.cancelled || sender.state == UIGestureRecognizer.State.ended {
|
||||
let draggedPositions = Int (ceil (Double(translation.x) / Double(computedWidth)))
|
||||
position = ((position + draggedPositions) < 1) ? 1 : (position + draggedPositions)
|
||||
if sender.state == UIGestureRecognizer.State.ended {
|
||||
onThumbTouchEndPublisher.send(position)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Move the scrollbar thumb to the left while tapping on the left side of the scrubber.
|
||||
@objc func onLeftViewLongPressRecognizer(_ gesture: UILongPressGestureRecognizer) {
|
||||
animateOverlay(layer: leftActiveOverlayLayer, with: gesture, onGestureEnd: movePositionBackward)
|
||||
}
|
||||
|
||||
// Move the scrollbar thumb to the right while tapping on the right side of the scrubber.
|
||||
@objc func onRightViewLongPressRecognizer(_ gesture: UILongPressGestureRecognizer) {
|
||||
animateOverlay(layer: rightActiveOverlayLayer, with: gesture, onGestureEnd: movePositionForward)
|
||||
}
|
||||
|
||||
private func animateOverlay(layer: CALayer, with gesture: UILongPressGestureRecognizer, onGestureEnd: @escaping(() -> Void)) {
|
||||
UIView.animate(withDuration: 0.1, delay: 0.0, options: .curveLinear, animations: { [weak self] in
|
||||
guard let self else { return }
|
||||
if gesture.state == .began {
|
||||
layer.backgroundColor = activeOverlayColorConfiguration.getColor(self).cgColor
|
||||
layer.opacity = activeOpacity
|
||||
} else if gesture.state == .cancelled {
|
||||
layer.backgroundColor = UIColor.clear.cgColor
|
||||
layer.opacity = defaultOpacity
|
||||
} else if gesture.state == .ended {
|
||||
layer.backgroundColor = UIColor.clear.cgColor
|
||||
layer.opacity = defaultOpacity
|
||||
onGestureEnd()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
MM/DD/YYYY
|
||||
----------------
|
||||
|
||||
07/29/22
|
||||
----------------
|
||||
- Initial Brand 3.0 handoff
|
||||
|
||||
08/10/2022
|
||||
----------------
|
||||
- Updated default and inverted prop to light and dark surface.
|
||||
|
||||
11/30/2022
|
||||
----------------
|
||||
- Added "(web only)" to any instance of "keyboard focus"
|
||||
|
||||
12/13/2022
|
||||
----------------
|
||||
- Replaced focus border pixel and style & spacing values with tokens.
|
||||
|
||||
01/09/2023
|
||||
----------------
|
||||
- Updated Specs to use new SPEC Templates and SPEC DOC Components.
|
||||
|
||||
05/19/2023
|
||||
----------------
|
||||
- Changed Carousel Scrubber to Carousel Scrollbar and replaced all instances of Scrubber to Scrollbar.
|
||||
- Removed KF states and behaviors from States and Behaviors > Interaction Types.
|
||||
|
||||
@ -44,7 +44,7 @@ open class Icon: View {
|
||||
//--------------------------------------------------
|
||||
/// UIImageView used to render the icon.
|
||||
open var imageView = UIImageView().with {
|
||||
$0.isAccessibilityElement = false
|
||||
$0.isAccessibilityElement = false
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
$0.contentMode = .scaleAspectFill
|
||||
$0.clipsToBounds = true
|
||||
@ -109,8 +109,8 @@ open class Icon: View {
|
||||
|
||||
//get the image name
|
||||
//set the image
|
||||
if let name, let image = getImage(for: name.rawValue) {
|
||||
setImage(image: image, imageColor: imageColor)
|
||||
if let name, let image = UIImage.image(for: name, color: imageColor) {
|
||||
imageView.image = image
|
||||
} else {
|
||||
imageView.image = nil
|
||||
}
|
||||
@ -129,17 +129,17 @@ open class Icon: View {
|
||||
super.updateAccessibility()
|
||||
accessibilityLabel = name?.rawValue ?? "icon"
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Methods
|
||||
//--------------------------------------------------
|
||||
private func getImage(for imageName: String) -> UIImage? {
|
||||
|
||||
return BundleManager.shared.image(for: imageName)
|
||||
}
|
||||
|
||||
private func setImage(image: UIImage, imageColor: UIColor) {
|
||||
imageView.image = image.withTintColor(imageColor)
|
||||
}
|
||||
}
|
||||
|
||||
extension UIImage {
|
||||
|
||||
/// UIImage helper for finding images based on the Icon.Name which uses the internal BundleManager.
|
||||
/// - Parameters:
|
||||
/// - name: Icon.Name rawRepresentable.
|
||||
/// - color: Color to Tint the image with
|
||||
/// - renderingMode: UIImage Rendering mode.
|
||||
/// - Returns: UIImage for this proecess
|
||||
public static func image(for iconName: Icon.Name, color: UIColor? = nil, renderingMode: UIImage.RenderingMode = .alwaysOriginal) -> UIImage? {
|
||||
image(representing: iconName, color: color, renderingMode: renderingMode)
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,7 +48,6 @@ 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 checkmark = Name(name: "checkmark")
|
||||
public static let checkmarkAlt = Name(name: "checkmark-alt")
|
||||
public static let close = Name(name: "close")
|
||||
|
||||
@ -56,6 +56,8 @@ public struct ActionLabelAttribute: ActionLabelAttributeModel {
|
||||
}
|
||||
|
||||
public func setAttribute(on attributedString: NSMutableAttributedString) {
|
||||
guard isValidRange(on: attributedString) else { return }
|
||||
|
||||
if(shouldUnderline){
|
||||
UnderlineLabelAttribute(location: location, length: length).setAttribute(on: attributedString)
|
||||
}
|
||||
|
||||
@ -14,6 +14,8 @@ public protocol AttachmentLabelAttributeModel: LabelAttributeModel {
|
||||
|
||||
extension AttachmentLabelAttributeModel {
|
||||
public func setAttribute(on attributedString: NSMutableAttributedString) {
|
||||
guard isValidRange(on: attributedString) else { return }
|
||||
|
||||
do {
|
||||
let mutableString = NSMutableAttributedString()
|
||||
let attachment = try getAttachment()
|
||||
|
||||
@ -31,11 +31,21 @@ extension LabelAttributeModel {
|
||||
}
|
||||
|
||||
public func isValidRange(on attributedString: NSMutableAttributedString) -> Bool {
|
||||
range.location + range.length <= attributedString.string.count
|
||||
attributedString.isValid(range: range)
|
||||
}
|
||||
}
|
||||
|
||||
public extension String {
|
||||
func isValid(range: NSRange) -> Bool {
|
||||
range.location >= 0 && range.length > 0 && range.location + range.length <= count
|
||||
}
|
||||
}
|
||||
|
||||
public extension NSAttributedString {
|
||||
func isValid(range: NSRange) -> Bool {
|
||||
range.location >= 0 && range.length > 0 && range.location + range.length <= length
|
||||
}
|
||||
|
||||
func createAttributeModels() -> [(any LabelAttributeModel)] {
|
||||
var attributes: [any VDS.LabelAttributeModel] = []
|
||||
enumerateAttributes(in: NSMakeRange(0, length)) { attributeMap, range, stop in
|
||||
@ -61,6 +71,7 @@ public extension NSAttributedString {
|
||||
|
||||
extension NSMutableAttributedString {
|
||||
public func apply(attribute: any LabelAttributeModel) {
|
||||
guard isValid(range: attribute.range) else { return }
|
||||
attribute.setAttribute(on: self)
|
||||
}
|
||||
|
||||
|
||||
@ -24,7 +24,9 @@ public class TooltipLabelAttribute: ActionLabelAttributeModel, TooltipLaunchable
|
||||
public var presenter: UIView?
|
||||
|
||||
public func setAttribute(on attributedString: NSMutableAttributedString) {
|
||||
//update the location
|
||||
guard isValidRange(on: attributedString) else { return }
|
||||
|
||||
//update the location
|
||||
location = attributedString.string.count - 1
|
||||
|
||||
//set the default color off surface for text
|
||||
|
||||
@ -42,6 +42,13 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Properties
|
||||
//--------------------------------------------------
|
||||
private enum TextSetMode {
|
||||
case text
|
||||
case attributedText
|
||||
}
|
||||
|
||||
private var textSetMode: TextSetMode = .text
|
||||
|
||||
private var initialSetupPerformed = false
|
||||
|
||||
private var edgeInsets: UIEdgeInsets { textStyle.edgeInsets }
|
||||
@ -101,18 +108,14 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
/// Key of whether or not updateView() is called in setNeedsUpdate()
|
||||
open var shouldUpdateView: Bool = true
|
||||
|
||||
/// Determines if the label should use its own attributedText property instead of rendering the attributedText propert
|
||||
/// based of other local properties, such as textStyle, textColor, surface, etc... The default value is false.
|
||||
open var useAttributedText: Bool = false
|
||||
open var shouldUpdateView: Bool = true
|
||||
|
||||
/// Will determine if a scaled font should be used for the font.
|
||||
open var useScaledFont: Bool = false { didSet { setNeedsUpdate() }}
|
||||
|
||||
open var surface: Surface = .light { didSet { setNeedsUpdate() }}
|
||||
|
||||
/// Array of LabelAttributeModel objects used in rendering the text.
|
||||
/// Array of LabelAttributeModel objects used in rendering the text.
|
||||
open var attributes: [any LabelAttributeModel]? { didSet { setNeedsUpdate() }}
|
||||
|
||||
/// TextStyle used on the this label.
|
||||
@ -128,19 +131,40 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
|
||||
|
||||
/// Line break mode for the label, default is set to word wrapping.
|
||||
open override var lineBreakMode: NSLineBreakMode { didSet { setNeedsUpdate() }}
|
||||
|
||||
private var _text: String?
|
||||
|
||||
|
||||
/// Text that will be used in the label.
|
||||
override open var text: String? {
|
||||
get { _text }
|
||||
set {
|
||||
if _text != newValue || newValue != attributedText?.string {
|
||||
_text = newValue
|
||||
useAttributedText = false
|
||||
attributes?.removeAll()
|
||||
setNeedsUpdate()
|
||||
private var _text: String!
|
||||
override open var text: String! {
|
||||
didSet {
|
||||
_text = text
|
||||
textSetMode = .text
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
///AttributedText that will be used in the label.
|
||||
override open var attributedText: NSAttributedString? {
|
||||
didSet {
|
||||
textSetMode = .attributedText
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
override open var font: UIFont! {
|
||||
didSet {
|
||||
if let font, initialSetupPerformed {
|
||||
textStyle = TextStyle.convert(font: font)
|
||||
}
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
override open var textColor: UIColor! {
|
||||
didSet {
|
||||
if let textColor, initialSetupPerformed {
|
||||
textColorConfiguration = SurfaceColorConfiguration(textColor, textColor).eraseToAnyColorable()
|
||||
}
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
@ -162,13 +186,13 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
|
||||
//--------------------------------------------------
|
||||
open func initialSetup() {
|
||||
if !initialSetupPerformed {
|
||||
initialSetupPerformed = true
|
||||
//register for ContentSizeChanges
|
||||
NotificationCenter
|
||||
.Publisher(center: .default, name: UIContentSizeCategory.didChangeNotification)
|
||||
.sink { [weak self] notification in
|
||||
self?.setNeedsUpdate()
|
||||
}.store(in: &subscribers)
|
||||
|
||||
backgroundColor = .clear
|
||||
numberOfLines = 0
|
||||
lineBreakMode = .byWordWrapping
|
||||
@ -200,30 +224,13 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
|
||||
}
|
||||
|
||||
open func updateView() {
|
||||
if !useAttributedText {
|
||||
if let text {
|
||||
accessibilityCustomActions = []
|
||||
|
||||
//create the primary string
|
||||
let mutableText = NSMutableAttributedString.mutableText(for: text,
|
||||
textStyle: textStyle,
|
||||
useScaledFont: useScaledFont,
|
||||
textColor: textColorConfiguration.getColor(self),
|
||||
alignment: textAlignment,
|
||||
lineBreakMode: lineBreakMode)
|
||||
|
||||
applyAttributes(mutableText)
|
||||
|
||||
//set the attributed text
|
||||
attributedText = mutableText
|
||||
|
||||
//force a drawText
|
||||
setNeedsDisplay()
|
||||
|
||||
setNeedsLayout()
|
||||
layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
restyleText()
|
||||
|
||||
//force a drawText
|
||||
setNeedsDisplay()
|
||||
|
||||
setNeedsLayout()
|
||||
layoutIfNeeded()
|
||||
}
|
||||
|
||||
open func updateAccessibility() {
|
||||
@ -269,6 +276,55 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Methods
|
||||
//--------------------------------------------------
|
||||
private func restyleText() {
|
||||
if textSetMode == .text {
|
||||
styleText(_text)
|
||||
} else {
|
||||
styleAttributedText(attributedText)
|
||||
}
|
||||
}
|
||||
|
||||
private func styleText(_ newValue: String!) {
|
||||
defer { invalidateIntrinsicContentSize() }
|
||||
guard let newValue, !newValue.isEmpty else {
|
||||
// We don't need to use attributed text
|
||||
super.attributedText = nil
|
||||
super.text = newValue
|
||||
return
|
||||
}
|
||||
|
||||
accessibilityCustomActions = []
|
||||
|
||||
//create the primary string
|
||||
let mutableText = NSMutableAttributedString.mutableText(for: newValue,
|
||||
textStyle: textStyle,
|
||||
useScaledFont: useScaledFont,
|
||||
textColor: textColorConfiguration.getColor(self),
|
||||
alignment: textAlignment,
|
||||
lineBreakMode: lineBreakMode)
|
||||
|
||||
applyAttributes(mutableText)
|
||||
|
||||
// Set attributed text to match typography
|
||||
super.attributedText = mutableText
|
||||
}
|
||||
|
||||
private func styleAttributedText(_ newValue: NSAttributedString?) {
|
||||
defer { invalidateIntrinsicContentSize() }
|
||||
guard let newValue, !newValue.string.isEmpty else {
|
||||
// We don't need any additional styling
|
||||
super.attributedText = newValue
|
||||
return
|
||||
}
|
||||
|
||||
let mutableText = NSMutableAttributedString(attributedString: newValue)
|
||||
|
||||
applyAttributes(mutableText)
|
||||
|
||||
// Modify attributed text to match typography
|
||||
super.attributedText = mutableText
|
||||
}
|
||||
|
||||
private func applyAttributes(_ mutableAttributedString: NSMutableAttributedString) {
|
||||
actions = []
|
||||
|
||||
@ -288,7 +344,7 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
|
||||
for attribute in attributes {
|
||||
|
||||
//see if the attribute is Actionable
|
||||
if let actionable = attribute as? any ActionLabelAttributeModel{
|
||||
if let actionable = attribute as? any ActionLabelAttributeModel, mutableAttributedString.isValid(range: actionable.range) {
|
||||
//create a accessibleAction
|
||||
let customAccessibilityAction = customAccessibilityAction(text: mutableAttributedString.string, range: actionable.range, accessibleText: actionable.accessibleText)
|
||||
|
||||
@ -323,7 +379,7 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
|
||||
|
||||
guard let text = text, let attributedText else { return nil }
|
||||
|
||||
let actionText = accessibleText ?? NSString(string:text).substring(with: range)
|
||||
let actionText = accessibleText ?? (text.isValid(range: range) ? NSString(string:text).substring(with: range) : text)
|
||||
|
||||
// Calculate the frame of the substring
|
||||
let layoutManager = NSLayoutManager()
|
||||
@ -366,3 +422,5 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -53,11 +53,6 @@ open class Notification: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum used to describe the orientation of Notification.
|
||||
public enum Layout: String, CaseIterable {
|
||||
case vertical, horizontal
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Properties
|
||||
//--------------------------------------------------
|
||||
@ -80,19 +75,9 @@ open class Notification: View {
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
}
|
||||
|
||||
private var labelButtonViewSpacing: CGFloat {
|
||||
let spacing: CGFloat = UIDevice.isIPad ? 20 : 16
|
||||
return switch layout {
|
||||
case .vertical:
|
||||
0
|
||||
case .horizontal:
|
||||
spacing
|
||||
}
|
||||
}
|
||||
private var labelButtonViewSpacing: CGFloat { UIDevice.isIPad ? 20 : 16 }
|
||||
|
||||
internal var onCloseSubscriber: AnyCancellable?
|
||||
|
||||
private var maxWidthConstraint: NSLayoutConstraint?
|
||||
|
||||
private var leadingConstraint: NSLayoutConstraint?
|
||||
|
||||
@ -172,19 +157,6 @@ open class Notification: View {
|
||||
|
||||
/// Add this attribute determine your type of Notification.
|
||||
open var style: Style = .info { didSet { setNeedsUpdate()}}
|
||||
|
||||
private var _layout: Layout = .vertical
|
||||
|
||||
/// Determines the orientation of buttons and text in the Notification.
|
||||
open var layout: Layout {
|
||||
set {
|
||||
if !UIDevice.isIPad, newValue == .horizontal { return }
|
||||
_layout = newValue
|
||||
buttonGroup.alignment = _layout == .horizontal ? .center : .left
|
||||
setNeedsUpdate()
|
||||
}
|
||||
get { _layout }
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Configuration
|
||||
@ -219,18 +191,11 @@ open class Notification: View {
|
||||
return 288
|
||||
}
|
||||
|
||||
private var maxViewWidth: CGFloat {
|
||||
return 1232
|
||||
}
|
||||
|
||||
private var labelViewWidthConstraint: NSLayoutConstraint?
|
||||
private var labelViewBottomConstraint: NSLayoutConstraint?
|
||||
private var labelViewAndButtonViewConstraint: NSLayoutConstraint?
|
||||
private var buttonViewTopConstraint: NSLayoutConstraint?
|
||||
private var typeIconWidthConstraint: NSLayoutConstraint?
|
||||
private var closeIconWidthConstraint: NSLayoutConstraint?
|
||||
private var buttonGroupCenterYConstraint: NSLayoutConstraint?
|
||||
private var buttonGroupBottomConstraint: NSLayoutConstraint?
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Overrides
|
||||
@ -255,21 +220,18 @@ open class Notification: View {
|
||||
mainStackView.heightAnchor.constraint(greaterThanOrEqualToConstant: minContentHeight),
|
||||
layoutGuide.widthAnchor.constraint(greaterThanOrEqualToConstant: minViewWidth)
|
||||
])
|
||||
maxWidthConstraint = layoutGuide.widthAnchor.constraint(lessThanOrEqualToConstant: maxViewWidth)
|
||||
|
||||
labelButtonView.addSubview(labelsView)
|
||||
labelsView
|
||||
.pinTop()
|
||||
.pinLeading()
|
||||
labelViewWidthConstraint = labelsView.widthAnchor.constraint(equalTo: labelButtonView.widthAnchor, multiplier: 1.0)
|
||||
labelViewWidthConstraint?.activate()
|
||||
labelsView.widthAnchor.constraint(equalTo: labelButtonView.widthAnchor, multiplier: 1.0).activate()
|
||||
labelViewBottomConstraint = labelButtonView.bottomAnchor.constraint(equalTo: labelsView.bottomAnchor)
|
||||
|
||||
labelButtonView.addSubview(buttonGroup)
|
||||
buttonGroup
|
||||
.pinTrailing()
|
||||
buttonGroupBottomConstraint = labelButtonView.bottomAnchor.constraint(equalTo: buttonGroup.bottomAnchor)
|
||||
buttonGroupCenterYConstraint = buttonGroup.centerYAnchor.constraint(equalTo: labelButtonView.centerYAnchor)
|
||||
labelButtonView.bottomAnchor.constraint(equalTo: buttonGroup.bottomAnchor).activate()
|
||||
labelViewAndButtonViewConstraint = buttonGroup.topAnchor.constraint(equalTo: labelsView.bottomAnchor, constant: VDSLayout.Spacing.space3X.value)
|
||||
buttonGroup.widthAnchor.constraint(equalTo: labelsView.widthAnchor).activate()
|
||||
|
||||
@ -314,7 +276,6 @@ open class Notification: View {
|
||||
closeButton.size = UIDevice.isIPad ? .medium : .small
|
||||
closeButton.name = .close
|
||||
|
||||
layout = .vertical
|
||||
hideCloseButton = false
|
||||
|
||||
shouldUpdateView = true
|
||||
@ -338,6 +299,16 @@ open class Notification: View {
|
||||
layer.cornerRadius = UIScreen.main.bounds.width == bounds.width ? 0 : 4.0
|
||||
}
|
||||
|
||||
///Updating the accessiblity values i.e elements, label, value other items for the component.
|
||||
open override func updateAccessibility() {
|
||||
super.updateAccessibility()
|
||||
accessibilityElements = [closeButton, typeIcon, titleLabel, subTitleLabel, buttonGroup]
|
||||
typeIcon.accessibilityLabel = style.rawValue
|
||||
typeIcon.imageView.image?.isAccessibilityElement = false
|
||||
closeButton.accessibilityTraits = [.button]
|
||||
closeButton.accessibilityLabel = "Close Notification"
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Methods
|
||||
//--------------------------------------------------
|
||||
@ -383,32 +354,21 @@ open class Notification: View {
|
||||
secondaryButton.onClick = secondaryButtonModel.onClick
|
||||
buttons.append(secondaryButton)
|
||||
}
|
||||
labelViewWidthConstraint?.deactivate()
|
||||
if buttons.isEmpty {
|
||||
buttonGroup.isHidden = true
|
||||
labelViewWidthConstraint = labelsView.widthAnchor.constraint(equalTo: labelButtonView.widthAnchor)
|
||||
buttonGroup.buttons.removeAll()
|
||||
} else {
|
||||
labelsView.setCustomSpacing(VDSLayout.Spacing.space3X.value, after: subTitleLabel)
|
||||
buttonGroup.buttons = buttons
|
||||
buttonGroup.isHidden = false
|
||||
labelViewWidthConstraint = labelsView.widthAnchor.constraint(equalTo: labelButtonView.widthAnchor, multiplier: layout == .vertical ? 1.0 : 0.5, constant: layout == .vertical ? 0 : -labelButtonViewSpacing)
|
||||
}
|
||||
labelViewWidthConstraint?.activate()
|
||||
}
|
||||
|
||||
private func setConstraints() {
|
||||
maxWidthConstraint?.deactivate()
|
||||
labelViewAndButtonViewConstraint?.deactivate()
|
||||
labelViewBottomConstraint?.deactivate()
|
||||
buttonGroupCenterYConstraint?.deactivate()
|
||||
buttonGroupBottomConstraint?.deactivate()
|
||||
maxWidthConstraint?.constant = maxViewWidth
|
||||
maxWidthConstraint?.isActive = UIDevice.isIPad
|
||||
labelViewAndButtonViewConstraint?.isActive = layout == .vertical && !buttonGroup.buttons.isEmpty
|
||||
labelViewBottomConstraint?.isActive = layout == .horizontal || buttonGroup.buttons.isEmpty
|
||||
buttonGroupCenterYConstraint?.isActive = layout == .horizontal
|
||||
buttonGroupBottomConstraint?.isActive = layout == .vertical
|
||||
labelViewAndButtonViewConstraint?.isActive = !buttonGroup.buttons.isEmpty
|
||||
labelViewBottomConstraint?.isActive = buttonGroup.buttons.isEmpty
|
||||
typeIconWidthConstraint?.constant = typeIcon.size.dimensions.width
|
||||
closeIconWidthConstraint?.constant = closeButton.size.dimensions.width
|
||||
}
|
||||
|
||||
244
VDS/Components/Pagination/Pagination.swift
Normal file
244
VDS/Components/Pagination/Pagination.swift
Normal file
@ -0,0 +1,244 @@
|
||||
//
|
||||
// Pagination.swift
|
||||
// VDS
|
||||
//
|
||||
// Created by Bandaru, Krishna Kishore on 01/03/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import VDSColorTokens
|
||||
import Combine
|
||||
|
||||
///Pagination is a control that enables customers to navigate multiple pages of content by selecting either a specific page or the next or previous set of four pages.
|
||||
@objc(VDSPagination)
|
||||
open class Pagination: View {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Properties
|
||||
//--------------------------------------------------
|
||||
///Maximum component width
|
||||
private let maxWidth: CGFloat = 288.0
|
||||
///Collectionview width anchor
|
||||
private var collectionViewWidthAnchor: NSLayoutConstraint?
|
||||
///Collectionview container Center X constraint
|
||||
private var collectionContainerViewCenterXConstraint: NSLayoutConstraint?
|
||||
///Selected page index
|
||||
private var _selectedPageIndex: Int = 0
|
||||
///Custom flow layout defined for the Pagination
|
||||
private let flowLayout = PaginationFlowLayout()
|
||||
///A root view for the pagination
|
||||
public let containerView: View = View().with {
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
}
|
||||
///Collectionview to render pagination indexes
|
||||
private lazy var collectionView: UICollectionView = {
|
||||
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
|
||||
collectionView.isScrollEnabled = false
|
||||
collectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||
collectionView.showsHorizontalScrollIndicator = false
|
||||
collectionView.showsVerticalScrollIndicator = false
|
||||
collectionView.isAccessibilityElement = true
|
||||
collectionView.register(PaginationCellItem.self, forCellWithReuseIdentifier: PaginationCellItem.identifier)
|
||||
collectionView.backgroundColor = .clear
|
||||
collectionView.delegate = self
|
||||
collectionView.dataSource = self
|
||||
return collectionView
|
||||
}()
|
||||
///Container view to hold collectionview to render pagination indexes and to handler accessibility.
|
||||
private let collectionContainerView = PaginationContainer()
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
///Previous button to select previous page
|
||||
public let previousButton: PaginationButton = .init(type: .previous)
|
||||
///Next button to select next page
|
||||
public let nextButton: PaginationButton = .init(type: .next)
|
||||
/// A callback when the page changes. Passes parameters (selectedPage).
|
||||
public var onPageDidSelect: ((Int) -> Void)?
|
||||
/// Total number of pages, allows limit ranging from 0 to 9999.
|
||||
@Clamping(range: 0...9999)
|
||||
public var total: Int {
|
||||
didSet {
|
||||
previousButton.isHidden = true
|
||||
nextButton.isHidden = total <= 1
|
||||
_selectedPageIndex = 0
|
||||
setNeedsUpdate()
|
||||
updateSelection()
|
||||
}
|
||||
}
|
||||
///Selected active page number and clips to total pages if selected index is greater than the total pages.
|
||||
public var selectedPage: Int {
|
||||
set {
|
||||
if newValue >= total {
|
||||
_selectedPageIndex = total - 1
|
||||
} else if newValue < 0 {
|
||||
_selectedPageIndex = 0
|
||||
} else {
|
||||
_selectedPageIndex = max(newValue - 1, 0)
|
||||
}
|
||||
setNeedsUpdate()
|
||||
updateSelection()
|
||||
}
|
||||
get {
|
||||
_selectedPageIndex + 1 //Returns selected page value not index
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
/// Executed on initialization for this View.
|
||||
open override func initialSetup() {
|
||||
super.initialSetup()
|
||||
|
||||
collectionContainerView.addSubview(collectionView)
|
||||
containerView.addSubview(previousButton)
|
||||
containerView.addSubview(collectionContainerView)
|
||||
containerView.addSubview(nextButton)
|
||||
addSubview(containerView)
|
||||
|
||||
containerView
|
||||
.pinTop()
|
||||
.pinBottom()
|
||||
.pinLeadingGreaterThanOrEqualTo()
|
||||
.pinTrailingLessThanOrEqualTo()
|
||||
.pinCenterX()
|
||||
.width(maxWidth)
|
||||
.height(44)
|
||||
|
||||
previousButton
|
||||
.pinTop()
|
||||
.pinBottom()
|
||||
.pinLeading()
|
||||
.pinTrailingGreaterThanOrEqualTo(collectionContainerView.leadingAnchor)
|
||||
|
||||
collectionContainerView
|
||||
.pinTrailingGreaterThanOrEqualTo(nextButton.leadingAnchor)
|
||||
.pinTop()
|
||||
.pinBottom()
|
||||
|
||||
collectionView
|
||||
.height(VDSLayout.Spacing.space4X.value)
|
||||
.pinCenterY()
|
||||
.pinCenterX()
|
||||
|
||||
collectionViewWidthAnchor = collectionView.width(constant: 92)
|
||||
|
||||
nextButton
|
||||
.pinTop()
|
||||
.pinBottom()
|
||||
.pinTrailing()
|
||||
|
||||
nextButton.onClick = onbuttonTapped
|
||||
previousButton.onClick = onbuttonTapped
|
||||
previousButton.isHidden = true
|
||||
|
||||
flowLayout.$collectionViewWidth
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak self] value in
|
||||
self?.collectionViewWidthAnchor?.constant = value //As cell width is dynamic i.e cell may contain 2 or 3 or 4 charcters. Make sure that all the visible cells are displayed.
|
||||
}.store(in: &subscribers)
|
||||
collectionContainerView.onAccessibilityIncrement = { [weak self] in
|
||||
guard let self else { return }
|
||||
self.selectedPage = max(0, self.selectedPage + 1)
|
||||
}
|
||||
collectionContainerView.onAccessibilityDecrement = { [weak self] in
|
||||
guard let self else { return }
|
||||
self.selectedPage = max(0, self.selectedPage - 1)
|
||||
}
|
||||
}
|
||||
|
||||
///Updating the accessiblity values i.e elements, label, value other items for the component.
|
||||
open override func updateAccessibility() {
|
||||
super.updateAccessibility()
|
||||
accessibilityElements = [previousButton, collectionContainerView, nextButton]
|
||||
collectionContainerView.accessibilityLabel = "Pagination containing \(total) pages"
|
||||
collectionContainerView.accessibilityValue = "Page \(selectedPage) of \(total) selected"
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
open override func updateView() {
|
||||
super.updateView()
|
||||
nextButton.surface = surface
|
||||
previousButton.surface = surface
|
||||
collectionView.reloadData()
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Methods
|
||||
//--------------------------------------------------
|
||||
///When previous/next button is tapped
|
||||
private func onbuttonTapped(_ sender: UIButton) {
|
||||
let isNextAction = sender == nextButton
|
||||
_selectedPageIndex = if isNextAction { _selectedPageIndex + 1 } else { _selectedPageIndex - 1 }
|
||||
updateSelection()
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { [weak self] in
|
||||
guard let self else { return }
|
||||
UIAccessibility.post(notification: .announcement, argument: "Page \(self.selectedPage) of \(self.total) selected")
|
||||
}
|
||||
}
|
||||
|
||||
///Refreshing the UI based on the selected page
|
||||
private func updateSelection() {
|
||||
guard _selectedPageIndex < total else { return }
|
||||
//Need to make selected page as second element so scrolling previous index of the selected page to left
|
||||
collectionView.scrollToItem(at: IndexPath(row: max(_selectedPageIndex - 1, 0), section: 0), at: .left, animated: false)
|
||||
previousButton.isHidden = _selectedPageIndex == 0
|
||||
nextButton.isHidden = _selectedPageIndex == total - 1
|
||||
collectionView.reloadData()
|
||||
verifyIfMaxDigitChanged()
|
||||
}
|
||||
|
||||
///Identifying if there is any change in the digits of upcoming page
|
||||
private func verifyIfMaxDigitChanged() {
|
||||
let upperLimitPage = _selectedPageIndex + flowLayout.maxNumberOfColumns
|
||||
let upperLimitDigits = upperLimitPage.digitCount //future value digits
|
||||
switch (flowLayout.numberOfColumns, upperLimitDigits) {
|
||||
case (_, 1), (_, 2):
|
||||
flowLayout.numberOfColumns = 4
|
||||
default:
|
||||
flowLayout.numberOfColumns = 3
|
||||
}
|
||||
if upperLimitDigits != flowLayout.upperLimitDigits {
|
||||
flowLayout.upperLimitDigits = upperLimitDigits
|
||||
flowLayout.invalidateLayout()
|
||||
collectionView.reloadData()
|
||||
//Need to make selected page as second element so scrolling previous index of the selected page to left
|
||||
collectionView.scrollToItem(at: IndexPath(row: max(_selectedPageIndex - 1, 0), section: 0), at: .left, animated: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Pagination: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
|
||||
//--------------------------------------------------
|
||||
// MARK: - UICollectionView Delegate & Datasource
|
||||
//--------------------------------------------------
|
||||
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { total }
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PaginationCellItem.identifier, for: indexPath) as? PaginationCellItem else { return UICollectionViewCell() }
|
||||
cell.update(_selectedPageIndex, currentIndex: indexPath.row, surface: surface)
|
||||
return cell
|
||||
}
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
guard _selectedPageIndex != indexPath.row else { return }
|
||||
_selectedPageIndex = indexPath.row
|
||||
updateSelection()
|
||||
onPageDidSelect?(selectedPage)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension Int {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Extension on Int to identify number of digits in given number.
|
||||
//--------------------------------------------------
|
||||
var digitCount: Int {
|
||||
numberOfDigits(in: self)
|
||||
}
|
||||
|
||||
private func numberOfDigits(in number: Int) -> Int {
|
||||
number < 10 && number >= 0 ? 1 : 1 + numberOfDigits(in: number/10)
|
||||
}
|
||||
}
|
||||
111
VDS/Components/Pagination/PaginationButton.swift
Normal file
111
VDS/Components/Pagination/PaginationButton.swift
Normal file
@ -0,0 +1,111 @@
|
||||
//
|
||||
// PaginationButton.swift
|
||||
// VDS
|
||||
//
|
||||
// Created by Bandaru, Krishna Kishore on 05/03/24.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import VDSColorTokens
|
||||
|
||||
///This is customised button for Pagination view
|
||||
@objc(PaginationButton)
|
||||
open class PaginationButton: ButtonBase {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Properties
|
||||
//--------------------------------------------------
|
||||
/// Type of the PaginationButton
|
||||
private var type: Type = .next
|
||||
/// Button tint color configuration
|
||||
private let buttonTintColorConfiguration = SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteWhite)
|
||||
/// Button title color configuration
|
||||
private let buttonTextColorConfiguration = SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteWhite)
|
||||
/// Button configuration for iOS 15+
|
||||
@available(iOS 15.0, *)
|
||||
private var buttonConfiguration: Button.Configuration {
|
||||
var configuration = ButtonBase.Configuration.plain()
|
||||
configuration.imagePadding = VDSLayout.Spacing.space2X.value
|
||||
configuration.imagePlacement = type == .next ? .trailing : .leading
|
||||
configuration.titleAlignment = type == .next ? .trailing : .leading
|
||||
configuration.contentInsets = .zero
|
||||
return configuration
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
/// TextStyle used on the titleLabel.
|
||||
open override var textStyle: TextStyle { TextStyle.boldBodySmall }
|
||||
/// UIColor used on the titleLabel text.
|
||||
open override var textColor: UIColor { buttonTextColorConfiguration.getColor(surface) }
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
init(type: Type) {
|
||||
self.type = type
|
||||
super.init()
|
||||
}
|
||||
|
||||
required public init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
public required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
/// Executed on initialization for this View.
|
||||
open override func initialSetup() {
|
||||
super.initialSetup()
|
||||
if #available(iOS 15.0, *) {
|
||||
configuration = buttonConfiguration
|
||||
} else {
|
||||
semanticContentAttribute = type == .next ? .forceRightToLeft : .forceLeftToRight
|
||||
imageEdgeInsets = .init(top: 0, left: 0, bottom: 0, right: VDSLayout.Spacing.space2X.value)
|
||||
}
|
||||
contentHorizontalAlignment = type == .next ? .trailing : .leading
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
open override func updateView() {
|
||||
text = type.title
|
||||
let color = buttonTintColorConfiguration.getColor(surface)
|
||||
setImage(type.image(color), for: .normal)
|
||||
tintColor = color
|
||||
super.updateView()
|
||||
}
|
||||
}
|
||||
|
||||
extension PaginationButton {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Enum to configure PaginationButton
|
||||
//--------------------------------------------------
|
||||
enum `Type` {
|
||||
case previous, next
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .next:
|
||||
"Next"
|
||||
case .previous:
|
||||
"Previous"
|
||||
}
|
||||
}
|
||||
|
||||
private var imageSize: CGSize { Icon.Size.xsmall.dimensions }
|
||||
|
||||
///Image for the configuration type
|
||||
func image(_ color: UIColor) -> UIImage? {
|
||||
switch self {
|
||||
case .previous:
|
||||
UIImage.image(for: .paginationLeftArrow, color: color, renderingMode: .alwaysTemplate)?.resized(to: imageSize)
|
||||
case .next:
|
||||
UIImage.image(for: .paginationRightArrow, color: color, renderingMode: .alwaysTemplate)?.resized(to: imageSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
63
VDS/Components/Pagination/PaginationCellItem.swift
Normal file
63
VDS/Components/Pagination/PaginationCellItem.swift
Normal file
@ -0,0 +1,63 @@
|
||||
//
|
||||
// PaginationCellItem.swift
|
||||
// VDS
|
||||
//
|
||||
// Created by Bandaru, Krishna Kishore on 05/03/24.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import VDSColorTokens
|
||||
|
||||
///This is customised view for Pagination cell item
|
||||
final class PaginationCellItem: UICollectionViewCell {
|
||||
|
||||
///Identifier for the PaginationCellItem
|
||||
static let identifier: String = String(describing: PaginationCellItem.self)
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Properties
|
||||
//--------------------------------------------------
|
||||
///Text color configuration for the element
|
||||
private let textColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark)
|
||||
///Pagination index label
|
||||
private var indexLabel: Label = Label().with {
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
$0.textAlignment = .center
|
||||
$0.isAccessibilityElement = false
|
||||
$0.numberOfLines = 1
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setUp()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setUp()
|
||||
}
|
||||
|
||||
///Configuring the cell with default setup
|
||||
private func setUp() {
|
||||
let containerView = View()
|
||||
containerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
containerView.addSubview(indexLabel)
|
||||
contentView.addSubview(containerView)
|
||||
containerView.pinToSuperView()
|
||||
indexLabel.pinToSuperView()
|
||||
indexLabel.widthGreaterThanEqualTo(VDSLayout.Spacing.space5X.value)
|
||||
contentView.backgroundColor = .clear
|
||||
containerView.backgroundColor = .clear
|
||||
indexLabel.backgroundColor = .clear
|
||||
}
|
||||
|
||||
///Updating UI based on selected index, current index along with surface
|
||||
func update(_ selectedIndex: Int, currentIndex: Int, surface: Surface) {
|
||||
indexLabel.textStyle = selectedIndex == currentIndex ? .boldBodySmall : .bodySmall
|
||||
indexLabel.text = "\(currentIndex + 1)"
|
||||
indexLabel.textColor = textColorConfiguration.getColor(surface)
|
||||
}
|
||||
}
|
||||
34
VDS/Components/Pagination/PaginationChangeLog.txt
Normal file
34
VDS/Components/Pagination/PaginationChangeLog.txt
Normal file
@ -0,0 +1,34 @@
|
||||
MM/DD/YYYY
|
||||
----------------
|
||||
|
||||
Initial Brand 3.0 handoff
|
||||
|
||||
12/17/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
|
||||
|
||||
02/28/2022
|
||||
----------------
|
||||
- Change Page Item Active to Page Item Selected. All Active references changed to Selected.
|
||||
|
||||
03/01/2022
|
||||
----------------
|
||||
- Replaced Left and Right Arrow Non-Scaling icons with VDS Icon.
|
||||
- Removed “weight” and “vector effect” from Anatomy frame.
|
||||
|
||||
08/10/2022
|
||||
----------------
|
||||
- Updated default and inverted prop to light and dark surface.
|
||||
|
||||
11/30/2022
|
||||
----------------
|
||||
- Added "(web only)" to any instance of "keyboard focus"
|
||||
|
||||
12/13/2022
|
||||
----------------
|
||||
- Replaced focus border pixel and style & spacing values with tokens.
|
||||
|
||||
01/12/2023
|
||||
----------------
|
||||
- Removed “Page Item Selected” from Anatomy.
|
||||
45
VDS/Components/Pagination/PaginationContainer.swift
Normal file
45
VDS/Components/Pagination/PaginationContainer.swift
Normal file
@ -0,0 +1,45 @@
|
||||
//
|
||||
// PaginationContainerView.swift
|
||||
// VDS
|
||||
//
|
||||
// Created by Bandaru, Krishna Kishore on 12/03/24.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
///PaginationCollectionView is a container view that holds collectionview for displaying page indexes
|
||||
final class PaginationContainer: View {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Internal Properties
|
||||
//--------------------------------------------------
|
||||
///Notifies when accessibility increment is happend when user swipes up
|
||||
var onAccessibilityIncrement: (() -> Void)?
|
||||
///Notifies when accessibility decrement is happend when user swipes down
|
||||
var onAccessibilityDecrement: (() -> Void)?
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
///Accessibilty traits for the Pagination view
|
||||
override var accessibilityTraits: UIAccessibilityTraits {
|
||||
get { [.adjustable] }
|
||||
set { }
|
||||
}
|
||||
|
||||
///Accessibilty increment
|
||||
override func accessibilityIncrement() {
|
||||
onAccessibilityIncrement?()
|
||||
}
|
||||
|
||||
///Accessibilty decrement
|
||||
override func accessibilityDecrement() {
|
||||
onAccessibilityDecrement?()
|
||||
}
|
||||
|
||||
/// Executed on initialization for this View.
|
||||
override func setup() {
|
||||
super.setup()
|
||||
isAccessibilityElement = true
|
||||
}
|
||||
}
|
||||
91
VDS/Components/Pagination/PaginationFlowLayout.swift
Normal file
91
VDS/Components/Pagination/PaginationFlowLayout.swift
Normal file
@ -0,0 +1,91 @@
|
||||
//
|
||||
// PaginationFlowLayout.swift
|
||||
// VDS
|
||||
//
|
||||
// Created by Bandaru, Krishna Kishore on 06/03/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
///Customised flow layout for Pagination view
|
||||
final class PaginationFlowLayout : UICollectionViewLayout {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Properties
|
||||
//--------------------------------------------------
|
||||
///Spacing between the pagination cells
|
||||
private let spacingBetweenCell: CGFloat = VDSLayout.Spacing.space1X.value
|
||||
///Pre-defined sizes of the pagination cell based on number of digits.
|
||||
private var upperLimitSize: CGSize {
|
||||
switch upperLimitDigits {
|
||||
case 1, 2: .init(width: 20, height: 16)
|
||||
case 3: .init(width: 28, height: 16)
|
||||
default: .init(width: 34, height: 16)
|
||||
}
|
||||
}
|
||||
///Property to store the defined layout attributes.
|
||||
private var itemCache : [UICollectionViewLayoutAttributes] = []
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Internal Properties
|
||||
//--------------------------------------------------
|
||||
///Maximum number of page indexes shown on UI
|
||||
let maxNumberOfColumns: Int = 4
|
||||
///Number of digits of the maximum page index.
|
||||
var upperLimitDigits: Int = 0
|
||||
///Number of page indexes shown on UI.
|
||||
var numberOfColumns: Int = 4
|
||||
///A property that publishes when there is change in collection view width.
|
||||
@Published var collectionViewWidth: CGFloat = 0
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
///Preparing the layout collection attributes for pagination and updating the collectionview width.
|
||||
override func prepare() {
|
||||
|
||||
guard let collectionView else { return }
|
||||
|
||||
itemCache.removeAll()
|
||||
var xPos : CGFloat = 0
|
||||
for item in 0..<collectionView.numberOfItems(inSection: 0) {
|
||||
let indexPath = IndexPath(item: item, section: 0)
|
||||
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
|
||||
attributes.frame = .init(origin: .init(x: xPos, y: 0), size: upperLimitSize)
|
||||
xPos += (spacingBetweenCell + upperLimitSize.width)
|
||||
itemCache.append(attributes)
|
||||
}
|
||||
guard !itemCache.isEmpty else { return }
|
||||
var rightMostVisibleItem: UICollectionViewLayoutAttributes?
|
||||
if numberOfColumns < itemCache.count {
|
||||
rightMostVisibleItem = itemCache[max(numberOfColumns - 1, 0)]
|
||||
} else {
|
||||
rightMostVisibleItem = itemCache.last
|
||||
}
|
||||
if let rightMostVisibleItem {
|
||||
collectionViewWidth = rightMostVisibleItem.frame.minX + rightMostVisibleItem.frame.width
|
||||
}
|
||||
}
|
||||
|
||||
///This will return the layout attributes for the elements in the defined rect
|
||||
override func layoutAttributesForElements(in rect: CGRect)-> [UICollectionViewLayoutAttributes]? {
|
||||
var visibleLayoutAttributes: [UICollectionViewLayoutAttributes] = []
|
||||
for attributes in itemCache {
|
||||
if attributes.frame.intersects(rect) {
|
||||
visibleLayoutAttributes.append(attributes)
|
||||
}
|
||||
}
|
||||
return visibleLayoutAttributes
|
||||
}
|
||||
|
||||
///This will return the layout attributes at particular indexPath
|
||||
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
|
||||
return itemCache[indexPath.row]
|
||||
}
|
||||
|
||||
///Returns the collectionview content size
|
||||
override var collectionViewContentSize: CGSize {
|
||||
guard let lastAttribute = itemCache.last else { return super.collectionViewContentSize }
|
||||
return .init(width: lastAttribute.frame.width + lastAttribute.frame.origin.x, height: 16)
|
||||
}
|
||||
}
|
||||
@ -100,7 +100,6 @@ open class RadioBoxItem: Control, Changeable, FormFieldable {
|
||||
/// If provided, the RadioBox textAttributedText will be rendered.
|
||||
open var textAttributedText: NSAttributedString? {
|
||||
didSet {
|
||||
textLabel.useAttributedText = !(textAttributedText?.string.isEmpty ?? true)
|
||||
textLabel.attributedText = textAttributedText
|
||||
setNeedsUpdate()
|
||||
}
|
||||
@ -115,7 +114,6 @@ open class RadioBoxItem: Control, Changeable, FormFieldable {
|
||||
/// If provided, the RadioBox subTextAttributedText will be rendered.
|
||||
open var subTextAttributedText: NSAttributedString? {
|
||||
didSet {
|
||||
subTextLabel.useAttributedText = !(subTextAttributedText?.string.isEmpty ?? true)
|
||||
subTextLabel.attributedText = subTextAttributedText
|
||||
setNeedsUpdate()
|
||||
}
|
||||
@ -130,7 +128,6 @@ open class RadioBoxItem: Control, Changeable, FormFieldable {
|
||||
/// If provided, the RadioBox subTextRightAttributedText will be rendered.
|
||||
open var subTextRightAttributedText: NSAttributedString? {
|
||||
didSet {
|
||||
subTextRightLabel.useAttributedText = !(subTextRightAttributedText?.string.isEmpty ?? true)
|
||||
subTextRightLabel.attributedText = subTextRightAttributedText
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
@ -264,7 +264,6 @@ open class Tabs: View {
|
||||
model.onClick?(tab.index)
|
||||
self.selectedIndex = tab.index
|
||||
self.onTabDidSelect?(tab.index)
|
||||
let t = tabViews[tab.index]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ import Combine
|
||||
|
||||
/// Base Class used to build out a Input controls.
|
||||
@objc(VDSEntryField)
|
||||
open class EntryFieldBase: Control, Changeable, FormFieldable {
|
||||
open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
@ -153,8 +153,11 @@ open class EntryFieldBase: Control, Changeable, FormFieldable {
|
||||
/// Whether not to show the error.
|
||||
open var showError: Bool = false { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// FormFieldValidator
|
||||
internal var validator: (any FormFieldValidatorable)?
|
||||
|
||||
/// Whether or not to show the internal error
|
||||
open internal(set) var hasInternalError: Bool = false { didSet { setNeedsUpdate() } }
|
||||
open var hasInternalError: Bool { !(validator?.isValid ?? true) }
|
||||
|
||||
/// Override UIControl state to add the .error state if showError is true.
|
||||
open override var state: UIControl.State {
|
||||
@ -175,7 +178,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldable {
|
||||
}
|
||||
}
|
||||
|
||||
internal var internalErrorText: String? {
|
||||
open var internalErrorText: String? {
|
||||
didSet {
|
||||
updateContainerView()
|
||||
updateErrorLabel()
|
||||
@ -200,19 +203,18 @@ open class EntryFieldBase: Control, Changeable, FormFieldable {
|
||||
open var inputId: String? { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// The text of this textField.
|
||||
private var _value: AnyHashable?
|
||||
open var value: AnyHashable? {
|
||||
private var _value: String?
|
||||
open var value: String? {
|
||||
get { _value }
|
||||
set {
|
||||
if let newValue, newValue != _value {
|
||||
_value = newValue
|
||||
text = newValue as? String
|
||||
text = newValue
|
||||
}
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
open var defaultValue: AnyHashable? { didSet { setNeedsUpdate() } }
|
||||
|
||||
open var required: Bool = false { didSet { setNeedsUpdate() } }
|
||||
@ -324,6 +326,8 @@ open class EntryFieldBase: Control, Changeable, FormFieldable {
|
||||
updateHelperLabel()
|
||||
|
||||
backgroundColor = surface.color
|
||||
validator?.validate()
|
||||
internalErrorText = validator?.errorMessage
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
|
||||
@ -89,7 +89,7 @@ open class InputField: EntryFieldBase, UITextFieldDelegate {
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var _showError: Bool = false
|
||||
/// Whether not to show the error.
|
||||
open override var showError: Bool {
|
||||
|
||||
@ -107,17 +107,19 @@ open class TextArea: EntryFieldBase {
|
||||
}
|
||||
|
||||
/// The text of this textView
|
||||
private var _text: String?
|
||||
open override var text: String? {
|
||||
get { textView.text }
|
||||
set {
|
||||
if let newValue, newValue != text {
|
||||
if let newValue, newValue != _text {
|
||||
_text = newValue
|
||||
textView.text = newValue
|
||||
value = newValue
|
||||
}
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// UITextView shown in the TextArea.
|
||||
open var textView = TextView().with {
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
@ -125,6 +127,8 @@ open class TextArea: EntryFieldBase {
|
||||
$0.isScrollEnabled = false
|
||||
}
|
||||
|
||||
open override var maxLength: Int? { willSet { countRule.maxLength = newValue }}
|
||||
|
||||
/// Color configuration for error icon.
|
||||
internal var iconColorConfiguration = ControlColorConfiguration().with {
|
||||
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .normal)
|
||||
@ -147,6 +151,7 @@ open class TextArea: EntryFieldBase {
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
accessibilityLabel = "TextArea"
|
||||
validator = FormFieldValidator<TextArea>(field: self, rules: [.init(countRule)])
|
||||
|
||||
containerStackView.pinToSuperView(.uniform(VDSFormControls.spaceInset))
|
||||
minWidthConstraint = containerView.widthAnchor.constraint(greaterThanOrEqualToConstant: containerSize.width)
|
||||
@ -187,7 +192,6 @@ open class TextArea: EntryFieldBase {
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
open override func updateView() {
|
||||
super.updateView()
|
||||
|
||||
textView.isEditable = isEnabled
|
||||
textView.isEnabled = isEnabled
|
||||
textView.surface = surface
|
||||
@ -202,13 +206,8 @@ open class TextArea: EntryFieldBase {
|
||||
widthConstraint?.isActive = false
|
||||
minWidthConstraint?.isActive = true
|
||||
}
|
||||
|
||||
let characterError = getCharacterCounterText()
|
||||
if let maxLength, maxLength > 0 {
|
||||
characterCounterLabel.text = characterError
|
||||
} else {
|
||||
characterCounterLabel.text = ""
|
||||
}
|
||||
|
||||
characterCounterLabel.text = getCharacterCounterText()
|
||||
|
||||
icon.size = .medium
|
||||
icon.color = iconColorConfiguration.getColor(self)
|
||||
@ -247,17 +246,11 @@ open class TextArea: EntryFieldBase {
|
||||
let countStr = (count > maxLength ?? 0) ? ("-" + "\(count-(maxLength ?? 0))") : "\(count)"
|
||||
if let maxLength, maxLength > 0 {
|
||||
if count > maxLength {
|
||||
hasInternalError = true
|
||||
internalErrorText = "You have exceeded the character limit."
|
||||
return countStr
|
||||
} else {
|
||||
hasInternalError = false
|
||||
internalErrorText = nil
|
||||
return ("\(countStr)" + "/" + "\(maxLength)")
|
||||
}
|
||||
} else {
|
||||
hasInternalError = false
|
||||
internalErrorText = nil
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@ -277,6 +270,21 @@ open class TextArea: EntryFieldBase {
|
||||
|
||||
textView.textAttributes = textAttributes
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Validation
|
||||
//--------------------------------------------------
|
||||
var countRule = CharacterCountRule()
|
||||
|
||||
class CharacterCountRule: Rule {
|
||||
var maxLength: Int?
|
||||
var errorMessage: String = "You have exceeded the character limit."
|
||||
|
||||
func isValid(value: String?) -> Bool {
|
||||
guard let text = value, let maxLength, maxLength > 0 else { return true }
|
||||
return text.count <= maxLength
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TextArea: UITextViewDelegate {
|
||||
|
||||
@ -94,6 +94,7 @@ open class TextView: UITextView, ViewProtocol {
|
||||
//--------------------------------------------------
|
||||
open func initialSetup() {
|
||||
if !initialSetupPerformed {
|
||||
initialSetupPerformed = true
|
||||
backgroundColor = .clear
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
accessibilityCustomActions = []
|
||||
|
||||
@ -11,7 +11,39 @@ import VDSFormControlsTokens
|
||||
import UIKit
|
||||
|
||||
@objc(VDSTileContainer)
|
||||
open class TileContainer: Control {
|
||||
open class TileContainer: TileContainerBase<TileContainer.Padding> {
|
||||
|
||||
/// Enum used to describe the padding choices used for this component.
|
||||
public enum Padding: DefaultValuing {
|
||||
case padding2X
|
||||
case padding4X
|
||||
case padding6X
|
||||
case padding8X
|
||||
case padding12X
|
||||
case custom(CGFloat)
|
||||
|
||||
public static var defaultValue: Self { .padding4X }
|
||||
|
||||
public var value: CGFloat {
|
||||
switch self {
|
||||
case .padding2X:
|
||||
return VDSLayout.Spacing.space2X.value
|
||||
case .padding4X:
|
||||
return VDSLayout.Spacing.space4X.value
|
||||
case .padding6X:
|
||||
return VDSLayout.Spacing.space6X.value
|
||||
case .padding8X:
|
||||
return VDSLayout.Spacing.space8X.value
|
||||
case .padding12X:
|
||||
return VDSLayout.Spacing.space12X.value
|
||||
case .custom(let padding):
|
||||
return padding
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open class TileContainerBase<PaddingType: DefaultValuing>: Control where PaddingType.ValueType == CGFloat {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
@ -53,33 +85,6 @@ open class TileContainer: Control {
|
||||
case gradient(String, String)
|
||||
case none
|
||||
}
|
||||
|
||||
/// Enum used to describe the padding choices used for this component.
|
||||
public enum Padding {
|
||||
case padding2X
|
||||
case padding4X
|
||||
case padding6X
|
||||
case padding8X
|
||||
case padding12X
|
||||
case custom(CGFloat)
|
||||
|
||||
public var value: CGFloat {
|
||||
switch self {
|
||||
case .padding2X:
|
||||
return VDSLayout.Spacing.space2X.value
|
||||
case .padding4X:
|
||||
return VDSLayout.Spacing.space4X.value
|
||||
case .padding6X:
|
||||
return VDSLayout.Spacing.space6X.value
|
||||
case .padding8X:
|
||||
return VDSLayout.Spacing.space8X.value
|
||||
case .padding12X:
|
||||
return VDSLayout.Spacing.space12X.value
|
||||
case .custom(let padding):
|
||||
return padding
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum used to describe the aspect ratios used for this component.
|
||||
public enum AspectRatio: String, CaseIterable {
|
||||
@ -130,7 +135,7 @@ open class TileContainer: Control {
|
||||
open var backgroundEffect: BackgroundEffect = .none { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// Sets the inside padding for the component
|
||||
open var padding: Padding = .padding4X { didSet { setNeedsUpdate() } }
|
||||
open var padding: PaddingType = PaddingType.defaultValue { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// Applies a background color if backgroundImage prop fails or has trouble loading.
|
||||
open var imageFallbackColor: Surface = .light { didSet { setNeedsUpdate() } }
|
||||
@ -235,7 +240,12 @@ open class TileContainer: Control {
|
||||
|
||||
heightConstraint = layoutGuide.heightAnchor.constraint(equalToConstant: 0)
|
||||
|
||||
backgroundImageView.pin(layoutGuide)
|
||||
backgroundImageView
|
||||
.pinTop(layoutGuide.topAnchor)
|
||||
.pinLeading(layoutGuide.leadingAnchor)
|
||||
.pinTrailing(layoutGuide.trailingAnchor)
|
||||
.pinBottom(layoutGuide.bottomAnchor, 0, .defaultLow)
|
||||
|
||||
backgroundImageView.isUserInteractionEnabled = false
|
||||
backgroundImageView.isHidden = true
|
||||
|
||||
@ -252,6 +262,7 @@ open class TileContainer: Control {
|
||||
layer.cornerRadius = cornerRadius
|
||||
backgroundImageView.layer.cornerRadius = cornerRadius
|
||||
highlightView.layer.cornerRadius = cornerRadius
|
||||
clipsToBounds = true
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
@ -259,7 +270,6 @@ open class TileContainer: Control {
|
||||
super.reset()
|
||||
shouldUpdateView = false
|
||||
color = .white
|
||||
padding = .padding4X
|
||||
aspectRatio = .ratio1x1
|
||||
imageFallbackColor = .light
|
||||
width = nil
|
||||
@ -323,7 +333,14 @@ open class TileContainer: Control {
|
||||
}
|
||||
applyBackgroundEffects()
|
||||
}
|
||||
|
||||
|
||||
/// Used to update frames for the added CAlayers to our view
|
||||
open override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
dropShadowLayers?.forEach { $0.frame = bounds }
|
||||
gradientLayers?.forEach { $0.frame = bounds }
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Methods
|
||||
//--------------------------------------------------
|
||||
@ -401,11 +418,11 @@ open class TileContainer: Control {
|
||||
|
||||
}
|
||||
|
||||
extension TileContainer {
|
||||
extension TileContainerBase {
|
||||
|
||||
final class BackgroundColorConfiguration: ObjectColorable {
|
||||
|
||||
typealias ObjectType = TileContainer
|
||||
typealias ObjectType = TileContainerBase
|
||||
|
||||
let primaryColorConfig = SurfaceColorConfiguration(VDSColor.backgroundPrimaryLight, VDSColor.backgroundPrimaryDark)
|
||||
let secondaryColorConfig = SurfaceColorConfiguration(VDSColor.backgroundSecondaryLight, VDSColor.backgroundSecondaryDark)
|
||||
@ -415,7 +432,7 @@ extension TileContainer {
|
||||
|
||||
required init() { }
|
||||
|
||||
func getColor(_ object: TileContainer) -> UIColor {
|
||||
func getColor(_ object: ObjectType) -> UIColor {
|
||||
switch object.color {
|
||||
case .primary:
|
||||
primaryColorConfig.getColor(object.surface)
|
||||
|
||||
@ -16,7 +16,38 @@ import Combine
|
||||
/// while it can include an arrow CTA, it does not require one in order to
|
||||
/// function.
|
||||
@objc(VDSTilelet)
|
||||
open class Tilelet: TileContainer {
|
||||
open class Tilelet: TileContainerBase<Tilelet.Padding> {
|
||||
|
||||
/// Enum used to describe the padding choices used for this component.
|
||||
public enum Padding: String, DefaultValuing, CaseIterable {
|
||||
case small
|
||||
case large
|
||||
|
||||
public static var defaultValue: Self { .large }
|
||||
|
||||
public var value: CGFloat {
|
||||
switch self {
|
||||
case .small:
|
||||
return UIDevice.isIPad ? VDSLayout.Spacing.space3X.value : VDSLayout.Spacing.space4X.value
|
||||
case .large:
|
||||
return UIDevice.isIPad ? VDSLayout.Spacing.space4X.value : VDSLayout.Spacing.space6X.value
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate var titleLockupBottomSpacing: CGFloat {
|
||||
switch self.value {
|
||||
case VDSLayout.Spacing.space3X.value:
|
||||
return VDSLayout.Spacing.space4X.value
|
||||
case VDSLayout.Spacing.space4X.value:
|
||||
return VDSLayout.Spacing.space6X.value
|
||||
case VDSLayout.Spacing.space4X.value:
|
||||
return VDSLayout.Spacing.space8X.value
|
||||
default:
|
||||
return VDSLayout.Spacing.space4X.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
@ -39,6 +70,7 @@ open class Tilelet: TileContainer {
|
||||
/// Enum to represent the Vertical Layout of the Text.
|
||||
public enum TextPosition: String, CaseIterable {
|
||||
case top
|
||||
case middle
|
||||
case bottom
|
||||
}
|
||||
|
||||
@ -72,7 +104,7 @@ open class Tilelet: TileContainer {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
//--------------------------------------------------
|
||||
public override var onClickSubscriber: AnyCancellable? {
|
||||
didSet {
|
||||
isAccessibilityElement = onClickSubscriber != nil
|
||||
@ -82,6 +114,27 @@ open class Tilelet: TileContainer {
|
||||
/// Title lockup positioned in the contentView.
|
||||
open var titleLockup = TitleLockup().with {
|
||||
$0.standardStyleConfiguration = .init(styleConfigurations: [
|
||||
.init(deviceType: .iPhone,
|
||||
titleStandardStyles: [.bodySmall],
|
||||
spacingConfigurations: [
|
||||
.init(otherStandardStyles: [.bodySmall],
|
||||
topSpacing: VDSLayout.Spacing.space1X.value,
|
||||
bottomSpacing: VDSLayout.Spacing.space1X.value)
|
||||
]),
|
||||
.init(deviceType: .iPhone,
|
||||
titleStandardStyles: [.bodyMedium],
|
||||
spacingConfigurations: [
|
||||
.init(otherStandardStyles: [.bodyMedium],
|
||||
topSpacing: VDSLayout.Spacing.space1X.value,
|
||||
bottomSpacing: VDSLayout.Spacing.space1X.value)
|
||||
]),
|
||||
.init(deviceType: .iPhone,
|
||||
titleStandardStyles: [.bodyLarge],
|
||||
spacingConfigurations: [
|
||||
.init(otherStandardStyles: [.bodyLarge],
|
||||
topSpacing: VDSLayout.Spacing.space1X.value,
|
||||
bottomSpacing: VDSLayout.Spacing.space1X.value)
|
||||
]),
|
||||
.init(deviceType: .iPhone,
|
||||
titleStandardStyles: [.titleSmall],
|
||||
spacingConfigurations: [
|
||||
@ -103,6 +156,27 @@ open class Tilelet: TileContainer {
|
||||
topSpacing: VDSLayout.Spacing.space3X.value,
|
||||
bottomSpacing: VDSLayout.Spacing.space3X.value)
|
||||
]),
|
||||
.init(deviceType: .iPad,
|
||||
titleStandardStyles: [.bodySmall],
|
||||
spacingConfigurations: [
|
||||
.init(otherStandardStyles: [.bodySmall],
|
||||
topSpacing: VDSLayout.Spacing.space2X.value,
|
||||
bottomSpacing: VDSLayout.Spacing.space2X.value)
|
||||
]),
|
||||
.init(deviceType: .iPad,
|
||||
titleStandardStyles: [.bodyMedium],
|
||||
spacingConfigurations: [
|
||||
.init(otherStandardStyles: [.bodyMedium],
|
||||
topSpacing: VDSLayout.Spacing.space1X.value,
|
||||
bottomSpacing: VDSLayout.Spacing.space1X.value)
|
||||
]),
|
||||
.init(deviceType: .iPad,
|
||||
titleStandardStyles: [.bodyLarge],
|
||||
spacingConfigurations: [
|
||||
.init(otherStandardStyles: [.bodyLarge],
|
||||
topSpacing: VDSLayout.Spacing.space1X.value,
|
||||
bottomSpacing: VDSLayout.Spacing.space1X.value)
|
||||
]),
|
||||
.init(deviceType: .iPad,
|
||||
titleStandardStyles: [.titleSmall, .titleMedium],
|
||||
spacingConfigurations: [
|
||||
@ -176,6 +250,9 @@ open class Tilelet: TileContainer {
|
||||
/// If set, this is used to render the subTitleLabel of the TitleLockup.
|
||||
open var subTitleModel: SubTitleModel? { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// If set, this is used to render the eyebrowLabel of the TitleLockup.
|
||||
open var eyebrowModel: EyebrowModel? { didSet { setNeedsUpdate() } }
|
||||
|
||||
//only 1 Icon can be active
|
||||
private var _descriptiveIconModel: DescriptiveIcon?
|
||||
|
||||
@ -206,6 +283,17 @@ open class Tilelet: TileContainer {
|
||||
//--------------------------------------------------
|
||||
internal var titleLockupWidthConstraint: NSLayoutConstraint?
|
||||
internal var titleLockupTrailingConstraint: NSLayoutConstraint?
|
||||
internal var titleLockupTopConstraint: NSLayoutConstraint?
|
||||
internal var titleLockupBottomConstraint: NSLayoutConstraint?
|
||||
internal var titleLockupTopGreaterThanConstraint: NSLayoutConstraint?
|
||||
internal var titleLockupBottomGreaterThanConstraint: NSLayoutConstraint?
|
||||
internal var titleLockupCenterYConstraint: NSLayoutConstraint?
|
||||
internal var titleLockupTitleLabelBottomConstraint: NSLayoutConstraint?
|
||||
//Truncation constraints
|
||||
internal var badgeLabelHeightGreaterThanConstraint: NSLayoutConstraint?
|
||||
internal var titleLockupEyebrowLabelHeightGreaterThanConstraint: NSLayoutConstraint?
|
||||
internal var titleLockupTitleLabelHeightGreaterThanConstraint: NSLayoutConstraint?
|
||||
internal var titleLockupSubTitleLabelHeightGreaterThanConstraint: NSLayoutConstraint?
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Overrides
|
||||
@ -230,12 +318,17 @@ open class Tilelet: TileContainer {
|
||||
|
||||
titleLockupContainerView.addSubview(titleLockup)
|
||||
titleLockup
|
||||
.pinTop()
|
||||
.pinLeading()
|
||||
.pinBottom()
|
||||
titleLockupTopConstraint = titleLockup.topAnchor.constraint(equalTo: titleLockupContainerView.topAnchor)
|
||||
titleLockupTopConstraint?.activate()
|
||||
titleLockupBottomConstraint = titleLockupContainerView.bottomAnchor.constraint(equalTo: titleLockup.bottomAnchor)
|
||||
titleLockupBottomConstraint?.activate()
|
||||
titleLockupTrailingConstraint = titleLockup.trailingAnchor.constraint(equalTo: titleLockupContainerView.trailingAnchor)
|
||||
titleLockupTrailingConstraint?.isActive = true
|
||||
|
||||
titleLockupTrailingConstraint?.activate()
|
||||
titleLockupBottomGreaterThanConstraint = titleLockupContainerView.bottomAnchor.constraint(greaterThanOrEqualTo: titleLockup.bottomAnchor)
|
||||
titleLockupTopGreaterThanConstraint = titleLockup.topAnchor.constraint(greaterThanOrEqualTo: titleLockupContainerView.topAnchor)
|
||||
titleLockupCenterYConstraint = titleLockup.centerYAnchor.constraint(equalTo: titleLockupContainerView.centerYAnchor)
|
||||
|
||||
iconContainerView.addSubview(descriptiveIcon)
|
||||
iconContainerView.addSubview(directionalIcon)
|
||||
|
||||
@ -249,6 +342,47 @@ open class Tilelet: TileContainer {
|
||||
.pinTop()
|
||||
.pinBottom()
|
||||
|
||||
badge.bottomAnchor.constraint(equalTo: badge.label.bottomAnchor, constant: 2).activate()
|
||||
|
||||
/**
|
||||
Truncation:
|
||||
If a Tilelet has only a Title or a Subtitle, then the Title or Subtitle is truncated and appended with an ellipsis when there is not enough space to display the full text.
|
||||
If a Tilelet has both Title and Subtitle, then only Subtitle will be truncated.
|
||||
If a Tilelet has Badge, Title and Subtitle, then Subtitle will be truncated first and Badge will be truncated second. Title will be truncated last (lowest priority).
|
||||
Atleast one line text based on priority
|
||||
Minimum bottom space below Badge is 4px; less than 4px results in truncation.
|
||||
*/
|
||||
let labelPriority = UILayoutPriority.defaultHigh.rawValue
|
||||
titleLockup.titleLabel.setContentCompressionResistancePriority(UILayoutPriority(labelPriority), for: .vertical)
|
||||
badge.label.setContentCompressionResistancePriority(UILayoutPriority(labelPriority-1), for: .vertical)
|
||||
titleLockup.subTitleLabel.setContentCompressionResistancePriority(UILayoutPriority(labelPriority-2), for: .vertical)
|
||||
titleLockup.eyebrowLabel.setContentCompressionResistancePriority(UILayoutPriority(labelPriority-3), for: .vertical)
|
||||
|
||||
titleLockup.titleLabel.setContentHuggingPriority(UILayoutPriority(labelPriority), for: .vertical)
|
||||
badge.label.setContentHuggingPriority(UILayoutPriority(labelPriority-1), for: .vertical)
|
||||
titleLockup.subTitleLabel.setContentHuggingPriority(UILayoutPriority(labelPriority-2), for: .vertical)
|
||||
titleLockup.eyebrowLabel.setContentHuggingPriority(UILayoutPriority(labelPriority-3), for: .vertical)
|
||||
|
||||
/**
|
||||
Added these constraints for:
|
||||
At fixed width & height if all the labels(Badge, Eyebrow, Title, Subtitle) are having more number of lines then we should display atleast one line of content per label instead of pushing labels out of bounds.
|
||||
So adding minimum single line height constraint
|
||||
*/
|
||||
badgeLabelHeightGreaterThanConstraint = badge.label.heightGreaterThanEqualTo(constant: badge.label.minimumLineHeight)
|
||||
badgeLabelHeightGreaterThanConstraint?.priority = .defaultHigh
|
||||
badgeLabelHeightGreaterThanConstraint?.activate()
|
||||
|
||||
titleLockupEyebrowLabelHeightGreaterThanConstraint = titleLockup.eyebrowLabel.heightGreaterThanEqualTo(constant: titleLockup.eyebrowLabel.minimumLineHeight)
|
||||
titleLockupEyebrowLabelHeightGreaterThanConstraint?.priority = .defaultHigh
|
||||
titleLockupEyebrowLabelHeightGreaterThanConstraint?.activate()
|
||||
|
||||
titleLockupTitleLabelHeightGreaterThanConstraint = titleLockup.titleLabel.heightGreaterThanEqualTo(constant: titleLockup.titleLabel.minimumLineHeight)
|
||||
titleLockupTitleLabelHeightGreaterThanConstraint?.priority = .defaultHigh
|
||||
titleLockupTitleLabelHeightGreaterThanConstraint?.activate()
|
||||
|
||||
titleLockupSubTitleLabelHeightGreaterThanConstraint = titleLockup.subTitleLabel.heightGreaterThanEqualTo(constant: titleLockup.subTitleLabel.minimumLineHeight)
|
||||
titleLockupSubTitleLabelHeightGreaterThanConstraint?.priority = .defaultHigh
|
||||
titleLockupSubTitleLabelHeightGreaterThanConstraint?.activate()
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
@ -261,7 +395,7 @@ open class Tilelet: TileContainer {
|
||||
titleModel = nil
|
||||
subTitleModel = nil
|
||||
descriptiveIconModel = nil
|
||||
directionalIconModel = nil
|
||||
directionalIconModel = nil
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
@ -273,7 +407,11 @@ open class Tilelet: TileContainer {
|
||||
updateBadge()
|
||||
updateTitleLockup()
|
||||
updateIcons()
|
||||
|
||||
///Content-driven height Tilelets - Minimum height is configurable.
|
||||
///if width != nil && (aspectRatio != .none || height != nil) then tilelet is not self growing, so we can apply text position alignments.
|
||||
if width != nil && (aspectRatio != .none || height != nil) {
|
||||
updateTextPositionAlignment()
|
||||
}
|
||||
layoutIfNeeded()
|
||||
}
|
||||
|
||||
@ -294,6 +432,7 @@ open class Tilelet: TileContainer {
|
||||
badge.numberOfLines = badgeModel.numberOfLines
|
||||
badge.surface = badgeModel.surface
|
||||
badge.maxWidth = badgeModel.maxWidth
|
||||
badgeLabelHeightGreaterThanConstraint?.constant = badge.label.minimumLineHeight
|
||||
if badgeContainerView.superview == nil {
|
||||
stackView.insertArrangedSubview(badgeContainerView, at: 0)
|
||||
setNeedsLayout()
|
||||
@ -306,7 +445,11 @@ open class Tilelet: TileContainer {
|
||||
private func updateTitleLockup() {
|
||||
|
||||
var showTitleLockup = false
|
||||
|
||||
|
||||
if let eyebrowModel, !eyebrowModel.text.isEmpty {
|
||||
showTitleLockup = true
|
||||
}
|
||||
|
||||
if let titleModel, !titleModel.text.isEmpty {
|
||||
showTitleLockup = true
|
||||
}
|
||||
@ -350,6 +493,7 @@ open class Tilelet: TileContainer {
|
||||
}
|
||||
|
||||
//set models
|
||||
titleLockup.eyebrowModel = eyebrowModel?.toTitleLockupEyebrowModel()
|
||||
titleLockup.titleModel = titleModel?.toTitleLockupTitleModel()
|
||||
titleLockup.subTitleModel = subTitleModel?.toTitleLockupSubTitleModel()
|
||||
|
||||
@ -360,6 +504,9 @@ open class Tilelet: TileContainer {
|
||||
} else {
|
||||
removeFromSuperview(titleLockupContainerView)
|
||||
}
|
||||
titleLockupEyebrowLabelHeightGreaterThanConstraint?.constant = titleLockup.eyebrowLabel.minimumLineHeight
|
||||
titleLockupTitleLabelHeightGreaterThanConstraint?.constant = titleLockup.titleLabel.minimumLineHeight
|
||||
titleLockupSubTitleLabelHeightGreaterThanConstraint?.constant = titleLockup.subTitleLabel.minimumLineHeight
|
||||
}
|
||||
|
||||
private func updateIcons() {
|
||||
@ -392,7 +539,7 @@ open class Tilelet: TileContainer {
|
||||
view = titleLockupContainerView
|
||||
}
|
||||
if let view {
|
||||
stackView.setCustomSpacing(padding.tiletSpacing, after: view)
|
||||
stackView.setCustomSpacing(padding.titleLockupBottomSpacing, after: view)
|
||||
}
|
||||
if iconContainerView.superview == nil {
|
||||
stackView.addArrangedSubview(iconContainerView)
|
||||
@ -402,21 +549,33 @@ open class Tilelet: TileContainer {
|
||||
removeFromSuperview(iconContainerView)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TileContainer.Padding {
|
||||
fileprivate var tiletSpacing: CGFloat {
|
||||
switch self {
|
||||
case .padding2X:
|
||||
return 16
|
||||
case .padding4X:
|
||||
return 24
|
||||
case .padding6X:
|
||||
return 32
|
||||
case .padding8X:
|
||||
return 48
|
||||
default:
|
||||
return 16
|
||||
|
||||
private func updateTextPositionAlignment() {
|
||||
switch textPostion {
|
||||
case .top:
|
||||
titleLockupTopConstraint?.activate()
|
||||
titleLockupTopGreaterThanConstraint?.deactivate()
|
||||
titleLockupBottomConstraint?.deactivate()
|
||||
titleLockupBottomGreaterThanConstraint?.activate()
|
||||
titleLockupCenterYConstraint?.deactivate()
|
||||
case .middle:
|
||||
titleLockupTopConstraint?.deactivate()
|
||||
titleLockupTopGreaterThanConstraint?.activate()
|
||||
titleLockupBottomConstraint?.deactivate()
|
||||
titleLockupBottomGreaterThanConstraint?.activate()
|
||||
titleLockupCenterYConstraint?.activate()
|
||||
case .bottom:
|
||||
titleLockupTopConstraint?.deactivate()
|
||||
titleLockupTopGreaterThanConstraint?.activate()
|
||||
titleLockupBottomConstraint?.activate()
|
||||
titleLockupBottomGreaterThanConstraint?.deactivate()
|
||||
titleLockupCenterYConstraint?.deactivate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Label {
|
||||
|
||||
///To calculate label single line height
|
||||
fileprivate var minimumLineHeight: CGFloat { textStyle.lineHeight }
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
extension Tilelet {
|
||||
|
||||
@ -26,12 +27,16 @@ extension Tilelet {
|
||||
/// Max width that will be used for the badge.
|
||||
public var maxWidth: CGFloat?
|
||||
|
||||
public init(text: String, fillColor: Badge.FillColor = .red, surface: Surface = .light, numberOfLines: Int = 0, maxWidth: CGFloat? = nil) {
|
||||
/// LineBreakMode used in Badge label.
|
||||
public var lineBreakMode: NSLineBreakMode
|
||||
|
||||
public init(text: String, fillColor: Badge.FillColor = .red, surface: Surface = .light, numberOfLines: Int = 0, maxWidth: CGFloat? = nil, lineBreakMode: NSLineBreakMode = .byTruncatingTail) {
|
||||
self.text = text
|
||||
self.fillColor = fillColor
|
||||
self.surface = surface
|
||||
self.numberOfLines = numberOfLines
|
||||
self.maxWidth = maxWidth
|
||||
self.lineBreakMode = lineBreakMode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
extension Tilelet {
|
||||
/// Model that represents the options available for the sub title label.
|
||||
@ -18,7 +19,8 @@ extension Tilelet {
|
||||
case bodyLarge
|
||||
case bodyMedium
|
||||
case bodySmall
|
||||
|
||||
case titleSmall
|
||||
case titleMedium
|
||||
public var defaultValue: TitleLockup.OtherStandardStyle { .bodySmall }
|
||||
}
|
||||
//--------------------------------------------------
|
||||
@ -36,17 +38,22 @@ extension Tilelet {
|
||||
/// Text color that will be used for the subTitle label.
|
||||
public var textColor: Use = .primary
|
||||
|
||||
/// LineBreakMode used in Badge label.
|
||||
public var lineBreakMode: NSLineBreakMode
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
public init(text: String,
|
||||
otherStandardStyle: OtherStandardStyle = .bodySmall,
|
||||
textColor: Use = .primary,
|
||||
textAttributes: [any LabelAttributeModel]? = nil) {
|
||||
textAttributes: [any LabelAttributeModel]? = nil,
|
||||
lineBreakMode: NSLineBreakMode = .byTruncatingTail) {
|
||||
self.text = text
|
||||
self.otherStandardStyle = otherStandardStyle
|
||||
self.textAttributes = textAttributes
|
||||
self.textColor = textColor
|
||||
self.lineBreakMode = lineBreakMode
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
@ -57,7 +64,7 @@ extension Tilelet {
|
||||
TitleLockup.SubTitleModel(text: text,
|
||||
otherStandardStyle: otherStandardStyle.value,
|
||||
textColor: textColor,
|
||||
textAttributes: textAttributes)
|
||||
textAttributes: textAttributes, lineBreakMode: lineBreakMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
extension Tilelet {
|
||||
/// Model that represents the options available for the title label.
|
||||
@ -19,7 +20,10 @@ extension Tilelet {
|
||||
case titleLarge
|
||||
case titleMedium
|
||||
case titleSmall
|
||||
|
||||
case bodyLarge
|
||||
case bodyMedium
|
||||
case bodySmall
|
||||
|
||||
public var defaultValue: TitleLockup.TitleStandardStyle { .titleSmall }
|
||||
}
|
||||
//--------------------------------------------------
|
||||
@ -28,21 +32,30 @@ extension Tilelet {
|
||||
/// Text that will be used for the title label.
|
||||
public var text: String = ""
|
||||
|
||||
/// Used in combination with standardStyle to set the textStyle that will be used for the title label.
|
||||
public var isBold: Bool = false
|
||||
/// Text attributes that will be used for the title label.
|
||||
public var textAttributes: [any LabelAttributeModel]?
|
||||
|
||||
/// Text style that will be used for the title label.
|
||||
public var standardStyle: StandardStyle = .titleSmall
|
||||
|
||||
/// LineBreakMode used in Badge label.
|
||||
public var lineBreakMode: NSLineBreakMode
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
public init(text: String,
|
||||
textAttributes: [any LabelAttributeModel]? = nil,
|
||||
standardStyle: StandardStyle = .titleSmall) {
|
||||
isBold: Bool = true,
|
||||
standardStyle: StandardStyle = .titleSmall,
|
||||
lineBreakMode: NSLineBreakMode = .byTruncatingTail) {
|
||||
self.text = text
|
||||
self.textAttributes = textAttributes
|
||||
self.standardStyle = standardStyle
|
||||
self.isBold = isBold
|
||||
self.lineBreakMode = lineBreakMode
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
@ -52,7 +65,7 @@ extension Tilelet {
|
||||
public func toTitleLockupTitleModel() -> TitleLockup.TitleModel {
|
||||
TitleLockup.TitleModel(text: text,
|
||||
textAttributes: textAttributes,
|
||||
standardStyle: standardStyle.value)
|
||||
isBold: isBold, standardStyle: standardStyle.value, lineBreakMode: lineBreakMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
55
VDS/Components/Tilelet/TiletEyebrowModel.swift
Normal file
55
VDS/Components/Tilelet/TiletEyebrowModel.swift
Normal file
@ -0,0 +1,55 @@
|
||||
//
|
||||
// TiletEyebrowModel.swift
|
||||
// VDS
|
||||
//
|
||||
// Created by Bandaru, Krishna Kishore on 13/03/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
extension Tilelet {
|
||||
|
||||
/// Model that represents the options available for the eyebrow label.
|
||||
public struct EyebrowModel {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
/// Text that will be used for the eyebrow label.
|
||||
public var text: String = ""
|
||||
|
||||
/// Used in combination with standardStyle to set the textStyle that will be used for the eyebrow label.
|
||||
public var isBold: Bool = false
|
||||
/// Text attributes that will be used for the eyebrow label.
|
||||
public var textAttributes: [any LabelAttributeModel]?
|
||||
|
||||
/// Text style that will be used for the eyebrow label. If subtitle standard style
|
||||
public var standardStyle: Tilelet.SubTitleModel.OtherStandardStyle = .titleSmall
|
||||
|
||||
/// LineBreakMode used in Badge label.
|
||||
public var lineBreakMode: NSLineBreakMode
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
public init(text: String,
|
||||
textAttributes: [any LabelAttributeModel]? = nil,
|
||||
isBold: Bool = true,
|
||||
standardStyle: Tilelet.SubTitleModel.OtherStandardStyle = .bodySmall,
|
||||
lineBreakMode: NSLineBreakMode = .byTruncatingTail) {
|
||||
self.text = text
|
||||
self.textAttributes = textAttributes
|
||||
self.standardStyle = standardStyle
|
||||
self.isBold = isBold
|
||||
self.lineBreakMode = lineBreakMode
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Methods
|
||||
//--------------------------------------------------
|
||||
/// Converts this type of model to a TitleLockup.TitleModel.
|
||||
public func toTitleLockupEyebrowModel() -> TitleLockup.EyebrowModel {
|
||||
TitleLockup.EyebrowModel(text: text, isBold: isBold, standardStyle: standardStyle.value, textAttributes: textAttributes)
|
||||
}
|
||||
}
|
||||
}
|
||||
110
VDS/Components/Tilelet/TitleletChangeLog.txt
Normal file
110
VDS/Components/Tilelet/TitleletChangeLog.txt
Normal file
@ -0,0 +1,110 @@
|
||||
MM/DD/YYYY
|
||||
----------------
|
||||
|
||||
08/31/2022
|
||||
----------------
|
||||
- Updates to Typography and default Title Lockup configurations:
|
||||
- Default
|
||||
- Regular title updated to bold weight
|
||||
- Subtitle updated to primary color
|
||||
- Inverted
|
||||
- Regular title updated to bold weight
|
||||
- Subtitle updated to primary color
|
||||
|
||||
07/15/2022
|
||||
----------------
|
||||
- Updates to Typography and default Title Lockup configurations:
|
||||
- Default
|
||||
- Bold Title updated to Regular weight
|
||||
- Subtitle updated to secondary color
|
||||
- Inverted
|
||||
- Bold Title updated to Regular weight
|
||||
- Subtitle updated to secondary color
|
||||
|
||||
04/27/2022
|
||||
----------------
|
||||
- Updates to Title Lockup configurations.
|
||||
|
||||
04/14/2022
|
||||
----------------
|
||||
- Updates to Tilelet Group spec based on 4/14 handoff feedback.
|
||||
|
||||
04/04/2022
|
||||
----------------
|
||||
- Visuals added to Tilelet Group spec.
|
||||
|
||||
04/01/2022
|
||||
----------------
|
||||
- Updates based on 3/31 handoff feedback.
|
||||
|
||||
03/30/2022
|
||||
----------------
|
||||
- Tilelet added to Test App.
|
||||
|
||||
03/29/2022
|
||||
----------------
|
||||
- Interactive states updated.
|
||||
|
||||
03/22/2022
|
||||
----------------
|
||||
- Layout and spacing page added.
|
||||
|
||||
03/21/2022
|
||||
----------------
|
||||
- Updated anatomy, configuration, and element pages.
|
||||
|
||||
02/21/2022
|
||||
----------------
|
||||
- Title Lockup’s width properties added.
|
||||
|
||||
09/14/2022
|
||||
----------------
|
||||
- States section updated to reflect states inherited from Tile Container
|
||||
- Dev note added to Layouts.
|
||||
|
||||
11/30/2022
|
||||
----------------
|
||||
- Added "(web only)" to any instance of "keyboard focus"
|
||||
|
||||
12/13/2022
|
||||
----------------
|
||||
- Replaced focus border pixel and style & spacing values with tokens.
|
||||
|
||||
06/19/2023
|
||||
----------------
|
||||
- Updated anatomy element names to be local to Tilelet and indicated the core component that powers them under the hood.
|
||||
|
||||
09/21/2023
|
||||
----------------
|
||||
- Updated Anatomy to include Title Lockup with Eyebrow and Tooltip
|
||||
- Updated Configurations to include Title Lockup section
|
||||
- Updated Layout and spacing to include Alignment (Left, Center) and textPosition (Middle)
|
||||
- Updated Elements to include Eyebrow and uniform 1:1 pairings
|
||||
|
||||
10/09/2023
|
||||
----------------
|
||||
- Updated Configurations VDS Title Lockup and VDS Tile Container sections.
|
||||
|
||||
10/13/2023
|
||||
----------------
|
||||
- SPEC format changes to Anatomy to include nested component configurations
|
||||
- Removed Configurations page
|
||||
|
||||
11/20/2023
|
||||
----------------
|
||||
- Updated visuals to reflect new corner radius value - 12px
|
||||
- Updated focus border corner radius to 14px
|
||||
- View changes
|
||||
|
||||
12/14/2023
|
||||
----------------
|
||||
- Added secondary, primary backgroundColor options
|
||||
- Relabeled configurations section headings to use property names
|
||||
- Relabeled surface sections to use “surface:” labeling convention
|
||||
- Deprecated gray backgroundColor
|
||||
- View changes
|
||||
|
||||
02/08/2024
|
||||
----------------
|
||||
- Added external-icon option to Descriptive Icon in Configurations
|
||||
- View changes
|
||||
@ -68,6 +68,7 @@ open class TitleLockup: View {
|
||||
/// Label used to render the eyebrow model.
|
||||
open var eyebrowLabel = Label().with {
|
||||
$0.setContentCompressionResistancePriority(.required, for: .vertical)
|
||||
$0.setContentHuggingPriority(.required, for: .vertical)
|
||||
}
|
||||
|
||||
/// Model used in rendering the eyebrow label.
|
||||
@ -77,6 +78,7 @@ open class TitleLockup: View {
|
||||
/// Label used to render the title model.
|
||||
open var titleLabel = Label().with {
|
||||
$0.setContentCompressionResistancePriority(.required, for: .vertical)
|
||||
$0.setContentHuggingPriority(.required, for: .vertical)
|
||||
$0.accessibilityTraits.insert([.header])
|
||||
}
|
||||
|
||||
@ -87,6 +89,7 @@ open class TitleLockup: View {
|
||||
/// Label used to render the subtitle model.
|
||||
open var subTitleLabel = Label().with {
|
||||
$0.setContentCompressionResistancePriority(.required, for: .vertical)
|
||||
$0.setContentHuggingPriority(.required, for: .vertical)
|
||||
}
|
||||
|
||||
/// Model used in rendering the subtitle label.
|
||||
@ -350,7 +353,7 @@ open class TitleLockup: View {
|
||||
titleLabel.attributes = titleModel.textAttributes
|
||||
titleLabel.numberOfLines = titleModel.numberOfLines
|
||||
titleLabel.surface = surface
|
||||
|
||||
titleLabel.lineBreakMode = titleModel.lineBreakMode
|
||||
addSubview(titleLabel)
|
||||
titleLabel
|
||||
.pinTop(previousView?.bottomAnchor ?? self.topAnchor, eyebrowLabel == previousView ? topSpacing : 0)
|
||||
@ -369,7 +372,7 @@ open class TitleLockup: View {
|
||||
subTitleLabel.attributes = subTitleModel.textAttributes
|
||||
subTitleLabel.numberOfLines = subTitleModel.numberOfLines
|
||||
subTitleLabel.surface = surface
|
||||
|
||||
subTitleLabel.lineBreakMode = subTitleModel.lineBreakMode
|
||||
addSubview(subTitleLabel)
|
||||
subTitleLabel
|
||||
.pinTop(previousView?.bottomAnchor ?? self.topAnchor, (eyebrowLabel == previousView || titleLabel == previousView) ? bottomSpacing : 0)
|
||||
@ -380,7 +383,7 @@ open class TitleLockup: View {
|
||||
}
|
||||
|
||||
//pin the last view to the bottom of this view
|
||||
previousView?.pinBottom(0, .defaultHigh)
|
||||
previousView?.pinBottom(0)
|
||||
|
||||
//debugging for borders
|
||||
eyebrowLabel.debugBorder(show: hasDebugBorder, color: .green)
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
extension TitleLockup {
|
||||
/// Model that represents the options available for the sub title label.
|
||||
@ -25,16 +26,21 @@ extension TitleLockup {
|
||||
/// Number of lines used in the subtitle label.
|
||||
public var numberOfLines: Int
|
||||
|
||||
/// LineBreakMode used in subtitle label.
|
||||
public var lineBreakMode: NSLineBreakMode
|
||||
|
||||
public init(text: String,
|
||||
otherStandardStyle: OtherStandardStyle = .bodyLarge,
|
||||
textColor: Use = .primary,
|
||||
textAttributes: [any LabelAttributeModel]? = nil,
|
||||
numberOfLines: Int = 0) {
|
||||
numberOfLines: Int = 0,
|
||||
lineBreakMode: NSLineBreakMode = .byWordWrapping) {
|
||||
self.text = text
|
||||
self.otherStandardStyle = otherStandardStyle
|
||||
self.textColor = textColor
|
||||
self.textAttributes = textAttributes
|
||||
self.numberOfLines = numberOfLines
|
||||
self.lineBreakMode = lineBreakMode
|
||||
}
|
||||
|
||||
/// TextStyle used to render the text.
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
extension TitleLockup {
|
||||
/// Model that represents the options available for the sub title label.
|
||||
@ -25,16 +26,21 @@ extension TitleLockup {
|
||||
/// Number of lines that will be used for the title label.
|
||||
public var numberOfLines: Int
|
||||
|
||||
/// LineBreakMode used in subtitle label.
|
||||
public var lineBreakMode: NSLineBreakMode
|
||||
|
||||
public init(text: String,
|
||||
textAttributes: [any LabelAttributeModel]? = nil,
|
||||
isBold: Bool = true,
|
||||
standardStyle: TitleStandardStyle = .featureXSmall,
|
||||
numberOfLines: Int = 0) {
|
||||
numberOfLines: Int = 0,
|
||||
lineBreakMode: NSLineBreakMode = .byTruncatingTail) {
|
||||
self.text = text
|
||||
self.isBold = isBold
|
||||
self.textAttributes = textAttributes
|
||||
self.standardStyle = standardStyle
|
||||
self.numberOfLines = numberOfLines
|
||||
self.lineBreakMode = lineBreakMode
|
||||
}
|
||||
|
||||
/// TextStyle used to render the text.
|
||||
|
||||
39
VDS/Extensions/UIImage+Helper.swift
Normal file
39
VDS/Extensions/UIImage+Helper.swift
Normal file
@ -0,0 +1,39 @@
|
||||
//
|
||||
// UIImage+Icon.swift
|
||||
// VDS
|
||||
//
|
||||
// Created by Matt Bruce on 3/11/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
extension UIImage {
|
||||
|
||||
/// UIImage helper for finding images based on the Icon.Name which uses the internal BundleManager.
|
||||
/// - Parameters:
|
||||
/// - name: RawRepresentable.
|
||||
/// - color: Color to Tint the image with
|
||||
/// - renderingMode: UIImage Rendering mode.
|
||||
/// - Returns: UIImage for this proecess
|
||||
public static func image<T: RawRepresentable>(representing representable: T, color: UIColor? = nil, renderingMode: UIImage.RenderingMode = .alwaysOriginal) -> UIImage? where T.RawValue == String {
|
||||
guard let image = BundleManager.shared.image(for: representable.rawValue) else { return nil }
|
||||
|
||||
guard let color else { return image }
|
||||
|
||||
return image.withTintColor(color, renderingMode: renderingMode)
|
||||
}
|
||||
|
||||
/// Resizes image to a specific Size
|
||||
/// - Parameter size: Size to resize
|
||||
/// - Returns: Image that is resized
|
||||
public func resized(to size: CGSize) -> UIImage? {
|
||||
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
|
||||
|
||||
defer { UIGraphicsEndImageContext() }
|
||||
draw(in: .init(origin: .zero, size: size))
|
||||
|
||||
return UIGraphicsGetImageFromCurrentImageContext()
|
||||
}
|
||||
|
||||
}
|
||||
@ -10,7 +10,8 @@ import Foundation
|
||||
/// Represents constants used that deal with layout.
|
||||
public struct VDSLayout {
|
||||
/// Enum used to describe the spacing constants.
|
||||
public enum Spacing: String, CaseIterable {
|
||||
public enum Spacing: String, CaseIterable, Valuing {
|
||||
|
||||
case space1X
|
||||
case space2X
|
||||
case space3X
|
||||
|
||||
@ -6,15 +6,17 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
/// Enum that is matched up for the Verizon fonts.
|
||||
public enum Font: String, FontProtocol {
|
||||
public enum Font: FontProtocol {
|
||||
case edsBold
|
||||
case edsRegular
|
||||
case dsLight
|
||||
case etxBold
|
||||
case etxRegular
|
||||
|
||||
case custom(UIFont)
|
||||
|
||||
public var fontName: String {
|
||||
switch self {
|
||||
case .edsBold:
|
||||
@ -27,11 +29,32 @@ public enum Font: String, FontProtocol {
|
||||
return "VerizonNHGeTX-Bold"
|
||||
case .etxRegular:
|
||||
return "VerizonNHGeTX-Regular"
|
||||
case .custom(let font):
|
||||
return font.fontName
|
||||
}
|
||||
}
|
||||
|
||||
public static var allCases: [Font] {
|
||||
[.edsBold, .edsRegular, .dsLight, .etxBold, .etxRegular]
|
||||
}
|
||||
|
||||
/// File Extension for each of the Font enums.
|
||||
public var fontFileExtension: String {
|
||||
return "otf"
|
||||
}
|
||||
|
||||
/// Returns a UIFont for the fontName and size given.
|
||||
/// - Parameters:
|
||||
/// - size: Size of the font
|
||||
/// - Returns: UIFont for the fontName and Size.
|
||||
public func font(ofSize size: CGFloat) -> UIFont{
|
||||
DispatchQueue.once(block: { self.register() })
|
||||
switch self {
|
||||
case .custom(let font):
|
||||
return font
|
||||
default:
|
||||
guard let found = UIFont(name: self.fontName, size: size) else { return .systemFont(ofSize: size) }
|
||||
return found
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,9 +9,10 @@ import Foundation
|
||||
import UIKit
|
||||
|
||||
/// Used in Classes that require Fonts
|
||||
public protocol FontProtocol: CaseIterable, RawRepresentable, Hashable {
|
||||
public protocol FontProtocol {
|
||||
var fontFileExtension: String { get }
|
||||
var fontName: String { get }
|
||||
static var allCases: [Self] { get }
|
||||
}
|
||||
|
||||
extension FontProtocol {
|
||||
|
||||
12
VDS/Protocols/DefaultValuing.swift
Normal file
12
VDS/Protocols/DefaultValuing.swift
Normal file
@ -0,0 +1,12 @@
|
||||
//
|
||||
// EnumValuing.swift
|
||||
// VDS
|
||||
//
|
||||
// Created by Matt Bruce on 3/1/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public protocol DefaultValuing: Valuing {
|
||||
static var defaultValue: Self { get }
|
||||
}
|
||||
@ -50,7 +50,6 @@ extension ViewProtocol where Self: UIView {
|
||||
shadowLayer.shadowPath = shadowPath.cgPath
|
||||
shadowLayer.frame = bounds
|
||||
shadowLayer.position = .init(x: bounds.midX, y: bounds.midY)
|
||||
shadowLayer.backgroundColor = backgroundColor?.cgColor
|
||||
shadowLayer.cornerRadius = layer.cornerRadius
|
||||
shadowLayer.shadowColor = config.shadowColorConfiguration.getColor(self).cgColor
|
||||
shadowLayer.shadowOpacity = Float(config.shadowOpacityConfiguration.value(for: self))
|
||||
@ -86,4 +85,8 @@ extension ViewProtocol where Self: UIView {
|
||||
func removeGradientLayer() {
|
||||
layer.sublayers?.removeAll { $0.name == "gradientLayer" }
|
||||
}
|
||||
|
||||
//Helper variables to get gradient & shadow layers. These are exposed to update frame of layers when view's frame is updated.
|
||||
var dropShadowLayers: [CALayer]? { layer.sublayers?.filter { $0.name == "dropShadowLayer" } }
|
||||
var gradientLayers: [CAGradientLayer]? { layer.sublayers?.filter { $0.name == "gradientLayer" } as? [CAGradientLayer] }
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
public protocol EnumSubset<T>: RawRepresentable, CaseIterable {
|
||||
public protocol EnumSubset<T>: RawRepresentable, CaseIterable, Valuing {
|
||||
associatedtype T:RawRepresentable
|
||||
var defaultValue: T { get }
|
||||
}
|
||||
|
||||
@ -9,10 +9,91 @@ import Foundation
|
||||
|
||||
/// Protocol used for a FormField object.
|
||||
public protocol FormFieldable {
|
||||
associatedtype ValueType = AnyHashable
|
||||
|
||||
/// Unique Id for the Form Field object within a Form.
|
||||
var inputId: String? { get set }
|
||||
|
||||
/// Value for the Form Field.
|
||||
var value: AnyHashable? { get set }
|
||||
var value: ValueType? { get set }
|
||||
}
|
||||
|
||||
/// Protocol for FormFieldable that require internal validation.
|
||||
public protocol FormFieldInternalValidatable: FormFieldable {
|
||||
/// Is there an internalError
|
||||
var hasInternalError: Bool { get }
|
||||
/// Internal Error Message that will show.
|
||||
var internalErrorText: String? { get }
|
||||
}
|
||||
|
||||
/// Struct that will execute the validation.
|
||||
public protocol FormFieldValidatorable {
|
||||
associatedtype FieldType: FormFieldable
|
||||
/// FormFieldable to be validated.
|
||||
var field: FieldType { get set }
|
||||
/// Rules that will be applied against the FormFieldable
|
||||
var rules: [AnyRule<FieldType.ValueType>] { get set }
|
||||
/// Error Message that will show.
|
||||
var errorMessage: String? { get }
|
||||
/// Is the FormField valid.
|
||||
var isValid: Bool { get }
|
||||
/// Run the rules against the FormFieldable.
|
||||
func validate()
|
||||
}
|
||||
|
||||
/// Rule that will be executed against a specific ValueType.
|
||||
public protocol Rule<ValueType> {
|
||||
associatedtype ValueType
|
||||
/// Determines if this rule valid for the value passed.
|
||||
func isValid(value: ValueType?) -> Bool
|
||||
/// Error Message to be show if the value is invalid.
|
||||
var errorMessage: String { get }
|
||||
}
|
||||
|
||||
/// Type Erased Rule for a specific ValueType.
|
||||
public struct AnyRule<ValueType>: Rule {
|
||||
private let _isValid: (ValueType?) -> Bool
|
||||
|
||||
public let errorMessage: String
|
||||
|
||||
public init<R: Rule>(_ rule: R) where R.ValueType == ValueType {
|
||||
self._isValid = rule.isValid
|
||||
self.errorMessage = rule.errorMessage
|
||||
}
|
||||
|
||||
public func isValid(value: ValueType?) -> Bool {
|
||||
return _isValid(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic Validator for a specific FormFieldable.
|
||||
public class FormFieldValidator<Field:FormFieldable>: FormFieldValidatorable{
|
||||
public var field: Field
|
||||
public var rules: [AnyRule<Field.ValueType>]
|
||||
public var errorMessages = [String]()
|
||||
public var isValid: Bool = true
|
||||
|
||||
public init(field: Field, rules: [AnyRule<Field.ValueType>]) {
|
||||
self.field = field
|
||||
self.rules = rules
|
||||
}
|
||||
|
||||
public var errorMessage: String? {
|
||||
guard errorMessages.count > 0 else { return nil }
|
||||
return errorMessages.joined(separator: "\r")
|
||||
}
|
||||
|
||||
public func validate() {
|
||||
errorMessages.removeAll()
|
||||
|
||||
for rule in rules {
|
||||
if !rule.isValid(value: field.value) {
|
||||
errorMessages.append(rule.errorMessage)
|
||||
isValid = false
|
||||
return
|
||||
}
|
||||
}
|
||||
isValid = true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -478,7 +478,159 @@ extension LayoutConstraintable {
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Center X Constraints
|
||||
//--------------------------------------------------
|
||||
extension LayoutConstraintable {
|
||||
|
||||
@discardableResult
|
||||
/// Adds a centerXAnchor.
|
||||
/// - Parameter constant: Constant size.
|
||||
/// - Returns: Yourself.
|
||||
public func pinCenterX(_ constant: CGFloat = 0.0, _ priority: UILayoutPriority = .required) -> Self {
|
||||
pinCenterX(nil, constant)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
/// Adds a centerXAnchor to a specific XAxisAnchor.
|
||||
/// - Parameter anchor:The anchor in which to attach the centerXAnchor.
|
||||
/// - constant: Constant size.
|
||||
/// - Returns: Yourself.
|
||||
public func pinCenterX(_ anchor: NSLayoutXAxisAnchor? = nil, _ constant: CGFloat = 0.0, _ priority: UILayoutPriority = .required) -> Self {
|
||||
pinCenterX(anchor: anchor, constant: constant)
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
/// Adds a centerXAnchor to a specific XAxisAnchor passed in using a lessThanOrEqualTo Constraint
|
||||
/// - Parameter anchor:The anchor in which to attach the centerXAnchor
|
||||
/// - constant: Constant size.
|
||||
/// - Returns: Yourself.
|
||||
public func pinCenterXLessThanOrEqualTo(_ anchor: NSLayoutXAxisAnchor? = nil, _ constant: CGFloat = 0.0, _ priority: UILayoutPriority = .required) -> Self {
|
||||
pinCenterXLessThanOrEqualTo(anchor: anchor, constant: constant)
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
/// Adds a centerXAnchor to a specific XAxisAnchor passed in using a greaterThanOrEqualTo Constraint
|
||||
/// - Parameter anchor:The anchor in which to attach the centerXAnchor
|
||||
/// - constant: Constant size.
|
||||
/// - Returns: Yourself.
|
||||
public func pinCenterXGreaterThanOrEqualTo(_ anchor: NSLayoutXAxisAnchor? = nil, _ constant: CGFloat = 0.0) -> Self {
|
||||
pinCenterXGreaterThanOrEqualTo(anchor: anchor, constant: constant)
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
/// Adds a centerXAnchor for the constant passed into the method.
|
||||
/// - Parameter anchor:The anchor in which to attach the centerXAnchor
|
||||
/// - constant: Constant size.
|
||||
/// - Returns: The Constraint that was created.
|
||||
public func pinCenterX(anchor: NSLayoutXAxisAnchor?, constant: CGFloat = 0.0, priority: UILayoutPriority = .required) -> NSLayoutConstraint? {
|
||||
let found: NSLayoutXAxisAnchor? = anchor ?? superview?.centerXAnchor
|
||||
guard let found else { return nil }
|
||||
return centerXAnchor.constraint(equalTo: found, constant: -constant).with { $0.priority = priority; $0.isActive = true }
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
/// Adds a centerXAnchor with the constant passed in using a lessThanOrEqualTo Constraint.
|
||||
/// - Parameter anchor:The anchor in which to attach the centerXAnchor
|
||||
/// - constant: Constant size.
|
||||
/// - Returns: The Constraint that was created.
|
||||
public func pinCenterXLessThanOrEqualTo(anchor: NSLayoutXAxisAnchor?, constant: CGFloat = 0.0, priority: UILayoutPriority = .required) -> NSLayoutConstraint? {
|
||||
let found: NSLayoutXAxisAnchor? = anchor ?? superview?.centerXAnchor
|
||||
guard let found else { return nil }
|
||||
return centerXAnchor.constraint(lessThanOrEqualTo: found, constant: -constant).with { $0.priority = priority; $0.isActive = true }
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
/// Adds a centerXAnchor with the constant passed in using a greaterThanOrEqualTo Constraint.
|
||||
/// - Parameter anchor:The anchor in which to attach the centerXAnchor
|
||||
/// - constant: Constant size.
|
||||
/// - Returns: The Constraint that was created.
|
||||
public func pinCenterXGreaterThanOrEqualTo(anchor: NSLayoutXAxisAnchor?, constant: CGFloat = 0.0, priority: UILayoutPriority = .required) -> NSLayoutConstraint? {
|
||||
let found: NSLayoutXAxisAnchor? = anchor ?? superview?.centerXAnchor
|
||||
guard let found else { return nil }
|
||||
return centerXAnchor.constraint(greaterThanOrEqualTo: found, constant: -constant).with { $0.priority = priority; $0.isActive = true }
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Center Y Constraints
|
||||
//--------------------------------------------------
|
||||
extension LayoutConstraintable {
|
||||
|
||||
@discardableResult
|
||||
/// Adds a centerYAnchor.
|
||||
/// - Parameter constant: Constant size.
|
||||
/// - Returns: Yourself.
|
||||
public func pinCenterY(_ constant: CGFloat = 0.0, _ priority: UILayoutPriority = .required) -> Self {
|
||||
pinCenterY(nil, constant)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
/// Adds a centerYAnchor to a specific YAxisAnchor.
|
||||
/// - Parameter anchor:The anchor in which to attach the centerYAnchor.
|
||||
/// - constant: Constant size.
|
||||
/// - Returns: Yourself.
|
||||
public func pinCenterY(_ anchor: NSLayoutYAxisAnchor? = nil, _ constant: CGFloat = 0.0, _ priority: UILayoutPriority = .required) -> Self {
|
||||
pinCenterY(anchor: anchor, constant: constant)
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
/// Adds a centerYAnchor to a specific YAxisAnchor passed in using a lessThanOrEqualTo Constraint
|
||||
/// - Parameter anchor:The anchor in which to attach the centerYAnchor
|
||||
/// - constant: Constant size.
|
||||
/// - Returns: Yourself.
|
||||
public func pinCenterYLessThanOrEqualTo(_ anchor: NSLayoutYAxisAnchor? = nil, _ constant: CGFloat = 0.0, _ priority: UILayoutPriority = .required) -> Self {
|
||||
pinCenterYLessThanOrEqualTo(anchor: anchor, constant: constant)
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
/// Adds a centerYAnchor to a specific YAxisAnchor passed in using a greaterThanOrEqualTo Constraint
|
||||
/// - Parameter anchor:The anchor in which to attach the centerYAnchor
|
||||
/// - constant: Constant size.
|
||||
/// - Returns: Yourself.
|
||||
public func pinCenterYGreaterThanOrEqualTo(_ anchor: NSLayoutXAxisAnchor? = nil, _ constant: CGFloat = 0.0) -> Self {
|
||||
pinCenterXGreaterThanOrEqualTo(anchor: anchor, constant: constant)
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
/// Adds a centerYAnchor for the constant passed into the method.
|
||||
/// - Parameter anchor:The anchor in which to attach the centerYAnchor
|
||||
/// - constant: Constant size.
|
||||
/// - Returns: The Constraint that was created.
|
||||
public func pinCenterY(anchor: NSLayoutYAxisAnchor?, constant: CGFloat = 0.0, priority: UILayoutPriority = .required) -> NSLayoutConstraint? {
|
||||
let found: NSLayoutYAxisAnchor? = anchor ?? superview?.centerYAnchor
|
||||
guard let found else { return nil }
|
||||
return centerYAnchor.constraint(equalTo: found, constant: -constant).with { $0.priority = priority; $0.isActive = true }
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
/// Adds a centerYAnchor with the constant passed in using a lessThanOrEqualTo Constraint.
|
||||
/// - Parameter anchor:The anchor in which to attach the centerYAnchor
|
||||
/// - constant: Constant size.
|
||||
/// - Returns: The Constraint that was created.
|
||||
public func pinCenterYLessThanOrEqualTo(anchor: NSLayoutYAxisAnchor?, constant: CGFloat = 0.0, priority: UILayoutPriority = .required) -> NSLayoutConstraint? {
|
||||
let found: NSLayoutYAxisAnchor? = anchor ?? superview?.centerYAnchor
|
||||
guard let found else { return nil }
|
||||
return centerYAnchor.constraint(lessThanOrEqualTo: found, constant: -constant).with { $0.priority = priority; $0.isActive = true }
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
/// Adds a centerYAnchor with the constant passed in using a greaterThanOrEqualTo Constraint.
|
||||
/// - Parameter anchor:The anchor in which to attach the centerYAnchor
|
||||
/// - constant: Constant size.
|
||||
/// - Returns: The Constraint that was created.
|
||||
public func pinCenterYGreaterThanOrEqualTo(anchor: NSLayoutYAxisAnchor?, constant: CGFloat = 0.0, priority: UILayoutPriority = .required) -> NSLayoutConstraint? {
|
||||
let found: NSLayoutYAxisAnchor? = anchor ?? superview?.centerYAnchor
|
||||
guard let found else { return nil }
|
||||
return centerYAnchor.constraint(greaterThanOrEqualTo: found, constant: -constant).with { $0.priority = priority; $0.isActive = true }
|
||||
}
|
||||
}
|
||||
//--------------------------------------------------
|
||||
// MARK: - Implementations
|
||||
//--------------------------------------------------
|
||||
|
||||
13
VDS/Protocols/Valuing.swift
Normal file
13
VDS/Protocols/Valuing.swift
Normal file
@ -0,0 +1,13 @@
|
||||
//
|
||||
// Valuing.swift
|
||||
// VDS
|
||||
//
|
||||
// Created by Matt Bruce on 3/1/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public protocol Valuing {
|
||||
associatedtype ValueType
|
||||
var value: ValueType { get }
|
||||
}
|
||||
@ -209,6 +209,13 @@ extension TextStyle {
|
||||
boldMicro
|
||||
]
|
||||
}
|
||||
|
||||
public static func convert(font: UIFont) -> TextStyle {
|
||||
guard let found = allCases.first(where: { font.fontName == $0.fontFace.fontName && font.pointSize == $0.pointSize} ) else {
|
||||
return TextStyle(rawValue: "Custom\(font.fontName)", fontFace: .custom(font), pointSize: font.pointSize)
|
||||
}
|
||||
return found
|
||||
}
|
||||
}
|
||||
|
||||
extension TextStyle {
|
||||
|
||||
24
VDS/Utilities/Clamping.swift
Normal file
24
VDS/Utilities/Clamping.swift
Normal file
@ -0,0 +1,24 @@
|
||||
//
|
||||
// Clamping.swift
|
||||
// VDS
|
||||
//
|
||||
// Created by Bandaru, Krishna Kishore on 05/03/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@propertyWrapper public struct Clamping<Value: Comparable> {
|
||||
|
||||
private var value: Value
|
||||
private let range: ClosedRange<Value>
|
||||
|
||||
public init(range: ClosedRange<Value>) {
|
||||
self.value = range.lowerBound
|
||||
self.range = range
|
||||
}
|
||||
|
||||
public var wrappedValue: Value {
|
||||
get { value }
|
||||
set { value = min(max(range.lowerBound, newValue), range.upperBound) }
|
||||
}
|
||||
}
|
||||
@ -21,9 +21,11 @@ Using the system allows designers and developers to collaborate more easily and
|
||||
### Components
|
||||
- ``Badge``
|
||||
- ``BadgeIndicator``
|
||||
- ``Breadcrumbs``
|
||||
- ``Button``
|
||||
- ``ButtonIcon``
|
||||
- ``ButtonGroup``
|
||||
- ``CarouselScrollbar``
|
||||
- ``Checkbox``
|
||||
- ``CheckboxItem``
|
||||
- ``CheckboxGroup``
|
||||
@ -33,6 +35,7 @@ Using the system allows designers and developers to collaborate more easily and
|
||||
- ``Line``
|
||||
- ``Loader``
|
||||
- ``Notification``
|
||||
- ``Pagination``
|
||||
- ``RadioBoxItem``
|
||||
- ``RadioBoxGroup``
|
||||
- ``RadioButton``
|
||||
|
||||
Loading…
Reference in New Issue
Block a user