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: '

Hello

' + template: ` +

Hello

+ + +
+
+ ` }) class ArbitraryViewContainerRefComponent { + @ViewChild('template', {static: false}) template: TemplateRef; + @ViewChild(SaveParentNodeOnInit, {static: false}) saveParentNodeOnInit: SaveParentNodeOnInit; + constructor(public viewContainerRef: ViewContainerRef, public injector: Injector) { } } @@ -731,7 +772,7 @@ const TEST_COMPONENTS = [ @NgModule({ imports: [CommonModule, PortalModule], exports: TEST_COMPONENTS, - declarations: TEST_COMPONENTS, + declarations: [...TEST_COMPONENTS, SaveParentNodeOnInit], entryComponents: TEST_COMPONENTS, }) class PortalTestModule { }