diff --git a/src/components-examples/material/sidenav/index.ts b/src/components-examples/material/sidenav/index.ts
index 86e5e276dc64..a726006091c5 100644
--- a/src/components-examples/material/sidenav/index.ts
+++ b/src/components-examples/material/sidenav/index.ts
@@ -3,7 +3,7 @@ export {SidenavBackdropExample} from './sidenav-backdrop/sidenav-backdrop-exampl
export {SidenavDisableCloseExample} from './sidenav-disable-close/sidenav-disable-close-example';
export {SidenavDrawerOverviewExample} from './sidenav-drawer-overview/sidenav-drawer-overview-example';
export {SidenavFixedExample} from './sidenav-fixed/sidenav-fixed-example';
-export {SidenavModeExample} from './sidenav-mode/sidenav-mode-example';
+export {SidenavConfigurableFocusTrapExample} from './sidenav-configurable-focus-trap/sidenav-configurable-focus-trap-example';
export {SidenavOpenCloseExample} from './sidenav-open-close/sidenav-open-close-example';
export {SidenavOverviewExample} from './sidenav-overview/sidenav-overview-example';
export {SidenavPositionExample} from './sidenav-position/sidenav-position-example';
diff --git a/src/components-examples/material/sidenav/sidenav-configurable-focus-trap/sidenav-configurable-focus-trap-example.css b/src/components-examples/material/sidenav/sidenav-configurable-focus-trap/sidenav-configurable-focus-trap-example.css
new file mode 100644
index 000000000000..cd425d420f9c
--- /dev/null
+++ b/src/components-examples/material/sidenav/sidenav-configurable-focus-trap/sidenav-configurable-focus-trap-example.css
@@ -0,0 +1,14 @@
+.example-container {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+}
+
+.example-radio-group {
+ display: block;
+ border: 1px solid #555;
+ margin: 20px;
+ padding: 10px;
+}
diff --git a/src/components-examples/material/sidenav/sidenav-configurable-focus-trap/sidenav-configurable-focus-trap-example.html b/src/components-examples/material/sidenav/sidenav-configurable-focus-trap/sidenav-configurable-focus-trap-example.html
new file mode 100644
index 000000000000..e9542d34307c
--- /dev/null
+++ b/src/components-examples/material/sidenav/sidenav-configurable-focus-trap/sidenav-configurable-focus-trap-example.html
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Over
+ Side
+ Push
+
+
+
+ Default
+ true
+ false
+
+
+
+ Start
+ End
+
+
+
+
+
+
+
+
+
Please open on Stackblitz to see result
diff --git a/src/components-examples/material/sidenav/sidenav-configurable-focus-trap/sidenav-configurable-focus-trap-example.ts b/src/components-examples/material/sidenav/sidenav-configurable-focus-trap/sidenav-configurable-focus-trap-example.ts
new file mode 100644
index 000000000000..f397fcd2d04d
--- /dev/null
+++ b/src/components-examples/material/sidenav/sidenav-configurable-focus-trap/sidenav-configurable-focus-trap-example.ts
@@ -0,0 +1,31 @@
+import {Component} from '@angular/core';
+import {NgIf} from '@angular/common';
+import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
+import {MatDrawerMode, MatSidenavModule} from '@angular/material/sidenav';
+import {MatRadioModule} from '@angular/material/radio';
+import {MatButtonModule} from '@angular/material/button';
+import {ConfigurableFocusTrapFactory, FocusTrapFactory} from '@angular/cdk/a11y';
+
+/** @title Sidenav using injected ConfigurableFocusTrap */
+@Component({
+ selector: 'sidenav-configurable-focus-trap-example',
+ templateUrl: 'sidenav-configurable-focus-trap-example.html',
+ styleUrls: ['sidenav-configurable-focus-trap-example.css'],
+ standalone: true,
+ imports: [
+ NgIf,
+ MatSidenavModule,
+ MatButtonModule,
+ MatRadioModule,
+ FormsModule,
+ ReactiveFormsModule,
+ ],
+ providers: [{provide: FocusTrapFactory, useClass: ConfigurableFocusTrapFactory}],
+})
+export class SidenavConfigurableFocusTrapExample {
+ mode = new FormControl('over' as MatDrawerMode);
+ hasBackdrop = new FormControl(null as null | boolean);
+ position = new FormControl('start' as 'start' | 'end');
+
+ shouldRun = /(^|.)(stackblitz|webcontainer).(io|com)$/.test(window.location.host);
+}
diff --git a/src/material/sidenav/drawer.spec.ts b/src/material/sidenav/drawer.spec.ts
index 8ec61d14b97d..3758567e0f39 100644
--- a/src/material/sidenav/drawer.spec.ts
+++ b/src/material/sidenav/drawer.spec.ts
@@ -567,6 +567,19 @@ describe('MatDrawer', () => {
expect(document.activeElement).toBe(firstFocusableElement);
}));
+ it('should trap focus when opened in "side" mode if backdrop is explicitly enabled', fakeAsync(() => {
+ testComponent.mode = 'push';
+ testComponent.hasBackdrop = true;
+ fixture.detectChanges();
+ lastFocusableElement.focus();
+
+ drawer.open();
+ fixture.detectChanges();
+ tick();
+
+ expect(document.activeElement).toBe(firstFocusableElement);
+ }));
+
it('should not auto-focus by default when opened in "side" mode', fakeAsync(() => {
testComponent.mode = 'side';
fixture.detectChanges();
@@ -596,6 +609,23 @@ describe('MatDrawer', () => {
}),
);
+ it(
+ 'should auto-focus to first tabbable element when opened in "push" mode' +
+ 'when backdrop is enabled explicitly',
+ fakeAsync(() => {
+ testComponent.mode = 'push';
+ testComponent.hasBackdrop = true;
+ fixture.detectChanges();
+ lastFocusableElement.focus();
+
+ drawer.open();
+ fixture.detectChanges();
+ tick();
+
+ expect(document.activeElement).toBe(firstFocusableElement);
+ }),
+ );
+
it('should focus the drawer if there are no focusable elements', fakeAsync(() => {
fixture.destroy();
@@ -1229,7 +1259,7 @@ class DrawerDynamicPosition {
// Note: we use inputs here, because they're guaranteed
// to be focusable across all platforms.
template: `
-
+
@@ -1238,6 +1268,7 @@ class DrawerDynamicPosition {
})
class DrawerWithFocusableElements {
mode: string = 'over';
+ hasBackdrop: boolean | null = null;
}
@Component({
diff --git a/src/material/sidenav/drawer.ts b/src/material/sidenav/drawer.ts
index 342124513a0d..11f591dfd879 100644
--- a/src/material/sidenav/drawer.ts
+++ b/src/material/sidenav/drawer.ts
@@ -595,8 +595,9 @@ export class MatDrawer implements AfterViewInit, AfterContentChecked, OnDestroy
/** Updates the enabled state of the focus trap. */
private _updateFocusTrapState() {
if (this._focusTrap) {
- // The focus trap is only enabled when the drawer is open in any mode other than side.
- this._focusTrap.enabled = this.opened && this.mode !== 'side';
+ // Trap focus only if the backdrop is enabled. Otherwise, allow end user to interact with the
+ // sidenav content.
+ this._focusTrap.enabled = !!this._container?.hasBackdrop;
}
}
@@ -697,11 +698,7 @@ export class MatDrawerContainer implements AfterContentInit, DoCheck, OnDestroy
*/
@Input()
get hasBackdrop(): boolean {
- if (this._backdropOverride == null) {
- return !this._start || this._start.mode !== 'side' || !this._end || this._end.mode !== 'side';
- }
-
- return this._backdropOverride;
+ return this._drawerHasBackdrop(this._start) || this._drawerHasBackdrop(this._end);
}
set hasBackdrop(value: BooleanInput) {
this._backdropOverride = value == null ? null : coerceBooleanProperty(value);
@@ -1004,22 +1001,27 @@ export class MatDrawerContainer implements AfterContentInit, DoCheck, OnDestroy
_closeModalDrawersViaBackdrop() {
// Close all open drawers where closing is not disabled and the mode is not `side`.
[this._start, this._end]
- .filter(drawer => drawer && !drawer.disableClose && this._canHaveBackdrop(drawer))
+ .filter(drawer => drawer && !drawer.disableClose && this._drawerHasBackdrop(drawer))
.forEach(drawer => drawer!._closeViaBackdropClick());
}
_isShowingBackdrop(): boolean {
return (
- (this._isDrawerOpen(this._start) && this._canHaveBackdrop(this._start)) ||
- (this._isDrawerOpen(this._end) && this._canHaveBackdrop(this._end))
+ (this._isDrawerOpen(this._start) && this._drawerHasBackdrop(this._start)) ||
+ (this._isDrawerOpen(this._end) && this._drawerHasBackdrop(this._end))
);
}
- private _canHaveBackdrop(drawer: MatDrawer): boolean {
- return drawer.mode !== 'side' || !!this._backdropOverride;
- }
-
private _isDrawerOpen(drawer: MatDrawer | null): drawer is MatDrawer {
return drawer != null && drawer.opened;
}
+
+ // Whether argument drawer should have a backdrop when it opens
+ private _drawerHasBackdrop(drawer: MatDrawer | null) {
+ if (this._backdropOverride == null) {
+ return !!drawer && drawer.mode !== 'side';
+ }
+
+ return this._backdropOverride;
+ }
}