Skip to content

Commit 5ab04ec

Browse files
authored
refactor(cdk/dialog): expand and clean up API (#24842)
Adjusts the public API of the CDK dialog based on a recent feedback session by: * Expanding `DialogRef.restoreFocus` to allow CSS selectors and DOM nodes. * Changing `Dialog.openDialogs`, `DialogRef.componentInstance` and `DialogRef.containerInstance` to be readonly. * Allowing for numbers to be passed in to `DialogRef.updateSize`. * Updating the doc string of `DialogRef.updateSize`.
1 parent 587c9e3 commit 5ab04ec

File tree

7 files changed

+118
-26
lines changed

7 files changed

+118
-26
lines changed

src/cdk/dialog/dialog-config.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,14 @@ export class DialogConfig<D = unknown, R = unknown, C extends BasePortalOutlet =
104104
autoFocus?: AutoFocusTarget | string | boolean = 'first-tabbable';
105105

106106
/**
107-
* Whether the dialog should restore focus to the
108-
* previously-focused element upon closing.
107+
* Whether the dialog should restore focus to the previously-focused element upon closing.
108+
* Has the following behavior based on the type that is passed in:
109+
* - `boolean` - when true, will return focus to the element that was focused before the dialog
110+
* was opened, otherwise won't restore focus at all.
111+
* - `string` - focus will be restored to the first element that matches the CSS selector.
112+
* - `HTMLElement` - focus will be restored to the specific element.
109113
*/
110-
restoreFocus?: boolean = true;
114+
restoreFocus?: boolean | string | HTMLElement = true;
111115

112116
/**
113117
* Scroll strategy to be used for the dialog. This determines how

src/cdk/dialog/dialog-container.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -241,13 +241,22 @@ export class CdkDialogContainer<C extends DialogConfig = DialogConfig>
241241

242242
/** Restores focus to the element that was focused before the dialog opened. */
243243
private _restoreFocus() {
244-
const previousElement = this._elementFocusedBeforeDialogWasOpened;
244+
const focusConfig = this._config.restoreFocus;
245+
let focusTargetElement: HTMLElement | null = null;
246+
247+
if (typeof focusConfig === 'string') {
248+
focusTargetElement = this._document.querySelector(focusConfig);
249+
} else if (typeof focusConfig === 'boolean') {
250+
focusTargetElement = focusConfig ? this._elementFocusedBeforeDialogWasOpened : null;
251+
} else if (focusConfig) {
252+
focusTargetElement = focusConfig;
253+
}
245254

246255
// We need the extra check, because IE can set the `activeElement` to null in some cases.
247256
if (
248257
this._config.restoreFocus &&
249-
previousElement &&
250-
typeof previousElement.focus === 'function'
258+
focusTargetElement &&
259+
typeof focusTargetElement.focus === 'function'
251260
) {
252261
const activeElement = _getFocusedElementPierceShadowDom();
253262
const element = this._elementRef.nativeElement;
@@ -263,10 +272,10 @@ export class CdkDialogContainer<C extends DialogConfig = DialogConfig>
263272
element.contains(activeElement)
264273
) {
265274
if (this._focusMonitor) {
266-
this._focusMonitor.focusVia(previousElement, this._closeInteractionType);
275+
this._focusMonitor.focusVia(focusTargetElement, this._closeInteractionType);
267276
this._closeInteractionType = null;
268277
} else {
269-
previousElement.focus();
278+
focusTargetElement.focus();
270279
}
271280
}
272281
}

src/cdk/dialog/dialog-ref.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@ export class DialogRef<R = unknown, C = unknown> {
2727
* Instance of component opened into the dialog. Will be
2828
* null when the dialog is opened using a `TemplateRef`.
2929
*/
30-
componentInstance: C | null;
30+
readonly componentInstance: C | null;
3131

3232
/** Instance of the container that is rendering out the dialog content. */
33-
containerInstance: BasePortalOutlet & {_closeInteractionType?: FocusOrigin};
33+
readonly containerInstance: BasePortalOutlet & {_closeInteractionType?: FocusOrigin};
3434

3535
/** Whether the user is allowed to close the dialog. */
3636
disableClose: boolean | undefined;
@@ -86,11 +86,13 @@ export class DialogRef<R = unknown, C = unknown> {
8686
this.overlayRef.dispose();
8787
closedSubject.next(result);
8888
closedSubject.complete();
89-
this.componentInstance = this.containerInstance = null!;
89+
(this as {componentInstance: C}).componentInstance = (
90+
this as {containerInstance: BasePortalOutlet}
91+
).containerInstance = null!;
9092
}
9193
}
9294

93-
/** Updates the dialog's position. */
95+
/** Updates the position of the dialog based on the current position strategy. */
9496
updatePosition(): this {
9597
this.overlayRef.updatePosition();
9698
return this;
@@ -101,9 +103,8 @@ export class DialogRef<R = unknown, C = unknown> {
101103
* @param width New width of the dialog.
102104
* @param height New height of the dialog.
103105
*/
104-
updateSize(width: string = '', height: string = ''): this {
106+
updateSize(width: string | number = '', height: string | number = ''): this {
105107
this.overlayRef.updateSize({width, height});
106-
this.overlayRef.updatePosition();
107108
return this;
108109
}
109110

src/cdk/dialog/dialog.spec.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -934,6 +934,85 @@ describe('Dialog', () => {
934934
.withContext('Expected dialog container to be focused.')
935935
.toBe('cdk-dialog-container');
936936
}));
937+
938+
it('should allow for focus restoration to be disabled', fakeAsync(() => {
939+
// Create a element that has focus before the dialog is opened.
940+
const button = document.createElement('button');
941+
button.id = 'dialog-trigger';
942+
document.body.appendChild(button);
943+
button.focus();
944+
945+
const dialogRef = dialog.open(PizzaMsg, {
946+
viewContainerRef: testViewContainerRef,
947+
restoreFocus: false,
948+
});
949+
950+
flushMicrotasks();
951+
viewContainerFixture.detectChanges();
952+
flushMicrotasks();
953+
954+
expect(document.activeElement!.id).not.toBe('dialog-trigger');
955+
956+
dialogRef.close();
957+
flushMicrotasks();
958+
viewContainerFixture.detectChanges();
959+
flush();
960+
961+
expect(document.activeElement!.id).not.toBe('dialog-trigger');
962+
button.remove();
963+
}));
964+
965+
it('should allow for focus to be restored to an element matching a selector', fakeAsync(() => {
966+
// Create a element that has focus before the dialog is opened.
967+
const button = document.createElement('button');
968+
button.id = 'dialog-trigger';
969+
document.body.appendChild(button);
970+
971+
const dialogRef = dialog.open(PizzaMsg, {
972+
viewContainerRef: testViewContainerRef,
973+
restoreFocus: `#${button.id}`,
974+
});
975+
976+
flushMicrotasks();
977+
viewContainerFixture.detectChanges();
978+
flushMicrotasks();
979+
980+
expect(document.activeElement!.id).not.toBe('dialog-trigger');
981+
982+
dialogRef.close();
983+
flushMicrotasks();
984+
viewContainerFixture.detectChanges();
985+
flush();
986+
987+
expect(document.activeElement!.id).toBe('dialog-trigger');
988+
button.remove();
989+
}));
990+
991+
it('should allow for focus to be restored to a specific DOM node', fakeAsync(() => {
992+
// Create a element that has focus before the dialog is opened.
993+
const button = document.createElement('button');
994+
button.id = 'dialog-trigger';
995+
document.body.appendChild(button);
996+
997+
const dialogRef = dialog.open(PizzaMsg, {
998+
viewContainerRef: testViewContainerRef,
999+
restoreFocus: button,
1000+
});
1001+
1002+
flushMicrotasks();
1003+
viewContainerFixture.detectChanges();
1004+
flushMicrotasks();
1005+
1006+
expect(document.activeElement!.id).not.toBe('dialog-trigger');
1007+
1008+
dialogRef.close();
1009+
flushMicrotasks();
1010+
viewContainerFixture.detectChanges();
1011+
flush();
1012+
1013+
expect(document.activeElement!.id).toBe('dialog-trigger');
1014+
button.remove();
1015+
}));
9371016
});
9381017

9391018
describe('aria-label', () => {

src/cdk/dialog/dialog.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export class Dialog implements OnDestroy {
4848
private _scrollStrategy: () => ScrollStrategy;
4949

5050
/** Keeps track of the currently-open dialogs. */
51-
get openDialogs(): DialogRef<any, any>[] {
51+
get openDialogs(): readonly DialogRef<any, any>[] {
5252
return this._parentDialog ? this._parentDialog.openDialogs : this._openDialogsAtThisLevel;
5353
}
5454

@@ -129,15 +129,15 @@ export class Dialog implements OnDestroy {
129129
const dialogRef = new DialogRef(overlayRef, config);
130130
const dialogContainer = this._attachContainer(overlayRef, dialogRef, config);
131131

132-
dialogRef.containerInstance = dialogContainer;
132+
(dialogRef as {containerInstance: BasePortalOutlet}).containerInstance = dialogContainer;
133133
this._attachDialogContent(componentOrTemplateRef, dialogRef, dialogContainer, config);
134134

135135
// If this is the first dialog that we're opening, hide all the non-overlay content.
136136
if (!this.openDialogs.length) {
137137
this._hideNonDialogContentFromAssistiveTechnology();
138138
}
139139

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

@@ -278,7 +278,7 @@ export class Dialog implements OnDestroy {
278278
config.componentFactoryResolver,
279279
),
280280
);
281-
dialogRef.componentInstance = contentRef.instance;
281+
(dialogRef as {componentInstance: C}).componentInstance = contentRef.instance;
282282
}
283283
}
284284

@@ -331,7 +331,7 @@ export class Dialog implements OnDestroy {
331331
const index = this.openDialogs.indexOf(dialogRef);
332332

333333
if (index > -1) {
334-
this.openDialogs.splice(index, 1);
334+
(this.openDialogs as DialogRef<R, C>[]).splice(index, 1);
335335

336336
// If all the dialogs were closed, remove/restore the `aria-hidden`
337337
// to a the siblings and emit to the `afterAllClosed` stream.
@@ -375,7 +375,7 @@ export class Dialog implements OnDestroy {
375375
}
376376

377377
/** Closes all of the dialogs in an array. */
378-
private _closeDialogs(dialogs: DialogRef<unknown>[]) {
378+
private _closeDialogs(dialogs: readonly DialogRef<unknown>[]) {
379379
let i = dialogs.length;
380380

381381
while (i--) {

src/dev-app/cdk-dialog/dialog-demo.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ export class DialogDemo {
3434
maxHeight: defaultDialogConfig.maxHeight,
3535
data: {
3636
message: 'Jazzy jazz jazz',
37-
hmm: false,
3837
},
3938
};
4039
numTemplateOpens = 0;

tools/public_api_guard/cdk/dialog.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export class Dialog implements OnDestroy {
8989
open<R = unknown, D = unknown, C = unknown>(template: TemplateRef<C>, config?: DialogConfig<D, DialogRef<R, C>>): DialogRef<R, C>;
9090
// (undocumented)
9191
open<R = unknown, D = unknown, C = unknown>(componentOrTemplateRef: ComponentType<C> | TemplateRef<C>, config?: DialogConfig<D, DialogRef<R, C>>): DialogRef<R, C>;
92-
get openDialogs(): DialogRef<any, any>[];
92+
get openDialogs(): readonly DialogRef<any, any>[];
9393
// (undocumented)
9494
static ɵfac: i0.ɵɵFactoryDeclaration<Dialog, [null, null, { optional: true; }, { optional: true; skipSelf: true; }, null, null]>;
9595
// (undocumented)
@@ -145,7 +145,7 @@ export class DialogConfig<D = unknown, R = unknown, C extends BasePortalOutlet =
145145
panelClass?: string | string[];
146146
positionStrategy?: PositionStrategy;
147147
providers?: StaticProvider[] | ((dialogRef: R, config: DialogConfig<D, R, C>, container: C) => StaticProvider[]);
148-
restoreFocus?: boolean;
148+
restoreFocus?: boolean | string | HTMLElement;
149149
role?: DialogRole;
150150
scrollStrategy?: ScrollStrategy;
151151
templateContext?: Record<string, any> | (() => Record<string, any>);
@@ -170,10 +170,10 @@ export class DialogRef<R = unknown, C = unknown> {
170170
readonly backdropClick: Observable<MouseEvent>;
171171
close(result?: R, options?: DialogCloseOptions): void;
172172
readonly closed: Observable<R | undefined>;
173-
componentInstance: C | null;
173+
readonly componentInstance: C | null;
174174
// (undocumented)
175175
readonly config: DialogConfig<any, DialogRef<R, C>, BasePortalOutlet>;
176-
containerInstance: BasePortalOutlet & {
176+
readonly containerInstance: BasePortalOutlet & {
177177
_closeInteractionType?: FocusOrigin;
178178
};
179179
disableClose: boolean | undefined;
@@ -184,7 +184,7 @@ export class DialogRef<R = unknown, C = unknown> {
184184
readonly overlayRef: OverlayRef;
185185
removePanelClass(classes: string | string[]): this;
186186
updatePosition(): this;
187-
updateSize(width?: string, height?: string): this;
187+
updateSize(width?: string | number, height?: string | number): this;
188188
}
189189

190190
// @public

0 commit comments

Comments
 (0)