Skip to content

Commit 204db56

Browse files
authored
fix(document-injection): Update to use injected document (#18780)
* 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. * fix(document-injection): Update to use injected document Utilize private functions for _getWindow() and _getDocument() instead of using public getters. * fix(document-injection): Update to use injected document Update golden files to reflect API changes.
1 parent 26ab43b commit 204db56

File tree

12 files changed

+172
-30
lines changed

12 files changed

+172
-30
lines changed

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

Lines changed: 29 additions & 1 deletion
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,15 @@ 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+
}
138149

139150
/**
140151
* Monitors focus on an element and applies appropriate CSS classes.
@@ -257,6 +268,17 @@ export class FocusMonitor implements OnDestroy {
257268
this._elementInfo.forEach((_info, element) => this.stopMonitoring(element));
258269
}
259270

271+
/** Access injected document if available or fallback to global document reference */
272+
private _getDocument(): Document {
273+
return this._document || document;
274+
}
275+
276+
/** Use defaultView of injected document if available or fallback to global window reference */
277+
private _getWindow(): Window {
278+
const doc = this._getDocument();
279+
return doc.defaultView || window;
280+
}
281+
260282
private _toggleClass(element: Element, className: string, shouldSet: boolean) {
261283
if (shouldSet) {
262284
element.classList.add(className);
@@ -393,6 +415,9 @@ 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(() => {
418+
const document = this._getDocument();
419+
const window = this._getWindow();
420+
396421
document.addEventListener('keydown', this._documentKeydownListener,
397422
captureEventListenerOptions);
398423
document.addEventListener('mousedown', this._documentMousedownListener,
@@ -407,6 +432,9 @@ export class FocusMonitor implements OnDestroy {
407432
private _decrementMonitoredElementCount() {
408433
// Unregister global listeners when last element is unmonitored.
409434
if (!--this._monitoredElementCount) {
435+
const document = this._getDocument();
436+
const window = this._getWindow();
437+
410438
document.removeEventListener('keydown', this._documentKeydownListener,
411439
captureEventListenerOptions);
412440
document.removeEventListener('mousedown', this._documentMousedownListener,

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: 23 additions & 3 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>();
@@ -136,6 +144,17 @@ export class ScrollDispatcher implements OnDestroy {
136144
return scrollingContainers;
137145
}
138146

147+
/** Access injected document if available or fallback to global document reference */
148+
private _getDocument(): Document {
149+
return this._document || document;
150+
}
151+
152+
/** Use defaultView of injected document if available or fallback to global window reference */
153+
private _getWindow(): Window {
154+
const doc = this._getDocument();
155+
return doc.defaultView || window;
156+
}
157+
139158
/** Returns true if the element is contained within the provided Scrollable. */
140159
private _scrollableContainsElement(scrollable: CdkScrollable, elementRef: ElementRef): boolean {
141160
let element: HTMLElement | null = elementRef.nativeElement;
@@ -153,6 +172,7 @@ export class ScrollDispatcher implements OnDestroy {
153172
/** Sets up the global scroll listeners. */
154173
private _addGlobalListener() {
155174
this._globalSubscription = this._ngZone.runOutsideAngular(() => {
175+
const window = this._getWindow();
156176
return fromEvent(window.document, 'scroll').subscribe(() => this._scrolled.next());
157177
});
158178
}

src/cdk/scrolling/viewport-ruler.ts

Lines changed: 27 additions & 2 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,8 +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(() => {
49+
const window = this._getWindow();
50+
4051
this._change = _platform.isBrowser ?
4152
merge(fromEvent(window, 'resize'), fromEvent(window, 'orientationchange')) :
4253
observableOf();
@@ -105,6 +116,8 @@ export class ViewportRuler implements OnDestroy {
105116
// `scrollTop` and `scrollLeft` is inconsistent. However, using the bounding rect of
106117
// `document.documentElement` works consistently, where the `top` and `left` values will
107118
// equal negative the scroll position.
119+
const document = this._getDocument();
120+
const window = this._getWindow();
108121
const documentElement = document.documentElement!;
109122
const documentRect = documentElement.getBoundingClientRect();
110123

@@ -125,8 +138,20 @@ export class ViewportRuler implements OnDestroy {
125138
return throttleTime > 0 ? this._change.pipe(auditTime(throttleTime)) : this._change;
126139
}
127140

141+
/** Access injected document if available or fallback to global document reference */
142+
private _getDocument(): Document {
143+
return this._document || document;
144+
}
145+
146+
/** Use defaultView of injected document if available or fallback to global window reference */
147+
private _getWindow(): Window {
148+
const doc = this._getDocument();
149+
return doc.defaultView || window;
150+
}
151+
128152
/** Updates the cached viewport size. */
129153
private _updateViewportSize() {
154+
const window = this._getWindow();
130155
this._viewportSize = this._platform.isBrowser ?
131156
{width: window.innerWidth, height: window.innerHeight} :
132157
{width: 0, height: 0};

src/cdk/text-field/autosize.ts

Lines changed: 27 additions & 5 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,10 +91,16 @@ 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

@@ -124,6 +132,8 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy {
124132
this.resizeToFitContent();
125133

126134
this._ngZone.runOutsideAngular(() => {
135+
const window = this._getWindow();
136+
127137
fromEvent(window, 'resize')
128138
.pipe(auditTime(16), takeUntil(this._destroyed))
129139
.subscribe(() => this.resizeToFitContent(true));
@@ -263,13 +273,25 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy {
263273
// no-op handler that ensures we're running change detection on input events.
264274
}
265275

276+
/** Access injected document if available or fallback to global document reference */
277+
private _getDocument(): Document {
278+
return this._document || document;
279+
}
280+
281+
/** Use defaultView of injected document if available or fallback to global window reference */
282+
private _getWindow(): Window {
283+
const doc = this._getDocument();
284+
return doc.defaultView || window;
285+
}
286+
266287
/**
267288
* Scrolls a textarea to the caret position. On Firefox resizing the textarea will
268289
* prevent it from scrolling to the caret position. We need to re-set the selection
269290
* in order for it to scroll to the proper position.
270291
*/
271292
private _scrollToCaretPosition(textarea: HTMLTextAreaElement) {
272293
const {selectionStart, selectionEnd} = textarea;
294+
const document = this._getDocument();
273295

274296
// IE will throw an "Unspecified error" if we try to set the selection range after the
275297
// element has been removed from the DOM. Assert that the directive hasn't been destroyed

src/material/autocomplete/autocomplete-trigger.ts

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

227227
ngAfterViewInit() {
228+
const window = this._getWindow();
229+
228230
if (typeof window !== 'undefined') {
229231
this._zone.runOutsideAngular(() => {
230232
window.addEventListener('blur', this._windowBlurHandler);
@@ -245,6 +247,8 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, AfterViewIn
245247
}
246248

247249
ngOnDestroy() {
250+
const window = this._getWindow();
251+
248252
if (typeof window !== 'undefined') {
249253
window.removeEventListener('blur', this._windowBlurHandler);
250254
}
@@ -752,5 +756,10 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, AfterViewIn
752756
return !element.readOnly && !element.disabled && !this._autocompleteDisabled;
753757
}
754758

759+
/** Use defaultView of injected document if available or fallback to global window reference */
760+
private _getWindow(): Window {
761+
return this._document?.defaultView || window;
762+
}
763+
755764
static ngAcceptInputType_autocompleteDisabled: BooleanInput;
756765
}

src/material/core/common-behaviors/common-module.ts

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {HighContrastModeDetector} from '@angular/cdk/a11y';
1010
import {BidiModule} from '@angular/cdk/bidi';
1111
import {Inject, InjectionToken, isDevMode, NgModule, Optional, Version} from '@angular/core';
1212
import {VERSION as CDK_VERSION} from '@angular/cdk';
13-
13+
import {DOCUMENT} from '@angular/common';
1414

1515
// Private version constant to circumvent test/build issues,
1616
// i.e. avoid core to depend on the @angular/material primary entry-point
@@ -62,18 +62,19 @@ export class MatCommonModule {
6262
/** Whether we've done the global sanity checks (e.g. a theme is loaded, there is a doctype). */
6363
private _hasDoneGlobalChecks = false;
6464

65-
/** Reference to the global `document` object. */
66-
private _document = typeof document === 'object' && document ? document : null;
67-
68-
/** Reference to the global 'window' object. */
69-
private _window = typeof window === 'object' && window ? window : null;
70-
7165
/** Configured sanity checks. */
7266
private _sanityChecks: SanityChecks;
7367

68+
/** Used to reference correct document/window */
69+
protected _document?: Document;
70+
7471
constructor(
7572
highContrastModeDetector: HighContrastModeDetector,
76-
@Optional() @Inject(MATERIAL_SANITY_CHECKS) sanityChecks: any) {
73+
@Optional() @Inject(MATERIAL_SANITY_CHECKS) sanityChecks: any,
74+
/** @breaking-change 11.0.0 make document required */
75+
@Optional() @Inject(DOCUMENT) document?: any) {
76+
this._document = document;
77+
7778
// While A11yModule also does this, we repeat it here to avoid importing A11yModule
7879
// in MatCommonModule.
7980
highContrastModeDetector._applyBodyHighContrastModeCssClasses();
@@ -90,22 +91,36 @@ export class MatCommonModule {
9091
}
9192
}
9293

94+
/** Access injected document if available or fallback to global document reference */
95+
private _getDocument(): Document | null {
96+
const doc = this._document || document;
97+
return typeof doc === 'object' && doc ? doc : null;
98+
}
99+
100+
/** Use defaultView of injected document if available or fallback to global window reference */
101+
private _getWindow(): Window | null {
102+
const doc = this._getDocument();
103+
const win = doc?.defaultView || window;
104+
return typeof win === 'object' && win ? win : null;
105+
}
106+
93107
/** Whether any sanity checks are enabled. */
94108
private _checksAreEnabled(): boolean {
95109
return isDevMode() && !this._isTestEnv();
96110
}
97111

98112
/** Whether the code is running in tests. */
99113
private _isTestEnv() {
100-
const window = this._window as any;
114+
const window = this._getWindow() as any;
101115
return window && (window.__karma__ || window.jasmine);
102116
}
103117

104118
private _checkDoctypeIsDefined(): void {
105119
const isEnabled = this._checksAreEnabled() &&
106120
(this._sanityChecks === true || (this._sanityChecks as GranularSanityChecks).doctype);
121+
const document = this._getDocument();
107122

108-
if (isEnabled && this._document && !this._document.doctype) {
123+
if (isEnabled && document && !document.doctype) {
109124
console.warn(
110125
'Current document does not have a doctype. This may cause ' +
111126
'some Angular Material components not to behave as expected.'
@@ -118,16 +133,17 @@ export class MatCommonModule {
118133
// and the `body` won't be defined if the consumer put their scripts in the `head`.
119134
const isDisabled = !this._checksAreEnabled() ||
120135
(this._sanityChecks === false || !(this._sanityChecks as GranularSanityChecks).theme);
136+
const document = this._getDocument();
121137

122-
if (isDisabled || !this._document || !this._document.body ||
138+
if (isDisabled || !document || !document.body ||
123139
typeof getComputedStyle !== 'function') {
124140
return;
125141
}
126142

127-
const testElement = this._document.createElement('div');
143+
const testElement = document.createElement('div');
128144

129145
testElement.classList.add('mat-theme-loaded-marker');
130-
this._document.body.appendChild(testElement);
146+
document.body.appendChild(testElement);
131147

132148
const computedStyle = getComputedStyle(testElement);
133149

@@ -142,7 +158,7 @@ export class MatCommonModule {
142158
);
143159
}
144160

145-
this._document.body.removeChild(testElement);
161+
document.body.removeChild(testElement);
146162
}
147163

148164
/** Checks whether the material version matches the cdk version */

0 commit comments

Comments
 (0)