Skip to content

Commit 485bc75

Browse files
committed
fix(focus-monitor): cleanup global listeners and don't require Renderer2
1 parent cf11ff2 commit 485bc75

File tree

12 files changed

+124
-90
lines changed

12 files changed

+124
-90
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: 80 additions & 35 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,53 @@ 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());
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 get _monitoredElementCount() {
69+
return this._monitoredElementCountGetterSetterBacking;
70+
}
71+
private set _monitoredElementCount(n) {
72+
// Register global listeners when first element is monitored.
73+
if (!this._monitoredElementCountGetterSetterBacking && n) {
74+
this._registerGlobalListeners();
75+
}
76+
// Unregister global listeners when last element is unmonitored.
77+
else if (this._monitoredElementCountGetterSetterBacking && !n) {
78+
this._unregisterGlobalListeners();
79+
this._unregisterGlobalListeners = () => {};
80+
}
81+
this._monitoredElementCountGetterSetterBacking = n;
6782
}
83+
private _monitoredElementCountGetterSetterBacking = 0;
6884

85+
constructor(private _ngZone: NgZone, private _platform: Platform) {}
86+
87+
/**
88+
* @docs-private
89+
* @deprecated renderer param no longer needed.
90+
*/
91+
monitor(element: HTMLElement, renderer: Renderer2, checkChildren: boolean):
92+
Observable<FocusOrigin>;
6993
/**
7094
* Monitors focus on an element and applies appropriate CSS classes.
7195
* @param element The element to monitor
72-
* @param renderer The renderer to use to apply CSS classes to the element.
7396
* @param checkChildren Whether to count the element as focused when its children are focused.
7497
* @returns An observable that emits when the focus state of the element changes.
7598
* When the element is blurred, null will be emitted.
7699
*/
100+
monitor(element: HTMLElement, checkChildren: boolean): Observable<FocusOrigin>;
77101
monitor(
78102
element: HTMLElement,
79-
renderer: Renderer2,
80-
checkChildren: boolean): Observable<FocusOrigin> {
103+
renderer: Renderer2 | boolean,
104+
checkChildren?: boolean): Observable<FocusOrigin> {
105+
// TODO(mmalerba): clean up after deprecated signature is removed.
106+
if (!(renderer instanceof Renderer2)) {
107+
checkChildren = renderer;
108+
}
109+
checkChildren = !!checkChildren;
110+
81111
// Do nothing if we're not on the browser platform.
82112
if (!this._platform.isBrowser) {
83113
return observableOf(null);
@@ -93,10 +123,10 @@ export class FocusMonitor {
93123
let info: MonitoredElementInfo = {
94124
unlisten: () => {},
95125
checkChildren: checkChildren,
96-
renderer: renderer,
97126
subject: new Subject<FocusOrigin>()
98127
};
99128
this._elementInfo.set(element, info);
129+
this._monitoredElementCount++;
100130

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

129159
this._setClasses(element);
130160
this._elementInfo.delete(element);
161+
this._monitoredElementCount--;
131162
}
132163
}
133164

@@ -142,49 +173,69 @@ export class FocusMonitor {
142173
}
143174

144175
/** Register necessary event listeners on the document and window. */
145-
private _registerDocumentEvents() {
176+
private _registerGlobalListeners() {
146177
// Do nothing if we're not on the browser platform.
147178
if (!this._platform.isBrowser) {
148179
return;
149180
}
150181

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

160188
// On mousedown record the origin only if there is not touch target, since a mousedown can
161189
// happen as a result of a touch event.
162-
document.addEventListener('mousedown', () => {
190+
let documentMousedownListener = () => {
163191
if (!this._lastTouchTarget) {
164192
this._setOriginForCurrentEventQueue('mouse');
165193
}
166-
}, true);
194+
};
167195

168196
// When the touchstart event fires the focus event is not yet in the event queue. This means
169197
// we can't rely on the trick used above (setting timeout of 0ms). Instead we wait 650ms to
170198
// see if a focus happens.
171-
document.addEventListener('touchstart', (event: TouchEvent) => {
199+
let documentTouchstartListener = (event: TouchEvent) => {
172200
if (this._touchTimeout != null) {
173201
clearTimeout(this._touchTimeout);
174202
}
175203
this._lastTouchTarget = event.target;
176204
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);
205+
};
181206

182207
// Make a note of when the window regains focus, so we can restore the origin info for the
183208
// focused element.
184-
window.addEventListener('focus', () => {
209+
let windowFocusListener = () => {
185210
this._windowFocused = true;
186211
setTimeout(() => this._windowFocused = false, 0);
212+
};
213+
214+
// Note: we listen to events in the capture phase so we can detect them even if the user stops
215+
// propagation.
216+
this._ngZone.runOutsideAngular(() => {
217+
document.addEventListener('keydown', documentKeydownListener, true);
218+
document.addEventListener('mousedown', documentMousedownListener, true);
219+
document.addEventListener('touchstart', documentTouchstartListener,
220+
supportsPassiveEventListeners() ? ({passive: true, capture: true} as any) : true);
221+
window.addEventListener('focus', windowFocusListener);
187222
});
223+
224+
this._unregisterGlobalListeners = () => {
225+
document.removeEventListener('keydown', documentKeydownListener, true);
226+
document.removeEventListener('mousedown', documentMousedownListener, true);
227+
document.removeEventListener('touchstart', documentTouchstartListener,
228+
supportsPassiveEventListeners() ? ({passive: true, capture: true} as any) : true);
229+
window.removeEventListener('focus', windowFocusListener);
230+
};
231+
}
232+
233+
private _toggleClass(element: Element, className: string, shouldSet: boolean) {
234+
if (shouldSet) {
235+
element.classList.add(className);
236+
} else {
237+
element.classList.remove(className);
238+
}
188239
}
189240

190241
/**
@@ -196,16 +247,11 @@ export class FocusMonitor {
196247
const elementInfo = this._elementInfo.get(element);
197248

198249
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');
250+
this._toggleClass(element, 'cdk-focused', !!origin);
251+
this._toggleClass(element, 'cdk-touch-focused', origin === 'touch');
252+
this._toggleClass(element, 'cdk-keyboard-focused', origin === 'keyboard');
253+
this._toggleClass(element, 'cdk-mouse-focused', origin === 'mouse');
254+
this._toggleClass(element, 'cdk-program-focused', origin === 'program');
209255
}
210256
}
211257

@@ -235,7 +281,7 @@ export class FocusMonitor {
235281
// result, this code will still consider it to have been caused by the touch event and will
236282
// apply the cdk-touch-focused class rather than the cdk-program-focused class. This is a
237283
// relatively small edge-case that can be worked around by using
238-
// focusVia(parentEl, renderer, 'program') to focus the parent element.
284+
// focusVia(parentEl, 'program') to focus the parent element.
239285
//
240286
// If we decide that we absolutely must handle this case correctly, we can do so by listening
241287
// for the first focus event after the touchstart, and then the first blur event after that
@@ -323,10 +369,9 @@ export class CdkMonitorFocus implements OnDestroy {
323369
private _monitorSubscription: Subscription;
324370
@Output() cdkFocusChange = new EventEmitter<FocusOrigin>();
325371

326-
constructor(private _elementRef: ElementRef, private _focusMonitor: FocusMonitor,
327-
renderer: Renderer2) {
372+
constructor(private _elementRef: ElementRef, private _focusMonitor: FocusMonitor) {
328373
this._monitorSubscription = this._focusMonitor.monitor(
329-
this._elementRef.nativeElement, renderer,
374+
this._elementRef.nativeElement,
330375
this._elementRef.nativeElement.hasAttribute('cdkMonitorSubtreeFocus'))
331376
.subscribe(origin => this.cdkFocusChange.emit(origin));
332377
}

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)