diff --git a/src/cdk/dialog/dialog-config.ts b/src/cdk/dialog/dialog-config.ts index 9a37f05ee947..8f85a3befbb7 100644 --- a/src/cdk/dialog/dialog-config.ts +++ b/src/cdk/dialog/dialog-config.ts @@ -57,6 +57,13 @@ export class DialogConfig( + result: Result | undefined, + config: this, + componentInstance: Component | null, + ) => boolean; + /** Width of the dialog. */ width?: string = ''; diff --git a/src/cdk/dialog/dialog-container.ts b/src/cdk/dialog/dialog-container.ts index 3cd222fcb52b..bc51f86624b5 100644 --- a/src/cdk/dialog/dialog-container.ts +++ b/src/cdk/dialog/dialog-container.ts @@ -97,7 +97,7 @@ export class CdkDialogContainer @Inject(DialogConfig) readonly _config: C, private _interactivityChecker: InteractivityChecker, private _ngZone: NgZone, - private _overlayRef: OverlayRef, + protected _overlayRef: OverlayRef, private _focusMonitor?: FocusMonitor, ) { super(); @@ -107,7 +107,6 @@ export class CdkDialogContainer protected _contentAttached() { this._initializeFocusTrap(); - this._handleBackdropClicks(); this._captureInitialFocus(); } @@ -324,15 +323,4 @@ export class CdkDialogContainer 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._recaptureFocus(); - } - }); - } } diff --git a/src/cdk/dialog/dialog-ref.ts b/src/cdk/dialog/dialog-ref.ts index f5af3f1826b9..41ba743cb6f7 100644 --- a/src/cdk/dialog/dialog-ref.ts +++ b/src/cdk/dialog/dialog-ref.ts @@ -30,7 +30,10 @@ export class DialogRef { readonly componentInstance: C | null; /** Instance of the container that is rendering out the dialog content. */ - readonly containerInstance: BasePortalOutlet & {_closeInteractionType?: FocusOrigin}; + readonly containerInstance: BasePortalOutlet & { + _closeInteractionType?: FocusOrigin; + _recaptureFocus?: () => void; + }; /** Whether the user is allowed to close the dialog. */ disableClose: boolean | undefined; @@ -68,8 +71,12 @@ export class DialogRef { }); this.backdropClick.subscribe(() => { - if (!this.disableClose) { + if (!this.disableClose && this._canClose()) { this.close(undefined, {focusOrigin: 'mouse'}); + } else { + // Clicking on the backdrop will move focus out of dialog. + // Recapture it if closing via the backdrop is disabled. + this.containerInstance._recaptureFocus?.(); } }); } @@ -80,7 +87,7 @@ export class DialogRef { * @param options Additional options to customize the closing behavior. */ close(result?: R, options?: DialogCloseOptions): void { - if (this.containerInstance) { + if (this._canClose(result)) { const closedSubject = this.closed as Subject; this.containerInstance._closeInteractionType = options?.focusOrigin || 'program'; this.overlayRef.dispose(); @@ -119,4 +126,13 @@ export class DialogRef { this.overlayRef.removePanelClass(classes); return this; } + + /** Whether the dialog is allowed to close. */ + private _canClose(result?: R): boolean { + return ( + !!this.containerInstance && + (!this.config.closePredicate || + this.config.closePredicate(result, this.config, this.componentInstance)) + ); + } } diff --git a/src/cdk/dialog/dialog.spec.ts b/src/cdk/dialog/dialog.spec.ts index 0ae2ee5042bb..1bb5d3006e92 100644 --- a/src/cdk/dialog/dialog.spec.ts +++ b/src/cdk/dialog/dialog.spec.ts @@ -700,6 +700,202 @@ describe('Dialog', () => { expect(overlayContainerElement.querySelector('cdk-dialog-container')).toBeTruthy(); })); + + it('should recapture focus to the first tabbable element when clicking on the backdrop', fakeAsync(() => { + // When testing focus, all of the elements must be in the DOM. + document.body.appendChild(overlayContainerElement); + + dialog.open(PizzaMsg, { + disableClose: true, + viewContainerRef: testViewContainerRef, + }); + + viewContainerFixture.detectChanges(); + flushMicrotasks(); + + const backdrop = overlayContainerElement.querySelector( + '.cdk-overlay-backdrop', + ) as HTMLElement; + const input = overlayContainerElement.querySelector('input') as HTMLInputElement; + + expect(document.activeElement) + .withContext('Expected input to be focused on open') + .toBe(input); + + input.blur(); // Programmatic clicks might not move focus so we simulate it. + backdrop.click(); + viewContainerFixture.detectChanges(); + flush(); + + expect(document.activeElement) + .withContext('Expected input to stay focused after click') + .toBe(input); + + overlayContainerElement.remove(); + })); + }); + + describe('closePredicate option', () => { + function getDialogs() { + return overlayContainerElement.querySelectorAll('cdk-dialog-container'); + } + + it('should determine whether closing via the backdrop is allowed', fakeAsync(() => { + let canClose = false; + const closedSpy = jasmine.createSpy('closed spy'); + const ref = dialog.open(PizzaMsg, { + closePredicate: () => canClose, + viewContainerRef: testViewContainerRef, + }); + + ref.closed.subscribe(closedSpy); + viewContainerFixture.detectChanges(); + + expect(getDialogs().length).toBe(1); + + let backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement; + backdrop.click(); + viewContainerFixture.detectChanges(); + flush(); + + expect(getDialogs().length).toBe(1); + expect(closedSpy).not.toHaveBeenCalled(); + + canClose = true; + backdrop.click(); + viewContainerFixture.detectChanges(); + flush(); + + expect(getDialogs().length).toBe(0); + expect(closedSpy).toHaveBeenCalledTimes(1); + })); + + it('should determine whether closing via the escape key is allowed', fakeAsync(() => { + let canClose = false; + const closedSpy = jasmine.createSpy('closed spy'); + const ref = dialog.open(PizzaMsg, { + closePredicate: () => canClose, + viewContainerRef: testViewContainerRef, + }); + + ref.closed.subscribe(closedSpy); + viewContainerFixture.detectChanges(); + + expect(getDialogs().length).toBe(1); + + dispatchKeyboardEvent(document.body, 'keydown', ESCAPE); + viewContainerFixture.detectChanges(); + flush(); + + expect(getDialogs().length).toBe(1); + expect(closedSpy).not.toHaveBeenCalled(); + + canClose = true; + dispatchKeyboardEvent(document.body, 'keydown', ESCAPE); + viewContainerFixture.detectChanges(); + flush(); + + expect(getDialogs().length).toBe(0); + expect(closedSpy).toHaveBeenCalledTimes(1); + })); + + it('should determine whether closing via the `close` method is allowed', fakeAsync(() => { + let canClose = false; + const closedSpy = jasmine.createSpy('closed spy'); + const ref = dialog.open(PizzaMsg, { + closePredicate: () => canClose, + viewContainerRef: testViewContainerRef, + }); + + ref.closed.subscribe(closedSpy); + viewContainerFixture.detectChanges(); + + expect(getDialogs().length).toBe(1); + + ref.close(); + viewContainerFixture.detectChanges(); + flush(); + + expect(getDialogs().length).toBe(1); + expect(closedSpy).not.toHaveBeenCalled(); + + canClose = true; + ref.close('hello'); + viewContainerFixture.detectChanges(); + flush(); + + expect(getDialogs().length).toBe(0); + expect(closedSpy).toHaveBeenCalledTimes(1); + expect(closedSpy).toHaveBeenCalledWith('hello'); + })); + + it('should not be closed by `closeAll` if not allowed by the predicate', fakeAsync(() => { + let canClose = false; + const config = {closePredicate: () => canClose}; + const spy = jasmine.createSpy('afterAllClosed spy'); + dialog.open(PizzaMsg, config); + viewContainerFixture.detectChanges(); + dialog.open(PizzaMsg, config); + viewContainerFixture.detectChanges(); + dialog.open(PizzaMsg, config); + viewContainerFixture.detectChanges(); + + const subscription = dialog.afterAllClosed.subscribe(spy); + expect(getDialogs().length).toBe(3); + expect(dialog.openDialogs.length).toBe(3); + + dialog.closeAll(); + viewContainerFixture.detectChanges(); + flush(); + + expect(getDialogs().length).toBe(3); + expect(dialog.openDialogs.length).toBe(3); + expect(spy).not.toHaveBeenCalled(); + + canClose = true; + dialog.closeAll(); + viewContainerFixture.detectChanges(); + flush(); + + expect(getDialogs().length).toBe(0); + expect(dialog.openDialogs.length).toBe(0); + expect(spy).toHaveBeenCalledTimes(1); + + subscription.unsubscribe(); + })); + + it('should recapture focus to the first tabbable element when clicking on the backdrop while the `closePredicate` is blocking the close sequence', fakeAsync(() => { + // When testing focus, all of the elements must be in the DOM. + document.body.appendChild(overlayContainerElement); + + dialog.open(PizzaMsg, { + closePredicate: () => false, + viewContainerRef: testViewContainerRef, + }); + + viewContainerFixture.detectChanges(); + flushMicrotasks(); + + const backdrop = overlayContainerElement.querySelector( + '.cdk-overlay-backdrop', + ) as HTMLElement; + const input = overlayContainerElement.querySelector('input') as HTMLInputElement; + + expect(document.activeElement) + .withContext('Expected input to be focused on open') + .toBe(input); + + input.blur(); // Programmatic clicks might not move focus so we simulate it. + backdrop.click(); + viewContainerFixture.detectChanges(); + flush(); + + expect(document.activeElement) + .withContext('Expected input to stay focused after click') + .toBe(input); + + overlayContainerElement.remove(); + })); }); describe('hasBackdrop option', () => { diff --git a/src/material-experimental/mdc-dialog/dialog.spec.ts b/src/material-experimental/mdc-dialog/dialog.spec.ts index a0c383ab7b3e..1e0e7a4393bd 100644 --- a/src/material-experimental/mdc-dialog/dialog.spec.ts +++ b/src/material-experimental/mdc-dialog/dialog.spec.ts @@ -1044,6 +1044,169 @@ describe('MDC-based MatDialog', () => { ); }); + describe('closePredicate option', () => { + function getDialogs() { + return overlayContainerElement.querySelectorAll('mat-dialog-container'); + } + + it('should determine whether closing via the backdrop is allowed', fakeAsync(() => { + let canClose = false; + const closedSpy = jasmine.createSpy('closed spy'); + const ref = dialog.open(PizzaMsg, { + closePredicate: () => canClose, + viewContainerRef: testViewContainerRef, + }); + + ref.afterClosed().subscribe(closedSpy); + viewContainerFixture.detectChanges(); + + expect(getDialogs().length).toBe(1); + + let backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement; + backdrop.click(); + viewContainerFixture.detectChanges(); + flush(); + + expect(getDialogs().length).toBe(1); + expect(closedSpy).not.toHaveBeenCalled(); + + canClose = true; + backdrop.click(); + viewContainerFixture.detectChanges(); + flush(); + + expect(getDialogs().length).toBe(0); + expect(closedSpy).toHaveBeenCalledTimes(1); + })); + + it('should determine whether closing via the escape key is allowed', fakeAsync(() => { + let canClose = false; + const closedSpy = jasmine.createSpy('closed spy'); + const ref = dialog.open(PizzaMsg, { + closePredicate: () => canClose, + viewContainerRef: testViewContainerRef, + }); + + ref.afterClosed().subscribe(closedSpy); + viewContainerFixture.detectChanges(); + + expect(getDialogs().length).toBe(1); + + dispatchKeyboardEvent(document.body, 'keydown', ESCAPE); + viewContainerFixture.detectChanges(); + flush(); + + expect(getDialogs().length).toBe(1); + expect(closedSpy).not.toHaveBeenCalled(); + + canClose = true; + dispatchKeyboardEvent(document.body, 'keydown', ESCAPE); + viewContainerFixture.detectChanges(); + flush(); + + expect(getDialogs().length).toBe(0); + expect(closedSpy).toHaveBeenCalledTimes(1); + })); + + it('should determine whether closing via the `close` method is allowed', fakeAsync(() => { + let canClose = false; + const closedSpy = jasmine.createSpy('closed spy'); + const ref = dialog.open(PizzaMsg, { + closePredicate: () => canClose, + viewContainerRef: testViewContainerRef, + }); + + ref.afterClosed().subscribe(closedSpy); + viewContainerFixture.detectChanges(); + + expect(getDialogs().length).toBe(1); + + ref.close(); + viewContainerFixture.detectChanges(); + flush(); + + expect(getDialogs().length).toBe(1); + expect(closedSpy).not.toHaveBeenCalled(); + + canClose = true; + ref.close('hello'); + viewContainerFixture.detectChanges(); + flush(); + + expect(getDialogs().length).toBe(0); + expect(closedSpy).toHaveBeenCalledTimes(1); + expect(closedSpy).toHaveBeenCalledWith('hello'); + })); + + it('should not be closed by `closeAll` if not allowed by the predicate', fakeAsync(() => { + let canClose = false; + const config = {closePredicate: () => canClose}; + const spy = jasmine.createSpy('afterAllClosed spy'); + dialog.open(PizzaMsg, config); + viewContainerFixture.detectChanges(); + dialog.open(PizzaMsg, config); + viewContainerFixture.detectChanges(); + dialog.open(PizzaMsg, config); + viewContainerFixture.detectChanges(); + + const subscription = dialog.afterAllClosed.subscribe(spy); + expect(getDialogs().length).toBe(3); + expect(dialog.openDialogs.length).toBe(3); + + dialog.closeAll(); + viewContainerFixture.detectChanges(); + flush(); + + expect(getDialogs().length).toBe(3); + expect(dialog.openDialogs.length).toBe(3); + expect(spy).not.toHaveBeenCalled(); + + canClose = true; + dialog.closeAll(); + viewContainerFixture.detectChanges(); + flush(); + + expect(getDialogs().length).toBe(0); + expect(dialog.openDialogs.length).toBe(0); + expect(spy).toHaveBeenCalledTimes(1); + + subscription.unsubscribe(); + })); + + it('should recapture focus to the first tabbable element when clicking on the backdrop while the `closePredicate` is blocking the close sequence', fakeAsync(() => { + // When testing focus, all of the elements must be in the DOM. + document.body.appendChild(overlayContainerElement); + + dialog.open(PizzaMsg, { + closePredicate: () => false, + viewContainerRef: testViewContainerRef, + }); + + viewContainerFixture.detectChanges(); + flushMicrotasks(); + + const backdrop = overlayContainerElement.querySelector( + '.cdk-overlay-backdrop', + ) as HTMLElement; + const input = overlayContainerElement.querySelector('input') as HTMLInputElement; + + expect(document.activeElement) + .withContext('Expected input to be focused on open') + .toBe(input); + + input.blur(); // Programmatic clicks might not move focus so we simulate it. + backdrop.click(); + viewContainerFixture.detectChanges(); + flush(); + + expect(document.activeElement) + .withContext('Expected input to stay focused after click') + .toBe(input); + + overlayContainerElement.remove(); + })); + }); + it( 'should recapture focus to the first header when clicking on the backdrop with ' + 'autoFocus set to "first-heading"', diff --git a/src/material/dialog/dialog-config.ts b/src/material/dialog/dialog-config.ts index b0709eb26466..fd9d812a742f 100644 --- a/src/material/dialog/dialog-config.ts +++ b/src/material/dialog/dialog-config.ts @@ -10,6 +10,7 @@ import {ViewContainerRef, ComponentFactoryResolver, Injector} from '@angular/cor import {Direction} from '@angular/cdk/bidi'; import {ScrollStrategy} from '@angular/cdk/overlay'; import {defaultParams} from './dialog-animations'; +import {DialogConfig} from '@angular/cdk/dialog'; /** Options for where to set focus to automatically on dialog open */ export type AutoFocusTarget = 'dialog' | 'first-tabbable' | 'first-heading'; @@ -68,6 +69,17 @@ export class MatDialogConfig { /** Whether the user can use escape or clicking on the backdrop to close the modal. */ disableClose?: boolean = false; + /** Function used to determine whether the dialog is allowed to close. */ + closePredicate?: < + Result = unknown, + Config extends DialogConfig = MatDialogConfig, + Component = unknown, + >( + result: Result | undefined, + config: Config, + componentInstance: Component | null, + ) => boolean; + /** Width of the dialog. */ width?: string = ''; diff --git a/src/material/dialog/dialog-ref.ts b/src/material/dialog/dialog-ref.ts index 63cbaccb8894..a4ef449eaded 100644 --- a/src/material/dialog/dialog-ref.ts +++ b/src/material/dialog/dialog-ref.ts @@ -59,10 +59,10 @@ export class MatDialogRef { constructor( private _ref: DialogRef, - config: MatDialogConfig, + private _config: MatDialogConfig, public _containerInstance: _MatDialogContainerBase, ) { - this.disableClose = config.disableClose; + this.disableClose = _config.disableClose; this.id = _ref.id; // Emit when opening animation completes @@ -111,6 +111,17 @@ export class MatDialogRef { * @param dialogResult Optional result to return to the dialog opener. */ close(dialogResult?: R): void { + if ( + this._config.closePredicate && + !this._config.closePredicate( + dialogResult, + this._config, + this.componentInstance, + ) + ) { + return; + } + this._result = dialogResult; // Transition the backdrop in parallel to the dialog. diff --git a/src/material/dialog/dialog.spec.ts b/src/material/dialog/dialog.spec.ts index 428dd3b6f3af..ff2c6de8f3fa 100644 --- a/src/material/dialog/dialog.spec.ts +++ b/src/material/dialog/dialog.spec.ts @@ -1166,6 +1166,169 @@ describe('MatDialog', () => { ); }); + describe('closePredicate option', () => { + function getDialogs() { + return overlayContainerElement.querySelectorAll('mat-dialog-container'); + } + + it('should determine whether closing via the backdrop is allowed', fakeAsync(() => { + let canClose = false; + const closedSpy = jasmine.createSpy('closed spy'); + const ref = dialog.open(PizzaMsg, { + closePredicate: () => canClose, + viewContainerRef: testViewContainerRef, + }); + + ref.afterClosed().subscribe(closedSpy); + viewContainerFixture.detectChanges(); + + expect(getDialogs().length).toBe(1); + + let backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement; + backdrop.click(); + viewContainerFixture.detectChanges(); + flush(); + + expect(getDialogs().length).toBe(1); + expect(closedSpy).not.toHaveBeenCalled(); + + canClose = true; + backdrop.click(); + viewContainerFixture.detectChanges(); + flush(); + + expect(getDialogs().length).toBe(0); + expect(closedSpy).toHaveBeenCalledTimes(1); + })); + + it('should determine whether closing via the escape key is allowed', fakeAsync(() => { + let canClose = false; + const closedSpy = jasmine.createSpy('closed spy'); + const ref = dialog.open(PizzaMsg, { + closePredicate: () => canClose, + viewContainerRef: testViewContainerRef, + }); + + ref.afterClosed().subscribe(closedSpy); + viewContainerFixture.detectChanges(); + + expect(getDialogs().length).toBe(1); + + dispatchKeyboardEvent(document.body, 'keydown', ESCAPE); + viewContainerFixture.detectChanges(); + flush(); + + expect(getDialogs().length).toBe(1); + expect(closedSpy).not.toHaveBeenCalled(); + + canClose = true; + dispatchKeyboardEvent(document.body, 'keydown', ESCAPE); + viewContainerFixture.detectChanges(); + flush(); + + expect(getDialogs().length).toBe(0); + expect(closedSpy).toHaveBeenCalledTimes(1); + })); + + it('should determine whether closing via the `close` method is allowed', fakeAsync(() => { + let canClose = false; + const closedSpy = jasmine.createSpy('closed spy'); + const ref = dialog.open(PizzaMsg, { + closePredicate: () => canClose, + viewContainerRef: testViewContainerRef, + }); + + ref.afterClosed().subscribe(closedSpy); + viewContainerFixture.detectChanges(); + + expect(getDialogs().length).toBe(1); + + ref.close(); + viewContainerFixture.detectChanges(); + flush(); + + expect(getDialogs().length).toBe(1); + expect(closedSpy).not.toHaveBeenCalled(); + + canClose = true; + ref.close('hello'); + viewContainerFixture.detectChanges(); + flush(); + + expect(getDialogs().length).toBe(0); + expect(closedSpy).toHaveBeenCalledTimes(1); + expect(closedSpy).toHaveBeenCalledWith('hello'); + })); + + it('should not be closed by `closeAll` if not allowed by the predicate', fakeAsync(() => { + let canClose = false; + const config = {closePredicate: () => canClose}; + const spy = jasmine.createSpy('afterAllClosed spy'); + dialog.open(PizzaMsg, config); + viewContainerFixture.detectChanges(); + dialog.open(PizzaMsg, config); + viewContainerFixture.detectChanges(); + dialog.open(PizzaMsg, config); + viewContainerFixture.detectChanges(); + + const subscription = dialog.afterAllClosed.subscribe(spy); + expect(getDialogs().length).toBe(3); + expect(dialog.openDialogs.length).toBe(3); + + dialog.closeAll(); + viewContainerFixture.detectChanges(); + flush(); + + expect(getDialogs().length).toBe(3); + expect(dialog.openDialogs.length).toBe(3); + expect(spy).not.toHaveBeenCalled(); + + canClose = true; + dialog.closeAll(); + viewContainerFixture.detectChanges(); + flush(); + + expect(getDialogs().length).toBe(0); + expect(dialog.openDialogs.length).toBe(0); + expect(spy).toHaveBeenCalledTimes(1); + + subscription.unsubscribe(); + })); + + it('should recapture focus to the first tabbable element when clicking on the backdrop while the `closePredicate` is blocking the close sequence', fakeAsync(() => { + // When testing focus, all of the elements must be in the DOM. + document.body.appendChild(overlayContainerElement); + + dialog.open(PizzaMsg, { + closePredicate: () => false, + viewContainerRef: testViewContainerRef, + }); + + viewContainerFixture.detectChanges(); + flushMicrotasks(); + + const backdrop = overlayContainerElement.querySelector( + '.cdk-overlay-backdrop', + ) as HTMLElement; + const input = overlayContainerElement.querySelector('input') as HTMLInputElement; + + expect(document.activeElement) + .withContext('Expected input to be focused on open') + .toBe(input); + + input.blur(); // Programmatic clicks might not move focus so we simulate it. + backdrop.click(); + viewContainerFixture.detectChanges(); + flush(); + + expect(document.activeElement) + .withContext('Expected input to stay focused after click') + .toBe(input); + + overlayContainerElement.remove(); + })); + }); + describe('hasBackdrop option', () => { it('should have a backdrop', () => { dialog.open(PizzaMsg, { diff --git a/src/material/dialog/dialog.ts b/src/material/dialog/dialog.ts index 24decc088de5..6f4ca65391fb 100644 --- a/src/material/dialog/dialog.ts +++ b/src/material/dialog/dialog.ts @@ -166,6 +166,8 @@ export abstract class _MatDialogBase implemen positionStrategy: this._overlay.position().global().centerHorizontally().centerVertically(), // Disable closing since we need to sync it up to the animation ourselves. disableClose: true, + // Closing is tied to our animation so the close predicate has to be implemented separately. + closePredicate: undefined, // 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. diff --git a/tools/public_api_guard/cdk/dialog.md b/tools/public_api_guard/cdk/dialog.md index 2c5b99009d7a..b22f6801cf24 100644 --- a/tools/public_api_guard/cdk/dialog.md +++ b/tools/public_api_guard/cdk/dialog.md @@ -64,6 +64,8 @@ export class CdkDialogContainer extends B protected _focusTrapFactory: FocusTrapFactory; // (undocumented) ngOnDestroy(): void; + // (undocumented) + protected _overlayRef: OverlayRef; _portalOutlet: CdkPortalOutlet; _recaptureFocus(): void; protected _trapFocus(): void; @@ -127,6 +129,7 @@ export class DialogConfig(result: Result | undefined, config: this, componentInstance: Component | null) => boolean; componentFactoryResolver?: ComponentFactoryResolver; container?: Type | { type: Type; @@ -176,6 +179,7 @@ export class DialogRef { readonly config: DialogConfig, BasePortalOutlet>; readonly containerInstance: BasePortalOutlet & { _closeInteractionType?: FocusOrigin; + _recaptureFocus?: () => void; }; disableClose: boolean | undefined; readonly id: string; diff --git a/tools/public_api_guard/material/dialog.md b/tools/public_api_guard/material/dialog.md index cfdcc6202998..c036cf67aab9 100644 --- a/tools/public_api_guard/material/dialog.md +++ b/tools/public_api_guard/material/dialog.md @@ -10,6 +10,7 @@ import { CdkDialogContainer } from '@angular/cdk/dialog'; import { ChangeDetectorRef } from '@angular/core'; import { ComponentFactoryResolver } from '@angular/core'; import { ComponentType } from '@angular/cdk/portal'; +import { DialogConfig } from '@angular/cdk/dialog'; import { DialogRef } from '@angular/cdk/dialog'; import { Direction } from '@angular/cdk/bidi'; import { ElementRef } from '@angular/core'; @@ -161,6 +162,7 @@ export class MatDialogConfig { autoFocus?: AutoFocusTarget | string | boolean; backdropClass?: string | string[]; closeOnNavigation?: boolean; + closePredicate?: (result: Result | undefined, config: Config, componentInstance: Component | null) => boolean; componentFactoryResolver?: ComponentFactoryResolver; data?: D | null; delayFocusTrap?: boolean; @@ -240,7 +242,7 @@ export class MatDialogModule { // @public export class MatDialogRef { - constructor(_ref: DialogRef, config: MatDialogConfig, _containerInstance: _MatDialogContainerBase); + constructor(_ref: DialogRef, _config: MatDialogConfig, _containerInstance: _MatDialogContainerBase); addPanelClass(classes: string | string[]): this; afterClosed(): Observable; afterOpened(): Observable;