The problem

During our continuous accessibility (mainly screen reader) effort in React Native, 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 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 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=
listeners=
/>
</Stack.Navigator>
);
};

Worth noting that now, we have the added benefit that we can use the 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.