From 295af749a5a02cf00d90d96f97f2d2c32daf467e Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Tue, 12 May 2026 10:03:48 +0200 Subject: [PATCH 1/2] fix(material/sidenav): query not resolving Fixes that a content query for `MatDrawerContent` wasn't resolving `MatSidenavContent` in the sidenav container. --- src/material/sidenav/sidenav.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/material/sidenav/sidenav.ts b/src/material/sidenav/sidenav.ts index 29d3b43fb575..dc7b69e70ac0 100644 --- a/src/material/sidenav/sidenav.ts +++ b/src/material/sidenav/sidenav.ts @@ -35,6 +35,10 @@ import {CdkScrollable} from '@angular/cdk/scrolling'; provide: CdkScrollable, useExisting: MatSidenavContent, }, + { + provide: MatDrawerContent, + useExisting: MatSidenavContent, + }, ], }) export class MatSidenavContent extends MatDrawerContent {} From e1148e9670f09cd48f174ec78f30a7ba04560a14 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Tue, 12 May 2026 10:07:25 +0200 Subject: [PATCH 2/2] fix(material/sidenav): mark content as inert while open Even though we trap focus within the sidenav, it's still possible for users' focus to escape and go into the content that's hidden behind it. These changes mark the content as `inert` while a sidenav is open. Fixes #32805. --- goldens/material/sidenav/index.api.md | 2 + src/material/sidenav/drawer.spec.ts | 34 ++++++++++++++ src/material/sidenav/drawer.ts | 67 ++++++++++++++++++--------- 3 files changed, 82 insertions(+), 21 deletions(-) diff --git a/goldens/material/sidenav/index.api.md b/goldens/material/sidenav/index.api.md index e36372526a06..4d8e1b24bbf4 100644 --- a/goldens/material/sidenav/index.api.md +++ b/goldens/material/sidenav/index.api.md @@ -125,6 +125,8 @@ export class MatDrawerContent extends CdkScrollable implements AfterContentInit 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.spec.ts b/src/material/sidenav/drawer.spec.ts index b0e091e328b8..168993d70d81 100644 --- a/src/material/sidenav/drawer.spec.ts +++ b/src/material/sidenav/drawer.spec.ts @@ -717,6 +717,40 @@ describe('MatDrawer', () => { expect(anchors.length).toBeGreaterThan(0); expect(anchors.every(anchor => !anchor.hasAttribute('tabindex'))).toBe(true); }); + + it('should mark the content as `inert` in `over` mode', async () => { + testComponent.mode = 'over'; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + lastFocusableElement.focus(); + + const content = fixture.nativeElement.querySelector('.mat-drawer-content'); + expect(content.hasAttribute('inert')).toBe(false); + + drawer.open(); + fixture.detectChanges(); + await wait(100); + fixture.detectChanges(); + + expect(content.getAttribute('inert')).toBe('true'); + }); + + it('should not mark the content as `inert` in `side` mode', async () => { + testComponent.mode = 'side'; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + lastFocusableElement.focus(); + + const content = fixture.nativeElement.querySelector('.mat-drawer-content'); + expect(content.hasAttribute('inert')).toBe(false); + + drawer.open(); + fixture.detectChanges(); + await wait(100); + fixture.detectChanges(); + + expect(content.hasAttribute('inert')).toBe(false); + }); }); it('should mark the drawer content as scrollable', () => { diff --git a/src/material/sidenav/drawer.ts b/src/material/sidenav/drawer.ts index b8fb36fd6657..f25391abca95 100644 --- a/src/material/sidenav/drawer.ts +++ b/src/material/sidenav/drawer.ts @@ -99,12 +99,29 @@ export const MAT_DRAWER_CONTAINER = new InjectionToken('MAT_ export class MatDrawerContent extends CdkScrollable implements AfterContentInit { private _platform = inject(Platform); private _changeDetectorRef = inject(ChangeDetectorRef); + private _element = inject>(ElementRef); + private _isInert = false; _container = inject(MatDrawerContainer); ngAfterContentInit() { - this._container._contentMarginChanges.subscribe(() => { - this._changeDetectorRef.markForCheck(); - }); + this._container._contentMarginChanges.subscribe(() => this._changeDetectorRef.markForCheck()); + } + + _updateInert() { + const newValue = this._container._isShowingBackdrop(); + + if (newValue !== this._isInert) { + const element = this._element.nativeElement; + this._isInert = newValue; + + // This can be called right before we attempt to move focus. Set the value + // directly, instead of waiting on change detection, because the timing is tight. + if (newValue) { + element.setAttribute('inert', 'true'); + } else { + element.removeAttribute('inert'); + } + } } /** Determines whether the content element should be hidden from the user. */ @@ -360,11 +377,18 @@ export class MatDrawer implements AfterViewInit, OnDestroy { } /** - * Focuses the provided element. If the element is not focusable, it will add a tabIndex - * attribute to forcefully focus it. The attribute is removed after focus is moved. - * @param element The element to focus. + * Focuses the first element that matches the given selector within the focus trap. + * @param selector The CSS selector for the element to set focus to. */ - private _forceFocus(element: HTMLElement, options?: FocusOptions) { + private _focusByCssSelector(selector: string, options?: FocusOptions) { + const element = this._elementRef.nativeElement.querySelector(selector) as HTMLElement | null; + + if (!element) { + return; + } + + // If the element isn't focusable, force focus to it by + // setting a tabindex, focusing it and then clear it. if (!this._interactivityChecker.isFocusable(element)) { element.tabIndex = -1; // The tabindex attribute should be removed to avoid navigating to that element again @@ -379,20 +403,8 @@ export class MatDrawer implements AfterViewInit, OnDestroy { const cleanupMousedown = this._renderer.listen(element, 'mousedown', callback); }); } - element.focus(options); - } - /** - * Focuses the first element that matches the given selector within the focus trap. - * @param selector The CSS selector for the element to set focus to. - */ - private _focusByCssSelector(selector: string, options?: FocusOptions) { - let elementToFocus = this._elementRef.nativeElement.querySelector( - selector, - ) as HTMLElement | null; - if (elementToFocus) { - this._forceFocus(elementToFocus, options); - } + element.focus(options); } /** @@ -421,24 +433,38 @@ 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; } @@ -923,7 +949,6 @@ export class MatDrawerContainer implements AfterContentInit, DoCheck, OnDestroy * is properly hidden. */ private _watchDrawerToggle(drawer: MatDrawer): void { - // drawer._animationStarted.pipe(takeUntil(this._drawers.changes)).subscribe(() => { this.updateContentMargins(); this._changeDetectorRef.markForCheck();