diff --git a/src/cdk/a11y/focus-trap/focus-trap.ts b/src/cdk/a11y/focus-trap/focus-trap.ts index faa4b04ccc9a..2d91993ab7eb 100644 --- a/src/cdk/a11y/focus-trap/focus-trap.ts +++ b/src/cdk/a11y/focus-trap/focus-trap.ts @@ -7,6 +7,7 @@ */ import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion'; +import {_getFocusedElementPierceShadowDom} from '@angular/cdk/platform'; import {DOCUMENT} from '@angular/common'; import { AfterContentInit, @@ -388,8 +389,6 @@ export class FocusTrapFactory { exportAs: 'cdkTrapFocus', }) export class CdkTrapFocus implements OnDestroy, AfterContentInit, OnChanges, DoCheck { - private _document: Document; - /** Underlying FocusTrap instance. */ focusTrap: FocusTrap; @@ -413,9 +412,11 @@ export class CdkTrapFocus implements OnDestroy, AfterContentInit, OnChanges, DoC constructor( private _elementRef: ElementRef, private _focusTrapFactory: FocusTrapFactory, + /** + * @deprecated No longer being used. To be removed. + * @breaking-change 13.0.0 + */ @Inject(DOCUMENT) _document: any) { - - this._document = _document; this.focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement, true); } @@ -454,11 +455,7 @@ export class CdkTrapFocus implements OnDestroy, AfterContentInit, OnChanges, DoC } private _captureFocus() { - // 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 as HTMLElement|null; - this._previouslyFocusedElement = - activeElement?.shadowRoot?.activeElement as HTMLElement || activeElement; + this._previouslyFocusedElement = _getFocusedElementPierceShadowDom(); this.focusTrap.focusInitialElementWhenReady(); } diff --git a/src/cdk/platform/features/shadow-dom.ts b/src/cdk/platform/features/shadow-dom.ts index 005fe7b25127..2b9a99761ea7 100644 --- a/src/cdk/platform/features/shadow-dom.ts +++ b/src/cdk/platform/features/shadow-dom.ts @@ -32,3 +32,23 @@ export function _getShadowRoot(element: HTMLElement): ShadowRoot | null { return null; } + +/** + * Gets the currently-focused element on the page while + * also piercing through Shadow DOM boundaries. + */ +export function _getFocusedElementPierceShadowDom(): HTMLElement | null { + let activeElement = typeof document !== 'undefined' && document ? + document.activeElement as HTMLElement | null : null; + + while (activeElement && activeElement.shadowRoot) { + const newActiveElement = activeElement.shadowRoot.activeElement as HTMLElement | null; + if (newActiveElement === activeElement) { + break; + } else { + activeElement = newActiveElement; + } + } + + return activeElement; +} diff --git a/src/material/bottom-sheet/BUILD.bazel b/src/material/bottom-sheet/BUILD.bazel index 29b3104efa5a..51b7388a9e41 100644 --- a/src/material/bottom-sheet/BUILD.bazel +++ b/src/material/bottom-sheet/BUILD.bazel @@ -27,6 +27,7 @@ ng_module( "//src/cdk/keycodes", "//src/cdk/layout", "//src/cdk/overlay", + "//src/cdk/platform", "//src/cdk/portal", "//src/material/core", "@npm//@angular/animations", diff --git a/src/material/bottom-sheet/bottom-sheet-container.ts b/src/material/bottom-sheet/bottom-sheet-container.ts index c0caf5ce9cf5..45366617cce3 100644 --- a/src/material/bottom-sheet/bottom-sheet-container.ts +++ b/src/material/bottom-sheet/bottom-sheet-container.ts @@ -34,6 +34,7 @@ import {matBottomSheetAnimations} from './bottom-sheet-animations'; import {Subscription} from 'rxjs'; import {DOCUMENT} from '@angular/common'; import {FocusTrap, FocusTrapFactory} from '@angular/cdk/a11y'; +import {_getFocusedElementPierceShadowDom} from '@angular/cdk/platform'; // TODO(crisbeto): consolidate some logic between this, MatDialog and MatSnackBar @@ -207,7 +208,7 @@ export class MatBottomSheetContainer extends BasePortalOutlet implements OnDestr if (this.bottomSheetConfig.autoFocus) { this._focusTrap.focusInitialElementWhenReady(); } else { - const activeElement = this._getActiveElement(); + const activeElement = _getFocusedElementPierceShadowDom(); // 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: @@ -226,7 +227,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._getActiveElement(); + const activeElement = _getFocusedElementPierceShadowDom(); const element = this._elementRef.nativeElement; // Make sure that focus is still inside the bottom sheet or is on the body (usually because a @@ -246,19 +247,11 @@ 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._getActiveElement(); + this._elementFocusedBeforeOpened = _getFocusedElementPierceShadowDom(); // 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; - } } diff --git a/src/material/datepicker/BUILD.bazel b/src/material/datepicker/BUILD.bazel index cb8d9f65b696..bafe443428a2 100644 --- a/src/material/datepicker/BUILD.bazel +++ b/src/material/datepicker/BUILD.bazel @@ -32,6 +32,7 @@ ng_module( "//src/cdk/coercion", "//src/cdk/keycodes", "//src/cdk/overlay", + "//src/cdk/platform", "//src/cdk/portal", "//src/material/button", "//src/material/core", diff --git a/src/material/datepicker/datepicker-base.ts b/src/material/datepicker/datepicker-base.ts index d40c793aa64e..dad3feb5bd23 100644 --- a/src/material/datepicker/datepicker-base.ts +++ b/src/material/datepicker/datepicker-base.ts @@ -50,6 +50,7 @@ import { } from '@angular/material/core'; import {merge, Subject, Observable, Subscription} from 'rxjs'; import {filter, take} from 'rxjs/operators'; +import {_getFocusedElementPierceShadowDom} from '@angular/cdk/platform'; import {MatCalendar, MatCalendarView} from './calendar'; import {matDatepickerAnimations} from './datepicker-animations'; import {createMissingDateImplError} from './datepicker-errors'; @@ -452,7 +453,11 @@ export abstract class MatDatepickerBase, S, @Inject(MAT_DATEPICKER_SCROLL_STRATEGY) scrollStrategy: any, @Optional() private _dateAdapter: DateAdapter, @Optional() private _dir: Directionality, - @Optional() @Inject(DOCUMENT) private _document: any, + /** + * @deprecated No longer being used. To be removed. + * @breaking-change 13.0.0 + */ + @Optional() @Inject(DOCUMENT) _document: any, private _model: MatDateSelectionModel) { if (!this._dateAdapter && (typeof ngDevMode === 'undefined' || ngDevMode)) { throw createMissingDateImplError('DateAdapter'); @@ -553,11 +558,7 @@ export abstract class MatDatepickerBase, S, throw Error('Attempted to open an MatDatepicker with no associated input.'); } - // 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._focusedElementBeforeOpen = _getFocusedElementPierceShadowDom(); this._openOverlay(); this._opened = true; this.openedStream.emit(); diff --git a/src/material/dialog/BUILD.bazel b/src/material/dialog/BUILD.bazel index 2c9a4e88d846..f59918d1c25e 100644 --- a/src/material/dialog/BUILD.bazel +++ b/src/material/dialog/BUILD.bazel @@ -26,6 +26,7 @@ ng_module( "//src/cdk/bidi", "//src/cdk/keycodes", "//src/cdk/overlay", + "//src/cdk/platform", "//src/cdk/portal", "//src/material/core", "@npm//@angular/animations", diff --git a/src/material/dialog/dialog-container.ts b/src/material/dialog/dialog-container.ts index 32088f65589e..5e5ed7ad90e5 100644 --- a/src/material/dialog/dialog-container.ts +++ b/src/material/dialog/dialog-container.ts @@ -8,6 +8,7 @@ import {AnimationEvent} from '@angular/animations'; import {FocusMonitor, FocusOrigin, FocusTrap, FocusTrapFactory} from '@angular/cdk/a11y'; +import {_getFocusedElementPierceShadowDom} from '@angular/cdk/platform'; import { BasePortalOutlet, CdkPortalOutlet, @@ -182,7 +183,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._getActiveElement(); + const activeElement = _getFocusedElementPierceShadowDom(); const element = this._elementRef.nativeElement; // Make sure that focus is still inside the dialog or is on the body (usually because a @@ -213,7 +214,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._getActiveElement() as HTMLElement; + this._elementFocusedBeforeDialogWasOpened = _getFocusedElementPierceShadowDom(); } } @@ -228,17 +229,9 @@ export abstract class _MatDialogContainerBase extends BasePortalOutlet { /** Returns whether focus is inside the dialog. */ private _containsFocus() { const element = this._elementRef.nativeElement; - const activeElement = this._getActiveElement(); + const activeElement = _getFocusedElementPierceShadowDom(); 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; - } } /** diff --git a/tools/public_api_guard/cdk/a11y.d.ts b/tools/public_api_guard/cdk/a11y.d.ts index 0c3aa4af16a4..55b1da2eb1a2 100644 --- a/tools/public_api_guard/cdk/a11y.d.ts +++ b/tools/public_api_guard/cdk/a11y.d.ts @@ -51,7 +51,8 @@ export declare class CdkTrapFocus implements OnDestroy, AfterContentInit, OnChan get enabled(): boolean; set enabled(value: boolean); focusTrap: FocusTrap; - constructor(_elementRef: ElementRef, _focusTrapFactory: FocusTrapFactory, _document: any); + constructor(_elementRef: ElementRef, _focusTrapFactory: FocusTrapFactory, + _document: any); ngAfterContentInit(): void; ngDoCheck(): void; ngOnChanges(changes: SimpleChanges): void; diff --git a/tools/public_api_guard/cdk/platform.d.ts b/tools/public_api_guard/cdk/platform.d.ts index c6d3edd7bb28..b71e8603a1c5 100644 --- a/tools/public_api_guard/cdk/platform.d.ts +++ b/tools/public_api_guard/cdk/platform.d.ts @@ -1,3 +1,5 @@ +export declare function _getFocusedElementPierceShadowDom(): HTMLElement | null; + export declare function _getShadowRoot(element: HTMLElement): ShadowRoot | null; export declare function _supportsShadowDom(): boolean;