Text Component doesn’t disable click functionality when disabled

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

1 possible answer(s) on “Text Component doesn’t disable click functionality when disabled

  1. Edit: I’ve since realized that we need to set accessibilityRole=”button” for the screenreaders to recognize something as clickable. Have also identified that this is an issue on iOS — updated the description.

    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.

    Follow up question: Should we make the accessibilityRole default as “button” when an onPress or onLongPress is defined?

    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.

    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.

    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.