bug(material/menu): menu restores focus to its trigger after a menu item opens a dialog (animations disabled / prefers-reduced-motion)
Maintainer antworten meist innerhalb von 1 Tag
Dieses Issue hat noch niemand übernommen.
- #33989 von @MeAkib — ohne Merge geschlossen
Bewertung
- Schwierigkeit
- 3/5
- Geschätzter Aufwand
- 1-2 Tage
- Anfängerfreundlichkeit
- 45/100
- Issue-Typ
- Bug
- Klarheit
- Klar beschrieben
- Aktivitätsstatus
- Aktiv
- Tech-Stack
- typescript
- Bereich
- accessibility, frontend
Rechercherichtung
The restore logic is in _destroyMenu in src/material/menu/menu-trigger-base.ts, which calls this.focus(this._openedBy) after a closed('click') event without checking where focus currently is. Read how the dialog's FocusTrap sets initial focus, then reproduce with animations disabled to confirm focus lands on the trigger. Done means focus stays on the dialog's cdkFocusInitial button after a menu item opens it, while Escape, Tab and backdrop closes still restore focus to the trigger. Check that the maintainers accept the proposed guard before writing it.
Vom Indexierungsmodell aus dem Issue-Text verfasst.
Beschreibung
Is this a regression?
- Yes, this behavior used to work in the previous version
The previous version in which this bug was not present was
No response
Description
When a mat-menu item opens a MatDialog and animations are disabled (provideNoopAnimations / animations disabled, or the user's OS has reduced motion so prefers-reduced-motion: reduce matches), the dialog ends up with no focus: focus is moved back to the menu trigger behind the dialog's backdrop.
Cause: with animations disabled the dialog's initial focus runs synchronously during the menu item's click (in the microtask after the click listener). The click then reaches the menu panel, which emits closed('click'), and MatMenuTriggerBase._destroyMenu calls this.focus(this._openedBy) unconditionally, without checking where focus currently is. With animations enabled the dialog focuses ~150ms later, after the menu has closed, so the problem doesn't show.
Since prefers-reduced-motion comes from an OS setting (e.g. Windows "Animation effects" off), regular users hit this, and keyboard and screen-reader users land on a control hidden behind the modal.
This was reported in #14879, which was closed as a duplicate of #11696 before the reporter clarified it only happens with NoopAnimationsModule; it was then locked. The menu code on main still restores focus unconditionally.
Possible fix, following the dialog's own guard from #17300 ("don't move focus if it was moved during close animation"): in _destroyMenu, when the close reason is 'click', skip restoring focus if it is already inside a different overlay pane (not the menu's own pane, not a menu panel such as a submenu, and not the overlay pane that contains the trigger itself). Every other close (Escape, Tab, backdrop click, programmatic close) keeps today's
behaviour. This doesn't change the accessibility position in #11696: focus still returns to the trigger whenever the menu owns it; it only stops the menu from pulling focus out of a modal that an item just opened. Happy to send a PR with tests if this direction is acceptable.
Reproduction
StackBlitz (from #14879, older Angular/Material):
https://stackblitz.com/edit/checkbox-reactive-2zhjpp?file=app%2Fapp.component.ts
I wasn't able to save a new StackBlitz, so this links the reproduction from #14879. The bug is not specific to that
old version:
-
It reproduces on Angular 21.2.25 / @angular/cdk + @angular/material 21.1.2 in an application with animations
disabled through the OS setting (Windows "Animation effects" off, soprefers-reduced-motion: reducematches and
Material disables its animations). After a menu item opens a MatDialog,document.activeElementis the menu trigger
behind the dialog's backdrop, not the dialog'scdkFocusInitialbutton. -
The cause is unchanged on
main:MatMenuTriggerBase._destroyMenuinsrc/material/menu/menu-trigger-base.ts
still ends withif (this.restoreFocus && (reason === 'keydown' || !this._openedBy || !this._triggersSubmenu())) {
this.focus(this._openedBy);
}with no check of where focus currently is, so whenever the dialog takes focus before the menu finishes closing
(always the case with animations disabled), the menu takes it back.
Steps (old StackBlitz, which uses NoopAnimations):
- Open the StackBlitz and click the menu trigger.
- Click the menu item that opens the dialog.
- In the console, run
document.activeElement: it is the menu trigger, not the dialog's button.
Single-file reproduction for current versions (Angular/Material 21; paste as main.ts in a new Angular project):
import {Component, inject} from '@angular/core';
import {bootstrapApplication} from '@angular/platform-browser';
import {MatButtonModule} from '@angular/material/button';
import {MatMenuModule} from '@angular/material/menu';
import {MatDialog, MatDialogModule} from '@angular/material/dialog';
@Component({
imports: [MatButtonModule, MatDialogModule],
template: <mat-dialog-content>Delete this item?</mat-dialog-content> <mat-dialog-actions> <button mat-button mat-dialog-close cdkFocusInitial>Cancel</button> <button mat-button [mat-dialog-close]="true">Delete</button> </mat-dialog-actions>,
})
export class ConfirmDialog {}
@Component({
selector: 'app-root',
imports: [MatButtonModule, MatMenuModule, MatDialogModule],
template: <button mat-button [matMenuTriggerFor]="menu">Menu</button> <mat-menu #menu="matMenu"> <button mat-menu-item (click)="open()">Open dialog</button> </mat-menu>,
})
export class App {
private dialog = inject(MatDialog);
open() { this.dialog.open(ConfirmDialog); }
}
bootstrapApplication(App);
On 21, emulate reduced motion (DevTools > Rendering > "Emulate CSS media feature prefers-reduced-motion: reduce", then
reload) or set the OS animation setting off, then click Menu > Open dialog and check document.activeElement.
Expected Behavior
Focus stays on the dialog's initial focus target (the "Cancel" button with cdkFocusInitial), showing its focus indicator, exactly as it does when animations are enabled. The menu should not move focus back to its trigger while an overlay opened by the menu item holds focus.
Actual Behavior
The dialog opens, but focus is on the menu trigger ("Menu") behind the dialog's backdrop: document.activeElement is the trigger button with cdk-focused / cdk-mouse-focused. The dialog shows no focused control, Enter re-activates the hidden trigger, and Tab starts from outside the dialog. The dialog's FocusTrap did focus "Cancel" first; the menu's _destroyMenu then called focus() on the trigger.
document.activeElement after the dialog opens: button.mat-mdc-menu-trigger (the menu trigger, behind the backdrop)
Environment
- Angular: 21.2.25
- CDK/Material: 21.1.2 (the same unconditional restore is still in src/material/menu/menu-trigger-base.ts on main)
- Browser(s): Chrome ; also reproduced in an embedded Chromium. Any browser with animations disabled or
prefers-reduced-motion: reduce. - Operating System: Windows 11 (Settings > Accessibility > Visual effects > Animation effects: Off)
- Vorherrschende Sprache
- TypeScript
- Sterne
- 25k
- Forks
- 6.8k
- Ø Merge
- 2 T. 18 Std.
- Gemergte PRs (30 T.)
- 81
Entwicklungsumgebung
- Kein Dockerfile und keine Docker-Compose-Datei
- Keine Pull-Request-Vorlage
- Beitragsleitfaden lesen
Erste Schritte
- Lesen Sie das ganze Issue und danach den Beitragsleitfaden des Projekts.
- Schreiben Sie ins Issue, dass Sie es übernehmen — das erspart doppelte Arbeit.
- Forken Sie das Repository und arbeiten Sie in einem Branch.
- Öffnen Sie einen Pull Request, der die Issue-Nummer nennt.
Mehr aus angular/components
-
area: cdk/a11y gemini-triaged needs triage
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 67/100
angular/components#33999 ·
Maintainer antworten meist innerhalb von 1 Tag
-
bug(aria/combobox): keys pressed back to back are lost; only the last one reaches the popupEvtl. vergeben @drakeo338 hat das vor 3 Tagen übernommen. Offenarea: aria/combobox gemini-triaged
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 78/100
angular/components#33973 ·
Maintainer antworten meist innerhalb von 1 Tag
-
bug(material/sort): hover hint arrow on an unsorted header shows the active sort direction, not the resulting direction after click.Evtl. vergeben @lekhmanrus hat das vor 15 Tagen übernommen. Offenarea: material/sort P3
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 70/100
angular/components#33933 ·
Maintainer antworten meist innerhalb von 1 Tag
-
bug(mat-date-picker): month shown twice in date pickerEvtl. vergeben @PreethiPantangi hat das vor 32 Tagen übernommen. Offenarea: material/datepicker P4
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 72/100
angular/components#33763 · 2 Kommentare · 3 Reaktionen ·
Maintainer antworten meist innerhalb von 1 Tag
-
Bug in table.scss footer cell height: unset instead of footer row height unsetEvtl. vergeben @minwookshin hat das vor 19 Tagen übernommen. Offenarea: material/table P4
Schwierigkeit 1/5 Unter einer Stunde Anfängerfreundlichkeit 88/100
angular/components#33709 · 1 Kommentar ·
Maintainer antworten meist innerhalb von 1 Tag
Alle Issues in angular/components
Ähnliche Issues
-
level/task type/bug
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 75/100
wazuh/wazuh-dashboard-plugins#9508 ·
Maintainer antworten meist innerhalb von 1 Tag
-
Testnet EOL page links Zonescan mainnetEvtl. vergeben @napelvs hat das heute übernommen. Offen
Schwierigkeit 1/5 Unter einer Stunde Anfängerfreundlichkeit 76/100
-
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 78/100
Azure-Samples/azure-search-openai-demo#3249 ·
Maintainer antworten meist innerhalb von 1 Tag
-
[Docs] docs/architecture.mermaid leaves out 21 of the 51 src/ directoriesEvtl. vergeben @uyg7x hat das heute übernommen. Offen
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 75/100
Maintainer antworten meist innerhalb von 1 Tag
-
enhancement status:needs-triage
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 68/100
dongdongbh/Mindwtr#1438 ·
Maintainer antworten meist innerhalb von 1 Tag