Skip to content

Commit 78e45d0

Browse files
crisbetoandrewseguin
authored andcommitted
fix(material/bottom-sheet): focus restoration not working inside shadow dom (#21975)
Related to #21796. The bottom sheet focus restoration works by grabbing `document.activeElement` before the sheet is opened and restoring focus to the element on destroy. This won't work if the element is inside the shadow DOM, because the browser will return the shadow root. These changes add a workaround. (cherry picked from commit 7044153)
1 parent b0fc1f0 commit 78e45d0

File tree

3 files changed

+49
-3
lines changed

3 files changed

+49
-3
lines changed

src/material/bottom-sheet/BUILD.bazel

+1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ ng_test_library(
5858
"//src/cdk/bidi",
5959
"//src/cdk/keycodes",
6060
"//src/cdk/overlay",
61+
"//src/cdk/platform",
6162
"//src/cdk/scrolling",
6263
"//src/cdk/testing/private",
6364
"@npm//@angular/common",

src/material/bottom-sheet/bottom-sheet-container.ts

+11-3
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ export class MatBottomSheetContainer extends BasePortalOutlet implements OnDestr
207207
if (this.bottomSheetConfig.autoFocus) {
208208
this._focusTrap.focusInitialElementWhenReady();
209209
} else {
210-
const activeElement = this._document.activeElement;
210+
const activeElement = this._getActiveElement();
211211

212212
// Otherwise ensure that focus is on the container. It's possible that a different
213213
// component tried to move focus while the open animation was running. See:
@@ -226,7 +226,7 @@ export class MatBottomSheetContainer extends BasePortalOutlet implements OnDestr
226226

227227
// We need the extra check, because IE can set the `activeElement` to null in some cases.
228228
if (this.bottomSheetConfig.restoreFocus && toFocus && typeof toFocus.focus === 'function') {
229-
const activeElement = this._document.activeElement;
229+
const activeElement = this._getActiveElement();
230230
const element = this._elementRef.nativeElement;
231231

232232
// 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
246246

247247
/** Saves a reference to the element that was focused before the bottom sheet was opened. */
248248
private _savePreviouslyFocusedElement() {
249-
this._elementFocusedBeforeOpened = this._document.activeElement as HTMLElement;
249+
this._elementFocusedBeforeOpened = this._getActiveElement();
250250

251251
// The `focus` method isn't available during server-side rendering.
252252
if (this._elementRef.nativeElement.focus) {
253253
Promise.resolve().then(() => this._elementRef.nativeElement.focus());
254254
}
255255
}
256+
257+
/** Gets the currently-focused element on the page. */
258+
private _getActiveElement(): HTMLElement | null {
259+
// If the `activeElement` is inside a shadow root, `document.activeElement` will
260+
// point to the shadow root so we have to descend into it ourselves.
261+
const activeElement = this._document.activeElement;
262+
return activeElement?.shadowRoot?.activeElement as HTMLElement || activeElement;
263+
}
256264
}

src/material/bottom-sheet/bottom-sheet.spec.ts

+37
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {Directionality} from '@angular/cdk/bidi';
22
import {A, ESCAPE} from '@angular/cdk/keycodes';
33
import {OverlayContainer, ScrollStrategy} from '@angular/cdk/overlay';
4+
import {_supportsShadowDom} from '@angular/cdk/platform';
45
import {ViewportRuler} from '@angular/cdk/scrolling';
56
import {
67
dispatchKeyboardEvent,
@@ -18,6 +19,7 @@ import {
1819
TemplateRef,
1920
ViewChild,
2021
ViewContainerRef,
22+
ViewEncapsulation,
2123
} from '@angular/core';
2224
import {
2325
ComponentFixture,
@@ -28,6 +30,7 @@ import {
2830
TestBed,
2931
tick,
3032
} from '@angular/core/testing';
33+
import {By} from '@angular/platform-browser';
3134
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
3235

3336
import {MAT_BOTTOM_SHEET_DEFAULT_OPTIONS, MatBottomSheet} from './bottom-sheet';
@@ -741,6 +744,33 @@ describe('MatBottomSheet', () => {
741744
body.removeChild(otherButton);
742745
}));
743746

747+
it('should re-focus trigger element inside the shadow DOM when the bottom sheet is dismissed',
748+
fakeAsync(() => {
749+
if (!_supportsShadowDom()) {
750+
return;
751+
}
752+
753+
viewContainerFixture.destroy();
754+
const fixture = TestBed.createComponent(ShadowDomComponent);
755+
fixture.detectChanges();
756+
const button = fixture.debugElement.query(By.css('button'))!.nativeElement;
757+
758+
button.focus();
759+
760+
const ref = bottomSheet.open(PizzaMsg);
761+
flushMicrotasks();
762+
fixture.detectChanges();
763+
flushMicrotasks();
764+
765+
const spy = spyOn(button, 'focus').and.callThrough();
766+
ref.dismiss();
767+
flushMicrotasks();
768+
fixture.detectChanges();
769+
tick(500);
770+
771+
expect(spy).toHaveBeenCalled();
772+
}));
773+
744774
});
745775

746776
});
@@ -954,6 +984,12 @@ class BottomSheetWithInjectedData {
954984
constructor(@Inject(MAT_BOTTOM_SHEET_DATA) public data: any) { }
955985
}
956986

987+
@Component({
988+
template: `<button>I'm a button</button>`,
989+
encapsulation: ViewEncapsulation.ShadowDom
990+
})
991+
class ShadowDomComponent {}
992+
957993
// Create a real (non-test) NgModule as a workaround for
958994
// https://github.com/angular/angular/issues/10760
959995
const TEST_DIRECTIVES = [
@@ -963,6 +999,7 @@ const TEST_DIRECTIVES = [
963999
TacoMsg,
9641000
DirectiveWithViewContainer,
9651001
BottomSheetWithInjectedData,
1002+
ShadowDomComponent,
9661003
];
9671004

9681005
@NgModule({

0 commit comments

Comments
 (0)