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

Add support for screen reader blur and focus (accessibility) #24642

Closed

Conversation

estevaolucas
Copy link

Summary

Add support for screen reader blur and focus events via UIAccessibilityFocus for iOS and View.AccessibilityDelegate with AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED and TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED for Android.

When VoiceOver (iOS) or TalkBack (Android) enabled, onAccessibilityFocus will be fired once the element gains the screen reader focus, and onAccessibilityBlur will be fired when it loses the screen reader focus.

Changelog

[General] [Added] - Accessibility - Add support for screen reader blur and focus events

Test Plan

export default class App extends Component<Props> {
  render() {
    const onFocus = e => {
      console.log("--focus", e.target);
    };
    const onBlur = e => {
      console.log("--blur", e.target);
    };
    return (
      <View onAccessibilityFocus={onFocus} onAccessibilityBlur={onBlur}>
        <TouchableOpacity
          onAccessibilityFocus={onFocus}
          onAccessibilityBlur={onBlur}
        >
          <Text>TouchableOpacity element</Text>
        </TouchableOpacity>
        <Text onAccessibilityFocus={onFocus} onAccessibilityBlur={onBlur}>
          Test element
        </Text>
        <TextInput
          accessible={true}
          onAccessibilityFocus={onFocus} onAccessibilityBlur={onBlur}
        />
      </View>
    );
  }
}

P.S.: To make it work for TextInput on iOS, this fix #24641 is necessary.

@estevaolucas estevaolucas requested a review from shergin as a code owner April 29, 2019 00:07
@facebook-github-bot facebook-github-bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Apr 29, 2019
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);
Copy link
Contributor

Choose a reason for hiding this comment

The 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 onInitializeAccessibilityEvent will actually be fired. Every ancestor of the view has the chance to cancel the event before it bubbles to the top. Only if it makes it all the way to the top of the view hierarchy will it be passed off to a running accessibility service.

The AccessibilityEvent cycle goes like this:

1.) View.sendAccessibilityEvent() (intent to send event started)
2.) View.onInitializeAccessibilityEvent() (event populated with view info)
3.) View.dispatchPopulateAccessibilityEvent() (event text populated with additional info)
4.) ViewParent.requestSendAccessibilityEvent() (parent of View is free to modify or cancel the event, or pass it up to its parent)
5.) ViewRootImpl calls sendAccessibilityEvent on the AccessibilityManager (this is what actually passes the event to any running accessibility service).

The best way to actually handle this would probably be to override performAccessibilityAction instead. When Talkback (or any other accessibility service) receives a focus event, it's not guaranteed to actually change focus. For example, if the already focused view sent another TYPE_VIEW_ACCESSIBILITY_FOCUSED event, it would be ignored. If it does decide to change focus, it will fire the performAccessibilityAction method on the View and pass in an AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS action.

Here is the code in Talkback doing that for reference:
https://github.com/google/talkback/blob/e69d4731fce02bb9e69613d0e48c29033cad4a98/talkback/src/main/java/eventprocessor/ProcessorFocusAndSingleTap.java#L871

Copy link
Author

@estevaolucas estevaolucas Apr 30, 2019

Choose a reason for hiding this comment

The 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 performAccessibilityAction, blur is triggered just when the focus moves to a non react-native element (my assumption). When the focus is moving around react-native elements, nothing happens.

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?
Thank you.

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

@react-native-bot react-native-bot added the Type: Enhancement A new feature or enhancement of an existing feature. label May 3, 2019
@@ -1325,6 +1331,14 @@ const TextInput = createReactClass({
}
},

_onAccessibilityBlur: function(event: BlurEvent) {
this.props.oncAccessibilityBlur && this.props.oncAccessibilityBlur(event);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo oncAccessibilityBlur -> onAccessibilityBlur

},

_onAccessibilityFocus: function(event: BlurEvent) {
this.props.oncAccessibilityFocus && this.props.oncAccessibilityFocus(event);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo oncAccessibilityFocus -> onAccessibilityFocus

@@ -1325,6 +1331,14 @@ const TextInput = createReactClass({
}
},

_onAccessibilityBlur: function(event: BlurEvent) {
Copy link
Contributor

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?

this.props.oncAccessibilityBlur && this.props.oncAccessibilityBlur(event);
},

_onAccessibilityFocus: function(event: BlurEvent) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shoudln't this be AccessibilityFocusEvent not BlurEvent?

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);
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

@blavalla blavalla left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just realized there is another PR that will conflict with this. Check out Marc's changes here (#24695).

His approach allows for full custom accessibility events, as well as pre-defined events like the ones you have here. It should be pretty easy to add your events into his framework.

I'll let you two figure out which of you makes the most sense to add these in :)

@estevaolucas
Copy link
Author

@blavalla good point. It makes sense to wait for @marcmulcahy's #24695 and then add blur and focus events on top of it.

I'm going to close it for now and open a new one as soon as "extended accessibility actions" gets merged.

Thanks for your review!

@chyamuna
Copy link

chyamuna commented Jun 7, 2022

@marcmulcahy's 24695 PR has been merged. Is the functionality to add support for voice over focus and blur has bee added. or any active PR which is adding this functionality? Thank You estevaolucas

@blavalla
Copy link
Contributor

@chyamuna , this functionality hasn't been added, but if you'd like to take up the mantle here and make a PR I'd be happy to review.

I think the API for this is pretty clear, since 24695 added an accessibilityActions prop with a few built-in actions like "activate" and "longpress". I think "focus" and "blur" could be added as additional built-in actions that work for both iOS and Android.

See the documentation here if you aren't familiar with this API - https://reactnative.dev/docs/accessibility#accessibility-actions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Type: Enhancement A new feature or enhancement of an existing feature.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants