Are accessibility labels supported within ConfirmationDialogState?
acosmicflamingo opened this issue · comments
Let's say I want to initialize a ConfirmationDialogState object within a reducer like this:
state.confirmationDialog = .init(
title: .init("Skip tutorial?"),
buttons: [
.destructive(
.init("Yes, skip"), action: .send(.confirmDeletionButtonTapped, animation: .default)
),
.cancel(.init("No, resume"), action: .send(.dismissDeletionPrompt))
]
)
In addition, I want to add accessibility labels (for example, I want to have a confirmation dialog display a timestamp but for VoiceOver users, I want it to not say 'zero colon 54'). I think the way to do this the SwiftUI way would be:
state.confirmationDialog = .init(
title: .init("Skip tutorial?"),
buttons: [
.destructive(
.init("Yes, skip").accessibilityLabel("HELLO WORLD"), action: .send(.confirmDeletionButtonTapped, animation: .default)
),
.cancel(.init("No, resume").accessibilityLabel("HOWDY"), action: .send(.dismissDeletionPrompt))
]
)
Doing this however does not actually pass accessibility labels as I'd expect, and it might have to do with how the convenience initializers and helper functions setup the buttons. But I do see that TCA includes accessibility support in TextState.swift. Perhaps I'm missing how to properly use accessibility labels? Or is there an issue with how ConfirmationDialogState supports accessibility labels? I'd be happy to debug this myself and create a PR, but I want to make sure I'm not missing anything. Thanks!
@acosmicflamingo Have you tried reproducing in vanilla SwiftUI using Text
in a confirmation dialog? Many text modifiers, especially those that do styling, do not work in alerts. If vanilla SwiftUI has different behavior, though, definitely sounds like a bug we should track down!
Actually, if I use the CaseStudies example and add accessibilityLabel modifier to a button, it works as expected. I think the problem (which in hindsight I should've mentioned in the initial issue description) is actually coming from how UIAlertController configures the buttons using ConfirmationDialogState in the convenience initializer:
/// Creates a `UIAlertController` from `ConfirmationDialogState`.
///
/// - Parameters:
/// - state: The state of dialog that can be shown to the user.
/// - send: A function that wraps a dialog action in the view store's action type.
public convenience init<Action>(
state: ConfirmationDialogState<Action>, send: @escaping (Action) -> Void
) {
self.init(
title: String(state: state.title),
message: state.message.map { String(state: $0) },
preferredStyle: .actionSheet
)
for button in state.buttons {
self.addAction(.init(button, action: send))
}
}
In the addAction function, it's getting a UIAlertAction object instantiated from the following convenience initializer:
extension UIAlertAction {
convenience init<Action>(
_ button: AlertState<Action>.Button,
action: @escaping (Action) -> Void
) {
self.init(
title: String(state: button.label),
style: button.role.map(UIAlertAction.Style.init) ?? .default,
handler: button.action.map { _ in { _ in button.withAction(action) } }
)
self.accessibilityLabel = button.label.??? <= theoretical solution but don't know what property to use
}
}
It looks like that is where the accessibility label information is not translating from SwiftUI to UIKit. The button parameter of type AlertState<Action>.Button
may have a property label of type TextState
, but calling .accessibilityLabel doesn't return the current accessibility label as it would in UIKit. Instead it is transforming the TextState
type to have a specific accessibility label. I don't know if I'm missing something because I'm unfamiliar with SwiftUI, or maybe keeping some properties hidden inTextState
is intentional, but the solution to this problem might be a simple one that resides in TextState
.
It's definitely possible that TextState
should expose methods/properties to compute these kinds of things. Simple examples are simple to theorize:
var label = TextState("Hello!").accessibilityLabel("An enthusiastic greeting!")
label.accessibilityLabel // "An enthusiastic greeting!"
We'd have to figure out how to treat certain defaults, though:
var label = TextState("Hello!")
label.accessibilityLabel
// nil
// ...or...
// "Hello"?
And composition makes it a little more tricky:
var label = TextState("Hello!").accessibilityLabel("Greetings") + TextState(" And welcome!")
label.accessibilityLabel // What should be returned here?
That is tricky indeed. Well, what currently happens in UIKit is that accessibilityLabel is nil by default. Then, UIButton checks whether the property has a value and if it doesn't, then the UIButton's label's text value is instead used.
If we were going to mimic this kind of behavior, then a solution below might get us close to that:
extension UIAlertAction {
convenience init<Action>(
_ button: AlertState<Action>.Button,
action: @escaping (Action) -> Void
) {
self.init(
title: String(state: button.label),
style: button.role.map(UIAlertAction.Style.init) ?? .default,
handler: button.action.map { _ in { _ in button.withAction(action) } }
)
self.accessibilityLabel = String(state: (button.accessibilityLabel ?? button.label))
}
}
For your examples, that would mean I'd expect the following:
var label = TextState("Hello!")
label.accessibilityLabel // should be nil
var label = TextState("Hello!").accessibilityLabel("Greetings") + TextState(" And welcome!")
// oh noooooooooooooooooooooo things break down here :( I guess to be consistent, label.accessibilityLabel
// would be "Greetings"?
@acosmicflamingo Doing some spring cleaning and found this. I'm going to convert this to a discussion since we think it'd be a fine thing to revisit in the future, but we don't consider it a bug 😄