Skip to content

Commit

Permalink
feat(overlay): add keyboard dispatcher for targeting correct overlay
Browse files Browse the repository at this point in the history
Fix typo and add comment

Address comments

Add keyboard tracking demo to overlay

Address comments

Revert no-longer-needed dispose changes
  • Loading branch information
willshowell committed Aug 31, 2017
1 parent 70bd5fc commit e074fa0
Show file tree
Hide file tree
Showing 13 changed files with 305 additions and 32 deletions.
97 changes: 97 additions & 0 deletions src/cdk/overlay/keyboard/overlay-keyboard-dispatcher.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
});

});
96 changes: 96 additions & 0 deletions src/cdk/overlay/keyboard/overlay-keyboard-dispatcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/**
* @license
* Copyright Google Inc. 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 {Subject} from 'rxjs/Subject';
import {RxChain, filter, takeUntil} 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[] = [];

/** Emits when the service is destroyed. */
private _onDestroy = new Subject<void>();

constructor() {
this._dispatchKeydownEvents();
}

ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}

/** Add a new overlay to the list of attached overlay refs. */
add(overlayRef: OverlayRef): void {
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 _dispatchKeydownEvents(): void {
const bodyKeydownEvents = fromEvent<KeyboardEvent>(document.body, 'keydown');

RxChain.from(bodyKeydownEvents)
.call(filter, () => !!this._attachedOverlays.length)
.call(takeUntil, this._onDestroy)
.subscribe(event => {
// Dispatch keydown event to correct overlay reference
this._selectOverlayFromEvent(event)._dispatchedKeydownEvents.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.contains(event.target as HTMLElement) ||
overlay.overlayElement === event.target;
});

// Use that overlay if it exists, otherwise choose the most recently attached one
return targetedOverlay ?
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
};
23 changes: 19 additions & 4 deletions src/cdk/overlay/overlay-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import {NgZone} from '@angular/core';
import {PortalHost, Portal} from '@angular/cdk/portal';
import {OverlayState} from './overlay-state';
import {OverlayKeyboardDispatcher} from './keyboard/overlay-keyboard-dispatcher';
import {Observable} from 'rxjs/Observable';
import {Subject} from 'rxjs/Subject';

Expand All @@ -23,11 +24,14 @@ export class OverlayRef implements PortalHost {
private _attachments = new Subject<void>();
private _detachments = new Subject<void>();

_dispatchedKeydownEvents = new Subject<KeyboardEvent>();

constructor(
private _portalHost: PortalHost,
private _pane: HTMLElement,
private _state: OverlayState,
private _ngZone: NgZone) {
private _ngZone: NgZone,
private _keyboardDispatcher: OverlayKeyboardDispatcher) {

_state.scrollStrategy.attach(this);
}
Expand Down Expand Up @@ -75,6 +79,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;
}

Expand All @@ -96,6 +103,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;
}

Expand Down Expand Up @@ -124,22 +134,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._dispatchedKeydownEvents.asObservable();
}

/**
* Gets the current state config of the overlay.
*/
Expand Down
6 changes: 5 additions & 1 deletion src/cdk/overlay/overlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {DomPortalHost} from '@angular/cdk/portal';
import {OverlayState} from './overlay-state';
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';

Expand All @@ -38,10 +39,12 @@ let defaultState = new OverlayState();
*/
@Injectable()
export class Overlay {

constructor(public scrollStrategies: ScrollStrategyOptions,
private _overlayContainer: OverlayContainer,
private _componentFactoryResolver: ComponentFactoryResolver,
private _positionBuilder: OverlayPositionBuilder,
private _keyboardDispatcher: OverlayKeyboardDispatcher,
private _appRef: ApplicationRef,
private _injector: Injector,
private _ngZone: NgZone) { }
Expand All @@ -54,7 +57,7 @@ export class Overlay {
create(state: OverlayState = defaultState): OverlayRef {
const pane = this._createPaneElement();
const portalHost = this._createPortalHost(pane);
return new OverlayRef(portalHost, pane, state, this._ngZone);
return new OverlayRef(portalHost, pane, state, this._ngZone, this._keyboardDispatcher);
}

/**
Expand Down Expand Up @@ -87,4 +90,5 @@ export class Overlay {
private _createPortalHost(pane: HTMLElement): DomPortalHost {
return new DomPortalHost(pane, this._componentFactoryResolver, this._appRef, this._injector);
}

}
2 changes: 2 additions & 0 deletions src/cdk/overlay/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ import {
OverlayOrigin,
} from './overlay-directives';
import {OverlayPositionBuilder} from './position/overlay-position-builder';
import {OVERLAY_KEYBOARD_DISPATCHER_PROVIDER} from './keyboard/overlay-keyboard-dispatcher';
import {OVERLAY_CONTAINER_PROVIDER} from './overlay-container';
import {ScrollStrategyOptions} from './scroll/scroll-strategy-options';


export const OVERLAY_PROVIDERS: Provider[] = [
Overlay,
OverlayPositionBuilder,
OVERLAY_KEYBOARD_DISPATCHER_PROVIDER,
VIEWPORT_RULER_PROVIDER,
OVERLAY_CONTAINER_PROVIDER,
MD_CONNECTED_OVERLAY_SCROLL_STRATEGY_PROVIDER,
Expand Down
5 changes: 3 additions & 2 deletions src/cdk/testing/dispatch-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ export function dispatchFakeEvent(node: Node | Window, type: string): Event {
}

/** 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. */
Expand Down
9 changes: 8 additions & 1 deletion src/demo-app/demo-app/demo-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@ import {ListDemo} from '../list/list-demo';
import {BaselineDemo} from '../baseline/baseline-demo';
import {GridListDemo} from '../grid-list/grid-list-demo';
import {LiveAnnouncerDemo} from '../live-announcer/live-announcer-demo';
import {OverlayDemo, RotiniPanel, SpagettiPanel} from '../overlay/overlay-demo';
import {
OverlayDemo,
RotiniPanel,
SpagettiPanel,
KeyboardTrackingPanel
} from '../overlay/overlay-demo';
import {SlideToggleDemo} from '../slide-toggle/slide-toggle-demo';
import {ToolbarDemo} from '../toolbar/toolbar-demo';
import {ButtonDemo} from '../button/button-demo';
Expand Down Expand Up @@ -72,6 +77,7 @@ import {TableHeaderDemo} from '../table/table-header-demo';
IconDemo,
InputDemo,
JazzDialog,
KeyboardTrackingPanel,
ContentElementDialog,
IFrameDialog,
ListDemo,
Expand Down Expand Up @@ -117,6 +123,7 @@ import {TableHeaderDemo} from '../table/table-header-demo';
RotiniPanel,
ScienceJoke,
SpagettiPanel,
KeyboardTrackingPanel,
],
})
export class DemoModule {}
2 changes: 2 additions & 0 deletions src/demo-app/overlay/overlay-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,5 @@
</ng-template>

<button (click)="openPanelWithBackdrop()">Backdrop panel</button>

<button (click)="openKeyboardTracking()">Keyboard tracking</button>
8 changes: 8 additions & 0 deletions src/demo-app/overlay/overlay-demo.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Loading

0 comments on commit e074fa0

Please sign in to comment.