diff --git a/packages/angular/common/src/directives/navigation/stack-controller.ts b/packages/angular/common/src/directives/navigation/stack-controller.ts index 8241afa864f..996c8b4e0bf 100644 --- a/packages/angular/common/src/directives/navigation/stack-controller.ts +++ b/packages/angular/common/src/directives/navigation/stack-controller.ts @@ -1,7 +1,7 @@ import { Location } from '@angular/common'; import { ComponentRef, NgZone } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import type { AnimationBuilder, RouterDirection } from '@ionic/core/components'; +import type { AnimationBuilder, NavDirection, RouterDirection } from '@ionic/core/components'; import { bindLifecycleEvents } from '../../providers/angular-delegate'; import { NavController } from '../../providers/nav-controller'; @@ -62,50 +62,72 @@ export class StackController { } setActive(enteringView: RouteView): Promise { - const consumeResult = this.navCtrl.consumeTransition(); + const { isDirectionBasedOnNavigationIds, ...consumeResult } = this.navCtrl.consumeTransition(); let { direction, animation, animationBuilder } = consumeResult; - const leavingView = this.activeView; - const tabSwitch = isTabSwitch(enteringView, leavingView); - if (tabSwitch) { - direction = 'back'; - animation = undefined; - } const viewsSnapshot = this.views.slice(); - let currentNavigation; + const currentNavigation = this.router.getCurrentNavigation(); - const router = this.router as any; + /** + * If the navigation action sets `replaceUrl: true` then we need to make sure + * we remove the last item from our views stack + */ + if (currentNavigation?.extras?.replaceUrl && currentNavigation?.trigger !== 'popstate') { + if (this.views.length > 0) { + this.views.splice(-1, 1); + } + } - // Angular >= 7.2.0 - if (router.getCurrentNavigation) { - currentNavigation = router.getCurrentNavigation(); + // determine direction based on the order of the views in the stack + const leavingView = this.activeView; + const isEnteringViewReused = this.views.includes(enteringView); + const leavingViewIndex = leavingView ? this.views.indexOf(leavingView) : -1; + const enteringViewIndex = isEnteringViewReused ? this.views.indexOf(enteringView) : this.views.length; + const suggestedDirectionBasedOnStackOrder: NavDirection | undefined = + leavingViewIndex === -1 ? undefined : enteringViewIndex < leavingViewIndex ? 'back' : 'forward'; - // Angular < 7.2.0 - } else if (router.navigations?.value) { - currentNavigation = router.navigations.value; - } + /** + * The user triggered a back navigation on a page that was navigated to with root. In this case, the new page + * becomes the root and the leavingView is removed. + * + * This can happen e.g. when navigating to a page with navigateRoot and then using the browser back button + */ + const isPopStateTransitionFromRootPage = + direction === 'back' && leavingView?.root && currentNavigation?.trigger === 'popstate'; /** - * If the navigation action - * sets `replaceUrl: true` - * then we need to make sure - * we remove the last item - * from our views stack + * whether direction based on stack order takes precedence over direction based on navigation ids + * + * only applied if the user did not explicitly set the direction + * (e.g. via the NavController, routerLink directive etc.) */ - if (currentNavigation?.extras?.replaceUrl) { - if (this.views.length > 0) { - this.views.splice(-1, 1); + const useDirectionBasedOnStackOrder = isDirectionBasedOnNavigationIds && suggestedDirectionBasedOnStackOrder; + + if (isPopStateTransitionFromRootPage) { + direction = 'root'; + animation = undefined; + + if (leavingViewIndex >= 0) { + this.views.splice(leavingViewIndex, 1); } + } else if (useDirectionBasedOnStackOrder) { + direction = suggestedDirectionBasedOnStackOrder; + animation = suggestedDirectionBasedOnStackOrder; + } + + const tabSwitch = isTabSwitch(enteringView, leavingView); + if (tabSwitch) { + direction = 'back'; + animation = undefined; } - const reused = this.views.includes(enteringView); const views = this.insertView(enteringView, direction); // Trigger change detection before transition starts // This will call ngOnInit() the first time too, just after the view // was attached to the dom, but BEFORE the transition starts - if (!reused) { + if (!isEnteringViewReused) { enteringView.ref.changeDetectorRef.detectChanges(); } diff --git a/packages/angular/common/src/directives/navigation/stack-utils.ts b/packages/angular/common/src/directives/navigation/stack-utils.ts index 41203407599..44d05e1f2b9 100644 --- a/packages/angular/common/src/directives/navigation/stack-utils.ts +++ b/packages/angular/common/src/directives/navigation/stack-utils.ts @@ -14,6 +14,7 @@ export const insertView = (views: RouteView[], view: RouteView, direction: Route const setRoot = (views: RouteView[], view: RouteView) => { views = views.filter((v) => v.stackId !== view.stackId); + view.root = true; views.push(view); return views; }; @@ -110,4 +111,5 @@ export interface RouteView { savedExtras?: NavigationExtras; unlistenEvents: () => void; animationBuilder?: AnimationBuilder; + root?: boolean; } diff --git a/packages/angular/common/src/providers/nav-controller.ts b/packages/angular/common/src/providers/nav-controller.ts index aff31b090b3..fe1c6817f16 100644 --- a/packages/angular/common/src/providers/nav-controller.ts +++ b/packages/angular/common/src/providers/nav-controller.ts @@ -37,9 +37,9 @@ export class NavController { if (router) { router.events.subscribe((ev) => { if (ev instanceof NavigationStart) { + // restoredState is set if the browser back/forward button is used const id = ev.restoredState ? ev.restoredState.navigationId : ev.id; - this.guessDirection = id < this.lastNavId ? 'back' : 'forward'; - this.guessAnimation = !ev.restoredState ? this.guessDirection : undefined; + this.guessAnimation = this.guessDirection = id < this.lastNavId ? 'back' : 'forward'; this.lastNavId = this.guessDirection === 'forward' ? ev.id : id; } }); @@ -180,11 +180,14 @@ export class NavController { direction: RouterDirection; animation: NavDirection | undefined; animationBuilder: AnimationBuilder | undefined; + isDirectionBasedOnNavigationIds: boolean; } { let direction: RouterDirection = 'root'; let animation: NavDirection | undefined; const animationBuilder = this.animationBuilder; + const isDirectionBasedOnNavigationIds = this.direction === 'auto'; + if (this.direction === 'auto') { direction = this.guessDirection; animation = this.guessAnimation; @@ -200,6 +203,7 @@ export class NavController { direction, animation, animationBuilder, + isDirectionBasedOnNavigationIds, }; }