Skip to content

Commit

Permalink
fix(cdk/overlay): OverlayRef.outsidePointerEvents() should only emit …
Browse files Browse the repository at this point in the history
…due to pointerdown outside overlay (#23679)

Currently OverlayRef.outsidePointerEvents() emits when a user starts a click inside the overlay,
drags the cursor outside the overlay and releases the click (e.g. selecting text and moving the
mouse outside the overlay). In order to only emit when the click originates outside the overlay,
we track the target of the preceding pointerdown event and check if it originated from outside
the overlay.

Fixes #23643

(cherry picked from commit 335a798)
  • Loading branch information
kyubisation authored and andrewseguin committed Oct 19, 2021
1 parent a4302fd commit 25ba758
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 2 deletions.
Expand Up @@ -186,6 +186,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();
Expand Down
26 changes: 24 additions & 2 deletions src/cdk/overlay/dispatchers/overlay-outside-click-dispatcher.ts
Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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.
Expand All @@ -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;
}

Expand Down

0 comments on commit 25ba758

Please sign in to comment.