Skip to content

Commit 54bae80

Browse files
committed
refactor(material/dialog): switch to CDK dialog internally
Switches the Material dialog to be based on the CDK dialog.
1 parent fb4e395 commit 54bae80

File tree

18 files changed

+288
-681
lines changed

18 files changed

+288
-681
lines changed

src/cdk/dialog/dialog-config.ts

Lines changed: 6 additions & 0 deletions
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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,14 @@ export class CdkDialogContainer<C extends DialogConfig = DialogConfig>
161161
return this._portalOutlet.attachDomPortal(portal);
162162
};
163163

164+
// TODO(crisbeto): this shouldn't be exposed, but there are internal references to it.
165+
/** Captures focus if it isn't already inside the dialog. */
166+
_recaptureFocus() {
167+
if (!this._containsFocus()) {
168+
this._trapFocus();
169+
}
170+
}
171+
164172
/**
165173
* Focuses the provided element. If the element is not focusable, it will add a tabIndex
166174
* attribute to forcefully focus it. The attribute is removed after focus is moved.
@@ -316,8 +324,8 @@ export class CdkDialogContainer<C extends DialogConfig = DialogConfig>
316324
// Clicking on the backdrop will move focus out of dialog.
317325
// Recapture it if closing via the backdrop is disabled.
318326
this._overlayRef.backdropClick().subscribe(() => {
319-
if (this._config.disableClose && !this._containsFocus()) {
320-
this._trapFocus();
327+
if (this._config.disableClose) {
328+
this._recaptureFocus();
321329
}
322330
});
323331
}

src/cdk/dialog/dialog.ts

Lines changed: 36 additions & 20 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,18 @@
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,
1717
OnDestroy,
1818
Optional,
1919
ViewEncapsulation,
2020
NgZone,
21+
AfterViewInit,
2122
} from '@angular/core';
2223
import {MatDialogConfig, _MatDialogContainerBase} from '@angular/material/dialog';
2324
import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
@@ -38,16 +39,19 @@ import {cssClasses, numbers} from '@material/dialog';
3839
host: {
3940
'class': 'mat-mdc-dialog-container mdc-dialog',
4041
'tabindex': '-1',
41-
'aria-modal': 'true',
42-
'[id]': '_id',
42+
'[attr.aria-modal]': '_config.ariaModal',
43+
'[id]': '_config.id',
4344
'[attr.role]': '_config.role',
4445
'[attr.aria-labelledby]': '_config.ariaLabel ? null : _ariaLabelledBy',
4546
'[attr.aria-label]': '_config.ariaLabel',
4647
'[attr.aria-describedby]': '_config.ariaDescribedBy || null',
4748
'[class._mat-animation-noopable]': '!_animationsEnabled',
4849
},
4950
})
50-
export class MatDialogContainer extends _MatDialogContainerBase implements OnDestroy {
51+
export class MatDialogContainer
52+
extends _MatDialogContainerBase
53+
implements OnDestroy, AfterViewInit
54+
{
5155
/** Whether animations are enabled. */
5256
_animationsEnabled: boolean = this._animationMode !== 'NoopAnimations';
5357

@@ -67,30 +71,31 @@ export class MatDialogContainer extends _MatDialogContainerBase implements OnDes
6771
constructor(
6872
elementRef: ElementRef,
6973
focusTrapFactory: FocusTrapFactory,
70-
changeDetectorRef: ChangeDetectorRef,
7174
@Optional() @Inject(DOCUMENT) document: any,
72-
config: MatDialogConfig,
75+
dialogConfig: MatDialogConfig,
7376
checker: InteractivityChecker,
7477
ngZone: NgZone,
78+
overlayRef: OverlayRef,
7579
@Optional() @Inject(ANIMATION_MODULE_TYPE) private _animationMode?: string,
7680
focusMonitor?: FocusMonitor,
7781
) {
7882
super(
7983
elementRef,
8084
focusTrapFactory,
81-
changeDetectorRef,
8285
document,
83-
config,
86+
dialogConfig,
8487
checker,
8588
ngZone,
89+
overlayRef,
8690
focusMonitor,
8791
);
8892
}
8993

90-
override _initializeWithAttachedContent() {
94+
override ngAfterViewInit(): void {
9195
// Delegate to the original dialog-container initialization (i.e. saving the
9296
// previous element, setting up the focus trap and moving focus to the container).
93-
super._initializeWithAttachedContent();
97+
super.ngAfterViewInit();
98+
9499
// Note: Usually we would be able to use the MDC dialog foundation here to handle
95100
// the dialog animation for us, but there are a few reasons why we just leverage
96101
// their styles and not use the runtime foundation code:
@@ -103,7 +108,9 @@ export class MatDialogContainer extends _MatDialogContainerBase implements OnDes
103108
this._startOpenAnimation();
104109
}
105110

106-
ngOnDestroy() {
111+
override ngOnDestroy() {
112+
super.ngOnDestroy();
113+
107114
if (this._animationTimer !== null) {
108115
clearTimeout(this._animationTimer);
109116
}
@@ -177,7 +184,6 @@ export class MatDialogContainer extends _MatDialogContainerBase implements OnDes
177184
*/
178185
private _finishDialogClose = () => {
179186
this._clearAnimationClasses();
180-
this._restoreFocus();
181187
this._animationStateChanged.emit({state: 'closed', totalTime: this._closeAnimationDuration});
182188
};
183189

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

Lines changed: 1 addition & 14 deletions
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

Lines changed: 4 additions & 0 deletions
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

Lines changed: 15 additions & 2 deletions
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

Lines changed: 2 additions & 1 deletion
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,

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ export {
1717
MatDialogState,
1818
MatDialogConfig,
1919
matDialogAnimations,
20-
throwMatDialogContentAlreadyAttachedError,
2120
DialogRole,
2221
DialogPosition,
2322
MAT_DIALOG_SCROLL_STRATEGY_FACTORY,

src/material/dialog/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ ng_module(
2323
"//src:dev_mode_types",
2424
"//src/cdk/a11y",
2525
"//src/cdk/bidi",
26+
"//src/cdk/dialog",
2627
"//src/cdk/keycodes",
2728
"//src/cdk/overlay",
2829
"//src/cdk/platform",

0 commit comments

Comments
 (0)