diff --git a/Mistica/Source/Components/Cards/DataCard.swift b/Mistica/Source/Components/Cards/DataCard.swift index b3e1c50a9..d554f689c 100644 --- a/Mistica/Source/Components/Cards/DataCard.swift +++ b/Mistica/Source/Components/Cards/DataCard.swift @@ -63,6 +63,7 @@ public class DataCard: UIView { static let largeIconSize = CGFloat(24) } + private lazy var cardAccessibilityElement = UIAccessibilityElement(accessibilityContainer: self) private let iconContainerView = UIView() private var iconImageView = DataCardAsset() private let cardBaseView = CardBase() @@ -83,6 +84,18 @@ public class DataCard: UIView { } } + override public var accessibilityElements: [Any]? { + get { + // We must set the frame and be sure it is already calculated. + cardAccessibilityElement.accessibilityFrameInContainerSpace = bounds + return [ + cardAccessibilityElement, + cardBaseView + ].compactMap { $0 } + } + set {} + } + override public init(frame: CGRect) { super.init(frame: frame) commomInit() @@ -122,6 +135,15 @@ public extension DataCard { var linkButton: Button { cardBaseView.buttonsView.linkButton } + + override var accessibilityTraits: UIAccessibilityTraits { + get { + cardAccessibilityElement.accessibilityTraits + } + set { + cardAccessibilityElement.accessibilityTraits = newValue + } + } } // MARK: Private @@ -199,6 +221,13 @@ private extension DataCard { case .primaryAndLink(let primaryButton, let linkButton): cardBaseView.configureButtons(primaryButton: primaryButton, linkButton: linkButton) } + + cardAccessibilityElement.accessibilityLabel = [ + configuration.headline, + configuration.title, + configuration.subtitle, + configuration.descriptionTitle + ].compactMap { $0 }.joined(separator: " ") } } diff --git a/Mistica/Source/Components/Cards/HighlightedCard.swift b/Mistica/Source/Components/Cards/HighlightedCard.swift index 5e8b9fc99..9c8f7743d 100644 --- a/Mistica/Source/Components/Cards/HighlightedCard.swift +++ b/Mistica/Source/Components/Cards/HighlightedCard.swift @@ -31,6 +31,7 @@ public class HighlightedCard: UIView { case secondary } + private lazy var cardAccessibilityElement = UIAccessibilityElement(accessibilityContainer: self) private lazy var verticalStackView = UIStackView() private lazy var horizontalStackView = UIStackView() @@ -140,6 +141,20 @@ public class HighlightedCard: UIView { } } + override public var accessibilityElements: [Any]? { + get { + updateAccessibilityLabel() + // We must set the frame and be sure it is already calculated. + cardAccessibilityElement.accessibilityFrameInContainerSpace = bounds + return [ + cardAccessibilityElement, + actionButton.isHidden ? nil : actionButton, + closeButton.isHidden ? nil : closeButton + ].compactMap { $0 } + } + set {} + } + public init(title: String? = nil, subtitle: String? = nil, rightImage: UIImage? = nil, @@ -292,6 +307,15 @@ public extension HighlightedCard { backgroundImageView.accessibilityIdentifier = newValue } } + + override var accessibilityTraits: UIAccessibilityTraits { + get { + cardAccessibilityElement.accessibilityTraits + } + set { + cardAccessibilityElement.accessibilityTraits = newValue + } + } } // MARK: Private @@ -443,4 +467,11 @@ private extension HighlightedCard { @objc func actionButtonTapped() { actionButtonCallback?() } + + func updateAccessibilityLabel() { + cardAccessibilityElement.accessibilityLabel = [ + titleAccessibilityLabel, + subtitleAccessibilityLabel + ].compactMap { $0 }.joined(separator: " ") + } } diff --git a/Mistica/Source/Components/Cards/Internals/CardBase.swift b/Mistica/Source/Components/Cards/Internals/CardBase.swift index 53b1cab20..7a0c1cef1 100644 --- a/Mistica/Source/Components/Cards/Internals/CardBase.swift +++ b/Mistica/Source/Components/Cards/Internals/CardBase.swift @@ -77,7 +77,17 @@ extension CardBase { contentView.descriptionTitle = newValue } } - + + override var accessibilityElements: [Any]? { + get { + [ + buttonsView, + fragmentView as Any + ].compactMap { $0 } + } + set { } + } + func configureButtons(primaryButton: CardButton?, linkButton: CardLinkButton?) { buttonsView.configureButtons(primaryButton: primaryButton, linkButton: linkButton) diff --git a/Mistica/Source/Components/Cards/Internals/CardButtonsView.swift b/Mistica/Source/Components/Cards/Internals/CardButtonsView.swift index e7d251b14..d039f6583 100644 --- a/Mistica/Source/Components/Cards/Internals/CardButtonsView.swift +++ b/Mistica/Source/Components/Cards/Internals/CardButtonsView.swift @@ -17,6 +17,16 @@ class CardButtons: UIStackView { private var primaryActionHandler: (() -> Void)? private var linkActionHandler: (() -> Void)? + override var accessibilityElements: [Any]? { + get { + [ + primaryButton.superview == nil ? nil : primaryButton, + linkButton.superview == nil ? nil : linkButton + ].compactMap { $0 } + } + set {} + } + override public init(frame: CGRect) { super.init(frame: frame) commomInit() diff --git a/Mistica/Source/Components/Cards/MediaCard.swift b/Mistica/Source/Components/Cards/MediaCard.swift index b3a59e8cd..fbd7f8fc8 100644 --- a/Mistica/Source/Components/Cards/MediaCard.swift +++ b/Mistica/Source/Components/Cards/MediaCard.swift @@ -48,6 +48,7 @@ public class MediaCard: UIView { static let spacingAfterRichMediaView = CGFloat(8) } + private lazy var cardAccessibilityElement = UIAccessibilityElement(accessibilityContainer: self) private var richMediaContainerView = UIView() private let baseCardView = CardBase() @@ -56,6 +57,18 @@ public class MediaCard: UIView { baseCardView.fragmentView = fragmentView } } + + override public var accessibilityElements: [Any]? { + get { + cardAccessibilityElement.accessibilityFrameInContainerSpace = bounds + return [ + cardAccessibilityElement, + fragmentView as Any, + baseCardView.buttonsView + ].compactMap { $0 } + } + set {} + } public var contentConfiguration: MediaCardConfiguration? { didSet { @@ -93,6 +106,15 @@ public extension MediaCard { var linkButton: Button { baseCardView.buttonsView.linkButton } + + override var accessibilityTraits: UIAccessibilityTraits { + get { + cardAccessibilityElement.accessibilityTraits + } + set { + cardAccessibilityElement.accessibilityTraits = newValue + } + } } // MARK: Private @@ -160,6 +182,13 @@ private extension MediaCard { baseCardView.descriptionTitle = configuration.descriptionTitle baseCardView.configureButtons(primaryButton: configuration.button, linkButton: configuration.link) + + cardAccessibilityElement.accessibilityLabel = [ + baseCardView.headline, + baseCardView.title, + baseCardView.subtitle, + baseCardView.descriptionTitle + ].compactMap { $0 }.joined(separator: " ") } } diff --git a/Mistica/Source/Components/Cards/README.md b/Mistica/Source/Components/Cards/README.md index 5d73c8ed9..16d11ef6a 100644 --- a/Mistica/Source/Components/Cards/README.md +++ b/Mistica/Source/Components/Cards/README.md @@ -7,6 +7,7 @@ * [HighlightedCard](#highlightedcard) * [Right Image](#right-image) * [How to use a HighlightedCard](#how-to-use-a-highlightedcard) +* [Accessibility](#accessibility) ## DataCard @@ -112,3 +113,59 @@ highlightedCard.showCloseButton = true ``` When using with autolayout, **HighlightedCard** has no intrinsic size for the width but it has an specific intrinsic size for the height. + +# Accessibility + +Cards are ready for VoiceOver. +They will be an unique element and if has buttons, them will be also focusable with VoiceOver. + +VoiceOver will read the following components (in this particular order): +- headline +- title +- subtitle +- description + +## Extra content Accessibility + +If extra view is provided to the cell, extra view is reponsible about his accessibility and will be a focusable element for voice over inside the card like the buttons. +To provide a good Accessibility Experience watch [this video](https://developer.apple.com/videos/play/wwdc2018/230/) is highly recommended. + +This is an example extra view class that wil be an unique element for VoiceOver and will read Title and Text. + +```swift +class ExtraView: UIView { + private let titleLabel = UILabel() + private let textLabel = UILabel() + + // Initializers... + + func configure(title: String, text: String) { + titleLabel.text = title + textLabel.text = text + + // Update the accessibility label with the + // content to be readed by VoiceOver + accessibilityLabel = "\(title) \(text)" + } + + private func commonInit() { + // Setup code... + + // Make this view as an accesible element to + // be focusable by VoiceOver. + isAccessibleElement = true + } +} + +card.fragmentView = extraView +``` + + +## Cards as Cells (UITableViewCell/UICollectionViewCell) + +To use the cards as cells, as selectable items, set the `accessibilityTraits` of the Card to `[.button]` is recommended. +And double tap with the focused card will launch `didSelect` on table/collection delegate. + +## UITapGesture + +Same as working with cells, Cards can be tappables with `UITapGestureRecognizer`, setting the `accessibilityTrait` to `[.button]` is also recommended.