mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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.
This commit is contained in:
committed by
Jeremy Elbourn
parent
bfb11b7689
commit
66b6c97bf7
@@ -28,7 +28,6 @@ export const TOUCH_BUFFER_MS = 650;
|
||||
|
||||
export type FocusOrigin = 'touch' | 'mouse' | 'keyboard' | 'program' | null;
|
||||
|
||||
|
||||
/**
|
||||
* Corresponds to the options that can be passed to the native `focus` event.
|
||||
* via https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus
|
||||
@@ -38,13 +37,21 @@ export interface FocusOptions {
|
||||
preventScroll?: boolean;
|
||||
}
|
||||
|
||||
|
||||
type MonitoredElementInfo = {
|
||||
unlisten: Function,
|
||||
checkChildren: boolean,
|
||||
subject: Subject<FocusOrigin>
|
||||
};
|
||||
|
||||
/**
|
||||
* Event listener options that enable capturing and also
|
||||
* mark the the listener as passive if the browser supports it.
|
||||
*/
|
||||
const captureEventListenerOptions = normalizePassiveListenerOptions({
|
||||
passive: true,
|
||||
capture: true
|
||||
});
|
||||
|
||||
|
||||
/** Monitors mouse and keyboard events to determine the cause of focus events. */
|
||||
@Injectable({providedIn: 'root'})
|
||||
@@ -73,12 +80,57 @@ export class FocusMonitor implements OnDestroy {
|
||||
/** Map of elements being monitored to their info. */
|
||||
private _elementInfo = new Map<HTMLElement, MonitoredElementInfo>();
|
||||
|
||||
/** A map of global objects to lists of current listeners. */
|
||||
private _unregisterGlobalListeners = () => {};
|
||||
|
||||
/** The number of elements currently being monitored. */
|
||||
private _monitoredElementCount = 0;
|
||||
|
||||
/**
|
||||
* Event listener for `keydown` events on the document.
|
||||
* Needs to be an arrow function in order to preserve the context when it gets bound.
|
||||
*/
|
||||
private _documentKeydownListener = () => {
|
||||
// On keydown record the origin and clear any touch event that may be in progress.
|
||||
this._lastTouchTarget = null;
|
||||
this._setOriginForCurrentEventQueue('keyboard');
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for `mousedown` events on the document.
|
||||
* Needs to be an arrow function in order to preserve the context when it gets bound.
|
||||
*/
|
||||
private _documentMousedownListener = () => {
|
||||
// On mousedown record the origin only if there is not touch
|
||||
// target, since a mousedown can happen as a result of a touch event.
|
||||
if (!this._lastTouchTarget) {
|
||||
this._setOriginForCurrentEventQueue('mouse');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for `touchstart` events on the document.
|
||||
* Needs to be an arrow function in order to preserve the context when it gets bound.
|
||||
*/
|
||||
private _documentTouchstartListener = (event: TouchEvent) => {
|
||||
// When the touchstart event fires the focus event is not yet in the event queue. This means
|
||||
// we can't rely on the trick used above (setting timeout of 1ms). Instead we wait 650ms to
|
||||
// see if a focus happens.
|
||||
if (this._touchTimeoutId != null) {
|
||||
clearTimeout(this._touchTimeoutId);
|
||||
}
|
||||
this._lastTouchTarget = event.target;
|
||||
this._touchTimeoutId = setTimeout(() => this._lastTouchTarget = null, TOUCH_BUFFER_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for `focus` events on the window.
|
||||
* Needs to be an arrow function in order to preserve the context when it gets bound.
|
||||
*/
|
||||
private _windowFocusListener = () => {
|
||||
// Make a note of when the window regains focus, so we can
|
||||
// restore the origin info for the focused element.
|
||||
this._windowFocused = true;
|
||||
this._windowFocusTimeoutId = setTimeout(() => this._windowFocused = false);
|
||||
}
|
||||
|
||||
constructor(private _ngZone: NgZone, private _platform: Platform) {}
|
||||
|
||||
/**
|
||||
@@ -202,78 +254,6 @@ export class FocusMonitor implements OnDestroy {
|
||||
this._elementInfo.forEach((_info, element) => this.stopMonitoring(element));
|
||||
}
|
||||
|
||||
/** Register necessary event listeners on the document and window. */
|
||||
private _registerGlobalListeners() {
|
||||
// Do nothing if we're not on the browser platform.
|
||||
if (!this._platform.isBrowser) {
|
||||
return;
|
||||
}
|
||||
|
||||
// On keydown record the origin and clear any touch event that may be in progress.
|
||||
let documentKeydownListener = () => {
|
||||
this._lastTouchTarget = null;
|
||||
this._setOriginForCurrentEventQueue('keyboard');
|
||||
};
|
||||
|
||||
// On mousedown record the origin only if there is not touch target, since a mousedown can
|
||||
// happen as a result of a touch event.
|
||||
let documentMousedownListener = () => {
|
||||
if (!this._lastTouchTarget) {
|
||||
this._setOriginForCurrentEventQueue('mouse');
|
||||
}
|
||||
};
|
||||
|
||||
// When the touchstart event fires the focus event is not yet in the event queue. This means
|
||||
// we can't rely on the trick used above (setting timeout of 1ms). Instead we wait 650ms to
|
||||
// see if a focus happens.
|
||||
let documentTouchstartListener = (event: TouchEvent) => {
|
||||
if (this._touchTimeoutId != null) {
|
||||
clearTimeout(this._touchTimeoutId);
|
||||
}
|
||||
this._lastTouchTarget = event.target;
|
||||
this._touchTimeoutId = setTimeout(() => this._lastTouchTarget = null, TOUCH_BUFFER_MS);
|
||||
};
|
||||
|
||||
// Make a note of when the window regains focus, so we can restore the origin info for the
|
||||
// focused element.
|
||||
let windowFocusListener = () => {
|
||||
this._windowFocused = true;
|
||||
this._windowFocusTimeoutId = setTimeout(() => this._windowFocused = false);
|
||||
};
|
||||
|
||||
// Event listener options that enable capturing and also mark the the listener as passive
|
||||
// if the browser supports it.
|
||||
const captureEventListenerOptions = normalizePassiveListenerOptions({
|
||||
passive: true,
|
||||
capture: true
|
||||
});
|
||||
|
||||
// Note: we listen to events in the capture phase so we can detect them even if the user stops
|
||||
// propagation.
|
||||
this._ngZone.runOutsideAngular(() => {
|
||||
document.addEventListener('keydown', documentKeydownListener, captureEventListenerOptions);
|
||||
document.addEventListener('mousedown', documentMousedownListener,
|
||||
captureEventListenerOptions);
|
||||
document.addEventListener('touchstart', documentTouchstartListener,
|
||||
captureEventListenerOptions);
|
||||
window.addEventListener('focus', windowFocusListener);
|
||||
});
|
||||
|
||||
this._unregisterGlobalListeners = () => {
|
||||
document.removeEventListener('keydown', documentKeydownListener, captureEventListenerOptions);
|
||||
document.removeEventListener('mousedown', documentMousedownListener,
|
||||
captureEventListenerOptions);
|
||||
document.removeEventListener('touchstart', documentTouchstartListener,
|
||||
captureEventListenerOptions);
|
||||
window.removeEventListener('focus', windowFocusListener);
|
||||
|
||||
// Clear timeouts for all potentially pending timeouts to prevent the leaks.
|
||||
clearTimeout(this._windowFocusTimeoutId);
|
||||
clearTimeout(this._touchTimeoutId);
|
||||
clearTimeout(this._originTimeoutId);
|
||||
};
|
||||
}
|
||||
|
||||
private _toggleClass(element: Element, className: string, shouldSet: boolean) {
|
||||
if (shouldSet) {
|
||||
element.classList.add(className);
|
||||
@@ -406,16 +386,36 @@ export class FocusMonitor implements OnDestroy {
|
||||
|
||||
private _incrementMonitoredElementCount() {
|
||||
// Register global listeners when first element is monitored.
|
||||
if (++this._monitoredElementCount == 1) {
|
||||
this._registerGlobalListeners();
|
||||
if (++this._monitoredElementCount == 1 && this._platform.isBrowser) {
|
||||
// Note: we listen to events in the capture phase so we
|
||||
// can detect them even if the user stops propagation.
|
||||
this._ngZone.runOutsideAngular(() => {
|
||||
document.addEventListener('keydown', this._documentKeydownListener,
|
||||
captureEventListenerOptions);
|
||||
document.addEventListener('mousedown', this._documentMousedownListener,
|
||||
captureEventListenerOptions);
|
||||
document.addEventListener('touchstart', this._documentTouchstartListener,
|
||||
captureEventListenerOptions);
|
||||
window.addEventListener('focus', this._windowFocusListener);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _decrementMonitoredElementCount() {
|
||||
// Unregister global listeners when last element is unmonitored.
|
||||
if (!--this._monitoredElementCount) {
|
||||
this._unregisterGlobalListeners();
|
||||
this._unregisterGlobalListeners = () => {};
|
||||
document.removeEventListener('keydown', this._documentKeydownListener,
|
||||
captureEventListenerOptions);
|
||||
document.removeEventListener('mousedown', this._documentMousedownListener,
|
||||
captureEventListenerOptions);
|
||||
document.removeEventListener('touchstart', this._documentTouchstartListener,
|
||||
captureEventListenerOptions);
|
||||
window.removeEventListener('focus', this._windowFocusListener);
|
||||
|
||||
// Clear timeouts for all potentially pending timeouts to prevent the leaks.
|
||||
clearTimeout(this._windowFocusTimeoutId);
|
||||
clearTimeout(this._touchTimeoutId);
|
||||
clearTimeout(this._originTimeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user