Skip to content

Commit daa6ca3

Browse files
authored
fix(material/sidenav): only trap focus when backdrop is enabled (#27355)
Correct when Sidenav enabled focus trapping. When backdrop is show, trap focus. Do no trap focus when backdrop is not shown. Existing behavior is that Sidenav traps focus whenever it is not in side mode. This causes the end user to not be able to interact with the sidenav content when the mode is push/over, backdrop is disabled and using ConfigurableFocusTrapFactory (#26572). With this commit applied, Sidenav always traps focus when backdrop is shown. Sidenav never traps focus when backdrop is not shown, regardless of what mode the sidenav is in, focus trapping will respect if the backdrop is shown or not shown. Fix this issue by correcting boolean logic for detecting if backdrop is enabled and using that logic to determine when to trap focus. Add an example that injects ConfigurableFocusTrapFactory. Fix #26572
1 parent 2683084 commit daa6ca3

File tree

6 files changed

+130
-16
lines changed

6 files changed

+130
-16
lines changed

src/components-examples/material/sidenav/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ export {SidenavBackdropExample} from './sidenav-backdrop/sidenav-backdrop-exampl
33
export {SidenavDisableCloseExample} from './sidenav-disable-close/sidenav-disable-close-example';
44
export {SidenavDrawerOverviewExample} from './sidenav-drawer-overview/sidenav-drawer-overview-example';
55
export {SidenavFixedExample} from './sidenav-fixed/sidenav-fixed-example';
6-
export {SidenavModeExample} from './sidenav-mode/sidenav-mode-example';
6+
export {SidenavConfigurableFocusTrapExample} from './sidenav-configurable-focus-trap/sidenav-configurable-focus-trap-example';
77
export {SidenavOpenCloseExample} from './sidenav-open-close/sidenav-open-close-example';
88
export {SidenavOverviewExample} from './sidenav-overview/sidenav-overview-example';
99
export {SidenavPositionExample} from './sidenav-position/sidenav-position-example';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
.example-container {
2+
position: absolute;
3+
top: 0;
4+
bottom: 0;
5+
left: 0;
6+
right: 0;
7+
}
8+
9+
.example-radio-group {
10+
display: block;
11+
border: 1px solid #555;
12+
margin: 20px;
13+
padding: 10px;
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<mat-sidenav-container class="example-container" *ngIf="shouldRun" [hasBackdrop]="hasBackdrop.value">
2+
<mat-sidenav #sidenav [mode]="mode.value!" [position]="position.value!">
3+
<p><button mat-button (click)="sidenav.toggle()">Toggle</button></p>
4+
<p>
5+
<label>Test input for drawer<input/></label>
6+
</p>
7+
</mat-sidenav>
8+
9+
<mat-sidenav-content>
10+
<p><button mat-button (click)="sidenav.toggle()">Toggle</button></p>
11+
<p>
12+
<mat-radio-group class="example-radio-group" [formControl]="mode">
13+
<label>Mode:</label>
14+
<mat-radio-button value="over">Over</mat-radio-button>
15+
<mat-radio-button value="side">Side</mat-radio-button>
16+
<mat-radio-button value="push">Push</mat-radio-button>
17+
</mat-radio-group>
18+
<mat-radio-group class="example-radio-group" [formControl]="hasBackdrop">
19+
<label>Has Backdrop:</label>
20+
<mat-radio-button [value]="null">Default</mat-radio-button>
21+
<mat-radio-button [value]="true">true</mat-radio-button>
22+
<mat-radio-button [value]="false">false</mat-radio-button>
23+
</mat-radio-group>
24+
<mat-radio-group class="example-radio-group" [formControl]="position">
25+
<label>Position:</label>
26+
<mat-radio-button value="start">Start</mat-radio-button>
27+
<mat-radio-button value="end">End</mat-radio-button>
28+
</mat-radio-group>
29+
</p>
30+
<p>
31+
<label>Test input for drawer content<input/></label>
32+
</p>
33+
</mat-sidenav-content>
34+
</mat-sidenav-container>
35+
36+
<div *ngIf="!shouldRun">Please open on Stackblitz to see result</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import {Component} from '@angular/core';
2+
import {NgIf} from '@angular/common';
3+
import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
4+
import {MatDrawerMode, MatSidenavModule} from '@angular/material/sidenav';
5+
import {MatRadioModule} from '@angular/material/radio';
6+
import {MatButtonModule} from '@angular/material/button';
7+
import {ConfigurableFocusTrapFactory, FocusTrapFactory} from '@angular/cdk/a11y';
8+
9+
/** @title Sidenav using injected ConfigurableFocusTrap */
10+
@Component({
11+
selector: 'sidenav-configurable-focus-trap-example',
12+
templateUrl: 'sidenav-configurable-focus-trap-example.html',
13+
styleUrls: ['sidenav-configurable-focus-trap-example.css'],
14+
standalone: true,
15+
imports: [
16+
NgIf,
17+
MatSidenavModule,
18+
MatButtonModule,
19+
MatRadioModule,
20+
FormsModule,
21+
ReactiveFormsModule,
22+
],
23+
providers: [{provide: FocusTrapFactory, useClass: ConfigurableFocusTrapFactory}],
24+
})
25+
export class SidenavConfigurableFocusTrapExample {
26+
mode = new FormControl('over' as MatDrawerMode);
27+
hasBackdrop = new FormControl(null as null | boolean);
28+
position = new FormControl('start' as 'start' | 'end');
29+
30+
shouldRun = /(^|.)(stackblitz|webcontainer).(io|com)$/.test(window.location.host);
31+
}

src/material/sidenav/drawer.spec.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,19 @@ describe('MatDrawer', () => {
567567
expect(document.activeElement).toBe(firstFocusableElement);
568568
}));
569569

570+
it('should trap focus when opened in "side" mode if backdrop is explicitly enabled', fakeAsync(() => {
571+
testComponent.mode = 'push';
572+
testComponent.hasBackdrop = true;
573+
fixture.detectChanges();
574+
lastFocusableElement.focus();
575+
576+
drawer.open();
577+
fixture.detectChanges();
578+
tick();
579+
580+
expect(document.activeElement).toBe(firstFocusableElement);
581+
}));
582+
570583
it('should not auto-focus by default when opened in "side" mode', fakeAsync(() => {
571584
testComponent.mode = 'side';
572585
fixture.detectChanges();
@@ -596,6 +609,23 @@ describe('MatDrawer', () => {
596609
}),
597610
);
598611

612+
it(
613+
'should auto-focus to first tabbable element when opened in "push" mode' +
614+
'when backdrop is enabled explicitly',
615+
fakeAsync(() => {
616+
testComponent.mode = 'push';
617+
testComponent.hasBackdrop = true;
618+
fixture.detectChanges();
619+
lastFocusableElement.focus();
620+
621+
drawer.open();
622+
fixture.detectChanges();
623+
tick();
624+
625+
expect(document.activeElement).toBe(firstFocusableElement);
626+
}),
627+
);
628+
599629
it('should focus the drawer if there are no focusable elements', fakeAsync(() => {
600630
fixture.destroy();
601631

@@ -1229,7 +1259,7 @@ class DrawerDynamicPosition {
12291259
// Note: we use inputs here, because they're guaranteed
12301260
// to be focusable across all platforms.
12311261
template: `
1232-
<mat-drawer-container>
1262+
<mat-drawer-container [hasBackdrop]="hasBackdrop">
12331263
<mat-drawer position="start" [mode]="mode">
12341264
<input type="text" class="input1"/>
12351265
</mat-drawer>
@@ -1238,6 +1268,7 @@ class DrawerDynamicPosition {
12381268
})
12391269
class DrawerWithFocusableElements {
12401270
mode: string = 'over';
1271+
hasBackdrop: boolean | null = null;
12411272
}
12421273

12431274
@Component({

src/material/sidenav/drawer.ts

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -595,8 +595,9 @@ export class MatDrawer implements AfterViewInit, AfterContentChecked, OnDestroy
595595
/** Updates the enabled state of the focus trap. */
596596
private _updateFocusTrapState() {
597597
if (this._focusTrap) {
598-
// The focus trap is only enabled when the drawer is open in any mode other than side.
599-
this._focusTrap.enabled = this.opened && this.mode !== 'side';
598+
// Trap focus only if the backdrop is enabled. Otherwise, allow end user to interact with the
599+
// sidenav content.
600+
this._focusTrap.enabled = !!this._container?.hasBackdrop;
600601
}
601602
}
602603

@@ -697,11 +698,7 @@ export class MatDrawerContainer implements AfterContentInit, DoCheck, OnDestroy
697698
*/
698699
@Input()
699700
get hasBackdrop(): boolean {
700-
if (this._backdropOverride == null) {
701-
return !this._start || this._start.mode !== 'side' || !this._end || this._end.mode !== 'side';
702-
}
703-
704-
return this._backdropOverride;
701+
return this._drawerHasBackdrop(this._start) || this._drawerHasBackdrop(this._end);
705702
}
706703
set hasBackdrop(value: BooleanInput) {
707704
this._backdropOverride = value == null ? null : coerceBooleanProperty(value);
@@ -1004,22 +1001,27 @@ export class MatDrawerContainer implements AfterContentInit, DoCheck, OnDestroy
10041001
_closeModalDrawersViaBackdrop() {
10051002
// Close all open drawers where closing is not disabled and the mode is not `side`.
10061003
[this._start, this._end]
1007-
.filter(drawer => drawer && !drawer.disableClose && this._canHaveBackdrop(drawer))
1004+
.filter(drawer => drawer && !drawer.disableClose && this._drawerHasBackdrop(drawer))
10081005
.forEach(drawer => drawer!._closeViaBackdropClick());
10091006
}
10101007

10111008
_isShowingBackdrop(): boolean {
10121009
return (
1013-
(this._isDrawerOpen(this._start) && this._canHaveBackdrop(this._start)) ||
1014-
(this._isDrawerOpen(this._end) && this._canHaveBackdrop(this._end))
1010+
(this._isDrawerOpen(this._start) && this._drawerHasBackdrop(this._start)) ||
1011+
(this._isDrawerOpen(this._end) && this._drawerHasBackdrop(this._end))
10151012
);
10161013
}
10171014

1018-
private _canHaveBackdrop(drawer: MatDrawer): boolean {
1019-
return drawer.mode !== 'side' || !!this._backdropOverride;
1020-
}
1021-
10221015
private _isDrawerOpen(drawer: MatDrawer | null): drawer is MatDrawer {
10231016
return drawer != null && drawer.opened;
10241017
}
1018+
1019+
// Whether argument drawer should have a backdrop when it opens
1020+
private _drawerHasBackdrop(drawer: MatDrawer | null) {
1021+
if (this._backdropOverride == null) {
1022+
return !!drawer && drawer.mode !== 'side';
1023+
}
1024+
1025+
return this._backdropOverride;
1026+
}
10251027
}

0 commit comments

Comments
 (0)