Skip to content

Commit 8dc8dc4

Browse files
mmalerbajosephperrott
authored andcommitted
fix(focus-monitor): cleanup global listeners and don't require Renderer2 (#7728)
* fix(focus-monitor): cleanup global listeners and don't require Renderer2
1 parent 0e03bf4 commit 8dc8dc4

File tree

13 files changed

+127
-92
lines changed

13 files changed

+127
-92
lines changed

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {A11yModule} from './index';
1010
describe('FocusMonitor', () => {
1111
let fixture: ComponentFixture<PlainButton>;
1212
let buttonElement: HTMLElement;
13-
let buttonRenderer: Renderer2;
1413
let focusMonitor: FocusMonitor;
1514
let changeHandler: (origin: FocusOrigin) => void;
1615

@@ -28,11 +27,10 @@ describe('FocusMonitor', () => {
2827
fixture.detectChanges();
2928

3029
buttonElement = fixture.debugElement.query(By.css('button')).nativeElement;
31-
buttonRenderer = fixture.componentInstance.renderer;
3230
focusMonitor = fm;
3331

3432
changeHandler = jasmine.createSpy('focus origin change handler');
35-
focusMonitor.monitor(buttonElement, buttonRenderer, false).subscribe(changeHandler);
33+
focusMonitor.monitor(buttonElement, false).subscribe(changeHandler);
3634
patchElementFocus(buttonElement);
3735
}));
3836

src/cdk/a11y/focus-monitor.ts

Lines changed: 82 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ export type FocusOrigin = 'touch' | 'mouse' | 'keyboard' | 'program' | null;
3636
type MonitoredElementInfo = {
3737
unlisten: Function,
3838
checkChildren: boolean,
39-
renderer: Renderer2,
4039
subject: Subject<FocusOrigin>
4140
};
4241

@@ -62,22 +61,38 @@ export class FocusMonitor {
6261
/** Weak map of elements being monitored to their info. */
6362
private _elementInfo = new WeakMap<Element, MonitoredElementInfo>();
6463

65-
constructor(private _ngZone: NgZone, private _platform: Platform) {
66-
this._ngZone.runOutsideAngular(() => this._registerDocumentEvents());
67-
}
64+
/** A map of global objects to lists of current listeners. */
65+
private _unregisterGlobalListeners = () => {};
66+
67+
/** The number of elements currently being monitored. */
68+
private _monitoredElementCount = 0;
6869

70+
constructor(private _ngZone: NgZone, private _platform: Platform) {}
71+
72+
/**
73+
* @docs-private
74+
* @deprecated renderer param no longer needed.
75+
*/
76+
monitor(element: HTMLElement, renderer: Renderer2, checkChildren: boolean):
77+
Observable<FocusOrigin>;
6978
/**
7079
* Monitors focus on an element and applies appropriate CSS classes.
7180
* @param element The element to monitor
72-
* @param renderer The renderer to use to apply CSS classes to the element.
7381
* @param checkChildren Whether to count the element as focused when its children are focused.
7482
* @returns An observable that emits when the focus state of the element changes.
7583
* When the element is blurred, null will be emitted.
7684
*/
85+
monitor(element: HTMLElement, checkChildren: boolean): Observable<FocusOrigin>;
7786
monitor(
7887
element: HTMLElement,
79-
renderer: Renderer2,
80-
checkChildren: boolean): Observable<FocusOrigin> {
88+
renderer: Renderer2 | boolean,
89+
checkChildren?: boolean): Observable<FocusOrigin> {
90+
// TODO(mmalerba): clean up after deprecated signature is removed.
91+
if (!(renderer instanceof Renderer2)) {
92+
checkChildren = renderer;
93+
}
94+
checkChildren = !!checkChildren;
95+
8196
// Do nothing if we're not on the browser platform.
8297
if (!this._platform.isBrowser) {
8398
return observableOf(null);
@@ -93,10 +108,10 @@ export class FocusMonitor {
93108
let info: MonitoredElementInfo = {
94109
unlisten: () => {},
95110
checkChildren: checkChildren,
96-
renderer: renderer,
97111
subject: new Subject<FocusOrigin>()
98112
};
99113
this._elementInfo.set(element, info);
114+
this._incrementMonitoredElementCount();
100115

101116
// Start listening. We need to listen in capture phase since focus events don't bubble.
102117
let focusListener = (event: FocusEvent) => this._onFocus(event, element);
@@ -128,6 +143,7 @@ export class FocusMonitor {
128143

129144
this._setClasses(element);
130145
this._elementInfo.delete(element);
146+
this._decrementMonitoredElementCount();
131147
}
132148
}
133149

@@ -142,49 +158,69 @@ export class FocusMonitor {
142158
}
143159

144160
/** Register necessary event listeners on the document and window. */
145-
private _registerDocumentEvents() {
161+
private _registerGlobalListeners() {
146162
// Do nothing if we're not on the browser platform.
147163
if (!this._platform.isBrowser) {
148164
return;
149165
}
150166

151-
// Note: we listen to events in the capture phase so we can detect them even if the user stops
152-
// propagation.
153-
154167
// On keydown record the origin and clear any touch event that may be in progress.
155-
document.addEventListener('keydown', () => {
168+
let documentKeydownListener = () => {
156169
this._lastTouchTarget = null;
157170
this._setOriginForCurrentEventQueue('keyboard');
158-
}, true);
171+
};
159172

160173
// On mousedown record the origin only if there is not touch target, since a mousedown can
161174
// happen as a result of a touch event.
162-
document.addEventListener('mousedown', () => {
175+
let documentMousedownListener = () => {
163176
if (!this._lastTouchTarget) {
164177
this._setOriginForCurrentEventQueue('mouse');
165178
}
166-
}, true);
179+
};
167180

168181
// When the touchstart event fires the focus event is not yet in the event queue. This means
169182
// we can't rely on the trick used above (setting timeout of 0ms). Instead we wait 650ms to
170183
// see if a focus happens.
171-
document.addEventListener('touchstart', (event: TouchEvent) => {
184+
let documentTouchstartListener = (event: TouchEvent) => {
172185
if (this._touchTimeout != null) {
173186
clearTimeout(this._touchTimeout);
174187
}
175188
this._lastTouchTarget = event.target;
176189
this._touchTimeout = setTimeout(() => this._lastTouchTarget = null, TOUCH_BUFFER_MS);
177-
178-
// Note that we need to cast the event options to `any`, because at the time of writing
179-
// (TypeScript 2.5), the built-in types don't support the `addEventListener` options param.
180-
}, supportsPassiveEventListeners() ? ({passive: true, capture: true} as any) : true);
190+
};
181191

182192
// Make a note of when the window regains focus, so we can restore the origin info for the
183193
// focused element.
184-
window.addEventListener('focus', () => {
194+
let windowFocusListener = () => {
185195
this._windowFocused = true;
186196
setTimeout(() => this._windowFocused = false, 0);
197+
};
198+
199+
// Note: we listen to events in the capture phase so we can detect them even if the user stops
200+
// propagation.
201+
this._ngZone.runOutsideAngular(() => {
202+
document.addEventListener('keydown', documentKeydownListener, true);
203+
document.addEventListener('mousedown', documentMousedownListener, true);
204+
document.addEventListener('touchstart', documentTouchstartListener,
205+
supportsPassiveEventListeners() ? ({passive: true, capture: true} as any) : true);
206+
window.addEventListener('focus', windowFocusListener);
187207
});
208+
209+
this._unregisterGlobalListeners = () => {
210+
document.removeEventListener('keydown', documentKeydownListener, true);
211+
document.removeEventListener('mousedown', documentMousedownListener, true);
212+
document.removeEventListener('touchstart', documentTouchstartListener,
213+
supportsPassiveEventListeners() ? ({passive: true, capture: true} as any) : true);
214+
window.removeEventListener('focus', windowFocusListener);
215+
};
216+
}
217+
218+
private _toggleClass(element: Element, className: string, shouldSet: boolean) {
219+
if (shouldSet) {
220+
element.classList.add(className);
221+
} else {
222+
element.classList.remove(className);
223+
}
188224
}
189225

190226
/**
@@ -196,16 +232,11 @@ export class FocusMonitor {
196232
const elementInfo = this._elementInfo.get(element);
197233

198234
if (elementInfo) {
199-
const toggleClass = (className: string, shouldSet: boolean) => {
200-
shouldSet ? elementInfo.renderer.addClass(element, className) :
201-
elementInfo.renderer.removeClass(element, className);
202-
};
203-
204-
toggleClass('cdk-focused', !!origin);
205-
toggleClass('cdk-touch-focused', origin === 'touch');
206-
toggleClass('cdk-keyboard-focused', origin === 'keyboard');
207-
toggleClass('cdk-mouse-focused', origin === 'mouse');
208-
toggleClass('cdk-program-focused', origin === 'program');
235+
this._toggleClass(element, 'cdk-focused', !!origin);
236+
this._toggleClass(element, 'cdk-touch-focused', origin === 'touch');
237+
this._toggleClass(element, 'cdk-keyboard-focused', origin === 'keyboard');
238+
this._toggleClass(element, 'cdk-mouse-focused', origin === 'mouse');
239+
this._toggleClass(element, 'cdk-program-focused', origin === 'program');
209240
}
210241
}
211242

@@ -235,7 +266,7 @@ export class FocusMonitor {
235266
// result, this code will still consider it to have been caused by the touch event and will
236267
// apply the cdk-touch-focused class rather than the cdk-program-focused class. This is a
237268
// relatively small edge-case that can be worked around by using
238-
// focusVia(parentEl, renderer, 'program') to focus the parent element.
269+
// focusVia(parentEl, 'program') to focus the parent element.
239270
//
240271
// If we decide that we absolutely must handle this case correctly, we can do so by listening
241272
// for the first focus event after the touchstart, and then the first blur event after that
@@ -304,6 +335,22 @@ export class FocusMonitor {
304335
this._setClasses(element);
305336
elementInfo.subject.next(null);
306337
}
338+
339+
private _incrementMonitoredElementCount() {
340+
// Register global listeners when first element is monitored.
341+
if (++this._monitoredElementCount == 1) {
342+
this._registerGlobalListeners();
343+
}
344+
}
345+
346+
private _decrementMonitoredElementCount() {
347+
// Unregister global listeners when last element is unmonitored.
348+
if (!--this._monitoredElementCount) {
349+
this._unregisterGlobalListeners();
350+
this._unregisterGlobalListeners = () => {};
351+
}
352+
}
353+
307354
}
308355

309356

@@ -323,10 +370,9 @@ export class CdkMonitorFocus implements OnDestroy {
323370
private _monitorSubscription: Subscription;
324371
@Output() cdkFocusChange = new EventEmitter<FocusOrigin>();
325372

326-
constructor(private _elementRef: ElementRef, private _focusMonitor: FocusMonitor,
327-
renderer: Renderer2) {
373+
constructor(private _elementRef: ElementRef, private _focusMonitor: FocusMonitor) {
328374
this._monitorSubscription = this._focusMonitor.monitor(
329-
this._elementRef.nativeElement, renderer,
375+
this._elementRef.nativeElement,
330376
this._elementRef.nativeElement.hasAttribute('cdkMonitorSubtreeFocus'))
331377
.subscribe(origin => this.cdkFocusChange.emit(origin));
332378
}

src/lib/button-toggle/button-toggle.ts

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,30 +6,29 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {FocusMonitor} from '@angular/cdk/a11y';
10+
import {coerceBooleanProperty} from '@angular/cdk/coercion';
11+
import {UniqueSelectionDispatcher} from '@angular/cdk/collections';
912
import {
13+
ChangeDetectionStrategy,
14+
ChangeDetectorRef,
1015
Component,
1116
ContentChildren,
1217
Directive,
1318
ElementRef,
14-
Renderer2,
1519
EventEmitter,
20+
forwardRef,
1621
Input,
17-
OnInit,
1822
OnDestroy,
23+
OnInit,
1924
Optional,
2025
Output,
2126
QueryList,
2227
ViewChild,
2328
ViewEncapsulation,
24-
forwardRef,
25-
ChangeDetectionStrategy,
26-
ChangeDetectorRef,
2729
} from '@angular/core';
28-
import {NG_VALUE_ACCESSOR, ControlValueAccessor} from '@angular/forms';
29-
import {coerceBooleanProperty} from '@angular/cdk/coercion';
30+
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
3031
import {CanDisable, mixinDisabled} from '@angular/material/core';
31-
import {FocusMonitor} from '@angular/cdk/a11y';
32-
import {UniqueSelectionDispatcher} from '@angular/cdk/collections';
3332

3433
/** Acceptable types for a button toggle. */
3534
export type ToggleType = 'checkbox' | 'radio';
@@ -386,7 +385,6 @@ export class MatButtonToggle implements OnInit, OnDestroy {
386385
@Optional() toggleGroupMultiple: MatButtonToggleGroupMultiple,
387386
private _changeDetectorRef: ChangeDetectorRef,
388387
private _buttonToggleDispatcher: UniqueSelectionDispatcher,
389-
private _renderer: Renderer2,
390388
private _elementRef: ElementRef,
391389
private _focusMonitor: FocusMonitor) {
392390

@@ -421,7 +419,7 @@ export class MatButtonToggle implements OnInit, OnDestroy {
421419
if (this.buttonToggleGroup && this._value == this.buttonToggleGroup.value) {
422420
this._checked = true;
423421
}
424-
this._focusMonitor.monitor(this._elementRef.nativeElement, this._renderer, true);
422+
this._focusMonitor.monitor(this._elementRef.nativeElement, true);
425423
}
426424

427425
/** Focuses the button. */

src/lib/button/button.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {FocusMonitor} from '@angular/cdk/a11y';
10+
import {Platform} from '@angular/cdk/platform';
911
import {
1012
ChangeDetectionStrategy,
1113
Component,
@@ -19,7 +21,6 @@ import {
1921
Self,
2022
ViewEncapsulation,
2123
} from '@angular/core';
22-
import {Platform} from '@angular/cdk/platform';
2324
import {
2425
CanColor,
2526
CanDisable,
@@ -28,7 +29,6 @@ import {
2829
mixinDisabled,
2930
mixinDisableRipple
3031
} from '@angular/material/core';
31-
import {FocusMonitor} from '@angular/cdk/a11y';
3232

3333

3434
// TODO(kara): Convert attribute selectors to classes when attr maps become available
@@ -141,7 +141,7 @@ export class MatButton extends _MatButtonMixinBase
141141
private _platform: Platform,
142142
private _focusMonitor: FocusMonitor) {
143143
super(renderer, elementRef);
144-
this._focusMonitor.monitor(this._elementRef.nativeElement, this._renderer, true);
144+
this._focusMonitor.monitor(this._elementRef.nativeElement, true);
145145
}
146146

147147
ngOnDestroy() {

src/lib/checkbox/checkbox.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {FocusMonitor, FocusOrigin} from '@angular/cdk/a11y';
910
import {coerceBooleanProperty} from '@angular/cdk/coercion';
1011
import {
1112
AfterViewInit,
@@ -36,7 +37,6 @@ import {
3637
mixinTabIndex,
3738
RippleRef,
3839
} from '@angular/material/core';
39-
import {FocusMonitor, FocusOrigin} from '@angular/cdk/a11y';
4040

4141

4242
// Increasing integer for generating unique ids for checkbox components.
@@ -209,7 +209,7 @@ export class MatCheckbox extends _MatCheckboxMixinBase implements ControlValueAc
209209

210210
ngAfterViewInit() {
211211
this._focusMonitor
212-
.monitor(this._inputElement.nativeElement, this._renderer, false)
212+
.monitor(this._inputElement.nativeElement, false)
213213
.subscribe(focusOrigin => this._onInputFocusChange(focusOrigin));
214214
}
215215

src/lib/expansion/expansion-panel-header.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import {
1919
Host,
2020
Input,
2121
OnDestroy,
22-
Renderer2,
2322
ViewEncapsulation,
2423
} from '@angular/core';
2524
import {merge} from 'rxjs/observable/merge';
@@ -85,7 +84,6 @@ export class MatExpansionPanelHeader implements OnDestroy {
8584
private _parentChangeSubscription = Subscription.EMPTY;
8685

8786
constructor(
88-
renderer: Renderer2,
8987
@Host() public panel: MatExpansionPanel,
9088
private _element: ElementRef,
9189
private _focusMonitor: FocusMonitor,
@@ -100,7 +98,7 @@ export class MatExpansionPanelHeader implements OnDestroy {
10098
)
10199
.subscribe(() => this._changeDetectorRef.markForCheck());
102100

103-
_focusMonitor.monitor(_element.nativeElement, renderer, false);
101+
_focusMonitor.monitor(_element.nativeElement, false);
104102
}
105103

106104
/** Height of the header while the panel is expanded. */

0 commit comments

Comments
 (0)