Skip to content

Commit 5402a8b

Browse files
kyubisationandrewseguin
authored andcommitted
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 (cherry picked from commit 335a798)
1 parent 18fcc45 commit 5402a8b

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
@@ -184,6 +184,79 @@ describe('OverlayOutsideClickDispatcher', () => {
184184
overlayRef.dispose();
185185
});
186186

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