fix(react): nested router outlets will not reanimate entered views (#24672)

Resolves #24107
This commit is contained in:
Sean Perkins
2022-01-31 11:55:08 -05:00
committed by GitHub
parent 484de5074d
commit 43aa6c11f4
7 changed files with 94 additions and 84 deletions

View File

@@ -18,6 +18,8 @@ interface StackManagerProps {
interface StackManagerState {}
const isViewVisible = (el: HTMLElement) => !el.classList.contains('ion-page-invisible') && !el.classList.contains('ion-page-hidden');
export class StackManager extends React.PureComponent<StackManagerProps, StackManagerState> {
id: string;
context!: React.ContextType<typeof RouteManagerContext>;
@@ -97,16 +99,64 @@ export class StackManager extends React.PureComponent<StackManagerProps, StackMa
this.ionRouterOutlet?.props.children,
routeInfo
) as React.ReactElement;
if (enteringViewItem) {
enteringViewItem.reactElement = enteringRoute;
} else if (enteringRoute) {
enteringViewItem = this.context.createViewItem(this.id, enteringRoute, routeInfo);
this.context.addViewItem(enteringViewItem);
}
if (!enteringViewItem) {
if (enteringRoute) {
enteringViewItem = this.context.createViewItem(this.id, enteringRoute, routeInfo);
this.context.addViewItem(enteringViewItem);
}
}
if (enteringViewItem && enteringViewItem.ionPageElement) {
/**
* If the entering view item is the same as the leaving view item,
* then we don't need to transition.
*/
if (enteringViewItem === leavingViewItem) {
/**
* If the entering view item is the same as the leaving view item,
* we are either transitioning using parameterized routes to the same view
* or a parent router outlet is re-rendering as a result of React props changing.
*
* If the route data does not match the current path, the parent router outlet
* is attempting to transition and we cancel the operation.
*/
if (enteringViewItem.routeData.match.url !== routeInfo.pathname) {
return;
}
}
/**
* If there isn't a leaving view item, but the route info indicates
* that the user has routed from a previous path, then we need
* to find the leaving view item to transition between.
*/
if (!leavingViewItem && this.props.routeInfo.prevRouteLastPathname) {
leavingViewItem = this.context.findViewItemByPathname(this.props.routeInfo.prevRouteLastPathname, this.id);
}
/**
* If the entering view is already visible and the leaving view is not, the transition does not need to occur.
*/
if (isViewVisible(enteringViewItem.ionPageElement) && leavingViewItem !== undefined && !isViewVisible(leavingViewItem.ionPageElement!)) {
return;
}
/**
* The view should only be transitioned in the following cases:
* 1. Performing a replace or pop action, such as a swipe to go back gesture
* to animation the leaving view off the screen.
*
* 2. Navigating between top-level router outlets, such as /page-1 to /page-2;
* or navigating within a nested outlet, such as /tabs/tab-1 to /tabs/tab-2.
*
* 3. The entering view is an ion-router-outlet containing a page
* matching the current route and that hasn't already transitioned in.
*
* This should only happen when navigating directly to a nested router outlet
* route or on an initial page load (i.e. refreshing). In cases when loading
* /tabs/tab-1, we need to transition the /tabs page element into the view.
*/
this.transitionPage(routeInfo, enteringViewItem, leavingViewItem);
} else if (leavingViewItem && !enteringRoute && !enteringViewItem) {
// If we have a leavingView but no entering view/route, we are probably leaving to

View File

@@ -54,7 +54,7 @@ describe('Nested Outlets 2', () => {
Utilizes `ionPage` prop on `IonRouterOutlet` to make the router outlet
a target of the transition.
This one uses a few more nested outlets.
Note: There is a limitation when going back to the Home page from Welcome in
Note: There is a limitation when going back to the Home page from Welcome in
that the transition doesn't do a backwards animation. This is because the top level
outlet is transition from and to itself, therefore it can't animate.
I think the same issue exists when going from a item page back to the list page.
@@ -83,12 +83,12 @@ describe('Nested Outlets 2', () => {
cy.ionPageVisible('list');
});
it(`/nested-outlet2 >
it(`/nested-outlet2 >
Go to Welcome IonItem click >
Go to list from Welcome IonItem click >
Go to list from Welcome IonItem click >
Item#1 IonItem Click >
Item page should be visible
`, () => {
Item page should be visible
`, () => {
cy.visit(`http://localhost:${port}/nested-outlet2`);
cy.ionPageVisible('home');
cy.ionNav('ion-item', 'Go to Welcome');
@@ -99,13 +99,32 @@ describe('Nested Outlets 2', () => {
cy.ionPageVisible('item');
});
it(`/nested-outlet2 >
it(`/nested-outlet2 >
Go to list from Home IonItem click >
Item#1 IonItem Click >
Item page should be visible >
Item page should be visible >
Back >
List page should be visible
`, () => {
cy.visit(`http://localhost:${port}/nested-outlet2`);
cy.ionPageVisible('home');
cy.ionNav('ion-item', 'Go to list from Home');
cy.ionPageVisible('list');
cy.ionNav('ion-item', 'Item #1');
cy.ionPageVisible('item');
cy.ionBackClick('item');
cy.ionPageVisible('list');
});
it(`/nested-outlet2 >
Go to list from Home IonItem click >
Item#1 IonItem Click >
Item page should be visible >
Back >
List page should be visible
Back >
Home page should be visible
`, () => {
`, () => {
cy.visit(`http://localhost:${port}/nested-outlet2`);
cy.ionPageVisible('home');
cy.ionNav('ion-item', 'Go to list from Home');

View File

@@ -214,7 +214,7 @@ describe('Routing Tests', () => {
cy.get('div[data-testid=count-label]').contains('3');
});
it('/asdf, when accessing a route not defined from root outlet, should show not found page', () => {
it('/routing/asdf, when accessing a route not defined from root outlet, should show not found page', () => {
cy.visit(`http://localhost:${port}/routing/asdf`, { failOnStatusCode: false });
cy.ionPageVisible('not-found');
cy.get('div').contains('Not found');

View File

@@ -35,7 +35,7 @@ export const IonRouterContext = React.createContext<IonRouterContextState>({
});
/**
* A hook for more direct control over routing in an Ionic React applicaiton. Allows you to pass additional meta-data to the router before the call to the native router.
* A hook for more direct control over routing in an Ionic React application. Allows you to pass additional meta-data to the router before the call to the native router.
*/
export function useIonRouter(): UseIonRouterResult {
const context = useContext(IonRouterContext);

View File

@@ -1,3 +1,4 @@
import { componentOnReady } from '@ionic/core';
import React from 'react';
import { IonRouterOutletInner } from '../components/inner-proxies';
@@ -24,9 +25,9 @@ export class OutletPageManager extends React.Component<OutletPageManagerProps> {
componentDidMount() {
if (this.ionRouterOutlet) {
setTimeout(() => {
componentOnReady(this.ionRouterOutlet, () => {
this.context.registerIonPage(this.ionRouterOutlet!, this.props.routeInfo!);
}, 25);
});
this.ionRouterOutlet.addEventListener(
'ionViewWillEnter',

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { Route } from 'react-router-dom';
import { IonApp, setupIonicReact } from '@ionic/react';
import { IonApp, IonButton, IonContent, IonHeader, IonPage, IonRouterOutlet, IonTitle, IonToolbar, setupIonicReact } from '@ionic/react';
import { IonReactRouter } from '@ionic/react-router';
/* Core CSS required for Ionic components to work properly */
@@ -24,17 +24,17 @@ import './theme/variables.css';
import Main from './pages/Main';
import OverlayHooks from './pages/overlay-hooks/OverlayHooks';
import OverlayComponents from './pages/overlay-components/OverlayComponents';
import Tabs from './pages/Tabs';
setupIonicReact();
const App: React.FC = () => (
<IonApp>
<IonReactRouter>
<Route path="/" component={Main} />
<Route path="/overlay-hooks" component={OverlayHooks} />
<Route path="/overlay-components" component={OverlayComponents} />
<Route path="/tabs" component={Tabs} />
<IonRouterOutlet>
<Route path="/" component={Main} />
<Route path="/overlay-hooks" component={OverlayHooks} />
<Route path="/overlay-components" component={OverlayComponents} />
</IonRouterOutlet>
</IonReactRouter>
</IonApp>
);

View File

@@ -1,60 +0,0 @@
import { IonContent, IonHeader, IonLabel, IonPage, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs, IonTitle, IonToolbar } from '@ionic/react';
import React from 'react';
import { Redirect, Route } from 'react-router';
const Tab1: React.FC = () => (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>
Tab 1
</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonLabel>Tab 1 page</IonLabel>
</IonContent>
</IonPage>
);
const Tab2: React.FC = () => (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>
Tab 2
</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonLabel>Tab 2 page</IonLabel>
</IonContent>
</IonPage>
);
const Tabs: React.FC = () => (
<IonTabs>
<IonRouterOutlet>
<Route exact path="/tabs/tab1">
<Tab1 />
</Route>
<Route exact path="/tabs/tab2">
<Tab2 />
</Route>
<Route exact path="/tabs">
<Redirect to ="/tabs/tab1" />
</Route>
</IonRouterOutlet>
<IonTabBar slot="bottom">
<IonTabButton tab="tab1" href="/tabs/tab1">
<IonLabel>Tab 1</IonLabel>
</IonTabButton>
<IonTabButton tab="tab2" href="/tabs/tab2">
<IonLabel>Tab 2</IonLabel>
</IonTabButton>
</IonTabBar>
</IonTabs>
)
export default Tabs;