Skip to content

refactor(cdk/platform): add utility for resolving event targets in shadow dom #23121

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 1 commit into from
Jul 9, 2021
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
3 changes: 2 additions & 1 deletion src/cdk-experimental/combobox/combobox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
} from '@angular/cdk/overlay';
import {Directionality} from '@angular/cdk/bidi';
import {BooleanInput, coerceBooleanProperty, coerceArray} from '@angular/cdk/coercion';
import {_getEventTarget} from '@angular/cdk/platform';
import {DOWN_ARROW, ENTER, ESCAPE, TAB} from '@angular/cdk/keycodes';

const allowedOpenActions = ['focus', 'click', 'downKey', 'toggle'];
Expand Down Expand Up @@ -165,7 +166,7 @@ export class CdkCombobox<T = unknown> implements OnDestroy, AfterContentInit {
/** Given a click in the document, determines if the click was inside a combobox. */
_attemptClose(event: MouseEvent) {
if (this.isOpen()) {
let target = event.composedPath ? event.composedPath()[0] : event.target;
let target = _getEventTarget(event);
while (target instanceof Element) {
if (target.className.indexOf('cdk-combobox') !== -1) {
return;
Expand Down
12 changes: 8 additions & 4 deletions src/cdk/a11y/focus-monitor/focus-monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
* found in the LICENSE file at https://angular.io/license
*/

import {Platform, normalizePassiveListenerOptions, _getShadowRoot} from '@angular/cdk/platform';
import {
Platform,
normalizePassiveListenerOptions,
_getShadowRoot,
_getEventTarget,
} from '@angular/cdk/platform';
import {
Directive,
ElementRef,
Expand All @@ -25,7 +30,6 @@ import {takeUntil} from 'rxjs/operators';
import {coerceElement} from '@angular/cdk/coercion';
import {DOCUMENT} from '@angular/common';
import {
getTarget,
InputModalityDetector,
TOUCH_BUFFER_MS,
} from '../input-modality/input-modality-detector';
Expand Down Expand Up @@ -159,7 +163,7 @@ export class FocusMonitor implements OnDestroy {
* Needs to be an arrow function in order to preserve the context when it gets bound.
*/
private _rootNodeFocusAndBlurListener = (event: Event) => {
const target = getTarget(event);
const target = _getEventTarget<HTMLElement>(event);
const handler = event.type === 'focus' ? this._onFocus : this._onBlur;

// We need to walk up the ancestor chain in order to support `checkChildren`.
Expand Down Expand Up @@ -410,7 +414,7 @@ export class FocusMonitor implements OnDestroy {
// If we are not counting child-element-focus as focused, make sure that the event target is the
// monitored element itself.
const elementInfo = this._elementInfo.get(element);
const focusEventTarget = getTarget(event);
const focusEventTarget = _getEventTarget<HTMLElement>(event);
if (!elementInfo || (!elementInfo.checkChildren && element !== focusEventTarget)) {
return;
}
Expand Down
15 changes: 4 additions & 11 deletions src/cdk/a11y/input-modality/input-modality-detector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import {ALT, CONTROL, MAC_META, META, SHIFT} from '@angular/cdk/keycodes';
import {Inject, Injectable, InjectionToken, OnDestroy, Optional, NgZone} from '@angular/core';
import {normalizePassiveListenerOptions, Platform} from '@angular/cdk/platform';
import {normalizePassiveListenerOptions, Platform, _getEventTarget} from '@angular/cdk/platform';
import {DOCUMENT} from '@angular/common';
import {BehaviorSubject, Observable} from 'rxjs';
import {distinctUntilChanged, skip} from 'rxjs/operators';
Expand Down Expand Up @@ -128,7 +128,7 @@ export class InputModalityDetector implements OnDestroy {
if (this._options?.ignoreKeys?.some(keyCode => keyCode === event.keyCode)) { return; }

this._modality.next('keyboard');
this._mostRecentTarget = getTarget(event);
this._mostRecentTarget = _getEventTarget(event);
}

/**
Expand All @@ -144,7 +144,7 @@ export class InputModalityDetector implements OnDestroy {
// Fake mousedown events are fired by some screen readers when controls are activated by the
// screen reader. Attribute them to keyboard input modality.
this._modality.next(isFakeMousedownFromScreenReader(event) ? 'keyboard' : 'mouse');
this._mostRecentTarget = getTarget(event);
this._mostRecentTarget = _getEventTarget(event);
}

/**
Expand All @@ -164,7 +164,7 @@ export class InputModalityDetector implements OnDestroy {
this._lastTouchMs = Date.now();

this._modality.next('touch');
this._mostRecentTarget = getTarget(event);
this._mostRecentTarget = _getEventTarget(event);
}

constructor(
Expand Down Expand Up @@ -203,10 +203,3 @@ export class InputModalityDetector implements OnDestroy {
document.removeEventListener('touchstart', this._onTouchstart, modalityEventListenerOptions);
}
}

/** Gets the target of an event, accounting for Shadow DOM. */
export function getTarget(event: Event): HTMLElement|null {
// If an event is bound outside the Shadow DOM, the `event.target` will
// point to the shadow root so we have to use `composedPath` instead.
return (event.composedPath ? event.composedPath()[0] : event.target) as HTMLElement | null;
}
16 changes: 10 additions & 6 deletions src/cdk/drag-drop/drag-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
import {EmbeddedViewRef, ElementRef, NgZone, ViewContainerRef, TemplateRef} from '@angular/core';
import {ViewportRuler} from '@angular/cdk/scrolling';
import {Direction} from '@angular/cdk/bidi';
import {normalizePassiveListenerOptions, _getShadowRoot} from '@angular/cdk/platform';
import {
normalizePassiveListenerOptions,
_getEventTarget,
_getShadowRoot,
} from '@angular/cdk/platform';
import {coerceBooleanProperty, coerceElement} from '@angular/cdk/coercion';
import {Subscription, Subject, Observable} from 'rxjs';
import {DropListRefInternal as DropListRef} from './drop-list-ref';
Expand All @@ -22,7 +26,7 @@ import {
} from './drag-styling';
import {getTransformTransitionDurationInMs} from './transition-duration';
import {getMutableClientRect, adjustClientRect} from './client-rect';
import {getEventTarget, ParentPositionTracker} from './parent-position-tracker';
import {ParentPositionTracker} from './parent-position-tracker';
import {deepCloneNode} from './clone-node';

/** Object that can be used to configure the behavior of DragRef. */
Expand Down Expand Up @@ -623,7 +627,7 @@ export class DragRef<T = any> {
// Delegate the event based on whether it started from a handle or the element itself.
if (this._handles.length) {
const targetHandle = this._handles.find(handle => {
const target = getEventTarget(event);
const target = _getEventTarget(event);
return !!target && (target === handle || handle.contains(target as HTMLElement));
});

Expand Down Expand Up @@ -851,7 +855,7 @@ export class DragRef<T = any> {
const isTouchSequence = isTouchEvent(event);
const isAuxiliaryMouseButton = !isTouchSequence && (event as MouseEvent).button !== 0;
const rootElement = this._rootElement;
const target = getEventTarget(event);
const target = _getEventTarget(event);
const isSyntheticEvent = !isTouchSequence && this._lastTouchEventTime &&
this._lastTouchEventTime + MOUSE_EVENT_IGNORE_TIME > Date.now();

Expand Down Expand Up @@ -1085,7 +1089,7 @@ export class DragRef<T = any> {
return this._ngZone.runOutsideAngular(() => {
return new Promise(resolve => {
const handler = ((event: TransitionEvent) => {
if (!event || (getEventTarget(event) === this._preview &&
if (!event || (_getEventTarget(event) === this._preview &&
event.propertyName === 'transform')) {
this._preview.removeEventListener('transitionend', handler);
resolve();
Expand Down Expand Up @@ -1381,7 +1385,7 @@ export class DragRef<T = any> {
const scrollDifference = this._parentPositions.handleScroll(event);

if (scrollDifference) {
const target = getEventTarget(event);
const target = _getEventTarget<HTMLElement|Document>(event)!;

// ClientRect dimensions are based on the scroll position of the page and its parent node so
// we have to update the cached boundary ClientRect if the user has scrolled. Check for
Expand Down
8 changes: 2 additions & 6 deletions src/cdk/drag-drop/parent-position-tracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import {ViewportRuler} from '@angular/cdk/scrolling';
import {_getEventTarget} from '@angular/cdk/platform';
import {getMutableClientRect, adjustClientRect} from './client-rect';

/** Object holding the scroll position of something. */
Expand Down Expand Up @@ -47,7 +48,7 @@ export class ParentPositionTracker {

/** Handles scrolling while a drag is taking place. */
handleScroll(event: Event): ScrollPosition | null {
const target = getEventTarget(event);
const target = _getEventTarget<HTMLElement|Document>(event)!;
const cachedPosition = this.positions.get(target);

if (!cachedPosition) {
Expand Down Expand Up @@ -88,8 +89,3 @@ export class ParentPositionTracker {
return {top: topDifference, left: leftDifference};
}
}

/** Gets the target of an event while accounting for shadow dom. */
export function getEventTarget(event: Event): HTMLElement | Document {
return (event.composedPath ? event.composedPath()[0] : event.target) as HTMLElement | Document;
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import {DOCUMENT} from '@angular/common';
import {Inject, Injectable} from '@angular/core';
import {OverlayReference} from '../overlay-reference';
import {Platform} from '@angular/cdk/platform';
import {Platform, _getEventTarget} from '@angular/cdk/platform';
import {BaseOverlayDispatcher} from './base-overlay-dispatcher';

/**
Expand Down Expand Up @@ -71,8 +71,7 @@ export class OverlayOutsideClickDispatcher extends BaseOverlayDispatcher {

/** Click event listener that will be attached to the body propagate phase. */
private _clickListener = (event: MouseEvent) => {
// Get the target through the `composedPath` if possible to account for shadow DOM.
const target = event.composedPath ? event.composedPath()[0] : event.target;
const target = _getEventTarget(event);
// We copy the array because the original may be modified asynchronously if the
// outsidePointerEvents listener decides to detach overlays resulting in index errors inside
// the for loop.
Expand Down
8 changes: 8 additions & 0 deletions src/cdk/platform/features/shadow-dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,11 @@ export function _getFocusedElementPierceShadowDom(): HTMLElement | null {

return activeElement;
}


/** Gets the target of an event while accounting for Shadow DOM. */
export function _getEventTarget<T extends EventTarget>(event: Event): T|null {
// If an event is bound outside the Shadow DOM, the `event.target` will
// point to the shadow root so we have to use `composedPath` instead.
return (event.composedPath ? event.composedPath()[0] : event.target) as T | null;
}
15 changes: 2 additions & 13 deletions src/material/autocomplete/autocomplete-trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
ScrollStrategy,
ConnectedPosition,
} from '@angular/cdk/overlay';
import {_getShadowRoot} from '@angular/cdk/platform';
import {_getEventTarget} from '@angular/cdk/platform';
import {TemplatePortal} from '@angular/cdk/portal';
import {ViewportRuler} from '@angular/cdk/scrolling';
import {DOCUMENT} from '@angular/common';
Expand Down Expand Up @@ -126,9 +126,6 @@ export abstract class _MatAutocompleteTriggerBase implements ControlValueAccesso
*/
private _canOpenOnNextFocus = true;

/** Whether the element is inside of a ShadowRoot component. */
private _isInsideShadowRoot: boolean;

/** Stream of keyboard events that can close the panel. */
private readonly _closeKeyEventStream = new Subject<void>();

Expand Down Expand Up @@ -334,9 +331,7 @@ export abstract class _MatAutocompleteTriggerBase implements ControlValueAccesso
.pipe(filter(event => {
// If we're in the Shadow DOM, the event target will be the shadow root, so we have to
// fall back to check the first element in the path of the click event.
const clickTarget =
(this._isInsideShadowRoot && event.composedPath ? event.composedPath()[0] :
event.target) as HTMLElement;
const clickTarget = _getEventTarget<HTMLElement>(event)!;
const formField = this._formField ? this._formField._elementRef.nativeElement : null;
const customOrigin = this.connectedTo ? this.connectedTo.elementRef.nativeElement : null;

Expand Down Expand Up @@ -563,12 +558,6 @@ export abstract class _MatAutocompleteTriggerBase implements ControlValueAccesso
throw getMatAutocompleteMissingPanelError();
}

// We want to resolve this once, as late as possible so that we can be
// sure that the element has been moved into its final place in the DOM.
if (this._isInsideShadowRoot == null) {
this._isInsideShadowRoot = !!_getShadowRoot(this._element.nativeElement);
}

let overlayRef = this._overlayRef;

if (!overlayRef) {
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 _getEventTarget<T extends EventTarget>(event: Event): T | null;

export declare function _getFocusedElementPierceShadowDom(): HTMLElement | null;

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