Compare commits
327 Commits
refactor/e
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| 98dde01e18 | |||
| 78dd2d5df8 | |||
| 58850aeb6e | |||
|
|
15a53964c5 | ||
|
|
762868609b | ||
|
|
e256dfd209 | ||
|
|
a1236df9a9 | ||
|
|
539f10ffbf | ||
|
|
cc530e5903 | ||
|
|
482212e88c | ||
|
|
47409d2905 | ||
|
|
16516fbeed | ||
|
|
484d12626e | ||
|
|
9856c0a482 | ||
|
|
4797e5a7f7 | ||
|
|
942834ec3e | ||
|
|
62aedc7c5a | ||
|
|
2e3524cde6 | ||
|
|
735749eaa5 | ||
|
|
4e581491fa | ||
|
|
4fbbe485f7 | ||
|
|
216971b878 | ||
|
|
d66d4d0e26 | ||
|
|
c352fbaac9 | ||
|
|
759ef0db5b | ||
|
|
dc7dbffcf6 | ||
|
|
a019c1ba36 | ||
|
|
fc6b2991b8 | ||
|
|
dd437d7423 | ||
|
|
025700a632 | ||
|
|
50117f3f2f | ||
|
|
8057b16e5b | ||
|
|
1728592bff | ||
|
|
a336066387 | ||
|
|
049234831f | ||
|
|
dc83d1f565 | ||
|
|
92a80b6496 | ||
|
|
83ce02e879 | ||
|
|
9f2da6f072 | ||
|
|
c91da2189c | ||
|
|
2febe4595c | ||
|
|
c68faaec31 | ||
|
|
0aab83c4cb | ||
|
|
ff125d388d | ||
|
|
f741b3df8b | ||
|
|
a3a714dfc1 | ||
|
|
b210135566 | ||
|
|
81725ffdeb | ||
|
|
08be77242d | ||
|
|
8a63ea5c75 | ||
|
|
e71b94bc4f | ||
|
|
183630f6b5 | ||
|
|
c17e74dea0 | ||
|
|
12ad80643d | ||
|
|
4d5704d47b | ||
|
|
56155abbb0 | ||
|
|
821b06360e | ||
|
|
663c6072be | ||
|
|
d5cca1626e | ||
|
|
5e932000da | ||
|
|
3c1caa7f5a | ||
|
|
c39a89a3f3 | ||
|
|
cb37d80f65 | ||
|
|
ebbbffc86e | ||
|
|
26a3206639 | ||
|
|
b5f3e37a4c | ||
|
|
ae460b8e4f | ||
|
|
905c5e9fb0 | ||
|
|
1f37597117 | ||
|
|
f868389cb8 | ||
|
|
aa092023de | ||
|
|
e951e82b88 | ||
|
|
e77bbea163 | ||
|
|
e1f88e600b | ||
|
|
8028c8a785 | ||
|
|
58b83fc37f | ||
|
|
fe8fb7f3cc | ||
|
|
8ce9108d45 | ||
|
|
9f6c115f50 | ||
|
|
2854aea8c2 | ||
|
|
e08de8fb3e | ||
|
|
9b885da7ca | ||
|
|
09351db136 | ||
|
|
6df8932d5d | ||
|
|
f27f04c23d | ||
|
|
e22b64095b | ||
|
|
a0b139b0af | ||
|
|
73caedce5f | ||
|
|
973c45adbd | ||
|
|
2365f8dbf4 | ||
|
|
296703076e | ||
|
|
c76d3e7b76 | ||
|
|
35036ca804 | ||
|
|
ad6291af30 | ||
|
|
31f01773c4 | ||
|
|
63499838c6 | ||
|
|
e4630b65ea | ||
|
|
47747c801e | ||
|
|
1f2637ed04 | ||
|
|
b0d48568a0 | ||
|
|
aa013f62e3 | ||
|
|
343f674251 | ||
|
|
6147568bc3 | ||
|
|
b3f602087d | ||
|
|
952d688cc2 | ||
|
|
c41599578a | ||
|
|
d336b8dfda | ||
|
|
9cbfe1d46a | ||
|
|
359a729379 | ||
|
|
b9ae68ab20 | ||
|
|
29714501cc | ||
|
|
012d6b3daf | ||
|
|
f2c6eb0fc2 | ||
|
|
7536774c0f | ||
|
|
7667908ffd | ||
|
|
030ebdf897 | ||
|
|
592d2c1f1f | ||
|
|
a53a8ef26e | ||
|
|
41b2f398df | ||
|
|
bd2b1b0de9 | ||
|
|
658624aadd | ||
|
|
99da32947f | ||
|
|
940493ef10 | ||
|
|
eea54b26ff | ||
|
|
1549832647 | ||
|
|
93b89ee754 | ||
|
|
0951a48467 | ||
|
|
352affeddd | ||
|
|
baf9136e26 | ||
|
|
3c92411703 | ||
|
|
633a8d3b78 | ||
|
|
dcc0307e9b | ||
|
|
30a4b1e0f7 | ||
|
|
7efc0096af | ||
|
|
5a255aac52 | ||
|
|
aae72b435b | ||
|
|
c5972ccd0e | ||
|
|
31ce0c6e30 | ||
|
|
69cfb38149 | ||
|
|
a95499b186 | ||
|
|
393b791a40 | ||
|
|
d0c40aaeca | ||
|
|
c534072a12 | ||
|
|
67bdcf5104 | ||
|
|
bbf521b1e5 | ||
|
|
e5d9f77d3d | ||
|
|
1bd58256f7 | ||
|
|
01c05deebd | ||
|
|
7ae5f9853d | ||
|
|
8f7581d0d6 | ||
|
|
20c407f82a | ||
|
|
73db6dff2d | ||
|
|
d958ef817e | ||
|
|
731836b346 | ||
|
|
1bb54d174f | ||
|
|
f0ceaf11ff | ||
|
|
3ee592f638 | ||
|
|
307ac909a8 | ||
|
|
3809e96b16 | ||
|
|
b1adbd7e0d | ||
|
|
47a38e5d3f | ||
|
|
19b07b782c | ||
|
|
9edd360303 | ||
|
|
23a56bded2 | ||
|
|
67c401f87a | ||
|
|
f4cb8aa510 | ||
|
|
bf4ff94933 | ||
|
|
fc7650f7d9 | ||
|
|
ac1957d1c3 | ||
|
|
59294ebd51 | ||
|
|
973e25034e | ||
|
|
09886ffe26 | ||
|
|
0decdb5a16 | ||
|
|
c2c88c7b90 | ||
|
|
760433243e | ||
|
|
eb8edd2acd | ||
|
|
9f6a83f8ce | ||
|
|
f713cc0659 | ||
|
|
317cf89fb6 | ||
|
|
06b92f48c3 | ||
|
|
c8502610f2 | ||
|
|
0434b3f63e | ||
|
|
e0d769ccf3 | ||
|
|
7e36bc074a | ||
|
|
1e9aa876db | ||
|
|
d4e7d7476d | ||
|
|
6c4fb1c5ba | ||
|
|
cbdd82ee31 | ||
|
|
6938d91860 | ||
|
|
815024cc5c | ||
|
|
0158d2f5db | ||
|
|
a024e1d5b3 | ||
|
|
9fc100d3d6 | ||
|
|
417cddb382 | ||
|
|
ce3d9e6e22 | ||
|
|
dba0879b99 | ||
|
|
0d38eb495b | ||
|
|
8723cfa5c3 | ||
|
|
3ec2c98425 | ||
|
|
77325f0034 | ||
|
|
46b11c2585 | ||
|
|
c326213302 | ||
|
|
10c7435597 | ||
|
|
d02ca3eebd | ||
|
|
06b0c80a58 | ||
|
|
c120c746d2 | ||
|
|
3ea4b74af2 | ||
|
|
90eb01cb24 | ||
|
|
4e88ee936a | ||
|
|
a659393709 | ||
|
|
6bf3e19ebe | ||
|
|
781df9ff1a | ||
|
|
0210e0d128 | ||
|
|
f87e2ddc98 | ||
|
|
af58e43f6c | ||
|
|
2a1b9380c8 | ||
|
|
7cabfdb78c | ||
|
|
a253e2efc8 | ||
|
|
54a2a63ba4 | ||
|
|
00f73a503c | ||
|
|
1e35f61ec0 | ||
|
|
be8de7fbca | ||
|
|
e02ea9712e | ||
|
|
11af2e5eb9 | ||
|
|
fb629a192b | ||
|
|
afc4d71f38 | ||
|
|
7c431974ac | ||
|
|
99fe7e22ac | ||
|
|
938364535b | ||
|
|
f1b5fd18c9 | ||
|
|
b91017068c | ||
|
|
bf40368dc4 | ||
|
|
076f4c082c | ||
|
|
7f13aebc7e | ||
|
|
a38268df0a | ||
|
|
93004f6e12 | ||
|
|
559423f629 | ||
|
|
c2ec941a3c | ||
|
|
4ae07b6402 | ||
|
|
928db0f1fc | ||
|
|
6661e60333 | ||
|
|
bf46f85622 | ||
|
|
10f4f1db11 | ||
|
|
699626a4b8 | ||
|
|
161f690488 | ||
|
|
29d00ec352 | ||
|
|
6c26655cbb | ||
|
|
17f9240c7e | ||
|
|
cbe28583ed | ||
|
|
4ca74fa445 | ||
|
|
95f4ad06a1 | ||
|
|
f95317eb24 | ||
|
|
71c5e444f4 | ||
|
|
15515b6815 | ||
|
|
cdede872c0 | ||
|
|
548984d499 | ||
|
|
1647e44e2f | ||
|
|
d573002be5 | ||
|
|
acd688405d | ||
|
|
405a84b359 | ||
|
|
0fdc9c9109 | ||
|
|
11622b84d3 | ||
|
|
fae53c4f5b | ||
|
|
c2e7465c4e | ||
|
|
46d4098cf2 | ||
|
|
d943202e83 | ||
|
|
d7086b0702 | ||
|
|
1c01fabef0 | ||
|
|
9656389d5c | ||
|
|
d3a7d45ab9 | ||
|
|
7a9910e830 | ||
|
|
77288e4c54 | ||
|
|
2b60e8cff3 | ||
|
|
88fdcabe67 | ||
|
|
0ee459946f | ||
|
|
1e0b0279b7 | ||
|
|
8619c64109 | ||
|
|
668b20f77b | ||
|
|
801b488eb4 | ||
|
|
315601e048 | ||
|
|
72e86dafc2 | ||
|
|
bf731a8a84 | ||
|
|
965d8c7dd6 | ||
|
|
f13451ea37 | ||
|
|
6f5e94887e | ||
|
|
4b016974d2 | ||
|
|
fa5051d258 | ||
|
|
90e88f37da | ||
|
|
f6287df8f4 | ||
|
|
ec61771059 | ||
|
|
da389fdbf1 | ||
|
|
540dc35e2f | ||
|
|
842bb6f0ad | ||
|
|
51b151a73f | ||
|
|
c51093cfaf | ||
|
|
16c2360324 | ||
|
|
7d399a42ef | ||
|
|
9db5257132 | ||
|
|
8dfcb41691 | ||
|
|
25ebcff2d9 | ||
|
|
1044282c33 | ||
|
|
289353a443 | ||
|
|
5fcc59d8af | ||
|
|
742bae9169 | ||
|
|
0f368b5132 | ||
|
|
bd908aeb47 | ||
|
|
db3ba4f1a1 | ||
|
|
a432534e1a | ||
|
|
f4892918e3 | ||
|
|
9c32ea4870 | ||
|
|
9efd50a0fa | ||
|
|
0840ed5ed3 | ||
|
|
0b47d70352 | ||
|
|
c00ddc68fd | ||
|
|
993afcc367 | ||
|
|
fc10fbf9da | ||
|
|
0a27c27189 | ||
|
|
a829df86e5 | ||
|
|
de8a2ebdee | ||
|
|
ba1ffdf390 | ||
|
|
8a8bb73da3 | ||
|
|
e8e0e7fed2 | ||
|
|
35e4cf0301 | ||
|
|
0fc8c5c530 | ||
|
|
bc35186bf6 | ||
|
|
57a0486b3f | ||
|
|
0673a876d1 |
@ -7,18 +7,37 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
180636C72C29B0A400C92D86 /* InputStepper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 180636C62C29B0A400C92D86 /* InputStepper.swift */; };
|
||||
180636C92C29B0DF00C92D86 /* InputStepperLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 180636C82C29B0DF00C92D86 /* InputStepperLog.txt */; };
|
||||
1808BEBC2BA41C3200129230 /* CarouselScrollbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1808BEBB2BA41C3200129230 /* CarouselScrollbar.swift */; };
|
||||
1818D04D2C9BD2170053E73C /* ModalDialogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1818D04C2C9BD2170053E73C /* ModalDialogViewController.swift */; };
|
||||
1818D04F2C9BD3F60053E73C /* ModalDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1818D04E2C9BD3F60053E73C /* ModalDialog.swift */; };
|
||||
1818D0512C9BD4090053E73C /* ModalLaunchable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1818D0502C9BD4090053E73C /* ModalLaunchable.swift */; };
|
||||
1818D0532C9BD47C0053E73C /* ModalModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1818D0522C9BD47C0053E73C /* ModalModel.swift */; };
|
||||
1832AC572BA0791D008AE476 /* BreadcrumbCellItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1832AC562BA0791D008AE476 /* BreadcrumbCellItem.swift */; };
|
||||
183B16F32C78CF7C00BA6A10 /* CarouselSlotCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 183B16F22C78CF7C00BA6A10 /* CarouselSlotCell.swift */; };
|
||||
183B16F72C80B32200BA6A10 /* FootnoteGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 183B16F62C80B32200BA6A10 /* FootnoteGroup.swift */; };
|
||||
184023452C61E7AD00A412C8 /* PriceLockup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 184023442C61E7AD00A412C8 /* PriceLockup.swift */; };
|
||||
184023472C61E7EC00A412C8 /* PriceLockupChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 184023462C61E7EC00A412C8 /* PriceLockupChangeLog.txt */; };
|
||||
1842B1DF2BECE28B0021AFCA /* CalendarDateViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1842B1DE2BECE28B0021AFCA /* CalendarDateViewCell.swift */; };
|
||||
1842B1E12BECE7B70021AFCA /* CalendarHeaderReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1842B1E02BECE7B70021AFCA /* CalendarHeaderReusableView.swift */; };
|
||||
1842B1E32BECF0A20021AFCA /* CalendarFooterReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1842B1E22BECF0A10021AFCA /* CalendarFooterReusableView.swift */; };
|
||||
1855EC662BAABF2A002ACAC2 /* BreadcrumbItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1855EC652BAABF2A002ACAC2 /* BreadcrumbItemModel.swift */; };
|
||||
1859B30F2CBF6FEB0031CD70 /* ListUnordered.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1859B30E2CBF6FDD0031CD70 /* ListUnordered.swift */; };
|
||||
1859B31B2CBFA0180031CD70 /* ListUnorderedItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1859B31A2CBFA0180031CD70 /* ListUnorderedItemModel.swift */; };
|
||||
186D13CB2BBA8B1500986B53 /* DropdownSelect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 186D13CA2BBA8B1500986B53 /* DropdownSelect.swift */; };
|
||||
18792A902B7431F2008C0D29 /* ButtonIconBadgeIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18792A8F2B7431F2008C0D29 /* ButtonIconBadgeIndicatorModel.swift */; };
|
||||
18926F5B2C7616A500C55BF6 /* FootnoteItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18926F5A2C7616A500C55BF6 /* FootnoteItem.swift */; };
|
||||
18926F5D2C7616C600C55BF6 /* FootnoteChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 18926F5C2C7616C600C55BF6 /* FootnoteChangeLog.txt */; };
|
||||
18A3F12A2BD9298900498E4A /* Calendar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A3F1292BD9298900498E4A /* Calendar.swift */; };
|
||||
18A65A022B96E848006602CC /* Breadcrumbs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A65A012B96E848006602CC /* Breadcrumbs.swift */; };
|
||||
18A65A042B96F050006602CC /* BreadcrumbItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A65A032B96F050006602CC /* BreadcrumbItem.swift */; };
|
||||
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 */; };
|
||||
18B9763F2C11BA4A009271DF /* CarouselPaginationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B9763E2C11BA4A009271DF /* CarouselPaginationModel.swift */; };
|
||||
18C0F9462C98175900E1DD71 /* Modal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18C0F9452C98175900E1DD71 /* Modal.swift */; };
|
||||
18C0F94A2C9817C100E1DD71 /* ModalChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 18C0F9492C9817C100E1DD71 /* ModalChangeLog.txt */; };
|
||||
18FEA1AD2BDD137500A56439 /* CalendarIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1AC2BDD137500A56439 /* CalendarIndicatorModel.swift */; };
|
||||
18FEA1B52BE0E63600A56439 /* Date+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1B42BE0E63600A56439 /* Date+Extension.swift */; };
|
||||
445BA07829C07B3D0036A7C5 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 445BA07729C07B3D0036A7C5 /* Notification.swift */; };
|
||||
@ -52,6 +71,8 @@
|
||||
EA0D1C412A6AD61C00E5C127 /* Typography+Additional.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA0D1C402A6AD61C00E5C127 /* Typography+Additional.swift */; };
|
||||
EA0D1C452A6AD73000E5C127 /* RawRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA0D1C442A6AD73000E5C127 /* RawRepresentable.swift */; };
|
||||
EA0FC2C62914222900DF80B4 /* ButtonGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA0FC2C52914222900DF80B4 /* ButtonGroup.swift */; };
|
||||
EA225FC72CA4845100B6B3B3 /* LanguageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA225FC62CA4845100B6B3B3 /* LanguageManager.swift */; };
|
||||
EA225FC92CA4932900B6B3B3 /* Typography+StyleProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA225FC82CA4932900B6B3B3 /* Typography+StyleProvider.swift */; };
|
||||
EA297A5529FB07760031ED56 /* TooltipLabelAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA297A5429FB07760031ED56 /* TooltipLabelAttribute.swift */; };
|
||||
EA297A5729FB0A360031ED56 /* AppleGuidelinesTouchable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA297A5629FB0A360031ED56 /* AppleGuidelinesTouchable.swift */; };
|
||||
EA2DC9B02BE175BA004F58C5 /* RequiredRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA2DC9AF2BE175BA004F58C5 /* RequiredRule.swift */; };
|
||||
@ -63,7 +84,6 @@
|
||||
EA33617C288B19210071C351 /* VDSTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA33617B288B19210071C351 /* VDSTests.swift */; };
|
||||
EA33617D288B19210071C351 /* VDS.h in Headers */ = {isa = PBXBuildFile; fileRef = EA33616F288B19200071C351 /* VDS.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
EA3361A8288B23300071C351 /* UIColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA3361A7288B23300071C351 /* UIColor.swift */; };
|
||||
EA3361AA288B25E40071C351 /* Disabling.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA3361A9288B25E40071C351 /* Disabling.swift */; };
|
||||
EA3361AF288B26310071C351 /* FormFieldable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA3361AE288B26310071C351 /* FormFieldable.swift */; };
|
||||
EA3361B6288B2A410071C351 /* Control.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA3361B5288B2A410071C351 /* Control.swift */; };
|
||||
EA3361B8288B2AAA0071C351 /* ViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA3361B7288B2AAA0071C351 /* ViewProtocol.swift */; };
|
||||
@ -95,6 +115,7 @@
|
||||
EA6642952BCEBF9500D81DC4 /* TextLinkModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA6642942BCEBF9500D81DC4 /* TextLinkModel.swift */; };
|
||||
EA6F330E2B911E9000BACAB9 /* TextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA6F330D2B911E9000BACAB9 /* TextView.swift */; };
|
||||
EA78C7962C00CAC200430AD1 /* Groupable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA78C7952C00CAC200430AD1 /* Groupable.swift */; };
|
||||
EA7AE5592C78C7D000107C74 /* ParentViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7AE5582C78C7D000107C74 /* ParentViewProtocol.swift */; };
|
||||
EA81410B2A0E8E3C004F60D2 /* ButtonIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA81410A2A0E8E3C004F60D2 /* ButtonIcon.swift */; };
|
||||
EA8141102A127066004F60D2 /* UIColor+VDSColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA81410F2A127066004F60D2 /* UIColor+VDSColor.swift */; };
|
||||
EA89200428AECF4B006B9984 /* UITextField+Publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA89200328AECF4B006B9984 /* UITextField+Publisher.swift */; };
|
||||
@ -204,23 +225,44 @@
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
180636C62C29B0A400C92D86 /* InputStepper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputStepper.swift; sourceTree = "<group>"; };
|
||||
180636C82C29B0DF00C92D86 /* InputStepperLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = InputStepperLog.txt; sourceTree = "<group>"; };
|
||||
1808BEBB2BA41C3200129230 /* CarouselScrollbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselScrollbar.swift; sourceTree = "<group>"; };
|
||||
1808BEBF2BA456B700129230 /* CarouselScrollbarChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = CarouselScrollbarChangeLog.txt; sourceTree = "<group>"; };
|
||||
1818D04C2C9BD2170053E73C /* ModalDialogViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalDialogViewController.swift; sourceTree = "<group>"; };
|
||||
1818D04E2C9BD3F60053E73C /* ModalDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalDialog.swift; sourceTree = "<group>"; };
|
||||
1818D0502C9BD4090053E73C /* ModalLaunchable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalLaunchable.swift; sourceTree = "<group>"; };
|
||||
1818D0522C9BD47C0053E73C /* ModalModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalModel.swift; sourceTree = "<group>"; };
|
||||
1832AC562BA0791D008AE476 /* BreadcrumbCellItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreadcrumbCellItem.swift; sourceTree = "<group>"; };
|
||||
183B16F22C78CF7C00BA6A10 /* CarouselSlotCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselSlotCell.swift; sourceTree = "<group>"; };
|
||||
183B16F62C80B32200BA6A10 /* FootnoteGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FootnoteGroup.swift; sourceTree = "<group>"; };
|
||||
184023442C61E7AD00A412C8 /* PriceLockup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriceLockup.swift; sourceTree = "<group>"; };
|
||||
184023462C61E7EC00A412C8 /* PriceLockupChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = PriceLockupChangeLog.txt; sourceTree = "<group>"; };
|
||||
1842B1DE2BECE28B0021AFCA /* CalendarDateViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarDateViewCell.swift; sourceTree = "<group>"; };
|
||||
1842B1E02BECE7B70021AFCA /* CalendarHeaderReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarHeaderReusableView.swift; sourceTree = "<group>"; };
|
||||
1842B1E22BECF0A10021AFCA /* CalendarFooterReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarFooterReusableView.swift; sourceTree = "<group>"; };
|
||||
18450CF02BA1B19C009FDF2A /* BreadcrumbsChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = BreadcrumbsChangeLog.txt; sourceTree = "<group>"; };
|
||||
1855EC652BAABF2A002ACAC2 /* BreadcrumbItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreadcrumbItemModel.swift; sourceTree = "<group>"; };
|
||||
1859B30E2CBF6FDD0031CD70 /* ListUnordered.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListUnordered.swift; sourceTree = "<group>"; };
|
||||
1859B3122CBF70AB0031CD70 /* ListUnordered.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ListUnordered.txt; sourceTree = "<group>"; };
|
||||
1859B31A2CBFA0180031CD70 /* ListUnorderedItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListUnorderedItemModel.swift; sourceTree = "<group>"; };
|
||||
186B2A892B88DA7F001AB71F /* TextAreaChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = TextAreaChangeLog.txt; sourceTree = "<group>"; };
|
||||
186D13CA2BBA8B1500986B53 /* DropdownSelect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropdownSelect.swift; sourceTree = "<group>"; };
|
||||
186D13CE2BBC36EE00986B53 /* DropdownSelectChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = DropdownSelectChangeLog.txt; sourceTree = "<group>"; };
|
||||
18792A8F2B7431F2008C0D29 /* ButtonIconBadgeIndicatorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonIconBadgeIndicatorModel.swift; sourceTree = "<group>"; };
|
||||
18926F5A2C7616A500C55BF6 /* FootnoteItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FootnoteItem.swift; sourceTree = "<group>"; };
|
||||
18926F5C2C7616C600C55BF6 /* FootnoteChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = FootnoteChangeLog.txt; 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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
18C0F9452C98175900E1DD71 /* Modal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modal.swift; sourceTree = "<group>"; };
|
||||
18C0F9492C9817C100E1DD71 /* ModalChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ModalChangeLog.txt; 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>"; };
|
||||
18FEA1B82BE1301700A56439 /* CalendarChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = CalendarChangeLog.txt; sourceTree = "<group>"; };
|
||||
@ -235,7 +277,7 @@
|
||||
44CCF4942C0493A1005C9C5E /* TableChangeLog.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = TableChangeLog.txt; sourceTree = "<group>"; };
|
||||
5F21D7BE28DCEB3D003E7CD6 /* Useable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Useable.swift; sourceTree = "<group>"; };
|
||||
5FC35BE228D51405004EBEAC /* Button.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Button.swift; sourceTree = "<group>"; };
|
||||
710607942B91A99500F2863F /* TitleletChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = TitleletChangeLog.txt; sourceTree = "<group>"; };
|
||||
710607942B91A99500F2863F /* TileletChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = TileletChangeLog.txt; sourceTree = "<group>"; };
|
||||
7115BD3B2B84C0C200E0A610 /* TileContainerChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = TileContainerChangeLog.txt; sourceTree = "<group>"; };
|
||||
71ACE89B2BA0451200FB6ADC /* PaginationContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationContainer.swift; sourceTree = "<group>"; };
|
||||
71ACE89D2BA1CC1700FB6ADC /* TiletEyebrowModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TiletEyebrowModel.swift; sourceTree = "<group>"; };
|
||||
@ -263,6 +305,8 @@
|
||||
EA0D1C442A6AD73000E5C127 /* RawRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RawRepresentable.swift; sourceTree = "<group>"; };
|
||||
EA0FC2C52914222900DF80B4 /* ButtonGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonGroup.swift; sourceTree = "<group>"; };
|
||||
EA21C5DA2B600EDD00CFC139 /* VDSTokens.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = VDSTokens.xcframework; path = ../SharedFrameworks/VDSTokens.xcframework; sourceTree = "<group>"; };
|
||||
EA225FC62CA4845100B6B3B3 /* LanguageManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageManager.swift; sourceTree = "<group>"; };
|
||||
EA225FC82CA4932900B6B3B3 /* Typography+StyleProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Typography+StyleProvider.swift"; sourceTree = "<group>"; };
|
||||
EA297A5429FB07760031ED56 /* TooltipLabelAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TooltipLabelAttribute.swift; sourceTree = "<group>"; };
|
||||
EA297A5629FB0A360031ED56 /* AppleGuidelinesTouchable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleGuidelinesTouchable.swift; sourceTree = "<group>"; };
|
||||
EA2DC9AF2BE175BA004F58C5 /* RequiredRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequiredRule.swift; sourceTree = "<group>"; };
|
||||
@ -275,7 +319,6 @@
|
||||
EA336176288B19210071C351 /* VDSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = VDSTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
EA33617B288B19210071C351 /* VDSTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VDSTests.swift; sourceTree = "<group>"; };
|
||||
EA3361A7288B23300071C351 /* UIColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIColor.swift; sourceTree = "<group>"; };
|
||||
EA3361A9288B25E40071C351 /* Disabling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Disabling.swift; sourceTree = "<group>"; };
|
||||
EA3361AE288B26310071C351 /* FormFieldable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormFieldable.swift; sourceTree = "<group>"; };
|
||||
EA3361B5288B2A410071C351 /* Control.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Control.swift; sourceTree = "<group>"; };
|
||||
EA3361B7288B2AAA0071C351 /* ViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewProtocol.swift; sourceTree = "<group>"; };
|
||||
@ -310,6 +353,7 @@
|
||||
EA78C7952C00CAC200430AD1 /* Groupable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Groupable.swift; sourceTree = "<group>"; };
|
||||
EA78C7A12C0E63D200430AD1 /* vds-dev.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "vds-dev.xcconfig"; sourceTree = "<group>"; };
|
||||
EA78C7A22C0E63DD00430AD1 /* vds.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = vds.xcconfig; sourceTree = "<group>"; };
|
||||
EA7AE5582C78C7D000107C74 /* ParentViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParentViewProtocol.swift; sourceTree = "<group>"; };
|
||||
EA81410A2A0E8E3C004F60D2 /* ButtonIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonIcon.swift; sourceTree = "<group>"; };
|
||||
EA81410F2A127066004F60D2 /* UIColor+VDSColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+VDSColor.swift"; sourceTree = "<group>"; };
|
||||
EA89200328AECF4B006B9984 /* UITextField+Publisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextField+Publisher.swift"; sourceTree = "<group>"; };
|
||||
@ -442,6 +486,15 @@
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
180636C52C29B06200C92D86 /* InputStepper */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
180636C62C29B0A400C92D86 /* InputStepper.swift */,
|
||||
180636C82C29B0DF00C92D86 /* InputStepperLog.txt */,
|
||||
);
|
||||
path = InputStepper;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
1808BEBA2BA41B1D00129230 /* CarouselScrollbar */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -451,6 +504,25 @@
|
||||
path = CarouselScrollbar;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
184023432C61E78D00A412C8 /* PriceLockup */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
184023442C61E7AD00A412C8 /* PriceLockup.swift */,
|
||||
184023462C61E7EC00A412C8 /* PriceLockupChangeLog.txt */,
|
||||
);
|
||||
path = PriceLockup;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
1859B30D2CBF6EF80031CD70 /* ListUnordered */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1859B30E2CBF6FDD0031CD70 /* ListUnordered.swift */,
|
||||
1859B31A2CBFA0180031CD70 /* ListUnorderedItemModel.swift */,
|
||||
1859B3122CBF70AB0031CD70 /* ListUnordered.txt */,
|
||||
);
|
||||
path = ListUnordered;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
186D13C92BBA8A3500986B53 /* DropdownSelect */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -461,6 +533,16 @@
|
||||
path = DropdownSelect;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
18926F592C76168300C55BF6 /* Footnote */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
18926F5A2C7616A500C55BF6 /* FootnoteItem.swift */,
|
||||
183B16F62C80B32200BA6A10 /* FootnoteGroup.swift */,
|
||||
18926F5C2C7616C600C55BF6 /* FootnoteChangeLog.txt */,
|
||||
);
|
||||
path = Footnote;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
18A3F1202BD8F5DE00498E4A /* Calendar */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -487,6 +569,31 @@
|
||||
path = Breadcrumbs;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
18AE874E2C06FD610075F181 /* Carousel */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
18AE874F2C06FDA60075F181 /* Carousel.swift */,
|
||||
183B16F22C78CF7C00BA6A10 /* CarouselSlotCell.swift */,
|
||||
18B9763E2C11BA4A009271DF /* CarouselPaginationModel.swift */,
|
||||
18B42AC52C09D197008D6262 /* CarouselSlotAlignmentModel.swift */,
|
||||
18AE87532C06FE610075F181 /* CarouselChangeLog.txt */,
|
||||
);
|
||||
path = Carousel;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
18C0F9442C980CE500E1DD71 /* Modal */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
18C0F9452C98175900E1DD71 /* Modal.swift */,
|
||||
1818D04E2C9BD3F60053E73C /* ModalDialog.swift */,
|
||||
1818D04C2C9BD2170053E73C /* ModalDialogViewController.swift */,
|
||||
1818D0502C9BD4090053E73C /* ModalLaunchable.swift */,
|
||||
1818D0522C9BD47C0053E73C /* ModalModel.swift */,
|
||||
18C0F9492C9817C100E1DD71 /* ModalChangeLog.txt */,
|
||||
);
|
||||
path = Modal;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
440B84C82BD8E0CE004A732A /* Table */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -657,16 +764,22 @@
|
||||
18A65A002B96E7E1006602CC /* Breadcrumbs */,
|
||||
EA0FC2BE2912D18200DF80B4 /* Buttons */,
|
||||
18A3F1202BD8F5DE00498E4A /* Calendar */,
|
||||
18AE874E2C06FD610075F181 /* Carousel */,
|
||||
1808BEBA2BA41B1D00129230 /* CarouselScrollbar */,
|
||||
EAF7F092289985E200B287F5 /* Checkbox */,
|
||||
EAC58C1F2BF127F000BA39FA /* DatePicker */,
|
||||
186D13C92BBA8A3500986B53 /* DropdownSelect */,
|
||||
18926F592C76168300C55BF6 /* Footnote */,
|
||||
EA985BF3296C609E00F2FF2E /* Icon */,
|
||||
180636C52C29B06200C92D86 /* InputStepper */,
|
||||
EA3362412892EF700071C351 /* Label */,
|
||||
44604AD529CE195300E62B51 /* Line */,
|
||||
1859B30D2CBF6EF80031CD70 /* ListUnordered */,
|
||||
EAD0688C2A55F801002E3A2D /* Loader */,
|
||||
18C0F9442C980CE500E1DD71 /* Modal */,
|
||||
445BA07629C07ABA0036A7C5 /* Notification */,
|
||||
71B23C2B2B91FA510027F7D9 /* Pagination */,
|
||||
184023432C61E78D00A412C8 /* PriceLockup */,
|
||||
EA89200B28B530F0006B9984 /* RadioBox */,
|
||||
EAF7F11428A1470D00B287F5 /* RadioButton */,
|
||||
440B84C82BD8E0CE004A732A /* Table */,
|
||||
@ -724,7 +837,6 @@
|
||||
EAF1FE9829D4850E00101452 /* Clickable.swift */,
|
||||
EAA5EEDF28F49DB3003B3210 /* Colorable.swift */,
|
||||
EAACB8972B92706F006A3869 /* DefaultValuing.swift */,
|
||||
EA3361A9288B25E40071C351 /* Disabling.swift */,
|
||||
71BFA7092B7F70E6000DCE33 /* DropShadowable.swift */,
|
||||
EAF978202A99035B00C2FEA9 /* Enabling.swift */,
|
||||
EA5E305929510F8B0082B959 /* EnumSubset.swift */,
|
||||
@ -733,6 +845,7 @@
|
||||
EA78C7952C00CAC200430AD1 /* Groupable.swift */,
|
||||
EA33624628931B050071C351 /* Initable.swift */,
|
||||
EA471F392A95587500CE9E58 /* LayoutConstraintable.swift */,
|
||||
EA7AE5582C78C7D000107C74 /* ParentViewProtocol.swift */,
|
||||
EA985C7C297DAED300F2FF2E /* Primitive.swift */,
|
||||
EAF7F0A5289B0CE000B287F5 /* Resetable.swift */,
|
||||
EA3361C8289054C50071C351 /* Surfaceable.swift */,
|
||||
@ -747,12 +860,13 @@
|
||||
EA3361B4288B2A360071C351 /* Classes */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */,
|
||||
EAF2F4882C2A1075007BFEDC /* AlertViewController.swift */,
|
||||
EA985C1C296CD13600F2FF2E /* BundleManager.swift */,
|
||||
EAC58C282BF4118C00BA39FA /* ClearPopoverViewController.swift */,
|
||||
EAF7F0B8289C139800B287F5 /* ColorConfiguration.swift */,
|
||||
EA225FC62CA4845100B6B3B3 /* LanguageManager.swift */,
|
||||
EAB5FEF02927F4AA00998C17 /* SelfSizingCollectionView.swift */,
|
||||
EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */,
|
||||
EAF2F4882C2A1075007BFEDC /* AlertViewController.swift */,
|
||||
);
|
||||
path = Classes;
|
||||
sourceTree = "<group>";
|
||||
@ -865,7 +979,7 @@
|
||||
EA985BE72968951C00F2FF2E /* TileletTitleModel.swift */,
|
||||
EA985BE929689B6D00F2FF2E /* TileletSubTitleModel.swift */,
|
||||
EA985C2C296F03FE00F2FF2E /* TileletIconModels.swift */,
|
||||
710607942B91A99500F2863F /* TitleletChangeLog.txt */,
|
||||
710607942B91A99500F2863F /* TileletChangeLog.txt */,
|
||||
);
|
||||
path = Tilelet;
|
||||
sourceTree = "<group>";
|
||||
@ -920,6 +1034,7 @@
|
||||
EA0D1C3C2A6AD57600E5C127 /* Typography+Enums.swift */,
|
||||
EA0D1C382A6AD4DF00E5C127 /* Typography+SpacingConfig.swift */,
|
||||
EA0D1C3A2A6AD51B00E5C127 /* Typogprahy+Styles.swift */,
|
||||
EA225FC82CA4932900B6B3B3 /* Typography+StyleProvider.swift */,
|
||||
);
|
||||
path = Typography;
|
||||
sourceTree = "<group>";
|
||||
@ -1165,10 +1280,14 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
18926F5D2C7616C600C55BF6 /* FootnoteChangeLog.txt in Resources */,
|
||||
EA3362042891E14D0071C351 /* VerizonNHGeTX-Bold.otf in Resources */,
|
||||
184023472C61E7EC00A412C8 /* PriceLockupChangeLog.txt in Resources */,
|
||||
EA3362072891E14D0071C351 /* VerizonNHGeDS-Regular.otf in Resources */,
|
||||
EA3362062891E14D0071C351 /* VerizonNHGeTX-Regular.otf in Resources */,
|
||||
EA3362052891E14D0071C351 /* VerizonNHGeDS-Bold.otf in Resources */,
|
||||
180636C92C29B0DF00C92D86 /* InputStepperLog.txt in Resources */,
|
||||
18C0F94A2C9817C100E1DD71 /* ModalChangeLog.txt in Resources */,
|
||||
EAA5EEB928ECD24B003B3210 /* Icons.xcassets in Resources */,
|
||||
EAA5EEE428F5B855003B3210 /* VerizonNHGDS-Light.otf in Resources */,
|
||||
);
|
||||
@ -1212,6 +1331,7 @@
|
||||
files = (
|
||||
445BA07829C07B3D0036A7C5 /* Notification.swift in Sources */,
|
||||
EA5E304C294CBDD00082B959 /* TileContainer.swift in Sources */,
|
||||
180636C72C29B0A400C92D86 /* InputStepper.swift in Sources */,
|
||||
EAF7F0A6289B0CE000B287F5 /* Resetable.swift in Sources */,
|
||||
EA985C2D296F03FE00F2FF2E /* TileletIconModels.swift in Sources */,
|
||||
EA89200428AECF4B006B9984 /* UITextField+Publisher.swift in Sources */,
|
||||
@ -1226,6 +1346,7 @@
|
||||
EA3361C328902D960071C351 /* Toggle.swift in Sources */,
|
||||
EAF7F0A0289AB7EC00B287F5 /* View.swift in Sources */,
|
||||
EAC58C232BF2824200BA39FA /* DatePicker.swift in Sources */,
|
||||
183B16F72C80B32200BA6A10 /* FootnoteGroup.swift in Sources */,
|
||||
EA89201328B568D8006B9984 /* RadioBoxItem.swift in Sources */,
|
||||
71FC86E42B9841AC00700965 /* PaginationFlowLayout.swift in Sources */,
|
||||
EAC9258C2911C9DE00091998 /* InputField.swift in Sources */,
|
||||
@ -1242,6 +1363,7 @@
|
||||
EA6642952BCEBF9500D81DC4 /* TextLinkModel.swift in Sources */,
|
||||
71FC86E22B97483000700965 /* Clamping.swift in Sources */,
|
||||
EAF7F0B3289B1ADC00B287F5 /* ActionLabelAttribute.swift in Sources */,
|
||||
1859B30F2CBF6FEB0031CD70 /* ListUnordered.swift in Sources */,
|
||||
1855EC662BAABF2A002ACAC2 /* BreadcrumbItemModel.swift in Sources */,
|
||||
EAC925832911B35400091998 /* TextLinkCaret.swift in Sources */,
|
||||
EA33622E2891EA3C0071C351 /* DispatchQueue+Once.swift in Sources */,
|
||||
@ -1252,10 +1374,12 @@
|
||||
EA8141102A127066004F60D2 /* UIColor+VDSColor.swift in Sources */,
|
||||
EAF7F0AF289B144C00B287F5 /* UnderlineLabelAttribute.swift in Sources */,
|
||||
EA0D1C412A6AD61C00E5C127 /* Typography+Additional.swift in Sources */,
|
||||
18C0F9462C98175900E1DD71 /* Modal.swift in Sources */,
|
||||
EAC925842911C63100091998 /* Colorable.swift in Sources */,
|
||||
18B463A42BBD3C46005C4528 /* DropdownOptionModel.swift in Sources */,
|
||||
EAF2F4762C231EAA007BFEDC /* AccessibilityActionElement.swift in Sources */,
|
||||
EAC58BFD2BE935C300BA39FA /* TitleLockupTextColor.swift in Sources */,
|
||||
18926F5B2C7616A500C55BF6 /* FootnoteItem.swift in Sources */,
|
||||
EAACB89A2B927108006A3869 /* Valuing.swift in Sources */,
|
||||
EAE785312BA0A438009428EA /* UIImage+Helper.swift in Sources */,
|
||||
EAF193422C134F3400C68D18 /* Table.swift in Sources */,
|
||||
@ -1268,6 +1392,7 @@
|
||||
EA596ABD2A16B4EC00300C4B /* Tab.swift in Sources */,
|
||||
71ACE89E2BA1CC1700FB6ADC /* TiletEyebrowModel.swift in Sources */,
|
||||
EAF7F11728A1475A00B287F5 /* RadioButtonItem.swift in Sources */,
|
||||
1818D0512C9BD4090053E73C /* ModalLaunchable.swift in Sources */,
|
||||
EA985BEE2968A92400F2FF2E /* TitleLockupSubTitleModel.swift in Sources */,
|
||||
EA2DC9B22BE175E6004F58C5 /* CharacterCountRule.swift in Sources */,
|
||||
EA985BF22968B5BB00F2FF2E /* TitleLockupTextStyle.swift in Sources */,
|
||||
@ -1288,20 +1413,25 @@
|
||||
EAF7F0A4289B017C00B287F5 /* LabelAttributeModel.swift in Sources */,
|
||||
EA0B18022A9E236900F2D0CD /* SelectorGroupBase.swift in Sources */,
|
||||
EA5F86D02A1F936100BC83E4 /* TabsContainer.swift in Sources */,
|
||||
EA7AE5592C78C7D000107C74 /* ParentViewProtocol.swift in Sources */,
|
||||
EAF7F0B1289B177F00B287F5 /* ColorLabelAttribute.swift in Sources */,
|
||||
EAC9258F2911C9DE00091998 /* EntryFieldBase.swift in Sources */,
|
||||
18B9763F2C11BA4A009271DF /* CarouselPaginationModel.swift in Sources */,
|
||||
EAB1D2EA28AE84AA00DAE764 /* UIControlPublisher.swift in Sources */,
|
||||
EAD068922A560B65002E3A2D /* LoaderViewController.swift in Sources */,
|
||||
44BD43B62C04866600644F87 /* TableRowModel.swift in Sources */,
|
||||
71FC86DA2B96F44C00700965 /* PaginationButton.swift in Sources */,
|
||||
EABFEB642A26473700C4C106 /* NSAttributedString.swift in Sources */,
|
||||
EA225FC72CA4845100B6B3B3 /* LanguageManager.swift in Sources */,
|
||||
EAF7F13328A2A16500B287F5 /* AttachmentLabelAttributeModel.swift in Sources */,
|
||||
EA0FC2C62914222900DF80B4 /* ButtonGroup.swift in Sources */,
|
||||
EA89200628B526D6006B9984 /* CheckboxGroup.swift in Sources */,
|
||||
EA8E40932A82889500934ED3 /* TooltipDialog.swift in Sources */,
|
||||
44604AD429CE186A00E62B51 /* NotificationButtonModel.swift in Sources */,
|
||||
EAD8D2C128BFDE8B006EB6A6 /* UIGestureRecognizer+Publisher.swift in Sources */,
|
||||
18B42AC62C09D197008D6262 /* CarouselSlotAlignmentModel.swift in Sources */,
|
||||
71B23C2D2B91FA690027F7D9 /* Pagination.swift in Sources */,
|
||||
1859B31B2CBFA0180031CD70 /* ListUnorderedItemModel.swift in Sources */,
|
||||
EA0D1C372A681CCE00E5C127 /* ToggleView.swift in Sources */,
|
||||
EAF7F0B9289C139800B287F5 /* ColorConfiguration.swift in Sources */,
|
||||
EA3361BD288B2C760071C351 /* TypeAlias.swift in Sources */,
|
||||
@ -1316,6 +1446,7 @@
|
||||
EAF2F4892C2A1075007BFEDC /* AlertViewController.swift in Sources */,
|
||||
EA0D1C3D2A6AD57600E5C127 /* Typography+Enums.swift in Sources */,
|
||||
EAF1FE9B29DB1A6000101452 /* Changeable.swift in Sources */,
|
||||
1818D04F2C9BD3F60053E73C /* ModalDialog.swift in Sources */,
|
||||
EAC58C0C2BED01D500BA39FA /* Telephone.swift in Sources */,
|
||||
EAF7F0A2289AFB3900B287F5 /* Errorable.swift in Sources */,
|
||||
EA8E40912A7D3F6300934ED3 /* UIView+Accessibility.swift in Sources */,
|
||||
@ -1330,6 +1461,7 @@
|
||||
44604AD729CE196600E62B51 /* Line.swift in Sources */,
|
||||
1808BEBC2BA41C3200129230 /* CarouselScrollbar.swift in Sources */,
|
||||
EAF978212A99035B00C2FEA9 /* Enabling.swift in Sources */,
|
||||
EA225FC92CA4932900B6B3B3 /* Typography+StyleProvider.swift in Sources */,
|
||||
EAC58C062BED000200BA39FA /* CreditCard.swift in Sources */,
|
||||
EA5E3058295105A40082B959 /* Tilelet.swift in Sources */,
|
||||
186D13CB2BBA8B1500986B53 /* DropdownSelect.swift in Sources */,
|
||||
@ -1339,6 +1471,7 @@
|
||||
EA0B18052A9E2D2D00F2D0CD /* SelectorBase.swift in Sources */,
|
||||
EAC71A1D2A2E155A00E47A9F /* Checkbox.swift in Sources */,
|
||||
EAF7F0AB289B13FD00B287F5 /* TextStyleLabelAttribute.swift in Sources */,
|
||||
18AE87502C06FDA60075F181 /* Carousel.swift in Sources */,
|
||||
EAB1D29C28A5618900DAE764 /* RadioButtonGroup.swift in Sources */,
|
||||
EA81410B2A0E8E3C004F60D2 /* ButtonIcon.swift in Sources */,
|
||||
EA985BE629688F6A00F2FF2E /* TileletBadgeModel.swift in Sources */,
|
||||
@ -1346,6 +1479,7 @@
|
||||
EA985BF02968A93600F2FF2E /* TitleLockupEyebrowModel.swift in Sources */,
|
||||
EA5E30532950DDA60082B959 /* TitleLockup.swift in Sources */,
|
||||
EAD062B02A3B873E0015965D /* BadgeIndicator.swift in Sources */,
|
||||
183B16F32C78CF7C00BA6A10 /* CarouselSlotCell.swift in Sources */,
|
||||
44A952DD2BE3DA820009F874 /* TableFlowLayout.swift in Sources */,
|
||||
EAA5EEB528ECBFB4003B3210 /* ImageLabelAttribute.swift in Sources */,
|
||||
18792A902B7431F2008C0D29 /* ButtonIconBadgeIndicatorModel.swift in Sources */,
|
||||
@ -1353,7 +1487,6 @@
|
||||
EAB5FF0129424ACB00998C17 /* UIControl.swift in Sources */,
|
||||
EA985BF5296C60C000F2FF2E /* Icon.swift in Sources */,
|
||||
1842B1E32BECF0A20021AFCA /* CalendarFooterReusableView.swift in Sources */,
|
||||
EA3361AA288B25E40071C351 /* Disabling.swift in Sources */,
|
||||
EA3361B6288B2A410071C351 /* Control.swift in Sources */,
|
||||
EAC58C122BED0DDD00BA39FA /* Text.swift in Sources */,
|
||||
5F21D7BF28DCEB3D003E7CD6 /* Useable.swift in Sources */,
|
||||
@ -1366,13 +1499,16 @@
|
||||
EAB5FED429267EB300998C17 /* UIView+NSLayoutConstraint.swift in Sources */,
|
||||
EAB2376829E9992800AABE9A /* TooltipAlertViewController.swift in Sources */,
|
||||
EA33623E2892EE950071C351 /* UIDevice.swift in Sources */,
|
||||
1818D04D2C9BD2170053E73C /* ModalDialogViewController.swift in Sources */,
|
||||
EA985C692971B90B00F2FF2E /* IconSize.swift in Sources */,
|
||||
71FC86E02B973AE500700965 /* DropShadowConfiguration.swift in Sources */,
|
||||
EA3362302891EB4A0071C351 /* Font.swift in Sources */,
|
||||
EAC58C0E2BED021600BA39FA /* Password.swift in Sources */,
|
||||
EAF7F0AD289B142900B287F5 /* StrikeThroughLabelAttribute.swift in Sources */,
|
||||
EAB5FEF12927F4AA00998C17 /* SelfSizingCollectionView.swift in Sources */,
|
||||
184023452C61E7AD00A412C8 /* PriceLockup.swift in Sources */,
|
||||
EA3361B8288B2AAA0071C351 /* ViewProtocol.swift in Sources */,
|
||||
1818D0532C9BD47C0053E73C /* ModalModel.swift in Sources */,
|
||||
EA3361A8288B23300071C351 /* UIColor.swift in Sources */,
|
||||
EA2DC9B42BE2C6FE004F58C5 /* TextField.swift in Sources */,
|
||||
EAC58C182BED0E2300BA39FA /* SecurityCode.swift in Sources */,
|
||||
@ -1535,7 +1671,7 @@
|
||||
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 68;
|
||||
CURRENT_PROJECT_VERSION = 75;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@ -1573,7 +1709,7 @@
|
||||
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 68;
|
||||
CURRENT_PROJECT_VERSION = 75;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
|
||||
@ -77,20 +77,30 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Lifecycle
|
||||
//--------------------------------------------------
|
||||
open func initialSetup() {
|
||||
private func initialSetup() {
|
||||
if !initialSetupPerformed {
|
||||
initialSetupPerformed = true
|
||||
shouldUpdateView = false
|
||||
setup()
|
||||
setDefaults()
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
open func setup() {
|
||||
backgroundColor = .clear
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
insetsLayoutMarginsFromSafeArea = false
|
||||
}
|
||||
|
||||
open func setDefaults() {
|
||||
backgroundColor = .clear
|
||||
surface = .light
|
||||
isEnabled = true
|
||||
onClick = nil
|
||||
userInfo.removeAll()
|
||||
}
|
||||
|
||||
open func updateView() { }
|
||||
|
||||
open func updateAccessibility() {
|
||||
@ -109,11 +119,10 @@ open class Control: UIControl, ViewProtocol, UserInfoable, Clickable {
|
||||
}
|
||||
|
||||
open func reset() {
|
||||
backgroundColor = .clear
|
||||
surface = .light
|
||||
isEnabled = true
|
||||
onClick = nil
|
||||
userInfo.removeAll()
|
||||
shouldUpdateView = false
|
||||
setDefaults()
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
|
||||
@ -28,7 +28,9 @@ public protocol SelectorControlable: Control, Changeable {
|
||||
}
|
||||
|
||||
/// Base Class used to build out a Selector control.
|
||||
open class SelectorBase: Control, SelectorControlable {
|
||||
@objc(VDSSelectorBase)
|
||||
open class SelectorBase: Control, SelectorControlable, ParentViewProtocol {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
@ -47,6 +49,8 @@ open class SelectorBase: Control, SelectorControlable {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
open var children: [any ViewProtocol] { [selectorView] }
|
||||
|
||||
open var onChangeSubscriber: AnyCancellable?
|
||||
|
||||
open var size = CGSize(width: 20, height: 20) { didSet { setNeedsUpdate() } }
|
||||
@ -82,9 +86,6 @@ open class SelectorBase: Control, SelectorControlable {
|
||||
|
||||
open var selectorColorConfiguration = ControlColorConfiguration() { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// The natural size for the receiving view, considering only properties of the view itself.
|
||||
open override var intrinsicContentSize: CGSize { size }
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Properties
|
||||
//--------------------------------------------------
|
||||
@ -98,13 +99,35 @@ open class SelectorBase: Control, SelectorControlable {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
/// 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 = true
|
||||
accessibilityTraits = .button
|
||||
|
||||
let layoutGuide = UILayoutGuide()
|
||||
addLayoutGuide(layoutGuide)
|
||||
layoutGuide
|
||||
.pinTop(0)
|
||||
.pinLeading(0)
|
||||
.pinTrailing(0, .defaultHigh)
|
||||
.pinBottom(0, .defaultHigh)
|
||||
.width(size.width)
|
||||
.height(size.height)
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
|
||||
showError = false
|
||||
|
||||
onClick = { control in
|
||||
control.toggle()
|
||||
}
|
||||
|
||||
onChange = nil
|
||||
|
||||
bridge_accessibilityLabelBlock = { [weak self] in
|
||||
guard let self else { return "" }
|
||||
return "\(Self.self)\(showError ? ", error" : "")"
|
||||
@ -116,14 +139,6 @@ open class SelectorBase: Control, SelectorControlable {
|
||||
}
|
||||
}
|
||||
|
||||
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
|
||||
isAccessibilityElement = true
|
||||
accessibilityTraits = .button
|
||||
}
|
||||
|
||||
open override func updateView() {
|
||||
super.updateView()
|
||||
setNeedsLayout()
|
||||
|
||||
@ -39,7 +39,7 @@ extension SelectorGroupSingleSelect {
|
||||
}
|
||||
|
||||
/// Base Class used for any Grouped Form Control of a Selector Type.
|
||||
open class SelectorGroupBase<SelectorItemType: Groupable>: Control, SelectorGroup, Changeable {
|
||||
open class SelectorGroupBase<SelectorItemType: Groupable>: Control, SelectorGroup, Changeable, ParentViewProtocol {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Properties
|
||||
@ -57,6 +57,8 @@ open class SelectorGroupBase<SelectorItemType: Groupable>: Control, SelectorGrou
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
open var children: [any ViewProtocol] { items }
|
||||
|
||||
/// Array of the HandlerType registered.
|
||||
/// Array of HandlerType that the user will have the ability to select from.
|
||||
open var items: [SelectorItemType] = [] {
|
||||
@ -65,6 +67,16 @@ open class SelectorGroupBase<SelectorItemType: Groupable>: Control, SelectorGrou
|
||||
}
|
||||
|
||||
didSet {
|
||||
setItemsActions()
|
||||
for selector in items {
|
||||
mainStackView.addArrangedSubview(selector)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open var onChangeSubscriber: AnyCancellable?
|
||||
|
||||
private func setItemsActions() {
|
||||
for selector in items {
|
||||
selector.onClick = { [weak self] handler in
|
||||
self?.didSelect(handler)
|
||||
@ -76,13 +88,8 @@ open class SelectorGroupBase<SelectorItemType: Groupable>: Control, SelectorGrou
|
||||
self?.didSelect(handler)
|
||||
self?.setNeedsUpdate()
|
||||
}
|
||||
|
||||
mainStackView.addArrangedSubview(selector)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open var onChangeSubscriber: AnyCancellable?
|
||||
|
||||
/// Whether the Control is enabled or not.
|
||||
override open var isEnabled: Bool {
|
||||
@ -115,6 +122,12 @@ open class SelectorGroupBase<SelectorItemType: Groupable>: Control, SelectorGrou
|
||||
.pinBottom(0, .defaultHigh)
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
onChange = nil
|
||||
items = []
|
||||
}
|
||||
|
||||
/// Handler for the Group to override on a select event.
|
||||
/// - Parameter selectedControl: Selected Control the user interacted.
|
||||
open func didSelect(_ selectedControl: SelectorItemType) {
|
||||
@ -127,13 +140,6 @@ open class SelectorGroupBase<SelectorItemType: Groupable>: Control, SelectorGrou
|
||||
self?.sendActions(for: .valueChanged)
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
onChange = nil
|
||||
items.forEach{ $0.reset() }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ import Combine
|
||||
import VDSCoreTokens
|
||||
|
||||
/// Base Class used to build out a SelectorControlable control.
|
||||
open class SelectorItemBase<Selector: SelectorBase>: Control, Errorable, Changeable, Groupable {
|
||||
open class SelectorItemBase<Selector: SelectorBase>: Control, Errorable, Changeable, Groupable, ParentViewProtocol {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
@ -43,13 +43,14 @@ open class SelectorItemBase<Selector: SelectorBase>: Control, Errorable, Changea
|
||||
|
||||
private var mainStackView = UIStackView().with {
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
$0.alignment = .top
|
||||
$0.alignment = .fill
|
||||
$0.axis = .vertical
|
||||
}
|
||||
|
||||
private var selectorStackView = UIStackView().with {
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
$0.alignment = .top
|
||||
$0.alignment = .fill
|
||||
|
||||
$0.axis = .horizontal
|
||||
}
|
||||
|
||||
@ -61,23 +62,28 @@ open class SelectorItemBase<Selector: SelectorBase>: Control, Errorable, Changea
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
open var children: [any ViewProtocol] { [label, childLabel, errorLabel, selectorView] }
|
||||
|
||||
open var onChangeSubscriber: AnyCancellable?
|
||||
|
||||
/// Label used to render labelText.
|
||||
open var label = Label().with {
|
||||
$0.setContentCompressionResistancePriority(.required, for: .vertical)
|
||||
$0.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
$0.textStyle = .boldBodyLarge
|
||||
}
|
||||
|
||||
/// Label used to render childText.
|
||||
open var childLabel = Label().with {
|
||||
$0.setContentCompressionResistancePriority(.required, for: .vertical)
|
||||
$0.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
$0.textStyle = .bodyLarge
|
||||
}
|
||||
|
||||
/// Label used to render errorText.
|
||||
open var errorLabel = Label().with {
|
||||
$0.setContentCompressionResistancePriority(.required, for: .vertical)
|
||||
$0.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
$0.textStyle = .bodyMedium
|
||||
}
|
||||
|
||||
@ -157,9 +163,38 @@ open class SelectorItemBase<Selector: SelectorBase>: Control, Errorable, Changea
|
||||
//--------------------------------------------------
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
/// 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()
|
||||
|
||||
selectorView.isAccessibilityElement = true
|
||||
isAccessibilityElement = false
|
||||
addSubview(mainStackView)
|
||||
|
||||
//wrap the selectorView in a view that won't stretch it
|
||||
//do this by not pinning the bottom
|
||||
let selectorViewWrapper = UIView().with { $0.translatesAutoresizingMaskIntoConstraints = false }
|
||||
selectorViewWrapper.addSubview(selectorView)
|
||||
selectorView.pinTop().pinLeading().pinTrailing().pinBottomLessThanOrEqualTo()
|
||||
|
||||
mainStackView.isUserInteractionEnabled = false
|
||||
mainStackView.addArrangedSubview(selectorStackView)
|
||||
mainStackView.addArrangedSubview(errorLabel)
|
||||
selectorStackView.addArrangedSubview(selectorViewWrapper)
|
||||
selectorStackView.addArrangedSubview(selectorLabelStackView)
|
||||
selectorLabelStackView.addArrangedSubview(label)
|
||||
selectorLabelStackView.addArrangedSubview(childLabel)
|
||||
mainStackView
|
||||
.pinTop()
|
||||
.pinLeading()
|
||||
.pinTrailing()
|
||||
.pinBottom(0, .defaultHigh)
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
|
||||
onClick = { [weak self] control in
|
||||
guard let self, isEnabled else { return }
|
||||
toggle()
|
||||
@ -204,28 +239,22 @@ open class SelectorItemBase<Selector: SelectorBase>: Control, Errorable, Changea
|
||||
return !isEnabled ? "" : "Double tap to activate."
|
||||
}
|
||||
|
||||
}
|
||||
label.textStyle = .boldBodyLarge
|
||||
childLabel.textStyle = .bodyLarge
|
||||
errorLabel.textStyle = .bodyMedium
|
||||
|
||||
/// 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()
|
||||
labelText = nil
|
||||
labelTextAttributes = nil
|
||||
labelAttributedText = nil
|
||||
childText = nil
|
||||
childTextAttributes = nil
|
||||
childAttributedText = nil
|
||||
showError = false
|
||||
errorText = nil
|
||||
inputId = nil
|
||||
isSelected = false
|
||||
|
||||
selectorView.isAccessibilityElement = true
|
||||
isAccessibilityElement = false
|
||||
addSubview(mainStackView)
|
||||
|
||||
mainStackView.isUserInteractionEnabled = false
|
||||
mainStackView.addArrangedSubview(selectorStackView)
|
||||
mainStackView.addArrangedSubview(errorLabel)
|
||||
selectorStackView.addArrangedSubview(selectorView)
|
||||
selectorStackView.addArrangedSubview(selectorLabelStackView)
|
||||
selectorLabelStackView.addArrangedSubview(label)
|
||||
selectorLabelStackView.addArrangedSubview(childLabel)
|
||||
mainStackView
|
||||
.pinTop()
|
||||
.pinLeading()
|
||||
.pinTrailing()
|
||||
.pinBottom(0, .defaultHigh)
|
||||
onChange = nil
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
@ -281,30 +310,10 @@ open class SelectorItemBase<Selector: SelectorBase>: Control, Errorable, Changea
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
shouldUpdateView = false
|
||||
label.reset()
|
||||
childLabel.reset()
|
||||
errorLabel.reset()
|
||||
|
||||
label.textStyle = .boldBodyLarge
|
||||
childLabel.textStyle = .bodyLarge
|
||||
errorLabel.textStyle = .bodyMedium
|
||||
|
||||
labelText = nil
|
||||
labelTextAttributes = nil
|
||||
labelAttributedText = nil
|
||||
childText = nil
|
||||
childTextAttributes = nil
|
||||
childAttributedText = nil
|
||||
showError = false
|
||||
errorText = nil
|
||||
inputId = nil
|
||||
isSelected = false
|
||||
|
||||
onChange = nil
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
super.reset()
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
|
||||
@ -11,7 +11,7 @@ import Combine
|
||||
|
||||
/// Base Class used to build Views.
|
||||
@objc(VDSView)
|
||||
open class View: UIView, ViewProtocol, UserInfoable {
|
||||
open class View: UIView, ViewProtocol, UserInfoable, Clickable {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
@ -36,6 +36,7 @@ open class View: UIView, ViewProtocol, UserInfoable {
|
||||
//--------------------------------------------------
|
||||
open var subscribers = Set<AnyCancellable>()
|
||||
|
||||
open var onClickSubscriber: AnyCancellable?
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Properties
|
||||
//--------------------------------------------------
|
||||
@ -56,20 +57,30 @@ open class View: UIView, ViewProtocol, UserInfoable {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Lifecycle
|
||||
//--------------------------------------------------
|
||||
open func initialSetup() {
|
||||
private func initialSetup() {
|
||||
if !initialSetupPerformed {
|
||||
initialSetupPerformed = true
|
||||
shouldUpdateView = false
|
||||
setup()
|
||||
setDefaults()
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
open func setup() {
|
||||
backgroundColor = .clear
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
insetsLayoutMarginsFromSafeArea = false
|
||||
}
|
||||
|
||||
open func setDefaults() {
|
||||
backgroundColor = .clear
|
||||
surface = .light
|
||||
isEnabled = true
|
||||
onClick = nil
|
||||
userInfo.removeAll()
|
||||
}
|
||||
|
||||
open func updateView() { }
|
||||
|
||||
open func updateAccessibility() {
|
||||
@ -81,9 +92,10 @@ open class View: UIView, ViewProtocol, UserInfoable {
|
||||
}
|
||||
|
||||
open func reset() {
|
||||
backgroundColor = .clear
|
||||
surface = .light
|
||||
isEnabled = true
|
||||
shouldUpdateView = false
|
||||
setDefaults()
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
open override func layoutSubviews() {
|
||||
|
||||
@ -10,6 +10,7 @@ import UIKit
|
||||
import Combine
|
||||
import VDSCoreTokens
|
||||
|
||||
@objc(VDSAlertViewController)
|
||||
open class AlertViewController: UIViewController, Surfaceable {
|
||||
|
||||
/// Set of Subscribers for any Publishers for this Control.
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
@objc(VDSClearPopoverViewController)
|
||||
open class ClearPopoverViewController: UIViewController, UIPopoverPresentationControllerDelegate {
|
||||
|
||||
/// The view to be inserted inside the popover
|
||||
|
||||
63
VDS/Classes/LanguageManager.swift
Normal file
63
VDS/Classes/LanguageManager.swift
Normal file
@ -0,0 +1,63 @@
|
||||
//
|
||||
// LanguageManager.swift
|
||||
// VDS
|
||||
//
|
||||
// Created by Matt Bruce on 9/25/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// Language Manager to control the current language setting
|
||||
public class LanguageManager {
|
||||
|
||||
// Enum to define supported languages
|
||||
public enum SupportedLanguage: String, CustomStringConvertible {
|
||||
case english = "en"
|
||||
case spanish = "es"
|
||||
|
||||
public var description: String { self == .english ? "English" : "Spanish"}
|
||||
}
|
||||
|
||||
// Private static variable to hold the in-memory current language
|
||||
private static var _currentLanguage: SupportedLanguage? {
|
||||
didSet {
|
||||
TextStyle.Provider.updateCurrentStyles()
|
||||
}
|
||||
}
|
||||
|
||||
// Static property to manage the current language setting
|
||||
public static var currentLanguage: SupportedLanguage {
|
||||
get {
|
||||
// Check if there is an in-memory language setting
|
||||
guard let _currentLanguage else {
|
||||
|
||||
// set default
|
||||
var deviceCurrentLanguage: SupportedLanguage = .english
|
||||
|
||||
// Check device's preferred language
|
||||
let deviceLanguage = Locale.preferredLanguages.first ?? "en"
|
||||
if deviceLanguage.starts(with: "es") {
|
||||
deviceCurrentLanguage = .spanish
|
||||
}
|
||||
|
||||
_currentLanguage = deviceCurrentLanguage
|
||||
return deviceCurrentLanguage
|
||||
}
|
||||
|
||||
return _currentLanguage
|
||||
}
|
||||
set {
|
||||
// Set the in-memory language
|
||||
_currentLanguage = newValue
|
||||
}
|
||||
}
|
||||
|
||||
// Method to set language using a language code string
|
||||
public static func setLanguage(with code: String) {
|
||||
if code.starts(with: "es") {
|
||||
_currentLanguage = .spanish
|
||||
} else {
|
||||
_currentLanguage = .english
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -16,7 +16,7 @@ import Combine
|
||||
/// 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.
|
||||
@objc(VDSBadge)
|
||||
open class Badge: View {
|
||||
open class Badge: View, ParentViewProtocol {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
@ -37,13 +37,23 @@ open class Badge: View {
|
||||
// MARK: - Enums
|
||||
//--------------------------------------------------
|
||||
/// Enum used to describe the primary color for the view.
|
||||
public enum FillColor: String, CaseIterable {
|
||||
public enum FillColor: Equatable {
|
||||
case red, yellow, green, orange, blue, black, white
|
||||
case token(UIColor.VDSColor)
|
||||
case custom(UIColor)
|
||||
|
||||
private var reflectedValue: String { String(reflecting: self) }
|
||||
|
||||
public static func == (lhs: Self, rhs: Self) -> Bool {
|
||||
lhs.reflectedValue == rhs.reflectedValue
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
open var children: [any ViewProtocol] { [label] }
|
||||
|
||||
/// Label used to render text
|
||||
open var label = Label().with {
|
||||
$0.isAccessibilityElement = false
|
||||
@ -62,6 +72,8 @@ open class Badge: View {
|
||||
/// The text that will be shown in the label.
|
||||
open var text: String = "" { didSet { setNeedsUpdate() }}
|
||||
|
||||
open var textColor: TextColor? { didSet { setNeedsUpdate() }}
|
||||
|
||||
/// When applied, this property takes a px value that will restrict the width at that point.
|
||||
open var maxWidth: CGFloat? { didSet { setNeedsUpdate() }}
|
||||
|
||||
@ -90,25 +102,71 @@ open class Badge: View {
|
||||
right: VDSLayout.space1X)
|
||||
|
||||
/// ColorConfiguration that is mapped to the 'fillColor' for the surface.
|
||||
private var backgroundColorConfiguration: AnyColorable = {
|
||||
let config = KeyedColorConfiguration<Badge, FillColor>(keyPath: \.fillColor)
|
||||
config.setSurfaceColors(VDSColor.badgesBackgroundRedOnlight, VDSColor.badgesBackgroundRedOndark, forKey: .red)
|
||||
config.setSurfaceColors(VDSColor.badgesBackgroundYellowOnlight, VDSColor.badgesBackgroundYellowOndark, forKey: .yellow)
|
||||
config.setSurfaceColors(VDSColor.badgesBackgroundGreenOnlight, VDSColor.badgesBackgroundGreenOndark, forKey: .green)
|
||||
config.setSurfaceColors(VDSColor.badgesBackgroundOrangeOnlight, VDSColor.badgesBackgroundOrangeOndark, forKey: .orange)
|
||||
config.setSurfaceColors(VDSColor.badgesBackgroundBlueOnlight, VDSColor.badgesBackgroundBlueOndark, forKey: .blue)
|
||||
config.setSurfaceColors(VDSColor.badgesBackgroundBlackOnlight, VDSColor.badgesBackgroundBlackOndark, forKey: .black)
|
||||
config.setSurfaceColors(VDSColor.badgesBackgroundWhiteOnlight, VDSColor.badgesBackgroundWhiteOndark, forKey: .white)
|
||||
return config.eraseToAnyColorable()
|
||||
}()
|
||||
private var backgroundColorConfiguration = SurfaceColorConfiguration()
|
||||
|
||||
/// ColorConfiguration for the Text.
|
||||
private var textColorConfiguration = ViewColorConfiguration()
|
||||
|
||||
/// Updates the textColorConfiguration based on the fillColor.
|
||||
public func updateTextColorConfig() {
|
||||
public func updateColorConfig() {
|
||||
var config = backgroundColorConfiguration
|
||||
switch fillColor {
|
||||
case .red:
|
||||
config.lightColor = VDSColor.badgesBackgroundRedOnlight
|
||||
config.darkColor = VDSColor.badgesBackgroundRedOndark
|
||||
case .yellow:
|
||||
config.lightColor = VDSColor.badgesBackgroundYellowOnlight
|
||||
config.darkColor = VDSColor.badgesBackgroundYellowOndark
|
||||
case .green:
|
||||
config.lightColor = VDSColor.badgesBackgroundGreenOnlight
|
||||
config.darkColor = VDSColor.badgesBackgroundGreenOndark
|
||||
case .orange:
|
||||
config.lightColor = VDSColor.badgesBackgroundOrangeOnlight
|
||||
config.darkColor = VDSColor.badgesBackgroundOrangeOndark
|
||||
case .blue:
|
||||
config.lightColor = VDSColor.badgesBackgroundBlueOnlight
|
||||
config.darkColor = VDSColor.badgesBackgroundBlueOndark
|
||||
case .black:
|
||||
config.lightColor = VDSColor.badgesBackgroundBlackOnlight
|
||||
config.darkColor = VDSColor.badgesBackgroundBlackOndark
|
||||
case .white:
|
||||
config.lightColor = VDSColor.badgesBackgroundWhiteOnlight
|
||||
config.darkColor = VDSColor.badgesBackgroundWhiteOndark
|
||||
case .token(let color):
|
||||
config.lightColor = color.uiColor
|
||||
config.darkColor = color.uiColor
|
||||
case .custom(let color):
|
||||
config.lightColor = color
|
||||
config.darkColor = color
|
||||
}
|
||||
|
||||
textColorConfiguration.reset()
|
||||
|
||||
func update(for color: UIColor) {
|
||||
if let configuration = textColor?.configuration {
|
||||
textColorConfiguration = configuration
|
||||
} else {
|
||||
if color.isDark() {
|
||||
textColorConfiguration.setSurfaceColors(VDSColor.elementsPrimaryOndark, VDSColor.elementsPrimaryOndark, forDisabled: false)
|
||||
textColorConfiguration.setSurfaceColors(VDSColor.elementsPrimaryOndark, VDSColor.elementsPrimaryOndark, forDisabled: true)
|
||||
} else {
|
||||
textColorConfiguration.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOnlight, forDisabled: false)
|
||||
textColorConfiguration.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOnlight, forDisabled: true)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let textColor {
|
||||
switch textColor {
|
||||
case .token(let color):
|
||||
textColorConfiguration.setSurfaceColors(color.uiColor, color.uiColor, forDisabled: false)
|
||||
textColorConfiguration.setSurfaceColors(color.uiColor, color.uiColor, forDisabled: true)
|
||||
case .custom(let color):
|
||||
textColorConfiguration.setSurfaceColors(color, color, forDisabled: false)
|
||||
textColorConfiguration.setSurfaceColors(color, color, forDisabled: true)
|
||||
}
|
||||
} else {
|
||||
switch fillColor {
|
||||
|
||||
case .red, .black:
|
||||
@ -122,6 +180,13 @@ open class Badge: View {
|
||||
case .orange, .green, .blue:
|
||||
textColorConfiguration.setSurfaceColors(VDSColor.elementsPrimaryOndark, VDSColor.elementsPrimaryOnlight, forDisabled: false)
|
||||
textColorConfiguration.setSurfaceColors(VDSColor.elementsPrimaryOndark, VDSColor.elementsPrimaryOnlight, forDisabled: true)
|
||||
|
||||
case .token(let color):
|
||||
update(for: color.uiColor)
|
||||
|
||||
case .custom(let color):
|
||||
update(for: color)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -148,32 +213,35 @@ open class Badge: View {
|
||||
maxWidthConstraint = label.widthLessThanEqualTo(constant: 0).with { $0.isActive = false }
|
||||
clipsToBounds = true
|
||||
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
|
||||
bridge_accessibilityLabelBlock = { [weak self] in
|
||||
guard let self else { return "" }
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
shouldUpdateView = false
|
||||
label.reset()
|
||||
label.lineBreakMode = .byTruncatingTail
|
||||
label.textStyle = .boldBodySmall
|
||||
fillColor = .red
|
||||
text = ""
|
||||
maxWidth = nil
|
||||
numberOfLines = 1
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
label.reset()
|
||||
super.reset()
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
open override func updateView() {
|
||||
super.updateView()
|
||||
|
||||
updateTextColorConfig()
|
||||
updateColorConfig()
|
||||
updateMaxWidth()
|
||||
|
||||
backgroundColor = backgroundColorConfiguration.getColor(self)
|
||||
@ -185,3 +253,29 @@ open class Badge: View {
|
||||
label.isEnabled = isEnabled
|
||||
}
|
||||
}
|
||||
|
||||
extension Badge{
|
||||
public enum TextColor: Equatable {
|
||||
case token(UIColor.VDSColor)
|
||||
case custom(UIColor)
|
||||
|
||||
private var reflectedValue: String { String(reflecting: self) }
|
||||
|
||||
public static func == (lhs: Self, rhs: Self) -> Bool {
|
||||
lhs.reflectedValue == rhs.reflectedValue
|
||||
}
|
||||
|
||||
public var configuration: ViewColorConfiguration {
|
||||
let config = ViewColorConfiguration()
|
||||
switch self {
|
||||
case .token(let color):
|
||||
config.setSurfaceColors(color.uiColor, color.uiColor, forDisabled: true)
|
||||
config.setSurfaceColors(color.uiColor, color.uiColor, forDisabled: false)
|
||||
case .custom(let color):
|
||||
config.setSurfaceColors(color, color, forDisabled: true)
|
||||
config.setSurfaceColors(color, color, forDisabled: false)
|
||||
}
|
||||
return config
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ import Combine
|
||||
|
||||
/// A badge indicator is a visual label used to convey status or highlight supplemental information.
|
||||
@objc(VDSBadgeIndicator)
|
||||
open class BadgeIndicator: View {
|
||||
open class BadgeIndicator: View, ParentViewProtocol {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
@ -134,6 +134,8 @@ open class BadgeIndicator: View {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
open var children: [any ViewProtocol] { [label, badgeView] }
|
||||
|
||||
/// Label used for the numeric kind.
|
||||
open var label = Label().with {
|
||||
$0.setContentCompressionResistancePriority(.required, for: .vertical)
|
||||
@ -292,6 +294,28 @@ open class BadgeIndicator: View {
|
||||
label.centerYAnchor.constraint(equalTo: badgeView.centerYAnchor).isActive = true
|
||||
labelContraints.isActive = true
|
||||
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
label.lineBreakMode = .byTruncatingTail
|
||||
label.textAlignment = .center
|
||||
fillColor = .red
|
||||
number = nil
|
||||
kind = .simple
|
||||
leadingCharacter = nil
|
||||
trailingText = nil
|
||||
size = .xxlarge
|
||||
dotSize = nil
|
||||
verticalPadding = nil
|
||||
horizontalPadding = nil
|
||||
hideDot = false
|
||||
hideBorder = false
|
||||
width = nil
|
||||
height = nil
|
||||
accessibilityText = nil
|
||||
maximumDigits = .two
|
||||
|
||||
bridge_accessibilityLabelBlock = { [weak self] in
|
||||
guard let self else { return "" }
|
||||
if let accessibilityText {
|
||||
@ -306,15 +330,8 @@ open class BadgeIndicator: View {
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
shouldUpdateView = false
|
||||
label.reset()
|
||||
label.lineBreakMode = .byTruncatingTail
|
||||
label.textAlignment = .center
|
||||
fillColor = .red
|
||||
number = nil
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
super.reset()
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
|
||||
@ -71,17 +71,15 @@ open class BreadcrumbItem: ButtonBase {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
isAccessibilityElement = true
|
||||
accessibilityTraits = .link
|
||||
|
||||
titleLabel?.numberOfLines = 0
|
||||
titleLabel?.lineBreakMode = .byWordWrapping
|
||||
contentHorizontalAlignment = .left
|
||||
|
||||
isAccessibilityElement = true
|
||||
accessibilityTraits = .link
|
||||
|
||||
bridge_accessibilityHintBlock = { [weak self] in
|
||||
guard let self else { return "" }
|
||||
return !isEnabled ? "" : "Double tap to open."
|
||||
@ -130,17 +128,4 @@ open class BreadcrumbItem: ButtonBase {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
shouldUpdateView = false
|
||||
text = nil
|
||||
accessibilityCustomActions = []
|
||||
isAccessibilityElement = true
|
||||
accessibilityTraits = .button
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -8,8 +8,7 @@
|
||||
import Foundation
|
||||
|
||||
extension Breadcrumbs {
|
||||
public struct BreadcrumbItemModel {
|
||||
|
||||
public struct BreadcrumbItemModel: Equatable {
|
||||
///Text that goes in the breadcrumb item
|
||||
public var text: String
|
||||
|
||||
@ -24,5 +23,10 @@ extension Breadcrumbs {
|
||||
self.selected = selected
|
||||
self.onClick = onClick
|
||||
}
|
||||
|
||||
public static func == (lhs: Breadcrumbs.BreadcrumbItemModel, rhs: Breadcrumbs.BreadcrumbItemModel) -> Bool {
|
||||
lhs.text == rhs.text
|
||||
&& lhs.selected == rhs.selected
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,11 +14,13 @@ import Combine
|
||||
/// It contains Breadcrumb Item Default, Breadcrumb Item Selected, Separator.
|
||||
/// Breadcrumbs are secondary navigation that use a hierarchy of internal links to tell customers where they are in an experience. Each breadcrumb links to its respective page, except for that of current page.
|
||||
@objc(VDSBreadcrumbs)
|
||||
open class Breadcrumbs: View {
|
||||
open class Breadcrumbs: View, ParentViewProtocol {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
open var children: [any ViewProtocol] { breadcrumbs }
|
||||
|
||||
/// Array of ``BreadcrumbItem`` views for the Breadcrumbs.
|
||||
open var breadcrumbs: [BreadcrumbItem] = [] { didSet { setNeedsUpdate() } }
|
||||
|
||||
@ -108,21 +110,19 @@ open class Breadcrumbs: View {
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
/// Executed on initialization for this View.
|
||||
open override func initialSetup() {
|
||||
super.initialSetup()
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
containerView.addSubview(collectionView)
|
||||
collectionView.pinToSuperView()
|
||||
addSubview(containerView)
|
||||
containerView.pinToSuperView()
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
shouldUpdateView = false
|
||||
breadcrumbs.forEach { $0.reset() }
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
breadcrumbs = []
|
||||
breadcrumbModels = []
|
||||
isEnabled = true
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
|
||||
@ -223,15 +223,11 @@ open class Button: ButtonBase, Useable {
|
||||
accessibilityTraits = .button
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
shouldUpdateView = false
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
use = .primary
|
||||
width = nil
|
||||
size = .large
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
|
||||
@ -96,13 +96,13 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Lifecycle
|
||||
//--------------------------------------------------
|
||||
open func initialSetup() {
|
||||
private func initialSetup() {
|
||||
if !initialSetupPerformed {
|
||||
initialSetupPerformed = true
|
||||
backgroundColor = .clear
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
accessibilityCustomActions = []
|
||||
shouldUpdateView = false
|
||||
setup()
|
||||
setDefaults()
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
@ -110,10 +110,19 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable {
|
||||
|
||||
open func setup() {
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
}
|
||||
|
||||
open func setDefaults() {
|
||||
backgroundColor = .clear
|
||||
accessibilityCustomActions = []
|
||||
titleLabel?.adjustsFontSizeToFitWidth = false
|
||||
titleLabel?.lineBreakMode = .byTruncatingTail
|
||||
titleLabel?.numberOfLines = 1
|
||||
surface = .light
|
||||
isEnabled = true
|
||||
text = nil
|
||||
onClick = nil
|
||||
userInfo.removeAll()
|
||||
}
|
||||
|
||||
open func updateView() {
|
||||
@ -130,12 +139,7 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable {
|
||||
|
||||
open func reset() {
|
||||
shouldUpdateView = false
|
||||
surface = .light
|
||||
isEnabled = true
|
||||
text = nil
|
||||
accessibilityCustomActions = []
|
||||
onClick = nil
|
||||
userInfo.removeAll()
|
||||
setDefaults()
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
@ -166,15 +166,18 @@ open class ButtonGroup: View {
|
||||
collectionView.reloadData()
|
||||
}
|
||||
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
shouldUpdateView = false
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
rowQuantityPhone = 0
|
||||
rowQuantityTablet = 0
|
||||
alignment = .center
|
||||
childWidth = nil
|
||||
buttons = []
|
||||
}
|
||||
|
||||
open override func reset() {
|
||||
buttons.forEach { $0.reset() }
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
super.reset()
|
||||
}
|
||||
|
||||
open override func layoutSubviews() {
|
||||
|
||||
@ -49,56 +49,53 @@ class ButtonCollectionViewRow {
|
||||
func layout(for position: ButtonGroup.Alignment, with collectionViewWidth: CGFloat){
|
||||
var offset = 0.0
|
||||
let height = rowHeight
|
||||
|
||||
attributes.last?.spacing = 0
|
||||
|
||||
//filter only the buttons since this is the only
|
||||
//object we can change the frames for.
|
||||
let buttonAttributes = attributes.filter{$0.isButton}
|
||||
|
||||
if !buttonAttributes.isEmpty {
|
||||
let buttonCount = CGFloat(buttonAttributes.count)
|
||||
|
||||
///Calculate the spaces between items in the row
|
||||
let totalSpacingBetweenAttributes = attributes.reduce(0.0) { $0 + $1.spacing }
|
||||
|
||||
//see how much of the rows width is used for
|
||||
//non-buttons that are BaseButton Subclasses that are not "Button"
|
||||
let nonButtonSpace = attributes.filter { !$0.isButton }.reduce(0.0) { $0 + $1.frame.width }
|
||||
|
||||
//getting available button space since textlinks need their space
|
||||
let buttonsAvailableSpace = collectionViewWidth - nonButtonSpace - totalSpacingBetweenAttributes
|
||||
let buttonEqualSpacing = buttonsAvailableSpace / buttonCount
|
||||
var maxButtonWidth = buttonEqualSpacing //default to equal spacing
|
||||
var buttonCalculatedPercentage: CGFloat = 0.0
|
||||
|
||||
//check to see if you have buttons and there is a percentage
|
||||
if let buttonPercentage, hasButtons, buttonPercentage > 0 {
|
||||
|
||||
var usedSpace = 0.0
|
||||
//get the width for the buttons
|
||||
for attribute in attributes {
|
||||
if !attribute.isButton {
|
||||
usedSpace += attribute.frame.width
|
||||
}
|
||||
usedSpace += attribute.spacing
|
||||
}
|
||||
let buttonAvailableSpace = collectionViewWidth - usedSpace
|
||||
let realPercentage = (buttonPercentage / 100)
|
||||
let buttonWidth = realPercentage * buttonAvailableSpace
|
||||
// print("buttonPercentage :\(realPercentage)")
|
||||
// print("collectionView width:\(collectionViewWidth)")
|
||||
// print("usedSpace width:\(usedSpace)")
|
||||
// print("button available width:\(buttonAvailableSpace)")
|
||||
// print("each button width:\(buttonWidth)\n")
|
||||
// print("minimum widht:\(ButtonSize.large.minimumWidth)")
|
||||
// test sizing
|
||||
var testSize = 0.0
|
||||
var buttonCount = 0.0
|
||||
for attribute in attributes {
|
||||
if attribute.isButton {
|
||||
testSize += buttonWidth
|
||||
buttonCount += 1
|
||||
}
|
||||
buttonCalculatedPercentage = CGFloat(buttonPercentage / 100.0)
|
||||
let buttonPercentageWidth = buttonCalculatedPercentage * buttonsAvailableSpace
|
||||
maxButtonWidth = min(max(buttonPercentageWidth, Button.Size.large.minimumWidth), maxButtonWidth)
|
||||
}
|
||||
|
||||
if buttonWidth >= Button.Size.large.minimumWidth {
|
||||
if testSize <= buttonAvailableSpace {
|
||||
for attribute in attributes {
|
||||
if attribute.isButton {
|
||||
attribute.frame.size.width = buttonWidth
|
||||
}
|
||||
//resize the buttonAttributes
|
||||
if maxButtonWidth >= Button.Size.large.minimumWidth {
|
||||
//if there is enough room for all buttons
|
||||
if maxButtonWidth * buttonCount <= buttonsAvailableSpace {
|
||||
for attribute in buttonAttributes {
|
||||
attribute.frame.size.width = buttonCalculatedPercentage.isZero ? min(attribute.frame.size.width, maxButtonWidth) : maxButtonWidth
|
||||
}
|
||||
} else {
|
||||
let distributedSize = buttonAvailableSpace / buttonCount
|
||||
for attribute in attributes {
|
||||
if attribute.isButton {
|
||||
attribute.frame.size.width = distributedSize
|
||||
}
|
||||
//if not enough room, give all buttons the same width
|
||||
for attribute in buttonAttributes {
|
||||
attribute.frame.size.width = buttonEqualSpacing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//update the offset based on position
|
||||
switch position {
|
||||
case .left:
|
||||
break
|
||||
@ -181,7 +178,7 @@ class ButtonGroupPositionLayout: UICollectionViewLayout {
|
||||
var rows = [ButtonCollectionViewRow]()
|
||||
rows.append(ButtonCollectionViewRow())
|
||||
|
||||
let collectionViewWidth = collectionView.frame.width
|
||||
let collectionViewWidth = collectionView.horizontalPinnedWidth() ?? collectionView.frame.width
|
||||
|
||||
for item in 0..<totalItems {
|
||||
|
||||
@ -200,7 +197,8 @@ class ButtonGroupPositionLayout: UICollectionViewLayout {
|
||||
// determine if the current button will fit in the row
|
||||
let rowItemCount = rows.last?.attributes.count ?? 0
|
||||
|
||||
if (layoutWidthIterator + itemSize.width) > collectionViewWidth || (rowQuantity > 0 && rowItemCount == rowQuantity) {
|
||||
if (layoutWidthIterator + itemSize.width) > collectionViewWidth && rowQuantity == 0
|
||||
|| (rowQuantity > 0 && rowItemCount == rowQuantity) {
|
||||
|
||||
// If the current row width (after this item being laid out) is exceeding
|
||||
// the width of the collection view content, put it in the next line
|
||||
@ -318,3 +316,5 @@ class ButtonGroupPositionLayout: UICollectionViewLayout {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@ -90,11 +90,6 @@ open class TextLink: ButtonBase {
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
isAccessibilityElement = true
|
||||
accessibilityTraits = .link
|
||||
|
||||
//left align titleLabel in case this is pinned leading/trailing
|
||||
//default is always set to center
|
||||
contentHorizontalAlignment = .left
|
||||
|
||||
if let titleLabel {
|
||||
addSubview(line)
|
||||
@ -105,12 +100,21 @@ open class TextLink: ButtonBase {
|
||||
lineHeightConstraint = line.height(constant: 1)
|
||||
lineHeightConstraint?.isActive = true
|
||||
}
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
size = .large
|
||||
accessibilityTraits = .link
|
||||
|
||||
//left align titleLabel in case this is pinned leading/trailing
|
||||
//default is always set to center
|
||||
contentHorizontalAlignment = .left
|
||||
|
||||
bridge_accessibilityHintBlock = { [weak self] in
|
||||
guard let self else { return "" }
|
||||
return !isEnabled ? "" : "Double tap to open."
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
@ -123,17 +127,4 @@ open class TextLink: ButtonBase {
|
||||
super.updateView()
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
shouldUpdateView = false
|
||||
text = nil
|
||||
size = .large
|
||||
accessibilityCustomActions = []
|
||||
isAccessibilityElement = true
|
||||
accessibilityTraits = .link
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -75,10 +75,8 @@ open class TextLinkCaret: ButtonBase {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
//left align titleLabel in case this is pinned leading/trailing
|
||||
//default is always set to center
|
||||
contentHorizontalAlignment = .left
|
||||
@ -87,11 +85,12 @@ open class TextLinkCaret: ButtonBase {
|
||||
titleLabel?.numberOfLines = 0
|
||||
titleLabel?.lineBreakMode = .byWordWrapping
|
||||
|
||||
iconPosition = .right
|
||||
|
||||
bridge_accessibilityHintBlock = { [weak self] in
|
||||
guard let self else { return "" }
|
||||
return !isEnabled ? "" : "Double tap to open."
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
@ -100,13 +99,6 @@ open class TextLinkCaret: ButtonBase {
|
||||
super.updateView()
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
iconPosition = .right
|
||||
text = nil
|
||||
}
|
||||
|
||||
/// The natural size for the receiving view, considering only properties of the view itself.
|
||||
open override var intrinsicContentSize: CGSize {
|
||||
guard let titleLabel else { return super.intrinsicContentSize }
|
||||
|
||||
@ -124,10 +124,6 @@ open class CalendarBase: Control, Changeable {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
open override func initialSetup() {
|
||||
super.initialSetup()
|
||||
}
|
||||
|
||||
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
@ -154,6 +150,19 @@ open class CalendarBase: Control, Changeable {
|
||||
collectionView.pinCenterX(anchor: containerView.centerXAnchor)
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
hideContainerBorder = false
|
||||
hideCurrentDateIndicator = false
|
||||
transparentBackground = false
|
||||
activeDates = []
|
||||
inactiveDates = []
|
||||
indicators = []
|
||||
minDate = Date()
|
||||
maxDate = Calendar.current.date(byAdding: .year, value: 10, to: Date())!
|
||||
selectedDate = Date()
|
||||
}
|
||||
|
||||
open override func updateView() {
|
||||
super.updateView()
|
||||
// range check between min & max dates
|
||||
@ -174,17 +183,6 @@ open class CalendarBase: Control, Changeable {
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
hideContainerBorder = false
|
||||
hideCurrentDateIndicator = false
|
||||
transparentBackground = false
|
||||
activeDates = []
|
||||
inactiveDates = []
|
||||
indicators = []
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Methods
|
||||
//--------------------------------------------------
|
||||
|
||||
@ -9,7 +9,7 @@ import Foundation
|
||||
|
||||
/// Custom data type for indicators prop
|
||||
extension CalendarBase {
|
||||
public struct CalendarIndicatorModel {
|
||||
public struct CalendarIndicatorModel: Equatable {
|
||||
|
||||
/// Text that shown to an indicator for legend
|
||||
public var label: String
|
||||
|
||||
539
VDS/Components/Carousel/Carousel.swift
Normal file
539
VDS/Components/Carousel/Carousel.swift
Normal file
@ -0,0 +1,539 @@
|
||||
//
|
||||
// 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.
|
||||
@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 carousel’s 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, Valuing {
|
||||
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 lazy var collectionView: UICollectionView = {
|
||||
let layout = UICollectionViewFlowLayout()
|
||||
layout.scrollDirection = .horizontal
|
||||
let collectionView = UICollectionView(frame: frame, collectionViewLayout: layout)
|
||||
collectionView.isScrollEnabled = true
|
||||
collectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||
collectionView.delegate = self
|
||||
collectionView.dataSource = self
|
||||
collectionView.showsHorizontalScrollIndicator = false
|
||||
collectionView.showsVerticalScrollIndicator = false
|
||||
collectionView.backgroundColor = .clear
|
||||
collectionView.register(CarouselSlotCell.self,
|
||||
forCellWithReuseIdentifier: CarouselSlotCell.identifier)
|
||||
return collectionView
|
||||
}()
|
||||
|
||||
/// Previous button to show previous slide.
|
||||
private var previousButton = ButtonIcon().with {
|
||||
$0.kind = .lowContrast
|
||||
$0.iconName = .paginationLeftCaret
|
||||
$0.iconOffset = .init(x: -2, y: 0)
|
||||
$0.customContainerSize = UIDevice.isIPad ? 40 : 28
|
||||
$0.customIconSize = UIDevice.isIPad ? 16 : 12
|
||||
}
|
||||
|
||||
/// Next button to show next slide.
|
||||
private var nextButton = ButtonIcon().with {
|
||||
$0.kind = .lowContrast
|
||||
$0.iconName = .paginationRightCaret
|
||||
$0.iconOffset = .init(x: 2, y: 0)
|
||||
$0.customContainerSize = UIDevice.isIPad ? 40 : 28
|
||||
$0.customIconSize = 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
|
||||
//--------------------------------------------------
|
||||
/// 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(collectionView)
|
||||
collectionView.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()
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
gutter = UIDevice.isIPad ? .gutter6X : .gutter3X
|
||||
layout = UIDevice.isIPad ? .threeUP : .oneUP
|
||||
onChange = nil
|
||||
pagination = .init(kind: .lowContrast, floating: true)
|
||||
paginationDisplay = .none
|
||||
paginationInset = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space2X
|
||||
peek = .standard
|
||||
groupIndex = 0
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
open override func updateView() {
|
||||
super.updateView()
|
||||
updateScrollbar()
|
||||
updateCarousel()
|
||||
collectionView.collectionViewLayout.invalidateLayout()
|
||||
collectionView.reloadData()
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Methods
|
||||
//--------------------------------------------------
|
||||
private func updateScrollbar() {
|
||||
carouselScrollBar.numberOfSlides = views.count
|
||||
carouselScrollBar.layout = layout
|
||||
if (carouselScrollBar.position == 0 || carouselScrollBar.position > carouselScrollBar.numberOfSlides) {
|
||||
carouselScrollBar.position = 1
|
||||
}
|
||||
carouselScrollBar.isHidden = (totalPositions() <= 1) ? true : false
|
||||
}
|
||||
|
||||
private func updateCarousel() {
|
||||
// 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()
|
||||
updateContainerHeight()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// update carousel size and load data if any
|
||||
private func updateContainerHeight() {
|
||||
getSlotWidth()
|
||||
if containerView.frame.size.width > 0 {
|
||||
containerViewHeightConstraint?.isActive = false
|
||||
containerStackHeightConstraint?.isActive = false
|
||||
let slotHeight = fetchCarouselHeight()
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 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 = collectionView.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 collectionview offset relative to scrollbar thumb position
|
||||
private func updateScrollPosition(position: Int, callbackText: String) {
|
||||
if carouselScrollBar.numberOfSlides > 0 {
|
||||
let scrollContentSizeWidth = collectionView.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 = collectionView.contentOffset.y
|
||||
collectionView.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)
|
||||
}
|
||||
}
|
||||
extension Carousel: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
|
||||
//--------------------------------------------------
|
||||
// MARK: - UICollectionView Delegate & Datasource
|
||||
//--------------------------------------------------
|
||||
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
views.count
|
||||
}
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CarouselSlotCell.identifier, for: indexPath) as? CarouselSlotCell else { return UICollectionViewCell() }
|
||||
let component = views[indexPath.row]
|
||||
cell.update(with: component, slotAlignment: slotAlignment, surface: surface)
|
||||
cell.layoutIfNeeded()
|
||||
//component.setNeedsLayout()
|
||||
if hasDebugBorder {
|
||||
cell.addDebugBorder()
|
||||
} else {
|
||||
cell.removeDebugBorder()
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
|
||||
return gutter.value
|
||||
}
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
|
||||
return CGSize(width: minimumSlotWidth, height: fetchCarouselHeight())
|
||||
}
|
||||
|
||||
}
|
||||
15
VDS/Components/Carousel/CarouselChangeLog.txt
Normal file
15
VDS/Components/Carousel/CarouselChangeLog.txt
Normal 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
|
||||
26
VDS/Components/Carousel/CarouselPaginationModel.swift
Normal file
26
VDS/Components/Carousel/CarouselPaginationModel.swift
Normal 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: Equatable {
|
||||
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
27
VDS/Components/Carousel/CarouselSlotAlignmentModel.swift
Normal file
27
VDS/Components/Carousel/CarouselSlotAlignmentModel.swift
Normal 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: Equatable {
|
||||
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
85
VDS/Components/Carousel/CarouselSlotCell.swift
Normal file
85
VDS/Components/Carousel/CarouselSlotCell.swift
Normal file
@ -0,0 +1,85 @@
|
||||
//
|
||||
// CarouselSlotCell.swift
|
||||
// VDS
|
||||
//
|
||||
// Created by Kanamarlapudi, Vasavi on 23/08/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
final class CarouselSlotCell: UICollectionViewCell {
|
||||
|
||||
///Identifier for the Calendar Date Cell.
|
||||
static let identifier: String = String(describing: CarouselSlotCell.self)
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setUp()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setUp()
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Methods
|
||||
//--------------------------------------------------
|
||||
|
||||
/// Configuring the cell with default setup.
|
||||
private func setUp() {
|
||||
isAccessibilityElement = true
|
||||
}
|
||||
|
||||
/// Updating UI based on data along with surface.
|
||||
func update(with component: UIView, slotAlignment: Carousel.CarouselSlotAlignmentModel?, surface: Surface) {
|
||||
contentView.subviews.forEach { $0.removeFromSuperview() }
|
||||
contentView.addSubview(component)
|
||||
if var surfacedView = component as? Surfaceable {
|
||||
surfacedView.surface = surface
|
||||
}
|
||||
setSlotAlignment(alignment: slotAlignment, contentView: component)
|
||||
}
|
||||
|
||||
// Set slot alignment if provided. Used only when slot content have different heights or widths.
|
||||
private func setSlotAlignment(alignment: Carousel.CarouselSlotAlignmentModel?, contentView: UIView) {
|
||||
switch alignment?.vertical {
|
||||
case .top:
|
||||
contentView
|
||||
.pinTop()
|
||||
.pinBottomLessThanOrEqualTo()
|
||||
case .middle:
|
||||
contentView
|
||||
.pinTopGreaterThanOrEqualTo()
|
||||
.pinBottomLessThanOrEqualTo()
|
||||
.pinCenterY()
|
||||
case .bottom:
|
||||
contentView
|
||||
.pinTopGreaterThanOrEqualTo()
|
||||
.pinBottom()
|
||||
default: break
|
||||
}
|
||||
|
||||
switch alignment?.horizontal {
|
||||
case .left:
|
||||
contentView
|
||||
.pinLeading()
|
||||
.pinTrailingLessThanOrEqualTo()
|
||||
case .center:
|
||||
contentView
|
||||
.pinLeadingGreaterThanOrEqualTo()
|
||||
.pinTrailingLessThanOrEqualTo()
|
||||
.pinCenterX()
|
||||
case .right:
|
||||
contentView
|
||||
.pinLeadingGreaterThanOrEqualTo()
|
||||
.pinTrailing()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -45,13 +45,13 @@ open class CarouselScrollbar: View {
|
||||
}
|
||||
|
||||
/// The number of slides that can appear at once in a set in a carousel container.
|
||||
open var selectedLayout: Layout? {
|
||||
get { return _selectedLayout }
|
||||
open var layout: Layout? {
|
||||
get { return _layout }
|
||||
set {
|
||||
if let newValue {
|
||||
_selectedLayout = newValue
|
||||
_layout = newValue
|
||||
} else {
|
||||
_selectedLayout = .oneUP
|
||||
_layout = .oneUP
|
||||
}
|
||||
setThumbWidth()
|
||||
scrollThumbToPosition(position)
|
||||
@ -105,14 +105,15 @@ open class CarouselScrollbar: View {
|
||||
|
||||
/// A callback when the scrubber position changes. Passes parameters (position).
|
||||
open var onScrubberDrag: ((Int) -> Void)? {
|
||||
get { nil }
|
||||
set {
|
||||
didSet {
|
||||
onScrubberDragCancellable?.cancel()
|
||||
if let newValue {
|
||||
if let onScrubberDrag {
|
||||
onScrubberDragCancellable = onScrubberDragPublisher
|
||||
.sink { c in
|
||||
newValue(c)
|
||||
onScrubberDrag(c)
|
||||
}
|
||||
} else {
|
||||
onScrubberDragCancellable = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -123,14 +124,15 @@ open class CarouselScrollbar: View {
|
||||
|
||||
/// A callback when the thumb move forward. Passes parameters (position).
|
||||
open var onMoveForward: ((Int) -> Void)? {
|
||||
get { nil }
|
||||
set {
|
||||
didSet {
|
||||
onMoveForwardCancellable?.cancel()
|
||||
if let newValue {
|
||||
if let onMoveForward {
|
||||
onMoveForwardCancellable = onMoveForwardPublisher
|
||||
.sink { c in
|
||||
newValue(c)
|
||||
onMoveForward(c)
|
||||
}
|
||||
} else {
|
||||
onMoveForwardCancellable = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -141,14 +143,15 @@ open class CarouselScrollbar: View {
|
||||
|
||||
/// A callback when the thumb move backward. Passes parameters (position).
|
||||
open var onMoveBackward: ((Int) -> Void)? {
|
||||
get { nil }
|
||||
set {
|
||||
didSet {
|
||||
onMoveBackwardCancellable?.cancel()
|
||||
if let newValue {
|
||||
if let onMoveBackward {
|
||||
onMoveBackwardCancellable = onMoveBackwardPublisher
|
||||
.sink { c in
|
||||
newValue(c)
|
||||
onMoveBackward(c)
|
||||
}
|
||||
} else {
|
||||
onMoveBackwardCancellable = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -159,14 +162,15 @@ open class CarouselScrollbar: View {
|
||||
|
||||
/// A callback when the thumb touch start. Passes parameters (position).
|
||||
open var onThumbTouchStart: ((Int) -> Void)? {
|
||||
get { nil }
|
||||
set {
|
||||
didSet {
|
||||
onThumbTouchStartCancellable?.cancel()
|
||||
if let newValue {
|
||||
if let onThumbTouchStart {
|
||||
onThumbTouchStartCancellable = onThumbTouchStartPublisher
|
||||
.sink { c in
|
||||
newValue(c)
|
||||
onThumbTouchStart(c)
|
||||
}
|
||||
} else {
|
||||
onThumbTouchStartCancellable = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -177,14 +181,15 @@ open class CarouselScrollbar: View {
|
||||
|
||||
/// A callback when the thumb touch end. Passes parameters (position).
|
||||
open var onThumbTouchEnd: ((Int) -> Void)? {
|
||||
get { nil }
|
||||
set {
|
||||
didSet {
|
||||
onThumbTouchEndCancellable?.cancel()
|
||||
if let newValue {
|
||||
if let onThumbTouchEnd {
|
||||
onThumbTouchEndCancellable = onThumbTouchEndPublisher
|
||||
.sink { c in
|
||||
newValue(c)
|
||||
onThumbTouchEnd(c)
|
||||
}
|
||||
} else {
|
||||
onThumbTouchEndCancellable = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -198,7 +203,7 @@ open class CarouselScrollbar: View {
|
||||
//--------------------------------------------------
|
||||
// Sizes are from InVision design specs.
|
||||
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 totalPositions: Int = 1
|
||||
internal var _position: Int = 1
|
||||
@ -234,10 +239,6 @@ open class CarouselScrollbar: View {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Lifecycle
|
||||
//--------------------------------------------------
|
||||
open override func initialSetup() {
|
||||
super.initialSetup()
|
||||
}
|
||||
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
isAccessibilityElement = false
|
||||
@ -297,6 +298,19 @@ open class CarouselScrollbar: View {
|
||||
thumbView.layer.addSublayer(thumbViewLayer)
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
onMoveForward = nil
|
||||
onMoveBackward = nil
|
||||
onScrubberDrag = nil
|
||||
onThumbTouchEnd = nil
|
||||
onThumbTouchStart = nil
|
||||
layout = .oneUP
|
||||
numberOfSlides = 1
|
||||
totalPositions = 1
|
||||
position = 1
|
||||
}
|
||||
|
||||
open override func updateView() {
|
||||
super.updateView()
|
||||
trackView.backgroundColor = trackColorConfiguration.getColor(surface)
|
||||
@ -329,7 +343,7 @@ open class CarouselScrollbar: View {
|
||||
|
||||
// Compute track width and should maintain minimum thumb width if needed
|
||||
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
|
||||
thumbWidth = (width <= Float(trackViewWidth) && width > minThumbWidth) ? width : ((width > Float(trackViewWidth)) ? Float(trackViewWidth) : minThumbWidth)
|
||||
thumbView.frame.size.width = CGFloat(thumbWidth)
|
||||
@ -362,7 +376,7 @@ open class CarouselScrollbar: View {
|
||||
}
|
||||
|
||||
private func checkPositions() {
|
||||
totalPositions = Int (ceil (Double(numberOfSlides) / Double(_selectedLayout.value)))
|
||||
totalPositions = Int (ceil (Double(numberOfSlides) / Double(_layout.value)))
|
||||
}
|
||||
|
||||
private func scrollThumbToPosition(_ position: Int) {
|
||||
|
||||
@ -61,6 +61,11 @@ open class Checkbox: SelectorBase {
|
||||
selectorColorConfiguration.setSurfaceColors(VDSColor.elementsPrimaryOndark, VDSColor.elementsPrimaryOnlight, forState: .selected)
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
isAnimated = false
|
||||
}
|
||||
|
||||
/// This will change the state of the Selector and execute the actionBlock if provided.
|
||||
open override func toggle() {
|
||||
guard isEnabled else { return }
|
||||
|
||||
@ -43,7 +43,7 @@ open class CheckboxGroup: SelectorGroupBase<CheckboxItem>, SelectorGroupMultiSel
|
||||
if let selectorModels {
|
||||
items = selectorModels.enumerated().map { index, model in
|
||||
return CheckboxItem().with {
|
||||
$0.isEnabled = !model.disabled
|
||||
$0.isEnabled = model.enabled
|
||||
$0.surface = model.surface
|
||||
$0.inputId = model.inputId
|
||||
$0.hiddenValue = model.value
|
||||
@ -86,7 +86,13 @@ open class CheckboxGroup: SelectorGroupBase<CheckboxItem>, SelectorGroupMultiSel
|
||||
mainStackView.spacing = VDSLayout.space6X
|
||||
}
|
||||
|
||||
public override func didSelect(_ selectedControl: CheckboxItem) {
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
showError = false
|
||||
inputId = nil
|
||||
}
|
||||
|
||||
open override func didSelect(_ selectedControl: CheckboxItem) {
|
||||
selectedControl.toggle()
|
||||
if selectedControl.isSelected, showError{
|
||||
showError.toggle()
|
||||
@ -101,10 +107,10 @@ open class CheckboxGroup: SelectorGroupBase<CheckboxItem>, SelectorGroupMultiSel
|
||||
}
|
||||
|
||||
extension CheckboxGroup {
|
||||
public struct CheckboxItemModel : Surfaceable, Initable, Errorable {
|
||||
public struct CheckboxItemModel : Surfaceable, Initable, Errorable, Equatable {
|
||||
|
||||
/// Whether this object is disabled or not
|
||||
public var disabled: Bool
|
||||
/// Whether this object is enabled or not
|
||||
public var enabled: Bool
|
||||
/// Current Surface and this is used to pass down to child objects that implement Surfacable
|
||||
public var surface: Surface
|
||||
public var inputId: String?
|
||||
@ -121,8 +127,8 @@ extension CheckboxGroup {
|
||||
public var showError: Bool
|
||||
public var errorText: String?
|
||||
|
||||
public init(disabled: Bool, surface: Surface = .light, inputId: String? = nil, value: AnyHashable? = nil, accessibileText: String? = nil, labelText: String? = nil, labelTextAttributes: [any LabelAttributeModel]? = nil, childText: String? = nil, childTextAttributes: [any LabelAttributeModel]? = nil, selected: Bool = false, showError: Bool = false, errorText: String? = nil) {
|
||||
self.disabled = disabled
|
||||
public init(enabled: Bool, surface: Surface = .light, inputId: String? = nil, value: AnyHashable? = nil, accessibileText: String? = nil, labelText: String? = nil, labelTextAttributes: [any LabelAttributeModel]? = nil, childText: String? = nil, childTextAttributes: [any LabelAttributeModel]? = nil, selected: Bool = false, showError: Bool = false, errorText: String? = nil) {
|
||||
self.enabled = enabled
|
||||
self.surface = surface
|
||||
self.inputId = inputId
|
||||
self.value = value
|
||||
@ -137,7 +143,22 @@ extension CheckboxGroup {
|
||||
}
|
||||
|
||||
public init() {
|
||||
self.init(disabled: false)
|
||||
self.init(enabled: true)
|
||||
}
|
||||
|
||||
public static func == (lhs: CheckboxGroup.CheckboxItemModel, rhs: CheckboxGroup.CheckboxItemModel) -> Bool {
|
||||
lhs.enabled == rhs.enabled
|
||||
&& lhs.surface == rhs.surface
|
||||
&& lhs.inputId == rhs.inputId
|
||||
&& lhs.value == rhs.value
|
||||
&& lhs.accessibileText == rhs.accessibileText
|
||||
&& lhs.labelText == rhs.labelText
|
||||
&& lhs.labelTextAttributes == rhs.labelTextAttributes
|
||||
&& lhs.childText == rhs.childText
|
||||
&& lhs.childTextAttributes == rhs.childTextAttributes
|
||||
&& lhs.selected == rhs.selected
|
||||
&& lhs.showError == rhs.showError
|
||||
&& lhs.errorText == rhs.errorText
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -48,6 +48,11 @@ open class CheckboxItem: SelectorItemBase<Checkbox> {
|
||||
sendActions(for: .valueChanged)
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
isAnimated = false
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
open override func updateView() {
|
||||
selectorView.isAnimated = isAnimated
|
||||
|
||||
@ -5,7 +5,7 @@ import Combine
|
||||
|
||||
/// A dropdown select is an expandable menu of predefined options that allows a customer to make a single selection.
|
||||
@objc(VDSDatePicker)
|
||||
open class DatePicker: EntryFieldBase {
|
||||
open class DatePicker: EntryFieldBase<String> {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
@ -27,6 +27,19 @@ open class DatePicker: EntryFieldBase {
|
||||
/// A callback when the selected option changes. Passes parameters (option).
|
||||
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
|
||||
//--------------------------------------------------
|
||||
@ -37,6 +50,7 @@ open class DatePicker: EntryFieldBase {
|
||||
}
|
||||
|
||||
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 = {
|
||||
@ -79,6 +93,12 @@ open class DatePicker: EntryFieldBase {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
open override var children: [any ViewProtocol] {
|
||||
var current = super.children
|
||||
current.append(selectedDateLabel)
|
||||
return current
|
||||
}
|
||||
|
||||
open var calendarIcon = Icon().with {
|
||||
$0.name = .calendar
|
||||
$0.size = .medium
|
||||
@ -138,17 +158,6 @@ open class DatePicker: EntryFieldBase {
|
||||
// setting color config
|
||||
selectedDateLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable()
|
||||
|
||||
// tap gesture
|
||||
containerView
|
||||
.publisher(for: UITapGestureRecognizer())
|
||||
.sink { [weak self] _ in
|
||||
guard let self else { return }
|
||||
if isEnabled && !isReadOnly {
|
||||
showPopover()
|
||||
}
|
||||
}
|
||||
.store(in: &subscribers)
|
||||
|
||||
NotificationCenter.default
|
||||
.publisher(for: UIDevice.orientationDidChangeNotification).sink { [weak self] _ in
|
||||
guard let self else { return }
|
||||
@ -159,6 +168,31 @@ open class DatePicker: EntryFieldBase {
|
||||
popoverOverlayView.isHidden = true
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
selectedDate = nil
|
||||
calendarModel = .init()
|
||||
dateFormat = .shortNumeric
|
||||
selectedDateLabel.textStyle = .bodyLarge
|
||||
|
||||
// tap gesture
|
||||
containerView.onClick = { [weak self] _ in
|
||||
guard let self else { return }
|
||||
if isEnabled && !isReadOnly {
|
||||
showPopover()
|
||||
}
|
||||
}
|
||||
|
||||
containerView.accessibilityTraits = [.button]
|
||||
|
||||
containerView.bridge_accessibilityHintBlock = { [weak self] in
|
||||
guard let self else { return "" }
|
||||
return isReadOnly || !isEnabled
|
||||
? ""
|
||||
: isCalendarShowing ? "Expanded, Double tap to close" : "Collapsed, \(accessibilityHintText)"
|
||||
}
|
||||
}
|
||||
|
||||
open override func getFieldContainer() -> UIView {
|
||||
// stackview for controls in EntryFieldBase.controlContainerView
|
||||
let controlStackView = UIStackView().with {
|
||||
@ -185,12 +219,6 @@ open class DatePicker: EntryFieldBase {
|
||||
calendarIcon.color = iconColorConfiguration.getColor(self)
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
selectedDateLabel.textStyle = .bodyLarge
|
||||
}
|
||||
|
||||
internal func formatDate(_ date: Date) {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = dateFormat.format
|
||||
@ -207,6 +235,9 @@ extension DatePicker {
|
||||
}
|
||||
|
||||
let calendar = CalendarBase()
|
||||
if let selectedDate, selectedDate != calendar.selectedDate {
|
||||
calendar.selectedDate = selectedDate
|
||||
}
|
||||
calendar.activeDates = calendarModel.activeDates
|
||||
calendar.hideContainerBorder = calendarModel.hideContainerBorder
|
||||
calendar.hideCurrentDateIndicator = calendarModel.hideCurrentDateIndicator
|
||||
@ -214,7 +245,7 @@ extension DatePicker {
|
||||
calendar.indicators = calendarModel.indicators
|
||||
calendar.maxDate = calendarModel.maxDate
|
||||
calendar.minDate = calendarModel.minDate
|
||||
calendar.surface = calendarModel.surface
|
||||
calendar.surface = surface
|
||||
calendar.setNeedsLayout()
|
||||
calendar.layoutIfNeeded()
|
||||
|
||||
@ -223,7 +254,7 @@ extension DatePicker {
|
||||
|
||||
//find scrollView
|
||||
if scrollView == nil {
|
||||
scrollView = findScrollView(from: containerView)
|
||||
scrollView = containerView.findSuperview(ofType: UIScrollView.self)
|
||||
scrollViewContentSize = scrollView?.contentSize
|
||||
}
|
||||
|
||||
@ -315,6 +346,7 @@ extension DatePicker {
|
||||
}
|
||||
}
|
||||
|
||||
isCalendarShowing = true
|
||||
}
|
||||
|
||||
private func hidePopoverView() {
|
||||
@ -346,17 +378,7 @@ extension DatePicker {
|
||||
UIAccessibility.post(notification: .layoutChanged, argument: containerView)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func findScrollView(from view: UIView) -> UIScrollView? {
|
||||
var currentView = view
|
||||
while let superview = currentView.superview {
|
||||
if let scrollView = superview as? UIScrollView {
|
||||
return scrollView
|
||||
}
|
||||
currentView = superview
|
||||
}
|
||||
return nil
|
||||
isCalendarShowing = false
|
||||
}
|
||||
|
||||
private func calculatePopoverPosition(relativeTo sourceView: UIView, in parentView: UIView, size: CGSize, with spacing: CGFloat) -> CGPoint? {
|
||||
@ -442,3 +464,16 @@ extension DatePicker {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,9 +9,7 @@ import Foundation
|
||||
import UIKit
|
||||
|
||||
extension DatePicker {
|
||||
public struct CalendarModel {
|
||||
public let surface: Surface
|
||||
|
||||
public struct CalendarModel: Equatable {
|
||||
/// If set to true, the calendar will not have a border.
|
||||
public let hideContainerBorder: Bool
|
||||
|
||||
@ -26,6 +24,8 @@ extension DatePicker {
|
||||
/// All other dates will be active.
|
||||
public let inactiveDates: [Date]
|
||||
|
||||
public let selectedDate: Date
|
||||
|
||||
/// If provided, the calendar will allow a selection to be made from this date forward. Defaults to today.
|
||||
public let minDate: Date
|
||||
|
||||
@ -35,19 +35,19 @@ extension DatePicker {
|
||||
/// Array of ``CalendarIndicatorModel`` you are wanting to show on legend.
|
||||
public let indicators: [CalendarBase.CalendarIndicatorModel]
|
||||
|
||||
public init(surface: Surface = .light,
|
||||
hideContainerBorder: Bool = false,
|
||||
public init(hideContainerBorder: Bool = false,
|
||||
hideCurrentDateIndicator: Bool = false,
|
||||
activeDates: [Date] = [],
|
||||
inactiveDates: [Date] = [],
|
||||
minDate: Date = Date().startOfMonth,
|
||||
maxDate: Date = Date().endOfMonth,
|
||||
selectedDate: Date = Date(),
|
||||
minDate: Date = Date(),
|
||||
maxDate: Date = Calendar.current.date(byAdding: .year, value: 10, to: Date())!,
|
||||
indicators: [CalendarBase.CalendarIndicatorModel] = []) {
|
||||
self.surface = surface
|
||||
self.hideContainerBorder = hideContainerBorder
|
||||
self.hideCurrentDateIndicator = hideCurrentDateIndicator
|
||||
self.activeDates = activeDates
|
||||
self.inactiveDates = inactiveDates
|
||||
self.selectedDate = selectedDate
|
||||
self.minDate = minDate
|
||||
self.maxDate = maxDate
|
||||
self.indicators = indicators
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
import Foundation
|
||||
|
||||
extension DropdownSelect {
|
||||
public struct DropdownOptionModel {
|
||||
public struct DropdownOptionModel: Equatable {
|
||||
|
||||
/// Text that goes as option to DropdownSelect
|
||||
public var text: String
|
||||
|
||||
@ -12,7 +12,7 @@ import Combine
|
||||
|
||||
/// A dropdown select is an expandable menu of predefined options that allows a customer to make a single selection.
|
||||
@objc(VDSDropdownSelect)
|
||||
open class DropdownSelect: EntryFieldBase {
|
||||
open class DropdownSelect: EntryFieldBase<String> {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
@ -60,8 +60,9 @@ open class DropdownSelect: EntryFieldBase {
|
||||
internal var minWidthInlineLabel = 102.0
|
||||
internal override var minWidth: CGFloat { showInlineLabel ? minWidthInlineLabel : minWidthDefault }
|
||||
internal override var maxWidth: CGFloat {
|
||||
let frameWidth = frame.size.width
|
||||
return helperTextPlacement == .right ? (frameWidth - horizontalStackView.spacing) / 2 : frameWidth
|
||||
let frameWidth = constrainedWidth
|
||||
let halfWidth = (frameWidth - horizontalStackView.spacing) / 2
|
||||
return helperTextPlacement == .right && halfWidth > minWidth * 2 ? halfWidth : frameWidth
|
||||
}
|
||||
|
||||
/// The is used for the for adding the helperLabel to the right of the containerView.
|
||||
@ -81,24 +82,15 @@ open class DropdownSelect: EntryFieldBase {
|
||||
open var inlineDisplayLabel = Label().with {
|
||||
$0.setContentCompressionResistancePriority(.required, for: .vertical)
|
||||
$0.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
$0.textAlignment = .left
|
||||
$0.textStyle = .boldBodyLarge
|
||||
$0.numberOfLines = 1
|
||||
$0.sizeToFit()
|
||||
}
|
||||
|
||||
open var selectedOptionLabel = Label().with {
|
||||
$0.setContentCompressionResistancePriority(.required, for: .vertical)
|
||||
$0.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
$0.textAlignment = .left
|
||||
$0.textStyle = .bodyLarge
|
||||
$0.numberOfLines = 1
|
||||
}
|
||||
|
||||
open var dropdownField = UITextField().with {
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
$0.tintColor = UIColor.clear
|
||||
$0.font = TextStyle.bodyLarge.font
|
||||
}
|
||||
|
||||
open var optionsPicker = UIPickerView()
|
||||
@ -152,15 +144,35 @@ open class DropdownSelect: EntryFieldBase {
|
||||
}()
|
||||
|
||||
// tap gesture
|
||||
containerView
|
||||
.publisher(for: UITapGestureRecognizer())
|
||||
.sink { [weak self] _ in
|
||||
containerView.onClick = { [weak self] _ in
|
||||
self?.launchPicker()
|
||||
}
|
||||
.store(in: &subscribers)
|
||||
containerView.height(44)
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
showInlineLabel = false
|
||||
selectId = nil
|
||||
inlineDisplayLabel.textAlignment = .left
|
||||
inlineDisplayLabel.textStyle = .boldBodyLarge
|
||||
inlineDisplayLabel.numberOfLines = 1
|
||||
selectedOptionLabel.textAlignment = .left
|
||||
selectedOptionLabel.textStyle = .bodyLarge
|
||||
selectedOptionLabel.numberOfLines = 1
|
||||
dropdownField.tintColor = UIColor.clear
|
||||
dropdownField.font = TextStyle.bodyLarge.font
|
||||
showInlineLabel = false
|
||||
options = []
|
||||
selectId = nil
|
||||
}
|
||||
|
||||
open override func reset() {
|
||||
inlineDisplayLabel.reset()
|
||||
selectedOptionLabel.reset()
|
||||
super.reset()
|
||||
}
|
||||
|
||||
open override func getFieldContainer() -> UIView {
|
||||
let controlStackView = UIStackView().with {
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
@ -188,17 +200,6 @@ open class DropdownSelect: EntryFieldBase {
|
||||
selectedOptionLabel.isEnabled = isEnabled
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
|
||||
inlineDisplayLabel.textStyle = .boldBodyLarge
|
||||
selectedOptionLabel.textStyle = .bodyLarge
|
||||
showInlineLabel = false
|
||||
options = []
|
||||
selectId = nil
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Methods
|
||||
//--------------------------------------------------
|
||||
|
||||
17
VDS/Components/Footnote/FootnoteChangeLog.txt
Normal file
17
VDS/Components/Footnote/FootnoteChangeLog.txt
Normal file
@ -0,0 +1,17 @@
|
||||
MM/DD/YYYY
|
||||
----------------
|
||||
Initial Brand 3.0 handoff
|
||||
|
||||
12/18/2023
|
||||
----------------
|
||||
- New
|
||||
|
||||
12/28/2023
|
||||
----------------
|
||||
- hideSymbol updated to showSymbol and default set to True.
|
||||
- Figma-only properties section added in Footnote Item Configurations section.
|
||||
|
||||
01/16/2024
|
||||
----------------
|
||||
- hideSymbol reverted to hideSymbol and default set to False.
|
||||
- Figma-only properties section removed.
|
||||
165
VDS/Components/Footnote/FootnoteGroup.swift
Normal file
165
VDS/Components/Footnote/FootnoteGroup.swift
Normal file
@ -0,0 +1,165 @@
|
||||
//
|
||||
// FootnoteGroup.swift
|
||||
// VDS
|
||||
//
|
||||
// Created by Kanamarlapudi, Vasavi on 29/08/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import VDSCoreTokens
|
||||
|
||||
/// This must always be paired with one or more ``Footnote`` in a FootnoteGroup.
|
||||
@objc(VDSFootnoteGroup)
|
||||
open class FootnoteGroup: View {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
required public init() {
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
|
||||
public required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - enums
|
||||
//--------------------------------------------------
|
||||
/// Enum used to describe the width of a fixed value or percentage of parent's width.
|
||||
public enum Width {
|
||||
case percentage(CGFloat)
|
||||
case value(CGFloat)
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
/// Array of ``Footnote`` for the Footnote items.
|
||||
open var footnoteItems: [FootnoteItem] = [] { didSet { updateFootnoteItems() } }
|
||||
|
||||
/// Any percentage or pixel value and cannot exceed container size.
|
||||
/// If there is a width that is larger than container size, the footnote will resize to container's width.
|
||||
open var width: Width? {
|
||||
get { _width }
|
||||
set {
|
||||
if let newValue {
|
||||
switch newValue {
|
||||
case .percentage(let percentage):
|
||||
if percentage <= 100.0 {
|
||||
_width = newValue
|
||||
}
|
||||
case .value(let value):
|
||||
if value > 0 {
|
||||
_width = newValue
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_width = nil
|
||||
}
|
||||
updateContainerWidth()
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Properties
|
||||
//--------------------------------------------------
|
||||
private var _width: Width? = nil
|
||||
|
||||
private lazy var stackView = UIStackView().with {
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
$0.axis = .vertical
|
||||
$0.distribution = .fill
|
||||
$0.spacing = VDSLayout.space3X
|
||||
$0.backgroundColor = .clear
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Configuration Properties
|
||||
//--------------------------------------------------
|
||||
internal var maxWidth: CGFloat { constrainedWidth }
|
||||
internal var minWidth: CGFloat { containerSize.width }
|
||||
internal var containerSize: CGSize { CGSize(width: 55, height: 44) }
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Constraints
|
||||
//--------------------------------------------------
|
||||
internal var widthConstraint: NSLayoutConstraint?
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
|
||||
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
|
||||
// add footnote item stackview.
|
||||
addSubview(stackView)
|
||||
stackView.pinToSuperView()
|
||||
widthConstraint = widthAnchor.constraint(equalToConstant: 0).deactivate()
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
width = nil
|
||||
footnoteItems = []
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
open override func updateView() {
|
||||
super.updateView()
|
||||
updateFootnoteItems()
|
||||
}
|
||||
|
||||
internal func updateFootnoteItems() {
|
||||
// symbol containers are as wide as the widest symbol container in the group.
|
||||
var symbolMaxWidth = 0.0
|
||||
|
||||
footnoteItems.forEach { footnote in
|
||||
let separatorWidth = Label().with {
|
||||
$0.text = footnote.symbolType
|
||||
$0.textStyle = footnote.symbolLabel.textStyle
|
||||
$0.sizeToFit()
|
||||
}.intrinsicContentSize.width
|
||||
symbolMaxWidth = max(separatorWidth, symbolMaxWidth)
|
||||
}
|
||||
|
||||
stackView.removeArrangedSubviews()
|
||||
|
||||
// add symbol label, text label to stack.
|
||||
footnoteItems.forEach { footnote in
|
||||
footnote.symbolWidth = symbolMaxWidth
|
||||
footnote.surface = surface
|
||||
stackView.addArrangedSubview(footnote)
|
||||
}
|
||||
}
|
||||
|
||||
/// Update container width after updating content.
|
||||
internal func updateContainerWidth() {
|
||||
var newWidth = 0.0
|
||||
|
||||
switch width {
|
||||
case .percentage(let percentage):
|
||||
newWidth = max(maxWidth * ((percentage) / 100), minWidth)
|
||||
|
||||
case .value(let value):
|
||||
newWidth = value > maxWidth ? maxWidth : value
|
||||
|
||||
case nil: break
|
||||
|
||||
}
|
||||
|
||||
widthConstraint?.deactivate()
|
||||
|
||||
if newWidth > minWidth && newWidth < maxWidth {
|
||||
widthConstraint?.constant = newWidth
|
||||
widthConstraint?.activate()
|
||||
}
|
||||
}
|
||||
}
|
||||
256
VDS/Components/Footnote/FootnoteItem.swift
Normal file
256
VDS/Components/Footnote/FootnoteItem.swift
Normal file
@ -0,0 +1,256 @@
|
||||
//
|
||||
// Footnote.swift
|
||||
// VDS
|
||||
//
|
||||
// Created by Kanamarlapudi, Vasavi on 21/08/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import VDSCoreTokens
|
||||
|
||||
/// A footnote is text that provides supporting details, legal copy and links to related content.
|
||||
/// It exists at the bottom or "foot" of a page or section.
|
||||
@objc(VDSFootnoteItem)
|
||||
open class FootnoteItem: View {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
required public init() {
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
|
||||
public required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - enums
|
||||
//--------------------------------------------------
|
||||
/// Enum used to describe the kind of component.
|
||||
public enum Kind: String, DefaultValuing, CaseIterable {
|
||||
case primary, secondary
|
||||
|
||||
/// The default kind is 'primary'.
|
||||
public static var defaultValue : Self { .secondary }
|
||||
|
||||
/// Color configuation to Symbol and Text relative to kind.
|
||||
public var colorConfiguration: SurfaceColorConfiguration {
|
||||
switch self {
|
||||
case .primary:
|
||||
return SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark)
|
||||
case .secondary:
|
||||
return SurfaceColorConfiguration(VDSColor.elementsSecondaryOnlight, VDSColor.elementsSecondaryOndark)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum that represents the size availble for component.
|
||||
public enum Size: String, DefaultValuing, CaseIterable {
|
||||
case micro
|
||||
case small
|
||||
case large
|
||||
|
||||
public static var defaultValue: Self { .micro }
|
||||
|
||||
/// TextStyle relative to Size.
|
||||
public var textStyle: TextStyle.StandardStyle {
|
||||
switch self {
|
||||
case .micro:
|
||||
return .micro
|
||||
case .small:
|
||||
return .bodySmall
|
||||
case .large:
|
||||
return .bodyLarge
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum used to describe the width of a fixed value or percentage of parent's width.
|
||||
public enum Width {
|
||||
case percentage(CGFloat)
|
||||
case value(CGFloat)
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
/// Color to the component. The default kind is Secondary.
|
||||
open var kind: Kind = .defaultValue { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// Size of the component. The default size is Micro.
|
||||
open var size: Size = .defaultValue { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// If hideSymbol true, the component will show text without symbol.
|
||||
open var hideSymbol: Bool = false { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// symbol type will be shown for the footnote item. The default symbolType is 'asterisk'.
|
||||
open var symbolType: String = "*" { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// Text of the footnote item.
|
||||
open var text: String? { didSet { setNeedsUpdate() } }
|
||||
|
||||
open var tooltipModel: Tooltip.TooltipModel? { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// Any percentage or pixel value and cannot exceed container size.
|
||||
/// If there is a width that is larger than container size, the footnote will resize to container's width.
|
||||
open var width: Width? {
|
||||
get { _width }
|
||||
set {
|
||||
if let newValue {
|
||||
switch newValue {
|
||||
case .percentage(let percentage):
|
||||
if percentage <= 100.0 {
|
||||
_width = newValue
|
||||
}
|
||||
case .value(let value):
|
||||
if value > 0 {
|
||||
_width = newValue
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_width = nil
|
||||
}
|
||||
updateContainerWidth()
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Properties
|
||||
//--------------------------------------------------
|
||||
private var _width: Width? = nil
|
||||
|
||||
/// To set the widest symbol width from the symbol container in the group.
|
||||
internal var symbolWidth: CGFloat? { didSet { setNeedsUpdate() } }
|
||||
|
||||
private lazy var itemStackView = UIStackView().with {
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
$0.axis = .horizontal
|
||||
$0.alignment = .leading
|
||||
$0.distribution = .fill
|
||||
$0.spacing = VDSLayout.space1X
|
||||
$0.backgroundColor = .clear
|
||||
}
|
||||
|
||||
internal var symbolLabel = Label().with {
|
||||
$0.isAccessibilityElement = true
|
||||
$0.numberOfLines = 1
|
||||
$0.sizeToFit()
|
||||
}
|
||||
|
||||
internal var textLabel = Label().with {
|
||||
$0.isAccessibilityElement = true
|
||||
$0.lineBreakMode = .byWordWrapping
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Configuration Properties
|
||||
//--------------------------------------------------
|
||||
internal var maxWidth: CGFloat { constrainedWidth }
|
||||
internal var minWidth: CGFloat { containerSize.width }
|
||||
internal var containerSize: CGSize { CGSize(width: 45, height: 44) }
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Constraints
|
||||
//--------------------------------------------------
|
||||
internal var symbolWidthConstraint: NSLayoutConstraint?
|
||||
internal var itemWidthConstraint: NSLayoutConstraint?
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
|
||||
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
|
||||
// add footnote item stackview.
|
||||
addSubview(itemStackView)
|
||||
itemStackView.pinToSuperView()
|
||||
|
||||
// width constraints
|
||||
itemWidthConstraint = widthAnchor.constraint(equalToConstant: 0).deactivate()
|
||||
|
||||
// add symbol label, text label to stack.
|
||||
itemStackView.addArrangedSubview(symbolLabel)
|
||||
itemStackView.addArrangedSubview(textLabel)
|
||||
itemStackView.setCustomSpacing(VDSLayout.space1X, after: symbolLabel)
|
||||
|
||||
symbolWidthConstraint = symbolLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 0)
|
||||
symbolWidthConstraint?.isActive = true
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
hideSymbol = false
|
||||
text = nil
|
||||
tooltipModel = nil
|
||||
width = nil
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
symbolLabel.reset()
|
||||
textLabel.reset()
|
||||
super.reset()
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
open override func updateView() {
|
||||
super.updateView()
|
||||
|
||||
// Update symbolLabel
|
||||
symbolLabel.text = symbolType
|
||||
symbolLabel.isHidden = symbolType.isEmpty ? true : hideSymbol
|
||||
symbolLabel.textColor = kind.colorConfiguration.getColor(self)
|
||||
symbolLabel.textStyle = size.textStyle.regular
|
||||
symbolLabel.surface = surface
|
||||
|
||||
//Set width to the symbol label
|
||||
if let symbolWidth, symbolWidth > 0 {
|
||||
// Set the widest symbol width from the symbol container in the group.
|
||||
symbolWidthConstraint?.constant = symbolWidth
|
||||
} else {
|
||||
symbolWidthConstraint?.constant = symbolLabel.intrinsicContentSize.width
|
||||
}
|
||||
|
||||
// Update textLabel
|
||||
textLabel.text = text
|
||||
textLabel.textColor = kind.colorConfiguration.getColor(self)
|
||||
textLabel.textStyle = size.textStyle.regular
|
||||
textLabel.surface = surface
|
||||
|
||||
// Set the textLabel attributes
|
||||
if let tooltipModel {
|
||||
var attributes: [any LabelAttributeModel] = []
|
||||
attributes.append(TooltipLabelAttribute(surface: surface, model: tooltipModel, presenter: self))
|
||||
textLabel.attributes = attributes
|
||||
}
|
||||
}
|
||||
|
||||
/// Update container width after updating content.
|
||||
internal func updateContainerWidth() {
|
||||
var newWidth = 0.0
|
||||
switch width {
|
||||
case .percentage(let percentage):
|
||||
newWidth = max(maxWidth * ((percentage) / 100), minWidth)
|
||||
|
||||
case .value(let value):
|
||||
newWidth = value > maxWidth ? maxWidth : value
|
||||
|
||||
case nil:
|
||||
break
|
||||
}
|
||||
itemWidthConstraint?.deactivate()
|
||||
|
||||
if newWidth > minWidth && newWidth < maxWidth {
|
||||
itemWidthConstraint?.constant = newWidth
|
||||
itemWidthConstraint?.activate()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -14,7 +14,7 @@ import Combine
|
||||
/// It usually represents a supplementary or utilitarian action. A button icon can stand alone, but often
|
||||
/// exists in a group when there are several actions that can be performed.
|
||||
@objc(VDSButtonIcon)
|
||||
open class ButtonIcon: Control, Changeable {
|
||||
open class ButtonIcon: Control, Changeable, ParentViewProtocol {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
@ -109,6 +109,8 @@ open class ButtonIcon: Control, Changeable {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
open var children: [any ViewProtocol] { [icon] }
|
||||
|
||||
public var onChangeSubscriber: AnyCancellable?
|
||||
|
||||
///Badge Indicator object used to render for the ButtonIcon.
|
||||
@ -402,17 +404,36 @@ open class ButtonIcon: Control, Changeable {
|
||||
centerXConstraint?.activate()
|
||||
centerYConstraint = icon.centerYAnchor.constraint(equalTo: iconLayoutGuide.centerYAnchor, constant: 0)
|
||||
centerYConstraint?.activate()
|
||||
|
||||
publisher(for: .touchUpInside)
|
||||
.sink(receiveValue: { [weak self] _ in
|
||||
guard let self, isEnabled,
|
||||
selectedIconName != nil,
|
||||
selectable else { return }
|
||||
toggle()
|
||||
})
|
||||
.store(in: &subscribers)
|
||||
}
|
||||
|
||||
/// Executed on initialization for this View.
|
||||
open override func initialSetup() {
|
||||
super.initialSetup()
|
||||
onClick = { control in
|
||||
guard control.isEnabled else { return }
|
||||
if control.selectedIconName != nil && control.selectable {
|
||||
control.toggle()
|
||||
}
|
||||
}
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
badgeIndicatorModel = nil
|
||||
kind = .ghost
|
||||
surfaceType = .colorFill
|
||||
iconName = nil
|
||||
selectedIconName = nil
|
||||
selectedIconColorConfiguration = nil
|
||||
size = .large
|
||||
floating = false
|
||||
fitToIcon = false
|
||||
hideBorder = true
|
||||
showBadgeIndicator = false
|
||||
selectable = false
|
||||
iconOffset = .init(x: 0, y: 0)
|
||||
customContainerSize = nil
|
||||
customIconSize = nil
|
||||
customBadgeIndicatorOffset = nil
|
||||
onChange = nil
|
||||
}
|
||||
|
||||
/// This will change the state of the Selector and execute the actionBlock if provided.
|
||||
@ -422,26 +443,6 @@ open class ButtonIcon: Control, Changeable {
|
||||
sendActions(for: .valueChanged)
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
shouldUpdateView = false
|
||||
kind = .ghost
|
||||
surfaceType = .colorFill
|
||||
size = .large
|
||||
floating = false
|
||||
hideBorder = true
|
||||
iconOffset = .init(x: 0, y: 0)
|
||||
iconName = nil
|
||||
selectedIconName = nil
|
||||
showBadgeIndicator = false
|
||||
selectable = false
|
||||
badgeIndicatorModel = nil
|
||||
onChange = nil
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
open override func updateView() {
|
||||
super.updateView()
|
||||
|
||||
@ -10,7 +10,7 @@ import Foundation
|
||||
extension ButtonIcon {
|
||||
|
||||
//Model that represents the options available for the Badge Indicator
|
||||
public struct BadgeIndicatorModel {
|
||||
public struct BadgeIndicatorModel: Equatable {
|
||||
/// Enum used to describe the badge indicator direction of icon button determining the expand direction.
|
||||
public enum ExpandDirection: String, CaseIterable {
|
||||
case right, center, left
|
||||
|
||||
@ -90,18 +90,28 @@ open class Icon: View {
|
||||
addSubview(imageView)
|
||||
imageView.pinToSuperView()
|
||||
|
||||
backgroundColor = .clear
|
||||
|
||||
isAccessibilityElement = true
|
||||
accessibilityTraits = .image
|
||||
accessibilityTraits = .none
|
||||
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
backgroundColor = .clear
|
||||
color = VDSColor.paletteBlack
|
||||
size = .medium
|
||||
name = nil
|
||||
customSize = nil
|
||||
imageView.image = nil
|
||||
|
||||
accessibilityHint = "image"
|
||||
bridge_accessibilityLabelBlock = { [weak self] in
|
||||
guard let self else { return "" }
|
||||
return name?.rawValue ?? "icon"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
open override func updateView() {
|
||||
super.updateView()
|
||||
@ -119,12 +129,6 @@ open class Icon: View {
|
||||
invalidateIntrinsicContentSize()
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
color = VDSColor.paletteBlack
|
||||
imageView.image = nil
|
||||
}
|
||||
}
|
||||
|
||||
extension UIImage {
|
||||
|
||||
@ -24,7 +24,7 @@ extension Icon {
|
||||
/// let icon = Icon()
|
||||
/// icon.name = .foo
|
||||
/// ```
|
||||
public struct Name: RawRepresentable {
|
||||
public struct Name: RawRepresentable, Equatable {
|
||||
public typealias RawValue = String
|
||||
public var rawValue: String
|
||||
|
||||
|
||||
422
VDS/Components/InputStepper/InputStepper.swift
Normal file
422
VDS/Components/InputStepper/InputStepper.swift
Normal file
@ -0,0 +1,422 @@
|
||||
//
|
||||
// InputStepper.swift
|
||||
// VDS
|
||||
//
|
||||
// Created by Kanamarlapudi, Vasavi on 24/06/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import VDSCoreTokens
|
||||
import Combine
|
||||
|
||||
/// A stepper is a two-segment control that people use to increase or decrease an incremental value.'
|
||||
@objc(VDSInputStepper)
|
||||
open class InputStepper: EntryFieldBase<Int> {
|
||||
|
||||
//--------------------------------------------------
|
||||
// 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 size of Input Stepper.
|
||||
public enum Size: String, CaseIterable {
|
||||
case large, small
|
||||
|
||||
var minWidth: CGFloat {
|
||||
self == .large ? 121 : 90
|
||||
}
|
||||
|
||||
var minHeight: CGFloat {
|
||||
self == .large ? 44 : 32
|
||||
}
|
||||
|
||||
var space: CGFloat {
|
||||
self == .large ? VDSLayout.space3X : VDSLayout.space2X
|
||||
}
|
||||
var padding: CGFloat {
|
||||
self == .large ? 6.0 : VDSLayout.space1X
|
||||
}
|
||||
|
||||
var buttonContainerSize: Int {
|
||||
self == .large ? 32 : 24
|
||||
}
|
||||
|
||||
var textStyle: TextStyle {
|
||||
self == .large ? .boldBodyLarge : .boldBodySmall
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum used to describe the width of a fixed value or percentage of the input stepper control.
|
||||
public enum ControlWidth {
|
||||
case percentage(CGFloat)
|
||||
case value(CGFloat)
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
open override var children: [any ViewProtocol] {
|
||||
var current = super.children
|
||||
current.append(contentsOf: [decrementButton, incrementButton, textLabel])
|
||||
return current
|
||||
}
|
||||
|
||||
/// If there is a width that is larger than this size's minimumWidth, the input stepper will resize to this width.
|
||||
open var controlWidth: ControlWidth? {
|
||||
get { _controlWidth }
|
||||
set {
|
||||
if let newValue {
|
||||
switch newValue {
|
||||
case .percentage(let percentage):
|
||||
if percentage <= 100.0 {
|
||||
_controlWidth = newValue
|
||||
}
|
||||
case .value(let value):
|
||||
if value > 0 && value > containerSize.width {
|
||||
_controlWidth = newValue
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_controlWidth = nil
|
||||
}
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
/// Accepts percentage value to width of parent container.
|
||||
open var widthPercentage: CGFloat? {
|
||||
didSet {
|
||||
if let percentage = widthPercentage, percentage > 100 {
|
||||
widthPercentage = 100
|
||||
}
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
private var _defaultValue: Int = 0
|
||||
open override var defaultValue: Int? {
|
||||
get { _defaultValue }
|
||||
set {
|
||||
if let newValue {
|
||||
_defaultValue = newValue > maxValue ? maxValue : newValue < minValue ? minValue : newValue
|
||||
} else {
|
||||
_defaultValue = 0
|
||||
}
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
open override var value: Int? { return defaultValue }
|
||||
|
||||
/// Maximum value of the input stepper, defaults to '99'.
|
||||
lazy open var maxValue: Int = { _defaultMaxValue }() {
|
||||
didSet {
|
||||
if maxValue > _defaultMaxValue || maxValue < _defaultMinValue && maxValue > minValue {
|
||||
maxValue = _defaultMaxValue
|
||||
}
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
/// Minimum value of the input stepper, defaults to '0'.
|
||||
lazy open var minValue: Int = { _defaultMinValue }() {
|
||||
didSet {
|
||||
if minValue < _defaultMinValue && minValue >= _defaultMaxValue && minValue < maxValue {
|
||||
minValue = _defaultMinValue
|
||||
}
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
/// The size of the input stepper. Defaults to 'large'.
|
||||
open var size: Size = .large { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// Accepts any text or character to appear next to input stepper value.
|
||||
open var trailingText: String? { didSet { setNeedsUpdate() } }
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Properties
|
||||
//--------------------------------------------------
|
||||
private var _controlWidth: ControlWidth? = nil
|
||||
private var _defaultMinValue: Int = 0
|
||||
private var _defaultMaxValue: Int = 99
|
||||
|
||||
/// This is the view that will be wrapped with the border for userInteraction.
|
||||
/// The only subview of this view is the stepperStackView.
|
||||
internal var stepperContainerView = View().with {
|
||||
$0.isAccessibilityElement = true
|
||||
$0.accessibilityLabel = "Input Stepper"
|
||||
}
|
||||
|
||||
internal var stepperStackView = UIStackView().with {
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
$0.axis = .horizontal
|
||||
$0.distribution = .fill
|
||||
$0.alignment = .fill
|
||||
}
|
||||
|
||||
internal var decrementButton = ButtonIcon().with {
|
||||
$0.kind = .ghost
|
||||
$0.iconName = Icon.Name(name: "minus")
|
||||
$0.iconOffset = .init(x: -2, y: 0)
|
||||
$0.customContainerSize = 32
|
||||
$0.icon.customSize = 16
|
||||
$0.backgroundColor = .clear
|
||||
}
|
||||
|
||||
internal var incrementButton = ButtonIcon().with {
|
||||
$0.kind = .ghost
|
||||
$0.iconName = Icon.Name(name: "plus")
|
||||
$0.iconOffset = .init(x: 2, y: 0)
|
||||
$0.customContainerSize = 32
|
||||
$0.icon.customSize = 16
|
||||
$0.backgroundColor = .clear
|
||||
}
|
||||
|
||||
internal var textLabel = Label().with {
|
||||
$0.setContentCompressionResistancePriority(.required, for: .vertical)
|
||||
$0.textStyle = .boldBodyLarge
|
||||
$0.numberOfLines = 1
|
||||
$0.lineBreakMode = .byTruncatingTail
|
||||
$0.textAlignment = .center
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Constraints
|
||||
//--------------------------------------------------
|
||||
internal var stepperWidthConstraint: NSLayoutConstraint?
|
||||
internal var stepperHeightConstraint: NSLayoutConstraint?
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Configuration Properties
|
||||
//--------------------------------------------------
|
||||
internal override var containerSize: CGSize { CGSize(width: size.minWidth, height: size.minHeight) }
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Lifecycle
|
||||
//--------------------------------------------------
|
||||
|
||||
/// 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()
|
||||
// Set initial states
|
||||
defaultValue = 0
|
||||
containerView.isEnabled = false
|
||||
statusIcon.isHidden = true
|
||||
|
||||
//override the default settings since the containerView
|
||||
//fieldStackView relationShip needs to be updated
|
||||
//we are not applying spacing either in the edges since this
|
||||
//is the view that will take place of the containerView for the
|
||||
//design of the original "containerView". This will get refactored at
|
||||
//some point.
|
||||
fieldStackView.applyAlignment(.leading)
|
||||
|
||||
// Add listeners
|
||||
decrementButton.onClick = { _ in self.decrementButtonClick() }
|
||||
incrementButton.onClick = { _ in self.incrementButtonClick() }
|
||||
|
||||
// setting color config
|
||||
textLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable()
|
||||
}
|
||||
|
||||
open override func getFieldContainer() -> UIView {
|
||||
stepperStackView.addArrangedSubview(decrementButton)
|
||||
stepperStackView.addArrangedSubview(textLabel)
|
||||
stepperStackView.addArrangedSubview(incrementButton)
|
||||
|
||||
// Set space between decrement button, label, and increment button relative to input Stepper size.
|
||||
stepperStackView.setCustomSpacing(size.space, after: decrementButton)
|
||||
stepperStackView.setCustomSpacing(size.space, after: textLabel)
|
||||
|
||||
// stepperContainerView for controls in EntryFieldBase.controlContainerView
|
||||
stepperContainerView.addSubview(stepperStackView)
|
||||
|
||||
// Update Edge insets relative to input Stepper size.
|
||||
stepperStackView.pinToSuperView(.uniform(size.padding))
|
||||
|
||||
stepperWidthConstraint = stepperContainerView.width(constant: containerSize.width, priority: .required)
|
||||
stepperHeightConstraint = stepperContainerView.height(constant: containerSize.height, priority: .required)
|
||||
|
||||
return stepperContainerView
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
open override func updateView() {
|
||||
super.updateView()
|
||||
statusIcon.isHidden = true
|
||||
|
||||
// Update label
|
||||
textLabel.isEnabled = isEnabled
|
||||
textLabel.surface = surface
|
||||
textLabel.text = "\(_defaultValue) " + (trailingText ?? "")
|
||||
textLabel.textStyle = size.textStyle
|
||||
|
||||
updateButtonStates()
|
||||
}
|
||||
|
||||
open override var accessibilityElements: [Any]? {
|
||||
get {
|
||||
var elements = [Any]()
|
||||
|
||||
if !isReadOnly || isEnabled {
|
||||
elements.append(contentsOf: [titleLabel, containerView, decrementButton, textLabel, incrementButton])
|
||||
} else {
|
||||
elements.append(contentsOf: [titleLabel, containerView, textLabel])
|
||||
}
|
||||
|
||||
if showError {
|
||||
if let errorText, !errorText.isEmpty {
|
||||
elements.append(errorLabel)
|
||||
}
|
||||
}
|
||||
if let helperText, !helperText.isEmpty {
|
||||
elements.append(helperLabel)
|
||||
}
|
||||
return elements
|
||||
}
|
||||
|
||||
set { super.accessibilityElements = newValue }
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
textLabel.reset()
|
||||
controlWidth = nil
|
||||
widthPercentage = nil
|
||||
defaultValue = 0
|
||||
minValue = _defaultMinValue
|
||||
maxValue = _defaultMaxValue
|
||||
trailingText = nil
|
||||
size = .large
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
override func updateContainerView() {
|
||||
//we are not calling super since we
|
||||
//are using the fieldStackView as the "containerView"
|
||||
//which will get the look/feel of the containerView.
|
||||
//this will get refactored in the future.
|
||||
fieldStackView.backgroundColor = containerBackgroundColor
|
||||
fieldStackView.layer.borderColor = borderColorConfiguration.getColor(self).cgColor
|
||||
fieldStackView.layer.borderWidth = VDSFormControls.borderWidth
|
||||
}
|
||||
|
||||
internal override func updateContainerWidth() {
|
||||
//we are not calling super here since
|
||||
//we are changing how the widths are getting calculated
|
||||
//now by including a percentage.
|
||||
|
||||
defer {
|
||||
fieldStackView.layer.cornerRadius = containerSize.height / 2
|
||||
}
|
||||
|
||||
stepperWidthConstraint?.deactivate()
|
||||
widthConstraint?.deactivate()
|
||||
|
||||
var widthConstraintConstant: CGFloat?
|
||||
|
||||
if let widthPercentage, let superWidth = horizontalPinnedWidth() {
|
||||
// test value vs minimum width and take the greater value
|
||||
widthConstraintConstant = max(superWidth * (widthPercentage / 100), minWidth)
|
||||
} else if let width, width >= minWidth, width <= maxWidth {
|
||||
widthConstraintConstant = width
|
||||
} else if let parentWidth = width, parentWidth >= maxWidth {
|
||||
widthConstraintConstant = maxWidth
|
||||
} else if let parentWidth = width, parentWidth <= minWidth {
|
||||
widthConstraintConstant = minWidth
|
||||
}
|
||||
|
||||
if let widthConstraintConstant {
|
||||
widthConstraint?.constant = widthConstraintConstant
|
||||
widthConstraint?.activate()
|
||||
}
|
||||
|
||||
// Update Edge insets if size changes applied.
|
||||
stepperStackView.applyAlignment(.fill, edges: .uniform(size.padding))
|
||||
|
||||
// Update height if size changes applied.
|
||||
stepperHeightConstraint?.constant = containerSize.height
|
||||
|
||||
//update the stepper's widthConstraint if
|
||||
//controlWidth was set
|
||||
guard let controlWidth else {
|
||||
return
|
||||
}
|
||||
|
||||
// Set the inputStepper's controlWidth based on percentage received relative to its parentView's frame.
|
||||
let containerWidth: CGFloat = widthConstraintConstant ?? containerView.frame.size.width
|
||||
var stepperWidthConstant: CGFloat?
|
||||
var stepperWidth: CGFloat
|
||||
|
||||
switch controlWidth {
|
||||
case .percentage(let percentage):
|
||||
stepperWidth = max(containerWidth * ((percentage) / 100), minWidth)
|
||||
|
||||
case .value(let value):
|
||||
stepperWidth = value
|
||||
}
|
||||
|
||||
//get the value of the stepperWidthConstant
|
||||
if stepperWidth >= containerSize.width && stepperWidth <= containerWidth {
|
||||
stepperWidthConstant = stepperWidth
|
||||
} else if stepperWidth >= containerWidth {
|
||||
stepperWidthConstant = containerWidth
|
||||
}
|
||||
|
||||
if let stepperWidthConstant {
|
||||
stepperWidthConstraint?.constant = stepperWidthConstant
|
||||
stepperWidthConstraint?.activate()
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Methods
|
||||
//--------------------------------------------------
|
||||
internal func decrementButtonClick() {
|
||||
if _defaultValue > minValue {
|
||||
defaultValue = _defaultValue - 1
|
||||
sendActions(for: .valueChanged)
|
||||
}
|
||||
}
|
||||
|
||||
internal func incrementButtonClick() {
|
||||
if _defaultValue < maxValue {
|
||||
defaultValue = _defaultValue + 1
|
||||
sendActions(for: .valueChanged)
|
||||
}
|
||||
}
|
||||
|
||||
internal func updateButtonStates() {
|
||||
decrementButton.customContainerSize = size.buttonContainerSize
|
||||
incrementButton.customContainerSize = size.buttonContainerSize
|
||||
decrementButton.surface = surface
|
||||
incrementButton.surface = surface
|
||||
|
||||
if isReadOnly || !isEnabled {
|
||||
decrementButton.isEnabled = false
|
||||
incrementButton.isEnabled = false
|
||||
} else {
|
||||
decrementButton.isEnabled = (defaultValue ?? _defaultMaxValue ) > minValue ? true : false
|
||||
incrementButton.isEnabled = (defaultValue ?? _defaultMinValue) < maxValue ? true : false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
44
VDS/Components/InputStepper/InputStepperLog.txt
Normal file
44
VDS/Components/InputStepper/InputStepperLog.txt
Normal file
@ -0,0 +1,44 @@
|
||||
|
||||
MM/DD/YYYY
|
||||
----------------
|
||||
|
||||
02/2024
|
||||
----------------
|
||||
- New component
|
||||
|
||||
02/15/2024
|
||||
----------------
|
||||
- Added Border align: Inside to Anatomy
|
||||
- Removed leadingText property values from States.
|
||||
- Added Read-only to States.
|
||||
- Created a section for Minimum width in Layout and spacing and updated the minWidth to 145px.
|
||||
- Added trailingText spacing to Layout and spacing.
|
||||
- Reduced space between Button Icons and text to 12px in Layout and spacing.
|
||||
- Added top/bottom padding values to Layout and spacing.
|
||||
|
||||
03/01/2024
|
||||
----------------
|
||||
- Removed Leading Text from “Content and other properties” section.
|
||||
|
||||
03/20/2024
|
||||
----------------
|
||||
- Updated Anatomy artwork and items
|
||||
- Added width and controlWidth to Configurations
|
||||
- Updated Width under Layout and spacing to show layout examples
|
||||
|
||||
04/12/2024
|
||||
----------------
|
||||
- Added a new configuration property (size) that includes large and small
|
||||
- Reduced details from Anatomy page and added them to Configurations/Size
|
||||
- Added Hit area, Small Input Stepper spacing properties to Layout and spacing
|
||||
- Updated the Behavior page to display disabled button icon when value is at max and min
|
||||
|
||||
04/29/2024
|
||||
----------------
|
||||
- Updated the Behavior page to display disabled button icon when value is at max and min
|
||||
|
||||
05/10/2024
|
||||
----------------
|
||||
- Added helperTextPlacement property to Configurations
|
||||
- Added Layout examples for right Helper Text placement in Layout and Spacing
|
||||
- Added overflow examples in Overflow section of Layout and Spacing
|
||||
@ -39,6 +39,27 @@ public extension String {
|
||||
func isValid(range: NSRange) -> Bool {
|
||||
range.location >= 0 && range.length > 0 && range.location + range.length <= count
|
||||
}
|
||||
|
||||
func index(from: Int) -> Index {
|
||||
return self.index(startIndex, offsetBy: from)
|
||||
}
|
||||
|
||||
func substring(from: Int) -> String {
|
||||
let fromIndex = index(from: from)
|
||||
return String(self[fromIndex...])
|
||||
}
|
||||
|
||||
func substring(to: Int) -> String {
|
||||
let toIndex = index(from: to)
|
||||
return String(self[..<toIndex])
|
||||
}
|
||||
|
||||
func substring(with r: Range<Int>) -> String {
|
||||
let startIndex = index(from: r.lowerBound)
|
||||
let endIndex = index(from: r.upperBound)
|
||||
return String(self[startIndex..<endIndex])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public extension NSAttributedString {
|
||||
|
||||
@ -63,9 +63,7 @@ public struct TextStyleLabelAttribute: LabelAttributeModel {
|
||||
|
||||
//set lineHeight
|
||||
if textStyle.lineHeight > 0.0 {
|
||||
let lineHeight = textStyle.lineHeight
|
||||
let adjustment = lineHeight > textStyle.font.lineHeight ? 2.0 : 1.0
|
||||
let baselineOffset = (lineHeight - textStyle.font.lineHeight) / 2.0 / adjustment
|
||||
let lineHeight = textStyle.lineHeight + abs(textStyle.edgeInsets.bottom) + abs(textStyle.edgeInsets.top)
|
||||
let paragraph = NSMutableParagraphStyle().with {
|
||||
$0.maximumLineHeight = lineHeight
|
||||
$0.minimumLineHeight = lineHeight
|
||||
@ -74,7 +72,6 @@ public struct TextStyleLabelAttribute: LabelAttributeModel {
|
||||
}
|
||||
attributedString.removeAttribute(.baselineOffset, range: range)
|
||||
attributedString.removeAttribute(.paragraphStyle, range: range)
|
||||
attributedString.addAttribute(.baselineOffset, value: baselineOffset, range: range)
|
||||
attributedString.addAttribute(.paragraphStyle, value: paragraph, range: range)
|
||||
|
||||
} else if textPosition != .left {
|
||||
@ -87,3 +84,4 @@ public struct TextStyleLabelAttribute: LabelAttributeModel {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -191,42 +191,46 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Lifecycle
|
||||
//--------------------------------------------------
|
||||
open func initialSetup() {
|
||||
private func initialSetup() {
|
||||
if !initialSetupPerformed {
|
||||
initialSetupPerformed = true
|
||||
shouldUpdateView = false
|
||||
setup()
|
||||
setDefaults()
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
open func setup() {
|
||||
//register for ContentSizeChanges
|
||||
NotificationCenter
|
||||
.Publisher(center: .default, name: UIContentSizeCategory.didChangeNotification)
|
||||
.sink { [weak self] notification in
|
||||
self?.setNeedsUpdate()
|
||||
}.store(in: &subscribers)
|
||||
backgroundColor = .clear
|
||||
numberOfLines = 0
|
||||
lineBreakMode = .byTruncatingTail
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
accessibilityCustomActions = []
|
||||
isAccessibilityElement = true
|
||||
}
|
||||
|
||||
open func setDefaults() {
|
||||
backgroundColor = .clear
|
||||
accessibilityTraits = .staticText
|
||||
textAlignment = .left
|
||||
setup()
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
open func setup() {
|
||||
}
|
||||
|
||||
open func reset() {
|
||||
shouldUpdateView = false
|
||||
accessibilityCustomActions = []
|
||||
surface = .light
|
||||
isEnabled = true
|
||||
attributes = nil
|
||||
textStyle = .defaultStyle
|
||||
lineBreakMode = .byTruncatingTail
|
||||
textAlignment = .left
|
||||
text = nil
|
||||
attributedText = nil
|
||||
numberOfLines = 0
|
||||
backgroundColor = .clear
|
||||
}
|
||||
|
||||
open func reset() {
|
||||
shouldUpdateView = false
|
||||
setDefaults()
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
@ -80,11 +80,6 @@ open class Line: View {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
open override func updateView() {
|
||||
super.updateView()
|
||||
@ -93,8 +88,8 @@ open class Line: View {
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
style = .primary
|
||||
orientation = .horizontal
|
||||
}
|
||||
|
||||
239
VDS/Components/ListUnordered/ListUnordered.swift
Normal file
239
VDS/Components/ListUnordered/ListUnordered.swift
Normal file
@ -0,0 +1,239 @@
|
||||
//
|
||||
// ListUnordered.swift
|
||||
// VDS
|
||||
//
|
||||
// Created by Vasavi Kanamarlapudi on 16/10/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import VDSCoreTokens
|
||||
|
||||
/// List unordered breaks up related content into distinct phrases or sentences, which improves scannability.
|
||||
/// This component should be used when the text items don’t need to be in numeric order.
|
||||
@objcMembers
|
||||
@objc(VDSListUnordered)
|
||||
open class ListUnordered: 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 that represents the size availble for the component.
|
||||
public enum Size: String, DefaultValuing, CaseIterable {
|
||||
case large
|
||||
case medium
|
||||
case small
|
||||
case micro
|
||||
|
||||
public static var defaultValue: Self { .large }
|
||||
|
||||
/// TextStyle relative to Size.
|
||||
public var textStyle: TextStyle.StandardStyle {
|
||||
switch self {
|
||||
case .large:
|
||||
return .bodyLarge
|
||||
case .medium:
|
||||
return .bodyMedium
|
||||
case .small:
|
||||
return .bodySmall
|
||||
case .micro:
|
||||
return .micro
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum that represents the type of spacing available for the component.
|
||||
public enum Spacing: String, CaseIterable {
|
||||
case standard, compact
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
/// Size of the component. The default size is Large.
|
||||
open var size: Size = .defaultValue { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// Spacing type of the component.
|
||||
open var spacing: Spacing = .standard { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// Lead-in text that shows as the top text for the component. This is optional.
|
||||
open var leadInText: String? = nil { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// Array of unordered list items to show for the component.
|
||||
open var unorderedList: [ListUnorderedItemModel] = [] { didSet { setNeedsUpdate() }}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Configuration Properties
|
||||
//--------------------------------------------------
|
||||
// It can be used for Glyph level 1.
|
||||
private var disc = "•"
|
||||
|
||||
// It can be used for Glyph Level 2.
|
||||
private var endash = "–"
|
||||
|
||||
// Spacing between the list items.
|
||||
private var spaceBetweenItems: CGFloat {
|
||||
switch (size, spacing) {
|
||||
case (.large, .standard):
|
||||
return VDSLayout.space4X
|
||||
case (.medium, .standard), (.small, .standard), (.micro, .standard):
|
||||
return VDSLayout.space3X
|
||||
case (.large, .compact):
|
||||
return VDSLayout.space2X
|
||||
case (.medium, .compact), (.small, .compact), (.micro, .compact):
|
||||
return VDSLayout.space1X
|
||||
}
|
||||
}
|
||||
|
||||
// Padding that can be used in an item between the glyph and the item text.
|
||||
private var padding: CGFloat {
|
||||
switch (size, spacing) {
|
||||
case (.large, .standard), (.large, .compact):
|
||||
return VDSLayout.space3X
|
||||
case (.medium, .standard), (.small, .standard), (.micro, .standard), (.medium, .compact), (.small, .compact), (.micro, .compact):
|
||||
return VDSLayout.space2X
|
||||
}
|
||||
}
|
||||
|
||||
private let textColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark)
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Properties
|
||||
//--------------------------------------------------
|
||||
private lazy var listStackView = UIStackView().with {
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
$0.axis = .vertical
|
||||
$0.distribution = .fill
|
||||
$0.spacing = spaceBetweenItems
|
||||
$0.backgroundColor = .clear
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
/// Called once when a view is initialized and is used to Setup additional UI or other constants and config.texturations.
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
|
||||
// add stackview
|
||||
addSubview(listStackView)
|
||||
listStackView.heightGreaterThanEqualTo(constant:0)
|
||||
listStackView.pinToSuperView()
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
leadInText = nil
|
||||
unorderedList = []
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
open override func updateView() {
|
||||
super.updateView()
|
||||
listStackView.removeArrangedSubviews()
|
||||
listStackView.subviews.forEach { $0.removeFromSuperview() }
|
||||
listStackView.spacing = spaceBetweenItems
|
||||
|
||||
if leadInText != nil {
|
||||
let listItem = getListItem(with:self.leadInText, surface: surface)
|
||||
listStackView.addArrangedSubview(listItem)
|
||||
}
|
||||
|
||||
unorderedList.forEach { item in
|
||||
let listItem = getListItem(levelOneText: item.levelOneText, surface: surface)
|
||||
listStackView.addArrangedSubview(listItem)
|
||||
|
||||
item.levelTwoText?.forEach { text in
|
||||
let listItem = getListItem(levelTwoText: text, surface: surface)
|
||||
listStackView.addArrangedSubview(listItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Methods
|
||||
//--------------------------------------------------
|
||||
// Get Label with the required text and text formats.
|
||||
func getLabel(with text: String?, surface: Surface) -> Label {
|
||||
let textLabel = Label().with {
|
||||
$0.isAccessibilityElement = true
|
||||
$0.lineBreakMode = .byWordWrapping
|
||||
$0.text = text
|
||||
$0.textStyle = size.textStyle.regular
|
||||
$0.textColor = textColorConfiguration.getColor(surface)
|
||||
$0.surface = surface
|
||||
}
|
||||
return textLabel
|
||||
}
|
||||
|
||||
// Get the list item with the required text (LeadInText, Level 1 Text, Level 2 Text).
|
||||
func getListItem(with leadInText:String? = nil, levelOneText: String? = nil, levelTwoText: String? = nil, surface:Surface) -> UIView {
|
||||
let itemStackView = UIStackView().with {
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
$0.axis = .horizontal
|
||||
$0.alignment = .leading
|
||||
$0.distribution = .fill
|
||||
$0.spacing = padding
|
||||
$0.backgroundColor = .clear
|
||||
}
|
||||
|
||||
// StackView with LeadIntext if provided.
|
||||
if leadInText != nil {
|
||||
let leadTextLabel = getLabel(with: leadInText, surface: surface)
|
||||
itemStackView.addArrangedSubview(leadTextLabel)
|
||||
}
|
||||
|
||||
// StackView with Level 1 Text if provided.
|
||||
if levelOneText != nil {
|
||||
|
||||
// Add level 1 glyph: 'disc, bold'
|
||||
let discLabel = getLabel(with: disc, surface: surface)
|
||||
discLabel.widthAnchor.constraint(equalToConstant: discLabel.intrinsicContentSize.width).activate()
|
||||
itemStackView.addArrangedSubview(discLabel)
|
||||
|
||||
// Add level 1 Text
|
||||
let levelOneLabel = getLabel(with: levelOneText, surface: surface)
|
||||
itemStackView.addArrangedSubview(levelOneLabel)
|
||||
}
|
||||
|
||||
// StackView with Level 2 Text if provided.
|
||||
if levelTwoText != nil {
|
||||
|
||||
// Set level 2 leading space as needed for alignment.
|
||||
let discSpaceView = View()
|
||||
let discLabel = getLabel(with: disc, surface: surface)
|
||||
discSpaceView.widthAnchor.constraint(equalToConstant: discLabel.intrinsicContentSize.width).activate()
|
||||
itemStackView.addArrangedSubview(discSpaceView)
|
||||
|
||||
// Add level 2 glyph: 'en dash, regular'
|
||||
let endashLabel = getLabel(with: endash, surface: surface)
|
||||
endashLabel.widthAnchor.constraint(equalToConstant: endashLabel.intrinsicContentSize.width).activate()
|
||||
itemStackView.addArrangedSubview(endashLabel)
|
||||
|
||||
// Add level 2 Text
|
||||
let levelTwoLabel = getLabel(with: levelTwoText, surface: surface)
|
||||
itemStackView.addArrangedSubview(levelTwoLabel)
|
||||
}
|
||||
return itemStackView
|
||||
}
|
||||
}
|
||||
41
VDS/Components/ListUnordered/ListUnordered.txt
Normal file
41
VDS/Components/ListUnordered/ListUnordered.txt
Normal file
@ -0,0 +1,41 @@
|
||||
MM/DD/YYYY
|
||||
----------------
|
||||
- Initial Brand 3.0 handoff
|
||||
|
||||
05/2/2022
|
||||
----------------
|
||||
- Added Body Medium to size configuration
|
||||
|
||||
05/5/2022
|
||||
----------------
|
||||
- Added Spacing configuration (Standard, Compact) Web handoff
|
||||
|
||||
08/2/2022
|
||||
----------------
|
||||
- Included a VDS Note about the Spacing prop naming rationale
|
||||
|
||||
08/10/2022
|
||||
----------------
|
||||
- Updated default and inverted prop to light and dark surface.
|
||||
|
||||
12/13/2022
|
||||
----------------
|
||||
- Replaced focus border pixel and style & spacing values with tokens.
|
||||
|
||||
01/10/2023
|
||||
----------------
|
||||
- Removed from Anatomy section: “List item text”
|
||||
- Updated “Glyph level 1” to “List Item Level 1”
|
||||
- Updated “Glyph level 2” to “List Item Level 2”
|
||||
- Updated image markers to reflect changes
|
||||
|
||||
02/02/2023
|
||||
----------------
|
||||
- Reduced left padding for all Level 2 sizes so that the Glyph aligns with the text in Level 1.
|
||||
- Added dashed line on all sizes to indicate Level 2 alignment under Level 1.
|
||||
- Changed “endash” to “endash, regular” under Size section.
|
||||
- Updated all Level 1 and Level 2 glyph widths to “Hug”
|
||||
|
||||
12/26/23
|
||||
----------------
|
||||
- Deleted Decisions log
|
||||
23
VDS/Components/ListUnordered/ListUnorderedItemModel.swift
Normal file
23
VDS/Components/ListUnordered/ListUnorderedItemModel.swift
Normal file
@ -0,0 +1,23 @@
|
||||
//
|
||||
// ListUnorderedItemModel.swift
|
||||
// VDS
|
||||
//
|
||||
// Created by Vasavi Kanamarlapudi on 16/10/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
extension ListUnordered {
|
||||
public struct ListUnorderedItemModel: Equatable {
|
||||
|
||||
/// Item Level 1 that shows text with glyph - disc, bold.
|
||||
public var levelOneText: String
|
||||
|
||||
/// Item Level 2 that shows text (one or many) with glyph - en dash. This is optional.
|
||||
public var levelTwoText: [String?]?
|
||||
|
||||
public init(itemLevelOneText: String, itemLevelTwoTexts: [String?]? = nil) {
|
||||
self.levelOneText = itemLevelOneText
|
||||
self.levelTwoText = itemLevelTwoTexts
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -10,6 +10,7 @@ import UIKit
|
||||
import VDSCoreTokens
|
||||
|
||||
/// ViewController to show the Loader, this will be presented using the LoaderLaunchable Protocl.
|
||||
@objc(VDSLoaderViewController)
|
||||
open class LoaderViewController: UIViewController, Surfaceable {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Properties
|
||||
|
||||
149
VDS/Components/Modal/Modal.swift
Normal file
149
VDS/Components/Modal/Modal.swift
Normal file
@ -0,0 +1,149 @@
|
||||
//
|
||||
// Modal.swift
|
||||
// VDS
|
||||
//
|
||||
// Created by Kanamarlapudi, Vasavi on 05/09/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import VDSCoreTokens
|
||||
import Combine
|
||||
|
||||
/// A Modal is an overlay that interrupts the user flow to force the customer to provide information or a response.
|
||||
/// After the customer interacts with the modal, they can return to the parent content.
|
||||
@objc(VDSModal)
|
||||
open class Modal: Control, ModalLaunchable {
|
||||
|
||||
//--------------------------------------------------
|
||||
// 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: - Private Properties
|
||||
//--------------------------------------------------
|
||||
internal var showModalButton = Button().with {
|
||||
$0.use = .primary
|
||||
$0.text = "Show Modal"
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
/// Text rendered for the title of the modal
|
||||
open var title: String? { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// Text rendered for the content of the modal
|
||||
open var content: String? { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// UIView rendered for the content area of the modal
|
||||
open var contentView: UIView? { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// Array of Buttonable Views that are shown as Modal Footer. Primary and Close button data for modal button group.
|
||||
open var buttonData: [ButtonBase]? { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// If provided, the Modal has the option to be displayed at full screen.
|
||||
open var fullScreenDialog: Bool = false { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// If provided, close button can not be present.
|
||||
open var hideCloseButton: Bool = false { didSet { setNeedsUpdate() } }
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
|
||||
addSubview(showModalButton)
|
||||
showModalButton.pinToSuperView()
|
||||
backgroundColor = .clear
|
||||
|
||||
isAccessibilityElement = true
|
||||
accessibilityTraits = .button
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
title = nil
|
||||
content = nil
|
||||
contentView = nil
|
||||
buttonData = nil
|
||||
fullScreenDialog = false
|
||||
hideCloseButton = false
|
||||
|
||||
showModalButton.onClick = { _ in self.showModalButtonClick() }
|
||||
|
||||
bridge_accessibilityLabelBlock = { [weak self] in
|
||||
guard let self else { return "" }
|
||||
var label = title
|
||||
if label == nil {
|
||||
label = content
|
||||
}
|
||||
if let label, !label.isEmpty {
|
||||
return label
|
||||
} else {
|
||||
return "Modal"
|
||||
}
|
||||
}
|
||||
|
||||
bridge_accessibilityHintBlock = { [weak self] in
|
||||
guard let self else { return "" }
|
||||
return isEnabled ? "Double tap to open." : ""
|
||||
}
|
||||
}
|
||||
|
||||
internal func showModalButtonClick() {
|
||||
self.presentModal(surface: self.surface,
|
||||
modalModel: .init(closeButtonText: showModalButton.text ?? "",
|
||||
title: title,
|
||||
content: content,
|
||||
contentView: contentView,
|
||||
buttonData: buttonData,
|
||||
fullScreenDialog: fullScreenDialog,
|
||||
hideCloseButton: hideCloseButton),
|
||||
presenter: self)
|
||||
}
|
||||
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
open override func updateView() {
|
||||
super.updateView()
|
||||
showModalButton.surface = surface
|
||||
}
|
||||
|
||||
public static func accessibleText(for title: String?, content: String?, closeButtonText: String) -> String {
|
||||
var label = ""
|
||||
if let title {
|
||||
label = title
|
||||
}
|
||||
if let content {
|
||||
if !label.isEmpty {
|
||||
label += ","
|
||||
}
|
||||
label += content
|
||||
}
|
||||
return label
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: AppleGuidelinesTouchable
|
||||
extension Modal: AppleGuidelinesTouchable {
|
||||
/// Overrides to ensure that the touch point meets a minimum of the minimumTappableArea.
|
||||
override open func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||
Self.acceptablyOutsideBounds(point: point, bounds: bounds)
|
||||
}
|
||||
|
||||
}
|
||||
67
VDS/Components/Modal/ModalChangeLog.txt
Normal file
67
VDS/Components/Modal/ModalChangeLog.txt
Normal file
@ -0,0 +1,67 @@
|
||||
MM/DD/YYYY
|
||||
----------------
|
||||
- Initial Brand 3.0 handoff
|
||||
|
||||
12/17/2021
|
||||
----------------
|
||||
- Replaced focusring colors (previously interactive/onlight/ondark) with accessibility/onlight/ondark colors
|
||||
- Updated focus border name (previously interactive.focusring.onlight) with focusring.onlight/ondark
|
||||
|
||||
12/31/2021
|
||||
----------------
|
||||
- Updated Hover and Active state trigger specs. If triggered by mouse, Active same as Hover. If not, Active same as Default.
|
||||
|
||||
03/01/2022
|
||||
----------------
|
||||
- Replaced Close Non-Scaling icon with VDS Icon.
|
||||
- Removed “vector effect” from Anatomy.
|
||||
- Removed “weight” from Configurations.
|
||||
|
||||
08/10/2022
|
||||
----------------
|
||||
- Updated default and inverted prop to light and dark surface.
|
||||
- Noted that button is optional within anatomy
|
||||
|
||||
09/06/2022
|
||||
----------------
|
||||
- Updated Anatomy element names to remove the word “Modal” from text elements, updated Button to be Button Group,
|
||||
and noted Button Group as optional across all visuals within Anatomy.
|
||||
|
||||
11/30/2022
|
||||
----------------
|
||||
- Added "(web only)" to any instance of "keyboard focus"
|
||||
|
||||
12/13/2022
|
||||
----------------
|
||||
- Replaced focus border pixel and style & spacing values with tokens.
|
||||
|
||||
04/24/2023
|
||||
----------------
|
||||
- Updated all instances of Close Button (VDS Icon) with VDS Button Icon (size small)
|
||||
- Button Icon placed 8px from top/right edge
|
||||
- Use the Ghost variant of Button Icon
|
||||
- Added Button Icon props to Elements spec
|
||||
|
||||
10/17/2023
|
||||
----------------
|
||||
- Added component tokens table
|
||||
- Applied component tokens to light, dark surface configurations
|
||||
|
||||
11/22/2023
|
||||
----------------
|
||||
- Updated tab/desk visuals to reflect new corner radius value - 12px
|
||||
- Updated border radius value in Anatomy
|
||||
|
||||
11/27/2023
|
||||
----------------
|
||||
- Updated ‘border radius” to “corner radius” in Anatomy
|
||||
|
||||
12/1/2023
|
||||
----------------
|
||||
- Applied palette tokens instead of hardcoded values where component tokens included an opacity
|
||||
- Removed layer opacity annotation for instances where opacity is built into a component token
|
||||
|
||||
07/18/2024
|
||||
----------------
|
||||
- Added Scrollbar hit area with z-index specifications to the Behaviors page
|
||||
- Decreased the height of the Grab zone to equal the height of the scrollbar thumb on the Behaviors page
|
||||
237
VDS/Components/Modal/ModalDialog.swift
Normal file
237
VDS/Components/Modal/ModalDialog.swift
Normal file
@ -0,0 +1,237 @@
|
||||
//
|
||||
// ModalDialog.swift
|
||||
// VDS
|
||||
//
|
||||
// Created by Kanamarlapudi, Vasavi on 09/09/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import VDSCoreTokens
|
||||
|
||||
@objc(VDSModalDialog)
|
||||
open class ModalDialog: View, UIScrollViewDelegate, ParentViewProtocol {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
required public init() {
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
|
||||
public required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
open var children: [any ViewProtocol] { [closeCrossButton, titleLabel, contentLabel, buttonGroupData] }
|
||||
|
||||
open var modalModel = Modal.ModalModel() { didSet { setNeedsUpdate() } }
|
||||
|
||||
open var titleLabel = Label().with { label in
|
||||
label.isAccessibilityElement = true
|
||||
label.textStyle = .boldTitleLarge
|
||||
}
|
||||
|
||||
open var contentLabel = Label().with { label in
|
||||
label.isAccessibilityElement = true
|
||||
label.textStyle = .bodyLarge
|
||||
}
|
||||
|
||||
open lazy var closeCrossButton = ButtonIcon().with {
|
||||
$0.kind = .ghost
|
||||
$0.surfaceType = .colorFill
|
||||
$0.iconName = .close
|
||||
$0.size = .small
|
||||
$0.customContainerSize = UIDevice.isIPad ? 48 : 48
|
||||
$0.customIconSize = UIDevice.isIPad ? 32 : 32
|
||||
}
|
||||
|
||||
open var buttonGroupData = ButtonGroup().with {
|
||||
$0.alignment = .left
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Properties
|
||||
//--------------------------------------------------
|
||||
private var scrollView = UIScrollView().with {
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
$0.backgroundColor = .clear
|
||||
}
|
||||
|
||||
private var contentStackView = UIStackView().with {
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
$0.axis = .vertical
|
||||
$0.alignment = .leading
|
||||
$0.distribution = .fillProportionally
|
||||
$0.spacing = 0
|
||||
}
|
||||
|
||||
lazy var primaryAccessibilityElement = UIAccessibilityElement(accessibilityContainer: self).with {
|
||||
$0.accessibilityLabel = "Modal"
|
||||
}
|
||||
|
||||
// close button with the 48 x 48 px
|
||||
private var closeCrossButtonSize = 48.0
|
||||
|
||||
private let containerViewInset = UIDevice.isIPad ? VDSLayout.space12X : VDSLayout.space4X
|
||||
private let contentLabelTopSpace = UIDevice.isIPad ? VDSLayout.space8X : VDSLayout.space6X
|
||||
private let contentLabelBottomSpace = UIDevice.isIPad ? VDSLayout.space8X : VDSLayout.space12X
|
||||
private let gapBetweenButtonItems = VDSLayout.space3X
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Configuration Properties
|
||||
//--------------------------------------------------
|
||||
private let backgroundColorConfiguration = SurfaceColorConfiguration(VDSColor.backgroundPrimaryLight, VDSColor.backgroundPrimaryDark)
|
||||
private let textColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark)
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Constraints
|
||||
//--------------------------------------------------
|
||||
private var contentStackViewBottomConstraint: NSLayoutConstraint?
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
|
||||
titleLabel.accessibilityTraits = .header
|
||||
layer.cornerRadius = 12
|
||||
|
||||
// Add titleLabel, contentLabel to contentStack.
|
||||
contentStackView.addArrangedSubview(titleLabel)
|
||||
contentStackView.addArrangedSubview(contentLabel)
|
||||
contentStackView.setCustomSpacing(contentLabelTopSpace, after: titleLabel)
|
||||
scrollView.addSubview(contentStackView)
|
||||
|
||||
// Add crossButon, scrollView, buttonsData.
|
||||
addSubview(closeCrossButton)
|
||||
addSubview(scrollView)
|
||||
addSubview(buttonGroupData)
|
||||
self.bringSubviewToFront(closeCrossButton)
|
||||
|
||||
let crossTopSpace = UIDevice.isIPad && !modalModel.fullScreenDialog ? 0 : VDSLayout.space12X
|
||||
let scrollTopSpace = UIDevice.isIPad && !modalModel.fullScreenDialog ? containerViewInset : (crossTopSpace + closeCrossButtonSize)
|
||||
let contentTrailingSpace = UIDevice.isIPad ? (containerViewInset/2) - 6 : containerViewInset
|
||||
|
||||
// Activate constraints
|
||||
NSLayoutConstraint.activate([
|
||||
// Constraints for the closeCrossButton
|
||||
closeCrossButton.topAnchor.constraint(equalTo: topAnchor, constant: crossTopSpace),
|
||||
closeCrossButton.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor),
|
||||
closeCrossButton.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
closeCrossButton.heightAnchor.constraint(equalToConstant: closeCrossButtonSize),
|
||||
|
||||
// Constraints for the bottom button view
|
||||
buttonGroupData.leadingAnchor.constraint(equalTo: leadingAnchor, constant:containerViewInset),
|
||||
buttonGroupData.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -containerViewInset),
|
||||
buttonGroupData.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -containerViewInset),
|
||||
|
||||
// Constraints for the scrollView
|
||||
scrollView.topAnchor.constraint(equalTo: topAnchor, constant: scrollTopSpace),
|
||||
scrollView.leadingAnchor.constraint(equalTo: leadingAnchor, constant:containerViewInset),
|
||||
scrollView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -contentTrailingSpace),
|
||||
scrollView.bottomAnchor.constraint(equalTo: buttonGroupData.topAnchor, constant: -contentLabelBottomSpace),
|
||||
|
||||
// Constraints for the contentStackView
|
||||
contentStackView.topAnchor.constraint(equalTo: scrollView.topAnchor),
|
||||
contentStackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
|
||||
contentStackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: -contentTrailingSpace),
|
||||
contentStackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -contentTrailingSpace),
|
||||
contentStackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor)
|
||||
])
|
||||
|
||||
contentStackViewBottomConstraint = contentStackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor)
|
||||
contentStackViewBottomConstraint?.activate()
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
open override func updateView() {
|
||||
super.updateView()
|
||||
|
||||
// Update surface and background
|
||||
backgroundColor = backgroundColorConfiguration.getColor(self)
|
||||
scrollView.indicatorStyle = surface == .light ? .black : .white
|
||||
closeCrossButton.surface = surface
|
||||
buttonGroupData.surface = surface
|
||||
titleLabel.surface = surface
|
||||
contentLabel.surface = surface
|
||||
|
||||
// Re-arrange contentStack
|
||||
contentStackView.removeArrangedSubviews()
|
||||
|
||||
titleLabel.text = modalModel.title
|
||||
contentLabel.text = modalModel.content
|
||||
titleLabel.textColor = textColorConfiguration.getColor(self)
|
||||
contentLabel.textColor = textColorConfiguration.getColor(self)
|
||||
titleLabel.sizeToFit()
|
||||
contentLabel.sizeToFit()
|
||||
|
||||
// Add buttons data if provided
|
||||
if let buttons = modalModel.buttonData, buttons.count > 0 {
|
||||
buttonGroupData.buttons = buttons
|
||||
let percent = UIDevice.isIPad ? 50.0 : 100.0
|
||||
buttonGroupData.rowQuantityTablet = 2
|
||||
buttonGroupData.rowQuantityPhone = 1
|
||||
buttonGroupData.childWidth = .percentage(percent)
|
||||
}
|
||||
|
||||
// Update title, content and contentview
|
||||
var addedTitle = false
|
||||
|
||||
if let titleText = modalModel.title, !titleText.isEmpty {
|
||||
contentStackView.addArrangedSubview(titleLabel)
|
||||
addedTitle = true
|
||||
}
|
||||
|
||||
var addedContent = false
|
||||
if let contentText = modalModel.content, !contentText.isEmpty {
|
||||
contentStackView.addArrangedSubview(contentLabel)
|
||||
addedContent = true
|
||||
} else if let contentView = modalModel.contentView {
|
||||
contentView.translatesAutoresizingMaskIntoConstraints = false
|
||||
if var surfaceable = contentView as? Surfaceable {
|
||||
surfaceable.surface = surface
|
||||
}
|
||||
contentStackView.addArrangedSubview(contentView)
|
||||
addedContent = true
|
||||
}
|
||||
|
||||
if addedTitle && addedContent {
|
||||
contentStackView.spacing = contentLabelTopSpace
|
||||
}
|
||||
|
||||
closeCrossButton.isHidden = modalModel.hideCloseButton
|
||||
|
||||
contentStackView.setNeedsLayout()
|
||||
contentStackView.layoutIfNeeded()
|
||||
scrollView.setNeedsLayout()
|
||||
scrollView.layoutIfNeeded()
|
||||
}
|
||||
|
||||
/// Used to update any Accessibility properties.
|
||||
open override func updateAccessibility() {
|
||||
super.updateAccessibility()
|
||||
primaryAccessibilityElement.accessibilityHint = "Double tap on the cross button to close."
|
||||
primaryAccessibilityElement.accessibilityFrameInContainerSpace = .init(origin: .zero, size: frame.size)
|
||||
}
|
||||
|
||||
open override var accessibilityElements: [Any]? {
|
||||
get {
|
||||
var elements: [Any] = [primaryAccessibilityElement]
|
||||
contentStackView.arrangedSubviews.forEach{ elements.append($0) }
|
||||
elements.append(buttonGroupData)
|
||||
return elements
|
||||
}
|
||||
set {}
|
||||
}
|
||||
|
||||
}
|
||||
123
VDS/Components/Modal/ModalDialogViewController.swift
Normal file
123
VDS/Components/Modal/ModalDialogViewController.swift
Normal file
@ -0,0 +1,123 @@
|
||||
//
|
||||
// ModalDialogViewController.swift
|
||||
// VDS
|
||||
//
|
||||
// Created by Kanamarlapudi, Vasavi on 09/09/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Combine
|
||||
import VDSCoreTokens
|
||||
|
||||
@objc(VDSModalDialogViewController)
|
||||
open class ModalDialogViewController: 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let modalDialog = ModalDialog()
|
||||
|
||||
//--------------------------------------------------
|
||||
// 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 modalModel = Modal.ModalModel() { didSet { updateView() }}
|
||||
open var presenter: UIView? { didSet { updateView() }}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Configuration
|
||||
//--------------------------------------------------
|
||||
private let backgroundColorConfiguration = SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteWhite)
|
||||
|
||||
//--------------------------------------------------
|
||||
// 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: modalDialog)
|
||||
}
|
||||
|
||||
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() {
|
||||
view.accessibilityElements = [modalDialog]
|
||||
|
||||
//left-right swipe
|
||||
view.publisher(for: UISwipeGestureRecognizer().with{ $0.direction = .right })
|
||||
.sink { [weak self] swipe in
|
||||
guard let self, !UIAccessibility.isVoiceOverRunning else { return }
|
||||
self.dismiss()
|
||||
}.store(in: &subscribers)
|
||||
|
||||
//tapping in background
|
||||
view.publisher(for: UITapGestureRecognizer().with{ $0.numberOfTapsRequired = 1 })
|
||||
.sink { [weak self] swipe in
|
||||
guard let self, !UIAccessibility.isVoiceOverRunning else { return }
|
||||
self.dismiss()
|
||||
}.store(in: &subscribers)
|
||||
|
||||
//clicking button
|
||||
onClickSubscriber = modalDialog.closeCrossButton.publisher(for: .touchUpInside)
|
||||
.sink {[weak self] button in
|
||||
guard let self else { return }
|
||||
self.dismiss()
|
||||
}
|
||||
|
||||
view.addSubview(modalDialog)
|
||||
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
open func updateView() {
|
||||
modalDialog.surface = surface
|
||||
modalDialog.modalModel = modalModel
|
||||
|
||||
|
||||
// Activate constraints
|
||||
modalDialog.removeConstraints()
|
||||
let isFullScreen = UIDevice.isIPad && !modalModel.fullScreenDialog ? false : true
|
||||
|
||||
if isFullScreen {
|
||||
view.backgroundColor = modalDialog.backgroundColor
|
||||
modalDialog
|
||||
.pinLeading()
|
||||
.pinTrailing()
|
||||
modalDialog.pinTop(anchor: UIDevice.isIPad ? view.safeAreaLayoutGuide.topAnchor : view.topAnchor)
|
||||
modalDialog.pinBottom(UIDevice.isIPad ? view.bottomAnchor : view.safeAreaLayoutGuide.bottomAnchor)
|
||||
|
||||
} else {
|
||||
view.backgroundColor = backgroundColorConfiguration.getColor(self).withAlphaComponent(0.8)
|
||||
NSLayoutConstraint.activate([
|
||||
// Constraints for the floating modal view for Tablet.
|
||||
modalDialog.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
||||
modalDialog.centerYAnchor.constraint(equalTo: view.centerYAnchor),
|
||||
modalDialog.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.7),
|
||||
modalDialog.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.7)
|
||||
])
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
28
VDS/Components/Modal/ModalLaunchable.swift
Normal file
28
VDS/Components/Modal/ModalLaunchable.swift
Normal file
@ -0,0 +1,28 @@
|
||||
//
|
||||
// ModalLaunchable.swift
|
||||
// VDS
|
||||
//
|
||||
// Created by Kanamarlapudi, Vasavi on 09/09/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
public protocol ModalLaunchable {
|
||||
func presentModal(surface: Surface, modalModel: Modal.ModalModel, presenter: UIView?)
|
||||
}
|
||||
|
||||
extension ModalLaunchable {
|
||||
public func presentModal(surface: Surface, modalModel: Modal.ModalModel, presenter: UIView? = nil) {
|
||||
if let presenting = UIApplication.topViewController() {
|
||||
let modalViewController = ModalDialogViewController(nibName: nil, bundle: nil).with {
|
||||
$0.surface = surface
|
||||
$0.modalModel = modalModel
|
||||
$0.presenter = presenter
|
||||
$0.modalPresentationStyle = UIDevice.isIPad && !modalModel.fullScreenDialog ? .overCurrentContext : .fullScreen
|
||||
$0.modalTransitionStyle = .crossDissolve
|
||||
}
|
||||
presenting.present(modalViewController, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
45
VDS/Components/Modal/ModalModel.swift
Normal file
45
VDS/Components/Modal/ModalModel.swift
Normal file
@ -0,0 +1,45 @@
|
||||
//
|
||||
// ModalModel.swift
|
||||
// VDS
|
||||
//
|
||||
// Created by Kanamarlapudi, Vasavi on 09/09/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
extension Modal {
|
||||
|
||||
/// Model used to represent the modal.
|
||||
public struct ModalModel: Equatable {
|
||||
/// Current Surface and this is used to pass down to child objects that implement Surfacable
|
||||
public var closeButtonText: String
|
||||
public var title: String?
|
||||
public var content: String?
|
||||
public var contentView: UIView?
|
||||
public var accessibleText: String?
|
||||
public var contentViewAlignment: UIStackView.Alignment?
|
||||
public var buttonData: [ButtonBase]?
|
||||
public var fullScreenDialog: Bool
|
||||
public var hideCloseButton: Bool
|
||||
public init(closeButtonText: String = "Close",
|
||||
title: String? = nil,
|
||||
content: String? = nil,
|
||||
contentView: UIView? = nil,
|
||||
buttonData: [ButtonBase]? = nil,
|
||||
fullScreenDialog: Bool = false,
|
||||
hideCloseButton: Bool = false,
|
||||
accessibleText: String? = "Modal",
|
||||
contentViewAlignment: UIStackView.Alignment = .leading) {
|
||||
self.closeButtonText = closeButtonText
|
||||
self.title = title
|
||||
self.content = content
|
||||
self.contentView = contentView
|
||||
self.accessibleText = accessibleText
|
||||
self.contentViewAlignment = contentViewAlignment
|
||||
self.buttonData = buttonData
|
||||
self.fullScreenDialog = fullScreenDialog
|
||||
self.hideCloseButton = hideCloseButton
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -15,7 +15,7 @@ import Combine
|
||||
/// with different color and content. They may be screen-specific, flow-specific or
|
||||
/// experience-wide.
|
||||
@objc(VDSNotification)
|
||||
open class Notification: View {
|
||||
open class Notification: View, ParentViewProtocol {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
@ -100,6 +100,8 @@ open class Notification: View {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
open var children: [any ViewProtocol] { [typeIcon, closeButton, titleLabel, subTitleLabel, primaryButton, secondaryButton] }
|
||||
|
||||
/// Icon used for denoting type.
|
||||
open var typeIcon = Icon().with {
|
||||
$0.name = .infoBold
|
||||
@ -267,26 +269,15 @@ open class Notification: View {
|
||||
closeButton.accessibilityTraits = [.button]
|
||||
closeButton.accessibilityLabel = "Close Notification"
|
||||
|
||||
typeIcon.bridge_accessibilityLabelBlock = { [weak self] in
|
||||
guard let self else { return "" }
|
||||
return style.accessibleText
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
|
||||
shouldUpdateView = false
|
||||
|
||||
titleLabel.reset()
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
titleLabel.text = ""
|
||||
titleLabel.textStyle = UIDevice.isIPad ? .boldBodyLarge : .boldBodySmall
|
||||
|
||||
subTitleLabel.reset()
|
||||
subTitleLabel.textStyle = UIDevice.isIPad ? .bodyLarge : .bodySmall
|
||||
|
||||
buttonGroup.reset()
|
||||
buttonGroup.alignment = .left
|
||||
|
||||
primaryButtonModel = nil
|
||||
@ -302,8 +293,18 @@ open class Notification: View {
|
||||
|
||||
hideCloseButton = false
|
||||
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
typeIcon.bridge_accessibilityLabelBlock = { [weak self] in
|
||||
guard let self else { return "" }
|
||||
return style.accessibleText
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
titleLabel.reset()
|
||||
subTitleLabel.reset()
|
||||
buttonGroup.reset()
|
||||
super.reset()
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
|
||||
@ -8,12 +8,16 @@
|
||||
import Foundation
|
||||
|
||||
extension Notification {
|
||||
public struct ButtonModel {
|
||||
public struct ButtonModel: Equatable {
|
||||
public var text: String
|
||||
public var onClick: (Button) -> ()
|
||||
public init(text: String, onClick: @escaping (Button) -> Void) {
|
||||
self.text = text
|
||||
self.onClick = onClick
|
||||
}
|
||||
|
||||
public static func == (lhs: Notification.ButtonModel, rhs: Notification.ButtonModel) -> Bool {
|
||||
lhs.text == rhs.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,6 +17,8 @@ open class Pagination: View {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Properties
|
||||
//--------------------------------------------------
|
||||
private let pageChangedSubject = PassthroughSubject<Pagination, Never>()
|
||||
|
||||
///Maximum component width
|
||||
private let maxWidth: CGFloat = 288.0
|
||||
///Collectionview width anchor
|
||||
@ -51,12 +53,14 @@ open class Pagination: View {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
public var pageChangedPublisher: AnyPublisher<Pagination, Never> { pageChangedSubject.eraseToAnyPublisher() }
|
||||
|
||||
///Previous button to select previous page
|
||||
public let previousButton: PaginationButton = .init(type: .previous)
|
||||
///Next button to select next page
|
||||
public let nextButton: PaginationButton = .init(type: .next)
|
||||
/// A callback when the page changes. Passes parameters (selectedPage).
|
||||
public var onPageDidSelect: ((Int) -> Void)?
|
||||
open var onPageDidSelect: ((Int) -> Void)?
|
||||
/// Total number of pages, allows limit ranging from 0 to 9999.
|
||||
@Clamping(range: 0...9999)
|
||||
public var total: Int {
|
||||
@ -69,7 +73,7 @@ open class Pagination: View {
|
||||
}
|
||||
}
|
||||
///Selected active page number and clips to total pages if selected index is greater than the total pages.
|
||||
public var selectedPage: Int {
|
||||
open var selectedPage: Int {
|
||||
set {
|
||||
if newValue >= total {
|
||||
_selectedPageIndex = total - 1
|
||||
@ -94,8 +98,8 @@ open class Pagination: View {
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
/// Executed on initialization for this View.
|
||||
open override func initialSetup() {
|
||||
super.initialSetup()
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
|
||||
collectionContainerView.addSubview(collectionView)
|
||||
containerView.addSubview(previousButton)
|
||||
@ -144,6 +148,15 @@ open class Pagination: View {
|
||||
.sink { [weak self] value in
|
||||
self?.collectionViewWidthAnchor?.constant = value //As cell width is dynamic i.e cell may contain 2 or 3 or 4 charcters. Make sure that all the visible cells are displayed.
|
||||
}.store(in: &subscribers)
|
||||
|
||||
pageChangedPublisher.sink { [weak self] control in
|
||||
guard let self else { return }
|
||||
onPageDidSelect?(control.selectedPage)
|
||||
}.store(in: &subscribers)
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
collectionContainerView.onAccessibilityIncrement = { [weak self] in
|
||||
guard let self else { return }
|
||||
self.selectedPage = max(0, self.selectedPage + 1)
|
||||
@ -190,6 +203,7 @@ open class Pagination: View {
|
||||
let isNextAction = sender == nextButton
|
||||
_selectedPageIndex = if isNextAction { _selectedPageIndex + 1 } else { _selectedPageIndex - 1 }
|
||||
updateSelection()
|
||||
pageChangedSubject.send(self)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
|
||||
guard let self else { return }
|
||||
UIAccessibility.post(notification: .announcement, argument: paginationDescription)
|
||||
@ -244,7 +258,7 @@ extension Pagination: UICollectionViewDelegate, UICollectionViewDataSource, UICo
|
||||
guard _selectedPageIndex != indexPath.row else { return }
|
||||
_selectedPageIndex = indexPath.row
|
||||
updateSelection()
|
||||
onPageDidSelect?(selectedPage)
|
||||
pageChangedSubject.send(self)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -59,8 +59,8 @@ open class PaginationButton: ButtonBase {
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
/// Executed on initialization for this View.
|
||||
open override func initialSetup() {
|
||||
super.initialSetup()
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
if #available(iOS 15.0, *) {
|
||||
configuration = buttonConfiguration
|
||||
} else {
|
||||
|
||||
352
VDS/Components/PriceLockup/PriceLockup.swift
Normal file
352
VDS/Components/PriceLockup/PriceLockup.swift
Normal file
@ -0,0 +1,352 @@
|
||||
//
|
||||
// PriceLockup.swift
|
||||
// VDS
|
||||
//
|
||||
// Created by Kanamarlapudi, Vasavi on 06/08/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import VDSCoreTokens
|
||||
|
||||
@objc(VDSPriceLockup)
|
||||
open class PriceLockup: View, ParentViewProtocol {
|
||||
|
||||
//--------------------------------------------------
|
||||
// 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 term of PriceLockup.
|
||||
public enum Term: String, DefaultValuing, CaseIterable {
|
||||
case month, year, biennial, none
|
||||
|
||||
/// The default term is 'month'.
|
||||
public static var defaultValue : Self { .month }
|
||||
|
||||
/// Text for this term of PriceLockup.
|
||||
public var type: String {
|
||||
switch self {
|
||||
case .month:
|
||||
return "mo"
|
||||
case .year:
|
||||
return "yr"
|
||||
case .biennial:
|
||||
return "biennial"
|
||||
case .none:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum that represents the size availble for PriceLockup.
|
||||
public enum Size: String, DefaultValuing, CaseIterable {
|
||||
case xxxsmall = "3XSmall"
|
||||
case xxsmall = "2XSmall"
|
||||
case xsmall = "XSmall"
|
||||
case small
|
||||
case medium
|
||||
case large
|
||||
case xlarge = "XLarge"
|
||||
case xxlarge = "2XLarge"
|
||||
|
||||
public static var defaultValue: Self { .medium }
|
||||
}
|
||||
|
||||
/// Enum used to describe the kind of PriceLockup.
|
||||
public enum Kind: String, DefaultValuing, CaseIterable {
|
||||
case primary, secondary, savings
|
||||
|
||||
/// The default kind is 'primary'.
|
||||
public static var defaultValue : Self { .primary }
|
||||
|
||||
/// Color configuation relative to kind.
|
||||
public var colorConfiguration: SurfaceColorConfiguration {
|
||||
switch self {
|
||||
case .primary:
|
||||
return SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark)
|
||||
case .secondary:
|
||||
return SurfaceColorConfiguration(VDSColor.elementsSecondaryOnlight, VDSColor.elementsSecondaryOndark)
|
||||
case .savings:
|
||||
return SurfaceColorConfiguration(VDSColor.paletteGreen26, VDSColor.paletteGreen36)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
open var children: [any ViewProtocol] { [priceLockupLabel] }
|
||||
|
||||
/// If true, the component will render as bold.
|
||||
open var bold: Bool = false { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// Currency - If hideCurrency true, the component will render without currency.
|
||||
open var hideCurrency: Bool = false { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// Leading text for the component.
|
||||
open var leadingText: String? { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// Value rendered for the component.
|
||||
open var price: Float? { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// Color to the component. The default kind is primary.
|
||||
open var kind: Kind = .defaultValue { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// Size of the component. It varies by size and viewport(mobile/Tablet).
|
||||
/// The default size is medium with viewport mobile.
|
||||
open var size: Size = .defaultValue { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// If true, the component with a strikethrough. It applies only when uniformSize is true.
|
||||
/// Does not apply a strikethrough format to leading and trailing text.
|
||||
open var strikethrough: Bool = false { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// Term text for the component. The default term is 'month'.
|
||||
/// Superscript placement can vary when term and delimeter are "none".
|
||||
open var term: Term = .defaultValue { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// Trailing text for the component.
|
||||
open var trailingText: String? { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// Superscript text for the component.
|
||||
open var superscript: String? { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// If true, currency and value have the same font text style as delimeter, term label and superscript.
|
||||
/// This will render the pricing and term sections as a uniform size.
|
||||
open var uniformSize: Bool = false { didSet { setNeedsUpdate() } }
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Properties
|
||||
//--------------------------------------------------
|
||||
internal var priceLockupLabel = Label().with {
|
||||
$0.isAccessibilityElement = true
|
||||
$0.lineBreakMode = .byWordWrapping
|
||||
}
|
||||
|
||||
internal var delimiterIndex = 0
|
||||
internal var strikethroughLocation = 0
|
||||
internal var strikethroughLength = 0
|
||||
internal var strikethroughAccessibilityText: String = "price not offering anymore"
|
||||
|
||||
internal var textPosition:TextPosition = .preDelimiter
|
||||
enum TextPosition: String, CaseIterable {
|
||||
case preDelimiter, postDelimiter
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Configuration Properties
|
||||
//--------------------------------------------------
|
||||
internal var containerSize: CGSize { CGSize(width: 45, height: 44) }
|
||||
|
||||
// TextStyle for the size.
|
||||
private var textStyle: TextStyle.StandardStyle {
|
||||
switch (size, textPosition) {
|
||||
case (.xxxsmall, .preDelimiter), (.xxxsmall, .postDelimiter):
|
||||
return .micro
|
||||
|
||||
case (.xxsmall, .preDelimiter), (.xxsmall, .postDelimiter):
|
||||
return .bodySmall
|
||||
|
||||
case (.xsmall, .preDelimiter), (.xsmall, .postDelimiter):
|
||||
return .bodyMedium
|
||||
|
||||
case (.small, .preDelimiter), (.small, .postDelimiter):
|
||||
return .bodyLarge
|
||||
|
||||
case (.medium, .preDelimiter):
|
||||
return UIDevice.isIPad ? .titleSmall : .titleMedium
|
||||
|
||||
case (.medium, .postDelimiter):
|
||||
return .bodyLarge
|
||||
|
||||
case (.large, .preDelimiter):
|
||||
return UIDevice.isIPad ? .titleMedium : .titleLarge
|
||||
|
||||
case (.large, .postDelimiter):
|
||||
return UIDevice.isIPad ? .titleSmall : .titleMedium
|
||||
|
||||
case (.xlarge, .preDelimiter):
|
||||
return UIDevice.isIPad ? .titleLarge : .titleXLarge
|
||||
|
||||
case (.xlarge, .postDelimiter):
|
||||
return UIDevice.isIPad ? .titleMedium : .titleLarge
|
||||
|
||||
case (.xxlarge, .preDelimiter):
|
||||
return UIDevice.isIPad ? .titleXLarge : .featureSmall
|
||||
|
||||
case (.xxlarge, .postDelimiter):
|
||||
return UIDevice.isIPad ? .titleLarge : .titleXLarge
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
|
||||
// Price lockup label
|
||||
addSubview(priceLockupLabel)
|
||||
priceLockupLabel.pinToSuperView()
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
open override func updateView() {
|
||||
super.updateView()
|
||||
|
||||
priceLockupLabel.text = formatText()
|
||||
priceLockupLabel.surface = surface
|
||||
|
||||
// Set the attributed text
|
||||
updateLabelAttributes()
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
|
||||
priceLockupLabel.bridge_accessibilityLabelBlock = { [weak self] in
|
||||
guard let self else { return "" }
|
||||
var accessibilityLabels = [String]()
|
||||
if let text = priceLockupLabel.text, !text.isEmpty {
|
||||
if strikethrough, strikethroughLength > 0 {
|
||||
let preText = text.substring(to: strikethroughLocation)
|
||||
let postText = text.substring(from: strikethroughLocation)
|
||||
accessibilityLabels.append(preText)
|
||||
accessibilityLabels.append(strikethroughAccessibilityText)
|
||||
accessibilityLabels.append(postText)
|
||||
} else {
|
||||
accessibilityLabels.append(text)
|
||||
}
|
||||
}
|
||||
return accessibilityLabels.joined(separator: " ")
|
||||
}
|
||||
|
||||
bold = false
|
||||
hideCurrency = false
|
||||
leadingText = nil
|
||||
price = nil
|
||||
kind = .defaultValue
|
||||
size = .defaultValue
|
||||
strikethrough = false
|
||||
term = .defaultValue
|
||||
trailingText = nil
|
||||
superscript = nil
|
||||
uniformSize = false
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
priceLockupLabel.reset()
|
||||
super.reset()
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Private Methods
|
||||
//--------------------------------------------------
|
||||
// Update PriceLockup text attributes
|
||||
func updateLabelAttributes() {
|
||||
var attributes: [any LabelAttributeModel] = []
|
||||
attributes.append(ColorLabelAttribute(location: 0,
|
||||
length: priceLockupLabel.text.count,
|
||||
color: kind.colorConfiguration.getColor(self)))
|
||||
textPosition = .postDelimiter
|
||||
if strikethrough {
|
||||
|
||||
// strike applies only when uniformSize true. Does not apply a strikethrough format to leading, trailing, and superscript text.
|
||||
attributes.append(TextStyleLabelAttribute(location: 0,
|
||||
length: priceLockupLabel.text.count,
|
||||
textStyle: bold ? textStyle.bold : textStyle.regular,
|
||||
textPosition: .left))
|
||||
attributes.append(StrikeThroughLabelAttribute(location:strikethroughLocation, length: strikethroughLength))
|
||||
|
||||
} else if uniformSize {
|
||||
|
||||
// currency and value have the same font text style as delimeter, term, trailing text and superscript.
|
||||
attributes.append(TextStyleLabelAttribute(location: 0,
|
||||
length: priceLockupLabel.text.count,
|
||||
textStyle: bold ? textStyle.bold : textStyle.regular,
|
||||
textPosition: .left))
|
||||
|
||||
} else {
|
||||
|
||||
// size updates relative to predelimiter, postdelimiter
|
||||
if delimiterIndex > 0 {
|
||||
textPosition = .preDelimiter
|
||||
attributes.append(TextStyleLabelAttribute(location: 0,
|
||||
length: delimiterIndex,
|
||||
textStyle: bold ? textStyle.bold : textStyle.regular,
|
||||
textPosition: .left))
|
||||
|
||||
textPosition = .postDelimiter
|
||||
attributes.append(TextStyleLabelAttribute(location: delimiterIndex,
|
||||
length: priceLockupLabel.text.count-delimiterIndex,
|
||||
textStyle: bold ? textStyle.bold : textStyle.regular,
|
||||
textPosition: .left))
|
||||
}
|
||||
}
|
||||
priceLockupLabel.attributes = attributes
|
||||
}
|
||||
|
||||
// Get text for PriceLockup.
|
||||
private func formatText() -> String {
|
||||
var text : String = ""
|
||||
let space = " "
|
||||
let delimiter = "/"
|
||||
delimiterIndex = 0
|
||||
strikethroughLength = 0
|
||||
let currency: String = hideCurrency ? "" : "$"
|
||||
|
||||
if let leadingText {
|
||||
text.append(leadingText)
|
||||
text.append(space)
|
||||
delimiterIndex = delimiterIndex + leadingText.count + space.count
|
||||
}
|
||||
|
||||
strikethroughLocation = delimiterIndex
|
||||
|
||||
if let price = price?.clean {
|
||||
text.append(currency)
|
||||
text.append(price)
|
||||
delimiterIndex = delimiterIndex + price.count + currency.count
|
||||
strikethroughLength = price.count + currency.count
|
||||
}
|
||||
|
||||
if term != .none {
|
||||
text.append(delimiter)
|
||||
text.append(term.type)
|
||||
strikethroughLength = strikethroughLength + delimiter.count + term.type.count
|
||||
}
|
||||
|
||||
if let trailingText {
|
||||
text.append(space)
|
||||
text.append(trailingText)
|
||||
}
|
||||
|
||||
if let superscript {
|
||||
text.append(superscript)
|
||||
}
|
||||
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
extension Float {
|
||||
// remove a decimal from a float if the decimal is equal to 0
|
||||
var clean: String {
|
||||
return self.truncatingRemainder(dividingBy: 1) == 0 ? String(format: "%.0f", self) : String(describing: self)
|
||||
}
|
||||
}
|
||||
28
VDS/Components/PriceLockup/PriceLockupChangeLog.txt
Normal file
28
VDS/Components/PriceLockup/PriceLockupChangeLog.txt
Normal file
@ -0,0 +1,28 @@
|
||||
MM/DD/YYYY
|
||||
----------------
|
||||
|
||||
11/16/2023
|
||||
----------------
|
||||
- Added leadingText and trailingText to anatomy
|
||||
- Added leadingText and trailingText props to configurations
|
||||
- Added term prop to configurations
|
||||
- Removed Suspended orange color and corresponding color tokens
|
||||
|
||||
11/27/2023
|
||||
----------------
|
||||
- Removed “Delimiter” from Anatomy as “Term” includes both delimiter and term
|
||||
- Added “Figma only” badge to leadingText and trailingText in Configurations
|
||||
- Added superscript to “none” under term in Configurations
|
||||
- Added Overflow section to Layout and spacing
|
||||
- Updated Spacing to allow for leading and trailing text
|
||||
|
||||
12/18/23
|
||||
----------------
|
||||
- Updated all pages with spec template updates from Doc Utility Expansion Pack
|
||||
- Added Content props section to Config page
|
||||
|
||||
1/15/24
|
||||
----------------
|
||||
- Clarified strikethrough does not apply to leading or trailing text
|
||||
- Clarified and added to text overflow examples
|
||||
- Correct Success to Savings in the configuration seciton
|
||||
@ -48,7 +48,7 @@ open class RadioBoxGroup: SelectorGroupBase<RadioBoxItem>, SelectorGroupSingleSe
|
||||
$0.subTextAttributes = model.subTextAttributes
|
||||
$0.subTextRight = model.subTextRight
|
||||
$0.subTextRightAttributes = model.subTextRightAttributes
|
||||
$0.isEnabled = !model.disabled
|
||||
$0.isEnabled = model.enabled
|
||||
$0.inputId = model.inputId
|
||||
$0.hiddenValue = model.value
|
||||
$0.isSelected = model.selected
|
||||
@ -66,17 +66,26 @@ open class RadioBoxGroup: SelectorGroupBase<RadioBoxItem>, SelectorGroupSingleSe
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
private func ensureDevice() {
|
||||
if UIDevice.isIPad {
|
||||
mainStackView.axis = .horizontal
|
||||
mainStackView.distribution = .fillEqually
|
||||
} else {
|
||||
if UIDevice.current.orientation.isPortrait || UIDevice.current.orientation == .unknown {
|
||||
mainStackView.axis = .vertical
|
||||
mainStackView.distribution = .fill
|
||||
var axis: NSLayoutConstraint.Axis = .vertical
|
||||
var distribution: UIStackView.Distribution = .fill
|
||||
|
||||
defer {
|
||||
mainStackView.axis = axis
|
||||
mainStackView.distribution = distribution
|
||||
}
|
||||
|
||||
if UIDevice.isIPad {
|
||||
axis = .horizontal
|
||||
distribution = .fillEqually
|
||||
} else {
|
||||
mainStackView.axis = .horizontal
|
||||
mainStackView.distribution = .fillEqually
|
||||
guard let supportedOrientations = UIApplication.shared.windows.first?.rootViewController?.supportedInterfaceOrientations else {
|
||||
return
|
||||
}
|
||||
|
||||
let orientation = UIDevice.current.orientation
|
||||
if supportedOrientations.contains(.landscape) && (orientation == .landscapeLeft || orientation == .landscapeRight) {
|
||||
axis = .horizontal
|
||||
distribution = .fillEqually
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -103,9 +112,9 @@ open class RadioBoxGroup: SelectorGroupBase<RadioBoxItem>, SelectorGroupSingleSe
|
||||
}
|
||||
|
||||
extension RadioBoxGroup {
|
||||
public struct RadioBoxItemModel: Surfaceable, Initable, FormFieldable {
|
||||
/// Whether this object is disabled or not
|
||||
public var disabled: Bool
|
||||
public struct RadioBoxItemModel: Surfaceable, Initable, FormFieldable, Equatable {
|
||||
/// Whether this object is enabled or not
|
||||
public var enabled: Bool
|
||||
/// Current Surface and this is used to pass down to child objects that implement Surfacable
|
||||
public var surface: Surface
|
||||
public var inputId: String?
|
||||
@ -124,12 +133,12 @@ extension RadioBoxGroup {
|
||||
public var strikethrough: Bool = false
|
||||
public var strikethroughAccessibileText: String
|
||||
|
||||
public init(disabled: Bool, surface: Surface = .light, inputId: String? = nil, value: String? = nil,
|
||||
public init(enabled: Bool, surface: Surface = .light, inputId: String? = nil, value: String? = nil,
|
||||
text: String = "", textAttributes: [any LabelAttributeModel]? = nil,
|
||||
subText: String? = nil, subTextAttributes: [any LabelAttributeModel]? = nil,
|
||||
subTextRight: String? = nil, subTextRightAttributes: [any LabelAttributeModel]? = nil,
|
||||
selected: Bool = false, errorText: String? = nil, accessibileText: String? = nil, strikethrough: Bool = false, strikethroughAccessibileText: String = "not available") {
|
||||
self.disabled = disabled
|
||||
self.enabled = enabled
|
||||
self.surface = surface
|
||||
self.inputId = inputId
|
||||
self.value = value
|
||||
@ -146,7 +155,24 @@ extension RadioBoxGroup {
|
||||
}
|
||||
|
||||
public init() {
|
||||
self.init(disabled: false)
|
||||
self.init(enabled: true)
|
||||
}
|
||||
|
||||
public static func == (lhs: RadioBoxGroup.RadioBoxItemModel, rhs: RadioBoxGroup.RadioBoxItemModel) -> Bool {
|
||||
lhs.enabled == rhs.enabled
|
||||
&& lhs.surface == rhs.surface
|
||||
&& lhs.inputId == rhs.inputId
|
||||
&& lhs.value == rhs.value
|
||||
&& lhs.accessibileText == rhs.accessibileText
|
||||
&& lhs.text == rhs.text
|
||||
&& lhs.textAttributes == rhs.textAttributes
|
||||
&& lhs.subText == rhs.subText
|
||||
&& lhs.subTextAttributes == rhs.subTextAttributes
|
||||
&& lhs.subTextRight == rhs.subTextRight
|
||||
&& lhs.subTextRightAttributes == rhs.subTextRightAttributes
|
||||
&& lhs.selected == rhs.selected
|
||||
&& lhs.strikethrough == rhs.strikethrough
|
||||
&& lhs.strikethroughAccessibileText == rhs.strikethroughAccessibileText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ import VDSCoreTokens
|
||||
/// Radio boxes are single-select components through which a customer indicates a choice
|
||||
/// that are used within a ``RadioBoxGroup``.
|
||||
@objc(VDSRadioBoxItem)
|
||||
open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable {
|
||||
open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable, ParentViewProtocol {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
@ -52,6 +52,8 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
open var children: [any ViewProtocol] { [textLabel, subTextLabel, subTextRightLabel, selectorView] }
|
||||
|
||||
open var onChangeSubscriber: AnyCancellable?
|
||||
|
||||
/// Label used to render the text.
|
||||
@ -164,11 +166,38 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
/// Executed on initialization for this View.
|
||||
open override func initialSetup() {
|
||||
super.initialSetup()
|
||||
onClick = { control in
|
||||
control.toggle()
|
||||
/// 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
|
||||
selectorView.isAccessibilityElement = true
|
||||
selectorView.accessibilityTraits = .button
|
||||
addSubview(selectorView)
|
||||
selectorView.isUserInteractionEnabled = false
|
||||
|
||||
selectorView.addSubview(selectorStackView)
|
||||
|
||||
selectorStackView.addArrangedSubview(selectorLeftLabelStackView)
|
||||
selectorStackView.addArrangedSubview(subTextRightLabel)
|
||||
selectorLeftLabelStackView.addArrangedSubview(textLabel)
|
||||
selectorLeftLabelStackView.addArrangedSubview(subTextLabel)
|
||||
|
||||
selectorView
|
||||
.pinTop()
|
||||
.pinLeading()
|
||||
.pinTrailing(0, .defaultHigh)
|
||||
.pinBottom(0, .defaultHigh)
|
||||
|
||||
selectorStackView.pinToSuperView(.uniform(VDSLayout.space3X))
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
|
||||
onClick = { [weak self] _ in
|
||||
guard let self, isEnabled else { return }
|
||||
toggle()
|
||||
}
|
||||
|
||||
selectorView.bridge_accessibilityLabelBlock = { [weak self] in
|
||||
@ -204,42 +233,6 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable {
|
||||
return accessibilityLabels.joined(separator: ", ")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// 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
|
||||
selectorView.isAccessibilityElement = true
|
||||
selectorView.accessibilityTraits = .button
|
||||
addSubview(selectorView)
|
||||
selectorView.isUserInteractionEnabled = true
|
||||
|
||||
selectorView.addSubview(selectorStackView)
|
||||
|
||||
selectorStackView.addArrangedSubview(selectorLeftLabelStackView)
|
||||
selectorStackView.addArrangedSubview(subTextRightLabel)
|
||||
selectorLeftLabelStackView.addArrangedSubview(textLabel)
|
||||
selectorLeftLabelStackView.addArrangedSubview(subTextLabel)
|
||||
|
||||
selectorView
|
||||
.pinTop()
|
||||
.pinLeading()
|
||||
.pinTrailing(0, .defaultHigh)
|
||||
.pinBottom(0, .defaultHigh)
|
||||
|
||||
selectorStackView.pinToSuperView(.uniform(VDSLayout.space3X))
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
shouldUpdateView = false
|
||||
textLabel.reset()
|
||||
subTextLabel.reset()
|
||||
subTextRightLabel.reset()
|
||||
|
||||
textLabel.textStyle = .boldBodyLarge
|
||||
subTextLabel.textStyle = .bodyLarge
|
||||
subTextRightLabel.textStyle = .bodyLarge
|
||||
@ -259,9 +252,14 @@ open class RadioBoxItem: Control, Changeable, FormFieldable, Groupable {
|
||||
|
||||
isSelected = false
|
||||
onChange = nil
|
||||
}
|
||||
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
textLabel.reset()
|
||||
subTextLabel.reset()
|
||||
subTextRightLabel.reset()
|
||||
super.reset()
|
||||
}
|
||||
|
||||
/// This will change the state of the Selector and execute the actionBlock if provided.
|
||||
|
||||
@ -42,7 +42,7 @@ open class RadioButtonGroup: SelectorGroupBase<RadioButtonItem>, SelectorGroupSi
|
||||
if let selectorModels {
|
||||
items = selectorModels.enumerated().map { index, model in
|
||||
return RadioButtonItem().with {
|
||||
$0.isEnabled = !model.disabled
|
||||
$0.isEnabled = model.enabled
|
||||
$0.surface = model.surface
|
||||
$0.inputId = model.inputId
|
||||
$0.hiddenValue = model.value
|
||||
@ -76,12 +76,13 @@ open class RadioButtonGroup: SelectorGroupBase<RadioButtonItem>, SelectorGroupSi
|
||||
}
|
||||
}
|
||||
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
inputId = nil
|
||||
showError = false
|
||||
}
|
||||
|
||||
public override func didSelect(_ selectedControl: RadioButtonItem) {
|
||||
open override func didSelect(_ selectedControl: RadioButtonItem) {
|
||||
if let selectedItem {
|
||||
updateToggle(selectedItem)
|
||||
}
|
||||
@ -101,10 +102,10 @@ open class RadioButtonGroup: SelectorGroupBase<RadioButtonItem>, SelectorGroupSi
|
||||
}
|
||||
|
||||
extension RadioButtonGroup {
|
||||
public struct RadioButtonItemModel: Surfaceable, Initable, FormFieldable, Errorable {
|
||||
public struct RadioButtonItemModel: Surfaceable, Initable, FormFieldable, Errorable, Equatable {
|
||||
|
||||
/// Whether this object is disabled or not
|
||||
public var disabled: Bool
|
||||
/// Whether this object is enabled or not
|
||||
public var enabled: Bool
|
||||
/// Current Surface and this is used to pass down to child objects that implement Surfacable
|
||||
public var surface: Surface
|
||||
public var inputId: String?
|
||||
@ -121,8 +122,8 @@ extension RadioButtonGroup {
|
||||
public var showError: Bool
|
||||
public var errorText: String?
|
||||
|
||||
public init(disabled: Bool, surface: Surface = .light, inputId: String? = nil, value: AnyHashable? = nil, accessibileText: String? = nil, labelText: String? = nil, labelTextAttributes: [any LabelAttributeModel]? = nil, childText: String? = nil, childTextAttributes: [any LabelAttributeModel]? = nil, selected: Bool = false, showError: Bool = false, errorText: String? = nil) {
|
||||
self.disabled = disabled
|
||||
public init(enabled: Bool, surface: Surface = .light, inputId: String? = nil, value: AnyHashable? = nil, accessibileText: String? = nil, labelText: String? = nil, labelTextAttributes: [any LabelAttributeModel]? = nil, childText: String? = nil, childTextAttributes: [any LabelAttributeModel]? = nil, selected: Bool = false, showError: Bool = false, errorText: String? = nil) {
|
||||
self.enabled = enabled
|
||||
self.surface = surface
|
||||
self.inputId = inputId
|
||||
self.value = value
|
||||
@ -137,7 +138,22 @@ extension RadioButtonGroup {
|
||||
}
|
||||
|
||||
public init() {
|
||||
self.init(disabled: false)
|
||||
self.init(enabled: true)
|
||||
}
|
||||
|
||||
public static func == (lhs: RadioButtonGroup.RadioButtonItemModel, rhs: RadioButtonGroup.RadioButtonItemModel) -> Bool {
|
||||
lhs.enabled == rhs.enabled
|
||||
&& lhs.surface == rhs.surface
|
||||
&& lhs.inputId == rhs.inputId
|
||||
&& lhs.value == rhs.value
|
||||
&& lhs.accessibileText == rhs.accessibileText
|
||||
&& lhs.labelText == rhs.labelText
|
||||
&& lhs.labelTextAttributes == rhs.labelTextAttributes
|
||||
&& lhs.childText == rhs.childText
|
||||
&& lhs.childTextAttributes == rhs.childTextAttributes
|
||||
&& lhs.selected == rhs.selected
|
||||
&& lhs.showError == rhs.showError
|
||||
&& lhs.errorText == rhs.errorText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,7 +26,6 @@ open class Table: View {
|
||||
$0.allowsSelection = false
|
||||
$0.showsVerticalScrollIndicator = false
|
||||
$0.showsHorizontalScrollIndicator = false
|
||||
$0.isAccessibilityElement = true
|
||||
$0.backgroundColor = .clear
|
||||
}
|
||||
|
||||
@ -51,10 +50,8 @@ open class Table: View {
|
||||
|
||||
func horizontalValue() -> CGFloat {
|
||||
switch self {
|
||||
case .standard:
|
||||
return UIDevice.isIPad ? VDSLayout.space8X : VDSLayout.space6X
|
||||
case .compact:
|
||||
return UIDevice.isIPad ? VDSLayout.space8X : VDSLayout.space6X
|
||||
case .standard, .compact:
|
||||
return UIDevice.isIPad ? VDSLayout.space4X : VDSLayout.space3X
|
||||
}
|
||||
}
|
||||
|
||||
@ -93,8 +90,8 @@ open class Table: View {
|
||||
//--------------------------------------------------
|
||||
|
||||
///Called upon initializing the table view
|
||||
open override func initialSetup() {
|
||||
super.initialSetup()
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
addSubview(matrixView)
|
||||
matrixView.pinToSuperView()
|
||||
}
|
||||
@ -110,16 +107,14 @@ open class Table: View {
|
||||
matrixView.collectionViewLayout.invalidateLayout()
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
striped = false
|
||||
padding = .standard
|
||||
tableHeader = []
|
||||
tableRows = []
|
||||
fillContainer = true
|
||||
columnWidths = nil
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
func calculateColumnWidths() -> [CGFloat] {
|
||||
@ -147,7 +142,11 @@ extension Table: UICollectionViewDelegate, UICollectionViewDataSource, TableColl
|
||||
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TableCellItem.Identifier, for: indexPath) as? TableCellItem else { return UICollectionViewCell() }
|
||||
let currentItem = tableData[indexPath.section].columns[indexPath.row]
|
||||
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)
|
||||
setAccessibilityForCell(cell: cell, content: currentItem, path: indexPath)
|
||||
return cell
|
||||
}
|
||||
|
||||
@ -162,4 +161,38 @@ extension Table: UICollectionViewDelegate, UICollectionViewDataSource, TableColl
|
||||
func collectionView(_ collectionView: UICollectionView, widthForItemAt indexPath: IndexPath) -> CGFloat {
|
||||
return columnWidths?[indexPath.row] ?? 0.0
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Accessibility
|
||||
//--------------------------------------------------
|
||||
|
||||
/// To set the accessibility label for the each cell based on the criteria. Table name along with total no of column & row information should be passed in the first cell's accessibility label.
|
||||
private func setAccessibilityForCell(cell: TableCellItem, content: TableItemModel, path: IndexPath) {
|
||||
|
||||
var accLabel = content.component?.accessibilityLabel ?? "Empty"
|
||||
|
||||
///Set the type of header label
|
||||
if path.section == 0 {
|
||||
accLabel.append(", Column Header")
|
||||
} else if path.row == 0 {
|
||||
///As per design team, inspite of column 0 may not look like a header, it should be read as header.
|
||||
accLabel.append(", Row Header")
|
||||
}
|
||||
|
||||
///Set the Row/Column number for each cell
|
||||
if path.row == 0 {
|
||||
accLabel.append(", Row \(path.section + 1), Column \(path.row + 1)")
|
||||
} else {
|
||||
accLabel.append(", Column \(path.row + 1)")
|
||||
}
|
||||
|
||||
///Set the Row header accessibilityLabel at the end of the non-header cells accessibilityLabel
|
||||
if path.section != 0,
|
||||
path.row != 0,
|
||||
let columnHeaderAccLabel = tableHeader.first?.columns[path.row].component?.accessibilityLabel {
|
||||
accLabel.append(", \(columnHeaderAccLabel)")
|
||||
}
|
||||
|
||||
cell.accessibilityLabel = accLabel
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,9 +30,6 @@ final class TableCellItem: UICollectionViewCell {
|
||||
/// Color configuration for striped background color
|
||||
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
|
||||
//--------------------------------------------------
|
||||
@ -48,6 +45,7 @@ final class TableCellItem: UICollectionViewCell {
|
||||
|
||||
private func setupCell() {
|
||||
contentView.backgroundColor = .clear
|
||||
isAccessibilityElement = true
|
||||
|
||||
addSubview(containerView)
|
||||
containerView.pinToSuperView()
|
||||
@ -58,10 +56,10 @@ final class TableCellItem: UICollectionViewCell {
|
||||
//--------------------------------------------------
|
||||
|
||||
/// 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() })
|
||||
self.padding = padding
|
||||
|
||||
containerView.surface = surface
|
||||
containerView.backgroundColor = striped ? stripedColorConfiguration.getColor(surface) : backgroundColorConfiguration.getColor(surface)
|
||||
|
||||
@ -82,11 +80,11 @@ final class TableCellItem: UICollectionViewCell {
|
||||
}
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
component.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: VDSLayout.space1X),
|
||||
component.topAnchor.constraint(greaterThanOrEqualTo: containerView.topAnchor, constant: padding.verticalValue()),
|
||||
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.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: padding.left),
|
||||
containerView.trailingAnchor.constraint(greaterThanOrEqualTo: component.trailingAnchor, constant: padding.right)
|
||||
])
|
||||
|
||||
component.topAnchor.constraint(equalTo: containerView.topAnchor, constant: padding.top).isActive = !isHeader
|
||||
containerView.bottomAnchor.constraint(equalTo: component.bottomAnchor, constant: padding.bottom).isActive = isHeader
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.
|
||||
var layoutPadding: Table.Padding = .standard
|
||||
|
||||
///Striped status of Table, based on this status padding of leading attribute changes.
|
||||
var striped: Bool = false
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
@ -77,7 +80,7 @@ class MatrixFlowLayout : UICollectionViewFlowLayout {
|
||||
let selectedItem = delegate.collectionView(collectionView, dataForItemAt: indexPath)
|
||||
|
||||
///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
|
||||
|
||||
@ -108,9 +111,8 @@ class MatrixFlowLayout : UICollectionViewFlowLayout {
|
||||
}
|
||||
|
||||
/// Fetches estimated height by calling the cell's component estimated height and adding padding
|
||||
private func estimateHeightFor(item: TableItemModel, with width: CGFloat) -> CGFloat {
|
||||
|
||||
let itemWidth = width - layoutPadding.horizontalValue() - defaultLeadingPadding
|
||||
private func estimateHeightFor(item: TableItemModel, with width: CGFloat, index: IndexPath) -> CGFloat {
|
||||
let itemWidth = width - layoutPadding.horizontalValue() - (index.row == 0 ? defaultLeadingPadding:layoutPadding.horizontalValue())
|
||||
let maxSize = CGSize(width: itemWidth, height: CGFloat.greatestFiniteMagnitude)
|
||||
let estItemSize = item.component?.systemLayoutSizeFitting(maxSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel) ?? CGSize(width: itemWidth, height: item.defaultHeight)
|
||||
return estItemSize.height + (2 * layoutPadding.verticalValue())
|
||||
|
||||
@ -10,7 +10,7 @@ import UIKit
|
||||
import VDSCoreTokens
|
||||
|
||||
/// Model that represent the content of each cell of Table component
|
||||
public struct TableItemModel {
|
||||
public struct TableItemModel: Equatable {
|
||||
|
||||
public let defaultHeight: CGFloat = 50.0
|
||||
|
||||
|
||||
@ -7,15 +7,18 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct TableRowModel {
|
||||
public struct TableRowModel: Equatable {
|
||||
|
||||
public var columns: [TableItemModel]
|
||||
|
||||
public var isHeader: Bool = false
|
||||
|
||||
public var columnsCount: Int {
|
||||
return columns.count
|
||||
}
|
||||
|
||||
public init(columns: [TableItemModel]) {
|
||||
public init(columns: [TableItemModel], isHeader: Bool = false) {
|
||||
self.columns = columns
|
||||
self.isHeader = isHeader
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,7 +11,6 @@ import VDSCoreTokens
|
||||
import Combine
|
||||
|
||||
extension Tabs {
|
||||
|
||||
@objc(VDSTab)
|
||||
open class Tab: Control, Groupable {
|
||||
|
||||
@ -150,6 +149,10 @@ extension Tabs {
|
||||
labelLeadingConstraint = label.pinLeading(anchor: layoutGuide.leadingAnchor)
|
||||
labelBottomConstraint = label.pinBottom(anchor: layoutGuide.bottomAnchor, priority: .defaultHigh)
|
||||
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
bridge_accessibilityLabelBlock = { [weak self] in
|
||||
guard let self else { return "" }
|
||||
return text
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
import Foundation
|
||||
|
||||
extension Tabs {
|
||||
public struct TabModel {
|
||||
public struct TabModel: Equatable {
|
||||
|
||||
///Text that goes in the Tab
|
||||
public var text: String
|
||||
@ -24,5 +24,10 @@ extension Tabs {
|
||||
self.onClick = onClick
|
||||
self.width = width
|
||||
}
|
||||
|
||||
public static func == (lhs: Tabs.TabModel, rhs: Tabs.TabModel) -> Bool {
|
||||
lhs.text == rhs.text
|
||||
&& lhs.width == rhs.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@ 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 doesn’t need to be compared.
|
||||
@objc(VDSTabs)
|
||||
open class Tabs: View {
|
||||
open class Tabs: View, ParentViewProtocol {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
@ -83,6 +83,8 @@ open class Tabs: View {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
open var children: [any ViewProtocol] { tabViews }
|
||||
|
||||
/// A callback when the selectedIndex changes. Passes parameters (tabIndex).
|
||||
open var onTabDidSelect: ((Int) -> Void)?
|
||||
|
||||
@ -221,9 +223,10 @@ open class Tabs: View {
|
||||
updateContentView()
|
||||
}
|
||||
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
shouldUpdateView = false
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
onTabDidSelect = nil
|
||||
onTabShouldSelect = nil
|
||||
orientation = .horizontal
|
||||
borderLine = true
|
||||
fillContainer = false
|
||||
@ -234,9 +237,7 @@ open class Tabs: View {
|
||||
selectedIndex = 0
|
||||
size = .medium
|
||||
sticky = false
|
||||
tabViews.forEach{ $0.reset() }
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
tabModels = []
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
|
||||
@ -11,9 +11,7 @@ import VDSCoreTokens
|
||||
import Combine
|
||||
|
||||
/// Base Class used to build out a Input controls.
|
||||
@objc(VDSEntryField)
|
||||
open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
|
||||
open class EntryFieldBase<ValueType>: Control, Changeable, FormFieldInternalValidatable, ParentViewProtocol {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
@ -92,12 +90,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.
|
||||
internal var bottomContainerView: UIView!
|
||||
|
||||
@ -113,14 +105,12 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
// MARK: - Constraints
|
||||
//--------------------------------------------------
|
||||
internal var widthConstraint: NSLayoutConstraint?
|
||||
internal var trailingEqualsConstraint: NSLayoutConstraint?
|
||||
internal var trailingLessThanEqualsConstraint: NSLayoutConstraint?
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Configuration Properties
|
||||
//--------------------------------------------------
|
||||
// Sizes are from InVision design specs.
|
||||
internal var maxWidth: CGFloat { frame.size.width }
|
||||
internal var maxWidth: CGFloat { constrainedWidth }
|
||||
internal var minWidth: CGFloat { containerSize.width }
|
||||
internal var containerSize: CGSize { CGSize(width: minWidth, height: 44) }
|
||||
|
||||
@ -143,8 +133,8 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
|
||||
internal var borderColorConfiguration = ControlColorConfiguration().with {
|
||||
$0.setSurfaceColors(VDSFormControlsColor.borderOnlight, VDSFormControlsColor.borderOndark, forState: .normal)
|
||||
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOnlight, forState: .focused)
|
||||
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOnlight, forState: [.focused, .error])
|
||||
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .focused)
|
||||
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: [.focused, .error])
|
||||
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled)
|
||||
$0.setSurfaceColors(VDSColor.feedbackErrorOnlight, VDSColor.feedbackErrorOndark, forState: .error)
|
||||
$0.setSurfaceColors(VDSFormControlsColor.borderReadonlyOnlight, VDSFormControlsColor.borderReadonlyOndark, forState: .readonly)
|
||||
@ -163,6 +153,14 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
open var children: [any ViewProtocol] { [titleLabel, helperLabel, errorLabel, statusIcon] }
|
||||
|
||||
/// 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 titleLabel = Label().with {
|
||||
@ -186,6 +184,8 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
$0.isAccessibilityElement = true
|
||||
}
|
||||
|
||||
open var useRequiredRule: Bool = true { didSet { setNeedsUpdate() } }
|
||||
|
||||
open var labelText: String? { didSet { setNeedsUpdate() } }
|
||||
|
||||
open var helperText: String? { didSet { setNeedsUpdate() } }
|
||||
@ -226,11 +226,11 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
open var inputId: String? { didSet { setNeedsUpdate() } }
|
||||
|
||||
/// The text of this textField.
|
||||
open var value: String? {
|
||||
open var value: ValueType? {
|
||||
get { fatalError("must be read from subclass")}
|
||||
}
|
||||
|
||||
open var defaultValue: AnyHashable? { didSet { setNeedsUpdate() } }
|
||||
open var defaultValue: ValueType? { didSet { setNeedsUpdate() } }
|
||||
|
||||
open var isRequired: Bool = false { didSet { setNeedsUpdate() } }
|
||||
|
||||
@ -242,7 +242,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
}
|
||||
}
|
||||
|
||||
open var rules = [AnyRule<String>]()
|
||||
open var rules = [AnyRule<ValueType>]()
|
||||
|
||||
open var accessibilityHintText: String = "Double tap to open"
|
||||
|
||||
@ -256,15 +256,9 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
|
||||
let layoutGuide = UILayoutGuide()
|
||||
addLayoutGuide(layoutGuide)
|
||||
layoutGuide
|
||||
.pinTop()
|
||||
.pinLeading()
|
||||
.pinBottom()
|
||||
|
||||
trailingEqualsConstraint = layoutGuide.pinTrailing(anchor: trailingAnchor)
|
||||
layoutGuide.pinToSuperView()
|
||||
|
||||
// width constraints
|
||||
trailingLessThanEqualsConstraint = layoutGuide.pinTrailingLessThanOrEqualTo(anchor: trailingAnchor)?.deactivate()
|
||||
widthConstraint = layoutGuide.widthAnchor.constraint(equalToConstant: 0).deactivate()
|
||||
|
||||
// Add mainStackView to the view
|
||||
@ -313,6 +307,41 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
errorLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable()
|
||||
helperLabel.textColorConfiguration = secondaryColorConfiguration.eraseToAnyColorable()
|
||||
|
||||
}
|
||||
|
||||
/// Updates the UI
|
||||
open override func updateView() {
|
||||
super.updateView()
|
||||
updateRules()
|
||||
updateContainerView()
|
||||
updateContainerWidth()
|
||||
updateTitleLabel()
|
||||
updateErrorLabel()
|
||||
updateHelperLabel()
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
|
||||
titleLabel.textStyle = .bodySmall
|
||||
errorLabel.textStyle = .bodySmall
|
||||
helperLabel.textStyle = .bodySmall
|
||||
|
||||
labelText = nil
|
||||
helperText = nil
|
||||
showError = false
|
||||
errorText = nil
|
||||
tooltipModel = nil
|
||||
transparentBackground = false
|
||||
width = nil
|
||||
inputId = nil
|
||||
defaultValue = nil
|
||||
isRequired = false
|
||||
isReadOnly = false
|
||||
helperTextPlacement = .bottom
|
||||
rules = []
|
||||
onChange = nil
|
||||
|
||||
containerView.bridge_accessibilityLabelBlock = { [weak self] in
|
||||
guard let self else { return "" }
|
||||
var accessibilityLabels = [String]()
|
||||
@ -330,8 +359,6 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
accessibilityLabels.append("error, \(errorText)")
|
||||
}
|
||||
|
||||
accessibilityLabels.append("\(Self.self)")
|
||||
|
||||
return accessibilityLabels.joined(separator: ", ")
|
||||
}
|
||||
|
||||
@ -341,49 +368,23 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
}
|
||||
|
||||
containerView.bridge_accessibilityValueBlock = { [weak self] in
|
||||
guard let self else { return "" }
|
||||
return value
|
||||
guard let self, let value else { return "" }
|
||||
return "\(value)"
|
||||
}
|
||||
|
||||
statusIcon.bridge_accessibilityLabelBlock = { [weak self] in
|
||||
guard let self else { return "" }
|
||||
return showError || hasInternalError ? "error" : nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the UI
|
||||
open override func updateView() {
|
||||
super.updateView()
|
||||
updateContainerView()
|
||||
updateContainerWidth()
|
||||
updateTitleLabel()
|
||||
updateErrorLabel()
|
||||
updateHelperLabel()
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
titleLabel.reset()
|
||||
errorLabel.reset()
|
||||
helperLabel.reset()
|
||||
|
||||
titleLabel.textStyle = .bodySmall
|
||||
errorLabel.textStyle = .bodySmall
|
||||
helperLabel.textStyle = .bodySmall
|
||||
|
||||
labelText = nil
|
||||
helperText = nil
|
||||
showError = false
|
||||
errorText = nil
|
||||
tooltipModel = nil
|
||||
transparentBackground = false
|
||||
width = nil
|
||||
inputId = nil
|
||||
defaultValue = nil
|
||||
isRequired = false
|
||||
isReadOnly = false
|
||||
onChange = nil
|
||||
super.reset()
|
||||
}
|
||||
|
||||
open override var canBecomeFirstResponder: Bool {
|
||||
@ -416,7 +417,6 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
}
|
||||
|
||||
open func validate(){
|
||||
updateRules()
|
||||
validator = FormFieldValidator<EntryFieldBase>(field: self, rules: rules)
|
||||
validator?.validate()
|
||||
setNeedsUpdate()
|
||||
@ -453,27 +453,34 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
}
|
||||
|
||||
open func updateErrorLabel(){
|
||||
if showError, let errorText {
|
||||
errorLabel.text = errorText
|
||||
errorLabel.surface = surface
|
||||
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
|
||||
|
||||
/// always show the errorIcon if there is an error
|
||||
if showError || hasInternalError {
|
||||
statusIcon.name = .error
|
||||
statusIcon.surface = surface
|
||||
statusIcon.isHidden = !isEnabled || state.contains(.focused)
|
||||
} else {
|
||||
statusIcon.isHidden = true
|
||||
errorLabel.isHidden = true
|
||||
}
|
||||
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(){
|
||||
@ -515,8 +522,8 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
//--------------------------------------------------
|
||||
internal func updateRules() {
|
||||
rules.removeAll()
|
||||
if self.isRequired {
|
||||
let rule = RequiredRule()
|
||||
if isRequired && useRequiredRule && ValueType.self == String.self {
|
||||
let rule = RequiredRule<ValueType>()
|
||||
if let errorText, !errorText.isEmpty {
|
||||
rule.errorMessage = errorText
|
||||
} else if let labelText{
|
||||
@ -537,15 +544,10 @@ open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable {
|
||||
|
||||
internal func updateContainerWidth() {
|
||||
widthConstraint?.deactivate()
|
||||
trailingLessThanEqualsConstraint?.deactivate()
|
||||
trailingEqualsConstraint?.deactivate()
|
||||
|
||||
if let width, width >= minWidth, width <= maxWidth {
|
||||
widthConstraint?.constant = width
|
||||
widthConstraint?.activate()
|
||||
trailingLessThanEqualsConstraint?.activate()
|
||||
} else {
|
||||
trailingEqualsConstraint?.activate()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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] {
|
||||
var indices: [Int] = [4, 8, 12]
|
||||
switch self {
|
||||
@ -135,7 +143,7 @@ extension InputField {
|
||||
fileprivate func updateLeftImage(_ inputField: InputField) {
|
||||
let imageName = inputField.cardType.imageName(surface: inputField.surface)
|
||||
creditCardImageView.image = BundleManager.shared.image(for: imageName)
|
||||
creditCardImageView.accessibilityLabel = inputField.cardType.rawValue
|
||||
creditCardImageView.accessibilityLabel = inputField.cardType.accessibilityLabel
|
||||
}
|
||||
|
||||
override func updateView(_ inputField: InputField) {
|
||||
|
||||
@ -68,6 +68,12 @@ extension InputField {
|
||||
actionModel.onClick(inputField)
|
||||
}
|
||||
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)
|
||||
} else {
|
||||
inputField.actionTextLink.isHidden = true
|
||||
|
||||
@ -10,6 +10,35 @@ import UIKit
|
||||
|
||||
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 {
|
||||
static let shared = TelephoneHandler()
|
||||
|
||||
@ -25,14 +54,7 @@ extension InputField {
|
||||
}
|
||||
|
||||
override func appendRules(_ inputField: InputField) {
|
||||
if let text = inputField.textField.text, text.count > 0 {
|
||||
let rule = CharacterCountRule().copyWith {
|
||||
$0.maxLength = "XXX-XXX-XXXX".count
|
||||
$0.compareType = .equals
|
||||
$0.errorMessage = "Enter a valid telephone."
|
||||
}
|
||||
inputField.rules.append(.init(rule))
|
||||
}
|
||||
inputField.rules.append(.init(TelephoneNumberValidator(format: "XXX-XXX-XXXX")))
|
||||
}
|
||||
|
||||
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 }
|
||||
|
||||
// Format the number with dashes
|
||||
let formattedNumber = formatUSNumber(rawNumber)
|
||||
let formattedNumber = rawNumber.formatUSNumber()
|
||||
|
||||
// Set the formatted text
|
||||
textField.text = formattedNumber
|
||||
@ -62,18 +84,32 @@ extension InputField {
|
||||
textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition)
|
||||
}
|
||||
|
||||
value = formattedNumber
|
||||
|
||||
// Prevent the default behavior
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
internal func formatUSNumber(_ number: String) -> String {
|
||||
override func textFieldDidEndEditing(_ inputField: InputField, textField: UITextField) {
|
||||
if let text = inputField.text {
|
||||
textField.text = text.formatUSNumber()
|
||||
value = textField.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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))
|
||||
@ -97,8 +133,5 @@ extension InputField {
|
||||
|
||||
return formattedNumber
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ import Combine
|
||||
/// Specialized input fields capture credit card numbers, inline actions, passwords, phone numbers,
|
||||
/// dates and security codes in their correct formats.
|
||||
@objc(VDSInputField)
|
||||
open class InputField: EntryFieldBase {
|
||||
open class InputField: EntryFieldBase<String> {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
@ -46,8 +46,9 @@ open class InputField: EntryFieldBase {
|
||||
|
||||
internal override var minWidth: CGFloat { fieldType.handler().minWidth }
|
||||
internal override var maxWidth: CGFloat {
|
||||
let frameWidth = frame.size.width
|
||||
return helperTextPlacement == .right ? (frameWidth - horizontalStackView.spacing) / 2 : frameWidth
|
||||
let frameWidth = constrainedWidth
|
||||
let halfWidth = (frameWidth - horizontalStackView.spacing) / 2
|
||||
return helperTextPlacement == .right && halfWidth > minWidth * 2 ? halfWidth : frameWidth
|
||||
}
|
||||
|
||||
/// The is used for the for adding the helperLabel to the right of the containerView.
|
||||
@ -105,6 +106,11 @@ open class InputField: EntryFieldBase {
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
$0.textStyle = TextStyle.bodyLarge
|
||||
$0.isAccessibilityElement = false
|
||||
$0.autocorrectionType = .no
|
||||
$0.spellCheckingType = .no
|
||||
$0.smartQuotesType = .no
|
||||
$0.smartDashesType = .no
|
||||
$0.smartInsertDeleteType = .no
|
||||
}
|
||||
|
||||
/// Color configuration for the textField.
|
||||
@ -182,6 +188,8 @@ open class InputField: EntryFieldBase {
|
||||
super.setup()
|
||||
accessibilityHintText = "Double tap to edit"
|
||||
|
||||
actionTextLink.accessibilityTraits = .button
|
||||
|
||||
textField.heightAnchor.constraint(equalToConstant: 20).isActive = true
|
||||
textField.delegate = self
|
||||
bottomContainerStackView.insertArrangedSubview(successLabel, at: 0)
|
||||
@ -197,6 +205,22 @@ open class InputField: EntryFieldBase {
|
||||
|
||||
textField.textColorConfiguration = textFieldTextColorConfiguration
|
||||
|
||||
}
|
||||
|
||||
open override func getFieldContainer() -> UIView {
|
||||
return textField
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
textField.text = ""
|
||||
|
||||
successLabel.textStyle = .bodySmall
|
||||
|
||||
fieldType = .text
|
||||
showSuccess = false
|
||||
successText = nil
|
||||
|
||||
containerView.bridge_accessibilityLabelBlock = { [weak self] in
|
||||
guard let self else { return "" }
|
||||
var accessibilityLabels = [String]()
|
||||
@ -205,11 +229,11 @@ open class InputField: EntryFieldBase {
|
||||
accessibilityLabels.append(text)
|
||||
}
|
||||
|
||||
if let formatText = textField.formatText, !formatText.isEmpty {
|
||||
if let formatText = textField.formatText, !formatText.isEmpty, textField.text.isEmpty {
|
||||
accessibilityLabels.append("format, \(formatText)")
|
||||
}
|
||||
|
||||
if let placeholderText = textField.placeholder, !placeholderText.isEmpty {
|
||||
if let placeholderText = textField.placeholder, !placeholderText.isEmpty, textField.text.isEmpty {
|
||||
accessibilityLabels.append("placeholder, \(placeholderText)")
|
||||
}
|
||||
|
||||
@ -244,24 +268,17 @@ open class InputField: EntryFieldBase {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open override func getFieldContainer() -> UIView {
|
||||
return textField
|
||||
containerView.bridge_accessibilityValueBlock = { [weak self] in
|
||||
guard let self else { return "" }
|
||||
return textField.isSecureTextEntry ? "\(textField.text.count) stars" : value
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
textField.text = ""
|
||||
|
||||
successLabel.reset()
|
||||
successLabel.textStyle = .bodySmall
|
||||
|
||||
fieldType = .text
|
||||
showSuccess = false
|
||||
successText = nil
|
||||
helperTextPlacement = .bottom
|
||||
super.reset()
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
@ -299,6 +316,21 @@ open class InputField: EntryFieldBase {
|
||||
}
|
||||
}
|
||||
|
||||
open var widthPercentage: CGFloat? { didSet { setNeedsUpdate() } }
|
||||
|
||||
internal override func updateContainerWidth() {
|
||||
widthConstraint?.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()
|
||||
} else {
|
||||
super.updateContainerWidth()
|
||||
}
|
||||
}
|
||||
|
||||
override func updateRules() {
|
||||
super.updateRules()
|
||||
fieldType.handler().appendRules(self)
|
||||
@ -338,29 +370,33 @@ open class InputField: EntryFieldBase {
|
||||
}
|
||||
|
||||
extension InputField: UITextFieldDelegate {
|
||||
public func textFieldDidBeginEditing(_ textField: UITextField) {
|
||||
open func textFieldDidBeginEditing(_ textField: UITextField) {
|
||||
fieldType.handler().textFieldDidBeginEditing(self, textField: textField)
|
||||
updateContainerView()
|
||||
updateErrorLabel()
|
||||
}
|
||||
|
||||
public func textFieldDidEndEditing(_ textField: UITextField) {
|
||||
open func textFieldDidEndEditing(_ textField: UITextField) {
|
||||
fieldType.handler().textFieldDidEndEditing(self, textField: textField)
|
||||
validate()
|
||||
UIAccessibility.post(notification: .layoutChanged, argument: self.containerView)
|
||||
}
|
||||
|
||||
public func textFieldDidChangeSelection(_ textField: UITextField) {
|
||||
open func textFieldDidChangeSelection(_ textField: UITextField) {
|
||||
fieldType.handler().textFieldDidChangeSelection(self, textField: textField)
|
||||
text = textField.text
|
||||
sendActions(for: .valueChanged)
|
||||
if fieldType.handler().validateOnChange {
|
||||
validate()
|
||||
}
|
||||
sendActions(for: .valueChanged)
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
||||
return fieldType.handler().textField(self, textField: textField, shouldChangeCharactersIn: range, replacementString: string)
|
||||
open func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
||||
let shouldChange = fieldType.handler().textField(self, textField: textField, shouldChangeCharactersIn: range, replacementString: string)
|
||||
if shouldChange {
|
||||
UIAccessibility.post(notification: .announcement, argument: string)
|
||||
}
|
||||
return shouldChange
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -47,6 +47,11 @@ open class TextField: UITextField, ViewProtocol, Errorable {
|
||||
//--------------------------------------------------
|
||||
// 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()
|
||||
open var shouldUpdateView: Bool = true
|
||||
|
||||
@ -92,19 +97,22 @@ open class TextField: UITextField, ViewProtocol, Errorable {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Lifecycle
|
||||
//--------------------------------------------------
|
||||
open func initialSetup() {
|
||||
private func initialSetup() {
|
||||
if !initialSetupPerformed {
|
||||
initialSetupPerformed = true
|
||||
backgroundColor = .clear
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
clipsToBounds = true
|
||||
shouldUpdateView = false
|
||||
setup()
|
||||
setDefaults()
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
open func setup() {
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
clipsToBounds = true
|
||||
|
||||
let accessView = UIView(frame: .init(origin: .zero, size: .init(width: UIScreen.main.bounds.width, height: 44)))
|
||||
accessView.backgroundColor = .white
|
||||
accessView.addBorder(side: .top, width: 1, color: .lightGray)
|
||||
@ -118,6 +126,17 @@ open class TextField: UITextField, ViewProtocol, Errorable {
|
||||
inputAccessoryView = accessView
|
||||
}
|
||||
|
||||
open func setDefaults() {
|
||||
backgroundColor = .clear
|
||||
surface = .light
|
||||
text = nil
|
||||
formatText = nil
|
||||
useScaledFont = false
|
||||
showError = false
|
||||
errorText = nil
|
||||
textStyle = .defaultStyle
|
||||
}
|
||||
|
||||
@objc func doneButtonAction() {
|
||||
// Resigns the first responder status when 'Done' is tapped
|
||||
let _ = resignFirstResponder()
|
||||
@ -168,8 +187,7 @@ open class TextField: UITextField, ViewProtocol, Errorable {
|
||||
|
||||
open func reset() {
|
||||
shouldUpdateView = false
|
||||
surface = .light
|
||||
text = nil
|
||||
setDefaults()
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
@ -209,6 +227,23 @@ open class TextField: UITextField, ViewProtocol, Errorable {
|
||||
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
|
||||
//--------------------------------------------------
|
||||
|
||||
@ -7,12 +7,14 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
class RequiredRule: Rule {
|
||||
class RequiredRule<ValueType>: Rule {
|
||||
var maxLength: Int?
|
||||
var errorMessage: String = "This field is required."
|
||||
|
||||
func isValid(value: String?) -> Bool {
|
||||
guard let value, !value.isEmpty, value.count > 0 else { return false }
|
||||
func isValid(value: ValueType?) -> Bool {
|
||||
guard let value,
|
||||
!"\(value)".isEmpty,
|
||||
"\(value)".count > 0 else { return false }
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ import Combine
|
||||
/// A text area is an input wherein a customer enters long-form information.
|
||||
/// Use a text area when you want customers to enter text that’s longer than a single line.
|
||||
@objc(VDSTextArea)
|
||||
open class TextArea: EntryFieldBase {
|
||||
open class TextArea: EntryFieldBase<String> {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
@ -101,6 +101,7 @@ open class TextArea: EntryFieldBase {
|
||||
$0.isScrollEnabled = true
|
||||
$0.textContainerInset = .zero
|
||||
$0.autocorrectionType = .no
|
||||
$0.spellCheckingType = .no
|
||||
$0.textContainer.lineFragmentPadding = 0
|
||||
}
|
||||
|
||||
@ -110,9 +111,13 @@ open class TextArea: EntryFieldBase {
|
||||
}
|
||||
|
||||
didSet {
|
||||
setNeedsUpdate()
|
||||
|
||||
if textView.isFirstResponder {
|
||||
validate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Color configuration for character counter's highlight background color
|
||||
internal var highlightBackgroundColor = ControlColorConfiguration().with {
|
||||
@ -132,6 +137,7 @@ open class TextArea: EntryFieldBase {
|
||||
super.setup()
|
||||
|
||||
accessibilityHintText = "Double tap to edit"
|
||||
textView.delegate = self
|
||||
|
||||
//events
|
||||
textView
|
||||
@ -160,13 +166,18 @@ open class TextArea: EntryFieldBase {
|
||||
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
minHeight = .twoX
|
||||
maxLength = nil
|
||||
textView.text = ""
|
||||
characterCounterLabel.textStyle = .bodySmall
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
textView.text = ""
|
||||
characterCounterLabel.reset()
|
||||
characterCounterLabel.textStyle = .bodySmall
|
||||
setNeedsUpdate()
|
||||
super.reset()
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
@ -187,9 +198,10 @@ open class TextArea: EntryFieldBase {
|
||||
|
||||
override func updateRules() {
|
||||
super.updateRules()
|
||||
|
||||
if let maxLength, maxLength > 0 {
|
||||
rules.append(.init(countRule))
|
||||
}
|
||||
}
|
||||
|
||||
open override func getFieldContainer() -> UIView {
|
||||
textView
|
||||
@ -226,7 +238,7 @@ open class TextArea: EntryFieldBase {
|
||||
}
|
||||
}
|
||||
|
||||
func textViewDidChange(_ textView: UITextView) {
|
||||
public func textViewDidChange(_ textView: UITextView) {
|
||||
|
||||
//dynamic textView Height sizing based on Figma
|
||||
//if you want it to work "as-is" delete this code
|
||||
@ -288,3 +300,10 @@ open class TextArea: EntryFieldBase {
|
||||
//--------------------------------------------------
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,6 +45,15 @@ open class TextView: UITextView, ViewProtocol, Errorable {
|
||||
//--------------------------------------------------
|
||||
// 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()
|
||||
open var shouldUpdateView: Bool = true
|
||||
|
||||
@ -88,6 +97,7 @@ open class TextView: UITextView, ViewProtocol, Errorable {
|
||||
if textAlignment != oldValue {
|
||||
// Text alignment can be part of our paragraph style, so we may need to
|
||||
// re-style when changed
|
||||
placeholderLabel.textAlignment = textAlignment
|
||||
updateLabel()
|
||||
}
|
||||
}
|
||||
@ -96,17 +106,20 @@ open class TextView: UITextView, ViewProtocol, Errorable {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Lifecycle
|
||||
//--------------------------------------------------
|
||||
open func initialSetup() {
|
||||
private func initialSetup() {
|
||||
if !initialSetupPerformed {
|
||||
initialSetupPerformed = true
|
||||
backgroundColor = .clear
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
shouldUpdateView = false
|
||||
setup()
|
||||
setDefaults()
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
open func setup() {
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let accessView = UIView(frame: .init(origin: .zero, size: .init(width: UIScreen.main.bounds.width, height: 44)))
|
||||
accessView.backgroundColor = .white
|
||||
accessView.addBorder(side: .top, width: 1, color: .lightGray)
|
||||
@ -118,6 +131,18 @@ open class TextView: UITextView, ViewProtocol, Errorable {
|
||||
done.pinCenterY()
|
||||
.pinTrailing(16)
|
||||
inputAccessoryView = accessView
|
||||
|
||||
addSubview(placeholderLabel)
|
||||
placeholderLabel.pinToSuperView()
|
||||
}
|
||||
|
||||
open func setDefaults() {
|
||||
backgroundColor = .clear
|
||||
surface = .light
|
||||
text = nil
|
||||
placeholder = nil
|
||||
errorText = nil
|
||||
showError = false
|
||||
}
|
||||
|
||||
@objc func doneButtonAction() {
|
||||
@ -139,12 +164,15 @@ open class TextView: UITextView, ViewProtocol, Errorable {
|
||||
|
||||
open func reset() {
|
||||
shouldUpdateView = false
|
||||
surface = .light
|
||||
text = nil
|
||||
setDefaults()
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
open override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
placeholderLabel.preferredMaxLayoutWidth = textContainer.size.width - textContainer.lineFragmentPadding * 2
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Accessibility
|
||||
@ -297,6 +325,10 @@ open class TextView: UITextView, ViewProtocol, Errorable {
|
||||
} else {
|
||||
attributedText = nil
|
||||
}
|
||||
placeholderLabel.textStyle = textStyle
|
||||
placeholderLabel.surface = surface
|
||||
placeholderLabel.text = placeholder
|
||||
placeholderLabel.isHidden = !text.isEmpty
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ import Combine
|
||||
open class TileContainer: TileContainerBase<TileContainer.Padding> {
|
||||
|
||||
/// Enum used to describe the padding choices used for this component.
|
||||
public enum Padding: DefaultValuing {
|
||||
public enum Padding: DefaultValuing, Valuing {
|
||||
case padding3X
|
||||
case padding4X
|
||||
case padding6X
|
||||
@ -43,7 +43,8 @@ open class TileContainer: TileContainerBase<TileContainer.Padding> {
|
||||
}
|
||||
}
|
||||
|
||||
open class TileContainerBase<PaddingType: DefaultValuing>: Control where PaddingType.ValueType == CGFloat {
|
||||
open class TileContainerBase<PaddingType: DefaultValuing & Valuing>: View where PaddingType.ValueType == CGFloat {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
//--------------------------------------------------
|
||||
@ -80,10 +81,25 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
}
|
||||
|
||||
/// Enum used to describe the background effect choices used for this component.
|
||||
public enum BackgroundEffect {
|
||||
public enum BackgroundEffect: Equatable {
|
||||
case transparency
|
||||
case gradient(UIColor, UIColor)
|
||||
case none
|
||||
|
||||
public static func == (lhs: TileContainerBase.BackgroundEffect, rhs: TileContainerBase.BackgroundEffect) -> Bool {
|
||||
lhs.description == lhs.description
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .transparency:
|
||||
"transparency"
|
||||
case .gradient(let first, let second):
|
||||
"gradient(\(first), \(second)"
|
||||
case .none:
|
||||
"none"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum used to describe the aspect ratios used for this component.
|
||||
@ -109,7 +125,14 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
$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)
|
||||
}
|
||||
|
||||
private var isHighlighted: Bool = false { didSet { setNeedsUpdate() } }
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
@ -179,11 +202,7 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
//--------------------------------------------------
|
||||
internal var widthConstraint: NSLayoutConstraint?
|
||||
internal var heightConstraint: NSLayoutConstraint?
|
||||
internal var heightGreaterThanConstraint: NSLayoutConstraint?
|
||||
internal var containerTopConstraint: NSLayoutConstraint?
|
||||
internal var containerBottomConstraint: NSLayoutConstraint?
|
||||
internal var containerLeadingConstraint: NSLayoutConstraint?
|
||||
internal var containerTrailingConstraint: NSLayoutConstraint?
|
||||
internal var aspectRatioConstraint: NSLayoutConstraint?
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Configuration
|
||||
@ -222,29 +241,21 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
super.setup()
|
||||
isAccessibilityElement = false
|
||||
|
||||
let layoutGuide = UILayoutGuide()
|
||||
addLayoutGuide(layoutGuide)
|
||||
layoutGuide
|
||||
.pinTop()
|
||||
.pinLeading()
|
||||
.pinTrailing(0, .defaultHigh)
|
||||
.pinBottom(0, .defaultHigh)
|
||||
addSubview(backgroundImageView)
|
||||
addSubview(containerView)
|
||||
containerView.addSubview(contentView)
|
||||
addSubview(highlightView)
|
||||
|
||||
containerView.pinToSuperView()
|
||||
|
||||
widthConstraint = layoutGuide.widthAnchor.constraint(equalToConstant: 0)
|
||||
|
||||
heightGreaterThanConstraint = layoutGuide.heightAnchor.constraint(greaterThanOrEqualToConstant: 44.0)
|
||||
heightGreaterThanConstraint?.isActive = false
|
||||
|
||||
heightConstraint = layoutGuide.heightAnchor.constraint(equalToConstant: 0)
|
||||
|
||||
containerView.addSubview(backgroundImageView)
|
||||
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: .vertical)
|
||||
backgroundImageView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
@ -252,25 +263,28 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
backgroundImageView.isUserInteractionEnabled = false
|
||||
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.backgroundColor = .clear
|
||||
|
||||
//corner radius
|
||||
layer.cornerRadius = cornerRadius
|
||||
containerView.layer.cornerRadius = cornerRadius
|
||||
backgroundImageView.layer.cornerRadius = cornerRadius
|
||||
highlightView.layer.cornerRadius = cornerRadius
|
||||
clipsToBounds = true
|
||||
containerView.clipsToBounds = true
|
||||
|
||||
containerView.bridge_isAccessibilityElementBlock = { [weak self] in self?.onClickSubscriber != nil }
|
||||
containerView.accessibilityHint = "Double tap to open."
|
||||
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
|
||||
@ -281,19 +295,18 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
return view
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
shouldUpdateView = false
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
backgroundImage = nil
|
||||
color = .white
|
||||
backgroundEffect = .none
|
||||
padding = .defaultValue
|
||||
aspectRatio = .ratio1x1
|
||||
imageFallbackColor = .light
|
||||
width = nil
|
||||
height = nil
|
||||
showBorder = false
|
||||
showDropShadow = false
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
@ -303,44 +316,14 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
highlightView.backgroundColor = hightLightViewColorConfiguration.getColor(self)
|
||||
highlightView.isHidden = !isHighlighted
|
||||
|
||||
layer.borderColor = borderColorConfiguration.getColor(self).cgColor
|
||||
layer.borderWidth = showBorder ? VDSFormControls.borderWidth : 0
|
||||
containerView.layer.borderColor = borderColorConfiguration.getColor(self).cgColor
|
||||
containerView.layer.borderWidth = showBorder ? VDSFormControls.borderWidth : 0
|
||||
|
||||
containerTopConstraint?.constant = padding.value
|
||||
containerLeadingConstraint?.constant = padding.value
|
||||
containerBottomConstraint?.constant = padding.value
|
||||
containerTrailingConstraint?.constant = padding.value
|
||||
contentView.removeConstraints()
|
||||
contentView.pinToSuperView(.uniform(padding.value))
|
||||
|
||||
if let width, aspectRatio == .none && height == nil{
|
||||
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
|
||||
}
|
||||
updateContainerView()
|
||||
|
||||
applyBackgroundEffects()
|
||||
|
||||
if showDropShadow, surface == .light {
|
||||
addDropShadow(dropShadowConfiguration)
|
||||
} else {
|
||||
removeDropShadows()
|
||||
}
|
||||
}
|
||||
|
||||
open override var accessibilityElements: [Any]? {
|
||||
@ -362,18 +345,39 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
containerView.setAccessibilityLabel(for: views)
|
||||
|
||||
//append all children that are accessible
|
||||
if containerView.isAccessibilityElement {
|
||||
elements.forEach({ element in
|
||||
if element.accessibilityTraits.contains(.button) || element.accessibilityTraits.contains(.link) {
|
||||
items.append(element)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
items.append(contentsOf: elements)
|
||||
|
||||
}
|
||||
return items
|
||||
}
|
||||
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 }
|
||||
open override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
if onClickSubscriber != nil {
|
||||
isHighlighted = true
|
||||
}
|
||||
}
|
||||
|
||||
open override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
super.touchesEnded(touches, with: event)
|
||||
if onClickSubscriber != nil {
|
||||
isHighlighted = false
|
||||
}
|
||||
}
|
||||
|
||||
open override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
super.touchesCancelled(touches, with: event)
|
||||
if onClickSubscriber != nil {
|
||||
isHighlighted = false
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
@ -400,58 +404,109 @@ open class TileContainerBase<PaddingType: DefaultValuing>: Control where Padding
|
||||
switch backgroundEffect {
|
||||
case .transparency:
|
||||
alphaConfiguration = 0.8
|
||||
removeGradientLayer()
|
||||
containerView.removeGradientLayer()
|
||||
case .gradient(let firstColor, let secondColor):
|
||||
alphaConfiguration = 1.0
|
||||
addGradientLayer(with: firstColor, secondColor: secondColor)
|
||||
containerView.addGradientLayer(with: firstColor, secondColor: secondColor)
|
||||
backgroundImageView.isHidden = true
|
||||
backgroundImageView.alpha = 1.0
|
||||
case .none:
|
||||
alphaConfiguration = 1.0
|
||||
removeGradientLayer()
|
||||
containerView.removeGradientLayer()
|
||||
}
|
||||
if let backgroundImage {
|
||||
backgroundImageView.image = backgroundImage
|
||||
backgroundImageView.isHidden = false
|
||||
backgroundImageView.alpha = alphaConfiguration
|
||||
backgroundColor = imageFallbackColor.withAlphaComponent(alphaConfiguration)
|
||||
containerView.backgroundColor = imageFallbackColor.withAlphaComponent(alphaConfiguration)
|
||||
} else {
|
||||
backgroundImageView.isHidden = true
|
||||
backgroundImageView.alpha = 1.0
|
||||
backgroundColor = color.withAlphaComponent(alphaConfiguration)
|
||||
containerView.backgroundColor = color.withAlphaComponent(alphaConfiguration)
|
||||
}
|
||||
}
|
||||
|
||||
private func ratioSize(for width: CGFloat) -> CGSize {
|
||||
var height: CGFloat = width
|
||||
private func updateContainerView() {
|
||||
applyBackgroundEffects()
|
||||
|
||||
switch aspectRatio {
|
||||
case .ratio1x1:
|
||||
break;
|
||||
case .ratio3x4:
|
||||
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
|
||||
if showDropShadow, surface == .light {
|
||||
containerView.addDropShadow(dropShadowConfiguration)
|
||||
} else {
|
||||
containerView.removeDropShadows()
|
||||
}
|
||||
|
||||
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
|
||||
//-------------------------------------------------------------------------
|
||||
//-------------------------------------------------------------------------
|
||||
//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()
|
||||
|
||||
}
|
||||
//-------------------------------------------------------------------------
|
||||
//Multiplier, meaning it was pinned with width only Constraint
|
||||
//-------------------------------------------------------------------------
|
||||
else if let multiplier {
|
||||
aspectRatioConstraint = heightAnchor.constraint(equalTo: widthAnchor, 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TileContainerBase {
|
||||
@ -491,3 +546,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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,10 +16,10 @@ import Combine
|
||||
/// while it can include an arrow CTA, it does not require one in order to
|
||||
/// function.
|
||||
@objc(VDSTilelet)
|
||||
open class Tilelet: TileContainerBase<Tilelet.Padding> {
|
||||
open class Tilelet: TileContainerBase<Tilelet.Padding>, ParentViewProtocol {
|
||||
|
||||
/// Enum used to describe the padding choices used for this component.
|
||||
public enum Padding: String, DefaultValuing, CaseIterable {
|
||||
public enum Padding: String, DefaultValuing, Valuing, CaseIterable {
|
||||
case small
|
||||
case large
|
||||
|
||||
@ -28,9 +28,9 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
|
||||
public var value: CGFloat {
|
||||
switch self {
|
||||
case .small:
|
||||
return UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space4X
|
||||
return UIDevice.isIPad ? VDSLayout.space4X : VDSLayout.space3X
|
||||
case .large:
|
||||
return UIDevice.isIPad ? VDSLayout.space4X : VDSLayout.space6X
|
||||
return UIDevice.isIPad ? VDSLayout.space6X : VDSLayout.space4X
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,12 +103,14 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
|
||||
}
|
||||
|
||||
private var backgroundColorSurface: Surface {
|
||||
backgroundColorConfiguration.getColor(self).surface
|
||||
backgroundColorConfiguration.getColor(self).isDark() ? .dark : .light
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
open var children: [any ViewProtocol] { [badge, titleLockup, descriptiveIcon, directionalIcon] }
|
||||
|
||||
/// Title lockup positioned in the contentView.
|
||||
open var titleLockup = TitleLockup().with {
|
||||
$0.standardStyleConfiguration = .init(styleConfigurations: [
|
||||
@ -205,15 +207,10 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
|
||||
}
|
||||
|
||||
/// Descriptive Icon positioned in the contentView.
|
||||
open var descriptiveIcon = Icon().with {
|
||||
$0.isAccessibilityElement = false
|
||||
}
|
||||
open var descriptiveIcon = Icon()
|
||||
|
||||
/// Directional Icon positioned in the contentView.
|
||||
open var directionalIcon = Icon().with {
|
||||
$0.isAccessibilityElement = false
|
||||
$0.name = .rightArrow
|
||||
}
|
||||
open var directionalIcon = Icon()
|
||||
|
||||
private var _textWidth: TextWidth?
|
||||
|
||||
@ -282,6 +279,7 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Constraints
|
||||
//--------------------------------------------------
|
||||
internal var iconContainerHeightConstraint: NSLayoutConstraint?
|
||||
internal var titleLockupWidthConstraint: NSLayoutConstraint?
|
||||
internal var titleLockupTrailingConstraint: NSLayoutConstraint?
|
||||
internal var titleLockupTopConstraint: NSLayoutConstraint?
|
||||
@ -302,8 +300,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.
|
||||
open override func setup() {
|
||||
super.setup()
|
||||
aspectRatio = .none
|
||||
color = .black
|
||||
aspectRatio = .none
|
||||
|
||||
addContentView(stackView)
|
||||
|
||||
//badge
|
||||
@ -329,15 +328,15 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
|
||||
|
||||
iconContainerView.addSubview(descriptiveIcon)
|
||||
iconContainerView.addSubview(directionalIcon)
|
||||
|
||||
iconContainerHeightConstraint = iconContainerView.height(constant: 0)
|
||||
descriptiveIcon
|
||||
.pinLeading()
|
||||
.pinTop()
|
||||
.pinTopGreaterThanOrEqualTo()
|
||||
.pinBottom()
|
||||
|
||||
directionalIcon
|
||||
.pinTrailing()
|
||||
.pinTop()
|
||||
.pinTopGreaterThanOrEqualTo()
|
||||
.pinBottom()
|
||||
|
||||
badge.bottomAnchor.constraint(equalTo: badge.label.bottomAnchor, constant: 2).activate()
|
||||
@ -381,21 +380,32 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
|
||||
titleLockupSubTitleLabelHeightGreaterThanConstraint = titleLockup.subTitleLabel.heightGreaterThanEqualTo(constant: titleLockup.subTitleLabel.minimumLineHeight)
|
||||
titleLockupSubTitleLabelHeightGreaterThanConstraint?.priority = .defaultHigh
|
||||
titleLockupSubTitleLabelHeightGreaterThanConstraint?.activate()
|
||||
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
shouldUpdateView = false
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
aspectRatio = .none
|
||||
color = .black
|
||||
textWidth = nil
|
||||
textPostion = .top
|
||||
|
||||
//models
|
||||
badgeModel = nil
|
||||
titleModel = nil
|
||||
subTitleModel = nil
|
||||
descriptiveIconModel = nil
|
||||
directionalIconModel = nil
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
@ -405,18 +415,14 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
|
||||
updateBadge()
|
||||
updateTitleLockup()
|
||||
updateIcons()
|
||||
///Content-driven height Tilelets - Minimum height is configurable.
|
||||
///if width != nil && (aspectRatio != .none || height != nil) then tilelet is not self growing, so we can apply text position alignments.
|
||||
if width != nil && (aspectRatio != .none || height != nil) {
|
||||
updateTextPositionAlignment()
|
||||
}
|
||||
setNeedsLayout()
|
||||
}
|
||||
|
||||
/// Used to update any Accessibility properties.
|
||||
open override var accessibilityElements: [Any]? {
|
||||
get {
|
||||
var views = [UIView]()
|
||||
var views = [AnyObject]()
|
||||
|
||||
// grab the available views in order
|
||||
if badgeModel != nil {
|
||||
@ -424,7 +430,15 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
|
||||
}
|
||||
|
||||
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)
|
||||
@ -454,6 +468,7 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
|
||||
private func updateBadge() {
|
||||
if let badgeModel {
|
||||
badge.text = badgeModel.text
|
||||
badge.textColor = badgeModel.textColor
|
||||
badge.fillColor = badgeModel.fillColor
|
||||
badge.numberOfLines = badgeModel.numberOfLines
|
||||
badge.surface = backgroundColorSurface
|
||||
@ -544,6 +559,7 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
|
||||
descriptiveIcon.color = color
|
||||
}
|
||||
descriptiveIcon.size = descriptiveIconModel.size
|
||||
iconContainerHeightConstraint?.constant = descriptiveIcon.size.dimensions.height
|
||||
descriptiveIcon.surface = backgroundColorSurface
|
||||
showIconContainerView = true
|
||||
}
|
||||
@ -554,6 +570,7 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
|
||||
directionalIcon.color = color
|
||||
}
|
||||
directionalIcon.size = directionalIconModel.size.value
|
||||
iconContainerHeightConstraint?.constant = directionalIcon.size.dimensions.height
|
||||
directionalIcon.surface = backgroundColorSurface
|
||||
showIconContainerView = true
|
||||
}
|
||||
@ -584,6 +601,7 @@ open class Tilelet: TileContainerBase<Tilelet.Padding> {
|
||||
}
|
||||
|
||||
private func updateTextPositionAlignment() {
|
||||
guard aspectRatio != .none || height != nil else { return }
|
||||
switch textPostion {
|
||||
case .top:
|
||||
titleLockupTopConstraint?.activate()
|
||||
|
||||
@ -11,10 +11,13 @@ import UIKit
|
||||
extension Tilelet {
|
||||
|
||||
/// Model that represents the options available for the badge.
|
||||
public struct BadgeModel {
|
||||
public struct BadgeModel: Equatable {
|
||||
/// Text that will be used for the badge.
|
||||
public var text: String = ""
|
||||
|
||||
/// Text color that will be used for the badge.
|
||||
public var textColor: Badge.TextColor?
|
||||
|
||||
/// Fill color that will be used for the badge.
|
||||
public var fillColor: Badge.FillColor
|
||||
|
||||
@ -30,8 +33,9 @@ extension Tilelet {
|
||||
/// LineBreakMode used in Badge label.
|
||||
public var lineBreakMode: NSLineBreakMode
|
||||
|
||||
public init(text: String, fillColor: Badge.FillColor = .red, surface: Surface = .light, numberOfLines: Int = 0, maxWidth: CGFloat? = nil, lineBreakMode: NSLineBreakMode = .byTruncatingTail) {
|
||||
public init(text: String, textColor: Badge.TextColor? = nil, fillColor: Badge.FillColor = .red, surface: Surface = .light, numberOfLines: Int = 0, maxWidth: CGFloat? = nil, lineBreakMode: NSLineBreakMode = .byTruncatingTail) {
|
||||
self.text = text
|
||||
self.textColor = textColor
|
||||
self.fillColor = fillColor
|
||||
self.surface = surface
|
||||
self.numberOfLines = numberOfLines
|
||||
|
||||
@ -32,7 +32,7 @@ extension Tilelet {
|
||||
}
|
||||
|
||||
/// Model that represents the options available for the descriptive icon.
|
||||
public struct DescriptiveIcon {
|
||||
public struct DescriptiveIcon: Equatable {
|
||||
/// A representation that will be used to render the icon with corresponding name.
|
||||
public var name: Icon.Name
|
||||
|
||||
@ -58,7 +58,7 @@ extension Tilelet {
|
||||
}
|
||||
|
||||
/// Model that represents the options available for the directional icon.
|
||||
public struct DirectionalIcon {
|
||||
public struct DirectionalIcon: Equatable {
|
||||
public enum IconType: String, CaseIterable {
|
||||
case rightArrow
|
||||
case externalLink
|
||||
@ -66,6 +66,10 @@ extension Tilelet {
|
||||
public var iconName: Icon.Name {
|
||||
return self == .rightArrow ? .rightArrow : .externalLink
|
||||
}
|
||||
|
||||
public var accessibilityLabel: String {
|
||||
self == .rightArrow ? "Directional right arrow" : "External link"
|
||||
}
|
||||
}
|
||||
|
||||
public enum IconSize: String, EnumSubset {
|
||||
@ -80,7 +84,7 @@ extension Tilelet {
|
||||
public var iconColor: IconColor?
|
||||
|
||||
/// Accessible Text for the Icon
|
||||
public var accessibleText: String
|
||||
public var accessibleText: String?
|
||||
|
||||
/// Enum for a icon type you want shown..
|
||||
public var iconType: IconType
|
||||
@ -95,7 +99,7 @@ extension Tilelet {
|
||||
|
||||
self.iconType = iconType
|
||||
self.iconColor = iconColor
|
||||
self.accessibleText = accessibleText ?? iconType.iconName.rawValue
|
||||
self.accessibleText = accessibleText ?? iconType.accessibilityLabel
|
||||
self.size = size
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ import UIKit
|
||||
|
||||
extension Tilelet {
|
||||
/// Model that represents the options available for the sub title label.
|
||||
public struct SubTitleModel {
|
||||
public struct SubTitleModel: Equatable {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Enums
|
||||
//--------------------------------------------------
|
||||
@ -67,5 +67,13 @@ extension Tilelet {
|
||||
textAttributes: textAttributes,
|
||||
lineBreakMode: lineBreakMode)
|
||||
}
|
||||
|
||||
public static func == (lhs: Tilelet.SubTitleModel, rhs: Tilelet.SubTitleModel) -> Bool {
|
||||
lhs.text == rhs.text
|
||||
&& lhs.textColor == rhs.textColor
|
||||
&& lhs.otherStandardStyle == rhs.otherStandardStyle
|
||||
&& lhs.textAttributes == rhs.textAttributes
|
||||
&& lhs.lineBreakMode == rhs.lineBreakMode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ import UIKit
|
||||
|
||||
extension Tilelet {
|
||||
/// Model that represents the options available for the title label.
|
||||
public struct TitleModel {
|
||||
public struct TitleModel: Equatable {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Enums
|
||||
//--------------------------------------------------
|
||||
@ -75,5 +75,14 @@ extension Tilelet {
|
||||
standardStyle: standardStyle.value,
|
||||
lineBreakMode: lineBreakMode)
|
||||
}
|
||||
|
||||
public static func == (lhs: Tilelet.TitleModel, rhs: Tilelet.TitleModel) -> Bool {
|
||||
lhs.text == rhs.text
|
||||
&& lhs.textColor == rhs.textColor
|
||||
&& lhs.isBold == rhs.isBold
|
||||
&& lhs.textAttributes == rhs.textAttributes
|
||||
&& lhs.standardStyle == rhs.standardStyle
|
||||
&& lhs.lineBreakMode == rhs.lineBreakMode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@ import UIKit
|
||||
extension Tilelet {
|
||||
|
||||
/// Model that represents the options available for the eyebrow label.
|
||||
public struct EyebrowModel {
|
||||
public struct EyebrowModel: Equatable {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
@ -60,5 +60,14 @@ extension Tilelet {
|
||||
standardStyle: standardStyle.value,
|
||||
textAttributes: textAttributes)
|
||||
}
|
||||
|
||||
public static func == (lhs: Tilelet.EyebrowModel, rhs: Tilelet.EyebrowModel) -> Bool {
|
||||
lhs.text == rhs.text
|
||||
&& lhs.textColor == rhs.textColor
|
||||
&& lhs.isBold == rhs.isBold
|
||||
&& lhs.textAttributes == rhs.textAttributes
|
||||
&& lhs.standardStyle == rhs.standardStyle
|
||||
&& lhs.lineBreakMode == rhs.lineBreakMode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ import Combine
|
||||
/// Title Lockup ensures the readability of words on the screen
|
||||
/// with approved built in text size configurations.
|
||||
@objc(VDSTitleLockup)
|
||||
open class TitleLockup: View {
|
||||
open class TitleLockup: View, ParentViewProtocol {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
@ -61,6 +61,8 @@ open class TitleLockup: View {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
open var children: [any ViewProtocol] { [eyebrowLabel, titleLabel, subTitleLabel] }
|
||||
|
||||
/// Aligns TitleLockup's subcomponent's text
|
||||
open var textAlignment: TextAlignment = .left { didSet { setNeedsUpdate() } }
|
||||
|
||||
@ -279,18 +281,14 @@ open class TitleLockup: View {
|
||||
set {}
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
shouldUpdateView = false
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
textAlignment = .left
|
||||
eyebrowModel = nil
|
||||
titleModel = nil
|
||||
subTitleModel = nil
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
var labelViews = [UIView]()
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
open override func updateView() {
|
||||
super.updateView()
|
||||
|
||||
@ -9,7 +9,7 @@ import Foundation
|
||||
|
||||
extension TitleLockup {
|
||||
/// Model that represents the options available for the eyebrow label.
|
||||
public struct EyebrowModel {
|
||||
public struct EyebrowModel: Equatable {
|
||||
/// Text that will be used for the eyebrow label.
|
||||
public var text: String
|
||||
|
||||
@ -44,6 +44,15 @@ extension TitleLockup {
|
||||
|
||||
/// Text style that will be used for the eyebrow label.
|
||||
public var textStyle: TextStyle { isBold ? standardStyle.value.bold : standardStyle.value.regular }
|
||||
|
||||
public static func == (lhs: TitleLockup.EyebrowModel, rhs: TitleLockup.EyebrowModel) -> Bool {
|
||||
lhs.text == rhs.text
|
||||
&& lhs.textColor == rhs.textColor
|
||||
&& lhs.isBold == rhs.isBold
|
||||
&& lhs.standardStyle == rhs.standardStyle
|
||||
&& lhs.textAttributes == rhs.textAttributes
|
||||
&& lhs.numberOfLines == rhs.numberOfLines
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -46,6 +46,14 @@ extension TitleLockup {
|
||||
/// TextStyle used to render the text.
|
||||
public var textStyle: TextStyle { otherStandardStyle.value.regular }
|
||||
|
||||
public static func == (lhs: TitleLockup.SubTitleModel, rhs: TitleLockup.SubTitleModel) -> Bool {
|
||||
lhs.text == rhs.text
|
||||
&& lhs.textColor == rhs.textColor
|
||||
&& lhs.otherStandardStyle == rhs.otherStandardStyle
|
||||
&& lhs.textAttributes == rhs.textAttributes
|
||||
&& lhs.lineBreakMode == rhs.lineBreakMode
|
||||
&& lhs.numberOfLines == rhs.numberOfLines
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ import UIKit
|
||||
|
||||
extension TitleLockup {
|
||||
/// Model that represents the options available for the sub title label.
|
||||
public struct TitleModel {
|
||||
public struct TitleModel: Equatable {
|
||||
/// Text that will be used for the title label.
|
||||
public var text: String
|
||||
|
||||
@ -51,5 +51,14 @@ extension TitleLockup {
|
||||
/// TextStyle used to render the text.
|
||||
public var textStyle: TextStyle { isBold ? standardStyle.value.bold : standardStyle.value.regular }
|
||||
|
||||
public static func == (lhs: TitleLockup.TitleModel, rhs: TitleLockup.TitleModel) -> Bool {
|
||||
lhs.text == rhs.text
|
||||
&& lhs.textColor == rhs.textColor
|
||||
&& lhs.isBold == rhs.isBold
|
||||
&& lhs.standardStyle == rhs.standardStyle
|
||||
&& lhs.textAttributes == rhs.textAttributes
|
||||
&& lhs.numberOfLines == rhs.numberOfLines
|
||||
&& lhs.lineBreakMode == rhs.lineBreakMode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ import Combine
|
||||
/// A toggle is a control that lets customers instantly turn on
|
||||
/// or turn off a single option, setting or function.
|
||||
@objc(VDSToggle)
|
||||
open class Toggle: Control, Changeable, FormFieldable {
|
||||
open class Toggle: Control, Changeable, FormFieldable, ParentViewProtocol {
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Initializers
|
||||
@ -54,6 +54,7 @@ open class Toggle: Control, Changeable, FormFieldable {
|
||||
private var leftConstraints: [NSLayoutConstraint] = []
|
||||
private var rightConstraints: [NSLayoutConstraint] = []
|
||||
private var labelConstraints: [NSLayoutConstraint] = []
|
||||
private var toggleConstraints: [NSLayoutConstraint] = []
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Configuration
|
||||
@ -88,6 +89,8 @@ open class Toggle: Control, Changeable, FormFieldable {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Public Properties
|
||||
//--------------------------------------------------
|
||||
open var children: [any ViewProtocol] { [toggleView, label] }
|
||||
|
||||
open var onChangeSubscriber: AnyCancellable?
|
||||
|
||||
/// Actual toggle used in this component.
|
||||
@ -147,31 +150,11 @@ open class Toggle: Control, Changeable, FormFieldable {
|
||||
|
||||
open var value: AnyHashable? { isOn }
|
||||
|
||||
/// The natural size for the receiving view, considering only properties of the view itself.
|
||||
open override var intrinsicContentSize: CGSize {
|
||||
if showLabel {
|
||||
label.sizeToFit()
|
||||
let size = CGSize(width: label.frame.width + spacingBetween + toggleContainerSize.width,
|
||||
height: max(toggleContainerSize.height, label.frame.height))
|
||||
return size
|
||||
} else {
|
||||
return toggleContainerSize
|
||||
}
|
||||
}
|
||||
|
||||
open override var shouldHighlight: Bool { false }
|
||||
|
||||
//--------------------------------------------------
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
/// Executed on initialization for this View.
|
||||
open override func initialSetup() {
|
||||
super.initialSetup()
|
||||
onClick = { control in
|
||||
control.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
/// 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()
|
||||
@ -207,6 +190,59 @@ open class Toggle: Control, Changeable, FormFieldable {
|
||||
label.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor)
|
||||
]
|
||||
|
||||
// Set content hugging priority
|
||||
setContentHuggingPriority(.required, for: .horizontal)
|
||||
|
||||
isAccessibilityElement = true
|
||||
if #available(iOS 17.0, *) {
|
||||
accessibilityTraits = .toggleButton
|
||||
} else {
|
||||
accessibilityTraits = .button
|
||||
}
|
||||
addSubview(label)
|
||||
addSubview(toggleView)
|
||||
|
||||
// Set up initial constraints for label and switch
|
||||
toggleView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
|
||||
|
||||
//toggle
|
||||
toggleConstraints = [
|
||||
toggleView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
toggleView.trailingAnchor.constraint(equalTo: trailingAnchor)
|
||||
]
|
||||
|
||||
//toggle and label variants
|
||||
labelConstraints = [
|
||||
height(constant: toggleContainerSize.height, priority: .defaultLow),
|
||||
heightGreaterThanEqualTo(constant: toggleContainerSize.height, priority: .defaultHigh),
|
||||
label.topAnchor.constraint(equalTo: topAnchor),
|
||||
label.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
]
|
||||
|
||||
//label-toggle
|
||||
leftConstraints = [
|
||||
label.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
toggleView.leadingAnchor.constraint(equalTo: label.trailingAnchor, constant: spacingBetween),
|
||||
toggleView.trailingAnchor.constraint(equalTo: trailingAnchor)
|
||||
]
|
||||
|
||||
//toggle-label
|
||||
rightConstraints = [
|
||||
toggleView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
label.leadingAnchor.constraint(equalTo: toggleView.trailingAnchor, constant: spacingBetween),
|
||||
label.trailingAnchor.constraint(equalTo: trailingAnchor)
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
|
||||
onClick = { [weak self] _ in
|
||||
guard let self else { return }
|
||||
toggle()
|
||||
}
|
||||
|
||||
bridge_accessibilityValueBlock = { [weak self] in
|
||||
guard let self else { return "" }
|
||||
if showText {
|
||||
@ -215,13 +251,7 @@ open class Toggle: Control, Changeable, FormFieldable {
|
||||
return isSelected ? "On" : "Off"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
shouldUpdateView = false
|
||||
label.reset()
|
||||
isEnabled = true
|
||||
isOn = false
|
||||
isAnimated = true
|
||||
@ -233,8 +263,12 @@ open class Toggle: Control, Changeable, FormFieldable {
|
||||
textPosition = .left
|
||||
inputId = nil
|
||||
onChange = nil
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
label.reset()
|
||||
super.reset()
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
@ -260,6 +294,8 @@ open class Toggle: Control, Changeable, FormFieldable {
|
||||
label.isHidden = !showLabel
|
||||
|
||||
if showLabel {
|
||||
NSLayoutConstraint.deactivate(toggleConstraints)
|
||||
|
||||
label.textAlignment = textPosition == .left ? .right : .left
|
||||
label.textStyle = textStyle
|
||||
label.text = statusText
|
||||
@ -278,6 +314,7 @@ open class Toggle: Control, Changeable, FormFieldable {
|
||||
NSLayoutConstraint.deactivate(leftConstraints)
|
||||
NSLayoutConstraint.deactivate(rightConstraints)
|
||||
NSLayoutConstraint.deactivate(labelConstraints)
|
||||
NSLayoutConstraint.activate(toggleConstraints)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -104,14 +104,6 @@ open class ToggleView: Control, Changeable, FormFieldable {
|
||||
//--------------------------------------------------
|
||||
// MARK: - Overrides
|
||||
//--------------------------------------------------
|
||||
/// Executed on initialization for this View.
|
||||
open override func initialSetup() {
|
||||
super.initialSetup()
|
||||
onClick = { control in
|
||||
control.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
/// 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()
|
||||
@ -156,18 +148,19 @@ open class ToggleView: Control, Changeable, FormFieldable {
|
||||
accessibilityLabel = "Toggle"
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
shouldUpdateView = false
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
isOn = false
|
||||
isAnimated = true
|
||||
inputId = nil
|
||||
toggleView.backgroundColor = toggleColorConfiguration.getColor(self)
|
||||
knobView.backgroundColor = knobColorConfiguration.getColor(self)
|
||||
onChange = nil
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
|
||||
onClick = { [weak self] _ in
|
||||
guard let self else { return }
|
||||
toggle()
|
||||
}
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
|
||||
@ -128,6 +128,16 @@ open class Tooltip: Control, TooltipLaunchable {
|
||||
|
||||
isAccessibilityElement = true
|
||||
accessibilityTraits = .button
|
||||
}
|
||||
|
||||
open override func setDefaults() {
|
||||
super.setDefaults()
|
||||
closeButtonText = "Close"
|
||||
fillColor = .primary
|
||||
size = .medium
|
||||
title = nil
|
||||
content = nil
|
||||
contentView = nil
|
||||
|
||||
onClick = { [weak self] tooltip in
|
||||
guard let self else { return}
|
||||
@ -158,18 +168,6 @@ open class Tooltip: Control, TooltipLaunchable {
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets to default settings.
|
||||
open override func reset() {
|
||||
super.reset()
|
||||
shouldUpdateView = false
|
||||
size = .medium
|
||||
title = ""
|
||||
content = ""
|
||||
fillColor = .primary
|
||||
closeButtonText = "Close"
|
||||
shouldUpdateView = true
|
||||
setNeedsUpdate()
|
||||
}
|
||||
|
||||
/// Used to make changes to the View based off a change events or from local properties.
|
||||
open override func updateView() {
|
||||
|
||||
@ -10,6 +10,7 @@ import UIKit
|
||||
import Combine
|
||||
import VDSCoreTokens
|
||||
|
||||
@objc(VDSTooltipAlertViewController)
|
||||
open class TooltipAlertViewController: UIViewController, Surfaceable {
|
||||
|
||||
/// Set of Subscribers for any Publishers for this Control.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user