diff --git a/e2e-app/src/app/app.module.ts b/e2e-app/src/app/app.module.ts
index 2b08dcc290..17f1eda576 100644
--- a/e2e-app/src/app/app.module.ts
+++ b/e2e-app/src/app/app.module.ts
@@ -17,6 +17,7 @@ import {ModalAutoCloseComponent} from './modal/autoclose/modal-autoclose.compone
import {ModalFocusComponent} from './modal/focus/modal-focus.component';
import {ModalNestingComponent} from './modal/nesting/modal-nesting.component';
import {ModalStackComponent} from './modal/stack/modal-stack.component';
+import {ModalStackConfirmationComponent} from './modal/stack-confirmation/modal-stack-confirmation.component';
import {PopoverAutocloseComponent} from './popover/autoclose/popover-autoclose.component';
import {TooltipAutocloseComponent} from './tooltip/autoclose/tooltip-autoclose.component';
import {TooltipFocusComponent} from './tooltip/focus/tooltip-focus.component';
@@ -40,6 +41,7 @@ import {TypeaheadValidationComponent} from './typeahead/validation/typeahead-val
ModalFocusComponent,
ModalNestingComponent,
ModalStackComponent,
+ ModalStackConfirmationComponent,
PopoverAutocloseComponent,
TooltipAutocloseComponent,
TooltipFocusComponent,
diff --git a/e2e-app/src/app/app.routing.ts b/e2e-app/src/app/app.routing.ts
index 4a2f46f47d..de5f14fbd1 100644
--- a/e2e-app/src/app/app.routing.ts
+++ b/e2e-app/src/app/app.routing.ts
@@ -5,10 +5,12 @@ import {DatepickerAutoCloseComponent} from './datepicker/autoclose/datepicker-au
import {DatepickerFocusComponent} from './datepicker/focus/datepicker-focus.component';
import {DropdownAutoCloseComponent} from './dropdown/autoclose/dropdown-autoclose.component';
import {DropdownFocusComponent} from './dropdown/focus/dropdown-focus.component';
+import {DropdownPositionComponent} from './dropdown/position/dropdown-position.component';
import {ModalAutoCloseComponent} from './modal/autoclose/modal-autoclose.component';
import {ModalFocusComponent} from './modal/focus/modal-focus.component';
import {ModalNestingComponent} from './modal/nesting/modal-nesting.component';
import {ModalStackComponent} from './modal/stack/modal-stack.component';
+import {ModalStackConfirmationComponent} from './modal/stack-confirmation/modal-stack-confirmation.component';
import {PopoverAutocloseComponent} from './popover/autoclose/popover-autoclose.component';
import {TooltipAutocloseComponent} from './tooltip/autoclose/tooltip-autoclose.component';
import {TooltipFocusComponent} from './tooltip/focus/tooltip-focus.component';
@@ -17,7 +19,6 @@ import {TypeaheadAutoCloseComponent} from './typeahead/autoclose/typeahead-autoc
import {TypeaheadFocusComponent} from './typeahead/focus/typeahead-focus.component';
import {TimepickerNavigationComponent} from './timepicker/navigation/timepicker-navigation.component';
import {TypeaheadValidationComponent} from './typeahead/validation/typeahead-validation.component';
-import {DropdownPositionComponent} from './dropdown/position/dropdown-position.component';
export const routes: Routes = [
@@ -35,6 +36,7 @@ export const routes: Routes = [
{path: 'focus', component: ModalFocusComponent},
{path: 'nesting', component: ModalNestingComponent},
{path: 'stack', component: ModalStackComponent},
+ {path: 'stack-confirmation', component: ModalStackConfirmationComponent},
]
},
{
diff --git a/e2e-app/src/app/modal/stack-confirmation/modal-stack-confirmation.component.html b/e2e-app/src/app/modal/stack-confirmation/modal-stack-confirmation.component.html
new file mode 100644
index 0000000000..e2dbf38608
--- /dev/null
+++ b/e2e-app/src/app/modal/stack-confirmation/modal-stack-confirmation.component.html
@@ -0,0 +1,27 @@
+
Modal closure confirmation test
+
+
+
+
+
This modal will trigger confirmation
+
+
+
+
+
+
+
+
+
+
+
diff --git a/e2e-app/src/app/modal/stack-confirmation/modal-stack-confirmation.component.ts b/e2e-app/src/app/modal/stack-confirmation/modal-stack-confirmation.component.ts
new file mode 100644
index 0000000000..036aae991b
--- /dev/null
+++ b/e2e-app/src/app/modal/stack-confirmation/modal-stack-confirmation.component.ts
@@ -0,0 +1,13 @@
+import {Component, TemplateRef, ViewChild} from '@angular/core';
+import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
+
+@Component({templateUrl: './modal-stack-confirmation.component.html'})
+export class ModalStackConfirmationComponent {
+ @ViewChild('confirmation', {static: true, read: TemplateRef}) confirmationTpl: TemplateRef;
+
+ constructor(private modalService: NgbModal) {}
+
+ openModal(content: TemplateRef) {
+ this.modalService.open(content, {beforeDismiss: () => this.modalService.open(this.confirmationTpl).result});
+ }
+}
diff --git a/e2e-app/src/app/modal/stack-confirmation/modal-stack-confirmation.e2e-spec.ts b/e2e-app/src/app/modal/stack-confirmation/modal-stack-confirmation.e2e-spec.ts
new file mode 100644
index 0000000000..a104307289
--- /dev/null
+++ b/e2e-app/src/app/modal/stack-confirmation/modal-stack-confirmation.e2e-spec.ts
@@ -0,0 +1,71 @@
+import {expectNoOpenModals, openUrl, sendKey} from '../../tools.po';
+import {ModalStackConfirmationPage} from './modal-stack-confirmation.po';
+import {Key} from 'protractor';
+
+describe('Modal stacked with confirmation', () => {
+ let page: ModalStackConfirmationPage;
+
+ beforeAll(() => { page = new ModalStackConfirmationPage(); });
+
+ beforeEach(async() => await openUrl('modal/stack-confirmation'));
+
+ afterEach(async() => { await expectNoOpenModals(); });
+
+ it('should close modals correctly using close button', async() => {
+ await page.openModal();
+
+ // close with button
+ await page.getModalClose().click();
+ expect(await page.getOpenModals().count()).toBe(2, 'Confirmation modal should be opened');
+
+ // cancel closure with button
+ await page.getDismissalButton().click();
+ expect(await page.getOpenModals().count()).toBe(1, 'Confirmation modal should be dismissed');
+
+ // close again
+ await page.getModalClose().click();
+ expect(await page.getOpenModals().count()).toBe(2, 'Confirmation modal should be re-opened');
+
+ // close all modals
+ await page.getConfirmationButton().click();
+ });
+
+ it('should close modals correctly using ESC', async() => {
+ await page.openModal();
+
+ // close with Escape
+ await sendKey(Key.ESCAPE);
+ expect(await page.getOpenModals().count()).toBe(2, 'Confirmation modal should be opened');
+
+ // cancel closure with Escape
+ await sendKey(Key.ESCAPE);
+ expect(await page.getOpenModals().count()).toBe(1, 'Confirmation modal should be dismissed');
+
+ // close again
+ await sendKey(Key.ESCAPE);
+ expect(await page.getOpenModals().count()).toBe(2, 'Confirmation modal should be re-opened');
+
+ // close all modals
+ await page.getConfirmationButton().click();
+ });
+
+ it('should close modals correctly using backdrop click', async() => {
+ await page.openModal();
+
+ // close with click
+ await page.getModal(0).click();
+ expect(await page.getOpenModals().count()).toBe(2, 'Confirmation modal should be opened');
+
+ // cancel closure with click
+ await page.getModal(1).click();
+ expect(await page.getOpenModals().count()).toBe(1, 'Confirmation modal should be dismissed');
+
+ // close again
+ await page.getModal(0).click();
+ expect(await page.getOpenModals().count()).toBe(2, 'Confirmation modal should be re-opened');
+
+ // close all modals
+ await page.getConfirmationButton().click();
+ });
+
+});
diff --git a/e2e-app/src/app/modal/stack-confirmation/modal-stack-confirmation.po.ts b/e2e-app/src/app/modal/stack-confirmation/modal-stack-confirmation.po.ts
new file mode 100644
index 0000000000..7054a6afdb
--- /dev/null
+++ b/e2e-app/src/app/modal/stack-confirmation/modal-stack-confirmation.po.ts
@@ -0,0 +1,22 @@
+import {$, $$} from 'protractor';
+
+export class ModalStackConfirmationPage {
+ getOpenModals() { return $$('ngb-modal-window'); }
+
+ getModal(index) { return this.getOpenModals().get(index); }
+
+ getModalButton() { return $('#open-modal'); }
+
+ getModalClose() { return $('#close'); }
+
+ getConfirmationButton() { return $('#confirm'); }
+
+ getDismissalButton() { return $('#dismiss'); }
+
+ async openModal() {
+ await this.getModalButton().click();
+ const modal = this.getModal(0);
+ expect(await modal.isPresent()).toBeTruthy(`A modal should have been opened`);
+ return modal;
+ }
+}
diff --git a/src/modal/modal-window.ts b/src/modal/modal-window.ts
index 345b2cc0a8..5eb6791b73 100644
--- a/src/modal/modal-window.ts
+++ b/src/modal/modal-window.ts
@@ -13,7 +13,7 @@ import {
ViewChild,
ViewEncapsulation
} from '@angular/core';
-import {fromEvent} from 'rxjs';
+import {fromEvent, Subject} from 'rxjs';
import {filter, switchMap, take, takeUntil, tap} from 'rxjs/operators';
import {getFocusableBoundaryElements} from '../util/focus-trap';
@@ -40,6 +40,7 @@ import {ModalDismissReasons} from './modal-dismiss-reasons';
})
export class NgbModalWindow implements OnInit,
AfterViewInit, OnDestroy {
+ private _closed$ = new Subject();
private _elWithFocus: Element; // element that is focused prior to modal opening
@ViewChild('dialog', {static: true}) private _dialogEl: ElementRef;
@@ -67,7 +68,7 @@ export class NgbModalWindow implements OnInit,
fromEvent(nativeElement, 'keydown')
.pipe(
- takeUntil(this.dismissEvent),
+ takeUntil(this._closed$),
// tslint:disable-next-line:deprecation
filter(e => e.which === Key.Escape && this.keyboard))
.subscribe(event => requestAnimationFrame(() => {
@@ -81,9 +82,8 @@ export class NgbModalWindow implements OnInit,
let preventClose = false;
fromEvent(this._dialogEl.nativeElement, 'mousedown')
.pipe(
- takeUntil(this.dismissEvent), tap(() => preventClose = false),
- switchMap(
- () => fromEvent(nativeElement, 'mouseup').pipe(takeUntil(this.dismissEvent), take(1))),
+ takeUntil(this._closed$), tap(() => preventClose = false),
+ switchMap(() => fromEvent(nativeElement, 'mouseup').pipe(takeUntil(this._closed$), take(1))),
filter(({target}) => nativeElement === target))
.subscribe(() => { preventClose = true; });
@@ -91,7 +91,7 @@ export class NgbModalWindow implements OnInit,
// 1. clicking on modal dialog itself
// 2. closing was prevented by mousedown/up handlers
// 3. clicking on scrollbar when the viewport is too small and modal doesn't fit (click is not triggered at all)
- fromEvent(nativeElement, 'click').pipe(takeUntil(this.dismissEvent)).subscribe(({target}) => {
+ fromEvent(nativeElement, 'click').pipe(takeUntil(this._closed$)).subscribe(({target}) => {
if (this.backdrop === true && nativeElement === target && !preventClose) {
this._zone.run(() => this.dismiss(ModalDismissReasons.BACKDROP_CLICK));
}
@@ -122,5 +122,7 @@ export class NgbModalWindow implements OnInit,
setTimeout(() => elementToFocus.focus());
this._elWithFocus = null;
});
+
+ this._closed$.next();
}
}