From 429eebc3b3eb4c4ece7dee70d18fa58fd6221ad0 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 14 May 2026 09:01:32 +0200 Subject: [PATCH] fix(material/sidenav): more robust reset logic for inert attribute In #33247 I added some logic to mark the drawer content as `inert` while it's closed. That logic is too brittle and it seems to fail to reset the attribute if the user calls `close` from the `backdropClick` callback. These changes switch to a more robust approach where we have have the reset logic in a single place and we always execute it, rather than trying to do it right when focus is moved in/out of the sidenav. --- goldens/material/sidenav/index.api.md | 4 +-- src/material/sidenav/drawer.ts | 35 +++++++++++++++------------ 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/goldens/material/sidenav/index.api.md b/goldens/material/sidenav/index.api.md index 4d8e1b24bbf4..ae94567df92f 100644 --- a/goldens/material/sidenav/index.api.md +++ b/goldens/material/sidenav/index.api.md @@ -122,11 +122,11 @@ export class MatDrawerContent extends CdkScrollable implements AfterContentInit // (undocumented) _container: MatDrawerContainer; // (undocumented) + _drawerToggled(drawer: MatDrawer): void; + // (undocumented) ngAfterContentInit(): void; protected _shouldBeHidden(): boolean; // (undocumented) - _updateInert(): void; - // (undocumented) static ɵcmp: i0.ɵɵComponentDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; diff --git a/src/material/sidenav/drawer.ts b/src/material/sidenav/drawer.ts index f25391abca95..a96a65fa2f57 100644 --- a/src/material/sidenav/drawer.ts +++ b/src/material/sidenav/drawer.ts @@ -44,7 +44,7 @@ import { signal, } from '@angular/core'; import {merge, Observable, Subject} from 'rxjs'; -import {debounceTime, filter, map, mapTo, startWith, take, takeUntil} from 'rxjs/operators'; +import {debounceTime, delay, filter, map, mapTo, startWith, take, takeUntil} from 'rxjs/operators'; import {_animationsDisabled} from '../core'; /** @@ -100,6 +100,7 @@ export class MatDrawerContent extends CdkScrollable implements AfterContentInit private _platform = inject(Platform); private _changeDetectorRef = inject(ChangeDetectorRef); private _element = inject>(ElementRef); + private _ngZone = inject(NgZone); private _isInert = false; _container = inject(MatDrawerContainer); @@ -107,7 +108,22 @@ export class MatDrawerContent extends CdkScrollable implements AfterContentInit this._container._contentMarginChanges.subscribe(() => this._changeDetectorRef.markForCheck()); } - _updateInert() { + _drawerToggled(drawer: MatDrawer) { + if (drawer.opened) { + // If the drawer is being opened, we need to wait until the animation is done before marking + // the content is inert, because the drawer moves focus during the animation. We add a delay + // to be safe. + this._ngZone.runOutsideAngular(() => { + drawer._animationEnd.pipe(delay(50), take(1)).subscribe(() => this._updateInert()); + }); + } else { + // When the drawer is closing, we need to remove `inert` immediately so + // the elements that focus is being restored to can become focusable. + this._updateInert(); + } + } + + private _updateInert() { const newValue = this._container._isShowingBackdrop(); if (newValue !== this._isInert) { @@ -433,38 +449,24 @@ export class MatDrawer implements AfterViewInit, OnDestroy { if (!hasMovedFocus && typeof element.focus === 'function') { element.focus(); } - - // When capturing focus, we need to delay making the - // container inert until focus has actually been moved. - this._notifyContentFocus(); }, {injector: this._injector}, ); break; case 'first-heading': this._focusByCssSelector('h1, h2, h3, h4, h5, h6, [role="heading"]'); - this._notifyContentFocus(); break; default: this._focusByCssSelector(this.autoFocus!); - this._notifyContentFocus(); break; } } - private _notifyContentFocus() { - (this._container?._content || this._container?._userContent)?._updateInert(); - } - /** * Restores focus to the element that was originally focused when the drawer opened. * If no element was focused at that time, the focus will be restored to the drawer. */ private _restoreFocus(focusOrigin: Exclude) { - // When restoring focus, we need remove `inert` as early as possible, - // because the element needs to become focusable before we can focus it. - this._notifyContentFocus(); - if (this.autoFocus === 'dialog') { return; } @@ -577,6 +579,7 @@ export class MatDrawer implements AfterViewInit, OnDestroy { } this._opened.set(isOpen); + (this._container?._content || this._container?._userContent)?._drawerToggled(this); if (this._container?._transitionsEnabled) { // Note: it's important to set this as early as possible,