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.

Android: Role description is announced before the components text, rather than after

See original GitHub issue

Description

For some elements, when you add an accessibilityRole to them, screen readers are announcing that role at the beginning of its announcement (e.g. “Button, Like”) rather than at the end (e.g. “Like, Button”).

The reason for this is a complex, and requires some knowledge about how screen readers like Talkback work. I’ll sum up those details below under “Android Details”, but be warned, it’ll be a long (but hopefully interesting) read!

In the core component library, this mostly impacts <Button> which automatically sets an accessibilityRole of “button”.

React Native version:

v0.63

Expected Behavior

When focus is put on an element that has an accessibilityRole set, the role should be at the end of the announcement, rather than the beginning. The only things that should come after the role are “disabled” or usage hints such as “Double-tap to activate”.

Android Details

The cause of this issue is related to Talkback’s rules on what elements will be focusable, and which will not, as well as how it composes together an elements text and its role.

Talkback’s focusability rules are not well documented, and require reading the source code to fully understand, but the one causing the issue here is one I’ll refer to as automatic content grouping.

Automatic content grouping basically work like this. Talkback will walk the entire view hierarchy and parse content out of the AccessibilityNodeInfo’s associated with the views. It then applies the following rules:

1.) If an AccessibilityNodeInfo is considered “actionable” (which Talkback defines as having clickable=true, longClickable=true, or focusable=true, or having AccessibilityActions for any of those), AND it has some content to read like a contentDescription or text, it will be considered focusable. 2.) If an AccessibilityNodeInfo is considered “actionable” AND it does not have content to read like a contentDescription or text Talkback will parse descendant elements looking for non-focusable descendants to use as content.

Item 2 is what is important here. If Talkback were to encounter an element like this:

<View clickable="true" contentDescription="Some Text">
   <Text>Some Other Text</Text>
</View>

It would deem that the <View> should be focusable, due to rule #1 (it has clickable=“true”) and it has a contentDescription. So on focus, Talkback will read “Some Text”, and “Some Other Text” will be ignored, and never announced.

But what if it didn’t have a content description? For example:

<View clickable="true">
   <Text>Some Other Text</Text>
</View>

Talkback would parse <View> and still deem it as focusable, due to clickable=true, and then go looking for content to announce. It would see the <Text> node, which by itself is not focusable, so it will use this elements text as its own.

On focus of <View> talkback will then pull the content <Text> and read read “Some Other Text”, with the focus on <View>, and not on <Text> like you may expect.

This becomes particularly interesting when an element has multiple non-focusable descendants, for example:

<View clickable="true">
   <Text>Some Text</Text>
   <Text>Some More Text</Text>
</View>

When Talkback parses this tree, it will again determine that <View> is focusable, and that both <Text> elements are not. And when focus is placed on <View> it will announce “Some Text, Some More Text”, grouping the text of all non-focusable descendants together.

Okay, so now you understand how automatic content grouping works. Now let’s talk about how roles work.

When Talkback is walking the tree, and encounters an element with an accessibility role defined (which is really just the className property of the AccessibilityNodeInfo), it will look at the role, and add it to the very end of its focus announcement. Lets look at an example:

<View contentDescription="Some Text" accessibilityRole="button" clickable="true" />

When talkback looks at this element, it will determine that is is focusable due to rule #1, see that it has no descendants, but has a contentDescription set, and also see that it has a role set. So it will announce “Some Text, Button” appending the role text to the end of the announcement.

Now what about if it sees this view again, but with a role:

<View clickable="true" accessibilityRole="button">
   <Text>Some Text</Text>
</View>

Again, it will determine that it is focusable, see that it has no content description of its own, but does has non-focusable descendants with text, and see that it has a role. However, it now does things in the wrong order. It will first append the role text to the empty announcement, and then it will parse the descendants and append their text, ending up with an announcement of “Button, Some Text”. This is the root of our bug.

Now that we can see the cause, the “fix” is fairly straightforward. If we had that same View hierarchy, but simply set an identical contentDescription on it, we avoid the problem altogether:

<View clickable="true" accessibilityRole="button" contentDescription="Some Text">
   <Text>Some Text</Text>
</View>

This doesn’t fix the root cause, which is in Talkback’s logic itself, but it works around the issue in a way that will not present the bug to the user.

In React Native, this issue happens most commonly on <Button>, which takes a title prop and then itself renders out something like this:

<Touchable>
   <Text>{title}</Text>
</Touchable>

You can see how this looks very similar to the examples above, with <Touchable> being considered a focusable element, and the <Text> within it containing the actual content. A fix would look something like this:

<Touchable accessibilityLabel={title}>
   <Text>{title}</Text>
</Touchable>

Issue Analytics

  • State:open
  • Created 3 years ago
  • Reactions:3
  • Comments:5 (3 by maintainers)

github_iconTop GitHub Comments

1reaction
blavallacommented, Feb 24, 2021

@kacieb, @lunaleaps, @nadiia, this mostly impacts <Button> in that it has a required “title” prop that does this automatically, but it could also impact the various <Touchable> components as well, if you set the accessibilityRole on the <Touchable> itself, and not on the content. I’m not sure if those are worth worrying about, as they can take arbitrary content, and parsing out all of the text from the entire tree that is inside them would be pretty difficult to do in a performant manner. Thoughts?

0reactions
fabriziobertoglio1987commented, Jul 4, 2022
Read more comments on GitHub >

github_iconTop Results From Across the Web

Android Accessibility — Resolving common Talkback issues
Action descriptions. As Talkback navigates the elements with click actions, the announcement generated will include some information, for ...
Read more >
Android accessibility: roles and TalkBack - TetraLogical
TalkBack only announces role information for a relatively small number of user interface (UI) elements within native apps.
Read more >
Principles for improving app accessibility - Android Developers
For more details, see the following section. ... In particular, include additional text or contextual information in elements within reused ...
Read more >
Content labels - Android Accessibility Help - Google Support
If elements in a user interface don't provide content labels, it can be difficult for some users to understand the information presented to...
Read more >
Android TalkBack: Hint overwrites contentDescription
The down side to hints, is that they disappear after text is entered, ... provide an android:hint attribute instead of a content description,...
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