Skip to content

Commit 0f83b20

Browse files
crisbetojelbourn
authored andcommitted
fix(overlay): better handling of server-side rendering (#8422)
Fixes multiple server-side rendering issues in the overlay that were causing errors when an overlay is opened in Universal. Fixes #8412.
1 parent c7ab877 commit 0f83b20

16 files changed

+87
-61
lines changed

src/cdk/a11y/live-announcer.ts

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
SkipSelf,
1515
OnDestroy,
1616
} from '@angular/core';
17-
import {Platform} from '@angular/cdk/platform';
17+
import {DOCUMENT} from '@angular/common';
1818

1919

2020
export const LIVE_ANNOUNCER_ELEMENT_TOKEN = new InjectionToken<HTMLElement>('liveAnnouncerElement');
@@ -28,14 +28,12 @@ export class LiveAnnouncer implements OnDestroy {
2828

2929
constructor(
3030
@Optional() @Inject(LIVE_ANNOUNCER_ELEMENT_TOKEN) elementToken: any,
31-
platform: Platform) {
32-
// Only do anything if we're on the browser platform.
33-
if (platform.isBrowser) {
34-
// We inject the live element as `any` because the constructor signature cannot reference
35-
// browser globals (HTMLElement) on non-browser environments, since having a class decorator
36-
// causes TypeScript to preserve the constructor signature types.
37-
this._liveElement = elementToken || this._createLiveElement();
38-
}
31+
@Inject(DOCUMENT) private _document: any) {
32+
33+
// We inject the live element as `any` because the constructor signature cannot reference
34+
// browser globals (HTMLElement) on non-browser environments, since having a class decorator
35+
// causes TypeScript to preserve the constructor signature types.
36+
this._liveElement = elementToken || this._createLiveElement();
3937
}
4038

4139
/**
@@ -64,13 +62,13 @@ export class LiveAnnouncer implements OnDestroy {
6462
}
6563

6664
private _createLiveElement(): Element {
67-
let liveEl = document.createElement('div');
65+
let liveEl = this._document.createElement('div');
6866

6967
liveEl.classList.add('cdk-visually-hidden');
7068
liveEl.setAttribute('aria-atomic', 'true');
7169
liveEl.setAttribute('aria-live', 'polite');
7270

73-
document.body.appendChild(liveEl);
71+
this._document.body.appendChild(liveEl);
7472

7573
return liveEl;
7674
}
@@ -79,8 +77,8 @@ export class LiveAnnouncer implements OnDestroy {
7977

8078
/** @docs-private */
8179
export function LIVE_ANNOUNCER_PROVIDER_FACTORY(
82-
parentDispatcher: LiveAnnouncer, liveElement: any, platform: Platform) {
83-
return parentDispatcher || new LiveAnnouncer(liveElement, platform);
80+
parentDispatcher: LiveAnnouncer, liveElement: any, _document: any) {
81+
return parentDispatcher || new LiveAnnouncer(liveElement, _document);
8482
}
8583

8684
/** @docs-private */
@@ -90,7 +88,7 @@ export const LIVE_ANNOUNCER_PROVIDER = {
9088
deps: [
9189
[new Optional(), new SkipSelf(), LiveAnnouncer],
9290
[new Optional(), new Inject(LIVE_ANNOUNCER_ELEMENT_TOKEN)],
93-
Platform,
91+
DOCUMENT,
9492
],
9593
useFactory: LIVE_ANNOUNCER_PROVIDER_FACTORY
9694
};

src/cdk/bidi/bidi-module.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import {NgModule} from '@angular/core';
10-
import {DOCUMENT} from '@angular/platform-browser';
10+
import {DOCUMENT} from '@angular/common';
1111
import {Dir} from './dir';
1212
import {DIR_DOCUMENT, Directionality} from './directionality';
1313

src/cdk/overlay/keyboard/overlay-keyboard-dispatcher.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Injectable, Optional, SkipSelf, OnDestroy} from '@angular/core';
9+
import {Injectable, Inject, InjectionToken, Optional, SkipSelf, OnDestroy} from '@angular/core';
1010
import {OverlayRef} from '../overlay-ref';
1111
import {Subscription} from 'rxjs/Subscription';
1212
import {filter} from 'rxjs/operators/filter';
1313
import {fromEvent} from 'rxjs/observable/fromEvent';
14+
import {DOCUMENT} from '@angular/common';
1415

1516
/**
1617
* Service for dispatching keyboard events that land on the body to appropriate overlay ref,
@@ -25,6 +26,8 @@ export class OverlayKeyboardDispatcher implements OnDestroy {
2526

2627
private _keydownEventSubscription: Subscription | null;
2728

29+
constructor(@Inject(DOCUMENT) private _document: any) {}
30+
2831
ngOnDestroy() {
2932
if (this._keydownEventSubscription) {
3033
this._keydownEventSubscription.unsubscribe();
@@ -55,7 +58,7 @@ export class OverlayKeyboardDispatcher implements OnDestroy {
5558
* events to the appropriate overlay.
5659
*/
5760
private _subscribeToKeydownEvents(): void {
58-
const bodyKeydownEvents = fromEvent<KeyboardEvent>(document.body, 'keydown');
61+
const bodyKeydownEvents = fromEvent<KeyboardEvent>(this._document.body, 'keydown');
5962

6063
this._keydownEventSubscription = bodyKeydownEvents.pipe(
6164
filter(() => !!this._attachedOverlays.length)
@@ -81,15 +84,21 @@ export class OverlayKeyboardDispatcher implements OnDestroy {
8184

8285
/** @docs-private */
8386
export function OVERLAY_KEYBOARD_DISPATCHER_PROVIDER_FACTORY(
84-
dispatcher: OverlayKeyboardDispatcher) {
85-
return dispatcher || new OverlayKeyboardDispatcher();
87+
dispatcher: OverlayKeyboardDispatcher, _document: any) {
88+
return dispatcher || new OverlayKeyboardDispatcher(_document);
8689
}
8790

8891
/** @docs-private */
8992
export const OVERLAY_KEYBOARD_DISPATCHER_PROVIDER = {
9093
// If there is already an OverlayKeyboardDispatcher available, use that.
9194
// Otherwise, provide a new one.
9295
provide: OverlayKeyboardDispatcher,
93-
deps: [[new Optional(), new SkipSelf(), OverlayKeyboardDispatcher]],
96+
deps: [
97+
[new Optional(), new SkipSelf(), OverlayKeyboardDispatcher],
98+
99+
// Coerce to `InjectionToken` so that the `deps` match the "shape"
100+
// of the type expected by Angular
101+
DOCUMENT as InjectionToken<any>
102+
],
94103
useFactory: OVERLAY_KEYBOARD_DISPATCHER_PROVIDER_FACTORY
95104
};

src/cdk/overlay/overlay-container.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,17 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Injectable, Optional, SkipSelf, OnDestroy} from '@angular/core';
9+
import {Injectable, InjectionToken, Inject, Optional, SkipSelf, OnDestroy} from '@angular/core';
10+
import {DOCUMENT} from '@angular/common';
1011

1112

1213
/** Container inside which all overlays will render. */
1314
@Injectable()
1415
export class OverlayContainer implements OnDestroy {
1516
protected _containerElement: HTMLElement;
1617

18+
constructor(@Inject(DOCUMENT) private _document: any) {}
19+
1720
ngOnDestroy() {
1821
if (this._containerElement && this._containerElement.parentNode) {
1922
this._containerElement.parentNode.removeChild(this._containerElement);
@@ -36,23 +39,27 @@ export class OverlayContainer implements OnDestroy {
3639
* with the 'cdk-overlay-container' class on the document body.
3740
*/
3841
protected _createContainer(): void {
39-
let container = document.createElement('div');
40-
container.classList.add('cdk-overlay-container');
42+
const container = this._document.createElement('div');
4143

42-
document.body.appendChild(container);
44+
container.classList.add('cdk-overlay-container');
45+
this._document.body.appendChild(container);
4346
this._containerElement = container;
4447
}
4548
}
4649

4750
/** @docs-private */
48-
export function OVERLAY_CONTAINER_PROVIDER_FACTORY(parentContainer: OverlayContainer) {
49-
return parentContainer || new OverlayContainer();
51+
export function OVERLAY_CONTAINER_PROVIDER_FACTORY(parentContainer: OverlayContainer,
52+
_document: any) {
53+
return parentContainer || new OverlayContainer(_document);
5054
}
5155

5256
/** @docs-private */
5357
export const OVERLAY_CONTAINER_PROVIDER = {
5458
// If there is already an OverlayContainer available, use that. Otherwise, provide a new one.
5559
provide: OverlayContainer,
56-
deps: [[new Optional(), new SkipSelf(), OverlayContainer]],
60+
deps: [
61+
[new Optional(), new SkipSelf(), OverlayContainer],
62+
DOCUMENT as InjectionToken<any> // We need to use the InjectionToken somewhere to keep TS happy
63+
],
5764
useFactory: OVERLAY_CONTAINER_PROVIDER_FACTORY
5865
};

src/cdk/overlay/overlay.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
ApplicationRef,
1313
Injector,
1414
NgZone,
15+
Inject,
1516
} from '@angular/core';
1617
import {DomPortalOutlet} from '@angular/cdk/portal';
1718
import {OverlayConfig} from './overlay-config';
@@ -20,6 +21,7 @@ import {OverlayPositionBuilder} from './position/overlay-position-builder';
2021
import {OverlayKeyboardDispatcher} from './keyboard/overlay-keyboard-dispatcher';
2122
import {OverlayContainer} from './overlay-container';
2223
import {ScrollStrategyOptions} from './scroll/index';
24+
import {DOCUMENT} from '@angular/common';
2325

2426

2527
/** Next overlay unique ID. */
@@ -48,7 +50,8 @@ export class Overlay {
4850
private _keyboardDispatcher: OverlayKeyboardDispatcher,
4951
private _appRef: ApplicationRef,
5052
private _injector: Injector,
51-
private _ngZone: NgZone) { }
53+
private _ngZone: NgZone,
54+
@Inject(DOCUMENT) private _document: any) { }
5255

5356
/**
5457
* Creates an overlay.
@@ -75,7 +78,7 @@ export class Overlay {
7578
* @returns Newly-created pane element
7679
*/
7780
private _createPaneElement(): HTMLElement {
78-
let pane = document.createElement('div');
81+
const pane = this._document.createElement('div');
7982

8083
pane.id = `cdk-overlay-${nextUniqueId++}`;
8184
pane.classList.add('cdk-overlay-pane');

src/cdk/overlay/position/connected-position-strategy.spec.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ describe('ConnectedPositionStrategy', () => {
5858
overlayContainerElement.appendChild(overlayElement);
5959

6060
fakeElementRef = new FakeElementRef(originElement);
61-
positionBuilder = new OverlayPositionBuilder(viewportRuler);
61+
positionBuilder = new OverlayPositionBuilder(viewportRuler, document);
6262
});
6363

6464
afterEach(() => {
@@ -177,7 +177,7 @@ describe('ConnectedPositionStrategy', () => {
177177
});
178178

179179
it('should reposition the overlay if it would go off the bottom of the screen', () => {
180-
positionBuilder = new OverlayPositionBuilder(viewportRuler);
180+
positionBuilder = new OverlayPositionBuilder(viewportRuler, document);
181181

182182
originElement.style.bottom = '25px';
183183
originElement.style.left = '200px';
@@ -200,7 +200,7 @@ describe('ConnectedPositionStrategy', () => {
200200
});
201201

202202
it('should reposition the overlay if it would go off the right of the screen', () => {
203-
positionBuilder = new OverlayPositionBuilder(viewportRuler);
203+
positionBuilder = new OverlayPositionBuilder(viewportRuler, document);
204204

205205
originElement.style.top = '200px';
206206
originElement.style.right = '25px';
@@ -224,7 +224,7 @@ describe('ConnectedPositionStrategy', () => {
224224
});
225225

226226
it('should recalculate and set the last position with recalculateLastPosition()', () => {
227-
positionBuilder = new OverlayPositionBuilder(viewportRuler);
227+
positionBuilder = new OverlayPositionBuilder(viewportRuler, document);
228228

229229
// Push the trigger down so the overlay doesn't have room to open on the bottom.
230230
originElement.style.bottom = '25px';
@@ -254,7 +254,7 @@ describe('ConnectedPositionStrategy', () => {
254254
});
255255

256256
it('should default to the initial position, if no positions fit in the viewport', () => {
257-
positionBuilder = new OverlayPositionBuilder(viewportRuler);
257+
positionBuilder = new OverlayPositionBuilder(viewportRuler, document);
258258

259259
// Make the origin element taller than the viewport.
260260
originElement.style.height = '1000px';
@@ -353,7 +353,7 @@ describe('ConnectedPositionStrategy', () => {
353353
});
354354

355355
it('should emit onPositionChange event when position changes', () => {
356-
positionBuilder = new OverlayPositionBuilder(viewportRuler);
356+
positionBuilder = new OverlayPositionBuilder(viewportRuler, document);
357357
originElement.style.top = '200px';
358358
originElement.style.right = '25px';
359359

@@ -390,7 +390,7 @@ describe('ConnectedPositionStrategy', () => {
390390
});
391391

392392
it('should emit the onPositionChange event even if none of the positions fit', () => {
393-
positionBuilder = new OverlayPositionBuilder(viewportRuler);
393+
positionBuilder = new OverlayPositionBuilder(viewportRuler, document);
394394
originElement.style.bottom = '25px';
395395
originElement.style.right = '25px';
396396

@@ -414,7 +414,7 @@ describe('ConnectedPositionStrategy', () => {
414414
});
415415

416416
it('should pick the fallback position that shows the largest area of the element', () => {
417-
positionBuilder = new OverlayPositionBuilder(viewportRuler);
417+
positionBuilder = new OverlayPositionBuilder(viewportRuler, document);
418418

419419
originElement.style.top = '200px';
420420
originElement.style.right = '25px';
@@ -561,7 +561,7 @@ describe('ConnectedPositionStrategy', () => {
561561
scrollable.appendChild(originElement);
562562

563563
// Create a strategy with knowledge of the scrollable container
564-
let positionBuilder = new OverlayPositionBuilder(viewportRuler);
564+
let positionBuilder = new OverlayPositionBuilder(viewportRuler, document);
565565
let fakeElementRef = new FakeElementRef(originElement);
566566
strategy = positionBuilder.connectedTo(
567567
fakeElementRef,
@@ -655,7 +655,7 @@ describe('ConnectedPositionStrategy', () => {
655655
overlayContainerElement.appendChild(overlayElement);
656656

657657
fakeElementRef = new FakeElementRef(originElement);
658-
positionBuilder = new OverlayPositionBuilder(viewportRuler);
658+
positionBuilder = new OverlayPositionBuilder(viewportRuler, document);
659659
});
660660

661661
afterEach(() => {

src/cdk/overlay/position/connected-position-strategy.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ export class ConnectedPositionStrategy implements PositionStrategy {
8080
originPos: OriginConnectionPosition,
8181
overlayPos: OverlayConnectionPosition,
8282
private _connectedTo: ElementRef,
83-
private _viewportRuler: ViewportRuler) {
83+
private _viewportRuler: ViewportRuler,
84+
private _document: any) {
8485
this._origin = this._connectedTo.nativeElement;
8586
this.withFallbackPosition(originPos, overlayPos);
8687
}
@@ -359,7 +360,7 @@ export class ConnectedPositionStrategy implements PositionStrategy {
359360
// from the bottom of the viewport rather than the top.
360361
let y = verticalStyleProperty === 'top' ?
361362
overlayPoint.y :
362-
document.documentElement.clientHeight - (overlayPoint.y + overlayRect.height);
363+
this._document.documentElement.clientHeight - (overlayPoint.y + overlayRect.height);
363364

364365
// We want to set either `left` or `right` based on whether the overlay wants to appear "before"
365366
// or "after" the origin, which determines the direction in which the element will expand.
@@ -376,7 +377,7 @@ export class ConnectedPositionStrategy implements PositionStrategy {
376377
// from the right edge of the viewport rather than the left edge.
377378
let x = horizontalStyleProperty === 'left' ?
378379
overlayPoint.x :
379-
document.documentElement.clientWidth - (overlayPoint.x + overlayRect.width);
380+
this._document.documentElement.clientWidth - (overlayPoint.x + overlayRect.width);
380381

381382

382383
// Reset any existing styles. This is necessary in case the preferred position has

src/cdk/overlay/position/global-position-strategy.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ describe('GlobalPositonStrategy', () => {
88

99
beforeEach(() => {
1010
element = document.createElement('div');
11-
strategy = new GlobalPositionStrategy();
11+
strategy = new GlobalPositionStrategy(document);
1212
document.body.appendChild(element);
1313
strategy.attach({overlayElement: element} as OverlayRef);
1414
});

src/cdk/overlay/position/global-position-strategy.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ export class GlobalPositionStrategy implements PositionStrategy {
3333
/* A lazily-created wrapper for the overlay element that is used as a flex container. */
3434
private _wrapper: HTMLElement | null = null;
3535

36+
constructor(private _document: any) {}
37+
3638
attach(overlayRef: OverlayRef): void {
3739
this._overlayRef = overlayRef;
3840
}
@@ -147,10 +149,10 @@ export class GlobalPositionStrategy implements PositionStrategy {
147149
const element = this._overlayRef.overlayElement;
148150

149151
if (!this._wrapper && element.parentNode) {
150-
this._wrapper = document.createElement('div');
151-
this._wrapper.classList.add('cdk-global-overlay-wrapper');
152-
element.parentNode.insertBefore(this._wrapper, element);
153-
this._wrapper.appendChild(element);
152+
this._wrapper = this._document.createElement('div');
153+
this._wrapper!.classList.add('cdk-global-overlay-wrapper');
154+
element.parentNode.insertBefore(this._wrapper!, element);
155+
this._wrapper!.appendChild(element);
154156
}
155157

156158
let styles = element.style;

0 commit comments

Comments
 (0)