question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Placeholder property included in TextInput Componenent prevents accessibilityLabel and accessibiltyHint from being read by screenreader

See original GitHub issue

When including a placeholder property in a TextInput component, the accessibilityLabel and accessibilityHint properties are not read out in the screen reader. Example:

<TextInput accessible={true} accessibilityLabel={'My Accessibility Label'} accessibilityHint={'My Accessibility Hint')} accessibilityRole={'search'} style={[searchInput, { backgroundColor: searchFocus ? theme.INPUT_FOCUS_COLOR : theme.WHITE_COLOR }]} onFocus={(bool) => {set_searchFocus(bool);}} value={searchKey} inputAccessoryViewID={inputAccessoryViewID} placeholderTextColor={'#767676'} keyboardType= "default" returnKeyType={Platform.OS === 'ios' ? '' : "search"} enablesReturnKeyAutomatically={true} onSubmitEditing={() => {searchOrder();}} placeholder={formatMessageString('My Placeholder Text')} onChangeText={(searchString) =>{setSearchKey(searchString);}} underlineColorAndroid="transparent" />

One would expect the accessibilityLabel and Hint to be read out, and in my experience prior to 0.60 they were, along with the placeholder. Now, only the placeholder text his read out. Removing the placeholder property reads out the Label and Hint.

React Native version:

0.60.5

Steps To Reproduce

  1. Include a TextInput in project with accessibilityLabel, accessibilityHint, and placeholder text.
  2. Open project in device, turn on screenreader (e.g. TalkBack for Android, VoiceOver for iOS), and click the field.
  3. Click on field
  4. Note that screenreader only reads out the placeholder text, and not the accessibilityLabel and accessibilityHint.
  5. Remove placeholder text and reload.
  6. Click on field
  7. Note that accessbilityLabel and accessibilityHint are read out by the screenreader

Issue Analytics

  • State:open
  • Created 4 years ago
  • Reactions:8
  • Comments:23 (2 by maintainers)

github_iconTop GitHub Comments

11reactions
tostringtheorycommented, May 7, 2020

I’m putting my hat in the ring on this. We’ve spent the last couple days trying to figure out our field accessibility with Android. Digging into this it is clear that react-native’s accessibility on Android is broken, and I’ve got some evidence of our findings.

Essentially, we were able to discern that there are 4 variables that determine what Android or iOS says - existence of value, accessibilityLabel, accessibilityHint, or placeholder. React-Native on iOS seems to treat all of the scenarios logically, while Android does not. I’ve created a minimal example demonstrating all 7 combinations of specification of accessibilityLabel / accessibilityHint / placeholder, and you can clear the text box/value to investigate the other 7 combinations (without value). As well, above each example I specify what Android/iOS reads with no value, and below each example I specify what Android/iOS reads with “1” as the value:

import * as React from 'react';
import { AccessibilityInfo, TextInput, TouchableWithoutFeedback, Text, View, StyleSheet } from 'react-native';

const labelText = "Test";

export default class App extends React.Component {
  state = { text: "1" }

  render() {
    const Example = ({title, label, hint, placeholder}) => {
      const accessibilityProps = {
        ...(hint && { accessibilityHint: `${labelText} hint`}),
        ...(placeholder && { placeholder: `${labelText} placeholder`}),
        ...(label && { accessibilityLabel: `${labelText} label`})
      };

      return (
      <React.Fragment>
      <View style={styles.sectionContainer}>
            <Text accessibilityRole='header' style={styles.sectionTitle}>
              {title}
            </Text>
          </View>
          <View style={styles.sectionContainer}>
            <TextInput
                style={{height: 30, borderWidth: 1}}
                keyboardType="phone-pad"
                onChangeText={ text => this.setState({text}) }
                value={this.state.text}

                {...accessibilityProps}
              />
          </View>
        </React.Fragment>
    )};

    return (
    <View >

        <View style={styles.body}>
        
          {/* ANDROID: Test Label, Edit Box */}
          {/* iOS: Test Label, Text Field, .. */}
          <Example title="Label Only" label />
          {/* ANDROID: One, Edit Box */}
          {/* iOS: Test Label, One, Text Field */}
          
          {/* ANDROID: Test Hint, Edit Box */}
          {/* iOS: Test Hint, Text Field, .. */}
          <Example title="Hint Only" hint />
          {/* ANDROID: One, Edit Box */}
          {/* iOS: One, Text Field, Test Hint .. */}
          
          {/* ANDROID: Test Placeholder, Edit Box */}
          {/* iOS: Test Placeholder, Text Field, .. */}
          <Example title="Placeholder Only" placeholder />
          {/* ANDROID: One, Edit Box, Test Placeholder */}
          {/* iOS: One, Text Field .. */}
          
          {/* ANDROID: Test Placeholder, Edit Box */}
          {/* iOS: Test Label, Test Placeholder, Text Field, .. */}
          <Example title="Label, Placeholder" label placeholder />
          {/* ANDROID: One, Edit Box, Test Placeholder */}
          {/* iOS: Test Label, One, Text Field .. */}
          
          {/* ANDROID: Test Label, Test Hint, Edit Box */}
          {/* iOS: Test Label, Text Field, Test Hint.. */}
          <Example title="Label, Hint" label hint />
          {/* ANDROID: One, Edit Box */}
          {/* iOS: Test Label, One, Text Field, Text Hint .. */}

          {/* ANDROID: Test Placeholder, Edit Box */}
          {/* iOS: Test Placeholder, Text Field, Test Hint.. */}
          <Example title="Hint, Placeholder" hint placeholder />
          {/* ANDROID: One, Edit Box, Test Placeholder */}
          {/* iOS: One, Text Field, Test Hint .. */}

          {/* ANDROID: Test Placeholder, Edit Box */}
          {/* iOS: Test Label, Test Placeholder, Text Field, Test Hint.. */}
          <Example title="Label, Hint, Placeholder Only" label hint placeholder />
          {/* ANDROID: One, Edit Box, Test Placeholder */}
          {/* iOS: Test Label, One, Text Field, Text Hint .. */}

        </View>
    </View>
  ); 
 }
}

const styles = StyleSheet.create({
  body: {
    padding: 48,
    backgroundColor: '#fff',
  },
  sectionContainer: {
    marginTop: 14,
    paddingHorizontal: 24,
  },
  sectionTitle: {
    fontSize: 14,
    fontWeight: '600',
    color: '#000',
  }
});

This is available as a Snack. This is on react-native 0.61 from the Expo 37 SDK, but we’ve confirmed this in vanilla 0.62.2 in house as well. Furthermore, even if I switch the Snack version to Expo 34, the issue still applies for Android.

Essentially however, the bug is that react-native completely ignores the label if there is either a value or placeholder specified on the field, and if there is not a placeholder specified but a value exists, it only reads the value.

7reactions
blavallacommented, Mar 23, 2021

@jychiao, @tostringtheory , @HenrikDK2 and others.

Sorry for the delay on this, I work on Android accessibility, and understand the root cause of this issue, but only recently started working with React Native, so I didn’t know about this problem.

Unfortunately, the issue here is on Talkback’s side and there isn’t much that can be done in RN to resolve it directly, however we can work around the limitations that Talkback has. Let me explain how both iOS and Android work with text inputs, as they have some core differences that will make the problem a bit more clear.

On iOS with VoiceOver, when a text input is focused, VoiceOver will read the accessibilityLabel (if one exists), the accessibilityValue (if one exists and if not, the text input’s actual value), and the accessibilityHint (if one exists). The placeholder, being visible text inside the input is the “value” portion of that announcement. Just like when a user enters text, it visibly overrides the placeholder, any entered text will override the placeholder in the announcement as well. iOS’s behavior here is likely exactly what you expect when you see these three properties set on a text input.

Android on the other hand, is a bit different. On Android with Talkback, when a text input is focused, Talkback will read the text entered into the input (if any exists), then fallback to reading the placeholder if there us no entered text, and finally fall back to reading the contentDescription (Androids version of iOS’s accessibilityLabel) if no entered text or placeholder exists. Because of this fallback behavior, you will never be able to have a text input on Android with both an accessibilityLabel and content inside of it (either placeholder or user entered).

This logic in Talkback is shown in the compositor.json file here:

https://github.com/google/talkback/blob/master/compositor/src/main/res/raw/compositor.json#L1510-L1512

That line $node.text is referencing any entered text into the input, and the “fallback” block around it is saying to try to read that first, and only if it’s empty read the next line, which is $node.contentDescription.

So how does this impact accessibiltyHint you may ask? Well, accessibilityHint doesn’t actually exist on Android. This is an iOS-specific property that React Native has polyfilled into Android by simply concatenating this string into the contentDescription. Since the contentDescription is ignored in the fallback behavior above, so is the accessibilityHint.

@Akanksha-Thakur posted a workaround above, and the reason this works is that by setting a role of “none” rather than the default role of “edit_text” you are bypassing talkback’s edit-text-specific logic here. Unfortunately, that also means you are losing out on other edit-text-specific features, such as telling a user whether “editing mode” or “selection mode” is active.

Not all hope is lost however, as Android does have alternate ways to present this information in text inputs, but right now React Native doesn’t yet support them. These approaches have issues open here (#30846) and here (#31056) if you are interested in helping improve RN’s accessibility support.

In order to get something like iOS’s “accessibiltyLabel” to work on text inputs in Android, RN will need to support Android’s “labelFor” property. This is Android-specific and has no iOS counterpart. This property allows you to have one element act as a label for another, and in the example of text input, would allow an external element to label it even if it has content entered inside it. This is what that API could look like (psuedo-code), when built.

<Text labelFor={123}>First Name</Text>
<TextInput value="Brett" placeholder="Enter your first name" id={123}>

On focus, Talkback would announce “Brett, Edit Text for First Name”.

To fix the accessibilityHint issue, we’ll need to change how accessibilityHint works on Android, and rather than concatenating into the contentDescription instead add it elsewhere, such as the toolTipText property. TooltipText is another Android-specific feature, which is meant to contain text that is inside a tooltip pointing at an element. It can be used as a generic placeholder for secondary content about an element though, similar to iOS’s AccessibilityHint. If we changed accessibilityHint to instead set toolTipText on Android, and had the following code:

<Text labelFor={123}>First Name</Text>
<TextInput value="Brett" placeholder="Enter your first name" id={123} accessibilityHint="Be sure not to enter any numbers or spaces.">

on focus Talkback would announce “Brett, Edit Text for First Name. Be sure not to enter any numbers or spaces.”

This would be very close to iOS’s behavior, missing only the 500ms pause before the accessibilityHint (which is already missing on Android).

I know this was a long comment, but I want to make sure the issue is well understood here, and that it’s clear that this isn’t a straightforward fix. If anyone wants to work on the two issues I mentioned in order to add support here, I’d be happy to review any and all PRs!

Read more comments on GitHub >

github_iconTop Results From Across the Web

Is placeholder text a sufficient accessible label for form fields
On some stacks, the placeholder attribute will populate the Accessible Name in the accessibility API, because there is no other Accessible Name.
Read more >
Form Instructions | Web Accessibility Initiative (WAI) - W3C
Indicate any required and optional input, data formats, and other relevant ... It is critical to include form instructions in ways that can...
Read more >
Disable placeholder text to be read by all accessibility API ...
Is there a way in which reading placeholder value can be disabled for All screen reader(in context with HTML input). Especially the IOS...
Read more >
React Native Android Accessibility Tips | blog {callstack}
However, in contrast to iOS, Android emulator doesn't have the screen reader app installed by default. To install it, you can download Android ......
Read more >
The Anatomy of Accessible Forms - Deque Systems
The placeholder attribute works with the following input types: ... If it is not read by a screen reader, the information may be...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found