diff --git a/src/cdk/overlay/dispatchers/overlay-outside-click-dispatcher.spec.ts b/src/cdk/overlay/dispatchers/overlay-outside-click-dispatcher.spec.ts index 1d571d68fbbb..34e3e808cf20 100644 --- a/src/cdk/overlay/dispatchers/overlay-outside-click-dispatcher.spec.ts +++ b/src/cdk/overlay/dispatchers/overlay-outside-click-dispatcher.spec.ts @@ -180,6 +180,79 @@ describe('OverlayOutsideClickDispatcher', () => { overlayRef.dispose(); }); + it('should dispatch an event when a click is started outside the overlay and ' + + 'released outside of it', () => { + const portal = new ComponentPortal(TestComponent); + const overlayRef = overlay.create(); + overlayRef.attach(portal); + const context = document.createElement('div'); + document.body.appendChild(context); + + const spy = jasmine.createSpy('overlay mouse click event spy'); + overlayRef.outsidePointerEvents().subscribe(spy); + + dispatchMouseEvent(context, 'pointerdown'); + context.click(); + expect(spy).toHaveBeenCalled(); + + context.remove(); + overlayRef.dispose(); + }); + + it('should not dispatch an event when a click is started inside the overlay and ' + + 'released inside of it', () => { + const portal = new ComponentPortal(TestComponent); + const overlayRef = overlay.create(); + overlayRef.attach(portal); + + const spy = jasmine.createSpy('overlay mouse click event spy'); + overlayRef.outsidePointerEvents().subscribe(spy); + + dispatchMouseEvent(overlayRef.overlayElement, 'pointerdown'); + overlayRef.overlayElement.click(); + expect(spy).not.toHaveBeenCalled(); + + overlayRef.dispose(); + }); + + it('should not dispatch an event when a click is started inside the overlay and ' + + 'released outside of it', () => { + const portal = new ComponentPortal(TestComponent); + const overlayRef = overlay.create(); + overlayRef.attach(portal); + const context = document.createElement('div'); + document.body.appendChild(context); + + const spy = jasmine.createSpy('overlay mouse click event spy'); + overlayRef.outsidePointerEvents().subscribe(spy); + + dispatchMouseEvent(overlayRef.overlayElement, 'pointerdown'); + context.click(); + expect(spy).not.toHaveBeenCalled(); + + context.remove(); + overlayRef.dispose(); + }); + + it('should not dispatch an event when a click is started outside the overlay and ' + + 'released inside of it', () => { + const portal = new ComponentPortal(TestComponent); + const overlayRef = overlay.create(); + overlayRef.attach(portal); + const context = document.createElement('div'); + document.body.appendChild(context); + + const spy = jasmine.createSpy('overlay mouse click event spy'); + overlayRef.outsidePointerEvents().subscribe(spy); + + dispatchMouseEvent(context, 'pointerdown'); + overlayRef.overlayElement.click(); + expect(spy).not.toHaveBeenCalled(); + + context.remove(); + overlayRef.dispose(); + }); + it('should dispatch an event when a context menu is triggered outside the overlay', () => { const portal = new ComponentPortal(TestComponent); const overlayRef = overlay.create(); diff --git a/src/cdk/overlay/dispatchers/overlay-outside-click-dispatcher.ts b/src/cdk/overlay/dispatchers/overlay-outside-click-dispatcher.ts index 38f06362543f..b2a4217a2844 100644 --- a/src/cdk/overlay/dispatchers/overlay-outside-click-dispatcher.ts +++ b/src/cdk/overlay/dispatchers/overlay-outside-click-dispatcher.ts @@ -21,6 +21,7 @@ import {BaseOverlayDispatcher} from './base-overlay-dispatcher'; export class OverlayOutsideClickDispatcher extends BaseOverlayDispatcher { private _cursorOriginalValue: string; private _cursorStyleIsSet = false; + private _pointerDownEventTarget: EventTarget | null; constructor(@Inject(DOCUMENT) document: any, private _platform: Platform) { super(document); @@ -38,6 +39,7 @@ export class OverlayOutsideClickDispatcher extends BaseOverlayDispatcher { // https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/HandlingEvents/HandlingEvents.html if (!this._isAttached) { const body = this._document.body; + body.addEventListener('pointerdown', this._pointerDownListener, true); body.addEventListener('click', this._clickListener, true); body.addEventListener('auxclick', this._clickListener, true); body.addEventListener('contextmenu', this._clickListener, true); @@ -58,6 +60,7 @@ export class OverlayOutsideClickDispatcher extends BaseOverlayDispatcher { protected detach() { if (this._isAttached) { const body = this._document.body; + body.removeEventListener('pointerdown', this._pointerDownListener, true); body.removeEventListener('click', this._clickListener, true); body.removeEventListener('auxclick', this._clickListener, true); body.removeEventListener('contextmenu', this._clickListener, true); @@ -69,9 +72,26 @@ export class OverlayOutsideClickDispatcher extends BaseOverlayDispatcher { } } + /** Store pointerdown event target to track origin of click. */ + private _pointerDownListener = (event: PointerEvent) => { + this._pointerDownEventTarget = _getEventTarget(event); + } + /** Click event listener that will be attached to the body propagate phase. */ private _clickListener = (event: MouseEvent) => { const target = _getEventTarget(event); + // In case of a click event, we want to check the origin of the click + // (e.g. in case where a user starts a click inside the overlay and + // releases the click outside of it). + // This is done by using the event target of the preceding pointerdown event. + // Every click event caused by a pointer device has a preceding pointerdown + // event, unless the click was programmatically triggered (e.g. in a unit test). + const origin = event.type === 'click' && this._pointerDownEventTarget + ? this._pointerDownEventTarget : target; + // Reset the stored pointerdown event target, to avoid having it interfere + // in subsequent events. + this._pointerDownEventTarget = null; + // We copy the array because the original may be modified asynchronously if the // outsidePointerEvents listener decides to detach overlays resulting in index errors inside // the for loop. @@ -88,8 +108,10 @@ export class OverlayOutsideClickDispatcher extends BaseOverlayDispatcher { } // If it's a click inside the overlay, just break - we should do nothing - // If it's an outside click dispatch the mouse event, and proceed with the next overlay - if (overlayRef.overlayElement.contains(target as Node)) { + // If it's an outside click (both origin and target of the click) dispatch the mouse event, + // and proceed with the next overlay + if (overlayRef.overlayElement.contains(target as Node) || + overlayRef.overlayElement.contains(origin as Node)) { break; }