DEV Community

Thibault Maekelbergh for In The Pocket

Posted on • Edited on

"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];
}

Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (2)

Collapse
 
renaudamsellem profile image
renaudAmsellem

Hi,

Thank you for your post. I had to implement this myself and I found a similar solution (with useFocusEffect and a component that I use in every screen to target the correct element to receive focus).

I see that your code targets only iOS. I currently have trouble with android. Talkback does not read anything when calling setAccessibilityFocus when useFocusEffect occurs.

Did you found a way to make it work on android?
(PS: I think talkback in broken since react-navigation 6)

Collapse
 
verunar profile image
veronika

Your "Void" in [MutableRefObject, Void] was giving me an error, so I wrote this instead [MutableRefObject, () => void]