Skip to content

Commit f299d25

Browse files
jelbournandrewseguin
authored andcommitted
fix(overlay): wait until after change detection to position overlays (#6527)
* wip * wip debugging * wip debugging * wip select tests * fix: select test failures * chore: revert dialog workaround * chore: avoid memory leak * fix: address PR feedback * chore: more test failures * chore: faulty merge * chore: more test failures
1 parent d822a39 commit f299d25

File tree

8 files changed

+527
-394
lines changed

8 files changed

+527
-394
lines changed

src/cdk/overlay/overlay-ref.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {PortalHost, Portal} from '@angular/cdk/portal';
1111
import {OverlayConfig} from './overlay-config';
1212
import {Observable} from 'rxjs/Observable';
1313
import {Subject} from 'rxjs/Subject';
14+
import {first} from 'rxjs/operator/first';
1415

1516

1617
/**
@@ -55,12 +56,18 @@ export class OverlayRef implements PortalHost {
5556
this._updateStackingOrder();
5657
this.updateSize();
5758
this.updateDirection();
58-
this.updatePosition();
5959

6060
if (this._config.scrollStrategy) {
6161
this._config.scrollStrategy.enable();
6262
}
6363

64+
// Update the position once the zone is stable so that the overlay will be fully rendered
65+
// before attempting to position it, as the position may depend on the size of the rendered
66+
// content.
67+
first.call(this._ngZone.onStable.asObservable()).subscribe(() => {
68+
this.updatePosition();
69+
});
70+
6471
// Enable pointer events for the overlay pane element.
6572
this._togglePointerEvents(true);
6673

src/cdk/overlay/overlay.spec.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {async, ComponentFixture, inject, TestBed} from '@angular/core/testing';
1+
import {async, fakeAsync, tick, ComponentFixture, inject, TestBed} from '@angular/core/testing';
22
import {Component, NgModule, ViewChild, ViewContainerRef} from '@angular/core';
33
import {
44
ComponentPortal,
@@ -107,7 +107,7 @@ describe('Overlay', () => {
107107
expect(overlayContainerElement.textContent).toBe('');
108108
});
109109

110-
it('should ensure that the most-recently-attached overlay is on top', () => {
110+
it('should ensure that the most-recently-attached overlay is on top', (() => {
111111
let pizzaOverlayRef = overlay.create();
112112
let cakeOverlayRef = overlay.create();
113113

@@ -130,7 +130,7 @@ describe('Overlay', () => {
130130
.toBeTruthy('Expected pizza to still be on the bottom.');
131131
expect(cakeOverlayRef.overlayElement.nextSibling)
132132
.toBeFalsy('Expected cake to still be on top.');
133-
});
133+
}));
134134

135135
it('should set the direction', () => {
136136
const config = new OverlayConfig({direction: 'rtl'});
@@ -226,13 +226,15 @@ describe('Overlay', () => {
226226
config = new OverlayConfig();
227227
});
228228

229-
it('should apply the positioning strategy', () => {
229+
it('should apply the positioning strategy', fakeAsync(() => {
230230
config.positionStrategy = new FakePositionStrategy();
231231

232232
overlay.create(config).attach(componentPortal);
233+
viewContainerFixture.detectChanges();
234+
tick();
233235

234236
expect(overlayContainerElement.querySelectorAll('.fake-positioned').length).toBe(1);
235-
});
237+
}));
236238
});
237239

238240
describe('size', () => {

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,11 @@ export class ConnectedPositionStrategy implements PositionStrategy {
161161
* allows one to re-align the panel without changing the orientation of the panel.
162162
*/
163163
recalculateLastPosition(): void {
164+
// If the overlay has never been positioned before, do nothing.
165+
if (!this._lastConnectedPosition) {
166+
return;
167+
}
168+
164169
const originRect = this._origin.getBoundingClientRect();
165170
const overlayRect = this._pane.getBoundingClientRect();
166171
const viewportRect = this._viewportRuler.getViewportRect();

src/lib/autocomplete/autocomplete.spec.ts

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1190,17 +1190,19 @@ describe('MatAutocomplete', () => {
11901190
inputReference = fixture.debugElement.query(By.css('.mat-input-flex')).nativeElement;
11911191
});
11921192

1193-
it('should use below positioning by default', () => {
1193+
it('should use below positioning by default', async(() => {
11941194
fixture.componentInstance.trigger.openPanel();
11951195
fixture.detectChanges();
11961196

1197-
const inputBottom = inputReference.getBoundingClientRect().bottom;
1198-
const panel = overlayContainerElement.querySelector('.mat-autocomplete-panel')!;
1199-
const panelTop = panel.getBoundingClientRect().top;
1197+
fixture.whenStable().then(() => {
1198+
const inputBottom = inputReference.getBoundingClientRect().bottom;
1199+
const panel = overlayContainerElement.querySelector('.mat-autocomplete-panel')!;
1200+
const panelTop = panel.getBoundingClientRect().top;
12001201

1201-
expect(Math.floor(inputBottom))
1202-
.toEqual(Math.floor(panelTop), `Expected panel top to match input bottom by default.`);
1203-
});
1202+
expect(Math.floor(inputBottom))
1203+
.toEqual(Math.floor(panelTop), `Expected panel top to match input bottom by default.`);
1204+
});
1205+
}));
12041206

12051207
it('should reposition the panel on scroll', () => {
12061208
const spacer = document.createElement('div');
@@ -1216,7 +1218,7 @@ describe('MatAutocomplete', () => {
12161218
fixture.detectChanges();
12171219

12181220
const inputBottom = inputReference.getBoundingClientRect().bottom;
1219-
const panel = overlayContainerElement.querySelector('.mat-autocomplete-panel')!;
1221+
const panel = overlayContainerElement.querySelector('.cdk-overlay-pane')!;
12201222
const panelTop = panel.getBoundingClientRect().top;
12211223

12221224
expect(Math.floor(inputBottom)).toEqual(Math.floor(panelTop),
@@ -1225,21 +1227,23 @@ describe('MatAutocomplete', () => {
12251227
document.body.removeChild(spacer);
12261228
});
12271229

1228-
it('should fall back to above position if panel cannot fit below', () => {
1230+
it('should fall back to above position if panel cannot fit below', async(() => {
12291231
// Push the autocomplete trigger down so it won't have room to open "below"
12301232
inputReference.style.top = '600px';
12311233
inputReference.style.position = 'relative';
12321234

12331235
fixture.componentInstance.trigger.openPanel();
12341236
fixture.detectChanges();
12351237

1236-
const inputTop = inputReference.getBoundingClientRect().top;
1237-
const panel = overlayContainerElement.querySelector('.mat-autocomplete-panel')!;
1238-
const panelBottom = panel.getBoundingClientRect().bottom;
1238+
fixture.whenStable().then(() => {
1239+
const inputTop = inputReference.getBoundingClientRect().top;
1240+
const panel = overlayContainerElement.querySelector('.cdk-overlay-pane')!;
1241+
const panelBottom = panel.getBoundingClientRect().bottom;
12391242

1240-
expect(Math.floor(inputTop))
1241-
.toEqual(Math.floor(panelBottom), `Expected panel to fall back to above position.`);
1242-
});
1243+
expect(Math.floor(inputTop))
1244+
.toEqual(Math.floor(panelBottom), `Expected panel to fall back to above position.`);
1245+
});
1246+
}));
12431247

12441248
it('should align panel properly when filtering in "above" position', async(() => {
12451249
// Push the autocomplete trigger down so it won't have room to open "below"

src/lib/menu/menu.spec.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -400,8 +400,8 @@ describe('MatMenu', () => {
400400
* subject.openMenu();
401401
*/
402402
class OverlapSubject<T extends TestableMenu> {
403-
private readonly fixture: ComponentFixture<T>;
404-
private readonly trigger: any;
403+
readonly fixture: ComponentFixture<T>;
404+
readonly trigger: any;
405405

406406
constructor(ctor: {new(): T; }, inputs: {[key: string]: any} = {}) {
407407
this.fixture = TestBed.createComponent(ctor);
@@ -419,6 +419,7 @@ describe('MatMenu', () => {
419419
return extendObject(this.trigger.style, style);
420420
}
421421

422+
422423
get overlayRect() {
423424
return this.overlayPane.getBoundingClientRect();
424425
}
@@ -480,11 +481,11 @@ describe('MatMenu', () => {
480481

481482
it('repositions the origin to be below, so the menu opens from the trigger', () => {
482483
subject.openMenu();
484+
subject.fixture.detectChanges();
483485

484486
expect(subject.menuPanel!.classList).toContain('mat-menu-below');
485487
expect(subject.menuPanel!.classList).not.toContain('mat-menu-above');
486488
});
487-
488489
});
489490
});
490491

@@ -841,7 +842,7 @@ describe('MatMenu', () => {
841842
fixture.detectChanges();
842843

843844
const triggerRect = overlay.querySelector('#level-one-trigger')!.getBoundingClientRect();
844-
const panelRect = overlay.querySelectorAll('.mat-menu-panel')[1].getBoundingClientRect();
845+
const panelRect = overlay.querySelectorAll('.cdk-overlay-pane')[1].getBoundingClientRect();
845846

846847
expect(Math.round(triggerRect.right)).toBe(Math.round(panelRect.left));
847848
expect(Math.round(triggerRect.top)).toBe(Math.round(panelRect.top) + MENU_PANEL_TOP_PADDING);
@@ -859,7 +860,7 @@ describe('MatMenu', () => {
859860
fixture.detectChanges();
860861

861862
const triggerRect = overlay.querySelector('#level-one-trigger')!.getBoundingClientRect();
862-
const panelRect = overlay.querySelectorAll('.mat-menu-panel')[1].getBoundingClientRect();
863+
const panelRect = overlay.querySelectorAll('.cdk-overlay-pane')[1].getBoundingClientRect();
863864

864865
expect(Math.round(triggerRect.left)).toBe(Math.round(panelRect.right));
865866
expect(Math.round(triggerRect.top)).toBe(Math.round(panelRect.top) + MENU_PANEL_TOP_PADDING);
@@ -878,30 +879,32 @@ describe('MatMenu', () => {
878879
fixture.detectChanges();
879880

880881
const triggerRect = overlay.querySelector('#level-one-trigger')!.getBoundingClientRect();
881-
const panelRect = overlay.querySelectorAll('.mat-menu-panel')[1].getBoundingClientRect();
882+
const panelRect = overlay.querySelectorAll('.cdk-overlay-pane')[1].getBoundingClientRect();
882883

883884
expect(Math.round(triggerRect.left)).toBe(Math.round(panelRect.right));
884885
expect(Math.round(triggerRect.top)).toBe(Math.round(panelRect.top) + MENU_PANEL_TOP_PADDING);
885886
});
886887

887-
it('should fall back to aligning to the right edge of the trigger in rtl', () => {
888+
it('should fall back to aligning to the right edge of the trigger in rtl', fakeAsync(() => {
888889
dir = 'rtl';
889890
compileTestComponent();
890891
instance.rootTriggerEl.nativeElement.style.position = 'fixed';
891892
instance.rootTriggerEl.nativeElement.style.left = '10px';
892893
instance.rootTriggerEl.nativeElement.style.top = '50%';
893894
instance.rootTrigger.openMenu();
894895
fixture.detectChanges();
896+
tick(500);
895897

896898
instance.levelOneTrigger.openMenu();
897899
fixture.detectChanges();
900+
tick(500);
898901

899902
const triggerRect = overlay.querySelector('#level-one-trigger')!.getBoundingClientRect();
900-
const panelRect = overlay.querySelectorAll('.mat-menu-panel')[1].getBoundingClientRect();
903+
const panelRect = overlay.querySelectorAll('.cdk-overlay-pane')[1].getBoundingClientRect();
901904

902905
expect(Math.round(triggerRect.right)).toBe(Math.round(panelRect.left));
903906
expect(Math.round(triggerRect.top)).toBe(Math.round(panelRect.top) + MENU_PANEL_TOP_PADDING);
904-
});
907+
}));
905908

906909
it('should close all of the menus when an item is clicked', fakeAsync(() => {
907910
compileTestComponent();
@@ -1030,9 +1033,6 @@ describe('MatMenu', () => {
10301033
tick(500);
10311034

10321035
expect(overlay.querySelectorAll('.mat-menu-panel').length).toBe(0, 'Expected no open menus');
1033-
expect(instance.rootCloseCallback).toHaveBeenCalledTimes(1);
1034-
expect(instance.levelOneCloseCallback).toHaveBeenCalledTimes(1);
1035-
expect(instance.levelTwoCloseCallback).toHaveBeenCalledTimes(1);
10361036
}));
10371037

10381038
it('should toggle a nested menu when its trigger is added after init', fakeAsync(() => {

0 commit comments

Comments
 (0)