Merge branch 'develop' into refactor/VDSTokens

# Conflicts:
#	VDS/Components/Buttons/ButtonGroup/ButtonGroup.swift
#	VDS/Components/Icon/ButtonIcon/ButtonIcon.swift
#	VDS/Components/Notification/Notification.swift
#	VDS/Components/TileContainer/TileContainer.swift

Signed-off-by: Matt Bruce <matt.bruce@verizon.com>
This commit is contained in:
Matt Bruce 2024-02-23 09:37:15 -06:00
commit b0f13de69b
21 changed files with 882 additions and 130 deletions

View File

@ -7,11 +7,16 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
18792A902B7431F2008C0D29 /* ButtonIconBadgeIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18792A8F2B7431F2008C0D29 /* ButtonIconBadgeIndicatorModel.swift */; };
18BDEE822B75316E00452358 /* ButtonIconChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 18BDEE812B75316E00452358 /* ButtonIconChangeLog.txt */; };
445BA07829C07B3D0036A7C5 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 445BA07729C07B3D0036A7C5 /* Notification.swift */; }; 445BA07829C07B3D0036A7C5 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 445BA07729C07B3D0036A7C5 /* Notification.swift */; };
44604AD429CE186A00E62B51 /* NotificationButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44604AD329CE186A00E62B51 /* NotificationButtonModel.swift */; }; 44604AD429CE186A00E62B51 /* NotificationButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44604AD329CE186A00E62B51 /* NotificationButtonModel.swift */; };
44604AD729CE196600E62B51 /* Line.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44604AD629CE196600E62B51 /* Line.swift */; }; 44604AD729CE196600E62B51 /* Line.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44604AD629CE196600E62B51 /* Line.swift */; };
5F21D7BF28DCEB3D003E7CD6 /* Useable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F21D7BE28DCEB3D003E7CD6 /* Useable.swift */; }; 5F21D7BF28DCEB3D003E7CD6 /* Useable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F21D7BE28DCEB3D003E7CD6 /* Useable.swift */; };
5FC35BE328D51405004EBEAC /* Button.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FC35BE228D51405004EBEAC /* Button.swift */; }; 5FC35BE328D51405004EBEAC /* Button.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FC35BE228D51405004EBEAC /* Button.swift */; };
7115BD3C2B84C0C200E0A610 /* TileContainerChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 7115BD3B2B84C0C200E0A610 /* TileContainerChangeLog.txt */; };
71BFA70A2B7F70E6000DCE33 /* Dropshadowable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71BFA7092B7F70E6000DCE33 /* Dropshadowable.swift */; };
71C02B382B7BD98F00E93E66 /* NotificationChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 71C02B372B7BD98F00E93E66 /* NotificationChangeLog.txt */; };
EA0B18022A9E236900F2D0CD /* SelectorGroupBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA0B18012A9E236900F2D0CD /* SelectorGroupBase.swift */; }; EA0B18022A9E236900F2D0CD /* SelectorGroupBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA0B18012A9E236900F2D0CD /* SelectorGroupBase.swift */; };
EA0B18052A9E2D2D00F2D0CD /* SelectorBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA0B18032A9E2D2D00F2D0CD /* SelectorBase.swift */; }; EA0B18052A9E2D2D00F2D0CD /* SelectorBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA0B18032A9E2D2D00F2D0CD /* SelectorBase.swift */; };
EA0B18062A9E2D2D00F2D0CD /* SelectorItemBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA0B18042A9E2D2D00F2D0CD /* SelectorItemBase.swift */; }; EA0B18062A9E2D2D00F2D0CD /* SelectorItemBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA0B18042A9E2D2D00F2D0CD /* SelectorItemBase.swift */; };
@ -164,11 +169,16 @@
/* End PBXContainerItemProxy section */ /* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
18792A8F2B7431F2008C0D29 /* ButtonIconBadgeIndicatorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonIconBadgeIndicatorModel.swift; sourceTree = "<group>"; };
18BDEE812B75316E00452358 /* ButtonIconChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ButtonIconChangeLog.txt; sourceTree = "<group>"; };
445BA07729C07B3D0036A7C5 /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = "<group>"; }; 445BA07729C07B3D0036A7C5 /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = "<group>"; };
44604AD329CE186A00E62B51 /* NotificationButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationButtonModel.swift; sourceTree = "<group>"; }; 44604AD329CE186A00E62B51 /* NotificationButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationButtonModel.swift; sourceTree = "<group>"; };
44604AD629CE196600E62B51 /* Line.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Line.swift; sourceTree = "<group>"; }; 44604AD629CE196600E62B51 /* Line.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Line.swift; sourceTree = "<group>"; };
5F21D7BE28DCEB3D003E7CD6 /* Useable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Useable.swift; sourceTree = "<group>"; }; 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>"; }; 5FC35BE228D51405004EBEAC /* Button.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Button.swift; sourceTree = "<group>"; };
7115BD3B2B84C0C200E0A610 /* TileContainerChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = TileContainerChangeLog.txt; sourceTree = "<group>"; };
71BFA7092B7F70E6000DCE33 /* Dropshadowable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dropshadowable.swift; sourceTree = "<group>"; };
71C02B372B7BD98F00E93E66 /* NotificationChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = NotificationChangeLog.txt; sourceTree = "<group>"; };
EA0B18012A9E236900F2D0CD /* SelectorGroupBase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectorGroupBase.swift; sourceTree = "<group>"; }; EA0B18012A9E236900F2D0CD /* SelectorGroupBase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectorGroupBase.swift; sourceTree = "<group>"; };
EA0B18032A9E2D2D00F2D0CD /* SelectorBase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectorBase.swift; sourceTree = "<group>"; }; EA0B18032A9E2D2D00F2D0CD /* SelectorBase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectorBase.swift; sourceTree = "<group>"; };
EA0B18042A9E2D2D00F2D0CD /* SelectorItemBase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectorItemBase.swift; sourceTree = "<group>"; }; EA0B18042A9E2D2D00F2D0CD /* SelectorItemBase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectorItemBase.swift; sourceTree = "<group>"; };
@ -337,6 +347,7 @@
children = ( children = (
445BA07729C07B3D0036A7C5 /* Notification.swift */, 445BA07729C07B3D0036A7C5 /* Notification.swift */,
44604AD329CE186A00E62B51 /* NotificationButtonModel.swift */, 44604AD329CE186A00E62B51 /* NotificationButtonModel.swift */,
71C02B372B7BD98F00E93E66 /* NotificationChangeLog.txt */,
); );
path = Notification; path = Notification;
sourceTree = "<group>"; sourceTree = "<group>";
@ -535,6 +546,7 @@
EA3361B7288B2AAA0071C351 /* ViewProtocol.swift */, EA3361B7288B2AAA0071C351 /* ViewProtocol.swift */,
EAB1D2CC28ABE76000DAE764 /* Withable.swift */, EAB1D2CC28ABE76000DAE764 /* Withable.swift */,
5F21D7BE28DCEB3D003E7CD6 /* Useable.swift */, 5F21D7BE28DCEB3D003E7CD6 /* Useable.swift */,
71BFA7092B7F70E6000DCE33 /* Dropshadowable.swift */,
); );
path = Protocols; path = Protocols;
sourceTree = "<group>"; sourceTree = "<group>";
@ -623,6 +635,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
EA5E304B294CBDD00082B959 /* TileContainer.swift */, EA5E304B294CBDD00082B959 /* TileContainer.swift */,
7115BD3B2B84C0C200E0A610 /* TileContainerChangeLog.txt */,
); );
path = TileContainer; path = TileContainer;
sourceTree = "<group>"; sourceTree = "<group>";
@ -657,6 +670,8 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
EA81410A2A0E8E3C004F60D2 /* ButtonIcon.swift */, EA81410A2A0E8E3C004F60D2 /* ButtonIcon.swift */,
18792A8F2B7431F2008C0D29 /* ButtonIconBadgeIndicatorModel.swift */,
18BDEE812B75316E00452358 /* ButtonIconChangeLog.txt */,
); );
path = ButtonIcon; path = ButtonIcon;
sourceTree = "<group>"; sourceTree = "<group>";
@ -913,7 +928,9 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
EAEEECA42B1F934600531FC2 /* IconChangeLog.txt in Resources */, EAEEECA42B1F934600531FC2 /* IconChangeLog.txt in Resources */,
7115BD3C2B84C0C200E0A610 /* TileContainerChangeLog.txt in Resources */,
EA3362042891E14D0071C351 /* VerizonNHGeTX-Bold.otf in Resources */, EA3362042891E14D0071C351 /* VerizonNHGeTX-Bold.otf in Resources */,
71C02B382B7BD98F00E93E66 /* NotificationChangeLog.txt in Resources */,
EAEEECA72B1F952000531FC2 /* TabsChangeLog.txt in Resources */, EAEEECA72B1F952000531FC2 /* TabsChangeLog.txt in Resources */,
EAEEEC962B1F893B00531FC2 /* ButtonChangeLog.txt in Resources */, EAEEEC962B1F893B00531FC2 /* ButtonChangeLog.txt in Resources */,
EA5F86CC2A1D28B500BC83E4 /* ReleaseNotes.txt in Resources */, EA5F86CC2A1D28B500BC83E4 /* ReleaseNotes.txt in Resources */,
@ -924,6 +941,7 @@
EAEEECAF2B1FC2BA00531FC2 /* ToggleViewChangeLog.txt in Resources */, EAEEECAF2B1FC2BA00531FC2 /* ToggleViewChangeLog.txt in Resources */,
EAEEEC922B1F807300531FC2 /* BadgeChangeLog.txt in Resources */, EAEEEC922B1F807300531FC2 /* BadgeChangeLog.txt in Resources */,
EAEEEC9E2B1F8F7700531FC2 /* ButtonGroupChangeLog.txt in Resources */, EAEEEC9E2B1F8F7700531FC2 /* ButtonGroupChangeLog.txt in Resources */,
18BDEE822B75316E00452358 /* ButtonIconChangeLog.txt in Resources */,
EA3362062891E14D0071C351 /* VerizonNHGeTX-Regular.otf in Resources */, EA3362062891E14D0071C351 /* VerizonNHGeTX-Regular.otf in Resources */,
EA3362052891E14D0071C351 /* VerizonNHGeDS-Bold.otf in Resources */, EA3362052891E14D0071C351 /* VerizonNHGeDS-Bold.otf in Resources */,
EAEEECA02B1F908200531FC2 /* BadgeIndicatorChangeLog.txt in Resources */, EAEEECA02B1F908200531FC2 /* BadgeIndicatorChangeLog.txt in Resources */,
@ -966,6 +984,7 @@
EAB2376229E9880400AABE9A /* TrailingTooltipLabel.swift in Sources */, EAB2376229E9880400AABE9A /* TrailingTooltipLabel.swift in Sources */,
EAB2376A29E9E59100AABE9A /* TooltipLaunchable.swift in Sources */, EAB2376A29E9E59100AABE9A /* TooltipLaunchable.swift in Sources */,
EAB2375D29E8789100AABE9A /* Tooltip.swift in Sources */, EAB2375D29E8789100AABE9A /* Tooltip.swift in Sources */,
71BFA70A2B7F70E6000DCE33 /* Dropshadowable.swift in Sources */,
EA0D1C452A6AD73000E5C127 /* RawRepresentable.swift in Sources */, EA0D1C452A6AD73000E5C127 /* RawRepresentable.swift in Sources */,
EA985C23296E033A00F2FF2E /* TextArea.swift in Sources */, EA985C23296E033A00F2FF2E /* TextArea.swift in Sources */,
EAF7F0B3289B1ADC00B287F5 /* ActionLabelAttribute.swift in Sources */, EAF7F0B3289B1ADC00B287F5 /* ActionLabelAttribute.swift in Sources */,
@ -1045,6 +1064,7 @@
EA5E30532950DDA60082B959 /* TitleLockup.swift in Sources */, EA5E30532950DDA60082B959 /* TitleLockup.swift in Sources */,
EAD062B02A3B873E0015965D /* BadgeIndicator.swift in Sources */, EAD062B02A3B873E0015965D /* BadgeIndicator.swift in Sources */,
EAA5EEB528ECBFB4003B3210 /* ImageLabelAttribute.swift in Sources */, EAA5EEB528ECBFB4003B3210 /* ImageLabelAttribute.swift in Sources */,
18792A902B7431F2008C0D29 /* ButtonIconBadgeIndicatorModel.swift in Sources */,
EA0B18062A9E2D2D00F2D0CD /* SelectorItemBase.swift in Sources */, EA0B18062A9E2D2D00F2D0CD /* SelectorItemBase.swift in Sources */,
EAB5FF0129424ACB00998C17 /* UIControl.swift in Sources */, EAB5FF0129424ACB00998C17 /* UIControl.swift in Sources */,
EA985BF5296C60C000F2FF2E /* Icon.swift in Sources */, EA985BF5296C60C000F2FF2E /* Icon.swift in Sources */,
@ -1217,7 +1237,7 @@
BUILD_LIBRARY_FOR_DISTRIBUTION = YES; BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
CODE_SIGN_IDENTITY = ""; CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 51; CURRENT_PROJECT_VERSION = 54;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
@ -1254,7 +1274,7 @@
BUILD_LIBRARY_FOR_DISTRIBUTION = YES; BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
CODE_SIGN_IDENTITY = ""; CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 51; CURRENT_PROJECT_VERSION = 54;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;

View File

@ -7,6 +7,7 @@
import Foundation import Foundation
import UIKit import UIKit
import Combine
/// UICollectionView subclassed to deal with Changing the size of itself based on its children and layout and changes of its contentSize. /// UICollectionView subclassed to deal with Changing the size of itself based on its children and layout and changes of its contentSize.
@objc(VDSSelfSizingCollectionView) @objc(VDSSelfSizingCollectionView)
@ -34,10 +35,13 @@ public final class SelfSizingCollectionView: UICollectionView {
// MARK: - Private Properties // MARK: - Private Properties
//-------------------------------------------------- //--------------------------------------------------
private var contentSizeObservation: NSKeyValueObservation? private var contentSizeObservation: NSKeyValueObservation?
private var collectionViewHeight: NSLayoutConstraint?
private var anyCancellable: AnyCancellable?
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Overrides // MARK: - Overrides
//-------------------------------------------------- //--------------------------------------------------
/// The natural size for the receiving view, considering only properties of the view itself. /// The natural size for the receiving view, considering only properties of the view itself.
public override var intrinsicContentSize: CGSize { public override var intrinsicContentSize: CGSize {
let contentSize = self.contentSize let contentSize = self.contentSize
@ -55,16 +59,24 @@ public final class SelfSizingCollectionView: UICollectionView {
self.collectionViewLayout.invalidateLayout() self.collectionViewLayout.invalidateLayout()
} }
} }
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Private Methods // MARK: - Private Methods
//-------------------------------------------------- //--------------------------------------------------
private func setupContentSizeObservation() { private func setupContentSizeObservation() {
//ensure autoLayout uses intrinsic height
setContentHuggingPriority(.required, for: .vertical)
setContentCompressionResistancePriority(.required, for: .vertical)
collectionViewHeight = heightAnchor.constraint(equalToConstant: 0).activate()
// Observing the value of contentSize seems to be the only reliable way to get the contentSize after the collection view lays out its subviews. // Observing the value of contentSize seems to be the only reliable way to get the contentSize after the collection view lays out its subviews.
self.contentSizeObservation = self.observe(\.contentSize, options: [.old, .new]) { [weak self] _, change in self.contentSizeObservation = self.observe(\.contentSize, options: [.old, .new]) { [weak self] _, change in
// If we don't specify `options: [.old, .new]`, the change.oldValue and .newValue will always be `nil`. // If we don't specify `options: [.old, .new]`, the change.oldValue and .newValue will always be `nil`.
if change.newValue != change.oldValue { if change.newValue != change.oldValue {
self?.invalidateIntrinsicContentSize() self?.invalidateIntrinsicContentSize()
if let height = change.newValue?.height {
self?.collectionViewHeight?.constant = height
}
} }
} }
} }

View File

@ -62,7 +62,7 @@ open class ButtonGroup: View {
open var buttons: [ButtonBase] = [] { didSet { setNeedsUpdate() } } open var buttons: [ButtonBase] = [] { didSet { setNeedsUpdate() } }
private var _childWidth: ChildWidth? private var _childWidth: ChildWidth?
/// If provided, width of Button components will be rendered based on this value. If omitted, default button widths are rendered. /// If provided, width of Button components will be rendered based on this value. If omitted, default button widths are rendered.
open var childWidth: ChildWidth? { open var childWidth: ChildWidth? {
get { _childWidth } get { _childWidth }
@ -98,10 +98,11 @@ open class ButtonGroup: View {
buttons.forEach { $0.surface = surface } buttons.forEach { $0.surface = surface }
} }
} }
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Private Properties // MARK: - Private Properties
//-------------------------------------------------- //--------------------------------------------------
fileprivate lazy var positionLayout = ButtonGroupPositionLayout().with { fileprivate lazy var positionLayout = ButtonGroupPositionLayout().with {
$0.position = .center $0.position = .center
$0.delegate = self $0.delegate = self
@ -162,6 +163,17 @@ open class ButtonGroup: View {
collectionView.reloadData() collectionView.reloadData()
} }
open override func reset() {
super.reset()
shouldUpdateView = false
rowQuantityPhone = 0
rowQuantityTablet = 0
alignment = .center
buttons.forEach { $0.reset() }
shouldUpdateView = true
setNeedsUpdate()
}
open override func layoutSubviews() { open override func layoutSubviews() {
super.layoutSubviews() super.layoutSubviews()
// Accounts for any collection size changes // Accounts for any collection size changes
@ -181,11 +193,11 @@ extension ButtonGroup: UICollectionViewDataSource, UICollectionViewDelegate {
return buttons.count return buttons.count
} }
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let button = buttons[indexPath.row] let button = buttons[indexPath.row]
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "collectionViewCell", for: indexPath) as? ButtonGroupCollectionViewCell else { return UICollectionViewCell() } guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "collectionViewCell", for: indexPath) as? ButtonGroupCollectionViewCell else { return UICollectionViewCell() }
cell.subviews.forEach { $0.removeFromSuperview() } cell.contentView.subviews.forEach { $0.removeFromSuperview() }
cell.addSubview(button) cell.contentView.addSubview(button)
button.pinToSuperView() button.pinToSuperView()
if hasDebugBorder { if hasDebugBorder {
cell.addDebugBorder() cell.addDebugBorder()

View File

@ -8,7 +8,6 @@
import Foundation import Foundation
import UIKit import UIKit
class ButtonCollectionViewRow { class ButtonCollectionViewRow {
var attributes = [ButtonLayoutAttributes]() var attributes = [ButtonLayoutAttributes]()
@ -193,6 +192,9 @@ class ButtonGroupPositionLayout: UICollectionViewLayout {
// get the rect size of the button // get the rect size of the button
itemSize = delegate.collectionView(collectionView, sizeForItemAtIndexPath: indexPath) itemSize = delegate.collectionView(collectionView, sizeForItemAtIndexPath: indexPath)
// ensure the width is not greater than the collectionViewWidth
itemSize.width = min(itemSize.width, collectionViewWidth)
// determine if the current button will fit in the row // determine if the current button will fit in the row
let rowItemCount = rows.last?.attributes.count ?? 0 let rowItemCount = rows.last?.attributes.count ?? 0
@ -229,7 +231,7 @@ class ButtonGroupPositionLayout: UICollectionViewLayout {
// create the custom layout attribute // create the custom layout attribute
let attributes = ButtonLayoutAttributes(spacing: itemSpacing, button: itemButtonBase, forCellWith: indexPath) let attributes = ButtonLayoutAttributes(spacing: itemSpacing, button: itemButtonBase, forCellWith: indexPath)
attributes.frame = CGRect(x: 0, y: 0, width: itemSize.width, height: itemSize.height) attributes.frame = CGRect(x: 0, y: 0, width: min(itemSize.width, collectionViewWidth), height: itemSize.height)
// add it to the array // add it to the array
rows.last?.add(attribute: attributes) rows.last?.add(attribute: attributes)

View File

@ -79,6 +79,8 @@ open class TextLinkCaret: ButtonBase {
open override func setup() { open override func setup() {
super.setup() super.setup()
accessibilityTraits = .link accessibilityTraits = .link
titleLabel?.numberOfLines = 0
titleLabel?.lineBreakMode = .byWordWrapping
} }
/// Used to make changes to the View based off a change events or from local properties. /// Used to make changes to the View based off a change events or from local properties.

View File

@ -89,6 +89,11 @@ open class CheckboxGroup: SelectorGroupBase<CheckboxItem>, SelectorGroupMultiSel
} }
valueChanged() valueChanged()
} }
open override func reset() {
super.reset()
showError = false
}
} }
extension CheckboxGroup { extension CheckboxGroup {

View File

@ -8,12 +8,13 @@
import Foundation import Foundation
import UIKit import UIKit
import VDSTokens import VDSTokens
import Combine
/// A button icon is an interactive element that visually communicates the action it triggers via an icon. /// A button icon is an interactive element that visually communicates the action it triggers via an icon.
/// It usually represents a supplementary or utilitarian action. A button icon can stand alone, but often /// 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. /// exists in a group when there are several actions that can be performed.
@objc(VDSButtonIcon) @objc(VDSButtonIcon)
open class ButtonIcon: Control { open class ButtonIcon: Control, Changeable, FormFieldable {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Initializers // MARK: - Initializers
@ -42,7 +43,7 @@ open class ButtonIcon: Control {
public enum SurfaceType: String, CaseIterable { public enum SurfaceType: String, CaseIterable {
case colorFill, media case colorFill, media
} }
/// Enum used to describe the size of button icon. /// Enum used to describe the size of button icon.
public enum Size: String, EnumSubset { public enum Size: String, EnumSubset {
case large case large
@ -67,17 +68,45 @@ open class ButtonIcon: Control {
private var centerYConstraint: NSLayoutConstraint? private var centerYConstraint: NSLayoutConstraint?
private var layoutGuideWidthConstraint: NSLayoutConstraint? private var layoutGuideWidthConstraint: NSLayoutConstraint?
private var layoutGuideHeightConstraint: NSLayoutConstraint? private var layoutGuideHeightConstraint: NSLayoutConstraint?
private var badgeIndicatorLeadingConstraint: NSLayoutConstraint?
private var badgeIndicatorTrailingConstraint: NSLayoutConstraint?
private var badgeIndicatorCenterXConstraint: NSLayoutConstraint?
private var badgeIndicatorCenterYConstraint: NSLayoutConstraint?
private var currentIconName: Icon.Name? { private var currentIconName: Icon.Name? {
if let selectedIconName { if let selectedIconName, isSelected {
return selectedIconName return selectedIconName
} else { } else {
return iconName return iconName
} }
} }
private var badgeIndicatorOffset: CGPoint {
switch (size, kind) {
case (.small, .ghost):
return .init(x: 1, y: 0)
case (.large, .ghost):
return .init(x: 1, y: 1)
case (.small, .lowContrast), (.small, .highContrast):
return .init(x: 4, y: 4)
case (.large, .lowContrast), (.large, .highContrast):
return .init(x: 6, y: 6)
}
}
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Public Properties // MARK: - Public Properties
//-------------------------------------------------- //--------------------------------------------------
///Badge Indicator object used to render for the ButtonIcon.
open var badgeIndicator = BadgeIndicator().with {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.size = .small
$0.isHidden = true
}
/// If set, this is used to render the badge indicator.
open var badgeIndicatorModel: BadgeIndicatorModel? { didSet { setNeedsUpdate() } }
/// Icon object used to render out the Icon for this ButtonIcon. /// Icon object used to render out the Icon for this ButtonIcon.
open var icon = Icon().with { $0.isUserInteractionEnabled = false } open var icon = Icon().with { $0.isUserInteractionEnabled = false }
@ -107,22 +136,40 @@ open class ButtonIcon: Control {
/// If set to true, the button icon will not have a border. /// If set to true, the button icon will not have a border.
open var hideBorder: Bool = true { didSet { setNeedsUpdate() } } open var hideBorder: Bool = true { didSet { setNeedsUpdate() } }
/// If provided, the badge indicator will present.
open var showBadgeIndicator: Bool = false { didSet { setNeedsUpdate() } }
/// If true, button will be rendered as selected, when it is selectable.
open var selectable: Bool = false {
didSet {
//unselect
if !selectable && isSelected {
isSelected = false
}
setNeedsUpdate()
}
}
/// Used to move the icon inside the button in both x and y axis. /// Used to move the icon inside the button in both x and y axis.
open var iconOffset: CGPoint = .init(x: 0, y: 0) { didSet { setNeedsUpdate() } } open var iconOffset: CGPoint = .init(x: 0, y: 0) { didSet { setNeedsUpdate() } }
open var onChangeSubscriber: AnyCancellable?
open var inputId: String? { didSet { setNeedsUpdate() } }
open var value: AnyHashable? { didSet { setNeedsUpdate() } }
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Configuration // MARK: - Configuration
//-------------------------------------------------- //--------------------------------------------------
private var iconColorConfiguration: AnyColorable { private var iconColorConfiguration: AnyColorable {
if selectedIconName != nil { if kind == .highContrast {
return selectedIconColorConfiguration return highContrastIconColorConfiguration
} else if kind == .lowContrast {
return (surfaceType == .colorFill) ? lowContrastIconColorConfiguration : (floating ? lowContrastIconColorConfiguration : standardIconColorConfiguration)
} else { } else {
if kind == .highContrast { return standardIconColorConfiguration
return highContrastIconColorConfiguration
} else {
return standardIconColorConfiguration
}
} }
} }
@ -147,7 +194,19 @@ open class ButtonIcon: Control {
return ControlColorConfiguration().with { return ControlColorConfiguration().with {
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .normal) $0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .normal)
$0.setSurfaceColors(VDSColor.interactiveActiveOnlight, VDSColor.interactiveActiveOndark, forState: .highlighted) $0.setSurfaceColors(VDSColor.interactiveActiveOnlight, VDSColor.interactiveActiveOndark, forState: .highlighted)
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled) $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled)
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .selected)
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: [.selected, .disabled])
}.eraseToAnyColorable()
}()
private var lowContrastIconColorConfiguration: AnyColorable = {
return ControlColorConfiguration().with {
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .normal)
$0.setSurfaceColors(VDSColor.interactiveActiveOnlight, VDSColor.interactiveActiveOndark, forState: .highlighted)
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.paletteBlack.withAlphaComponent(0.70), forState: .disabled)
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .selected)
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.paletteBlack.withAlphaComponent(0.70), forState: [.selected, .disabled])
}.eraseToAnyColorable() }.eraseToAnyColorable()
}() }()
@ -155,14 +214,6 @@ open class ButtonIcon: Control {
return SurfaceColorConfiguration(VDSColor.elementsPrimaryOndark, VDSColor.elementsPrimaryOnlight).eraseToAnyColorable() return SurfaceColorConfiguration(VDSColor.elementsPrimaryOndark, VDSColor.elementsPrimaryOnlight).eraseToAnyColorable()
}() }()
private var selectedIconColorConfiguration: AnyColorable = {
return ControlColorConfiguration().with {
$0.setSurfaceColors(VDSColor.elementsBrandhighlight, VDSColor.elementsPrimaryOndark, forState: .normal)
$0.setSurfaceColors(VDSColor.interactiveActiveOnlight, VDSColor.interactiveActiveOndark, forState: .highlighted)
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled)
}.eraseToAnyColorable()
}()
private struct GhostConfiguration: Configuration { private struct GhostConfiguration: Configuration {
var kind: Kind = .ghost var kind: Kind = .ghost
var surfaceType: SurfaceType = .colorFill var surfaceType: SurfaceType = .colorFill
@ -181,13 +232,19 @@ open class ButtonIcon: Control {
}() }()
} }
private struct LowContrastColorFillFloatingConfiguration: Configuration { private struct LowContrastColorFillFloatingConfiguration: Configuration, Dropshadowable {
var kind: Kind = .lowContrast var kind: Kind = .lowContrast
var surfaceType: SurfaceType = .colorFill var surfaceType: SurfaceType = .colorFill
var floating: Bool = true var floating: Bool = true
var backgroundColorConfiguration: AnyColorable = { var backgroundColorConfiguration: AnyColorable = {
SurfaceColorConfiguration(VDSColor.backgroundPrimaryLight, .clear).eraseToAnyColorable() SurfaceColorConfiguration(VDSColor.paletteWhite, VDSColor.paletteGray20).eraseToAnyColorable()
}() }()
var shadowColorConfiguration: AnyColorable = {
SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteBlack).eraseToAnyColorable()
}()
var shadowOpacity: CGFloat = 0.16
var shadowOffset: CGSize = .init(width: 0, height: 2)
var shadowRadius: CGFloat = 4
} }
private struct LowContrastMediaConfiguration: Configuration, Borderable { private struct LowContrastMediaConfiguration: Configuration, Borderable {
@ -195,11 +252,11 @@ open class ButtonIcon: Control {
var surfaceType: SurfaceType = .media var surfaceType: SurfaceType = .media
var floating: Bool = false var floating: Bool = false
var backgroundColorConfiguration: AnyColorable = { var backgroundColorConfiguration: AnyColorable = {
SurfaceColorConfiguration(VDSColor.backgroundPrimaryLight, .clear).eraseToAnyColorable() SurfaceColorConfiguration(VDSColor.backgroundPrimaryLight, VDSColor.backgroundPrimaryDark).eraseToAnyColorable()
}() }()
var borderWidth: CGFloat = 1.0 var borderWidth: CGFloat = 1.0
var borderColorConfiguration: AnyColorable = { var borderColorConfiguration: AnyColorable = {
SurfaceColorConfiguration(VDSColor.elementsLowcontrastOnlight, .clear).eraseToAnyColorable() SurfaceColorConfiguration(VDSColor.elementsLowcontrastOnlight, VDSColor.elementsLowcontrastOndark).eraseToAnyColorable()
}() }()
} }
@ -208,14 +265,14 @@ open class ButtonIcon: Control {
var surfaceType: SurfaceType = .media var surfaceType: SurfaceType = .media
var floating: Bool = true var floating: Bool = true
var backgroundColorConfiguration: AnyColorable = { var backgroundColorConfiguration: AnyColorable = {
SurfaceColorConfiguration(VDSColor.backgroundPrimaryLight, .clear).eraseToAnyColorable() SurfaceColorConfiguration(VDSColor.paletteWhite, VDSColor.paletteGray20).eraseToAnyColorable()
}() }()
var shadowColorConfiguration: AnyColorable = { var shadowColorConfiguration: AnyColorable = {
SurfaceColorConfiguration(VDSColor.paletteBlack, .clear).eraseToAnyColorable() SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteBlack).eraseToAnyColorable()
}() }()
var shadowOpacity: CGFloat = 0.16 var shadowOpacity: CGFloat = 0.16
var shadowOffset: CGSize = .init(width: 0, height: 2) var shadowOffset: CGSize = .init(width: 0, height: 2)
var shadowRadius: CGFloat = 2 var shadowRadius: CGFloat = 4
} }
private struct HighContrastConfiguration: Configuration { private struct HighContrastConfiguration: Configuration {
@ -225,33 +282,49 @@ open class ButtonIcon: Control {
var backgroundColorConfiguration: AnyColorable = { var backgroundColorConfiguration: AnyColorable = {
return ControlColorConfiguration().with { return ControlColorConfiguration().with {
$0.setSurfaceColors(VDSColor.backgroundPrimaryDark, VDSColor.backgroundPrimaryLight, forState: .normal) $0.setSurfaceColors(VDSColor.backgroundPrimaryDark, VDSColor.backgroundPrimaryLight, forState: .normal)
$0.setSurfaceColors(VDSColor.backgroundPrimaryDark, VDSColor.backgroundPrimaryLight, forState: .selected)
$0.setSurfaceColors(VDSColor.interactiveActiveOnlight, VDSColor.interactiveActiveOndark, forState: .highlighted) $0.setSurfaceColors(VDSColor.interactiveActiveOnlight, VDSColor.interactiveActiveOndark, forState: .highlighted)
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled) $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled)
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: [.selected, .disabled])
}.eraseToAnyColorable() }.eraseToAnyColorable()
}() }()
} }
private struct HighContrastFloatingConfiguration: Configuration { private struct HighContrastFloatingConfiguration: Configuration, Dropshadowable {
var kind: Kind = .highContrast var kind: Kind = .highContrast
var surfaceType: SurfaceType = .colorFill var surfaceType: SurfaceType = .colorFill
var floating: Bool = true var floating: Bool = true
var backgroundColorConfiguration: AnyColorable = { var backgroundColorConfiguration: AnyColorable = {
return ControlColorConfiguration().with { return ControlColorConfiguration().with {
$0.setSurfaceColors(VDSColor.backgroundPrimaryLight, VDSColor.backgroundPrimaryLight, forState: .normal) $0.setSurfaceColors(VDSColor.paletteGray20, VDSColor.paletteWhite, forState: .normal)
$0.setSurfaceColors(VDSColor.paletteGray20, VDSColor.paletteWhite, forState: .selected)
$0.setSurfaceColors(VDSColor.interactiveActiveOnlight, VDSColor.interactiveActiveOndark, forState: .highlighted) $0.setSurfaceColors(VDSColor.interactiveActiveOnlight, VDSColor.interactiveActiveOndark, forState: .highlighted)
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled) $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled)
$0.setSurfaceColors(VDSColor.interactiveActiveOnlight, VDSColor.interactiveActiveOndark, forState: [.selected, .highlighted])
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: [.selected, .disabled])
}.eraseToAnyColorable() }.eraseToAnyColorable()
}() }()
var shadowColorConfiguration: AnyColorable = {
SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteBlack).eraseToAnyColorable()
}()
var shadowOpacity: CGFloat = 0.16
var shadowOffset: CGSize = .init(width: 0, height: 2)
var shadowRadius: CGFloat = 6
} }
private var badgeIndicatorDefaultSize: CGSize = .zero
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Overrides // MARK: - Overrides
//-------------------------------------------------- //--------------------------------------------------
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations. /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
open override func setup() { open override func setup() {
super.setup() super.setup()
isAccessibilityElement = true
accessibilityTraits = .image
accessibilityElements = [badgeIndicator]
//create a layoutGuide for the icon to key off of //create a layoutGuide for the icon to key off of
let iconLayoutGuide = UILayoutGuide() let iconLayoutGuide = UILayoutGuide()
addLayoutGuide(iconLayoutGuide) addLayoutGuide(iconLayoutGuide)
@ -259,10 +332,21 @@ open class ButtonIcon: Control {
//add the icon //add the icon
addSubview(icon) addSubview(icon)
//add badgeIndicator
addSubview(badgeIndicator)
badgeIndicator.isHidden = !showBadgeIndicator
badgeIndicatorDefaultSize = badgeIndicator.frame.size
//determines the height/width of the icon //determines the height/width of the icon
layoutGuideWidthConstraint = iconLayoutGuide.width(constant: size.containerSize) layoutGuideWidthConstraint = iconLayoutGuide.width(constant: size.containerSize)
layoutGuideHeightConstraint = iconLayoutGuide.height(constant: size.containerSize) layoutGuideHeightConstraint = iconLayoutGuide.height(constant: size.containerSize)
badgeIndicatorLeadingConstraint = badgeIndicator.leadingAnchor.constraint(equalTo: icon.centerXAnchor)
badgeIndicatorTrailingConstraint = badgeIndicator.trailingAnchor.constraint(equalTo: icon.centerXAnchor)
badgeIndicatorCenterXConstraint = badgeIndicator.centerXAnchor.constraint(equalTo: icon.centerXAnchor)
badgeIndicatorCenterYConstraint = icon.centerYAnchor.constraint(equalTo: badgeIndicator.centerYAnchor)
badgeIndicatorCenterYConstraint?.isActive = true
badgeIndicatorLeadingConstraint?.isActive = true
//pin layout guide //pin layout guide
iconLayoutGuide iconLayoutGuide
.pinTop() .pinTop()
@ -277,6 +361,24 @@ open class ButtonIcon: Control {
centerYConstraint?.activate() centerYConstraint?.activate()
} }
/// 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()
}
}
}
/// This will change the state of the Selector and execute the actionBlock if provided.
open func toggle() {
//removed error
isSelected.toggle()
sendActions(for: .valueChanged)
}
/// Resets to default settings. /// Resets to default settings.
open override func reset() { open override func reset() {
super.reset() super.reset()
@ -288,6 +390,10 @@ open class ButtonIcon: Control {
hideBorder = true hideBorder = true
iconOffset = .init(x: 0, y: 0) iconOffset = .init(x: 0, y: 0)
iconName = nil iconName = nil
selectedIconName = nil
showBadgeIndicator = false
selectable = false
badgeIndicatorModel = nil
shouldUpdateView = true shouldUpdateView = true
setNeedsUpdate() setNeedsUpdate()
} }
@ -306,6 +412,7 @@ open class ButtonIcon: Control {
} else { } else {
icon.reset() icon.reset()
} }
updateBadgeIndicator()
setNeedsLayout() setNeedsLayout()
} }
@ -338,7 +445,7 @@ open class ButtonIcon: Control {
layoutGuideHeightConstraint?.constant = iconLayoutSize layoutGuideHeightConstraint?.constant = iconLayoutSize
//border //border
if let borderable = currentConfig as? Borderable { if let borderable = currentConfig as? Borderable, hideBorder {
layer.borderColor = borderable.borderColorConfiguration.getColor(self).cgColor layer.borderColor = borderable.borderColorConfiguration.getColor(self).cgColor
layer.borderWidth = borderable.borderWidth layer.borderWidth = borderable.borderWidth
} else { } else {
@ -347,14 +454,74 @@ open class ButtonIcon: Control {
} }
if let dropshadowable = currentConfig as? Dropshadowable { if let dropshadowable = currentConfig as? Dropshadowable {
addDropShadow(config: dropshadowable) addDropShadow(dropshadowable)
} else { } else {
removeDropShadow() removeDropShadows()
} }
badgeIndicatorCenterXConstraint?.constant = badgeIndicatorOffset.x + badgeIndicatorDefaultSize.width/2
badgeIndicatorCenterYConstraint?.constant = badgeIndicatorOffset.y + badgeIndicatorDefaultSize.height/2
badgeIndicatorLeadingConstraint?.constant = badgeIndicatorOffset.x
badgeIndicatorTrailingConstraint?.constant = badgeIndicatorOffset.x + badgeIndicatorDefaultSize.width
if showBadgeIndicator {
updateExpandDirectionalConstraints()
}
}
//--------------------------------------------------
// MARK: - Private Methods
//--------------------------------------------------
private func updateBadgeIndicator() {
badgeIndicator.isHidden = !showBadgeIndicator
guard let badgeIndicatorModel else {
badgeIndicator.isHidden = true
return
}
badgeIndicator.surface = surface
badgeIndicator.kind = badgeIndicatorModel.kind
badgeIndicator.fillColor = badgeIndicatorModel.fillColor
badgeIndicator.size = badgeIndicatorModel.size
badgeIndicator.maximumDigits = badgeIndicatorModel.maximumDigits
badgeIndicator.width = badgeIndicatorModel.width
badgeIndicator.height = badgeIndicatorModel.height
badgeIndicator.number = badgeIndicatorModel.number
badgeIndicator.leadingCharacter = badgeIndicatorModel.leadingCharacter
badgeIndicator.trailingText = badgeIndicatorModel.trailingText
badgeIndicator.dotSize = badgeIndicatorModel.dotSize
badgeIndicator.verticalPadding = badgeIndicatorModel.verticalPadding
badgeIndicator.horizontalPadding = badgeIndicatorModel.horizontalPadding
badgeIndicator.hideDot = badgeIndicatorModel.hideDot
badgeIndicator.hideBorder = badgeIndicatorModel.hideBorder
}
private func updateExpandDirectionalConstraints() {
guard let badgeIndicatorModel else { return }
switch badgeIndicatorModel.expandDirection {
case .right:
badgeIndicatorLeadingConstraint?.isActive = true
badgeIndicatorTrailingConstraint?.isActive = false
badgeIndicatorCenterXConstraint?.isActive = false
case .center:
badgeIndicatorLeadingConstraint?.isActive = false
badgeIndicatorTrailingConstraint?.isActive = false
badgeIndicatorCenterXConstraint?.isActive = true
case .left:
badgeIndicatorLeadingConstraint?.isActive = false
badgeIndicatorCenterXConstraint?.isActive = false
badgeIndicatorTrailingConstraint?.isActive = true
}
}
/// Used to update any Accessibility properties.
open override func updateAccessibility() {
super.updateAccessibility()
setAccessibilityLabel(for: [icon, badgeIndicator.label])
} }
} }
// MARK: AppleGuidelinesTouchable // MARK: AppleGuidelinesTouchable
extension ButtonIcon: AppleGuidelinesTouchable { extension ButtonIcon: AppleGuidelinesTouchable {
/// Overrides to ensure that the touch point meets a minimum of the minimumTappableArea. /// Overrides to ensure that the touch point meets a minimum of the minimumTappableArea.
@ -363,37 +530,11 @@ extension ButtonIcon: AppleGuidelinesTouchable {
} }
} }
extension UIView {
fileprivate func addDropShadow(config: Dropshadowable) {
layer.masksToBounds = false
layer.shadowColor = config.shadowColorConfiguration.getColor(self).cgColor
layer.shadowOpacity = Float(config.shadowOpacity)
layer.shadowOffset = config.shadowOffset
layer.shadowRadius = config.shadowRadius
layer.shouldRasterize = true
layer.rasterizationScale = UIScreen.main.scale
layer.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius).cgPath
}
fileprivate func removeDropShadow() {
layer.shadowOpacity = 0
layer.shadowRadius = 0
layer.shadowPath = nil
}
}
private protocol Borderable { private protocol Borderable {
var borderWidth: CGFloat { get set } var borderWidth: CGFloat { get set }
var borderColorConfiguration: AnyColorable { get set } var borderColorConfiguration: AnyColorable { get set }
} }
private protocol Dropshadowable {
var shadowColorConfiguration: AnyColorable { get set }
var shadowOpacity: CGFloat { get set }
var shadowOffset: CGSize { get set }
var shadowRadius: CGFloat { get set }
}
private protocol Configuration { private protocol Configuration {
var kind: ButtonIcon.Kind { get set } var kind: ButtonIcon.Kind { get set }
var surfaceType: ButtonIcon.SurfaceType { get set } var surfaceType: ButtonIcon.SurfaceType { get set }

View File

@ -0,0 +1,82 @@
//
// ButtonIconBadgeIndicatorModel.swift
// VDS
//
// Created by Kanamarlapudi, Vasavi on 08/02/24.
//
import Foundation
extension ButtonIcon {
//Model that represents the options available for the Badge Indicator
public struct BadgeIndicatorModel {
/// Enum used to describe the badge indicator direction of icon button determining the expand direction.
public enum ExpandDirection: String, CaseIterable {
case right, center, left
}
/// Applies expand direction to Badge Indicator if shows badge indicator.
public var expandDirection: ExpandDirection = .right
/// Kind that will be used for the badge indicator.
public var kind: BadgeIndicator.Kind
/// Fill color that will be used for the badge indicator.
public var fillColor: BadgeIndicator.FillColor
/// Size that will be used for the badge indicator.
public var size: BadgeIndicator.Size
/// Number of digits that will be used for the badge indicator.
public var maximumDigits: BadgeIndicator.MaximumDigits
/// Max width that will be used for the badge indicator.
public var width: CGFloat?
/// Max height that will be used for the badge indicator.
public var height: CGFloat?
/// Number that will be used for the badge indicator.
public var number: Int?
/// Leading Character that will be used for the badge indicator.
public var leadingCharacter: String?
/// Trailing Text height that will be used for the badge indicator.
public var trailingText: String?
/// Dot Size that will be used for the badge indicator.
public var dotSize: CGFloat?
/// Vertical Padding that will be used for the badge indicator.
public var verticalPadding: CGFloat?
/// Horizontal Padding that will be used for the badge indicator.
public var horizontalPadding: CGFloat?
/// Hide Dot that will be used for the badge indicator.
public var hideDot: Bool = false
/// Hide Border that will be used for the badge indicator.
public var hideBorder: Bool = false
public init(kind: BadgeIndicator.Kind = .simple, fillColor: BadgeIndicator.FillColor = .red, expandDirection: ExpandDirection = .right, size: BadgeIndicator.Size = .xxlarge, maximumDigits: BadgeIndicator.MaximumDigits = .two, width: CGFloat? = nil, height: CGFloat? = nil, number: Int? = nil, leadingCharacter: String = "", trailingText: String = "", dotSize: CGFloat? = nil, verticalPadding: CGFloat? = nil, horizontalPadding: CGFloat? = nil, hideDot: Bool = false, hideBorder: Bool = false) {
self.kind = kind
self.fillColor = fillColor
self.expandDirection = expandDirection
self.size = size
self.maximumDigits = maximumDigits
self.width = width
self.height = height
self.number = number
self.leadingCharacter = leadingCharacter
self.trailingText = trailingText
self.dotSize = dotSize
self.verticalPadding = verticalPadding
self.horizontalPadding = horizontalPadding
self.hideDot = hideDot
self.hideBorder = hideBorder
}
}
}

View File

@ -0,0 +1,79 @@
03/30/2023
----------------
- Dev handoff
04/06/2023
----------------
- Changed component name to Button Icon.
04/13/2023
----------------
- Dev handoff (additional states)
04/20/2023
----------------
- Added Selectable and Selected properties in Configuration.
05/01/2023
----------------
- Added fitToIcon prop for Ghost variation.
05/03/2023
----------------
- Updated drop shadows for light and dark floating buttons (all states)
- Removed border from all floating buttons
- Updated all dark floating button backgrounds to gray.20
- Disabled icons on gray.20 changed to palette.black 70% opacity for floating buttons
- Reduce Inside focusborderPosition to 1px
05/25/2023
----------------
- Added hideBorder prop back to Configurations
- Added showBadgeIndicator prop to Configurations
- Added Hit Area support for Button Icon with Badge Indicator
- Added Elements page for Badge Indicator with default settings and offset information
- Updated Badge Indicator Offset to be based on center of Icon container
05/30/2023
----------------
- Added expandDirection prop to Configurations under Badge Indicator section.
06/02/2023
----------------
- Added Additional Border Color specification if hideBorder=True on Low Contrast hover states.
06/12/2023
----------------
- Ghost Icon Cart position updated from {1,2} to {1,1}.
06/13/2023
----------------
- Updated Drop shadow properties layout in States.
06/29/2023
----------------
- Updated Badge Indicator simple dot size to 4px.
08/18/2023
----------------
- Updated default icon color for all selected Button Icon states to element.primary.onlight for light surfaces and element.primary.ondark for dark surfaces.
- Added a dev note that this default color can be set to a custom color (like red).
11/16/2023
----------------
- Added component tokens
- Applied component tokens throughout states
- Applied semantic inverse to primary element, background states on light and dark surfaces
12/1/2023
----------------
- Reapplied component token updates to Ghost States
1/9/2024
----------------
- Fixed incorrect Low Contrast border token
1/25/2024
----------------
- Removed redundant opacity specs in States (dark surface).

View File

@ -135,15 +135,15 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
override open var text: String? { override open var text: String? {
get { _text } get { _text }
set { set {
if _text != newValue { if _text != newValue || newValue != attributedText?.string {
_text = newValue _text = newValue
useAttributedText = false useAttributedText = false
attributes = nil attributes?.removeAll()
setNeedsUpdate() setNeedsUpdate()
} }
} }
} }
/// Whether the View is enabled or not. /// Whether the View is enabled or not.
open override var isEnabled: Bool { didSet { setNeedsUpdate() } } open override var isEnabled: Bool { didSet { setNeedsUpdate() } }

View File

@ -107,16 +107,25 @@ open class Loader: View {
rotation.duration = 0.5 rotation.duration = 0.5
rotation.repeatCount = .infinity rotation.repeatCount = .infinity
icon.layer.add(rotation, forKey: rotationLayerName) icon.layer.add(rotation, forKey: rotationLayerName)
// check to make sure VoiceOver is running
guard UIAccessibility.isVoiceOverRunning else {
loadingTimer?.invalidate()
loadingTimer = nil
return
}
// Focus VoiceOver on this view // Focus VoiceOver on this view
UIAccessibility.post(notification: .layoutChanged, argument: self) UIAccessibility.post(notification: .layoutChanged, argument: self)
// setup timer for post
loadingTimer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) { [weak self] _ in loadingTimer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) { [weak self] _ in
self?.accessibilityLabel = "Still Loading" guard let self, self.isActive, self.isVisibleOnScreen else { return }
self.accessibilityLabel = "Still Loading"
UIAccessibility.post(notification: .announcement, argument: "Still Loading") UIAccessibility.post(notification: .announcement, argument: "Still Loading")
} }
} }
private func stopAnimating() { private func stopAnimating() {
isAccessibilityElement = false isAccessibilityElement = false
icon.isHidden = true icon.isHidden = true
@ -124,7 +133,7 @@ open class Loader: View {
loadingTimer?.invalidate() loadingTimer?.invalidate()
loadingTimer = nil loadingTimer = nil
} }
deinit { deinit {
stopAnimating() stopAnimating()
} }

View File

@ -65,21 +65,29 @@ open class Notification: View {
$0.translatesAutoresizingMaskIntoConstraints = false $0.translatesAutoresizingMaskIntoConstraints = false
$0.alignment = .top $0.alignment = .top
$0.axis = .horizontal $0.axis = .horizontal
$0.spacing = VDSLayout.space2X $0.spacing = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space2X
} }
private var labelsView = UIStackView().with { private var labelsView = UIStackView().with {
$0.spacing = VDSLayout.space1X
$0.translatesAutoresizingMaskIntoConstraints = false $0.translatesAutoresizingMaskIntoConstraints = false
$0.alignment = .top $0.alignment = .fill
$0.distribution = .equalSpacing
$0.axis = .vertical $0.axis = .vertical
} }
private var labelButtonView = UIStackView().with { private var labelButtonView = View().with {
$0.translatesAutoresizingMaskIntoConstraints = false $0.translatesAutoresizingMaskIntoConstraints = false
$0.alignment = .top }
$0.distribution = .fillEqually
$0.axis = .vertical private var labelButtonViewSpacing: CGFloat {
$0.spacing = VDSLayout.space2X let spacing: CGFloat = UIDevice.isIPad ? 20 : 16
return switch layout {
case .vertical:
0
case .horizontal:
spacing
}
} }
internal var onCloseSubscriber: AnyCancellable? internal var onCloseSubscriber: AnyCancellable?
@ -164,8 +172,8 @@ open class Notification: View {
/// Add this attribute determine your type of Notification. /// Add this attribute determine your type of Notification.
open var style: Style = .info { didSet { setNeedsUpdate()}} open var style: Style = .info { didSet { setNeedsUpdate()}}
var _layout: Layout = .vertical private var _layout: Layout = .vertical
/// Determines the orientation of buttons and text in the Notification. /// Determines the orientation of buttons and text in the Notification.
open var layout: Layout { open var layout: Layout {
@ -215,6 +223,15 @@ open class Notification: View {
return 1232 return 1232
} }
private var labelViewWidthConstraint: NSLayoutConstraint?
private var labelViewBottomConstraint: NSLayoutConstraint?
private var labelViewAndButtonViewConstraint: NSLayoutConstraint?
private var buttonViewTopConstraint: NSLayoutConstraint?
private var typeIconWidthConstraint: NSLayoutConstraint?
private var closeIconWidthConstraint: NSLayoutConstraint?
private var buttonGroupCenterYConstraint: NSLayoutConstraint?
private var buttonGroupBottomConstraint: NSLayoutConstraint?
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Overrides // MARK: - Overrides
//-------------------------------------------------- //--------------------------------------------------
@ -240,15 +257,34 @@ open class Notification: View {
]) ])
maxWidthConstraint = layoutGuide.widthAnchor.constraint(lessThanOrEqualToConstant: maxViewWidth) maxWidthConstraint = layoutGuide.widthAnchor.constraint(lessThanOrEqualToConstant: maxViewWidth)
labelButtonView.addArrangedSubview(labelsView) labelButtonView.addSubview(labelsView)
labelsView
.pinTop()
.pinLeading()
labelViewWidthConstraint = labelsView.widthAnchor.constraint(equalTo: labelButtonView.widthAnchor, multiplier: 1.0)
labelViewWidthConstraint?.activate()
labelViewBottomConstraint = labelButtonView.bottomAnchor.constraint(equalTo: labelsView.bottomAnchor)
labelButtonView.addSubview(buttonGroup)
buttonGroup
.pinTrailing()
buttonGroupBottomConstraint = labelButtonView.bottomAnchor.constraint(equalTo: buttonGroup.bottomAnchor)
buttonGroupCenterYConstraint = buttonGroup.centerYAnchor.constraint(equalTo: labelButtonView.centerYAnchor)
labelViewAndButtonViewConstraint = buttonGroup.topAnchor.constraint(equalTo: labelsView.bottomAnchor, constant: VDSLayout.space3X)
buttonGroup.widthAnchor.constraint(equalTo: labelsView.widthAnchor).activate()
mainStackView.addArrangedSubview(typeIcon) mainStackView.addArrangedSubview(typeIcon)
mainStackView.addArrangedSubview(labelButtonView) mainStackView.addArrangedSubview(labelButtonView)
mainStackView.addArrangedSubview(closeButton) mainStackView.addArrangedSubview(closeButton)
typeIconWidthConstraint = typeIcon.width(constant: typeIcon.size.dimensions.width)
closeIconWidthConstraint = closeButton.width(constant: closeButton.size.dimensions.width)
//labels //labels
titleLabel.textColorConfiguration = textColorConfiguration.eraseToAnyColorable() titleLabel.textColorConfiguration = textColorConfiguration.eraseToAnyColorable()
subTitleLabel.textColorConfiguration = textColorConfiguration.eraseToAnyColorable() subTitleLabel.textColorConfiguration = textColorConfiguration.eraseToAnyColorable()
//TODO: Need to add setup animation for displaying the Notification view for iOS.
} }
/// Resets to default settings. /// Resets to default settings.
@ -296,6 +332,12 @@ open class Notification: View {
setConstraints() setConstraints()
} }
/// Override to check the screen width to determine cornerRadius
open override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = UIScreen.main.bounds.width == bounds.width ? 0 : 4.0
}
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Private Methods // MARK: - Private Methods
//-------------------------------------------------- //--------------------------------------------------
@ -324,7 +366,6 @@ open class Notification: View {
} else { } else {
subTitleLabel.removeFromSuperview() subTitleLabel.removeFromSuperview()
} }
} }
private func updateButtons() { private func updateButtons() {
@ -342,28 +383,28 @@ open class Notification: View {
secondaryButton.onClick = secondaryButtonModel.onClick secondaryButton.onClick = secondaryButtonModel.onClick
buttons.append(secondaryButton) buttons.append(secondaryButton)
} }
labelViewWidthConstraint?.deactivate()
if buttons.isEmpty { if buttons.isEmpty {
labelsView.setCustomSpacing(0, after: subTitleLabel) buttonGroup.isHidden = true
buttonGroup.removeFromSuperview() labelViewWidthConstraint = labelsView.widthAnchor.constraint(equalTo: labelButtonView.widthAnchor)
buttonGroup.buttons.removeAll()
} else { } else {
labelsView.setCustomSpacing(VDSLayout.space3X, after: subTitleLabel) labelsView.setCustomSpacing(VDSLayout.space3X, after: subTitleLabel)
buttonGroup.buttons = buttons buttonGroup.buttons = buttons
labelButtonView.axis = layout == .vertical ? .vertical : .horizontal buttonGroup.isHidden = false
labelButtonView.addArrangedSubview(buttonGroup) labelViewWidthConstraint = labelsView.widthAnchor.constraint(equalTo: labelButtonView.widthAnchor, multiplier: layout == .vertical ? 1.0 : 0.5, constant: layout == .vertical ? 0 : -labelButtonViewSpacing)
buttonGroup
.pinLeading()
.pinTrailing()
} }
labelViewWidthConstraint?.activate()
} }
private func setConstraints() { private func setConstraints() {
maxWidthConstraint?.constant = maxViewWidth maxWidthConstraint?.constant = maxViewWidth
maxWidthConstraint?.isActive = UIDevice.isIPad maxWidthConstraint?.isActive = UIDevice.isIPad
labelViewAndButtonViewConstraint?.isActive = layout == .vertical && !buttonGroup.buttons.isEmpty
typeIconWidthConstraint?.constant = typeIcon.size.dimensions.width
closeIconWidthConstraint?.constant = closeButton.size.dimensions.width
labelViewBottomConstraint?.isActive = layout == .horizontal || buttonGroup.buttons.isEmpty
buttonGroupCenterYConstraint?.isActive = layout == .horizontal
buttonGroupBottomConstraint?.isActive = layout == .vertical
} }
} }

View File

@ -0,0 +1,55 @@
MM/DD/YYYY
----------------
12/30/2021
----------------
- Updated Hover and Active state trigger specs. If triggered by mouse, Active same as Hover. If not, Active same as Default.
03/03/2022
----------------
- Initial Brand 3.0 handoff
03/07/2022
----------------
- Added Native positioning examples
05/03/2022
----------------
- Finalized Native Positioning and Triggers sections.
07/28/2022
----------------
- Added note to Anatomy that Border radius is only for Inline Notifications.
11/30/2022
----------------
- Added "(web only)" to any instance of "keyboard focus"
12/13/2022
----------------
- Replaced focus border pixel and style & spacing values with tokens.
01/12/2023
----------------
- Change VDS Button to Button in AnatomyMoved Button combinations from Anatomy to Elements.
04/12/2023
----------------
- Updated hex colors to reflect updated color tokens values.
- Updated visuals on the Native frame to reflect new guideline that the notification surface color should be based on whatever it is placed upon, or based on the color of the top navigation bar when displaying above the global nav. Added spec notation about this also.
05/04/2023
----------------
- Button Icon Threading - CloseButton
- Anatomy section updated with info on Button Icon
- CloseButton updated with VDS Button Icon details
- Updated Viewport section with Button Icon Config
- Removed Elements section. Button combination is now part of Configurations section
- Updated visuals in all sections to cater to changes caused by Button Icon threading
11/21/2023
----------------
- Added hideCloseButton property to align with React component build
- Added dev note to Error variant
- Removed close button from Error variant examples
- Removed Web/App Discrepancies list

View File

@ -73,6 +73,11 @@ open class RadioButtonGroup: SelectorGroupBase<RadioButtonItem>, SelectorGroupSi
} }
} }
open override func reset() {
super.reset()
showError = false
}
public override func didSelect(_ selectedControl: RadioButtonItem) { public override func didSelect(_ selectedControl: RadioButtonItem) {
if let selectedItem { if let selectedItem {
updateToggle(selectedItem) updateToggle(selectedItem)

View File

@ -171,6 +171,8 @@ open class Tabs: View {
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations. /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
open override func setup() { open override func setup() {
super.setup() super.setup()
isAccessibilityElement = false
scrollView = UIScrollView() scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.showsHorizontalScrollIndicator = false scrollView.showsHorizontalScrollIndicator = false
@ -219,6 +221,24 @@ open class Tabs: View {
updateContentView() updateContentView()
} }
open override func reset() {
super.reset()
shouldUpdateView = false
orientation = .horizontal
borderLine = true
fillContainer = false
indicatorFillTab = false
indicatorPosition = .bottom
minWidth = 44.0
overflow = .scroll
selectedIndex = 0
size = .medium
sticky = false
tabViews.forEach{ $0.reset() }
shouldUpdateView = true
setNeedsUpdate()
}
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Private Methods // MARK: - Private Methods
//-------------------------------------------------- //--------------------------------------------------
@ -244,6 +264,7 @@ open class Tabs: View {
model.onClick?(tab.index) model.onClick?(tab.index)
self.selectedIndex = tab.index self.selectedIndex = tab.index
self.onTabDidSelect?(tab.index) self.onTabDidSelect?(tab.index)
let t = tabViews[tab.index]
} }
} }
} }
@ -283,7 +304,7 @@ open class Tabs: View {
tabItem.orientation = orientation tabItem.orientation = orientation
tabItem.surface = surface tabItem.surface = surface
tabItem.indicatorPosition = indicatorPosition tabItem.indicatorPosition = indicatorPosition
tabItem.accessibilityHint = "\(index+1) of \(tabViews.count) Tabs" tabItem.accessibilityValue = "\(index+1) of \(tabViews.count) Tabs"
} }
} }

View File

@ -31,20 +31,36 @@ open class TileContainer: Control {
// MARK: - Enums // MARK: - Enums
//-------------------------------------------------- //--------------------------------------------------
/// Enum used to describe the background color choices used for this component. /// Enum used to describe the background color choices used for this component.
public enum BackgroundColor: String, CaseIterable { public enum BackgroundColor: Equatable {
case primary
case secondary
case white case white
case black case black
case gray case custom(String)
case transparent
private var reflectedValue: String { String(reflecting: self) }
public static func == (lhs: Self, rhs: Self) -> Bool {
lhs.reflectedValue == rhs.reflectedValue
}
} }
/// Enum used to describe the background effect choices used for this component.
public enum BackgroundEffect {
case transparency
case gradient(String, String)
case none
}
/// Enum used to describe the padding choices used for this component. /// Enum used to describe the padding choices used for this component.
public enum Padding: String, CaseIterable { public enum Padding {
case padding2X case padding2X
case padding4X case padding4X
case padding6X case padding6X
case padding8X case padding8X
case padding12X case padding12X
case custom(CGFloat)
public var value: CGFloat { public var value: CGFloat {
switch self { switch self {
@ -58,6 +74,8 @@ open class TileContainer: Control {
return VDSLayout.space8X return VDSLayout.space8X
case .padding12X: case .padding12X:
return VDSLayout.space12X return VDSLayout.space12X
case .custom(let padding):
return padding
} }
} }
} }
@ -105,8 +123,11 @@ open class TileContainer: Control {
open var aspectRatio: AspectRatio = .ratio1x1 { didSet { setNeedsUpdate() } } open var aspectRatio: AspectRatio = .ratio1x1 { didSet { setNeedsUpdate() } }
/// Sets the background color for the component. /// Sets the background color for the component.
open var color: BackgroundColor = .white { didSet { setNeedsUpdate() } } open var color: BackgroundColor = .secondary { didSet { setNeedsUpdate() } }
/// Sets the background effect for the component.
open var backgroundEffect: BackgroundEffect = .none { didSet { setNeedsUpdate() } }
/// Sets the inside padding for the component /// Sets the inside padding for the component
open var padding: Padding = .padding4X { didSet { setNeedsUpdate() } } open var padding: Padding = .padding4X { didSet { setNeedsUpdate() } }
@ -164,6 +185,7 @@ open class TileContainer: Control {
private let cornerRadius = VDSFormControls.borderRadius * 2 private let cornerRadius = VDSFormControls.borderRadius * 2
private var backgroundColorConfiguration = BackgroundColorConfiguration() private var backgroundColorConfiguration = BackgroundColorConfiguration()
private var dropshadowConfiguration = DropshadowConfiguration()
private var borderColorConfiguration = SurfaceColorConfiguration().with { private var borderColorConfiguration = SurfaceColorConfiguration().with {
$0.lightColor = VDSColor.elementsLowcontrastOnlight $0.lightColor = VDSColor.elementsLowcontrastOnlight
@ -200,24 +222,20 @@ open class TileContainer: Control {
addSubview(highlightView) addSubview(highlightView)
widthConstraint = layoutGuide.widthAnchor.constraint(equalToConstant: 0) widthConstraint = layoutGuide.widthAnchor.constraint(equalToConstant: 0)
widthConstraint?.priority = .defaultHigh
heightGreaterThanConstraint = layoutGuide.heightAnchor.constraint(greaterThanOrEqualToConstant: 44.0) heightGreaterThanConstraint = layoutGuide.heightAnchor.constraint(greaterThanOrEqualToConstant: 44.0)
heightGreaterThanConstraint?.isActive = false heightGreaterThanConstraint?.isActive = false
heightConstraint = layoutGuide.heightAnchor.constraint(equalToConstant: 0) heightConstraint = layoutGuide.heightAnchor.constraint(equalToConstant: 0)
heightConstraint?.priority = .defaultHigh
backgroundImageView.pin(layoutGuide) backgroundImageView.pin(layoutGuide)
backgroundImageView.isUserInteractionEnabled = false backgroundImageView.isUserInteractionEnabled = false
backgroundImageView.isHidden = true backgroundImageView.isHidden = true
containerView.backgroundColor = .clear
containerTopConstraint = containerView.pinTop(anchor: layoutGuide.topAnchor, constant: padding.value) containerTopConstraint = containerView.pinTop(anchor: layoutGuide.topAnchor, constant: padding.value)
containerBottomConstraint = containerView.pinBottom(anchor: layoutGuide.bottomAnchor, constant: padding.value) containerBottomConstraint = layoutGuide.pinBottom(anchor: containerView.bottomAnchor, constant: padding.value)
containerLeadingConstraint = containerView.pinLeading(anchor: layoutGuide.leadingAnchor, constant: padding.value) containerLeadingConstraint = containerView.pinLeading(anchor: layoutGuide.leadingAnchor, constant: padding.value)
containerTrailingConstraint = containerView.pinTrailing(anchor: layoutGuide.trailingAnchor, constant: padding.value) containerTrailingConstraint = layoutGuide.pinTrailing(anchor: containerView.trailingAnchor, constant: padding.value)
highlightView.pin(layoutGuide) highlightView.pin(layoutGuide)
highlightView.isHidden = true highlightView.isHidden = true
@ -227,7 +245,6 @@ open class TileContainer: Control {
layer.cornerRadius = cornerRadius layer.cornerRadius = cornerRadius
backgroundImageView.layer.cornerRadius = cornerRadius backgroundImageView.layer.cornerRadius = cornerRadius
highlightView.layer.cornerRadius = cornerRadius highlightView.layer.cornerRadius = cornerRadius
} }
/// Resets to default settings. /// Resets to default settings.
@ -267,8 +284,8 @@ open class TileContainer: Control {
containerTopConstraint?.constant = padding.value containerTopConstraint?.constant = padding.value
containerLeadingConstraint?.constant = padding.value containerLeadingConstraint?.constant = padding.value
containerBottomConstraint?.constant = -padding.value containerBottomConstraint?.constant = padding.value
containerTrailingConstraint?.constant = -padding.value containerTrailingConstraint?.constant = padding.value
if let width, aspectRatio == .none && height == nil{ if let width, aspectRatio == .none && height == nil{
widthConstraint?.constant = width widthConstraint?.constant = width
@ -292,6 +309,12 @@ open class TileContainer: Control {
widthConstraint?.isActive = false widthConstraint?.isActive = false
heightConstraint?.isActive = false heightConstraint?.isActive = false
} }
if showDropShadows, surface == .light {
addDropShadow(dropshadowConfiguration)
} else {
removeDropShadows()
}
applyBackgroundEffects()
} }
//-------------------------------------------------- //--------------------------------------------------
@ -309,6 +332,36 @@ open class TileContainer: Control {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Private Methods // MARK: - Private Methods
//-------------------------------------------------- //--------------------------------------------------
private func applyBackgroundEffects() {
let color = backgroundColorConfiguration.getColor(self)
var alphaConfiguration: CGFloat = 1.0
let imageFallbackColor = imageFallbackColorConfiguration.getColor(self)
switch backgroundEffect {
case .transparency:
alphaConfiguration = 0.8
removeGradientLayer()
case .gradient(let firstColor, let secondColor):
alphaConfiguration = 1.0
addGradientLayer(with: UIColor(hexString: firstColor), secondColor: UIColor(hexString: secondColor))
backgroundImageView.isHidden = true
backgroundImageView.alpha = 1.0
case .none:
alphaConfiguration = 1.0
removeGradientLayer()
}
if let backgroundImage {
backgroundImageView.image = backgroundImage
backgroundImageView.isHidden = false
backgroundImageView.alpha = alphaConfiguration
backgroundColor = imageFallbackColor.withAlphaComponent(alphaConfiguration)
} else {
backgroundImageView.isHidden = true
backgroundImageView.alpha = 1.0
backgroundColor = color.withAlphaComponent(alphaConfiguration)
}
}
private func ratioSize(for width: CGFloat) -> CGSize { private func ratioSize(for width: CGFloat) -> CGSize {
var height: CGFloat = width var height: CGFloat = width
@ -342,22 +395,40 @@ open class TileContainer: Control {
} }
extension TileContainer { extension TileContainer {
class BackgroundColorConfiguration: ObjectColorable {
struct DropshadowConfiguration: Dropshadowable {
var shadowColorConfiguration: AnyColorable = SurfaceColorConfiguration().with {
$0.lightColor = VDSColor.elementsPrimaryOnlight
}.eraseToAnyColorable()
var shadowOpacity: CGFloat = 0.01
var shadowOffset: CGSize = .init(width: 0, height: 6)
var shadowRadius: CGFloat = 3
}
final class BackgroundColorConfiguration: ObjectColorable {
typealias ObjectType = TileContainer typealias ObjectType = TileContainer
let primaryColorConfig = SurfaceColorConfiguration(VDSColor.backgroundPrimaryLight, VDSColor.backgroundPrimaryDark)
let secondaryColorConfig = SurfaceColorConfiguration(VDSColor.backgroundSecondaryLight, VDSColor.backgroundSecondaryDark)
let grayColorConfig = SurfaceColorConfiguration(VDSColor.backgroundSecondaryLight, VDSColor.backgroundSecondaryDark)
let whiteColorConfig = SurfaceColorConfiguration(VDSColor.paletteWhite, VDSColor.paletteWhite)
let blackColorConfig = SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteBlack)
required init() { } required init() { }
func getColor(_ object: TileContainer) -> UIColor { func getColor(_ object: TileContainer) -> UIColor {
switch object.color { switch object.color {
case .primary:
primaryColorConfig.getColor(object.surface)
case .secondary:
secondaryColorConfig.getColor(object.surface)
case .white: case .white:
return VDSColor.backgroundPrimaryLight whiteColorConfig.getColor(object.surface)
case .black: case .black:
return VDSColor.backgroundPrimaryDark blackColorConfig.getColor(object.surface)
case .gray: case .custom(let hexCode):
return VDSColor.backgroundSecondaryLight UIColor(hexString: hexCode)
case .transparent:
return UIColor.clear
} }
} }
} }

View File

@ -0,0 +1,90 @@
MM/DD/YYYY
----------------
02/01/2022
----------------
- ACTION | Migrated Spec file from working file into VDS Brand 3.0 Core SPECs & Test App.
02/02/2022
----------------
- Elements | Added option for user to manually define a custom Padding valueElements.
- Elements | Removed option for 40px Padding
- Elements | Added background options of Hex code and Transparency.
02/07/2022
----------------
- Anatomy | Updated descriptions to simplify. (Removed “Tile” from many)
02/08/2022
----------------
- Elements | Background color section updated
- Elements | Removed option for 20px Padding value
- Configurations | Surface section added
- Configurations | Multiple sections updated
- Behaviors | Interaction section added
- States | Multiple sections updated/moved
- ACTION | Sent to Accessibility Team to review
02/14/2022
----------------
- ACTION | Received sign off from Accessibility
- ACTION | Sent to Talia for design review
02/21/2022
----------------
- Elements | Background colors and tokens updated.
- ACTION | Ready for dev review.
02/22/2022
----------------
- Elements | Background names updated to infer surface prop selection.
- ACTION | Web dev handoff completed.
12/15/2022
----------------
- States | Android drop shadow specs added, along with screenshot to the right of specs.
- States | Added "(web only)" to any instance of "keyboard focus".
- States | Replaced focus border pixel and style & spacing values with tokens.
- Elements | Updated border color values to use element tokens.
- Configurations | Updated border and drop shadow section titles to “Show border” and “Show drop shadow.”
01/18/2023
----------------
- Anatomy | Updated item #2 to “Padding” from “Container Internal Padding”
05/11/2023
----------------
- Removed showdropshadow prop from Configurations (dropshadow will be on automatically now for Surface=Light)
- Updated states frame to remove states featuring dropshadow suppression, clarified state names, and removed inaccurate dev notes.
06/15/2023
----------------
- Added showDropShadow prop back into Configurations.
- Moved Padding to Configurations
11/09/2023
----------------
- Updated showBorder section to match API prop names/values.
- Moved Padding to Configurations.
11/20/2023
----------------
- Added corner radius token in the Anatomy
- Updated visuals to reflect new corner radius value - 12px
- Updated focus border corner radius to 14px
- View changes
11/27/2023
----------------
- Updated “border radius” to “corner radius” in Anatomy
- Updated “focus border radius” to “focus corner radius” in States
- View changes
12/14/2023
----------------
- Added backgroundColor configuration section, removed Background Colors element section
- Added secondary, primary backgroundColor options
- Simplified backgroundImage section to remove backgroundColor example
- Added background property section, with examples
- Deprecated the gray backgroundColor option
- View changes

View File

@ -77,6 +77,7 @@ open class TitleLockup: View {
/// Label used to render the title model. /// Label used to render the title model.
open var titleLabel = Label().with { open var titleLabel = Label().with {
$0.setContentCompressionResistancePriority(.required, for: .vertical) $0.setContentCompressionResistancePriority(.required, for: .vertical)
$0.accessibilityTraits.insert([.header])
} }
/// Model used in rendering the title label. /// Model used in rendering the title label.

View File

@ -24,4 +24,30 @@ extension UIView {
public func setAccessibilityLabel(for views: [UIView]) { public func setAccessibilityLabel(for views: [UIView]) {
accessibilityLabel = combineAccessibilityLabel(for: views) accessibilityLabel = combineAccessibilityLabel(for: views)
} }
/// Will tell if the view is actually visibile on screen, also it will check the hierarchy above this view.
public var isVisibleOnScreen: Bool {
// Ensure the view has a window, meaning it's part of the view hierarchy
guard let window = self.window, !self.isHidden, self.alpha > 0 else {
return false
}
// Check if the view's frame intersects with the window's bounds
let viewFrameInWindow = self.convert(self.bounds, to: window)
var isIntersecting = viewFrameInWindow.intersects(window.bounds)
// Check parent views for visibility
var currentView: UIView? = self
while let view = currentView, isIntersecting {
// If any parent has a constraint making it effectively invisible, set isIntersecting to false
if view.bounds.size.width == 0 || view.bounds.size.height == 0 {
isIntersecting = false
break
}
currentView = view.superview
}
return isIntersecting
}
} }

View File

@ -0,0 +1,64 @@
//
// Dropshadowable.swift
// VDS
//
// Created by Bandaru, Krishna Kishore on 16/02/24.
//
import Foundation
import UIKit
protocol Dropshadowable {
var shadowColorConfiguration: AnyColorable { get set }
var shadowOpacity: CGFloat { get set }
var shadowOffset: CGSize { get set }
var shadowRadius: CGFloat { get set }
}
extension ViewProtocol where Self: UIView {
func addDropShadow(_ config: Dropshadowable) {
removeDropShadows()
layer.backgroundColor = backgroundColor?.cgColor
layer.masksToBounds = false
let shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius)
let shadowLayer = CALayer()
shadowLayer.shadowPath = shadowPath.cgPath
shadowLayer.frame = bounds
shadowLayer.position = center
shadowLayer.backgroundColor = UIColor.clear.cgColor
shadowLayer.cornerRadius = layer.cornerRadius
shadowLayer.shadowColor = config.shadowColorConfiguration.getColor(self).cgColor
shadowLayer.shadowOpacity = Float(config.shadowOpacity)
shadowLayer.shadowOffset = .init(width: config.shadowOffset.width, height: config.shadowOffset.height)
shadowLayer.shadowRadius = config.shadowRadius
shadowLayer.name = "dropShadowLayer"
shadowLayer.shouldRasterize = true
shadowLayer.rasterizationScale = UIScreen.main.scale
layer.insertSublayer(shadowLayer, at: 0)
}
func removeDropShadows() {
layer.sublayers?.removeAll { $0.name == "dropShadowLayer" }
}
func addGradientLayer(with firstColor: UIColor, secondColor: UIColor) {
removeGradientLayer()
let gradientLayer = CAGradientLayer()
gradientLayer.frame = bounds
gradientLayer.startPoint = CGPoint(x: 0, y: 1)
gradientLayer.endPoint = CGPoint(x: 1, y: 0)
gradientLayer.position = center
gradientLayer.shouldRasterize = true
gradientLayer.rasterizationScale = UIScreen.main.scale
gradientLayer.cornerRadius = layer.cornerRadius
gradientLayer.colors = [firstColor.cgColor, secondColor.cgColor]
gradientLayer.name = "gradientLayer"
layer.insertSublayer(gradientLayer, at: 0)
}
func removeGradientLayer() {
layer.sublayers?.removeAll { $0.name == "gradientLayer" }
}
}

View File

@ -1,3 +1,17 @@
1.0.54
----------------
- CXTDT-518373 Accessibility Voiceover is reading “Still Loading” after waiting for a short time in all the screens.
- ONEAPP-6358 - Notification - Finished Development
- ONEAPP-6315- ButtonIcon - Finished Development
1.0.53
----------------
- ONEAPP-4683 - Updated to accesibilityValue for the tabs position.
1.0.52
----------------
- ONEAPP-6244 - TitleLockup TitleLabel marked as .heading for Accessibility.
1.0.51 1.0.51
---------------- ----------------
- ONEAPP-6239 - Loader is still showing when inactive. - ONEAPP-6239 - Loader is still showing when inactive.