From f272dda6818028b6950bf9bd67d57d28fa1f0db1 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Wed, 26 Feb 2025 09:11:00 +0100 Subject: [PATCH] fix(material/bottom-sheet): focus first tabbable element by default Switches the bottom sheet to focusing the first tabbale element by default like we do in `MatDialog`. Note that we had a comment saying that the previous behavior was intentional. That seems to be a leftover from #14534 where the underlying cause was that we were delaying focus until the animation is done. We now have better ways of handling it so the comment isn't relevant anymore. Fixes #30483. --- .../bottom-sheet/bottom-sheet-config.ts | 5 +- .../bottom-sheet/bottom-sheet-container.ts | 6 - .../bottom-sheet/bottom-sheet.spec.ts | 106 ++++++++---------- .../public_api_guard/material/bottom-sheet.md | 2 - 4 files changed, 48 insertions(+), 71 deletions(-) diff --git a/src/material/bottom-sheet/bottom-sheet-config.ts b/src/material/bottom-sheet/bottom-sheet-config.ts index 6740e47bf4aa..c7ffe34d14d5 100644 --- a/src/material/bottom-sheet/bottom-sheet-config.ts +++ b/src/material/bottom-sheet/bottom-sheet-config.ts @@ -58,15 +58,12 @@ export class MatBottomSheetConfig { */ closeOnNavigation?: boolean = true; - // Note that this is set to 'dialog' by default, because while the a11y recommendations - // are to focus the first focusable element, doing so prevents screen readers from reading out the - // rest of the bottom sheet content. /** * Where the bottom sheet should focus on open. * @breaking-change 14.0.0 Remove boolean option from autoFocus. Use string or * AutoFocusTarget instead. */ - autoFocus?: AutoFocusTarget | string | boolean = 'dialog'; + autoFocus?: AutoFocusTarget | string | boolean = 'first-tabbable'; /** * Whether the bottom sheet should restore focus to the diff --git a/src/material/bottom-sheet/bottom-sheet-container.ts b/src/material/bottom-sheet/bottom-sheet-container.ts index 0dae8f85adff..d7beb99f7d9e 100644 --- a/src/material/bottom-sheet/bottom-sheet-container.ts +++ b/src/material/bottom-sheet/bottom-sheet-container.ts @@ -137,10 +137,6 @@ export class MatBottomSheetContainer extends CdkDialogContainer implements OnDes const isEnter = animationName === ENTER_ANIMATION; const isExit = animationName === EXIT_ANIMATION; - if (isEnter) { - this._trapFocus(); - } - if (isEnter || isExit) { this._animationStateChanged.emit({ toState: isEnter ? 'visible' : 'hidden', @@ -148,6 +144,4 @@ export class MatBottomSheetContainer extends CdkDialogContainer implements OnDes }); } } - - protected override _captureInitialFocus(): void {} } diff --git a/src/material/bottom-sheet/bottom-sheet.spec.ts b/src/material/bottom-sheet/bottom-sheet.spec.ts index b632d66e7f99..1ba7af4a3917 100644 --- a/src/material/bottom-sheet/bottom-sheet.spec.ts +++ b/src/material/bottom-sheet/bottom-sheet.spec.ts @@ -622,7 +622,7 @@ describe('MatBottomSheet', () => { beforeEach(() => document.body.appendChild(overlayContainerElement)); afterEach(() => overlayContainerElement.remove()); - it('should focus the bottom sheet container by default', fakeAsync(() => { + it('should focus the first tabbable element by default', fakeAsync(() => { bottomSheet.open(PizzaMsg, { viewContainerRef: testViewContainerRef, }); @@ -631,9 +631,7 @@ describe('MatBottomSheet', () => { flush(); viewContainerFixture.detectChanges(); - expect(document.activeElement!.tagName) - .withContext('Expected bottom sheet container to be focused.') - .toBe('MAT-BOTTOM-SHEET-CONTAINER'); + expect(document.activeElement!.tagName).toBe('INPUT'); })); it('should create a focus trap if autoFocus is disabled', fakeAsync(() => { @@ -669,72 +667,62 @@ describe('MatBottomSheet', () => { }, ); - it( - 'should focus the bottom sheet element on open when autoFocus is set to ' + - '"dialog" (the default)', - fakeAsync(() => { - bottomSheet.open(PizzaMsg, { - viewContainerRef: testViewContainerRef, - }); + it('should focus the bottom sheet element on open when autoFocus is set to "dialog"', fakeAsync(() => { + bottomSheet.open(PizzaMsg, { + viewContainerRef: testViewContainerRef, + autoFocus: 'dialog', + }); - viewContainerFixture.detectChanges(); - flush(); - viewContainerFixture.detectChanges(); + viewContainerFixture.detectChanges(); + flush(); + viewContainerFixture.detectChanges(); - let container = overlayContainerElement.querySelector( - '.mat-bottom-sheet-container', - ) as HTMLInputElement; + let container = overlayContainerElement.querySelector( + '.mat-bottom-sheet-container', + ) as HTMLInputElement; - expect(document.activeElement) - .withContext('Expected container to be focused on open') - .toBe(container); - }), - ); + expect(document.activeElement) + .withContext('Expected container to be focused on open') + .toBe(container); + })); - it( - 'should focus the bottom sheet element on open when autoFocus is set to ' + '"first-heading"', - fakeAsync(() => { - bottomSheet.open(ContentElementDialog, { - viewContainerRef: testViewContainerRef, - autoFocus: 'first-heading', - }); + it('should focus the bottom sheet element on open when autoFocus is set to "first-heading"', fakeAsync(() => { + bottomSheet.open(ContentElementDialog, { + viewContainerRef: testViewContainerRef, + autoFocus: 'first-heading', + }); - viewContainerFixture.detectChanges(); - flush(); - viewContainerFixture.detectChanges(); + viewContainerFixture.detectChanges(); + flush(); + viewContainerFixture.detectChanges(); - let firstHeader = overlayContainerElement.querySelector( - 'h1[tabindex="-1"]', - ) as HTMLInputElement; + let firstHeader = overlayContainerElement.querySelector( + 'h1[tabindex="-1"]', + ) as HTMLInputElement; - expect(document.activeElement) - .withContext('Expected first header to be focused on open') - .toBe(firstHeader); - }), - ); + expect(document.activeElement) + .withContext('Expected first header to be focused on open') + .toBe(firstHeader); + })); - it( - 'should focus the first element that matches the css selector on open when ' + - 'autoFocus is set to a css selector', - fakeAsync(() => { - bottomSheet.open(ContentElementDialog, { - viewContainerRef: testViewContainerRef, - autoFocus: 'p', - }); + it('should focus the first element that matches the css selector on open when autoFocus is set to a css selector', fakeAsync(() => { + bottomSheet.open(ContentElementDialog, { + viewContainerRef: testViewContainerRef, + autoFocus: 'p', + }); - viewContainerFixture.detectChanges(); - flush(); - viewContainerFixture.detectChanges(); + viewContainerFixture.detectChanges(); + flush(); + viewContainerFixture.detectChanges(); - let firstParagraph = overlayContainerElement.querySelector( - 'p[tabindex="-1"]', - ) as HTMLInputElement; + let firstParagraph = overlayContainerElement.querySelector( + 'p[tabindex="-1"]', + ) as HTMLInputElement; - expect(document.activeElement) - .withContext('Expected first paragraph to be focused on open') - .toBe(firstParagraph); - }), - ); + expect(document.activeElement) + .withContext('Expected first paragraph to be focused on open') + .toBe(firstParagraph); + })); it('should re-focus trigger element when bottom sheet closes', fakeAsync(() => { const button = document.createElement('button'); diff --git a/tools/public_api_guard/material/bottom-sheet.md b/tools/public_api_guard/material/bottom-sheet.md index 798061904c89..11d0bac8127b 100644 --- a/tools/public_api_guard/material/bottom-sheet.md +++ b/tools/public_api_guard/material/bottom-sheet.md @@ -81,8 +81,6 @@ export class MatBottomSheetContainer extends CdkDialogContainer implements OnDes toState: "visible" | "hidden"; phase: "start" | "done"; }>; - // (undocumented) - protected _captureInitialFocus(): void; enter(): void; exit(): void; // (undocumented)