diff --git a/src/cdk/a11y/focus-monitor/focus-monitor.ts b/src/cdk/a11y/focus-monitor/focus-monitor.ts index 68b0daa7bf6d..7bfb4950245f 100644 --- a/src/cdk/a11y/focus-monitor/focus-monitor.ts +++ b/src/cdk/a11y/focus-monitor/focus-monitor.ts @@ -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 @@ -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. @@ -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); @@ -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, @@ -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, diff --git a/src/cdk/drag-drop/directives/drag.spec.ts b/src/cdk/drag-drop/directives/drag.spec.ts index 1b13b3205392..e735ea1e5906 100644 --- a/src/cdk/drag-drop/directives/drag.spec.ts +++ b/src/cdk/drag-drop/directives/drag.spec.ts @@ -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]) { diff --git a/src/cdk/scrolling/scroll-dispatcher.ts b/src/cdk/scrolling/scroll-dispatcher.ts index 425770f4b170..c3aed0b9eb52 100644 --- a/src/cdk/scrolling/scroll-dispatcher.ts +++ b/src/cdk/scrolling/scroll-dispatcher.ts @@ -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; @@ -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(); @@ -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; @@ -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()); }); } diff --git a/src/cdk/scrolling/viewport-ruler.ts b/src/cdk/scrolling/viewport-ruler.ts index a5399abff392..6281aa66b166 100644 --- a/src/cdk/scrolling/viewport-ruler.ts +++ b/src/cdk/scrolling/viewport-ruler.ts @@ -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; @@ -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(); @@ -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(); @@ -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}; diff --git a/src/cdk/text-field/autosize.ts b/src/cdk/text-field/autosize.ts index 673ec39fc0ea..98486a71f4cd 100644 --- a/src/cdk/text-field/autosize.ts +++ b/src/cdk/text-field/autosize.ts @@ -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({ @@ -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, - private _platform: Platform, - private _ngZone: NgZone) { + /** Used to reference correct document/window */ + protected _document?: Document; + + constructor(private _elementRef: ElementRef, + 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; } @@ -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)); @@ -263,6 +273,17 @@ 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 @@ -270,6 +291,7 @@ export class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy { */ 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 diff --git a/src/material/autocomplete/autocomplete-trigger.ts b/src/material/autocomplete/autocomplete-trigger.ts index a7cb88cb2d13..be1f4dc86fbd 100644 --- a/src/material/autocomplete/autocomplete-trigger.ts +++ b/src/material/autocomplete/autocomplete-trigger.ts @@ -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); @@ -245,6 +247,8 @@ export class MatAutocompleteTrigger implements ControlValueAccessor, AfterViewIn } ngOnDestroy() { + const window = this._getWindow(); + if (typeof window !== 'undefined') { window.removeEventListener('blur', this._windowBlurHandler); } @@ -752,5 +756,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; } diff --git a/src/material/core/common-behaviors/common-module.ts b/src/material/core/common-behaviors/common-module.ts index 897f02337ec8..c2685f20bdc1 100644 --- a/src/material/core/common-behaviors/common-module.ts +++ b/src/material/core/common-behaviors/common-module.ts @@ -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 @@ -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(); @@ -90,6 +91,19 @@ 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(); @@ -97,15 +111,16 @@ export class MatCommonModule { /** 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.' @@ -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); @@ -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 */ diff --git a/src/material/slider/slider.ts b/src/material/slider/slider.ts index 55c208cf0a04..ef9da8720751 100644 --- a/src/material/slider/slider.ts +++ b/src/material/slider/slider.ts @@ -698,6 +698,11 @@ export class MatSlider extends _MatSliderMixinBase } } + /** Use defaultView of injected document if available or fallback to global window reference */ + private _getWindow(): Window { + return this._document?.defaultView || window; + } + /** * Binds our global move and end events. They're bound at the document level and only while * dragging so that the user doesn't have to keep their pointer exactly over the slider @@ -716,6 +721,9 @@ export class MatSlider extends _MatSliderMixinBase body.addEventListener('touchcancel', this._pointerUp, activeEventOptions); } } + + const window = this._getWindow(); + if (typeof window !== 'undefined' && window) { window.addEventListener('blur', this._windowBlur); } @@ -731,6 +739,9 @@ export class MatSlider extends _MatSliderMixinBase body.removeEventListener('touchend', this._pointerUp, activeEventOptions); body.removeEventListener('touchcancel', this._pointerUp, activeEventOptions); } + + const window = this._getWindow(); + if (typeof window !== 'undefined' && window) { window.removeEventListener('blur', this._windowBlur); } diff --git a/tools/public_api_guard/cdk/a11y.d.ts b/tools/public_api_guard/cdk/a11y.d.ts index 6985f7e16ef4..7266f30ff560 100644 --- a/tools/public_api_guard/cdk/a11y.d.ts +++ b/tools/public_api_guard/cdk/a11y.d.ts @@ -92,7 +92,9 @@ export declare class FocusKeyManager extends ListKeyManager, origin: FocusOrigin, options?: FocusOptions): void; diff --git a/tools/public_api_guard/cdk/scrolling.d.ts b/tools/public_api_guard/cdk/scrolling.d.ts index ef7b62d6c38a..b0cda3cab7c5 100644 --- a/tools/public_api_guard/cdk/scrolling.d.ts +++ b/tools/public_api_guard/cdk/scrolling.d.ts @@ -154,9 +154,11 @@ export declare class FixedSizeVirtualScrollStrategy implements VirtualScrollStra } export declare class ScrollDispatcher implements OnDestroy { + protected _document?: Document; _globalSubscription: Subscription | null; scrollContainers: Map; - constructor(_ngZone: NgZone, _platform: Platform); + constructor(_ngZone: NgZone, _platform: Platform, + document?: any); ancestorScrolled(elementRef: ElementRef, auditTimeInMs?: number): Observable; deregister(scrollable: CdkScrollable): void; getAncestorScrollContainers(elementRef: ElementRef): CdkScrollable[]; @@ -173,7 +175,9 @@ export declare class ScrollingModule { } export declare class ViewportRuler implements OnDestroy { - constructor(_platform: Platform, ngZone: NgZone); + protected _document?: Document; + constructor(_platform: Platform, ngZone: NgZone, + document?: any); change(throttleTime?: number): Observable; getViewportRect(): ClientRect; getViewportScrollPosition(): ViewportScrollPosition; diff --git a/tools/public_api_guard/cdk/text-field.d.ts b/tools/public_api_guard/cdk/text-field.d.ts index 6f8b80e3920d..62c8bbdade3f 100644 --- a/tools/public_api_guard/cdk/text-field.d.ts +++ b/tools/public_api_guard/cdk/text-field.d.ts @@ -24,13 +24,15 @@ export declare class CdkAutofill implements OnDestroy, OnInit { } export declare class CdkTextareaAutosize implements AfterViewInit, DoCheck, OnDestroy { + protected _document?: Document; get enabled(): boolean; set enabled(value: boolean); get maxRows(): number; set maxRows(value: number); get minRows(): number; set minRows(value: number); - constructor(_elementRef: ElementRef, _platform: Platform, _ngZone: NgZone); + constructor(_elementRef: ElementRef, _platform: Platform, _ngZone: NgZone, + document?: any); _noopInputHandler(): void; _setMaxHeight(): void; _setMinHeight(): void; diff --git a/tools/public_api_guard/material/core.d.ts b/tools/public_api_guard/material/core.d.ts index 61775aa88254..13090726d2b6 100644 --- a/tools/public_api_guard/material/core.d.ts +++ b/tools/public_api_guard/material/core.d.ts @@ -201,7 +201,9 @@ export declare const MAT_OPTION_PARENT_COMPONENT: InjectionToken; export declare class MatCommonModule { - constructor(highContrastModeDetector: HighContrastModeDetector, sanityChecks: any); + protected _document?: Document; + constructor(highContrastModeDetector: HighContrastModeDetector, sanityChecks: any, + document?: any); static ɵinj: i0.ɵɵInjectorDef; static ɵmod: i0.ɵɵNgModuleDefWithMeta; }