Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(material/dialog): focus restoration not working inside shadow dom #21811

Merged
merged 1 commit into from
Mar 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/material-experimental/mdc-dialog/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,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
38 changes: 37 additions & 1 deletion src/material-experimental/mdc-dialog/dialog.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {FocusMonitor, FocusOrigin} from '@angular/cdk/a11y';
import {Directionality} from '@angular/cdk/bidi';
import {A, ESCAPE} from '@angular/cdk/keycodes';
import {Overlay, OverlayContainer, ScrollStrategy} from '@angular/cdk/overlay';
import {_supportsShadowDom} from '@angular/cdk/platform';
import {ScrollDispatcher} from '@angular/cdk/scrolling';
import {
createKeyboardEvent,
Expand All @@ -23,7 +24,8 @@ import {
NgZone,
TemplateRef,
ViewChild,
ViewContainerRef
ViewContainerRef,
ViewEncapsulation
} from '@angular/core';
import {
ComponentFixture,
Expand All @@ -34,6 +36,7 @@ import {
TestBed,
tick,
} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {BrowserAnimationsModule, NoopAnimationsModule} from '@angular/platform-browser/animations';
import {numbers} from '@material/dialog';
import {Subject} from 'rxjs';
Expand Down Expand Up @@ -1075,6 +1078,32 @@ describe('MDC-based MatDialog', () => {
document.body.removeChild(button);
}));

it('should re-focus trigger element inside the shadow DOM when dialog closes', 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 dialogRef = dialog.open(PizzaMsg);
flushMicrotasks();
fixture.detectChanges();
flushMicrotasks();

const spy = spyOn(button, 'focus').and.callThrough();
dialogRef.close();
flushMicrotasks();
fixture.detectChanges();
tick(500);

expect(spy).toHaveBeenCalled();
}));

it('should re-focus the trigger via keyboard when closed via escape key', fakeAsync(() => {
const button = document.createElement('button');
let lastFocusOrigin: FocusOrigin = null;
Expand Down Expand Up @@ -1870,6 +1899,12 @@ class DialogWithInjectedData {
class DialogWithoutFocusableElements {
}

@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 @@ -1882,6 +1917,7 @@ const TEST_DIRECTIVES = [
DialogWithInjectedData,
DialogWithoutFocusableElements,
ComponentWithContentElementTemplateRef,
ShadowDomComponent,
];

@NgModule({
Expand Down
1 change: 1 addition & 0 deletions src/material/dialog/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,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/dialog/dialog-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ export abstract class _MatDialogContainerBase extends BasePortalOutlet {
// We need the extra check, because IE can set the `activeElement` to null in some cases.
if (this._config.restoreFocus && previousElement &&
typeof previousElement.focus === 'function') {
const activeElement = this._document.activeElement;
const activeElement = this._getActiveElement();
const element = this._elementRef.nativeElement;

// Make sure that focus is still inside the dialog or is on the body (usually because a
Expand Down Expand Up @@ -213,7 +213,7 @@ export abstract class _MatDialogContainerBase extends BasePortalOutlet {
/** Captures the element that was focused before the dialog was opened. */
private _capturePreviouslyFocusedElement() {
if (this._document) {
this._elementFocusedBeforeDialogWasOpened = this._document.activeElement as HTMLElement;
this._elementFocusedBeforeDialogWasOpened = this._getActiveElement() as HTMLElement;
}
}

Expand All @@ -228,9 +228,17 @@ export abstract class _MatDialogContainerBase extends BasePortalOutlet {
/** Returns whether focus is inside the dialog. */
private _containsFocus() {
const element = this._elementRef.nativeElement;
const activeElement = this._document.activeElement;
const activeElement = this._getActiveElement();
return element === activeElement || element.contains(activeElement);
}

/** Gets the currently-focused element on the page. */
private _getActiveElement(): Element | 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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't it possible for the element to be within multiple layers of shadow dom?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is possible, but my assumption was activeElement would point to the closest shadow root. That being said, I haven't actually tried it. If that's the case, we might need some reusable utility under cdk/platform since we have a lot of places that look only at document.activeElement.

}
}

/**
Expand Down
37 changes: 36 additions & 1 deletion src/material/dialog/dialog.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ import {
ViewChild,
ViewContainerRef,
ComponentFactoryResolver,
NgZone
NgZone,
ViewEncapsulation
} from '@angular/core';
import {By} from '@angular/platform-browser';
import {BrowserAnimationsModule, NoopAnimationsModule} from '@angular/platform-browser/animations';
import {Location} from '@angular/common';
import {SpyLocation} from '@angular/common/testing';
import {Directionality} from '@angular/cdk/bidi';
import {_supportsShadowDom} from '@angular/cdk/platform';
import {MatDialogContainer} from './dialog-container';
import {OverlayContainer, ScrollStrategy, Overlay} from '@angular/cdk/overlay';
import {ScrollDispatcher} from '@angular/cdk/scrolling';
Expand Down Expand Up @@ -1166,6 +1168,32 @@ describe('MatDialog', () => {
document.body.removeChild(button);
}));

it('should re-focus trigger element inside the shadow DOM when dialog closes', 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 dialogRef = dialog.open(PizzaMsg);
flushMicrotasks();
fixture.detectChanges();
flushMicrotasks();

const spy = spyOn(button, 'focus').and.callThrough();
dialogRef.close();
flushMicrotasks();
fixture.detectChanges();
tick(500);

expect(spy).toHaveBeenCalled();
}));

it('should re-focus the trigger via keyboard when closed via escape key', fakeAsync(() => {
const button = document.createElement('button');
let lastFocusOrigin: FocusOrigin = null;
Expand Down Expand Up @@ -1947,6 +1975,12 @@ class DialogWithInjectedData {
@Component({template: '<p>Pasta</p>'})
class DialogWithoutFocusableElements {}

@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 @@ -1959,6 +1993,7 @@ const TEST_DIRECTIVES = [
DialogWithInjectedData,
DialogWithoutFocusableElements,
ComponentWithContentElementTemplateRef,
ShadowDomComponent,
];

@NgModule({
Expand Down