Skip to content

Commit 174e4cd

Browse files
mleibmanmmalerba
authored andcommitted
perf(focus-monitor): optimize event registration (#18667)
* 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. * Fix a bug in _getFocusOrigin(). * Add a comment explaining why we need to walk up the tree * Cleanup
1 parent 142c55e commit 174e4cd

File tree

1 file changed

+49
-45
lines changed

1 file changed

+49
-45
lines changed

src/cdk/a11y/focus-monitor/focus-monitor.ts

Lines changed: 49 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ export const FOCUS_MONITOR_DEFAULT_OPTIONS =
6565
new InjectionToken<FocusMonitorOptions>('cdk-focus-monitor-default-options');
6666

6767
type MonitoredElementInfo = {
68-
unlisten: Function,
6968
checkChildren: boolean,
7069
subject: Subject<FocusOrigin>
7170
};
@@ -181,6 +180,19 @@ export class FocusMonitor implements OnDestroy {
181180
this._document = document;
182181
this._detectionMode = options?.detectionMode || FocusMonitorDetectionMode.IMMEDIATE;
183182
}
183+
/**
184+
* Event listener for `focus` and 'blur' events on the document.
185+
* Needs to be an arrow function in order to preserve the context when it gets bound.
186+
*/
187+
private _documentFocusAndBlurListener = (event: FocusEvent) => {
188+
const target = event.target as HTMLElement|null;
189+
const handler = event.type === 'focus' ? this._onFocus : this._onBlur;
190+
191+
// We need to walk up the ancestor chain in order to support `checkChildren`.
192+
for (let el = target; el; el = el.parentElement) {
193+
handler.call(this, event, el);
194+
}
195+
}
184196

185197
/**
186198
* Monitors focus on an element and applies appropriate CSS classes.
@@ -211,34 +223,19 @@ export class FocusMonitor implements OnDestroy {
211223

212224
// Check if we're already monitoring this element.
213225
if (this._elementInfo.has(nativeElement)) {
214-
let cachedInfo = this._elementInfo.get(nativeElement);
226+
const cachedInfo = this._elementInfo.get(nativeElement);
215227
cachedInfo!.checkChildren = checkChildren;
216228
return cachedInfo!.subject.asObservable();
217229
}
218230

219231
// Create monitored element info.
220-
let info: MonitoredElementInfo = {
221-
unlisten: () => {},
232+
const info: MonitoredElementInfo = {
222233
checkChildren: checkChildren,
223234
subject: new Subject<FocusOrigin>()
224235
};
225236
this._elementInfo.set(nativeElement, info);
226237
this._incrementMonitoredElementCount();
227238

228-
// Start listening. We need to listen in capture phase since focus events don't bubble.
229-
let focusListener = (event: FocusEvent) => this._onFocus(event, nativeElement);
230-
let blurListener = (event: FocusEvent) => this._onBlur(event, nativeElement);
231-
this._ngZone.runOutsideAngular(() => {
232-
nativeElement.addEventListener('focus', focusListener, true);
233-
nativeElement.addEventListener('blur', blurListener, true);
234-
});
235-
236-
// Create an unlisten function for later.
237-
info.unlisten = () => {
238-
nativeElement.removeEventListener('focus', focusListener, true);
239-
nativeElement.removeEventListener('blur', blurListener, true);
240-
};
241-
242239
return info.subject.asObservable();
243240
}
244241

@@ -259,7 +256,6 @@ export class FocusMonitor implements OnDestroy {
259256
const elementInfo = this._elementInfo.get(nativeElement);
260257

261258
if (elementInfo) {
262-
elementInfo.unlisten();
263259
elementInfo.subject.complete();
264260

265261
this._setClasses(nativeElement);
@@ -322,21 +318,37 @@ export class FocusMonitor implements OnDestroy {
322318
}
323319
}
324320

321+
private _getFocusOrigin(event: FocusEvent): FocusOrigin {
322+
// If we couldn't detect a cause for the focus event, it's due to one of three reasons:
323+
// 1) The window has just regained focus, in which case we want to restore the focused state of
324+
// the element from before the window blurred.
325+
// 2) It was caused by a touch event, in which case we mark the origin as 'touch'.
326+
// 3) The element was programmatically focused, in which case we should mark the origin as
327+
// 'program'.
328+
if (this._origin) {
329+
return this._origin;
330+
}
331+
332+
if (this._windowFocused && this._lastFocusOrigin) {
333+
return this._lastFocusOrigin;
334+
} else if (this._wasCausedByTouch(event)) {
335+
return 'touch';
336+
} else {
337+
return 'program';
338+
}
339+
}
340+
325341
/**
326342
* Sets the focus classes on the element based on the given focus origin.
327343
* @param element The element to update the classes on.
328344
* @param origin The focus origin.
329345
*/
330346
private _setClasses(element: HTMLElement, origin?: FocusOrigin): void {
331-
const elementInfo = this._elementInfo.get(element);
332-
333-
if (elementInfo) {
334-
this._toggleClass(element, 'cdk-focused', !!origin);
335-
this._toggleClass(element, 'cdk-touch-focused', origin === 'touch');
336-
this._toggleClass(element, 'cdk-keyboard-focused', origin === 'keyboard');
337-
this._toggleClass(element, 'cdk-mouse-focused', origin === 'mouse');
338-
this._toggleClass(element, 'cdk-program-focused', origin === 'program');
339-
}
347+
this._toggleClass(element, 'cdk-focused', !!origin);
348+
this._toggleClass(element, 'cdk-touch-focused', origin === 'touch');
349+
this._toggleClass(element, 'cdk-keyboard-focused', origin === 'keyboard');
350+
this._toggleClass(element, 'cdk-mouse-focused', origin === 'mouse');
351+
this._toggleClass(element, 'cdk-program-focused', origin === 'program');
340352
}
341353

342354
/**
@@ -403,23 +415,7 @@ export class FocusMonitor implements OnDestroy {
403415
return;
404416
}
405417

406-
// If we couldn't detect a cause for the focus event, it's due to one of three reasons:
407-
// 1) The window has just regained focus, in which case we want to restore the focused state of
408-
// the element from before the window blurred.
409-
// 2) It was caused by a touch event, in which case we mark the origin as 'touch'.
410-
// 3) The element was programmatically focused, in which case we should mark the origin as
411-
// 'program'.
412-
let origin = this._origin;
413-
if (!origin) {
414-
if (this._windowFocused && this._lastFocusOrigin) {
415-
origin = this._lastFocusOrigin;
416-
} else if (this._wasCausedByTouch(event)) {
417-
origin = 'touch';
418-
} else {
419-
origin = 'program';
420-
}
421-
}
422-
418+
const origin = this._getFocusOrigin(event);
423419
this._setClasses(element, origin);
424420
this._emitOrigin(elementInfo.subject, origin);
425421
this._lastFocusOrigin = origin;
@@ -457,6 +453,10 @@ export class FocusMonitor implements OnDestroy {
457453
const document = this._getDocument();
458454
const window = this._getWindow();
459455

456+
document.addEventListener('focus', this._documentFocusAndBlurListener,
457+
captureEventListenerOptions);
458+
document.addEventListener('blur', this._documentFocusAndBlurListener,
459+
captureEventListenerOptions);
460460
document.addEventListener('keydown', this._documentKeydownListener,
461461
captureEventListenerOptions);
462462
document.addEventListener('mousedown', this._documentMousedownListener,
@@ -474,6 +474,10 @@ export class FocusMonitor implements OnDestroy {
474474
const document = this._getDocument();
475475
const window = this._getWindow();
476476

477+
document.removeEventListener('focus', this._documentFocusAndBlurListener,
478+
captureEventListenerOptions);
479+
document.removeEventListener('blur', this._documentFocusAndBlurListener,
480+
captureEventListenerOptions);
477481
document.removeEventListener('keydown', this._documentKeydownListener,
478482
captureEventListenerOptions);
479483
document.removeEventListener('mousedown', this._documentMousedownListener,

0 commit comments

Comments
 (0)