Skip to content

Commit 9b00e20

Browse files
committed
fix(angular): fix navigation stack when using browser back and forward button
fix(angular): respect direction set via the NavController fix(angular): adapt tests fix(angular): fixes
1 parent 45bbea6 commit 9b00e20

File tree

4 files changed

+63
-20
lines changed

4 files changed

+63
-20
lines changed

packages/angular/src/directives/navigation/stack-controller.ts

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Location } from '@angular/common';
22
import { ComponentRef, NgZone } from '@angular/core';
33
import { ActivatedRoute, Router } from '@angular/router';
4-
import { AnimationBuilder, RouterDirection } from '@ionic/core';
4+
import { AnimationBuilder, NavDirection, RouterDirection } from '@ionic/core';
55

66
import { bindLifecycleEvents } from '../../providers/angular-delegate';
77
import { NavController } from '../../providers/nav-controller';
@@ -62,14 +62,8 @@ export class StackController {
6262
}
6363

6464
setActive(enteringView: RouteView): Promise<StackDidChangeEvent> {
65-
const consumeResult = this.navCtrl.consumeTransition();
65+
const { isDirectionBasedOnNavigationIds, ...consumeResult } = this.navCtrl.consumeTransition();
6666
let { direction, animation, animationBuilder } = consumeResult;
67-
const leavingView = this.activeView;
68-
const tabSwitch = isTabSwitch(enteringView, leavingView);
69-
if (tabSwitch) {
70-
direction = 'back';
71-
animation = undefined;
72-
}
7367

7468
const viewsSnapshot = this.views.slice();
7569

@@ -87,25 +81,65 @@ export class StackController {
8781
}
8882

8983
/**
90-
* If the navigation action
91-
* sets `replaceUrl: true`
92-
* then we need to make sure
93-
* we remove the last item
94-
* from our views stack
84+
* If the navigation action sets `replaceUrl: true` then we need to make sure
85+
* we remove the last item from our views stack
9586
*/
96-
if (currentNavigation?.extras?.replaceUrl) {
87+
if (currentNavigation?.extras?.replaceUrl && currentNavigation?.trigger !== 'popstate') {
9788
if (this.views.length > 0) {
9889
this.views.splice(-1, 1);
9990
}
10091
}
10192

102-
const reused = this.views.includes(enteringView);
93+
// determine direction based on the order of the views in the stack
94+
const leavingView = this.activeView;
95+
const isEnteringViewReused = this.views.includes(enteringView);
96+
const leavingViewIndex = leavingView ? this.views.indexOf(leavingView) : -1;
97+
const enteringViewIndex = isEnteringViewReused ? this.views.indexOf(enteringView) : this.views.length;
98+
const suggestedDirectionBasedOnStackOrder: NavDirection | undefined =
99+
leavingViewIndex === -1 ? undefined : enteringViewIndex < leavingViewIndex ? 'back' : 'forward';
100+
101+
/**
102+
* The user triggered a back navigation on a page that was navigated to with root. In this case, the new page
103+
* becomes the root and the leavingView is removed.
104+
*
105+
* This can happen when e.g.:
106+
* - using the NavController's navigateBack on a root page
107+
* - navigating to a page with navigateRoot and then using the browser back button
108+
*/
109+
if (
110+
direction === 'back' &&
111+
isDirectionBasedOnNavigationIds &&
112+
leavingView?.root &&
113+
currentNavigation?.trigger === 'popstate'
114+
) {
115+
if (leavingViewIndex >= 0) {
116+
this.views.splice(leavingViewIndex, 1);
117+
}
118+
}
119+
120+
/**
121+
* direction based on stack order takes precedence over direction based on navigation ids
122+
*
123+
* only applied if the user did not explicitly set the direction
124+
* (e.g. via the NavController, routerLink directive etc.)
125+
*/
126+
if (isDirectionBasedOnNavigationIds && suggestedDirectionBasedOnStackOrder) {
127+
direction = suggestedDirectionBasedOnStackOrder;
128+
animation = suggestedDirectionBasedOnStackOrder;
129+
}
130+
131+
const tabSwitch = isTabSwitch(enteringView, leavingView);
132+
if (tabSwitch) {
133+
direction = 'back';
134+
animation = undefined;
135+
}
136+
103137
const views = this.insertView(enteringView, direction);
104138

105139
// Trigger change detection before transition starts
106140
// This will call ngOnInit() the first time too, just after the view
107141
// was attached to the dom, but BEFORE the transition starts
108-
if (!reused) {
142+
if (!isEnteringViewReused) {
109143
enteringView.ref.changeDetectorRef.detectChanges();
110144
}
111145

packages/angular/src/directives/navigation/stack-utils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const insertView = (views: RouteView[], view: RouteView, direction: Route
1414

1515
const setRoot = (views: RouteView[], view: RouteView) => {
1616
views = views.filter((v) => v.stackId !== view.stackId);
17+
view.root = true;
1718
views.push(view);
1819
return views;
1920
};
@@ -110,4 +111,5 @@ export interface RouteView {
110111
savedExtras?: NavigationExtras;
111112
unlistenEvents: () => void;
112113
animationBuilder?: AnimationBuilder;
114+
root?: boolean;
113115
}

packages/angular/src/providers/nav-controller.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,11 @@ export class NavController {
3737
if (router) {
3838
router.events.subscribe((ev) => {
3939
if (ev instanceof NavigationStart) {
40+
// restoredState is set if the browser back/forward button is used
4041
const id = ev.restoredState ? ev.restoredState.navigationId : ev.id;
4142
this.guessDirection = id < this.lastNavId ? 'back' : 'forward';
42-
this.guessAnimation = !ev.restoredState ? this.guessDirection : undefined;
43-
this.lastNavId = this.guessDirection === 'forward' ? ev.id : id;
43+
this.guessAnimation = this.guessDirection;
44+
this.lastNavId = id;
4445
}
4546
});
4647
}
@@ -180,11 +181,14 @@ export class NavController {
180181
direction: RouterDirection;
181182
animation: NavDirection | undefined;
182183
animationBuilder: AnimationBuilder | undefined;
184+
isDirectionBasedOnNavigationIds: boolean;
183185
} {
184186
let direction: RouterDirection = 'root';
185187
let animation: NavDirection | undefined;
186188
const animationBuilder = this.animationBuilder;
187189

190+
const isDirectionBasedOnNavigationIds = this.direction === 'auto';
191+
188192
if (this.direction === 'auto') {
189193
direction = this.guessDirection;
190194
animation = this.guessAnimation;
@@ -200,6 +204,7 @@ export class NavController {
200204
direction,
201205
animation,
202206
animationBuilder,
207+
isDirectionBasedOnNavigationIds,
203208
};
204209
}
205210

packages/angular/test/base/e2e/src/router-link.spec.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ describe('Router Link', () => {
113113
describe('back', () => {
114114
it('should go back with ion-button[routerLink][routerDirection=back]', () => {
115115
cy.get('#routerLink-back').click();
116+
testBack();
116117
});
117118

118119
it('should go back with a[routerLink][routerDirection=back]', () => {
@@ -138,6 +139,7 @@ function testForward() {
138139
cy.get('app-router-link-page #canGoBack').should('have.text', 'true');
139140

140141
cy.go('back');
142+
cy.wait(500);
141143
cy.testStack('ion-router-outlet', ['app-router-link']);
142144
cy.testLifeCycle('app-router-link', {
143145
ionViewWillEnter: 2,
@@ -159,7 +161,7 @@ function testRoot() {
159161
cy.get('app-router-link-page #canGoBack').should('have.text', 'false');
160162

161163
cy.go('back');
162-
cy.wait(100);
164+
cy.wait(500);
163165
cy.testStack('ion-router-outlet', ['app-router-link']);
164166
cy.testLifeCycle('app-router-link', {
165167
ionViewWillEnter: 1,
@@ -181,7 +183,7 @@ function testBack() {
181183
cy.get('app-router-link-page #canGoBack').should('have.text', 'false');
182184

183185
cy.go('back');
184-
cy.wait(100);
186+
cy.wait(500);
185187
cy.testStack('ion-router-outlet', ['app-router-link']);
186188
cy.testLifeCycle('app-router-link', {
187189
ionViewWillEnter: 1,

0 commit comments

Comments
 (0)