Merged PR 203007: MSBadgeView with MSBadgeBaseView

This PR introduces MSBadgeView with MSBadgeBaseView to represent a "badge". This badge will be used in an upcoming field to represent selected personas from the MSPersonaListView. The badge can be selected with a tap and then tapped again once in a selected state to reveal more details about the selected person. It includes 3 styles: default, error, and warning with a disabled state as well.

Also included in this PR:
-  A small update to the accessibility for MSAvatarView to become an accessibility element so details about the avatar can be used in VoiceOver when the avatar view is used outside of a MSPersonaCell.
- An update to Physical colors in MSColors to include dark yellow colors for 'warning' and 'light warning'. The previous red colors used for 'warning' and 'light warning' are now named 'error' and 'lightError' respectively.

![Screen Shot 2018-11-14 at 3.56.30 PM.png](https://onedrive.visualstudio.com/4dcbf0bc-c3cd-49c8-a7c3-ec1924691d9b/_apis/git/repositories/93ac71ee-b53a-4fc6-a8c4-d46a80d4ca39/pullRequests/203007/attachments/Screen%20Shot%202018-11-14%20at%203.56.30%20PM.png)

Related work items: #627343
This commit is contained in:
Phil Worthington 2018-11-20 21:10:21 +00:00
Родитель a7c3bdb2db
Коммит 515a798eb5
10 изменённых файлов: 447 добавлений и 8 удалений

Просмотреть файл

@ -7,6 +7,7 @@ import UIKit
// Register your control demos here
let demos: [(title: String, controllerClass: UIViewController.Type)] = [
("MSAvatarView", MSAvatarViewDemoController.self),
("MSBadgeView", MSBadgeViewDemoController.self),
("MSDatePicker", MSDatePickerDemoController.self),
("MSDrawerController", MSDrawerDemoController.self),
("MSLabel", MSLabelDemoController.self),

Просмотреть файл

@ -0,0 +1,56 @@
//
// Copyright © 2018 Microsoft Corporation. All rights reserved.
//
import OfficeUIFabric
import UIKit
class MSBadgeViewDemoController: DemoController {
override func viewDidLoad() {
super.viewDidLoad()
container.alignment = .leading
addTitle(text: "Default badge")
addBadge(text: "Kat Larrson", style: .default)
container.addArrangedSubview(UIView())
addTitle(text: "Error badge")
addBadge(text: "Allan Munger", style: .error)
container.addArrangedSubview(UIView())
addTitle(text: "Warning badge")
addBadge(text: "Mona Kane", style: .warning)
container.addArrangedSubview(UIView())
addTitle(text: "Disabled badge")
addBadge(text: "Mauricio August", style: .default, isEnabled: false)
}
func addTitle(text: String) {
let titleLabel = MSLabel(style: .subhead, colorStyle: .regular)
titleLabel.text = text
container.addArrangedSubview(titleLabel)
}
func addBadge(text: String, style: MSBadgeViewStyle, isEnabled: Bool = true) {
let data = MSBadgeViewDataSource(text: text, style: style)
let badge = MSBadgeView()
badge.dataSource = data
badge.delegate = self
badge.isEnabled = isEnabled
container.addArrangedSubview(badge)
}
}
extension MSBadgeViewDemoController: MSBadgeBaseViewDelegate {
func didSelectBadge(_ badge: MSBadgeBaseView) { }
func didTapSelectedBadge(_ badge: MSBadgeBaseView) {
badge.isSelected = false
let alert = UIAlertController(title: "A selected badge was tapped", message: nil, preferredStyle: .alert)
let action = UIAlertAction(title: "OK", style: .default)
alert.addAction(action)
present(alert, animated: true)
}
}

Просмотреть файл

@ -47,6 +47,8 @@ extension MSTextColorStyle {
return "White"
case .primary:
return "Primary"
case .error:
return "Error"
case .warning:
return "Warning"
}

Просмотреть файл

@ -41,9 +41,11 @@
B42661422148568800E25423 /* MSAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B42661412148568800E25423 /* MSAvatarView.swift */; };
B444D6AD218126C90002B4D4 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = B444D6AC218126C90002B4D4 /* Localizable.stringsdict */; };
B444D6B12181403C0002B4D4 /* UITableViewCell+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B444D6B02181403C0002B4D4 /* UITableViewCell+Extension.swift */; };
B444D6B62183A9740002B4D4 /* MSBadgeBaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B444D6B52183A9740002B4D4 /* MSBadgeBaseView.swift */; };
B46D3F91215056940029772C /* CharacterSet+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B46D3F90215056940029772C /* CharacterSet+Extension.swift */; };
B46D3F932151D95F0029772C /* MSPersonaCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B46D3F922151D95F0029772C /* MSPersonaCell.swift */; };
B46D3F9D215985AC0029772C /* MSPersonaListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B46D3F9C215985AC0029772C /* MSPersonaListView.swift */; };
B487EA7321911FD4005E90B3 /* MSBadgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B487EA7221911FD4005E90B3 /* MSBadgeView.swift */; };
B4E782C12176AD5E00A7DFCE /* MSActionsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4E782C02176AD5E00A7DFCE /* MSActionsCell.swift */; };
B4E782C321793AB200A7DFCE /* MSActivityIndicatorCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4E782C221793AB200A7DFCE /* MSActivityIndicatorCell.swift */; };
B4E782C521793BB900A7DFCE /* MSActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4E782C421793BB900A7DFCE /* MSActivityIndicatorView.swift */; };
@ -127,9 +129,11 @@
B42661412148568800E25423 /* MSAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSAvatarView.swift; sourceTree = "<group>"; };
B444D6AC218126C90002B4D4 /* Localizable.stringsdict */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; path = Localizable.stringsdict; sourceTree = "<group>"; };
B444D6B02181403C0002B4D4 /* UITableViewCell+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableViewCell+Extension.swift"; sourceTree = "<group>"; };
B444D6B52183A9740002B4D4 /* MSBadgeBaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSBadgeBaseView.swift; sourceTree = "<group>"; };
B46D3F90215056940029772C /* CharacterSet+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CharacterSet+Extension.swift"; sourceTree = "<group>"; };
B46D3F922151D95F0029772C /* MSPersonaCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSPersonaCell.swift; sourceTree = "<group>"; };
B46D3F9C215985AC0029772C /* MSPersonaListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSPersonaListView.swift; sourceTree = "<group>"; };
B487EA7221911FD4005E90B3 /* MSBadgeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSBadgeView.swift; sourceTree = "<group>"; };
B4E782C02176AD5E00A7DFCE /* MSActionsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSActionsCell.swift; sourceTree = "<group>"; };
B4E782C221793AB200A7DFCE /* MSActivityIndicatorCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSActivityIndicatorCell.swift; sourceTree = "<group>"; };
B4E782C421793BB900A7DFCE /* MSActivityIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSActivityIndicatorView.swift; sourceTree = "<group>"; };
@ -310,6 +314,8 @@
isa = PBXGroup;
children = (
B4E782C421793BB900A7DFCE /* MSActivityIndicatorView.swift */,
B444D6B52183A9740002B4D4 /* MSBadgeBaseView.swift */,
B487EA7221911FD4005E90B3 /* MSBadgeView.swift */,
FDA1AF8B21484625001AE720 /* MSBlurringView.swift */,
A5CEC23020E451D00016922A /* MSButton.swift */,
A5B87B03211E22B70038C37C /* MSDimmingView.swift */,
@ -563,6 +569,7 @@
A5B87B06211E23650038C37C /* UIView+Extensions.swift in Sources */,
A5961FA7218A2E4500E2A506 /* UIImage+Extensions.swift in Sources */,
B42661422148568800E25423 /* MSAvatarView.swift in Sources */,
B444D6B62183A9740002B4D4 /* MSBadgeBaseView.swift in Sources */,
FD7254E72146E946002F4069 /* MSCalendarViewDayCell.swift in Sources */,
A589F854211BA03200471C23 /* MSLabel.swift in Sources */,
B4266140214852B400E25423 /* NSString+Extension.swift in Sources */,
@ -575,6 +582,7 @@
FDA1AF8F21484A26001AE720 /* Obscurable.swift in Sources */,
FDA1AF91214871B5001AE720 /* MSCardTransitionAnimator.swift in Sources */,
A5CEC16F20D98F340016922A /* Fonts.swift in Sources */,
B487EA7321911FD4005E90B3 /* MSBadgeView.swift in Sources */,
FD599D0C2134AB1E008845EE /* MSCalendarViewDataSource.swift in Sources */,
A5B87B04211E22B70038C37C /* MSDimmingView.swift in Sources */,
A5961F9F218A256B00E2A506 /* MSPopupMenuItem.swift in Sources */,

Просмотреть файл

@ -0,0 +1,151 @@
//
// Copyright © 2018 Microsoft Corporation. All rights reserved.
//
import UIKit
// MARK: MSBadgeBaseViewDelegate
@objc public protocol MSBadgeBaseViewDelegate {
func didSelectBadge(_ badge: MSBadgeBaseView)
func didTapSelectedBadge(_ badge: MSBadgeBaseView)
}
// MARK: - MSBadgeBaseViewDataSource
open class MSBadgeBaseViewDataSource: NSObject {
@objc open private(set) var text: String
@objc public init(text: String) {
self.text = text
super.init()
}
}
// MARK: - MSBadgeBaseView
/**
`MSBadgeBaseView` is the base view used in `MSBadgeView` as a "badge".
It defines the needed interface for views handled by MSBadgeView such as:
- Colored background
- Selection logic
*/
open class MSBadgeBaseView: UIView {
private struct Constants {
static let defaultMinWidth: CGFloat = 25
static let backgroundViewCornerRadius: CGFloat = 2
}
open class var defaultHeight: CGFloat {
assertionFailure("MSBadgeBaseView defaultHeight method must be overridden")
return 0
}
@objc open var dataSource: MSBadgeBaseViewDataSource? {
didSet {
reload()
}
}
open weak var delegate: MSBadgeBaseViewDelegate?
open var isEnabled: Bool = true {
didSet {
updateBackgroundColor()
accessibilityHint = nil
isUserInteractionEnabled = isEnabled
}
}
open var isSelected: Bool = false {
didSet {
updateBackgroundColor()
updateAccessibility()
}
}
open var minWidth: CGFloat = Constants.defaultMinWidth {
didSet {
setNeedsLayout()
}
}
open override var intrinsicContentSize: CGSize {
return sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude))
}
internal var badgeBackgroundColor: UIColor = MSColors.Badge.background {
didSet {
updateBackgroundColor()
}
}
internal var badgeSelectedBackgroundColor: UIColor = MSColors.Badge.backgroundSelected {
didSet {
updateBackgroundColor()
}
}
internal var badgeDisabledBackgroundColor: UIColor = MSColors.Badge.backgroundDisabled {
didSet {
updateBackgroundColor()
}
}
private let backgroundView = UIView()
public init() {
super.init(frame: .zero)
backgroundView.layer.cornerRadius = Constants.backgroundViewCornerRadius
addSubview(backgroundView)
updateBackgroundColor()
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(badgeTapped))
addGestureRecognizer(tapGesture)
isUserInteractionEnabled = true
accessibilityTraits = UIAccessibilityTraitButton
updateAccessibility()
}
public required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
open override func layoutSubviews() {
super.layoutSubviews()
backgroundView.frame = bounds
}
open func reload() { }
open override func sizeThatFits(_ size: CGSize) -> CGSize {
assertionFailure("MSBadgeBaseView sizeThatFits method must be overridden")
return .zero
}
private func updateAccessibility() {
if isSelected {
accessibilityValue = "Accessibility.Selected.Value".localized
accessibilityHint = "Accessibility.Selected.Hint".localized
} else {
accessibilityValue = nil
accessibilityHint = "Accessibility.Select.Hint".localized
}
}
private func updateBackgroundColor() {
if !isEnabled {
backgroundView.backgroundColor = badgeDisabledBackgroundColor
return
}
backgroundView.backgroundColor = isSelected ? badgeSelectedBackgroundColor : badgeBackgroundColor
}
@objc private func badgeTapped() {
if isSelected {
delegate?.didTapSelectedBadge(self)
} else {
isSelected = true
delegate?.didSelectBadge(self)
}
}
}

Просмотреть файл

@ -0,0 +1,193 @@
//
// Copyright © 2018 Microsoft Corporation. All rights reserved.
//
import UIKit
// MARK: MSBadgeViewStyle
@objc public enum MSBadgeViewStyle: Int {
case `default`
case warning
case error
}
// MARK: - MSBadgeViewDataSource
open class MSBadgeViewDataSource: MSBadgeBaseViewDataSource {
public var style: MSBadgeViewStyle
@objc public init(text: String, style: MSBadgeViewStyle) {
self.style = style
super.init(text: text)
}
}
// MARK: - MSBadgeView
/**
`MSBadgeView` is used to present text with a colored background supplied by `MSBadgeBaseView` in the form of a "badge". It is used in `MSBadgeListField` to represent a selected item from `MSPersonaListView`.
`MSBadgeView` can be selected with a tap gesture and tapped again after entering a selected state for the purpose of displaying more details about the entity represented by the selected badge.
*/
open class MSBadgeView: MSBadgeBaseView {
private struct Constants {
static let labelFont: UIFont = MSFonts.body
static let paddingHorizontal: CGFloat = 5
static let paddingVertical: CGFloat = 4
}
open override class var defaultHeight: CGFloat {
return Constants.paddingVertical + Constants.labelFont.deviceLineHeight + Constants.paddingVertical
}
private static func backgroundColor(for style: MSBadgeViewStyle, selected: Bool, enabled: Bool) -> UIColor {
switch style {
case .default:
if !enabled {
return MSColors.Badge.backgroundDisabled
} else if selected {
return MSColors.Badge.backgroundSelected
} else {
return MSColors.Badge.background
}
case .warning:
if selected {
return MSColors.Badge.backgroundWarningSelected
} else {
return MSColors.Badge.backgroundWarning
}
case .error:
if selected {
return MSColors.Badge.backgroundErrorSelected
} else {
return MSColors.Badge.backgroundError
}
}
}
private static func textColor(for style: MSBadgeViewStyle, selected: Bool, enabled: Bool) -> UIColor {
switch style {
case .default:
if !enabled {
return MSColors.Badge.textDisabled
} else if selected {
return MSColors.Badge.textSelected
} else {
return MSColors.Badge.text
}
case .warning:
if selected {
return MSColors.Badge.textWarningSelected
} else {
return MSColors.Badge.textWarning
}
case .error:
if selected {
return MSColors.Badge.textErrorSelected
} else {
return MSColors.Badge.textError
}
}
}
open var badgeViewDataSource: MSBadgeViewDataSource? { return dataSource as? MSBadgeViewDataSource }
open override var isEnabled: Bool {
didSet {
updateLabelTextColor()
}
}
open override var isSelected: Bool {
didSet {
updateLabelTextColor()
}
}
private var style: MSBadgeViewStyle = .default {
didSet {
badgeBackgroundColor = MSBadgeView.backgroundColor(for: style, selected: false, enabled: true)
badgeSelectedBackgroundColor = MSBadgeView.backgroundColor(for: style, selected: true, enabled: true)
badgeDisabledBackgroundColor = MSBadgeView.backgroundColor(for: style, selected: false, enabled: false)
textColor = MSBadgeView.textColor(for: style, selected: false, enabled: true)
selectedTextColor = MSBadgeView.textColor(for: style, selected: true, enabled: true)
disabledTextColor = MSBadgeView.textColor(for: style, selected: false, enabled: false)
}
}
private var textColor: UIColor = MSColors.Badge.text {
didSet {
updateLabelTextColor()
}
}
private var selectedTextColor: UIColor = MSColors.Badge.textSelected {
didSet {
updateLabelTextColor()
}
}
private var disabledTextColor: UIColor = MSColors.Badge.textDisabled {
didSet {
updateLabelTextColor()
}
}
private let label = UILabel()
public override init() {
super.init()
label.font = Constants.labelFont
label.lineBreakMode = .byTruncatingMiddle
label.textAlignment = .center
label.backgroundColor = .clear
addSubview(label)
updateLabelTextColor()
isAccessibilityElement = true
}
public required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
open override func reload() {
label.text = dataSource?.text
style = badgeViewDataSource?.style ?? .default
setNeedsLayout()
}
open override func layoutSubviews() {
super.layoutSubviews()
let labelHeight = label.font.deviceLineHeight
let labelSize = label.sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude))
let fittingLabelWidth = UIScreen.main.roundToDevicePixels(labelSize.width)
let minLabelWidth = minWidth - 2 * Constants.paddingHorizontal
let maxLabelWidth = width - 2 * Constants.paddingHorizontal
let labelWidth = max(minLabelWidth, min(maxLabelWidth, fittingLabelWidth))
label.frame = CGRect(x: Constants.paddingHorizontal, y: Constants.paddingVertical, width: labelWidth, height: labelHeight)
}
open override func sizeThatFits(_ size: CGSize) -> CGSize {
let labelSize = label.sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude))
let fittingLabelWidth = UIScreen.main.roundToDevicePixels(labelSize.width)
let horizontalPadding = 2 * Constants.paddingHorizontal
let labelWidth = max(min(fittingLabelWidth, size.width), minWidth - horizontalPadding)
return CGSize(width: labelWidth + horizontalPadding, height: MSBadgeView.defaultHeight)
}
private func updateLabelTextColor() {
if !isEnabled {
label.textColor = disabledTextColor
return
}
label.textColor = isSelected ? selectedTextColor : textColor
}
// MARK: Accessibility
open override var accessibilityLabel: String? { get { return label.text } set { } }
}

Просмотреть файл

@ -37,9 +37,14 @@ public struct MSColors {
public static let black: UIColor = #colorLiteral(red: 0.1333333333, green: 0.1333333333, blue: 0.1333333333, alpha: 1)
/// #E8484C
public static let warning: UIColor = #colorLiteral(red: 0.9098039216, green: 0.2823529412, blue: 0.2980392157, alpha: 1)
public static let error: UIColor = #colorLiteral(red: 0.9098039216, green: 0.2823529412, blue: 0.2980392157, alpha: 1)
/// #FFF3F4
public static let lightWarning: UIColor = #colorLiteral(red: 1, green: 0.9529411765, blue: 0.9568627451, alpha: 1)
public static let lightError: UIColor = #colorLiteral(red: 1, green: 0.9529411765, blue: 0.9568627451, alpha: 1)
/// #574305
public static let warning: UIColor = #colorLiteral(red: 0.3411764706, green: 0.262745098, blue: 0.01960784314, alpha: 1)
/// #E2DDCC
public static let lightWarning: UIColor = #colorLiteral(red: 0.8862745098, green: 0.8666666667, blue: 0.8, alpha: 1)
// MARK: Avatar background colors
@ -79,14 +84,31 @@ public struct MSColors {
public struct Action {
public static let text: UIColor = primary
public static let textHighlighted: UIColor = primary.withAlphaComponent(0.4)
public static let textDestructive: UIColor = warning
public static let textDestructiveHighlighted: UIColor = warning.withAlphaComponent(0.4)
public static let textDestructive: UIColor = error
public static let textDestructiveHighlighted: UIColor = error.withAlphaComponent(0.4)
}
public struct Avatar {
public static let text: UIColor = white
}
public struct Badge {
public static let background: UIColor = primary.withAlphaComponent(0.24)
public static let backgroundDisabled: UIColor = backgroundLightGray
public static let backgroundError: UIColor = lightError
public static let backgroundErrorSelected: UIColor = error
public static let backgroundSelected: UIColor = primary
public static let backgroundWarning: UIColor = lightWarning
public static let backgroundWarningSelected: UIColor = warning
public static let text: UIColor = primary
public static let textDisabled: UIColor = darkGray
public static let textError: UIColor = error
public static let textErrorSelected: UIColor = lightError
public static let textSelected: UIColor = white
public static let textWarning: UIColor = warning
public static let textWarningSelected: UIColor = lightWarning
}
public struct CalendarView {
public struct TodayCell {
public static let background: UIColor = white
@ -138,6 +160,7 @@ public enum MSTextColorStyle: Int {
case secondary
case white
case primary
case error
case warning
// TODO: Replace with conformance to CaseIterable after switch to Swift 4.2
@ -153,6 +176,8 @@ public enum MSTextColorStyle: Int {
return MSColors.white
case .primary:
return MSColors.primary
case .error:
return MSColors.error
case .warning:
return MSColors.warning
}

Просмотреть файл

@ -223,6 +223,7 @@ open class MSAvatarView: UIView {
// MARK: Accessibility
open override var accessibilityLabel: String? { get { return name ?? email } set {} }
open override var accessibilityTraits: UIAccessibilityTraits { get { return UIAccessibilityTraitImage } set {} }
open override var isAccessibilityElement: Bool { get { return true } set { } }
open override var accessibilityLabel: String? { get { return name ?? email } set { } }
open override var accessibilityTraits: UIAccessibilityTraits { get { return UIAccessibilityTraitImage } set { } }
}

Просмотреть файл

@ -45,6 +45,7 @@ open class MSPersonaCell: UITableViewCell {
setupCellBackgroundColors()
nameLabel.lineBreakMode = .byTruncatingTail
subtitleLabel.lineBreakMode = .byTruncatingTail
avatarView.accessibilityElementsHidden = true
}
public required init(coder aDecoder: NSCoder) {

Просмотреть файл

@ -6,6 +6,9 @@
"Accessibility.Alert" = "Alert";
"Accessibility.Dismiss.Label" = "Dismiss";
"Accessibility.Dismiss.Hint" = "Double tap to dismiss";
"Accessibility.Select.Hint" = "Double tap to select";
"Accessibility.Selected.Hint" = "Double tap to see details";
"Accessibility.Selected.Value" = "Selected";
/* Accessibility label for the upper calendar date picker view. */
"Accessibility.Calendar.Label" = "Calendar";
@ -23,5 +26,3 @@
// MSPersonaListView
"MSPersonaListView.SearchDirectory" = "Search Directory";
"MSPersonaListView.SingleResult" = "result found from directory";
"MSPersonaListView.MultipleResults" = "results found from directory";