diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index aa027e915f88..ed2e825ca392 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -174,6 +174,7 @@ /src/dev-app/mdc-card/** @mmalerba /src/dev-app/mdc-checkbox/** @mmalerba /src/dev-app/mdc-chips/** @mmalerba +/src/dev-app/mdc-dialog/** @devversion /src/dev-app/mdc-input/** @devversion @mmalerba /src/dev-app/mdc-list/** @mmalerba /src/dev-app/mdc-menu/** @crisbeto @@ -234,6 +235,7 @@ /src/e2e-app/mdc-card/** @mmalerba /src/e2e-app/mdc-checkbox/** @mmalerba /src/e2e-app/mdc-chips/** @mmalerba +/src/e2e-app/mdc-dialog/** @devversion /src/e2e-app/mdc-input/** @devversion /src/e2e-app/mdc-menu/** @crisbeto /src/e2e-app/mdc-progress-bar/** @crisbeto diff --git a/src/cdk-experimental/dialog/dialog-container.ts b/src/cdk-experimental/dialog/dialog-container.ts index 74e6aa680057..a3630250768f 100644 --- a/src/cdk-experimental/dialog/dialog-container.ts +++ b/src/cdk-experimental/dialog/dialog-container.ts @@ -17,6 +17,7 @@ import { } from '@angular/cdk/portal'; import {DOCUMENT} from '@angular/common'; import { + AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, @@ -72,7 +73,7 @@ export function throwDialogContentAlreadyAttachedError() { '(@dialog.done)': '_animationDone.next($event)', }, }) -export class CdkDialogContainer extends BasePortalOutlet implements OnDestroy { +export class CdkDialogContainer extends BasePortalOutlet implements OnDestroy, AfterViewInit { private readonly _document: Document; /** State of the dialog animation. */ @@ -150,6 +151,16 @@ export class CdkDialogContainer extends BasePortalOutlet implements OnDestroy { }); } + /** If the dialog view completes initialization, the open animation starts. */ + ngAfterViewInit() { + // Save the previously focused element. This element will be re-focused + // when the dialog closes. + this._savePreviouslyFocusedElement(); + // Move focus onto the dialog immediately in order to prevent the user + // from accidentally opening multiple dialogs at the same time. + this._focusDialogContainer(); + } + /** Destroy focus trap to place focus back to the element focused before the dialog opened. */ ngOnDestroy() { this._focusTrap.destroy(); @@ -165,7 +176,6 @@ export class CdkDialogContainer extends BasePortalOutlet implements OnDestroy { throwDialogContentAlreadyAttachedError(); } - this._savePreviouslyFocusedElement(); return this._portalHost.attachComponentPortal(portal); } @@ -178,7 +188,6 @@ export class CdkDialogContainer extends BasePortalOutlet implements OnDestroy { throwDialogContentAlreadyAttachedError(); } - this._savePreviouslyFocusedElement(); return this._portalHost.attachTemplatePortal(portal); } @@ -193,7 +202,6 @@ export class CdkDialogContainer extends BasePortalOutlet implements OnDestroy { throwDialogContentAlreadyAttachedError(); } - this._savePreviouslyFocusedElement(); return this._portalHost.attachDomPortal(portal); } @@ -222,11 +230,14 @@ export class CdkDialogContainer extends BasePortalOutlet implements OnDestroy { private _savePreviouslyFocusedElement() { if (this._document) { this._elementFocusedBeforeDialogWasOpened = this._document.activeElement as HTMLElement; + } + } - // Move focus onto the dialog immediately in order to prevent the user from accidentally - // opening multiple dialogs at the same time. Needs to be async, because the element - // may not be focusable immediately. - Promise.resolve().then(() => this._elementRef.nativeElement.focus()); + /** Focuses the dialog container. */ + private _focusDialogContainer() { + // Note that there is no focus method when rendering on the server. + if (this._elementRef.nativeElement.focus) { + this._elementRef.nativeElement.focus(); } } diff --git a/src/cdk/drag-drop/directives/drag.spec.ts b/src/cdk/drag-drop/directives/drag.spec.ts index 35a2f1c5625d..da0ba9b9c411 100644 --- a/src/cdk/drag-drop/directives/drag.spec.ts +++ b/src/cdk/drag-drop/directives/drag.spec.ts @@ -2169,7 +2169,7 @@ describe('CdkDrag', () => { fixture.componentInstance.boundarySelector = '.cdk-drop-list'; fixture.detectChanges(); - const container: HTMLElement = fixture.nativeElement.querySelector('.container'); + const container: HTMLElement = fixture.nativeElement.querySelector('.scroll-container'); const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; const list = fixture.componentInstance.dropInstance.element.nativeElement; const cleanup = makeScrollable('vertical', container); @@ -3889,7 +3889,7 @@ describe('CdkDrag', () => { const fixture = createComponent(DraggableInScrollableParentContainer); fixture.detectChanges(); const item = fixture.componentInstance.dragItems.first.element.nativeElement; - const container = fixture.nativeElement.querySelector('.container'); + const container = fixture.nativeElement.querySelector('.scroll-container'); const containerRect = container.getBoundingClientRect(); expect(container.scrollTop).toBe(0); @@ -5519,7 +5519,7 @@ class StandaloneDraggableWithMultipleHandles { const DROP_ZONE_FIXTURE_TEMPLATE = `
+
+
+
+
+
+
+
+
+
+
Last afterClosed result: {{lastAfterClosedResult}}
+Last beforeClose result: {{lastBeforeCloseResult}}
+ +It's Jazz!
+ +{{ data.message }}
+ + +It's Jazz!
+ +{{ data.message }}
+ + + ++ Neptune is the eighth and farthest known planet from the Sun in the Solar System. In the + Solar System, it is the fourth-largest planet by diameter, the third-most-massive planet, + and the densest giant planet. Neptune is 17 times the mass of Earth and is slightly more + massive than its near-twin Uranus, which is 15 times the mass of Earth and slightly larger + than Neptune. Neptune orbits the Sun once every 164.8 years at an average distance of 30.1 + astronomical units (4.50×109 km). It is named after the Roman god of the sea and has the + astronomical symbol ♆, a stylised version of the god Neptune's trident. +
+Lorem ipsum dolor sit amet, consectetur adipisicing elit.
+ + ` +}) +export class TestDialog { + constructor(public dialogRef: MatDialogRefPizza
'}) +class PizzaMsg { + constructor( + public dialogRef: MatDialogRefPasta
'}) +class DialogWithoutFocusableElements { +} + +// Create a real (non-test) NgModule as a workaround for +// https://github.com/angular/angular/issues/10760 +const TEST_DIRECTIVES = [ + ComponentWithChildViewContainer, + ComponentWithTemplateRef, + PizzaMsg, + DirectiveWithViewContainer, + ComponentWithOnPushViewContainer, + ContentElementDialog, + DialogWithInjectedData, + DialogWithoutFocusableElements, + ComponentWithContentElementTemplateRef, +]; + +@NgModule({ + imports: [MatDialogModule, NoopAnimationsModule], + exports: TEST_DIRECTIVES, + declarations: TEST_DIRECTIVES, + entryComponents: [ + ComponentWithChildViewContainer, + ComponentWithTemplateRef, + PizzaMsg, + ContentElementDialog, + DialogWithInjectedData, + DialogWithoutFocusableElements, + ], +}) +class DialogTestModule { +} diff --git a/src/material-experimental/mdc-dialog/dialog.ts b/src/material-experimental/mdc-dialog/dialog.ts new file mode 100644 index 000000000000..d27b61434fea --- /dev/null +++ b/src/material-experimental/mdc-dialog/dialog.ts @@ -0,0 +1,60 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Overlay, OverlayContainer, ScrollStrategy} from '@angular/cdk/overlay'; +import {Location} from '@angular/common'; +import {Inject, Injectable, InjectionToken, Injector, Optional, SkipSelf} from '@angular/core'; +import {_MatDialogBase, MatDialogConfig} from '@angular/material/dialog'; +import {MatDialogContainer} from './dialog-container'; +import {MatDialogRef} from './dialog-ref'; + +/** Injection token that can be used to access the data that was passed in to a dialog. */ +export const MAT_DIALOG_DATA = new InjectionToken