Skip to content

Commit cdd400c

Browse files
committed
fix(document-injection): Update to use injected document
Update several classes in Material and the CDK to reference the injected document instead of accessing the global document and window variables. This fixes use-cases where the document is dynamically provided. This change follows the pattern used in several other places such as the Cdk OverlayContainer.
1 parent 59ae34b commit cdd400c

File tree

7 files changed

+150
-46
lines changed

7 files changed

+150
-46
lines changed

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

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@ import {
1515
NgZone,
1616
OnDestroy,
1717
Output,
18+
Optional,
19+
Inject,
1820
} from '@angular/core';
1921
import {Observable, of as observableOf, Subject, Subscription} from 'rxjs';
2022
import {coerceElement} from '@angular/cdk/coercion';
23+
import {DOCUMENT} from '@angular/common';
2124

2225

2326
// This is the value used by AngularJS Material. Through trial and error (on iPhone 6S) they found
@@ -134,7 +137,26 @@ export class FocusMonitor implements OnDestroy {
134137
this._windowFocusTimeoutId = setTimeout(() => this._windowFocused = false);
135138
}
136139

137-
constructor(private _ngZone: NgZone, private _platform: Platform) {}
140+
/** Used to reference correct document/window */
141+
protected _document?: Document;
142+
143+
constructor(private _ngZone: NgZone,
144+
private _platform: Platform,
145+
/** @breaking-change 11.0.0 make document required */
146+
@Optional() @Inject(DOCUMENT) document?: any
147+
) {
148+
this._document = document;
149+
}
150+
151+
/** Access injected document if available or fallback to global document reference */
152+
get document(): Document {
153+
return this._document || document;
154+
}
155+
156+
/** Use defaultView of injected document if available or fallback to global window reference */
157+
get window(): Window {
158+
return this.document.defaultView || window;
159+
}
138160

139161
/**
140162
* Monitors focus on an element and applies appropriate CSS classes.
@@ -393,27 +415,27 @@ export class FocusMonitor implements OnDestroy {
393415
// Note: we listen to events in the capture phase so we
394416
// can detect them even if the user stops propagation.
395417
this._ngZone.runOutsideAngular(() => {
396-
document.addEventListener('keydown', this._documentKeydownListener,
418+
this.document.addEventListener('keydown', this._documentKeydownListener,
397419
captureEventListenerOptions);
398-
document.addEventListener('mousedown', this._documentMousedownListener,
420+
this.document.addEventListener('mousedown', this._documentMousedownListener,
399421
captureEventListenerOptions);
400-
document.addEventListener('touchstart', this._documentTouchstartListener,
422+
this.document.addEventListener('touchstart', this._documentTouchstartListener,
401423
captureEventListenerOptions);
402-
window.addEventListener('focus', this._windowFocusListener);
424+
this.window.addEventListener('focus', this._windowFocusListener);
403425
});
404426
}
405427
}
406428

407429
private _decrementMonitoredElementCount() {
408430
// Unregister global listeners when last element is unmonitored.
409431
if (!--this._monitoredElementCount) {
410-
document.removeEventListener('keydown', this._documentKeydownListener,
432+
this.document.removeEventListener('keydown', this._documentKeydownListener,
411433
captureEventListenerOptions);
412-
document.removeEventListener('mousedown', this._documentMousedownListener,
434+
this.document.removeEventListener('mousedown', this._documentMousedownListener,
413435
captureEventListenerOptions);
414-
document.removeEventListener('touchstart', this._documentTouchstartListener,
436+
this.document.removeEventListener('touchstart', this._documentTouchstartListener,
415437
captureEventListenerOptions);
416-
window.removeEventListener('focus', this._windowFocusListener);
438+
this.window.removeEventListener('focus', this._windowFocusListener);
417439

418440
// Clear timeouts for all potentially pending timeouts to prevent the leaks.
419441
clearTimeout(this._windowFocusTimeoutId);

src/cdk/scrolling/scroll-dispatcher.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@
77
*/
88

99
import {Platform} from '@angular/cdk/platform';
10-
import {ElementRef, Injectable, NgZone, OnDestroy} from '@angular/core';
10+
import {ElementRef, Injectable, NgZone, OnDestroy, Optional, Inject} from '@angular/core';
1111
import {fromEvent, of as observableOf, Subject, Subscription, Observable, Observer} from 'rxjs';
1212
import {auditTime, filter} from 'rxjs/operators';
1313
import {CdkScrollable} from './scrollable';
14+
import {DOCUMENT} from '@angular/common';
1415

1516

1617
/** Time in ms to throttle the scrolling events by default. */
@@ -22,7 +23,16 @@ export const DEFAULT_SCROLL_TIME = 20;
2223
*/
2324
@Injectable({providedIn: 'root'})
2425
export class ScrollDispatcher implements OnDestroy {
25-
constructor(private _ngZone: NgZone, private _platform: Platform) { }
26+
27+
/** Used to reference correct document/window */
28+
protected _document?: Document;
29+
30+
constructor(private _ngZone: NgZone,
31+
private _platform: Platform,
32+
/** @breaking-change 11.0.0 make document required */
33+
@Optional() @Inject(DOCUMENT) document?: any) {
34+
this._document = document;
35+
}
2636

2737
/** Subject for notifying that a registered scrollable reference element has been scrolled. */
2838
private _scrolled = new Subject<CdkScrollable|void>();
@@ -39,6 +49,16 @@ export class ScrollDispatcher implements OnDestroy {
3949
*/
4050
scrollContainers: Map<CdkScrollable, Subscription> = new Map();
4151

52+
/** Access injected document if available or fallback to global document reference */
53+
get document(): Document {
54+
return this._document || document;
55+
}
56+
57+
/** Use defaultView of injected document if available or fallback to global window reference */
58+
get window(): Window {
59+
return this.document.defaultView || window;
60+
}
61+
4262
/**
4363
* Registers a scrollable instance with the service and listens for its scrolled events. When the
4464
* scrollable is scrolled, the service emits the event to its scrolled observable.
@@ -153,7 +173,7 @@ export class ScrollDispatcher implements OnDestroy {
153173
/** Sets up the global scroll listeners. */
154174
private _addGlobalListener() {
155175
this._globalSubscription = this._ngZone.runOutsideAngular(() => {
156-
return fromEvent(window.document, 'scroll').subscribe(() => this._scrolled.next());
176+
return fromEvent(this.window.document, 'scroll').subscribe(() => this._scrolled.next());
157177
});
158178
}
159179

src/cdk/scrolling/viewport-ruler.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@
77
*/
88

99
import {Platform} from '@angular/cdk/platform';
10-
import {Injectable, NgZone, OnDestroy} from '@angular/core';
10+
import {Injectable, NgZone, OnDestroy, Optional, Inject} from '@angular/core';
1111
import {merge, of as observableOf, fromEvent, Observable, Subscription} from 'rxjs';
1212
import {auditTime} from 'rxjs/operators';
13+
import {DOCUMENT} from '@angular/common';
1314

1415
/** Time in ms to throttle the resize events by default. */
1516
export const DEFAULT_RESIZE_TIME = 20;
@@ -35,10 +36,18 @@ export class ViewportRuler implements OnDestroy {
3536
/** Subscription to streams that invalidate the cached viewport dimensions. */
3637
private _invalidateCache: Subscription;
3738

38-
constructor(private _platform: Platform, ngZone: NgZone) {
39+
/** Used to reference correct document/window */
40+
protected _document?: Document;
41+
42+
constructor(private _platform: Platform,
43+
ngZone: NgZone,
44+
/** @breaking-change 11.0.0 make document required */
45+
@Optional() @Inject(DOCUMENT) document?: any) {
46+
this._document = document;
47+
3948
ngZone.runOutsideAngular(() => {
4049
this._change = _platform.isBrowser ?
41-
merge(fromEvent(window, 'resize'), fromEvent(window, 'orientationchange')) :
50+
merge(fromEvent(this.window, 'resize'), fromEvent(this.window, 'orientationchange')) :
4251
observableOf();
4352

4453
// Note that we need to do the subscription inside `runOutsideAngular`
@@ -47,6 +56,16 @@ export class ViewportRuler implements OnDestroy {
4756
});
4857
}
4958

59+
/** Access injected document if available or fallback to global document reference */
60+
get document(): Document {
61+
return this._document || document;
62+
}
63+
64+
/** Use defaultView of injected document if available or fallback to global window reference */
65+
get window(): Window {
66+
return this.document.defaultView || window;
67+
}
68+
5069
ngOnDestroy() {
5170
this._invalidateCache.unsubscribe();
5271
}
@@ -105,13 +124,13 @@ export class ViewportRuler implements OnDestroy {
105124
// `scrollTop` and `scrollLeft` is inconsistent. However, using the bounding rect of
106125
// `document.documentElement` works consistently, where the `top` and `left` values will
107126
// equal negative the scroll position.
108-
const documentElement = document.documentElement!;
127+
const documentElement = this.document.documentElement!;
109128
const documentRect = documentElement.getBoundingClientRect();
110129

111-
const top = -documentRect.top || document.body.scrollTop || window.scrollY ||
130+
const top = -documentRect.top || this.document.body.scrollTop || this.window.scrollY ||
112131
documentElement.scrollTop || 0;
113132

114-
const left = -documentRect.left || document.body.scrollLeft || window.scrollX ||
133+
const left = -documentRect.left || this.document.body.scrollLeft || this.window.scrollX ||
115134
documentElement.scrollLeft || 0;
116135

117136
return {top, left};
@@ -128,7 +147,7 @@ export class ViewportRuler implements OnDestroy {
128147
/** Updates the cached viewport size. */
129148
private _updateViewportSize() {
130149
this._viewportSize = this._platform.isBrowser ?
131-
{width: window.innerWidth, height: window.innerHeight} :
150+
{width: this.window.innerWidth, height: this.window.innerHeight} :
132151
{width: 0, height: 0};
133152
}
134153
}

src/cdk/text-field/autosize.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,13 @@ import {
2121
OnDestroy,
2222
NgZone,
2323
HostListener,
24+
Optional,
25+
Inject,
2426
} from '@angular/core';
2527
import {Platform} from '@angular/cdk/platform';
2628
import {auditTime, takeUntil} from 'rxjs/operators';
2729
import {fromEvent, Subject} from 'rxjs';
30+
import {DOCUMENT} from '@angular/common';
2831

2932

3033
/** Directive to automatically resize a textarea to fit its content. */
@@ -89,13 +92,29 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy {
8992
/** Cached height of a textarea with a single row. */
9093
private _cachedLineHeight: number;
9194

92-
constructor(
93-
private _elementRef: ElementRef<HTMLElement>,
94-
private _platform: Platform,
95-
private _ngZone: NgZone) {
95+
/** Used to reference correct document/window */
96+
protected _document?: Document;
97+
98+
constructor(private _elementRef: ElementRef<HTMLElement>,
99+
private _platform: Platform,
100+
private _ngZone: NgZone,
101+
/** @breaking-change 11.0.0 make document required */
102+
@Optional() @Inject(DOCUMENT) document?: any) {
103+
this._document = document;
104+
96105
this._textareaElement = this._elementRef.nativeElement as HTMLTextAreaElement;
97106
}
98107

108+
/** Access injected document if available or fallback to global document reference */
109+
get document(): Document {
110+
return this._document || document;
111+
}
112+
113+
/** Use defaultView of injected document if available or fallback to global window reference */
114+
get window(): Window {
115+
return this.document.defaultView || window;
116+
}
117+
99118
/** Sets the minimum height of the textarea as determined by minRows. */
100119
_setMinHeight(): void {
101120
const minHeight = this.minRows && this._cachedLineHeight ?
@@ -124,7 +143,7 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy {
124143
this.resizeToFitContent();
125144

126145
this._ngZone.runOutsideAngular(() => {
127-
fromEvent(window, 'resize')
146+
fromEvent(this.window, 'resize')
128147
.pipe(auditTime(16), takeUntil(this._destroyed))
129148
.subscribe(() => this.resizeToFitContent(true));
130149
});
@@ -277,7 +296,7 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy {
277296
// Also note that we have to assert that the textarea is focused before we set the
278297
// selection range. Setting the selection range on a non-focused textarea will cause
279298
// it to receive focus on IE and Edge.
280-
if (!this._destroyed.isStopped && document.activeElement === textarea) {
299+
if (!this._destroyed.isStopped && this.document.activeElement === textarea) {
281300
textarea.setSelectionRange(selectionStart, selectionEnd);
282301
}
283302
}

src/material/autocomplete/autocomplete-trigger.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -225,9 +225,9 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, AfterViewIn
225225
}
226226

227227
ngAfterViewInit() {
228-
if (typeof window !== 'undefined') {
228+
if (typeof this.window !== 'undefined') {
229229
this._zone.runOutsideAngular(() => {
230-
window.addEventListener('blur', this._windowBlurHandler);
230+
this.window.addEventListener('blur', this._windowBlurHandler);
231231
});
232232

233233
if (_supportsShadowDom()) {
@@ -252,8 +252,8 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, AfterViewIn
252252
}
253253

254254
ngOnDestroy() {
255-
if (typeof window !== 'undefined') {
256-
window.removeEventListener('blur', this._windowBlurHandler);
255+
if (typeof this.window !== 'undefined') {
256+
this.window.removeEventListener('blur', this._windowBlurHandler);
257257
}
258258

259259
this._viewportSubscription.unsubscribe();
@@ -262,6 +262,11 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, AfterViewIn
262262
this._closeKeyEventStream.complete();
263263
}
264264

265+
/** Use defaultView of injected document if available or fallback to global window reference */
266+
get window(): Window {
267+
return this._document?.defaultView || window;
268+
}
269+
265270
/** Whether or not the autocomplete panel is open. */
266271
get panelOpen(): boolean {
267272
return this._overlayAttached && this.autocomplete.showPanel;

0 commit comments

Comments
 (0)