Skip to content

fix(document-injection): Update to use injected document #18780

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion src/cdk/a11y/focus-monitor/focus-monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ import {
NgZone,
OnDestroy,
Output,
Optional,
Inject,
} from '@angular/core';
import {Observable, of as observableOf, Subject, Subscription} from 'rxjs';
import {coerceElement} from '@angular/cdk/coercion';
import {DOCUMENT} from '@angular/common';


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

constructor(private _ngZone: NgZone, private _platform: Platform) {}
/** Used to reference correct document/window */
protected _document?: Document;

constructor(private _ngZone: NgZone,
private _platform: Platform,
/** @breaking-change 11.0.0 make document required */
@Optional() @Inject(DOCUMENT) document?: any) {
this._document = document;
}

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

/** Access injected document if available or fallback to global document reference */
private _getDocument(): Document {
return this._document || document;
}

/** Use defaultView of injected document if available or fallback to global window reference */
private _getWindow(): Window {
const doc = this._getDocument();
return doc.defaultView || window;
}

private _toggleClass(element: Element, className: string, shouldSet: boolean) {
if (shouldSet) {
element.classList.add(className);
Expand Down Expand Up @@ -393,6 +415,9 @@ export class FocusMonitor implements OnDestroy {
// Note: we listen to events in the capture phase so we
// can detect them even if the user stops propagation.
this._ngZone.runOutsideAngular(() => {
const document = this._getDocument();
const window = this._getWindow();

document.addEventListener('keydown', this._documentKeydownListener,
captureEventListenerOptions);
document.addEventListener('mousedown', this._documentMousedownListener,
Expand All @@ -407,6 +432,9 @@ export class FocusMonitor implements OnDestroy {
private _decrementMonitoredElementCount() {
// Unregister global listeners when last element is unmonitored.
if (!--this._monitoredElementCount) {
const document = this._getDocument();
const window = this._getWindow();

document.removeEventListener('keydown', this._documentKeydownListener,
captureEventListenerOptions);
document.removeEventListener('mousedown', this._documentMousedownListener,
Expand Down
1 change: 1 addition & 0 deletions src/cdk/drag-drop/directives/drag.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1948,6 +1948,7 @@ describe('CdkDrag', () => {
// mode in unit tests and there are some issues with doing it in e2e tests.
const fakeDocument = {
body: document.body,
documentElement: document.documentElement,
fullscreenElement: document.createElement('div'),
ELEMENT_NODE: Node.ELEMENT_NODE,
querySelectorAll: function(...args: [string]) {
Expand Down
26 changes: 23 additions & 3 deletions src/cdk/scrolling/scroll-dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
*/

import {Platform} from '@angular/cdk/platform';
import {ElementRef, Injectable, NgZone, OnDestroy} from '@angular/core';
import {ElementRef, Injectable, NgZone, OnDestroy, Optional, Inject} from '@angular/core';
import {fromEvent, of as observableOf, Subject, Subscription, Observable, Observer} from 'rxjs';
import {auditTime, filter} from 'rxjs/operators';
import {CdkScrollable} from './scrollable';

import {DOCUMENT} from '@angular/common';

/** Time in ms to throttle the scrolling events by default. */
export const DEFAULT_SCROLL_TIME = 20;
Expand All @@ -22,7 +22,15 @@ export const DEFAULT_SCROLL_TIME = 20;
*/
@Injectable({providedIn: 'root'})
export class ScrollDispatcher implements OnDestroy {
constructor(private _ngZone: NgZone, private _platform: Platform) { }
/** Used to reference correct document/window */
protected _document?: Document;

constructor(private _ngZone: NgZone,
private _platform: Platform,
/** @breaking-change 11.0.0 make document required */
@Optional() @Inject(DOCUMENT) document?: any) {
this._document = document;
}

/** Subject for notifying that a registered scrollable reference element has been scrolled. */
private _scrolled = new Subject<CdkScrollable|void>();
Expand Down Expand Up @@ -136,6 +144,17 @@ export class ScrollDispatcher implements OnDestroy {
return scrollingContainers;
}

/** Access injected document if available or fallback to global document reference */
private _getDocument(): Document {
return this._document || document;
}

/** Use defaultView of injected document if available or fallback to global window reference */
private _getWindow(): Window {
const doc = this._getDocument();
return doc.defaultView || window;
}

/** Returns true if the element is contained within the provided Scrollable. */
private _scrollableContainsElement(scrollable: CdkScrollable, elementRef: ElementRef): boolean {
let element: HTMLElement | null = elementRef.nativeElement;
Expand All @@ -153,6 +172,7 @@ export class ScrollDispatcher implements OnDestroy {
/** Sets up the global scroll listeners. */
private _addGlobalListener() {
this._globalSubscription = this._ngZone.runOutsideAngular(() => {
const window = this._getWindow();
return fromEvent(window.document, 'scroll').subscribe(() => this._scrolled.next());
});
}
Expand Down
29 changes: 27 additions & 2 deletions src/cdk/scrolling/viewport-ruler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
*/

import {Platform} from '@angular/cdk/platform';
import {Injectable, NgZone, OnDestroy} from '@angular/core';
import {Injectable, NgZone, OnDestroy, Optional, Inject} from '@angular/core';
import {merge, of as observableOf, fromEvent, Observable, Subscription} from 'rxjs';
import {auditTime} from 'rxjs/operators';
import {DOCUMENT} from '@angular/common';

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

constructor(private _platform: Platform, ngZone: NgZone) {
/** Used to reference correct document/window */
protected _document?: Document;

constructor(private _platform: Platform,
ngZone: NgZone,
/** @breaking-change 11.0.0 make document required */
@Optional() @Inject(DOCUMENT) document?: any) {
this._document = document;

ngZone.runOutsideAngular(() => {
const window = this._getWindow();

this._change = _platform.isBrowser ?
merge(fromEvent(window, 'resize'), fromEvent(window, 'orientationchange')) :
observableOf();
Expand Down Expand Up @@ -105,6 +116,8 @@ export class ViewportRuler implements OnDestroy {
// `scrollTop` and `scrollLeft` is inconsistent. However, using the bounding rect of
// `document.documentElement` works consistently, where the `top` and `left` values will
// equal negative the scroll position.
const document = this._getDocument();
const window = this._getWindow();
const documentElement = document.documentElement!;
const documentRect = documentElement.getBoundingClientRect();

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

/** Access injected document if available or fallback to global document reference */
private _getDocument(): Document {
return this._document || document;
}

/** Use defaultView of injected document if available or fallback to global window reference */
private _getWindow(): Window {
const doc = this._getDocument();
return doc.defaultView || window;
}

/** Updates the cached viewport size. */
private _updateViewportSize() {
const window = this._getWindow();
this._viewportSize = this._platform.isBrowser ?
{width: window.innerWidth, height: window.innerHeight} :
{width: 0, height: 0};
Expand Down
32 changes: 27 additions & 5 deletions src/cdk/text-field/autosize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ import {
OnDestroy,
NgZone,
HostListener,
Optional,
Inject,
} from '@angular/core';
import {Platform} from '@angular/cdk/platform';
import {auditTime, takeUntil} from 'rxjs/operators';
import {fromEvent, Subject} from 'rxjs';

import {DOCUMENT} from '@angular/common';

/** Directive to automatically resize a textarea to fit its content. */
@Directive({
Expand Down Expand Up @@ -89,10 +91,16 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy {
/** Cached height of a textarea with a single row. */
private _cachedLineHeight: number;

constructor(
private _elementRef: ElementRef<HTMLElement>,
private _platform: Platform,
private _ngZone: NgZone) {
/** Used to reference correct document/window */
protected _document?: Document;

constructor(private _elementRef: ElementRef<HTMLElement>,
private _platform: Platform,
private _ngZone: NgZone,
/** @breaking-change 11.0.0 make document required */
@Optional() @Inject(DOCUMENT) document?: any) {
this._document = document;

this._textareaElement = this._elementRef.nativeElement as HTMLTextAreaElement;
}

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

this._ngZone.runOutsideAngular(() => {
const window = this._getWindow();

fromEvent(window, 'resize')
.pipe(auditTime(16), takeUntil(this._destroyed))
.subscribe(() => this.resizeToFitContent(true));
Expand Down Expand Up @@ -263,13 +273,25 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy {
// no-op handler that ensures we're running change detection on input events.
}

/** Access injected document if available or fallback to global document reference */
private _getDocument(): Document {
return this._document || document;
}

/** Use defaultView of injected document if available or fallback to global window reference */
private _getWindow(): Window {
const doc = this._getDocument();
return doc.defaultView || window;
}

/**
* Scrolls a textarea to the caret position. On Firefox resizing the textarea will
* prevent it from scrolling to the caret position. We need to re-set the selection
* in order for it to scroll to the proper position.
*/
private _scrollToCaretPosition(textarea: HTMLTextAreaElement) {
const {selectionStart, selectionEnd} = textarea;
const document = this._getDocument();

// IE will throw an "Unspecified error" if we try to set the selection range after the
// element has been removed from the DOM. Assert that the directive hasn't been destroyed
Expand Down
9 changes: 9 additions & 0 deletions src/material/autocomplete/autocomplete-trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, AfterViewIn
}

ngAfterViewInit() {
const window = this._getWindow();

if (typeof window !== 'undefined') {
this._zone.runOutsideAngular(() => {
window.addEventListener('blur', this._windowBlurHandler);
Expand Down Expand Up @@ -252,6 +254,8 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, AfterViewIn
}

ngOnDestroy() {
const window = this._getWindow();

if (typeof window !== 'undefined') {
window.removeEventListener('blur', this._windowBlurHandler);
}
Expand Down Expand Up @@ -759,5 +763,10 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, AfterViewIn
return !element.readOnly && !element.disabled && !this._autocompleteDisabled;
}

/** Use defaultView of injected document if available or fallback to global window reference */
private _getWindow(): Window {
return this._document?.defaultView || window;
}

static ngAcceptInputType_autocompleteDisabled: BooleanInput;
}
44 changes: 30 additions & 14 deletions src/material/core/common-behaviors/common-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {HighContrastModeDetector} from '@angular/cdk/a11y';
import {BidiModule} from '@angular/cdk/bidi';
import {Inject, InjectionToken, isDevMode, NgModule, Optional, Version} from '@angular/core';
import {VERSION as CDK_VERSION} from '@angular/cdk';

import {DOCUMENT} from '@angular/common';

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

/** Reference to the global `document` object. */
private _document = typeof document === 'object' && document ? document : null;

/** Reference to the global 'window' object. */
private _window = typeof window === 'object' && window ? window : null;

/** Configured sanity checks. */
private _sanityChecks: SanityChecks;

/** Used to reference correct document/window */
protected _document?: Document;

constructor(
highContrastModeDetector: HighContrastModeDetector,
@Optional() @Inject(MATERIAL_SANITY_CHECKS) sanityChecks: any) {
@Optional() @Inject(MATERIAL_SANITY_CHECKS) sanityChecks: any,
/** @breaking-change 11.0.0 make document required */
@Optional() @Inject(DOCUMENT) document?: any) {
this._document = document;

// While A11yModule also does this, we repeat it here to avoid importing A11yModule
// in MatCommonModule.
highContrastModeDetector._applyBodyHighContrastModeCssClasses();
Expand All @@ -90,22 +91,36 @@ export class MatCommonModule {
}
}

/** Access injected document if available or fallback to global document reference */
private _getDocument(): Document | null {
const doc = this._document || document;
return typeof doc === 'object' && doc ? doc : null;
}

/** Use defaultView of injected document if available or fallback to global window reference */
private _getWindow(): Window | null {
const doc = this._getDocument();
const win = doc?.defaultView || window;
return typeof win === 'object' && win ? win : null;
}

/** Whether any sanity checks are enabled. */
private _checksAreEnabled(): boolean {
return isDevMode() && !this._isTestEnv();
}

/** Whether the code is running in tests. */
private _isTestEnv() {
const window = this._window as any;
const window = this._getWindow() as any;
return window && (window.__karma__ || window.jasmine);
}

private _checkDoctypeIsDefined(): void {
const isEnabled = this._checksAreEnabled() &&
(this._sanityChecks === true || (this._sanityChecks as GranularSanityChecks).doctype);
const document = this._getDocument();

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

if (isDisabled || !this._document || !this._document.body ||
if (isDisabled || !document || !document.body ||
typeof getComputedStyle !== 'function') {
return;
}

const testElement = this._document.createElement('div');
const testElement = document.createElement('div');

testElement.classList.add('mat-theme-loaded-marker');
this._document.body.appendChild(testElement);
document.body.appendChild(testElement);

const computedStyle = getComputedStyle(testElement);

Expand All @@ -142,7 +158,7 @@ export class MatCommonModule {
);
}

this._document.body.removeChild(testElement);
document.body.removeChild(testElement);
}

/** Checks whether the material version matches the cdk version */
Expand Down
Loading