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; + } }