ipsos-components
Version:
Material Design components for Angular
116 lines (95 loc) • 4.06 kB
text/typescript
/**
* @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, Inject, InjectionToken, Optional, SkipSelf, OnDestroy} from '@angular/core';
import {OverlayRef} from '../overlay-ref';
import {Subscription} from 'rxjs/Subscription';
import {filter} from 'rxjs/operators/filter';
import {fromEvent} from 'rxjs/observable/fromEvent';
import {DOCUMENT} from '@angular/common';
/**
* 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.
*/
export class OverlayKeyboardDispatcher implements OnDestroy {
/** Currently attached overlays in the order they were attached. */
_attachedOverlays: OverlayRef[] = [];
private _keydownEventSubscription: Subscription | null;
constructor( private _document: any) {}
ngOnDestroy() {
this._unsubscribeFromKeydownEvents();
}
/** 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);
}
// Remove the global listener once there are no more overlays.
if (this._attachedOverlays.length === 0) {
this._unsubscribeFromKeydownEvents();
}
}
/**
* Subscribe to keydown events that land on the body and dispatch those
* events to the appropriate overlay.
*/
private _subscribeToKeydownEvents(): void {
const bodyKeydownEvents = fromEvent<KeyboardEvent>(this._document.body, 'keydown');
this._keydownEventSubscription = bodyKeydownEvents.pipe(
filter(() => !!this._attachedOverlays.length)
).subscribe(event => {
// Dispatch keydown event to correct overlay reference
this._selectOverlayFromEvent(event)._keydownEvents.next(event);
});
}
/** Removes the global keydown subscription. */
private _unsubscribeFromKeydownEvents(): void {
if (this._keydownEventSubscription) {
this._keydownEventSubscription.unsubscribe();
this._keydownEventSubscription = null;
}
}
/** 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, _document: any) {
return dispatcher || new OverlayKeyboardDispatcher(_document);
}
/** @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],
// Coerce to `InjectionToken` so that the `deps` match the "shape"
// of the type expected by Angular
DOCUMENT as InjectionToken<any>
],
useFactory: OVERLAY_KEYBOARD_DISPATCHER_PROVIDER_FACTORY
};