-
Notifications
You must be signed in to change notification settings - Fork 6.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
1 parent
70bd5fc
commit e074fa0
Showing
13 changed files
with
305 additions
and
32 deletions.
There are no files selected for viewing
97 changes: 97 additions & 0 deletions
97
src/cdk/overlay/keyboard/overlay-keyboard-dispatcher.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
|
||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.