mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
fix(react): nested router outlets will not reanimate entered views (#24672)
Resolves #24107
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user