Skip to content

Commit 66b6c97

Browse files
crisbetojelbourn
authored andcommitted
refactor(a11y): reuse global event listeners in focus monitor (#14205)
Currently whenever we bind global events in the `FocusMonitor`, we re-create the global event listeners and we re-declare the function to unregister them. This isn't necessary, because the functions won't change. These change rework the listeners so they're declared once and reused.
1 parent bfb11b7 commit 66b6c97

File tree

1 file changed

+81
-81
lines changed

1 file changed

+81
-81
lines changed

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

Lines changed: 81 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ export const TOUCH_BUFFER_MS = 650;
2828

2929
export type FocusOrigin = 'touch' | 'mouse' | 'keyboard' | 'program' | null;
3030

31-
3231
/**
3332
* Corresponds to the options that can be passed to the native `focus` event.
3433
* via https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus
@@ -38,13 +37,21 @@ export interface FocusOptions {
3837
preventScroll?: boolean;
3938
}
4039

41-
4240
type MonitoredElementInfo = {
4341
unlisten: Function,
4442
checkChildren: boolean,
4543
subject: Subject<FocusOrigin>
4644
};
4745

46+
/**
47+
* Event listener options that enable capturing and also
48+
* mark the the listener as passive if the browser supports it.
49+
*/
50+
const captureEventListenerOptions = normalizePassiveListenerOptions({
51+
passive: true,
52+
capture: true
53+
});
54+
4855

4956
/** Monitors mouse and keyboard events to determine the cause of focus events. */
5057
@Injectable({providedIn: 'root'})
@@ -73,12 +80,57 @@ export class FocusMonitor implements OnDestroy {
7380
/** Map of elements being monitored to their info. */
7481
private _elementInfo = new Map<HTMLElement, MonitoredElementInfo>();
7582

76-
/** A map of global objects to lists of current listeners. */
77-
private _unregisterGlobalListeners = () => {};
78-
7983
/** The number of elements currently being monitored. */
8084
private _monitoredElementCount = 0;
8185

86+
/**
87+
* Event listener for `keydown` events on the document.
88+
* Needs to be an arrow function in order to preserve the context when it gets bound.
89+
*/
90+
private _documentKeydownListener = () => {
91+
// On keydown record the origin and clear any touch event that may be in progress.
92+
this._lastTouchTarget = null;
93+
this._setOriginForCurrentEventQueue('keyboard');
94+
}
95+
96+
/**
97+
* Event listener for `mousedown` events on the document.
98+
* Needs to be an arrow function in order to preserve the context when it gets bound.
99+
*/
100+
private _documentMousedownListener = () => {
101+
// On mousedown record the origin only if there is not touch
102+
// target, since a mousedown can happen as a result of a touch event.
103+
if (!this._lastTouchTarget) {
104+
this._setOriginForCurrentEventQueue('mouse');
105+
}
106+
}
107+
108+
/**
109+
* Event listener for `touchstart` events on the document.
110+
* Needs to be an arrow function in order to preserve the context when it gets bound.
111+
*/
112+
private _documentTouchstartListener = (event: TouchEvent) => {
113+
// When the touchstart event fires the focus event is not yet in the event queue. This means
114+
// we can't rely on the trick used above (setting timeout of 1ms). Instead we wait 650ms to
115+
// see if a focus happens.
116+
if (this._touchTimeoutId != null) {
117+
clearTimeout(this._touchTimeoutId);
118+
}
119+
this._lastTouchTarget = event.target;
120+
this._touchTimeoutId = setTimeout(() => this._lastTouchTarget = null, TOUCH_BUFFER_MS);
121+
}
122+
123+
/**
124+
* Event listener for `focus` events on the window.
125+
* Needs to be an arrow function in order to preserve the context when it gets bound.
126+
*/
127+
private _windowFocusListener = () => {
128+
// Make a note of when the window regains focus, so we can
129+
// restore the origin info for the focused element.
130+
this._windowFocused = true;
131+
this._windowFocusTimeoutId = setTimeout(() => this._windowFocused = false);
132+
}
133+
82134
constructor(private _ngZone: NgZone, private _platform: Platform) {}
83135

84136
/**
@@ -202,78 +254,6 @@ export class FocusMonitor implements OnDestroy {
202254
this._elementInfo.forEach((_info, element) => this.stopMonitoring(element));
203255
}
204256

205-
/** Register necessary event listeners on the document and window. */
206-
private _registerGlobalListeners() {
207-
// Do nothing if we're not on the browser platform.
208-
if (!this._platform.isBrowser) {
209-
return;
210-
}
211-
212-
// On keydown record the origin and clear any touch event that may be in progress.
213-
let documentKeydownListener = () => {
214-
this._lastTouchTarget = null;
215-
this._setOriginForCurrentEventQueue('keyboard');
216-
};
217-
218-
// On mousedown record the origin only if there is not touch target, since a mousedown can
219-
// happen as a result of a touch event.
220-
let documentMousedownListener = () => {
221-
if (!this._lastTouchTarget) {
222-
this._setOriginForCurrentEventQueue('mouse');
223-
}
224-
};
225-
226-
// When the touchstart event fires the focus event is not yet in the event queue. This means
227-
// we can't rely on the trick used above (setting timeout of 1ms). Instead we wait 650ms to
228-
// see if a focus happens.
229-
let documentTouchstartListener = (event: TouchEvent) => {
230-
if (this._touchTimeoutId != null) {
231-
clearTimeout(this._touchTimeoutId);
232-
}
233-
this._lastTouchTarget = event.target;
234-
this._touchTimeoutId = setTimeout(() => this._lastTouchTarget = null, TOUCH_BUFFER_MS);
235-
};
236-
237-
// Make a note of when the window regains focus, so we can restore the origin info for the
238-
// focused element.
239-
let windowFocusListener = () => {
240-
this._windowFocused = true;
241-
this._windowFocusTimeoutId = setTimeout(() => this._windowFocused = false);
242-
};
243-
244-
// Event listener options that enable capturing and also mark the the listener as passive
245-
// if the browser supports it.
246-
const captureEventListenerOptions = normalizePassiveListenerOptions({
247-
passive: true,
248-
capture: true
249-
});
250-
251-
// Note: we listen to events in the capture phase so we can detect them even if the user stops
252-
// propagation.
253-
this._ngZone.runOutsideAngular(() => {
254-
document.addEventListener('keydown', documentKeydownListener, captureEventListenerOptions);
255-
document.addEventListener('mousedown', documentMousedownListener,
256-
captureEventListenerOptions);
257-
document.addEventListener('touchstart', documentTouchstartListener,
258-
captureEventListenerOptions);
259-
window.addEventListener('focus', windowFocusListener);
260-
});
261-
262-
this._unregisterGlobalListeners = () => {
263-
document.removeEventListener('keydown', documentKeydownListener, captureEventListenerOptions);
264-
document.removeEventListener('mousedown', documentMousedownListener,
265-
captureEventListenerOptions);
266-
document.removeEventListener('touchstart', documentTouchstartListener,
267-
captureEventListenerOptions);
268-
window.removeEventListener('focus', windowFocusListener);
269-
270-
// Clear timeouts for all potentially pending timeouts to prevent the leaks.
271-
clearTimeout(this._windowFocusTimeoutId);
272-
clearTimeout(this._touchTimeoutId);
273-
clearTimeout(this._originTimeoutId);
274-
};
275-
}
276-
277257
private _toggleClass(element: Element, className: string, shouldSet: boolean) {
278258
if (shouldSet) {
279259
element.classList.add(className);
@@ -406,16 +386,36 @@ export class FocusMonitor implements OnDestroy {
406386

407387
private _incrementMonitoredElementCount() {
408388
// Register global listeners when first element is monitored.
409-
if (++this._monitoredElementCount == 1) {
410-
this._registerGlobalListeners();
389+
if (++this._monitoredElementCount == 1 && this._platform.isBrowser) {
390+
// Note: we listen to events in the capture phase so we
391+
// can detect them even if the user stops propagation.
392+
this._ngZone.runOutsideAngular(() => {
393+
document.addEventListener('keydown', this._documentKeydownListener,
394+
captureEventListenerOptions);
395+
document.addEventListener('mousedown', this._documentMousedownListener,
396+
captureEventListenerOptions);
397+
document.addEventListener('touchstart', this._documentTouchstartListener,
398+
captureEventListenerOptions);
399+
window.addEventListener('focus', this._windowFocusListener);
400+
});
411401
}
412402
}
413403

414404
private _decrementMonitoredElementCount() {
415405
// Unregister global listeners when last element is unmonitored.
416406
if (!--this._monitoredElementCount) {
417-
this._unregisterGlobalListeners();
418-
this._unregisterGlobalListeners = () => {};
407+
document.removeEventListener('keydown', this._documentKeydownListener,
408+
captureEventListenerOptions);
409+
document.removeEventListener('mousedown', this._documentMousedownListener,
410+
captureEventListenerOptions);
411+
document.removeEventListener('touchstart', this._documentTouchstartListener,
412+
captureEventListenerOptions);
413+
window.removeEventListener('focus', this._windowFocusListener);
414+
415+
// Clear timeouts for all potentially pending timeouts to prevent the leaks.
416+
clearTimeout(this._windowFocusTimeoutId);
417+
clearTimeout(this._touchTimeoutId);
418+
clearTimeout(this._originTimeoutId);
419419
}
420420
}
421421

0 commit comments

Comments
 (0)