Skip to content

Commit 92863cc

Browse files
authored
fix(material/dialog): don't wait for animation before moving focus (#24121)
* fix(material/dialog): don't wait for animation before moving focus For a long time we used to move focus into the dialog after the opening animation was finished, because it allowed for the content to settle in case something like an autocomplete needed to open. This approach has caused us accessibility issues in the past, because we've had to find ways to prevent the user from interacting with content behind the dialog while the opening animation is running. Initially we used to move focus to the dialog container before transferring it into the dialog, but that ended up interrupting the screen reader. In a later change we started returning the previous dialog ref if the same kind of dialog is opened while the animation is still running. The problem with this approach is that it doesn't allow for multiple dialog to be opened quickly (see #24037). These changes aim to address the root cause by moving focus immediately after the dialog content is attached. If this causes regressions with autocompletes inside dialogs, we now have APIs that allow users to customize the focus behavior. Fixes #24037. * fixup! fix(material/dialog): don't wait for animation before moving focus
1 parent a5ab8e9 commit 92863cc

File tree

6 files changed

+41
-57
lines changed

6 files changed

+41
-57
lines changed

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

-7
Original file line numberDiff line numberDiff line change
@@ -2016,13 +2016,6 @@ describe('MDC-based MatDialog with animations enabled', () => {
20162016
flush();
20172017
expect(dialogRef.getState()).toBe(MatDialogState.CLOSED);
20182018
}));
2019-
2020-
it("should return the previous dialogRef if the previous dialog hasn't finished animating open", () => {
2021-
let dialogRef1: MatDialogRef<PizzaMsg>, dialogRef2: MatDialogRef<PizzaMsg>;
2022-
dialogRef1 = dialog.open(PizzaMsg);
2023-
dialogRef2 = dialog.open(PizzaMsg);
2024-
expect(dialogRef1).toEqual(dialogRef2);
2025-
});
20262019
});
20272020

20282021
@Directive({selector: 'dir-with-view-container'})

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

+4
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ export class MatDialog extends _MatDialogBase<MatDialogContainer> {
5858
@Inject(MAT_DIALOG_SCROLL_STRATEGY) scrollStrategy: any,
5959
@Optional() @SkipSelf() parentDialog: MatDialog,
6060
overlayContainer: OverlayContainer,
61+
/**
62+
* @deprecated No longer used. To be removed.
63+
* @breaking-change 14.0.0
64+
*/
6165
@Optional()
6266
@Inject(ANIMATION_MODULE_TYPE)
6367
animationMode?: 'NoopAnimations' | 'BrowserAnimations',

src/material/dialog/dialog-config.ts

+3
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,9 @@ export class MatDialogConfig<D = any> {
110110
*/
111111
restoreFocus?: boolean = true;
112112

113+
/** Whether to wait for the opening animation to finish before trapping focus. */
114+
delayFocusTrap?: boolean = true;
115+
113116
/** Scroll strategy to be used for the dialog. */
114117
scrollStrategy?: ScrollStrategy;
115118

src/material/dialog/dialog-container.ts

+17-15
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,13 @@ export abstract class _MatDialogContainerBase extends BasePortalOutlet {
110110

111111
/** Initializes the dialog container with the attached content. */
112112
_initializeWithAttachedContent() {
113-
this._setupFocusTrap();
113+
this._focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement);
114+
114115
// Save the previously focused element. This element will be re-focused
115116
// when the dialog closes.
116-
this._capturePreviouslyFocusedElement();
117+
if (this._document) {
118+
this._elementFocusedBeforeDialogWasOpened = _getFocusedElementPierceShadowDom();
119+
}
117120
}
118121

119122
/**
@@ -270,18 +273,6 @@ export abstract class _MatDialogContainerBase extends BasePortalOutlet {
270273
}
271274
}
272275

273-
/** Sets up the focus trap. */
274-
private _setupFocusTrap() {
275-
this._focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement);
276-
}
277-
278-
/** Captures the element that was focused before the dialog was opened. */
279-
private _capturePreviouslyFocusedElement() {
280-
if (this._document) {
281-
this._elementFocusedBeforeDialogWasOpened = _getFocusedElementPierceShadowDom();
282-
}
283-
}
284-
285276
/** Focuses the dialog container. */
286277
private _focusDialogContainer() {
287278
// Note that there is no focus method when rendering on the server.
@@ -333,7 +324,10 @@ export class MatDialogContainer extends _MatDialogContainerBase {
333324
/** Callback, invoked whenever an animation on the host completes. */
334325
_onAnimationDone({toState, totalTime}: AnimationEvent) {
335326
if (toState === 'enter') {
336-
this._trapFocus();
327+
if (this._config.delayFocusTrap) {
328+
this._trapFocus();
329+
}
330+
337331
this._animationStateChanged.next({state: 'opened', totalTime});
338332
} else if (toState === 'exit') {
339333
this._restoreFocus();
@@ -358,4 +352,12 @@ export class MatDialogContainer extends _MatDialogContainerBase {
358352
// view container is using OnPush change detection.
359353
this._changeDetectorRef.markForCheck();
360354
}
355+
356+
override _initializeWithAttachedContent() {
357+
super._initializeWithAttachedContent();
358+
359+
if (!this._config.delayFocusTrap) {
360+
this._trapFocus();
361+
}
362+
}
361363
}

src/material/dialog/dialog.ts

+10-33
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import {
3030
TemplateRef,
3131
Type,
3232
} from '@angular/core';
33-
import {defer, Observable, of as observableOf, Subject, Subscription} from 'rxjs';
33+
import {defer, Observable, of as observableOf, Subject} from 'rxjs';
3434
import {startWith} from 'rxjs/operators';
3535
import {MatDialogConfig} from './dialog-config';
3636
import {MatDialogContainer, _MatDialogContainerBase} from './dialog-container';
@@ -80,9 +80,6 @@ export abstract class _MatDialogBase<C extends _MatDialogContainerBase> implemen
8080
private readonly _afterOpenedAtThisLevel = new Subject<MatDialogRef<any>>();
8181
private _ariaHiddenElements = new Map<Element, string | null>();
8282
private _scrollStrategy: () => ScrollStrategy;
83-
private _dialogAnimatingOpen = false;
84-
private _animationStateSubscriptions: Subscription;
85-
private _lastDialogRef: MatDialogRef<any>;
8683

8784
/** Keeps track of the currently-open dialogs. */
8885
get openDialogs(): MatDialogRef<any>[] {
@@ -120,7 +117,11 @@ export abstract class _MatDialogBase<C extends _MatDialogContainerBase> implemen
120117
private _dialogRefConstructor: Type<MatDialogRef<any>>,
121118
private _dialogContainerType: Type<C>,
122119
private _dialogDataToken: InjectionToken<any>,
123-
private _animationMode?: 'NoopAnimations' | 'BrowserAnimations',
120+
/**
121+
* @deprecated No longer used. To be removed.
122+
* @breaking-change 14.0.0
123+
*/
124+
_animationMode?: 'NoopAnimations' | 'BrowserAnimations',
124125
) {
125126
this._scrollStrategy = scrollStrategy;
126127
}
@@ -166,38 +167,14 @@ export abstract class _MatDialogBase<C extends _MatDialogContainerBase> implemen
166167
throw Error(`Dialog with id "${config.id}" exists already. The dialog id must be unique.`);
167168
}
168169

169-
// If there is a dialog that is currently animating open, return the MatDialogRef of that dialog
170-
if (this._dialogAnimatingOpen) {
171-
return this._lastDialogRef;
172-
}
173-
174170
const overlayRef = this._createOverlay(config);
175171
const dialogContainer = this._attachDialogContainer(overlayRef, config);
176-
if (this._animationMode !== 'NoopAnimations') {
177-
const animationStateSubscription = dialogContainer._animationStateChanged.subscribe(
178-
dialogAnimationEvent => {
179-
if (dialogAnimationEvent.state === 'opening') {
180-
this._dialogAnimatingOpen = true;
181-
}
182-
if (dialogAnimationEvent.state === 'opened') {
183-
this._dialogAnimatingOpen = false;
184-
animationStateSubscription.unsubscribe();
185-
}
186-
},
187-
);
188-
if (!this._animationStateSubscriptions) {
189-
this._animationStateSubscriptions = new Subscription();
190-
}
191-
this._animationStateSubscriptions.add(animationStateSubscription);
192-
}
193-
194172
const dialogRef = this._attachDialogContent<T, R>(
195173
componentOrTemplateRef,
196174
dialogContainer,
197175
overlayRef,
198176
config,
199177
);
200-
this._lastDialogRef = dialogRef;
201178

202179
// If this is the first dialog that we're opening, hide all the non-overlay content.
203180
if (!this.openDialogs.length) {
@@ -235,10 +212,6 @@ export abstract class _MatDialogBase<C extends _MatDialogContainerBase> implemen
235212
this._closeDialogs(this._openDialogsAtThisLevel);
236213
this._afterAllClosedAtThisLevel.complete();
237214
this._afterOpenedAtThisLevel.complete();
238-
// Clean up any subscriptions to dialogs that never finished opening.
239-
if (this._animationStateSubscriptions) {
240-
this._animationStateSubscriptions.unsubscribe();
241-
}
242215
}
243216

244217
/**
@@ -468,6 +441,10 @@ export class MatDialog extends _MatDialogBase<MatDialogContainer> {
468441
@Inject(MAT_DIALOG_SCROLL_STRATEGY) scrollStrategy: any,
469442
@Optional() @SkipSelf() parentDialog: MatDialog,
470443
overlayContainer: OverlayContainer,
444+
/**
445+
* @deprecated No longer used. To be removed.
446+
* @breaking-change 14.0.0
447+
*/
471448
@Optional()
472449
@Inject(ANIMATION_MODULE_TYPE)
473450
animationMode?: 'NoopAnimations' | 'BrowserAnimations',

tools/public_api_guard/material/dialog.md

+7-2
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ export function MAT_DIALOG_SCROLL_STRATEGY_PROVIDER_FACTORY(overlay: Overlay): (
8787
// @public
8888
export class MatDialog extends _MatDialogBase<MatDialogContainer> {
8989
constructor(overlay: Overlay, injector: Injector,
90-
location: Location_2, defaultOptions: MatDialogConfig, scrollStrategy: any, parentDialog: MatDialog, overlayContainer: OverlayContainer, animationMode?: 'NoopAnimations' | 'BrowserAnimations');
90+
location: Location_2, defaultOptions: MatDialogConfig, scrollStrategy: any, parentDialog: MatDialog, overlayContainer: OverlayContainer,
91+
animationMode?: 'NoopAnimations' | 'BrowserAnimations');
9192
// (undocumented)
9293
static ɵfac: i0.ɵɵFactoryDeclaration<MatDialog, [null, null, { optional: true; }, { optional: true; }, null, { optional: true; skipSelf: true; }, null, { optional: true; }]>;
9394
// (undocumented)
@@ -110,7 +111,8 @@ export const matDialogAnimations: {
110111

111112
// @public
112113
export abstract class _MatDialogBase<C extends _MatDialogContainerBase> implements OnDestroy {
113-
constructor(_overlay: Overlay, _injector: Injector, _defaultOptions: MatDialogConfig | undefined, _parentDialog: _MatDialogBase<C> | undefined, _overlayContainer: OverlayContainer, scrollStrategy: any, _dialogRefConstructor: Type<MatDialogRef<any>>, _dialogContainerType: Type<C>, _dialogDataToken: InjectionToken<any>, _animationMode?: "NoopAnimations" | "BrowserAnimations" | undefined);
114+
constructor(_overlay: Overlay, _injector: Injector, _defaultOptions: MatDialogConfig | undefined, _parentDialog: _MatDialogBase<C> | undefined, _overlayContainer: OverlayContainer, scrollStrategy: any, _dialogRefConstructor: Type<MatDialogRef<any>>, _dialogContainerType: Type<C>, _dialogDataToken: InjectionToken<any>,
115+
_animationMode?: 'NoopAnimations' | 'BrowserAnimations');
114116
readonly afterAllClosed: Observable<void>;
115117
get afterOpened(): Subject<MatDialogRef<any>>;
116118
closeAll(): void;
@@ -163,6 +165,7 @@ export class MatDialogConfig<D = any> {
163165
closeOnNavigation?: boolean;
164166
componentFactoryResolver?: ComponentFactoryResolver;
165167
data?: D | null;
168+
delayFocusTrap?: boolean;
166169
direction?: Direction;
167170
disableClose?: boolean;
168171
hasBackdrop?: boolean;
@@ -183,6 +186,8 @@ export class MatDialogConfig<D = any> {
183186

184187
// @public
185188
export class MatDialogContainer extends _MatDialogContainerBase {
189+
// (undocumented)
190+
_initializeWithAttachedContent(): void;
186191
_onAnimationDone({ toState, totalTime }: AnimationEvent_2): void;
187192
_onAnimationStart({ toState, totalTime }: AnimationEvent_2): void;
188193
_startExitAnimation(): void;

0 commit comments

Comments
 (0)