From 2972a556b2982948e2eb31d6081cabd4eb2386b0 Mon Sep 17 00:00:00 2001 From: Andrew Bulhak Date: Mon, 2 Dec 2024 19:06:35 +0100 Subject: [PATCH] Add accessibility identifiers to SingleChoiceList rows and text fields --- .../SwiftUI components/SingleChoiceList.swift | 38 +++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/ios/MullvadVPN/View controllers/Settings/SwiftUI components/SingleChoiceList.swift b/ios/MullvadVPN/View controllers/Settings/SwiftUI components/SingleChoiceList.swift index 0b48846d86d6..a01b51b54caf 100644 --- a/ios/MullvadVPN/View controllers/Settings/SwiftUI components/SingleChoiceList.swift +++ b/ios/MullvadVPN/View controllers/Settings/SwiftUI components/SingleChoiceList.swift @@ -57,6 +57,7 @@ struct SingleChoiceList: View where Value: Equatable { var value: Binding @State var initialValue: Value? let itemDescription: (Value) -> String + let itemAccessibilityIdentifier: (Value) -> String let customFieldMode: CustomFieldMode /// The configuration for the field for a custom value row @@ -83,6 +84,7 @@ struct SingleChoiceList: View where Value: Equatable { // this row consists of a text field into which the user can enter a custom value, which may yield a valid Value. This has accompanying text, and functions to translate between text field contents and the Value. (The fromValue method only needs to give a non-nil value if its input is a custom value that could have come from this row.) case custom( label: String, + accessibilityIdentifier: String, prompt: String, legend: String?, minInputWidth: CGFloat?, @@ -102,12 +104,14 @@ struct SingleChoiceList: View where Value: Equatable { optionSpecs: [OptionSpec.OptValue], value: Binding, itemDescription: ((Value) -> String)? = nil, + itemAccessibilityIdentifier: ((Value) -> String)? = nil, customFieldMode: CustomFieldMode = .freeText ) { self.title = title self.options = optionSpecs.enumerated().map { OptionSpec(id: $0.offset, value: $0.element) } self.value = value self.itemDescription = itemDescription ?? { "\($0)" } + self.itemAccessibilityIdentifier = itemAccessibilityIdentifier ?? { "\($0)" } self.customFieldMode = customFieldMode self.initialValue = value.wrappedValue } @@ -118,12 +122,20 @@ struct SingleChoiceList: View where Value: Equatable { /// - title: The title of the list, which is typically the name of the item being chosen. /// - options: A list of `Value`s to be presented. /// - itemDescription: An optional function that, when given a `Value`, returns the string representation to present in the list. If not provided, this will be generated naïvely using string interpolation. - init(title: String, options: [Value], value: Binding, itemDescription: ((Value) -> String)? = nil) { + /// - itemAccessibilityIdentifier: An optional function that, when given a `Value`, returns the accessibility identifier for the value's list item. If not provided, this will be generated naïvely using string interpolation. + init( + title: String, + options: [Value], + value: Binding, + itemDescription: ((Value) -> String)? = nil, + itemAccessibilityIdentifier: ((Value) -> String)? = nil + ) { self.init( title: title, optionSpecs: options.map { .literal($0) }, value: value, - itemDescription: itemDescription + itemDescription: itemDescription, + itemAccessibilityIdentifier: itemAccessibilityIdentifier ) } @@ -133,9 +145,11 @@ struct SingleChoiceList: View where Value: Equatable { /// - title: The title of the list, which is typically the name of the item being chosen. /// - options: A list of fixed `Value`s to be presented. /// - itemDescription: An optional function that, when given a `Value`, returns the string representation to present in the list. If not provided, this will be generated naïvely using string interpolation. This is only used for the non-custom values. + /// - itemAccessibilityIdentifier: An optional function that, when given a `Value`, returns the accessibility identifier for the value's list item. If not provided, this will be generated naïvely using string interpolation. /// - parseCustomValue: A function that attempts to parse the text entered into the text field and produce a `Value` (typically the tagged custom value with an argument applied to it). If the text is not valid for a value, it should return `nil` /// - formatCustomValue: A function that, when passed a `Value` containing user-entered custom data, formats that data into a string, which should match what the user would have entered. This function can expect to only be called for the custom value, and should return `nil` in the event of its argument not being a valid custom value. /// - customLabel: The caption to display in the custom row, next to the text field. + /// - customAccessibilityIdentifier: The accessibility identifier to use for the custom row. If not provided, "customValue" will be used. The accessibility identifier for the text field will be this value with ".input" appended. /// - customPrompt: The text to display, greyed, in the text field when it is empty. This also serves to set the width of the field, and should be right-padded with spaces as appropriate. /// - customLegend: Optional text to display below the custom field, i.e., to explain sensible values /// - customInputWidth: An optional minimum width (in pseudo-pixels) for the custom input field @@ -146,9 +160,11 @@ struct SingleChoiceList: View where Value: Equatable { options: [Value], value: Binding, itemDescription: ((Value) -> String)? = nil, + itemAccessibilityIdentifier: ((Value) -> String)? = nil, parseCustomValue: @escaping ((String) -> Value?), formatCustomValue: @escaping ((Value) -> String?), customLabel: String, + customAccessibilityIdentifier: String = "customValue", customPrompt: String, customLegend: String? = nil, customInputMinWidth: CGFloat? = nil, @@ -159,6 +175,7 @@ struct SingleChoiceList: View where Value: Equatable { title: title, optionSpecs: options.map { .literal($0) } + [.custom( label: customLabel, + accessibilityIdentifier: customAccessibilityIdentifier, prompt: customPrompt, legend: customLegend, minInputWidth: customInputMinWidth, @@ -168,6 +185,7 @@ struct SingleChoiceList: View where Value: Equatable { )], value: value, itemDescription: itemDescription, + itemAccessibilityIdentifier: itemAccessibilityIdentifier, customFieldMode: customFieldMode ) } @@ -202,12 +220,14 @@ struct SingleChoiceList: View where Value: Equatable { customValueIsFocused = false customValueInput = "" } + .accessibilityIdentifier(itemAccessibilityIdentifier(item)) } // Construct the one row with a custom input field for a custom value // swiftlint:disable function_body_length private func customRow( label: String, + accessibilityIdentifier: String, prompt: String, inputWidth: CGFloat?, maxInputLength: Int?, @@ -288,6 +308,7 @@ struct SingleChoiceList: View where Value: Equatable { customValueInput = valueText } } + .accessibilityIdentifier(accessibilityIdentifier + ".input") } .onTapGesture { if let v = toValue(customValueInput) { @@ -296,6 +317,7 @@ struct SingleChoiceList: View where Value: Equatable { customValueIsFocused = true } } + .accessibilityIdentifier(accessibilityIdentifier) } // swiftlint:enable function_body_length @@ -323,9 +345,19 @@ struct SingleChoiceList: View where Value: Equatable { switch opt.value { case let .literal(v): literalRow(v) - case let .custom(label, prompt, legend, inputWidth, maxInputLength, toValue, fromValue): + case let .custom( + label, + accessibilityIdentifier, + prompt, + legend, + inputWidth, + maxInputLength, + toValue, + fromValue + ): customRow( label: label, + accessibilityIdentifier: accessibilityIdentifier, prompt: prompt, inputWidth: inputWidth, maxInputLength: maxInputLength,