From 9e991abbb8877a350a59af2dfed6d384df443b95 Mon Sep 17 00:00:00 2001 From: Michael Leibman Date: Sun, 1 Mar 2020 21:26:41 -0800 Subject: [PATCH 1/4] perf(focus-monitor): optimize event registration Improve focus-monitor scalability by implementing event delegation instead of adding individual 'focus' and 'blur' event listeners to each monitored element. --- src/cdk/a11y/focus-monitor/focus-monitor.ts | 95 +++++++++++---------- 1 file changed, 50 insertions(+), 45 deletions(-) diff --git a/src/cdk/a11y/focus-monitor/focus-monitor.ts b/src/cdk/a11y/focus-monitor/focus-monitor.ts index 41a8c9dc56f2..e35c80814c23 100644 --- a/src/cdk/a11y/focus-monitor/focus-monitor.ts +++ b/src/cdk/a11y/focus-monitor/focus-monitor.ts @@ -65,7 +65,6 @@ export const FOCUS_MONITOR_DEFAULT_OPTIONS = new InjectionToken('cdk-focus-monitor-default-options'); type MonitoredElementInfo = { - unlisten: Function, checkChildren: boolean, subject: Subject }; @@ -181,6 +180,20 @@ export class FocusMonitor implements OnDestroy { this._document = document; this._detectionMode = options?.detectionMode || FocusMonitorDetectionMode.IMMEDIATE; } + /** + * Event listener for `focus` and 'blur' events on the document. + * Needs to be an arrow function in order to preserve the context when it gets bound. + */ + private _documentFocusAndBlurListener = (event: FocusEvent) => { + const target = event.target as HTMLElement|null; + for (let el = target; el; el = el.parentElement) { + if (event.type === 'focus') { + this._onFocus(event as FocusEvent, el); + } else if (event.type === 'blur') { + this._onBlur(event as FocusEvent, el); + } + } + } /** * Monitors focus on an element and applies appropriate CSS classes. @@ -211,34 +224,19 @@ export class FocusMonitor implements OnDestroy { // Check if we're already monitoring this element. if (this._elementInfo.has(nativeElement)) { - let cachedInfo = this._elementInfo.get(nativeElement); + const cachedInfo = this._elementInfo.get(nativeElement); cachedInfo!.checkChildren = checkChildren; return cachedInfo!.subject.asObservable(); } // Create monitored element info. - let info: MonitoredElementInfo = { - unlisten: () => {}, + const info: MonitoredElementInfo = { checkChildren: checkChildren, subject: new Subject() }; this._elementInfo.set(nativeElement, info); this._incrementMonitoredElementCount(); - // Start listening. We need to listen in capture phase since focus events don't bubble. - let focusListener = (event: FocusEvent) => this._onFocus(event, nativeElement); - let blurListener = (event: FocusEvent) => this._onBlur(event, nativeElement); - this._ngZone.runOutsideAngular(() => { - nativeElement.addEventListener('focus', focusListener, true); - nativeElement.addEventListener('blur', blurListener, true); - }); - - // Create an unlisten function for later. - info.unlisten = () => { - nativeElement.removeEventListener('focus', focusListener, true); - nativeElement.removeEventListener('blur', blurListener, true); - }; - return info.subject.asObservable(); } @@ -259,7 +257,6 @@ export class FocusMonitor implements OnDestroy { const elementInfo = this._elementInfo.get(nativeElement); if (elementInfo) { - elementInfo.unlisten(); elementInfo.subject.complete(); this._setClasses(nativeElement); @@ -322,21 +319,37 @@ export class FocusMonitor implements OnDestroy { } } + private _getFocusOrigin(event: FocusEvent): FocusOrigin { + // If we couldn't detect a cause for the focus event, it's due to one of three reasons: + // 1) The window has just regained focus, in which case we want to restore the focused state of + // the element from before the window blurred. + // 2) It was caused by a touch event, in which case we mark the origin as 'touch'. + // 3) The element was programmatically focused, in which case we should mark the origin as + // 'program'. + let origin = this._origin; + if (!origin) { + if (this._windowFocused && this._lastFocusOrigin) { + return this._lastFocusOrigin; + } else if (this._wasCausedByTouch(event)) { + return 'touch'; + } else { + return 'program'; + } + } + return null; + } + /** * Sets the focus classes on the element based on the given focus origin. * @param element The element to update the classes on. * @param origin The focus origin. */ private _setClasses(element: HTMLElement, origin?: FocusOrigin): void { - const elementInfo = this._elementInfo.get(element); - - if (elementInfo) { - this._toggleClass(element, 'cdk-focused', !!origin); - this._toggleClass(element, 'cdk-touch-focused', origin === 'touch'); - this._toggleClass(element, 'cdk-keyboard-focused', origin === 'keyboard'); - this._toggleClass(element, 'cdk-mouse-focused', origin === 'mouse'); - this._toggleClass(element, 'cdk-program-focused', origin === 'program'); - } + this._toggleClass(element, 'cdk-focused', !!origin); + this._toggleClass(element, 'cdk-touch-focused', origin === 'touch'); + this._toggleClass(element, 'cdk-keyboard-focused', origin === 'keyboard'); + this._toggleClass(element, 'cdk-mouse-focused', origin === 'mouse'); + this._toggleClass(element, 'cdk-program-focused', origin === 'program'); } /** @@ -403,23 +416,7 @@ export class FocusMonitor implements OnDestroy { return; } - // If we couldn't detect a cause for the focus event, it's due to one of three reasons: - // 1) The window has just regained focus, in which case we want to restore the focused state of - // the element from before the window blurred. - // 2) It was caused by a touch event, in which case we mark the origin as 'touch'. - // 3) The element was programmatically focused, in which case we should mark the origin as - // 'program'. - let origin = this._origin; - if (!origin) { - if (this._windowFocused && this._lastFocusOrigin) { - origin = this._lastFocusOrigin; - } else if (this._wasCausedByTouch(event)) { - origin = 'touch'; - } else { - origin = 'program'; - } - } - + const origin = this._getFocusOrigin(event); this._setClasses(element, origin); this._emitOrigin(elementInfo.subject, origin); this._lastFocusOrigin = origin; @@ -457,6 +454,10 @@ export class FocusMonitor implements OnDestroy { const document = this._getDocument(); const window = this._getWindow(); + document.addEventListener('focus', this._documentFocusAndBlurListener, + captureEventListenerOptions); + document.addEventListener('blur', this._documentFocusAndBlurListener, + captureEventListenerOptions); document.addEventListener('keydown', this._documentKeydownListener, captureEventListenerOptions); document.addEventListener('mousedown', this._documentMousedownListener, @@ -474,6 +475,10 @@ export class FocusMonitor implements OnDestroy { const document = this._getDocument(); const window = this._getWindow(); + document.removeEventListener('focus', this._documentFocusAndBlurListener, + captureEventListenerOptions); + document.removeEventListener('blur', this._documentFocusAndBlurListener, + captureEventListenerOptions); document.removeEventListener('keydown', this._documentKeydownListener, captureEventListenerOptions); document.removeEventListener('mousedown', this._documentMousedownListener, From 6d96332e64c9de687eab81662b5d35f2c14076d1 Mon Sep 17 00:00:00 2001 From: Michael Leibman Date: Sun, 1 Mar 2020 23:02:15 -0800 Subject: [PATCH 2/4] Fix a bug in _getFocusOrigin(). --- src/cdk/a11y/focus-monitor/focus-monitor.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/cdk/a11y/focus-monitor/focus-monitor.ts b/src/cdk/a11y/focus-monitor/focus-monitor.ts index e35c80814c23..8b71403a1954 100644 --- a/src/cdk/a11y/focus-monitor/focus-monitor.ts +++ b/src/cdk/a11y/focus-monitor/focus-monitor.ts @@ -326,17 +326,17 @@ export class FocusMonitor implements OnDestroy { // 2) It was caused by a touch event, in which case we mark the origin as 'touch'. // 3) The element was programmatically focused, in which case we should mark the origin as // 'program'. - let origin = this._origin; - if (!origin) { - if (this._windowFocused && this._lastFocusOrigin) { - return this._lastFocusOrigin; - } else if (this._wasCausedByTouch(event)) { - return 'touch'; - } else { - return 'program'; - } + if (this._origin) { + return this._origin; + } + + if (this._windowFocused && this._lastFocusOrigin) { + return this._lastFocusOrigin; + } else if (this._wasCausedByTouch(event)) { + return 'touch'; + } else { + return 'program'; } - return null; } /** From 51ce874d236649ca878a66c6f870d2c539d10a1b Mon Sep 17 00:00:00 2001 From: Michael Leibman Date: Mon, 2 Mar 2020 08:19:43 -0800 Subject: [PATCH 3/4] Add a comment explaining why we need to walk up the tree --- src/cdk/a11y/focus-monitor/focus-monitor.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cdk/a11y/focus-monitor/focus-monitor.ts b/src/cdk/a11y/focus-monitor/focus-monitor.ts index 8b71403a1954..9e6b201929be 100644 --- a/src/cdk/a11y/focus-monitor/focus-monitor.ts +++ b/src/cdk/a11y/focus-monitor/focus-monitor.ts @@ -186,6 +186,7 @@ export class FocusMonitor implements OnDestroy { */ private _documentFocusAndBlurListener = (event: FocusEvent) => { const target = event.target as HTMLElement|null; + // We need to walk up the ancestor chain in order to support `checkChildren`. for (let el = target; el; el = el.parentElement) { if (event.type === 'focus') { this._onFocus(event as FocusEvent, el); From 4513fb3b57d4fb37d5441fef60eb9cbf03200bf6 Mon Sep 17 00:00:00 2001 From: Michael Leibman Date: Mon, 2 Mar 2020 15:02:30 -0800 Subject: [PATCH 4/4] Cleanup --- src/cdk/a11y/focus-monitor/focus-monitor.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/cdk/a11y/focus-monitor/focus-monitor.ts b/src/cdk/a11y/focus-monitor/focus-monitor.ts index 9e6b201929be..2cb550f1d01f 100644 --- a/src/cdk/a11y/focus-monitor/focus-monitor.ts +++ b/src/cdk/a11y/focus-monitor/focus-monitor.ts @@ -186,13 +186,11 @@ export class FocusMonitor implements OnDestroy { */ private _documentFocusAndBlurListener = (event: FocusEvent) => { const target = event.target as HTMLElement|null; + const handler = event.type === 'focus' ? this._onFocus : this._onBlur; + // We need to walk up the ancestor chain in order to support `checkChildren`. for (let el = target; el; el = el.parentElement) { - if (event.type === 'focus') { - this._onFocus(event as FocusEvent, el); - } else if (event.type === 'blur') { - this._onBlur(event as FocusEvent, el); - } + handler.call(this, event, el); } }