Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 3d2aefb

Browse files
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

Lines changed: 25 additions & 5 deletions
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

Lines changed: 21 additions & 4 deletions
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

Lines changed: 69 additions & 3 deletions
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

Lines changed: 28 additions & 7 deletions
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

Lines changed: 6 additions & 4 deletions
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)

AltStyle によって変換されたページ (->オリジナル) /