Skip to content

fix(cdk/overlay): OverlayRef.outsidePointerEvents() should only emit … #23679

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
26 changes: 24 additions & 2 deletions src/cdk/overlay/dispatchers/overlay-outside-click-dispatcher.ts
Original file line number Diff line number Diff line change
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