mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(overlay): add keyboard dispatcher for targeting correct overlay (#6682)
* feat(overlay): add keyboard dispatcher for targeting correct overlay Fix typo and add comment Address comments Add keyboard tracking demo to overlay Address comments Revert no-longer-needed dispose changes Address comments Fix prerender error by lazily starting dispatcher Address naming comment * Update license * Remove DOCUMENT token
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
import {TestBed, inject} from '@angular/core/testing';
|
||||
import {dispatchKeyboardEvent} from '@angular/cdk/testing';
|
||||
import {ESCAPE} from '@angular/cdk/keycodes';
|
||||
import {Overlay} from '../overlay';
|
||||
import {OverlayContainer} from '../overlay-container';
|
||||
import {OverlayModule} from '../index';
|
||||
import {OverlayKeyboardDispatcher} from './overlay-keyboard-dispatcher';
|
||||
|
||||
describe('OverlayKeyboardDispatcher', () => {
|
||||
let keyboardDispatcher: OverlayKeyboardDispatcher;
|
||||
let overlay: Overlay;
|
||||
let overlayContainerElement: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [OverlayModule],
|
||||
providers: [
|
||||
{provide: OverlayContainer, useFactory: () => {
|
||||
overlayContainerElement = document.createElement('div');
|
||||
return {getContainerElement: () => overlayContainerElement};
|
||||
}}
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(inject([OverlayKeyboardDispatcher, Overlay],
|
||||
(kbd: OverlayKeyboardDispatcher, o: Overlay) => {
|
||||
keyboardDispatcher = kbd;
|
||||
overlay = o;
|
||||
}));
|
||||
|
||||
it('should track overlays in order as they are attached and detached', () => {
|
||||
const overlayOne = overlay.create();
|
||||
const overlayTwo = overlay.create();
|
||||
|
||||
// Attach overlays
|
||||
keyboardDispatcher.add(overlayOne);
|
||||
keyboardDispatcher.add(overlayTwo);
|
||||
|
||||
expect(keyboardDispatcher._attachedOverlays.length)
|
||||
.toBe(2, 'Expected both overlays to be tracked.');
|
||||
expect(keyboardDispatcher._attachedOverlays[0]).toBe(overlayOne, 'Expected one to be first.');
|
||||
expect(keyboardDispatcher._attachedOverlays[1]).toBe(overlayTwo, 'Expected two to be last.');
|
||||
|
||||
// Detach first one and re-attach it
|
||||
keyboardDispatcher.remove(overlayOne);
|
||||
keyboardDispatcher.add(overlayOne);
|
||||
|
||||
expect(keyboardDispatcher._attachedOverlays[0])
|
||||
.toBe(overlayTwo, 'Expected two to now be first.');
|
||||
expect(keyboardDispatcher._attachedOverlays[1])
|
||||
.toBe(overlayOne, 'Expected one to now be last.');
|
||||
});
|
||||
|
||||
it('should dispatch body keyboard events to the most recently attached overlay', () => {
|
||||
const overlayOne = overlay.create();
|
||||
const overlayTwo = overlay.create();
|
||||
const overlayOneSpy = jasmine.createSpy('overlayOne keyboard event spy');
|
||||
const overlayTwoSpy = jasmine.createSpy('overlayOne keyboard event spy');
|
||||
|
||||
overlayOne.keydownEvents().subscribe(overlayOneSpy);
|
||||
overlayTwo.keydownEvents().subscribe(overlayTwoSpy);
|
||||
|
||||
// Attach overlays
|
||||
keyboardDispatcher.add(overlayOne);
|
||||
keyboardDispatcher.add(overlayTwo);
|
||||
|
||||
dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);
|
||||
|
||||
// Most recent overlay should receive event
|
||||
expect(overlayOneSpy).not.toHaveBeenCalled();
|
||||
expect(overlayTwoSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should dispatch targeted keyboard events to the overlay containing that target', () => {
|
||||
const overlayOne = overlay.create();
|
||||
const overlayTwo = overlay.create();
|
||||
const overlayOneSpy = jasmine.createSpy('overlayOne keyboard event spy');
|
||||
const overlayTwoSpy = jasmine.createSpy('overlayOne keyboard event spy');
|
||||
|
||||
overlayOne.keydownEvents().subscribe(overlayOneSpy);
|
||||
overlayTwo.keydownEvents().subscribe(overlayTwoSpy);
|
||||
|
||||
// Attach overlays
|
||||
keyboardDispatcher.add(overlayOne);
|
||||
keyboardDispatcher.add(overlayTwo);
|
||||
|
||||
const overlayOnePane = overlayOne.overlayElement;
|
||||
|
||||
dispatchKeyboardEvent(document.body, 'keydown', ESCAPE, overlayOnePane);
|
||||
|
||||
// Targeted overlay should receive event
|
||||
expect(overlayOneSpy).toHaveBeenCalled();
|
||||
expect(overlayTwoSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Injectable, Optional, SkipSelf, OnDestroy} from '@angular/core';
|
||||
import {OverlayRef} from '../overlay-ref';
|
||||
import {Subscription} from 'rxjs/Subscription';
|
||||
import {RxChain, filter} from '@angular/cdk/rxjs';
|
||||
import {fromEvent} from 'rxjs/observable/fromEvent';
|
||||
|
||||
/**
|
||||
* Service for dispatching keyboard events that land on the body to appropriate overlay ref,
|
||||
* if any. It maintains a list of attached overlays to determine best suited overlay based
|
||||
* on event target and order of overlay opens.
|
||||
*/
|
||||
@Injectable()
|
||||
export class OverlayKeyboardDispatcher implements OnDestroy {
|
||||
|
||||
/** Currently attached overlays in the order they were attached. */
|
||||
_attachedOverlays: OverlayRef[] = [];
|
||||
|
||||
private _keydownEventSubscription: Subscription | null;
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this._keydownEventSubscription) {
|
||||
this._keydownEventSubscription.unsubscribe();
|
||||
this._keydownEventSubscription = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Add a new overlay to the list of attached overlay refs. */
|
||||
add(overlayRef: OverlayRef): void {
|
||||
// Lazily start dispatcher once first overlay is added
|
||||
if (!this._keydownEventSubscription) {
|
||||
this._subscribeToKeydownEvents();
|
||||
}
|
||||
|
||||
this._attachedOverlays.push(overlayRef);
|
||||
}
|
||||
|
||||
/** Remove an overlay from the list of attached overlay refs. */
|
||||
remove(overlayRef: OverlayRef): void {
|
||||
const index = this._attachedOverlays.indexOf(overlayRef);
|
||||
if (index > -1) {
|
||||
this._attachedOverlays.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to keydown events that land on the body and dispatch those
|
||||
* events to the appropriate overlay.
|
||||
*/
|
||||
private _subscribeToKeydownEvents(): void {
|
||||
const bodyKeydownEvents = fromEvent<KeyboardEvent>(document.body, 'keydown');
|
||||
|
||||
this._keydownEventSubscription = RxChain.from(bodyKeydownEvents)
|
||||
.call(filter, () => !!this._attachedOverlays.length)
|
||||
.subscribe(event => {
|
||||
// Dispatch keydown event to correct overlay reference
|
||||
this._selectOverlayFromEvent(event)._keydownEvents.next(event);
|
||||
});
|
||||
}
|
||||
|
||||
/** Select the appropriate overlay from a keydown event. */
|
||||
private _selectOverlayFromEvent(event: KeyboardEvent): OverlayRef {
|
||||
// Check if any overlays contain the event
|
||||
const targetedOverlay = this._attachedOverlays.find(overlay => {
|
||||
return overlay.overlayElement === event.target ||
|
||||
overlay.overlayElement.contains(event.target as HTMLElement);
|
||||
});
|
||||
|
||||
// Use that overlay if it exists, otherwise choose the most recently attached one
|
||||
return targetedOverlay || this._attachedOverlays[this._attachedOverlays.length - 1];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/** @docs-private */
|
||||
export function OVERLAY_KEYBOARD_DISPATCHER_PROVIDER_FACTORY(
|
||||
dispatcher: OverlayKeyboardDispatcher) {
|
||||
return dispatcher || new OverlayKeyboardDispatcher();
|
||||
}
|
||||
|
||||
/** @docs-private */
|
||||
export const OVERLAY_KEYBOARD_DISPATCHER_PROVIDER = {
|
||||
// If there is already an OverlayKeyboardDispatcher available, use that.
|
||||
// Otherwise, provide a new one.
|
||||
provide: OverlayKeyboardDispatcher,
|
||||
deps: [[new Optional(), new SkipSelf(), OverlayKeyboardDispatcher]],
|
||||
useFactory: OVERLAY_KEYBOARD_DISPATCHER_PROVIDER_FACTORY
|
||||
};
|
||||
@@ -18,11 +18,13 @@ import {
|
||||
OverlayOrigin,
|
||||
} from './overlay-directives';
|
||||
import {OverlayPositionBuilder} from './position/overlay-position-builder';
|
||||
import {OVERLAY_KEYBOARD_DISPATCHER_PROVIDER} from './keyboard/overlay-keyboard-dispatcher';
|
||||
import {ScrollStrategyOptions} from './scroll/scroll-strategy-options';
|
||||
|
||||
export const OVERLAY_PROVIDERS: Provider[] = [
|
||||
Overlay,
|
||||
OverlayPositionBuilder,
|
||||
OVERLAY_KEYBOARD_DISPATCHER_PROVIDER,
|
||||
VIEWPORT_RULER_PROVIDER,
|
||||
OVERLAY_CONTAINER_PROVIDER,
|
||||
MAT_CONNECTED_OVERLAY_SCROLL_STRATEGY_PROVIDER,
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import {NgZone} from '@angular/core';
|
||||
import {PortalHost, Portal} from '@angular/cdk/portal';
|
||||
import {OverlayConfig} from './overlay-config';
|
||||
import {OverlayKeyboardDispatcher} from './keyboard/overlay-keyboard-dispatcher';
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
import {Subject} from 'rxjs/Subject';
|
||||
import {first} from 'rxjs/operator/first';
|
||||
@@ -24,11 +25,15 @@ export class OverlayRef implements PortalHost {
|
||||
private _attachments = new Subject<void>();
|
||||
private _detachments = new Subject<void>();
|
||||
|
||||
/** Stream of keydown events dispatched to this overlay. */
|
||||
_keydownEvents = new Subject<KeyboardEvent>();
|
||||
|
||||
constructor(
|
||||
private _portalHost: PortalHost,
|
||||
private _pane: HTMLElement,
|
||||
private _config: OverlayConfig,
|
||||
private _ngZone: NgZone) {
|
||||
private _ngZone: NgZone,
|
||||
private _keyboardDispatcher: OverlayKeyboardDispatcher) {
|
||||
|
||||
if (_config.scrollStrategy) {
|
||||
_config.scrollStrategy.attach(this);
|
||||
@@ -87,6 +92,9 @@ export class OverlayRef implements PortalHost {
|
||||
// Only emit the `attachments` event once all other setup is done.
|
||||
this._attachments.next();
|
||||
|
||||
// Track this overlay by the keyboard dispatcher
|
||||
this._keyboardDispatcher.add(this);
|
||||
|
||||
return attachResult;
|
||||
}
|
||||
|
||||
@@ -115,6 +123,9 @@ export class OverlayRef implements PortalHost {
|
||||
// Only emit after everything is detached.
|
||||
this._detachments.next();
|
||||
|
||||
// Remove this overlay from keyboard dispatcher tracking
|
||||
this._keyboardDispatcher.remove(this);
|
||||
|
||||
return detachmentResult;
|
||||
}
|
||||
|
||||
@@ -146,22 +157,27 @@ export class OverlayRef implements PortalHost {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable that emits when the backdrop has been clicked.
|
||||
* Gets an observable that emits when the backdrop has been clicked.
|
||||
*/
|
||||
backdropClick(): Observable<void> {
|
||||
return this._backdropClick.asObservable();
|
||||
}
|
||||
|
||||
/** Returns an observable that emits when the overlay has been attached. */
|
||||
/** Gets an observable that emits when the overlay has been attached. */
|
||||
attachments(): Observable<void> {
|
||||
return this._attachments.asObservable();
|
||||
}
|
||||
|
||||
/** Returns an observable that emits when the overlay has been detached. */
|
||||
/** Gets an observable that emits when the overlay has been detached. */
|
||||
detachments(): Observable<void> {
|
||||
return this._detachments.asObservable();
|
||||
}
|
||||
|
||||
/** Gets an observable of keydown events targeted to this overlay. */
|
||||
keydownEvents(): Observable<KeyboardEvent> {
|
||||
return this._keydownEvents.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current config of the overlay.
|
||||
*/
|
||||
|
||||
@@ -17,6 +17,7 @@ import {DomPortalHost} from '@angular/cdk/portal';
|
||||
import {OverlayConfig} from './overlay-config';
|
||||
import {OverlayRef} from './overlay-ref';
|
||||
import {OverlayPositionBuilder} from './position/overlay-position-builder';
|
||||
import {OverlayKeyboardDispatcher} from './keyboard/overlay-keyboard-dispatcher';
|
||||
import {OverlayContainer} from './overlay-container';
|
||||
import {ScrollStrategyOptions} from './scroll/index';
|
||||
|
||||
@@ -44,6 +45,7 @@ export class Overlay {
|
||||
private _overlayContainer: OverlayContainer,
|
||||
private _componentFactoryResolver: ComponentFactoryResolver,
|
||||
private _positionBuilder: OverlayPositionBuilder,
|
||||
private _keyboardDispatcher: OverlayKeyboardDispatcher,
|
||||
private _appRef: ApplicationRef,
|
||||
private _injector: Injector,
|
||||
private _ngZone: NgZone) { }
|
||||
@@ -56,7 +58,7 @@ export class Overlay {
|
||||
create(config: OverlayConfig = defaultConfig): OverlayRef {
|
||||
const pane = this._createPaneElement();
|
||||
const portalHost = this._createPortalHost(pane);
|
||||
return new OverlayRef(portalHost, pane, config, this._ngZone);
|
||||
return new OverlayRef(portalHost, pane, config, this._ngZone, this._keyboardDispatcher);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,4 +92,5 @@ export class Overlay {
|
||||
private _createPortalHost(pane: HTMLElement): DomPortalHost {
|
||||
return new DomPortalHost(pane, this._componentFactoryResolver, this._appRef, this._injector);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -25,8 +25,9 @@ export function dispatchFakeEvent(node: Node | Window, type: string, canBubble?:
|
||||
}
|
||||
|
||||
/** Shorthand to dispatch a keyboard event with a specified key code. */
|
||||
export function dispatchKeyboardEvent(node: Node, type: string, keyCode: number): KeyboardEvent {
|
||||
return dispatchEvent(node, createKeyboardEvent(type, keyCode)) as KeyboardEvent;
|
||||
export function dispatchKeyboardEvent(node: Node, type: string, keyCode: number, target?: Element):
|
||||
KeyboardEvent {
|
||||
return dispatchEvent(node, createKeyboardEvent(type, keyCode, target)) as KeyboardEvent;
|
||||
}
|
||||
|
||||
/** Shorthand to dispatch a mouse event on the specified coordinates. */
|
||||
|
||||
@@ -23,7 +23,12 @@ import {InputDemo} from '../input/input-demo';
|
||||
import {ListDemo} from '../list/list-demo';
|
||||
import {LiveAnnouncerDemo} from '../live-announcer/live-announcer-demo';
|
||||
import {MenuDemo} from '../menu/menu-demo';
|
||||
import {OverlayDemo, RotiniPanel, SpagettiPanel} from '../overlay/overlay-demo';
|
||||
import {
|
||||
OverlayDemo,
|
||||
RotiniPanel,
|
||||
SpagettiPanel,
|
||||
KeyboardTrackingPanel
|
||||
} from '../overlay/overlay-demo';
|
||||
import {PlatformDemo} from '../platform/platform-demo';
|
||||
import {PortalDemo, ScienceJoke} from '../portal/portal-demo';
|
||||
import {ProgressBarDemo} from '../progress-bar/progress-bar-demo';
|
||||
@@ -80,6 +85,7 @@ import {DEMO_APP_ROUTES} from './routes';
|
||||
IFrameDialog,
|
||||
InputDemo,
|
||||
JazzDialog,
|
||||
KeyboardTrackingPanel,
|
||||
ListDemo,
|
||||
LiveAnnouncerDemo,
|
||||
MatCheckboxDemoNestedChecklist,
|
||||
@@ -119,6 +125,7 @@ import {DEMO_APP_ROUTES} from './routes';
|
||||
DemoApp,
|
||||
IFrameDialog,
|
||||
JazzDialog,
|
||||
KeyboardTrackingPanel,
|
||||
RotiniPanel,
|
||||
ScienceJoke,
|
||||
SpagettiPanel,
|
||||
|
||||
@@ -36,3 +36,5 @@
|
||||
</ng-template>
|
||||
|
||||
<button (click)="openPanelWithBackdrop()">Backdrop panel</button>
|
||||
|
||||
<button (click)="openKeyboardTracking()">Keyboard tracking</button>
|
||||
|
||||
@@ -27,3 +27,11 @@
|
||||
background-color: orangered;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.demo-keyboard {
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
border: 1px solid black;
|
||||
background-color: mediumturquoise;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
ViewContainerRef,
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core';
|
||||
import 'rxjs/add/operator/filter';
|
||||
import 'rxjs/add/operator/do';
|
||||
|
||||
|
||||
@Component({
|
||||
@@ -104,6 +106,26 @@ export class OverlayDemo {
|
||||
overlayRef.backdropClick().subscribe(() => overlayRef.detach());
|
||||
}
|
||||
|
||||
openKeyboardTracking() {
|
||||
let config = new OverlayConfig();
|
||||
|
||||
config.positionStrategy = this.overlay.position()
|
||||
.global()
|
||||
.centerHorizontally()
|
||||
.top(`${this.nextPosition}px`);
|
||||
|
||||
this.nextPosition += 30;
|
||||
|
||||
let overlayRef = this.overlay.create(config);
|
||||
const componentRef = overlayRef
|
||||
.attach(new ComponentPortal(KeyboardTrackingPanel, this.viewContainerRef));
|
||||
|
||||
overlayRef.keydownEvents()
|
||||
.do(e => componentRef.instance.lastKeydown = e.key)
|
||||
.filter(e => e.key === 'Escape')
|
||||
.subscribe(() => overlayRef.detach());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/** Simple component to load into an overlay */
|
||||
@@ -124,3 +146,12 @@ export class RotiniPanel {
|
||||
export class SpagettiPanel {
|
||||
value: string = 'Omega';
|
||||
}
|
||||
|
||||
/** Simple component to load into an overlay */
|
||||
@Component({
|
||||
selector: 'keyboard-panel',
|
||||
template: '<div class="demo-keyboard">Last Keydown: {{ lastKeydown }}</div>'
|
||||
})
|
||||
export class KeyboardTrackingPanel {
|
||||
lastKeydown = '';
|
||||
}
|
||||
|
||||
@@ -115,6 +115,13 @@ export class MatDialogRef<T> {
|
||||
return this._overlayRef.backdropClick();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an observable that emits when keydown events are targeted on the overlay.
|
||||
*/
|
||||
keydownEvents(): Observable<KeyboardEvent> {
|
||||
return this._overlayRef.keydownEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the dialog's position.
|
||||
* @param position New dialog position.
|
||||
|
||||
@@ -25,7 +25,7 @@ import {SpyLocation} from '@angular/common/testing';
|
||||
import {Directionality} from '@angular/cdk/bidi';
|
||||
import {MatDialogContainer} from './dialog-container';
|
||||
import {OverlayContainer} from '@angular/cdk/overlay';
|
||||
import {ESCAPE} from '@angular/cdk/keycodes';
|
||||
import {A, ESCAPE} from '@angular/cdk/keycodes';
|
||||
import {dispatchKeyboardEvent} from '@angular/cdk/testing';
|
||||
import {MAT_DIALOG_DATA, MatDialog, MatDialogModule, MatDialogRef} from './index';
|
||||
|
||||
@@ -203,7 +203,7 @@ describe('MatDialog', () => {
|
||||
viewContainerRef: testViewContainerRef
|
||||
});
|
||||
|
||||
dispatchKeyboardEvent(document, 'keydown', ESCAPE);
|
||||
dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);
|
||||
viewContainerFixture.detectChanges();
|
||||
|
||||
viewContainerFixture.whenStable().then(() => {
|
||||
@@ -277,6 +277,23 @@ describe('MatDialog', () => {
|
||||
});
|
||||
}));
|
||||
|
||||
it('should emit the keyboardEvent stream when key events target the overlay', async(() => {
|
||||
const dialogRef = dialog.open(PizzaMsg, {viewContainerRef: testViewContainerRef});
|
||||
|
||||
const spy = jasmine.createSpy('keyboardEvent spy');
|
||||
dialogRef.keydownEvents().subscribe(spy);
|
||||
|
||||
viewContainerFixture.detectChanges();
|
||||
|
||||
let backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement;
|
||||
let container = overlayContainerElement.querySelector('mat-dialog-container') as HTMLElement;
|
||||
dispatchKeyboardEvent(document.body, 'keydown', A);
|
||||
dispatchKeyboardEvent(document.body, 'keydown', A, backdrop);
|
||||
dispatchKeyboardEvent(document.body, 'keydown', A, container);
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(3);
|
||||
}));
|
||||
|
||||
it('should notify the observers if a dialog has been opened', () => {
|
||||
dialog.afterOpen.subscribe(ref => {
|
||||
expect(dialog.open(PizzaMsg, {
|
||||
@@ -665,7 +682,7 @@ describe('MatDialog', () => {
|
||||
});
|
||||
|
||||
viewContainerFixture.detectChanges();
|
||||
dispatchKeyboardEvent(document, 'keydown', ESCAPE);
|
||||
dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);
|
||||
|
||||
expect(overlayContainerElement.querySelector('mat-dialog-container')).toBeTruthy();
|
||||
});
|
||||
@@ -986,7 +1003,7 @@ describe('MatDialog with a parent MatDialog', () => {
|
||||
it('should close the top dialog via the escape key', async(() => {
|
||||
childDialog.open(PizzaMsg);
|
||||
|
||||
dispatchKeyboardEvent(document, 'keydown', ESCAPE);
|
||||
dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
ScrollStrategy,
|
||||
} from '@angular/cdk/overlay';
|
||||
import {ComponentPortal, ComponentType, PortalInjector, TemplatePortal} from '@angular/cdk/portal';
|
||||
import {startWith} from '@angular/cdk/rxjs';
|
||||
import {RxChain, startWith, filter} from '@angular/cdk/rxjs';
|
||||
import {Location} from '@angular/common';
|
||||
import {
|
||||
ComponentRef,
|
||||
@@ -67,7 +67,6 @@ export class MatDialog {
|
||||
private _openDialogsAtThisLevel: MatDialogRef<any>[] = [];
|
||||
private _afterAllClosedAtThisLevel = new Subject<void>();
|
||||
private _afterOpenAtThisLevel = new Subject<MatDialogRef<any>>();
|
||||
private _boundKeydown = this._handleKeydown.bind(this);
|
||||
|
||||
/** Keeps track of the currently-open dialogs. */
|
||||
get openDialogs(): MatDialogRef<any>[] {
|
||||
@@ -135,10 +134,6 @@ export class MatDialog {
|
||||
const dialogRef =
|
||||
this._attachDialogContent(componentOrTemplateRef, dialogContainer, overlayRef, config);
|
||||
|
||||
if (!this.openDialogs.length) {
|
||||
document.addEventListener('keydown', this._boundKeydown);
|
||||
}
|
||||
|
||||
this.openDialogs.push(dialogRef);
|
||||
dialogRef.afterClosed().subscribe(() => this._removeOpenDialog(dialogRef));
|
||||
this.afterOpen.next(dialogRef);
|
||||
@@ -246,6 +241,11 @@ export class MatDialog {
|
||||
});
|
||||
}
|
||||
|
||||
// Close when escape keydown event occurs
|
||||
RxChain.from(overlayRef.keydownEvents())
|
||||
.call(filter, event => event.keyCode === ESCAPE && !dialogRef.disableClose)
|
||||
.subscribe(() => dialogRef.close());
|
||||
|
||||
if (componentOrTemplateRef instanceof TemplateRef) {
|
||||
dialogContainer.attachTemplatePortal(
|
||||
new TemplatePortal<T>(componentOrTemplateRef, null!,
|
||||
@@ -304,23 +304,9 @@ export class MatDialog {
|
||||
// no open dialogs are left, call next on afterAllClosed Subject
|
||||
if (!this.openDialogs.length) {
|
||||
this._afterAllClosed.next();
|
||||
document.removeEventListener('keydown', this._boundKeydown);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles global key presses while there are open dialogs. Closes the
|
||||
* top dialog when the user presses escape.
|
||||
*/
|
||||
private _handleKeydown(event: KeyboardEvent): void {
|
||||
const topDialog = this.openDialogs[this.openDialogs.length - 1];
|
||||
const canClose = topDialog ? !topDialog.disableClose : false;
|
||||
|
||||
if (event.keyCode === ESCAPE && canClose) {
|
||||
topDialog.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user