Skip to content

Commit 25665dc

Browse files
authored
refactor(cdk/platform): add common utility for resolving focused element (#22708)
Based on a discussion on an earlier PR, these changes move the logic for resolving the `activeElement` while piercing through the shadow DOM into a common helper. Furthermore, they expand the logic to pierce through multiple layers of shadow DOM.
1 parent 561a38d commit 25665dc

File tree

10 files changed

+48
-38
lines changed

10 files changed

+48
-38
lines changed

src/cdk/a11y/focus-trap/focus-trap.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
10+
import {_getFocusedElementPierceShadowDom} from '@angular/cdk/platform';
1011
import {DOCUMENT} from '@angular/common';
1112
import {
1213
AfterContentInit,
@@ -388,8 +389,6 @@ export class FocusTrapFactory {
388389
exportAs: 'cdkTrapFocus',
389390
})
390391
export class CdkTrapFocus implements OnDestroy, AfterContentInit, OnChanges, DoCheck {
391-
private _document: Document;
392-
393392
/** Underlying FocusTrap instance. */
394393
focusTrap: FocusTrap;
395394

@@ -413,9 +412,11 @@ export class CdkTrapFocus implements OnDestroy, AfterContentInit, OnChanges, DoC
413412
constructor(
414413
private _elementRef: ElementRef<HTMLElement>,
415414
private _focusTrapFactory: FocusTrapFactory,
415+
/**
416+
* @deprecated No longer being used. To be removed.
417+
* @breaking-change 13.0.0
418+
*/
416419
@Inject(DOCUMENT) _document: any) {
417-
418-
this._document = _document;
419420
this.focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement, true);
420421
}
421422

@@ -454,11 +455,7 @@ export class CdkTrapFocus implements OnDestroy, AfterContentInit, OnChanges, DoC
454455
}
455456

456457
private _captureFocus() {
457-
// If the `activeElement` is inside a shadow root, `document.activeElement` will
458-
// point to the shadow root so we have to descend into it ourselves.
459-
const activeElement = this._document?.activeElement as HTMLElement|null;
460-
this._previouslyFocusedElement =
461-
activeElement?.shadowRoot?.activeElement as HTMLElement || activeElement;
458+
this._previouslyFocusedElement = _getFocusedElementPierceShadowDom();
462459
this.focusTrap.focusInitialElementWhenReady();
463460
}
464461

src/cdk/platform/features/shadow-dom.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,23 @@ export function _getShadowRoot(element: HTMLElement): ShadowRoot | null {
3232

3333
return null;
3434
}
35+
36+
/**
37+
* Gets the currently-focused element on the page while
38+
* also piercing through Shadow DOM boundaries.
39+
*/
40+
export function _getFocusedElementPierceShadowDom(): HTMLElement | null {
41+
let activeElement = typeof document !== 'undefined' && document ?
42+
document.activeElement as HTMLElement | null : null;
43+
44+
while (activeElement && activeElement.shadowRoot) {
45+
const newActiveElement = activeElement.shadowRoot.activeElement as HTMLElement | null;
46+
if (newActiveElement === activeElement) {
47+
break;
48+
} else {
49+
activeElement = newActiveElement;
50+
}
51+
}
52+
53+
return activeElement;
54+
}

src/material/bottom-sheet/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ ng_module(
2727
"//src/cdk/keycodes",
2828
"//src/cdk/layout",
2929
"//src/cdk/overlay",
30+
"//src/cdk/platform",
3031
"//src/cdk/portal",
3132
"//src/material/core",
3233
"@npm//@angular/animations",

src/material/bottom-sheet/bottom-sheet-container.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {matBottomSheetAnimations} from './bottom-sheet-animations';
3434
import {Subscription} from 'rxjs';
3535
import {DOCUMENT} from '@angular/common';
3636
import {FocusTrap, FocusTrapFactory} from '@angular/cdk/a11y';
37+
import {_getFocusedElementPierceShadowDom} from '@angular/cdk/platform';
3738

3839
// TODO(crisbeto): consolidate some logic between this, MatDialog and MatSnackBar
3940

@@ -207,7 +208,7 @@ export class MatBottomSheetContainer extends BasePortalOutlet implements OnDestr
207208
if (this.bottomSheetConfig.autoFocus) {
208209
this._focusTrap.focusInitialElementWhenReady();
209210
} else {
210-
const activeElement = this._getActiveElement();
211+
const activeElement = _getFocusedElementPierceShadowDom();
211212

212213
// Otherwise ensure that focus is on the container. It's possible that a different
213214
// component tried to move focus while the open animation was running. See:
@@ -226,7 +227,7 @@ export class MatBottomSheetContainer extends BasePortalOutlet implements OnDestr
226227

227228
// We need the extra check, because IE can set the `activeElement` to null in some cases.
228229
if (this.bottomSheetConfig.restoreFocus && toFocus && typeof toFocus.focus === 'function') {
229-
const activeElement = this._getActiveElement();
230+
const activeElement = _getFocusedElementPierceShadowDom();
230231
const element = this._elementRef.nativeElement;
231232

232233
// 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
246247

247248
/** Saves a reference to the element that was focused before the bottom sheet was opened. */
248249
private _savePreviouslyFocusedElement() {
249-
this._elementFocusedBeforeOpened = this._getActiveElement();
250+
this._elementFocusedBeforeOpened = _getFocusedElementPierceShadowDom();
250251

251252
// The `focus` method isn't available during server-side rendering.
252253
if (this._elementRef.nativeElement.focus) {
253254
Promise.resolve().then(() => this._elementRef.nativeElement.focus());
254255
}
255256
}
256-
257-
/** Gets the currently-focused element on the page. */
258-
private _getActiveElement(): HTMLElement | null {
259-
// If the `activeElement` is inside a shadow root, `document.activeElement` will
260-
// point to the shadow root so we have to descend into it ourselves.
261-
const activeElement = this._document.activeElement;
262-
return activeElement?.shadowRoot?.activeElement as HTMLElement || activeElement;
263-
}
264257
}

src/material/datepicker/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ ng_module(
3232
"//src/cdk/coercion",
3333
"//src/cdk/keycodes",
3434
"//src/cdk/overlay",
35+
"//src/cdk/platform",
3536
"//src/cdk/portal",
3637
"//src/material/button",
3738
"//src/material/core",

src/material/datepicker/datepicker-base.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import {
5050
} from '@angular/material/core';
5151
import {merge, Subject, Observable, Subscription} from 'rxjs';
5252
import {filter, take} from 'rxjs/operators';
53+
import {_getFocusedElementPierceShadowDom} from '@angular/cdk/platform';
5354
import {MatCalendar, MatCalendarView} from './calendar';
5455
import {matDatepickerAnimations} from './datepicker-animations';
5556
import {createMissingDateImplError} from './datepicker-errors';
@@ -452,7 +453,11 @@ export abstract class MatDatepickerBase<C extends MatDatepickerControl<D>, S,
452453
@Inject(MAT_DATEPICKER_SCROLL_STRATEGY) scrollStrategy: any,
453454
@Optional() private _dateAdapter: DateAdapter<D>,
454455
@Optional() private _dir: Directionality,
455-
@Optional() @Inject(DOCUMENT) private _document: any,
456+
/**
457+
* @deprecated No longer being used. To be removed.
458+
* @breaking-change 13.0.0
459+
*/
460+
@Optional() @Inject(DOCUMENT) _document: any,
456461
private _model: MatDateSelectionModel<S, D>) {
457462
if (!this._dateAdapter && (typeof ngDevMode === 'undefined' || ngDevMode)) {
458463
throw createMissingDateImplError('DateAdapter');
@@ -553,11 +558,7 @@ export abstract class MatDatepickerBase<C extends MatDatepickerControl<D>, S,
553558
throw Error('Attempted to open an MatDatepicker with no associated input.');
554559
}
555560

556-
// If the `activeElement` is inside a shadow root, `document.activeElement` will
557-
// point to the shadow root so we have to descend into it ourselves.
558-
const activeElement: HTMLElement|null = this._document?.activeElement;
559-
this._focusedElementBeforeOpen =
560-
activeElement?.shadowRoot?.activeElement as HTMLElement || activeElement;
561+
this._focusedElementBeforeOpen = _getFocusedElementPierceShadowDom();
561562
this._openOverlay();
562563
this._opened = true;
563564
this.openedStream.emit();

src/material/dialog/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ ng_module(
2626
"//src/cdk/bidi",
2727
"//src/cdk/keycodes",
2828
"//src/cdk/overlay",
29+
"//src/cdk/platform",
2930
"//src/cdk/portal",
3031
"//src/material/core",
3132
"@npm//@angular/animations",

src/material/dialog/dialog-container.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import {AnimationEvent} from '@angular/animations';
1010
import {FocusMonitor, FocusOrigin, FocusTrap, FocusTrapFactory} from '@angular/cdk/a11y';
11+
import {_getFocusedElementPierceShadowDom} from '@angular/cdk/platform';
1112
import {
1213
BasePortalOutlet,
1314
CdkPortalOutlet,
@@ -182,7 +183,7 @@ export abstract class _MatDialogContainerBase extends BasePortalOutlet {
182183
// We need the extra check, because IE can set the `activeElement` to null in some cases.
183184
if (this._config.restoreFocus && previousElement &&
184185
typeof previousElement.focus === 'function') {
185-
const activeElement = this._getActiveElement();
186+
const activeElement = _getFocusedElementPierceShadowDom();
186187
const element = this._elementRef.nativeElement;
187188

188189
// 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 {
213214
/** Captures the element that was focused before the dialog was opened. */
214215
private _capturePreviouslyFocusedElement() {
215216
if (this._document) {
216-
this._elementFocusedBeforeDialogWasOpened = this._getActiveElement() as HTMLElement;
217+
this._elementFocusedBeforeDialogWasOpened = _getFocusedElementPierceShadowDom();
217218
}
218219
}
219220

@@ -228,17 +229,9 @@ export abstract class _MatDialogContainerBase extends BasePortalOutlet {
228229
/** Returns whether focus is inside the dialog. */
229230
private _containsFocus() {
230231
const element = this._elementRef.nativeElement;
231-
const activeElement = this._getActiveElement();
232+
const activeElement = _getFocusedElementPierceShadowDom();
232233
return element === activeElement || element.contains(activeElement);
233234
}
234-
235-
/** Gets the currently-focused element on the page. */
236-
private _getActiveElement(): Element | null {
237-
// If the `activeElement` is inside a shadow root, `document.activeElement` will
238-
// point to the shadow root so we have to descend into it ourselves.
239-
const activeElement = this._document.activeElement;
240-
return activeElement?.shadowRoot?.activeElement as HTMLElement || activeElement;
241-
}
242235
}
243236

244237
/**

tools/public_api_guard/cdk/a11y.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ export declare class CdkTrapFocus implements OnDestroy, AfterContentInit, OnChan
5151
get enabled(): boolean;
5252
set enabled(value: boolean);
5353
focusTrap: FocusTrap;
54-
constructor(_elementRef: ElementRef<HTMLElement>, _focusTrapFactory: FocusTrapFactory, _document: any);
54+
constructor(_elementRef: ElementRef<HTMLElement>, _focusTrapFactory: FocusTrapFactory,
55+
_document: any);
5556
ngAfterContentInit(): void;
5657
ngDoCheck(): void;
5758
ngOnChanges(changes: SimpleChanges): void;

tools/public_api_guard/cdk/platform.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
export declare function _getFocusedElementPierceShadowDom(): HTMLElement | null;
2+
13
export declare function _getShadowRoot(element: HTMLElement): ShadowRoot | null;
24

35
export declare function _supportsShadowDom(): boolean;

0 commit comments

Comments
 (0)