Description
When a Text component passes true
to disabled
prop, the component does not announce “disabled” when using a screen reader.
import {Text, View, StyleSheet} from 'react-native';
import * as React from 'react';
type Props = SurfaceProps<PlaygroundRoute>;
export default function Playground(props: Props): React.Node {
const [pressCount, setPressCount] = React.useState(0);
return (
<View style={styles.container}>
<Text> Press count: {pressCount} </Text>
<Text
style={styles.text}
accessibilityState={{disabled: true}}
accessibilityRole="button"
onPress={() => setPressCount(pressCount + 1)}>
Case 1: onPressText - disabled via accessibilityState prop
</Text>
<Text
style={styles.text}
disabled
accessibilityRole="button"
onPress={() => setPressCount(pressCount + 1)}>
Case 2: onPressText - disabled via disabled prop
</Text>
<Text
style={styles.text}
accessibilityRole="button"
onPress={() => setPressCount(pressCount + 1)}>
Case 3: onPressText - not disabled
</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
padding: 10,
paddingTop: 30,
},
text: {
margin: 20,
},
The above examples have the following problems (bolded text highlights problems)
Case | Android (with TalkBack off) | Android (with TalkBack on) | iOS (VoiceOver off) | iOS (VoiceOver on) |
---|---|---|---|---|
Case 1: disabled from accessibilityState | Can still trigger onPress | Can still trigger onPress although it reads the button as disabled | Can still trigger onPress | Can still trigger onPress — does not read button as disabled |
Case 2: disabled from disabled prop | Cannot trigger onPress | Cannot trigger onPress | Can still trigger onPress | Can still trigger onPress — does not read button as disabled |
Case 3: not disabled | Can trigger onPress | Can trigger onPress | Can trigger onPress | Can trigger onPress |
Notes:
- You must set accessibilityRole=”button” to correctly tell VoiceOver/TalkBack that the Text component is clickable and screen reader will relay that information.
Identified Problems:
- Android’s
disabled
via accessibilityState allows you to still trigger onPress behavior regardless if you have TalkBack on. - iOS also disregards accessibilityState’s disabled regardless of VoiceOver — onPress will be triggered.
- iOS disregards
disabled
prop — onPress will be triggered regardless of VoiceOver - iOS VoiceOver does not read buttons as disabled regardless via accessibilityState or disabled prop.
Reproduce:
- On Android device, toggle TalkBack (Settings > Accessibility > TalkBack on (turn on volume shortcut for ease)
- On iOS device, toggle VoiceOver by (Settings > Accessibility > VoiceOver). You can also turn on a shortcut (Settings > Accessibility > Accessibility Shortcut > VoiceOver) which allows you to triple click the power button to turn off/on VoiceOver.
- Render the above example
- Swipe to focus on the 3 different cases to test
React Native version:
0.63
This is not expected, as setting the onPress prop should ideally be enough to trigger this to be recognized as clickable. If that’s not the case, that’s probably a separate issue that should be fixed. On iOS, you may need to also set
accessible={true}
, but I am assuming that was the case already, as otherwise it would probably not have even been focusable.This is a very common pattern on iOS, where almost all clickable elements have a trait of “button”, but it is not very common on Android, where “button” is really only used for things that look and act like buttons. Because of this, I don’t think we should make this change globally.
So it seems like we intended for the view properties of enabled/selected to be hard coded (I’m not sure why we would, but thats pretty clear from the code), and instead only represent these on the AccessibilityNodeInfo.
To give some context, for every View, an AccessibilityNodeInfo is created and initialized in the onInitializeAccessibilityNodeInfo method. This method is meant to sync the current state of the view properties to the same properties on the AccessibilityNodeInfo. This method is called basically every time a view property changes, so the AccessibilityNodeInfo should always be up to date with the state of the View. Talkback parses the contents of these AccessibilityNodeInfos to inform its announcements and behavior.
The way we’re hard-coding the view properties will cause issues however, since while Talkback really only parses the AccessibilityNodeInfo for it’s information, when it does that parsing is often triggered off of events that Android’s View system fires, and these events are often fired based on view property changes. If the view properties are not changing, these events will not get fired, so Talkback’s will not react to them. There is really no good reason to intentionally have the View and the AccessibilityNodeInfo out of sync like we have them.
As a bonus, if we keep these in sync, we likely only need to set them on the View, as the super() call in the onInitializeAccessibilityNodeInfo’s method will take care of syncing them to the AccessibilityNodeInfo for us.