Skip to content

Commit 2972a55

Browse files
acb-mvbuggmagnet
authored andcommitted
Add accessibility identifiers to SingleChoiceList rows and text fields
1 parent d44108b commit 2972a55

File tree

1 file changed

+35
-3
lines changed

1 file changed

+35
-3
lines changed

ios/MullvadVPN/View controllers/Settings/SwiftUI components/SingleChoiceList.swift

+35-3
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ struct SingleChoiceList<Value>: View where Value: Equatable {
5757
var value: Binding<Value>
5858
@State var initialValue: Value?
5959
let itemDescription: (Value) -> String
60+
let itemAccessibilityIdentifier: (Value) -> String
6061
let customFieldMode: CustomFieldMode
6162

6263
/// The configuration for the field for a custom value row
@@ -83,6 +84,7 @@ struct SingleChoiceList<Value>: View where Value: Equatable {
8384
// 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.)
8485
case custom(
8586
label: String,
87+
accessibilityIdentifier: String,
8688
prompt: String,
8789
legend: String?,
8890
minInputWidth: CGFloat?,
@@ -102,12 +104,14 @@ struct SingleChoiceList<Value>: View where Value: Equatable {
102104
optionSpecs: [OptionSpec.OptValue],
103105
value: Binding<Value>,
104106
itemDescription: ((Value) -> String)? = nil,
107+
itemAccessibilityIdentifier: ((Value) -> String)? = nil,
105108
customFieldMode: CustomFieldMode = .freeText
106109
) {
107110
self.title = title
108111
self.options = optionSpecs.enumerated().map { OptionSpec(id: $0.offset, value: $0.element) }
109112
self.value = value
110113
self.itemDescription = itemDescription ?? { "\($0)" }
114+
self.itemAccessibilityIdentifier = itemAccessibilityIdentifier ?? { "\($0)" }
111115
self.customFieldMode = customFieldMode
112116
self.initialValue = value.wrappedValue
113117
}
@@ -118,12 +122,20 @@ struct SingleChoiceList<Value>: View where Value: Equatable {
118122
/// - title: The title of the list, which is typically the name of the item being chosen.
119123
/// - options: A list of `Value`s to be presented.
120124
/// - 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.
121-
init(title: String, options: [Value], value: Binding<Value>, itemDescription: ((Value) -> String)? = nil) {
125+
/// - 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.
126+
init(
127+
title: String,
128+
options: [Value],
129+
value: Binding<Value>,
130+
itemDescription: ((Value) -> String)? = nil,
131+
itemAccessibilityIdentifier: ((Value) -> String)? = nil
132+
) {
122133
self.init(
123134
title: title,
124135
optionSpecs: options.map { .literal($0) },
125136
value: value,
126-
itemDescription: itemDescription
137+
itemDescription: itemDescription,
138+
itemAccessibilityIdentifier: itemAccessibilityIdentifier
127139
)
128140
}
129141

@@ -133,9 +145,11 @@ struct SingleChoiceList<Value>: View where Value: Equatable {
133145
/// - title: The title of the list, which is typically the name of the item being chosen.
134146
/// - options: A list of fixed `Value`s to be presented.
135147
/// - 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.
148+
/// - 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.
136149
/// - 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`
137150
/// - 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.
138151
/// - customLabel: The caption to display in the custom row, next to the text field.
152+
/// - 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.
139153
/// - 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.
140154
/// - customLegend: Optional text to display below the custom field, i.e., to explain sensible values
141155
/// - customInputWidth: An optional minimum width (in pseudo-pixels) for the custom input field
@@ -146,9 +160,11 @@ struct SingleChoiceList<Value>: View where Value: Equatable {
146160
options: [Value],
147161
value: Binding<Value>,
148162
itemDescription: ((Value) -> String)? = nil,
163+
itemAccessibilityIdentifier: ((Value) -> String)? = nil,
149164
parseCustomValue: @escaping ((String) -> Value?),
150165
formatCustomValue: @escaping ((Value) -> String?),
151166
customLabel: String,
167+
customAccessibilityIdentifier: String = "customValue",
152168
customPrompt: String,
153169
customLegend: String? = nil,
154170
customInputMinWidth: CGFloat? = nil,
@@ -159,6 +175,7 @@ struct SingleChoiceList<Value>: View where Value: Equatable {
159175
title: title,
160176
optionSpecs: options.map { .literal($0) } + [.custom(
161177
label: customLabel,
178+
accessibilityIdentifier: customAccessibilityIdentifier,
162179
prompt: customPrompt,
163180
legend: customLegend,
164181
minInputWidth: customInputMinWidth,
@@ -168,6 +185,7 @@ struct SingleChoiceList<Value>: View where Value: Equatable {
168185
)],
169186
value: value,
170187
itemDescription: itemDescription,
188+
itemAccessibilityIdentifier: itemAccessibilityIdentifier,
171189
customFieldMode: customFieldMode
172190
)
173191
}
@@ -202,12 +220,14 @@ struct SingleChoiceList<Value>: View where Value: Equatable {
202220
customValueIsFocused = false
203221
customValueInput = ""
204222
}
223+
.accessibilityIdentifier(itemAccessibilityIdentifier(item))
205224
}
206225

207226
// Construct the one row with a custom input field for a custom value
208227
// swiftlint:disable function_body_length
209228
private func customRow(
210229
label: String,
230+
accessibilityIdentifier: String,
211231
prompt: String,
212232
inputWidth: CGFloat?,
213233
maxInputLength: Int?,
@@ -288,6 +308,7 @@ struct SingleChoiceList<Value>: View where Value: Equatable {
288308
customValueInput = valueText
289309
}
290310
}
311+
.accessibilityIdentifier(accessibilityIdentifier + ".input")
291312
}
292313
.onTapGesture {
293314
if let v = toValue(customValueInput) {
@@ -296,6 +317,7 @@ struct SingleChoiceList<Value>: View where Value: Equatable {
296317
customValueIsFocused = true
297318
}
298319
}
320+
.accessibilityIdentifier(accessibilityIdentifier)
299321
}
300322

301323
// swiftlint:enable function_body_length
@@ -323,9 +345,19 @@ struct SingleChoiceList<Value>: View where Value: Equatable {
323345
switch opt.value {
324346
case let .literal(v):
325347
literalRow(v)
326-
case let .custom(label, prompt, legend, inputWidth, maxInputLength, toValue, fromValue):
348+
case let .custom(
349+
label,
350+
accessibilityIdentifier,
351+
prompt,
352+
legend,
353+
inputWidth,
354+
maxInputLength,
355+
toValue,
356+
fromValue
357+
):
327358
customRow(
328359
label: label,
360+
accessibilityIdentifier: accessibilityIdentifier,
329361
prompt: prompt,
330362
inputWidth: inputWidth,
331363
maxInputLength: maxInputLength,

0 commit comments

Comments
 (0)