Merge branch 'develop' of https://gitlab.verizon.com/BPHV_MIPS/vds_ios into vasavk/inputStepper

# Conflicts:
#	VDS/Components/TextFields/EntryFieldBase.swift
This commit is contained in:
Vasavi Kanamarlapudi 2024-07-29 16:40:24 +05:30
commit c120c746d2
73 changed files with 2316 additions and 573 deletions

View File

@ -20,7 +20,10 @@
18A3F12A2BD9298900498E4A /* Calendar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A3F1292BD9298900498E4A /* Calendar.swift */; }; 18A3F12A2BD9298900498E4A /* Calendar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A3F1292BD9298900498E4A /* Calendar.swift */; };
18A65A022B96E848006602CC /* Breadcrumbs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A65A012B96E848006602CC /* Breadcrumbs.swift */; }; 18A65A022B96E848006602CC /* Breadcrumbs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A65A012B96E848006602CC /* Breadcrumbs.swift */; };
18A65A042B96F050006602CC /* BreadcrumbItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A65A032B96F050006602CC /* BreadcrumbItem.swift */; }; 18A65A042B96F050006602CC /* BreadcrumbItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A65A032B96F050006602CC /* BreadcrumbItem.swift */; };
18AE87502C06FDA60075F181 /* Carousel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18AE874F2C06FDA60075F181 /* Carousel.swift */; };
18B42AC62C09D197008D6262 /* CarouselSlotAlignmentModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B42AC52C09D197008D6262 /* CarouselSlotAlignmentModel.swift */; };
18B463A42BBD3C46005C4528 /* DropdownOptionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B463A32BBD3C46005C4528 /* DropdownOptionModel.swift */; }; 18B463A42BBD3C46005C4528 /* DropdownOptionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B463A32BBD3C46005C4528 /* DropdownOptionModel.swift */; };
18B9763F2C11BA4A009271DF /* CarouselPaginationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B9763E2C11BA4A009271DF /* CarouselPaginationModel.swift */; };
18FEA1AD2BDD137500A56439 /* CalendarIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1AC2BDD137500A56439 /* CalendarIndicatorModel.swift */; }; 18FEA1AD2BDD137500A56439 /* CalendarIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1AC2BDD137500A56439 /* CalendarIndicatorModel.swift */; };
18FEA1B52BE0E63600A56439 /* Date+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1B42BE0E63600A56439 /* Date+Extension.swift */; }; 18FEA1B52BE0E63600A56439 /* Date+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1B42BE0E63600A56439 /* Date+Extension.swift */; };
445BA07829C07B3D0036A7C5 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 445BA07729C07B3D0036A7C5 /* Notification.swift */; }; 445BA07829C07B3D0036A7C5 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 445BA07729C07B3D0036A7C5 /* Notification.swift */; };
@ -154,7 +157,7 @@
EAC58C182BED0E2300BA39FA /* SecurityCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C172BED0E2300BA39FA /* SecurityCode.swift */; }; EAC58C182BED0E2300BA39FA /* SecurityCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C172BED0E2300BA39FA /* SecurityCode.swift */; };
EAC58C232BF2824200BA39FA /* DatePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C222BF2824200BA39FA /* DatePicker.swift */; }; EAC58C232BF2824200BA39FA /* DatePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C222BF2824200BA39FA /* DatePicker.swift */; };
EAC58C272BF4116200BA39FA /* DatePickerCalendarModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C262BF4116200BA39FA /* DatePickerCalendarModel.swift */; }; EAC58C272BF4116200BA39FA /* DatePickerCalendarModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C262BF4116200BA39FA /* DatePickerCalendarModel.swift */; };
EAC58C292BF4118C00BA39FA /* DatePickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C282BF4118C00BA39FA /* DatePickerViewController.swift */; }; EAC58C292BF4118C00BA39FA /* ClearPopoverViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C282BF4118C00BA39FA /* ClearPopoverViewController.swift */; };
EAC71A1D2A2E155A00E47A9F /* Checkbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC71A1C2A2E155A00E47A9F /* Checkbox.swift */; }; EAC71A1D2A2E155A00E47A9F /* Checkbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC71A1C2A2E155A00E47A9F /* Checkbox.swift */; };
EAC71A1F2A2E173D00E47A9F /* RadioButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC71A1E2A2E173D00E47A9F /* RadioButton.swift */; }; EAC71A1F2A2E173D00E47A9F /* RadioButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC71A1E2A2E173D00E47A9F /* RadioButton.swift */; };
EAC846F3294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC846F2294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift */; }; EAC846F3294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC846F2294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift */; };
@ -176,6 +179,7 @@
EAF1FE9B29DB1A6000101452 /* Changeable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF1FE9A29DB1A6000101452 /* Changeable.swift */; }; EAF1FE9B29DB1A6000101452 /* Changeable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF1FE9A29DB1A6000101452 /* Changeable.swift */; };
EAF2F4762C231EAA007BFEDC /* AccessibilityActionElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */; }; EAF2F4762C231EAA007BFEDC /* AccessibilityActionElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */; };
EAF2F4782C249D72007BFEDC /* AccessibilityUpdatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF2F4772C249D72007BFEDC /* AccessibilityUpdatable.swift */; }; EAF2F4782C249D72007BFEDC /* AccessibilityUpdatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF2F4772C249D72007BFEDC /* AccessibilityUpdatable.swift */; };
EAF2F4892C2A1075007BFEDC /* AlertViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF2F4882C2A1075007BFEDC /* AlertViewController.swift */; };
EAF7F0952899861000B287F5 /* CheckboxItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0932899861000B287F5 /* CheckboxItem.swift */; }; EAF7F0952899861000B287F5 /* CheckboxItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0932899861000B287F5 /* CheckboxItem.swift */; };
EAF7F09A2899B17200B287F5 /* CATransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0992899B17200B287F5 /* CATransaction.swift */; }; EAF7F09A2899B17200B287F5 /* CATransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0992899B17200B287F5 /* CATransaction.swift */; };
EAF7F0A0289AB7EC00B287F5 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F09F289AB7EC00B287F5 /* View.swift */; }; EAF7F0A0289AB7EC00B287F5 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F09F289AB7EC00B287F5 /* View.swift */; };
@ -222,7 +226,11 @@
18A3F1292BD9298900498E4A /* Calendar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Calendar.swift; sourceTree = "<group>"; }; 18A3F1292BD9298900498E4A /* Calendar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Calendar.swift; sourceTree = "<group>"; };
18A65A012B96E848006602CC /* Breadcrumbs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Breadcrumbs.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>"; }; 18A65A032B96F050006602CC /* BreadcrumbItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreadcrumbItem.swift; sourceTree = "<group>"; };
18AE874F2C06FDA60075F181 /* Carousel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Carousel.swift; sourceTree = "<group>"; };
18AE87532C06FE610075F181 /* CarouselChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = CarouselChangeLog.txt; sourceTree = "<group>"; };
18B42AC52C09D197008D6262 /* CarouselSlotAlignmentModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselSlotAlignmentModel.swift; sourceTree = "<group>"; };
18B463A32BBD3C46005C4528 /* DropdownOptionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropdownOptionModel.swift; sourceTree = "<group>"; }; 18B463A32BBD3C46005C4528 /* DropdownOptionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropdownOptionModel.swift; sourceTree = "<group>"; };
18B9763E2C11BA4A009271DF /* CarouselPaginationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselPaginationModel.swift; sourceTree = "<group>"; };
18BDEE812B75316E00452358 /* ButtonIconChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ButtonIconChangeLog.txt; sourceTree = "<group>"; }; 18BDEE812B75316E00452358 /* ButtonIconChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ButtonIconChangeLog.txt; sourceTree = "<group>"; };
18FEA1AC2BDD137500A56439 /* CalendarIndicatorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarIndicatorModel.swift; sourceTree = "<group>"; }; 18FEA1AC2BDD137500A56439 /* CalendarIndicatorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarIndicatorModel.swift; sourceTree = "<group>"; };
18FEA1B42BE0E63600A56439 /* Date+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extension.swift"; sourceTree = "<group>"; }; 18FEA1B42BE0E63600A56439 /* Date+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extension.swift"; sourceTree = "<group>"; };
@ -372,7 +380,7 @@
EAC58C222BF2824200BA39FA /* DatePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePicker.swift; sourceTree = "<group>"; }; EAC58C222BF2824200BA39FA /* DatePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePicker.swift; sourceTree = "<group>"; };
EAC58C242BF2A7FB00BA39FA /* DatePickerChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = DatePickerChangeLog.txt; sourceTree = "<group>"; }; EAC58C242BF2A7FB00BA39FA /* DatePickerChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = DatePickerChangeLog.txt; sourceTree = "<group>"; };
EAC58C262BF4116200BA39FA /* DatePickerCalendarModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickerCalendarModel.swift; sourceTree = "<group>"; }; EAC58C262BF4116200BA39FA /* DatePickerCalendarModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickerCalendarModel.swift; sourceTree = "<group>"; };
EAC58C282BF4118C00BA39FA /* DatePickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickerViewController.swift; sourceTree = "<group>"; }; EAC58C282BF4118C00BA39FA /* ClearPopoverViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClearPopoverViewController.swift; sourceTree = "<group>"; };
EAC71A1C2A2E155A00E47A9F /* Checkbox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Checkbox.swift; sourceTree = "<group>"; }; EAC71A1C2A2E155A00E47A9F /* Checkbox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Checkbox.swift; sourceTree = "<group>"; };
EAC71A1E2A2E173D00E47A9F /* RadioButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButton.swift; sourceTree = "<group>"; }; EAC71A1E2A2E173D00E47A9F /* RadioButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButton.swift; sourceTree = "<group>"; };
EAC846F2294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonGroupCollectionViewCell.swift; sourceTree = "<group>"; }; EAC846F2294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonGroupCollectionViewCell.swift; sourceTree = "<group>"; };
@ -406,6 +414,7 @@
EAF1FE9A29DB1A6000101452 /* Changeable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Changeable.swift; sourceTree = "<group>"; }; EAF1FE9A29DB1A6000101452 /* Changeable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Changeable.swift; sourceTree = "<group>"; };
EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityActionElement.swift; sourceTree = "<group>"; }; EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityActionElement.swift; sourceTree = "<group>"; };
EAF2F4772C249D72007BFEDC /* AccessibilityUpdatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityUpdatable.swift; sourceTree = "<group>"; }; EAF2F4772C249D72007BFEDC /* AccessibilityUpdatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityUpdatable.swift; sourceTree = "<group>"; };
EAF2F4882C2A1075007BFEDC /* AlertViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertViewController.swift; sourceTree = "<group>"; };
EAF7F0932899861000B287F5 /* CheckboxItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckboxItem.swift; sourceTree = "<group>"; }; EAF7F0932899861000B287F5 /* CheckboxItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckboxItem.swift; sourceTree = "<group>"; };
EAF7F0992899B17200B287F5 /* CATransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CATransaction.swift; sourceTree = "<group>"; }; EAF7F0992899B17200B287F5 /* CATransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CATransaction.swift; sourceTree = "<group>"; };
EAF7F09F289AB7EC00B287F5 /* View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = "<group>"; }; EAF7F09F289AB7EC00B287F5 /* View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = "<group>"; };
@ -498,6 +507,17 @@
path = Breadcrumbs; path = Breadcrumbs;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
18AE874E2C06FD610075F181 /* Carousel */ = {
isa = PBXGroup;
children = (
18AE874F2C06FDA60075F181 /* Carousel.swift */,
18B9763E2C11BA4A009271DF /* CarouselPaginationModel.swift */,
18B42AC52C09D197008D6262 /* CarouselSlotAlignmentModel.swift */,
18AE87532C06FE610075F181 /* CarouselChangeLog.txt */,
);
path = Carousel;
sourceTree = "<group>";
};
440B84C82BD8E0CE004A732A /* Table */ = { 440B84C82BD8E0CE004A732A /* Table */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -668,6 +688,7 @@
18A65A002B96E7E1006602CC /* Breadcrumbs */, 18A65A002B96E7E1006602CC /* Breadcrumbs */,
EA0FC2BE2912D18200DF80B4 /* Buttons */, EA0FC2BE2912D18200DF80B4 /* Buttons */,
18A3F1202BD8F5DE00498E4A /* Calendar */, 18A3F1202BD8F5DE00498E4A /* Calendar */,
18AE874E2C06FD610075F181 /* Carousel */,
1808BEBA2BA41B1D00129230 /* CarouselScrollbar */, 1808BEBA2BA41B1D00129230 /* CarouselScrollbar */,
EAF7F092289985E200B287F5 /* Checkbox */, EAF7F092289985E200B287F5 /* Checkbox */,
EAC58C1F2BF127F000BA39FA /* DatePicker */, EAC58C1F2BF127F000BA39FA /* DatePicker */,
@ -760,9 +781,11 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
EA985C1C296CD13600F2FF2E /* BundleManager.swift */, EA985C1C296CD13600F2FF2E /* BundleManager.swift */,
EAC58C282BF4118C00BA39FA /* ClearPopoverViewController.swift */,
EAF7F0B8289C139800B287F5 /* ColorConfiguration.swift */, EAF7F0B8289C139800B287F5 /* ColorConfiguration.swift */,
EAB5FEF02927F4AA00998C17 /* SelfSizingCollectionView.swift */, EAB5FEF02927F4AA00998C17 /* SelfSizingCollectionView.swift */,
EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */, EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */,
EAF2F4882C2A1075007BFEDC /* AlertViewController.swift */,
); );
path = Classes; path = Classes;
sourceTree = "<group>"; sourceTree = "<group>";
@ -980,7 +1003,6 @@
children = ( children = (
EAC58C222BF2824200BA39FA /* DatePicker.swift */, EAC58C222BF2824200BA39FA /* DatePicker.swift */,
EAC58C262BF4116200BA39FA /* DatePickerCalendarModel.swift */, EAC58C262BF4116200BA39FA /* DatePickerCalendarModel.swift */,
EAC58C282BF4118C00BA39FA /* DatePickerViewController.swift */,
EAC58C242BF2A7FB00BA39FA /* DatePickerChangeLog.txt */, EAC58C242BF2A7FB00BA39FA /* DatePickerChangeLog.txt */,
); );
path = DatePicker; path = DatePicker;
@ -1303,6 +1325,7 @@
EA5F86D02A1F936100BC83E4 /* TabsContainer.swift in Sources */, EA5F86D02A1F936100BC83E4 /* TabsContainer.swift in Sources */,
EAF7F0B1289B177F00B287F5 /* ColorLabelAttribute.swift in Sources */, EAF7F0B1289B177F00B287F5 /* ColorLabelAttribute.swift in Sources */,
EAC9258F2911C9DE00091998 /* EntryFieldBase.swift in Sources */, EAC9258F2911C9DE00091998 /* EntryFieldBase.swift in Sources */,
18B9763F2C11BA4A009271DF /* CarouselPaginationModel.swift in Sources */,
EAB1D2EA28AE84AA00DAE764 /* UIControlPublisher.swift in Sources */, EAB1D2EA28AE84AA00DAE764 /* UIControlPublisher.swift in Sources */,
EAD068922A560B65002E3A2D /* LoaderViewController.swift in Sources */, EAD068922A560B65002E3A2D /* LoaderViewController.swift in Sources */,
44BD43B62C04866600644F87 /* TableRowModel.swift in Sources */, 44BD43B62C04866600644F87 /* TableRowModel.swift in Sources */,
@ -1314,18 +1337,20 @@
EA8E40932A82889500934ED3 /* TooltipDialog.swift in Sources */, EA8E40932A82889500934ED3 /* TooltipDialog.swift in Sources */,
44604AD429CE186A00E62B51 /* NotificationButtonModel.swift in Sources */, 44604AD429CE186A00E62B51 /* NotificationButtonModel.swift in Sources */,
EAD8D2C128BFDE8B006EB6A6 /* UIGestureRecognizer+Publisher.swift in Sources */, EAD8D2C128BFDE8B006EB6A6 /* UIGestureRecognizer+Publisher.swift in Sources */,
18B42AC62C09D197008D6262 /* CarouselSlotAlignmentModel.swift in Sources */,
71B23C2D2B91FA690027F7D9 /* Pagination.swift in Sources */, 71B23C2D2B91FA690027F7D9 /* Pagination.swift in Sources */,
EA0D1C372A681CCE00E5C127 /* ToggleView.swift in Sources */, EA0D1C372A681CCE00E5C127 /* ToggleView.swift in Sources */,
EAF7F0B9289C139800B287F5 /* ColorConfiguration.swift in Sources */, EAF7F0B9289C139800B287F5 /* ColorConfiguration.swift in Sources */,
EA3361BD288B2C760071C351 /* TypeAlias.swift in Sources */, EA3361BD288B2C760071C351 /* TypeAlias.swift in Sources */,
EAC58C0A2BED004E00BA39FA /* FieldType.swift in Sources */, EAC58C0A2BED004E00BA39FA /* FieldType.swift in Sources */,
EA471F3A2A95587500CE9E58 /* LayoutConstraintable.swift in Sources */, EA471F3A2A95587500CE9E58 /* LayoutConstraintable.swift in Sources */,
EAC58C292BF4118C00BA39FA /* DatePickerViewController.swift in Sources */, EAC58C292BF4118C00BA39FA /* ClearPopoverViewController.swift in Sources */,
EAF193432C134F3800C68D18 /* TableCellItem.swift in Sources */, EAF193432C134F3800C68D18 /* TableCellItem.swift in Sources */,
EAB1D2CF28ABEF2B00DAE764 /* Typography+Base.swift in Sources */, EAB1D2CF28ABEF2B00DAE764 /* Typography+Base.swift in Sources */,
EA0D1C3B2A6AD51B00E5C127 /* Typogprahy+Styles.swift in Sources */, EA0D1C3B2A6AD51B00E5C127 /* Typogprahy+Styles.swift in Sources */,
EAF7F09A2899B17200B287F5 /* CATransaction.swift in Sources */, EAF7F09A2899B17200B287F5 /* CATransaction.swift in Sources */,
EAC58C162BED0E0300BA39FA /* InlineAction.swift in Sources */, EAC58C162BED0E0300BA39FA /* InlineAction.swift in Sources */,
EAF2F4892C2A1075007BFEDC /* AlertViewController.swift in Sources */,
EA0D1C3D2A6AD57600E5C127 /* Typography+Enums.swift in Sources */, EA0D1C3D2A6AD57600E5C127 /* Typography+Enums.swift in Sources */,
EAF1FE9B29DB1A6000101452 /* Changeable.swift in Sources */, EAF1FE9B29DB1A6000101452 /* Changeable.swift in Sources */,
EAC58C0C2BED01D500BA39FA /* Telephone.swift in Sources */, EAC58C0C2BED01D500BA39FA /* Telephone.swift in Sources */,
@ -1351,6 +1376,7 @@
EA0B18052A9E2D2D00F2D0CD /* SelectorBase.swift in Sources */, EA0B18052A9E2D2D00F2D0CD /* SelectorBase.swift in Sources */,
EAC71A1D2A2E155A00E47A9F /* Checkbox.swift in Sources */, EAC71A1D2A2E155A00E47A9F /* Checkbox.swift in Sources */,
EAF7F0AB289B13FD00B287F5 /* TextStyleLabelAttribute.swift in Sources */, EAF7F0AB289B13FD00B287F5 /* TextStyleLabelAttribute.swift in Sources */,
18AE87502C06FDA60075F181 /* Carousel.swift in Sources */,
EAB1D29C28A5618900DAE764 /* RadioButtonGroup.swift in Sources */, EAB1D29C28A5618900DAE764 /* RadioButtonGroup.swift in Sources */,
EA81410B2A0E8E3C004F60D2 /* ButtonIcon.swift in Sources */, EA81410B2A0E8E3C004F60D2 /* ButtonIcon.swift in Sources */,
EA985BE629688F6A00F2FF2E /* TileletBadgeModel.swift in Sources */, EA985BE629688F6A00F2FF2E /* TileletBadgeModel.swift in Sources */,
@ -1547,7 +1573,7 @@
BUILD_LIBRARY_FOR_DISTRIBUTION = YES; BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
CODE_SIGN_IDENTITY = ""; CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 67; CURRENT_PROJECT_VERSION = 71;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
@ -1585,7 +1611,7 @@
BUILD_LIBRARY_FOR_DISTRIBUTION = YES; BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
CODE_SIGN_IDENTITY = ""; CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 67; CURRENT_PROJECT_VERSION = 71;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;

View File

@ -10,6 +10,7 @@ import UIKit
import Combine import Combine
/// Base Class use to build Controls. /// Base Class use to build Controls.
@objcMembers
@objc(VDSControl) @objc(VDSControl)
open class Control: UIControl, ViewProtocol, UserInfoable, Clickable { open class Control: UIControl, ViewProtocol, UserInfoable, Clickable {
//-------------------------------------------------- //--------------------------------------------------
@ -129,7 +130,6 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable {
//-------------------------------------------------- //--------------------------------------------------
open var accessibilityAction: ((Control) -> Void)? open var accessibilityAction: ((Control) -> Void)?
private var _isAccessibilityElement: Bool = false
open override var isAccessibilityElement: Bool { open override var isAccessibilityElement: Bool {
get { get {
var block: AXBoolReturnBlock? var block: AXBoolReturnBlock?
@ -145,15 +145,14 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable {
if let block { if let block {
return block() return block()
} else { } else {
return _isAccessibilityElement return super.isAccessibilityElement
} }
} }
set { set {
_isAccessibilityElement = newValue super.isAccessibilityElement = newValue
} }
} }
private var _accessibilityLabel: String?
open override var accessibilityLabel: String? { open override var accessibilityLabel: String? {
get { get {
var block: AXStringReturnBlock? var block: AXStringReturnBlock?
@ -168,15 +167,14 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable {
if let block { if let block {
return block() return block()
} else { } else {
return _accessibilityLabel return super.accessibilityLabel
} }
} }
set { set {
_accessibilityLabel = newValue super.accessibilityLabel = newValue
} }
} }
private var _accessibilityHint: String?
open override var accessibilityHint: String? { open override var accessibilityHint: String? {
get { get {
var block: AXStringReturnBlock? var block: AXStringReturnBlock?
@ -191,15 +189,14 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable {
if let block { if let block {
return block() return block()
} else { } else {
return _accessibilityHint return super.accessibilityHint
} }
} }
set { set {
_accessibilityHint = newValue super.accessibilityHint = newValue
} }
} }
private var _accessibilityValue: String?
open override var accessibilityValue: String? { open override var accessibilityValue: String? {
get { get {
var block: AXStringReturnBlock? var block: AXStringReturnBlock?
@ -215,11 +212,11 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable {
if let block{ if let block{
return block() return block()
} else { } else {
return _accessibilityValue return super.accessibilityValue
} }
} }
set { set {
_accessibilityValue = newValue super.accessibilityValue = newValue
} }
} }

View File

@ -28,6 +28,8 @@ public protocol SelectorControlable: Control, Changeable {
} }
/// Base Class used to build out a Selector control. /// Base Class used to build out a Selector control.
@objcMembers
@objc(VDSSelectorBase)
open class SelectorBase: Control, SelectorControlable { open class SelectorBase: Control, SelectorControlable {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Initializers // MARK: - Initializers

View File

@ -10,6 +10,7 @@ import UIKit
import Combine import Combine
/// Base Class used to build Views. /// Base Class used to build Views.
@objcMembers
@objc(VDSView) @objc(VDSView)
open class View: UIView, ViewProtocol, UserInfoable { open class View: UIView, ViewProtocol, UserInfoable {
@ -96,7 +97,6 @@ open class View: UIView, ViewProtocol, UserInfoable {
//-------------------------------------------------- //--------------------------------------------------
open var accessibilityAction: ((View) -> Void)? open var accessibilityAction: ((View) -> Void)?
private var _isAccessibilityElement: Bool = false
open override var isAccessibilityElement: Bool { open override var isAccessibilityElement: Bool {
get { get {
var block: AXBoolReturnBlock? var block: AXBoolReturnBlock?
@ -112,22 +112,21 @@ open class View: UIView, ViewProtocol, UserInfoable {
if let block { if let block {
return block() return block()
} else { } else {
return _isAccessibilityElement return super.isAccessibilityElement
} }
} }
set { set {
_isAccessibilityElement = newValue super.isAccessibilityElement = newValue
} }
} }
private var _accessibilityLabel: String?
open override var accessibilityLabel: String? { open override var accessibilityLabel: String? {
get { get {
var block: AXStringReturnBlock? var block: AXStringReturnBlock?
// if #available(iOS 17, *) { // if #available(iOS 17, *) {
// block = accessibilityLabelBlock // block = accessibilityLabelBlock
// } // }
//
if block == nil { if block == nil {
block = bridge_accessibilityLabelBlock block = bridge_accessibilityLabelBlock
} }
@ -135,15 +134,14 @@ open class View: UIView, ViewProtocol, UserInfoable {
if let block { if let block {
return block() return block()
} else { } else {
return _accessibilityLabel return super.accessibilityLabel
} }
} }
set { set {
_accessibilityLabel = newValue super.accessibilityLabel = newValue
} }
} }
private var _accessibilityHint: String?
open override var accessibilityHint: String? { open override var accessibilityHint: String? {
get { get {
var block: AXStringReturnBlock? var block: AXStringReturnBlock?
@ -158,15 +156,14 @@ open class View: UIView, ViewProtocol, UserInfoable {
if let block { if let block {
return block() return block()
} else { } else {
return _accessibilityHint return super.accessibilityHint
} }
} }
set { set {
_accessibilityHint = newValue super.accessibilityHint = newValue
} }
} }
private var _accessibilityValue: String?
open override var accessibilityValue: String? { open override var accessibilityValue: String? {
get { get {
var block: AXStringReturnBlock? var block: AXStringReturnBlock?
@ -182,11 +179,11 @@ open class View: UIView, ViewProtocol, UserInfoable {
if let block{ if let block{
return block() return block()
} else { } else {
return _accessibilityValue return super.accessibilityValue
} }
} }
set { set {
_accessibilityValue = newValue super.accessibilityValue = newValue
} }
} }

View File

@ -0,0 +1,98 @@
//
// AlertViewController.swift
// VDS
//
// Created by Matt Bruce on 6/24/24.
//
import Foundation
import UIKit
import Combine
import VDSCoreTokens
@objcMembers
@objc(VDSAlertViewController)
open class AlertViewController: UIViewController, Surfaceable {
/// Set of Subscribers for any Publishers for this Control.
open var subscribers = Set<AnyCancellable>()
//--------------------------------------------------
// MARK: - Private Properties
//--------------------------------------------------
private var onClickSubscriber: AnyCancellable? {
willSet {
if let onClickSubscriber {
onClickSubscriber.cancel()
}
}
}
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
/// Current Surface and this is used to pass down to child objects that implement Surfacable
open var surface: Surface = .light { didSet { updateView() }}
open var presenter: UIView? { didSet { updateView() }}
open var dialog: UIView!
//--------------------------------------------------
// MARK: - Configuration
//--------------------------------------------------
private let backgroundColorConfiguration = SurfaceColorConfiguration(VDSColor.backgroundPrimaryDark, VDSColor.backgroundPrimaryLight)
//--------------------------------------------------
// MARK: - Lifecycle
//--------------------------------------------------
open override func viewDidLoad() {
super.viewDidLoad()
isModalInPresentation = true
setup()
}
open override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
UIAccessibility.post(notification: .screenChanged, argument: dialog)
}
private func dismiss() {
dismiss(animated: true) { [weak self] in
guard let self, let presenter else { return }
UIAccessibility.post(notification: .layoutChanged, argument: presenter)
}
}
open func setup() {
guard let dialog else { return }
view.accessibilityElements = [dialog]
view.addSubview(dialog)
// Activate constraints
NSLayoutConstraint.activate([
// Constraints for the floating modal view
dialog.centerXAnchor.constraint(equalTo: view.centerXAnchor),
dialog.centerYAnchor.constraint(equalTo: view.centerYAnchor),
dialog.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor, constant: 10),
dialog.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor, constant: -10),
dialog.topAnchor.constraint(greaterThanOrEqualTo: view.topAnchor, constant: 10),
dialog.bottomAnchor.constraint(lessThanOrEqualTo: view.bottomAnchor, constant: -10)
])
}
open override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location = touch.location(in: view)
if dialog.frame.contains(location) {
super.touchesBegan(touches, with: event)
} else {
dismiss()
}
}
/// Used to make changes to the View based off a change events or from local properties.
open func updateView() {
view.backgroundColor = backgroundColorConfiguration.getColor(self).withAlphaComponent(0.3)
if var dialog = dialog as? Surfaceable {
dialog.surface = surface
}
}
}

View File

@ -0,0 +1,154 @@
//
// DatePickerPopoverViewController.swift
// VDS
//
// Created by Matt Bruce on 5/14/24.
//
import Foundation
import UIKit
@objcMembers
@objc(VDSClearPopoverViewController)
open class ClearPopoverViewController: UIViewController, UIPopoverPresentationControllerDelegate {
/// The view to be inserted inside the popover
private var contentView: UIView!
/// An object representing the arrow of the popover.
private var arrow: UIPopoverArrowDirection
/// Popover presentation controller of the popover
private var popOver: UIPopoverPresentationController!
open var maxWidth: CGFloat?
open var sourceRect: CGRect?
open var spacing: CGFloat = 0
/**
A controller that manages the popover.
- Parameter contentView: The view to be inserted inside the popover.
- Parameter design: An object used for defining visual attributes of the popover.
- Parameter arrow: An object representing the arrow in popover.
- Parameter sourceView: The view containing the anchor rectangle for the popover.
- Parameter sourceRect: The rectangle in the specified view in which to anchor the popover.
- Parameter barButtonItem: The bar button item on which to anchor the popover.
Assign a value to `barButton` to anchor the popover to the specified bar button item. When presented, the popovers arrow points to the specified item. Alternatively, you may specify the anchor location for the popover using the `sourceView` and `sourceRect` properties.
*/
public init(contentView: UIView, arrow: UIPopoverArrowDirection, sourceView: UIView? = nil, sourceRect: CGRect? = nil, spacing: CGFloat = 0, barButtonItem: UIBarButtonItem? = nil) {
self.contentView = contentView
self.spacing = spacing
self.arrow = arrow
self.sourceRect = sourceRect
super.init(nibName: nil, bundle: nil)
setupPopover(sourceView, sourceRect, barButtonItem)
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
open override func viewIsAppearing(_ animated: Bool) {
super.viewIsAppearing(animated)
view.superview?.accessibilityIdentifier = "HadCornerRadius"
view.accessibilityIdentifier = "PopoverViewController.View"
contentView.accessibilityIdentifier = "PopoverViewController.ContentView"
view.superview?.layer.cornerRadius = 0
}
open override func viewDidLayoutSubviews() {
contentView.frame.origin = CGPoint(x: 0, y: 0)
}
///Sets up the Popover and starts the timer for its closing.
private func setupPopover(_ sourceView: UIView?, _ sourceRect: CGRect?, _ barButtonItem: UIBarButtonItem?) {
modalPresentationStyle = .popover
view.addSubview(contentView)
popOver = self.popoverPresentationController!
popOver.popoverLayoutMargins = .zero
popOver.popoverBackgroundViewClass = ClearPopoverBackgroundView.self
popOver.sourceView = sourceView
popOver.popoverLayoutMargins = .zero
if let sourceRect = sourceRect {
popOver.sourceRect = sourceRect
}
popOver.barButtonItem = barButtonItem
popOver.delegate = self
popOver.permittedArrowDirections = arrow
popOver.backgroundColor = .clear
}
open func popoverPresentationController(_ popoverPresentationController: UIPopoverPresentationController, willRepositionPopoverTo rect: UnsafeMutablePointer<CGRect>, in view: AutoreleasingUnsafeMutablePointer<UIView>) {
if let presentedView = popoverPresentationController.presentedViewController.view.superview {
presentedView.layer.cornerRadius = 0
}
}
private func updatePopoverPosition() {
guard let popoverPresentationController = popoverPresentationController else { return }
if let sourceView = popoverPresentationController.sourceView, let sourceRect {
popoverPresentationController.sourceRect = sourceRect
}
}
// Ensure to handle rotations
open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: { [weak self] _ in
self?.updatePopoverPosition()
})
}
open func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
return .none
}
// Returns presentation controller of the popover
open func getPopoverPresentationController() -> UIPopoverPresentationController {
return popOver
}
}
open class ClearPopoverBackgroundView: UIPopoverBackgroundView {
open override var arrowOffset: CGFloat {
get { 0 }
set { }
}
open override var arrowDirection: UIPopoverArrowDirection {
get { .any }
set { }
}
open override class var wantsDefaultContentAppearance: Bool {
false
}
open override class func contentViewInsets() -> UIEdgeInsets{
.zero
}
open override class func arrowHeight() -> CGFloat {
0
}
open override class func arrowBase() -> CGFloat{
0
}
open override func layoutSubviews() {
super.layoutSubviews()
layer.shadowOpacity = 0
layer.shadowRadius = 0
layer.cornerRadius = 0
}
open override func draw(_ rect: CGRect) {
}
}

View File

@ -15,6 +15,7 @@ import Combine
/// If you are using AutoLayoutConstraints you have a combination of Leading/Left and Trailing/Right NSLayoutConstraints, /// If you are using AutoLayoutConstraints you have a combination of Leading/Left and Trailing/Right NSLayoutConstraints,
/// you need to ensure that one of these Horizontal Contraints is not constraint of "equatTo". If you are to pin the left/right edges /// you need to ensure that one of these Horizontal Contraints is not constraint of "equatTo". If you are to pin the left/right edges
/// to its parent this object will stretch to the parent's width. /// to its parent this object will stretch to the parent's width.
@objcMembers
@objc(VDSBadge) @objc(VDSBadge)
open class Badge: View { open class Badge: View {

View File

@ -11,6 +11,7 @@ import VDSCoreTokens
import Combine import Combine
/// A badge indicator is a visual label used to convey status or highlight supplemental information. /// A badge indicator is a visual label used to convey status or highlight supplemental information.
@objcMembers
@objc(VDSBadgeIndicator) @objc(VDSBadgeIndicator)
open class BadgeIndicator: View { open class BadgeIndicator: View {

View File

@ -13,6 +13,7 @@ import Combine
/// A Breadcrumb Item contains href(link) and selected flag. /// A Breadcrumb Item contains href(link) and selected flag.
/// Breadcrumb links to its respective page if it is not disabled. /// 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. /// Breadcrumb contains text with a separator by default, highlights text in bold without a separator if selected.
@objcMembers
@objc (VDSBreadcrumbItem) @objc (VDSBreadcrumbItem)
open class BreadcrumbItem: ButtonBase { open class BreadcrumbItem: ButtonBase {

View File

@ -13,6 +13,7 @@ import Combine
/// A Breadcrumbs contains BreadcrumbItems. /// A Breadcrumbs contains BreadcrumbItems.
/// It contains Breadcrumb Item Default, Breadcrumb Item Selected, Separator. /// 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. /// 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.
@objcMembers
@objc(VDSBreadcrumbs) @objc(VDSBreadcrumbs)
open class Breadcrumbs: View { open class Breadcrumbs: View {

View File

@ -15,6 +15,7 @@ import Combine
/// If you are using AutoLayoutConstraints you have a combination of Leading/Left and Trailing/Right NSLayoutConstraints, /// If you are using AutoLayoutConstraints you have a combination of Leading/Left and Trailing/Right NSLayoutConstraints,
/// you need to ensure that one of these Horizontal Contraints is not constraint of "equatTo". If you are to pin the left/right edges /// you need to ensure that one of these Horizontal Contraints is not constraint of "equatTo". If you are to pin the left/right edges
/// to its parent this object will stretch to the parent's width. /// to its parent this object will stretch to the parent's width.
@objcMembers
@objc(VDSButton) @objc(VDSButton)
open class Button: ButtonBase, Useable { open class Button: ButtonBase, Useable {

View File

@ -11,6 +11,7 @@ import VDSCoreTokens
import Combine import Combine
/// Base class used for UIButton type classes. /// Base class used for UIButton type classes.
@objcMembers
@objc(VDSButtonBase) @objc(VDSButtonBase)
open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable { open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable {
@ -177,7 +178,6 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable {
//-------------------------------------------------- //--------------------------------------------------
open var accessibilityAction: ((ButtonBase) -> Void)? open var accessibilityAction: ((ButtonBase) -> Void)?
private var _isAccessibilityElement: Bool = false
open override var isAccessibilityElement: Bool { open override var isAccessibilityElement: Bool {
get { get {
var block: AXBoolReturnBlock? var block: AXBoolReturnBlock?
@ -193,15 +193,14 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable {
if let block { if let block {
return block() return block()
} else { } else {
return _isAccessibilityElement return super.isAccessibilityElement
} }
} }
set { set {
_isAccessibilityElement = newValue super.isAccessibilityElement = newValue
} }
} }
private var _accessibilityLabel: String?
open override var accessibilityLabel: String? { open override var accessibilityLabel: String? {
get { get {
var block: AXStringReturnBlock? var block: AXStringReturnBlock?
@ -216,15 +215,14 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable {
if let block { if let block {
return block() return block()
} else { } else {
return _accessibilityLabel return super.accessibilityLabel
} }
} }
set { set {
_accessibilityLabel = newValue super.accessibilityLabel = newValue
} }
} }
private var _accessibilityHint: String?
open override var accessibilityHint: String? { open override var accessibilityHint: String? {
get { get {
var block: AXStringReturnBlock? var block: AXStringReturnBlock?
@ -239,15 +237,14 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable {
if let block { if let block {
return block() return block()
} else { } else {
return _accessibilityHint return super.accessibilityHint
} }
} }
set { set {
_accessibilityHint = newValue super.accessibilityHint = newValue
} }
} }
private var _accessibilityValue: String?
open override var accessibilityValue: String? { open override var accessibilityValue: String? {
get { get {
var block: AXStringReturnBlock? var block: AXStringReturnBlock?
@ -263,11 +260,11 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable {
if let block{ if let block{
return block() return block()
} else { } else {
return _accessibilityValue return super.accessibilityValue
} }
} }
set { set {
_accessibilityValue = newValue super.accessibilityValue = newValue
} }
} }

View File

@ -11,6 +11,7 @@ import VDSCoreTokens
import Combine 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. /// 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.
@objcMembers
@objc(VDSButtonGroup) @objc(VDSButtonGroup)
open class ButtonGroup: View { open class ButtonGroup: View {

View File

@ -16,6 +16,7 @@ import Combine
/// If you are using AutoLayoutConstraints you have a combination of Leading/Left and Trailing/Right NSLayoutConstraints, /// If you are using AutoLayoutConstraints you have a combination of Leading/Left and Trailing/Right NSLayoutConstraints,
/// you need to ensure that one of these Horizontal Contraints is not constraint of "equatTo". If you are to pin the left/right edges /// you need to ensure that one of these Horizontal Contraints is not constraint of "equatTo". If you are to pin the left/right edges
/// to its parent this object will stretch to the parent's width. /// to its parent this object will stretch to the parent's width.
@objcMembers
@objc(VDSTextLink) @objc(VDSTextLink)
open class TextLink: ButtonBase { open class TextLink: ButtonBase {
//-------------------------------------------------- //--------------------------------------------------

View File

@ -16,6 +16,7 @@ import Combine
/// If you are using AutoLayoutConstraints you have a combination of Leading/Left and Trailing/Right NSLayoutConstraints, /// If you are using AutoLayoutConstraints you have a combination of Leading/Left and Trailing/Right NSLayoutConstraints,
/// you need to ensure that one of these Horizontal Contraints is not constraint of "equatTo". If you are to pin the left/right edges /// you need to ensure that one of these Horizontal Contraints is not constraint of "equatTo". If you are to pin the left/right edges
/// to its parent this object will stretch to the parent's width. /// to its parent this object will stretch to the parent's width.
@objcMembers
@objc(VDSTextLinkCaret) @objc(VDSTextLinkCaret)
open class TextLinkCaret: ButtonBase { open class TextLinkCaret: ButtonBase {
//-------------------------------------------------- //--------------------------------------------------

View File

@ -11,6 +11,7 @@ import VDSCoreTokens
import Combine import Combine
/// A calendar is a monthly view that lets customers select a single date. /// A calendar is a monthly view that lets customers select a single date.
@objcMembers
@objc(VDSCalendar) @objc(VDSCalendar)
open class CalendarBase: Control, Changeable { open class CalendarBase: Control, Changeable {

View File

@ -0,0 +1,566 @@
//
// Carousel.swift
// VDS
//
// Created by Kanamarlapudi, Vasavi on 29/05/24.
//
import Foundation
import UIKit
import VDSCoreTokens
import Combine
/// A carousel is a collection of related content in a row that a customer can navigate through horizontally.
/// Use this component to show content that is supplementary, not essential for task completion.
@objcMembers
@objc(VDSCarousel)
open class Carousel: View {
//--------------------------------------------------
// MARK: - Initializers
//--------------------------------------------------
required public init() {
super.init(frame: .zero)
}
public override init(frame: CGRect) {
super.init(frame: .zero)
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
}
//--------------------------------------------------
// MARK: - Enums
//--------------------------------------------------
/// Enum used to describe the pagination display for this component.
public enum PaginationDisplay: String, CaseIterable {
case persistent, none
}
/// Enum used to describe the peek for this component.
/// This is how much a tile is partially visible. It is measured by the distance between the edge of
/// the tile and the edge of the viewport or carousel container. A peek can appear on the left and/or
/// right edge of the carousel container or viewport, depending on the carousels scroll position.
public enum Peek: String, CaseIterable {
case standard, minimum, none
}
/// Enum used to describe the vertical of slotAlignment.
public enum Vertical: String, CaseIterable {
case top, middle, bottom
}
/// Enum used to describe the horizontal of slotAlignment.
public enum Horizontal: String, CaseIterable {
case left, center, right
}
/// Space between each tile. The default value will be 6X in tablet and 3X in mobile.
public enum Gutter: String, CaseIterable , DefaultValuing {
case gutter3X = "3X"
case gutter6X = "6X"
public static var defaultValue: Self { UIDevice.isIPad ? .gutter6X : .gutter3X }
public var value: CGFloat {
switch self {
case .gutter3X:
VDSLayout.space3X
case .gutter6X:
VDSLayout.space6X
}
}
}
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
/// views used to render view in the carousel slots.
open var views: [UIView] = [] { didSet { setNeedsUpdate() } }
/// Space between each tile. The default value will be 6X in tablet and 3X in mobile.
open var gutter: Gutter = Gutter.defaultValue { didSet { setNeedsUpdate() } }
/// The amount of slides visible in the carousel container at one time.
/// The default value will be 3UP in tablet and 1UP in mobile.
open var layout: CarouselScrollbar.Layout = UIDevice.isIPad ? .threeUP : .oneUP {
didSet {
carouselScrollBar.position = 0
setNeedsUpdate()
}
}
/// A callback when moving the carousel. Returns selectedGroupIndex.
open var onChange: ((Int) -> Void)? {
get { nil }
set {
onChangeCancellable?.cancel()
if let newValue {
onChangeCancellable = onChangePublisher
.sink { c in
newValue(c)
}
}
}
}
/// Config object for pagination.
open var pagination: CarouselPaginationModel = .init(kind: .lowContrast, floating: true) { didSet {setNeedsUpdate() } }
/// If provided, will determine the conditions to render the pagination arrows.
open var paginationDisplay: PaginationDisplay = .none { didSet {setNeedsUpdate() } }
/// If provided, will apply margin to pagination arrows. Can be set to either positive or negative values.
/// The default value will be 3X in tablet and 2X in mobile. These values are the default in order to avoid overlapping content within the carousel.
open var paginationInset: CGFloat = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space2X { didSet { updatePaginationInset() } }
/// Options for user to configure the partially-visible tile in group.
/// Setting peek to 'none' will display arrow navigation icons on mobile devices.
open var peek: Peek = .standard { didSet { setNeedsUpdate() } }
/// The initial visible slide's index in the carousel.
open var groupIndex: Int = 0 { didSet { setNeedsUpdate() } }
/// If provided, will set the alignment for slot content when the slots has different heights.
open var slotAlignment: CarouselSlotAlignmentModel? = .init(vertical: .top, horizontal: .left) { didSet { setNeedsUpdate() } }
//--------------------------------------------------
// MARK: - Private Properties
//--------------------------------------------------
internal var containerSize: CGSize { CGSize(width: frame.size.width, height: 44) }
private let contentStackView = UIStackView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.axis = .vertical
$0.distribution = .fill
$0.spacing = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space1X
$0.backgroundColor = .clear
}
internal var carouselScrollBar = CarouselScrollbar().with {
$0.layout = UIDevice.isIPad ? .threeUP : .oneUP
$0.position = 0
$0.backgroundColor = .clear
}
internal var containerView = View().with {
$0.clipsToBounds = true
$0.backgroundColor = .clear
}
internal var scrollContainerView = View().with {
$0.clipsToBounds = true
$0.backgroundColor = .clear
}
private var scrollView = UIScrollView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.backgroundColor = .clear
}
/// Previous button to show previous slide.
private var previousButton = ButtonIcon().with {
$0.kind = .lowContrast
$0.iconName = .leftCaret
$0.iconOffset = .init(x: -2, y: 0)
$0.customContainerSize = UIDevice.isIPad ? 40 : 28
$0.icon.customSize = UIDevice.isIPad ? 16 : 12
}
/// Next button to show next slide.
private var nextButton = ButtonIcon().with {
$0.kind = .lowContrast
$0.iconName = .rightCaret
$0.iconOffset = .init(x: 2, y: 0)
$0.customContainerSize = UIDevice.isIPad ? 40 : 28
$0.icon.customSize = UIDevice.isIPad ? 16 : 12
}
/// A publisher for when moving the carousel. Passes parameters selectedGroupIndex (position).
open var onChangePublisher = PassthroughSubject<Int, Never>()
private var onChangeCancellable: AnyCancellable?
private var containerStackHeightConstraint: NSLayoutConstraint?
private var containerViewHeightConstraint: NSLayoutConstraint?
private var prevButtonLeadingConstraint: NSLayoutConstraint?
private var nextButtonTrailingConstraint: NSLayoutConstraint?
// The scrollbar has top 5X space. So the expected top space is adjusted for tablet and mobile.
let scrollbarTopSpace = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space1X
var slotDefaultHeight = 50.0
var peekMinimum = 24.0
var minimumSlotWidth = 0.0
//--------------------------------------------------
// MARK: - Lifecycle
//--------------------------------------------------
/// Executed on initialization for this View.
open override func initialSetup() {
super.initialSetup()
}
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
open override func setup() {
super.setup()
isAccessibilityElement = false
// Add containerView
addSubview(containerView)
containerView
.pinTop()
.pinBottom()
.pinLeading()
.pinTrailing()
.heightGreaterThanEqualTo(containerSize.height)
containerView.centerYAnchor.constraint(equalTo: centerYAnchor).activate()
// Add content stackview
containerView.addSubview(contentStackView)
// Add scrollview
scrollContainerView.addSubview(scrollView)
scrollView.pinToSuperView()
// Add pagination button icons
scrollContainerView.addSubview(previousButton)
previousButton
.pinLeadingGreaterThanOrEqualTo()
.pinCenterY()
scrollContainerView.addSubview(nextButton)
nextButton
.pinTrailingLessThanOrEqualTo()
.pinCenterY()
// Add scroll container view & carousel scrollbar
contentStackView.addArrangedSubview(scrollContainerView)
contentStackView.addArrangedSubview(carouselScrollBar)
contentStackView.setCustomSpacing(scrollbarTopSpace, after: scrollContainerView)
contentStackView
.pinTop()
.pinBottom()
.pinLeading()
.pinTrailing()
.heightGreaterThanEqualTo(containerSize.height)
addlisteners()
updatePaginationInset()
}
/// Used to make changes to the View based off a change events or from local properties.
open override func updateView() {
super.updateView()
carouselScrollBar.numberOfSlides = views.count
carouselScrollBar.layout = layout
if (carouselScrollBar.position == 0 || carouselScrollBar.position > carouselScrollBar.numberOfSlides) {
carouselScrollBar.position = 1
}
carouselScrollBar.isHidden = (totalPositions() <= 1) ? true : false
// Mobile/Tablet layouts without peek - must show pagination controls.
// If peek is none, pagination controls should show. So set to persistent.
if peek == .none {
paginationDisplay = .persistent
}
// Minimum (Mobile only) Supported only on Mobile viewports. If a user passes Minimum for tablet carousel, the peek reverts to Standard.
if UIDevice.isIPad && peek == .minimum {
peek = .standard
}
// Standard(Default) Peek - Supported for all Tablet viewports and layouts. Supported only for 1up layouts on Mobile viewports.
if peek == .standard && !UIDevice.isIPad && layout != CarouselScrollbar.Layout.oneUP {
peek = .minimum
}
updatePaginationControls()
addCarouselSlots()
}
/// Resets to default settings.
open override func reset() {
super.reset()
shouldUpdateView = false
layout = UIDevice.isIPad ? .threeUP : .oneUP
pagination = .init(kind: .lowContrast, floating: true)
paginationDisplay = .none
paginationInset = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space2X
gutter = UIDevice.isIPad ? .gutter6X : .gutter3X
peek = .standard
}
//--------------------------------------------------
// MARK: - Private Methods
//--------------------------------------------------
private func addlisteners() {
nextButton.onClick = { _ in self.nextButtonClick() }
previousButton.onClick = { _ in self.previousButtonClick() }
/// Will be called when the scrubber position changes.
carouselScrollBar.onScrubberDrag = { [weak self] scrubberId in
guard let self else { return }
updateScrollPosition(position: scrubberId, callbackText:"onThumbPositionChange")
}
/// Will be called when the scrollbar thumb move forward.
carouselScrollBar.onMoveForward = { [weak self] scrubberId in
guard let self else { return }
updateScrollPosition(position: scrubberId, callbackText:"onMoveForward")
}
/// Will be called when the scrollbar thumb move backward.
carouselScrollBar.onMoveBackward = { [weak self] scrubberId in
guard let self else { return }
updateScrollPosition(position: scrubberId, callbackText:"onMoveBackward")
}
/// Will be called when the scrollbar thumb touch start.
carouselScrollBar.onThumbTouchStart = { [weak self] scrubberId in
guard let self else { return }
updateScrollPosition(position: scrubberId, callbackText:"onThumbTouchStart")
}
/// Will be called when the scrollbar thumb touch end.
carouselScrollBar.onThumbTouchEnd = { [weak self] scrubberId in
guard let self else { return }
updateScrollPosition(position: scrubberId, callbackText:"onThumbTouchEnd")
}
}
// Update pagination buttons with selected surface, kind, floating values
private func updatePaginationControls() {
containerView.surface = surface
showPaginationControls()
previousButton.kind = pagination.kind
previousButton.floating = pagination.floating
nextButton.kind = pagination.kind
nextButton.floating = pagination.floating
previousButton.surface = surface
nextButton.surface = surface
}
// Show/Hide pagination buttons of Carousel based on First or Middle or Last
private func showPaginationControls() {
if carouselScrollBar.numberOfSlides == layout.value {
previousButton.isHidden = true
nextButton.isHidden = true
} else {
previousButton.isHidden = (carouselScrollBar.position == 1) || (paginationDisplay == .none)
nextButton.isHidden = (carouselScrollBar.position == totalPositions()) || (paginationDisplay == .none)
}
}
private func estimateHeightFor(component: UIView, with itemWidth: CGFloat) -> CGFloat {
let maxSize = CGSize(width: itemWidth, height: CGFloat.greatestFiniteMagnitude)
let estItemSize = component.systemLayoutSizeFitting(maxSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
return estItemSize.height
}
private func fetchCarouselHeight() -> CGFloat {
var height = slotDefaultHeight
if views.count > 0 {
for index in 0...views.count - 1 {
let estHeight = estimateHeightFor(component: views[index], with: minimumSlotWidth)
height = estHeight > height ? estHeight : height
}
}
return height
}
// Add carousel slots and load data if any
private func addCarouselSlots() {
getSlotWidth()
if containerView.frame.size.width > 0 {
containerViewHeightConstraint?.isActive = false
containerStackHeightConstraint?.isActive = false
let slotHeight = fetchCarouselHeight()
// Perform a loop to iterate each subView
scrollView.subviews.forEach { subView in
// Removing subView from its parent view
subView.removeFromSuperview()
}
// Add carousel items
if views.count > 0 {
var xPos = 0.0
for index in 0...views.count - 1 {
// Add Carousel Slot
let carouselSlot = View().with {
$0.clipsToBounds = true
}
scrollView.addSubview(carouselSlot)
scrollView.delegate = self
carouselSlot
.pinTop()
.pinBottom()
.pinLeading(xPos)
.width(minimumSlotWidth)
.height(slotHeight)
xPos = xPos + minimumSlotWidth + gutter.value
let component = views[index]
carouselSlot.addSubview(component)
setSlotAlignment(contentView: component)
}
scrollView.contentSize = CGSize(width: xPos - gutter.value, height: slotHeight)
}
let containerHeight = slotHeight + scrollbarTopSpace + containerSize.height
if carouselScrollBar.isHidden {
containerStackHeightConstraint = contentStackView.heightAnchor.constraint(equalToConstant: slotHeight)
containerViewHeightConstraint = containerView.heightAnchor.constraint(equalToConstant: slotHeight)
} else {
containerStackHeightConstraint = contentStackView.heightAnchor.constraint(equalToConstant: containerHeight)
containerViewHeightConstraint = containerView.heightAnchor.constraint(equalToConstant: containerHeight)
}
containerViewHeightConstraint?.isActive = true
containerStackHeightConstraint?.isActive = true
}
}
// Set slot alignment if provided. Used only when slot content have different heights or widths.
private func setSlotAlignment(contentView: UIView) {
switch slotAlignment?.vertical {
case .top:
contentView
.pinTop()
.pinBottomLessThanOrEqualTo()
case .middle:
contentView
.pinTopGreaterThanOrEqualTo()
.pinBottomLessThanOrEqualTo()
.pinCenterY()
case .bottom:
contentView
.pinTopGreaterThanOrEqualTo()
.pinBottom()
default: break
}
switch slotAlignment?.horizontal {
case .left:
contentView
.pinLeading()
.pinTrailingLessThanOrEqualTo()
case .center:
contentView
.pinLeadingGreaterThanOrEqualTo()
.pinTrailingLessThanOrEqualTo()
.pinCenterX()
case .right:
contentView
.pinLeadingGreaterThanOrEqualTo()
.pinTrailing()
default: break
}
}
// Get the slot width relative to the peak
private func getSlotWidth() {
let actualWidth = containerView.frame.size.width
let isScrollbarSuppressed = views.count > 0 && layout.value == views.count
let isPeekMinimumOnTablet = UIDevice.isIPad && peek == .minimum
let isPeekNone: Bool = peek == .none
minimumSlotWidth = isScrollbarSuppressed || isPeekMinimumOnTablet || isPeekNone ? actualWidth - ((CGFloat(layout.value)-1) * gutter.value): actualWidth - (CGFloat(layout.value) * gutter.value)
if !isScrollbarSuppressed {
switch peek {
case .standard:
// Standard(Default) Peek - Supported for all Tablet viewports and layouts. Supported only for 1up layouts on Mobile viewports.
if UIDevice.isIPad {
minimumSlotWidth = minimumSlotWidth - (minimumSlotWidth/(CGFloat(layout.value) + 3))
} else if layout == .oneUP {
minimumSlotWidth = minimumSlotWidth - (minimumSlotWidth/4)
}
case .minimum:
// Peek Mimumum Width: 24px from edge of container (at the default view of the carousel with one peek visible)
// Minimum (Mobile only) Supported only on Mobile viewports. If a user passes Minimum for tablet carousel, the peek reverts to Standard.
minimumSlotWidth = isPeekMinimumOnTablet ? minimumSlotWidth : minimumSlotWidth - peekMinimum - gutter.value
case .none:
break
}
}
minimumSlotWidth = ceil(minimumSlotWidth / CGFloat(layout.value))
}
private func nextButtonClick() {
carouselScrollBar.position = carouselScrollBar.position+1
showPaginationControls()
updateScrollPosition(position: carouselScrollBar.position, callbackText:"pageControlClicks")
}
private func previousButtonClick() {
carouselScrollBar.position = carouselScrollBar.position-1
showPaginationControls()
updateScrollPosition(position: carouselScrollBar.position, callbackText:"pageControlClicks")
}
private func updatePaginationInset() {
prevButtonLeadingConstraint?.isActive = false
nextButtonTrailingConstraint?.isActive = false
prevButtonLeadingConstraint = previousButton.leadingAnchor.constraint(equalTo: scrollContainerView.leadingAnchor, constant: paginationInset)
nextButtonTrailingConstraint = nextButton.trailingAnchor.constraint(equalTo: scrollContainerView.trailingAnchor, constant: -paginationInset)
prevButtonLeadingConstraint?.isActive = true
nextButtonTrailingConstraint?.isActive = true
}
private func updateScrollbarPosition(targetContentOffsetXPos:CGFloat) {
let scrollContentSizeWidth = scrollView.contentSize.width
let totalPositions = totalPositions()
let layoutSpace = Int (floor( Double(scrollContentSizeWidth / Double(totalPositions))))
let remindSpace = Int(targetContentOffsetXPos) % layoutSpace
var contentPos = (Int(targetContentOffsetXPos) / layoutSpace) + 1
contentPos = remindSpace > layoutSpace/2 ? contentPos+1 : contentPos
carouselScrollBar.position = contentPos
updateScrollPosition(position: contentPos, callbackText: "ScrollViewMoved")
}
// Update scrollview offset relative to scrollbar thumb position
private func updateScrollPosition(position: Int, callbackText: String) {
if carouselScrollBar.numberOfSlides > 0 {
let scrollContentSizeWidth = scrollView.contentSize.width
let totalPositions = totalPositions()
var xPos = 0.0
if position == 1 {
xPos = 0.0
} else if position == totalPositions {
xPos = scrollContentSizeWidth - containerView.frame.size.width
} else {
let isScrollbarSuppressed = views.count > 0 && layout.value == views.count
let isPeekMinimumOnTablet = UIDevice.isIPad && peek == .minimum
if !isScrollbarSuppressed {
let slotWidthWithGutter = minimumSlotWidth + gutter.value
let xPosition = CGFloat( Float(position-1) * Float(layout.value) * Float(slotWidthWithGutter))
let peekWidth = (containerView.frame.size.width - gutter.value - (Double(layout.value) * (minimumSlotWidth + gutter.value)))/2
xPos = (peek == .none || isPeekMinimumOnTablet) ? xPosition : xPosition - gutter.value - peekWidth
}
}
carouselScrollBar.scrubberId = position+1
let yPos = scrollView.contentOffset.y
scrollView.setContentOffset(CGPoint(x: xPos, y: yPos), animated: true)
showPaginationControls()
groupIndex = position-1
onChangePublisher.send(groupIndex)
}
}
// Get the overall positions of the carousel scrollbar relative to the slides and selected layout
private func totalPositions() -> Int {
return Int (ceil (Double(carouselScrollBar.numberOfSlides) / Double(layout.value)))
}
}
extension Carousel: UIScrollViewDelegate {
//--------------------------------------------------
// MARK: - UIScrollView Delegate
//--------------------------------------------------
public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
updateScrollbarPosition(targetContentOffsetXPos: targetContentOffset.pointee.x)
}
}

View File

@ -0,0 +1,15 @@
MM/DD/YYYY
----------------
06/22/2023
----------------
- Initial Beta Release
10/02/2023
----------------
- Removed (Beta) from header. Removed deprecated sections and “New” badge from Kind section.
11/20/2023
----------------
- Updated visuals to reflect new corner radius value - 12px
- Updated focus border corner radius to 14px

View File

@ -0,0 +1,26 @@
//
// CarouselPaginationModel.swift
// VDS
//
// Created by Kanamarlapudi, Vasavi on 06/06/24.
//
import Foundation
import UIKit
/// Custom data type for pagination prop for 'Carousel' component.
extension Carousel {
public struct CarouselPaginationModel {
/// Pagination supports Button icon property 'kind'.
public var kind: ButtonIcon.Kind
/// Pagination supports Button icon property 'floating'.
public var floating: Bool
public init(kind: ButtonIcon.Kind, floating: Bool) {
self.kind = kind
self.floating = floating
}
}
}

View File

@ -0,0 +1,27 @@
//
// CarouselSlotAlignmentModel.swift
// VDS
//
// Created by Kanamarlapudi, Vasavi on 31/05/24.
//
import Foundation
/// Custom data type for the SlotAlignment prop for the 'carousel' component.
extension Carousel {
/// Used only when slot content have different heights or widths.
public struct CarouselSlotAlignmentModel {
/// Used for vertical alignment of slot alignment.
public var vertical: Carousel.Vertical
/// Used for horizontal alignment of slot alignment.
public var horizontal: Carousel.Horizontal
public init(vertical: Carousel.Vertical, horizontal: Carousel.Horizontal) {
self.vertical = vertical
self.horizontal = horizontal
}
}
}

View File

@ -12,6 +12,7 @@ import Combine
/// A carousel scrollbar is a control that allows to navigate between items in a carousel. /// 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. /// It's also a status indicator that conveys the relative amount of content in a carousel and a location within it.
@objcMembers
@objc(VDSCarouselScrollbar) @objc(VDSCarouselScrollbar)
open class CarouselScrollbar: View { open class CarouselScrollbar: View {
@ -45,13 +46,13 @@ open class CarouselScrollbar: View {
} }
/// The number of slides that can appear at once in a set in a carousel container. /// The number of slides that can appear at once in a set in a carousel container.
open var selectedLayout: Layout? { open var layout: Layout? {
get { return _selectedLayout } get { return _layout }
set { set {
if let newValue { if let newValue {
_selectedLayout = newValue _layout = newValue
} else { } else {
_selectedLayout = .oneUP _layout = .oneUP
} }
setThumbWidth() setThumbWidth()
scrollThumbToPosition(position) scrollThumbToPosition(position)
@ -198,7 +199,7 @@ open class CarouselScrollbar: View {
//-------------------------------------------------- //--------------------------------------------------
// Sizes are from InVision design specs. // Sizes are from InVision design specs.
internal var containerSize: CGSize { CGSize(width: 45, height: 44) } internal var containerSize: CGSize { CGSize(width: 45, height: 44) }
internal var _selectedLayout: Layout = .oneUP internal var _layout: Layout = .oneUP
internal var _numberOfSlides: Int = 1 internal var _numberOfSlides: Int = 1
internal var totalPositions: Int = 1 internal var totalPositions: Int = 1
internal var _position: Int = 1 internal var _position: Int = 1
@ -329,7 +330,7 @@ open class CarouselScrollbar: View {
// Compute track width and should maintain minimum thumb width if needed // Compute track width and should maintain minimum thumb width if needed
private func setThumbWidth() { private func setThumbWidth() {
let width = (Float(trackViewWidth) / Float(numberOfSlides)) * Float(_selectedLayout.value) let width = (Float(trackViewWidth) / Float(numberOfSlides)) * Float(_layout.value)
computedWidth = (width > Float(trackViewWidth)) ? Float(trackViewWidth) : width computedWidth = (width > Float(trackViewWidth)) ? Float(trackViewWidth) : width
thumbWidth = (width <= Float(trackViewWidth) && width > minThumbWidth) ? width : ((width > Float(trackViewWidth)) ? Float(trackViewWidth) : minThumbWidth) thumbWidth = (width <= Float(trackViewWidth) && width > minThumbWidth) ? width : ((width > Float(trackViewWidth)) ? Float(trackViewWidth) : minThumbWidth)
thumbView.frame.size.width = CGFloat(thumbWidth) thumbView.frame.size.width = CGFloat(thumbWidth)
@ -362,7 +363,7 @@ open class CarouselScrollbar: View {
} }
private func checkPositions() { private func checkPositions() {
totalPositions = Int (ceil (Double(numberOfSlides) / Double(_selectedLayout.value))) totalPositions = Int (ceil (Double(numberOfSlides) / Double(_layout.value)))
} }
private func scrollThumbToPosition(_ position: Int) { private func scrollThumbToPosition(_ position: Int) {

View File

@ -12,6 +12,7 @@ import VDSCoreTokens
/// Checkboxes are a multi-select component through which a customer indicates a choice. This is also used within /// Checkboxes are a multi-select component through which a customer indicates a choice. This is also used within
/// ``CheckboxItem`` and ``CheckboxGroup`` /// ``CheckboxItem`` and ``CheckboxGroup``
@objcMembers
@objc(VDSCheckbox) @objc(VDSCheckbox)
open class Checkbox: SelectorBase { open class Checkbox: SelectorBase {

View File

@ -12,6 +12,7 @@ import VDSCoreTokens
/// When the choice has multiple options, use a checkbox group. For example, use a checkbox group when /// When the choice has multiple options, use a checkbox group. For example, use a checkbox group when
/// asking a customer which attributes they would like to filter their search by. This uses ``CheckboxItem`` /// asking a customer which attributes they would like to filter their search by. This uses ``CheckboxItem``
/// to allow user selection. /// to allow user selection.
@objcMembers
@objc(VDSCheckboxGroup) @objc(VDSCheckboxGroup)
open class CheckboxGroup: SelectorGroupBase<CheckboxItem>, SelectorGroupMultiSelect { open class CheckboxGroup: SelectorGroupBase<CheckboxItem>, SelectorGroupMultiSelect {

View File

@ -9,6 +9,7 @@ import Foundation
import UIKit import UIKit
/// Checkboxes are a multi-select component through which a customer indicates a choice. If a binary choice, the component is a checkbox. If the choice has multiple options, the component is a ``CheckboxGroup``. /// Checkboxes are a multi-select component through which a customer indicates a choice. If a binary choice, the component is a checkbox. If the choice has multiple options, the component is a ``CheckboxGroup``.
@objcMembers
@objc(VDSCheckboxItem) @objc(VDSCheckboxItem)
open class CheckboxItem: SelectorItemBase<Checkbox> { open class CheckboxItem: SelectorItemBase<Checkbox> {
@ -48,9 +49,37 @@ open class CheckboxItem: SelectorItemBase<Checkbox> {
sendActions(for: .valueChanged) sendActions(for: .valueChanged)
} }
open override func setup() {
super.setup()
let foo = ConcreteClass(customView: Checkbox())
print(foo.customView.isAnimated)
}
/// Used to make changes to the View based off a change events or from local properties. /// Used to make changes to the View based off a change events or from local properties.
open override func updateView() { open override func updateView() {
selectorView.isAnimated = isAnimated selectorView.isAnimated = isAnimated
super.updateView() super.updateView()
} }
} }
@objcMembers
open class GenericClass<T: UIView>: NSObject {
public var customView: T
public init(customView: T = T()) {
self.customView = customView
}
}
@objcMembers
@objc(VDSConcreteClass)
open class ConcreteClass: GenericClass<Checkbox> {
}
@objcMembers
@objc(VDSConcreteCheckboxClass)
open class ConcreteCheckboxClass: ConcreteClass {}

View File

@ -4,8 +4,9 @@ import VDSCoreTokens
import Combine import Combine
/// A dropdown select is an expandable menu of predefined options that allows a customer to make a single selection. /// A dropdown select is an expandable menu of predefined options that allows a customer to make a single selection.
@objcMembers
@objc(VDSDatePicker) @objc(VDSDatePicker)
open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopoverPresentationControllerDelegate { open class DatePicker: EntryFieldBase {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Initializers // MARK: - Initializers
//-------------------------------------------------- //--------------------------------------------------
@ -27,11 +28,32 @@ open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopov
/// A callback when the selected option changes. Passes parameters (option). /// A callback when the selected option changes. Passes parameters (option).
open var onDateSelected: ((Date, DatePicker) -> Void)? open var onDateSelected: ((Date, DatePicker) -> Void)?
/// Override UIControl state to add the .error state if showError is true.
open override var state: UIControl.State {
get {
var state = super.state
if isEnabled {
if isCalendarShowing {
state.insert(.focused)
}
}
return state
}
}
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Private Properties // MARK: - Private Properties
//-------------------------------------------------- //--------------------------------------------------
internal var minWidthDefault = 186.0 class Responder: UIView {
open override var canBecomeFirstResponder: Bool {
true
}
}
internal override var responder: UIResponder? { hiddenView }
internal var isCalendarShowing: Bool = false { didSet { setNeedsUpdate() } }
internal var hiddenView = Responder().with { $0.width(0) }
internal var minWidthDefault = 186.0
internal var bottomStackView: UIStackView = { internal var bottomStackView: UIStackView = {
return UIStackView().with { return UIStackView().with {
$0.translatesAutoresizingMaskIntoConstraints = false $0.translatesAutoresizingMaskIntoConstraints = false
@ -42,6 +64,33 @@ open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopov
} }
}() }()
//--------------------------------------------------
// MARK: - Private Popover/Alert Properties
//--------------------------------------------------
/// View shown inline
internal var popoverOverlayView = UIView().with {
$0.backgroundColor = .clear
$0.translatesAutoresizingMaskIntoConstraints = false
}
/// use this to track touch events outside of the popover in the overlay
internal var popupOverlayTapGesture: AnyCancellable?
/// View shown inline
internal var popoverView: UIView!
/// Size used for the popover
internal var popoverViewSize: CGSize = .zero
/// Spacing between the popover and the ContainerView when not a AlertViewController
internal var popoverSpacing: CGFloat = VDSLayout.space1X
/// Whether or not the popover is visible
internal var popoverVisible = false
/// If the ContainerView exists somewhere in the superview hierarch in a ScrollView.
internal var scrollView: UIScrollView?
/// Original Found ScrollView ContentSize, this will get reset back to this size when the Popover is removed.
internal var scrollViewContentSize: CGSize?
/// Presenting ViewController with showing the AlertViewController Version.
internal var topViewController: UIViewController?
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Public Properties // MARK: - Public Properties
//-------------------------------------------------- //--------------------------------------------------
@ -109,11 +158,20 @@ open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopov
.publisher(for: UITapGestureRecognizer()) .publisher(for: UITapGestureRecognizer())
.sink { [weak self] _ in .sink { [weak self] _ in
guard let self else { return } guard let self else { return }
if self.isEnabled && !self.isReadOnly { if isEnabled && !isReadOnly {
self.togglePicker() showPopover()
} }
} }
.store(in: &subscribers) .store(in: &subscribers)
NotificationCenter.default
.publisher(for: UIDevice.orientationDidChangeNotification).sink { [weak self] _ in
guard let self else { return }
hidePopoverView()
}
.store(in: &subscribers)
popoverOverlayView.isHidden = true
} }
open override func getFieldContainer() -> UIView { open override func getFieldContainer() -> UIView {
@ -125,6 +183,7 @@ open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopov
} }
controlStackView.addArrangedSubview(calendarIcon) controlStackView.addArrangedSubview(calendarIcon)
controlStackView.addArrangedSubview(selectedDateLabel) controlStackView.addArrangedSubview(selectedDateLabel)
controlStackView.addArrangedSubview(hiddenView)
return controlStackView return controlStackView
} }
@ -152,32 +211,253 @@ open class DatePicker: EntryFieldBase, DatePickerViewControllerDelegate, UIPopov
formatter.dateFormat = dateFormat.format formatter.dateFormat = dateFormat.format
selectedDateLabel.text = formatter.string(from: date) selectedDateLabel.text = formatter.string(from: date)
} }
}
internal func togglePicker() { extension DatePicker {
let calendarVC = DatePickerViewController(calendarModel, delegate: self)
calendarVC.modalPresentationStyle = .popover private func showPopover() {
calendarVC.selectedDate = selectedDate ?? Date() guard let viewController = UIApplication.topViewController(), var parentView = viewController.view, !popoverVisible else {
if let popoverController = calendarVC.popoverPresentationController { hidePopoverView()
popoverController.delegate = self return
popoverController.sourceView = containerView
popoverController.sourceRect = containerView.bounds
popoverController.permittedArrowDirections = .up
} }
if let viewController = UIApplication.topViewController() {
viewController.present(calendarVC, animated: true, completion: nil) let calendar = CalendarBase()
calendar.activeDates = calendarModel.activeDates
calendar.hideContainerBorder = calendarModel.hideContainerBorder
calendar.hideCurrentDateIndicator = calendarModel.hideCurrentDateIndicator
calendar.inactiveDates = calendarModel.inactiveDates
calendar.indicators = calendarModel.indicators
calendar.maxDate = calendarModel.maxDate
calendar.minDate = calendarModel.minDate
calendar.surface = surface
calendar.setNeedsLayout()
calendar.layoutIfNeeded()
//size the popover
popoverViewSize = .init(width: calendar.frame.width, height: calendar.frame.height)
//find scrollView
if scrollView == nil {
scrollView = containerView.findSuperview(ofType: UIScrollView.self)
scrollViewContentSize = scrollView?.contentSize
} }
if let scrollView {
parentView = scrollView
}
// see if you should use the popover or show an alert
if let popoverOrigin = calculatePopoverPosition(relativeTo: containerView,
in: parentView,
size: popoverViewSize,
with: popoverSpacing) {
calendar.onChange = { [weak self] control in
guard let self else { return }
selectedDate = control.selectedDate
sendActions(for: .valueChanged)
UIAccessibility.post(notification: .layoutChanged, argument: containerView)
hidePopoverView()
}
// popoverView container
popoverView = UIView()
popoverView.backgroundColor = .clear
popoverView.frame = CGRect(x: popoverOrigin.x, y: popoverOrigin.y, width: calendar.frame.width, height: calendar.frame.height)
popoverView.alpha = 0
popoverView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
popoverVisible = true
popoverView.addSubview(calendar)
calendar.pinToSuperView()
// add views
popoverOverlayView.isHidden = false
popupOverlayTapGesture = popoverOverlayView
.publisher(for: UITapGestureRecognizer())
.sink(receiveValue: { [weak self] gesture in
guard let self else { return }
gestureEventOccured(gesture, parentView: parentView)
})
parentView.addSubview(popoverOverlayView)
popoverOverlayView.pinToSuperView()
parentView.addSubview(popoverView)
parentView.layoutIfNeeded()
// update containerview
_ = responder?.becomeFirstResponder()
updateContainerView(flag: true)
// animate the calendar to show
UIView.animate(withDuration: 0.5,
delay: 0,
usingSpringWithDamping: 0.8,
initialSpringVelocity: 0.2,
options: .curveEaseOut,
animations: { [weak self] in
guard let self else { return }
popoverView.alpha = 1
popoverView.transform = CGAffineTransform.identity
UIAccessibility.post(notification: .layoutChanged, argument: calendar)
parentView.layoutIfNeeded()
})
} else {
let dialog = UIScrollView()
dialog.translatesAutoresizingMaskIntoConstraints = false
dialog.addSubview(calendar)
dialog.backgroundColor = .clear
dialog.contentSize = .init(width: calendar.frame.width + 20, height: calendar.frame.width + 20)
dialog.width(calendar.frame.width + 20)
dialog.height(calendar.frame.height + 20)
calendar.pinToSuperView(.uniform(10))
calendar.onChange = { [weak self] control in
guard let self else { return }
selectedDate = control.selectedDate
sendActions(for: .valueChanged)
UIAccessibility.post(notification: .layoutChanged, argument: containerView)
viewController.dismiss(animated: true)
}
let alert = AlertViewController().with {
$0.dialog = dialog
$0.modalPresentationStyle = .overCurrentContext
$0.modalTransitionStyle = .crossDissolve
}
topViewController = viewController
viewController.present(alert, animated: true){
dialog.flashScrollIndicators()
}
}
isCalendarShowing = true
} }
internal func didSelectDate(_ controller: DatePickerViewController, date: Date) { private func hidePopoverView() {
selectedDate = date if topViewController != nil {
controller.dismiss(animated: true) { [weak self] in topViewController?.dismiss(animated: true)
guard let self else { return } topViewController = nil
self.sendActions(for: .valueChanged) } else {
UIAccessibility.post(notification: .layoutChanged, argument: self.containerView) popoverOverlayView.isHidden = true
popoverOverlayView.removeFromSuperview()
popupOverlayTapGesture?.cancel()
popupOverlayTapGesture = nil
UIView.animate(withDuration: 0.2,
animations: {[weak self] in
guard let self, let popoverView else { return }
popoverView.alpha = 0
popoverView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
if let scrollView, let scrollViewContentSize {
scrollView.contentSize = scrollViewContentSize
}
}) { [weak self] _ in
guard let self, let popoverView else { return }
popoverView.isHidden = true
popoverView.removeFromSuperview()
popoverVisible = false
responder?.resignFirstResponder()
setNeedsUpdate()
UIAccessibility.post(notification: .layoutChanged, argument: containerView)
}
} }
isCalendarShowing = false
} }
public func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle { private func calculatePopoverPosition(relativeTo sourceView: UIView, in parentView: UIView, size: CGSize, with spacing: CGFloat) -> CGPoint? {
return .none let sourceFrameInParent = sourceView.convert(sourceView.bounds, to: parentView)
let parentBounds = parentView.bounds
let safeAreaInsets = parentView.safeAreaInsets
let popoverWidth = size.width
let popoverHeight = size.height
var popoverX: CGFloat = 0
var popoverY: CGFloat = 0
// Calculate horizontal position
if sourceFrameInParent.width <= popoverWidth {
if sourceFrameInParent.midX - popoverWidth / 2 < 0 {
// Align to left
popoverX = sourceFrameInParent.minX
} else if sourceFrameInParent.midX + popoverWidth / 2 > parentBounds.width {
// Align to right
popoverX = sourceFrameInParent.maxX - popoverWidth
} else {
// Center on source view
popoverX = sourceFrameInParent.midX - popoverWidth / 2
}
} else {
popoverX = sourceFrameInParent.minX //sourceFrameInParent.midX - popoverWidth / 2
}
// Ensure the popover is within the parent's bounds horizontally
popoverX = max(0, min(popoverX, parentBounds.width - popoverWidth))
var availableSpaceAbove: CGFloat = 0.0
var availableSpaceBelow: CGFloat = 0.0
/// if the scrollView is set we want to change how we calculate the containerView's position
if let scrollView = parentView as? UIScrollView {
// Calculate vertical position and height
availableSpaceAbove = sourceFrameInParent.minY - scrollView.bounds.minY - spacing
availableSpaceBelow = scrollView.bounds.maxY - sourceFrameInParent.maxY - spacing
if availableSpaceAbove > availableSpaceBelow {
// Show above
popoverY = sourceFrameInParent.minY - popoverHeight - spacing
} else {
// Show below
popoverY = sourceFrameInParent.maxY + spacing
// See if we need to expand the contentSize of the ScrollView
let diff = scrollView.contentSize.height - sourceFrameInParent.maxY
if diff < popoverHeight {
scrollView.contentSize.height += popoverHeight - diff + VDSLayout.space4X
}
}
} else {
// Calculate vertical position and height
availableSpaceAbove = sourceFrameInParent.minY - safeAreaInsets.top - spacing
availableSpaceBelow = parentBounds.height - sourceFrameInParent.maxY - safeAreaInsets.bottom - spacing
if availableSpaceAbove >= popoverHeight {
// Show above
popoverY = sourceFrameInParent.minY - popoverHeight - spacing
} else if availableSpaceBelow >= popoverHeight {
// Show below
popoverY = sourceFrameInParent.maxY + spacing
} else {
//return nil since there is no way we can show the popover without a scrollview
return nil
}
}
return .init(x: popoverX, y: popoverY)
}
private func gestureEventOccured(_ gesture: UIGestureRecognizer, parentView: UIView) {
guard let popoverView, popoverVisible else { return }
let location = gesture.location(in: parentView)
if !popoverView.frame.contains(location) {
hidePopoverView()
}
}
}
extension UIView {
public func findSuperview<T: UIView>(ofType type: T.Type) -> T? {
var currentView: UIView? = self
while let view = currentView {
if let superview = view.superview as? T {
return superview
}
currentView = view.superview
}
return nil
} }
} }

View File

@ -10,8 +10,6 @@ import UIKit
extension DatePicker { extension DatePicker {
public struct CalendarModel { public struct CalendarModel {
public let surface: Surface
/// If set to true, the calendar will not have a border. /// If set to true, the calendar will not have a border.
public let hideContainerBorder: Bool public let hideContainerBorder: Bool
@ -35,15 +33,13 @@ extension DatePicker {
/// Array of ``CalendarIndicatorModel`` you are wanting to show on legend. /// Array of ``CalendarIndicatorModel`` you are wanting to show on legend.
public let indicators: [CalendarBase.CalendarIndicatorModel] public let indicators: [CalendarBase.CalendarIndicatorModel]
public init(surface: Surface = .light, public init(hideContainerBorder: Bool = false,
hideContainerBorder: Bool = false,
hideCurrentDateIndicator: Bool = false, hideCurrentDateIndicator: Bool = false,
activeDates: [Date] = [], activeDates: [Date] = [],
inactiveDates: [Date] = [], inactiveDates: [Date] = [],
minDate: Date = Date().startOfMonth, minDate: Date = Date().startOfMonth,
maxDate: Date = Date().endOfMonth, maxDate: Date = Date().endOfMonth,
indicators: [CalendarBase.CalendarIndicatorModel] = []) { indicators: [CalendarBase.CalendarIndicatorModel] = []) {
self.surface = surface
self.hideContainerBorder = hideContainerBorder self.hideContainerBorder = hideContainerBorder
self.hideCurrentDateIndicator = hideCurrentDateIndicator self.hideCurrentDateIndicator = hideCurrentDateIndicator
self.activeDates = activeDates self.activeDates = activeDates

View File

@ -1,71 +0,0 @@
//
// DatePickerPopoverViewController.swift
// VDS
//
// Created by Matt Bruce on 5/14/24.
//
import Foundation
import UIKit
protocol DatePickerViewControllerDelegate: NSObject {
func didSelectDate(_ controller: DatePicker.DatePickerViewController, date: Date)
}
extension DatePicker {
class DatePickerViewController: UIViewController {
private var padding: CGFloat = 15
private var topPadding: CGFloat { 10 + padding }
private var calendarModel: CalendarModel
private let picker = CalendarBase()
weak var delegate: DatePickerViewControllerDelegate?
init(_ calendarModel: CalendarModel, delegate: DatePickerViewControllerDelegate?) {
self.delegate = delegate
self.calendarModel = calendarModel
super.init(nibName: nil, bundle: nil)
self.picker.onChange = { [weak self] control in
guard let self else { return }
self.delegate?.didSelectDate(self, date: control.selectedDate)
}
}
var selectedDate: Date = Date() {
didSet {
picker.selectedDate = selectedDate
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(picker)
picker.surface = calendarModel.surface
picker.hideContainerBorder = calendarModel.hideContainerBorder
picker.hideCurrentDateIndicator = calendarModel.hideCurrentDateIndicator
picker.indicators = calendarModel.indicators
picker.activeDates = calendarModel.activeDates
picker.inactiveDates = calendarModel.inactiveDates
picker.selectedDate = selectedDate
picker.minDate = calendarModel.minDate
picker.maxDate = calendarModel.maxDate
picker.pinToSuperView(.init(top: topPadding, left: padding, bottom: padding, right: padding))
view.backgroundColor = picker.backgroundColor
}
override var preferredContentSize: CGSize {
get {
var size = picker.frame.size
size.height += 40
size.width += 30
return size
}
set {
super.preferredContentSize = newValue
}
}
}
}

View File

@ -11,6 +11,7 @@ import VDSCoreTokens
import Combine import Combine
/// A dropdown select is an expandable menu of predefined options that allows a customer to make a single selection. /// A dropdown select is an expandable menu of predefined options that allows a customer to make a single selection.
@objcMembers
@objc(VDSDropdownSelect) @objc(VDSDropdownSelect)
open class DropdownSelect: EntryFieldBase { open class DropdownSelect: EntryFieldBase {
//-------------------------------------------------- //--------------------------------------------------
@ -31,18 +32,6 @@ open class DropdownSelect: EntryFieldBase {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Public Properties // MARK: - Public Properties
//-------------------------------------------------- //--------------------------------------------------
/// Override UIControl state to add the .error state if showSuccess is true and if showError is true.
open override var state: UIControl.State {
get {
var state = super.state
if dropdownField.isFirstResponder {
state.insert(.focused)
}
return state
}
}
/// If true, the label will be displayed inside the dropdown containerView. Otherwise, the label will be above the dropdown containerView like a normal text input. /// If true, the label will be displayed inside the dropdown containerView. Otherwise, the label will be above the dropdown containerView like a normal text input.
open var showInlineLabel: Bool = false { didSet { setNeedsUpdate() }} open var showInlineLabel: Bool = false { didSet { setNeedsUpdate() }}

View File

@ -13,6 +13,7 @@ import Combine
/// An icon is a graphical element that conveys information at a glance. It helps orient /// An icon is a graphical element that conveys information at a glance. It helps orient
/// a customer, explain functionality and draw attention to interactive elements. Icons /// a customer, explain functionality and draw attention to interactive elements. Icons
/// should have a functional purpose and should never be used for decoration. /// should have a functional purpose and should never be used for decoration.
@objcMembers
@objc(VDSIcon) @objc(VDSIcon)
open class Icon: View { open class Icon: View {
@ -93,13 +94,16 @@ open class Icon: View {
backgroundColor = .clear backgroundColor = .clear
isAccessibilityElement = true isAccessibilityElement = true
accessibilityTraits = .image accessibilityTraits = .none
accessibilityHint = "image"
bridge_accessibilityLabelBlock = { [weak self] in bridge_accessibilityLabelBlock = { [weak self] in
guard let self else { return "" } guard let self else { return "" }
return name?.rawValue ?? "icon" return name?.rawValue ?? "icon"
} }
} }
/// Used to make changes to the View based off a change events or from local properties. /// Used to make changes to the View based off a change events or from local properties.

View File

@ -360,7 +360,7 @@ open class InputStepper: EntryFieldBase {
} }
// Set control width to input stepper. // Set control width to input stepper.
internal func setControlWidth(_ text: String?) { private func setControlWidth(_ text: String?) {
if let text, text == "auto" { if let text, text == "auto" {
stepperWidthConstraint?.deactivate() stepperWidthConstraint?.deactivate()
} else if let controlWidth = Int(text ?? "") { } else if let controlWidth = Int(text ?? "") {
@ -371,7 +371,7 @@ open class InputStepper: EntryFieldBase {
} }
// Handling the controlwidth without going beyond the width of the parent container. // Handling the controlwidth without going beyond the width of the parent container.
internal func updateStepperContainerWidth(controlWidth: CGFloat, width: CGFloat) { private func updateStepperContainerWidth(controlWidth: CGFloat, width: CGFloat) {
if controlWidth >= containerSize.width && controlWidth <= width { if controlWidth >= containerSize.width && controlWidth <= width {
stepperWidthConstraint?.deactivate() stepperWidthConstraint?.deactivate()
stepperWidthConstraint?.constant = controlWidth stepperWidthConstraint?.constant = controlWidth

View File

@ -12,6 +12,7 @@ import Combine
/// Label is a standard view used to draw text with applying Typography through ``TextStyle`` as well /// Label is a standard view used to draw text with applying Typography through ``TextStyle`` as well
/// as other attributes using any implemetation of ``LabelAttributeModel``. /// as other attributes using any implemetation of ``LabelAttributeModel``.
@objcMembers
@objc(VDSLabel) @objc(VDSLabel)
open class Label: UILabel, ViewProtocol, UserInfoable { open class Label: UILabel, ViewProtocol, UserInfoable {
@ -214,10 +215,6 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
} }
open func setup() { open func setup() {
bridge_accessibilityLabelBlock = { [weak self] in
guard let self else { return "" }
return text
}
} }
open func reset() { open func reset() {
@ -389,60 +386,100 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
} }
} }
//--------------------------------------------------
// MARK: - Touch Events
//--------------------------------------------------
@objc private func textLinkTapped(_ gesture: UITapGestureRecognizer) { @objc private func textLinkTapped(_ gesture: UITapGestureRecognizer) {
for actionable in actions { let location = gesture.location(in: self)
// This determines if we tapped on the desired range of text. if let action = actions.first(where: { isAction(for: location, inRange: $0.range) }) {
let location = gesture.location(in: self) action.performAction()
if didTapActionInLabel(location, inRange: actionable.range) {
actionable.performAction()
return
}
} }
} }
public func isAction(for location: CGPoint) -> Bool { public func isAction(for location: CGPoint) -> Bool {
for actionable in actions { actions.contains(where: {isAction(for: location, inRange: $0.range)})
if didTapActionInLabel(location, inRange: actionable.range) { }
return true
public func isAction(for location: CGPoint, inRange targetRange: NSRange) -> Bool {
guard let abstractContainer = abstractTextContainer() else { return false }
let textContainer = abstractContainer.textContainer
let layoutManager = abstractContainer.layoutManager
let indexOfGlyph = layoutManager.glyphIndex(for: location, in: textContainer)
let intrinsicWidth = intrinsicContentSize.width
// Assert that tapped occured within acceptable bounds based on alignment.
switch textAlignment {
case .right:
if location.x < bounds.width - intrinsicWidth {
return false
}
case .center:
let halfBounds = bounds.width / 2
let halfIntrinsicWidth = intrinsicWidth / 2
if location.x > halfBounds + halfIntrinsicWidth {
return false
} else if location.x < halfBounds - halfIntrinsicWidth {
return false
}
default: // Left align
if location.x > intrinsicWidth {
return false
} }
} }
return false
// Affirms that the tap occured in the desired rect of provided by the target range.
return layoutManager.boundingRect(forGlyphRange: targetRange, in: textContainer).contains(location)
&& NSLocationInRange(indexOfGlyph, targetRange)
} }
private func didTapActionInLabel(_ location: CGPoint, inRange targetRange: NSRange) -> Bool { /**
Provides a text container and layout manager of how the text would appear on screen.
They are used in tandem to derive low-level TextKit results of the label.
*/
public func abstractTextContainer() -> (textContainer: NSTextContainer, layoutManager: NSLayoutManager, textStorage: NSTextStorage)? {
guard let attributedText else { return false } // Must configure the attributed string to translate what would appear on screen to accurately analyze.
guard let attributedText = attributedText else { return nil }
let paragraph = NSMutableParagraphStyle()
paragraph.alignment = textAlignment
let stagedAttributedString = NSMutableAttributedString(attributedString: attributedText)
stagedAttributedString.addAttributes([NSAttributedString.Key.paragraphStyle: paragraph], range: NSRange(location: 0, length: attributedText.string.count))
let textStorage = NSTextStorage(attributedString: stagedAttributedString)
let layoutManager = NSLayoutManager() let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: bounds.size) let textContainer = NSTextContainer(size: .zero)
let textStorage = NSTextStorage(attributedString: attributedText)
layoutManager.addTextContainer(textContainer) layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager) textStorage.addLayoutManager(layoutManager)
let characterIndex = layoutManager.characterIndex(for: location, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil) textContainer.lineFragmentPadding = 0.0
textContainer.lineBreakMode = lineBreakMode
textContainer.maximumNumberOfLines = numberOfLines
textContainer.size = bounds.size
guard let _ = attributedText.attribute(NSAttributedString.Key.action, at: characterIndex, effectiveRange: nil) as? String, characterIndex < attributedText.length else { return false } return (textContainer, layoutManager, textStorage)
return true
} }
//--------------------------------------------------
// MARK: - Accessibility
//--------------------------------------------------
private func customAccessibilityElement(text: String?, range: NSRange, accessibleText: String? = nil) -> AccessibilityActionElement? { private func customAccessibilityElement(text: String?, range: NSRange, accessibleText: String? = nil) -> AccessibilityActionElement? {
guard let text = text, let attributedText else { return nil } guard let text = text, let abstractContainer = abstractTextContainer() else { return nil }
let textContainer = abstractContainer.textContainer
let layoutManager = abstractContainer.layoutManager
let actionText = accessibleText ?? (text.isValid(range: range) ? NSString(string:text).substring(with: range) : text) let actionText = accessibleText ?? (text.isValid(range: range) ? NSString(string:text).substring(with: range) : text)
// Calculate the frame of the substring
let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: bounds.size)
let textStorage = NSTextStorage(attributedString: attributedText)
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
var glyphRange = NSRange() var glyphRange = NSRange()
// Convert the range for the substring into a range of glyphs // Convert the range for the substring into a range of glyphs
layoutManager.characterRange(forGlyphRange: range, actualGlyphRange: &glyphRange) layoutManager.characterRange(forGlyphRange: range, actualGlyphRange: &glyphRange)
let substringBounds = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer) let substringBounds = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
// Create custom accessibility element // Create custom accessibility element
@ -456,13 +493,8 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
return element return element
} }
//--------------------------------------------------
// MARK: - Accessibility
//--------------------------------------------------
open var accessibilityAction: ((Label) -> Void)? open var accessibilityAction: ((Label) -> Void)?
private var _isAccessibilityElement: Bool = false
open override var isAccessibilityElement: Bool { open override var isAccessibilityElement: Bool {
get { get {
var block: AXBoolReturnBlock? var block: AXBoolReturnBlock?
@ -478,15 +510,14 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
if let block { if let block {
return block() return block()
} else { } else {
return _isAccessibilityElement return super.isAccessibilityElement
} }
} }
set { set {
_isAccessibilityElement = newValue super.isAccessibilityElement = newValue
} }
} }
private var _accessibilityLabel: String?
open override var accessibilityLabel: String? { open override var accessibilityLabel: String? {
get { get {
var block: AXStringReturnBlock? var block: AXStringReturnBlock?
@ -501,15 +532,14 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
if let block { if let block {
return block() return block()
} else { } else {
return _accessibilityLabel return super.accessibilityLabel
} }
} }
set { set {
_accessibilityLabel = newValue super.accessibilityLabel = newValue
} }
} }
private var _accessibilityHint: String?
open override var accessibilityHint: String? { open override var accessibilityHint: String? {
get { get {
var block: AXStringReturnBlock? var block: AXStringReturnBlock?
@ -524,15 +554,14 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
if let block { if let block {
return block() return block()
} else { } else {
return _accessibilityHint return super.accessibilityHint
} }
} }
set { set {
_accessibilityHint = newValue super.accessibilityHint = newValue
} }
} }
private var _accessibilityValue: String?
open override var accessibilityValue: String? { open override var accessibilityValue: String? {
get { get {
var block: AXStringReturnBlock? var block: AXStringReturnBlock?
@ -548,11 +577,11 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
if let block{ if let block{
return block() return block()
} else { } else {
return _accessibilityValue return super.accessibilityValue
} }
} }
set { set {
_accessibilityValue = newValue super.accessibilityValue = newValue
} }
} }

View File

@ -10,6 +10,7 @@ import UIKit
import VDSCoreTokens import VDSCoreTokens
/// A line visually separates content sections or elements in lists, tables and layouts to indicate content hierarchy. /// A line visually separates content sections or elements in lists, tables and layouts to indicate content hierarchy.
@objcMembers
@objc(VDSLine) @objc(VDSLine)
open class Line: View { open class Line: View {

View File

@ -11,6 +11,7 @@ import VDSCoreTokens
/// A loader is an indicator that uses animation to show customers that there is an indefinite amount of wait time while a task is ongoing, e.g. a page is loading, a form is being submitted. The component disappears when the task is complete. /// A loader is an indicator that uses animation to show customers that there is an indefinite amount of wait time while a task is ongoing, e.g. a page is loading, a form is being submitted. The component disappears when the task is complete.
@objcMembers
@objc(VDSLoader) @objc(VDSLoader)
open class Loader: View { open class Loader: View {

View File

@ -10,6 +10,8 @@ import UIKit
import VDSCoreTokens import VDSCoreTokens
/// ViewController to show the Loader, this will be presented using the LoaderLaunchable Protocl. /// ViewController to show the Loader, this will be presented using the LoaderLaunchable Protocl.
@objcMembers
@objc(VDSLoaderViewController)
open class LoaderViewController: UIViewController, Surfaceable { open class LoaderViewController: UIViewController, Surfaceable {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Private Properties // MARK: - Private Properties

View File

@ -14,6 +14,7 @@ import Combine
/// in context. There are four types: information, success, warning and error; each /// in context. There are four types: information, success, warning and error; each
/// with different color and content. They may be screen-specific, flow-specific or /// with different color and content. They may be screen-specific, flow-specific or
/// experience-wide. /// experience-wide.
@objcMembers
@objc(VDSNotification) @objc(VDSNotification)
open class Notification: View { open class Notification: View {

View File

@ -11,6 +11,7 @@ import VDSCoreTokens
import Combine 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. ///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.
@objcMembers
@objc(VDSPagination) @objc(VDSPagination)
open class Pagination: View { open class Pagination: View {

View File

@ -9,6 +9,7 @@ import UIKit
import VDSCoreTokens import VDSCoreTokens
///This is customised button for Pagination view ///This is customised button for Pagination view
@objcMembers
@objc(PaginationButton) @objc(PaginationButton)
open class PaginationButton: ButtonBase { open class PaginationButton: ButtonBase {
//-------------------------------------------------- //--------------------------------------------------

View File

@ -11,6 +11,7 @@ import UIKit
/// Radio boxes are single-select components through which a customer indicates a choice. /// Radio boxes are single-select components through which a customer indicates a choice.
/// They're stylized ``RadioButtons`` that must always be paired with one or more ``RadioBoxItem`` /// They're stylized ``RadioButtons`` that must always be paired with one or more ``RadioBoxItem``
/// in a radio box group. Use radio boxes to display choices like device storage. /// in a radio box group. Use radio boxes to display choices like device storage.
@objcMembers
@objc(VDSRadioBoxGroup) @objc(VDSRadioBoxGroup)
open class RadioBoxGroup: SelectorGroupBase<RadioBoxItem>, SelectorGroupSingleSelect { open class RadioBoxGroup: SelectorGroupBase<RadioBoxItem>, SelectorGroupSingleSelect {

View File

@ -12,6 +12,7 @@ import VDSCoreTokens
/// Radio boxes are single-select components through which a customer indicates a choice /// Radio boxes are single-select components through which a customer indicates a choice
/// that are used within a ``RadioBoxGroup``. /// that are used within a ``RadioBoxGroup``.
@objcMembers
@objc(VDSRadioBoxItem) @objc(VDSRadioBoxItem)
open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable { open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable {
@ -214,7 +215,7 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable {
selectorView.isAccessibilityElement = true selectorView.isAccessibilityElement = true
selectorView.accessibilityTraits = .button selectorView.accessibilityTraits = .button
addSubview(selectorView) addSubview(selectorView)
selectorView.isUserInteractionEnabled = true selectorView.isUserInteractionEnabled = false
selectorView.addSubview(selectorStackView) selectorView.addSubview(selectorStackView)

View File

@ -13,6 +13,7 @@ import VDSCoreTokens
/// Radio buttons are single-select components through which a customer indicates a choice. /// Radio buttons are single-select components through which a customer indicates a choice.
/// They must always be paired with one or more ``RadioButtonItem`` within a ``RadioButtonGroup``. /// They must always be paired with one or more ``RadioButtonItem`` within a ``RadioButtonGroup``.
/// Use radio buttons to display choices like delivery method. /// Use radio buttons to display choices like delivery method.
@objcMembers
@objc(VDSRadioButton) @objc(VDSRadioButton)
open class RadioButton: SelectorBase { open class RadioButton: SelectorBase {

View File

@ -11,6 +11,7 @@ import UIKit
/// Radio buttons items are single-select components through which a customer indicates a choice. /// Radio buttons items are single-select components through which a customer indicates a choice.
/// They must always be paired with one or more other ``RadioButtonItem`` within a radio button group. /// They must always be paired with one or more other ``RadioButtonItem`` within a radio button group.
/// Use radio buttons to display choices like delivery method. /// Use radio buttons to display choices like delivery method.
@objcMembers
@objc(VDSRadioButtonGroup) @objc(VDSRadioButtonGroup)
open class RadioButtonGroup: SelectorGroupBase<RadioButtonItem>, SelectorGroupSingleSelect { open class RadioButtonGroup: SelectorGroupBase<RadioButtonItem>, SelectorGroupSingleSelect {

View File

@ -11,6 +11,7 @@ import UIKit
/// Radio buttons items are single-select components through which a customer indicates a choice. /// Radio buttons items are single-select components through which a customer indicates a choice.
/// They must always be paired with one or more other radio button items within a ``RadioButtonGroup``. /// They must always be paired with one or more other radio button items within a ``RadioButtonGroup``.
/// Use radio buttons to display choices like delivery method. /// Use radio buttons to display choices like delivery method.
@objcMembers
@objc(VDSRadioButtonItem) @objc(VDSRadioButtonItem)
open class RadioButtonItem: SelectorItemBase<RadioButton> { open class RadioButtonItem: SelectorItemBase<RadioButton> {

View File

@ -10,6 +10,7 @@ import UIKit
import VDSCoreTokens import VDSCoreTokens
///Table is view composed of rows and columns, which takes any view into each cell and resizes based on the highest cell height. ///Table is view composed of rows and columns, which takes any view into each cell and resizes based on the highest cell height.
@objcMembers
@objc(VDSTable) @objc(VDSTable)
open class Table: View { open class Table: View {
@ -51,10 +52,8 @@ open class Table: View {
func horizontalValue() -> CGFloat { func horizontalValue() -> CGFloat {
switch self { switch self {
case .standard: case .standard, .compact:
return UIDevice.isIPad ? VDSLayout.space8X : VDSLayout.space6X return UIDevice.isIPad ? VDSLayout.space4X : VDSLayout.space3X
case .compact:
return UIDevice.isIPad ? VDSLayout.space8X : VDSLayout.space6X
} }
} }
@ -147,7 +146,10 @@ extension Table: UICollectionViewDelegate, UICollectionViewDataSource, TableColl
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TableCellItem.Identifier, for: indexPath) as? TableCellItem else { return UICollectionViewCell() } guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TableCellItem.Identifier, for: indexPath) as? TableCellItem else { return UICollectionViewCell() }
let currentItem = tableData[indexPath.section].columns[indexPath.row] let currentItem = tableData[indexPath.section].columns[indexPath.row]
let shouldStrip = striped ? (indexPath.section % 2 != 0) : false let shouldStrip = striped ? (indexPath.section % 2 != 0) : false
cell.updateCell(content: currentItem, surface: surface, striped: shouldStrip, padding: padding) let isHeader = tableData[indexPath.section].isHeader
var edgePadding = UIEdgeInsets(top: padding.verticalValue(), left: 0, bottom: padding.verticalValue(), right: padding.horizontalValue())
edgePadding.left = (indexPath.row == 0 && !striped) ? VDSLayout.space1X : padding.horizontalValue()
cell.updateCell(content: currentItem, surface: surface, striped: shouldStrip, padding: edgePadding, isHeader: isHeader)
return cell return cell
} }

View File

@ -30,9 +30,6 @@ final class TableCellItem: UICollectionViewCell {
/// Color configuration for striped background color /// Color configuration for striped background color
private let stripedColorConfiguration = SurfaceColorConfiguration(VDSColor.backgroundSecondaryLight, VDSColor.backgroundSecondaryDark) private let stripedColorConfiguration = SurfaceColorConfiguration(VDSColor.backgroundSecondaryLight, VDSColor.backgroundSecondaryDark)
/// Padding parameter to maintain the edge spacing of the containerView
private var padding: Table.Padding = .standard
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Initializers // MARK: - Initializers
//-------------------------------------------------- //--------------------------------------------------
@ -58,10 +55,10 @@ final class TableCellItem: UICollectionViewCell {
//-------------------------------------------------- //--------------------------------------------------
/// Updates the cell content with ``TableItemModel`` and styling/padding attributes from other parameters /// Updates the cell content with ``TableItemModel`` and styling/padding attributes from other parameters
public func updateCell(content: TableItemModel, surface: Surface, striped: Bool = false, padding: Table.Padding = .standard) { public func updateCell(content: TableItemModel, surface: Surface, striped: Bool = false, padding: UIEdgeInsets, isHeader: Bool = false) {
containerView.subviews.forEach({ $0.removeFromSuperview() }) containerView.subviews.forEach({ $0.removeFromSuperview() })
self.padding = padding
containerView.surface = surface containerView.surface = surface
containerView.backgroundColor = striped ? stripedColorConfiguration.getColor(surface) : backgroundColorConfiguration.getColor(surface) containerView.backgroundColor = striped ? stripedColorConfiguration.getColor(surface) : backgroundColorConfiguration.getColor(surface)
@ -82,11 +79,11 @@ final class TableCellItem: UICollectionViewCell {
} }
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
component.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: VDSLayout.space1X), component.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: padding.left),
component.topAnchor.constraint(greaterThanOrEqualTo: containerView.topAnchor, constant: padding.verticalValue()), containerView.trailingAnchor.constraint(greaterThanOrEqualTo: component.trailingAnchor, constant: padding.right)
containerView.bottomAnchor.constraint(greaterThanOrEqualTo: component.bottomAnchor, constant: padding.verticalValue()),
containerView.trailingAnchor.constraint(greaterThanOrEqualTo: component.trailingAnchor, constant: padding.horizontalValue()),
containerView.centerYAnchor.constraint(equalTo: component.centerYAnchor)
]) ])
component.topAnchor.constraint(equalTo: containerView.topAnchor, constant: padding.top).isActive = !isHeader
containerView.bottomAnchor.constraint(equalTo: component.bottomAnchor, constant: padding.bottom).isActive = isHeader
} }
} }

View File

@ -40,6 +40,9 @@ class MatrixFlowLayout : UICollectionViewFlowLayout {
///padding type to be set from Table component, which is used to calculate the size & position of the cell. ///padding type to be set from Table component, which is used to calculate the size & position of the cell.
var layoutPadding: Table.Padding = .standard var layoutPadding: Table.Padding = .standard
///Striped status of Table, based on this status padding of leading attribute changes.
var striped: Bool = false
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Overrides // MARK: - Overrides
//-------------------------------------------------- //--------------------------------------------------
@ -77,7 +80,7 @@ class MatrixFlowLayout : UICollectionViewFlowLayout {
let selectedItem = delegate.collectionView(collectionView, dataForItemAt: indexPath) let selectedItem = delegate.collectionView(collectionView, dataForItemAt: indexPath)
///Calculate the estimated height of the cell ///Calculate the estimated height of the cell
let itemHeight = estimateHeightFor(item: selectedItem, with: itemWidth) let itemHeight = estimateHeightFor(item: selectedItem, with: itemWidth, index: indexPath)
layoutWidth += itemWidth layoutWidth += itemWidth
@ -108,8 +111,8 @@ class MatrixFlowLayout : UICollectionViewFlowLayout {
} }
/// Fetches estimated height by calling the cell's component estimated height and adding padding /// Fetches estimated height by calling the cell's component estimated height and adding padding
private func estimateHeightFor(item: TableItemModel, with width: CGFloat) -> CGFloat { private func estimateHeightFor(item: TableItemModel, with width: CGFloat, index: IndexPath) -> CGFloat {
let horizontalPadding = (index.row == 0 && !striped) ? (VDSLayout.space1X + layoutPadding.horizontalValue()) : (2 * layoutPadding.horizontalValue())
let itemWidth = width - layoutPadding.horizontalValue() - defaultLeadingPadding let itemWidth = width - layoutPadding.horizontalValue() - defaultLeadingPadding
let maxSize = CGSize(width: itemWidth, height: CGFloat.greatestFiniteMagnitude) let maxSize = CGSize(width: itemWidth, height: CGFloat.greatestFiniteMagnitude)
let estItemSize = item.component?.systemLayoutSizeFitting(maxSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel) ?? CGSize(width: itemWidth, height: item.defaultHeight) let estItemSize = item.component?.systemLayoutSizeFitting(maxSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel) ?? CGSize(width: itemWidth, height: item.defaultHeight)

View File

@ -11,11 +11,14 @@ public struct TableRowModel {
public var columns: [TableItemModel] public var columns: [TableItemModel]
public var isHeader: Bool = false
public var columnsCount: Int { public var columnsCount: Int {
return columns.count return columns.count
} }
public init(columns: [TableItemModel]) { public init(columns: [TableItemModel], isHeader: Bool = false) {
self.columns = columns self.columns = columns
self.isHeader = isHeader
} }
} }

View File

@ -11,7 +11,7 @@ import VDSCoreTokens
import Combine import Combine
extension Tabs { extension Tabs {
@objcMembers
@objc(VDSTab) @objc(VDSTab)
open class Tab: Control, Groupable { open class Tab: Control, Groupable {

View File

@ -10,6 +10,7 @@ import UIKit
import VDSCoreTokens import VDSCoreTokens
/// Tabs are organizational components that group content and allow customers to navigate its display. Use them to separate content when the content is related but doesnt need to be compared. /// Tabs are organizational components that group content and allow customers to navigate its display. Use them to separate content when the content is related but doesnt need to be compared.
@objcMembers
@objc(VDSTabs) @objc(VDSTabs)
open class Tabs: View { open class Tabs: View {

View File

@ -8,6 +8,7 @@
import Foundation import Foundation
import UIKit import UIKit
@objcMembers
@objc(VDSTabsContainer) @objc(VDSTabsContainer)
open class TabsContainer: View { open class TabsContainer: View {

View File

@ -11,6 +11,7 @@ import VDSCoreTokens
import Combine import Combine
/// Base Class used to build out a Input controls. /// Base Class used to build out a Input controls.
@objcMembers
@objc(VDSEntryField) @objc(VDSEntryField)
open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
@ -92,12 +93,6 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
} }
}() }()
/// This is the view that will be wrapped with the border for userInteraction.
/// The only subview of this view is the fieldStackView
internal var containerView = View().with {
$0.isAccessibilityElement = true
}
/// This is set by a local method. /// This is set by a local method.
internal var bottomContainerView: UIView! internal var bottomContainerView: UIView!
@ -143,8 +138,8 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
internal var borderColorConfiguration = ControlColorConfiguration().with { internal var borderColorConfiguration = ControlColorConfiguration().with {
$0.setSurfaceColors(VDSFormControlsColor.borderOnlight, VDSFormControlsColor.borderOndark, forState: .normal) $0.setSurfaceColors(VDSFormControlsColor.borderOnlight, VDSFormControlsColor.borderOndark, forState: .normal)
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOnlight, forState: .focused) $0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .focused)
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOnlight, forState: [.focused, .error]) $0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: [.focused, .error])
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled) $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled)
$0.setSurfaceColors(VDSColor.feedbackErrorOnlight, VDSColor.feedbackErrorOndark, forState: .error) $0.setSurfaceColors(VDSColor.feedbackErrorOnlight, VDSColor.feedbackErrorOndark, forState: .error)
$0.setSurfaceColors(VDSFormControlsColor.borderReadonlyOnlight, VDSFormControlsColor.borderReadonlyOndark, forState: .readonly) $0.setSurfaceColors(VDSFormControlsColor.borderReadonlyOnlight, VDSFormControlsColor.borderReadonlyOndark, forState: .readonly)
@ -163,6 +158,12 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Public Properties // MARK: - Public Properties
//-------------------------------------------------- //--------------------------------------------------
/// This is the view that will be wrapped with the border for userInteraction.
/// The only subview of this view is the fieldStackView
open var containerView = View().with {
$0.isAccessibilityElement = true
}
open var onChangeSubscriber: AnyCancellable? open var onChangeSubscriber: AnyCancellable?
open var titleLabel = Label().with { open var titleLabel = Label().with {
@ -183,9 +184,11 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
open var statusIcon: Icon = Icon().with { open var statusIcon: Icon = Icon().with {
$0.size = .medium $0.size = .medium
$0.isAccessibilityElement = false $0.isAccessibilityElement = true
} }
open var useRequiredRule: Bool = true { didSet { setNeedsUpdate() } }
open var labelText: String? { didSet { setNeedsUpdate() } } open var labelText: String? { didSet { setNeedsUpdate() } }
open var helperText: String? { didSet { setNeedsUpdate() } } open var helperText: String? { didSet { setNeedsUpdate() } }
@ -207,6 +210,9 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
if isReadOnly { if isReadOnly {
state.insert(.readonly) state.insert(.readonly)
} }
if let responder, responder.isFirstResponder {
state.insert(.focused)
}
} }
return state return state
} }
@ -327,8 +333,6 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
accessibilityLabels.append("error, \(errorText)") accessibilityLabels.append("error, \(errorText)")
} }
accessibilityLabels.append("\(Self.self)")
return accessibilityLabels.joined(separator: ", ") return accessibilityLabels.joined(separator: ", ")
} }
@ -341,11 +345,17 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
guard let self else { return "" } guard let self else { return "" }
return value return value
} }
statusIcon.bridge_accessibilityLabelBlock = { [weak self] in
guard let self else { return "" }
return showError || hasInternalError ? "error" : nil
}
} }
/// Updates the UI /// Updates the UI
open override func updateView() { open override func updateView() {
super.updateView() super.updateView()
updateRules()
updateContainerView(flag: true) updateContainerView(flag: true)
updateContainerWidth() updateContainerWidth()
updateTitleLabel() updateTitleLabel()
@ -408,7 +418,6 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
} }
open func validate(){ open func validate(){
updateRules()
validator = FormFieldValidator<EntryFieldBase>(field: self, rules: rules) validator = FormFieldValidator<EntryFieldBase>(field: self, rules: rules)
validator?.validate() validator?.validate()
setNeedsUpdate() setNeedsUpdate()
@ -445,27 +454,34 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
} }
open func updateErrorLabel(){ open func updateErrorLabel(){
if showError, let errorText {
errorLabel.text = errorText /// always show the errorIcon if there is an error
errorLabel.surface = surface if showError || hasInternalError {
errorLabel.isEnabled = isEnabled
errorLabel.isHidden = false
statusIcon.name = .error
statusIcon.surface = surface
statusIcon.isHidden = !isEnabled || state.contains(.focused)
} else if hasInternalError, let internalErrorText {
errorLabel.text = internalErrorText
errorLabel.surface = surface
errorLabel.isEnabled = isEnabled
errorLabel.isHidden = false
statusIcon.name = .error statusIcon.name = .error
statusIcon.surface = surface statusIcon.surface = surface
statusIcon.isHidden = !isEnabled || state.contains(.focused) statusIcon.isHidden = !isEnabled || state.contains(.focused)
} else { } else {
statusIcon.isHidden = true statusIcon.isHidden = true
errorLabel.isHidden = true
} }
statusIcon.color = iconColorConfiguration.getColor(self) statusIcon.color = iconColorConfiguration.getColor(self)
// only show errorLabel if there is a message
var message: String?
if showError, let errorText {
message = errorText
} else if hasInternalError, let internalErrorText {
message = internalErrorText
}
if let message {
errorLabel.text = message
errorLabel.surface = surface
errorLabel.isEnabled = isEnabled
errorLabel.isHidden = false
} else {
errorLabel.isHidden = true
}
} }
open func updateHelperLabel(){ open func updateHelperLabel(){
@ -507,7 +523,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
//-------------------------------------------------- //--------------------------------------------------
internal func updateRules() { internal func updateRules() {
rules.removeAll() rules.removeAll()
if self.isRequired { if isRequired && useRequiredRule {
let rule = RequiredRule() let rule = RequiredRule()
if let errorText, !errorText.isEmpty { if let errorText, !errorText.isEmpty {
rule.errorMessage = errorText rule.errorMessage = errorText

View File

@ -51,6 +51,14 @@ extension InputField {
} }
} }
public var accessibilityLabel: String {
switch self {
case .generic, .placeholder: return "credit card"
default: return rawValue
}
}
func separatorIndices(_ length: Int) -> [Int] { func separatorIndices(_ length: Int) -> [Int] {
var indices: [Int] = [4, 8, 12] var indices: [Int] = [4, 8, 12]
switch self { switch self {
@ -135,6 +143,7 @@ extension InputField {
fileprivate func updateLeftImage(_ inputField: InputField) { fileprivate func updateLeftImage(_ inputField: InputField) {
let imageName = inputField.cardType.imageName(surface: inputField.surface) let imageName = inputField.cardType.imageName(surface: inputField.surface)
creditCardImageView.image = BundleManager.shared.image(for: imageName) creditCardImageView.image = BundleManager.shared.image(for: imageName)
creditCardImageView.accessibilityLabel = inputField.cardType.accessibilityLabel
} }
override func updateView(_ inputField: InputField) { override func updateView(_ inputField: InputField) {
@ -155,7 +164,7 @@ extension InputField {
internal var creditCardImageView = UIImageView().with { internal var creditCardImageView = UIImageView().with {
$0.height(20) $0.height(20)
$0.width(32) $0.width(32)
$0.isAccessibilityElement = false $0.isAccessibilityElement = true
$0.translatesAutoresizingMaskIntoConstraints = false $0.translatesAutoresizingMaskIntoConstraints = false
$0.contentMode = .scaleAspectFill $0.contentMode = .scaleAspectFill
$0.clipsToBounds = true $0.clipsToBounds = true

View File

@ -68,6 +68,12 @@ extension InputField {
actionModel.onClick(inputField) actionModel.onClick(inputField)
} }
inputField.actionTextLink.isHidden = false inputField.actionTextLink.isHidden = false
// set the accessibilityLabel
if let labelText = inputField.labelText {
inputField.actionTextLink.bridge_accessibilityLabelBlock = {
return "\(actionModel.text) \(labelText)"
}
}
inputField.fieldStackView.setCustomSpacing(VDSLayout.space2X, after: inputField.statusIcon) inputField.fieldStackView.setCustomSpacing(VDSLayout.space2X, after: inputField.statusIcon)
} else { } else {
inputField.actionTextLink.isHidden = true inputField.actionTextLink.isHidden = true

View File

@ -10,6 +10,35 @@ import UIKit
extension InputField { extension InputField {
public class TelephoneNumberValidator: Rule, Withable {
public var format: String
public var errorMessage: String = "Please enter a valid telephone number"
public init(format: String) {
self.format = format
}
public func isValid(value: String?) -> Bool {
guard let value, !value.isEmpty else { return true }
let regex = createRegex(from: format)
let predicate = NSPredicate(format: "SELF MATCHES %@", regex)
let valid = predicate.evaluate(with: value)
return valid
}
private func createRegex(from format: String) -> String {
// Escape special regex characters in the format string
let escapedFormat = NSRegularExpression.escapedPattern(for: format)
// Replace placeholder characters with regex patterns
let regex = escapedFormat
.replacingOccurrences(of: "X", with: "\\d")
return "^" + regex + "$"
}
}
class TelephoneHandler: FieldTypeHandler { class TelephoneHandler: FieldTypeHandler {
static let shared = TelephoneHandler() static let shared = TelephoneHandler()
@ -25,14 +54,7 @@ extension InputField {
} }
override func appendRules(_ inputField: InputField) { override func appendRules(_ inputField: InputField) {
if let text = inputField.textField.text, text.count > 0 { inputField.rules.append(.init(TelephoneNumberValidator(format: "XXX-XXX-XXXX")))
let rule = CharacterCountRule().copyWith {
$0.maxLength = "XXX-XXX-XXXX".count
$0.compareType = .equals
$0.errorMessage = "Enter a valid telephone."
}
inputField.rules.append(.init(rule))
}
} }
override func textField(_ inputField: InputField, textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { override func textField(_ inputField: InputField, textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
@ -49,7 +71,7 @@ extension InputField {
let rawNumber = newText.filter { $0.isNumber } let rawNumber = newText.filter { $0.isNumber }
// Format the number with dashes // Format the number with dashes
let formattedNumber = formatUSNumber(rawNumber) let formattedNumber = rawNumber.formatUSNumber()
// Set the formatted text // Set the formatted text
textField.text = formattedNumber textField.text = formattedNumber
@ -62,43 +84,54 @@ extension InputField {
textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition) textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition)
} }
value = formattedNumber
// Prevent the default behavior // Prevent the default behavior
return false return false
} }
internal func formatUSNumber(_ number: String) -> String { override func textFieldDidEndEditing(_ inputField: InputField, textField: UITextField) {
// Format the number in the style XXX-XXX-XXXX if let text = inputField.text {
let areaCodeLength = 3 textField.text = text.formatUSNumber()
let centralOfficeCodeLength = 3 value = textField.text
let lineNumberLength = 4
var formattedNumber = ""
if number.count > 0 {
formattedNumber.append(contentsOf: number.prefix(areaCodeLength))
} }
if number.count > areaCodeLength {
let startIndex = number.index(number.startIndex, offsetBy: areaCodeLength)
let endIndex = number.index(startIndex, offsetBy: min(centralOfficeCodeLength, number.count - areaCodeLength))
let centralOfficeCode = number[startIndex..<endIndex]
formattedNumber.append("-")
formattedNumber.append(contentsOf: centralOfficeCode)
}
if number.count > areaCodeLength + centralOfficeCodeLength {
let startIndex = number.index(number.startIndex, offsetBy: areaCodeLength + centralOfficeCodeLength)
let endIndex = number.index(startIndex, offsetBy: min(lineNumberLength, number.count - areaCodeLength - centralOfficeCodeLength))
let lineNumber = number[startIndex..<endIndex]
formattedNumber.append("-")
formattedNumber.append(contentsOf: lineNumber)
}
return formattedNumber
} }
} }
} }
extension String {
public func formatUSNumber() -> String {
// Format the number in the style XXX-XXX-XXXX
let areaCodeLength = 3
let centralOfficeCodeLength = 3
let lineNumberLength = 4
var formattedNumber = ""
let number = filter { $0.isNumber }
if number.count > 0 {
formattedNumber.append(contentsOf: number.prefix(areaCodeLength))
}
if number.count > areaCodeLength {
let startIndex = number.index(number.startIndex, offsetBy: areaCodeLength)
let endIndex = number.index(startIndex, offsetBy: min(centralOfficeCodeLength, number.count - areaCodeLength))
let centralOfficeCode = number[startIndex..<endIndex]
formattedNumber.append("-")
formattedNumber.append(contentsOf: centralOfficeCode)
}
if number.count > areaCodeLength + centralOfficeCodeLength {
let startIndex = number.index(number.startIndex, offsetBy: areaCodeLength + centralOfficeCodeLength)
let endIndex = number.index(startIndex, offsetBy: min(lineNumberLength, number.count - areaCodeLength - centralOfficeCodeLength))
let lineNumber = number[startIndex..<endIndex]
formattedNumber.append("-")
formattedNumber.append(contentsOf: lineNumber)
}
return formattedNumber
}
}

View File

@ -13,6 +13,7 @@ import Combine
/// An input field is an input wherein a customer enters information. They typically appear in forms. /// An input field is an input wherein a customer enters information. They typically appear in forms.
/// Specialized input fields capture credit card numbers, inline actions, passwords, phone numbers, /// Specialized input fields capture credit card numbers, inline actions, passwords, phone numbers,
/// dates and security codes in their correct formats. /// dates and security codes in their correct formats.
@objcMembers
@objc(VDSInputField) @objc(VDSInputField)
open class InputField: EntryFieldBase { open class InputField: EntryFieldBase {
@ -105,6 +106,11 @@ open class InputField: EntryFieldBase {
$0.translatesAutoresizingMaskIntoConstraints = false $0.translatesAutoresizingMaskIntoConstraints = false
$0.textStyle = TextStyle.bodyLarge $0.textStyle = TextStyle.bodyLarge
$0.isAccessibilityElement = false $0.isAccessibilityElement = false
$0.autocorrectionType = .no
$0.spellCheckingType = .no
$0.smartQuotesType = .no
$0.smartDashesType = .no
$0.smartInsertDeleteType = .no
} }
/// Color configuration for the textField. /// Color configuration for the textField.
@ -167,10 +173,6 @@ open class InputField: EntryFieldBase {
state.insert(.success) state.insert(.success)
} }
if textField.isFirstResponder {
state.insert(.focused)
}
return state return state
} }
} }
@ -186,6 +188,8 @@ open class InputField: EntryFieldBase {
super.setup() super.setup()
accessibilityHintText = "Double tap to edit" accessibilityHintText = "Double tap to edit"
actionTextLink.accessibilityTraits = .button
textField.heightAnchor.constraint(equalToConstant: 20).isActive = true textField.heightAnchor.constraint(equalToConstant: 20).isActive = true
textField.delegate = self textField.delegate = self
bottomContainerStackView.insertArrangedSubview(successLabel, at: 0) bottomContainerStackView.insertArrangedSubview(successLabel, at: 0)
@ -200,6 +204,59 @@ open class InputField: EntryFieldBase {
borderColorConfiguration.setSurfaceColors(VDSColor.feedbackSuccessOnlight, VDSColor.feedbackSuccessOndark, forState: .success) borderColorConfiguration.setSurfaceColors(VDSColor.feedbackSuccessOnlight, VDSColor.feedbackSuccessOndark, forState: .success)
textField.textColorConfiguration = textFieldTextColorConfiguration textField.textColorConfiguration = textFieldTextColorConfiguration
containerView.bridge_accessibilityLabelBlock = { [weak self] in
guard let self else { return "" }
var accessibilityLabels = [String]()
if let text = titleLabel.text?.trimmingCharacters(in: .whitespaces) {
accessibilityLabels.append(text)
}
if let formatText = textField.formatText, !formatText.isEmpty, textField.text.isEmpty {
accessibilityLabels.append("format, \(formatText)")
}
if let placeholderText = textField.placeholder, !placeholderText.isEmpty, textField.text.isEmpty {
accessibilityLabels.append("placeholder, \(placeholderText)")
}
if isReadOnly {
accessibilityLabels.append("read only")
}
if !isEnabled {
accessibilityLabels.append("dimmed")
}
if let errorText, showError {
accessibilityLabels.append("error, \(errorText)")
}
if let successText, showSuccess {
accessibilityLabels.append("success, \(successText)")
}
accessibilityLabels.append("\(Self.self)")
return accessibilityLabels.joined(separator: ", ")
}
statusIcon.bridge_accessibilityLabelBlock = { [weak self] in
guard let self else { return "" }
if showError {
return "error"
} else if showSuccess {
return "success"
} else {
return nil
}
}
containerView.bridge_accessibilityValueBlock = { [weak self] in
guard let self else { return "" }
return textField.isSecureTextEntry ? "\(textField.text.count) stars" : value
}
} }
open override func getFieldContainer() -> UIView { open override func getFieldContainer() -> UIView {
@ -255,6 +312,24 @@ open class InputField: EntryFieldBase {
} }
} }
open var widthPercentage: CGFloat? { didSet { setNeedsUpdate() } }
internal override func updateContainerWidth() {
widthConstraint?.deactivate()
trailingLessThanEqualsConstraint?.deactivate()
trailingEqualsConstraint?.deactivate()
//see if there is a widthPercentage and follow the same pattern as done for "width"
let currentWidth = (horizontalPinnedWidth() ?? 0) * (widthPercentage ?? 0)
if currentWidth >= minWidth, currentWidth <= maxWidth {
widthConstraint?.constant = currentWidth
widthConstraint?.activate()
trailingLessThanEqualsConstraint?.activate()
} else {
super.updateContainerWidth()
}
}
override func updateRules() { override func updateRules() {
super.updateRules() super.updateRules()
fieldType.handler().appendRules(self) fieldType.handler().appendRules(self)
@ -264,11 +339,20 @@ open class InputField: EntryFieldBase {
get { get {
var elements = [Any]() var elements = [Any]()
elements.append(contentsOf: [titleLabel, containerView]) elements.append(contentsOf: [titleLabel, containerView])
if showError { if let leftView = textField.leftView {
elements.append(leftView)
}
if !statusIcon.isHidden{
elements.append(statusIcon) elements.append(statusIcon)
if let errorText, !errorText.isEmpty { }
elements.append(errorLabel)
} if !actionTextLink.isHidden {
elements.append(actionTextLink)
}
if let errorText, !errorText.isEmpty, showError || hasInternalError {
elements.append(errorLabel)
} else if showSuccess, let successText, !successText.isEmpty { } else if showSuccess, let successText, !successText.isEmpty {
elements.append(successLabel) elements.append(successLabel)
} }
@ -285,29 +369,33 @@ open class InputField: EntryFieldBase {
} }
extension InputField: UITextFieldDelegate { extension InputField: UITextFieldDelegate {
public func textFieldDidBeginEditing(_ textField: UITextField) { open func textFieldDidBeginEditing(_ textField: UITextField) {
fieldType.handler().textFieldDidBeginEditing(self, textField: textField) fieldType.handler().textFieldDidBeginEditing(self, textField: textField)
updateContainerView(flag: true) updateContainerView(flag: true)
updateErrorLabel() updateErrorLabel()
} }
public func textFieldDidEndEditing(_ textField: UITextField) { open func textFieldDidEndEditing(_ textField: UITextField) {
fieldType.handler().textFieldDidEndEditing(self, textField: textField) fieldType.handler().textFieldDidEndEditing(self, textField: textField)
validate() validate()
UIAccessibility.post(notification: .layoutChanged, argument: self.containerView) UIAccessibility.post(notification: .layoutChanged, argument: self.containerView)
} }
public func textFieldDidChangeSelection(_ textField: UITextField) { open func textFieldDidChangeSelection(_ textField: UITextField) {
fieldType.handler().textFieldDidChangeSelection(self, textField: textField) fieldType.handler().textFieldDidChangeSelection(self, textField: textField)
text = textField.text
sendActions(for: .valueChanged)
if fieldType.handler().validateOnChange { if fieldType.handler().validateOnChange {
validate() validate()
} }
sendActions(for: .valueChanged)
setNeedsUpdate()
} }
public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { open func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
return fieldType.handler().textField(self, textField: textField, shouldChangeCharactersIn: range, replacementString: string) let shouldChange = fieldType.handler().textField(self, textField: textField, shouldChangeCharactersIn: range, replacementString: string)
if shouldChange {
UIAccessibility.post(notification: .announcement, argument: string)
}
return shouldChange
} }
} }

View File

@ -10,6 +10,7 @@ import UIKit
import Combine import Combine
import VDSCoreTokens import VDSCoreTokens
@objcMembers
@objc(VDSTextField) @objc(VDSTextField)
open class TextField: UITextField, ViewProtocol, Errorable { open class TextField: UITextField, ViewProtocol, Errorable {
@ -47,6 +48,11 @@ open class TextField: UITextField, ViewProtocol, Errorable {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Properties // MARK: - Properties
//-------------------------------------------------- //--------------------------------------------------
/// Set to true to hide the blinking textField cursor.
open var hideBlinkingCaret = false
open var enableClipboardActions: Bool = true
open var onDidDeleteBackwards: (() -> Void)?
/// Key of whether or not updateView() is called in setNeedsUpdate() /// Key of whether or not updateView() is called in setNeedsUpdate()
open var shouldUpdateView: Bool = true open var shouldUpdateView: Bool = true
@ -209,6 +215,23 @@ open class TextField: UITextField, ViewProtocol, Errorable {
return success return success
} }
open override func caretRect(for position: UITextPosition) -> CGRect {
if hideBlinkingCaret {
return .zero
}
let caretRect = super.caretRect(for: position)
return CGRect(origin: caretRect.origin, size: CGSize(width: 1, height: caretRect.height))
}
open override func deleteBackward() {
super.deleteBackward()
onDidDeleteBackwards?()
}
open override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { enableClipboardActions }
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Private Methods // MARK: - Private Methods
//-------------------------------------------------- //--------------------------------------------------
@ -236,7 +259,6 @@ open class TextField: UITextField, ViewProtocol, Errorable {
//-------------------------------------------------- //--------------------------------------------------
open var accessibilityAction: ((TextField) -> Void)? open var accessibilityAction: ((TextField) -> Void)?
private var _isAccessibilityElement: Bool = false
open override var isAccessibilityElement: Bool { open override var isAccessibilityElement: Bool {
get { get {
var block: AXBoolReturnBlock? var block: AXBoolReturnBlock?
@ -252,15 +274,14 @@ open class TextField: UITextField, ViewProtocol, Errorable {
if let block { if let block {
return block() return block()
} else { } else {
return _isAccessibilityElement return super.isAccessibilityElement
} }
} }
set { set {
_isAccessibilityElement = newValue super.isAccessibilityElement = newValue
} }
} }
private var _accessibilityLabel: String?
open override var accessibilityLabel: String? { open override var accessibilityLabel: String? {
get { get {
var block: AXStringReturnBlock? var block: AXStringReturnBlock?
@ -275,15 +296,14 @@ open class TextField: UITextField, ViewProtocol, Errorable {
if let block { if let block {
return block() return block()
} else { } else {
return _accessibilityLabel return super.accessibilityLabel
} }
} }
set { set {
_accessibilityLabel = newValue super.accessibilityLabel = newValue
} }
} }
private var _accessibilityHint: String?
open override var accessibilityHint: String? { open override var accessibilityHint: String? {
get { get {
var block: AXStringReturnBlock? var block: AXStringReturnBlock?
@ -298,15 +318,14 @@ open class TextField: UITextField, ViewProtocol, Errorable {
if let block { if let block {
return block() return block()
} else { } else {
return _accessibilityHint return super.accessibilityHint
} }
} }
set { set {
_accessibilityHint = newValue super.accessibilityHint = newValue
} }
} }
private var _accessibilityValue: String?
open override var accessibilityValue: String? { open override var accessibilityValue: String? {
get { get {
var block: AXStringReturnBlock? var block: AXStringReturnBlock?
@ -322,11 +341,11 @@ open class TextField: UITextField, ViewProtocol, Errorable {
if let block{ if let block{
return block() return block()
} else { } else {
return _accessibilityValue return super.accessibilityValue
} }
} }
set { set {
_accessibilityValue = newValue super.accessibilityValue = newValue
} }
} }

View File

@ -12,6 +12,7 @@ import Combine
/// A text area is an input wherein a customer enters long-form information. /// A text area is an input wherein a customer enters long-form information.
/// Use a text area when you want customers to enter text thats longer than a single line. /// Use a text area when you want customers to enter text thats longer than a single line.
@objcMembers
@objc(VDSTextArea) @objc(VDSTextArea)
open class TextArea: EntryFieldBase { open class TextArea: EntryFieldBase {
//-------------------------------------------------- //--------------------------------------------------
@ -57,17 +58,6 @@ open class TextArea: EntryFieldBase {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Public Properties // MARK: - Public Properties
//-------------------------------------------------- //--------------------------------------------------
/// Override UIControl state to add the .error state if showSuccess is true and if showError is true.
open override var state: UIControl.State {
get {
var state = super.state
if textView.isFirstResponder {
state.insert(.focused)
}
return state
}
}
override var containerSize: CGSize { CGSize(width: 182, height: Height.twoX.value) } override var containerSize: CGSize { CGSize(width: 182, height: Height.twoX.value) }
/// Enum used to describe the the height of TextArea. /// Enum used to describe the the height of TextArea.
@ -112,6 +102,7 @@ open class TextArea: EntryFieldBase {
$0.isScrollEnabled = true $0.isScrollEnabled = true
$0.textContainerInset = .zero $0.textContainerInset = .zero
$0.autocorrectionType = .no $0.autocorrectionType = .no
$0.spellCheckingType = .no
$0.textContainer.lineFragmentPadding = 0 $0.textContainer.lineFragmentPadding = 0
} }
@ -121,7 +112,11 @@ open class TextArea: EntryFieldBase {
} }
didSet { didSet {
validate() setNeedsUpdate()
if textView.isFirstResponder {
validate()
}
} }
} }
@ -143,6 +138,7 @@ open class TextArea: EntryFieldBase {
super.setup() super.setup()
accessibilityHintText = "Double tap to edit" accessibilityHintText = "Double tap to edit"
textView.delegate = self
//events //events
textView textView
@ -198,8 +194,9 @@ open class TextArea: EntryFieldBase {
override func updateRules() { override func updateRules() {
super.updateRules() super.updateRules()
if let maxLength, maxLength > 0 {
rules.append(.init(countRule)) rules.append(.init(countRule))
}
} }
open override func getFieldContainer() -> UIView { open override func getFieldContainer() -> UIView {
@ -237,7 +234,7 @@ open class TextArea: EntryFieldBase {
} }
} }
func textViewDidChange(_ textView: UITextView) { public func textViewDidChange(_ textView: UITextView) {
//dynamic textView Height sizing based on Figma //dynamic textView Height sizing based on Figma
//if you want it to work "as-is" delete this code //if you want it to work "as-is" delete this code
@ -299,3 +296,10 @@ open class TextArea: EntryFieldBase {
//-------------------------------------------------- //--------------------------------------------------
var countRule = CharacterCountRule() var countRule = CharacterCountRule()
} }
extension TextArea: UITextViewDelegate {
public func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
UIAccessibility.post(notification: .announcement, argument: text)
return true
}
}

View File

@ -10,6 +10,7 @@ import UIKit
import Combine import Combine
import VDSCoreTokens import VDSCoreTokens
@objcMembers
@objc(VDSTextView) @objc(VDSTextView)
open class TextView: UITextView, ViewProtocol, Errorable { open class TextView: UITextView, ViewProtocol, Errorable {
@ -45,6 +46,15 @@ open class TextView: UITextView, ViewProtocol, Errorable {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Properties // MARK: - Properties
//-------------------------------------------------- //--------------------------------------------------
open var placeholder: String? { didSet { setNeedsUpdate() } }
open var placeholderLabel = Label().with {
$0.textColorConfiguration = ViewColorConfiguration().with {
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forDisabled: true)
$0.setSurfaceColors(VDSColor.elementsSecondaryOnlight, VDSColor.elementsSecondaryOndark, forDisabled: false)
}.eraseToAnyColorable()
}
/// Key of whether or not updateView() is called in setNeedsUpdate() /// Key of whether or not updateView() is called in setNeedsUpdate()
open var shouldUpdateView: Bool = true open var shouldUpdateView: Bool = true
@ -88,6 +98,7 @@ open class TextView: UITextView, ViewProtocol, Errorable {
if textAlignment != oldValue { if textAlignment != oldValue {
// Text alignment can be part of our paragraph style, so we may need to // Text alignment can be part of our paragraph style, so we may need to
// re-style when changed // re-style when changed
placeholderLabel.textAlignment = textAlignment
updateLabel() updateLabel()
} }
} }
@ -118,6 +129,9 @@ open class TextView: UITextView, ViewProtocol, Errorable {
done.pinCenterY() done.pinCenterY()
.pinTrailing(16) .pinTrailing(16)
inputAccessoryView = accessView inputAccessoryView = accessView
addSubview(placeholderLabel)
placeholderLabel.pinToSuperView()
} }
@objc func doneButtonAction() { @objc func doneButtonAction() {
@ -145,13 +159,16 @@ open class TextView: UITextView, ViewProtocol, Errorable {
setNeedsUpdate() setNeedsUpdate()
} }
open override func layoutSubviews() {
super.layoutSubviews()
placeholderLabel.preferredMaxLayoutWidth = textContainer.size.width - textContainer.lineFragmentPadding * 2
}
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Accessibility // MARK: - Accessibility
//-------------------------------------------------- //--------------------------------------------------
open var accessibilityAction: ((TextView) -> Void)? open var accessibilityAction: ((TextView) -> Void)?
private var _isAccessibilityElement: Bool = false
open override var isAccessibilityElement: Bool { open override var isAccessibilityElement: Bool {
get { get {
var block: AXBoolReturnBlock? var block: AXBoolReturnBlock?
@ -167,15 +184,14 @@ open class TextView: UITextView, ViewProtocol, Errorable {
if let block { if let block {
return block() return block()
} else { } else {
return _isAccessibilityElement return super.isAccessibilityElement
} }
} }
set { set {
_isAccessibilityElement = newValue super.isAccessibilityElement = newValue
} }
} }
private var _accessibilityLabel: String?
open override var accessibilityLabel: String? { open override var accessibilityLabel: String? {
get { get {
var block: AXStringReturnBlock? var block: AXStringReturnBlock?
@ -190,15 +206,14 @@ open class TextView: UITextView, ViewProtocol, Errorable {
if let block { if let block {
return block() return block()
} else { } else {
return _accessibilityLabel return super.accessibilityLabel
} }
} }
set { set {
_accessibilityLabel = newValue super.accessibilityLabel = newValue
} }
} }
private var _accessibilityHint: String?
open override var accessibilityHint: String? { open override var accessibilityHint: String? {
get { get {
var block: AXStringReturnBlock? var block: AXStringReturnBlock?
@ -213,15 +228,14 @@ open class TextView: UITextView, ViewProtocol, Errorable {
if let block { if let block {
return block() return block()
} else { } else {
return _accessibilityHint return super.accessibilityHint
} }
} }
set { set {
_accessibilityHint = newValue super.accessibilityHint = newValue
} }
} }
private var _accessibilityValue: String?
open override var accessibilityValue: String? { open override var accessibilityValue: String? {
get { get {
var block: AXStringReturnBlock? var block: AXStringReturnBlock?
@ -237,11 +251,11 @@ open class TextView: UITextView, ViewProtocol, Errorable {
if let block{ if let block{
return block() return block()
} else { } else {
return _accessibilityValue return super.accessibilityValue
} }
} }
set { set {
_accessibilityValue = newValue super.accessibilityValue = newValue
} }
} }
@ -301,6 +315,10 @@ open class TextView: UITextView, ViewProtocol, Errorable {
} else { } else {
attributedText = nil attributedText = nil
} }
placeholderLabel.textStyle = textStyle
placeholderLabel.surface = surface
placeholderLabel.text = placeholder
placeholderLabel.isHidden = !text.isEmpty
} }
} }

View File

@ -10,6 +10,7 @@ import VDSCoreTokens
import UIKit import UIKit
import Combine import Combine
@objcMembers
@objc(VDSTileContainer) @objc(VDSTileContainer)
open class TileContainer: TileContainerBase<TileContainer.Padding> { open class TileContainer: TileContainerBase<TileContainer.Padding> {
@ -44,6 +45,7 @@ open class TileContainer: TileContainerBase<TileContainer.Padding> {
} }
open class TileContainerBase<PaddingType: DefaultValuing>: Control where PaddingType.ValueType == CGFloat { open class TileContainerBase<PaddingType: DefaultValuing>: Control where PaddingType.ValueType == CGFloat {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Initializers // MARK: - Initializers
//-------------------------------------------------- //--------------------------------------------------
@ -109,7 +111,12 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
$0.clipsToBounds = true $0.clipsToBounds = true
} }
internal var containerView = View() open var containerView = View().with {
$0.setContentHuggingPriority(.defaultLow, for: .horizontal)
$0.setContentHuggingPriority(.defaultLow, for: .vertical)
$0.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
$0.setContentCompressionResistancePriority(.defaultHigh, for: .vertical)
}
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Public Properties // MARK: - Public Properties
@ -179,11 +186,7 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
//-------------------------------------------------- //--------------------------------------------------
internal var widthConstraint: NSLayoutConstraint? internal var widthConstraint: NSLayoutConstraint?
internal var heightConstraint: NSLayoutConstraint? internal var heightConstraint: NSLayoutConstraint?
internal var heightGreaterThanConstraint: NSLayoutConstraint? internal var aspectRatioConstraint: NSLayoutConstraint?
internal var containerTopConstraint: NSLayoutConstraint?
internal var containerBottomConstraint: NSLayoutConstraint?
internal var containerLeadingConstraint: NSLayoutConstraint?
internal var containerTrailingConstraint: NSLayoutConstraint?
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Configuration // MARK: - Configuration
@ -222,29 +225,21 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
super.setup() super.setup()
isAccessibilityElement = false isAccessibilityElement = false
let layoutGuide = UILayoutGuide()
addLayoutGuide(layoutGuide)
layoutGuide
.pinTop()
.pinLeading()
.pinTrailing(0, .defaultHigh)
.pinBottom(0, .defaultHigh)
addSubview(backgroundImageView)
addSubview(containerView) addSubview(containerView)
containerView.addSubview(contentView)
addSubview(highlightView)
containerView.pinToSuperView() containerView.pinToSuperView()
widthConstraint = layoutGuide.widthAnchor.constraint(equalToConstant: 0) containerView.addSubview(backgroundImageView)
heightGreaterThanConstraint = layoutGuide.heightAnchor.constraint(greaterThanOrEqualToConstant: 44.0)
heightGreaterThanConstraint?.isActive = false
heightConstraint = layoutGuide.heightAnchor.constraint(equalToConstant: 0)
backgroundImageView.pinToSuperView() backgroundImageView.pinToSuperView()
containerView.addSubview(contentView)
contentView.pinToSuperView()
containerView.addSubview(highlightView)
highlightView.pinToSuperView()
widthConstraint = widthAnchor.constraint(equalToConstant: 0).deactivate()
heightConstraint = heightAnchor.constraint(equalToConstant: 0).deactivate()
backgroundImageView.setContentHuggingPriority(.defaultLow, for: .horizontal) backgroundImageView.setContentHuggingPriority(.defaultLow, for: .horizontal)
backgroundImageView.setContentHuggingPriority(.defaultLow, for: .vertical) backgroundImageView.setContentHuggingPriority(.defaultLow, for: .vertical)
backgroundImageView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) backgroundImageView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
@ -252,25 +247,28 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
backgroundImageView.isUserInteractionEnabled = false backgroundImageView.isUserInteractionEnabled = false
backgroundImageView.isHidden = true backgroundImageView.isHidden = true
containerTopConstraint = contentView.pinTop(anchor: layoutGuide.topAnchor, constant: padding.value)
containerBottomConstraint = layoutGuide.pinBottom(anchor: contentView.bottomAnchor, constant: padding.value)
containerLeadingConstraint = contentView.pinLeading(anchor: layoutGuide.leadingAnchor, constant: padding.value)
containerTrailingConstraint = layoutGuide.pinTrailing(anchor: contentView.trailingAnchor, constant: padding.value)
highlightView.pin(layoutGuide)
highlightView.isHidden = true highlightView.isHidden = true
highlightView.backgroundColor = .clear highlightView.backgroundColor = .clear
//corner radius //corner radius
layer.cornerRadius = cornerRadius containerView.layer.cornerRadius = cornerRadius
backgroundImageView.layer.cornerRadius = cornerRadius backgroundImageView.layer.cornerRadius = cornerRadius
highlightView.layer.cornerRadius = cornerRadius highlightView.layer.cornerRadius = cornerRadius
clipsToBounds = true containerView.clipsToBounds = true
containerView.bridge_isAccessibilityElementBlock = { [weak self] in self?.onClickSubscriber != nil } containerView.bridge_isAccessibilityElementBlock = { [weak self] in self?.onClickSubscriber != nil }
containerView.accessibilityHint = "Double tap to open." containerView.accessibilityHint = "Double tap to open."
containerView.accessibilityLabel = nil containerView.accessibilityLabel = nil
NotificationCenter.default
.publisher(for: UIDevice.orientationDidChangeNotification)
.sink() { [weak self] _ in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.001) { [weak self] in
guard let self else { return }
setNeedsUpdate()
}
}.store(in: &subscribers)
} }
/// Overriden to take the hit if there is an onClickSubscriber and the view is not a UIControl /// Overriden to take the hit if there is an onClickSubscriber and the view is not a UIControl
@ -286,7 +284,7 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
super.reset() super.reset()
shouldUpdateView = false shouldUpdateView = false
color = .white color = .white
aspectRatio = .ratio1x1 aspectRatio = .none
imageFallbackColor = .light imageFallbackColor = .light
width = nil width = nil
height = nil height = nil
@ -303,44 +301,14 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
highlightView.backgroundColor = hightLightViewColorConfiguration.getColor(self) highlightView.backgroundColor = hightLightViewColorConfiguration.getColor(self)
highlightView.isHidden = !isHighlighted highlightView.isHidden = !isHighlighted
layer.borderColor = borderColorConfiguration.getColor(self).cgColor containerView.layer.borderColor = borderColorConfiguration.getColor(self).cgColor
layer.borderWidth = showBorder ? VDSFormControls.borderWidth : 0 containerView.layer.borderWidth = showBorder ? VDSFormControls.borderWidth : 0
containerTopConstraint?.constant = padding.value contentView.removeConstraints()
containerLeadingConstraint?.constant = padding.value contentView.pinToSuperView(.uniform(padding.value))
containerBottomConstraint?.constant = padding.value
containerTrailingConstraint?.constant = padding.value
if let width, aspectRatio == .none && height == nil{ updateContainerView()
widthConstraint?.constant = width
widthConstraint?.isActive = true
heightConstraint?.isActive = false
heightGreaterThanConstraint?.isActive = true
} else if let height, let width {
widthConstraint?.constant = width
heightConstraint?.constant = height
heightConstraint?.isActive = true
widthConstraint?.isActive = true
heightGreaterThanConstraint?.isActive = false
} else if let width {
let size = ratioSize(for: width)
widthConstraint?.constant = size.width
heightConstraint?.constant = size.height
widthConstraint?.isActive = true
heightConstraint?.isActive = true
heightGreaterThanConstraint?.isActive = false
} else {
widthConstraint?.isActive = false
heightConstraint?.isActive = false
}
applyBackgroundEffects()
if showDropShadow, surface == .light {
addDropShadow(dropShadowConfiguration)
} else {
removeDropShadows()
}
} }
open override var accessibilityElements: [Any]? { open override var accessibilityElements: [Any]? {
@ -369,13 +337,6 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
set {} set {}
} }
/// 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 // MARK: - Public Methods
//-------------------------------------------------- //--------------------------------------------------
@ -400,56 +361,132 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
switch backgroundEffect { switch backgroundEffect {
case .transparency: case .transparency:
alphaConfiguration = 0.8 alphaConfiguration = 0.8
removeGradientLayer() containerView.removeGradientLayer()
case .gradient(let firstColor, let secondColor): case .gradient(let firstColor, let secondColor):
alphaConfiguration = 1.0 alphaConfiguration = 1.0
addGradientLayer(with: firstColor, secondColor: secondColor) containerView.addGradientLayer(with: firstColor, secondColor: secondColor)
backgroundImageView.isHidden = true backgroundImageView.isHidden = true
backgroundImageView.alpha = 1.0 backgroundImageView.alpha = 1.0
case .none: case .none:
alphaConfiguration = 1.0 alphaConfiguration = 1.0
removeGradientLayer() containerView.removeGradientLayer()
} }
if let backgroundImage { if let backgroundImage {
backgroundImageView.image = backgroundImage backgroundImageView.image = backgroundImage
backgroundImageView.isHidden = false backgroundImageView.isHidden = false
backgroundImageView.alpha = alphaConfiguration backgroundImageView.alpha = alphaConfiguration
backgroundColor = imageFallbackColor.withAlphaComponent(alphaConfiguration) containerView.backgroundColor = imageFallbackColor.withAlphaComponent(alphaConfiguration)
} else { } else {
backgroundImageView.isHidden = true backgroundImageView.isHidden = true
backgroundImageView.alpha = 1.0 backgroundImageView.alpha = 1.0
backgroundColor = color.withAlphaComponent(alphaConfiguration) containerView.backgroundColor = color.withAlphaComponent(alphaConfiguration)
} }
} }
private func ratioSize(for width: CGFloat) -> CGSize { private func updateContainerView() {
var height: CGFloat = width applyBackgroundEffects()
switch aspectRatio { if showDropShadow, surface == .light {
case .ratio1x1: containerView.addDropShadow(dropShadowConfiguration)
break; } else {
case .ratio3x4: containerView.removeDropShadows()
height = (4 / 3) * width
case .ratio4x3:
height = (3 / 4) * width
case .ratio2x3:
height = (3 / 2) * width
case .ratio3x2:
height = (2 / 3) * width
case .ratio9x16:
height = (16 / 9) * width
case .ratio16x9:
height = (9 / 16) * width
case .ratio1x2:
height = (2 / 1) * width
case .ratio2x1:
height = (1 / 2) * width
default:
break
} }
return CGSize(width: width, height: height) containerView.dropShadowLayers?.forEach { $0.frame = containerView.bounds }
containerView.gradientLayers?.forEach { $0.frame = containerView.bounds }
//sizing the container with constraints
//Set local vars
var containerViewWidth: CGFloat? = width
let containerViewHeight: CGFloat? = height
let multiplier = aspectRatio.multiplier
//turn off the constraints
aspectRatioConstraint?.deactivate()
widthConstraint?.deactivate()
heightConstraint?.deactivate()
//-------------------------------------------------------------------------
//Overriding Nil Width Rules
//-------------------------------------------------------------------------
//Rule 1:
//In the scenario where we only have a height but the multiplie is nil, we
//want to set the width with the parent's width which will more or less "fill"
//the container horizontally
//- height is set
//- width is not set
//- aspectRatio is not set
if let superviewWidth, superviewWidth > 0,
containerViewHeight != nil,
containerViewWidth == nil,
multiplier == nil {
containerViewWidth = superviewWidth
}
//Rule 2:
//In the scenario where no width and height is set, want to set the width with the
//parent's width which will more or less "fill" the container horizontally
//- height is not set
//- width is not set
else if let superviewWidth, superviewWidth > 0,
containerViewWidth == nil,
containerViewHeight == nil {
containerViewWidth = superviewWidth
}
//-------------------------------------------------------------------------
//-------------------------------------------------------------------------
//Width + AspectRatio Constraint
//-------------------------------------------------------------------------
if let containerViewWidth,
let multiplier,
containerViewWidth > 0,
containerViewHeight == nil {
widthConstraint?.constant = containerViewWidth
widthConstraint?.activate()
aspectRatioConstraint = heightAnchor.constraint(equalTo: widthAnchor, multiplier: multiplier)
aspectRatioConstraint?.activate()
}
//-------------------------------------------------------------------------
//Height + AspectRatio Constraint
//-------------------------------------------------------------------------
else if let containerViewHeight,
let multiplier,
containerViewHeight > 0,
containerViewWidth == nil {
heightConstraint?.constant = containerViewHeight
heightConstraint?.activate()
aspectRatioConstraint = widthAnchor.constraint(equalTo: heightAnchor, multiplier: multiplier)
aspectRatioConstraint?.activate()
} else {
//-------------------------------------------------------------------------
//Width Constraint
//-------------------------------------------------------------------------
if let containerViewWidth,
containerViewWidth > 0 {
widthConstraint?.constant = containerViewWidth
widthConstraint?.activate()
}
//-------------------------------------------------------------------------
//Height Constraint
//-------------------------------------------------------------------------
if let containerViewHeight,
containerViewHeight > 0 {
heightConstraint?.constant = containerViewHeight
heightConstraint?.activate()
}
}
}
/// This is the size of the superview's allowed space for this container first by constrained size which would include padding/inset values an
private var superviewWidth: CGFloat? {
horizontalPinnedWidth() ?? superview?.frame.size.width
} }
} }
@ -491,3 +528,30 @@ extension TileContainerBase {
} }
} }
} }
extension TileContainerBase.AspectRatio {
var multiplier: CGFloat? {
switch self {
case .ratio1x1:
return 1
case .ratio3x4:
return 4 / 3
case .ratio4x3:
return 3 / 4
case .ratio2x3:
return 3 / 2
case .ratio3x2:
return 2 / 3
case .ratio9x16:
return 16 / 9
case .ratio16x9:
return 9 / 16
case .ratio1x2:
return 2 / 1
case .ratio2x1:
return 1 / 2
case .none:
return nil
}
}
}

View File

@ -15,6 +15,7 @@ import Combine
/// support quick scanning and engagement. A Tilelet is fully clickable and /// support quick scanning and engagement. A Tilelet is fully clickable and
/// while it can include an arrow CTA, it does not require one in order to /// while it can include an arrow CTA, it does not require one in order to
/// function. /// function.
@objcMembers
@objc(VDSTilelet) @objc(VDSTilelet)
open class Tilelet: TileContainerBase<Tilelet.Padding> { open class Tilelet: TileContainerBase<Tilelet.Padding> {
@ -205,15 +206,10 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
} }
/// Descriptive Icon positioned in the contentView. /// Descriptive Icon positioned in the contentView.
open var descriptiveIcon = Icon().with { open var descriptiveIcon = Icon()
$0.isAccessibilityElement = false
}
/// Directional Icon positioned in the contentView. /// Directional Icon positioned in the contentView.
open var directionalIcon = Icon().with { open var directionalIcon = Icon()
$0.isAccessibilityElement = false
$0.name = .rightArrow
}
private var _textWidth: TextWidth? private var _textWidth: TextWidth?
@ -302,8 +298,9 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations. /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
open override func setup() { open override func setup() {
super.setup() super.setup()
aspectRatio = .none
color = .black color = .black
aspectRatio = .none
addContentView(stackView) addContentView(stackView)
//badge //badge
@ -381,11 +378,22 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
titleLockupSubTitleLabelHeightGreaterThanConstraint = titleLockup.subTitleLabel.heightGreaterThanEqualTo(constant: titleLockup.subTitleLabel.minimumLineHeight) titleLockupSubTitleLabelHeightGreaterThanConstraint = titleLockup.subTitleLabel.heightGreaterThanEqualTo(constant: titleLockup.subTitleLabel.minimumLineHeight)
titleLockupSubTitleLabelHeightGreaterThanConstraint?.priority = .defaultHigh titleLockupSubTitleLabelHeightGreaterThanConstraint?.priority = .defaultHigh
titleLockupSubTitleLabelHeightGreaterThanConstraint?.activate() titleLockupSubTitleLabelHeightGreaterThanConstraint?.activate()
directionalIcon.bridge_accessibilityLabelBlock = { [weak self] in
guard let self, let directionalIconModel else { return nil }
return directionalIconModel.accessibleText
}
descriptiveIcon.bridge_accessibilityLabelBlock = { [weak self] in
guard let self, let descriptiveIconModel else { return nil }
return descriptiveIconModel.accessibleText
}
} }
/// Resets to default settings. /// Resets to default settings.
open override func reset() { open override func reset() {
shouldUpdateView = false shouldUpdateView = false
super.reset()
aspectRatio = .none aspectRatio = .none
color = .black color = .black
//models //models
@ -405,18 +413,14 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
updateBadge() updateBadge()
updateTitleLockup() updateTitleLockup()
updateIcons() updateIcons()
///Content-driven height Tilelets - Minimum height is configurable. updateTextPositionAlignment()
///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()
}
setNeedsLayout() setNeedsLayout()
} }
/// Used to update any Accessibility properties. /// Used to update any Accessibility properties.
open override var accessibilityElements: [Any]? { open override var accessibilityElements: [Any]? {
get { get {
var views = [UIView]() var views = [AnyObject]()
// grab the available views in order // grab the available views in order
if badgeModel != nil { if badgeModel != nil {
@ -424,7 +428,15 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
} }
if titleModel != nil || subTitleModel != nil || eyebrowModel != nil { if titleModel != nil || subTitleModel != nil || eyebrowModel != nil {
views.append(titleLockup) let titleLockupViews = gatherAccessibilityElements(from: titleLockup)
views.append(contentsOf: titleLockupViews)
}
if descriptiveIconModel != nil {
views.append(descriptiveIcon)
} else if directionalIconModel != nil {
views.append(directionalIcon)
} }
containerView.setAccessibilityLabel(for: views) containerView.setAccessibilityLabel(for: views)
@ -584,6 +596,7 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
} }
private func updateTextPositionAlignment() { private func updateTextPositionAlignment() {
guard width != nil && (aspectRatio != .none || height != nil) else { return }
switch textPostion { switch textPostion {
case .top: case .top:
titleLockupTopConstraint?.activate() titleLockupTopConstraint?.activate()

View File

@ -66,6 +66,10 @@ extension Tilelet {
public var iconName: Icon.Name { public var iconName: Icon.Name {
return self == .rightArrow ? .rightArrow : .externalLink return self == .rightArrow ? .rightArrow : .externalLink
} }
public var accessibilityLabel: String {
self == .rightArrow ? "Directional right arrow" : "External link"
}
} }
public enum IconSize: String, EnumSubset { public enum IconSize: String, EnumSubset {
@ -80,7 +84,7 @@ extension Tilelet {
public var iconColor: IconColor? public var iconColor: IconColor?
/// Accessible Text for the Icon /// Accessible Text for the Icon
public var accessibleText: String public var accessibleText: String?
/// Enum for a icon type you want shown.. /// Enum for a icon type you want shown..
public var iconType: IconType public var iconType: IconType
@ -95,7 +99,7 @@ extension Tilelet {
self.iconType = iconType self.iconType = iconType
self.iconColor = iconColor self.iconColor = iconColor
self.accessibleText = accessibleText ?? iconType.iconName.rawValue self.accessibleText = accessibleText ?? iconType.accessibilityLabel
self.size = size self.size = size
} }
} }

View File

@ -12,6 +12,7 @@ import Combine
/// Title Lockup ensures the readability of words on the screen /// Title Lockup ensures the readability of words on the screen
/// with approved built in text size configurations. /// with approved built in text size configurations.
@objcMembers
@objc(VDSTitleLockup) @objc(VDSTitleLockup)
open class TitleLockup: View { open class TitleLockup: View {

View File

@ -12,6 +12,7 @@ import Combine
/// A toggle is a control that lets customers instantly turn on /// A toggle is a control that lets customers instantly turn on
/// or turn off a single option, setting or function. /// or turn off a single option, setting or function.
@objcMembers
@objc(VDSToggle) @objc(VDSToggle)
open class Toggle: Control, Changeable, FormFieldable { open class Toggle: Control, Changeable, FormFieldable {

View File

@ -12,6 +12,7 @@ import Combine
/// A toggle is a control that lets customers instantly turn on /// A toggle is a control that lets customers instantly turn on
/// or turn off a single option, setting or function. /// or turn off a single option, setting or function.
@objcMembers
@objc(VDSToggleView) @objc(VDSToggleView)
open class ToggleView: Control, Changeable, FormFieldable { open class ToggleView: Control, Changeable, FormFieldable {
@ -219,7 +220,7 @@ open class ToggleView: Control, Changeable, FormFieldable {
} }
knobTrailingConstraint?.isActive = true knobTrailingConstraint?.isActive = true
knobLeadingConstraint?.isActive = true knobLeadingConstraint?.isActive = true
setNeedsLayout() layoutIfNeeded()
} }
private func updateToggle() { private func updateToggle() {

View File

@ -13,6 +13,7 @@ import Combine
/// A tooltip is an overlay that clarifies another component or content /// A tooltip is an overlay that clarifies another component or content
/// element. It is triggered when a customer hovers, clicks or taps /// element. It is triggered when a customer hovers, clicks or taps
/// the tooltip icon. /// the tooltip icon.
@objcMembers
@objc(VDSTooltip) @objc(VDSTooltip)
open class Tooltip: Control, TooltipLaunchable { open class Tooltip: Control, TooltipLaunchable {

View File

@ -10,6 +10,8 @@ import UIKit
import Combine import Combine
import VDSCoreTokens import VDSCoreTokens
@objcMembers
@objc(VDSTooltipAlertViewController)
open class TooltipAlertViewController: UIViewController, Surfaceable { open class TooltipAlertViewController: UIViewController, Surfaceable {
/// Set of Subscribers for any Publishers for this Control. /// Set of Subscribers for any Publishers for this Control.

View File

@ -9,6 +9,8 @@ import Foundation
import UIKit import UIKit
import VDSCoreTokens import VDSCoreTokens
@objcMembers
@objc(VDSTooltipDialog)
open class TooltipDialog: View, UIScrollViewDelegate { open class TooltipDialog: View, UIScrollViewDelegate {
//-------------------------------------------------- //--------------------------------------------------

View File

@ -12,23 +12,11 @@ extension UITapGestureRecognizer {
/// Determines if the touch event has a action attribute within the range given /// Determines if the touch event has a action attribute within the range given
/// - Parameters: /// - Parameters:
/// - label: UILabel in question /// - label: Label in question
/// - targetRange: Range to look within /// - targetRange: Range to look within
/// - Returns: Wether the range in the label has an action /// - Returns: Wether the range in the label has an action
public func didTapActionInLabel(_ label: UILabel, inRange targetRange: NSRange) -> Bool { public func didTapActionInLabel(_ label: Label, inRange targetRange: NSRange) -> Bool {
let tapLocation = location(in: label)
guard let attributedText = label.attributedText else { return false } return label.isAction(for: tapLocation, inRange: targetRange)
let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: label.bounds.size)
let textStorage = NSTextStorage(attributedString: attributedText)
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
let location = location(in: label)
let characterIndex = layoutManager.characterIndex(for: location, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
guard let _ = attributedText.attribute(NSAttributedString.Key.action, at: characterIndex, effectiveRange: nil) as? String, characterIndex < attributedText.length else { return false }
return true
} }
} }

View File

@ -14,14 +14,14 @@ extension UIView {
/// - views: Array of Views that you want to join the accessibilityLabel. /// - views: Array of Views that you want to join the accessibilityLabel.
/// - separator: Separator used between the accessibilityLabel for each UIView. /// - separator: Separator used between the accessibilityLabel for each UIView.
/// - Returns: Joined String. /// - Returns: Joined String.
public func combineAccessibilityLabel(for views: [UIView], separator: String = ", ") -> String? { public func combineAccessibilityLabel(for views: [AnyObject], separator: String = ", ") -> String? {
let labels = views.map({($0.accessibilityLabel?.isEmpty ?? true) ? nil : $0.accessibilityLabel}).compactMap({$0}) let labels: [String] = views.map({($0.accessibilityLabel?.isEmpty ?? true) ? nil : $0.accessibilityLabel}).compactMap({$0})
return labels.joined(separator: separator) return labels.joined(separator: separator)
} }
/// AccessibilityLabel helper for joining the accessibilityLabel property of all views passed in. /// AccessibilityLabel helper for joining the accessibilityLabel property of all views passed in.
/// - Parameter views: Array of Views that you want to join the accessibilityLabel. /// - Parameter views: Array of Views that you want to join the accessibilityLabel.
public func setAccessibilityLabel(for views: [UIView]) { public func setAccessibilityLabel(for views: [AnyObject]) {
accessibilityLabel = combineAccessibilityLabel(for: views) accessibilityLabel = combineAccessibilityLabel(for: views)
} }
@ -50,8 +50,8 @@ extension UIView {
return isIntersecting return isIntersecting
} }
public func gatherAccessibilityElements(from view: UIView) -> [Any] { public func gatherAccessibilityElements(from view: AnyObject) -> [AnyObject] {
var elements: [Any] = [] var elements: [AnyObject] = []
for subview in view.subviews { for subview in view.subviews {
if subview.isAccessibilityElement && subview.isVisibleOnScreen { if subview.isAccessibilityElement && subview.isVisibleOnScreen {

View File

@ -20,6 +20,9 @@ public protocol FormFieldable {
/// Protocol for FormFieldable that require internal validation. /// Protocol for FormFieldable that require internal validation.
public protocol FormFieldInternalValidatable: FormFieldable, Errorable { public protocol FormFieldInternalValidatable: FormFieldable, Errorable {
/// Rules that drive the validator
var rules: [AnyRule<ValueType>] { get set }
/// Is there an internalError /// Is there an internalError
var hasInternalError: Bool { get } var hasInternalError: Bool { get }
/// Internal Error Message that will show. /// Internal Error Message that will show.

View File

@ -631,6 +631,257 @@ extension LayoutConstraintable {
return centerYAnchor.constraint(greaterThanOrEqualTo: found, constant: -constant).with { $0.priority = priority; $0.isActive = true } return centerYAnchor.constraint(greaterThanOrEqualTo: found, constant: -constant).with { $0.priority = priority; $0.isActive = true }
} }
} }
// alignment
public enum LayoutAlignment: String, CaseIterable {
case fill
case leading
case top
case center
case trailing
case bottom
}
public enum LayoutDistribution: String, CaseIterable {
case fill
case fillProportionally
}
extension LayoutConstraintable {
public func removeConstraints() {
guard let view = self as? UIView, let superview = view.superview else { return }
// Remove all existing constraints on the containerView
let superviewConstraints = superview.constraints
for constraint in superviewConstraints {
if constraint.firstItem as? UIView == view
|| constraint.secondItem as? UIView == view {
superview.removeConstraint(constraint)
}
}
}
public func applyAlignment(_ alignment: LayoutAlignment, edges: UIEdgeInsets = UIEdgeInsets.zero) {
guard let superview = superview else { return }
removeConstraints()
switch alignment {
case .fill:
pinToSuperView(edges)
case .leading:
pinTop(edges.top)
pinLeading(edges.left)
pinTrailingLessThanOrEqualTo(anchor: superview.trailingAnchor, constant: edges.right)
pinBottom(edges.bottom)
case .trailing:
pinTop(edges.top)
pinLeadingGreaterThanOrEqualTo(anchor: superview.leadingAnchor, constant: edges.left)
pinTrailing(edges.right)
pinBottom(edges.bottom)
case .top:
pinTop(edges.top)
pinLeadingGreaterThanOrEqualTo(anchor: superview.leadingAnchor, constant: edges.left)
pinTrailingLessThanOrEqualTo(anchor: superview.trailingAnchor, constant: edges.right)
pinBottomLessThanOrEqualTo(anchor: superview.bottomAnchor, constant: edges.bottom)
case .bottom:
pinTopGreaterThanOrEqualTo(anchor: superview.topAnchor, constant: edges.top)
pinLeadingGreaterThanOrEqualTo(anchor: superview.leadingAnchor, constant: edges.left)
pinTrailingLessThanOrEqualTo(anchor: superview.trailingAnchor, constant: edges.right)
pinBottom(edges.bottom)
case .center:
pinCenterX()
pinTop(edges.top)
pinLeadingGreaterThanOrEqualTo(anchor: superview.leadingAnchor, constant: edges.left)
pinTrailingLessThanOrEqualTo(anchor: superview.trailingAnchor, constant: edges.right)
pinBottom(edges.bottom)
}
}
// Method to check if the view is pinned to its superview
public func isPinnedEqual() -> Bool {
isPinnedEqualVertically() && isPinnedEqualHorizontally()
}
public func horizontalPinnedWidth() -> CGFloat? {
guard let view = self as? UIView, let superview = view.superview else { return nil }
let constraints = superview.constraints
var leadingPinnedObject: AnyObject?
var trailingPinnedObject: AnyObject?
for constraint in constraints {
if (constraint.firstItem === view && (constraint.firstAttribute == .leading || constraint.firstAttribute == .left)) {
leadingPinnedObject = constraint.secondItem as AnyObject?
} else if (constraint.secondItem === view && (constraint.secondAttribute == .leading || constraint.secondAttribute == .left)) {
leadingPinnedObject = constraint.firstItem as AnyObject?
} else if (constraint.firstItem === view && (constraint.firstAttribute == .trailing || constraint.firstAttribute == .right)) {
trailingPinnedObject = constraint.secondItem as AnyObject?
} else if (constraint.secondItem === view && (constraint.secondAttribute == .trailing || constraint.secondAttribute == .right)) {
trailingPinnedObject = constraint.firstItem as AnyObject?
}
}
// Ensure both leading and trailing pinned objects are identified
if let leadingObject = leadingPinnedObject, let trailingObject = trailingPinnedObject {
// Calculate the size based on the pinned objects
if let leadingView = leadingObject as? UIView, let trailingView = trailingObject as? UIView {
let leadingPosition = leadingView.convert(leadingView.bounds.origin, to: superview).x
let trailingPosition = trailingView.convert(trailingView.bounds.origin, to: superview).x + trailingView.bounds.width
return trailingPosition - leadingPosition
} else if let leadingGuide = leadingObject as? UILayoutGuide, let trailingGuide = trailingObject as? UILayoutGuide {
let leadingPosition = leadingGuide.layoutFrame.minX
let trailingPosition = trailingGuide.layoutFrame.maxX
return trailingPosition - leadingPosition
} else if let leadingView = leadingObject as? UIView, let trailingGuide = trailingObject as? UILayoutGuide {
let leadingPosition = leadingView.convert(leadingView.bounds.origin, to: superview).x
let trailingPosition = trailingGuide.layoutFrame.maxX
return trailingPosition - leadingPosition
} else if let leadingGuide = leadingObject as? UILayoutGuide, let trailingView = trailingObject as? UIView {
let leadingPosition = leadingGuide.layoutFrame.minX
let trailingPosition = trailingView.convert(trailingView.bounds.origin, to: superview).x + trailingView.bounds.width
return trailingPosition - leadingPosition
}
} else if let pinnedObject = leadingPinnedObject {
if let view = pinnedObject as? UIView {
return view.bounds.size.width
} else if let layoutGuide = pinnedObject as? UILayoutGuide {
return layoutGuide.layoutFrame.size.width
}
} else if let pinnedObject = trailingPinnedObject {
if let view = pinnedObject as? UIView {
return view.bounds.size.width
} else if let layoutGuide = pinnedObject as? UILayoutGuide {
return layoutGuide.layoutFrame.size.width
}
}
return nil
}
public func verticalPinnedHeight() -> CGFloat? {
guard let view = self as? UIView, let superview = view.superview else { return nil }
let constraints = superview.constraints
var topPinnedObject: AnyObject?
var bottomPinnedObject: AnyObject?
for constraint in constraints {
if (constraint.firstItem === view && (constraint.firstAttribute == .top || constraint.firstAttribute == .topMargin)) {
topPinnedObject = constraint.secondItem as AnyObject?
} else if (constraint.secondItem === view && (constraint.secondAttribute == .top || constraint.secondAttribute == .topMargin)) {
topPinnedObject = constraint.firstItem as AnyObject?
} else if (constraint.firstItem === view && (constraint.firstAttribute == .bottom || constraint.firstAttribute == .bottomMargin)) {
bottomPinnedObject = constraint.secondItem as AnyObject?
} else if (constraint.secondItem === view && (constraint.secondAttribute == .bottom || constraint.secondAttribute == .bottomMargin)) {
bottomPinnedObject = constraint.firstItem as AnyObject?
}
}
// Ensure both top and bottom pinned objects are identified
if let topObject = topPinnedObject, let bottomObject = bottomPinnedObject {
// Calculate the size based on the pinned objects
if let topView = topObject as? UIView, let bottomView = bottomObject as? UIView {
let topPosition = topView.convert(topView.bounds.origin, to: superview).y
let bottomPosition = bottomView.convert(bottomView.bounds.origin, to: superview).y + bottomView.bounds.height
return bottomPosition - topPosition
} else if let topGuide = topObject as? UILayoutGuide, let bottomGuide = bottomObject as? UILayoutGuide {
let topPosition = topGuide.layoutFrame.minY
let bottomPosition = bottomGuide.layoutFrame.maxY
return bottomPosition - topPosition
} else if let topView = topObject as? UIView, let bottomGuide = bottomObject as? UILayoutGuide {
let topPosition = topView.convert(topView.bounds.origin, to: superview).y
let bottomPosition = bottomGuide.layoutFrame.maxY
return bottomPosition - topPosition
} else if let topGuide = topObject as? UILayoutGuide, let bottomView = bottomObject as? UIView {
let topPosition = topGuide.layoutFrame.minY
let bottomPosition = bottomView.convert(bottomView.bounds.origin, to: superview).y + bottomView.bounds.height
return bottomPosition - topPosition
}
} else if let pinnedObject = topPinnedObject {
if let view = pinnedObject as? UIView {
return view.bounds.size.height
} else if let layoutGuide = pinnedObject as? UILayoutGuide {
return layoutGuide.layoutFrame.size.height
}
} else if let pinnedObject = bottomPinnedObject {
if let view = pinnedObject as? UIView {
return view.bounds.size.height
} else if let layoutGuide = pinnedObject as? UILayoutGuide {
return layoutGuide.layoutFrame.size.height
}
}
return nil
}
public func isPinnedEqualHorizontally() -> Bool {
guard let view = self as? UIView, let superview = view.superview else { return false }
let constraints = superview.constraints
var leadingPinned = false
var trailingPinned = false
for constraint in constraints {
if (constraint.firstItem as? UIView == view && constraint.firstAttribute == .leading && constraint.relation == .equal) ||
(constraint.secondItem as? UIView == view && constraint.secondAttribute == .leading && constraint.relation == .equal) ||
(constraint.firstItem as? UIView == view && constraint.firstAttribute == .left && constraint.relation == .equal) ||
(constraint.secondItem as? UIView == view && constraint.secondAttribute == .left && constraint.relation == .equal) {
leadingPinned = true
}
if (constraint.firstItem as? UIView == view && constraint.firstAttribute == .trailing && constraint.relation == .equal) ||
(constraint.secondItem as? UIView == view && constraint.secondAttribute == .trailing && constraint.relation == .equal) ||
(constraint.firstItem as? UIView == view && constraint.firstAttribute == .right && constraint.relation == .equal) ||
(constraint.secondItem as? UIView == view && constraint.secondAttribute == .right && constraint.relation == .equal) {
trailingPinned = true
}
}
return leadingPinned && trailingPinned
}
public func isPinnedEqualVertically() -> Bool {
guard let view = self as? UIView, let superview = view.superview else { return false }
let constraints = superview.constraints
var topPinned = false
var bottomPinned = false
for constraint in constraints {
if (constraint.firstItem as? UIView == view && constraint.firstAttribute == .top && constraint.relation == .equal) ||
(constraint.secondItem as? UIView == view && constraint.secondAttribute == .top && constraint.relation == .equal) {
topPinned = true
}
if (constraint.firstItem as? UIView == view && constraint.firstAttribute == .bottom && constraint.relation == .equal) ||
(constraint.secondItem as? UIView == view && constraint.secondAttribute == .bottom && constraint.relation == .equal) {
bottomPinned = true
}
}
return topPinned && bottomPinned
}
}
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Implementations // MARK: - Implementations
//-------------------------------------------------- //--------------------------------------------------

View File

@ -1,6 +1,32 @@
1.0.68 1.0.71
---------------- ----------------
- CXTDT-581800 - DatePicker - Selected Error state icon
- CXTDT-581801 - DatePicker - border disappears for on dark focus state
- CXTDT-581803 - DatePicker - Calendar does not switch to Dark Mode
- CXTDT-584278 InputField - Accessibility
- CXTDT-586375 - Table - Issue With Stripe
- CXTDT-577463 - InputField - Accessibility - #7
- CXTDT-565796 - DropdownSelect Removed the "Type" from the VoiceOver
1.0.70
----------------
- CXTDT-577463 - InputField - Accessibility - #1 Typing Feedback
- CXTDT-577463 - InputField - Accessibility - #5 Password / Inline Action
- CXTDT-560485 - Tilelet - Accessibility Icons
- DatePicker - Final logic for how the calendar shows.
1.0.69
----------------
- DatePicker - Refactored how this is shown
- Checkbox Item/Group - Accessibility Refactor
- Radiobox Item/Group - Accessibility Refactor
- Radiobutton Item/Group - Accessibility Refactor
- CXTDT-553663 - DropdownSelect - Accessibility - has popup - CXTDT-553663 - DropdownSelect - Accessibility - has popup
- CXTDT-577463 - InputField - Accessibility
1.0.69
----------------
- Expired Build because of a issue
1.0.67 1.0.67
---------------- ----------------

View File

@ -26,6 +26,7 @@ Using the system allows designers and developers to collaborate more easily and
- ``ButtonIcon`` - ``ButtonIcon``
- ``ButtonGroup`` - ``ButtonGroup``
- ``CalendarBase`` - ``CalendarBase``
- ``Carousel``
- ``CarouselScrollbar`` - ``CarouselScrollbar``
- ``Checkbox`` - ``Checkbox``
- ``CheckboxItem`` - ``CheckboxItem``