Skip to content

Commit

Permalink
fix(material/datepicker): focus restoration not working inside shadow…
Browse files Browse the repository at this point in the history
… dom (#21796)

Our focus restoration works by checking `document.activeElement` before the
panel is opened and restoring to that element on close. The problem is that
`activeElement` will return the shadow root, if the focused element is inside one.

These changes add some extra logic to account for it.

Fixes #21785.
  • Loading branch information
crisbeto authored Feb 9, 2021
1 parent 915791b commit 85d8de9
Show file tree
Hide file tree
Showing 3 changed files with 46 additions and 4 deletions.
1 change: 1 addition & 0 deletions src/material/datepicker/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ ng_test_library(
"//src/cdk/bidi",
"//src/cdk/keycodes",
"//src/cdk/overlay",
"//src/cdk/platform",
"//src/cdk/scrolling",
"//src/cdk/testing/private",
"//src/material/core",
Expand Down
8 changes: 5 additions & 3 deletions src/material/datepicker/datepicker-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -550,10 +550,12 @@ export abstract class MatDatepickerBase<C extends MatDatepickerControl<D>, S,
if (!this.datepickerInput && (typeof ngDevMode === 'undefined' || ngDevMode)) {
throw Error('Attempted to open an MatDatepicker with no associated input.');
}
if (this._document) {
this._focusedElementBeforeOpen = this._document.activeElement;
}

// 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: HTMLElement|null = this._document?.activeElement;
this._focusedElementBeforeOpen =
activeElement?.shadowRoot?.activeElement as HTMLElement || activeElement;
this.touchUi ? this._openAsDialog() : this._openAsPopup();
this._opened = true;
this.openedStream.emit();
Expand Down
41 changes: 40 additions & 1 deletion src/material/datepicker/datepicker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
dispatchKeyboardEvent,
dispatchMouseEvent,
} from '@angular/cdk/testing/private';
import {Component, Type, ViewChild, Provider, Directive} from '@angular/core';
import {Component, Type, ViewChild, Provider, Directive, ViewEncapsulation} from '@angular/core';
import {ComponentFixture, fakeAsync, flush, inject, TestBed, tick} from '@angular/core/testing';
import {
FormControl,
Expand All @@ -27,6 +27,7 @@ import {
import {MatFormField, MatFormFieldModule} from '@angular/material/form-field';
import {DEC, JAN, JUL, JUN, SEP} from '@angular/material/testing';
import {By} from '@angular/platform-browser';
import {_supportsShadowDom} from '@angular/cdk/platform';
import {BrowserDynamicTestingModule} from '@angular/platform-browser-dynamic/testing';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {MAT_DIALOG_DEFAULT_OPTIONS, MatDialogConfig} from '@angular/material/dialog';
Expand Down Expand Up @@ -1139,6 +1140,33 @@ describe('MatDatepicker', () => {
expect(document.activeElement).toBe(toggle, 'Expected focus to be restored to toggle.');
});

it('should restore focus when placed inside a shadow root', () => {
if (!_supportsShadowDom()) {
return;
}

fixture.destroy();
TestBed.resetTestingModule();
fixture = createComponent(DatepickerWithToggleInShadowDom, [MatNativeDateModule]);
fixture.detectChanges();
testComponent = fixture.componentInstance;

const toggle = fixture.debugElement.query(By.css('button'))!.nativeElement;
fixture.componentInstance.touchUI = false;
fixture.detectChanges();

toggle.focus();
spyOn(toggle, 'focus').and.callThrough();
fixture.componentInstance.datepicker.open();
fixture.detectChanges();
fixture.componentInstance.datepicker.close();
fixture.detectChanges();

// We have to assert by looking at the `focus` method, because
// `document.activeElement` will return the shadow root.
expect(toggle.focus).toHaveBeenCalled();
});

it('should allow for focus restoration to be disabled', () => {
let toggle = fixture.debugElement.query(By.css('button'))!.nativeElement;

Expand Down Expand Up @@ -2352,6 +2380,17 @@ class DatepickerWithToggle {
}


@Component({
encapsulation: ViewEncapsulation.ShadowDom,
template: `
<input [matDatepicker]="d">
<mat-datepicker-toggle [for]="d" [aria-label]="ariaLabel"></mat-datepicker-toggle>
<mat-datepicker #d [touchUi]="touchUI" [restoreFocus]="restoreFocus"></mat-datepicker>
`,
})
class DatepickerWithToggleInShadowDom extends DatepickerWithToggle {}


@Component({
template: `
<input [matDatepicker]="d">
Expand Down

0 comments on commit 85d8de9

Please sign in to comment.