Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(overlays): getTop now returns the top-most presented overlay #24547

Merged
merged 9 commits into from
Jan 11, 2022
17 changes: 17 additions & 0 deletions angular/test/test-app/e2e/src/overlay-nested.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
describe('Overlay: Nested', () => {
beforeEach(() => {
cy.visit('/overlay-nested');
});

it('should dismiss the top overlay', () => {
liamdebeasi marked this conversation as resolved.
Show resolved Hide resolved
cy.get('#action-button').click();

cy.get('ion-modal').should('exist').should('be.visible');

cy.get('#dismiss-button').click();
cy.get('ion-modal').should('not.exist');

cy.get('h1').should('have.text', 'Overlay Nested Page');
});

});
1 change: 1 addition & 0 deletions angular/test/test-app/src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const routes: Routes = [
{ path: 'modals', component: ModalComponent },
{ path: 'modal-inline', loadChildren: () => import('./modal-inline').then(m => m.ModalInlineModule) },
{ path: 'view-child', component: ViewChildComponent },
{ path: 'overlay-nested', loadChildren: () => import('./overlay-nested').then(m => m.OverlayNestedPageModule) },
{ path: 'popover-inline', loadChildren: () => import('./popover-inline').then(m => m.PopoverInlineModule) },
{ path: 'providers', component: ProvidersComponent },
{ path: 'router-link', component: RouterLinkComponent },
Expand Down
135 changes: 70 additions & 65 deletions angular/test/test-app/src/app/home-page/home-page.component.html
Original file line number Diff line number Diff line change
@@ -1,66 +1,71 @@
<ion-header>
<ion-toolbar>
<ion-title>
Test App
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>
<ion-item routerLink="/alerts" [routerAnimation]="routerAnimation">
<ion-label>
Alerts test
</ion-label>
</ion-item>
<ion-item routerLink="/inputs">
<ion-label>
Inputs test
</ion-label>
</ion-item>
<ion-item routerLink="/form">
<ion-label>
Form test
</ion-label>
</ion-item>
<ion-item routerLink="/modals">
<ion-label>
Modals test
</ion-label>
</ion-item>
<ion-item routerLink="/router-link">
<ion-label>
Router link test
</ion-label>
</ion-item>
<ion-item routerLink="/tabs">
<ion-label>
Tabs test
</ion-label>
</ion-item>
<ion-item routerLink="/slides">
<ion-label>
Slides test
</ion-label>
</ion-item>
<ion-item routerLink="/virtual-scroll">
<ion-label>
Virtual Scroll
</ion-label>
</ion-item>
<ion-item routerLink="/nested-outlet/page">
<ion-label>
Nested ion-router-outlet
</ion-label>
</ion-item>
<ion-item routerLink="/view-child">
<ion-label>
ViewChild()
</ion-label>
</ion-item>
<ion-item routerLink="/providers">
<ion-label>
Providers
</ion-label>
</ion-item>
</ion-list>
</ion-content>
<ion-toolbar>
<ion-title>
Test App
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>
<ion-item routerLink="/alerts" [routerAnimation]="routerAnimation">
<ion-label>
Alerts test
</ion-label>
</ion-item>
<ion-item routerLink="/inputs">
<ion-label>
Inputs test
</ion-label>
</ion-item>
<ion-item routerLink="/form">
<ion-label>
Form test
</ion-label>
</ion-item>
<ion-item routerLink="/modals">
<ion-label>
Modals test
</ion-label>
</ion-item>
<ion-item routerLink="/overlays-nested">
<ion-label>
Overlays nested test
</ion-label>
</ion-item>
<ion-item routerLink="/router-link">
<ion-label>
Router link test
</ion-label>
</ion-item>
<ion-item routerLink="/tabs">
<ion-label>
Tabs test
</ion-label>
</ion-item>
<ion-item routerLink="/slides">
<ion-label>
Slides test
</ion-label>
</ion-item>
<ion-item routerLink="/virtual-scroll">
<ion-label>
Virtual Scroll
</ion-label>
</ion-item>
<ion-item routerLink="/nested-outlet/page">
<ion-label>
Nested ion-router-outlet
</ion-label>
</ion-item>
<ion-item routerLink="/view-child">
<ion-label>
ViewChild()
</ion-label>
</ion-item>
<ion-item routerLink="/providers">
<ion-label>
Providers
</ion-label>
</ion-item>
</ion-list>
</ion-content>
1 change: 1 addition & 0 deletions angular/test/test-app/src/app/overlay-nested/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './overlay-nested-page';
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './overlay-nested-example.component';
export * from './overlay-nested-example.module';
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<ion-content>
<ion-button fill="clear" id="open-start-date-input">
<ion-icon icon="calendar"></ion-icon>
<ion-input></ion-input>
</ion-button>
<ion-modal class="datetime" trigger="open-start-date-input" show-backdrop="false">
<ng-template>
<ion-datetime presentation="date" [showDefaultButtons]="true">
</ion-datetime>
</ng-template>
</ion-modal>

<ion-button id="dismiss-button" (click)="dismiss()">Dismiss</ion-button>
</ion-content>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Component } from "@angular/core";
import { ModalController } from "@ionic/angular";

@Component({
selector: 'app-overlay-nested-example',
templateUrl: 'overlay-nested-example.component.html'
})
export class OverlayNestedExampleComponent {

constructor(private modalCtrl: ModalController) { }

dismiss() {
this.modalCtrl.dismiss();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { IonicModule } from "@ionic/angular";
import { OverlayNestedExampleComponent } from "./overlay-nested-example.component";

@NgModule({
imports: [IonicModule, CommonModule],
declarations: [OverlayNestedExampleComponent],
exports: [OverlayNestedExampleComponent]
})
export class OverlayNestedModule { }
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './overlay-nested-page.module';
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { NgModule } from "@angular/core";
import { RouterModule } from "@angular/router";
import { OverlayNestedPageComponent } from "./overlay-nested-page.component";

@NgModule({
imports: [
RouterModule.forChild([
{
path: '',
component: OverlayNestedPageComponent
}
])
],
exports: [RouterModule]
})
export class OverlayNestedPageRoutingModule { }
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<ion-content>
<h1>Overlay Nested Page</h1>
<ion-button id="action-button" (click)="openModal()">Open modal</ion-button>
</ion-content>
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Component } from "@angular/core";
import { ModalController } from "@ionic/angular";
import { OverlayNestedExampleComponent } from "../overlay-nested-example";


@Component({
selector: 'app-overlay-nested-page',
templateUrl: 'overlay-nested-page.component.html'
})
export class OverlayNestedPageComponent {

constructor(private modalCtrl: ModalController) { }

async openModal() {
const modal = await this.modalCtrl.create({
component: OverlayNestedExampleComponent
});
modal.present();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { NgModule } from "@angular/core";
import { IonicModule } from "@ionic/angular";
import { OverlayNestedPageRoutingModule } from "./overlay-nested-page-routing.module";
import { OverlayNestedPageComponent } from "./overlay-nested-page.component";


@NgModule({
imports: [OverlayNestedPageRoutingModule, IonicModule],
declarations: [OverlayNestedPageComponent]
})
export class OverlayNestedPageModule { }
37 changes: 9 additions & 28 deletions core/src/utils/overlays.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ export const focusFirstDescendant = (ref: Element, overlay: HTMLIonOverlayElemen
}
};

const isOverlayHidden = (overlay: Element) => overlay.classList.contains('overlay-hidden');

const focusLastDescendant = (ref: Element, overlay: HTMLIonOverlayElement) => {
const inputs = Array.from(ref.querySelectorAll(focusableQueryString)) as HTMLElement[];
let lastInput = inputs.length > 0 ? inputs[inputs.length - 1] : null;
Expand Down Expand Up @@ -291,7 +293,7 @@ export const connectListeners = (doc: Document) => {

// handle back-button click
doc.addEventListener('ionBackButton', ev => {
const lastOverlay = getTopOpenOverlay(doc);
const lastOverlay = getOverlay(doc);
if (lastOverlay && lastOverlay.backdropDismiss) {
(ev as BackButtonEvent).detail.register(OVERLAY_BACK_BUTTON_PRIORITY, () => {
return lastOverlay.dismiss(undefined, BACKDROP);
Expand All @@ -302,7 +304,7 @@ export const connectListeners = (doc: Document) => {
// handle ESC to close overlay
doc.addEventListener('keyup', ev => {
if (ev.key === 'Escape') {
const lastOverlay = getTopOpenOverlay(doc);
const lastOverlay = getOverlay(doc);
if (lastOverlay && lastOverlay.backdropDismiss) {
lastOverlay.dismiss(undefined, BACKDROP);
}
Expand All @@ -327,34 +329,13 @@ export const getOverlays = (doc: Document, selector?: string): HTMLIonOverlayEle
.filter(c => c.overlayIndex > 0);
};

/**
* Gets the top-most/last opened
* overlay that is currently presented.
*/
const getTopOpenOverlay = (doc: Document): HTMLIonOverlayElement | undefined => {
const overlays = getOverlays(doc);
for (let i = overlays.length - 1; i >= 0; i--) {
const overlay = overlays[i];

/**
* Only consider overlays that
* are presented. Presented overlays
* will not have the .overlay-hidden
* class on the host.
*/
if (!overlay.classList.contains('overlay-hidden')) {
return overlay;
}
}

return;
}

export const getOverlay = (doc: Document, overlayTag?: string, id?: string): HTMLIonOverlayElement | undefined => {
const overlays = getOverlays(doc, overlayTag);
return (id === undefined)
? overlays[overlays.length - 1]
: overlays.find(o => o.id === id);
if (id === undefined) {
const visibleOverlays = overlays.filter(o => !isOverlayHidden(o));
return visibleOverlays[visibleOverlays.length - 1];
}
return overlays.find(o => o.id === id);
liamdebeasi marked this conversation as resolved.
Show resolved Hide resolved
};

/**
Expand Down
8 changes: 4 additions & 4 deletions core/src/utils/test/overlays/overlays.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { newE2EPage } from '@stencil/core/testing';

test('overlays: hardware back button: should dismss a presented overlay', async () => {
test('overlays: hardware back button: should dismiss a presented overlay', async () => {
const page = await newE2EPage({ url: '/src/utils/test/overlays?ionic:_testing=true' });

const createAndPresentButton = await page.find('#create-and-present');
Expand All @@ -24,7 +24,7 @@ test('overlays: hardware back button: should dismss a presented overlay', async
await page.waitForSelector('ion-modal', { hidden: true })
});

test('overlays: hardware back button: should dismss the presented overlay, even though another hidden modal was added last', async () => {
test('overlays: hardware back button: should dismiss the presented overlay, even though another hidden modal was added last', async () => {
const page = await newE2EPage({ url: '/src/utils/test/overlays?ionic:_testing=true' });

const createAndPresentButton = await page.find('#create-and-present');
Expand Down Expand Up @@ -56,7 +56,7 @@ test('overlays: hardware back button: should dismss the presented overlay, even
expect(await modals[1].evaluate(node => node.classList.contains('overlay-hidden'))).toEqual(true);
});

test('overlays: Esc: should dismss a presented overlay', async () => {
test('overlays: Esc: should dismiss a presented overlay', async () => {
const page = await newE2EPage({ url: '/src/utils/test/overlays?ionic:_testing=true' });

const createAndPresentButton = await page.find('#create-and-present');
Expand All @@ -78,7 +78,7 @@ test('overlays: Esc: should dismss a presented overlay', async () => {
});


test('overlays: Esc: should dismss the presented overlay, even though another hidden modal was added last', async () => {
test('overlays: Esc: should dismiss the presented overlay, even though another hidden modal was added last', async () => {
const page = await newE2EPage({ url: '/src/utils/test/overlays?ionic:_testing=true' });

const createAndPresentButton = await page.find('#create-and-present');
Expand Down