Skip to content

Commit

Permalink
perf(cdk/overlay): add event listeners for overlay dispatchers outsid…
Browse files Browse the repository at this point in the history
…e of zone (#24408)
  • Loading branch information
arturovt authored Feb 24, 2022
1 parent 7480e3b commit 3d2aefb
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 23 deletions.
30 changes: 25 additions & 5 deletions src/cdk/overlay/dispatchers/overlay-keyboard-dispatcher.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import {TestBed, inject} from '@angular/core/testing';
import {dispatchKeyboardEvent} from '../../testing/private';
import {ESCAPE} from '@angular/cdk/keycodes';
import {Component} from '@angular/core';
import {ApplicationRef, Component} from '@angular/core';
import {OverlayModule, Overlay} from '../index';
import {OverlayKeyboardDispatcher} from './overlay-keyboard-dispatcher';
import {ComponentPortal} from '@angular/cdk/portal';

describe('OverlayKeyboardDispatcher', () => {
let appRef: ApplicationRef;
let keyboardDispatcher: OverlayKeyboardDispatcher;
let overlay: Overlay;

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

inject([OverlayKeyboardDispatcher, Overlay], (kbd: OverlayKeyboardDispatcher, o: Overlay) => {
keyboardDispatcher = kbd;
overlay = o;
})();
inject(
[ApplicationRef, OverlayKeyboardDispatcher, Overlay],
(ar: ApplicationRef, kbd: OverlayKeyboardDispatcher, o: Overlay) => {
appRef = ar;
keyboardDispatcher = kbd;
overlay = o;
},
)();
});

it('should track overlays in order as they are attached and detached', () => {
Expand Down Expand Up @@ -179,6 +184,21 @@ describe('OverlayKeyboardDispatcher', () => {
expect(overlayTwoSpy).not.toHaveBeenCalled();
expect(overlayOneSpy).toHaveBeenCalled();
});

it('should not run change detection if there are no `keydownEvents` observers', () => {
spyOn(appRef, 'tick');
const overlayRef = overlay.create();
keyboardDispatcher.add(overlayRef);

expect(appRef.tick).toHaveBeenCalledTimes(0);
dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);
expect(appRef.tick).toHaveBeenCalledTimes(0);

overlayRef.keydownEvents().subscribe();
dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);

expect(appRef.tick).toHaveBeenCalledTimes(1);
});
});

@Component({
Expand Down
25 changes: 21 additions & 4 deletions src/cdk/overlay/dispatchers/overlay-keyboard-dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

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

Expand All @@ -18,7 +18,11 @@ import {BaseOverlayDispatcher} from './base-overlay-dispatcher';
*/
@Injectable({providedIn: 'root'})
export class OverlayKeyboardDispatcher extends BaseOverlayDispatcher {
constructor(@Inject(DOCUMENT) document: any) {
constructor(
@Inject(DOCUMENT) document: any,
/** @breaking-change 14.0.0 _ngZone will be required. */
@Optional() private _ngZone?: NgZone,
) {
super(document);
}

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

// Lazily start dispatcher once first overlay is added
if (!this._isAttached) {
this._document.body.addEventListener('keydown', this._keydownListener);
/** @breaking-change 14.0.0 _ngZone will be required. */
if (this._ngZone) {
this._ngZone.runOutsideAngular(() =>
this._document.body.addEventListener('keydown', this._keydownListener),
);
} else {
this._document.body.addEventListener('keydown', this._keydownListener);
}
this._isAttached = true;
}
}
Expand All @@ -53,7 +64,13 @@ export class OverlayKeyboardDispatcher extends BaseOverlayDispatcher {
// because we don't want overlays that don't handle keyboard events to block the ones below
// them that do.
if (overlays[i]._keydownEvents.observers.length > 0) {
overlays[i]._keydownEvents.next(event);
const keydownEvents = overlays[i]._keydownEvents;
/** @breaking-change 14.0.0 _ngZone will be required. */
if (this._ngZone) {
this._ngZone.run(() => keydownEvents.next(event));
} else {
keydownEvents.next(event);
}
break;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import {TestBed, inject, fakeAsync} from '@angular/core/testing';
import {Component} from '@angular/core';
import {ApplicationRef, Component} from '@angular/core';
import {dispatchFakeEvent, dispatchMouseEvent} from '../../testing/private';
import {OverlayModule, Overlay} from '../index';
import {OverlayOutsideClickDispatcher} from './overlay-outside-click-dispatcher';
import {ComponentPortal} from '@angular/cdk/portal';

describe('OverlayOutsideClickDispatcher', () => {
let appRef: ApplicationRef;
let outsideClickDispatcher: OverlayOutsideClickDispatcher;
let overlay: Overlay;

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

inject(
[OverlayOutsideClickDispatcher, Overlay],
(ocd: OverlayOutsideClickDispatcher, o: Overlay) => {
[ApplicationRef, OverlayOutsideClickDispatcher, Overlay],
(ar: ApplicationRef, ocd: OverlayOutsideClickDispatcher, o: Overlay) => {
appRef = ar;
outsideClickDispatcher = ocd;
overlay = o;
},
Expand Down Expand Up @@ -336,6 +338,70 @@ describe('OverlayOutsideClickDispatcher', () => {
thirdOverlayRef.dispose();
}),
);

describe('change detection behavior', () => {
it('should not run change detection if there is no portal attached to the overlay', () => {
spyOn(appRef, 'tick');
const overlayRef = overlay.create();
outsideClickDispatcher.add(overlayRef);

const context = document.createElement('div');
document.body.appendChild(context);

overlayRef.outsidePointerEvents().subscribe();
dispatchMouseEvent(context, 'click');

expect(appRef.tick).toHaveBeenCalledTimes(0);
});

it('should not run change detection if the click was made outside the overlay but there are no `outsidePointerEvents` observers', () => {
spyOn(appRef, 'tick');
const portal = new ComponentPortal(TestComponent);
const overlayRef = overlay.create();
overlayRef.attach(portal);
outsideClickDispatcher.add(overlayRef);

const context = document.createElement('div');
document.body.appendChild(context);

dispatchMouseEvent(context, 'click');

expect(appRef.tick).toHaveBeenCalledTimes(0);
});

it('should not run change detection if the click was made inside the overlay and there are `outsidePointerEvents` observers', () => {
spyOn(appRef, 'tick');
const portal = new ComponentPortal(TestComponent);
const overlayRef = overlay.create();
overlayRef.attach(portal);
outsideClickDispatcher.add(overlayRef);

overlayRef.outsidePointerEvents().subscribe();
dispatchMouseEvent(overlayRef.overlayElement, 'click');

expect(appRef.tick).toHaveBeenCalledTimes(0);
});

it('should run change detection if the click was made outside the overlay and there are `outsidePointerEvents` observers', () => {
spyOn(appRef, 'tick');
const portal = new ComponentPortal(TestComponent);
const overlayRef = overlay.create();
overlayRef.attach(portal);
outsideClickDispatcher.add(overlayRef);

const context = document.createElement('div');
document.body.appendChild(context);

expect(appRef.tick).toHaveBeenCalledTimes(0);
dispatchMouseEvent(context, 'click');
expect(appRef.tick).toHaveBeenCalledTimes(0);

overlayRef.outsidePointerEvents().subscribe();

dispatchMouseEvent(context, 'click');
expect(appRef.tick).toHaveBeenCalledTimes(1);
});
});
});

@Component({
Expand Down
35 changes: 28 additions & 7 deletions src/cdk/overlay/dispatchers/overlay-outside-click-dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

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

constructor(@Inject(DOCUMENT) document: any, private _platform: Platform) {
constructor(
@Inject(DOCUMENT) document: any,
private _platform: Platform,
/** @breaking-change 14.0.0 _ngZone will be required. */
@Optional() private _ngZone?: NgZone,
) {
super(document);
}

Expand All @@ -39,10 +44,13 @@ 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);

/** @breaking-change 14.0.0 _ngZone will be required. */
if (this._ngZone) {
this._ngZone.runOutsideAngular(() => this._addEventListeners(body));
} else {
this._addEventListeners(body);
}

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

private _addEventListeners(body: HTMLElement): void {
body.addEventListener('pointerdown', this._pointerDownListener, true);
body.addEventListener('click', this._clickListener, true);
body.addEventListener('auxclick', this._clickListener, true);
body.addEventListener('contextmenu', this._clickListener, true);
}

/** Store pointerdown event target to track origin of click. */
private _pointerDownListener = (event: PointerEvent) => {
this._pointerDownEventTarget = _getEventTarget(event);
Expand Down Expand Up @@ -119,7 +134,13 @@ export class OverlayOutsideClickDispatcher extends BaseOverlayDispatcher {
break;
}

overlayRef._outsidePointerEvents.next(event);
const outsidePointerEvents = overlayRef._outsidePointerEvents;
/** @breaking-change 14.0.0 _ngZone will be required. */
if (this._ngZone) {
this._ngZone.run(() => outsidePointerEvents.next(event));
} else {
outsidePointerEvents.next(event);
}
}
};
}
10 changes: 6 additions & 4 deletions tools/public_api_guard/cdk/overlay.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,11 +317,12 @@ export class OverlayContainer implements OnDestroy {

// @public
export class OverlayKeyboardDispatcher extends BaseOverlayDispatcher {
constructor(document: any);
constructor(document: any,
_ngZone?: NgZone | undefined);
add(overlayRef: OverlayReference): void;
protected detach(): void;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<OverlayKeyboardDispatcher, never>;
static ɵfac: i0.ɵɵFactoryDeclaration<OverlayKeyboardDispatcher, [null, { optional: true; }]>;
// (undocumented)
static ɵprov: i0.ɵɵInjectableDeclaration<OverlayKeyboardDispatcher>;
}
Expand All @@ -338,11 +339,12 @@ export class OverlayModule {

// @public
export class OverlayOutsideClickDispatcher extends BaseOverlayDispatcher {
constructor(document: any, _platform: Platform);
constructor(document: any, _platform: Platform,
_ngZone?: NgZone | undefined);
add(overlayRef: OverlayReference): void;
protected detach(): void;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<OverlayOutsideClickDispatcher, never>;
static ɵfac: i0.ɵɵFactoryDeclaration<OverlayOutsideClickDispatcher, [null, null, { optional: true; }]>;
// (undocumented)
static ɵprov: i0.ɵɵInjectableDeclaration<OverlayOutsideClickDispatcher>;
}
Expand Down

0 comments on commit 3d2aefb

Please sign in to comment.