From 0eb132eb9c030753c03482481dc54ce6af96d1d6 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Tue, 19 Apr 2022 10:49:20 +0200 Subject: [PATCH] refactor(cdk-experimental/dialog): rewrite experimental dialog Rewrites most of the CDK experimental dialog to prepare it for public release. The changes were largely informed by implementing `material/dialog`, `material-experimental/mdc-dialog` and `material/bottom-sheet` using the new API, however the API can be used directly without having to be wrapped. Overview of the changes: * The classes for the dialog container, dialog config and dialog data aren't provided using DI anymore. The previous approach could've been a problem for apps that use multiple different versions of a CDK-based dialog. * Several fixes from the Material dialog that were never backported to the CDK version have been incorporated. * All animations and styles have been removed from the CDK dialog since they were going to be difficult to override by users. Custom animations can be achieved by extending the dialog container class. * The public API has been cleaned up to avoid some of the issues that were inherited from the Material dialog. * A demo has been added to the dev app. Example of how the new APIs was used to implement the Material components: https://github.com/crisbeto/material2/commit/446a5520f1a0357612a9cc2d9e0c38b506665fb9. --- .github/CODEOWNERS | 1 + goldens/ts-circular-deps.json | 4 - src/cdk-experimental/dialog/BUILD.bazel | 9 +- src/cdk-experimental/dialog/dialog-config.ts | 129 +++-- .../dialog/dialog-container.scss | 6 - .../dialog/dialog-container.ts | 293 +++++------ .../dialog/dialog-injectors.ts | 24 +- src/cdk-experimental/dialog/dialog-module.ts | 17 +- src/cdk-experimental/dialog/dialog-ref.ts | 186 +++---- src/cdk-experimental/dialog/dialog.spec.ts | 401 +++++---------- src/cdk-experimental/dialog/dialog.ts | 459 ++++++++++-------- src/dev-app/BUILD.bazel | 1 + src/dev-app/cdk-dialog/BUILD.bazel | 22 + src/dev-app/cdk-dialog/dialog-demo-module.ts | 19 + src/dev-app/cdk-dialog/dialog-demo.html | 61 +++ src/dev-app/cdk-dialog/dialog-demo.scss | 23 + src/dev-app/cdk-dialog/dialog-demo.ts | 100 ++++ src/dev-app/dev-app/dev-app-layout.ts | 1 + src/dev-app/routes.ts | 4 + 19 files changed, 902 insertions(+), 858 deletions(-) delete mode 100644 src/cdk-experimental/dialog/dialog-container.scss create mode 100644 src/dev-app/cdk-dialog/BUILD.bazel create mode 100644 src/dev-app/cdk-dialog/dialog-demo-module.ts create mode 100644 src/dev-app/cdk-dialog/dialog-demo.html create mode 100644 src/dev-app/cdk-dialog/dialog-demo.scss create mode 100644 src/dev-app/cdk-dialog/dialog-demo.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 23794dba5fe2..9aaca9909e76 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -153,6 +153,7 @@ /src/dev-app/mdc-autocomplete/** @crisbeto /src/dev-app/button/** @andrewseguin /src/dev-app/card/** @andrewseguin +/src/dev-app/cdk-dialog/** @crisbeto /src/dev-app/cdk-experimental-combobox/** @jelbourn /src/dev-app/cdk-experimental-listbox/** @jelbourn /src/dev-app/cdk-experimental-menu/** @jelbourn diff --git a/goldens/ts-circular-deps.json b/goldens/ts-circular-deps.json index 8405fc95263a..46eea5a9d942 100644 --- a/goldens/ts-circular-deps.json +++ b/goldens/ts-circular-deps.json @@ -1,8 +1,4 @@ [ - [ - "src/cdk-experimental/dialog/dialog-config.ts", - "src/cdk-experimental/dialog/dialog-container.ts" - ], ["src/cdk/drag-drop/directives/drag.ts", "src/cdk/drag-drop/directives/drop-list.ts"], ["src/cdk/drag-drop/directives/drag.ts", "src/cdk/drag-drop/drag-events.ts"], [ diff --git a/src/cdk-experimental/dialog/BUILD.bazel b/src/cdk-experimental/dialog/BUILD.bazel index 6e865271a412..a38aef4827a5 100644 --- a/src/cdk-experimental/dialog/BUILD.bazel +++ b/src/cdk-experimental/dialog/BUILD.bazel @@ -1,4 +1,4 @@ -load("//tools:defaults.bzl", "ng_module", "ng_test_library", "ng_web_test_suite", "sass_binary") +load("//tools:defaults.bzl", "ng_module", "ng_test_library", "ng_web_test_suite") package(default_visibility = ["//visibility:public"]) @@ -8,7 +8,7 @@ ng_module( ["**/*.ts"], exclude = ["**/*.spec.ts"], ), - assets = [":dialog-container.css"] + glob(["**/*.html"]), + assets = glob(["**/*.html"]), deps = [ "//src:dev_mode_types", "//src/cdk/a11y", @@ -23,11 +23,6 @@ ng_module( ], ) -sass_binary( - name = "dialog_container_scss", - src = "dialog-container.scss", -) - ng_test_library( name = "unit_test_sources", srcs = glob( diff --git a/src/cdk-experimental/dialog/dialog-config.ts b/src/cdk-experimental/dialog/dialog-config.ts index 1218a8ff18d6..d985e37db01a 100644 --- a/src/cdk-experimental/dialog/dialog-config.ts +++ b/src/cdk-experimental/dialog/dialog-config.ts @@ -5,29 +5,26 @@ * 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 {Injector, ViewContainerRef} from '@angular/core'; + +import { + ViewContainerRef, + ComponentFactoryResolver, + Injector, + StaticProvider, + Type, +} from '@angular/core'; import {Direction} from '@angular/cdk/bidi'; -import {ComponentType} from '@angular/cdk/overlay'; -import {CdkDialogContainer} from './dialog-container'; +import {PositionStrategy, ScrollStrategy} from '@angular/cdk/overlay'; +import {BasePortalOutlet} from '@angular/cdk/portal'; /** Options for where to set focus to automatically on dialog open */ export type AutoFocusTarget = 'dialog' | 'first-tabbable' | 'first-heading'; -/** Valid ARIA roles for a dialog element. */ +/** Valid ARIA roles for a dialog. */ export type DialogRole = 'dialog' | 'alertdialog'; -/** Possible overrides for a dialog's position. */ -export interface DialogPosition { - top?: string; - bottom?: string; - left?: string; - right?: string; -} - -export class DialogConfig { - /** Component to use as the container for the dialog. */ - containerComponent?: ComponentType; - +/** Configuration for opening a modal dialog. */ +export class DialogConfig { /** * Where the attached component should live in Angular's *logical* component tree. * This affects what is available for injection and the change detection order for the @@ -42,57 +39,63 @@ export class DialogConfig { */ injector?: Injector; - /** The id of the dialog. */ + /** ID for the dialog. If omitted, a unique one will be generated. */ id?: string; - /** The ARIA role of the dialog. */ + /** The ARIA role of the dialog element. */ role?: DialogRole = 'dialog'; - /** Custom class(es) for the overlay panel. */ + /** Optional CSS class or classes applied to the overlay panel. */ panelClass?: string | string[] = ''; - /** Whether the dialog has a background. */ + /** Whether the dialog has a backdrop. */ hasBackdrop?: boolean = true; - /** Custom class(es) for the backdrop. */ - backdropClass?: string | undefined = ''; + /** Optional CSS class or classes applied to the overlay backdrop. */ + backdropClass?: string | string[] = ''; - /** Whether the dialog can be closed by user interaction. */ + /** Whether the dialog closes with the escape key or pointer events outside the panel element. */ disableClose?: boolean = false; - /** The width of the dialog. */ + /** Width of the dialog. */ width?: string = ''; - /** The height of the dialog. */ + /** Height of the dialog. */ height?: string = ''; - /** The minimum width of the dialog. */ - minWidth?: string | number = ''; + /** Min-width of the dialog. If a number is provided, assumes pixel units. */ + minWidth?: number | string; - /** The minimum height of the dialog. */ - minHeight?: string | number = ''; + /** Min-height of the dialog. If a number is provided, assumes pixel units. */ + minHeight?: number | string; - /** The maximum width of the dialog. */ - maxWidth?: string | number = '80vw'; + /** Max-width of the dialog. If a number is provided, assumes pixel units. Defaults to 80vw. */ + maxWidth?: number | string; - /** The maximum height of the dialog. */ - maxHeight?: string | number = ''; + /** Max-height of the dialog. If a number is provided, assumes pixel units. */ + maxHeight?: number | string; - /** The position of the dialog. */ - position?: DialogPosition; + /** Strategy to use when positioning the dialog. Defaults to centering it on the page. */ + positionStrategy?: PositionStrategy; - /** Data to be injected into the dialog content. */ + /** Data being injected into the child component. */ data?: D | null = null; - /** The layout direction for the dialog content. */ + /** Layout direction for the dialog's content. */ direction?: Direction; /** ID of the element that describes the dialog. */ ariaDescribedBy?: string | null = null; - /** Aria label to assign to the dialog element */ + /** ID of the element that labels the dialog. */ + ariaLabelledBy?: string | null = null; + + /** Dialog label applied via `aria-label` */ ariaLabel?: string | null = null; + /** Whether this a modal dialog. Used to set the `aria-modal` attribute. */ + ariaModal?: boolean = true; + /** * Where the dialog should focus on open. * @breaking-change 14.0.0 Remove boolean option from autoFocus. Use string or @@ -100,9 +103,51 @@ export class DialogConfig { */ autoFocus?: AutoFocusTarget | string | boolean = 'first-tabbable'; - /** Duration of the enter animation. Has to be a valid CSS value (e.g. 100ms). */ - enterAnimationDuration?: string = '225ms'; + /** + * Whether the dialog should restore focus to the + * previously-focused element upon closing. + */ + restoreFocus?: boolean = true; + + /** + * Scroll strategy to be used for the dialog. This determines how + * the dialog responds to scrolling underneath the panel element. + */ + scrollStrategy?: ScrollStrategy; - /** Duration of the exit animation. Has to be a valid CSS value (e.g. 50ms). */ - exitAnimationDuration?: string = '225ms'; + /** + * Whether the dialog should close when the user navigates backwards or forwards through browser + * history. This does not apply to navigation via anchor element unless using URL-hash based + * routing (`HashLocationStrategy` in the Angular router). + */ + closeOnNavigation?: boolean = true; + + /** Alternate `ComponentFactoryResolver` to use when resolving the associated component. */ + componentFactoryResolver?: ComponentFactoryResolver; + + /** + * Providers that will be exposed to the contents of the dialog. Can also + * be provided as a function in order to generate the providers lazily. + */ + providers?: + | StaticProvider[] + | ((dialogRef: R, config: DialogConfig, container: C) => StaticProvider[]); + + /** + * Component into which the dialog content will be rendered. Defaults to `CdkDialogContainer`. + * A configuration object can be passed in to customize the providers that will be exposed + * to the dialog container. + */ + container?: + | Type + | { + type: Type; + providers: (config: DialogConfig) => StaticProvider[]; + }; + + /** + * Context that will be passed to template-based dialogs. + * A function can be passed in to resolve the context lazily. + */ + templateContext?: Record | (() => Record); } diff --git a/src/cdk-experimental/dialog/dialog-container.scss b/src/cdk-experimental/dialog/dialog-container.scss deleted file mode 100644 index 8eb48baae9d5..000000000000 --- a/src/cdk-experimental/dialog/dialog-container.scss +++ /dev/null @@ -1,6 +0,0 @@ -cdk-dialog-container { - background: white; - border-radius: 5px; - display: block; - padding: 10px; -} diff --git a/src/cdk-experimental/dialog/dialog-container.ts b/src/cdk-experimental/dialog/dialog-container.ts index 88916d34b1dc..0e62bc9624cf 100644 --- a/src/cdk-experimental/dialog/dialog-container.ts +++ b/src/cdk-experimental/dialog/dialog-container.ts @@ -7,17 +7,13 @@ */ import { - animate, - animateChild, - AnimationEvent, - group, - query, - state, - style, - transition, - trigger, -} from '@angular/animations'; -import {FocusTrapFactory, InteractivityChecker} from '@angular/cdk/a11y'; + FocusMonitor, + FocusOrigin, + FocusTrap, + FocusTrapFactory, + InteractivityChecker, +} from '@angular/cdk/a11y'; +import {OverlayRef} from '@angular/cdk/overlay'; import {_getFocusedElementPierceShadowDom} from '@angular/cdk/platform'; import { BasePortalOutlet, @@ -28,8 +24,8 @@ import { } from '@angular/cdk/portal'; import {DOCUMENT} from '@angular/common'; import { + AfterViewInit, ChangeDetectionStrategy, - ChangeDetectorRef, Component, ComponentRef, ElementRef, @@ -41,8 +37,6 @@ import { ViewChild, ViewEncapsulation, } from '@angular/core'; -import {Subject} from 'rxjs'; -import {distinctUntilChanged} from 'rxjs/operators'; import {DialogConfig} from './dialog-config'; export function throwDialogContentAlreadyAttachedError() { @@ -56,131 +50,77 @@ export function throwDialogContentAlreadyAttachedError() { @Component({ selector: 'cdk-dialog-container', templateUrl: './dialog-container.html', - styleUrls: ['dialog-container.css'], encapsulation: ViewEncapsulation.None, // Using OnPush for dialogs caused some G3 sync issues. Disabled until we can track them down. // tslint:disable-next-line:validate-decorators changeDetection: ChangeDetectionStrategy.Default, - animations: [ - trigger('dialog', [ - state('enter', style({opacity: 1})), - state('exit, void', style({opacity: 0})), - transition( - '* => enter', - group([ - animate('{{enterAnimationDuration}}'), - query('@*', animateChild(), {optional: true}), - ]), - ), - transition( - '* => exit, * => void', - group([ - animate('{{exitAnimationDuration}}'), - query('@*', animateChild(), {optional: true}), - ]), - ), - ]), - ], host: { - '[@dialog]': `{ - value: _state, - params: { - enterAnimationDuration: _config.enterAnimationDuration, - exitAnimationDuration: _config.exitAnimationDuration - } - }`, - '(@dialog.start)': '_onAnimationStart($event)', - '(@dialog.done)': '_animationDone.next($event)', + 'class': 'cdk-dialog-container', 'tabindex': '-1', + '[attr.id]': '_config.id || null', '[attr.role]': '_config.role', - 'aria-modal': 'true', - '[attr.aria-label]': '_config.ariaLabel || null', - '[attr.aria-describedby]': '_config.ariaDescribedBy', + '[attr.aria-modal]': '_config.ariaModal', + '[attr.aria-labelledby]': '_config.ariaLabel ? null : _ariaLabelledBy', + '[attr.aria-label]': '_config.ariaLabel', + '[attr.aria-describedby]': '_config.ariaDescribedBy || null', }, }) -export class CdkDialogContainer extends BasePortalOutlet implements OnDestroy { - private readonly _document: Document; +export class CdkDialogContainer + extends BasePortalOutlet + implements AfterViewInit, OnDestroy +{ + protected _document: Document; - /** State of the dialog animation. */ - _state: 'void' | 'enter' | 'exit' = 'enter'; - - /** Element that was focused before the dialog was opened. Save this to restore upon close. */ - private _elementFocusedBeforeDialogWasOpened: HTMLElement | null = null; + /** The portal outlet inside of this container into which the dialog content will be loaded. */ + @ViewChild(CdkPortalOutlet, {static: true}) _portalOutlet: CdkPortalOutlet; /** The class that traps and manages focus within the dialog. */ - private _focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement); - - /** The portal host inside of this container into which the dialog content will be loaded. */ - @ViewChild(CdkPortalOutlet, {static: true}) _portalHost: CdkPortalOutlet; + private _focusTrap: FocusTrap; - /** A subject emitting before the dialog enters the view. */ - readonly _beforeEnter = new Subject(); - - /** A subject emitting after the dialog enters the view. */ - readonly _afterEnter = new Subject(); - - /** A subject emitting before the dialog exits the view. */ - readonly _beforeExit = new Subject(); + /** Element that was focused before the dialog was opened. Save this to restore upon close. */ + private _elementFocusedBeforeDialogWasOpened: HTMLElement | null = null; - /** A subject emitting after the dialog exits the view. */ - readonly _afterExit = new Subject(); + /** + * Type of interaction that led to the dialog being closed. This is used to determine + * whether the focus style will be applied when returning focus to its original location + * after the dialog is closed. + */ + _closeInteractionType: FocusOrigin | null = null; - /** Stream of animation `done` events. */ - readonly _animationDone = new Subject(); + /** ID of the element that should be considered as the dialog's label. */ + _ariaLabelledBy: string | null; constructor( - private _elementRef: ElementRef, - private _focusTrapFactory: FocusTrapFactory, - private _changeDetectorRef: ChangeDetectorRef, - private readonly _interactivityChecker: InteractivityChecker, - private readonly _ngZone: NgZone, + protected _elementRef: ElementRef, + protected _focusTrapFactory: FocusTrapFactory, @Optional() @Inject(DOCUMENT) _document: any, - /** The dialog configuration. */ - public _config: DialogConfig, + @Inject(DialogConfig) readonly _config: C, + private _interactivityChecker: InteractivityChecker, + private _ngZone: NgZone, + private _overlayRef: OverlayRef, + private _focusMonitor?: FocusMonitor, ) { super(); - + this._ariaLabelledBy = this._config.ariaLabelledBy || null; this._document = _document; + } - // We use a Subject with a distinctUntilChanged, rather than a callback attached to .done, - // because some browsers fire the done event twice and we don't want to emit duplicate events. - // See: https://github.com/angular/angular/issues/24084 - this._animationDone - .pipe( - distinctUntilChanged((x, y) => { - return x.fromState === y.fromState && x.toState === y.toState; - }), - ) - .subscribe(event => { - // Emit lifecycle events based on animation `done` callback. - if (event.toState === 'enter') { - this._autoFocus(); - this._afterEnter.next(); - this._afterEnter.complete(); - } - - if (event.fromState === 'enter' && (event.toState === 'void' || event.toState === 'exit')) { - this._returnFocusAfterDialog(); - this._afterExit.next(); - this._afterExit.complete(); - } - }); + ngAfterViewInit() { + this._initializeFocusTrap(); + this._handleBackdropClicks(); + this._captureInitialFocus(); } - /** Initializes the dialog container with the attached content. */ - _initializeWithAttachedContent() { - // 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(); + /** + * Can be used by child classes to customize the initial focus + * capturing behavior (e.g. if it's tied to an animation). + */ + protected _captureInitialFocus() { + this._trapFocus(); } - /** Destroy focus trap to place focus back to the element focused before the dialog opened. */ ngOnDestroy() { - this._focusTrap.destroy(); - this._animationDone.complete(); + this._restoreFocus(); } /** @@ -188,23 +128,23 @@ export class CdkDialogContainer extends BasePortalOutlet implements OnDestroy { * @param portal Portal to be attached as the dialog content. */ attachComponentPortal(portal: ComponentPortal): ComponentRef { - if (this._portalHost.hasAttached() && (typeof ngDevMode === 'undefined' || ngDevMode)) { + if (this._portalOutlet.hasAttached() && (typeof ngDevMode === 'undefined' || ngDevMode)) { throwDialogContentAlreadyAttachedError(); } - return this._portalHost.attachComponentPortal(portal); + return this._portalOutlet.attachComponentPortal(portal); } /** * Attach a TemplatePortal as content to this dialog container. * @param portal Portal to be attached as the dialog content. */ - attachTemplatePortal(portal: TemplatePortal): EmbeddedViewRef { - if (this._portalHost.hasAttached() && (typeof ngDevMode === 'undefined' || ngDevMode)) { + attachTemplatePortal(portal: TemplatePortal): EmbeddedViewRef { + if (this._portalOutlet.hasAttached() && (typeof ngDevMode === 'undefined' || ngDevMode)) { throwDialogContentAlreadyAttachedError(); } - return this._portalHost.attachTemplatePortal(portal); + return this._portalOutlet.attachTemplatePortal(portal); } /** @@ -214,49 +154,13 @@ export class CdkDialogContainer extends BasePortalOutlet implements OnDestroy { * @breaking-change 10.0.0 */ override attachDomPortal = (portal: DomPortal) => { - if (this._portalHost.hasAttached() && (typeof ngDevMode === 'undefined' || ngDevMode)) { + if (this._portalOutlet.hasAttached() && (typeof ngDevMode === 'undefined' || ngDevMode)) { throwDialogContentAlreadyAttachedError(); } - return this._portalHost.attachDomPortal(portal); + return this._portalOutlet.attachDomPortal(portal); }; - /** Emit lifecycle events based on animation `start` callback. */ - _onAnimationStart(event: AnimationEvent) { - if (event.toState === 'enter') { - this._beforeEnter.next(); - this._beforeEnter.complete(); - } - if (event.fromState === 'enter' && (event.toState === 'void' || event.toState === 'exit')) { - this._beforeExit.next(); - this._beforeExit.complete(); - } - } - - /** Starts the dialog exit animation. */ - _startExiting(): void { - this._state = 'exit'; - - // Mark the container for check so it can react if the - // view container is using OnPush change detection. - this._changeDetectorRef.markForCheck(); - } - - /** Saves a reference to the element that was focused before the dialog was opened. */ - private _savePreviouslyFocusedElement() { - if (this._document) { - this._elementFocusedBeforeDialogWasOpened = _getFocusedElementPierceShadowDom(); - } - } - - /** 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(); - } - } - /** * Focuses the provided element. If the element is not focusable, it will add a tabIndex * attribute to forcefully focus it. The attribute is removed after focus is moved. @@ -294,12 +198,11 @@ export class CdkDialogContainer extends BasePortalOutlet implements OnDestroy { } /** - * Autofocus the element specified by the autoFocus field. When autoFocus is not 'dialog', if - * for some reason the element cannot be focused, the dialog container will be focused. + * Moves the focus inside the focus trap. When autoFocus is not set to 'dialog', if focus + * cannot be moved then focus will go to the dialog container. */ - private _autoFocus() { + protected _trapFocus() { const element = this._elementRef.nativeElement; - // If were to attempt to focus immediately, then the content of the dialog would not yet be // ready in instances where change detection has to run first. To deal with this, we simply // wait for the microtask queue to be empty when setting focus when autoFocus isn't set to @@ -308,21 +211,22 @@ export class CdkDialogContainer extends BasePortalOutlet implements OnDestroy { switch (this._config.autoFocus) { case false: case 'dialog': - const activeElement = _getFocusedElementPierceShadowDom(); // Ensure that focus is on the dialog container. It's possible that a different // component tried to move focus while the open animation was running. See: // https://github.com/angular/components/issues/16215. Note that we only want to do this // if the focus isn't inside the dialog already, because it's possible that the consumer // turned off `autoFocus` in order to move focus themselves. - if (activeElement !== element && !element.contains(activeElement)) { + if (!this._containsFocus()) { element.focus(); } break; case true: case 'first-tabbable': - this._focusTrap.focusInitialElementWhenReady().then(hasMovedFocus => { - if (!hasMovedFocus) { - element.focus(); + this._focusTrap.focusInitialElementWhenReady().then(focusedSuccessfully => { + // If we weren't able to find a focusable element in the dialog, then focus the dialog + // container instead. + if (!focusedSuccessfully) { + this._focusDialogContainer(); } }); break; @@ -335,11 +239,16 @@ export class CdkDialogContainer extends BasePortalOutlet implements OnDestroy { } } - /** Returns the focus to the element focused before the dialog was open. */ - private _returnFocusAfterDialog() { - const toFocus = this._elementFocusedBeforeDialogWasOpened; + /** Restores focus to the element that was focused before the dialog opened. */ + private _restoreFocus() { + const previousElement = this._elementFocusedBeforeDialogWasOpened; + // We need the extra check, because IE can set the `activeElement` to null in some cases. - if (toFocus && typeof toFocus.focus === 'function') { + if ( + this._config.restoreFocus && + previousElement && + typeof previousElement.focus === 'function' + ) { const activeElement = _getFocusedElementPierceShadowDom(); const element = this._elementRef.nativeElement; @@ -353,8 +262,54 @@ export class CdkDialogContainer extends BasePortalOutlet implements OnDestroy { activeElement === element || element.contains(activeElement) ) { - toFocus.focus(); + if (this._focusMonitor) { + this._focusMonitor.focusVia(previousElement, this._closeInteractionType); + this._closeInteractionType = null; + } else { + previousElement.focus(); + } } } + + if (this._focusTrap) { + this._focusTrap.destroy(); + } + } + + /** 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(); + } + } + + /** Returns whether focus is inside the dialog. */ + private _containsFocus() { + const element = this._elementRef.nativeElement; + const activeElement = _getFocusedElementPierceShadowDom(); + return element === activeElement || element.contains(activeElement); + } + + /** Sets up the focus trap. */ + private _initializeFocusTrap() { + this._focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement); + + // Save the previously focused element. This element will be re-focused + // when the dialog closes. + if (this._document) { + this._elementFocusedBeforeDialogWasOpened = _getFocusedElementPierceShadowDom(); + } + } + + /** Sets up the listener that handles clicks on the dialog backdrop. */ + private _handleBackdropClicks() { + // Clicking on the backdrop will move focus out of dialog. + // Recapture it if closing via the backdrop is disabled. + this._overlayRef.backdropClick().subscribe(() => { + if (this._config.disableClose && !this._containsFocus()) { + this._trapFocus(); + } + }); } } diff --git a/src/cdk-experimental/dialog/dialog-injectors.ts b/src/cdk-experimental/dialog/dialog-injectors.ts index 198e65feb350..74dc46e1bebe 100644 --- a/src/cdk-experimental/dialog/dialog-injectors.ts +++ b/src/cdk-experimental/dialog/dialog-injectors.ts @@ -7,9 +7,7 @@ */ import {InjectionToken} from '@angular/core'; -import {ComponentType, Overlay, ScrollStrategy} from '@angular/cdk/overlay'; -import {DialogRef} from './dialog-ref'; -import {CdkDialogContainer} from './dialog-container'; +import {Overlay, ScrollStrategy} from '@angular/cdk/overlay'; import {DialogConfig} from './dialog-config'; /** Injection token for the Dialog's ScrollStrategy. */ @@ -20,27 +18,17 @@ export const DIALOG_SCROLL_STRATEGY = new InjectionToken<() => ScrollStrategy>( /** Injection token for the Dialog's Data. */ export const DIALOG_DATA = new InjectionToken('DialogData'); -/** Injection token for the DialogRef constructor. */ -export const DIALOG_REF = new InjectionToken>('DialogRef'); - -/** Injection token for the DialogConfig. */ -export const DIALOG_CONFIG = new InjectionToken('DialogConfig'); - -/** Injection token for the Dialog's DialogContainer component. */ -export const DIALOG_CONTAINER = new InjectionToken>( - 'DialogContainer', -); +/** Injection token that can be used to provide default options for the dialog module. */ +export const DEFAULT_DIALOG_CONFIG = new InjectionToken('DefaultDialogConfig'); /** @docs-private */ -export function MAT_DIALOG_SCROLL_STRATEGY_PROVIDER_FACTORY( - overlay: Overlay, -): () => ScrollStrategy { +export function DIALOG_SCROLL_STRATEGY_PROVIDER_FACTORY(overlay: Overlay): () => ScrollStrategy { return () => overlay.scrollStrategies.block(); } /** @docs-private */ -export const MAT_DIALOG_SCROLL_STRATEGY_PROVIDER = { +export const DIALOG_SCROLL_STRATEGY_PROVIDER = { provide: DIALOG_SCROLL_STRATEGY, deps: [Overlay], - useFactory: MAT_DIALOG_SCROLL_STRATEGY_PROVIDER_FACTORY, + useFactory: DIALOG_SCROLL_STRATEGY_PROVIDER_FACTORY, }; diff --git a/src/cdk-experimental/dialog/dialog-module.ts b/src/cdk-experimental/dialog/dialog-module.ts index 22bccdc6c8c0..6822ef5d5d57 100644 --- a/src/cdk-experimental/dialog/dialog-module.ts +++ b/src/cdk-experimental/dialog/dialog-module.ts @@ -12,14 +12,7 @@ import {PortalModule} from '@angular/cdk/portal'; import {A11yModule} from '@angular/cdk/a11y'; import {Dialog} from './dialog'; import {CdkDialogContainer} from './dialog-container'; -import {DialogConfig} from './dialog-config'; -import {DialogRef} from './dialog-ref'; -import { - DIALOG_CONFIG, - DIALOG_CONTAINER, - DIALOG_REF, - MAT_DIALOG_SCROLL_STRATEGY_PROVIDER, -} from './dialog-injectors'; +import {DIALOG_SCROLL_STRATEGY_PROVIDER} from './dialog-injectors'; @NgModule({ imports: [OverlayModule, PortalModule, A11yModule], @@ -30,12 +23,6 @@ import { CdkDialogContainer, ], declarations: [CdkDialogContainer], - providers: [ - Dialog, - MAT_DIALOG_SCROLL_STRATEGY_PROVIDER, - {provide: DIALOG_REF, useValue: DialogRef}, - {provide: DIALOG_CONTAINER, useValue: CdkDialogContainer}, - {provide: DIALOG_CONFIG, useValue: DialogConfig}, - ], + providers: [Dialog, DIALOG_SCROLL_STRATEGY_PROVIDER], }) export class DialogModule {} diff --git a/src/cdk-experimental/dialog/dialog-ref.ts b/src/cdk-experimental/dialog/dialog-ref.ts index 853c12c67766..e3f57f6efd3e 100644 --- a/src/cdk-experimental/dialog/dialog-ref.ts +++ b/src/cdk-experimental/dialog/dialog-ref.ts @@ -6,144 +6,116 @@ * found in the LICENSE file at https://angular.io/license */ -import {OverlayRef, GlobalPositionStrategy, OverlaySizeConfig} from '@angular/cdk/overlay'; +import {OverlayRef} from '@angular/cdk/overlay'; import {ESCAPE, hasModifierKey} from '@angular/cdk/keycodes'; -import {Observable} from 'rxjs'; -import {map, filter} from 'rxjs/operators'; -import {DialogPosition} from './dialog-config'; -import {CdkDialogContainer} from './dialog-container'; - -/** Unique id for the created dialog. */ -let uniqueId = 0; +import {Observable, Subject} from 'rxjs'; +import {DialogConfig} from './dialog-config'; +import {FocusOrigin} from '@angular/cdk/a11y'; +import {BasePortalOutlet} from '@angular/cdk/portal'; + +/** Additional options that can be passed in when closing a dialog. */ +export interface DialogCloseOptions { + /** Focus original to use when restoring focus. */ + focusOrigin?: FocusOrigin; +} /** * Reference to a dialog opened via the Dialog service. */ -export class DialogRef { - /** The instance of the component in the dialog. */ - componentInstance: T; +export class DialogRef { + /** + * Instance of component opened into the dialog. Will be + * null when the dialog is opened using a `TemplateRef`. + */ + componentInstance: C | null; + + /** Instance of the container that is rendering out the dialog content. */ + containerInstance: BasePortalOutlet & {_closeInteractionType?: FocusOrigin}; /** Whether the user is allowed to close the dialog. */ disableClose: boolean | undefined; - /** Result to be passed to afterClosed. */ - private _result: R | undefined; + /** Emits when the dialog has been closed. */ + readonly closed: Observable = new Subject(); - constructor( - public _overlayRef: OverlayRef, - protected _containerInstance: CdkDialogContainer, - readonly id: string = `dialog-${uniqueId++}`, - ) { - // If the dialog has a backdrop, handle clicks from the backdrop. - if (_containerInstance._config.hasBackdrop) { - _overlayRef.backdropClick().subscribe(() => { - if (!this.disableClose) { - this.close(); - } - }); - } + /** Emits when the backdrop of the dialog is clicked. */ + readonly backdropClick: Observable; - this.beforeClosed().subscribe(() => { - this._overlayRef.detachBackdrop(); - }); + /** Emits when on keyboard events within the dialog. */ + readonly keydownEvents: Observable; - this.afterClosed().subscribe(() => { - this._overlayRef.detach(); - this._overlayRef.dispose(); - this.componentInstance = null!; - }); + /** Emits on pointer events that happen outside of the dialog. */ + readonly outsidePointerEvents: Observable; - // Close when escape keydown event occurs - _overlayRef - .keydownEvents() - .pipe( - filter(event => { - return event.keyCode === ESCAPE && !this.disableClose && !hasModifierKey(event); - }), - ) - .subscribe(event => { + /** Unique ID for the dialog. */ + readonly id: string; + + constructor( + readonly overlayRef: OverlayRef, + readonly config: DialogConfig, BasePortalOutlet>, + ) { + this.disableClose = config.disableClose; + this.backdropClick = overlayRef.backdropClick(); + this.keydownEvents = overlayRef.keydownEvents(); + this.outsidePointerEvents = overlayRef.outsidePointerEvents(); + this.id = config.id!; // By the time the dialog is created we are guaranteed to have an ID. + + this.keydownEvents.subscribe(event => { + if (event.keyCode === ESCAPE && !this.disableClose && !hasModifierKey(event)) { event.preventDefault(); - this.close(); - }); - } + this.close(undefined, {focusOrigin: 'keyboard'}); + } + }); - /** Gets an observable that emits when the overlay's backdrop has been clicked. */ - backdropClick(): Observable { - return this._overlayRef.backdropClick(); + this.backdropClick.subscribe(() => { + if (!this.disableClose) { + this.close(undefined, {focusOrigin: 'mouse'}); + } + }); } /** * Close the dialog. - * @param dialogResult Optional result to return to the dialog opener. + * @param result Optional result to return to the dialog opener. + * @param options Additional options to customize the closing behavior. */ - close(dialogResult?: R): void { - this._result = dialogResult; - this._containerInstance._startExiting(); - } - - /** - * Updates the dialog's position. - * @param position New dialog position. - */ - updatePosition(position?: DialogPosition): this { - let strategy = this._getPositionStrategy(); - - if (position && (position.left || position.right)) { - position.left ? strategy.left(position.left) : strategy.right(position.right); - } else { - strategy.centerHorizontally(); + close(result?: R, options?: DialogCloseOptions): void { + if (this.containerInstance) { + const closedSubject = this.closed as Subject; + this.containerInstance._closeInteractionType = options?.focusOrigin || 'program'; + this.overlayRef.dispose(); + closedSubject.next(result); + closedSubject.complete(); + this.componentInstance = this.containerInstance = null!; } - - if (position && (position.top || position.bottom)) { - position.top ? strategy.top(position.top) : strategy.bottom(position.bottom); - } else { - strategy.centerVertically(); - } - - this._overlayRef.updatePosition(); - - return this; } - /** - * Gets an observable that emits when keydown events are targeted on the overlay. - */ - keydownEvents(): Observable { - return this._overlayRef.keydownEvents(); + /** Updates the dialog's position. */ + updatePosition(): this { + this.overlayRef.updatePosition(); + return this; } /** - * Updates the dialog's width and height, defined, min and max. - * @param size New size for the overlay. + * Updates the dialog's width and height. + * @param width New width of the dialog. + * @param height New height of the dialog. */ - updateSize(size: OverlaySizeConfig): this { - this._overlayRef.updateSize(size); - this._overlayRef.updatePosition(); + updateSize(width: string = '', height: string = ''): this { + this.overlayRef.updateSize({width, height}); + this.overlayRef.updatePosition(); return this; } - /** Fetches the position strategy object from the overlay ref. */ - private _getPositionStrategy(): GlobalPositionStrategy { - return this._overlayRef.getConfig().positionStrategy as GlobalPositionStrategy; - } - - /** Gets an observable that emits when dialog begins opening. */ - beforeOpened(): Observable { - return this._containerInstance._beforeEnter; - } - - /** Gets an observable that emits when dialog is finished opening. */ - afterOpened(): Observable { - return this._containerInstance._afterEnter; - } - - /** Gets an observable that emits when dialog begins closing. */ - beforeClosed(): Observable { - return this._containerInstance._beforeExit.pipe(map(() => this._result)); + /** Add a CSS class or an array of classes to the overlay pane. */ + addPanelClass(classes: string | string[]): this { + this.overlayRef.addPanelClass(classes); + return this; } - /** Gets an observable that emits when dialog is finished closing. */ - afterClosed(): Observable { - return this._containerInstance._afterExit.pipe(map(() => this._result)); + /** Remove a CSS class or an array of classes from the overlay pane. */ + removePanelClass(classes: string | string[]): this { + this.overlayRef.removePanelClass(classes); + return this; } } diff --git a/src/cdk-experimental/dialog/dialog.spec.ts b/src/cdk-experimental/dialog/dialog.spec.ts index ca98a1335e5f..4ebab1617509 100644 --- a/src/cdk-experimental/dialog/dialog.spec.ts +++ b/src/cdk-experimental/dialog/dialog.spec.ts @@ -2,7 +2,6 @@ import { ComponentFixture, fakeAsync, flushMicrotasks, - inject, TestBed, tick, flush, @@ -19,12 +18,10 @@ import { ViewEncapsulation, } from '@angular/core'; import {By} from '@angular/platform-browser'; -import {NoopAnimationsModule} from '@angular/platform-browser/animations'; import {Location} from '@angular/common'; import {SpyLocation} from '@angular/common/testing'; import {Directionality} from '@angular/cdk/bidi'; -import {CdkDialogContainer} from './dialog-container'; -import {OverlayContainer} from '@angular/cdk/overlay'; +import {Overlay, OverlayContainer} from '@angular/cdk/overlay'; import {A, ESCAPE} from '@angular/cdk/keycodes'; import {_supportsShadowDom} from '@angular/cdk/platform'; import { @@ -41,10 +38,11 @@ describe('Dialog', () => { let testViewContainerRef: ViewContainerRef; let viewContainerFixture: ComponentFixture; let mockLocation: SpyLocation; + let overlay: Overlay; beforeEach(fakeAsync(() => { TestBed.configureTestingModule({ - imports: [DialogModule, NoopAnimationsModule], + imports: [DialogModule], declarations: [ ComponentWithChildViewContainer, ComponentWithTemplateRef, @@ -58,26 +56,19 @@ describe('Dialog', () => { }); TestBed.compileComponents(); - })); - beforeEach(inject( - [Dialog, Location, OverlayContainer], - (d: Dialog, l: Location, o: OverlayContainer) => { - dialog = d; - mockLocation = l as SpyLocation; - overlayContainerElement = o.getContainerElement(); - }, - )); + dialog = TestBed.inject(Dialog); + mockLocation = TestBed.inject(Location) as SpyLocation; + overlay = TestBed.inject(Overlay); + overlayContainerElement = TestBed.inject(OverlayContainer).getContainerElement(); - beforeEach(() => { viewContainerFixture = TestBed.createComponent(ComponentWithChildViewContainer); - viewContainerFixture.detectChanges(); testViewContainerRef = viewContainerFixture.componentInstance.childViewContainer; - }); + })); it('should open a dialog with a component', () => { - let dialogRef = dialog.openFromComponent(PizzaMsg, { + let dialogRef = dialog.open(PizzaMsg, { viewContainerRef: testViewContainerRef, }); @@ -85,7 +76,7 @@ describe('Dialog', () => { expect(overlayContainerElement.textContent).toContain('Pizza'); expect(dialogRef.componentInstance instanceof PizzaMsg).toBe(true); - expect(dialogRef.componentInstance.dialogRef).toBe(dialogRef); + expect(dialogRef.componentInstance!.dialogRef).toBe(dialogRef); viewContainerFixture.detectChanges(); viewContainerFixture.detectChanges(); @@ -101,7 +92,7 @@ describe('Dialog', () => { const data = {value: 'Knees'}; - let dialogRef = dialog.openFromTemplate(templateRefFixture.componentInstance.templateRef, { + let dialogRef = dialog.open(templateRefFixture.componentInstance.templateRef, { data, }); @@ -119,44 +110,29 @@ describe('Dialog', () => { dialogRef.close(); }); - it('should emit when dialog opening animation is complete', fakeAsync(() => { - const dialogRef = dialog.openFromComponent(PizzaMsg, {viewContainerRef: testViewContainerRef}); - const spy = jasmine.createSpy('afterOpen spy'); - - dialogRef.afterOpened().subscribe(spy); - - viewContainerFixture.detectChanges(); - - // callback should not be called before animation is complete - expect(spy).not.toHaveBeenCalled(); - - flushMicrotasks(); - expect(spy).toHaveBeenCalled(); - })); - it('should use injector from viewContainerRef for DialogInjector', () => { - let dialogRef = dialog.openFromComponent(PizzaMsg, { + let dialogRef = dialog.open(PizzaMsg, { viewContainerRef: testViewContainerRef, }); viewContainerFixture.detectChanges(); - let dialogInjector = dialogRef.componentInstance.dialogInjector as Injector; + let dialogInjector = dialogRef.componentInstance!.dialogInjector as Injector; - expect(dialogRef.componentInstance.dialogRef).toBe(dialogRef); + expect(dialogRef.componentInstance!.dialogRef).toBe(dialogRef); expect(dialogInjector.get(DirectiveWithViewContainer)).toBeTruthy( 'Expected the dialog component to be created with the injector from the viewContainerRef.', ); }); it('should open a dialog with a component and no ViewContainerRef', () => { - let dialogRef = dialog.openFromComponent(PizzaMsg); + let dialogRef = dialog.open(PizzaMsg); viewContainerFixture.detectChanges(); expect(overlayContainerElement.textContent).toContain('Pizza'); expect(dialogRef.componentInstance instanceof PizzaMsg).toBe(true); - expect(dialogRef.componentInstance.dialogRef).toBe(dialogRef); + expect(dialogRef.componentInstance!.dialogRef).toBe(dialogRef); viewContainerFixture.detectChanges(); let dialogContainerElement = overlayContainerElement.querySelector('cdk-dialog-container')!; @@ -164,7 +140,7 @@ describe('Dialog', () => { }); it('should apply the configured role to the dialog element', () => { - dialog.openFromComponent(PizzaMsg, {role: 'alertdialog'}); + dialog.open(PizzaMsg, {role: 'alertdialog'}); viewContainerFixture.detectChanges(); @@ -173,7 +149,7 @@ describe('Dialog', () => { }); it('should apply the specified `aria-describedby`', () => { - dialog.openFromComponent(PizzaMsg, {ariaDescribedBy: 'description-element'}); + dialog.open(PizzaMsg, {ariaDescribedBy: 'description-element'}); viewContainerFixture.detectChanges(); @@ -182,11 +158,11 @@ describe('Dialog', () => { }); it('should close a dialog and get back a result', fakeAsync(() => { - let dialogRef = dialog.openFromComponent(PizzaMsg, {viewContainerRef: testViewContainerRef}); + let dialogRef = dialog.open(PizzaMsg, {viewContainerRef: testViewContainerRef}); let afterCloseCallback = jasmine.createSpy('afterClose callback'); viewContainerFixture.detectChanges(); - dialogRef.afterClosed().subscribe(afterCloseCallback); + dialogRef.closed.subscribe(afterCloseCallback); dialogRef.close('Charmander'); viewContainerFixture.detectChanges(); flush(); @@ -196,11 +172,11 @@ describe('Dialog', () => { })); it('should only emit the afterCloseEvent once when closed', fakeAsync(() => { - let dialogRef = dialog.openFromComponent(PizzaMsg, {viewContainerRef: testViewContainerRef}); + let dialogRef = dialog.open(PizzaMsg, {viewContainerRef: testViewContainerRef}); let afterCloseCallback = jasmine.createSpy('afterClose callback'); viewContainerFixture.detectChanges(); - dialogRef.afterClosed().subscribe(afterCloseCallback); + dialogRef.closed.subscribe(afterCloseCallback); dialogRef.close(); viewContainerFixture.detectChanges(); flush(); @@ -208,28 +184,8 @@ describe('Dialog', () => { expect(afterCloseCallback).toHaveBeenCalledTimes(1); })); - it('should close a dialog and get back a result before it is closed', fakeAsync(() => { - const dialogRef = dialog.openFromComponent(PizzaMsg, {viewContainerRef: testViewContainerRef}); - viewContainerFixture.detectChanges(); - - // beforeClose should emit before dialog container is destroyed - const beforeCloseHandler = jasmine.createSpy('beforeClose callback').and.callFake(() => { - expect(overlayContainerElement.querySelector('cdk-dialog-container')) - .not.withContext('dialog container exists when beforeClose is called') - .toBeNull(); - }); - - dialogRef.beforeClosed().subscribe(beforeCloseHandler); - dialogRef.close('Bulbasaur'); - viewContainerFixture.detectChanges(); - flush(); - - expect(beforeCloseHandler).toHaveBeenCalledWith('Bulbasaur'); - expect(overlayContainerElement.querySelector('cdk-dialog-container')).toBeNull(); - })); - it('should close a dialog via the escape key', fakeAsync(() => { - dialog.openFromComponent(PizzaMsg, {viewContainerRef: testViewContainerRef}); + dialog.open(PizzaMsg, {viewContainerRef: testViewContainerRef}); viewContainerFixture.detectChanges(); const event = dispatchKeyboardEvent(document.body, 'keydown', ESCAPE); @@ -241,7 +197,7 @@ describe('Dialog', () => { })); it('should not close a dialog via the escape key if a modifier is pressed', fakeAsync(() => { - dialog.openFromComponent(PizzaMsg, {viewContainerRef: testViewContainerRef}); + dialog.open(PizzaMsg, {viewContainerRef: testViewContainerRef}); viewContainerFixture.detectChanges(); const event = createKeyboardEvent('keydown', ESCAPE, undefined, {alt: true}); @@ -258,7 +214,7 @@ describe('Dialog', () => { onPushFixture.detectChanges(); - const dialogRef = dialog.openFromComponent(PizzaMsg, { + const dialogRef = dialog.open(PizzaMsg, { viewContainerRef: onPushFixture.componentInstance.viewContainerRef, }); @@ -281,7 +237,7 @@ describe('Dialog', () => { })); it('should close when clicking on the overlay backdrop', fakeAsync(() => { - dialog.openFromComponent(PizzaMsg, {viewContainerRef: testViewContainerRef}); + dialog.open(PizzaMsg, {viewContainerRef: testViewContainerRef}); viewContainerFixture.detectChanges(); const backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement; @@ -294,29 +250,33 @@ describe('Dialog', () => { })); it('should emit the backdropClick stream when clicking on the overlay backdrop', fakeAsync(() => { - const dialogRef = dialog.openFromComponent(PizzaMsg, {viewContainerRef: testViewContainerRef}); + const dialogRef = dialog.open(PizzaMsg, { + viewContainerRef: testViewContainerRef, + // Disable closing so the backdrop doesn't go away immediately. + disableClose: true, + }); const spy = jasmine.createSpy('backdropClick spy'); - dialogRef.backdropClick().subscribe(spy); + dialogRef.backdropClick.subscribe(spy); viewContainerFixture.detectChanges(); const backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement; backdrop.click(); - expect(spy).toHaveBeenCalledTimes(1); - viewContainerFixture.detectChanges(); - flush(); + expect(spy).toHaveBeenCalledTimes(1); // Additional clicks after the dialog has closed should not be emitted + dialogRef.disableClose = false; backdrop.click(); + viewContainerFixture.detectChanges(); expect(spy).toHaveBeenCalledTimes(1); })); it('should emit the keyboardEvent stream when key events target the overlay', fakeAsync(() => { - const dialogRef = dialog.openFromComponent(PizzaMsg, {viewContainerRef: testViewContainerRef}); + const dialogRef = dialog.open(PizzaMsg, {viewContainerRef: testViewContainerRef}); const spy = jasmine.createSpy('keyboardEvent spy'); - dialogRef.keydownEvents().subscribe(spy); + dialogRef.keydownEvents.subscribe(spy); viewContainerFixture.detectChanges(); @@ -333,7 +293,7 @@ describe('Dialog', () => { it('should notify the observers if a dialog has been opened', () => { dialog.afterOpened.subscribe(ref => { expect( - dialog.openFromComponent(PizzaMsg, { + dialog.open(PizzaMsg, { viewContainerRef: testViewContainerRef, }), ).toBe(ref); @@ -341,8 +301,8 @@ describe('Dialog', () => { }); it('should notify the observers if all open dialogs have finished closing', fakeAsync(() => { - const ref1 = dialog.openFromComponent(PizzaMsg, {viewContainerRef: testViewContainerRef}); - const ref2 = dialog.openFromComponent(ContentElementDialog, { + const ref1 = dialog.open(PizzaMsg, {viewContainerRef: testViewContainerRef}); + const ref2 = dialog.open(ContentElementDialog, { viewContainerRef: testViewContainerRef, }); const spy = jasmine.createSpy('afterAllClosed spy'); @@ -372,7 +332,7 @@ describe('Dialog', () => { }); it('should override the width of the overlay pane', () => { - dialog.openFromComponent(PizzaMsg, { + dialog.open(PizzaMsg, { width: '500px', }); @@ -384,7 +344,7 @@ describe('Dialog', () => { }); it('should override the height of the overlay pane', () => { - dialog.openFromComponent(PizzaMsg, { + dialog.open(PizzaMsg, { height: '100px', }); @@ -396,7 +356,7 @@ describe('Dialog', () => { }); it('should override the min-width of the overlay pane', () => { - dialog.openFromComponent(PizzaMsg, { + dialog.open(PizzaMsg, { minWidth: '500px', }); @@ -408,15 +368,13 @@ describe('Dialog', () => { }); it('should override the max-width of the overlay pane', fakeAsync(() => { - let dialogRef = dialog.openFromComponent(PizzaMsg); + let dialogRef = dialog.open(PizzaMsg); viewContainerFixture.detectChanges(); let overlayPane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement; - expect(overlayPane.style.maxWidth) - .withContext('Expected dialog to set a default max-width on overlay pane') - .toBe('80vw'); + expect(overlayPane.style.maxWidth).toBeFalsy(); dialogRef.close(); @@ -424,7 +382,7 @@ describe('Dialog', () => { viewContainerFixture.detectChanges(); flushMicrotasks(); - dialogRef = dialog.openFromComponent(PizzaMsg, { + dialogRef = dialog.open(PizzaMsg, { maxWidth: '100px', }); @@ -437,7 +395,7 @@ describe('Dialog', () => { })); it('should override the min-height of the overlay pane', () => { - dialog.openFromComponent(PizzaMsg, { + dialog.open(PizzaMsg, { minHeight: '300px', }); @@ -449,7 +407,7 @@ describe('Dialog', () => { }); it('should override the max-height of the overlay pane', () => { - dialog.openFromComponent(PizzaMsg, { + dialog.open(PizzaMsg, { maxHeight: '100px', }); @@ -460,11 +418,9 @@ describe('Dialog', () => { expect(overlayPane.style.maxHeight).toBe('100px'); }); - it('should override the top offset of the overlay pane', () => { - dialog.openFromComponent(PizzaMsg, { - position: { - top: '100px', - }, + it('should be able to customize the position strategy', () => { + dialog.open(PizzaMsg, { + positionStrategy: overlay.position().global().top('100px'), }); viewContainerFixture.detectChanges(); @@ -474,68 +430,8 @@ describe('Dialog', () => { expect(overlayPane.style.marginTop).toBe('100px'); }); - it('should override the bottom offset of the overlay pane', () => { - dialog.openFromComponent(PizzaMsg, { - position: { - bottom: '200px', - }, - }); - - viewContainerFixture.detectChanges(); - - let overlayPane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement; - - expect(overlayPane.style.marginBottom).toBe('200px'); - }); - - it('should override the left offset of the overlay pane', () => { - dialog.openFromComponent(PizzaMsg, { - position: { - left: '250px', - }, - }); - - viewContainerFixture.detectChanges(); - - let overlayPane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement; - - expect(overlayPane.style.marginLeft).toBe('250px'); - }); - - it('should override the right offset of the overlay pane', () => { - dialog.openFromComponent(PizzaMsg, { - position: { - right: '125px', - }, - }); - - viewContainerFixture.detectChanges(); - - let overlayPane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement; - - expect(overlayPane.style.marginRight).toBe('125px'); - }); - - it('should allow for the position to be updated', () => { - let dialogRef = dialog.openFromComponent(PizzaMsg, { - position: { - left: '250px', - }, - }); - - viewContainerFixture.detectChanges(); - - let overlayPane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement; - - expect(overlayPane.style.marginLeft).toBe('250px'); - - dialogRef.updatePosition({left: '500px'}); - - expect(overlayPane.style.marginLeft).toBe('500px'); - }); - it('should allow for the dimensions to be updated', () => { - let dialogRef = dialog.openFromComponent(PizzaMsg, {width: '100px'}); + let dialogRef = dialog.open(PizzaMsg, {width: '100px'}); viewContainerFixture.detectChanges(); @@ -543,13 +439,13 @@ describe('Dialog', () => { expect(overlayPane.style.width).toBe('100px'); - dialogRef.updateSize({width: '200px'}); + dialogRef.updateSize('200px'); expect(overlayPane.style.width).toBe('200px'); }); it('should allow setting the layout direction', () => { - dialog.openFromComponent(PizzaMsg, {direction: 'rtl'}); + dialog.open(PizzaMsg, {direction: 'rtl'}); viewContainerFixture.detectChanges(); @@ -559,27 +455,27 @@ describe('Dialog', () => { }); it('should inject the correct layout direction in the component instance', () => { - const dialogRef = dialog.openFromComponent(PizzaMsg, {direction: 'rtl'}); + const dialogRef = dialog.open(PizzaMsg, {direction: 'rtl'}); viewContainerFixture.detectChanges(); - expect(dialogRef.componentInstance.directionality.value).toBe('rtl'); + expect(dialogRef.componentInstance!.directionality.value).toBe('rtl'); }); it('should fall back to injecting the global direction if none is passed by the config', () => { - const dialogRef = dialog.openFromComponent(PizzaMsg, {}); + const dialogRef = dialog.open(PizzaMsg, {}); viewContainerFixture.detectChanges(); - expect(dialogRef.componentInstance.directionality.value).toBe('ltr'); + expect(dialogRef.componentInstance!.directionality.value).toBe('ltr'); }); it('should close all of the dialogs', fakeAsync(() => { - dialog.openFromComponent(PizzaMsg); + dialog.open(PizzaMsg); viewContainerFixture.detectChanges(); - dialog.openFromComponent(PizzaMsg); + dialog.open(PizzaMsg); viewContainerFixture.detectChanges(); - dialog.openFromComponent(PizzaMsg); + dialog.open(PizzaMsg); viewContainerFixture.detectChanges(); expect(overlayContainerElement.querySelectorAll('cdk-dialog-container').length).toBe(3); @@ -591,23 +487,10 @@ describe('Dialog', () => { expect(overlayContainerElement.querySelectorAll('cdk-dialog-container').length).toBe(0); })); - it('should set the proper animation states', () => { - let dialogRef = dialog.openFromComponent(PizzaMsg, {viewContainerRef: testViewContainerRef}); - let dialogContainer: CdkDialogContainer = viewContainerFixture.debugElement.query( - By.directive(CdkDialogContainer), - )!.componentInstance; - - expect(dialogContainer._state).toBe('enter'); - - dialogRef.close(); - - expect(dialogContainer._state).toBe('exit'); - }); - it('should close all dialogs when the user goes forwards/backwards in history', fakeAsync(() => { - dialog.openFromComponent(PizzaMsg); + dialog.open(PizzaMsg); viewContainerFixture.detectChanges(); - dialog.openFromComponent(PizzaMsg); + dialog.open(PizzaMsg); viewContainerFixture.detectChanges(); expect(overlayContainerElement.querySelectorAll('cdk-dialog-container').length).toBe(2); @@ -620,9 +503,9 @@ describe('Dialog', () => { })); it('should close all open dialogs when the location hash changes', fakeAsync(() => { - dialog.openFromComponent(PizzaMsg); + dialog.open(PizzaMsg); viewContainerFixture.detectChanges(); - dialog.openFromComponent(PizzaMsg); + dialog.open(PizzaMsg); viewContainerFixture.detectChanges(); expect(overlayContainerElement.querySelectorAll('cdk-dialog-container').length).toBe(2); @@ -635,14 +518,14 @@ describe('Dialog', () => { })); it('should have the componentInstance available in the afterClosed callback', fakeAsync(() => { - let dialogRef = dialog.openFromComponent(PizzaMsg); + let dialogRef = dialog.open(PizzaMsg); let spy = jasmine.createSpy('afterClosed spy'); flushMicrotasks(); viewContainerFixture.detectChanges(); flushMicrotasks(); - dialogRef.afterClosed().subscribe(() => { + dialogRef.closed.subscribe(() => { spy(); }); @@ -657,8 +540,8 @@ describe('Dialog', () => { })); it('should close all open dialogs on destroy', fakeAsync(() => { - dialog.openFromComponent(PizzaMsg, {viewContainerRef: testViewContainerRef}); - dialog.openFromComponent(PizzaMsg, {viewContainerRef: testViewContainerRef}); + dialog.open(PizzaMsg, {viewContainerRef: testViewContainerRef}); + dialog.open(PizzaMsg, {viewContainerRef: testViewContainerRef}); viewContainerFixture.detectChanges(); expect(overlayContainerElement.querySelectorAll('cdk-dialog-container').length).toBe(2); @@ -670,27 +553,18 @@ describe('Dialog', () => { expect(overlayContainerElement.querySelectorAll('cdk-dialog-container').length).toBe(0); })); - it('should complete the various lifecycle streams on destroy', fakeAsync(() => { - let dialogRef = dialog.openFromComponent(PizzaMsg, {viewContainerRef: testViewContainerRef}); - let beforeOpenedComplete = jasmine.createSpy('before opened complete spy'); - let afterOpenedComplete = jasmine.createSpy('after opened complete spy'); - let beforeClosedComplete = jasmine.createSpy('before closed complete spy'); - let afterClosedComplete = jasmine.createSpy('after closed complete spy'); + it('should complete the closed stream on destroy', fakeAsync(() => { + let dialogRef = dialog.open(PizzaMsg, {viewContainerRef: testViewContainerRef}); + let closedCompleteSpy = jasmine.createSpy('closed complete spy'); viewContainerFixture.detectChanges(); - dialogRef.beforeOpened().subscribe({complete: beforeOpenedComplete}); - dialogRef.afterOpened().subscribe({complete: afterOpenedComplete}); - dialogRef.beforeClosed().subscribe({complete: beforeClosedComplete}); - dialogRef.afterClosed().subscribe({complete: afterClosedComplete}); + dialogRef.closed.subscribe({complete: closedCompleteSpy}); dialogRef.close('Charmander'); viewContainerFixture.detectChanges(); flush(); - expect(beforeOpenedComplete).toHaveBeenCalled(); - expect(afterOpenedComplete).toHaveBeenCalled(); - expect(beforeClosedComplete).toHaveBeenCalled(); - expect(afterClosedComplete).toHaveBeenCalled(); + expect(closedCompleteSpy).toHaveBeenCalled(); })); describe('passing in data', () => { @@ -702,7 +576,7 @@ describe('Dialog', () => { }, }; - let instance = dialog.openFromComponent(DialogWithInjectedData, config).componentInstance; + let instance = dialog.open(DialogWithInjectedData, config).componentInstance!; expect(instance.data.stringParam).toBe(config.data.stringParam); expect(instance.data.dateParam).toBe(config.data.dateParam); @@ -710,15 +584,15 @@ describe('Dialog', () => { it('should default to null if no data is passed', () => { expect(() => { - let dialogRef = dialog.openFromComponent(DialogWithInjectedData); + let dialogRef = dialog.open(DialogWithInjectedData); viewContainerFixture.detectChanges(); - expect(dialogRef.componentInstance.data).toBeNull(); + expect(dialogRef.componentInstance!.data).toBeNull(); }).not.toThrow(); }); }); it('should not keep a reference to the component after the dialog is closed', fakeAsync(() => { - let dialogRef = dialog.openFromComponent(PizzaMsg); + let dialogRef = dialog.open(PizzaMsg); viewContainerFixture.detectChanges(); expect(dialogRef.componentInstance).toBeTruthy(); @@ -734,8 +608,8 @@ describe('Dialog', () => { })); it('should assign a unique id to each dialog', () => { - const one = dialog.openFromComponent(PizzaMsg); - const two = dialog.openFromComponent(PizzaMsg); + const one = dialog.open(PizzaMsg); + const two = dialog.open(PizzaMsg); expect(one.id).toBeTruthy(); expect(two.id).toBeTruthy(); @@ -743,23 +617,23 @@ describe('Dialog', () => { }); it('should allow for the id to be overwritten', () => { - const dialogRef = dialog.openFromComponent(PizzaMsg, {id: 'pizza'}); + const dialogRef = dialog.open(PizzaMsg, {id: 'pizza'}); expect(dialogRef.id).toBe('pizza'); }); it('should throw when trying to open a dialog with the same id as another dialog', () => { - dialog.openFromComponent(PizzaMsg, {id: 'pizza'}); - expect(() => dialog.openFromComponent(PizzaMsg, {id: 'pizza'})).toThrowError(/must be unique/g); + dialog.open(PizzaMsg, {id: 'pizza'}); + expect(() => dialog.open(PizzaMsg, {id: 'pizza'})).toThrowError(/must be unique/g); }); it('should be able to find a dialog by id', () => { - const dialogRef = dialog.openFromComponent(PizzaMsg, {id: 'pizza'}); - expect(dialog.getById('pizza')).toBe(dialogRef); + const dialogRef = dialog.open(PizzaMsg, {id: 'pizza'}); + expect(dialog.getDialogById('pizza')).toBe(dialogRef); }); describe('disableClose option', () => { it('should prevent closing via clicks on the backdrop', fakeAsync(() => { - dialog.openFromComponent(PizzaMsg, { + dialog.open(PizzaMsg, { disableClose: true, viewContainerRef: testViewContainerRef, }); @@ -775,7 +649,7 @@ describe('Dialog', () => { })); it('should prevent closing via the escape key', fakeAsync(() => { - dialog.openFromComponent(PizzaMsg, { + dialog.open(PizzaMsg, { disableClose: true, viewContainerRef: testViewContainerRef, }); @@ -789,7 +663,7 @@ describe('Dialog', () => { })); it('should allow for the disableClose option to be updated while open', fakeAsync(() => { - let dialogRef = dialog.openFromComponent(PizzaMsg, { + let dialogRef = dialog.open(PizzaMsg, { disableClose: true, viewContainerRef: testViewContainerRef, }); @@ -813,7 +687,7 @@ describe('Dialog', () => { const templateRefFixture = TestBed.createComponent(ComponentWithTemplateRef); templateRefFixture.detectChanges(); - dialog.openFromTemplate(templateRefFixture.componentInstance.templateRef, { + dialog.open(templateRefFixture.componentInstance.templateRef, { disableClose: true, }); @@ -830,7 +704,7 @@ describe('Dialog', () => { describe('hasBackdrop option', () => { it('should have a backdrop', () => { - dialog.openFromComponent(PizzaMsg, { + dialog.open(PizzaMsg, { hasBackdrop: true, viewContainerRef: testViewContainerRef, }); @@ -841,7 +715,7 @@ describe('Dialog', () => { }); it('should not have a backdrop', () => { - dialog.openFromComponent(PizzaMsg, { + dialog.open(PizzaMsg, { hasBackdrop: false, viewContainerRef: testViewContainerRef, }); @@ -854,7 +728,7 @@ describe('Dialog', () => { describe('panelClass option', () => { it('should have custom panel class', () => { - dialog.openFromComponent(PizzaMsg, { + dialog.open(PizzaMsg, { panelClass: 'custom-panel-class', viewContainerRef: testViewContainerRef, }); @@ -867,7 +741,7 @@ describe('Dialog', () => { describe('backdropClass option', () => { it('should have default backdrop class', () => { - dialog.openFromComponent(PizzaMsg, { + dialog.open(PizzaMsg, { backdropClass: '', viewContainerRef: testViewContainerRef, }); @@ -878,7 +752,7 @@ describe('Dialog', () => { }); it('should have custom backdrop class', () => { - dialog.openFromComponent(PizzaMsg, { + dialog.open(PizzaMsg, { backdropClass: 'custom-backdrop-class', viewContainerRef: testViewContainerRef, }); @@ -895,7 +769,7 @@ describe('Dialog', () => { afterEach(() => overlayContainerElement.remove()); it('should focus the first tabbable element of the dialog on open (the default)', fakeAsync(() => { - dialog.openFromComponent(PizzaMsg, { + dialog.open(PizzaMsg, { viewContainerRef: testViewContainerRef, }); @@ -908,7 +782,7 @@ describe('Dialog', () => { })); it('should focus the dialog element on open', fakeAsync(() => { - dialog.openFromComponent(PizzaMsg, { + dialog.open(PizzaMsg, { viewContainerRef: testViewContainerRef, autoFocus: 'dialog', }); @@ -926,7 +800,7 @@ describe('Dialog', () => { })); it('should focus the first header element on open', fakeAsync(() => { - dialog.openFromComponent(ContentElementDialog, { + dialog.open(ContentElementDialog, { viewContainerRef: testViewContainerRef, autoFocus: 'first-heading', }); @@ -944,7 +818,7 @@ describe('Dialog', () => { })); it('should focus the first element that matches the css selector from autoFocus on open', fakeAsync(() => { - dialog.openFromComponent(PizzaMsg, { + dialog.open(PizzaMsg, { viewContainerRef: testViewContainerRef, autoFocus: 'p', }); @@ -968,7 +842,7 @@ describe('Dialog', () => { document.body.appendChild(button); button.focus(); - let dialogRef = dialog.openFromComponent(PizzaMsg, {viewContainerRef: testViewContainerRef}); + let dialogRef = dialog.open(PizzaMsg, {viewContainerRef: testViewContainerRef}); flushMicrotasks(); viewContainerFixture.detectChanges(); @@ -980,11 +854,6 @@ describe('Dialog', () => { ); dialogRef.close(); - expect(document.activeElement!.id).not.toBe( - 'dialog-trigger', - 'Expcted the focus not to have changed before the animation finishes.', - ); - flushMicrotasks(); viewContainerFixture.detectChanges(); flush(); @@ -1008,7 +877,7 @@ describe('Dialog', () => { button.focus(); - const dialogRef = dialog.openFromComponent(PizzaMsg); + const dialogRef = dialog.open(PizzaMsg); flushMicrotasks(); fixture.detectChanges(); flushMicrotasks(); @@ -1022,50 +891,6 @@ describe('Dialog', () => { expect(spy).toHaveBeenCalled(); })); - it('should not move focus if it was moved outside the dialog while animating', fakeAsync(() => { - // Create a element that has focus before the dialog is opened. - const button = document.createElement('button'); - const otherButton = document.createElement('button'); - const body = document.body; - button.id = 'dialog-trigger'; - otherButton.id = 'other-button'; - body.appendChild(button); - body.appendChild(otherButton); - button.focus(); - - const dialogRef = dialog.openFromComponent(PizzaMsg, { - viewContainerRef: testViewContainerRef, - }); - - flushMicrotasks(); - viewContainerFixture.detectChanges(); - flushMicrotasks(); - - expect(document.activeElement!.id).not.toBe( - 'dialog-trigger', - 'Expected the focus to change when dialog was opened.', - ); - - // Start the closing sequence and move focus out of dialog. - dialogRef.close(); - otherButton.focus(); - - expect(document.activeElement!.id) - .withContext('Expected focus to be on the alternate button.') - .toBe('other-button'); - - flushMicrotasks(); - viewContainerFixture.detectChanges(); - flush(); - - expect(document.activeElement!.id) - .withContext('Expected focus to stay on the alternate button.') - .toBe('other-button'); - - button.remove(); - otherButton.remove(); - })); - it('should allow the consumer to shift focus in afterClosed', fakeAsync(() => { // Create a element that has focus before the dialog is opened. let button = document.createElement('button'); @@ -1078,12 +903,12 @@ describe('Dialog', () => { document.body.appendChild(input); button.focus(); - let dialogRef = dialog.openFromComponent(PizzaMsg, {viewContainerRef: testViewContainerRef}); + let dialogRef = dialog.open(PizzaMsg, {viewContainerRef: testViewContainerRef}); tick(500); viewContainerFixture.detectChanges(); - dialogRef.afterClosed().subscribe(() => input.focus()); + dialogRef.closed.subscribe(() => input.focus()); dialogRef.close(); tick(500); @@ -1100,7 +925,7 @@ describe('Dialog', () => { })); it('should move focus to the container if there are no focusable elements in the dialog', fakeAsync(() => { - dialog.openFromComponent(DialogWithoutFocusableElements); + dialog.open(DialogWithoutFocusableElements); viewContainerFixture.detectChanges(); flushMicrotasks(); @@ -1113,7 +938,7 @@ describe('Dialog', () => { describe('aria-label', () => { it('should be able to set a custom aria-label', () => { - dialog.openFromComponent(PizzaMsg, { + dialog.open(PizzaMsg, { ariaLabel: 'Hello there', viewContainerRef: testViewContainerRef, }); @@ -1124,7 +949,7 @@ describe('Dialog', () => { }); it('should not set the aria-labelledby automatically if it has an aria-label', fakeAsync(() => { - dialog.openFromComponent(ContentElementDialog, { + dialog.open(ContentElementDialog, { ariaLabel: 'Hello there', viewContainerRef: testViewContainerRef, }); @@ -1146,7 +971,7 @@ describe('Dialog with a parent Dialog', () => { beforeEach(fakeAsync(() => { TestBed.configureTestingModule({ - imports: [DialogModule, NoopAnimationsModule], + imports: [DialogModule], declarations: [ComponentThatProvidesMatDialog], providers: [ { @@ -1161,11 +986,7 @@ describe('Dialog with a parent Dialog', () => { }); TestBed.compileComponents(); - })); - - beforeEach(inject([Dialog], (d: Dialog) => { - parentDialog = d; - + parentDialog = TestBed.inject(Dialog); fixture = TestBed.createComponent(ComponentThatProvidesMatDialog); childDialog = fixture.componentInstance.dialog; fixture.detectChanges(); @@ -1176,7 +997,7 @@ describe('Dialog with a parent Dialog', () => { }); it('should close dialogs opened by a parent when calling closeAll on a child Dialog', fakeAsync(() => { - parentDialog.openFromComponent(PizzaMsg); + parentDialog.open(PizzaMsg); fixture.detectChanges(); flush(); @@ -1194,7 +1015,7 @@ describe('Dialog with a parent Dialog', () => { })); it('should close dialogs opened by a child when calling closeAll on a parent Dialog', fakeAsync(() => { - childDialog.openFromComponent(PizzaMsg); + childDialog.open(PizzaMsg); fixture.detectChanges(); expect(overlayContainerElement.textContent) @@ -1211,7 +1032,7 @@ describe('Dialog with a parent Dialog', () => { })); it('should not close the parent dialogs, when a child is destroyed', fakeAsync(() => { - parentDialog.openFromComponent(PizzaMsg); + parentDialog.open(PizzaMsg); fixture.detectChanges(); flush(); @@ -1229,7 +1050,7 @@ describe('Dialog with a parent Dialog', () => { })); it('should close the top dialog via the escape key', fakeAsync(() => { - childDialog.openFromComponent(PizzaMsg); + childDialog.open(PizzaMsg); fixture.detectChanges(); dispatchKeyboardEvent(document.body, 'keydown', ESCAPE); diff --git a/src/cdk-experimental/dialog/dialog.ts b/src/cdk-experimental/dialog/dialog.ts index cc9fc8cf0fc8..79da1243ba17 100644 --- a/src/cdk-experimental/dialog/dialog.ts +++ b/src/cdk-experimental/dialog/dialog.ts @@ -8,177 +8,163 @@ import { TemplateRef, - SkipSelf, - Optional, Injectable, Injector, - Inject, - ComponentRef, OnDestroy, Type, StaticProvider, InjectFlags, + Inject, + Optional, + SkipSelf, } from '@angular/core'; -import {ComponentPortal, TemplatePortal} from '@angular/cdk/portal'; +import {BasePortalOutlet, ComponentPortal, TemplatePortal} from '@angular/cdk/portal'; import {of as observableOf, Observable, Subject, defer} from 'rxjs'; import {DialogRef} from './dialog-ref'; -import {Location} from '@angular/common'; import {DialogConfig} from './dialog-config'; import {Directionality} from '@angular/cdk/bidi'; -import {CdkDialogContainer} from './dialog-container'; import { ComponentType, Overlay, OverlayRef, OverlayConfig, ScrollStrategy, + OverlayContainer, } from '@angular/cdk/overlay'; import {startWith} from 'rxjs/operators'; -import { - DIALOG_SCROLL_STRATEGY, - DIALOG_DATA, - DIALOG_REF, - DIALOG_CONTAINER, - DIALOG_CONFIG, -} from './dialog-injectors'; +import {DEFAULT_DIALOG_CONFIG, DIALOG_DATA, DIALOG_SCROLL_STRATEGY} from './dialog-injectors'; +import {CdkDialogContainer} from './dialog-container'; + +/** Unique id for the created dialog. */ +let uniqueId = 0; -/** - * Service to open modal dialogs. - */ @Injectable() export class Dialog implements OnDestroy { + private _openDialogsAtThisLevel: DialogRef[] = []; + private readonly _afterAllClosedAtThisLevel = new Subject(); + private readonly _afterOpenedAtThisLevel = new Subject(); + private _ariaHiddenElements = new Map(); private _scrollStrategy: () => ScrollStrategy; - /** Stream that emits when all dialogs are closed. */ - _getAfterAllClosed(): Observable { - return this._parentDialog ? this._parentDialog.afterAllClosed : this._afterAllClosedBase; + /** Keeps track of the currently-open dialogs. */ + get openDialogs(): DialogRef[] { + return this._parentDialog ? this._parentDialog.openDialogs : this._openDialogsAtThisLevel; } - readonly _afterAllClosedBase = new Subject(); - // TODO(jelbourn): tighten the type on the right-hand side of this expression. - afterAllClosed: Observable = defer(() => + /** Stream that emits when a dialog has been opened. */ + get afterOpened(): Subject> { + return this._parentDialog ? this._parentDialog.afterOpened : this._afterOpenedAtThisLevel; + } + + /** + * Stream that emits when all open dialog have finished closing. + * Will emit on subscribe if there are no open dialogs to begin with. + */ + readonly afterAllClosed: Observable = defer(() => this.openDialogs.length ? this._getAfterAllClosed() : this._getAfterAllClosed().pipe(startWith(undefined)), ); - /** Stream that emits when a dialog is opened. */ - get afterOpened(): Subject> { - return this._parentDialog ? this._parentDialog.afterOpened : this._afterOpened; - } - readonly _afterOpened = new Subject>(); - - /** Stream that emits when a dialog is opened. */ - get openDialogs(): DialogRef[] { - return this._parentDialog ? this._parentDialog.openDialogs : this._openDialogs; - } - _openDialogs: DialogRef[] = []; - constructor( private _overlay: Overlay, private _injector: Injector, - @Inject(DIALOG_REF) private _dialogRefConstructor: Type>, - // TODO(crisbeto): the `any` here can be replaced - // with the proper type once we start using Ivy. - @Inject(DIALOG_SCROLL_STRATEGY) scrollStrategy: any, + @Optional() @Inject(DEFAULT_DIALOG_CONFIG) private _defaultOptions: DialogConfig, @Optional() @SkipSelf() private _parentDialog: Dialog, - @Optional() location: Location, + private _overlayContainer: OverlayContainer, + @Inject(DIALOG_SCROLL_STRATEGY) scrollStrategy: any, ) { - // Close all of the dialogs when the user goes forwards/backwards in history or when the - // location hash changes. Note that this usually doesn't include clicking on links (unless - // the user is using the `HashLocationStrategy`). - if (!_parentDialog && location) { - location.subscribe(() => this.closeAll()); - } - this._scrollStrategy = scrollStrategy; } - /** Gets an open dialog by id. */ - getById(id: string): DialogRef | undefined { - return this._openDialogs.find(ref => ref.id === id); - } - - /** Closes all open dialogs. */ - closeAll(): void { - this.openDialogs.forEach(ref => ref.close()); - } + /** + * Opens a modal dialog containing the given component. + * @param component Type of the component to load into the dialog. + * @param config Extra configuration options. + * @returns Reference to the newly-opened dialog. + */ + open( + component: ComponentType, + config?: DialogConfig>, + ): DialogRef; - /** Opens a dialog from a component. */ - openFromComponent(component: ComponentType, config?: DialogConfig): DialogRef { - config = this._applyConfigDefaults(config); + /** + * Opens a modal dialog containing the given template. + * @param template TemplateRef to instantiate as the dialog content. + * @param config Extra configuration options. + * @returns Reference to the newly-opened dialog. + */ + open( + template: TemplateRef, + config?: DialogConfig>, + ): DialogRef; + + open( + componentOrTemplateRef: ComponentType | TemplateRef, + config?: DialogConfig>, + ): DialogRef; + + open( + componentOrTemplateRef: ComponentType | TemplateRef, + config?: DialogConfig>, + ): DialogRef { + const defaults = (this._defaultOptions || new DialogConfig()) as DialogConfig< + D, + DialogRef + >; + config = {...defaults, ...config}; + config.id = config.id || `cdk-dialog-${uniqueId++}`; - if (config.id && this.getById(config.id) && (typeof ngDevMode === 'undefined' || ngDevMode)) { + if ( + config.id && + this.getDialogById(config.id) && + (typeof ngDevMode === 'undefined' || ngDevMode) + ) { throw Error(`Dialog with id "${config.id}" exists already. The dialog id must be unique.`); } - const overlayRef = this._createOverlay(config); - const dialogContainer = this._attachDialogContainer(overlayRef, config); - const dialogRef = this._attachDialogContentForComponent( - component, - dialogContainer, - overlayRef, - config, - ); - - this._registerDialogRef(dialogRef); - dialogContainer._initializeWithAttachedContent(); - - return dialogRef; - } + const overlayConfig = this._getOverlayConfig(config); + const overlayRef = this._overlay.create(overlayConfig); + const dialogRef = new DialogRef(overlayRef, config); + const dialogContainer = this._attachContainer(overlayRef, dialogRef, config); - /** Opens a dialog from a template. */ - openFromTemplate(template: TemplateRef, config?: DialogConfig): DialogRef { - config = this._applyConfigDefaults(config); + dialogRef.containerInstance = dialogContainer; + this._attachDialogContent(componentOrTemplateRef, dialogRef, dialogContainer, config); - if (config.id && this.getById(config.id) && (typeof ngDevMode === 'undefined' || ngDevMode)) { - throw Error(`Dialog with id "${config.id}" exists already. The dialog id must be unique.`); + // If this is the first dialog that we're opening, hide all the non-overlay content. + if (!this.openDialogs.length) { + this._hideNonDialogContentFromAssistiveTechnology(); } - const overlayRef = this._createOverlay(config); - const dialogContainer = this._attachDialogContainer(overlayRef, config); - const dialogRef = this._attachDialogContentForTemplate( - template, - dialogContainer, - overlayRef, - config, - ); - - this._registerDialogRef(dialogRef); - dialogContainer._initializeWithAttachedContent(); + this.openDialogs.push(dialogRef); + dialogRef.closed.subscribe(() => this._removeOpenDialog(dialogRef)); + this.afterOpened.next(dialogRef); return dialogRef; } - ngOnDestroy() { - // Only close all the dialogs at this level. - this._openDialogs.forEach(ref => ref.close()); + /** + * Closes all of the currently-open dialogs. + */ + closeAll(): void { + this._closeDialogs(this.openDialogs); } /** - * Forwards emitting events for when dialogs are opened and all dialogs are closed. + * Finds an open dialog by its id. + * @param id ID to use when looking up the dialog. */ - private _registerDialogRef(dialogRef: DialogRef): void { - this.openDialogs.push(dialogRef); - - const dialogOpenSub = dialogRef.afterOpened().subscribe(() => { - this.afterOpened.next(dialogRef); - dialogOpenSub.unsubscribe(); - }); - - const dialogCloseSub = dialogRef.afterClosed().subscribe(() => { - let dialogIndex = this._openDialogs.indexOf(dialogRef); - - if (dialogIndex > -1) { - this._openDialogs.splice(dialogIndex, 1); - } + getDialogById(id: string): DialogRef | undefined { + return this.openDialogs.find(dialog => dialog.id === id); + } - if (!this._openDialogs.length) { - this._afterAllClosedBase.next(); - dialogCloseSub.unsubscribe(); - } - }); + ngOnDestroy() { + // Only close the dialogs at this level on destroy + // since the parent service may still be active. + this._closeDialogs(this._openDialogsAtThisLevel); + this._afterAllClosedAtThisLevel.complete(); + this._afterOpenedAtThisLevel.complete(); } /** @@ -186,10 +172,12 @@ export class Dialog implements OnDestroy { * @param config The dialog configuration. * @returns The overlay configuration. */ - protected _createOverlay(config: DialogConfig): OverlayRef { - const overlayConfig = new OverlayConfig({ - positionStrategy: this._overlay.position().global(), - scrollStrategy: this._scrollStrategy(), + private _getOverlayConfig(config: DialogConfig): OverlayConfig { + const state = new OverlayConfig({ + positionStrategy: + config.positionStrategy || + this._overlay.position().global().centerHorizontally().centerVertically(), + scrollStrategy: config.scrollStrategy || this._scrollStrategy(), panelClass: config.panelClass, hasBackdrop: config.hasBackdrop, direction: config.direction, @@ -197,107 +185,130 @@ export class Dialog implements OnDestroy { minHeight: config.minHeight, maxWidth: config.maxWidth, maxHeight: config.maxHeight, + width: config.width, + height: config.height, + disposeOnNavigation: config.closeOnNavigation, }); if (config.backdropClass) { - overlayConfig.backdropClass = config.backdropClass; + state.backdropClass = config.backdropClass; } - return this._overlay.create(overlayConfig); + + return state; } /** - * Attaches an MatDialogContainer to a dialog's already-created overlay. + * Attaches a dialog container to a dialog's already-created overlay. * @param overlay Reference to the dialog's underlying overlay. * @param config The dialog configuration. * @returns A promise resolving to a ComponentRef for the attached container. */ - protected _attachDialogContainer(overlay: OverlayRef, config: DialogConfig): CdkDialogContainer { - const container = config.containerComponent || this._injector.get(DIALOG_CONTAINER); + private _attachContainer( + overlay: OverlayRef, + dialogRef: DialogRef, + config: DialogConfig>, + ): BasePortalOutlet { const userInjector = config.injector ?? config.viewContainerRef?.injector; - const injector = Injector.create({ - parent: userInjector || this._injector, - providers: [{provide: DialogConfig, useValue: config}], - }); - const containerPortal = new ComponentPortal(container, config.viewContainerRef, injector); - const containerRef: ComponentRef = overlay.attach(containerPortal); - containerRef.instance._config = config; + const providers: StaticProvider[] = [ + {provide: DialogConfig, useValue: config}, + {provide: DialogRef, useValue: dialogRef}, + {provide: OverlayRef, useValue: overlay}, + ]; + let containerType: Type; + + if (config.container) { + if (typeof config.container === 'function') { + containerType = config.container; + } else { + containerType = config.container.type; + providers.push(...config.container.providers(config)); + } + } else { + containerType = CdkDialogContainer; + } + + const containerPortal = new ComponentPortal( + containerType, + config.viewContainerRef, + Injector.create({parent: userInjector || this._injector, providers}), + config.componentFactoryResolver, + ); + const containerRef = overlay.attach(containerPortal); return containerRef.instance; } /** - * Attaches the user-provided component to the already-created MatDialogContainer. + * Attaches the user-provided component to the already-created dialog container. * @param componentOrTemplateRef The type of component being loaded into the dialog, * or a TemplateRef to instantiate as the content. - * @param dialogContainer Reference to the wrapping MatDialogContainer. - * @param overlayRef Reference to the overlay in which the dialog resides. - * @param config The dialog configuration. - * @returns A promise resolving to the MatDialogRef that should be returned to the user. + * @param dialogRef Reference to the dialog being opened. + * @param dialogContainer Component that is going to wrap the dialog content. + * @param config Configuration used to open the dialog. */ - protected _attachDialogContentForComponent( - componentOrTemplateRef: ComponentType, - dialogContainer: CdkDialogContainer, - overlayRef: OverlayRef, - config: DialogConfig, - ): DialogRef { - // Create a reference to the dialog we're creating in order to give the user a handle - // to modify and close it. - const dialogRef = this._createDialogRef(overlayRef, dialogContainer, config); - const injector = this._createInjector(config, dialogRef, dialogContainer); - const contentRef = dialogContainer.attachComponentPortal( - new ComponentPortal(componentOrTemplateRef, undefined, injector), - ); - dialogRef.componentInstance = contentRef.instance; - return dialogRef; - } + private _attachDialogContent( + componentOrTemplateRef: ComponentType | TemplateRef, + dialogRef: DialogRef, + dialogContainer: BasePortalOutlet, + config: DialogConfig>, + ) { + const injector = this._createInjector(config, dialogRef, dialogContainer); + + if (componentOrTemplateRef instanceof TemplateRef) { + let context: any = {$implicit: config.data, dialogRef}; + + if (config.templateContext) { + context = { + ...context, + ...(typeof config.templateContext === 'function' + ? config.templateContext() + : config.templateContext), + }; + } - /** - * Attaches the user-provided component to the already-created MatDialogContainer. - * @param componentOrTemplateRef The type of component being loaded into the dialog, - * or a TemplateRef to instantiate as the content. - * @param dialogContainer Reference to the wrapping MatDialogContainer. - * @param overlayRef Reference to the overlay in which the dialog resides. - * @param config The dialog configuration. - * @returns A promise resolving to the MatDialogRef that should be returned to the user. - */ - protected _attachDialogContentForTemplate( - componentOrTemplateRef: TemplateRef, - dialogContainer: CdkDialogContainer, - overlayRef: OverlayRef, - config: DialogConfig, - ): DialogRef { - // Create a reference to the dialog we're creating in order to give the user a handle - // to modify and close it. - const dialogRef = this._createDialogRef(overlayRef, dialogContainer, config); - dialogContainer.attachTemplatePortal( - new TemplatePortal(componentOrTemplateRef, null!, { - $implicit: config.data, - dialogRef, - }), - ); - return dialogRef; + dialogContainer.attachTemplatePortal( + new TemplatePortal(componentOrTemplateRef, null!, context, injector), + ); + } else { + const contentRef = dialogContainer.attachComponentPortal( + new ComponentPortal( + componentOrTemplateRef, + config.viewContainerRef, + injector, + config.componentFactoryResolver, + ), + ); + dialogRef.componentInstance = contentRef.instance; + } } /** * Creates a custom injector to be used inside the dialog. This allows a component loaded inside * of a dialog to close itself and, optionally, to return a value. * @param config Config object that is used to construct the dialog. - * @param dialogRef Reference to the dialog. - * @param container Dialog container element that wraps all of the contents. + * @param dialogRef Reference to the dialog being opened. + * @param dialogContainer Component that is going to wrap the dialog content. * @returns The custom injector that can be used inside the dialog. */ - private _createInjector( - config: DialogConfig, - dialogRef: DialogRef, - dialogContainer: CdkDialogContainer, + private _createInjector( + config: DialogConfig>, + dialogRef: DialogRef, + dialogContainer: BasePortalOutlet, ): Injector { const userInjector = config && config.viewContainerRef && config.viewContainerRef.injector; const providers: StaticProvider[] = [ - {provide: this._injector.get(DIALOG_REF), useValue: dialogRef}, - {provide: this._injector.get(DIALOG_CONTAINER), useValue: dialogContainer}, {provide: DIALOG_DATA, useValue: config.data}, + {provide: DialogRef, useValue: dialogRef}, ]; + if (config.providers) { + if (typeof config.providers === 'function') { + providers.push(...config.providers(dialogRef, config, dialogContainer)); + } else { + providers.push(...config.providers); + } + } + if ( config.direction && (!userInjector || @@ -312,24 +323,72 @@ export class Dialog implements OnDestroy { return Injector.create({parent: userInjector || this._injector, providers}); } - /** Creates a new dialog ref. */ - private _createDialogRef( - overlayRef: OverlayRef, - dialogContainer: CdkDialogContainer, - config: DialogConfig, - ) { - const dialogRef = new this._dialogRefConstructor(overlayRef, dialogContainer, config.id); - dialogRef.disableClose = config.disableClose; - dialogRef.updateSize(config).updatePosition(config.position); - return dialogRef; - } - /** - * Expands the provided configuration object to include the default values for properties which - * are undefined. + * Removes a dialog from the array of open dialogs. + * @param dialogRef Dialog to be removed. */ - private _applyConfigDefaults(config?: DialogConfig): DialogConfig { - const dialogConfig = this._injector.get(DIALOG_CONFIG) as typeof DialogConfig; - return {...new dialogConfig(), ...config}; + private _removeOpenDialog(dialogRef: DialogRef) { + const index = this.openDialogs.indexOf(dialogRef); + + if (index > -1) { + this.openDialogs.splice(index, 1); + + // If all the dialogs were closed, remove/restore the `aria-hidden` + // to a the siblings and emit to the `afterAllClosed` stream. + if (!this.openDialogs.length) { + this._ariaHiddenElements.forEach((previousValue, element) => { + if (previousValue) { + element.setAttribute('aria-hidden', previousValue); + } else { + element.removeAttribute('aria-hidden'); + } + }); + + this._ariaHiddenElements.clear(); + this._getAfterAllClosed().next(); + } + } + } + + /** Hides all of the content that isn't an overlay from assistive technology. */ + private _hideNonDialogContentFromAssistiveTechnology() { + const overlayContainer = this._overlayContainer.getContainerElement(); + + // Ensure that the overlay container is attached to the DOM. + if (overlayContainer.parentElement) { + const siblings = overlayContainer.parentElement.children; + + for (let i = siblings.length - 1; i > -1; i--) { + const sibling = siblings[i]; + + if ( + sibling !== overlayContainer && + sibling.nodeName !== 'SCRIPT' && + sibling.nodeName !== 'STYLE' && + !sibling.hasAttribute('aria-live') + ) { + this._ariaHiddenElements.set(sibling, sibling.getAttribute('aria-hidden')); + sibling.setAttribute('aria-hidden', 'true'); + } + } + } + } + + /** Closes all of the dialogs in an array. */ + private _closeDialogs(dialogs: DialogRef[]) { + let i = dialogs.length; + + while (i--) { + // The `_openDialogs` property isn't updated after close until the rxjs subscription + // runs on the next microtask, in addition to modifying the array as we're going + // through it. We loop through all of them and call close without assuming that + // they'll be removed from the list instantaneously. + dialogs[i].close(); + } + } + + private _getAfterAllClosed(): Subject { + const parent = this._parentDialog; + return parent ? parent._getAfterAllClosed() : this._afterAllClosedAtThisLevel; } } diff --git a/src/dev-app/BUILD.bazel b/src/dev-app/BUILD.bazel index 896402e0c85f..28f7b78cbe8a 100644 --- a/src/dev-app/BUILD.bazel +++ b/src/dev-app/BUILD.bazel @@ -23,6 +23,7 @@ ng_module( "//src/dev-app/button", "//src/dev-app/button-toggle", "//src/dev-app/card", + "//src/dev-app/cdk-dialog", "//src/dev-app/cdk-experimental-combobox", "//src/dev-app/cdk-experimental-listbox", "//src/dev-app/cdk-experimental-menu", diff --git a/src/dev-app/cdk-dialog/BUILD.bazel b/src/dev-app/cdk-dialog/BUILD.bazel new file mode 100644 index 000000000000..f145415a29c2 --- /dev/null +++ b/src/dev-app/cdk-dialog/BUILD.bazel @@ -0,0 +1,22 @@ +load("//tools:defaults.bzl", "ng_module", "sass_binary") + +package(default_visibility = ["//visibility:public"]) + +ng_module( + name = "cdk-dialog", + srcs = glob(["**/*.ts"]), + assets = [ + "dialog-demo.html", + ":dialog_demo_scss", + ], + deps = [ + "//src/cdk-experimental/dialog", + "@npm//@angular/forms", + "@npm//@angular/router", + ], +) + +sass_binary( + name = "dialog_demo_scss", + src = "dialog-demo.scss", +) diff --git a/src/dev-app/cdk-dialog/dialog-demo-module.ts b/src/dev-app/cdk-dialog/dialog-demo-module.ts new file mode 100644 index 000000000000..bce7f21d03cd --- /dev/null +++ b/src/dev-app/cdk-dialog/dialog-demo-module.ts @@ -0,0 +1,19 @@ +/** + * @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 {DialogModule} from '@angular/cdk-experimental/dialog'; +import {NgModule} from '@angular/core'; +import {FormsModule} from '@angular/forms'; +import {RouterModule} from '@angular/router'; +import {DialogDemo, JazzDialog} from './dialog-demo'; + +@NgModule({ + imports: [DialogModule, FormsModule, RouterModule.forChild([{path: '', component: DialogDemo}])], + declarations: [DialogDemo, JazzDialog], +}) +export class DialogDemoModule {} diff --git a/src/dev-app/cdk-dialog/dialog-demo.html b/src/dev-app/cdk-dialog/dialog-demo.html new file mode 100644 index 000000000000..73b473ef3be4 --- /dev/null +++ b/src/dev-app/cdk-dialog/dialog-demo.html @@ -0,0 +1,61 @@ +
+

Dialog config

+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+

Last result: {{result}}

+ + + +
+ + + I'm a template dialog. I've been opened {{numTemplateOpens}} times! + +

It's Jazz!

+ + + + +

{{ data.message }}

+ + +
diff --git a/src/dev-app/cdk-dialog/dialog-demo.scss b/src/dev-app/cdk-dialog/dialog-demo.scss new file mode 100644 index 000000000000..1bdeeec93347 --- /dev/null +++ b/src/dev-app/cdk-dialog/dialog-demo.scss @@ -0,0 +1,23 @@ +.demo-cdk-dialog { + background: white; + padding: 20px; + border-radius: 8px; +} + +.demo-container { + button, label { + margin-right: 8px; + + [dir='rtl'] & { + margin-left: 8px; + margin-right: 0; + } + } +} + +.demo-field { + display: flex; + justify-content: space-between; + max-width: 300px; + margin-bottom: 8px; +} diff --git a/src/dev-app/cdk-dialog/dialog-demo.ts b/src/dev-app/cdk-dialog/dialog-demo.ts new file mode 100644 index 000000000000..33f2eee563f7 --- /dev/null +++ b/src/dev-app/cdk-dialog/dialog-demo.ts @@ -0,0 +1,100 @@ +/** + * @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 {Component, Inject, TemplateRef, ViewChild, ViewEncapsulation} from '@angular/core'; +import {DIALOG_DATA, Dialog, DialogConfig, DialogRef} from '@angular/cdk-experimental/dialog'; + +const defaultDialogConfig = new DialogConfig(); + +@Component({ + selector: 'dialog-demo', + templateUrl: 'dialog-demo.html', + styleUrls: ['dialog-demo.css'], + encapsulation: ViewEncapsulation.None, +}) +export class DialogDemo { + dialogRef: DialogRef | null; + result: string; + actionsAlignment: 'start' | 'center' | 'end'; + config = { + disableClose: defaultDialogConfig.disableClose, + panelClass: 'demo-cdk-dialog', + hasBackdrop: defaultDialogConfig.hasBackdrop, + backdropClass: defaultDialogConfig.backdropClass, + width: defaultDialogConfig.width, + height: defaultDialogConfig.height, + minWidth: defaultDialogConfig.minWidth, + minHeight: defaultDialogConfig.maxHeight, + maxWidth: defaultDialogConfig.maxWidth, + maxHeight: defaultDialogConfig.maxHeight, + data: { + message: 'Jazzy jazz jazz', + hmm: false, + }, + }; + numTemplateOpens = 0; + + @ViewChild(TemplateRef) template: TemplateRef; + + constructor(public dialog: Dialog) {} + + openJazz() { + this.dialogRef = this.dialog.open(JazzDialog, this.config); + + this.dialogRef.closed.subscribe(result => { + this.result = result!; + this.dialogRef = null; + }); + } + + openTemplate() { + this.numTemplateOpens++; + this.dialog.open(this.template, this.config); + } +} + +@Component({ + selector: 'demo-jazz-dialog', + template: ` +
+

It's Jazz!

+ + + + +

{{ data.message }}

+ + + +
+ `, + encapsulation: ViewEncapsulation.None, + styles: [`.hidden-dialog { opacity: 0; }`], +}) +export class JazzDialog { + private _dimesionToggle = false; + + constructor(public dialogRef: DialogRef, @Inject(DIALOG_DATA) public data: any) {} + + togglePosition(): void { + this._dimesionToggle = !this._dimesionToggle; + + if (this._dimesionToggle) { + this.dialogRef.updateSize('500px', '500px'); + } else { + this.dialogRef.updateSize().updatePosition(); + } + } + + temporarilyHide(): void { + this.dialogRef.addPanelClass('hidden-dialog'); + setTimeout(() => { + this.dialogRef.removePanelClass('hidden-dialog'); + }, 2000); + } +} diff --git a/src/dev-app/dev-app/dev-app-layout.ts b/src/dev-app/dev-app/dev-app-layout.ts index 725ecbd43137..905cf2ae5018 100644 --- a/src/dev-app/dev-app/dev-app-layout.ts +++ b/src/dev-app/dev-app/dev-app-layout.ts @@ -36,6 +36,7 @@ export class DevAppLayout { {name: 'Button Toggle', route: '/button-toggle'}, {name: 'Button', route: '/button'}, {name: 'Card', route: '/card'}, + {name: 'Cdk Dialog', route: '/cdk-dialog'}, {name: 'Cdk Experimental Combobox', route: '/cdk-experimental-combobox'}, {name: 'Cdk Experimental Listbox', route: '/cdk-experimental-listbox'}, {name: 'Cdk Experimental Menu', route: '/cdk-experimental-menu'}, diff --git a/src/dev-app/routes.ts b/src/dev-app/routes.ts index a1c2fe27d92e..325bd68c17f5 100644 --- a/src/dev-app/routes.ts +++ b/src/dev-app/routes.ts @@ -50,6 +50,10 @@ export const DEV_APP_ROUTES: Routes = [ m => m.CdkComboboxDemoModule, ), }, + { + path: 'cdk-dialog', + loadChildren: () => import('./cdk-dialog/dialog-demo-module').then(m => m.DialogDemoModule), + }, { path: 'cdk-experimental-listbox', loadChildren: () =>