Skip to content

Commit c9856ee

Browse files
crisbetommalerba
authored andcommitted
fix(sidenav): move focus into sidenav in side mode if autoFocu… (#17995)
This PR is the result of the discussions in #17967. Currently we don't move focus into the sidenav if it's in `side` mode, because we don't know the context that the component is used in, however in some cases it makes sense to move focus anyway. These changes make it so that if the consumer explicitly opted into `autoFocus`, we always move focus into the sidenav, no matter what mode it's in.
1 parent 5e9ca28 commit c9856ee

File tree

3 files changed

+39
-20
lines changed

3 files changed

+39
-20
lines changed

src/material/sidenav/drawer.spec.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -518,7 +518,7 @@ describe('MatDrawer', () => {
518518
expect(document.activeElement).toBe(firstFocusableElement);
519519
}));
520520

521-
it('should not trap focus when opened in "side" mode', fakeAsync(() => {
521+
it('should not auto-focus by default when opened in "side" mode', fakeAsync(() => {
522522
testComponent.mode = 'side';
523523
fixture.detectChanges();
524524
lastFocusableElement.focus();
@@ -530,6 +530,19 @@ describe('MatDrawer', () => {
530530
expect(document.activeElement).toBe(lastFocusableElement);
531531
}));
532532

533+
it('should auto-focus when opened in "side" mode when enabled explicitly', fakeAsync(() => {
534+
drawer.autoFocus = true;
535+
testComponent.mode = 'side';
536+
fixture.detectChanges();
537+
lastFocusableElement.focus();
538+
539+
drawer.open();
540+
fixture.detectChanges();
541+
tick();
542+
543+
expect(document.activeElement).toBe(firstFocusableElement);
544+
}));
545+
533546
it('should focus the drawer if there are no focusable elements', fakeAsync(() => {
534547
fixture.destroy();
535548

@@ -545,7 +558,7 @@ describe('MatDrawer', () => {
545558
}));
546559

547560
it('should be able to disable auto focus', fakeAsync(() => {
548-
testComponent.autoFocus = false;
561+
drawer.autoFocus = false;
549562
testComponent.mode = 'push';
550563
fixture.detectChanges();
551564
lastFocusableElement.focus();
@@ -981,15 +994,14 @@ class DrawerDynamicPosition {
981994
// to be focusable across all platforms.
982995
template: `
983996
<mat-drawer-container>
984-
<mat-drawer position="start" [mode]="mode" [autoFocus]="autoFocus">
997+
<mat-drawer position="start" [mode]="mode">
985998
<input type="text" class="input1"/>
986999
</mat-drawer>
9871000
<input type="text" class="input2"/>
9881001
</mat-drawer-container>`,
9891002
})
9901003
class DrawerWithFocusableElements {
9911004
mode: string = 'over';
992-
autoFocus = true;
9931005
}
9941006

9951007
@Component({

src/material/sidenav/drawer.ts

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -173,11 +173,22 @@ export class MatDrawer implements AfterContentInit, AfterContentChecked, OnDestr
173173
set disableClose(value: boolean) { this._disableClose = coerceBooleanProperty(value); }
174174
private _disableClose: boolean = false;
175175

176-
/** Whether the drawer should focus the first focusable element automatically when opened. */
176+
/**
177+
* Whether the drawer should focus the first focusable element automatically when opened.
178+
* Defaults to false in when `mode` is set to `side`, otherwise defaults to `true`. If explicitly
179+
* enabled, focus will be moved into the sidenav in `side` mode as well.
180+
*/
177181
@Input()
178-
get autoFocus(): boolean { return this._autoFocus; }
182+
get autoFocus(): boolean {
183+
const value = this._autoFocus;
184+
185+
// Note that usually we disable auto focusing in `side` mode, because we don't know how the
186+
// sidenav is being used, but in some cases it still makes sense to do it. If the consumer
187+
// explicitly enabled `autoFocus`, we take it as them always wanting to enable it.
188+
return value == null ? this.mode !== 'side' : value;
189+
}
179190
set autoFocus(value: boolean) { this._autoFocus = coerceBooleanProperty(value); }
180-
private _autoFocus: boolean = true;
191+
private _autoFocus: boolean | undefined;
181192

182193
/**
183194
* Whether the drawer is opened. We overload this because we trigger an event when it
@@ -253,11 +264,6 @@ export class MatDrawer implements AfterContentInit, AfterContentChecked, OnDestr
253264
*/
254265
readonly _modeChanged = new Subject<void>();
255266

256-
get _isFocusTrapEnabled(): boolean {
257-
// The focus trap is only enabled when the drawer is open in any mode other than side.
258-
return this.opened && this.mode !== 'side';
259-
}
260-
261267
constructor(private _elementRef: ElementRef<HTMLElement>,
262268
private _focusTrapFactory: FocusTrapFactory,
263269
private _focusMonitor: FocusMonitor,
@@ -276,9 +282,7 @@ export class MatDrawer implements AfterContentInit, AfterContentChecked, OnDestr
276282
this._elementFocusedBeforeDrawerWasOpened = this._doc.activeElement as HTMLElement;
277283
}
278284

279-
if (this._isFocusTrapEnabled && this._focusTrap) {
280-
this._trapFocus();
281-
}
285+
this._takeFocus();
282286
} else {
283287
this._restoreFocus();
284288
}
@@ -316,9 +320,12 @@ export class MatDrawer implements AfterContentInit, AfterContentChecked, OnDestr
316320
});
317321
}
318322

319-
/** Traps focus inside the drawer. */
320-
private _trapFocus() {
321-
if (!this.autoFocus) {
323+
/**
324+
* Moves focus into the drawer. Note that this works even if
325+
* the focus trap is disabled in `side` mode.
326+
*/
327+
private _takeFocus() {
328+
if (!this.autoFocus || !this._focusTrap) {
322329
return;
323330
}
324331

@@ -428,7 +435,8 @@ export class MatDrawer implements AfterContentInit, AfterContentChecked, OnDestr
428435
/** Updates the enabled state of the focus trap. */
429436
private _updateFocusTrapState() {
430437
if (this._focusTrap) {
431-
this._focusTrap.enabled = this._isFocusTrapEnabled;
438+
// The focus trap is only enabled when the drawer is open in any mode other than side.
439+
this._focusTrap.enabled = this.opened && this.mode !== 'side';
432440
}
433441
}
434442

tools/public_api_guard/material/sidenav.d.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ export declare class MatDrawer implements AfterContentInit, AfterContentChecked,
88
_animationState: 'open-instant' | 'open' | 'void';
99
readonly _closedStream: Observable<void>;
1010
_container?: MatDrawerContainer | undefined;
11-
readonly _isFocusTrapEnabled: boolean;
1211
readonly _modeChanged: Subject<void>;
1312
readonly _openedStream: Observable<void>;
1413
readonly _width: number;

0 commit comments

Comments
 (0)