diff --git a/src/cdk/a11y/BUILD.bazel b/src/cdk/a11y/BUILD.bazel
index 2b683c2ea75e..992b525d754d 100644
--- a/src/cdk/a11y/BUILD.bazel
+++ b/src/cdk/a11y/BUILD.bazel
@@ -49,6 +49,7 @@ ng_test_library(
"//src/cdk/keycodes",
"//src/cdk/observers",
"//src/cdk/platform",
+ "//src/cdk/portal",
"//src/cdk/testing/private",
"@npm//@angular/platform-browser",
"@npm//rxjs",
diff --git a/src/cdk/a11y/focus-trap/focus-trap.spec.ts b/src/cdk/a11y/focus-trap/focus-trap.spec.ts
index b0ec9a9a317c..fc49b2f1dbb8 100644
--- a/src/cdk/a11y/focus-trap/focus-trap.spec.ts
+++ b/src/cdk/a11y/focus-trap/focus-trap.spec.ts
@@ -1,6 +1,7 @@
import {Platform} from '@angular/cdk/platform';
-import {Component, ViewChild} from '@angular/core';
+import {Component, ViewChild, TemplateRef, ViewContainerRef} from '@angular/core';
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {PortalModule, CdkPortalOutlet, TemplatePortal} from '@angular/cdk/portal';
import {A11yModule, FocusTrap, CdkTrapFocus} from '../index';
@@ -8,7 +9,7 @@ describe('FocusTrap', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
- imports: [A11yModule],
+ imports: [A11yModule, PortalModule],
declarations: [
FocusTrapWithBindings,
SimpleFocusTrap,
@@ -17,6 +18,7 @@ describe('FocusTrap', () => {
FocusTrapWithoutFocusableElements,
FocusTrapWithAutoCapture,
FocusTrapUnfocusableTarget,
+ FocusTrapInsidePortal,
],
});
@@ -187,6 +189,27 @@ describe('FocusTrap', () => {
});
}));
});
+
+ it('should put anchors inside the outlet when set at the root of a template portal', () => {
+ const fixture = TestBed.createComponent(FocusTrapInsidePortal);
+ const instance = fixture.componentInstance;
+ fixture.detectChanges();
+ const outlet: HTMLElement = fixture.nativeElement.querySelector('.portal-outlet');
+
+ expect(outlet.querySelectorAll('button').length)
+ .toBe(0, 'Expected no buttons inside the outlet on init.');
+ expect(outlet.querySelectorAll('.cdk-focus-trap-anchor').length)
+ .toBe(0, 'Expected no focus trap anchors inside the outlet on init.');
+
+ const portal = new TemplatePortal(instance.template, instance.viewContainerRef);
+ instance.portalOutlet.attachTemplatePortal(portal);
+ fixture.detectChanges();
+
+ expect(outlet.querySelectorAll('button').length)
+ .toBe(1, 'Expected one button inside the outlet after attaching.');
+ expect(outlet.querySelectorAll('.cdk-focus-trap-anchor').length)
+ .toBe(2, 'Expected two focus trap anchors in the outlet after attaching.');
+ });
});
@@ -283,3 +306,24 @@ class FocusTrapWithSvg {
class FocusTrapWithoutFocusableElements {
@ViewChild(CdkTrapFocus) focusTrapDirective: CdkTrapFocus;
}
+
+
+@Component({
+ template: `
+
+
+
+
+
+
+
+
+
+ `,
+})
+class FocusTrapInsidePortal {
+ @ViewChild('template', {static: false}) template: TemplateRef;
+ @ViewChild(CdkPortalOutlet, {static: false}) portalOutlet: CdkPortalOutlet;
+
+ constructor(public viewContainerRef: ViewContainerRef) {}
+}
diff --git a/src/cdk/portal/dom-portal-outlet.ts b/src/cdk/portal/dom-portal-outlet.ts
index 78ed0f3df2d8..b30f3bd110a5 100644
--- a/src/cdk/portal/dom-portal-outlet.ts
+++ b/src/cdk/portal/dom-portal-outlet.ts
@@ -83,7 +83,6 @@ export class DomPortalOutlet extends BasePortalOutlet {
attachTemplatePortal(portal: TemplatePortal): EmbeddedViewRef {
let viewContainer = portal.viewContainerRef;
let viewRef = viewContainer.createEmbeddedView(portal.templateRef, portal.context);
- viewRef.detectChanges();
// The method `createEmbeddedView` will add the view as a child of the viewContainer.
// But for the DomPortalOutlet the view can be added everywhere in the DOM
@@ -91,6 +90,11 @@ export class DomPortalOutlet extends BasePortalOutlet {
// re-append the existing root nodes.
viewRef.rootNodes.forEach(rootNode => this.outletElement.appendChild(rootNode));
+ // Note that we want to detect changes after the nodes have been moved so that
+ // any directives inside the portal that are looking at the DOM inside a lifecycle
+ // hook won't be invoked too early.
+ viewRef.detectChanges();
+
this.setDisposeFn((() => {
let index = viewContainer.indexOf(viewRef);
if (index !== -1) {
diff --git a/src/cdk/portal/portal.spec.ts b/src/cdk/portal/portal.spec.ts
index 58ef243d919f..aab283087043 100644
--- a/src/cdk/portal/portal.spec.ts
+++ b/src/cdk/portal/portal.spec.ts
@@ -14,6 +14,8 @@ import {
ViewChild,
ViewChildren,
ViewContainerRef,
+ Directive,
+ AfterViewInit,
} from '@angular/core';
import {ComponentFixture, inject, TestBed} from '@angular/core/testing';
import {DomPortalOutlet} from './dom-portal-outlet';
@@ -404,7 +406,7 @@ describe('Portals', () => {
let componentFactoryResolver: ComponentFactoryResolver;
let someViewContainerRef: ViewContainerRef;
let someInjector: Injector;
- let someFixture: ComponentFixture;
+ let someFixture: ComponentFixture;
let someDomElement: HTMLElement;
let host: DomPortalOutlet;
let injector: Injector;
@@ -440,6 +442,19 @@ describe('Portals', () => {
expect(someDomElement.innerHTML).toBe('');
});
+ it('should move the DOM nodes before running change detection', () => {
+ someFixture.detectChanges();
+ let portal = new TemplatePortal(someFixture.componentInstance.template, someViewContainerRef);
+
+ host.attachTemplatePortal(portal);
+ someFixture.detectChanges();
+
+ expect(someFixture.componentInstance.saveParentNodeOnInit.parentOnViewInit)
+ .toBe(someDomElement);
+
+ host.dispose();
+ });
+
it('should attach and detach a component portal with a given injector', () => {
let fixture = TestBed.createComponent(ArbitraryViewContainerRefComponent);
someViewContainerRef = fixture.componentInstance.viewContainerRef;
@@ -634,12 +649,38 @@ class PizzaMsg {
constructor(@Optional() public snack: Chocolate) { }
}
+/**
+ * Saves the parent node that the directive was attached to on init.
+ * Useful to see where the element was in the DOM when it was first attached.
+ */
+@Directive({
+ selector: '[savesParentNodeOnInit]'
+})
+class SaveParentNodeOnInit implements AfterViewInit {
+ parentOnViewInit: HTMLElement;
+
+ constructor(private _elementRef: ElementRef) {}
+
+ ngAfterViewInit() {
+ this.parentOnViewInit = this._elementRef.nativeElement.parentElement!;
+ }
+}
+
/** Simple component to grab an arbitrary ViewContainerRef */
@Component({
selector: 'some-placeholder',
- template: '