Skip to content

Commit d4f9d3e

Browse files
committed
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.
1 parent c5c994b commit d4f9d3e

File tree

6 files changed

+27
-59
lines changed

6 files changed

+27
-59
lines changed

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

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2005,13 +2005,6 @@ describe('MDC-based MatDialog with animations enabled', () => {
20052005
flush();
20062006
expect(dialogRef.getState()).toBe(MatDialogState.CLOSED);
20072007
}));
2008-
2009-
it("should return the previous dialogRef if the previous dialog hasn't finished animating open", () => {
2010-
let dialogRef1: MatDialogRef<PizzaMsg>, dialogRef2: MatDialogRef<PizzaMsg>;
2011-
dialogRef1 = dialog.open(PizzaMsg);
2012-
dialogRef2 = dialog.open(PizzaMsg);
2013-
expect(dialogRef1).toEqual(dialogRef2);
2014-
});
20152008
});
20162009

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

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

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

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,15 @@ 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+
}
120+
121+
this._trapFocus();
117122
}
118123

119124
/**
@@ -270,18 +275,6 @@ export abstract class _MatDialogContainerBase extends BasePortalOutlet {
270275
}
271276
}
272277

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-
285278
/** Focuses the dialog container. */
286279
private _focusDialogContainer() {
287280
// Note that there is no focus method when rendering on the server.
@@ -333,7 +326,6 @@ export class MatDialogContainer extends _MatDialogContainerBase {
333326
/** Callback, invoked whenever an animation on the host completes. */
334327
_onAnimationDone({toState, totalTime}: AnimationEvent) {
335328
if (toState === 'enter') {
336-
this._trapFocus();
337329
this._animationStateChanged.next({state: 'opened', totalTime});
338330
} else if (toState === 'exit') {
339331
this._restoreFocus();

src/material/dialog/dialog.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1388,7 +1388,7 @@ describe('MatDialog', () => {
13881388

13891389
tick(500);
13901390
viewContainerFixture.detectChanges();
1391-
expect(lastFocusOrigin!).toBe('program');
1391+
expect(lastFocusOrigin).toBe(null);
13921392

13931393
dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);
13941394

@@ -1421,7 +1421,7 @@ describe('MatDialog', () => {
14211421

14221422
tick(500);
14231423
viewContainerFixture.detectChanges();
1424-
expect(lastFocusOrigin!).toBe('program');
1424+
expect(lastFocusOrigin).toBe(null);
14251425

14261426
const backdrop = overlayContainerElement.querySelector(
14271427
'.cdk-overlay-backdrop',

src/material/dialog/dialog.ts

Lines changed: 10 additions & 33 deletions
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
/**
@@ -463,6 +436,10 @@ export class MatDialog extends _MatDialogBase<MatDialogContainer> {
463436
@Inject(MAT_DIALOG_SCROLL_STRATEGY) scrollStrategy: any,
464437
@Optional() @SkipSelf() parentDialog: MatDialog,
465438
overlayContainer: OverlayContainer,
439+
/**
440+
* @deprecated No longer used. To be removed.
441+
* @breaking-change 14.0.0
442+
*/
466443
@Optional()
467444
@Inject(ANIMATION_MODULE_TYPE)
468445
animationMode?: 'NoopAnimations' | 'BrowserAnimations',

tools/public_api_guard/material/dialog.md

Lines changed: 4 additions & 2 deletions
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)
@@ -109,7 +110,8 @@ export const matDialogAnimations: {
109110

110111
// @public
111112
export abstract class _MatDialogBase<C extends _MatDialogContainerBase> implements OnDestroy {
112-
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);
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>,
114+
_animationMode?: 'NoopAnimations' | 'BrowserAnimations');
113115
readonly afterAllClosed: Observable<void>;
114116
get afterOpened(): Subject<MatDialogRef<any>>;
115117
closeAll(): void;

0 commit comments

Comments
 (0)