react-native icon indicating copy to clipboard operation
react-native copied to clipboard

Android: "accessible" prop issues.

Open amarlette opened this issue 4 years ago • 12 comments

Description

On Android there are a number of odd irregularities with how accessible prop works.

  • Sometimes this will group's descendants that are also marked as accessible together into one focusable element (e.g. accessible <View> with accessible <Text> descendants), and sometimes it does not (e.g. accessible <View> with accessible <View> descendants).
  • A <View> with accessible={true} and accessibilityLabel="foo"` is not focusable.
  • Interaction between accessible prop and collapsable prop also causes strange behavior, while fixing the issues in some cases it causes incomplete labels in others.

With this many inconsistencies with focus behavior, it's unclear what is working as intended by design, and what is working by accident, and what isn't working by design, and what isn't working by accident.

React Native version:

v0.63

Snack

https://snack.expo.io/jv1U2thqq

Expected Behavior

On Android accessible={true} should simply map to view.setFocusable(true), and accessible={false} should map to setImportantForAccessibility("NO"). This will block elements with accessible="false" from ever being considered by the accessibility system, and force elements with accessibility="true" to be focusable by the system, even if they don't normally meet the requirements.

Android Details

I think these issues all stem from the fact that "accessible" is handled in the ReactViewManager class, so it's set correctly on <View>'s, but is not set in the BaseViewManager class, so doesn't do anything at all for other components like <Text>, <Button>, etc. This is counter to the fact that these components do allow you to set accessible="true" on them, so we should expect them to work correctly.

amarlette avatar Feb 03 '21 22:02 amarlette

This one likely needs investigation from the React Native team, as the "collapsible" prop has some complex behavior trying to automatically reduce the view hierarchy, which can cause accessibility problems if a view that has accessibility properties on it is collapsed, as well as the issues with accessible lying fairly deep in the view manager classes.

Let me know if you need any help on determining what the proper behavior should be when this is used.

blavalla avatar Feb 27 '21 01:02 blavalla

Hey @blavalla! Thanks for reporting this.

I think several of the issues you noticed are actually from the Views being completely removed because they have no "height" property. This is an optimization in React Native to avoid unnecessary renders. It's definitely worth clarifying this behavior in the documentation. I think this is also related to the issue where "invisible" accessibility Views are not able to be created (https://github.com/facebook/react-native/issues/30853) - accessible things need to be backed by a real component with current implementation.

I've created a slightly updated snack to give the 3 previously unfocusable Views a small height. Those are now focusable. https://snack.expo.io/ksgL9sTHH

Nested Accessible Text Nodes

I did notice the same issues as you with Text, where these are focusable as a single element:

     <View accessible={true}>
        <Text style={styles.paragraph} accessible={true}>
          Text One
        </Text>
        <Text style={styles.paragraph} accessible={true}>
          Text Two
        </Text>
     </View>

I think what is happening here is that React Native is detecting the outer View as focusable. It then reads downward in the tree to put an "automatic" accessibility label which is all the Text within the View. The inner Text nodes are then completely ignored (even though they are marked focusable).

What is the expected behavior here? Should it focus 3 times, first on the outer View and then once on each of the inner Text nodes? Or should it ignore the outer View and focus on the inner Text nodes individually?

Nested Accessible Views

Second question - this is about the nested accessible Views:

    <View accessible={true}>
        <View
          style={styles.focusableViewRed}
          accessible={true}
          accessibilityLabel="Text One"
        />
        <View
          style={styles.focusableViewBlue}
          accessible={true}
          accessibilityLabel="Text Two"
        />
   </View>

Now that these are focusable (because I added height, I also added a background color to make it easier to see them when testing), the current behavior is to ignore the outer accessible View and instead focus twice - once on the View with label "Text One" and once on the View with label "Text Two". Is this expected? Or should it focus three times, or should it focus once and group both accessibility labels together (ex. by reading "Text One, Text Two")?

kacieb avatar Mar 03 '21 23:03 kacieb

@kacieb , both questions here are highlighting a core difference between iOS and Android.

On iOS the default behavior (and really, the only reasonably possible behavior), would be that the outer view is focusable, and must be given an accessibilityLabel to describe it. No accessible element can have accessible descendants, and all accessible elements must have an accessibility label. Apple has has this very straightforward, and easy to understand for developers.

Google on the other hand, has made things quite a bit more complicated.

On Android, the default behavior in this situation would be that all three views (the outer View and the inner two Text views) would attempt to become focusable (assume that the accessible prop maps to Androids focusable view property), but in this case the outer view would actually need some sort of label to be provided, as it has no content that it could announce. If no label was given, the outer view would become unfocusable and only the inner views would end up being focusable, which is exactly what you are seeing happen with the "Nested Text Views" example. If a label was given (and this label was mapped to the contentDescription property), then all three views would become focusable.

However, as with many things on Android related to accessibility, there are a few catches.

Catch one, If instead the layout was like this:

     <View accessible={true}>
        <Text style={styles.paragraph} accessible={true}>
          Text One
        </Text>
        <Text style={styles.paragraph}>
          Text Two
        </Text>
     </View>

The default Android behavior would actually be that the outer view and first text element would be focusable. Upon Talkback's focus evaluation of the outer view, it will automatically pull the "Text Two" text from the unfocusable child and use that text as the label. This will cause the confusing issue of the order of content being announced counterintuitively as "Text Two" (on focus of the parent) -> "Text One" (on focus of the first child). Looking at this layout, I think that very few people would expect this to be the behavior.

Catch number two, if the layout was instead like this:

     <View accessible={true} accessibilityLabel="custom label">
        <Text style={styles.paragraph}>
          Text One
        </Text>
        <Text style={styles.paragraph}>
          Text Two
        </Text>
     </View>

Then Talkback looks at this layout, and only sees the parent view as focusable. Knowing how it works from the previous example, you may expect it to announce "custom label, Text One, Text Two", since it has two unfocusable children now. But in this case, since an explicit label was provided, the unfocusable children are simply ignored. That text is never presented to the user. The assumption here is that by providing an explicit label, you are meant to describe that whole node, its children included, so the text of those children shouldn't be included. This is maybe expected if you come from an iOS background, but is also sort of a strange behavior.

Finally, the last catch relates to why these views are considered focusable. We've been working with the assumption that they are only focusable because accessible="true", but this is not the only property that can make a view focusable on Android. Android also makes all elements with onClick listeners or onLongPress listeners focusable, as well as a bunch of other less-clear edge cases. If our layout looked like this:

     <View accessible={true} accessibilityLabel="a couple of text views">
        <Text style={styles.paragraph} onClick={some callback...}>
          Text One
        </Text>
        <Text style={styles.paragraph} onClick={some callback...}>
          Text Two
        </Text>
     </View>

Then all three elements would be focusable. The parent element due to accessible="true" and having an accessibilityLabel, and the child elements due to being clickable. The label on the parent does not force the children to become unfocusable if they are explicitly defined as being focusable via either the accessible prop or some other prop that causes implicit focusability like onClick.

On iOS, this pattern would not be possible, as even children with click handlers that are descendants of an accessible element are not themselves focusable. The click handlers would instead have to be moved to the parent, or accessible="true" should not be set on it at all, and rather set on the individual child elements instead.

I am probably a bit biased, but I think Androids behavior here is probably what users expect, and it's pretty strange that on iOS you can set a click listener on something that can't ever be clicked by some users simply due to one of its ancestors having the accessible prop (which you may not even realize is happening).


This was a very roundabout way of saying that there is no real "one size fits all" approach to focusability between iOS and Android, and often even within Android itself. I think we need to decide what React Native developers expect to happen when using these properties, make sure that happens (if possible), and then document it well so that when things work differently from the default system it's explained why that is the case.

blavalla avatar Mar 03 '21 23:03 blavalla

@kacieb Another case that should be documented is when Text descendants of other Text elements are set to be accessible, but the screen reader will ignore them, see https://github.com/facebook/react-native/issues/32004

This pitfal is hitting consumers of react-native-render-html library pretty badly!

jsamr avatar Oct 16 '21 16:10 jsamr

TODOs 25th July (all completed)

  • [x] summarise all the use cases
  • [x] review Kacie comment
  • [x] test all scenarios on iOS and Android
  • [x] record test cases and include them in below comments

fabOnReact avatar Jul 25 '22 14:07 fabOnReact

accessible parent view and one not accessible child

iOS and Android have different behavior.

<View accessible={true}>
  <Text style={styles.paragraph} accessible={true}>
    Text One
  </Text>
  <Text style={styles.paragraph}>
    Text Two
  </Text>
</View>
video test on Android

  • The screenreader focuses on the parent view and announces ==> "Text Two"
  • The screenreader then focuses on the child Text One and announces ==> "Text One"

Note: On android reproduces only with the changes from PR https://github.com/facebook/react-native/pull/33076. These are the classes responsible for handling text in ReactAndroid.

video test on iOS

  • screenreader focuses on parent view and announces ==> "Text One Text Two"
result on Android Expo

  • screenreader focuses on parent view and announces ==> "Text One Text Two"

The difference in behavior is connected to the missing changes from PR https://github.com/facebook/react-native/pull/33076 in expo. Example from https://github.com/facebook/react-native/issues/30851#issuecomment-790136790.

fabOnReact avatar Jul 26 '22 03:07 fabOnReact

parent view with accessibilityLabel over-rides child Text

iOS and Android have the same behaviour.

<View accessible={true} accessibilityLabel="custom label">
  <Text style={styles.paragraph}>
    Text One
  </Text>
  <Text style={styles.paragraph}>
    Text Two
  </Text>
</View>
video test on Android

  • screenreader focuses on parent view and announces ==> "custom label"
video test on iOS

  • screenreader focuses on parent view and announces ==> "custom label"

fabOnReact avatar Jul 26 '22 03:07 fabOnReact

parent view with accessibilityLabel does not over-ride children with onPress handler

On Android the expected behaviour was:

  • the screenreader focuses on the parent view and announces ==> "a couple of text views"
  • the screenreader focuses on the first child Text and announces ==> "Text One"
  • the screenreader focuses on the second child Text and announces ==> "Text Two"

The actual behavior: Only the parent view is accessible. onPress does not trigger setClicakable(true) for Text component on Android. FIXED with PR https://github.com/facebook/react-native/pull/34284

Result: Currently there is no difference between iOS and Android (at least while using the Text component). Adding an onPress handler to a Text component does not trigger setClickable(true) on Android. I would consider it an Issue for Android Talkback to be fixed (see notes below).

Fix tracked in https://github.com/facebook/react-native/issues/30851#issuecomment-1196297746

<View accessible={true} accessibilityLabel="a couple of text views">
  <Text style={styles.paragraph} onClick={some callback...}>
    Text One
  </Text>
  <Text style={styles.paragraph} onClick={some callback...}>
    Text Two
  </Text>
</View>
video test Android

video test iOS

fabOnReact avatar Jul 26 '22 03:07 fabOnReact

screenreader difference in behavior between TalkBack and VoiceOver

Sometimes this will group's descendants that are also marked as accessible together into one focusable element (e.g. accessible with accessible descendants), and sometimes it does not (e.g. accessible with accessible descendants). It's definitely worth clarifying this behavior in the documentation

https://github.com/facebook/react-native/issues/30851#issuecomment-1194938293 https://github.com/facebook/react-native/issues/30851#issuecomment-1194944880 https://github.com/facebook/react-native/issues/30851#issuecomment-1194957300

On iOS the default behavior (and really, the only reasonably possible behavior), would be that the outer view is focusable, and must be given an accessibilityLabel to describe it. No accessible element can have accessible descendants, and all accessible elements must have an accessibility label. Apple has has this very straightforward, and easy to understand for developers.

Google on the other hand, has made things quite a bit more complicated.

On Android, the default behavior in this situation would be that all three views (the outer View and the inner two Text views) would attempt to become focusable (assume that the accessible prop maps to Androids focusable view property), but in this case the outer view would actually need some sort of label to be provided, as it has no content that it could announce. If no label was given, the outer view would become unfocusable and only the inner views would end up being focusable, which is exactly what you are seeing happen with the "Nested Text Views" example. If a label was given (and this label was mapped to the contentDescription property), then all three views would become focusable.

PR https://github.com/facebook/react-native-website/pull/3226

fabOnReact avatar Jul 26 '22 12:07 fabOnReact

view with no height and its children are not accessible with TalkBack

Interaction between accessible prop and collapsable prop also causes strange behavior, while fixing the issues in some cases it causes incomplete labels in others.

<View style={{height: 0}} accessible={true}>
   <Text accessible={true}>text to be announced</Text>
</View>

Tried the following solutions:

Related https://github.com/facebook/react-native/issues/27333#issuecomment-646135349

iOS/Android: Allowance of focus on non-view-backed elements #30853 The ability to create "virtual views" for accessibility would allow for making significantly more complex UI (charts, graphs, etc.), or have significant performance improvements (rendering multiple images to one bitmap, while keeping them individually focusable). An API would need to be created that allows you to mark a component as not needing to create an actual View, but instead a virtual view for accessibility. This would likely be used to create virtual child views of some actual view-backed parent. These virtual views would not have any impact for users who aren't using accessibility services, and could simply not be rendered at all for these users.

  • [ ] create "virtual views" for accessibility https://github.com/facebook/react-native/pull/33215 to (issue https://github.com/facebook/react-native/issues/30853)
  • [ ] otherwise adapt documentation also for Android
  • [ ] ~create View/Text clickable with hitslop with height of 0px~
  • [ ] ~debug logic included in comment~

fabOnReact avatar Jul 26 '22 13:07 fabOnReact

accessible prop not working on some components

Kacie commented on the different behavior on Android between Nested Accessible Text Nodes and Nested Accessible Views https://github.com/facebook/react-native/issues/30851#issuecomment-790136790

The original issue with the Text component accessible prop was fixed with the changes from PR https://github.com/facebook/react-native/pull/33076.

The fact that "accessible" is handled in the ReactViewManager class, so it's set correctly but not in the BaseViewManager class, so it doesn't do anything at all for other components like Text, etc. This is counter to the fact that these components allow you to set accessible="true" on them, so we should expect them to work correctly.

  • [x] ~considering adding an accessible prop to BaseViewManager and removing it from the component ViewManager~
  • [x] read ReactViewManager and BaseViewManager accessible setters
  • [x] verify if the accessible prop does not work on other components * Pressable, TouchableOpacity, Switch, TextInput, and TouchableNativeFeedback are focusable/accessible by default without an onPress handler * <TouchableOpacity> is accessible, <TouchableOpacity accessible={false}> is not accessible, <TouchableOpacity accessible={false} onPress={() => console.log('pressed')}> is accessible
  • [x] read how onPress prop is set in TouchableOpacity react-native sourcecode

fabOnReact avatar Jul 27 '22 05:07 fabOnReact

Text onPress handler does not trigger setClickable(true) on Android

Finally, the last catch relates to why these views are considered focusable. We've been working with the assumption that they are only focusable because accessible="true", but this is not the only property that can make a view focusable on Android. Android also makes all elements with onClick listeners or onLongPress listeners focusable, as well as a bunch of other less-clear edge cases. If our layout looked like this:

adding onPress handler to a Text Component does not call setClickable(true) (test case)

PR https://github.com/facebook/react-native/pull/34284

  • [x] verify that the functionality works on other components (Touchable)
  • [x] change accessible prop to true if onPress handler available in Text JavaScript Component (VIDEO)
  • [x] review existing use cases of setClickable() in ReactAndroid
  • [x] ~call setClickable when onPress handler added/removed~

fabOnReact avatar Jul 27 '22 05:07 fabOnReact