diff --git a/packages/core/src/__fixtures__/createNavigationContainer.js b/packages/core/src/__fixtures__/createNavigationContainer.js
new file mode 100644
index 00000000..d98cf6ce
--- /dev/null
+++ b/packages/core/src/__fixtures__/createNavigationContainer.js
@@ -0,0 +1,148 @@
+/* eslint-disable react/sort-comp */
+
+import React from 'react';
+import { NavigationActions, getNavigation, NavigationProvider } from '../index';
+
+export default function createNavigationContainer(Component) {
+ class NavigationContainer extends React.Component {
+ static router = Component.router;
+ static navigationOptions = null;
+
+ constructor(props) {
+ super(props);
+
+ this._initialAction = NavigationActions.init();
+
+ this.state = {
+ nav: !props.loadNavigationState
+ ? Component.router.getStateForAction(this._initialAction)
+ : null,
+ };
+ }
+
+ _actionEventSubscribers = new Set();
+
+ _onNavigationStateChange(prevNav, nav, action) {
+ if (typeof this.props.onNavigationStateChange === 'function') {
+ this.props.onNavigationStateChange(prevNav, nav, action);
+ }
+ }
+
+ componentDidUpdate() {
+ // Clear cached _navState every tick
+ if (this._navState === this.state.nav) {
+ this._navState = null;
+ }
+ }
+
+ async componentDidMount() {
+ // Initialize state. This must be done *after* any async code
+ // so we don't end up with a different value for this.state.nav
+ // due to changes while async function was resolving
+ let action = this._initialAction;
+ // eslint-disable-next-line react/no-access-state-in-setstate
+ let startupState = this.state.nav;
+ if (!startupState) {
+ startupState = Component.router.getStateForAction(action);
+ }
+
+ const dispatchActions = () =>
+ this._actionEventSubscribers.forEach((subscriber) =>
+ subscriber({
+ type: 'action',
+ action,
+ state: this.state.nav,
+ lastState: null,
+ })
+ );
+
+ if (startupState === this.state.nav) {
+ dispatchActions();
+ return;
+ }
+
+ // eslint-disable-next-line react/no-did-mount-set-state
+ this.setState({ nav: startupState }, () => {
+ dispatchActions();
+ });
+ }
+
+ dispatch = (action) => {
+ if (this.props.navigation) {
+ return this.props.navigation.dispatch(action);
+ }
+
+ // navState will have the most up-to-date value, because setState sometimes behaves asyncronously
+ this._navState = this._navState || this.state.nav;
+ const lastNavState = this._navState;
+
+ const reducedState = Component.router.getStateForAction(
+ action,
+ lastNavState
+ );
+ const navState = reducedState === null ? lastNavState : reducedState;
+
+ const dispatchActionEvents = () => {
+ this._actionEventSubscribers.forEach((subscriber) =>
+ subscriber({
+ type: 'action',
+ action,
+ state: navState,
+ lastState: lastNavState,
+ })
+ );
+ };
+
+ if (reducedState === null) {
+ // The router will return null when action has been handled and the state hasn't changed.
+ // dispatch returns true when something has been handled.
+ dispatchActionEvents();
+ return true;
+ }
+
+ if (navState !== lastNavState) {
+ // Cache updates to state.nav during the tick to ensure that subsequent calls will not discard this change
+ this._navState = navState;
+ this.setState({ nav: navState }, () => {
+ this._onNavigationStateChange(lastNavState, navState, action);
+ dispatchActionEvents();
+ });
+ return true;
+ }
+
+ dispatchActionEvents();
+ return false;
+ };
+
+ render() {
+ let navigation = this.props.navigation;
+
+ const navState = this.state.nav;
+
+ if (!navState) {
+ return null;
+ }
+
+ if (!this._navigation || this._navigation.state !== navState) {
+ this._navigation = getNavigation(
+ Component.router,
+ navState,
+ this.dispatch,
+ this._actionEventSubscribers,
+ this._getScreenProps,
+ () => this._navigation
+ );
+ }
+
+ navigation = this._navigation;
+
+ return (
+
+
+
+ );
+ }
+ }
+
+ return NavigationContainer;
+}
diff --git a/packages/core/src/__tests__/NavigationFocusEvents.test.js b/packages/core/src/__tests__/NavigationFocusEvents.test.js
new file mode 100644
index 00000000..56cb3c70
--- /dev/null
+++ b/packages/core/src/__tests__/NavigationFocusEvents.test.js
@@ -0,0 +1,241 @@
+import * as React from 'react';
+import { render, act } from 'react-native-testing-library';
+import { navigate } from '../NavigationActions';
+import TabRouter from '../routers/TabRouter';
+import createNavigator from '../navigators/createNavigator';
+import createNavigationContainer from '../__fixtures__/createNavigationContainer';
+
+it('fires focus and blur events in root navigator', async () => {
+ function createTestNavigator(routeConfigMap, config = {}) {
+ const router = TabRouter(routeConfigMap, config);
+
+ return createNavigator(
+ ({ descriptors, navigation }) =>
+ navigation.state.routes.map((route) => {
+ const Comp = descriptors[route.key].getComponent();
+ return (
+
+ );
+ }),
+ router,
+ config
+ );
+ }
+
+ const firstFocusCallback = jest.fn();
+ const firstBlurCallback = jest.fn();
+
+ const secondFocusCallback = jest.fn();
+ const secondBlurCallback = jest.fn();
+
+ const thirdFocusCallback = jest.fn();
+ const thirdBlurCallback = jest.fn();
+
+ const fourthFocusCallback = jest.fn();
+ const fourthBlurCallback = jest.fn();
+
+ const createComponent = (focusCallback, blurCallback) =>
+ class TestComponent extends React.Component {
+ componentDidMount() {
+ const { navigation } = this.props;
+
+ this.focusSub = navigation.addListener('willFocus', focusCallback);
+ this.blurSub = navigation.addListener('willBlur', blurCallback);
+ }
+
+ componentWillUnmount() {
+ this.focusSub?.remove();
+ this.blurSub?.remove();
+ }
+
+ render() {
+ return null;
+ }
+ };
+
+ const navigation = React.createRef();
+
+ const Navigator = createNavigationContainer(
+ createTestNavigator({
+ first: createComponent(firstFocusCallback, firstBlurCallback),
+ second: createComponent(secondFocusCallback, secondBlurCallback),
+ third: createComponent(thirdFocusCallback, thirdBlurCallback),
+ fourth: createComponent(fourthFocusCallback, fourthBlurCallback),
+ })
+ );
+
+ const element = ;
+
+ render(element);
+
+ expect(firstFocusCallback).toBeCalledTimes(1);
+ expect(firstBlurCallback).toBeCalledTimes(0);
+ expect(secondFocusCallback).toBeCalledTimes(0);
+ expect(secondBlurCallback).toBeCalledTimes(0);
+ expect(thirdFocusCallback).toBeCalledTimes(0);
+ expect(thirdBlurCallback).toBeCalledTimes(0);
+ expect(fourthFocusCallback).toBeCalledTimes(0);
+ expect(fourthBlurCallback).toBeCalledTimes(0);
+
+ act(() => {
+ navigation.current.dispatch(navigate({ routeName: 'second' }));
+ });
+
+ expect(firstBlurCallback).toBeCalledTimes(1);
+ expect(secondFocusCallback).toBeCalledTimes(1);
+
+ act(() => {
+ navigation.current.dispatch(navigate({ routeName: 'fourth' }));
+ });
+
+ expect(firstFocusCallback).toBeCalledTimes(1);
+ expect(firstBlurCallback).toBeCalledTimes(1);
+ expect(secondFocusCallback).toBeCalledTimes(1);
+ expect(secondBlurCallback).toBeCalledTimes(1);
+ expect(thirdFocusCallback).toBeCalledTimes(0);
+ expect(thirdBlurCallback).toBeCalledTimes(0);
+ expect(fourthFocusCallback).toBeCalledTimes(1);
+ expect(fourthBlurCallback).toBeCalledTimes(0);
+});
+
+it('fires focus and blur events in nested navigator', () => {
+ function createTestNavigator(routeConfigMap, config = {}) {
+ const router = TabRouter(routeConfigMap, config);
+
+ return createNavigator(
+ ({ descriptors, navigation }) =>
+ navigation.state.routes.map((route) => {
+ const Comp = descriptors[route.key].getComponent();
+ return (
+
+ );
+ }),
+ router,
+ config
+ );
+ }
+
+ const firstFocusCallback = jest.fn();
+ const firstBlurCallback = jest.fn();
+
+ const secondFocusCallback = jest.fn();
+ const secondBlurCallback = jest.fn();
+
+ const thirdFocusCallback = jest.fn();
+ const thirdBlurCallback = jest.fn();
+
+ const fourthFocusCallback = jest.fn();
+ const fourthBlurCallback = jest.fn();
+
+ const createComponent = (focusCallback, blurCallback) =>
+ class TestComponent extends React.Component {
+ componentDidMount() {
+ const { navigation } = this.props;
+
+ this.focusSub = navigation.addListener('willFocus', focusCallback);
+ this.blurSub = navigation.addListener('willBlur', blurCallback);
+ }
+
+ componentWillUnmount() {
+ this.focusSub?.remove();
+ this.blurSub?.remove();
+ }
+
+ render() {
+ return null;
+ }
+ };
+
+ const Navigator = createNavigationContainer(
+ createTestNavigator({
+ first: createComponent(firstFocusCallback, firstBlurCallback),
+ second: createComponent(secondFocusCallback, secondBlurCallback),
+ nested: createTestNavigator({
+ third: createComponent(thirdFocusCallback, thirdBlurCallback),
+ fourth: createComponent(fourthFocusCallback, fourthBlurCallback),
+ }),
+ })
+ );
+
+ const navigation = React.createRef();
+
+ const element = ;
+
+ render(element);
+
+ expect(thirdFocusCallback).toBeCalledTimes(0);
+ expect(firstFocusCallback).toBeCalledTimes(1);
+
+ act(() => {
+ navigation.current.dispatch(navigate({ routeName: 'nested' }));
+ });
+
+ expect(firstFocusCallback).toBeCalledTimes(1);
+ expect(fourthFocusCallback).toBeCalledTimes(0);
+ expect(thirdFocusCallback).toBeCalledTimes(1);
+
+ act(() => {
+ navigation.current.dispatch(navigate({ routeName: 'second' }));
+ });
+
+ expect(thirdFocusCallback).toBeCalledTimes(1);
+ expect(secondFocusCallback).toBeCalledTimes(1);
+ expect(fourthBlurCallback).toBeCalledTimes(0);
+
+ act(() => {
+ navigation.current.dispatch(navigate({ routeName: 'nested' }));
+ });
+
+ expect(firstBlurCallback).toBeCalledTimes(1);
+ expect(secondBlurCallback).toBeCalledTimes(1);
+ expect(thirdFocusCallback).toBeCalledTimes(2);
+ expect(fourthFocusCallback).toBeCalledTimes(0);
+
+ act(() => {
+ navigation.current.dispatch(navigate({ routeName: 'third' }));
+ });
+
+ expect(fourthBlurCallback).toBeCalledTimes(0);
+ expect(thirdFocusCallback).toBeCalledTimes(2);
+
+ act(() => {
+ navigation.current.dispatch(navigate({ routeName: 'first' }));
+ });
+
+ expect(firstFocusCallback).toBeCalledTimes(2);
+ expect(thirdBlurCallback).toBeCalledTimes(2);
+
+ act(() => {
+ navigation.current.dispatch(navigate({ routeName: 'fourth' }));
+ });
+
+ expect(fourthFocusCallback).toBeCalledTimes(1);
+ expect(thirdBlurCallback).toBeCalledTimes(2);
+ expect(firstBlurCallback).toBeCalledTimes(2);
+
+ act(() => {
+ navigation.current.dispatch(navigate({ routeName: 'third' }));
+ });
+
+ expect(thirdFocusCallback).toBeCalledTimes(3);
+ expect(fourthBlurCallback).toBeCalledTimes(1);
+
+ // Make sure nothing else has changed
+ expect(firstFocusCallback).toBeCalledTimes(2);
+ expect(firstBlurCallback).toBeCalledTimes(2);
+
+ expect(secondFocusCallback).toBeCalledTimes(1);
+ expect(secondBlurCallback).toBeCalledTimes(1);
+
+ expect(thirdFocusCallback).toBeCalledTimes(3);
+ expect(thirdBlurCallback).toBeCalledTimes(2);
+
+ expect(fourthFocusCallback).toBeCalledTimes(1);
+ expect(fourthBlurCallback).toBeCalledTimes(1);
+});
diff --git a/packages/core/src/__tests__/getChildEventSubscriber.test.js b/packages/core/src/__tests__/getChildEventSubscriber.test.js
deleted file mode 100644
index 9d5a20eb..00000000
--- a/packages/core/src/__tests__/getChildEventSubscriber.test.js
+++ /dev/null
@@ -1,543 +0,0 @@
-import getChildEventSubscriber from '../getChildEventSubscriber';
-
-it('child action events only flow when focused', () => {
- const parentSubscriber = jest.fn();
- const emitParentAction = (payload) => {
- parentSubscriber.mock.calls.forEach((subs) => {
- if (subs[0] === payload.type) {
- subs[1](payload);
- }
- });
- };
- const subscriptionRemove = () => {};
- parentSubscriber.mockReturnValueOnce({ remove: subscriptionRemove });
- const childEventSubscriber = getChildEventSubscriber(parentSubscriber, 'key1')
- .addListener;
- const testState = {
- key: 'foo',
- routeName: 'FooRoute',
- routes: [{ key: 'key0' }, { key: 'key1' }],
- index: 0,
- isTransitioning: false,
- };
- const focusedTestState = {
- ...testState,
- index: 1,
- };
- const childActionHandler = jest.fn();
- const childWillFocusHandler = jest.fn();
- const childDidFocusHandler = jest.fn();
- childEventSubscriber('action', childActionHandler);
- childEventSubscriber('willFocus', childWillFocusHandler);
- childEventSubscriber('didFocus', childDidFocusHandler);
- emitParentAction({
- type: 'action',
- state: focusedTestState,
- lastState: testState,
- action: { type: 'FooAction' },
- });
- expect(childActionHandler.mock.calls.length).toBe(0);
- expect(childWillFocusHandler.mock.calls.length).toBe(1);
- expect(childDidFocusHandler.mock.calls.length).toBe(1);
- emitParentAction({
- type: 'action',
- state: focusedTestState,
- lastState: focusedTestState,
- action: { type: 'FooAction' },
- });
- expect(childActionHandler.mock.calls.length).toBe(1);
- expect(childWillFocusHandler.mock.calls.length).toBe(1);
- expect(childDidFocusHandler.mock.calls.length).toBe(1);
-});
-
-it('grandchildren subscription', () => {
- const grandParentSubscriber = jest.fn();
- const emitGrandParentAction = (payload) => {
- grandParentSubscriber.mock.calls.forEach((subs) => {
- if (subs[0] === payload.type) {
- subs[1](payload);
- }
- });
- };
- const subscriptionRemove = () => {};
- grandParentSubscriber.mockReturnValueOnce({ remove: subscriptionRemove });
- const parentSubscriber = getChildEventSubscriber(
- grandParentSubscriber,
- 'parent'
- ).addListener;
- const childEventSubscriber = getChildEventSubscriber(parentSubscriber, 'key1')
- .addListener;
- const parentBlurState = {
- key: 'foo',
- routeName: 'FooRoute',
- routes: [
- { key: 'aunt' },
- {
- key: 'parent',
- routes: [{ key: 'key0' }, { key: 'key1' }],
- index: 1,
- isTransitioning: false,
- },
- ],
- index: 0,
- isTransitioning: false,
- };
- const parentTransitionState = {
- ...parentBlurState,
- index: 1,
- isTransitioning: true,
- };
- const parentFocusState = {
- ...parentTransitionState,
- isTransitioning: false,
- };
- const childActionHandler = jest.fn();
- const childWillFocusHandler = jest.fn();
- const childDidFocusHandler = jest.fn();
- childEventSubscriber('action', childActionHandler);
- childEventSubscriber('willFocus', childWillFocusHandler);
- childEventSubscriber('didFocus', childDidFocusHandler);
- emitGrandParentAction({
- type: 'action',
- state: parentTransitionState,
- lastState: parentBlurState,
- action: { type: 'FooAction' },
- });
- expect(childActionHandler.mock.calls.length).toBe(0);
- expect(childWillFocusHandler.mock.calls.length).toBe(1);
- expect(childDidFocusHandler.mock.calls.length).toBe(0);
- emitGrandParentAction({
- type: 'action',
- state: parentFocusState,
- lastState: parentTransitionState,
- action: { type: 'FooAction' },
- });
- expect(childActionHandler.mock.calls.length).toBe(0);
- expect(childWillFocusHandler.mock.calls.length).toBe(1);
- expect(childDidFocusHandler.mock.calls.length).toBe(1);
-});
-
-it('grandchildren transitions', () => {
- const grandParentSubscriber = jest.fn();
- const emitGrandParentAction = (payload) => {
- grandParentSubscriber.mock.calls.forEach((subs) => {
- if (subs[0] === payload.type) {
- subs[1](payload);
- }
- });
- };
- const subscriptionRemove = () => {};
- grandParentSubscriber.mockReturnValueOnce({ remove: subscriptionRemove });
- const parentSubscriber = getChildEventSubscriber(
- grandParentSubscriber,
- 'parent'
- ).addListener;
- const childEventSubscriber = getChildEventSubscriber(parentSubscriber, 'key1')
- .addListener;
- const makeFakeState = (childIndex, childIsTransitioning) => ({
- index: 1,
- isTransitioning: false,
- routes: [
- { key: 'nothing' },
- {
- key: 'parent',
- index: childIndex,
- isTransitioning: childIsTransitioning,
- routes: [{ key: 'key0' }, { key: 'key1' }, { key: 'key2' }],
- },
- ],
- });
- const blurredState = makeFakeState(0, false);
- const transitionState = makeFakeState(1, true);
- const focusState = makeFakeState(1, false);
- const transition2State = makeFakeState(2, true);
- const blurred2State = makeFakeState(2, false);
-
- const childActionHandler = jest.fn();
- const childWillFocusHandler = jest.fn();
- const childDidFocusHandler = jest.fn();
- const childWillBlurHandler = jest.fn();
- const childDidBlurHandler = jest.fn();
- childEventSubscriber('action', childActionHandler);
- childEventSubscriber('willFocus', childWillFocusHandler);
- childEventSubscriber('didFocus', childDidFocusHandler);
- childEventSubscriber('willBlur', childWillBlurHandler);
- childEventSubscriber('didBlur', childDidBlurHandler);
- emitGrandParentAction({
- type: 'action',
- state: transitionState,
- lastState: blurredState,
- action: { type: 'FooAction' },
- });
- expect(childActionHandler.mock.calls.length).toBe(0);
- expect(childWillFocusHandler.mock.calls.length).toBe(1);
- expect(childDidFocusHandler.mock.calls.length).toBe(0);
- emitGrandParentAction({
- type: 'action',
- state: focusState,
- lastState: transitionState,
- action: { type: 'FooAction' },
- });
- expect(childActionHandler.mock.calls.length).toBe(0);
- expect(childWillFocusHandler.mock.calls.length).toBe(1);
- expect(childDidFocusHandler.mock.calls.length).toBe(1);
- emitGrandParentAction({
- type: 'action',
- state: focusState,
- lastState: focusState,
- action: { type: 'TestAction' },
- });
- expect(childWillFocusHandler.mock.calls.length).toBe(1);
- expect(childDidFocusHandler.mock.calls.length).toBe(1);
- expect(childActionHandler.mock.calls.length).toBe(1);
- emitGrandParentAction({
- type: 'action',
- state: transition2State,
- lastState: focusState,
- action: { type: 'CauseWillBlurAction' },
- });
- expect(childWillBlurHandler.mock.calls.length).toBe(1);
- expect(childDidBlurHandler.mock.calls.length).toBe(0);
- expect(childActionHandler.mock.calls.length).toBe(1);
- emitGrandParentAction({
- type: 'action',
- state: blurred2State,
- lastState: transition2State,
- action: { type: 'CauseDidBlurAction' },
- });
- expect(childWillBlurHandler.mock.calls.length).toBe(1);
- expect(childDidBlurHandler.mock.calls.length).toBe(1);
- expect(childActionHandler.mock.calls.length).toBe(1);
-});
-
-it('grandchildren pass through transitions', () => {
- const grandParentSubscriber = jest.fn();
- const emitGrandParentAction = (payload) => {
- grandParentSubscriber.mock.calls.forEach((subs) => {
- if (subs[0] === payload.type) {
- subs[1](payload);
- }
- });
- };
- const subscriptionRemove = () => {};
- grandParentSubscriber.mockReturnValueOnce({ remove: subscriptionRemove });
- const parentSubscriber = getChildEventSubscriber(
- grandParentSubscriber,
- 'parent'
- ).addListener;
- const childEventSubscriber = getChildEventSubscriber(parentSubscriber, 'key1')
- .addListener;
- const makeFakeState = (childIndex, childIsTransitioning) => ({
- index: childIndex,
- isTransitioning: childIsTransitioning,
- routes: [
- { key: 'nothing' },
- {
- key: 'parent',
- index: 1,
- isTransitioning: false,
- routes: [{ key: 'key0' }, { key: 'key1' }, { key: 'key2' }],
- },
- ].slice(0, childIndex + 1),
- });
- const blurredState = makeFakeState(0, false);
- const transitionState = makeFakeState(1, true);
- const focusState = makeFakeState(1, false);
- const transition2State = makeFakeState(0, true);
- const blurred2State = makeFakeState(0, false);
-
- const childActionHandler = jest.fn();
- const childWillFocusHandler = jest.fn();
- const childDidFocusHandler = jest.fn();
- const childWillBlurHandler = jest.fn();
- const childDidBlurHandler = jest.fn();
- childEventSubscriber('action', childActionHandler);
- childEventSubscriber('willFocus', childWillFocusHandler);
- childEventSubscriber('didFocus', childDidFocusHandler);
- childEventSubscriber('willBlur', childWillBlurHandler);
- childEventSubscriber('didBlur', childDidBlurHandler);
- emitGrandParentAction({
- type: 'action',
- state: transitionState,
- lastState: blurredState,
- action: { type: 'FooAction' },
- });
- expect(childActionHandler.mock.calls.length).toBe(0);
- expect(childWillFocusHandler.mock.calls.length).toBe(1);
- expect(childDidFocusHandler.mock.calls.length).toBe(0);
- emitGrandParentAction({
- type: 'action',
- state: focusState,
- lastState: transitionState,
- action: { type: 'FooAction' },
- });
- expect(childActionHandler.mock.calls.length).toBe(0);
- expect(childWillFocusHandler.mock.calls.length).toBe(1);
- expect(childDidFocusHandler.mock.calls.length).toBe(1);
- emitGrandParentAction({
- type: 'action',
- state: focusState,
- lastState: focusState,
- action: { type: 'TestAction' },
- });
- expect(childWillFocusHandler.mock.calls.length).toBe(1);
- expect(childDidFocusHandler.mock.calls.length).toBe(1);
- expect(childActionHandler.mock.calls.length).toBe(1);
- emitGrandParentAction({
- type: 'action',
- state: transition2State,
- lastState: focusState,
- action: { type: 'CauseWillBlurAction' },
- });
- expect(childWillBlurHandler.mock.calls.length).toBe(1);
- expect(childDidBlurHandler.mock.calls.length).toBe(0);
- expect(childActionHandler.mock.calls.length).toBe(1);
- emitGrandParentAction({
- type: 'action',
- state: blurred2State,
- lastState: transition2State,
- action: { type: 'CauseDidBlurAction' },
- });
- expect(childWillBlurHandler.mock.calls.length).toBe(1);
- expect(childDidBlurHandler.mock.calls.length).toBe(1);
- expect(childActionHandler.mock.calls.length).toBe(1);
-});
-
-it('child focus with transition', () => {
- const parentSubscriber = jest.fn();
- const emitParentAction = (payload) => {
- parentSubscriber.mock.calls.forEach((subs) => {
- if (subs[0] === payload.type) {
- subs[1](payload);
- }
- });
- };
- const subscriptionRemove = () => {};
- parentSubscriber.mockReturnValueOnce({ remove: subscriptionRemove });
- const childEventSubscriber = getChildEventSubscriber(parentSubscriber, 'key1')
- .addListener;
- const randomAction = { type: 'FooAction' };
- const testState = {
- key: 'foo',
- routeName: 'FooRoute',
- routes: [{ key: 'key0' }, { key: 'key1' }],
- index: 0,
- isTransitioning: false,
- };
- const childWillFocusHandler = jest.fn();
- const childDidFocusHandler = jest.fn();
- const childWillBlurHandler = jest.fn();
- const childDidBlurHandler = jest.fn();
- childEventSubscriber('willFocus', childWillFocusHandler);
- childEventSubscriber('didFocus', childDidFocusHandler);
- childEventSubscriber('willBlur', childWillBlurHandler);
- childEventSubscriber('didBlur', childDidBlurHandler);
- emitParentAction({
- type: 'didFocus',
- action: randomAction,
- lastState: testState,
- state: testState,
- });
- emitParentAction({
- type: 'action',
- action: randomAction,
- lastState: testState,
- state: {
- ...testState,
- index: 1,
- isTransitioning: true,
- },
- });
- expect(childWillFocusHandler.mock.calls.length).toBe(1);
- emitParentAction({
- type: 'action',
- action: randomAction,
- lastState: {
- ...testState,
- index: 1,
- isTransitioning: true,
- },
- state: {
- ...testState,
- index: 1,
- isTransitioning: false,
- },
- });
- expect(childDidFocusHandler.mock.calls.length).toBe(1);
- emitParentAction({
- type: 'action',
- action: randomAction,
- lastState: {
- ...testState,
- index: 1,
- isTransitioning: false,
- },
- state: {
- ...testState,
- index: 0,
- isTransitioning: true,
- },
- });
- expect(childWillBlurHandler.mock.calls.length).toBe(1);
- emitParentAction({
- type: 'action',
- action: randomAction,
- lastState: {
- ...testState,
- index: 0,
- isTransitioning: true,
- },
- state: {
- ...testState,
- index: 0,
- isTransitioning: false,
- },
- });
- expect(childDidBlurHandler.mock.calls.length).toBe(1);
-});
-
-it('child focus with immediate transition', () => {
- const parentSubscriber = jest.fn();
- const emitParentAction = (payload) => {
- parentSubscriber.mock.calls.forEach((subs) => {
- if (subs[0] === payload.type) {
- subs[1](payload);
- }
- });
- };
- const subscriptionRemove = () => {};
- parentSubscriber.mockReturnValueOnce({ remove: subscriptionRemove });
- const childEventSubscriber = getChildEventSubscriber(parentSubscriber, 'key1')
- .addListener;
- const randomAction = { type: 'FooAction' };
- const testState = {
- key: 'foo',
- routeName: 'FooRoute',
- routes: [{ key: 'key0' }, { key: 'key1' }],
- index: 0,
- isTransitioning: false,
- };
- const childWillFocusHandler = jest.fn();
- const childDidFocusHandler = jest.fn();
- const childWillBlurHandler = jest.fn();
- const childDidBlurHandler = jest.fn();
- childEventSubscriber('willFocus', childWillFocusHandler);
- childEventSubscriber('didFocus', childDidFocusHandler);
- childEventSubscriber('willBlur', childWillBlurHandler);
- childEventSubscriber('didBlur', childDidBlurHandler);
- emitParentAction({
- type: 'didFocus',
- action: randomAction,
- lastState: testState,
- state: testState,
- });
- emitParentAction({
- type: 'action',
- action: randomAction,
- lastState: testState,
- state: {
- ...testState,
- index: 1,
- },
- });
- expect(childWillFocusHandler.mock.calls.length).toBe(1);
- expect(childDidFocusHandler.mock.calls.length).toBe(1);
-
- emitParentAction({
- type: 'action',
- action: randomAction,
- lastState: {
- ...testState,
- index: 1,
- },
- state: {
- ...testState,
- index: 0,
- },
- });
- expect(childWillBlurHandler.mock.calls.length).toBe(1);
- expect(childDidBlurHandler.mock.calls.length).toBe(1);
-});
-
-const setupEventTest = (subscriptionKey, initialLastFocusEvent) => {
- const parentSubscriber = jest.fn();
- const emitEvent = (payload) => {
- parentSubscriber.mock.calls.forEach((subs) => {
- if (subs[0] === payload.type) {
- subs[1](payload);
- }
- });
- };
- const subscriptionRemove = () => {};
- parentSubscriber.mockReturnValueOnce({ remove: subscriptionRemove });
- const evtProvider = getChildEventSubscriber(
- parentSubscriber,
- subscriptionKey,
- initialLastFocusEvent
- );
- const handlers = {};
- evtProvider.addListener('action', (handlers.action = jest.fn()));
- evtProvider.addListener('willFocus', (handlers.willFocus = jest.fn()));
- evtProvider.addListener('didFocus', (handlers.didFocus = jest.fn()));
- evtProvider.addListener('willBlur', (handlers.willBlur = jest.fn()));
- evtProvider.addListener('didBlur', (handlers.didBlur = jest.fn()));
- return { emitEvent, handlers, evtProvider };
-};
-
-it('immediate back with uncompleted transition will focus first screen again', () => {
- const { handlers, emitEvent } = setupEventTest('key0', 'didFocus');
- emitEvent({
- type: 'action',
- state: {
- index: 1,
- routes: [{ key: 'key0' }, { key: 'key1' }],
- isTransitioning: true,
- },
- lastState: {
- index: 0,
- routes: [{ key: 'key0' }],
- isTransitioning: false,
- },
- action: { type: 'Any action, does not matter here' },
- });
- expect(handlers.willFocus.mock.calls.length).toBe(0);
- expect(handlers.didFocus.mock.calls.length).toBe(0);
- expect(handlers.willBlur.mock.calls.length).toBe(1);
- expect(handlers.didBlur.mock.calls.length).toBe(0);
- emitEvent({
- type: 'action',
- state: {
- index: 0,
- routes: [{ key: 'key0' }],
- isTransitioning: true,
- },
- lastState: {
- index: 1,
- routes: [{ key: 'key0' }, { key: 'key1' }],
- isTransitioning: true,
- },
- action: { type: 'Any action, does not matter here' },
- });
- expect(handlers.willFocus.mock.calls.length).toBe(1);
- expect(handlers.didFocus.mock.calls.length).toBe(0);
- expect(handlers.willBlur.mock.calls.length).toBe(1);
- expect(handlers.didBlur.mock.calls.length).toBe(0);
- emitEvent({
- type: 'action',
- state: {
- index: 0,
- routes: [{ key: 'key0' }],
- isTransitioning: false,
- },
- lastState: {
- index: 0,
- routes: [{ key: 'key0' }],
- isTransitioning: true,
- },
- action: { type: 'Any action, does not matter here' },
- });
- expect(handlers.willFocus.mock.calls.length).toBe(1);
- expect(handlers.didFocus.mock.calls.length).toBe(1);
- expect(handlers.willBlur.mock.calls.length).toBe(1);
- expect(handlers.didBlur.mock.calls.length).toBe(0);
-});
diff --git a/packages/core/src/getChildEventSubscriber.js b/packages/core/src/getChildEventSubscriber.js
deleted file mode 100644
index 4b47f01d..00000000
--- a/packages/core/src/getChildEventSubscriber.js
+++ /dev/null
@@ -1,196 +0,0 @@
-/*
- * This is used to extract one children's worth of events from a stream of navigation action events
- *
- * Based on the 'action' events that get fired for this navigation state, this utility will fire
- * focus and blur events for this child
- */
-export default function getChildEventSubscriber(
- addListener,
- key,
- initialLastFocusEvent = 'didBlur'
-) {
- const actionSubscribers = new Set();
- const willFocusSubscribers = new Set();
- const didFocusSubscribers = new Set();
- const willBlurSubscribers = new Set();
- const didBlurSubscribers = new Set();
- const refocusSubscribers = new Set();
-
- const removeAll = () => {
- [
- actionSubscribers,
- willFocusSubscribers,
- didFocusSubscribers,
- willBlurSubscribers,
- didBlurSubscribers,
- refocusSubscribers,
- ].forEach((set) => set.clear());
-
- upstreamSubscribers.forEach((subs) => subs && subs.remove());
- };
-
- const getChildSubscribers = (evtName) => {
- switch (evtName) {
- case 'action':
- return actionSubscribers;
- case 'willFocus':
- return willFocusSubscribers;
- case 'didFocus':
- return didFocusSubscribers;
- case 'willBlur':
- return willBlurSubscribers;
- case 'didBlur':
- return didBlurSubscribers;
- case 'refocus':
- return refocusSubscribers;
- default:
- return null;
- }
- };
-
- const emit = (type, payload) => {
- const payloadWithType = { ...payload, type };
- const subscribers = getChildSubscribers(type);
- subscribers &&
- subscribers.forEach((subs) => {
- subs(payloadWithType);
- });
- };
-
- // lastFocusEvent keeps track of focus state for one route. First we assume
- // we are blurred. If we are focused on initialization, the first 'action'
- // event will cause onFocus+willFocus events because we had previously been
- // considered blurred
- let lastFocusEvent = initialLastFocusEvent;
-
- const upstreamEvents = [
- 'willFocus',
- 'didFocus',
- 'willBlur',
- 'didBlur',
- 'refocus',
- 'action',
- ];
-
- const upstreamSubscribers = upstreamEvents.map((eventName) =>
- addListener(eventName, (payload) => {
- if (eventName === 'refocus') {
- emit(eventName, payload);
- return;
- }
-
- const { state, lastState, action } = payload;
- const lastRoutes = lastState && lastState.routes;
- const routes = state && state.routes;
-
- // const lastFocusKey =
- // lastState && lastState.routes && lastState.routes[lastState.index].key;
- const focusKey = routes && routes[state.index].key;
-
- const isChildFocused = focusKey === key;
- const lastRoute =
- lastRoutes && lastRoutes.find((route) => route.key === key);
- const newRoute = routes && routes.find((route) => route.key === key);
- const childPayload = {
- context: `${key}:${action.type}_${payload.context || 'Root'}`,
- state: newRoute,
- lastState: lastRoute,
- action,
- type: eventName,
- };
- const isTransitioning = !!state && state.isTransitioning;
-
- const previouslylastFocusEvent = lastFocusEvent;
-
- if (lastFocusEvent === 'didBlur') {
- // The child is currently blurred. Look for willFocus conditions
- if (eventName === 'willFocus' && isChildFocused) {
- emit((lastFocusEvent = 'willFocus'), childPayload);
- } else if (eventName === 'action' && isChildFocused) {
- emit((lastFocusEvent = 'willFocus'), childPayload);
- }
- }
- if (lastFocusEvent === 'willFocus') {
- // We are currently mid-focus. Look for didFocus conditions.
- // If state.isTransitioning is false, this child event happens immediately after willFocus
- if (eventName === 'didFocus' && isChildFocused && !isTransitioning) {
- emit((lastFocusEvent = 'didFocus'), childPayload);
- } else if (
- eventName === 'action' &&
- isChildFocused &&
- !isTransitioning
- ) {
- emit((lastFocusEvent = 'didFocus'), childPayload);
- }
- }
-
- if (lastFocusEvent === 'didFocus') {
- // The child is currently focused. Look for blurring events
- if (!isChildFocused) {
- // The child is no longer focused within this navigation state
- emit((lastFocusEvent = 'willBlur'), childPayload);
- } else if (eventName === 'willBlur') {
- // The parent is getting a willBlur event
- emit((lastFocusEvent = 'willBlur'), childPayload);
- } else if (
- eventName === 'action' &&
- previouslylastFocusEvent === 'didFocus'
- ) {
- // While focused, pass action events to children for grandchildren focus
- emit('action', childPayload);
- }
- }
-
- if (lastFocusEvent === 'willBlur') {
- // The child is mid-blur. Wait for transition to end
- if (eventName === 'action' && !isChildFocused && !isTransitioning) {
- // The child is done blurring because transitioning is over, or isTransitioning
- // never began and didBlur fires immediately after willBlur
- emit((lastFocusEvent = 'didBlur'), childPayload);
- } else if (eventName === 'didBlur') {
- // Pass through the parent didBlur event if it happens
- emit((lastFocusEvent = 'didBlur'), childPayload);
- } else if (
- eventName === 'action' &&
- isChildFocused &&
- !isTransitioning
- ) {
- emit((lastFocusEvent = 'didFocus'), childPayload);
- } else if (
- eventName === 'action' &&
- isChildFocused &&
- isTransitioning
- ) {
- emit((lastFocusEvent = 'willFocus'), childPayload);
- }
- }
-
- if (lastFocusEvent === 'didBlur' && !newRoute) {
- removeAll();
- }
- })
- );
-
- return {
- addListener(eventName, eventHandler) {
- const subscribers = getChildSubscribers(eventName);
- if (!subscribers) {
- throw new Error(`Invalid event name "${eventName}"`);
- }
- subscribers.add(eventHandler);
- const remove = () => {
- subscribers.delete(eventHandler);
- };
- return { remove };
- },
- emit(eventName, payload) {
- if (eventName !== 'refocus') {
- console.error(
- `navigation.emit only supports the 'refocus' event currently.`
- );
- return;
- }
- emit(eventName, payload);
- },
- };
-}
diff --git a/packages/core/src/getChildNavigation.js b/packages/core/src/getChildNavigation.js
index a941a749..c911c539 100644
--- a/packages/core/src/getChildNavigation.js
+++ b/packages/core/src/getChildNavigation.js
@@ -1,7 +1,7 @@
-import getChildEventSubscriber from './getChildEventSubscriber';
import getChildRouter from './getChildRouter';
import getNavigationActionCreators from './routers/getNavigationActionCreators';
import getChildrenNavigationCache from './getChildrenNavigationCache';
+import getEventManager from './getEventManager';
const createParamGetter = (route) => (paramName, defaultValue) => {
const params = route.params;
@@ -78,10 +78,7 @@ function getChildNavigation(navigation, childKey, getCurrentParentNavigation) {
};
return children[childKey];
} else {
- const childSubscriber = getChildEventSubscriber(
- navigation.addListener,
- childKey
- );
+ const { addListener, emit } = getEventManager(childKey);
children[childKey] = {
...actionHelpers,
@@ -115,9 +112,10 @@ function getChildNavigation(navigation, childKey, getCurrentParentNavigation) {
dispatch: navigation.dispatch,
getScreenProps: navigation.getScreenProps,
dangerouslyGetParent: getCurrentParentNavigation,
- addListener: childSubscriber.addListener,
- emit: childSubscriber.emit,
+ addListener,
+ emit,
};
+
return children[childKey];
}
}
diff --git a/packages/core/src/getEventManager.js b/packages/core/src/getEventManager.js
new file mode 100644
index 00000000..3860fccb
--- /dev/null
+++ b/packages/core/src/getEventManager.js
@@ -0,0 +1,61 @@
+// @ts-check
+
+/**
+ * @param {string} target
+ */
+export default function getEventManager(target) {
+ /**
+ * @type {Record void)[]>>}
+ */
+ const listeners = {};
+
+ /**
+ * @param {string} type
+ * @param {() => void} callback
+ */
+ const removeListener = (type, callback) => {
+ const callbacks = listeners[type] ? listeners[type][target] : undefined;
+
+ if (!callbacks) {
+ return;
+ }
+
+ const index = callbacks.indexOf(callback);
+
+ callbacks.splice(index, 1);
+ };
+
+ /**
+ * @param {string} type
+ * @param {() => void} callback
+ */
+ const addListener = (type, callback) => {
+ listeners[type] = listeners[type] || {};
+ listeners[type][target] = listeners[type][target] || [];
+ listeners[type][target].push(callback);
+
+ return {
+ remove: () => removeListener(type, callback),
+ };
+ };
+
+ return {
+ addListener,
+
+ /**
+ * @param {string} type
+ * @param {any} [data]
+ */
+ emit: (type, data) => {
+ const items = listeners[type] || {};
+
+ /**
+ * Copy the current list of callbacks in case they are mutated during execution
+ * @type {((data: any) => void)[] | undefined}
+ */
+ const callbacks = items[target] && items[target].slice();
+
+ callbacks?.forEach((cb) => cb(data));
+ },
+ };
+}
diff --git a/packages/core/src/navigators/createNavigator.js b/packages/core/src/navigators/createNavigator.js
index 8336c747..e7b7bccd 100644
--- a/packages/core/src/navigators/createNavigator.js
+++ b/packages/core/src/navigators/createNavigator.js
@@ -1,6 +1,7 @@
-import React from 'react';
+import * as React from 'react';
import invariant from '../utils/invariant';
import ThemeContext from '../views/ThemeContext';
+import NavigationFocusEvents from '../views/NavigationFocusEvents';
function createNavigator(NavigatorView, router, navigationConfig) {
class Navigator extends React.Component {
@@ -78,13 +79,21 @@ function createNavigator(NavigatorView, router, navigationConfig) {
render() {
return (
-
+
+ {
+ this.state.descriptors[target]?.navigation.emit(type, data);
+ }}
+ />
+
+
);
}
}
diff --git a/packages/core/src/views/NavigationFocusEvents.js b/packages/core/src/views/NavigationFocusEvents.js
new file mode 100644
index 00000000..5cb8c93f
--- /dev/null
+++ b/packages/core/src/views/NavigationFocusEvents.js
@@ -0,0 +1,222 @@
+// @ts-check
+
+import * as React from 'react';
+
+/**
+ * @typedef {object} State
+ * @prop {number} index
+ * @prop {({ key: string } & (State | {}))[]} routes
+ * @prop {boolean} [isTransitioning]
+ *
+ * @typedef {object} ParentPayload
+ * @prop {string} type
+ * @prop {object} action
+ * @prop {State} state
+ * @prop {State | {key: string, routes?: undefined, index?: undefined, isTransitioning?: undefined} | undefined | null} lastState
+ * @prop {string} [context]
+ *
+ * @typedef {object} Payload
+ * @prop {string} type
+ * @prop {object} action
+ * @prop {State | {key: string}} state
+ * @prop {State | {key: string} | undefined | null} lastState
+ * @prop {string} [context]
+ *
+ * @typedef {object} Props
+ * @prop {object} navigation
+ * @prop {Function} navigation.addListener
+ * @prop {Function} navigation.removeListener
+ * @prop {() => boolean} navigation.isFocused
+ * @prop {() => object | undefined} navigation.dangerouslyGetParent
+ * @prop {State} navigation.state
+ * @prop {(target: string, type: string, data: any) => void} onEvent
+ *
+ * @extends {React.Component}
+ */
+export default class NavigationEventManager extends React.Component {
+ componentDidMount() {
+ const { navigation } = this.props;
+
+ this._actionSubscription = navigation.addListener(
+ 'action',
+ this._handleAction
+ );
+
+ this._willFocusSubscription = navigation.addListener(
+ 'willFocus',
+ this._handleWillFocus
+ );
+
+ this._willBlurSubscription = navigation.addListener(
+ 'willBlur',
+ this._handleWillBlur
+ );
+ }
+
+ componentWillUnmount() {
+ this._actionSubscription?.remove();
+ this._willFocusSubscription?.remove();
+ this._willBlurSubscription?.remove();
+ }
+
+ /**
+ * @type {{ remove(): void } | undefined}
+ */
+ _actionSubscription;
+
+ /**
+ * @type {{ remove(): void } | undefined}
+ */
+ _willFocusSubscription;
+
+ /**
+ * @type {{ remove(): void } | undefined}
+ */
+ _willBlurSubscription;
+
+ /**
+ * @type {string | undefined}
+ */
+ _lastWillBlurKey;
+
+ /**
+ * @type {string | undefined}
+ */
+ _lastWillFocusKey;
+
+ /**
+ * The 'action' event will fire when navigation state changes.
+ * Detect if the focused route changed here and emit appropriate events.
+ *
+ * @param {ParentPayload} payload
+ */
+ _handleAction = ({ state, lastState, action, type, context }) => {
+ const { navigation, onEvent } = this.props;
+
+ // We should only emit events when the navigator is focused
+ // When navigator is not focused, screens inside shouldn't receive focused status either
+ if (!navigation.isFocused()) {
+ return;
+ }
+
+ const previous = lastState
+ ? lastState.routes?.[lastState.index]
+ : undefined;
+ const current = state.routes[state.index];
+
+ const payload = {
+ context: `${current.key}:${action.type}_${context || 'Root'}`,
+ state: current,
+ lastState: previous,
+ action,
+ type,
+ };
+
+ if (previous?.key !== current.key) {
+ this._emitFocus(current.key, payload);
+
+ if (previous?.key) {
+ this._emitBlur(previous.key, payload);
+ }
+ }
+
+ if (
+ lastState?.isTransitioning !== state.isTransitioning &&
+ state.isTransitioning === false
+ ) {
+ if (this._lastWillBlurKey) {
+ onEvent(this._lastWillBlurKey, 'didBlur', payload);
+ }
+
+ if (this._lastWillFocusKey) {
+ onEvent(this._lastWillFocusKey, 'didFocus', payload);
+ }
+ }
+
+ onEvent(current.key, 'action', payload);
+ };
+
+ /**
+ * @param {ParentPayload} payload
+ */
+ _handleWillFocus = ({ lastState, action, context, type }) => {
+ const { navigation } = this.props;
+ const route = navigation.state.routes[navigation.state.index];
+
+ this._emitFocus(route.key, {
+ context: `${route.key}:${action.type}_${context || 'Root'}`,
+ state: route,
+ lastState: lastState?.routes?.find((r) => r.key === route.key),
+ action,
+ type,
+ });
+ };
+
+ /**
+ * @param {ParentPayload} payload
+ */
+ _handleWillBlur = ({ lastState, action, context, type }) => {
+ const { navigation } = this.props;
+ const route = navigation.state.routes[navigation.state.index];
+
+ this._emitBlur(route.key, {
+ context: `${route.key}:${action.type}_${context || 'Root'}`,
+ state: route,
+ lastState: lastState?.routes?.find((r) => r.key === route.key),
+ action,
+ type,
+ });
+ };
+
+ /**
+ * @param {string} target
+ * @param {Payload} payload
+ */
+ _emitFocus = (target, payload) => {
+ if (this._lastWillBlurKey === target) {
+ this._lastWillBlurKey = undefined;
+ }
+
+ if (this._lastWillFocusKey === target) {
+ return;
+ }
+
+ this._lastWillFocusKey = target;
+
+ const { navigation, onEvent } = this.props;
+
+ onEvent(target, 'willFocus', payload);
+
+ if (typeof navigation.state.isTransitioning !== 'boolean') {
+ onEvent(target, 'didFocus', payload);
+ }
+ };
+
+ /**
+ * @param {string} target
+ * @param {Payload} payload
+ */
+ _emitBlur = (target, payload) => {
+ if (this._lastWillFocusKey === target) {
+ this._lastWillFocusKey = undefined;
+ }
+
+ if (this._lastWillBlurKey === target) {
+ return;
+ }
+
+ this._lastWillBlurKey = target;
+
+ const { navigation, onEvent } = this.props;
+
+ onEvent(target, 'willBlur', payload);
+
+ if (typeof navigation.state.isTransitioning !== 'boolean') {
+ onEvent(target, 'didBlur', payload);
+ }
+ };
+
+ render() {
+ return null;
+ }
+}
diff --git a/packages/core/src/views/withNavigationFocus.js b/packages/core/src/views/withNavigationFocus.js
index d7b68ca5..a6edc55c 100644
--- a/packages/core/src/views/withNavigationFocus.js
+++ b/packages/core/src/views/withNavigationFocus.js
@@ -8,25 +8,25 @@ export default function withNavigationFocus(Component) {
Component.displayName || Component.name
})`;
- constructor(props) {
- super(props);
+ state = {
+ isFocused: this.props.navigation.isFocused(),
+ };
+
+ componentDidMount() {
+ const { navigation } = this.props;
this.subscriptions = [
- props.navigation.addListener('didFocus', () =>
+ navigation.addListener('willFocus', () =>
this.setState({ isFocused: true })
),
- props.navigation.addListener('willBlur', () =>
+ navigation.addListener('willBlur', () =>
this.setState({ isFocused: false })
),
];
-
- this.state = {
- isFocused: props.navigation ? props.navigation.isFocused() : false,
- };
}
componentWillUnmount() {
- this.subscriptions.forEach((sub) => sub.remove());
+ this.subscriptions?.forEach((sub) => sub.remove());
}
render() {
diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json
new file mode 100644
index 00000000..60ea0bbe
--- /dev/null
+++ b/packages/core/tsconfig.json
@@ -0,0 +1,6 @@
+{
+ "extends": "../../tsconfig",
+ "compilerOptions": {
+ "allowJs": true
+ }
+}
diff --git a/packages/native/src/createNavigationAwareScrollable.js b/packages/native/src/createNavigationAwareScrollable.js
index 56f89ba6..21c55014 100644
--- a/packages/native/src/createNavigationAwareScrollable.js
+++ b/packages/native/src/createNavigationAwareScrollable.js
@@ -4,7 +4,7 @@ import { withNavigation } from '@react-navigation/core';
export default function createNavigationAwareScrollable(Component) {
const ComponentWithNavigationScrolling = withNavigation(
- class extends React.PureComponent {
+ class extends React.PureComponent {
static displayName = `withNavigationScrolling(${
Component.displayName || Component.name
})`;
@@ -60,7 +60,7 @@ export default function createNavigationAwareScrollable(Component) {
}
);
- class NavigationAwareScrollable extends React.PureComponent {
+ class NavigationAwareScrollable extends React.PureComponent {
static displayName = `NavigationAwareScrollable(${
Component.displayName || Component.name
})`;
diff --git a/packages/stack/scripts/stack.patch b/packages/stack/scripts/stack.patch
index 7c8cd119..5f325e60 100644
--- a/packages/stack/scripts/stack.patch
+++ b/packages/stack/scripts/stack.patch
@@ -1,6 +1,6 @@
diff -Naur ../../node_modules/@react-navigation/stack/src/index.tsx src/vendor/index.tsx
---- ../../node_modules/@react-navigation/stack/src/index.tsx 2020-03-23 00:04:17.000000000 +0100
-+++ src/vendor/index.tsx 2020-03-23 00:04:37.000000000 +0100
+--- ../../node_modules/@react-navigation/stack/src/index.tsx 2020-03-23 11:43:17.000000000 +0100
++++ src/vendor/index.tsx 2020-03-23 11:43:59.000000000 +0100
@@ -3,11 +3,6 @@
import * as TransitionSpecs from './TransitionConfigs/TransitionSpecs';
import * as TransitionPresets from './TransitionConfigs/TransitionPresets';
@@ -28,7 +28,7 @@ diff -Naur ../../node_modules/@react-navigation/stack/src/index.tsx src/vendor/i
StackHeaderTitleProps,
StackCardInterpolatedStyle,
diff -Naur ../../node_modules/@react-navigation/stack/src/navigators/createStackNavigator.tsx src/vendor/navigators/createStackNavigator.tsx
---- ../../node_modules/@react-navigation/stack/src/navigators/createStackNavigator.tsx 2020-03-23 00:04:17.000000000 +0100
+--- ../../node_modules/@react-navigation/stack/src/navigators/createStackNavigator.tsx 2020-03-23 11:43:17.000000000 +0100
+++ src/vendor/navigators/createStackNavigator.tsx 1970-01-01 01:00:00.000000000 +0100
@@ -1,81 +0,0 @@
-import * as React from 'react';
@@ -113,8 +113,8 @@ diff -Naur ../../node_modules/@react-navigation/stack/src/navigators/createStack
- typeof StackNavigator
->(StackNavigator);
diff -Naur ../../node_modules/@react-navigation/stack/src/types.tsx src/vendor/types.tsx
---- ../../node_modules/@react-navigation/stack/src/types.tsx 2020-03-23 00:04:17.000000000 +0100
-+++ src/vendor/types.tsx 2020-03-23 00:04:40.000000000 +0100
+--- ../../node_modules/@react-navigation/stack/src/types.tsx 2020-03-23 11:43:17.000000000 +0100
++++ src/vendor/types.tsx 2020-03-23 11:43:59.000000000 +0100
@@ -8,14 +8,28 @@
} from 'react-native';
import { EdgeInsets } from 'react-native-safe-area-context';
@@ -240,8 +240,8 @@ diff -Naur ../../node_modules/@react-navigation/stack/src/types.tsx src/vendor/t
export type StackNavigationConfig = {
diff -Naur ../../node_modules/@react-navigation/stack/src/views/Header/Header.tsx src/vendor/views/Header/Header.tsx
---- ../../node_modules/@react-navigation/stack/src/views/Header/Header.tsx 2020-03-23 00:04:17.000000000 +0100
-+++ src/vendor/views/Header/Header.tsx 2020-03-23 00:04:40.000000000 +0100
+--- ../../node_modules/@react-navigation/stack/src/views/Header/Header.tsx 2020-03-23 11:43:17.000000000 +0100
++++ src/vendor/views/Header/Header.tsx 2020-03-23 11:43:59.000000000 +0100
@@ -1,12 +1,14 @@
import * as React from 'react';
-import { StackActions } from '@react-navigation/native';
@@ -321,8 +321,8 @@ diff -Naur ../../node_modules/@react-navigation/stack/src/views/Header/Header.ts
+
+export default Header;
diff -Naur ../../node_modules/@react-navigation/stack/src/views/Header/HeaderBackButton.tsx src/vendor/views/Header/HeaderBackButton.tsx
---- ../../node_modules/@react-navigation/stack/src/views/Header/HeaderBackButton.tsx 2020-03-23 00:04:17.000000000 +0100
-+++ src/vendor/views/Header/HeaderBackButton.tsx 2020-03-23 00:04:40.000000000 +0100
+--- ../../node_modules/@react-navigation/stack/src/views/Header/HeaderBackButton.tsx 2020-03-23 11:43:17.000000000 +0100
++++ src/vendor/views/Header/HeaderBackButton.tsx 2020-03-23 11:43:59.000000000 +0100
@@ -8,9 +8,9 @@
StyleSheet,
LayoutChangeEvent,
@@ -335,8 +335,8 @@ diff -Naur ../../node_modules/@react-navigation/stack/src/views/Header/HeaderBac
type Props = StackHeaderLeftButtonProps;
diff -Naur ../../node_modules/@react-navigation/stack/src/views/Header/HeaderBackground.tsx src/vendor/views/Header/HeaderBackground.tsx
---- ../../node_modules/@react-navigation/stack/src/views/Header/HeaderBackground.tsx 2020-03-23 00:04:17.000000000 +0100
-+++ src/vendor/views/Header/HeaderBackground.tsx 2020-03-23 00:04:40.000000000 +0100
+--- ../../node_modules/@react-navigation/stack/src/views/Header/HeaderBackground.tsx 2020-03-23 11:43:17.000000000 +0100
++++ src/vendor/views/Header/HeaderBackground.tsx 2020-03-23 11:43:59.000000000 +0100
@@ -1,6 +1,6 @@
import * as React from 'react';
import { Animated, StyleSheet, Platform, ViewProps } from 'react-native';
@@ -346,8 +346,8 @@ diff -Naur ../../node_modules/@react-navigation/stack/src/views/Header/HeaderBac
type Props = ViewProps & {
children?: React.ReactNode;
diff -Naur ../../node_modules/@react-navigation/stack/src/views/Header/HeaderContainer.tsx src/vendor/views/Header/HeaderContainer.tsx
---- ../../node_modules/@react-navigation/stack/src/views/Header/HeaderContainer.tsx 2020-03-23 00:04:17.000000000 +0100
-+++ src/vendor/views/Header/HeaderContainer.tsx 2020-03-23 00:04:40.000000000 +0100
+--- ../../node_modules/@react-navigation/stack/src/views/Header/HeaderContainer.tsx 2020-03-23 11:43:17.000000000 +0100
++++ src/vendor/views/Header/HeaderContainer.tsx 2020-03-23 11:43:59.000000000 +0100
@@ -1,11 +1,6 @@
import * as React from 'react';
import { View, StyleSheet, StyleProp, ViewStyle } from 'react-native';
@@ -399,8 +399,8 @@ diff -Naur ../../node_modules/@react-navigation/stack/src/views/Header/HeaderCon
);
})}
diff -Naur ../../node_modules/@react-navigation/stack/src/views/Header/HeaderSegment.tsx src/vendor/views/Header/HeaderSegment.tsx
---- ../../node_modules/@react-navigation/stack/src/views/Header/HeaderSegment.tsx 2020-03-23 00:04:17.000000000 +0100
-+++ src/vendor/views/Header/HeaderSegment.tsx 2020-03-23 00:04:40.000000000 +0100
+--- ../../node_modules/@react-navigation/stack/src/views/Header/HeaderSegment.tsx 2020-03-23 11:43:17.000000000 +0100
++++ src/vendor/views/Header/HeaderSegment.tsx 2020-03-23 11:43:59.000000000 +0100
@@ -8,7 +8,7 @@
ViewStyle,
} from 'react-native';
@@ -420,8 +420,8 @@ diff -Naur ../../node_modules/@react-navigation/stack/src/views/Header/HeaderSeg
};
diff -Naur ../../node_modules/@react-navigation/stack/src/views/Header/HeaderTitle.tsx src/vendor/views/Header/HeaderTitle.tsx
---- ../../node_modules/@react-navigation/stack/src/views/Header/HeaderTitle.tsx 2020-03-23 00:04:17.000000000 +0100
-+++ src/vendor/views/Header/HeaderTitle.tsx 2020-03-23 00:04:40.000000000 +0100
+--- ../../node_modules/@react-navigation/stack/src/views/Header/HeaderTitle.tsx 2020-03-23 11:43:17.000000000 +0100
++++ src/vendor/views/Header/HeaderTitle.tsx 2020-03-23 11:43:59.000000000 +0100
@@ -1,6 +1,6 @@
import * as React from 'react';
import { Animated, StyleSheet, Platform } from 'react-native';
@@ -431,8 +431,8 @@ diff -Naur ../../node_modules/@react-navigation/stack/src/views/Header/HeaderTit
type Props = React.ComponentProps & {
tintColor?: string;
diff -Naur ../../node_modules/@react-navigation/stack/src/views/Stack/Card.tsx src/vendor/views/Stack/Card.tsx
---- ../../node_modules/@react-navigation/stack/src/views/Stack/Card.tsx 2020-03-23 00:04:17.000000000 +0100
-+++ src/vendor/views/Stack/Card.tsx 2020-03-23 00:04:40.000000000 +0100
+--- ../../node_modules/@react-navigation/stack/src/views/Stack/Card.tsx 2020-03-23 11:43:17.000000000 +0100
++++ src/vendor/views/Stack/Card.tsx 2020-03-23 11:43:59.000000000 +0100
@@ -138,7 +138,7 @@
private interactionHandle: number | undefined;
@@ -443,8 +443,8 @@ diff -Naur ../../node_modules/@react-navigation/stack/src/views/Stack/Card.tsx s
private animate = ({
closing,
diff -Naur ../../node_modules/@react-navigation/stack/src/views/Stack/CardContainer.tsx src/vendor/views/Stack/CardContainer.tsx
---- ../../node_modules/@react-navigation/stack/src/views/Stack/CardContainer.tsx 2020-03-23 00:04:17.000000000 +0100
-+++ src/vendor/views/Stack/CardContainer.tsx 2020-03-23 00:04:40.000000000 +0100
+--- ../../node_modules/@react-navigation/stack/src/views/Stack/CardContainer.tsx 2020-03-23 11:43:17.000000000 +0100
++++ src/vendor/views/Stack/CardContainer.tsx 2020-03-27 14:41:20.000000000 +0100
@@ -1,10 +1,16 @@
import * as React from 'react';
import { Animated, View, StyleSheet, StyleProp, ViewStyle } from 'react-native';
@@ -464,33 +464,7 @@ diff -Naur ../../node_modules/@react-navigation/stack/src/views/Stack/CardContai
type Props = TransitionPreset & {
index: number;
-@@ -36,6 +42,7 @@
- closing: boolean
- ) => void;
- onTransitionEnd?: (props: { route: Route }, closing: boolean) => void;
-+ onTransitionComplete: (props: { route: Route }) => void;
- onPageChangeStart?: () => void;
- onPageChangeConfirm?: () => void;
- onPageChangeCancel?: () => void;
-@@ -83,6 +90,7 @@
- layout,
- onCloseRoute,
- onOpenRoute,
-+ onTransitionComplete,
- onPageChangeCancel,
- onPageChangeConfirm,
- onPageChangeStart,
-@@ -152,6 +160,9 @@
- };
- }, [pointerEvents, scene.progress.next]);
-
-+ // eslint-disable-next-line react-hooks/exhaustive-deps
-+ React.useEffect(() => onTransitionComplete({ route: scene.route }), []);
-+
- return (
-
diff -Naur ../../node_modules/@react-navigation/stack/src/views/Stack/CardStack.tsx src/vendor/views/Stack/CardStack.tsx
---- ../../node_modules/@react-navigation/stack/src/views/Stack/CardStack.tsx 2020-03-23 00:04:17.000000000 +0100
-+++ src/vendor/views/Stack/CardStack.tsx 2020-03-23 00:04:40.000000000 +0100
+--- ../../node_modules/@react-navigation/stack/src/views/Stack/CardStack.tsx 2020-03-23 11:43:17.000000000 +0100
++++ src/vendor/views/Stack/CardStack.tsx 2020-03-27 14:41:20.000000000 +0100
@@ -9,9 +9,8 @@
ViewProps,
} from 'react-native';
@@ -521,33 +495,9 @@ diff -Naur ../../node_modules/@react-navigation/stack/src/views/Stack/CardStack.
Layout,
StackHeaderMode,
StackCardMode,
-@@ -54,6 +54,7 @@
- renderHeader: (props: HeaderContainerProps) => React.ReactNode;
- renderScene: (props: { route: Route }) => React.ReactNode;
- headerMode: StackHeaderMode;
-+ onTransitionComplete: (props: { route: Route }) => void;
- onTransitionStart: (
- props: { route: Route },
- closing: boolean
-@@ -383,6 +384,7 @@
- renderHeader,
- renderScene,
- headerMode,
-+ onTransitionComplete,
- onTransitionStart,
- onTransitionEnd,
- onPageChangeStart,
-@@ -560,6 +562,7 @@
- renderScene={renderScene}
- onOpenRoute={onOpenRoute}
- onCloseRoute={onCloseRoute}
-+ onTransitionComplete={onTransitionComplete}
- onTransitionStart={onTransitionStart}
- onTransitionEnd={onTransitionEnd}
- gestureEnabled={index !== 0 && getGesturesEnabled({ route })}
diff -Naur ../../node_modules/@react-navigation/stack/src/views/Stack/StackView.tsx src/vendor/views/Stack/StackView.tsx
---- ../../node_modules/@react-navigation/stack/src/views/Stack/StackView.tsx 2020-03-23 00:04:17.000000000 +0100
-+++ src/vendor/views/Stack/StackView.tsx 2020-03-23 00:07:11.000000000 +0100
+--- ../../node_modules/@react-navigation/stack/src/views/Stack/StackView.tsx 2020-03-23 11:43:17.000000000 +0100
++++ src/vendor/views/Stack/StackView.tsx 2020-03-27 14:41:20.000000000 +0100
@@ -4,9 +4,9 @@
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import {
@@ -676,11 +626,3 @@ diff -Naur ../../node_modules/@react-navigation/stack/src/views/Stack/StackView.
render() {
const {
-@@ -395,6 +427,7 @@
- closingRouteKeys={closingRouteKeys}
- onOpenRoute={this.handleOpenRoute}
- onCloseRoute={this.handleCloseRoute}
-+ onTransitionComplete={this.handleTransitionComplete}
- onTransitionStart={this.handleTransitionStart}
- onTransitionEnd={this.handleTransitionEnd}
- renderHeader={this.renderHeader}
diff --git a/packages/stack/src/vendor/views/Stack/CardContainer.tsx b/packages/stack/src/vendor/views/Stack/CardContainer.tsx
index 409f97b9..d58ec1b0 100644
--- a/packages/stack/src/vendor/views/Stack/CardContainer.tsx
+++ b/packages/stack/src/vendor/views/Stack/CardContainer.tsx
@@ -42,7 +42,6 @@ type Props = TransitionPreset & {
closing: boolean
) => void;
onTransitionEnd?: (props: { route: Route }, closing: boolean) => void;
- onTransitionComplete: (props: { route: Route }) => void;
onPageChangeStart?: () => void;
onPageChangeConfirm?: () => void;
onPageChangeCancel?: () => void;
@@ -90,7 +89,6 @@ function CardContainer({
layout,
onCloseRoute,
onOpenRoute,
- onTransitionComplete,
onPageChangeCancel,
onPageChangeConfirm,
onPageChangeStart,
@@ -160,9 +158,6 @@ function CardContainer({
};
}, [pointerEvents, scene.progress.next]);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- React.useEffect(() => onTransitionComplete({ route: scene.route }), []);
-
return (
React.ReactNode;
renderScene: (props: { route: Route }) => React.ReactNode;
headerMode: StackHeaderMode;
- onTransitionComplete: (props: { route: Route }) => void;
onTransitionStart: (
props: { route: Route },
closing: boolean
@@ -384,7 +383,6 @@ export default class CardStack extends React.Component {
renderHeader,
renderScene,
headerMode,
- onTransitionComplete,
onTransitionStart,
onTransitionEnd,
onPageChangeStart,
@@ -562,7 +560,6 @@ export default class CardStack extends React.Component {
renderScene={renderScene}
onOpenRoute={onOpenRoute}
onCloseRoute={onCloseRoute}
- onTransitionComplete={onTransitionComplete}
onTransitionStart={onTransitionStart}
onTransitionEnd={onTransitionEnd}
gestureEnabled={index !== 0 && getGesturesEnabled({ route })}
diff --git a/packages/stack/src/vendor/views/Stack/StackView.tsx b/packages/stack/src/vendor/views/Stack/StackView.tsx
index 6c6a0873..a0196498 100644
--- a/packages/stack/src/vendor/views/Stack/StackView.tsx
+++ b/packages/stack/src/vendor/views/Stack/StackView.tsx
@@ -427,7 +427,6 @@ export default class StackView extends React.Component {
closingRouteKeys={closingRouteKeys}
onOpenRoute={this.handleOpenRoute}
onCloseRoute={this.handleCloseRoute}
- onTransitionComplete={this.handleTransitionComplete}
onTransitionStart={this.handleTransitionStart}
onTransitionEnd={this.handleTransitionEnd}
renderHeader={this.renderHeader}