diff --git a/packages/core/src/BaseNavigationContainer.tsx b/packages/core/src/BaseNavigationContainer.tsx index bb384f77..092a71a8 100644 --- a/packages/core/src/BaseNavigationContainer.tsx +++ b/packages/core/src/BaseNavigationContainer.tsx @@ -192,12 +192,13 @@ const BaseNavigationContainer = React.forwardRef( return acc; }, {}), ...emitter.create('root'), - resetRoot, dispatch, + resetRoot, + isFocused: () => true, canGoBack, - getRootState, - getState: () => stateRef.current, getParent: () => undefined, + getState: () => stateRef.current, + getRootState, getCurrentRoute, getCurrentOptions, isReady: () => listeners.focus[0] != null, diff --git a/packages/core/src/__tests__/createNavigationContainerRef.test.tsx b/packages/core/src/__tests__/createNavigationContainerRef.test.tsx new file mode 100644 index 00000000..d9629f17 --- /dev/null +++ b/packages/core/src/__tests__/createNavigationContainerRef.test.tsx @@ -0,0 +1,47 @@ +import type { NavigationState, ParamListBase } from '@react-navigation/routers'; +import { render } from '@testing-library/react-native'; +import * as React from 'react'; + +import BaseNavigationContainer from '../BaseNavigationContainer'; +import createNavigationContainerRef from '../createNavigationContainerRef'; +import Screen from '../Screen'; +import useNavigationBuilder from '../useNavigationBuilder'; +import MockRouter from './__fixtures__/MockRouter'; + +it('adds the listener even if container is mounted later', () => { + const ref = createNavigationContainerRef(); + const listener = jest.fn(); + + ref.addListener('state', listener); + + const TestNavigator = (props: any) => { + const { state, descriptors } = useNavigationBuilder< + NavigationState, + any, + {}, + { title?: string }, + any + >(MockRouter, props); + const { render, options } = descriptors[state.routes[state.index].key]; + + return ( +
+

{options.title}

+
{render()}
+
+ ); + }; + + const element = ( + + + {() => null} + {() => null} + + + ); + + render(element).update(element); + + expect(listener).toHaveBeenCalledTimes(1); +}); diff --git a/packages/core/src/createNavigationContainerRef.tsx b/packages/core/src/createNavigationContainerRef.tsx index 1abc4e62..896f603d 100644 --- a/packages/core/src/createNavigationContainerRef.tsx +++ b/packages/core/src/createNavigationContainerRef.tsx @@ -1,6 +1,10 @@ import { CommonActions } from '@react-navigation/routers'; -import type { NavigationContainerRefWithCurrent } from './types'; +import type { + NavigationContainerEventMap, + NavigationContainerRef, + NavigationContainerRefWithCurrent, +} from './types'; export const NOT_INITIALIZED_ERROR = "The 'navigation' object hasn't been initialized yet. This might happen if you don't have a navigator mounted, or if the navigator hasn't finished mounting. See https://reactnavigation.org/docs/navigating-without-navigation-prop#handling-initialization for more details."; @@ -14,6 +18,7 @@ export default function createNavigationContainerRef< 'removeListener', 'resetRoot', 'dispatch', + 'isFocused', 'canGoBack', 'getRootState', 'getState', @@ -22,26 +27,70 @@ export default function createNavigationContainerRef< 'getCurrentOptions', ] as const; + const listeners: Record void)[]> = {}; + + const removeListener = ( + event: string, + callback: (...args: any[]) => void + ) => { + listeners[event] = listeners[event]?.filter((cb) => cb !== callback); + }; + + let current: NavigationContainerRef | null = null; + const ref: NavigationContainerRefWithCurrent = { + get current() { + return current; + }, + set current(value: NavigationContainerRef | null) { + current = value; + + if (value != null) { + Object.entries(listeners).forEach(([event, callbacks]) => { + callbacks.forEach((callback) => { + value.addListener( + event as keyof NavigationContainerEventMap, + callback + ); + }); + }); + } + }, + isReady: () => { + if (current == null) { + return false; + } + + return current.isReady(); + }, ...methods.reduce((acc, name) => { acc[name] = (...args: any[]) => { - if (ref.current == null) { - console.error(NOT_INITIALIZED_ERROR); + if (current == null) { + switch (name) { + case 'addListener': { + const [event, callback] = args; + + listeners[event] = listeners[event] || []; + listeners[event].push(callback); + + return () => removeListener(event, callback); + } + case 'removeListener': { + const [event, callback] = args; + + removeListener(event, callback); + break; + } + default: + console.error(NOT_INITIALIZED_ERROR); + } } else { // @ts-expect-error: this is ok - return ref.current[name](...args); + return current[name](...args); } }; return acc; }, {}), - isReady: () => { - if (ref.current == null) { - return false; - } - - return ref.current.isReady(); - }, - current: null, }; return ref;