From 1c8226659f598babfe01da7c5cf1e20f445aada3 Mon Sep 17 00:00:00 2001 From: wnvko Date: Wed, 17 Jun 2020 11:59:07 +0300 Subject: [PATCH] feat(overlay): add detach on outside click --- .../dispatchers/base-overlay-dispatcher.ts | 59 +++++++ src/cdk/overlay/dispatchers/index.ts | 10 ++ .../overlay-keyboard-dispatcher.spec.ts | 0 .../overlay-keyboard-dispatcher.ts | 37 +--- .../overlay-outside-click-dispatcher.spec.ts | 166 ++++++++++++++++++ .../overlay-outside-click-dispatcher.ts | 95 ++++++++++ src/cdk/overlay/overlay-config.ts | 5 + src/cdk/overlay/overlay-directives.ts | 7 + src/cdk/overlay/overlay-module.ts | 2 +- src/cdk/overlay/overlay-ref.ts | 31 +++- src/cdk/overlay/overlay-reference.ts | 1 + src/cdk/overlay/overlay.ts | 9 +- src/cdk/overlay/public-api.ts | 2 +- tools/public_api_guard/cdk/overlay.d.ts | 26 ++- 14 files changed, 403 insertions(+), 47 deletions(-) create mode 100644 src/cdk/overlay/dispatchers/base-overlay-dispatcher.ts create mode 100644 src/cdk/overlay/dispatchers/index.ts rename src/cdk/overlay/{keyboard => dispatchers}/overlay-keyboard-dispatcher.spec.ts (100%) rename src/cdk/overlay/{keyboard => dispatchers}/overlay-keyboard-dispatcher.ts (76%) create mode 100644 src/cdk/overlay/dispatchers/overlay-outside-click-dispatcher.spec.ts create mode 100644 src/cdk/overlay/dispatchers/overlay-outside-click-dispatcher.ts diff --git a/src/cdk/overlay/dispatchers/base-overlay-dispatcher.ts b/src/cdk/overlay/dispatchers/base-overlay-dispatcher.ts new file mode 100644 index 000000000000..e25a42f10ed6 --- /dev/null +++ b/src/cdk/overlay/dispatchers/base-overlay-dispatcher.ts @@ -0,0 +1,59 @@ +/** + * @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 {DOCUMENT} from '@angular/common'; +import {Inject, Injectable, OnDestroy} from '@angular/core'; +import {OverlayReference} from '../overlay-reference'; + + +/** + * Service for dispatching 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({providedIn: 'root'}) +export abstract class BaseOverlayDispatcher implements OnDestroy { + + /** Currently attached overlays in the order they were attached. */ + _attachedOverlays: OverlayReference[] = []; + + protected _document: Document; + protected _isAttached: boolean; + + constructor(@Inject(DOCUMENT) document: any) { + this._document = document; + } + + ngOnDestroy(): void { + this.detach(); + } + + /** Add a new overlay to the list of attached overlay refs. */ + add(overlayRef: OverlayReference): void { + // Ensure that we don't get the same overlay multiple times. + this.remove(overlayRef); + this._attachedOverlays.push(overlayRef); + } + + /** Remove an overlay from the list of attached overlay refs. */ + remove(overlayRef: OverlayReference): 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.detach(); + } + } + + /** Detaches the global event listener. */ + protected abstract detach(): void; +} diff --git a/src/cdk/overlay/dispatchers/index.ts b/src/cdk/overlay/dispatchers/index.ts new file mode 100644 index 000000000000..a196468999f3 --- /dev/null +++ b/src/cdk/overlay/dispatchers/index.ts @@ -0,0 +1,10 @@ +/** + * @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 + */ + +export {OverlayOutsideClickDispatcher} from './overlay-outside-click-dispatcher'; +export {OverlayKeyboardDispatcher} from './overlay-keyboard-dispatcher'; diff --git a/src/cdk/overlay/keyboard/overlay-keyboard-dispatcher.spec.ts b/src/cdk/overlay/dispatchers/overlay-keyboard-dispatcher.spec.ts similarity index 100% rename from src/cdk/overlay/keyboard/overlay-keyboard-dispatcher.spec.ts rename to src/cdk/overlay/dispatchers/overlay-keyboard-dispatcher.spec.ts diff --git a/src/cdk/overlay/keyboard/overlay-keyboard-dispatcher.ts b/src/cdk/overlay/dispatchers/overlay-keyboard-dispatcher.ts similarity index 76% rename from src/cdk/overlay/keyboard/overlay-keyboard-dispatcher.ts rename to src/cdk/overlay/dispatchers/overlay-keyboard-dispatcher.ts index 0882ec760f98..53aeb56884e6 100644 --- a/src/cdk/overlay/keyboard/overlay-keyboard-dispatcher.ts +++ b/src/cdk/overlay/dispatchers/overlay-keyboard-dispatcher.ts @@ -11,11 +11,11 @@ import { Inject, Injectable, InjectionToken, - OnDestroy, Optional, SkipSelf, } from '@angular/core'; import {OverlayReference} from '../overlay-reference'; +import {BaseOverlayDispatcher} from './base-overlay-dispatcher'; /** @@ -24,52 +24,25 @@ import {OverlayReference} from '../overlay-reference'; * on event target and order of overlay opens. */ @Injectable({providedIn: 'root'}) -export class OverlayKeyboardDispatcher implements OnDestroy { - - /** Currently attached overlays in the order they were attached. */ - _attachedOverlays: OverlayReference[] = []; - - private _document: Document; - private _isAttached: boolean; +export class OverlayKeyboardDispatcher extends BaseOverlayDispatcher { constructor(@Inject(DOCUMENT) document: any) { - this._document = document; - } - - ngOnDestroy() { - this._detach(); + super(document); } /** Add a new overlay to the list of attached overlay refs. */ add(overlayRef: OverlayReference): void { - // Ensure that we don't get the same overlay multiple times. - this.remove(overlayRef); + super.add(overlayRef); // Lazily start dispatcher once first overlay is added if (!this._isAttached) { this._document.body.addEventListener('keydown', this._keydownListener); this._isAttached = true; } - - this._attachedOverlays.push(overlayRef); - } - - /** Remove an overlay from the list of attached overlay refs. */ - remove(overlayRef: OverlayReference): 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._detach(); - } } /** Detaches the global keyboard event listener. */ - private _detach() { + protected detach() { if (this._isAttached) { this._document.body.removeEventListener('keydown', this._keydownListener); this._isAttached = false; diff --git a/src/cdk/overlay/dispatchers/overlay-outside-click-dispatcher.spec.ts b/src/cdk/overlay/dispatchers/overlay-outside-click-dispatcher.spec.ts new file mode 100644 index 000000000000..989fd0d7a93f --- /dev/null +++ b/src/cdk/overlay/dispatchers/overlay-outside-click-dispatcher.spec.ts @@ -0,0 +1,166 @@ +import {TestBed, inject} from '@angular/core/testing'; +import {Component, NgModule} from '@angular/core'; +import {OverlayModule, OverlayContainer, Overlay} from '../index'; +import {OverlayOutsideClickDispatcher} from './overlay-outside-click-dispatcher'; +import {ComponentPortal} from '@angular/cdk/portal'; + + +describe('OverlayOutsideClickDispatcher', () => { + let outsideClickDispatcher: OverlayOutsideClickDispatcher; + let overlay: Overlay; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [OverlayModule, TestComponentModule], + }); + + inject([OverlayOutsideClickDispatcher, Overlay], + (ocd: OverlayOutsideClickDispatcher, o: Overlay) => { + outsideClickDispatcher = ocd; + overlay = o; + })(); + }); + + afterEach(inject([OverlayContainer], (overlayContainer: OverlayContainer) => { + overlayContainer.ngOnDestroy(); + })); + + it('should track overlays in order as they are attached and detached', () => { + const overlayOne = overlay.create(); + const overlayTwo = overlay.create(); + + outsideClickDispatcher.add(overlayOne); + outsideClickDispatcher.add(overlayTwo); + + expect(outsideClickDispatcher._attachedOverlays.length) + .toBe(2, 'Expected both overlays to be tracked.'); + expect(outsideClickDispatcher._attachedOverlays[0]) + .toBe(overlayOne, 'Expected one to be first.'); + expect(outsideClickDispatcher._attachedOverlays[1]) + .toBe(overlayTwo, 'Expected two to be last.'); + + outsideClickDispatcher.remove(overlayOne); + outsideClickDispatcher.add(overlayOne); + + expect(outsideClickDispatcher._attachedOverlays[0]) + .toBe(overlayTwo, 'Expected two to now be first.'); + expect(outsideClickDispatcher._attachedOverlays[1]) + .toBe(overlayOne, 'Expected one to now be last.'); + + overlayOne.dispose(); + overlayTwo.dispose(); + }); + + it( + 'should dispatch mouse click events to the attached overlays', + () => { + const overlayOne = overlay.create(); + const overlayTwo = overlay.create(); + const overlayOneSpy = jasmine.createSpy('overlayOne mouse click event spy'); + const overlayTwoSpy = jasmine.createSpy('overlayTwo mouse click event spy'); + + overlayOne.outsidePointerEvents().subscribe(overlayOneSpy); + overlayTwo.outsidePointerEvents().subscribe(overlayTwoSpy); + + outsideClickDispatcher.add(overlayOne); + outsideClickDispatcher.add(overlayTwo); + + const button = document.createElement('button'); + document.body.appendChild(button); + button.click(); + + expect(overlayOneSpy).toHaveBeenCalled(); + expect(overlayTwoSpy).toHaveBeenCalled(); + + button.parentNode!.removeChild(button); + overlayOne.dispose(); + overlayTwo.dispose(); + }); + + it( + 'should dispatch mouse click events to the attached overlays even when propagation is stopped', + () => { + const overlayRef = overlay.create(); + const spy = jasmine.createSpy('overlay mouse click event spy'); + overlayRef.outsidePointerEvents().subscribe(spy); + + outsideClickDispatcher.add(overlayRef); + + const button = document.createElement('button'); + document.body.appendChild(button); + button.addEventListener('click', event => event.stopPropagation()); + button.click(); + + expect(spy).toHaveBeenCalled(); + + button.parentNode!.removeChild(button); + overlayRef.dispose(); + }); + + it('should dispose of the global click event handler correctly', () => { + const overlayRef = overlay.create(); + const body = document.body; + + spyOn(body, 'addEventListener'); + spyOn(body, 'removeEventListener'); + + outsideClickDispatcher.add(overlayRef); + expect(body.addEventListener).toHaveBeenCalledWith('click', jasmine.any(Function), true); + + overlayRef.dispose(); + expect(body.removeEventListener).toHaveBeenCalledWith('click', jasmine.any(Function), true); + }); + + it('should not add the same overlay to the stack multiple times', () => { + const overlayOne = overlay.create(); + const overlayTwo = overlay.create(); + + outsideClickDispatcher.add(overlayOne); + outsideClickDispatcher.add(overlayTwo); + outsideClickDispatcher.add(overlayOne); + + expect(outsideClickDispatcher._attachedOverlays).toEqual([overlayTwo, overlayOne]); + + overlayOne.dispose(); + overlayTwo.dispose(); + }); + + it(`should not dispatch click event when click on element + included in excludeFromOutsideClick array`, () => { + const overlayRef = overlay.create(); + const spy = jasmine.createSpy('overlay mouse click event spy'); + overlayRef.outsidePointerEvents().subscribe(spy); + + const overlayConfig = overlayRef.getConfig(); + expect(overlayConfig.excludeFromOutsideClick).toBeDefined(); + expect(overlayConfig.excludeFromOutsideClick!.length).toBe(0); + + overlayRef.attach(new ComponentPortal(TestComponent)); + + const buttonShouldNotDetach = document.createElement('button'); + document.body.appendChild(buttonShouldNotDetach); + overlayConfig.excludeFromOutsideClick!.push(buttonShouldNotDetach); + buttonShouldNotDetach.click(); + + expect(spy).not.toHaveBeenCalled(); + + buttonShouldNotDetach.parentNode!.removeChild(buttonShouldNotDetach); + overlayRef.dispose(); + }); +}); + + +@Component({ + template: 'Hello' +}) +class TestComponent { } + + +// Create a real (non-test) NgModule as a workaround for +// https://github.com/angular/angular/issues/10760 +@NgModule({ + exports: [TestComponent], + declarations: [TestComponent], + entryComponents: [TestComponent], +}) +class TestComponentModule { } diff --git a/src/cdk/overlay/dispatchers/overlay-outside-click-dispatcher.ts b/src/cdk/overlay/dispatchers/overlay-outside-click-dispatcher.ts new file mode 100644 index 000000000000..ecbce283453c --- /dev/null +++ b/src/cdk/overlay/dispatchers/overlay-outside-click-dispatcher.ts @@ -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 {DOCUMENT} from '@angular/common'; +import {Inject, Injectable} from '@angular/core'; +import {OverlayReference} from '../overlay-reference'; +import {Platform} from '@angular/cdk/platform'; +import {BaseOverlayDispatcher} from './base-overlay-dispatcher'; + +/** + * Service for dispatching mouse click 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({providedIn: 'root'}) +export class OverlayOutsideClickDispatcher extends BaseOverlayDispatcher { + private _cursorOriginalValue: string; + private _cursorStyleIsSet = false; + + constructor(@Inject(DOCUMENT) document: any, private _platform: Platform) { + super(document); + } + + /** Add a new overlay to the list of attached overlay refs. */ + add(overlayRef: OverlayReference): void { + super.add(overlayRef); + + // tslint:disable: max-line-length + // Safari on iOS does not generate click events for non-interactive + // elements. However, we want to receive a click for any element outside + // the overlay. We can force a "clickable" state by setting + // `cursor: pointer` on the document body. + // See https://developer.mozilla.org/en-US/docs/Web/API/Element/click_event#Safari_Mobile + // and https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/HandlingEvents/HandlingEvents.html + // tslint:enable: max-line-length + if (!this._isAttached) { + this._document.body.addEventListener('click', this._clickListener, true); + + // click event is not fired on iOS. To make element "clickable" we are + // setting the cursor to pointer + if (this._platform.IOS && !this._cursorStyleIsSet) { + this._cursorOriginalValue = this._document.body.style.cursor; + this._document.body.style.cursor = 'pointer'; + this._cursorStyleIsSet = true; + } + + this._isAttached = true; + } + } + + /** Detaches the global keyboard event listener. */ + protected detach() { + if (this._isAttached) { + this._document.body.removeEventListener('click', this._clickListener, true); + if (this._platform.IOS && this._cursorStyleIsSet) { + this._document.body.style.cursor = this._cursorOriginalValue; + this._cursorStyleIsSet = false; + } + this._isAttached = false; + } + } + + /** Click event listener that will be attached to the body propagate phase. */ + private _clickListener = (event: MouseEvent) => { + const overlays = this._attachedOverlays; + + // Dispatch the mouse event to the top overlay which has subscribers to its mouse events. + // We want to target all overlays for which the click could be considered as outside click. + // As soon as we reach an overlay for which the click is not outside click we break off + // the loop. + for (let i = overlays.length - 1; i > -1; i--) { + const overlayRef = overlays[i]; + if (overlayRef._outsidePointerEvents.observers.length < 1) { + continue; + } + + const config = overlayRef.getConfig(); + const excludeElements = [...config.excludeFromOutsideClick!, overlayRef.overlayElement]; + const isInsideClick: boolean = excludeElements.some(e => e.contains(event.target as Node)); + + // If it is inside click just break - we should do nothing + // If it is outside click dispatch the mouse event, and proceed with the next overlay + if (isInsideClick) { + break; + } + + overlayRef._outsidePointerEvents.next(event); + } + } +} diff --git a/src/cdk/overlay/overlay-config.ts b/src/cdk/overlay/overlay-config.ts index f665e2164921..3d7e0ac1502e 100644 --- a/src/cdk/overlay/overlay-config.ts +++ b/src/cdk/overlay/overlay-config.ts @@ -59,6 +59,11 @@ export class OverlayConfig { */ disposeOnNavigation?: boolean = false; + /** + * Array of HTML elements clicking on which should not be considered as outside click + */ + excludeFromOutsideClick?: HTMLElement[] = []; + constructor(config?: OverlayConfig) { if (config) { // Use `Iterable` instead of `Array` because TypeScript, as of 3.6.3, diff --git a/src/cdk/overlay/overlay-directives.ts b/src/cdk/overlay/overlay-directives.ts index ca5262aa7ed9..9866e7e825af 100644 --- a/src/cdk/overlay/overlay-directives.ts +++ b/src/cdk/overlay/overlay-directives.ts @@ -222,6 +222,9 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { /** Emits when there are keyboard events that are targeted at the overlay. */ @Output() overlayKeydown = new EventEmitter(); + /** Emits when there are mouse outside click events that are targeted at the overlay. */ + @Output() overlayOutsideClick = new EventEmitter(); + // TODO(jelbourn): inputs for size, scroll behavior, animation, etc. constructor( @@ -289,6 +292,10 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges { this._detachOverlay(); } }); + + this._overlayRef.outsidePointerEvents().subscribe((event: MouseEvent) => { + this.overlayOutsideClick.next(event); + }); } /** Builds the overlay config based on the directive's inputs */ diff --git a/src/cdk/overlay/overlay-module.ts b/src/cdk/overlay/overlay-module.ts index 100750bf7100..1bf2d7016ec0 100644 --- a/src/cdk/overlay/overlay-module.ts +++ b/src/cdk/overlay/overlay-module.ts @@ -10,7 +10,7 @@ import {BidiModule} from '@angular/cdk/bidi'; import {PortalModule} from '@angular/cdk/portal'; import {ScrollingModule} from '@angular/cdk/scrolling'; import {NgModule, Provider} from '@angular/core'; -import {OVERLAY_KEYBOARD_DISPATCHER_PROVIDER} from './keyboard/overlay-keyboard-dispatcher'; +import {OVERLAY_KEYBOARD_DISPATCHER_PROVIDER} from './dispatchers/overlay-keyboard-dispatcher'; import {Overlay} from './overlay'; import {OVERLAY_CONTAINER_PROVIDER} from './overlay-container'; import { diff --git a/src/cdk/overlay/overlay-ref.ts b/src/cdk/overlay/overlay-ref.ts index e22ab4790ad7..8fade22303f3 100644 --- a/src/cdk/overlay/overlay-ref.ts +++ b/src/cdk/overlay/overlay-ref.ts @@ -12,7 +12,8 @@ import {ComponentRef, EmbeddedViewRef, NgZone} from '@angular/core'; import {Location} from '@angular/common'; import {Observable, Subject, merge, SubscriptionLike, Subscription} from 'rxjs'; import {take, takeUntil} from 'rxjs/operators'; -import {OverlayKeyboardDispatcher} from './keyboard/overlay-keyboard-dispatcher'; +import {OverlayKeyboardDispatcher} from './dispatchers/overlay-keyboard-dispatcher'; +import {OverlayOutsideClickDispatcher} from './dispatchers/overlay-outside-click-dispatcher'; import {OverlayConfig} from './overlay-config'; import {coerceCssPixelValue, coerceArray} from '@angular/cdk/coercion'; import {OverlayReference} from './overlay-reference'; @@ -48,6 +49,9 @@ export class OverlayRef implements PortalOutlet, OverlayReference { /** Stream of keydown events dispatched to this overlay. */ _keydownEvents = new Subject(); + /** Stream of mouse outside events dispatched to this overlay. */ + _outsidePointerEvents = new Subject(); + constructor( private _portalOutlet: PortalOutlet, private _host: HTMLElement, @@ -57,7 +61,9 @@ export class OverlayRef implements PortalOutlet, OverlayReference { private _keyboardDispatcher: OverlayKeyboardDispatcher, private _document: Document, // @breaking-change 8.0.0 `_location` parameter to be made required. - private _location?: Location) { + private _location?: Location, + // @breaking-change 9.0.0 `_mouseClickDispatcher` parameter to be made required. + private _outsideClickDispatcher?: OverlayOutsideClickDispatcher) { if (_config.scrollStrategy) { this._scrollStrategy = _config.scrollStrategy; @@ -153,6 +159,11 @@ export class OverlayRef implements PortalOutlet, OverlayReference { this._locationChanges = this._location.subscribe(() => this.dispose()); } + // @breaking-change 9.0.0 remove the null check for `_mouseClickDispatcher` + if (this._outsideClickDispatcher) { + this._outsideClickDispatcher.add(this); + } + return attachResult; } @@ -195,6 +206,11 @@ export class OverlayRef implements PortalOutlet, OverlayReference { // Stop listening for location changes. this._locationChanges.unsubscribe(); + // @breaking-change 9.0.0 remove the null check for `_outsideClickDispatcher` + if (this._outsideClickDispatcher) { + this._outsideClickDispatcher.remove(this); + } + return detachmentResult; } @@ -214,6 +230,12 @@ export class OverlayRef implements PortalOutlet, OverlayReference { this._attachments.complete(); this._backdropClick.complete(); this._keydownEvents.complete(); + this._outsidePointerEvents.complete(); + + // @breaking-change 9.0.0 remove the null check for `_outsideClickDispatcher` + if (this._outsideClickDispatcher) { + this._outsideClickDispatcher.remove(this); + } if (this._host && this._host.parentNode) { this._host.parentNode.removeChild(this._host); @@ -254,6 +276,11 @@ export class OverlayRef implements PortalOutlet, OverlayReference { return this._keydownEvents.asObservable(); } + /** Gets an observable of pointer events targeted outside this overlay. */ + outsidePointerEvents(): Observable { + return this._outsidePointerEvents.asObservable(); + } + /** Gets the current overlay configuration, which is immutable. */ getConfig(): OverlayConfig { return this._config; diff --git a/src/cdk/overlay/overlay-reference.ts b/src/cdk/overlay/overlay-reference.ts index cf1b1a117f60..e40e9527d3cd 100644 --- a/src/cdk/overlay/overlay-reference.ts +++ b/src/cdk/overlay/overlay-reference.ts @@ -27,5 +27,6 @@ export interface OverlayReference { updatePosition: () => void; getDirection: () => Direction; setDirection: (dir: Direction | Directionality) => void; + _outsidePointerEvents: Subject; _keydownEvents: Subject; } diff --git a/src/cdk/overlay/overlay.ts b/src/cdk/overlay/overlay.ts index 20c9d38c35d7..77d9d66a5e68 100644 --- a/src/cdk/overlay/overlay.ts +++ b/src/cdk/overlay/overlay.ts @@ -18,7 +18,8 @@ import { NgZone, Optional, } from '@angular/core'; -import {OverlayKeyboardDispatcher} from './keyboard/overlay-keyboard-dispatcher'; +import {OverlayKeyboardDispatcher} from './dispatchers/overlay-keyboard-dispatcher'; +import {OverlayOutsideClickDispatcher} from './dispatchers/overlay-outside-click-dispatcher'; import {OverlayConfig} from './overlay-config'; import {OverlayContainer} from './overlay-container'; import {OverlayRef} from './overlay-ref'; @@ -56,7 +57,9 @@ export class Overlay { @Inject(DOCUMENT) private _document: any, private _directionality: Directionality, // @breaking-change 8.0.0 `_location` parameter to be made required. - @Optional() private _location?: Location) { } + @Optional() private _location?: Location, + // @breaking-change 9.0.0 `_outsideClickDispatcher` parameter to be made required. + @Optional() private _outsideClickDispatcher?: OverlayOutsideClickDispatcher) { } /** * Creates an overlay. @@ -72,7 +75,7 @@ export class Overlay { overlayConfig.direction = overlayConfig.direction || this._directionality.value; return new OverlayRef(portalOutlet, host, pane, overlayConfig, this._ngZone, - this._keyboardDispatcher, this._document, this._location); + this._keyboardDispatcher, this._document, this._location, this._outsideClickDispatcher); } /** diff --git a/src/cdk/overlay/public-api.ts b/src/cdk/overlay/public-api.ts index f3e011a8397b..707e9bca4905 100644 --- a/src/cdk/overlay/public-api.ts +++ b/src/cdk/overlay/public-api.ts @@ -10,6 +10,7 @@ export * from './overlay-config'; export * from './position/connected-position'; export * from './scroll/index'; export * from './overlay-module'; +export * from './dispatchers/index'; export {Overlay} from './overlay'; export {OverlayContainer} from './overlay-container'; export {CdkOverlayOrigin, CdkConnectedOverlay} from './overlay-directives'; @@ -17,7 +18,6 @@ export {FullscreenOverlayContainer} from './fullscreen-overlay-container'; export {OverlayRef, OverlaySizeConfig} from './overlay-ref'; export {ViewportRuler} from '@angular/cdk/scrolling'; export {ComponentType} from '@angular/cdk/portal'; -export {OverlayKeyboardDispatcher} from './keyboard/overlay-keyboard-dispatcher'; export {OverlayPositionBuilder} from './position/overlay-position-builder'; // Export pre-defined position strategies and interface to build custom ones. diff --git a/tools/public_api_guard/cdk/overlay.d.ts b/tools/public_api_guard/cdk/overlay.d.ts index fe094416a887..7de3bbe0cc0c 100644 --- a/tools/public_api_guard/cdk/overlay.d.ts +++ b/tools/public_api_guard/cdk/overlay.d.ts @@ -29,6 +29,7 @@ export declare class CdkConnectedOverlay implements OnDestroy, OnChanges { open: boolean; origin: CdkOverlayOrigin; overlayKeydown: EventEmitter; + overlayOutsideClick: EventEmitter; get overlayRef(): OverlayRef; panelClass: string | string[]; positionChange: EventEmitter; @@ -48,7 +49,7 @@ export declare class CdkConnectedOverlay implements OnDestroy, OnChanges { static ngAcceptInputType_hasBackdrop: BooleanInput; static ngAcceptInputType_lockPosition: BooleanInput; static ngAcceptInputType_push: BooleanInput; - static ɵdir: i0.ɵɵDirectiveDefWithMeta; + static ɵdir: i0.ɵɵDirectiveDefWithMeta; static ɵfac: i0.ɵɵFactoryDef; } @@ -191,10 +192,10 @@ export interface OriginConnectionPosition { export declare class Overlay { scrollStrategies: ScrollStrategyOptions; constructor( - scrollStrategies: ScrollStrategyOptions, _overlayContainer: OverlayContainer, _componentFactoryResolver: ComponentFactoryResolver, _positionBuilder: OverlayPositionBuilder, _keyboardDispatcher: OverlayKeyboardDispatcher, _injector: Injector, _ngZone: NgZone, _document: any, _directionality: Directionality, _location?: Location | undefined); + scrollStrategies: ScrollStrategyOptions, _overlayContainer: OverlayContainer, _componentFactoryResolver: ComponentFactoryResolver, _positionBuilder: OverlayPositionBuilder, _keyboardDispatcher: OverlayKeyboardDispatcher, _injector: Injector, _ngZone: NgZone, _document: any, _directionality: Directionality, _location?: Location | undefined, _outsideClickDispatcher?: OverlayOutsideClickDispatcher | undefined); create(config?: OverlayConfig): OverlayRef; position(): OverlayPositionBuilder; - static ɵfac: i0.ɵɵFactoryDef; + static ɵfac: i0.ɵɵFactoryDef; static ɵprov: i0.ɵɵInjectableDef; } @@ -204,6 +205,7 @@ export declare class OverlayConfig { backdropClass?: string | string[]; direction?: Direction | Directionality; disposeOnNavigation?: boolean; + excludeFromOutsideClick?: HTMLElement[]; hasBackdrop?: boolean; height?: number | string; maxHeight?: number | string; @@ -235,12 +237,10 @@ export declare class OverlayContainer implements OnDestroy { static ɵprov: i0.ɵɵInjectableDef; } -export declare class OverlayKeyboardDispatcher implements OnDestroy { - _attachedOverlays: OverlayReference[]; +export declare class OverlayKeyboardDispatcher extends BaseOverlayDispatcher { constructor(document: any); add(overlayRef: OverlayReference): void; - ngOnDestroy(): void; - remove(overlayRef: OverlayReference): void; + protected detach(): void; static ɵfac: i0.ɵɵFactoryDef; static ɵprov: i0.ɵɵInjectableDef; } @@ -250,6 +250,14 @@ export declare class OverlayModule { static ɵmod: i0.ɵɵNgModuleDefWithMeta; } +export declare class OverlayOutsideClickDispatcher extends BaseOverlayDispatcher { + constructor(document: any, _platform: Platform); + add(overlayRef: OverlayReference): void; + protected detach(): void; + static ɵfac: i0.ɵɵFactoryDef; + static ɵprov: i0.ɵɵInjectableDef; +} + export declare class OverlayPositionBuilder { constructor(_viewportRuler: ViewportRuler, _document: any, _platform: Platform, _overlayContainer: OverlayContainer); connectedTo(elementRef: ElementRef, originPos: OriginConnectionPosition, overlayPos: OverlayConnectionPosition): ConnectedPositionStrategy; @@ -261,10 +269,11 @@ export declare class OverlayPositionBuilder { export declare class OverlayRef implements PortalOutlet, OverlayReference { _keydownEvents: Subject; + _outsidePointerEvents: Subject; get backdropElement(): HTMLElement | null; get hostElement(): HTMLElement; get overlayElement(): HTMLElement; - constructor(_portalOutlet: PortalOutlet, _host: HTMLElement, _pane: HTMLElement, _config: ImmutableObject, _ngZone: NgZone, _keyboardDispatcher: OverlayKeyboardDispatcher, _document: Document, _location?: Location | undefined); + constructor(_portalOutlet: PortalOutlet, _host: HTMLElement, _pane: HTMLElement, _config: ImmutableObject, _ngZone: NgZone, _keyboardDispatcher: OverlayKeyboardDispatcher, _document: Document, _location?: Location | undefined, _outsideClickDispatcher?: OverlayOutsideClickDispatcher | undefined); addPanelClass(classes: string | string[]): void; attach(portal: ComponentPortal): ComponentRef; attach(portal: TemplatePortal): EmbeddedViewRef; @@ -279,6 +288,7 @@ export declare class OverlayRef implements PortalOutlet, OverlayReference { getDirection(): Direction; hasAttached(): boolean; keydownEvents(): Observable; + outsidePointerEvents(): Observable; removePanelClass(classes: string | string[]): void; setDirection(dir: Direction | Directionality): void; updatePosition(): void;