diff --git a/e2e-app/src/app/app.module.ts b/e2e-app/src/app/app.module.ts
index 2d34155436..5a90cc144f 100644
--- a/e2e-app/src/app/app.module.ts
+++ b/e2e-app/src/app/app.module.ts
@@ -15,6 +15,7 @@ import {DropdownFocusComponent} from './dropdown/focus/dropdown-focus.component'
import {DropdownPositionComponent} from './dropdown/position/dropdown-position.component';
import {ModalFocusComponent} from './modal/focus/modal-focus.component';
import {ModalNestingComponent} from './modal/nesting/modal-nesting.component';
+import {ModalStackComponent, NgbdStackedModal} from './modal/stack/modal-stack.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';
@@ -36,6 +37,8 @@ import {TypeaheadValidationComponent} from './typeahead/validation/typeahead-val
DropdownPositionComponent,
ModalFocusComponent,
ModalNestingComponent,
+ ModalStackComponent,
+ NgbdStackedModal,
PopoverAutocloseComponent,
TooltipAutocloseComponent,
TooltipFocusComponent,
@@ -45,6 +48,7 @@ import {TypeaheadValidationComponent} from './typeahead/validation/typeahead-val
TypeaheadAutoCloseComponent,
TimepickerNavigationComponent,
],
+ entryComponents: [NgbdStackedModal],
imports: [BrowserModule, FormsModule, ReactiveFormsModule, routing, NgbModule],
bootstrap: [AppComponent]
})
diff --git a/e2e-app/src/app/app.routing.ts b/e2e-app/src/app/app.routing.ts
index 3a4840748d..3ab625bbe1 100644
--- a/e2e-app/src/app/app.routing.ts
+++ b/e2e-app/src/app/app.routing.ts
@@ -16,6 +16,7 @@ import {TimepickerNavigationComponent} from './timepicker/navigation/timepicker-
import {TypeaheadValidationComponent} from './typeahead/validation/typeahead-validation.component';
import {DropdownPositionComponent} from './dropdown/position/dropdown-position.component';
import {ModalNestingComponent} from './modal/nesting/modal-nesting.component';
+import {ModalStackComponent} from './modal/stack/modal-stack.component';
export const routes: Routes = [
@@ -28,7 +29,11 @@ export const routes: Routes = [
},
{
path: 'modal',
- children: [{path: 'focus', component: ModalFocusComponent}, {path: 'nesting', component: ModalNestingComponent}]
+ children: [
+ {path: 'focus', component: ModalFocusComponent},
+ {path: 'nesting', component: ModalNestingComponent},
+ {path: 'stack', component: ModalStackComponent},
+ ]
},
{
path: 'dropdown',
diff --git a/e2e-app/src/app/modal/stack/modal-stack.component.html b/e2e-app/src/app/modal/stack/modal-stack.component.html
new file mode 100644
index 0000000000..649cf7fcd3
--- /dev/null
+++ b/e2e-app/src/app/modal/stack/modal-stack.component.html
@@ -0,0 +1,16 @@
+
Modal nesting tests
+
+
+
+
+
+
+
+
+
+
diff --git a/e2e-app/src/app/modal/stack/modal-stack.component.ts b/e2e-app/src/app/modal/stack/modal-stack.component.ts
new file mode 100644
index 0000000000..bb630289d1
--- /dev/null
+++ b/e2e-app/src/app/modal/stack/modal-stack.component.ts
@@ -0,0 +1,33 @@
+import {Component, TemplateRef} from '@angular/core';
+import {NgbActiveModal, NgbModal} from '@ng-bootstrap/ng-bootstrap';
+import {Observable} from 'rxjs';
+import {map} from 'rxjs/operators';
+
+@Component({templateUrl: './modal-stack.component.html'})
+export class ModalStackComponent {
+ constructor(private modalService: NgbModal) {}
+
+ openModal(content: TemplateRef) { this.modalService.open(content); }
+
+ search = (text$: Observable) => text$.pipe(map(() => ['one', 'two', 'three']));
+
+ openSecondModal() { this.modalService.open(NgbdStackedModal); }
+}
+
+@Component({
+ template: `
+
+
+
+ `
+})
+export class NgbdStackedModal {
+ constructor(public activeModal: NgbActiveModal) {}
+}
diff --git a/e2e-app/src/app/modal/stack/modal-stack.e2e-spec.ts b/e2e-app/src/app/modal/stack/modal-stack.e2e-spec.ts
new file mode 100644
index 0000000000..b0a8b6df10
--- /dev/null
+++ b/e2e-app/src/app/modal/stack/modal-stack.e2e-spec.ts
@@ -0,0 +1,43 @@
+import {Key, browser} from 'protractor';
+import {expectFocused, expectNoOpenModals, openUrl, sendKey} from '../../tools.po';
+import {ModalStackPage} from './modal-stack.po';
+import {DatepickerPage} from '../../datepicker/datepicker.po';
+import {DropdownPage} from '../../dropdown/dropdown.po';
+import {TypeaheadPage} from '../../typeahead/typeahead.po';
+
+describe('Modal stacked', () => {
+ let page: ModalStackPage;
+ let datepickerPage: DatepickerPage;
+ let dropdownPage: DropdownPage;
+ let typeaheadPage: TypeaheadPage;
+
+ beforeAll(() => {
+ page = new ModalStackPage();
+ datepickerPage = new DatepickerPage();
+ dropdownPage = new DropdownPage();
+ typeaheadPage = new TypeaheadPage();
+ });
+
+ beforeEach(async() => await openUrl('modal/stack'));
+
+ afterEach(async() => { await expectNoOpenModals(); });
+
+ it('should keep tab on the first modal after the second modal has closed', async() => {
+ await page.openModal();
+ await page.openStackModal();
+
+ // close the stack modal
+ await sendKey(Key.ESCAPE);
+
+ // Check that the button is focused again
+ await expectFocused(page.getStackModalButton(), 'Button element not focused');
+ await sendKey(Key.TAB);
+
+ await expectFocused(page.getCoseIcon(), 'Close icon not focused');
+
+ // close the main modal
+ await sendKey(Key.ESCAPE);
+
+ });
+
+});
diff --git a/e2e-app/src/app/modal/stack/modal-stack.po.ts b/e2e-app/src/app/modal/stack/modal-stack.po.ts
new file mode 100644
index 0000000000..2227bcd5f2
--- /dev/null
+++ b/e2e-app/src/app/modal/stack/modal-stack.po.ts
@@ -0,0 +1,25 @@
+import {$, $$} from 'protractor';
+
+export class ModalStackPage {
+ getModal(index) { return $$('ngb-modal-window').get(index); }
+
+ getModalButton() { return $('#open-modal'); }
+
+ getStackModalButton() { return $('#open-inner-modal'); }
+
+ getCoseIcon() { return $('button.close'); }
+
+ async openModal() {
+ await this.getModalButton().click();
+ const modal = this.getModal(0);
+ expect(await modal.isPresent()).toBeTruthy(`A modal should have been opened`);
+ return modal;
+ }
+
+ async openStackModal() {
+ await this.getStackModalButton().click();
+ const modal = this.getModal(1);
+ expect(await modal.isPresent()).toBeTruthy(`A second modal should have been opened`);
+ return modal;
+ }
+}
diff --git a/e2e-app/src/app/tools.po.ts b/e2e-app/src/app/tools.po.ts
index e038591ade..382c116fd7 100644
--- a/e2e-app/src/app/tools.po.ts
+++ b/e2e-app/src/app/tools.po.ts
@@ -34,8 +34,9 @@ export const offsetClick = async(el: ElementFinder, offset) => {
* @param message to display in case of error
*/
export const expectFocused = async(el: ElementFinder, message: string) => {
- const focused = await browser.driver.switchTo().activeElement();
- expect(await WebElement.equals(el.getWebElement(), focused)).toBeTruthy(message);
+ await browser.wait(() => {
+ return WebElement.equals(el.getWebElement(), browser.driver.switchTo().activeElement());
+ }, 500, message);
};
/**
diff --git a/src/modal/modal-window.ts b/src/modal/modal-window.ts
index ef9bf3947f..f4b3ff4db4 100644
--- a/src/modal/modal-window.ts
+++ b/src/modal/modal-window.ts
@@ -97,7 +97,7 @@ export class NgbModalWindow implements OnInit,
} else {
elementToFocus = body;
}
- elementToFocus.focus();
+ setTimeout(() => elementToFocus.focus());
this._elWithFocus = null;
}
}
diff --git a/src/modal/modal.spec.ts b/src/modal/modal.spec.ts
index 6db15a5722..892d882b8a 100644
--- a/src/modal/modal.spec.ts
+++ b/src/modal/modal.spec.ts
@@ -597,6 +597,8 @@ describe('ngb-modal', () => {
expect(fixture.nativeElement).toHaveModal();
modalInstance.close();
+ tick();
+
fixture.detectChanges();
expect(fixture.nativeElement).not.toHaveModal();
}));
@@ -681,18 +683,20 @@ describe('ngb-modal', () => {
describe('focus management', () => {
- it('should return focus to previously focused element', () => {
- fixture.detectChanges();
- const openButtonEl = fixture.nativeElement.querySelector('button#open');
- openButtonEl.focus();
- openButtonEl.click();
- fixture.detectChanges();
- expect(fixture.nativeElement).toHaveModal('from button');
+ it('should return focus to previously focused element', fakeAsync(() => {
+ fixture.detectChanges();
+ const openButtonEl = fixture.nativeElement.querySelector('button#open');
+ openButtonEl.focus();
+ openButtonEl.click();
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal('from button');
- fixture.componentInstance.close();
- expect(fixture.nativeElement).not.toHaveModal();
- expect(document.activeElement).toBe(openButtonEl);
- });
+ fixture.componentInstance.close();
+ expect(fixture.nativeElement).not.toHaveModal();
+
+ tick();
+ expect(document.activeElement).toBe(openButtonEl);
+ }));
it('should return focus to body if no element focused prior to modal opening', () => {