Skip to content

refactor(cdk/platform): add common utility for resolving focused element #22708

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 6 additions & 9 deletions src/cdk/a11y/focus-trap/focus-trap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
import {_getFocusedElementPierceShadowDom} from '@angular/cdk/platform';
import {DOCUMENT} from '@angular/common';
import {
AfterContentInit,
Expand Down Expand Up @@ -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;

Expand All @@ -413,9 +412,11 @@ export class CdkTrapFocus implements OnDestroy, AfterContentInit, OnChanges, DoC
constructor(
private _elementRef: ElementRef<HTMLElement>,
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);
}

Expand Down Expand Up @@ -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();
}

Expand Down
20 changes: 20 additions & 0 deletions src/cdk/platform/features/shadow-dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
1 change: 1 addition & 0 deletions src/material/bottom-sheet/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
15 changes: 4 additions & 11 deletions src/material/bottom-sheet/bottom-sheet-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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;
}
}
1 change: 1 addition & 0 deletions src/material/datepicker/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
13 changes: 7 additions & 6 deletions src/material/datepicker/datepicker-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -452,7 +453,11 @@ export abstract class MatDatepickerBase<C extends MatDatepickerControl<D>, S,
@Inject(MAT_DATEPICKER_SCROLL_STRATEGY) scrollStrategy: any,
@Optional() private _dateAdapter: DateAdapter<D>,
@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<S, D>) {
if (!this._dateAdapter && (typeof ngDevMode === 'undefined' || ngDevMode)) {
throw createMissingDateImplError('DateAdapter');
Expand Down Expand Up @@ -553,11 +558,7 @@ export abstract class MatDatepickerBase<C extends MatDatepickerControl<D>, 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();
Expand Down
1 change: 1 addition & 0 deletions src/material/dialog/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
15 changes: 4 additions & 11 deletions src/material/dialog/dialog-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
}
}

Expand All @@ -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;
}
}

/**
Expand Down
3 changes: 2 additions & 1 deletion tools/public_api_guard/cdk/a11y.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ export declare class CdkTrapFocus implements OnDestroy, AfterContentInit, OnChan
get enabled(): boolean;
set enabled(value: boolean);
focusTrap: FocusTrap;
constructor(_elementRef: ElementRef<HTMLElement>, _focusTrapFactory: FocusTrapFactory, _document: any);
constructor(_elementRef: ElementRef<HTMLElement>, _focusTrapFactory: FocusTrapFactory,
_document: any);
ngAfterContentInit(): void;
ngDoCheck(): void;
ngOnChanges(changes: SimpleChanges): void;
Expand Down
2 changes: 2 additions & 0 deletions tools/public_api_guard/cdk/platform.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export declare function _getFocusedElementPierceShadowDom(): HTMLElement | null;

export declare function _getShadowRoot(element: HTMLElement): ShadowRoot | null;

export declare function _supportsShadowDom(): boolean;
Expand Down