Skip to content

Commit 335a798

Browse files
authored
fix(cdk/overlay): OverlayRef.outsidePointerEvents() should only emit 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
1 parent 9109c3c commit 335a798

File tree

2 files changed

+97
-2
lines changed

2 files changed

+97
-2
lines changed

src/cdk/overlay/dispatchers/overlay-outside-click-dispatcher.spec.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,79 @@ describe('OverlayOutsideClickDispatcher', () => {
186186
overlayRef.dispose();
187187
});
188188

189+
it('should dispatch an event when a click is started outside the overlay and ' +
190+
'released outside of it', () => {
191+
const portal = new ComponentPortal(TestComponent);
192+
const overlayRef = overlay.create();
193+
overlayRef.attach(portal);
194+
const context = document.createElement('div');
195+
document.body.appendChild(context);
196+
197+
const spy = jasmine.createSpy('overlay mouse click event spy');
198+
overlayRef.outsidePointerEvents().subscribe(spy);
199+
200+
dispatchMouseEvent(context, 'pointerdown');
201+
context.click();
202+
expect(spy).toHaveBeenCalled();
203+
204+
context.remove();
205+
overlayRef.dispose();
206+
});
207+
208+
it('should not dispatch an event when a click is started inside the overlay and ' +
209+
'released inside of it', () => {
210+
const portal = new ComponentPortal(TestComponent);
211+
const overlayRef = overlay.create();
212+
overlayRef.attach(portal);
213+
214+
const spy = jasmine.createSpy('overlay mouse click event spy');
215+
overlayRef.outsidePointerEvents().subscribe(spy);
216+
217+
dispatchMouseEvent(overlayRef.overlayElement, 'pointerdown');
218+
overlayRef.overlayElement.click();
219+
expect(spy).not.toHaveBeenCalled();
220+
221+
overlayRef.dispose();
222+
});
223+
224+
it('should not dispatch an event when a click is started inside the overlay and ' +
225+
'released outside of it', () => {
226+
const portal = new ComponentPortal(TestComponent);
227+
const overlayRef = overlay.create();
228+
overlayRef.attach(portal);
229+
const context = document.createElement('div');
230+
document.body.appendChild(context);
231+
232+
const spy = jasmine.createSpy('overlay mouse click event spy');
233+
overlayRef.outsidePointerEvents().subscribe(spy);
234+
235+
dispatchMouseEvent(overlayRef.overlayElement, 'pointerdown');
236+
context.click();
237+
expect(spy).not.toHaveBeenCalled();
238+
239+
context.remove();
240+
overlayRef.dispose();
241+
});
242+
243+
it('should not dispatch an event when a click is started outside the overlay and ' +
244+
'released inside of it', () => {
245+
const portal = new ComponentPortal(TestComponent);
246+
const overlayRef = overlay.create();
247+
overlayRef.attach(portal);
248+
const context = document.createElement('div');
249+
document.body.appendChild(context);
250+
251+
const spy = jasmine.createSpy('overlay mouse click event spy');
252+
overlayRef.outsidePointerEvents().subscribe(spy);
253+
254+
dispatchMouseEvent(context, 'pointerdown');
255+
overlayRef.overlayElement.click();
256+
expect(spy).not.toHaveBeenCalled();
257+
258+
context.remove();
259+
overlayRef.dispose();
260+
});
261+
189262
it('should dispatch an event when a context menu is triggered outside the overlay', () => {
190263
const portal = new ComponentPortal(TestComponent);
191264
const overlayRef = overlay.create();

src/cdk/overlay/dispatchers/overlay-outside-click-dispatcher.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {BaseOverlayDispatcher} from './base-overlay-dispatcher';
2121
export class OverlayOutsideClickDispatcher extends BaseOverlayDispatcher {
2222
private _cursorOriginalValue: string;
2323
private _cursorStyleIsSet = false;
24+
private _pointerDownEventTarget: EventTarget | null;
2425

2526
constructor(@Inject(DOCUMENT) document: any, private _platform: Platform) {
2627
super(document);
@@ -38,6 +39,7 @@ export class OverlayOutsideClickDispatcher extends BaseOverlayDispatcher {
3839
// https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/HandlingEvents/HandlingEvents.html
3940
if (!this._isAttached) {
4041
const body = this._document.body;
42+
body.addEventListener('pointerdown', this._pointerDownListener, true);
4143
body.addEventListener('click', this._clickListener, true);
4244
body.addEventListener('auxclick', this._clickListener, true);
4345
body.addEventListener('contextmenu', this._clickListener, true);
@@ -58,6 +60,7 @@ export class OverlayOutsideClickDispatcher extends BaseOverlayDispatcher {
5860
protected detach() {
5961
if (this._isAttached) {
6062
const body = this._document.body;
63+
body.removeEventListener('pointerdown', this._pointerDownListener, true);
6164
body.removeEventListener('click', this._clickListener, true);
6265
body.removeEventListener('auxclick', this._clickListener, true);
6366
body.removeEventListener('contextmenu', this._clickListener, true);
@@ -69,9 +72,26 @@ export class OverlayOutsideClickDispatcher extends BaseOverlayDispatcher {
6972
}
7073
}
7174

75+
/** Store pointerdown event target to track origin of click. */
76+
private _pointerDownListener = (event: PointerEvent) => {
77+
this._pointerDownEventTarget = _getEventTarget(event);
78+
}
79+
7280
/** Click event listener that will be attached to the body propagate phase. */
7381
private _clickListener = (event: MouseEvent) => {
7482
const target = _getEventTarget(event);
83+
// In case of a click event, we want to check the origin of the click
84+
// (e.g. in case where a user starts a click inside the overlay and
85+
// releases the click outside of it).
86+
// This is done by using the event target of the preceding pointerdown event.
87+
// Every click event caused by a pointer device has a preceding pointerdown
88+
// event, unless the click was programmatically triggered (e.g. in a unit test).
89+
const origin = event.type === 'click' && this._pointerDownEventTarget
90+
? this._pointerDownEventTarget : target;
91+
// Reset the stored pointerdown event target, to avoid having it interfere
92+
// in subsequent events.
93+
this._pointerDownEventTarget = null;
94+
7595
// We copy the array because the original may be modified asynchronously if the
7696
// outsidePointerEvents listener decides to detach overlays resulting in index errors inside
7797
// the for loop.
@@ -88,8 +108,10 @@ export class OverlayOutsideClickDispatcher extends BaseOverlayDispatcher {
88108
}
89109

90110
// If it's a click inside the overlay, just break - we should do nothing
91-
// If it's an outside click dispatch the mouse event, and proceed with the next overlay
92-
if (overlayRef.overlayElement.contains(target as Node)) {
111+
// If it's an outside click (both origin and target of the click) dispatch the mouse event,
112+
// and proceed with the next overlay
113+
if (overlayRef.overlayElement.contains(target as Node) ||
114+
overlayRef.overlayElement.contains(origin as Node)) {
93115
break;
94116
}
95117

0 commit comments

Comments
 (0)