-
Notifications
You must be signed in to change notification settings - Fork 24.9k
Add support for screen reader blur and focus (accessibility) #24642
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1098,6 +1098,8 @@ const TextInput = createReactClass({ | |
| <TouchableWithoutFeedback | ||
| onLayout={props.onLayout} | ||
| onPress={this._onPress} | ||
| onAccessibilityBlur={this._onAccessibilityBlur} | ||
| onAccessibilityFocus={this._onAccessibilityFocus} | ||
| rejectResponderTermination={true} | ||
| accessible={props.accessible} | ||
| accessibilityLabel={props.accessibilityLabel} | ||
|
|
@@ -1150,6 +1152,8 @@ const TextInput = createReactClass({ | |
| <TouchableWithoutFeedback | ||
| onLayout={props.onLayout} | ||
| onPress={this._onPress} | ||
| onAccessibilityBlur={this._onAccessibilityBlur} | ||
| onAccessibilityFocus={this._onAccessibilityFocus} | ||
| rejectResponderTermination={props.rejectResponderTermination} | ||
| accessible={props.accessible} | ||
| accessibilityLabel={props.accessibilityLabel} | ||
|
|
@@ -1208,6 +1212,8 @@ const TextInput = createReactClass({ | |
| <TouchableWithoutFeedback | ||
| onLayout={props.onLayout} | ||
| onPress={this._onPress} | ||
| onAccessibilityBlur={this._onAccessibilityBlur} | ||
| onAccessibilityFocus={this._onAccessibilityFocus} | ||
| accessible={this.props.accessible} | ||
| accessibilityLabel={this.props.accessibilityLabel} | ||
| accessibilityRole={this.props.accessibilityRole} | ||
|
|
@@ -1325,6 +1331,14 @@ const TextInput = createReactClass({ | |
| } | ||
| }, | ||
|
|
||
| _onAccessibilityBlur: function(event: BlurEvent) { | ||
| this.props.oncAccessibilityBlur && this.props.oncAccessibilityBlur(event); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. typo oncAccessibilityBlur -> onAccessibilityBlur |
||
| }, | ||
|
|
||
| _onAccessibilityFocus: function(event: BlurEvent) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shoudln't this be AccessibilityFocusEvent not BlurEvent? |
||
| this.props.oncAccessibilityFocus && this.props.oncAccessibilityFocus(event); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. typo oncAccessibilityFocus -> onAccessibilityFocus |
||
| }, | ||
|
|
||
| _onTextInput: function(event: TextInputEvent) { | ||
| this.props.onTextInput && this.props.onTextInput(event); | ||
| }, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,29 +6,44 @@ | |
| package com.facebook.react.uimanager; | ||
|
|
||
| import android.content.Context; | ||
| import androidx.core.view.AccessibilityDelegateCompat; | ||
| import androidx.core.view.ViewCompat; | ||
| import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; | ||
| import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionItemInfoCompat; | ||
| import android.text.SpannableString; | ||
| import android.text.style.URLSpan; | ||
| import android.view.View; | ||
| import android.view.accessibility.AccessibilityEvent; | ||
| import androidx.core.view.AccessibilityDelegateCompat; | ||
| import androidx.core.view.ViewCompat; | ||
| import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; | ||
| import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionItemInfoCompat; | ||
| import android.view.View; | ||
| import com.facebook.react.bridge.ReadableArray; | ||
| import com.facebook.react.R; | ||
| import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; | ||
|
|
||
| import java.util.Locale; | ||
| import javax.annotation.Nullable; | ||
|
|
||
| import com.facebook.react.R; | ||
| import com.facebook.react.bridge.Arguments; | ||
| import com.facebook.react.bridge.ReactContext; | ||
| import com.facebook.react.bridge.ReadableArray; | ||
| import com.facebook.react.bridge.ReadableMap; | ||
| import com.facebook.react.bridge.WritableMap; | ||
| import com.facebook.react.uimanager.events.RCTEventEmitter; | ||
|
|
||
| /** | ||
| * Utility class that handles the addition of a "role" for accessibility to | ||
| * either a View or AccessibilityNodeInfo. | ||
| */ | ||
|
|
||
| public class AccessibilityDelegateUtil { | ||
|
|
||
| public static final String BLUR_EVENT_NAME = "onAccessibilityBlur"; | ||
| public static final String FOCUS_EVENT_NAME = "onAccessibilityFocus"; | ||
|
|
||
| public static String getBlurEventName() { | ||
| return BLUR_EVENT_NAME; | ||
| } | ||
|
|
||
| public static String getFocusEventName() { | ||
| return FOCUS_EVENT_NAME; | ||
| } | ||
|
|
||
| /** | ||
| * These roles are defined by Google's TalkBack screen reader, and this list | ||
| * should be kept up to date with their implementation. Details can be seen in | ||
|
|
@@ -108,17 +123,34 @@ public static void setDelegate(final View view) { | |
| // if a view already has an accessibility delegate, replacing it could cause | ||
| // problems, | ||
| // so leave it alone. | ||
| if (!ViewCompat.hasAccessibilityDelegate(view) | ||
| && (accessibilityRole != null || view.getTag(R.id.accessibility_states) != null)) { | ||
| if (!ViewCompat.hasAccessibilityDelegate(view)) { | ||
| ViewCompat.setAccessibilityDelegate(view, new AccessibilityDelegateCompat() { | ||
| @Override | ||
| public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { | ||
| super.onInitializeAccessibilityNodeInfo(host, info); | ||
| setRole(info, accessibilityRole, view.getContext()); | ||
| // states are changable. | ||
| ReadableArray accessibilityStates = (ReadableArray) view.getTag(R.id.accessibility_states); | ||
| if (accessibilityStates != null) { | ||
| setState(info, accessibilityStates, view.getContext()); | ||
|
|
||
| if (accessibilityRole != null || view.getTag(R.id.accessibility_states) != null) { | ||
| setRole(info, accessibilityRole, view.getContext()); | ||
| // states are changable. | ||
| ReadableArray accessibilityStates = (ReadableArray) view.getTag(R.id.accessibility_states); | ||
| if (accessibilityStates != null) { | ||
| setState(info, accessibilityStates, view.getContext()); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public void onInitializeAccessibilityEvent(View host, AccessibilityEvent accessibilityEvent) { | ||
| super.onInitializeAccessibilityEvent(host, accessibilityEvent); | ||
| ReactContext reactContext = (ReactContext)host.getContext(); | ||
|
|
||
| WritableMap event = Arguments.createMap(); | ||
| event.putInt("target", host.getId()); | ||
|
|
||
| if (accessibilityEvent.getEventType() == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED) { | ||
| reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(host.getId(), BLUR_EVENT_NAME, event); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure how React Native handles the call to receiveEvent here, but it's worth pointing out that not all events that get passed through The AccessibilityEvent cycle goes like this: 1.) The best way to actually handle this would probably be to override Here is the code in Talkback doing that for reference: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @bravalla Thanks for this detailed feedback. I applied your suggestions, which works fine for focus event, but for the blur event it doesn't work as expected. Using In this video you can understand better what I say: blur just trigger when focus moves to status bar. code: https://gist.github.com/elucaswork/242c098a78577ddcdef4d75a60d2a4a5 Do you have an idea of what can be happening? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @elucaswork, The code here looks good to me. I've verified the approach in a non-ReactNative app to make sure that Talkback was correctly calling performAccessibilityAction on both focus and blur, and it is. It's possible that the issue is at the ReactNative side (I commented above on a few typos), but I am definitely not an expert on that layer so I'll defer to others if thats where the issue lies. You could verify that the Android side is working correctly by adding some logging into the performAccessibilityAction method itself to see if the expected actions are ever being passed in. |
||
| } else if (accessibilityEvent.getEventType() == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) { | ||
| reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(host.getId(), FOCUS_EVENT_NAME, event); | ||
| } | ||
| } | ||
| }); | ||
|
|
||

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shoudln't this be AccessibilityBlurEvent not BlurEvent?