Logo

dev-resources.site

for different kinds of informations.

"Fixing" React Navigation & iOS screen reader focus

Published at
4/8/2021
Categories
reactnative
reactnavigation
a11y
Author
Thibault Maekelbergh
Categories
3 categories in total
reactnative
open
reactnavigation
open
a11y
open
"Fixing" React Navigation & iOS screen reader focus

The problem

During some extensive accessibility (mainly screen reader) testing on a React Native app we're currently developing, we bumped into an issue where focus would be lost for the screen reader when navigating to a new view in a stack using React Navigation.

You would transition from the overview to a detail page, but instead of respecting the navigation tree for that page, React Navigation would focus the screen reader on the position where your focus was in the previous screen. This would pose a big problem of course: Imagine scrolling through a long list (FlatList), then navigating to the detail for the item you tapped, and having the screen reader focus somewhere on the middle of the screen.

This issue was however only happening on iOS. My guess was that React Navigation in some way does not respect the iOS native navigation elements and does some JS things inbetween, which makes iOS act up in this case.

The "fix"

To fix it, we created the useAccessibilityFocus hook! It’s very simple and will just return a ref to bind to your element, and a function to trigger focus to the element carrying the ref:

import type { MutableRefObject } from 'react';
import { useCallback, useRef } from 'react';
import { AccessibilityInfo, findNodeHandle, Platform } from 'react-native';

/**
 * Returns a ref object which when bound to an element, will focus that
 * element in VoiceOver/TalkBack on its appearance
 */
export default function useAccessibilityFocus(): [MutableRefObject<any>, Void] {
  const ref = useRef(null);

  const setFocus = useCallback(() => {
    if (Platform.OS === 'ios') {
      if (ref.current) {
        const focusPoint = findNodeHandle(ref.current);
        if (focusPoint) {
          AccessibilityInfo.setAccessibilityFocus(focusPoint);
        }
      }
    }
  }, [ref]);

  return [ref, setFocus];
}

Now we can call it in a useEffect or even better, React Navigation's useFocusEffect to focus on the element when the screen appears:

const DetailScreen = ({ navigation }) => {
  const [focusRef, setFocus] = useAccessibilityFocus();

  useFocusEffect(setFocus);

  return (
    <View>
      <TitleInput />
      <View ref={focusRef}>
        <Text>Content I want the focus on</Text>
      </View>
    <View>
  )
}

Another way would be to hook into a listener on your stack’s screens:

const RootNav = () => {
  const [focusRef, setFocus] = useAccessibilityFocus();

  return (
    <Stack.Navigator>
      <Stack.Screen component={OverviewScreen} />
      <Stack.Screen
        component={DetailScreen}
        options={{
          header: () => (
            <NavHeader ref={focusRef} title="Detail" />
          ),
        }}
        listeners={{
          transitionEnd: () => setFocus()
        }}
      />
    </Stack.Navigator>
  )
}

Worth noting that now, we have the added benefit that we can use this hook in other situations! Rather than moving to another view in the stack, we can now use this to trigger screen reader focus to modals, notifications, alerts, errors etc. Just be sure to remove the Platform.OS check in the hook in that case

The solution

The real fix however, is that React Navigation fixes this issue in their core implementation so that focus is carried along when navigating to another screen. A PR discussion about this is open, you can even see yours truly in the discussion, but sadly no development towards fixing this has been done I guess.

Featured ones: