"Fixing" React Navigation & iOS screen reader focus
Tags: reactnative, reactnavigation, a11y
Published: 2021-04-08 09:14:41

---
title: '"Fixing" React Navigation & iOS screen reader focus'
published: true
description: A workaround for flaky iOS screen reader focus in combination with React Navigation
tags: reactnative,reactnavigation,accessibility,a11y
---

## 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:

```ts
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:

```tsx
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:

```tsx
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](https://github.com/react-navigation/react-navigation/issues/7056) is open, you can even see yours truly in the discussion, but sadly no development towards fixing this has been done I guess.