Text Component doesn't disable click functionality when disabled
See original GitHub issueDescription
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
Issue Analytics
- State:
- Created 3 years ago
- Comments:6 (5 by maintainers)
Top Results From Across the Web
Clicking a disabled input or button - javascript - Stack Overflow
There is no way to capture a click on disabled elements. Your best bet is to react to a specific class on the...
Read more >How to disable or enable buttons using Javascript and jQuery
Learn how to enable or disable buttons using javascript and jQuery based on whether the input field is filled or empty.
Read more >Accessibility and usability considerations for disabling buttons ...
HTML buttons and inputs accept a Boolean disabled attribute. This stops the element from receiving events like clicks and keyboard focus.
Read more >How to disable a button in React | bobbyhadz
Use the disabled prop to disable a button in React, e.g. <button disabled={true}>Click</button> . You can use the prop to conditionally disable the...
Read more >HTML attribute: disabled - HTML: HyperText Markup Language
The Boolean disabled attribute, when present, makes the element not mutable, focusable, or even submitted with the form.
Read more >Top Related Medium Post
No results found
Top Related StackOverflow Question
No results found
Troubleshoot Live Code
Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start FreeTop Related Reddit Thread
No results found
Top Related Hackernoon Post
No results found
Top Related Tweet
No results found
Top Related Dev.to Post
No results found
Top Related Hashnode Post
No results found
Top GitHub Comments
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.
I notice that we hardcode the values of selected and enabled here and it seems to be expecting that somewhere we override these values by the comment immediate following:
But setting a breakpoint here where we read the accessibilityState to initializeAccessibiltyNodeInfo, it never gets hit in the examples above. I’m unfamiliar with how accessibiltyNodeInfo should be used but I see it being referenced in something like ReactHorizontalScrollView – does this imply that Text needs to do something as well? cc @blavalla if you have more context on how AccessibilityNodeInfo is used typically in Android views?