diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/RadioButton.swift b/MVMCoreUI/Atomic/Atoms/Selectors/RadioButton.swift index 2b22fcee..1ac28ded 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/RadioButton.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/RadioButton.swift @@ -20,7 +20,7 @@ import UIKit } } - public override var isSelected: Bool { + @objc public override var isSelected: Bool { didSet { radioModel?.state = isSelected updateAccessibilityLabel() @@ -127,7 +127,7 @@ import UIKit func updateAccessibilityLabel() { if let state = MVMCoreUIUtility.hardcodedString(withKey: isSelected ? "radio_selected_state" : "radio_not_selected_state") { - accessibilityLabel = String(format: MVMCoreUIUtility.hardcodedString(withKey: "radio_desc_state") ?? "%@%@", "", state) + accessibilityLabel = state } } diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/RadioButtonSelectionHelper.swift b/MVMCoreUI/Atomic/Atoms/Selectors/RadioButtonSelectionHelper.swift index 5196bb9c..4765766b 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/RadioButtonSelectionHelper.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/RadioButtonSelectionHelper.swift @@ -19,22 +19,22 @@ import Foundation private var selectedRadioButton: RadioButton? private var selectedRadioButtonModel: RadioButtonModel? public var baseValue: AnyHashable? - -//-------------------------------------------------- -// MARK: - Initializer -//-------------------------------------------------- - + + //-------------------------------------------------- + // MARK: - Initializer + //-------------------------------------------------- + public func set(_ radioButtonModel: RadioButtonModel, _ radioButton: RadioButton) { self.fieldKey = radioButtonModel.fieldKey self.groupName = radioButtonModel.groupName - + if radioButtonModel.state { if self.baseValue == nil, let selected = radioButtonModel.baseValue as? Bool, selected { self.baseValue = radioButtonModel.fieldValue } selectedRadioButtonModel = radioButtonModel - + // Below code is needed for cell resuse scenario. radioButton.isSelected = true selectedRadioButton = radioButton @@ -50,24 +50,24 @@ import Foundation public static func setupForRadioButtonGroup(_ radioButtonModel: RadioButtonModel, _ radioButton: RadioButton, delegateObject: MVMCoreUIDelegateObject?) { guard let groupName = radioButtonModel.fieldKey, - let formValidator = delegateObject?.formHolderDelegate?.formValidator else { - return - } - + let formValidator = delegateObject?.formHolderDelegate?.formValidator + else { return } + let radioButtonSelectionHelper = formValidator.radioButtonsModelByGroup[groupName] ?? RadioButtonSelectionHelper() radioButtonSelectionHelper.set(radioButtonModel, radioButton) formValidator.radioButtonsModelByGroup[groupName] = radioButtonSelectionHelper FormValidator.setupValidation(for: radioButtonSelectionHelper, delegate: delegateObject?.formHolderDelegate) } - + public func selected(_ radioButton: RadioButton) { + // Checks because the view could be reused if selectedRadioButton?.radioModel === selectedRadioButtonModel { selectedRadioButton?.isSelected = false } else { selectedRadioButtonModel?.state = false } - + selectedRadioButton = radioButton selectedRadioButton?.isSelected = true selectedRadioButtonModel = selectedRadioButton?.radioModel @@ -76,6 +76,7 @@ import Foundation // MARK: - FormValidationFormFieldProtocol extension RadioButtonSelectionHelper { + public func formFieldValue() -> AnyHashable? { return selectedRadioButtonModel?.fieldValue } diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableRadioButtonAndPaymentMethod.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableRadioButtonAndPaymentMethod.swift index ee4900c5..688f457c 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableRadioButtonAndPaymentMethod.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableRadioButtonAndPaymentMethod.swift @@ -14,11 +14,13 @@ import UIKit // MARK: - Outlets //----------------------------------------------------- - let radioButton = RadioButton(frame: .zero) + let radioButton = RadioButton() let leftImage = LoadImageView(pinnedEdges: .all) let eyebrowHeadlineBodyLink = EyebrowHeadlineBodyLink() var stack: Stack + private var observation: NSKeyValueObservation? = nil + //----------------------------------------------------- // MARK: - Initializers //----------------------------------------------------- @@ -46,6 +48,16 @@ import UIKit stack.restack() eyebrowHeadlineBodyLink.body.textColor = .mvmOrangeAA eyebrowHeadlineBodyLink.headline.styleBoldBodySmall(true) + radioButton.isAccessibilityElement = false + isAccessibilityElement = true + updateAccessibilityLabel() + accessibilityTraits = .button + accessibilityHint = MVMCoreUIUtility.hardcodedString(withKey: "radio_action_hint") + updateAccessibilityLabel() + + observation = observe(\.radioButton.isSelected, options: [.new]) { [weak self] _, _ in + self?.updateAccessibilityLabel() + } } open override func reset() { @@ -64,6 +76,7 @@ import UIKit radioButton.set(with: model.radioButton, delegateObject, additionalData) leftImage.set(with: model.image, delegateObject, additionalData) eyebrowHeadlineBodyLink.set(with: model.eyebrowHeadlineBodyLink, delegateObject, additionalData) + updateAccessibilityLabel() } open override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { @@ -71,6 +84,34 @@ import UIKit } public override func didSelectCell(at index: IndexPath, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) { - radioButton.tapAction() + radioButton.tapAction() + } + + func updateAccessibilityLabel() { + + var message = MVMCoreUIUtility.hardcodedString(withKey: "radio_button") ?? "" + + radioButton.updateAccessibilityLabel() + if let radioButtonLabel = radioButton.accessibilityLabel { + message += radioButtonLabel + ", " + } + + if let leftImageLabel = leftImage.accessibilityLabel { + message += leftImageLabel + ", " + } + + if let eyebrowLabel = eyebrowHeadlineBodyLink.eyebrow.text { + message += eyebrowLabel + ", " + } + + if let headlineLabel = eyebrowHeadlineBodyLink.headline.text { + message += headlineLabel + ", " + } + + if let bodyLabel = eyebrowHeadlineBodyLink.body.text { + message += bodyLabel + } + + accessibilityLabel = message } } diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableRadioButtonAndPaymentMethodModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableRadioButtonAndPaymentMethodModel.swift index 5ea28778..f455882e 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableRadioButtonAndPaymentMethodModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableRadioButtonAndPaymentMethodModel.swift @@ -9,11 +9,19 @@ import Foundation public class ListLeftVariableRadioButtonAndPaymentMethodModel: ListItemModel, MoleculeModelProtocol { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + public static var identifier: String = "listLVRBImg" public var radioButton: RadioButtonModel public var image: ImageViewModel public var eyebrowHeadlineBodyLink: EyebrowHeadlineBodyLinkModel + //-------------------------------------------------- + // MARK: - Initializer + //-------------------------------------------------- + public init(radioButton: RadioButtonModel, image: ImageViewModel, eyebrowHeadlineBodyLink:EyebrowHeadlineBodyLinkModel) { self.radioButton = radioButton self.image = image @@ -21,6 +29,10 @@ public class ListLeftVariableRadioButtonAndPaymentMethodModel: ListItemModel, Mo super.init() } + //-------------------------------------------------- + // MARK: - Methods + //-------------------------------------------------- + public override func setDefaults() { super.setDefaults() if image.width == nil, image.height == nil { @@ -29,6 +41,10 @@ public class ListLeftVariableRadioButtonAndPaymentMethodModel: ListItemModel, Mo } } + //-------------------------------------------------- + // MARK: - Keys + //-------------------------------------------------- + private enum CodingKeys: String, CodingKey { case moleculeName case radioButton @@ -36,6 +52,10 @@ public class ListLeftVariableRadioButtonAndPaymentMethodModel: ListItemModel, Mo case eyebrowHeadlineBodyLink } + //-------------------------------------------------- + // MARK: - Codec + //-------------------------------------------------- + required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) radioButton = try typeContainer.decode(RadioButtonModel.self, forKey: .radioButton) diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableRadioButtonBodyText.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableRadioButtonBodyText.swift index 9b45eec7..d89604f9 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableRadioButtonBodyText.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableRadioButtonBodyText.swift @@ -14,10 +14,12 @@ open class ListLeftVariableRadioButtonBodyText: TableViewCell { // MARK: - Outlets //----------------------------------------------------- - let radioButton = RadioButton(frame: .zero) + let radioButton = RadioButton() let headlineBody = HeadlineBody() var stack: Stack + private var observation: NSKeyValueObservation? = nil + //----------------------------------------------------- // MARK: - Initializers //----------------------------------------------------- @@ -42,6 +44,15 @@ open class ListLeftVariableRadioButtonBodyText: TableViewCell { addMolecule(stack) stack.restack() + isAccessibilityElement = true + radioButton.isAccessibilityElement = false + accessibilityTraits = .button + accessibilityHint = MVMCoreUIUtility.hardcodedString(withKey: "radio_action_hint") + updateAccessibilityLabel() + + observation = observe(\.radioButton.isSelected, options: [.new]) { [weak self] _, _ in + self?.updateAccessibilityLabel() + } } //---------------------------------------------------- @@ -55,6 +66,7 @@ open class ListLeftVariableRadioButtonBodyText: TableViewCell { radioButton.set(with: model.radioButton, delegateObject, additionalData) headlineBody.set(with: model.headlineBody, delegateObject, additionalData) + updateAccessibilityLabel() } open override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { @@ -64,4 +76,24 @@ open class ListLeftVariableRadioButtonBodyText: TableViewCell { public override func didSelectCell(at index: IndexPath, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) { radioButton.tapAction() } + + func updateAccessibilityLabel() { + + var message = MVMCoreUIUtility.hardcodedString(withKey: "radio_button") ?? "" + + radioButton.updateAccessibilityLabel() + if let radioButtonLabel = radioButton.accessibilityLabel { + message += radioButtonLabel + ", " + } + + if let headlineLabel = headlineBody.headlineLabel.text { + message += headlineLabel + ", " + } + + if let messageLabel = headlineBody.messageLabel.text { + message += messageLabel + } + + accessibilityLabel = message + } } diff --git a/MVMCoreUI/Atomic/Molecules/Items/MoleculeListItemModel.swift b/MVMCoreUI/Atomic/Molecules/Items/MoleculeListItemModel.swift index 1384ee88..bee9d612 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/MoleculeListItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/MoleculeListItemModel.swift @@ -9,21 +9,39 @@ import Foundation import MVMCore + @objcMembers public class MoleculeListItemModel: ListItemModel, MoleculeModelProtocol { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + public class var identifier: String { return "listItem" } + public var molecule: MoleculeModelProtocol + //-------------------------------------------------- + // MARK: - Keys + //-------------------------------------------------- + private enum CodingKeys: String, CodingKey { case moleculeName case molecule } + //-------------------------------------------------- + // MARK: - Initializer + //-------------------------------------------------- + public init(with moleculeModel: MoleculeModelProtocol) { molecule = moleculeModel super.init() } + + //-------------------------------------------------- + // MARK: - Codec + //-------------------------------------------------- required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) @@ -38,4 +56,3 @@ import MVMCore try container.encodeModel(molecule, forKey: .molecule) } } - diff --git a/MVMCoreUI/Atomic/Molecules/Items/MoleculeTableViewCell.swift b/MVMCoreUI/Atomic/Molecules/Items/MoleculeTableViewCell.swift index 0c9dec3b..6e1f2965 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/MoleculeTableViewCell.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/MoleculeTableViewCell.swift @@ -10,8 +10,9 @@ import UIKit @objcMembers open class MoleculeTableViewCell: TableViewCell { - + //-------------------------------------------------- // MARK: - MoleculeViewProtocol + //-------------------------------------------------- public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { diff --git a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/Lists/StringAndMoleculeStack/StringAndMoleculeStack.swift b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/Lists/StringAndMoleculeStack/StringAndMoleculeStack.swift index c887e838..8630e607 100644 --- a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/Lists/StringAndMoleculeStack/StringAndMoleculeStack.swift +++ b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/Lists/StringAndMoleculeStack/StringAndMoleculeStack.swift @@ -10,7 +10,7 @@ import UIKit // This class is only temporarily necessary. Eventually we will have initWithModel instad of just init for moleculeviews, which will remove this need. open class StringAndMoleculeStack: MoleculeStackView { - override open func createStackItemsFromModel(_ model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable : Any]?) { + override open func createStackItemsFromModel(_ model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { guard let model = model as? StackModelProtocol, let molcules = model.molecules as? [MoleculeStackItemModel] else { return } for stackItemModel in molcules { diff --git a/MVMCoreUI/Atomic/Templates/ModalMoleculeListTemplate.swift b/MVMCoreUI/Atomic/Templates/ModalMoleculeListTemplate.swift index e82e57a0..a141ef0e 100644 --- a/MVMCoreUI/Atomic/Templates/ModalMoleculeListTemplate.swift +++ b/MVMCoreUI/Atomic/Templates/ModalMoleculeListTemplate.swift @@ -9,6 +9,9 @@ import UIKit open class ModalMoleculeListTemplate: MoleculeListTemplate { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- public var closeButton: Button? @@ -17,5 +20,8 @@ open class ModalMoleculeListTemplate: MoleculeListTemplate { closeButton = MVMCoreUICommonViewsUtility.addCloseButton(to: view, action: { _ in MVMCoreNavigationHandler.shared()?.removeCurrentViewController() }) + + accessibilityElements = [closeButton as Any, tableView as Any] + UIAccessibility.post(notification: .layoutChanged, argument: closeButton) } } diff --git a/MVMCoreUI/Atomic/Templates/MoleculeListCellProtocol.swift b/MVMCoreUI/Atomic/Templates/MoleculeListCellProtocol.swift index b9cfc02f..46c9a0fa 100644 --- a/MVMCoreUI/Atomic/Templates/MoleculeListCellProtocol.swift +++ b/MVMCoreUI/Atomic/Templates/MoleculeListCellProtocol.swift @@ -21,12 +21,10 @@ public protocol MoleculeListCellProtocol: UITableViewCell { // Default implementation does nothing extension MoleculeListCellProtocol { - public func setLines(with model: LineModel?, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?, indexPath: IndexPath) { - } - public func didSelectCell(at index: IndexPath, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) { - } - - func willDisplay() { - } + public func setLines(with model: LineModel?, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?, indexPath: IndexPath) { } + + public func didSelectCell(at index: IndexPath, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) { } + + func willDisplay() { } } diff --git a/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift b/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift index 09ca1b41..521a0d45 100644 --- a/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift +++ b/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift @@ -209,8 +209,11 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol func getMoleculeInfo(with listItem: (ListItemModelProtocol & MoleculeModelProtocol)?) -> (identifier: String, class: AnyClass, molecule: ListItemModelProtocol & MoleculeModelProtocol)? { guard let listItem = listItem, - let moleculeClass = MoleculeObjectMapping.shared()?.getMoleculeClass(listItem) else { return nil } + let moleculeClass = MoleculeObjectMapping.shared()?.getMoleculeClass(listItem) + else { return nil } + let moleculeName = moleculeClass.nameForReuse(with: listItem, delegateObject() as? MVMCoreUIDelegateObject) ?? listItem.moleculeName + return (moleculeName, moleculeClass, listItem) } diff --git a/MVMCoreUI/BaseClasses/TableViewCell.swift b/MVMCoreUI/BaseClasses/TableViewCell.swift index b549b6d0..f07b1ee2 100644 --- a/MVMCoreUI/BaseClasses/TableViewCell.swift +++ b/MVMCoreUI/BaseClasses/TableViewCell.swift @@ -205,6 +205,8 @@ import UIKit let caret = CaretView(lineWidth: 1) caret.translatesAutoresizingMaskIntoConstraints = true caret.isAccessibilityElement = true + caret.accessibilityTraits = .button + caret.accessibilityLabel = "Caret," caret.accessibilityHint = MVMCoreUIUtility.hardcodedString(withKey: "AccTabHint") caret.size = .small(.vertical) if let size = caret.size?.dimensions() { diff --git a/MVMCoreUI/BaseControllers/ThreeLayerTableViewController.swift b/MVMCoreUI/BaseControllers/ThreeLayerTableViewController.swift index 8e01cf6c..7962dcf5 100644 --- a/MVMCoreUI/BaseControllers/ThreeLayerTableViewController.swift +++ b/MVMCoreUI/BaseControllers/ThreeLayerTableViewController.swift @@ -40,6 +40,7 @@ open class ThreeLayerTableViewController: ProgrammaticTableViewController { createViewForTableHeader() createViewForTableFooter() tableView?.reloadData() + accessibilityElements = [tableView as Any] } override open func viewDidLoad() { diff --git a/MVMCoreUI/Containers/Views/ContainerHelper.swift b/MVMCoreUI/Containers/Views/ContainerHelper.swift index 4ce97f5c..f60d1ee6 100644 --- a/MVMCoreUI/Containers/Views/ContainerHelper.swift +++ b/MVMCoreUI/Containers/Views/ContainerHelper.swift @@ -9,7 +9,12 @@ import Foundation + open class ContainerHelper: NSObject { + //-------------------------------------------------- + // MARK: - Constraints + //-------------------------------------------------- + var leftConstraint: NSLayoutConstraint? var topConstraint: NSLayoutConstraint? var bottomConstraint: NSLayoutConstraint? @@ -28,17 +33,26 @@ open class ContainerHelper: NSObject { var bottomLowConstraint: NSLayoutConstraint? var rightLowConstraint: NSLayoutConstraint? + //-------------------------------------------------- + // MARK: - Methods + //-------------------------------------------------- + open func constrainView(_ view: UIView) { guard let margins = view.superview?.layoutMarginsGuide else { return } + + leftConstraint?.isActive = false leftConstraint = view.leftAnchor.constraint(equalTo: margins.leftAnchor) leftConstraint?.isActive = true + topConstraint?.isActive = false topConstraint = view.topAnchor.constraint(equalTo: margins.topAnchor) topConstraint?.isActive = true + rightConstraint?.isActive = false rightConstraint = margins.rightAnchor.constraint(equalTo: view.rightAnchor) rightConstraint?.isActive = true + bottomConstraint?.isActive = false bottomConstraint = margins.bottomAnchor.constraint(equalTo: view.bottomAnchor) bottomConstraint?.isActive = true @@ -50,23 +64,25 @@ open class ContainerHelper: NSObject { alignCenterTopConstraint = view.topAnchor.constraint(greaterThanOrEqualTo: margins.topAnchor) alignCenterBottomConstraint = margins.bottomAnchor.constraint(greaterThanOrEqualTo: view.bottomAnchor) + leftLowConstraint?.isActive = false leftLowConstraint = view.leftAnchor.constraint(equalTo: margins.leftAnchor) leftLowConstraint?.priority = UILayoutPriority(rawValue: 200) leftLowConstraint?.isActive = true + topLowConstraint?.isActive = false topLowConstraint = view.topAnchor.constraint(equalTo: margins.topAnchor) topLowConstraint?.priority = UILayoutPriority(rawValue: 200) topLowConstraint?.isActive = true + rightLowConstraint?.isActive = false rightLowConstraint = margins.rightAnchor.constraint(equalTo: view.rightAnchor) rightLowConstraint?.priority = UILayoutPriority(rawValue: 200) rightLowConstraint?.isActive = true + bottomLowConstraint?.isActive = false bottomLowConstraint = margins.bottomAnchor.constraint(equalTo: view.bottomAnchor) bottomLowConstraint?.priority = UILayoutPriority(rawValue: 200) bottomLowConstraint?.isActive = true - - setAccessibility(view) } open func setAccessibility(_ view: UIView) { diff --git a/MVMCoreUI/SupportingFiles/Strings/en.lproj/Localizable.strings b/MVMCoreUI/SupportingFiles/Strings/en.lproj/Localizable.strings index 25dd849b..da55d597 100644 --- a/MVMCoreUI/SupportingFiles/Strings/en.lproj/Localizable.strings +++ b/MVMCoreUI/SupportingFiles/Strings/en.lproj/Localizable.strings @@ -53,6 +53,7 @@ // MARK: Radio Button +"radio_button" = "Radio Button,"; "radio_action_hint" = "Double tap to select"; "radio_selected_state" = "Selected"; "radio_not_selected_state" = "Not Selected";