Skip to content

Commit fb9ff16

Browse files
authored
refactor(material/dialog): switch to CDK dialog internally (#24857)
Switches the Material dialog to be based on the CDK dialog.
1 parent d84703a commit fb9ff16

18 files changed

+297
-690
lines changed

src/cdk/dialog/dialog-config.ts

+6
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,12 @@ export class DialogConfig<D = unknown, R = unknown, C extends BasePortalOutlet =
126126
*/
127127
closeOnNavigation?: boolean = true;
128128

129+
/**
130+
* Whether the dialog should close when the dialog service is destroyed. This is useful if
131+
* another service is wrapping the dialog and is managing the destruction instead.
132+
*/
133+
closeOnDestroy?: boolean = true;
134+
129135
/** Alternate `ComponentFactoryResolver` to use when resolving the associated component. */
130136
componentFactoryResolver?: ComponentFactoryResolver;
131137

src/cdk/dialog/dialog-container.ts

+21-8
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import {
2424
} from '@angular/cdk/portal';
2525
import {DOCUMENT} from '@angular/common';
2626
import {
27-
AfterViewInit,
2827
ChangeDetectionStrategy,
2928
Component,
3029
ComponentRef,
@@ -67,7 +66,7 @@ export function throwDialogContentAlreadyAttachedError() {
6766
})
6867
export class CdkDialogContainer<C extends DialogConfig = DialogConfig>
6968
extends BasePortalOutlet
70-
implements AfterViewInit, OnDestroy
69+
implements OnDestroy
7170
{
7271
protected _document: Document;
7372

@@ -105,7 +104,7 @@ export class CdkDialogContainer<C extends DialogConfig = DialogConfig>
105104
this._document = _document;
106105
}
107106

108-
ngAfterViewInit() {
107+
protected _contentAttached() {
109108
this._initializeFocusTrap();
110109
this._handleBackdropClicks();
111110
this._captureInitialFocus();
@@ -132,7 +131,9 @@ export class CdkDialogContainer<C extends DialogConfig = DialogConfig>
132131
throwDialogContentAlreadyAttachedError();
133132
}
134133

135-
return this._portalOutlet.attachComponentPortal(portal);
134+
const result = this._portalOutlet.attachComponentPortal(portal);
135+
this._contentAttached();
136+
return result;
136137
}
137138

138139
/**
@@ -144,7 +145,9 @@ export class CdkDialogContainer<C extends DialogConfig = DialogConfig>
144145
throwDialogContentAlreadyAttachedError();
145146
}
146147

147-
return this._portalOutlet.attachTemplatePortal(portal);
148+
const result = this._portalOutlet.attachTemplatePortal(portal);
149+
this._contentAttached();
150+
return result;
148151
}
149152

150153
/**
@@ -158,9 +161,19 @@ export class CdkDialogContainer<C extends DialogConfig = DialogConfig>
158161
throwDialogContentAlreadyAttachedError();
159162
}
160163

161-
return this._portalOutlet.attachDomPortal(portal);
164+
const result = this._portalOutlet.attachDomPortal(portal);
165+
this._contentAttached();
166+
return result;
162167
};
163168

169+
// TODO(crisbeto): this shouldn't be exposed, but there are internal references to it.
170+
/** Captures focus if it isn't already inside the dialog. */
171+
_recaptureFocus() {
172+
if (!this._containsFocus()) {
173+
this._trapFocus();
174+
}
175+
}
176+
164177
/**
165178
* Focuses the provided element. If the element is not focusable, it will add a tabIndex
166179
* attribute to forcefully focus it. The attribute is removed after focus is moved.
@@ -316,8 +329,8 @@ export class CdkDialogContainer<C extends DialogConfig = DialogConfig>
316329
// Clicking on the backdrop will move focus out of dialog.
317330
// Recapture it if closing via the backdrop is disabled.
318331
this._overlayRef.backdropClick().subscribe(() => {
319-
if (this._config.disableClose && !this._containsFocus()) {
320-
this._trapFocus();
332+
if (this._config.disableClose) {
333+
this._recaptureFocus();
321334
}
322335
});
323336
}

src/cdk/dialog/dialog.ts

+36-20
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ export class Dialog implements OnDestroy {
138138
}
139139

140140
(this.openDialogs as DialogRef<R, C>[]).push(dialogRef);
141-
dialogRef.closed.subscribe(() => this._removeOpenDialog(dialogRef));
141+
dialogRef.closed.subscribe(() => this._removeOpenDialog(dialogRef, true));
142142
this.afterOpened.next(dialogRef);
143143

144144
return dialogRef;
@@ -148,7 +148,7 @@ export class Dialog implements OnDestroy {
148148
* Closes all of the currently-open dialogs.
149149
*/
150150
closeAll(): void {
151-
this._closeDialogs(this.openDialogs);
151+
reverseForEach(this.openDialogs, dialog => dialog.close());
152152
}
153153

154154
/**
@@ -160,11 +160,24 @@ export class Dialog implements OnDestroy {
160160
}
161161

162162
ngOnDestroy() {
163-
// Only close the dialogs at this level on destroy
164-
// since the parent service may still be active.
165-
this._closeDialogs(this._openDialogsAtThisLevel);
163+
// Make one pass over all the dialogs that need to be untracked, but should not be closed. We
164+
// want to stop tracking the open dialog even if it hasn't been closed, because the tracking
165+
// determines when `aria-hidden` is removed from elements outside the dialog.
166+
reverseForEach(this._openDialogsAtThisLevel, dialog => {
167+
// Check for `false` specifically since we want `undefined` to be interpreted as `true`.
168+
if (dialog.config.closeOnDestroy === false) {
169+
this._removeOpenDialog(dialog, false);
170+
}
171+
});
172+
173+
// Make a second pass and close the remaining dialogs. We do this second pass in order to
174+
// correctly dispatch the `afterAllClosed` event in case we have a mixed array of dialogs
175+
// that should be closed and dialogs that should not.
176+
reverseForEach(this._openDialogsAtThisLevel, dialog => dialog.close());
177+
166178
this._afterAllClosedAtThisLevel.complete();
167179
this._afterOpenedAtThisLevel.complete();
180+
this._openDialogsAtThisLevel = [];
168181
}
169182

170183
/**
@@ -326,8 +339,9 @@ export class Dialog implements OnDestroy {
326339
/**
327340
* Removes a dialog from the array of open dialogs.
328341
* @param dialogRef Dialog to be removed.
342+
* @param emitEvent Whether to emit an event if this is the last dialog.
329343
*/
330-
private _removeOpenDialog<R, C>(dialogRef: DialogRef<R, C>) {
344+
private _removeOpenDialog<R, C>(dialogRef: DialogRef<R, C>, emitEvent: boolean) {
331345
const index = this.openDialogs.indexOf(dialogRef);
332346

333347
if (index > -1) {
@@ -345,7 +359,10 @@ export class Dialog implements OnDestroy {
345359
});
346360

347361
this._ariaHiddenElements.clear();
348-
this._getAfterAllClosed().next();
362+
363+
if (emitEvent) {
364+
this._getAfterAllClosed().next();
365+
}
349366
}
350367
}
351368
}
@@ -374,21 +391,20 @@ export class Dialog implements OnDestroy {
374391
}
375392
}
376393

377-
/** Closes all of the dialogs in an array. */
378-
private _closeDialogs(dialogs: readonly DialogRef<unknown>[]) {
379-
let i = dialogs.length;
380-
381-
while (i--) {
382-
// The `_openDialogs` property isn't updated after close until the rxjs subscription
383-
// runs on the next microtask, in addition to modifying the array as we're going
384-
// through it. We loop through all of them and call close without assuming that
385-
// they'll be removed from the list instantaneously.
386-
dialogs[i].close();
387-
}
388-
}
389-
390394
private _getAfterAllClosed(): Subject<void> {
391395
const parent = this._parentDialog;
392396
return parent ? parent._getAfterAllClosed() : this._afterAllClosedAtThisLevel;
393397
}
394398
}
399+
400+
/**
401+
* Executes a callback against all elements in an array while iterating in reverse.
402+
* Useful if the array is being modified as it is being iterated.
403+
*/
404+
function reverseForEach<T>(items: T[] | readonly T[], callback: (current: T) => void) {
405+
let i = items.length;
406+
407+
while (i--) {
408+
callback(items[i]);
409+
}
410+
}

src/material-experimental/mdc-dialog/BUILD.bazel

+1
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ ng_test_library(
6464
":mdc-dialog",
6565
"//src/cdk/a11y",
6666
"//src/cdk/bidi",
67+
"//src/cdk/dialog",
6768
"//src/cdk/keycodes",
6869
"//src/cdk/overlay",
6970
"//src/cdk/platform",

src/material-experimental/mdc-dialog/dialog-container.ts

+13-11
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
*/
88

99
import {FocusMonitor, FocusTrapFactory, InteractivityChecker} from '@angular/cdk/a11y';
10+
import {OverlayRef} from '@angular/cdk/overlay';
1011
import {DOCUMENT} from '@angular/common';
1112
import {
1213
ChangeDetectionStrategy,
13-
ChangeDetectorRef,
1414
Component,
1515
ElementRef,
1616
Inject,
@@ -38,8 +38,8 @@ import {cssClasses, numbers} from '@material/dialog';
3838
host: {
3939
'class': 'mat-mdc-dialog-container mdc-dialog',
4040
'tabindex': '-1',
41-
'aria-modal': 'true',
42-
'[id]': '_id',
41+
'[attr.aria-modal]': '_config.ariaModal',
42+
'[id]': '_config.id',
4343
'[attr.role]': '_config.role',
4444
'[attr.aria-labelledby]': '_config.ariaLabel ? null : _ariaLabelledBy',
4545
'[attr.aria-label]': '_config.ariaLabel',
@@ -67,30 +67,31 @@ export class MatDialogContainer extends _MatDialogContainerBase implements OnDes
6767
constructor(
6868
elementRef: ElementRef,
6969
focusTrapFactory: FocusTrapFactory,
70-
changeDetectorRef: ChangeDetectorRef,
7170
@Optional() @Inject(DOCUMENT) document: any,
72-
config: MatDialogConfig,
71+
dialogConfig: MatDialogConfig,
7372
checker: InteractivityChecker,
7473
ngZone: NgZone,
74+
overlayRef: OverlayRef,
7575
@Optional() @Inject(ANIMATION_MODULE_TYPE) private _animationMode?: string,
7676
focusMonitor?: FocusMonitor,
7777
) {
7878
super(
7979
elementRef,
8080
focusTrapFactory,
81-
changeDetectorRef,
8281
document,
83-
config,
82+
dialogConfig,
8483
checker,
8584
ngZone,
85+
overlayRef,
8686
focusMonitor,
8787
);
8888
}
8989

90-
override _initializeWithAttachedContent() {
90+
protected override _contentAttached(): void {
9191
// Delegate to the original dialog-container initialization (i.e. saving the
9292
// previous element, setting up the focus trap and moving focus to the container).
93-
super._initializeWithAttachedContent();
93+
super._contentAttached();
94+
9495
// Note: Usually we would be able to use the MDC dialog foundation here to handle
9596
// the dialog animation for us, but there are a few reasons why we just leverage
9697
// their styles and not use the runtime foundation code:
@@ -103,7 +104,9 @@ export class MatDialogContainer extends _MatDialogContainerBase implements OnDes
103104
this._startOpenAnimation();
104105
}
105106

106-
ngOnDestroy() {
107+
override ngOnDestroy() {
108+
super.ngOnDestroy();
109+
107110
if (this._animationTimer !== null) {
108111
clearTimeout(this._animationTimer);
109112
}
@@ -177,7 +180,6 @@ export class MatDialogContainer extends _MatDialogContainerBase implements OnDes
177180
*/
178181
private _finishDialogClose = () => {
179182
this._clearAnimationClasses();
180-
this._restoreFocus();
181183
this._animationStateChanged.emit({state: 'closed', totalTime: this._closeAnimationDuration});
182184
};
183185

src/material-experimental/mdc-dialog/dialog-ref.ts

+1-14
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,9 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {OverlayRef} from '@angular/cdk/overlay';
109
import {MatDialogRef as NonMdcDialogRef} from '@angular/material/dialog';
11-
import {MatDialogContainer} from './dialog-container';
12-
13-
// Counter for unique dialog ids.
14-
let uniqueId = 0;
1510

1611
/**
1712
* Reference to a dialog opened via the MatDialog service.
1813
*/
19-
export class MatDialogRef<T, R = any> extends NonMdcDialogRef<T, R> {
20-
constructor(
21-
overlayRef: OverlayRef,
22-
containerInstance: MatDialogContainer,
23-
id: string = `mat-mdc-dialog-${uniqueId++}`,
24-
) {
25-
super(overlayRef, containerInstance, id);
26-
}
27-
}
14+
export class MatDialogRef<T, R = any> extends NonMdcDialogRef<T, R> {}

src/material-experimental/mdc-dialog/dialog.spec.ts

+4
Original file line numberDiff line numberDiff line change
@@ -1325,6 +1325,7 @@ describe('MDC-based MatDialog', () => {
13251325

13261326
tick(500);
13271327
viewContainerFixture.detectChanges();
1328+
flushMicrotasks();
13281329
expect(lastFocusOrigin!).withContext('Expected the trigger button to be blurred').toBeNull();
13291330

13301331
dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);
@@ -1359,6 +1360,7 @@ describe('MDC-based MatDialog', () => {
13591360

13601361
tick(500);
13611362
viewContainerFixture.detectChanges();
1363+
flushMicrotasks();
13621364
expect(lastFocusOrigin!).withContext('Expected the trigger button to be blurred').toBeNull();
13631365

13641366
const backdrop = overlayContainerElement.querySelector(
@@ -1395,6 +1397,7 @@ describe('MDC-based MatDialog', () => {
13951397

13961398
tick(500);
13971399
viewContainerFixture.detectChanges();
1400+
flushMicrotasks();
13981401
expect(lastFocusOrigin!).withContext('Expected the trigger button to be blurred').toBeNull();
13991402

14001403
const closeButton = overlayContainerElement.querySelector(
@@ -1434,6 +1437,7 @@ describe('MDC-based MatDialog', () => {
14341437

14351438
tick(500);
14361439
viewContainerFixture.detectChanges();
1440+
flushMicrotasks();
14371441
expect(lastFocusOrigin!).withContext('Expected the trigger button to be blurred').toBeNull();
14381442

14391443
const closeButton = overlayContainerElement.querySelector(

src/material-experimental/mdc-dialog/dialog.ts

+15-2
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,18 @@
88

99
import {Overlay, OverlayContainer, ScrollStrategy} from '@angular/cdk/overlay';
1010
import {Location} from '@angular/common';
11-
import {Inject, Injectable, InjectionToken, Injector, Optional, SkipSelf} from '@angular/core';
11+
import {
12+
ANIMATION_MODULE_TYPE,
13+
Inject,
14+
Injectable,
15+
InjectionToken,
16+
Injector,
17+
Optional,
18+
SkipSelf,
19+
} from '@angular/core';
1220
import {_MatDialogBase, MatDialogConfig} from '@angular/material/dialog';
1321
import {MatDialogContainer} from './dialog-container';
1422
import {MatDialogRef} from './dialog-ref';
15-
import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
1623

1724
/** Injection token that can be used to access the data that was passed in to a dialog. */
1825
export const MAT_DIALOG_DATA = new InjectionToken<any>('MatMdcDialogData');
@@ -57,6 +64,10 @@ export class MatDialog extends _MatDialogBase<MatDialogContainer> {
5764
@Optional() @Inject(MAT_DIALOG_DEFAULT_OPTIONS) defaultOptions: MatDialogConfig,
5865
@Inject(MAT_DIALOG_SCROLL_STRATEGY) scrollStrategy: any,
5966
@Optional() @SkipSelf() parentDialog: MatDialog,
67+
/**
68+
* @deprecated No longer used. To be removed.
69+
* @breaking-change 15.0.0
70+
*/
6071
overlayContainer: OverlayContainer,
6172
/**
6273
* @deprecated No longer used. To be removed.
@@ -78,5 +89,7 @@ export class MatDialog extends _MatDialogBase<MatDialogContainer> {
7889
MAT_DIALOG_DATA,
7990
animationMode,
8091
);
92+
93+
this._idPrefix = 'mat-mdc-dialog-';
8194
}
8295
}

src/material-experimental/mdc-dialog/module.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {DialogModule} from '@angular/cdk/dialog';
910
import {OverlayModule} from '@angular/cdk/overlay';
1011
import {PortalModule} from '@angular/cdk/portal';
1112
import {NgModule} from '@angular/core';
@@ -20,7 +21,7 @@ import {
2021
} from './dialog-content-directives';
2122

2223
@NgModule({
23-
imports: [OverlayModule, PortalModule, MatCommonModule],
24+
imports: [DialogModule, OverlayModule, PortalModule, MatCommonModule],
2425
exports: [
2526
MatDialogContainer,
2627
MatDialogClose,

0 commit comments

Comments
 (0)