Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Android: Blocking elements from being focused #30850

Closed
amarlette opened this issue Feb 3, 2021 · 10 comments · May be fixed by facebook/react-native-website#3226
Closed

Android: Blocking elements from being focused #30850

amarlette opened this issue Feb 3, 2021 · 10 comments · May be fixed by facebook/react-native-website#3226
Labels
Accessibility Team - Evaluated Accessibility Good first issue Interested in collaborating? Take a stab at fixing one of these issues. Platform: Android Android applications.

Comments

@amarlette
Copy link

amarlette commented Feb 3, 2021

Description

  • ImportantForAccessibility="no" does not work on Text, Button, or Picker.
  • ImportantForAccessibility="no-hide-descendants" does not work on Text, Button, Picker, or ImageBackground.
  • Note: On ImageBackground, focus is prevented on the imageBackground node itself, but focus is not prevented on its descendants.

React Native version:

v0.63

Expected Behavior

Setting ImportantForAccessibility to "no" should block focus on that element, regardless of other properties that may indicate that it should be focusable (such as accessible={true}, focusable={true} onPress, etc.). Descendant elements however, may remain focusable. Setting ImportantForAccessibility to "no-hide-descendants" should block focus from this element, and any descendant element.

Setting ImportantForAccessibility to "yes" does not force an element to become focusable, but simply does not explicitly exclude it. It may become focusable if it was previously excluded by having its importantForAccessibility property set to no elsewhere.

Snack

https://snack.expo.io/ERFEn96ia

Android Details

This property should directly correlate with the View's importantForAccessibility property, and is currently being mapped correctly here:

Likely cause

The reason this doesn't work on some components and does work on others is likely due to some components them not strictly defining their props, so passing in standard ViewProps (like importantForAccessibility) works, but isn't intended behavior. Unless there is a good reason to not allow accessibility properties on a component, they should be supported.

This is likely a Javascript-only issue, and not related to any incorrect mapping on the Android side.

@blavalla blavalla added Accessibility Team - Evaluated Good first issue Interested in collaborating? Take a stab at fixing one of these issues. labels Feb 17, 2021
@grgr-dkrk
Copy link
Contributor

grgr-dkrk commented Apr 4, 2021

I'd like to work on this issue.

The Picker is deprecated, so I also work with @react-native-picker/picker.

@amarlette
Copy link
Author

Thank you @grgr-dkrk! I'll move this to in-progress

@aweary
Copy link

aweary commented Jul 14, 2022

@blavalla wondering if y'alls definition of "focus" here also includes keyboard focus? we're running into a problem where we're unable to prevent external bluetooth keyboards from focusing elements in offscreen views (hidden with importantForAccessibility="no-hide-descendants").

Does this issue track that behavior by your current understanding or should I create another?

@blavalla
Copy link
Contributor

@aweary , This one was specifically about screen reader focus.

Generally speaking, on Android screen reader focus and keyboard focus are two separate concepts. Most of the time screen reader focus will follow keyboard focus (for example if an input is auto-focused so that a users on screen keyboard appears), but not always (for example when a screen reader user swipes to navigate around keyboard focus can remain in the input).
The "importantForAccessibility" property only applies to screen reader focus, and not keyboard focus, as what it does under the hood is prevent accessibility events (such as focus events), hence blocking accessibility services from knowing when/where to move focus to.

If you are trying to prevent an off-screen element from being keyboard focusable, but you do want it to be screen reader focusable, then I think there isn't a convenient way to currently handle this in RN. In standard Android you'd set focusable="false" on the View to prevent keyboard focus, and then screenReaderFocusable="true" to still allow screen reader focus. I think your best bet would be to remove the thing that is making this keyboard focusable in the first place. Generally, only interactive elements such as those with a click handler, are going to get keyboard focus by default on Android, so if you don't need this element to be clickable, removing that will likely block keyboard focus from the element. If you do still need it to be clickable, but only for screen readers, you may be able to get this to work by using onAccessibilityTap or setting accessibilityActions to have an activate action. This, in theory at least as I've never tested this scenario, should keep this element interactive for screen readers, but not interactive for keyboards.

If you want this element to be not focusable by either keyboards or screen readers, then you'll just want to set accessible="false" on the element, which on Android maps directly to focusable="false".

@aweary
Copy link

aweary commented Jul 14, 2022

@blavalla I have a view that I don't want to be focusable by keyboards or screen reader, but even with <View accessible={false}> descendants are still being focused when tabbing with a physical keyboard. Are you saying accessible={false} should also prevent physical keyboard focus for descendants similar to no-hide-descendants for screen readers?

In this situation we cannot just remove the offscreen view for performance reasons.

@blavalla
Copy link
Contributor

blavalla commented Jul 14, 2022

@aweary, Ah, no accessible="false" just sets focusable="false" behind the scenes, which only applies to the element its set on, but not it's descendants. It's totally valid on Android to have something like this (android psuedo-code):

<View focusable="false">  <-- is not focusable by keyboard
  <View onClick="..." /> <-- is focusable by keyboard
</View>

The keyboard-focus equivalent of 'no-hide-descendants' would be to set the descendantFocusability property on the parent, along with the focusable property.

<View focusable="false" descendantFocusability="FOCUS_BLOCK_DESCENDANTS">  <-- is not focusable by keyboard
  <View onClick="..." /> <-- is also not focusable by keyboard, even though it has an onClick
</View>

Another common approach is to hide these elements with Android's "visibility" property. If this is set to INVISIBLE or GONE the element won't be focusable by a keyboard, and it's descendants will inherit this visibility. I'm guessing that either in RN thar "display: none" doesn't map to one of these (I think these go through Yoga, which I am not very familiar with), or you aren't using it because you want this to actually be drawn for performance reasons.

<View visibility="GONE">  <-- is not focusable by keyboard
  <View onClick="..." /> <-- is also not focusable by keyboard, even though it has an onClick
</View>

I think to really robustly support keyboard focus in RN we'd need to have separate properties for keyboard focus that aren't also manipulating accessibility focus. Something like having a "focusable" prop on all components that could be set to "true", "false", and "false-also-hide-descendants" would solve this use case for you, but unfortunately doesn't currently exist.

If you're interested in adding support for this though, feel free to make a PR and tag me on it, as I'd be happy to review.

@fabOnReact
Copy link
Contributor

fabOnReact commented Jul 22, 2022

importantForAccessibility="no" does not allow screenreader focus on nested Text Components with accessibilityRole="link" or inline Images

Nested Test with accessibilityRole="link" is accessible when parent sets importantForAccessibility="yes"

https://github.com/facebook/react-native/pull/33215/files#diff-37199b6b562523b453547e7fb56c95fd9aed5dc01fee3f6bdd50e97297ff8e7fR78
https://developer.android.com/reference/android/view/View#IMPORTANT_FOR_ACCESSIBILITY_YES
#30375 (comment)

sourcecode of the example

class AccessibilityExample extends React.Component<{}> {
  render(): React.Node {
    const uri =
      'https://portfoliofabrizio.s3.eu-central-1.amazonaws.com/videos/surfcheck-poster.png';
    return (
      <View>
        <RNTesterBlock title="TextView without label">
          <Text importantForAccessibility="no" accessible={true}>
            This is a Text with an Image as nested Child and a Link.
            <Text
              accessibilityRole="link">
              This is a link
            </Text>
            <Image
              accessible={true}
              focusable={true}
              importantForAccessibility="yes"
              accessibilityLabel="image accessibilityLabel"
              source={{uri}}
              style={{height: 400, width: 200}}
            />
          </Text>
        </RNTesterBlock>
      </View>
    );
  }
}

edited.copy.mp4
Nested Test with accessibilityRole="link" is **not** accessible when parent sets importantForAccessibility="no"

2022-07-22.15-47-18.mp4

ExploreByTouchHelper#requestAccessibiltyFocus

https://github.com/aosp-mirror/platform_frameworks_base/blob/19e53cfdc8a5c6ef45c0adf2dd239576ddce5822/core/java/com/android/internal/widget/ExploreByTouchHelper.java#L595

    /**
     * Attempts to give accessibility focus to a virtual view.
     * <p>
     * A virtual view will not actually take focus if
     * {@link AccessibilityManager#isEnabled()} returns false,
     * {@link AccessibilityManager#isTouchExplorationEnabled()} returns false,
     * or the view already has accessibility focus.
     *
     * @param virtualViewId The id of the virtual view on which to place
     *            accessibility focus.
     * @return Whether this virtual view actually took accessibility focus.
     */
    private boolean requestAccessibilityFocus(int virtualViewId) {
        final AccessibilityManager accessibilityManager =
                (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);

        if (!mManager.isEnabled()
                || !accessibilityManager.isTouchExplorationEnabled()) {
            return false;
        }
        // TODO: Check virtual view visibility.
        if (!isAccessibilityFocused(virtualViewId)) {
            // Clear focus from the previously focused view, if applicable.
            if (mFocusedVirtualViewId != INVALID_ID) {
                sendEventForVirtualView(mFocusedVirtualViewId,
                        AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
            }

            // Set focus on the new view.
            mFocusedVirtualViewId = virtualViewId;

            // TODO: Only invalidate virtual view bounds.
            mView.invalidate();
            sendEventForVirtualView(virtualViewId,
                    AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
            return true;
        }
        return false;
    }

View#onFocusChanged

https://github.com/aosp-mirror/platform_frameworks_base/blob/19e53cfdc8a5c6ef45c0adf2dd239576ddce5822/core/java/android/view/View.java#L12837

    /**
     * Called by the view system when the focus state of this view changes.
     * When the focus change event is caused by directional navigation, direction
     * and previouslyFocusedRect provide insight into where the focus is coming from.
     * When overriding, be sure to call up through to the super class so that
     * the standard focus handling will occur.
     *
     * @param gainFocus True if the View has focus; false otherwise.
     * @param direction The direction focus has moved when requestFocus()
     *                  is called to give this view focus. Values are
     *                  {@link #FOCUS_UP}, {@link #FOCUS_DOWN}, {@link #FOCUS_LEFT},
     *                  {@link #FOCUS_RIGHT}, {@link #FOCUS_FORWARD}, or {@link #FOCUS_BACKWARD}.
     *                  It may not always apply, in which case use the default.
     * @param previouslyFocusedRect The rectangle, in this view's coordinate
     *        system, of the previously focused view.  If applicable, this will be
     *        passed in as finer grained information about where the focus is coming
     *        from (in addition to direction).  Will be <code>null</code> otherwise.
     */
    @CallSuper
    protected void onFocusChanged(boolean gainFocus, @FocusDirection int direction,
            @Nullable Rect previouslyFocusedRect) {
        if (gainFocus) {
            sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
        } else {
            notifyViewAccessibilityStateChangedIfNeeded(
                    AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
        }

        // Here we check whether we still need the default focus highlight, and switch it on/off.
        switchDefaultFocusHighlight();

        if (!gainFocus) {
            if (isPressed()) {
                setPressed(false);
            }
            if (hasWindowFocus()) {
                notifyFocusChangeToImeFocusController(false /* hasFocus */);
            }
            onFocusLost();
        } else if (hasWindowFocus()) {
            notifyFocusChangeToImeFocusController(true /* hasFocus */);
        }

        invalidate(true);
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnFocusChangeListener != null) {
            li.mOnFocusChangeListener.onFocusChange(this, gainFocus);
        }

        if (mAttachInfo != null) {
            mAttachInfo.mKeyDispatchState.reset(this);
        }

        if (mParent != null) {
            mParent.onDescendantUnbufferedRequested();
        }

        notifyEnterOrExitForAutoFillIfNeeded(gainFocus);
    }

View#findAccessibilityFocusHost

    /**
     * Returns the view within this view's hierarchy that is hosting
     * accessibility focus.
     *
     * @param searchDescendants whether to search for focus in descendant views
     * @return the view hosting accessibility focus, or {@code null}
     */
    private View findAccessibilityFocusHost(boolean searchDescendants) {
        if (isAccessibilityFocusedViewOrHost()) {
            return this;
        }

        if (searchDescendants) {
            final ViewRootImpl viewRoot = getViewRootImpl();
            if (viewRoot != null) {
                final View focusHost = viewRoot.getAccessibilityFocusedHost();
                if (focusHost != null && ViewRootImpl.isViewDescendantOf(focusHost, this)) {
                    return focusHost;
                }
            }
        }

        return null;
    }

@fabOnReact
Copy link
Contributor

fabOnReact commented Jul 22, 2022

Text that has a nested child Image is 1 unique element (the child becomes a Span on Native Android)

sourcecode of the example

class AccessibilityExample extends React.Component<{}> {
  render(): React.Node {
    const uri =
      'https://portfoliofabrizio.s3.eu-central-1.amazonaws.com/videos/surfcheck-poster.png';
    return (
      <View>
        <RNTesterBlock title="TextView without label">
          <Text importantForAccessibility="yes" accessible={true}>
            This is a Text with an Image as nested Child.
            <Image
              accessible={true}
              importantForAccessibility="yes"
              accessibilityLabel="image accessibilityLabel"
              source={{uri}}
              style={{height: 400, width: 200}}
            />
          </Text>
        </RNTesterBlock>
      </View>
    );
  }
}


2022-07-22.15-54-15.mp4

Nested child Image is accessible

example sourcecode

class AccessibilityExample extends React.Component<{}> {
  render(): React.Node {
    const uri =
      'https://portfoliofabrizio.s3.eu-central-1.amazonaws.com/videos/surfcheck-poster.png';
    return (
      <View>
        <RNTesterBlock title="TextView without label">
          <Text importantForAccessibility="yes" accessible={true}>
            This is a Text with an Image as nested Child.
            <View accessible={true} focusable={true}>
              <Image
                accessibilityLabel="image accessibilityLabel"
                source={{uri}}
                style={{height: 400, width: 200}}
              />
            </View>
          </Text>
        </RNTesterBlock>
      </View>
    );
  }
}

2022-07-22.16-08-35.mp4

@fabOnReact
Copy link
Contributor

fabOnReact commented Jul 28, 2022

TODOs - importantForAccessibility="no" does not allow screenreader focus on nested Text Components with accessibilityRole="link" or inline Images

  • Read ReactAccessibiltyDelegate and ExploreByTouchHelper 15+15min
  • Debug Parent VirtualView with importantForAccessibility="no"
  • Read android View, ExploreByTouchHelper, and jetpack sourcecode 15+15min|
    Search for a method to control the screenreader focus (requestFocus,)
  • Debug Parent VirtualView with importantForAccessibility="yes"
  • Trigger Screenreader focus to move to next child (sendEventForVirtualView)
  • Avoid focusing on element with importantForAccessibility="no" (AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED)
  • the element with importantForAccessibility="no" is focusable when getVirtualViewAt is not called
    find the method used to trigger focus on that element when swiping outside of the virtual view (getVirtualViewAt is not called in this case) and change the logic (dispatchHoverEvent, 9c730f5)
    over-riding performAccessibilityAction to focus on the virtualView

fabOnReact added a commit to fabOnReact/react-native that referenced this issue Jul 29, 2022
fabOnReact added a commit to fabOnReact/react-native-website that referenced this issue Aug 3, 2022
…ested Text Components with accessibilityRole="link" or inline Images

facebook/react-native#30850 (comment)
<details><summary>Nested Test with accessibilityRole="link" is accessible when parent sets importantForAccessibility="yes"</summary>
<p>

https://github.com/facebook/react-native/pull/33215/files#diff-37199b6b562523b453547e7fb56c95fd9aed5dc01fee3f6bdd50e97297ff8e7fR78
https://developer.android.com/reference/android/view/View#IMPORTANT_FOR_ACCESSIBILITY_YES
facebook/react-native#30375 (comment)

<details><summary>sourcecode of the example</summary>
<p>

```javascript
class AccessibilityExample extends React.Component<{}> {
  render(): React.Node {
    const uri =
      'https://portfoliofabrizio.s3.eu-central-1.amazonaws.com/videos/surfcheck-poster.png';
    return (
      <View>
        <RNTesterBlock title="TextView without label">
          <Text importantForAccessibility="no" accessible={true}>
            This is a Text with an Image as nested Child and a Link.
            <Text
              accessibilityRole="link">
              This is a link
            </Text>
            <Image
              accessible={true}
              focusable={true}
              importantForAccessibility="yes"
              accessibilityLabel="image accessibilityLabel"
              source={{uri}}
              style={{height: 400, width: 200}}
            />
          </Text>
        </RNTesterBlock>
      </View>
    );
  }
}
```

</p>

</details>

<video src="https://user-images.githubusercontent.com/24992535/180412450-071467b1-41ca-4818-b8cc-cbe966b65859.mp4" width="1000" />

</p>
</details>

<details><summary>Nested Test with accessibilityRole="link" is **not** accessible when parent sets importantForAccessibility="no"</summary>
<p>

<video src="https://user-images.githubusercontent.com/24992535/180390384-62129561-5d80-417b-8146-15acee128f76.mp4" width="1000" />

</p>
</details>

<details><summary>ExploreByTouchHelper#requestAccessibiltyFocus</summary>
<p>

https://github.com/aosp-mirror/platform_frameworks_base/blob/19e53cfdc8a5c6ef45c0adf2dd239576ddce5822/core/java/com/android/internal/widget/ExploreByTouchHelper.java#L595

```java
    /**
     * Attempts to give accessibility focus to a virtual view.
     * <p>
     * A virtual view will not actually take focus if
     * {@link AccessibilityManager#isEnabled()} returns false,
     * {@link AccessibilityManager#isTouchExplorationEnabled()} returns false,
     * or the view already has accessibility focus.
     *
     * @param virtualViewId The id of the virtual view on which to place
     *            accessibility focus.
     * @return Whether this virtual view actually took accessibility focus.
     */
    private boolean requestAccessibilityFocus(int virtualViewId) {
        final AccessibilityManager accessibilityManager =
                (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);

        if (!mManager.isEnabled()
                || !accessibilityManager.isTouchExplorationEnabled()) {
            return false;
        }
        // TODO: Check virtual view visibility.
        if (!isAccessibilityFocused(virtualViewId)) {
            // Clear focus from the previously focused view, if applicable.
            if (mFocusedVirtualViewId != INVALID_ID) {
                sendEventForVirtualView(mFocusedVirtualViewId,
                        AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
            }

            // Set focus on the new view.
            mFocusedVirtualViewId = virtualViewId;

            // TODO: Only invalidate virtual view bounds.
            mView.invalidate();
            sendEventForVirtualView(virtualViewId,
                    AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
            return true;
        }
        return false;
    }
```

</p>
</details>

<details><summary>View#onFocusChanged</summary>
<p>

https://github.com/aosp-mirror/platform_frameworks_base/blob/19e53cfdc8a5c6ef45c0adf2dd239576ddce5822/core/java/android/view/View.java#L12837

```java
    /**
     * Called by the view system when the focus state of this view changes.
     * When the focus change event is caused by directional navigation, direction
     * and previouslyFocusedRect provide insight into where the focus is coming from.
     * When overriding, be sure to call up through to the super class so that
     * the standard focus handling will occur.
     *
     * @param gainFocus True if the View has focus; false otherwise.
     * @param direction The direction focus has moved when requestFocus()
     *                  is called to give this view focus. Values are
     *                  {@link #FOCUS_UP}, {@link #FOCUS_DOWN}, {@link #FOCUS_LEFT},
     *                  {@link #FOCUS_RIGHT}, {@link #FOCUS_FORWARD}, or {@link #FOCUS_BACKWARD}.
     *                  It may not always apply, in which case use the default.
     * @param previouslyFocusedRect The rectangle, in this view's coordinate
     *        system, of the previously focused view.  If applicable, this will be
     *        passed in as finer grained information about where the focus is coming
     *        from (in addition to direction).  Will be <code>null</code> otherwise.
     */
    @callsuper
    protected void onFocusChanged(boolean gainFocus, @FocusDirection int direction,
            @nullable Rect previouslyFocusedRect) {
        if (gainFocus) {
            sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
        } else {
            notifyViewAccessibilityStateChangedIfNeeded(
                    AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
        }

        // Here we check whether we still need the default focus highlight, and switch it on/off.
        switchDefaultFocusHighlight();

        if (!gainFocus) {
            if (isPressed()) {
                setPressed(false);
            }
            if (hasWindowFocus()) {
                notifyFocusChangeToImeFocusController(false /* hasFocus */);
            }
            onFocusLost();
        } else if (hasWindowFocus()) {
            notifyFocusChangeToImeFocusController(true /* hasFocus */);
        }

        invalidate(true);
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnFocusChangeListener != null) {
            li.mOnFocusChangeListener.onFocusChange(this, gainFocus);
        }

        if (mAttachInfo != null) {
            mAttachInfo.mKeyDispatchState.reset(this);
        }

        if (mParent != null) {
            mParent.onDescendantUnbufferedRequested();
        }

        notifyEnterOrExitForAutoFillIfNeeded(gainFocus);
    }
```
</p>
</details>

<details><summary>View#findAccessibilityFocusHost</summary>
<p>

```java
    /**
     * Returns the view within this view's hierarchy that is hosting
     * accessibility focus.
     *
     * @param searchDescendants whether to search for focus in descendant views
     * @return the view hosting accessibility focus, or {@code null}
     */
    private View findAccessibilityFocusHost(boolean searchDescendants) {
        if (isAccessibilityFocusedViewOrHost()) {
            return this;
        }

        if (searchDescendants) {
            final ViewRootImpl viewRoot = getViewRootImpl();
            if (viewRoot != null) {
                final View focusHost = viewRoot.getAccessibilityFocusedHost();
                if (focusHost != null && ViewRootImpl.isViewDescendantOf(focusHost, this)) {
                    return focusHost;
                }
            }
        }

        return null;
    }
```

</p>
</details>
roryabraham pushed a commit to Expensify/react-native that referenced this issue Aug 17, 2022
…acebook#34245)

Summary:
Previously published by [grgr-dkrk][2] as [https://github.com/facebook/react-native/issues/31296][3], fixes the following issues:

1) ImportantForAccessibility="no" does not work on Text, Button
2) ImportantForAccessibility="no-hide-descendants" does not work on Text, Button, or ImageBackground.

Note: On ImageBackground, focus is prevented on the imageBackground node itself, but focus is not prevented on its descendants.

Note: [Button component expected behavior for prop importantForAccessibility][4]
>Some components like Button seem like atomic units, but they end up rendering a hierarchy of components for example a wrapper View with a Text inside them. Allowing those descendants to become focusable, breaks the model of these being a single component to consider and forcing no-hide-descendants makes sense here.

>The other option is always to render any descendants of these elements with importantForAccessibility="no", so they can never be focusable on their own. This would have the same result, **BUT may potentially cause issues when the descendant content is supposed to automatically get moved up to the focusable ancestor to act as a label** (which is what Talkback does with unfocusable text elements by default).

Note: [importantForAccessibility="no" does not allow screenreader focus on nested Text Components with accessibilityRole="link" or inline Images][5]

fixes facebook#30850 related facebook#33690

## Changelog

[Android] [Fixed] - adding importantForAccessibility for Text, Button, ImageBackground

Pull Request resolved: facebook#34245

Test Plan:
1) testing ImageBackground importantForAccessiblity ([link to test][1])
2) importantForAccessibility="no" does not allow screenreader focus on nested Text Components with accessibilityRole="link" or inline Images ([link to test][5])
3) testing ImageBackground importantForAccessiblity ([link to test][6])
4) Button with importantForAccessibility=no ([link to test][7])

[1]: facebook#31296 (comment) ""
[2]: https://github.com/grgr-dkrk "grgr-dkrk"
[3]: facebook#31296 "facebook#31296"
[4]: facebook#31296 (comment) "expected behaviour with prop importantForAccessibility in Button component"
[5]: facebook#30850 (comment) "importantForAccessibility=no does not allow screenreader focus on nested Text Components with accessibilityRole=link or inline Images"
[6]: facebook#34245 (comment) "testing ImageBackground importantForAccessiblity"
[7]: facebook#34245 (comment) "Button with importantForAccessibility=no"

Reviewed By: cipolleschi

Differential Revision: D38121992

Pulled By: dmitryrykun

fbshipit-source-id: 368b4dcb47d7940274820aa2e39ed5e2ca068821
roryabraham pushed a commit to Expensify/react-native that referenced this issue Aug 17, 2022
…acebook#34245)

Summary:
Previously published by [grgr-dkrk][2] as [https://github.com/facebook/react-native/issues/31296][3], fixes the following issues:

1) ImportantForAccessibility="no" does not work on Text, Button
2) ImportantForAccessibility="no-hide-descendants" does not work on Text, Button, or ImageBackground.

Note: On ImageBackground, focus is prevented on the imageBackground node itself, but focus is not prevented on its descendants.

Note: [Button component expected behavior for prop importantForAccessibility][4]
>Some components like Button seem like atomic units, but they end up rendering a hierarchy of components for example a wrapper View with a Text inside them. Allowing those descendants to become focusable, breaks the model of these being a single component to consider and forcing no-hide-descendants makes sense here.

>The other option is always to render any descendants of these elements with importantForAccessibility="no", so they can never be focusable on their own. This would have the same result, **BUT may potentially cause issues when the descendant content is supposed to automatically get moved up to the focusable ancestor to act as a label** (which is what Talkback does with unfocusable text elements by default).

Note: [importantForAccessibility="no" does not allow screenreader focus on nested Text Components with accessibilityRole="link" or inline Images][5]

fixes facebook#30850 related facebook#33690

## Changelog

[Android] [Fixed] - adding importantForAccessibility for Text, Button, ImageBackground

Pull Request resolved: facebook#34245

Test Plan:
1) testing ImageBackground importantForAccessiblity ([link to test][1])
2) importantForAccessibility="no" does not allow screenreader focus on nested Text Components with accessibilityRole="link" or inline Images ([link to test][5])
3) testing ImageBackground importantForAccessiblity ([link to test][6])
4) Button with importantForAccessibility=no ([link to test][7])

[1]: facebook#31296 (comment) ""
[2]: https://github.com/grgr-dkrk "grgr-dkrk"
[3]: facebook#31296 "facebook#31296"
[4]: facebook#31296 (comment) "expected behaviour with prop importantForAccessibility in Button component"
[5]: facebook#30850 (comment) "importantForAccessibility=no does not allow screenreader focus on nested Text Components with accessibilityRole=link or inline Images"
[6]: facebook#34245 (comment) "testing ImageBackground importantForAccessiblity"
[7]: facebook#34245 (comment) "Button with importantForAccessibility=no"

Reviewed By: cipolleschi

Differential Revision: D38121992

Pulled By: dmitryrykun

fbshipit-source-id: 368b4dcb47d7940274820aa2e39ed5e2ca068821
@RowlandOti
Copy link

@aweary , This one was specifically about screen reader focus.

Generally speaking, on Android screen reader focus and keyboard focus are two separate concepts. Most of the time screen reader focus will follow keyboard focus (for example if an input is auto-focused so that a users on screen keyboard appears), but not always (for example when a screen reader user swipes to navigate around keyboard focus can remain in the input). The "importantForAccessibility" property only applies to screen reader focus, and not keyboard focus, as what it does under the hood is prevent accessibility events (such as focus events), hence blocking accessibility services from knowing when/where to move focus to.

If you are trying to prevent an off-screen element from being keyboard focusable, but you do want it to be screen reader focusable, then I think there isn't a convenient way to currently handle this in RN. In standard Android you'd set focusable="false" on the View to prevent keyboard focus, and then screenReaderFocusable="true" to still allow screen reader focus. I think your best bet would be to remove the thing that is making this keyboard focusable in the first place. Generally, only interactive elements such as those with a click handler, are going to get keyboard focus by default on Android, so if you don't need this element to be clickable, removing that will likely block keyboard focus from the element. If you do still need it to be clickable, but only for screen readers, you may be able to get this to work by using onAccessibilityTap or setting accessibilityActions to have an activate action. This, in theory at least as I've never tested this scenario, should keep this element interactive for screen readers, but not interactive for keyboards.

If you want this element to be not focusable by either keyboards or screen readers, then you'll just want to set accessible="false" on the element, which on Android maps directly to focusable="false".

@aweary did this resolve it for you? I am facing similar issues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment