Skip to content

Commit c78a58a

Browse files
committed
refactor(cdk/platform): add common utility for resolving focused element
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 8ba7148 commit c78a58a

File tree

9 files changed

+37
-32
lines changed

9 files changed

+37
-32
lines changed

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

Lines changed: 2 additions & 5 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,
@@ -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: 2 additions & 5 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';
@@ -553,11 +554,7 @@ export abstract class MatDatepickerBase<C extends MatDatepickerControl<D>, S,
553554
throw Error('Attempted to open an MatDatepicker with no associated input.');
554555
}
555556

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;
557+
this._focusedElementBeforeOpen = _getFocusedElementPierceShadowDom();
561558
this._openOverlay();
562559
this._opened = true;
563560
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/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)