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

View File

@ -7,6 +7,7 @@
import Foundation
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.
@objc(VDSSelfSizingCollectionView)
@ -34,10 +35,13 @@ public final class SelfSizingCollectionView: UICollectionView {
// MARK: - Private Properties
//--------------------------------------------------
private var contentSizeObservation: NSKeyValueObservation?
private var collectionViewHeight: NSLayoutConstraint?
private var anyCancellable: AnyCancellable?
//--------------------------------------------------
// MARK: - Overrides
//--------------------------------------------------
/// The natural size for the receiving view, considering only properties of the view itself.
public override var intrinsicContentSize: CGSize {
let contentSize = self.contentSize
@ -60,11 +64,19 @@ public final class SelfSizingCollectionView: UICollectionView {
// MARK: - Private Methods
//--------------------------------------------------
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.
self.contentSizeObservation = self.observe(\.contentSize, options: [.old, .new]) { [weak self] _, change in
// If we don't specify `options: [.old, .new]`, the change.oldValue and .newValue will always be `nil`.
if change.newValue != change.oldValue {
self?.invalidateIntrinsicContentSize()
if let height = change.newValue?.height {
self?.collectionViewHeight?.constant = height
}
}
}
}

View File

@ -102,6 +102,7 @@ open class ButtonGroup: View {
//--------------------------------------------------
// MARK: - Private Properties
//--------------------------------------------------
fileprivate lazy var positionLayout = ButtonGroupPositionLayout().with {
$0.position = .center
$0.delegate = self
@ -162,6 +163,17 @@ open class ButtonGroup: View {
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() {
super.layoutSubviews()
// Accounts for any collection size changes
@ -181,11 +193,11 @@ extension ButtonGroup: UICollectionViewDataSource, UICollectionViewDelegate {
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]
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "collectionViewCell", for: indexPath) as? ButtonGroupCollectionViewCell else { return UICollectionViewCell() }
cell.subviews.forEach { $0.removeFromSuperview() }
cell.addSubview(button)
cell.contentView.subviews.forEach { $0.removeFromSuperview() }
cell.contentView.addSubview(button)
button.pinToSuperView()
if hasDebugBorder {
cell.addDebugBorder()

View File

@ -8,7 +8,6 @@
import Foundation
import UIKit
class ButtonCollectionViewRow {
var attributes = [ButtonLayoutAttributes]()
@ -193,6 +192,9 @@ class ButtonGroupPositionLayout: UICollectionViewLayout {
// get the rect size of the button
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
let rowItemCount = rows.last?.attributes.count ?? 0
@ -229,7 +231,7 @@ class ButtonGroupPositionLayout: UICollectionViewLayout {
// create the custom layout attribute
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
rows.last?.add(attribute: attributes)

View File

@ -79,6 +79,8 @@ open class TextLinkCaret: ButtonBase {
open override func setup() {
super.setup()
accessibilityTraits = .link
titleLabel?.numberOfLines = 0
titleLabel?.lineBreakMode = .byWordWrapping
}
/// 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()
}
open override func reset() {
super.reset()
showError = false
}
}
extension CheckboxGroup {

View File

@ -8,12 +8,13 @@
import Foundation
import UIKit
import VDSTokens
import Combine
/// 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
/// exists in a group when there are several actions that can be performed.
@objc(VDSButtonIcon)
open class ButtonIcon: Control {
open class ButtonIcon: Control, Changeable, FormFieldable {
//--------------------------------------------------
// MARK: - Initializers
@ -67,17 +68,45 @@ open class ButtonIcon: Control {
private var centerYConstraint: NSLayoutConstraint?
private var layoutGuideWidthConstraint: 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? {
if let selectedIconName {
if let selectedIconName, isSelected {
return selectedIconName
} else {
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
//--------------------------------------------------
///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.
open var icon = Icon().with { $0.isUserInteractionEnabled = false }
@ -108,21 +137,39 @@ open class ButtonIcon: Control {
/// If set to true, the button icon will not have a border.
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.
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
//--------------------------------------------------
private var iconColorConfiguration: AnyColorable {
if selectedIconName != nil {
return selectedIconColorConfiguration
if kind == .highContrast {
return highContrastIconColorConfiguration
} else if kind == .lowContrast {
return (surfaceType == .colorFill) ? lowContrastIconColorConfiguration : (floating ? lowContrastIconColorConfiguration : standardIconColorConfiguration)
} else {
if kind == .highContrast {
return highContrastIconColorConfiguration
} else {
return standardIconColorConfiguration
}
return standardIconColorConfiguration
}
}
@ -147,7 +194,19 @@ open class ButtonIcon: Control {
return ControlColorConfiguration().with {
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .normal)
$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()
}()
@ -155,14 +214,6 @@ open class ButtonIcon: Control {
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 {
var kind: Kind = .ghost
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 surfaceType: SurfaceType = .colorFill
var floating: Bool = true
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 {
@ -195,11 +252,11 @@ open class ButtonIcon: Control {
var surfaceType: SurfaceType = .media
var floating: Bool = false
var backgroundColorConfiguration: AnyColorable = {
SurfaceColorConfiguration(VDSColor.backgroundPrimaryLight, .clear).eraseToAnyColorable()
SurfaceColorConfiguration(VDSColor.backgroundPrimaryLight, VDSColor.backgroundPrimaryDark).eraseToAnyColorable()
}()
var borderWidth: CGFloat = 1.0
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 floating: Bool = true
var backgroundColorConfiguration: AnyColorable = {
SurfaceColorConfiguration(VDSColor.backgroundPrimaryLight, .clear).eraseToAnyColorable()
SurfaceColorConfiguration(VDSColor.paletteWhite, VDSColor.paletteGray20).eraseToAnyColorable()
}()
var shadowColorConfiguration: AnyColorable = {
SurfaceColorConfiguration(VDSColor.paletteBlack, .clear).eraseToAnyColorable()
SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteBlack).eraseToAnyColorable()
}()
var shadowOpacity: CGFloat = 0.16
var shadowOffset: CGSize = .init(width: 0, height: 2)
var shadowRadius: CGFloat = 2
var shadowRadius: CGFloat = 4
}
private struct HighContrastConfiguration: Configuration {
@ -225,32 +282,48 @@ open class ButtonIcon: Control {
var backgroundColorConfiguration: AnyColorable = {
return ControlColorConfiguration().with {
$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.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled)
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: [.selected, .disabled])
}.eraseToAnyColorable()
}()
}
private struct HighContrastFloatingConfiguration: Configuration {
private struct HighContrastFloatingConfiguration: Configuration, Dropshadowable {
var kind: Kind = .highContrast
var surfaceType: SurfaceType = .colorFill
var floating: Bool = true
var backgroundColorConfiguration: AnyColorable = {
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.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled)
$0.setSurfaceColors(VDSColor.interactiveActiveOnlight, VDSColor.interactiveActiveOndark, forState: [.selected, .highlighted])
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: [.selected, .disabled])
}.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
//--------------------------------------------------
/// 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 = .image
accessibilityElements = [badgeIndicator]
//create a layoutGuide for the icon to key off of
let iconLayoutGuide = UILayoutGuide()
@ -259,10 +332,21 @@ open class ButtonIcon: Control {
//add the icon
addSubview(icon)
//add badgeIndicator
addSubview(badgeIndicator)
badgeIndicator.isHidden = !showBadgeIndicator
badgeIndicatorDefaultSize = badgeIndicator.frame.size
//determines the height/width of the icon
layoutGuideWidthConstraint = iconLayoutGuide.width(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
iconLayoutGuide
.pinTop()
@ -277,6 +361,24 @@ open class ButtonIcon: Control {
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.
open override func reset() {
super.reset()
@ -288,6 +390,10 @@ open class ButtonIcon: Control {
hideBorder = true
iconOffset = .init(x: 0, y: 0)
iconName = nil
selectedIconName = nil
showBadgeIndicator = false
selectable = false
badgeIndicatorModel = nil
shouldUpdateView = true
setNeedsUpdate()
}
@ -306,6 +412,7 @@ open class ButtonIcon: Control {
} else {
icon.reset()
}
updateBadgeIndicator()
setNeedsLayout()
}
@ -338,7 +445,7 @@ open class ButtonIcon: Control {
layoutGuideHeightConstraint?.constant = iconLayoutSize
//border
if let borderable = currentConfig as? Borderable {
if let borderable = currentConfig as? Borderable, hideBorder {
layer.borderColor = borderable.borderColorConfiguration.getColor(self).cgColor
layer.borderWidth = borderable.borderWidth
} else {
@ -347,11 +454,71 @@ open class ButtonIcon: Control {
}
if let dropshadowable = currentConfig as? Dropshadowable {
addDropShadow(config: dropshadowable)
addDropShadow(dropshadowable)
} 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])
}
}
@ -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 {
var borderWidth: CGFloat { 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 {
var kind: ButtonIcon.Kind { 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,10 +135,10 @@ open class Label: UILabel, ViewProtocol, UserInfoable {
override open var text: String? {
get { _text }
set {
if _text != newValue {
if _text != newValue || newValue != attributedText?.string {
_text = newValue
useAttributedText = false
attributes = nil
attributes?.removeAll()
setNeedsUpdate()
}
}

View File

@ -108,11 +108,20 @@ open class Loader: View {
rotation.repeatCount = .infinity
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
UIAccessibility.post(notification: .layoutChanged, argument: self)
// setup timer for post
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")
}
}

View File

@ -65,21 +65,29 @@ open class Notification: View {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.alignment = .top
$0.axis = .horizontal
$0.spacing = VDSLayout.space2X
$0.spacing = UIDevice.isIPad ? VDSLayout.space3X : VDSLayout.space2X
}
private var labelsView = UIStackView().with {
$0.spacing = VDSLayout.space1X
$0.translatesAutoresizingMaskIntoConstraints = false
$0.alignment = .top
$0.alignment = .fill
$0.distribution = .equalSpacing
$0.axis = .vertical
}
private var labelButtonView = UIStackView().with {
private var labelButtonView = View().with {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.alignment = .top
$0.distribution = .fillEqually
$0.axis = .vertical
$0.spacing = VDSLayout.space2X
}
private var labelButtonViewSpacing: CGFloat {
let spacing: CGFloat = UIDevice.isIPad ? 20 : 16
return switch layout {
case .vertical:
0
case .horizontal:
spacing
}
}
internal var onCloseSubscriber: AnyCancellable?
@ -165,7 +173,7 @@ open class Notification: View {
/// Add this attribute determine your type of Notification.
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.
open var layout: Layout {
@ -215,6 +223,15 @@ open class Notification: View {
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
//--------------------------------------------------
@ -240,15 +257,34 @@ open class Notification: View {
])
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(labelButtonView)
mainStackView.addArrangedSubview(closeButton)
typeIconWidthConstraint = typeIcon.width(constant: typeIcon.size.dimensions.width)
closeIconWidthConstraint = closeButton.width(constant: closeButton.size.dimensions.width)
//labels
titleLabel.textColorConfiguration = textColorConfiguration.eraseToAnyColorable()
subTitleLabel.textColorConfiguration = textColorConfiguration.eraseToAnyColorable()
//TODO: Need to add setup animation for displaying the Notification view for iOS.
}
/// Resets to default settings.
@ -296,6 +332,12 @@ open class Notification: View {
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
//--------------------------------------------------
@ -324,7 +366,6 @@ open class Notification: View {
} else {
subTitleLabel.removeFromSuperview()
}
}
private func updateButtons() {
@ -342,28 +383,28 @@ open class Notification: View {
secondaryButton.onClick = secondaryButtonModel.onClick
buttons.append(secondaryButton)
}
labelViewWidthConstraint?.deactivate()
if buttons.isEmpty {
labelsView.setCustomSpacing(0, after: subTitleLabel)
buttonGroup.removeFromSuperview()
buttonGroup.isHidden = true
labelViewWidthConstraint = labelsView.widthAnchor.constraint(equalTo: labelButtonView.widthAnchor)
buttonGroup.buttons.removeAll()
} else {
labelsView.setCustomSpacing(VDSLayout.space3X, after: subTitleLabel)
buttonGroup.buttons = buttons
labelButtonView.axis = layout == .vertical ? .vertical : .horizontal
labelButtonView.addArrangedSubview(buttonGroup)
buttonGroup
.pinLeading()
.pinTrailing()
buttonGroup.isHidden = false
labelViewWidthConstraint = labelsView.widthAnchor.constraint(equalTo: labelButtonView.widthAnchor, multiplier: layout == .vertical ? 1.0 : 0.5, constant: layout == .vertical ? 0 : -labelButtonViewSpacing)
}
labelViewWidthConstraint?.activate()
}
private func setConstraints() {
maxWidthConstraint?.constant = maxViewWidth
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) {
if let 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.
open override func setup() {
super.setup()
isAccessibilityElement = false
scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.showsHorizontalScrollIndicator = false
@ -219,6 +221,24 @@ open class Tabs: View {
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
//--------------------------------------------------
@ -244,6 +264,7 @@ open class Tabs: View {
model.onClick?(tab.index)
self.selectedIndex = tab.index
self.onTabDidSelect?(tab.index)
let t = tabViews[tab.index]
}
}
}
@ -283,7 +304,7 @@ open class Tabs: View {
tabItem.orientation = orientation
tabItem.surface = surface
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
//--------------------------------------------------
/// 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 black
case gray
case transparent
case custom(String)
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.
public enum Padding: String, CaseIterable {
public enum Padding {
case padding2X
case padding4X
case padding6X
case padding8X
case padding12X
case custom(CGFloat)
public var value: CGFloat {
switch self {
@ -58,6 +74,8 @@ open class TileContainer: Control {
return VDSLayout.space8X
case .padding12X:
return VDSLayout.space12X
case .custom(let padding):
return padding
}
}
}
@ -105,7 +123,10 @@ open class TileContainer: Control {
open var aspectRatio: AspectRatio = .ratio1x1 { didSet { setNeedsUpdate() } }
/// 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
open var padding: Padding = .padding4X { didSet { setNeedsUpdate() } }
@ -164,6 +185,7 @@ open class TileContainer: Control {
private let cornerRadius = VDSFormControls.borderRadius * 2
private var backgroundColorConfiguration = BackgroundColorConfiguration()
private var dropshadowConfiguration = DropshadowConfiguration()
private var borderColorConfiguration = SurfaceColorConfiguration().with {
$0.lightColor = VDSColor.elementsLowcontrastOnlight
@ -200,24 +222,20 @@ open class TileContainer: Control {
addSubview(highlightView)
widthConstraint = layoutGuide.widthAnchor.constraint(equalToConstant: 0)
widthConstraint?.priority = .defaultHigh
heightGreaterThanConstraint = layoutGuide.heightAnchor.constraint(greaterThanOrEqualToConstant: 44.0)
heightGreaterThanConstraint?.isActive = false
heightConstraint = layoutGuide.heightAnchor.constraint(equalToConstant: 0)
heightConstraint?.priority = .defaultHigh
backgroundImageView.pin(layoutGuide)
backgroundImageView.isUserInteractionEnabled = false
backgroundImageView.isHidden = true
containerView.backgroundColor = .clear
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)
containerTrailingConstraint = containerView.pinTrailing(anchor: layoutGuide.trailingAnchor, constant: padding.value)
containerTrailingConstraint = layoutGuide.pinTrailing(anchor: containerView.trailingAnchor, constant: padding.value)
highlightView.pin(layoutGuide)
highlightView.isHidden = true
@ -227,7 +245,6 @@ open class TileContainer: Control {
layer.cornerRadius = cornerRadius
backgroundImageView.layer.cornerRadius = cornerRadius
highlightView.layer.cornerRadius = cornerRadius
}
/// Resets to default settings.
@ -267,8 +284,8 @@ open class TileContainer: Control {
containerTopConstraint?.constant = padding.value
containerLeadingConstraint?.constant = padding.value
containerBottomConstraint?.constant = -padding.value
containerTrailingConstraint?.constant = -padding.value
containerBottomConstraint?.constant = padding.value
containerTrailingConstraint?.constant = padding.value
if let width, aspectRatio == .none && height == nil{
widthConstraint?.constant = width
@ -292,6 +309,12 @@ open class TileContainer: Control {
widthConstraint?.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
//--------------------------------------------------
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 {
var height: CGFloat = width
@ -342,22 +395,40 @@ open class TileContainer: Control {
}
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
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() { }
func getColor(_ object: TileContainer) -> UIColor {
switch object.color {
case .primary:
primaryColorConfig.getColor(object.surface)
case .secondary:
secondaryColorConfig.getColor(object.surface)
case .white:
return VDSColor.backgroundPrimaryLight
whiteColorConfig.getColor(object.surface)
case .black:
return VDSColor.backgroundPrimaryDark
case .gray:
return VDSColor.backgroundSecondaryLight
case .transparent:
return UIColor.clear
blackColorConfig.getColor(object.surface)
case .custom(let hexCode):
UIColor(hexString: hexCode)
}
}
}

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.
open var titleLabel = Label().with {
$0.setContentCompressionResistancePriority(.required, for: .vertical)
$0.accessibilityTraits.insert([.header])
}
/// Model used in rendering the title label.

View File

@ -24,4 +24,30 @@ extension UIView {
public func setAccessibilityLabel(for views: [UIView]) {
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
----------------
- ONEAPP-6239 - Loader is still showing when inactive.