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', () => {