From 815a0a8be4c2f6de9c594d23badd36d20a7a93d0 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Mon, 2 May 2022 12:54:10 +0200 Subject: [PATCH] refactor(material/dialog): switch to CDK dialog internally Switches the Material dialog to be based on the CDK dialog. --- src/cdk/dialog/dialog-config.ts | 6 + src/cdk/dialog/dialog-container.ts | 29 +- src/cdk/dialog/dialog.ts | 56 +-- .../mdc-dialog/BUILD.bazel | 1 + .../mdc-dialog/dialog-container.ts | 24 +- .../mdc-dialog/dialog-ref.ts | 15 +- .../mdc-dialog/dialog.spec.ts | 4 + .../mdc-dialog/dialog.ts | 17 +- .../mdc-dialog/module.ts | 3 +- .../mdc-dialog/public-api.ts | 1 - src/material/dialog/BUILD.bazel | 1 + src/material/dialog/dialog-container.ts | 301 +++------------- .../dialog/dialog-content-directives.ts | 2 +- src/material/dialog/dialog-module.ts | 3 +- src/material/dialog/dialog-ref.ts | 102 +++--- src/material/dialog/dialog.ts | 340 ++++-------------- tools/public_api_guard/cdk/dialog.md | 9 +- tools/public_api_guard/material/dialog.md | 73 ++-- 18 files changed, 297 insertions(+), 690 deletions(-) diff --git a/src/cdk/dialog/dialog-config.ts b/src/cdk/dialog/dialog-config.ts index 7f018282a661..9a37f05ee947 100644 --- a/src/cdk/dialog/dialog-config.ts +++ b/src/cdk/dialog/dialog-config.ts @@ -126,6 +126,12 @@ export class DialogConfig extends BasePortalOutlet - implements AfterViewInit, OnDestroy + implements OnDestroy { protected _document: Document; @@ -105,7 +104,7 @@ export class CdkDialogContainer this._document = _document; } - ngAfterViewInit() { + protected _contentAttached() { this._initializeFocusTrap(); this._handleBackdropClicks(); this._captureInitialFocus(); @@ -132,7 +131,9 @@ export class CdkDialogContainer throwDialogContentAlreadyAttachedError(); } - return this._portalOutlet.attachComponentPortal(portal); + const result = this._portalOutlet.attachComponentPortal(portal); + this._contentAttached(); + return result; } /** @@ -144,7 +145,9 @@ export class CdkDialogContainer throwDialogContentAlreadyAttachedError(); } - return this._portalOutlet.attachTemplatePortal(portal); + const result = this._portalOutlet.attachTemplatePortal(portal); + this._contentAttached(); + return result; } /** @@ -158,9 +161,19 @@ export class CdkDialogContainer throwDialogContentAlreadyAttachedError(); } - return this._portalOutlet.attachDomPortal(portal); + const result = this._portalOutlet.attachDomPortal(portal); + this._contentAttached(); + return result; }; + // TODO(crisbeto): this shouldn't be exposed, but there are internal references to it. + /** Captures focus if it isn't already inside the dialog. */ + _recaptureFocus() { + if (!this._containsFocus()) { + this._trapFocus(); + } + } + /** * 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. @@ -316,8 +329,8 @@ export class CdkDialogContainer // 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(); + if (this._config.disableClose) { + this._recaptureFocus(); } }); } diff --git a/src/cdk/dialog/dialog.ts b/src/cdk/dialog/dialog.ts index a1d5f9d59702..230d98c0f3cf 100644 --- a/src/cdk/dialog/dialog.ts +++ b/src/cdk/dialog/dialog.ts @@ -138,7 +138,7 @@ export class Dialog implements OnDestroy { } (this.openDialogs as DialogRef[]).push(dialogRef); - dialogRef.closed.subscribe(() => this._removeOpenDialog(dialogRef)); + dialogRef.closed.subscribe(() => this._removeOpenDialog(dialogRef, true)); this.afterOpened.next(dialogRef); return dialogRef; @@ -148,7 +148,7 @@ export class Dialog implements OnDestroy { * Closes all of the currently-open dialogs. */ closeAll(): void { - this._closeDialogs(this.openDialogs); + reverseForEach(this.openDialogs, dialog => dialog.close()); } /** @@ -160,11 +160,24 @@ export class Dialog implements OnDestroy { } ngOnDestroy() { - // Only close the dialogs at this level on destroy - // since the parent service may still be active. - this._closeDialogs(this._openDialogsAtThisLevel); + // Make one pass over all the dialogs that need to be untracked, but should not be closed. We + // want to stop tracking the open dialog even if it hasn't been closed, because the tracking + // determines when `aria-hidden` is removed from elements outside the dialog. + reverseForEach(this._openDialogsAtThisLevel, dialog => { + // Check for `false` specifically since we want `undefined` to be interpreted as `true`. + if (dialog.config.closeOnDestroy === false) { + this._removeOpenDialog(dialog, false); + } + }); + + // Make a second pass and close the remaining dialogs. We do this second pass in order to + // correctly dispatch the `afterAllClosed` event in case we have a mixed array of dialogs + // that should be closed and dialogs that should not. + reverseForEach(this._openDialogsAtThisLevel, dialog => dialog.close()); + this._afterAllClosedAtThisLevel.complete(); this._afterOpenedAtThisLevel.complete(); + this._openDialogsAtThisLevel = []; } /** @@ -326,8 +339,9 @@ export class Dialog implements OnDestroy { /** * Removes a dialog from the array of open dialogs. * @param dialogRef Dialog to be removed. + * @param emitEvent Whether to emit an event if this is the last dialog. */ - private _removeOpenDialog(dialogRef: DialogRef) { + private _removeOpenDialog(dialogRef: DialogRef, emitEvent: boolean) { const index = this.openDialogs.indexOf(dialogRef); if (index > -1) { @@ -345,7 +359,10 @@ export class Dialog implements OnDestroy { }); this._ariaHiddenElements.clear(); - this._getAfterAllClosed().next(); + + if (emitEvent) { + this._getAfterAllClosed().next(); + } } } } @@ -374,21 +391,20 @@ export class Dialog implements OnDestroy { } } - /** Closes all of the dialogs in an array. */ - private _closeDialogs(dialogs: readonly 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; } } + +/** + * Executes a callback against all elements in an array while iterating in reverse. + * Useful if the array is being modified as it is being iterated. + */ +function reverseForEach(items: T[] | readonly T[], callback: (current: T) => void) { + let i = items.length; + + while (i--) { + callback(items[i]); + } +} diff --git a/src/material-experimental/mdc-dialog/BUILD.bazel b/src/material-experimental/mdc-dialog/BUILD.bazel index f9865a023c56..92687dc5d1be 100644 --- a/src/material-experimental/mdc-dialog/BUILD.bazel +++ b/src/material-experimental/mdc-dialog/BUILD.bazel @@ -64,6 +64,7 @@ ng_test_library( ":mdc-dialog", "//src/cdk/a11y", "//src/cdk/bidi", + "//src/cdk/dialog", "//src/cdk/keycodes", "//src/cdk/overlay", "//src/cdk/platform", diff --git a/src/material-experimental/mdc-dialog/dialog-container.ts b/src/material-experimental/mdc-dialog/dialog-container.ts index 8fd5c2d7add0..d7763849ef19 100644 --- a/src/material-experimental/mdc-dialog/dialog-container.ts +++ b/src/material-experimental/mdc-dialog/dialog-container.ts @@ -7,10 +7,10 @@ */ import {FocusMonitor, FocusTrapFactory, InteractivityChecker} from '@angular/cdk/a11y'; +import {OverlayRef} from '@angular/cdk/overlay'; import {DOCUMENT} from '@angular/common'; import { ChangeDetectionStrategy, - ChangeDetectorRef, Component, ElementRef, Inject, @@ -38,8 +38,8 @@ import {cssClasses, numbers} from '@material/dialog'; host: { 'class': 'mat-mdc-dialog-container mdc-dialog', 'tabindex': '-1', - 'aria-modal': 'true', - '[id]': '_id', + '[attr.aria-modal]': '_config.ariaModal', + '[id]': '_config.id', '[attr.role]': '_config.role', '[attr.aria-labelledby]': '_config.ariaLabel ? null : _ariaLabelledBy', '[attr.aria-label]': '_config.ariaLabel', @@ -67,30 +67,31 @@ export class MatDialogContainer extends _MatDialogContainerBase implements OnDes constructor( elementRef: ElementRef, focusTrapFactory: FocusTrapFactory, - changeDetectorRef: ChangeDetectorRef, @Optional() @Inject(DOCUMENT) document: any, - config: MatDialogConfig, + dialogConfig: MatDialogConfig, checker: InteractivityChecker, ngZone: NgZone, + overlayRef: OverlayRef, @Optional() @Inject(ANIMATION_MODULE_TYPE) private _animationMode?: string, focusMonitor?: FocusMonitor, ) { super( elementRef, focusTrapFactory, - changeDetectorRef, document, - config, + dialogConfig, checker, ngZone, + overlayRef, focusMonitor, ); } - override _initializeWithAttachedContent() { + protected override _contentAttached(): void { // Delegate to the original dialog-container initialization (i.e. saving the // previous element, setting up the focus trap and moving focus to the container). - super._initializeWithAttachedContent(); + super._contentAttached(); + // Note: Usually we would be able to use the MDC dialog foundation here to handle // the dialog animation for us, but there are a few reasons why we just leverage // their styles and not use the runtime foundation code: @@ -103,7 +104,9 @@ export class MatDialogContainer extends _MatDialogContainerBase implements OnDes this._startOpenAnimation(); } - ngOnDestroy() { + override ngOnDestroy() { + super.ngOnDestroy(); + if (this._animationTimer !== null) { clearTimeout(this._animationTimer); } @@ -177,7 +180,6 @@ export class MatDialogContainer extends _MatDialogContainerBase implements OnDes */ private _finishDialogClose = () => { this._clearAnimationClasses(); - this._restoreFocus(); this._animationStateChanged.emit({state: 'closed', totalTime: this._closeAnimationDuration}); }; diff --git a/src/material-experimental/mdc-dialog/dialog-ref.ts b/src/material-experimental/mdc-dialog/dialog-ref.ts index 8bc1593c3b45..4dc4ecc23236 100644 --- a/src/material-experimental/mdc-dialog/dialog-ref.ts +++ b/src/material-experimental/mdc-dialog/dialog-ref.ts @@ -6,22 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ -import {OverlayRef} from '@angular/cdk/overlay'; import {MatDialogRef as NonMdcDialogRef} from '@angular/material/dialog'; -import {MatDialogContainer} from './dialog-container'; - -// Counter for unique dialog ids. -let uniqueId = 0; /** * Reference to a dialog opened via the MatDialog service. */ -export class MatDialogRef extends NonMdcDialogRef { - constructor( - overlayRef: OverlayRef, - containerInstance: MatDialogContainer, - id: string = `mat-mdc-dialog-${uniqueId++}`, - ) { - super(overlayRef, containerInstance, id); - } -} +export class MatDialogRef extends NonMdcDialogRef {} diff --git a/src/material-experimental/mdc-dialog/dialog.spec.ts b/src/material-experimental/mdc-dialog/dialog.spec.ts index 66deacdf2cb7..a0c383ab7b3e 100644 --- a/src/material-experimental/mdc-dialog/dialog.spec.ts +++ b/src/material-experimental/mdc-dialog/dialog.spec.ts @@ -1325,6 +1325,7 @@ describe('MDC-based MatDialog', () => { tick(500); viewContainerFixture.detectChanges(); + flushMicrotasks(); expect(lastFocusOrigin!).withContext('Expected the trigger button to be blurred').toBeNull(); dispatchKeyboardEvent(document.body, 'keydown', ESCAPE); @@ -1359,6 +1360,7 @@ describe('MDC-based MatDialog', () => { tick(500); viewContainerFixture.detectChanges(); + flushMicrotasks(); expect(lastFocusOrigin!).withContext('Expected the trigger button to be blurred').toBeNull(); const backdrop = overlayContainerElement.querySelector( @@ -1395,6 +1397,7 @@ describe('MDC-based MatDialog', () => { tick(500); viewContainerFixture.detectChanges(); + flushMicrotasks(); expect(lastFocusOrigin!).withContext('Expected the trigger button to be blurred').toBeNull(); const closeButton = overlayContainerElement.querySelector( @@ -1434,6 +1437,7 @@ describe('MDC-based MatDialog', () => { tick(500); viewContainerFixture.detectChanges(); + flushMicrotasks(); expect(lastFocusOrigin!).withContext('Expected the trigger button to be blurred').toBeNull(); const closeButton = overlayContainerElement.querySelector( diff --git a/src/material-experimental/mdc-dialog/dialog.ts b/src/material-experimental/mdc-dialog/dialog.ts index 81fd8a559032..b45dc04a07d0 100644 --- a/src/material-experimental/mdc-dialog/dialog.ts +++ b/src/material-experimental/mdc-dialog/dialog.ts @@ -8,11 +8,18 @@ import {Overlay, OverlayContainer, ScrollStrategy} from '@angular/cdk/overlay'; import {Location} from '@angular/common'; -import {Inject, Injectable, InjectionToken, Injector, Optional, SkipSelf} from '@angular/core'; +import { + ANIMATION_MODULE_TYPE, + Inject, + Injectable, + InjectionToken, + Injector, + Optional, + SkipSelf, +} from '@angular/core'; import {_MatDialogBase, MatDialogConfig} from '@angular/material/dialog'; import {MatDialogContainer} from './dialog-container'; import {MatDialogRef} from './dialog-ref'; -import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations'; /** Injection token that can be used to access the data that was passed in to a dialog. */ export const MAT_DIALOG_DATA = new InjectionToken('MatMdcDialogData'); @@ -57,6 +64,10 @@ export class MatDialog extends _MatDialogBase { @Optional() @Inject(MAT_DIALOG_DEFAULT_OPTIONS) defaultOptions: MatDialogConfig, @Inject(MAT_DIALOG_SCROLL_STRATEGY) scrollStrategy: any, @Optional() @SkipSelf() parentDialog: MatDialog, + /** + * @deprecated No longer used. To be removed. + * @breaking-change 15.0.0 + */ overlayContainer: OverlayContainer, /** * @deprecated No longer used. To be removed. @@ -78,5 +89,7 @@ export class MatDialog extends _MatDialogBase { MAT_DIALOG_DATA, animationMode, ); + + this._idPrefix = 'mat-mdc-dialog-'; } } diff --git a/src/material-experimental/mdc-dialog/module.ts b/src/material-experimental/mdc-dialog/module.ts index 955ff295e8c4..f7427b9b07a7 100644 --- a/src/material-experimental/mdc-dialog/module.ts +++ b/src/material-experimental/mdc-dialog/module.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {DialogModule} from '@angular/cdk/dialog'; import {OverlayModule} from '@angular/cdk/overlay'; import {PortalModule} from '@angular/cdk/portal'; import {NgModule} from '@angular/core'; @@ -20,7 +21,7 @@ import { } from './dialog-content-directives'; @NgModule({ - imports: [OverlayModule, PortalModule, MatCommonModule], + imports: [DialogModule, OverlayModule, PortalModule, MatCommonModule], exports: [ MatDialogContainer, MatDialogClose, diff --git a/src/material-experimental/mdc-dialog/public-api.ts b/src/material-experimental/mdc-dialog/public-api.ts index adc2d58d1a54..1f53cadb7a15 100644 --- a/src/material-experimental/mdc-dialog/public-api.ts +++ b/src/material-experimental/mdc-dialog/public-api.ts @@ -17,7 +17,6 @@ export { MatDialogState, MatDialogConfig, matDialogAnimations, - throwMatDialogContentAlreadyAttachedError, DialogRole, DialogPosition, MAT_DIALOG_SCROLL_STRATEGY_FACTORY, diff --git a/src/material/dialog/BUILD.bazel b/src/material/dialog/BUILD.bazel index a1618dea8a13..6d56e3a4bdf1 100644 --- a/src/material/dialog/BUILD.bazel +++ b/src/material/dialog/BUILD.bazel @@ -23,6 +23,7 @@ ng_module( "//src:dev_mode_types", "//src/cdk/a11y", "//src/cdk/bidi", + "//src/cdk/dialog", "//src/cdk/keycodes", "//src/cdk/overlay", "//src/cdk/platform", diff --git a/src/material/dialog/dialog-container.ts b/src/material/dialog/dialog-container.ts index 6d42eac3fb5b..3eb3565dbea1 100644 --- a/src/material/dialog/dialog-container.ts +++ b/src/material/dialog/dialog-container.ts @@ -7,35 +7,20 @@ */ import {AnimationEvent} from '@angular/animations'; -import { - FocusMonitor, - FocusOrigin, - FocusTrap, - FocusTrapFactory, - InteractivityChecker, -} from '@angular/cdk/a11y'; +import {CdkDialogContainer} from '@angular/cdk/dialog'; +import {FocusMonitor, FocusTrapFactory, InteractivityChecker} from '@angular/cdk/a11y'; +import {OverlayRef} from '@angular/cdk/overlay'; import {_getFocusedElementPierceShadowDom} from '@angular/cdk/platform'; -import { - BasePortalOutlet, - CdkPortalOutlet, - ComponentPortal, - DomPortal, - TemplatePortal, -} from '@angular/cdk/portal'; import {DOCUMENT} from '@angular/common'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, - ComponentRef, - Directive, ElementRef, - EmbeddedViewRef, EventEmitter, Inject, NgZone, Optional, - ViewChild, ViewEncapsulation, } from '@angular/core'; import {matDialogAnimations, defaultParams} from './dialog-animations'; @@ -47,257 +32,47 @@ interface DialogAnimationEvent { totalTime: number; } -/** - * Throws an exception for the case when a ComponentPortal is - * attached to a DomPortalOutlet without an origin. - * @docs-private - */ -export function throwMatDialogContentAlreadyAttachedError() { - throw Error('Attempting to attach dialog content after content is already attached'); -} - /** * Base class for the `MatDialogContainer`. The base class does not implement * animations as these are left to implementers of the dialog container. */ -@Directive() -export abstract class _MatDialogContainerBase extends BasePortalOutlet { - protected _document: Document; - - /** 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: FocusTrap; - +// tslint:disable-next-line:validate-decorators +@Component({template: ''}) +export abstract class _MatDialogContainerBase extends CdkDialogContainer { /** Emits when an animation state changes. */ _animationStateChanged = new EventEmitter(); - /** Element that was focused before the dialog was opened. Save this to restore upon close. */ - private _elementFocusedBeforeDialogWasOpened: HTMLElement | null = null; - - /** - * 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; - - /** ID of the element that should be considered as the dialog's label. */ - _ariaLabelledBy: string | null; - - /** ID for the container DOM element. */ - _id: string; - constructor( - protected _elementRef: ElementRef, - protected _focusTrapFactory: FocusTrapFactory, - protected _changeDetectorRef: ChangeDetectorRef, + elementRef: ElementRef, + focusTrapFactory: FocusTrapFactory, @Optional() @Inject(DOCUMENT) _document: any, - /** The dialog configuration. */ - public _config: MatDialogConfig, - private readonly _interactivityChecker: InteractivityChecker, - private readonly _ngZone: NgZone, - private _focusMonitor?: FocusMonitor, + dialogConfig: MatDialogConfig, + interactivityChecker: InteractivityChecker, + ngZone: NgZone, + overlayRef: OverlayRef, + focusMonitor?: FocusMonitor, ) { - super(); - this._ariaLabelledBy = _config.ariaLabelledBy || null; - this._document = _document; + super( + elementRef, + focusTrapFactory, + _document, + dialogConfig, + interactivityChecker, + ngZone, + overlayRef, + focusMonitor, + ); } /** Starts the dialog exit animation. */ abstract _startExitAnimation(): void; - /** Initializes the dialog container with the attached content. */ - _initializeWithAttachedContent() { - 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(); - } - + protected override _captureInitialFocus(): void { if (!this._config.delayFocusTrap) { this._trapFocus(); } } - /** - * Attach a ComponentPortal as content to this dialog container. - * @param portal Portal to be attached as the dialog content. - */ - attachComponentPortal(portal: ComponentPortal): ComponentRef { - if (this._portalOutlet.hasAttached() && (typeof ngDevMode === 'undefined' || ngDevMode)) { - throwMatDialogContentAlreadyAttachedError(); - } - - 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._portalOutlet.hasAttached() && (typeof ngDevMode === 'undefined' || ngDevMode)) { - throwMatDialogContentAlreadyAttachedError(); - } - - return this._portalOutlet.attachTemplatePortal(portal); - } - - /** - * Attaches a DOM portal to the dialog container. - * @param portal Portal to be attached. - * @deprecated To be turned into a method. - * @breaking-change 10.0.0 - */ - override attachDomPortal = (portal: DomPortal) => { - if (this._portalOutlet.hasAttached() && (typeof ngDevMode === 'undefined' || ngDevMode)) { - throwMatDialogContentAlreadyAttachedError(); - } - - return this._portalOutlet.attachDomPortal(portal); - }; - - /** Moves focus back into the dialog if it was moved out. */ - _recaptureFocus() { - if (!this._containsFocus()) { - this._trapFocus(); - } - } - - /** - * 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. - * @param element The element to focus. - */ - private _forceFocus(element: HTMLElement, options?: FocusOptions) { - if (!this._interactivityChecker.isFocusable(element)) { - element.tabIndex = -1; - // The tabindex attribute should be removed to avoid navigating to that element again - this._ngZone.runOutsideAngular(() => { - const callback = () => { - element.removeEventListener('blur', callback); - element.removeEventListener('mousedown', callback); - element.removeAttribute('tabindex'); - }; - - element.addEventListener('blur', callback); - element.addEventListener('mousedown', callback); - }); - } - element.focus(options); - } - - /** - * Focuses the first element that matches the given selector within the focus trap. - * @param selector The CSS selector for the element to set focus to. - */ - private _focusByCssSelector(selector: string, options?: FocusOptions) { - let elementToFocus = this._elementRef.nativeElement.querySelector( - selector, - ) as HTMLElement | null; - if (elementToFocus) { - this._forceFocus(elementToFocus, options); - } - } - - /** - * 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. - */ - 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 - // dialog. If the element inside the dialog can't be focused, then the container is focused - // so the user can't tab into other elements behind it. - switch (this._config.autoFocus) { - case false: - case 'dialog': - // 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 (!this._containsFocus()) { - element.focus(); - } - break; - case true: - case 'first-tabbable': - 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; - case 'first-heading': - this._focusByCssSelector('h1, h2, h3, h4, h5, h6, [role="heading"]'); - break; - default: - this._focusByCssSelector(this._config.autoFocus!); - break; - } - } - - /** Restores focus to the element that was focused before the dialog opened. */ - protected _restoreFocus() { - const previousElement = this._elementFocusedBeforeDialogWasOpened; - - // We need the extra check, because IE can set the `activeElement` to null in some cases. - if ( - this._config.restoreFocus && - previousElement && - typeof previousElement.focus === 'function' - ) { - const activeElement = _getFocusedElementPierceShadowDom(); - const element = this._elementRef.nativeElement; - - // Make sure that focus is still inside the dialog or is on the body (usually because a - // non-focusable element like the backdrop was clicked) before moving it. It's possible that - // the consumer moved it themselves before the animation was done, in which case we shouldn't - // do anything. - if ( - !activeElement || - activeElement === this._document.body || - activeElement === element || - element.contains(activeElement) - ) { - 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); - } - /** * Callback for when the open dialog animation has finished. Intended to * be called by sub-classes that use different animation implementations. @@ -328,8 +103,8 @@ export abstract class _MatDialogContainerBase extends BasePortalOutlet { host: { 'class': 'mat-dialog-container', 'tabindex': '-1', - 'aria-modal': 'true', - '[id]': '_id', + '[attr.aria-modal]': '_config.ariaModal', + '[id]': '_config.id', '[attr.role]': '_config.role', '[attr.aria-labelledby]': '_config.ariaLabel ? null : _ariaLabelledBy', '[attr.aria-label]': '_config.ariaLabel', @@ -348,7 +123,6 @@ export class MatDialogContainer extends _MatDialogContainerBase { if (toState === 'enter') { this._openAnimationDone(totalTime); } else if (toState === 'exit') { - this._restoreFocus(); this._animationStateChanged.next({state: 'closed', totalTime}); } } @@ -371,6 +145,29 @@ export class MatDialogContainer extends _MatDialogContainerBase { this._changeDetectorRef.markForCheck(); } + constructor( + elementRef: ElementRef, + focusTrapFactory: FocusTrapFactory, + @Optional() @Inject(DOCUMENT) document: any, + dialogConfig: MatDialogConfig, + checker: InteractivityChecker, + ngZone: NgZone, + overlayRef: OverlayRef, + private _changeDetectorRef: ChangeDetectorRef, + focusMonitor?: FocusMonitor, + ) { + super( + elementRef, + focusTrapFactory, + document, + dialogConfig, + checker, + ngZone, + overlayRef, + focusMonitor, + ); + } + _getAnimationState() { return { value: this._state, diff --git a/src/material/dialog/dialog-content-directives.ts b/src/material/dialog/dialog-content-directives.ts index bca919d3f87b..95600a064e72 100644 --- a/src/material/dialog/dialog-content-directives.ts +++ b/src/material/dialog/dialog-content-directives.ts @@ -16,7 +16,7 @@ import { ElementRef, } from '@angular/core'; import {MatDialog} from './dialog'; -import {_closeDialogVia, MatDialogRef} from './dialog-ref'; +import {MatDialogRef, _closeDialogVia} from './dialog-ref'; /** Counter used to generate unique IDs for dialog elements. */ let dialogElementUid = 0; diff --git a/src/material/dialog/dialog-module.ts b/src/material/dialog/dialog-module.ts index 01442e29c80b..0748d3b4a2d8 100644 --- a/src/material/dialog/dialog-module.ts +++ b/src/material/dialog/dialog-module.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {DialogModule} from '@angular/cdk/dialog'; import {OverlayModule} from '@angular/cdk/overlay'; import {PortalModule} from '@angular/cdk/portal'; import {NgModule} from '@angular/core'; @@ -20,7 +21,7 @@ import { } from './dialog-content-directives'; @NgModule({ - imports: [OverlayModule, PortalModule, MatCommonModule], + imports: [DialogModule, OverlayModule, PortalModule, MatCommonModule], exports: [ MatDialogContainer, MatDialogClose, diff --git a/src/material/dialog/dialog-ref.ts b/src/material/dialog/dialog-ref.ts index df3f51868be9..63cbaccb8894 100644 --- a/src/material/dialog/dialog-ref.ts +++ b/src/material/dialog/dialog-ref.ts @@ -6,19 +6,15 @@ * found in the LICENSE file at https://angular.io/license */ +import {DialogRef} from '@angular/cdk/dialog'; import {FocusOrigin} from '@angular/cdk/a11y'; import {ESCAPE, hasModifierKey} from '@angular/cdk/keycodes'; -import {GlobalPositionStrategy, OverlayRef} from '@angular/cdk/overlay'; -import {Observable, Subject} from 'rxjs'; +import {GlobalPositionStrategy} from '@angular/cdk/overlay'; +import {merge, Observable, Subject} from 'rxjs'; import {filter, take} from 'rxjs/operators'; -import {DialogPosition} from './dialog-config'; +import {DialogPosition, MatDialogConfig} from './dialog-config'; import {_MatDialogContainerBase} from './dialog-container'; -// TODO(jelbourn): resizing - -// Counter for unique dialog ids. -let uniqueId = 0; - /** Possible states of the lifecycle of a dialog. */ export const enum MatDialogState { OPEN, @@ -34,14 +30,14 @@ export class MatDialogRef { componentInstance: T; /** Whether the user is allowed to close the dialog. */ - disableClose: boolean | undefined = this._containerInstance._config.disableClose; + disableClose: boolean | undefined; + + /** Unique ID for the dialog. */ + id: string; /** Subject for notifying the user that the dialog has finished opening. */ private readonly _afterOpened = new Subject(); - /** Subject for notifying the user that the dialog has finished closing. */ - private readonly _afterClosed = new Subject(); - /** Subject for notifying the user that the dialog has started closing. */ private readonly _beforeClosed = new Subject(); @@ -54,14 +50,20 @@ export class MatDialogRef { /** Current state of the dialog. */ private _state = MatDialogState.OPEN; + // TODO(crisbeto): we shouldn't have to declare this property, because `DialogRef.close` + // already has a second `options` parameter that we can use. The problem is that internal tests + // have assertions like `expect(MatDialogRef.close).toHaveBeenCalledWith(foo)` which will break, + // because it'll be called with two arguments by things like `MatDialogClose`. + /** Interaction that caused the dialog to close. */ + private _closeInteractionType: FocusOrigin | undefined; + constructor( - private _overlayRef: OverlayRef, + private _ref: DialogRef, + config: MatDialogConfig, public _containerInstance: _MatDialogContainerBase, - /** Id of the dialog. */ - readonly id: string = `mat-dialog-${uniqueId++}`, ) { - // Pass the id along to the container. - _containerInstance._id = id; + this.disableClose = config.disableClose; + this.id = _ref.id; // Emit when opening animation completes _containerInstance._animationStateChanged @@ -85,32 +87,21 @@ export class MatDialogRef { this._finishDialogClose(); }); - _overlayRef.detachments().subscribe(() => { + _ref.overlayRef.detachments().subscribe(() => { this._beforeClosed.next(this._result); this._beforeClosed.complete(); - this._afterClosed.next(this._result); - this._afterClosed.complete(); - this.componentInstance = null!; - this._overlayRef.dispose(); + this._finishDialogClose(); }); - _overlayRef - .keydownEvents() - .pipe( - filter(event => { - return event.keyCode === ESCAPE && !this.disableClose && !hasModifierKey(event); - }), - ) - .subscribe(event => { + merge( + this.backdropClick(), + this.keydownEvents().pipe( + filter(event => event.keyCode === ESCAPE && !this.disableClose && !hasModifierKey(event)), + ), + ).subscribe(event => { + if (!this.disableClose) { event.preventDefault(); - _closeDialogVia(this, 'keyboard'); - }); - - _overlayRef.backdropClick().subscribe(() => { - if (this.disableClose) { - this._containerInstance._recaptureFocus(); - } else { - _closeDialogVia(this, 'mouse'); + _closeDialogVia(this, event.type === 'keydown' ? 'keyboard' : 'mouse'); } }); } @@ -131,7 +122,7 @@ export class MatDialogRef { .subscribe(event => { this._beforeClosed.next(dialogResult); this._beforeClosed.complete(); - this._overlayRef.detachBackdrop(); + this._ref.overlayRef.detachBackdrop(); // The logic that disposes of the overlay depends on the exit animation completing, however // it isn't guaranteed if the parent view is destroyed while it's running. Add a fallback @@ -159,7 +150,7 @@ export class MatDialogRef { * Gets an observable that is notified when the dialog is finished closing. */ afterClosed(): Observable { - return this._afterClosed; + return this._ref.closed; } /** @@ -173,14 +164,14 @@ export class MatDialogRef { * Gets an observable that emits when the overlay's backdrop has been clicked. */ backdropClick(): Observable { - return this._overlayRef.backdropClick(); + return this._ref.backdropClick; } /** * Gets an observable that emits when keydown events are targeted on the overlay. */ keydownEvents(): Observable { - return this._overlayRef.keydownEvents(); + return this._ref.keydownEvents; } /** @@ -188,7 +179,7 @@ export class MatDialogRef { * @param position New dialog position. */ updatePosition(position?: DialogPosition): this { - let strategy = this._getPositionStrategy(); + let strategy = this._ref.config.positionStrategy as GlobalPositionStrategy; if (position && (position.left || position.right)) { position.left ? strategy.left(position.left) : strategy.right(position.right); @@ -202,7 +193,7 @@ export class MatDialogRef { strategy.centerVertically(); } - this._overlayRef.updatePosition(); + this._ref.updatePosition(); return this; } @@ -213,20 +204,19 @@ export class MatDialogRef { * @param height New height of the dialog. */ updateSize(width: string = '', height: string = ''): this { - this._overlayRef.updateSize({width, height}); - this._overlayRef.updatePosition(); + this._ref.updateSize(width, height); return this; } /** Add a CSS class or an array of classes to the overlay pane. */ addPanelClass(classes: string | string[]): this { - this._overlayRef.addPanelClass(classes); + this._ref.addPanelClass(classes); return this; } /** Remove a CSS class or an array of classes from the overlay pane. */ removePanelClass(classes: string | string[]): this { - this._overlayRef.removePanelClass(classes); + this._ref.removePanelClass(classes); return this; } @@ -241,12 +231,8 @@ export class MatDialogRef { */ private _finishDialogClose() { this._state = MatDialogState.CLOSED; - this._overlayRef.dispose(); - } - - /** Fetches the position strategy object from the overlay ref. */ - private _getPositionStrategy(): GlobalPositionStrategy { - return this._overlayRef.getConfig().positionStrategy as GlobalPositionStrategy; + this._ref.close(this._result, {focusOrigin: this._closeInteractionType}); + this.componentInstance = null!; } } @@ -255,12 +241,8 @@ export class MatDialogRef { * `MatDialogRef` as that would conflict with custom dialog ref mocks provided in tests. * More details. See: https://github.com/angular/components/pull/9257#issuecomment-651342226. */ -// TODO: TODO: Move this back into `MatDialogRef` when we provide an official mock dialog ref. +// TODO: Move this back into `MatDialogRef` when we provide an official mock dialog ref. export function _closeDialogVia(ref: MatDialogRef, interactionType: FocusOrigin, result?: R) { - // Some mock dialog ref instances in tests do not have the `_containerInstance` property. - // For those, we keep the behavior as is and do not deal with the interaction type. - if (ref._containerInstance !== undefined) { - ref._containerInstance._closeInteractionType = interactionType; - } + (ref as unknown as {_closeInteractionType: FocusOrigin})._closeInteractionType = interactionType; return ref.close(result); } diff --git a/src/material/dialog/dialog.ts b/src/material/dialog/dialog.ts index 1533361b87c9..24decc088de5 100644 --- a/src/material/dialog/dialog.ts +++ b/src/material/dialog/dialog.ts @@ -6,36 +6,27 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directionality} from '@angular/cdk/bidi'; -import { - Overlay, - OverlayConfig, - OverlayContainer, - OverlayRef, - ScrollStrategy, -} from '@angular/cdk/overlay'; -import {ComponentPortal, ComponentType, TemplatePortal} from '@angular/cdk/portal'; +import {Overlay, OverlayContainer, ScrollStrategy} from '@angular/cdk/overlay'; +import {ComponentType} from '@angular/cdk/portal'; import {Location} from '@angular/common'; import { - Directive, Inject, Injectable, - InjectFlags, InjectionToken, Injector, OnDestroy, Optional, SkipSelf, - StaticProvider, TemplateRef, Type, } from '@angular/core'; -import {defer, Observable, of as observableOf, Subject} from 'rxjs'; +import {defer, Observable, Subject} from 'rxjs'; import {startWith} from 'rxjs/operators'; import {MatDialogConfig} from './dialog-config'; import {MatDialogContainer, _MatDialogContainerBase} from './dialog-container'; import {MatDialogRef} from './dialog-ref'; import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations'; +import {Dialog, DialogConfig} from '@angular/cdk/dialog'; /** Injection token that can be used to access the data that was passed in to a dialog. */ export const MAT_DIALOG_DATA = new InjectionToken('MatDialogData'); @@ -69,17 +60,21 @@ export const MAT_DIALOG_SCROLL_STRATEGY_PROVIDER = { useFactory: MAT_DIALOG_SCROLL_STRATEGY_PROVIDER_FACTORY, }; +// Counter for unique dialog ids. +let uniqueId = 0; + /** * Base class for dialog services. The base dialog service allows * for arbitrary dialog refs and dialog container components. */ -@Directive() +@Injectable() export abstract class _MatDialogBase implements OnDestroy { - private _openDialogsAtThisLevel: MatDialogRef[] = []; + private readonly _openDialogsAtThisLevel: MatDialogRef[] = []; private readonly _afterAllClosedAtThisLevel = new Subject(); private readonly _afterOpenedAtThisLevel = new Subject>(); - private _ariaHiddenElements = new Map(); private _scrollStrategy: () => ScrollStrategy; + protected _idPrefix = 'mat-dialog-'; + private _dialog: Dialog; /** Keeps track of the currently-open dialogs. */ get openDialogs(): MatDialogRef[] { @@ -91,12 +86,11 @@ export abstract class _MatDialogBase implemen return this._parentDialog ? this._parentDialog.afterOpened : this._afterOpenedAtThisLevel; } - _getAfterAllClosed(): Subject { + private _getAfterAllClosed(): Subject { const parent = this._parentDialog; return parent ? parent._getAfterAllClosed() : this._afterAllClosedAtThisLevel; } - // TODO (jelbourn): tighten the typing right-hand side of this expression. /** * Stream that emits when all open dialog have finished closing. * Will emit on subscribe if there are no open dialogs to begin with. @@ -109,10 +103,14 @@ export abstract class _MatDialogBase implemen constructor( private _overlay: Overlay, - private _injector: Injector, + injector: Injector, private _defaultOptions: MatDialogConfig | undefined, private _parentDialog: _MatDialogBase | undefined, - private _overlayContainer: OverlayContainer, + /** + * @deprecated No longer used. To be removed. + * @breaking-change 15.0.0 + */ + _overlayContainer: OverlayContainer, scrollStrategy: any, private _dialogRefConstructor: Type>, private _dialogContainerType: Type, @@ -124,6 +122,7 @@ export abstract class _MatDialogBase implemen _animationMode?: 'NoopAnimations' | 'BrowserAnimations', ) { this._scrollStrategy = scrollStrategy; + this._dialog = injector.get(Dialog); } /** @@ -157,38 +156,62 @@ export abstract class _MatDialogBase implemen componentOrTemplateRef: ComponentType | TemplateRef, config?: MatDialogConfig, ): MatDialogRef { - config = _applyConfigDefaults(config, this._defaultOptions || new MatDialogConfig()); + let dialogRef: MatDialogRef; + config = {...(this._defaultOptions || new MatDialogConfig()), ...config}; + config.id = config.id || `${this._idPrefix}${uniqueId++}`; + config.scrollStrategy = config.scrollStrategy || this._scrollStrategy(); + + const cdkRef = this._dialog.open(componentOrTemplateRef, { + ...config, + positionStrategy: this._overlay.position().global().centerHorizontally().centerVertically(), + // Disable closing since we need to sync it up to the animation ourselves. + disableClose: true, + // Disable closing on destroy, because this service cleans up its open dialogs as well. + // We want to do the cleanup here, rather than the CDK service, because the CDK destroys + // the dialogs immediately whereas we want it to wait for the animations to finish. + closeOnDestroy: false, + container: { + type: this._dialogContainerType, + providers: () => [ + // Provide our config as the CDK config as well since it has the same interface as the + // CDK one, but it contains the actual values passed in by the user for things like + // `disableClose` which we disable for the CDK dialog since we handle it ourselves. + {provide: MatDialogConfig, useValue: config}, + {provide: DialogConfig, useValue: config}, + ], + }, + templateContext: () => ({dialogRef}), + providers: (ref, cdkConfig, dialogContainer) => { + dialogRef = new this._dialogRefConstructor(ref, config, dialogContainer); + dialogRef.updatePosition(config?.position); + return [ + {provide: this._dialogContainerType, useValue: dialogContainer}, + {provide: this._dialogDataToken, useValue: cdkConfig.data}, + {provide: this._dialogRefConstructor, useValue: dialogRef}, + ]; + }, + }); - 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.`); - } + // This can't be assigned in the `providers` callback, because + // the instance hasn't been assigned to the CDK ref yet. + dialogRef!.componentInstance = cdkRef.componentInstance!; - const overlayRef = this._createOverlay(config); - const dialogContainer = this._attachDialogContainer(overlayRef, config); - const dialogRef = this._attachDialogContent( - componentOrTemplateRef, - dialogContainer, - overlayRef, - config, - ); + this.openDialogs.push(dialogRef!); + this.afterOpened.next(dialogRef!); - // If this is the first dialog that we're opening, hide all the non-overlay content. - if (!this.openDialogs.length) { - this._hideNonDialogContentFromAssistiveTechnology(); - } + dialogRef!.afterClosed().subscribe(() => { + const index = this.openDialogs.indexOf(dialogRef); - this.openDialogs.push(dialogRef); - dialogRef.afterClosed().subscribe(() => this._removeOpenDialog(dialogRef)); - this.afterOpened.next(dialogRef); + if (index > -1) { + this.openDialogs.splice(index, 1); - // Notify the dialog container that the content has been attached. - dialogContainer._initializeWithAttachedContent(); + if (!this.openDialogs.length) { + this._getAfterAllClosed().next(); + } + } + }); - return dialogRef; + return dialogRef!; } /** @@ -214,216 +237,10 @@ export abstract class _MatDialogBase implemen this._afterOpenedAtThisLevel.complete(); } - /** - * Creates the overlay into which the dialog will be loaded. - * @param config The dialog configuration. - * @returns A promise resolving to the OverlayRef for the created overlay. - */ - private _createOverlay(config: MatDialogConfig): OverlayRef { - const overlayConfig = this._getOverlayConfig(config); - return this._overlay.create(overlayConfig); - } - - /** - * Creates an overlay config from a dialog config. - * @param dialogConfig The dialog configuration. - * @returns The overlay configuration. - */ - private _getOverlayConfig(dialogConfig: MatDialogConfig): OverlayConfig { - const state = new OverlayConfig({ - positionStrategy: this._overlay.position().global(), - scrollStrategy: dialogConfig.scrollStrategy || this._scrollStrategy(), - panelClass: dialogConfig.panelClass, - hasBackdrop: dialogConfig.hasBackdrop, - direction: dialogConfig.direction, - minWidth: dialogConfig.minWidth, - minHeight: dialogConfig.minHeight, - maxWidth: dialogConfig.maxWidth, - maxHeight: dialogConfig.maxHeight, - disposeOnNavigation: dialogConfig.closeOnNavigation, - }); - - if (dialogConfig.backdropClass) { - state.backdropClass = dialogConfig.backdropClass; - } - - return state; - } - - /** - * 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. - */ - private _attachDialogContainer(overlay: OverlayRef, config: MatDialogConfig): C { - const userInjector = config.injector ?? config.viewContainerRef?.injector; - const injector = Injector.create({ - parent: userInjector || this._injector, - providers: [{provide: MatDialogConfig, useValue: config}], - }); - - const containerPortal = new ComponentPortal( - this._dialogContainerType, - config.viewContainerRef, - injector, - config.componentFactoryResolver, - ); - const containerRef = overlay.attach(containerPortal); - - return containerRef.instance; - } - - /** - * 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 dialog container. - * @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. - */ - private _attachDialogContent( - componentOrTemplateRef: ComponentType | TemplateRef, - dialogContainer: C, - overlayRef: OverlayRef, - config: MatDialogConfig, - ): MatDialogRef { - // Create a reference to the dialog we're creating in order to give the user a handle - // to modify and close it. - const dialogRef = new this._dialogRefConstructor(overlayRef, dialogContainer, config.id); - const injector = this._createInjector(config, dialogRef, dialogContainer); - - if (componentOrTemplateRef instanceof TemplateRef) { - dialogContainer.attachTemplatePortal( - new TemplatePortal( - componentOrTemplateRef, - null!, - { - $implicit: config.data, - dialogRef, - }, - injector, - ), - ); - } else { - const contentRef = dialogContainer.attachComponentPortal( - new ComponentPortal( - componentOrTemplateRef, - config.viewContainerRef, - injector, - config.componentFactoryResolver, - ), - ); - dialogRef.componentInstance = contentRef.instance; - } - - dialogRef.updateSize(config.width, config.height).updatePosition(config.position); - - return dialogRef; - } - - /** - * 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 dialogContainer Dialog container element that wraps all of the contents. - * @returns The custom injector that can be used inside the dialog. - */ - private _createInjector( - config: MatDialogConfig, - dialogRef: MatDialogRef, - dialogContainer: C, - ): Injector { - const userInjector = config && config.viewContainerRef && config.viewContainerRef.injector; - - // The dialog container should be provided as the dialog container and the dialog's - // content are created out of the same `ViewContainerRef` and as such, are siblings - // for injector purposes. To allow the hierarchy that is expected, the dialog - // container is explicitly provided in the injector. - const providers: StaticProvider[] = [ - {provide: this._dialogContainerType, useValue: dialogContainer}, - {provide: this._dialogDataToken, useValue: config.data}, - {provide: this._dialogRefConstructor, useValue: dialogRef}, - ]; - - if ( - config.direction && - (!userInjector || - !userInjector.get(Directionality, null, InjectFlags.Optional)) - ) { - providers.push({ - provide: Directionality, - useValue: {value: config.direction, change: observableOf()}, - }); - } - - return Injector.create({parent: userInjector || this._injector, providers}); - } - - /** - * Removes a dialog from the array of open dialogs. - * @param dialogRef Dialog to be removed. - */ - private _removeOpenDialog(dialogRef: MatDialogRef) { - 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--) { - let 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: MatDialogRef[]) { 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(); } } @@ -441,10 +258,14 @@ export class MatDialog extends _MatDialogBase { * @deprecated `_location` parameter to be removed. * @breaking-change 10.0.0 */ - @Optional() location: Location, + @Optional() _location: Location, @Optional() @Inject(MAT_DIALOG_DEFAULT_OPTIONS) defaultOptions: MatDialogConfig, @Inject(MAT_DIALOG_SCROLL_STRATEGY) scrollStrategy: any, @Optional() @SkipSelf() parentDialog: MatDialog, + /** + * @deprecated No longer used. To be removed. + * @breaking-change 15.0.0 + */ overlayContainer: OverlayContainer, /** * @deprecated No longer used. To be removed. @@ -468,16 +289,3 @@ export class MatDialog extends _MatDialogBase { ); } } - -/** - * Applies default options to the dialog config. - * @param config Config to be modified. - * @param defaultOptions Default options provided. - * @returns The new configuration object. - */ -function _applyConfigDefaults( - config?: MatDialogConfig, - defaultOptions?: MatDialogConfig, -): MatDialogConfig { - return {...defaultOptions, ...config}; -} diff --git a/tools/public_api_guard/cdk/dialog.md b/tools/public_api_guard/cdk/dialog.md index c140703aded1..2c5b99009d7a 100644 --- a/tools/public_api_guard/cdk/dialog.md +++ b/tools/public_api_guard/cdk/dialog.md @@ -4,7 +4,6 @@ ```ts -import { AfterViewInit } from '@angular/core'; import { BasePortalOutlet } from '@angular/cdk/portal'; import { CdkPortalOutlet } from '@angular/cdk/portal'; import { ComponentFactoryResolver } from '@angular/core'; @@ -44,7 +43,7 @@ import { ViewContainerRef } from '@angular/core'; export type AutoFocusTarget = 'dialog' | 'first-tabbable' | 'first-heading'; // @public -export class CdkDialogContainer extends BasePortalOutlet implements AfterViewInit, OnDestroy { +export class CdkDialogContainer extends BasePortalOutlet implements OnDestroy { constructor(_elementRef: ElementRef, _focusTrapFactory: FocusTrapFactory, _document: any, _config: C, _interactivityChecker: InteractivityChecker, _ngZone: NgZone, _overlayRef: OverlayRef, _focusMonitor?: FocusMonitor | undefined); _ariaLabelledBy: string | null; attachComponentPortal(portal: ComponentPortal): ComponentRef; @@ -56,16 +55,17 @@ export class CdkDialogContainer extends B // (undocumented) readonly _config: C; // (undocumented) + protected _contentAttached(): void; + // (undocumented) protected _document: Document; // (undocumented) protected _elementRef: ElementRef; // (undocumented) protected _focusTrapFactory: FocusTrapFactory; // (undocumented) - ngAfterViewInit(): void; - // (undocumented) ngOnDestroy(): void; _portalOutlet: CdkPortalOutlet; + _recaptureFocus(): void; protected _trapFocus(): void; // (undocumented) static ɵcmp: i0.ɵɵComponentDeclaration, "cdk-dialog-container", never, {}, {}, never, never, false>; @@ -125,6 +125,7 @@ export class DialogConfig | { diff --git a/tools/public_api_guard/material/dialog.md b/tools/public_api_guard/material/dialog.md index 90854ea46112..cfdcc6202998 100644 --- a/tools/public_api_guard/material/dialog.md +++ b/tools/public_api_guard/material/dialog.md @@ -6,25 +6,22 @@ import { AnimationEvent as AnimationEvent_2 } from '@angular/animations'; import { AnimationTriggerMetadata } from '@angular/animations'; -import { BasePortalOutlet } from '@angular/cdk/portal'; -import { CdkPortalOutlet } from '@angular/cdk/portal'; +import { CdkDialogContainer } from '@angular/cdk/dialog'; import { ChangeDetectorRef } from '@angular/core'; import { ComponentFactoryResolver } from '@angular/core'; -import { ComponentPortal } from '@angular/cdk/portal'; -import { ComponentRef } from '@angular/core'; import { ComponentType } from '@angular/cdk/portal'; +import { DialogRef } from '@angular/cdk/dialog'; import { Direction } from '@angular/cdk/bidi'; -import { DomPortal } from '@angular/cdk/portal'; import { ElementRef } from '@angular/core'; -import { EmbeddedViewRef } from '@angular/core'; import { EventEmitter } from '@angular/core'; import { FocusMonitor } from '@angular/cdk/a11y'; import { FocusOrigin } from '@angular/cdk/a11y'; import { FocusTrapFactory } from '@angular/cdk/a11y'; import * as i0 from '@angular/core'; -import * as i3 from '@angular/cdk/overlay'; -import * as i4 from '@angular/cdk/portal'; -import * as i5 from '@angular/material/core'; +import * as i3 from '@angular/cdk/dialog'; +import * as i4 from '@angular/cdk/overlay'; +import * as i5 from '@angular/cdk/portal'; +import * as i6 from '@angular/material/core'; import { InjectionToken } from '@angular/core'; import { Injector } from '@angular/core'; import { InteractivityChecker } from '@angular/cdk/a11y'; @@ -40,7 +37,6 @@ import { OverlayRef } from '@angular/cdk/overlay'; import { ScrollStrategy } from '@angular/cdk/overlay'; import { SimpleChanges } from '@angular/core'; import { Subject } from 'rxjs'; -import { TemplatePortal } from '@angular/cdk/portal'; import { TemplateRef } from '@angular/core'; import { Type } from '@angular/core'; import { ViewContainerRef } from '@angular/core'; @@ -87,7 +83,8 @@ export function MAT_DIALOG_SCROLL_STRATEGY_PROVIDER_FACTORY(overlay: Overlay): ( // @public export class MatDialog extends _MatDialogBase { constructor(overlay: Overlay, injector: Injector, - location: Location_2, defaultOptions: MatDialogConfig, scrollStrategy: any, parentDialog: MatDialog, overlayContainer: OverlayContainer, + _location: Location_2, defaultOptions: MatDialogConfig, scrollStrategy: any, parentDialog: MatDialog, + overlayContainer: OverlayContainer, animationMode?: 'NoopAnimations' | 'BrowserAnimations'); // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; @@ -111,15 +108,16 @@ export const matDialogAnimations: { // @public export abstract class _MatDialogBase implements OnDestroy { - constructor(_overlay: Overlay, _injector: Injector, _defaultOptions: MatDialogConfig | undefined, _parentDialog: _MatDialogBase | undefined, _overlayContainer: OverlayContainer, scrollStrategy: any, _dialogRefConstructor: Type>, _dialogContainerType: Type, _dialogDataToken: InjectionToken, + constructor(_overlay: Overlay, injector: Injector, _defaultOptions: MatDialogConfig | undefined, _parentDialog: _MatDialogBase | undefined, + _overlayContainer: OverlayContainer, scrollStrategy: any, _dialogRefConstructor: Type>, _dialogContainerType: Type, _dialogDataToken: InjectionToken, _animationMode?: 'NoopAnimations' | 'BrowserAnimations'); readonly afterAllClosed: Observable; get afterOpened(): Subject>; closeAll(): void; - // (undocumented) - _getAfterAllClosed(): Subject; getDialogById(id: string): MatDialogRef | undefined; // (undocumented) + protected _idPrefix: string; + // (undocumented) ngOnDestroy(): void; open(component: ComponentType, config?: MatDialogConfig): MatDialogRef; open(template: TemplateRef, config?: MatDialogConfig): MatDialogRef; @@ -127,9 +125,9 @@ export abstract class _MatDialogBase implemen open(template: ComponentType | TemplateRef, config?: MatDialogConfig): MatDialogRef; get openDialogs(): MatDialogRef[]; // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration<_MatDialogBase, never, never, {}, {}, never, never, false>; - // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration<_MatDialogBase, never>; + // (undocumented) + static ɵprov: i0.ɵɵInjectableDeclaration<_MatDialogBase>; } // @public @@ -189,6 +187,7 @@ export class MatDialogConfig { // @public export class MatDialogContainer extends _MatDialogContainerBase { + constructor(elementRef: ElementRef, focusTrapFactory: FocusTrapFactory, document: any, dialogConfig: MatDialogConfig, checker: InteractivityChecker, ngZone: NgZone, overlayRef: OverlayRef, _changeDetectorRef: ChangeDetectorRef, focusMonitor?: FocusMonitor); // (undocumented) _getAnimationState(): { value: "enter" | "void" | "exit"; @@ -204,41 +203,21 @@ export class MatDialogContainer extends _MatDialogContainerBase { // (undocumented) static ɵcmp: i0.ɵɵComponentDeclaration; // (undocumented) - static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵfac: i0.ɵɵFactoryDeclaration; } // @public -export abstract class _MatDialogContainerBase extends BasePortalOutlet { - constructor(_elementRef: ElementRef, _focusTrapFactory: FocusTrapFactory, _changeDetectorRef: ChangeDetectorRef, _document: any, - _config: MatDialogConfig, _interactivityChecker: InteractivityChecker, _ngZone: NgZone, _focusMonitor?: FocusMonitor | undefined); +export abstract class _MatDialogContainerBase extends CdkDialogContainer { + constructor(elementRef: ElementRef, focusTrapFactory: FocusTrapFactory, _document: any, dialogConfig: MatDialogConfig, interactivityChecker: InteractivityChecker, ngZone: NgZone, overlayRef: OverlayRef, focusMonitor?: FocusMonitor); _animationStateChanged: EventEmitter; - _ariaLabelledBy: string | null; - attachComponentPortal(portal: ComponentPortal): ComponentRef; - // @deprecated - attachDomPortal: (portal: DomPortal) => void; - attachTemplatePortal(portal: TemplatePortal): EmbeddedViewRef; - // (undocumented) - protected _changeDetectorRef: ChangeDetectorRef; - _closeInteractionType: FocusOrigin | null; - _config: MatDialogConfig; // (undocumented) - protected _document: Document; - // (undocumented) - protected _elementRef: ElementRef; - // (undocumented) - protected _focusTrapFactory: FocusTrapFactory; - _id: string; - _initializeWithAttachedContent(): void; + protected _captureInitialFocus(): void; protected _openAnimationDone(totalTime: number): void; - _portalOutlet: CdkPortalOutlet; - _recaptureFocus(): void; - protected _restoreFocus(): void; abstract _startExitAnimation(): void; - protected _trapFocus(): void; // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration<_MatDialogContainerBase, never, never, {}, {}, never, never, false>; + static ɵcmp: i0.ɵɵComponentDeclaration<_MatDialogContainerBase, "ng-component", never, {}, {}, never, never, false>; // (undocumented) - static ɵfac: i0.ɵɵFactoryDeclaration<_MatDialogContainerBase, [null, null, null, { optional: true; }, null, null, null, null]>; + static ɵfac: i0.ɵɵFactoryDeclaration<_MatDialogContainerBase, [null, null, { optional: true; }, null, null, null, null, null]>; } // @public @@ -256,13 +235,12 @@ export class MatDialogModule { // (undocumented) static ɵinj: i0.ɵɵInjectorDeclaration; // (undocumented) - static ɵmod: i0.ɵɵNgModuleDeclaration; + static ɵmod: i0.ɵɵNgModuleDeclaration; } // @public export class MatDialogRef { - constructor(_overlayRef: OverlayRef, _containerInstance: _MatDialogContainerBase, - id?: string); + constructor(_ref: DialogRef, config: MatDialogConfig, _containerInstance: _MatDialogContainerBase); addPanelClass(classes: string | string[]): this; afterClosed(): Observable; afterOpened(): Observable; @@ -274,7 +252,7 @@ export class MatDialogRef { _containerInstance: _MatDialogContainerBase; disableClose: boolean | undefined; getState(): MatDialogState; - readonly id: string; + id: string; keydownEvents(): Observable; removePanelClass(classes: string | string[]): this; updatePosition(position?: DialogPosition): this; @@ -303,9 +281,6 @@ export class MatDialogTitle implements OnInit { static ɵfac: i0.ɵɵFactoryDeclaration; } -// @public -export function throwMatDialogContentAlreadyAttachedError(): void; - // (No @packageDocumentation comment for this package) ```