diff --git a/src/material/bottom-sheet/BUILD.bazel b/src/material/bottom-sheet/BUILD.bazel index 121c5c24176b..29b3104efa5a 100644 --- a/src/material/bottom-sheet/BUILD.bazel +++ b/src/material/bottom-sheet/BUILD.bazel @@ -58,6 +58,7 @@ ng_test_library( "//src/cdk/bidi", "//src/cdk/keycodes", "//src/cdk/overlay", + "//src/cdk/platform", "//src/cdk/scrolling", "//src/cdk/testing/private", "@npm//@angular/common", diff --git a/src/material/bottom-sheet/bottom-sheet-container.ts b/src/material/bottom-sheet/bottom-sheet-container.ts index 0e60feec302b..c0caf5ce9cf5 100644 --- a/src/material/bottom-sheet/bottom-sheet-container.ts +++ b/src/material/bottom-sheet/bottom-sheet-container.ts @@ -207,7 +207,7 @@ export class MatBottomSheetContainer extends BasePortalOutlet implements OnDestr if (this.bottomSheetConfig.autoFocus) { this._focusTrap.focusInitialElementWhenReady(); } else { - const activeElement = this._document.activeElement; + const activeElement = this._getActiveElement(); // Otherwise ensure that focus is on the container. It's possible that a different // component tried to move focus while the open animation was running. See: @@ -226,7 +226,7 @@ export class MatBottomSheetContainer extends BasePortalOutlet implements OnDestr // We need the extra check, because IE can set the `activeElement` to null in some cases. if (this.bottomSheetConfig.restoreFocus && toFocus && typeof toFocus.focus === 'function') { - const activeElement = this._document.activeElement; + const activeElement = this._getActiveElement(); const element = this._elementRef.nativeElement; // Make sure that focus is still inside the bottom sheet or is on the body (usually because a @@ -246,11 +246,19 @@ export class MatBottomSheetContainer extends BasePortalOutlet implements OnDestr /** Saves a reference to the element that was focused before the bottom sheet was opened. */ private _savePreviouslyFocusedElement() { - this._elementFocusedBeforeOpened = this._document.activeElement as HTMLElement; + this._elementFocusedBeforeOpened = this._getActiveElement(); // The `focus` method isn't available during server-side rendering. if (this._elementRef.nativeElement.focus) { Promise.resolve().then(() => this._elementRef.nativeElement.focus()); } } + + /** Gets the currently-focused element on the page. */ + private _getActiveElement(): HTMLElement | null { + // If the `activeElement` is inside a shadow root, `document.activeElement` will + // point to the shadow root so we have to descend into it ourselves. + const activeElement = this._document.activeElement; + return activeElement?.shadowRoot?.activeElement as HTMLElement || activeElement; + } } diff --git a/src/material/bottom-sheet/bottom-sheet.spec.ts b/src/material/bottom-sheet/bottom-sheet.spec.ts index 96d648057960..1167f5e734ba 100644 --- a/src/material/bottom-sheet/bottom-sheet.spec.ts +++ b/src/material/bottom-sheet/bottom-sheet.spec.ts @@ -1,6 +1,7 @@ import {Directionality} from '@angular/cdk/bidi'; import {A, ESCAPE} from '@angular/cdk/keycodes'; import {OverlayContainer, ScrollStrategy} from '@angular/cdk/overlay'; +import {_supportsShadowDom} from '@angular/cdk/platform'; import {ViewportRuler} from '@angular/cdk/scrolling'; import { dispatchKeyboardEvent, @@ -18,6 +19,7 @@ import { TemplateRef, ViewChild, ViewContainerRef, + ViewEncapsulation, } from '@angular/core'; import { ComponentFixture, @@ -28,6 +30,7 @@ import { TestBed, tick, } from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; import {NoopAnimationsModule} from '@angular/platform-browser/animations'; import {MAT_BOTTOM_SHEET_DEFAULT_OPTIONS, MatBottomSheet} from './bottom-sheet'; @@ -741,6 +744,33 @@ describe('MatBottomSheet', () => { body.removeChild(otherButton); })); + it('should re-focus trigger element inside the shadow DOM when the bottom sheet is dismissed', + fakeAsync(() => { + if (!_supportsShadowDom()) { + return; + } + + viewContainerFixture.destroy(); + const fixture = TestBed.createComponent(ShadowDomComponent); + fixture.detectChanges(); + const button = fixture.debugElement.query(By.css('button'))!.nativeElement; + + button.focus(); + + const ref = bottomSheet.open(PizzaMsg); + flushMicrotasks(); + fixture.detectChanges(); + flushMicrotasks(); + + const spy = spyOn(button, 'focus').and.callThrough(); + ref.dismiss(); + flushMicrotasks(); + fixture.detectChanges(); + tick(500); + + expect(spy).toHaveBeenCalled(); + })); + }); }); @@ -954,6 +984,12 @@ class BottomSheetWithInjectedData { constructor(@Inject(MAT_BOTTOM_SHEET_DATA) public data: any) { } } +@Component({ + template: ``, + encapsulation: ViewEncapsulation.ShadowDom +}) +class ShadowDomComponent {} + // Create a real (non-test) NgModule as a workaround for // https://github.com/angular/angular/issues/10760 const TEST_DIRECTIVES = [ @@ -963,6 +999,7 @@ const TEST_DIRECTIVES = [ TacoMsg, DirectiveWithViewContainer, BottomSheetWithInjectedData, + ShadowDomComponent, ]; @NgModule({