Skip to content

Commit 21a7c5c

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 21a7c5c

File tree

8 files changed

+149
-49
lines changed

8 files changed

+149
-49
lines changed

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

Lines changed: 30 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,25 @@ 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+
this._document = document;
148+
}
149+
150+
/** Access injected document if available or fallback to global document reference */
151+
get document(): Document {
152+
return this._document || document;
153+
}
154+
155+
/** Use defaultView of injected document if available or fallback to global window reference */
156+
get window(): Window {
157+
return this.document.defaultView || window;
158+
}
138159

139160
/**
140161
* Monitors focus on an element and applies appropriate CSS classes.
@@ -393,27 +414,27 @@ export class FocusMonitor implements OnDestroy {
393414
// Note: we listen to events in the capture phase so we
394415
// can detect them even if the user stops propagation.
395416
this._ngZone.runOutsideAngular(() => {
396-
document.addEventListener('keydown', this._documentKeydownListener,
417+
this.document.addEventListener('keydown', this._documentKeydownListener,
397418
captureEventListenerOptions);
398-
document.addEventListener('mousedown', this._documentMousedownListener,
419+
this.document.addEventListener('mousedown', this._documentMousedownListener,
399420
captureEventListenerOptions);
400-
document.addEventListener('touchstart', this._documentTouchstartListener,
421+
this.document.addEventListener('touchstart', this._documentTouchstartListener,
401422
captureEventListenerOptions);
402-
window.addEventListener('focus', this._windowFocusListener);
423+
this.window.addEventListener('focus', this._windowFocusListener);
403424
});
404425
}
405426
}
406427

407428
private _decrementMonitoredElementCount() {
408429
// Unregister global listeners when last element is unmonitored.
409430
if (!--this._monitoredElementCount) {
410-
document.removeEventListener('keydown', this._documentKeydownListener,
431+
this.document.removeEventListener('keydown', this._documentKeydownListener,
411432
captureEventListenerOptions);
412-
document.removeEventListener('mousedown', this._documentMousedownListener,
433+
this.document.removeEventListener('mousedown', this._documentMousedownListener,
413434
captureEventListenerOptions);
414-
document.removeEventListener('touchstart', this._documentTouchstartListener,
435+
this.document.removeEventListener('touchstart', this._documentTouchstartListener,
415436
captureEventListenerOptions);
416-
window.removeEventListener('focus', this._windowFocusListener);
437+
this.window.removeEventListener('focus', this._windowFocusListener);
417438

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

src/cdk/drag-drop/directives/drag.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1948,6 +1948,7 @@ describe('CdkDrag', () => {
19481948
// mode in unit tests and there are some issues with doing it in e2e tests.
19491949
const fakeDocument = {
19501950
body: document.body,
1951+
documentElement: document.documentElement,
19511952
fullscreenElement: document.createElement('div'),
19521953
ELEMENT_NODE: Node.ELEMENT_NODE,
19531954
querySelectorAll: function(...args: [string]) {

src/cdk/scrolling/scroll-dispatcher.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +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-
14+
import {DOCUMENT} from '@angular/common';
1515

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

2735
/** Subject for notifying that a registered scrollable reference element has been scrolled. */
2836
private _scrolled = new Subject<CdkScrollable|void>();
@@ -39,6 +47,16 @@ export class ScrollDispatcher implements OnDestroy {
3947
*/
4048
scrollContainers: Map<CdkScrollable, Subscription> = new Map();
4149

50+
/** Access injected document if available or fallback to global document reference */
51+
get document(): Document {
52+
return this._document || document;
53+
}
54+
55+
/** Use defaultView of injected document if available or fallback to global window reference */
56+
get window(): Window {
57+
return this.document.defaultView || window;
58+
}
59+
4260
/**
4361
* Registers a scrollable instance with the service and listens for its scrolled events. When the
4462
* scrollable is scrolled, the service emits the event to its scrolled observable.
@@ -153,7 +171,7 @@ export class ScrollDispatcher implements OnDestroy {
153171
/** Sets up the global scroll listeners. */
154172
private _addGlobalListener() {
155173
this._globalSubscription = this._ngZone.runOutsideAngular(() => {
156-
return fromEvent(window.document, 'scroll').subscribe(() => this._scrolled.next());
174+
return fromEvent(this.window.document, 'scroll').subscribe(() => this._scrolled.next());
157175
});
158176
}
159177

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 & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +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';
28-
30+
import {DOCUMENT} from '@angular/common';
2931

3032
/** Directive to automatically resize a textarea to fit its content. */
3133
@Directive({
@@ -89,13 +91,29 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy {
8991
/** Cached height of a textarea with a single row. */
9092
private _cachedLineHeight: number;
9193

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

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

126144
this._ngZone.runOutsideAngular(() => {
127-
fromEvent(window, 'resize')
145+
fromEvent(this.window, 'resize')
128146
.pipe(auditTime(16), takeUntil(this._destroyed))
129147
.subscribe(() => this.resizeToFitContent(true));
130148
});
@@ -277,7 +295,7 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy {
277295
// Also note that we have to assert that the textarea is focused before we set the
278296
// selection range. Setting the selection range on a non-focused textarea will cause
279297
// it to receive focus on IE and Edge.
280-
if (!this._destroyed.isStopped && document.activeElement === textarea) {
298+
if (!this._destroyed.isStopped && this.document.activeElement === textarea) {
281299
textarea.setSelectionRange(selectionStart, selectionEnd);
282300
}
283301
}

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)