Skip to content

Commit 7fae1a9

Browse files
authored
refactor(cdk/platform): add utility for resolving event targets in shadow dom (#23121)
We were repeating the logic for resolving the event target while accounting for shadow DOM in a few places. These changes add a common utility instead.
1 parent 1e7b2f4 commit 7fae1a9

File tree

9 files changed

+40
-44
lines changed

9 files changed

+40
-44
lines changed

src/cdk-experimental/combobox/combobox.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
} from '@angular/cdk/overlay';
3232
import {Directionality} from '@angular/cdk/bidi';
3333
import {BooleanInput, coerceBooleanProperty, coerceArray} from '@angular/cdk/coercion';
34+
import {_getEventTarget} from '@angular/cdk/platform';
3435
import {DOWN_ARROW, ENTER, ESCAPE, TAB} from '@angular/cdk/keycodes';
3536

3637
const allowedOpenActions = ['focus', 'click', 'downKey', 'toggle'];
@@ -165,7 +166,7 @@ export class CdkCombobox<T = unknown> implements OnDestroy, AfterContentInit {
165166
/** Given a click in the document, determines if the click was inside a combobox. */
166167
_attemptClose(event: MouseEvent) {
167168
if (this.isOpen()) {
168-
let target = event.composedPath ? event.composedPath()[0] : event.target;
169+
let target = _getEventTarget(event);
169170
while (target instanceof Element) {
170171
if (target.className.indexOf('cdk-combobox') !== -1) {
171172
return;

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Platform, normalizePassiveListenerOptions, _getShadowRoot} from '@angular/cdk/platform';
9+
import {
10+
Platform,
11+
normalizePassiveListenerOptions,
12+
_getShadowRoot,
13+
_getEventTarget,
14+
} from '@angular/cdk/platform';
1015
import {
1116
Directive,
1217
ElementRef,
@@ -25,7 +30,6 @@ import {takeUntil} from 'rxjs/operators';
2530
import {coerceElement} from '@angular/cdk/coercion';
2631
import {DOCUMENT} from '@angular/common';
2732
import {
28-
getTarget,
2933
InputModalityDetector,
3034
TOUCH_BUFFER_MS,
3135
} from '../input-modality/input-modality-detector';
@@ -159,7 +163,7 @@ export class FocusMonitor implements OnDestroy {
159163
* Needs to be an arrow function in order to preserve the context when it gets bound.
160164
*/
161165
private _rootNodeFocusAndBlurListener = (event: Event) => {
162-
const target = getTarget(event);
166+
const target = _getEventTarget<HTMLElement>(event);
163167
const handler = event.type === 'focus' ? this._onFocus : this._onBlur;
164168

165169
// We need to walk up the ancestor chain in order to support `checkChildren`.
@@ -410,7 +414,7 @@ export class FocusMonitor implements OnDestroy {
410414
// If we are not counting child-element-focus as focused, make sure that the event target is the
411415
// monitored element itself.
412416
const elementInfo = this._elementInfo.get(element);
413-
const focusEventTarget = getTarget(event);
417+
const focusEventTarget = _getEventTarget<HTMLElement>(event);
414418
if (!elementInfo || (!elementInfo.checkChildren && element !== focusEventTarget)) {
415419
return;
416420
}

src/cdk/a11y/input-modality/input-modality-detector.ts

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

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

130130
this._modality.next('keyboard');
131-
this._mostRecentTarget = getTarget(event);
131+
this._mostRecentTarget = _getEventTarget(event);
132132
}
133133

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

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

166166
this._modality.next('touch');
167-
this._mostRecentTarget = getTarget(event);
167+
this._mostRecentTarget = _getEventTarget(event);
168168
}
169169

170170
constructor(
@@ -203,10 +203,3 @@ export class InputModalityDetector implements OnDestroy {
203203
document.removeEventListener('touchstart', this._onTouchstart, modalityEventListenerOptions);
204204
}
205205
}
206-
207-
/** Gets the target of an event, accounting for Shadow DOM. */
208-
export function getTarget(event: Event): HTMLElement|null {
209-
// If an event is bound outside the Shadow DOM, the `event.target` will
210-
// point to the shadow root so we have to use `composedPath` instead.
211-
return (event.composedPath ? event.composedPath()[0] : event.target) as HTMLElement | null;
212-
}

src/cdk/drag-drop/drag-ref.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@
99
import {EmbeddedViewRef, ElementRef, NgZone, ViewContainerRef, TemplateRef} from '@angular/core';
1010
import {ViewportRuler} from '@angular/cdk/scrolling';
1111
import {Direction} from '@angular/cdk/bidi';
12-
import {normalizePassiveListenerOptions, _getShadowRoot} from '@angular/cdk/platform';
12+
import {
13+
normalizePassiveListenerOptions,
14+
_getEventTarget,
15+
_getShadowRoot,
16+
} from '@angular/cdk/platform';
1317
import {coerceBooleanProperty, coerceElement} from '@angular/cdk/coercion';
1418
import {Subscription, Subject, Observable} from 'rxjs';
1519
import {DropListRefInternal as DropListRef} from './drop-list-ref';
@@ -22,7 +26,7 @@ import {
2226
} from './drag-styling';
2327
import {getTransformTransitionDurationInMs} from './transition-duration';
2428
import {getMutableClientRect, adjustClientRect} from './client-rect';
25-
import {getEventTarget, ParentPositionTracker} from './parent-position-tracker';
29+
import {ParentPositionTracker} from './parent-position-tracker';
2630
import {deepCloneNode} from './clone-node';
2731

2832
/** Object that can be used to configure the behavior of DragRef. */
@@ -629,7 +633,7 @@ export class DragRef<T = any> {
629633
// Delegate the event based on whether it started from a handle or the element itself.
630634
if (this._handles.length) {
631635
const targetHandle = this._handles.find(handle => {
632-
const target = getEventTarget(event);
636+
const target = _getEventTarget(event);
633637
return !!target && (target === handle || handle.contains(target as HTMLElement));
634638
});
635639

@@ -857,7 +861,7 @@ export class DragRef<T = any> {
857861
const isTouchSequence = isTouchEvent(event);
858862
const isAuxiliaryMouseButton = !isTouchSequence && (event as MouseEvent).button !== 0;
859863
const rootElement = this._rootElement;
860-
const target = getEventTarget(event);
864+
const target = _getEventTarget(event);
861865
const isSyntheticEvent = !isTouchSequence && this._lastTouchEventTime &&
862866
this._lastTouchEventTime + MOUSE_EVENT_IGNORE_TIME > Date.now();
863867

@@ -1091,7 +1095,7 @@ export class DragRef<T = any> {
10911095
return this._ngZone.runOutsideAngular(() => {
10921096
return new Promise(resolve => {
10931097
const handler = ((event: TransitionEvent) => {
1094-
if (!event || (getEventTarget(event) === this._preview &&
1098+
if (!event || (_getEventTarget(event) === this._preview &&
10951099
event.propertyName === 'transform')) {
10961100
this._preview.removeEventListener('transitionend', handler);
10971101
resolve();
@@ -1387,7 +1391,7 @@ export class DragRef<T = any> {
13871391
const scrollDifference = this._parentPositions.handleScroll(event);
13881392

13891393
if (scrollDifference) {
1390-
const target = getEventTarget(event);
1394+
const target = _getEventTarget<HTMLElement|Document>(event)!;
13911395

13921396
// ClientRect dimensions are based on the scroll position of the page and its parent node so
13931397
// we have to update the cached boundary ClientRect if the user has scrolled. Check for

src/cdk/drag-drop/parent-position-tracker.ts

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

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

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

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

5354
if (!cachedPosition) {
@@ -88,8 +89,3 @@ export class ParentPositionTracker {
8889
return {top: topDifference, left: leftDifference};
8990
}
9091
}
91-
92-
/** Gets the target of an event while accounting for shadow dom. */
93-
export function getEventTarget(event: Event): HTMLElement | Document {
94-
return (event.composedPath ? event.composedPath()[0] : event.target) as HTMLElement | Document;
95-
}

src/cdk/overlay/dispatchers/overlay-outside-click-dispatcher.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import {DOCUMENT} from '@angular/common';
1010
import {Inject, Injectable} from '@angular/core';
1111
import {OverlayReference} from '../overlay-reference';
12-
import {Platform} from '@angular/cdk/platform';
12+
import {Platform, _getEventTarget} from '@angular/cdk/platform';
1313
import {BaseOverlayDispatcher} from './base-overlay-dispatcher';
1414

1515
/**
@@ -71,8 +71,7 @@ export class OverlayOutsideClickDispatcher extends BaseOverlayDispatcher {
7171

7272
/** Click event listener that will be attached to the body propagate phase. */
7373
private _clickListener = (event: MouseEvent) => {
74-
// Get the target through the `composedPath` if possible to account for shadow DOM.
75-
const target = event.composedPath ? event.composedPath()[0] : event.target;
74+
const target = _getEventTarget(event);
7675
// We copy the array because the original may be modified asynchronously if the
7776
// outsidePointerEvents listener decides to detach overlays resulting in index errors inside
7877
// the for loop.

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,11 @@ export function _getFocusedElementPierceShadowDom(): HTMLElement | null {
5252

5353
return activeElement;
5454
}
55+
56+
57+
/** Gets the target of an event while accounting for Shadow DOM. */
58+
export function _getEventTarget<T extends EventTarget>(event: Event): T|null {
59+
// If an event is bound outside the Shadow DOM, the `event.target` will
60+
// point to the shadow root so we have to use `composedPath` instead.
61+
return (event.composedPath ? event.composedPath()[0] : event.target) as T | null;
62+
}

src/material/autocomplete/autocomplete-trigger.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
ScrollStrategy,
1818
ConnectedPosition,
1919
} from '@angular/cdk/overlay';
20-
import {_getShadowRoot} from '@angular/cdk/platform';
20+
import {_getEventTarget} from '@angular/cdk/platform';
2121
import {TemplatePortal} from '@angular/cdk/portal';
2222
import {ViewportRuler} from '@angular/cdk/scrolling';
2323
import {DOCUMENT} from '@angular/common';
@@ -126,9 +126,6 @@ export abstract class _MatAutocompleteTriggerBase implements ControlValueAccesso
126126
*/
127127
private _canOpenOnNextFocus = true;
128128

129-
/** Whether the element is inside of a ShadowRoot component. */
130-
private _isInsideShadowRoot: boolean;
131-
132129
/** Stream of keyboard events that can close the panel. */
133130
private readonly _closeKeyEventStream = new Subject<void>();
134131

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

@@ -563,12 +558,6 @@ export abstract class _MatAutocompleteTriggerBase implements ControlValueAccesso
563558
throw getMatAutocompleteMissingPanelError();
564559
}
565560

566-
// We want to resolve this once, as late as possible so that we can be
567-
// sure that the element has been moved into its final place in the DOM.
568-
if (this._isInsideShadowRoot == null) {
569-
this._isInsideShadowRoot = !!_getShadowRoot(this._element.nativeElement);
570-
}
571-
572561
let overlayRef = this._overlayRef;
573562

574563
if (!overlayRef) {

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 _getEventTarget<T extends EventTarget>(event: Event): T | null;
2+
13
export declare function _getFocusedElementPierceShadowDom(): HTMLElement | null;
24

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

0 commit comments

Comments
 (0)