Skip to content

Commit

Permalink
fix(material/dialog): Make autofocus work with animations disabled (#…
Browse files Browse the repository at this point in the history
…29195)

(cherry picked from commit e731203)
  • Loading branch information
mmalerba committed Jun 6, 2024
1 parent 9fe4b0f commit f6b993f
Show file tree
Hide file tree
Showing 3 changed files with 40 additions and 18 deletions.
39 changes: 33 additions & 6 deletions src/cdk/dialog/dialog-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,13 @@ import {
ElementRef,
EmbeddedViewRef,
Inject,
Injector,
NgZone,
OnDestroy,
Optional,
ViewChild,
ViewEncapsulation,
afterNextRender,
inject,
} from '@angular/core';
import {DialogConfig} from './dialog-config';
Expand Down Expand Up @@ -102,6 +104,10 @@ export class CdkDialogContainer<C extends DialogConfig = DialogConfig>

protected readonly _changeDetectorRef = inject(ChangeDetectorRef);

private _injector = inject(Injector);

private _isDestroyed = false;

constructor(
protected _elementRef: ElementRef,
protected _focusTrapFactory: FocusTrapFactory,
Expand Down Expand Up @@ -150,6 +156,7 @@ export class CdkDialogContainer<C extends DialogConfig = DialogConfig>
}

ngOnDestroy() {
this._isDestroyed = true;
this._restoreFocus();
}

Expand Down Expand Up @@ -246,23 +253,33 @@ export class CdkDialogContainer<C extends DialogConfig = DialogConfig>
* cannot be moved then focus will go to the dialog container.
*/
protected _trapFocus() {
if (this._isDestroyed) {
return;
}

const element = this._elementRef.nativeElement;
// If were to attempt to focus immediately, then the content of the dialog would not yet be
// ready in instances where change detection has to run first. To deal with this, we simply
// wait for the microtask queue to be empty when setting focus when autoFocus isn't set to
// dialog. If the element inside the dialog can't be focused, then the container is focused
// so the user can't tab into other elements behind it.
switch (this._config.autoFocus) {
const autoFocus = this._config.autoFocus;
switch (autoFocus) {
case false:
case 'dialog':
// Ensure that focus is on the dialog container. It's possible that a different
// component tried to move focus while the open animation was running. See:
// https://github.com/angular/components/issues/16215. Note that we only want to do this
// if the focus isn't inside the dialog already, because it's possible that the consumer
// turned off `autoFocus` in order to move focus themselves.
if (!this._containsFocus()) {
element.focus();
}
afterNextRender(
() => {
if (!this._containsFocus()) {
element.focus();
}
},
{injector: this._injector},
);
break;
case true:
case 'first-tabbable':
Expand All @@ -275,10 +292,20 @@ export class CdkDialogContainer<C extends DialogConfig = DialogConfig>
});
break;
case 'first-heading':
this._focusByCssSelector('h1, h2, h3, h4, h5, h6, [role="heading"]');
afterNextRender(
() => {
this._focusByCssSelector('h1, h2, h3, h4, h5, h6, [role="heading"]');
},
{injector: this._injector},
);
break;
default:
this._focusByCssSelector(this._config.autoFocus!);
afterNextRender(
() => {
this._focusByCssSelector(autoFocus!);
},
{injector: this._injector},
);
break;
}
}
Expand Down
8 changes: 0 additions & 8 deletions src/material/dialog/dialog-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,6 @@ export class MatDialogContainer extends CdkDialogContainer<MatDialogConfig> impl
/** Current timer for dialog animations. */
private _animationTimer: ReturnType<typeof setTimeout> | null = null;

private _isDestroyed = false;

constructor(
elementRef: ElementRef,
focusTrapFactory: FocusTrapFactory,
Expand Down Expand Up @@ -264,10 +262,6 @@ export class MatDialogContainer extends CdkDialogContainer<MatDialogConfig> impl
* be called by sub-classes that use different animation implementations.
*/
protected _openAnimationDone(totalTime: number) {
if (this._isDestroyed) {
return;
}

if (this._config.delayFocusTrap) {
this._trapFocus();
}
Expand All @@ -281,8 +275,6 @@ export class MatDialogContainer extends CdkDialogContainer<MatDialogConfig> impl
if (this._animationTimer !== null) {
clearTimeout(this._animationTimer);
}

this._isDestroyed = true;
}

override attachComponentPortal<T>(portal: ComponentPortal<T>): ComponentRef<T> {
Expand Down
11 changes: 7 additions & 4 deletions src/material/dialog/dialog.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ import {
ViewEncapsulation,
createNgModuleRef,
forwardRef,
signal,
provideZoneChangeDetection,
signal,
} from '@angular/core';
import {
ComponentFixture,
Expand Down Expand Up @@ -1041,7 +1041,8 @@ describe('MDC-based MatDialog', () => {
});

viewContainerFixture.detectChanges();
flushMicrotasks();
flush();
viewContainerFixture.detectChanges();

let backdrop = overlayContainerElement.querySelector(
'.cdk-overlay-backdrop',
Expand Down Expand Up @@ -1209,7 +1210,8 @@ describe('MDC-based MatDialog', () => {
});

viewContainerFixture.detectChanges();
flushMicrotasks();
flush();
viewContainerFixture.detectChanges();

let container = overlayContainerElement.querySelector(
'.mat-mdc-dialog-container',
Expand Down Expand Up @@ -1245,7 +1247,8 @@ describe('MDC-based MatDialog', () => {
});

viewContainerFixture.detectChanges();
flushMicrotasks();
flush();
viewContainerFixture.detectChanges();

let firstParagraph = overlayContainerElement.querySelector(
'p[tabindex="-1"]',
Expand Down

0 comments on commit f6b993f

Please sign in to comment.