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}