Skip to content

Commit

Permalink
fix(material/bottom-sheet): focus restoration not working inside shad…
Browse files Browse the repository at this point in the history
…ow 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.
  • Loading branch information
crisbeto authored Feb 23, 2021
1 parent 1f69ff4 commit 7044153
Show file tree
Hide file tree
Showing 3 changed files with 49 additions and 3 deletions.
1 change: 1 addition & 0 deletions src/material/bottom-sheet/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
14 changes: 11 additions & 3 deletions src/material/bottom-sheet/bottom-sheet-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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;
}
}
37 changes: 37 additions & 0 deletions src/material/bottom-sheet/bottom-sheet.spec.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -18,6 +19,7 @@ import {
TemplateRef,
ViewChild,
ViewContainerRef,
ViewEncapsulation,
} from '@angular/core';
import {
ComponentFixture,
Expand All @@ -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';
Expand Down Expand Up @@ -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();
}));

});

});
Expand Down Expand Up @@ -954,6 +984,12 @@ class BottomSheetWithInjectedData {
constructor(@Inject(MAT_BOTTOM_SHEET_DATA) public data: any) { }
}

@Component({
template: `<button>I'm a button</button>`,
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 = [
Expand All @@ -963,6 +999,7 @@ const TEST_DIRECTIVES = [
TacoMsg,
DirectiveWithViewContainer,
BottomSheetWithInjectedData,
ShadowDomComponent,
];

@NgModule({
Expand Down

0 comments on commit 7044153

Please sign in to comment.