Skip to content

Commit 85d8de9

Browse files
authored
fix(material/datepicker): focus restoration not working inside shadow 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.
1 parent 915791b commit 85d8de9

File tree

3 files changed

+46
-4
lines changed

3 files changed

+46
-4
lines changed

src/material/datepicker/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ ng_test_library(
9797
"//src/cdk/bidi",
9898
"//src/cdk/keycodes",
9999
"//src/cdk/overlay",
100+
"//src/cdk/platform",
100101
"//src/cdk/scrolling",
101102
"//src/cdk/testing/private",
102103
"//src/material/core",

src/material/datepicker/datepicker-base.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -550,10 +550,12 @@ export abstract class MatDatepickerBase<C extends MatDatepickerControl<D>, S,
550550
if (!this.datepickerInput && (typeof ngDevMode === 'undefined' || ngDevMode)) {
551551
throw Error('Attempted to open an MatDatepicker with no associated input.');
552552
}
553-
if (this._document) {
554-
this._focusedElementBeforeOpen = this._document.activeElement;
555-
}
556553

554+
// If the `activeElement` is inside a shadow root, `document.activeElement` will
555+
// point to the shadow root so we have to descend into it ourselves.
556+
const activeElement: HTMLElement|null = this._document?.activeElement;
557+
this._focusedElementBeforeOpen =
558+
activeElement?.shadowRoot?.activeElement as HTMLElement || activeElement;
557559
this.touchUi ? this._openAsDialog() : this._openAsPopup();
558560
this._opened = true;
559561
this.openedStream.emit();

src/material/datepicker/datepicker.spec.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
dispatchKeyboardEvent,
1010
dispatchMouseEvent,
1111
} from '@angular/cdk/testing/private';
12-
import {Component, Type, ViewChild, Provider, Directive} from '@angular/core';
12+
import {Component, Type, ViewChild, Provider, Directive, ViewEncapsulation} from '@angular/core';
1313
import {ComponentFixture, fakeAsync, flush, inject, TestBed, tick} from '@angular/core/testing';
1414
import {
1515
FormControl,
@@ -27,6 +27,7 @@ import {
2727
import {MatFormField, MatFormFieldModule} from '@angular/material/form-field';
2828
import {DEC, JAN, JUL, JUN, SEP} from '@angular/material/testing';
2929
import {By} from '@angular/platform-browser';
30+
import {_supportsShadowDom} from '@angular/cdk/platform';
3031
import {BrowserDynamicTestingModule} from '@angular/platform-browser-dynamic/testing';
3132
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
3233
import {MAT_DIALOG_DEFAULT_OPTIONS, MatDialogConfig} from '@angular/material/dialog';
@@ -1139,6 +1140,33 @@ describe('MatDatepicker', () => {
11391140
expect(document.activeElement).toBe(toggle, 'Expected focus to be restored to toggle.');
11401141
});
11411142

1143+
it('should restore focus when placed inside a shadow root', () => {
1144+
if (!_supportsShadowDom()) {
1145+
return;
1146+
}
1147+
1148+
fixture.destroy();
1149+
TestBed.resetTestingModule();
1150+
fixture = createComponent(DatepickerWithToggleInShadowDom, [MatNativeDateModule]);
1151+
fixture.detectChanges();
1152+
testComponent = fixture.componentInstance;
1153+
1154+
const toggle = fixture.debugElement.query(By.css('button'))!.nativeElement;
1155+
fixture.componentInstance.touchUI = false;
1156+
fixture.detectChanges();
1157+
1158+
toggle.focus();
1159+
spyOn(toggle, 'focus').and.callThrough();
1160+
fixture.componentInstance.datepicker.open();
1161+
fixture.detectChanges();
1162+
fixture.componentInstance.datepicker.close();
1163+
fixture.detectChanges();
1164+
1165+
// We have to assert by looking at the `focus` method, because
1166+
// `document.activeElement` will return the shadow root.
1167+
expect(toggle.focus).toHaveBeenCalled();
1168+
});
1169+
11421170
it('should allow for focus restoration to be disabled', () => {
11431171
let toggle = fixture.debugElement.query(By.css('button'))!.nativeElement;
11441172

@@ -2352,6 +2380,17 @@ class DatepickerWithToggle {
23522380
}
23532381

23542382

2383+
@Component({
2384+
encapsulation: ViewEncapsulation.ShadowDom,
2385+
template: `
2386+
<input [matDatepicker]="d">
2387+
<mat-datepicker-toggle [for]="d" [aria-label]="ariaLabel"></mat-datepicker-toggle>
2388+
<mat-datepicker #d [touchUi]="touchUI" [restoreFocus]="restoreFocus"></mat-datepicker>
2389+
`,
2390+
})
2391+
class DatepickerWithToggleInShadowDom extends DatepickerWithToggle {}
2392+
2393+
23552394
@Component({
23562395
template: `
23572396
<input [matDatepicker]="d">

0 commit comments

Comments
 (0)