From 725b13fa60775dc9f9c3491cb545c70a5a9162eb Mon Sep 17 00:00:00 2001 From: Sean Perkins Date: Thu, 8 Sep 2022 12:52:17 -0400 Subject: [PATCH] fix(angular): nav controller can pop views after leaving tabs outlet (#25690) Resolves #18593 --- .../navigation/ion-router-outlet.ts | 13 +++- angular/test/base/e2e/src/tabs.spec.ts | 68 +++++++++++++++++++ .../test/base/src/app/app-routing.module.ts | 6 +- .../tabs-global/tabs-global-routing.module.ts | 16 +++++ .../tabs-global/tabs-global.component.html | 17 +++++ .../app/tabs-global/tabs-global.component.ts | 17 +++++ .../src/app/tabs-global/tabs-global.module.ts | 13 ++++ .../tabs-tab1-nested.component.html | 3 + .../tabs-tab1-nested.component.ts | 5 +- .../app/tabs-tab1/tabs-tab1.component.html | 1 + .../src/app/tabs-tab1/tabs-tab1.component.ts | 3 + 11 files changed, 159 insertions(+), 3 deletions(-) create mode 100644 angular/test/base/src/app/tabs-global/tabs-global-routing.module.ts create mode 100644 angular/test/base/src/app/tabs-global/tabs-global.component.html create mode 100644 angular/test/base/src/app/tabs-global/tabs-global.component.ts create mode 100644 angular/test/base/src/app/tabs-global/tabs-global.module.ts diff --git a/angular/src/directives/navigation/ion-router-outlet.ts b/angular/src/directives/navigation/ion-router-outlet.ts index 6b4ec47ba4..60362d757b 100644 --- a/angular/src/directives/navigation/ion-router-outlet.ts +++ b/angular/src/directives/navigation/ion-router-outlet.ts @@ -308,8 +308,19 @@ export class IonRouterOutlet implements OnDestroy, OnInit { } this.activatedView = enteringView; + + /** + * The top outlet is set prior to the entering view's transition completing, + * so that when we have nested outlets (e.g. ion-tabs inside an ion-router-outlet), + * the tabs outlet will be assigned as the top outlet when a view inside tabs is + * activated. + * + * In this scenario, activeWith is called for both the tabs and the root router outlet. + * To avoid a race condition, we assign the top outlet synchronously. + */ + this.navCtrl.setTopOutlet(this); + this.stackCtrl.setActive(enteringView).then((data) => { - this.navCtrl.setTopOutlet(this); this.activateEvents.emit(cmpRef.instance); this.stackEvents.emit(data); }); diff --git a/angular/test/base/e2e/src/tabs.spec.ts b/angular/test/base/e2e/src/tabs.spec.ts index 589e8a1740..149ae79a1b 100644 --- a/angular/test/base/e2e/src/tabs.spec.ts +++ b/angular/test/base/e2e/src/tabs.spec.ts @@ -243,6 +243,74 @@ describe('Tabs', () => { }); }) + describe('entry url - /tabs/account', () => { + beforeEach(() => { + cy.visit('/tabs/account'); + }); + it('should pop to previous view when leaving tabs outlet', () => { + + cy.get('ion-title').should('contain.text', 'Tab 1 - Page 1'); + + cy.get('#goto-tab1-page2').click(); + + cy.get('ion-title').should('contain.text', 'Tab 1 - Page 2 (1)'); + + cy.get('#goto-global').click(); + + cy.get('ion-title').should('contain.text', 'Global Page'); + + cy.get('#goto-prev-pop').click(); + + cy.get('ion-title').should('contain.text', 'Tab 1 - Page 2 (1)'); + + cy.get('#goto-prev').click(); + + cy.get('ion-title').should('contain.text', 'Tab 1 - Page 1'); + + /** + * Verifies that when entering the tabs outlet directly, + * the navController.pop() method does not pop the previous view, + * when you are at the root of the tabs outlet. + */ + cy.get('#goto-previous-page').click(); + cy.get('ion-title').should('contain.text', 'Tab 1 - Page 1'); + }); + }); + + describe('entry url - /', () => { + it('should pop to the root outlet from the tabs outlet', () => { + cy.visit('/'); + + cy.get('ion-title').should('contain.text', 'Test App'); + + cy.get('ion-item').contains('Tabs test').click(); + + cy.get('ion-title').should('contain.text', 'Tab 1 - Page 1'); + + cy.get('#goto-tab1-page2').click(); + + cy.get('ion-title').should('contain.text', 'Tab 1 - Page 2 (1)'); + + cy.get('#goto-global').click(); + + cy.get('ion-title').should('contain.text', 'Global Page'); + + cy.get('#goto-prev-pop').click(); + + cy.get('ion-title').should('contain.text', 'Tab 1 - Page 2 (1)'); + + cy.get('#goto-prev').click(); + + cy.get('ion-title').should('contain.text', 'Tab 1 - Page 1'); + + cy.get('#goto-previous-page').click(); + + cy.get('ion-title').should('contain.text', 'Test App'); + + }); + }); + + describe('entry url - /tabs/account/nested/1', () => { beforeEach(() => { cy.visit('/tabs/account/nested/1'); diff --git a/angular/test/base/src/app/app-routing.module.ts b/angular/test/base/src/app/app-routing.module.ts index f40f265118..e03345759f 100644 --- a/angular/test/base/src/app/app-routing.module.ts +++ b/angular/test/base/src/app/app-routing.module.ts @@ -55,6 +55,10 @@ const routes: Routes = [ path: 'tabs', loadChildren: () => import('./tabs/tabs.module').then(m => m.TabsPageModule) }, + { + path: 'tabs-global', + loadChildren: () => import('./tabs-global/tabs-global.module').then(m => m.TabsGlobalModule) + }, { path: 'nested-outlet', component: NestedOutletComponent, @@ -68,7 +72,7 @@ const routes: Routes = [ component: NestedOutletPage2Component } ] - } + }, ]; @NgModule({ diff --git a/angular/test/base/src/app/tabs-global/tabs-global-routing.module.ts b/angular/test/base/src/app/tabs-global/tabs-global-routing.module.ts new file mode 100644 index 0000000000..e7ddb717b5 --- /dev/null +++ b/angular/test/base/src/app/tabs-global/tabs-global-routing.module.ts @@ -0,0 +1,16 @@ +import { NgModule } from "@angular/core"; +import { RouterModule } from "@angular/router"; +import { TabsGlobalComponent } from "./tabs-global.component"; + +@NgModule({ + imports: [ + RouterModule.forChild([ + { + path: '', + component: TabsGlobalComponent + } + ]) + ], + exports: [RouterModule] +}) +export class TabsGlobalRoutingModule { } diff --git a/angular/test/base/src/app/tabs-global/tabs-global.component.html b/angular/test/base/src/app/tabs-global/tabs-global.component.html new file mode 100644 index 0000000000..ec31c49344 --- /dev/null +++ b/angular/test/base/src/app/tabs-global/tabs-global.component.html @@ -0,0 +1,17 @@ + + + + + + + + Global Page + + + + + + + Go To Previous + + diff --git a/angular/test/base/src/app/tabs-global/tabs-global.component.ts b/angular/test/base/src/app/tabs-global/tabs-global.component.ts new file mode 100644 index 0000000000..0c49539541 --- /dev/null +++ b/angular/test/base/src/app/tabs-global/tabs-global.component.ts @@ -0,0 +1,17 @@ +import { Component } from "@angular/core"; +import { NavController } from "@ionic/angular"; + +/** + * This component is used in conjunction with a tabs router-outlet, + * to validate the behavior of different routing APIs (e.g. NavController) + * when leaving and re-entering a router-outlet. + */ +@Component({ + selector: 'app-tabs-global', + templateUrl: 'tabs-global.component.html' +}) +export class TabsGlobalComponent { + + constructor(public navCtrl: NavController) { } + +} diff --git a/angular/test/base/src/app/tabs-global/tabs-global.module.ts b/angular/test/base/src/app/tabs-global/tabs-global.module.ts new file mode 100644 index 0000000000..06a8530c0a --- /dev/null +++ b/angular/test/base/src/app/tabs-global/tabs-global.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from "@angular/core"; +import { IonicModule } from "@ionic/angular"; +import { TabsGlobalRoutingModule } from "./tabs-global-routing.module"; +import { TabsGlobalComponent } from "./tabs-global.component"; + +@NgModule({ + imports: [ + IonicModule, + TabsGlobalRoutingModule + ], + declarations: [TabsGlobalComponent] +}) +export class TabsGlobalModule { } diff --git a/angular/test/base/src/app/tabs-tab1-nested/tabs-tab1-nested.component.html b/angular/test/base/src/app/tabs-tab1-nested/tabs-tab1-nested.component.html index 8187f666a9..8c3980c956 100644 --- a/angular/test/base/src/app/tabs-tab1-nested/tabs-tab1-nested.component.html +++ b/angular/test/base/src/app/tabs-tab1-nested/tabs-tab1-nested.component.html @@ -12,6 +12,9 @@

Go to Tab 1 - Page 1 Go to Tab 2 - Page 1 + Go to Global Page + Go to Previous Page (NavController). + Go to Next

diff --git a/angular/test/base/src/app/tabs-tab1-nested/tabs-tab1-nested.component.ts b/angular/test/base/src/app/tabs-tab1-nested/tabs-tab1-nested.component.ts index 9130a78ea9..69ff9c11eb 100644 --- a/angular/test/base/src/app/tabs-tab1-nested/tabs-tab1-nested.component.ts +++ b/angular/test/base/src/app/tabs-tab1-nested/tabs-tab1-nested.component.ts @@ -1,5 +1,6 @@ import { ActivatedRoute } from '@angular/router'; import { Component, OnInit } from '@angular/core'; +import { NavController } from '@ionic/angular'; @Component({ selector: 'app-tabs-tab1-nested', @@ -9,7 +10,8 @@ export class TabsTab1NestedComponent implements OnInit { id = ''; constructor( private route: ActivatedRoute, - ) {} + public navCtrl: NavController + ) { } ngOnInit() { this.id = this.route.snapshot.paramMap.get('id'); @@ -18,4 +20,5 @@ export class TabsTab1NestedComponent implements OnInit { next() { return parseInt(this.id, 10) + 1; } + } diff --git a/angular/test/base/src/app/tabs-tab1/tabs-tab1.component.html b/angular/test/base/src/app/tabs-tab1/tabs-tab1.component.html index fabccd1fca..986ca90c1b 100644 --- a/angular/test/base/src/app/tabs-tab1/tabs-tab1.component.html +++ b/angular/test/base/src/app/tabs-tab1/tabs-tab1.component.html @@ -19,5 +19,6 @@ id="goto-nested-page1-with-query-params">Go to Page 2 with Query Params Go to Tab 3 - Page 2 Go to nested + Go to Previous Page

diff --git a/angular/test/base/src/app/tabs-tab1/tabs-tab1.component.ts b/angular/test/base/src/app/tabs-tab1/tabs-tab1.component.ts index 0e1bcc5318..f0b414adbd 100644 --- a/angular/test/base/src/app/tabs-tab1/tabs-tab1.component.ts +++ b/angular/test/base/src/app/tabs-tab1/tabs-tab1.component.ts @@ -1,4 +1,5 @@ import { Component, NgZone } from '@angular/core'; +import { NavController } from '@ionic/angular'; @Component({ selector: 'app-tabs-tab1', @@ -9,6 +10,8 @@ export class TabsTab1Component { segment = 'one'; changed = 'false'; + constructor(public navCtrl: NavController) {} + ionViewWillEnter() { NgZone.assertInAngularZone(); setTimeout(() => {