Missing Automatic Focus Restoration in Angular CDK Modal Dialogs
0 reputation · 23 Aug 2024, 10:30 UTC
0 reputation · 23 Aug 2024, 10:30 UTC
Angular’s CDK focus-trap directive confines keyboard focus inside a modal dialog. However, when the dialog is dismissed, the directive does not return focus to the element that originally triggered the dialog. This omission can disorient keyboard and screen‑reader users, breaking the expected navigation flow.
Developers currently capture the active element before opening the dialog and manually restore it afterward. This pattern adds boilerplate to every modal implementation and may fail in edge cases such as asynchronous openings or dynamic content that shifts focus targets.
The Angular team has discussed a potential restoreFocus option for the CDK or Material dialog API, but no definitive release or version has been announced. Until such an option is available, reliable focus restoration remains an open problem.
1. Which strategy can reliably restore focus after a dialog is closed without duplicating code across components?
2. Does creating a reusable directive that leverages the CDK’s focus-trap provide a clean, maintainable solution?
3. When might Angular introduce a restoreFocus feature, and how should developers prepare for potential API changes?
34775 reputation · 23 Aug 2024, 19:16 UTC
To restore focus automatically after a CDK dialog closes without duplicating code, wrap the dialog‑open logic in a single, reusable service (or directive). The service records the element that was focused before the dialog opens and restores focus in the dialog’s afterClosed() observable. This eliminates boilerplate in every component and works for both synchronous and asynchronous dialog content.
focus-trap directive confines focus but does not provide a built‑in restore mechanism.MatDialog exposes an afterClosed() observable, but no restoreFocus flag exists as of Angular 17.document.activeElement before opening a dialog and restores it afterward is the most maintainable pattern.*ngIf or ngTemplateOutlet), the restoration must wait until the dialog’s view is fully rendered to avoid race conditions.restoreFocus option in the CDK or Material API; monitoring the Angular GitHub issue tracker is recommended.afterClosed() but lack a native restoreFocus flag.focus() to avoid runtime errors.import { Injectable } from '@angular/core';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
@Injectable({ providedIn: 'root' })
export class FocusRestoreService {
constructor(private dialog: MatDialog) {}
open(component: any, config?: any): MatDialogRef {
const previous = document.activeElement as HTMLElement | null;
const dialogRef = this.dialog.open(component, config);
dialogRef.afterClosed().subscribe(() => {
if (previous && previous.focus) {
// If the element has been removed, fallback to body
if (document.contains(previous)) {
previous.focus();
} else {
document.body.focus();
}
}
});
return dialogRef;
}
}
import { FocusRestoreService } from './focus-restore.service';
@Component({ /* … */ })
export class ExampleComponent {
constructor(private focusService: FocusRestoreService) {}
openDialog() {
this.focusService.open(ExampleDialogComponent, {
width: '400px',
// other MatDialogConfig options
});
}
}
@Directive({ selector: '[appFocusRestore]' })
export class FocusRestoreDirective {
constructor(private el: ElementRef, private focusService: FocusRestoreService) {}
@HostListener('click')
onClick() {
// The directive assumes the element that triggers the dialog
// is the host element. The dialog should be opened via the
// FocusRestoreService to keep the logic in one place.
}
}
document.activeElement in the console.console.log('restored', previous); inside the afterClosed subscription to confirm the callback runs.*ngIf or ngTemplateOutlet) to ensure focus is restored only after the view is ready.restoreFocus flag is discussed but not yet released.If your dialog is opened asynchronously (e.g., the component is loaded via ngIf or ngTemplateOutlet), let us know so we can adjust the timing of the restoration logic accordingly.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.