diff --git a/src/material/datepicker/BUILD.bazel b/src/material/datepicker/BUILD.bazel index 57a63125da62..133f8db6f14e 100644 --- a/src/material/datepicker/BUILD.bazel +++ b/src/material/datepicker/BUILD.bazel @@ -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", diff --git a/src/material/datepicker/datepicker-base.ts b/src/material/datepicker/datepicker-base.ts index b2601c60543a..676a5aa81599 100644 --- a/src/material/datepicker/datepicker-base.ts +++ b/src/material/datepicker/datepicker-base.ts @@ -550,10 +550,12 @@ export abstract class MatDatepickerBase, 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(); diff --git a/src/material/datepicker/datepicker.spec.ts b/src/material/datepicker/datepicker.spec.ts index 1640c80466e8..6714e3d58346 100644 --- a/src/material/datepicker/datepicker.spec.ts +++ b/src/material/datepicker/datepicker.spec.ts @@ -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, @@ -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'; @@ -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; @@ -2352,6 +2380,17 @@ class DatepickerWithToggle { } +@Component({ + encapsulation: ViewEncapsulation.ShadowDom, + template: ` + + + + `, +}) +class DatepickerWithToggleInShadowDom extends DatepickerWithToggle {} + + @Component({ template: `