-
Notifications
You must be signed in to change notification settings - Fork 24.3k
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
Android: Blocking elements from being focused #30850
Comments
I'd like to work on this issue. The |
Thank you @grgr-dkrk! I'll move this to in-progress |
@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 Does this issue track that behavior by your current understanding or should I create another? |
@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). 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 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". |
@blavalla I have a view that I don't want to be focusable by keyboards or screen reader, but even with In this situation we cannot just remove the offscreen view for performance reasons. |
@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):
The keyboard-focus equivalent of 'no-hide-descendants' would be to set the descendantFocusability property on the parent, along with the focusable property.
Another common approach is to hide these elements with Android's "visibility" property. If this is set to
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. |
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 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.mp4Nested Test with accessibilityRole="link" is **not** accessible when parent sets importantForAccessibility="no"
2022-07-22.15-47-18.mp4ExploreByTouchHelper#requestAccessibiltyFocus
/**
* 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
/**
* 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;
} |
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>
);
}
} react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java Line 236 in 1237952
2022-07-22.15-54-15.mp4Nested 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 |
|
…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>
…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
…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
@aweary did this resolve it for you? I am facing similar issues. |
Description
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:
react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java
Line 267 in 49f10fd
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.
The text was updated successfully, but these errors were encountered: