Skip to content

Commit 3d2aefb

Browse files
authored
perf(cdk/overlay): add event listeners for overlay dispatchers outside of zone (#24408)
1 parent 7480e3b commit 3d2aefb

File tree

5 files changed

+149
-23
lines changed

5 files changed

+149
-23
lines changed

src/cdk/overlay/dispatchers/overlay-keyboard-dispatcher.spec.ts

+25-5
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import {TestBed, inject} from '@angular/core/testing';
22
import {dispatchKeyboardEvent} from '../../testing/private';
33
import {ESCAPE} from '@angular/cdk/keycodes';
4-
import {Component} from '@angular/core';
4+
import {ApplicationRef, Component} from '@angular/core';
55
import {OverlayModule, Overlay} from '../index';
66
import {OverlayKeyboardDispatcher} from './overlay-keyboard-dispatcher';
77
import {ComponentPortal} from '@angular/cdk/portal';
88

99
describe('OverlayKeyboardDispatcher', () => {
10+
let appRef: ApplicationRef;
1011
let keyboardDispatcher: OverlayKeyboardDispatcher;
1112
let overlay: Overlay;
1213

@@ -16,10 +17,14 @@ describe('OverlayKeyboardDispatcher', () => {
1617
declarations: [TestComponent],
1718
});
1819

19-
inject([OverlayKeyboardDispatcher, Overlay], (kbd: OverlayKeyboardDispatcher, o: Overlay) => {
20-
keyboardDispatcher = kbd;
21-
overlay = o;
22-
})();
20+
inject(
21+
[ApplicationRef, OverlayKeyboardDispatcher, Overlay],
22+
(ar: ApplicationRef, kbd: OverlayKeyboardDispatcher, o: Overlay) => {
23+
appRef = ar;
24+
keyboardDispatcher = kbd;
25+
overlay = o;
26+
},
27+
)();
2328
});
2429

2530
it('should track overlays in order as they are attached and detached', () => {
@@ -179,6 +184,21 @@ describe('OverlayKeyboardDispatcher', () => {
179184
expect(overlayTwoSpy).not.toHaveBeenCalled();
180185
expect(overlayOneSpy).toHaveBeenCalled();
181186
});
187+
188+
it('should not run change detection if there are no `keydownEvents` observers', () => {
189+
spyOn(appRef, 'tick');
190+
const overlayRef = overlay.create();
191+
keyboardDispatcher.add(overlayRef);
192+
193+
expect(appRef.tick).toHaveBeenCalledTimes(0);
194+
dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);
195+
expect(appRef.tick).toHaveBeenCalledTimes(0);
196+
197+
overlayRef.keydownEvents().subscribe();
198+
dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);
199+
200+
expect(appRef.tick).toHaveBeenCalledTimes(1);
201+
});
182202
});
183203

184204
@Component({

src/cdk/overlay/dispatchers/overlay-keyboard-dispatcher.ts

+21-4
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import {DOCUMENT} from '@angular/common';
10-
import {Inject, Injectable} from '@angular/core';
10+
import {Inject, Injectable, NgZone, Optional} from '@angular/core';
1111
import {OverlayReference} from '../overlay-reference';
1212
import {BaseOverlayDispatcher} from './base-overlay-dispatcher';
1313

@@ -18,7 +18,11 @@ import {BaseOverlayDispatcher} from './base-overlay-dispatcher';
1818
*/
1919
@Injectable({providedIn: 'root'})
2020
export class OverlayKeyboardDispatcher extends BaseOverlayDispatcher {
21-
constructor(@Inject(DOCUMENT) document: any) {
21+
constructor(
22+
@Inject(DOCUMENT) document: any,
23+
/** @breaking-change 14.0.0 _ngZone will be required. */
24+
@Optional() private _ngZone?: NgZone,
25+
) {
2226
super(document);
2327
}
2428

@@ -28,7 +32,14 @@ export class OverlayKeyboardDispatcher extends BaseOverlayDispatcher {
2832

2933
// Lazily start dispatcher once first overlay is added
3034
if (!this._isAttached) {
31-
this._document.body.addEventListener('keydown', this._keydownListener);
35+
/** @breaking-change 14.0.0 _ngZone will be required. */
36+
if (this._ngZone) {
37+
this._ngZone.runOutsideAngular(() =>
38+
this._document.body.addEventListener('keydown', this._keydownListener),
39+
);
40+
} else {
41+
this._document.body.addEventListener('keydown', this._keydownListener);
42+
}
3243
this._isAttached = true;
3344
}
3445
}
@@ -53,7 +64,13 @@ export class OverlayKeyboardDispatcher extends BaseOverlayDispatcher {
5364
// because we don't want overlays that don't handle keyboard events to block the ones below
5465
// them that do.
5566
if (overlays[i]._keydownEvents.observers.length > 0) {
56-
overlays[i]._keydownEvents.next(event);
67+
const keydownEvents = overlays[i]._keydownEvents;
68+
/** @breaking-change 14.0.0 _ngZone will be required. */
69+
if (this._ngZone) {
70+
this._ngZone.run(() => keydownEvents.next(event));
71+
} else {
72+
keydownEvents.next(event);
73+
}
5774
break;
5875
}
5976
}

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

+69-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import {TestBed, inject, fakeAsync} from '@angular/core/testing';
2-
import {Component} from '@angular/core';
2+
import {ApplicationRef, Component} from '@angular/core';
33
import {dispatchFakeEvent, dispatchMouseEvent} from '../../testing/private';
44
import {OverlayModule, Overlay} from '../index';
55
import {OverlayOutsideClickDispatcher} from './overlay-outside-click-dispatcher';
66
import {ComponentPortal} from '@angular/cdk/portal';
77

88
describe('OverlayOutsideClickDispatcher', () => {
9+
let appRef: ApplicationRef;
910
let outsideClickDispatcher: OverlayOutsideClickDispatcher;
1011
let overlay: Overlay;
1112

@@ -16,8 +17,9 @@ describe('OverlayOutsideClickDispatcher', () => {
1617
});
1718

1819
inject(
19-
[OverlayOutsideClickDispatcher, Overlay],
20-
(ocd: OverlayOutsideClickDispatcher, o: Overlay) => {
20+
[ApplicationRef, OverlayOutsideClickDispatcher, Overlay],
21+
(ar: ApplicationRef, ocd: OverlayOutsideClickDispatcher, o: Overlay) => {
22+
appRef = ar;
2123
outsideClickDispatcher = ocd;
2224
overlay = o;
2325
},
@@ -336,6 +338,70 @@ describe('OverlayOutsideClickDispatcher', () => {
336338
thirdOverlayRef.dispose();
337339
}),
338340
);
341+
342+
describe('change detection behavior', () => {
343+
it('should not run change detection if there is no portal attached to the overlay', () => {
344+
spyOn(appRef, 'tick');
345+
const overlayRef = overlay.create();
346+
outsideClickDispatcher.add(overlayRef);
347+
348+
const context = document.createElement('div');
349+
document.body.appendChild(context);
350+
351+
overlayRef.outsidePointerEvents().subscribe();
352+
dispatchMouseEvent(context, 'click');
353+
354+
expect(appRef.tick).toHaveBeenCalledTimes(0);
355+
});
356+
357+
it('should not run change detection if the click was made outside the overlay but there are no `outsidePointerEvents` observers', () => {
358+
spyOn(appRef, 'tick');
359+
const portal = new ComponentPortal(TestComponent);
360+
const overlayRef = overlay.create();
361+
overlayRef.attach(portal);
362+
outsideClickDispatcher.add(overlayRef);
363+
364+
const context = document.createElement('div');
365+
document.body.appendChild(context);
366+
367+
dispatchMouseEvent(context, 'click');
368+
369+
expect(appRef.tick).toHaveBeenCalledTimes(0);
370+
});
371+
372+
it('should not run change detection if the click was made inside the overlay and there are `outsidePointerEvents` observers', () => {
373+
spyOn(appRef, 'tick');
374+
const portal = new ComponentPortal(TestComponent);
375+
const overlayRef = overlay.create();
376+
overlayRef.attach(portal);
377+
outsideClickDispatcher.add(overlayRef);
378+
379+
overlayRef.outsidePointerEvents().subscribe();
380+
dispatchMouseEvent(overlayRef.overlayElement, 'click');
381+
382+
expect(appRef.tick).toHaveBeenCalledTimes(0);
383+
});
384+
385+
it('should run change detection if the click was made outside the overlay and there are `outsidePointerEvents` observers', () => {
386+
spyOn(appRef, 'tick');
387+
const portal = new ComponentPortal(TestComponent);
388+
const overlayRef = overlay.create();
389+
overlayRef.attach(portal);
390+
outsideClickDispatcher.add(overlayRef);
391+
392+
const context = document.createElement('div');
393+
document.body.appendChild(context);
394+
395+
expect(appRef.tick).toHaveBeenCalledTimes(0);
396+
dispatchMouseEvent(context, 'click');
397+
expect(appRef.tick).toHaveBeenCalledTimes(0);
398+
399+
overlayRef.outsidePointerEvents().subscribe();
400+
401+
dispatchMouseEvent(context, 'click');
402+
expect(appRef.tick).toHaveBeenCalledTimes(1);
403+
});
404+
});
339405
});
340406

341407
@Component({

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

+28-7
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import {DOCUMENT} from '@angular/common';
10-
import {Inject, Injectable} from '@angular/core';
10+
import {Inject, Injectable, NgZone, Optional} from '@angular/core';
1111
import {OverlayReference} from '../overlay-reference';
1212
import {Platform, _getEventTarget} from '@angular/cdk/platform';
1313
import {BaseOverlayDispatcher} from './base-overlay-dispatcher';
@@ -23,7 +23,12 @@ export class OverlayOutsideClickDispatcher extends BaseOverlayDispatcher {
2323
private _cursorStyleIsSet = false;
2424
private _pointerDownEventTarget: EventTarget | null;
2525

26-
constructor(@Inject(DOCUMENT) document: any, private _platform: Platform) {
26+
constructor(
27+
@Inject(DOCUMENT) document: any,
28+
private _platform: Platform,
29+
/** @breaking-change 14.0.0 _ngZone will be required. */
30+
@Optional() private _ngZone?: NgZone,
31+
) {
2732
super(document);
2833
}
2934

@@ -39,10 +44,13 @@ export class OverlayOutsideClickDispatcher extends BaseOverlayDispatcher {
3944
// https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/HandlingEvents/HandlingEvents.html
4045
if (!this._isAttached) {
4146
const body = this._document.body;
42-
body.addEventListener('pointerdown', this._pointerDownListener, true);
43-
body.addEventListener('click', this._clickListener, true);
44-
body.addEventListener('auxclick', this._clickListener, true);
45-
body.addEventListener('contextmenu', this._clickListener, true);
47+
48+
/** @breaking-change 14.0.0 _ngZone will be required. */
49+
if (this._ngZone) {
50+
this._ngZone.runOutsideAngular(() => this._addEventListeners(body));
51+
} else {
52+
this._addEventListeners(body);
53+
}
4654

4755
// click event is not fired on iOS. To make element "clickable" we are
4856
// setting the cursor to pointer
@@ -72,6 +80,13 @@ export class OverlayOutsideClickDispatcher extends BaseOverlayDispatcher {
7280
}
7381
}
7482

83+
private _addEventListeners(body: HTMLElement): void {
84+
body.addEventListener('pointerdown', this._pointerDownListener, true);
85+
body.addEventListener('click', this._clickListener, true);
86+
body.addEventListener('auxclick', this._clickListener, true);
87+
body.addEventListener('contextmenu', this._clickListener, true);
88+
}
89+
7590
/** Store pointerdown event target to track origin of click. */
7691
private _pointerDownListener = (event: PointerEvent) => {
7792
this._pointerDownEventTarget = _getEventTarget(event);
@@ -119,7 +134,13 @@ export class OverlayOutsideClickDispatcher extends BaseOverlayDispatcher {
119134
break;
120135
}
121136

122-
overlayRef._outsidePointerEvents.next(event);
137+
const outsidePointerEvents = overlayRef._outsidePointerEvents;
138+
/** @breaking-change 14.0.0 _ngZone will be required. */
139+
if (this._ngZone) {
140+
this._ngZone.run(() => outsidePointerEvents.next(event));
141+
} else {
142+
outsidePointerEvents.next(event);
143+
}
123144
}
124145
};
125146
}

tools/public_api_guard/cdk/overlay.md

+6-4
Original file line numberDiff line numberDiff line change
@@ -317,11 +317,12 @@ export class OverlayContainer implements OnDestroy {
317317

318318
// @public
319319
export class OverlayKeyboardDispatcher extends BaseOverlayDispatcher {
320-
constructor(document: any);
320+
constructor(document: any,
321+
_ngZone?: NgZone | undefined);
321322
add(overlayRef: OverlayReference): void;
322323
protected detach(): void;
323324
// (undocumented)
324-
static ɵfac: i0.ɵɵFactoryDeclaration<OverlayKeyboardDispatcher, never>;
325+
static ɵfac: i0.ɵɵFactoryDeclaration<OverlayKeyboardDispatcher, [null, { optional: true; }]>;
325326
// (undocumented)
326327
static ɵprov: i0.ɵɵInjectableDeclaration<OverlayKeyboardDispatcher>;
327328
}
@@ -338,11 +339,12 @@ export class OverlayModule {
338339

339340
// @public
340341
export class OverlayOutsideClickDispatcher extends BaseOverlayDispatcher {
341-
constructor(document: any, _platform: Platform);
342+
constructor(document: any, _platform: Platform,
343+
_ngZone?: NgZone | undefined);
342344
add(overlayRef: OverlayReference): void;
343345
protected detach(): void;
344346
// (undocumented)
345-
static ɵfac: i0.ɵɵFactoryDeclaration<OverlayOutsideClickDispatcher, never>;
347+
static ɵfac: i0.ɵɵFactoryDeclaration<OverlayOutsideClickDispatcher, [null, null, { optional: true; }]>;
346348
// (undocumented)
347349
static ɵprov: i0.ɵɵInjectableDeclaration<OverlayOutsideClickDispatcher>;
348350
}

0 commit comments

Comments
 (0)