@samitouri / QOS-React-2 / commits / a16b349745

ReactDOM.useEvent: Add support for experimental scopes API (#18375)

* ReactDOM.useEvent: Add support for experimental scopes API

Dominic Gannaway committed Mar 26, 2020 at 13:29 UTC a16b34974508cd23ce0844ad09a0e37a879d5591
7 files changed +307 -45
packages/react-debug-tools/src/ReactDebugHooks.js
+5 -1
@@ -15,6 +15,7 @@ import type {
15 ReactProviderType,
16 ReactEventResponder,
17 ReactEventResponderListener,
18 + ReactScopeMethods,
19 } from 'shared/ReactTypes';
20 import type {Fiber} from 'react-reconciler/src/ReactFiber';
21 import type {Hook, TimeoutConfig} from 'react-reconciler/src/ReactFiberHooks';
@@ -44,7 +45,10 @@ type HookLogEntry = {
45
46 type ReactDebugListenerMap = {|
47 clear: () => void,
47 - setListener: (target: EventTarget, callback: ?(Event) => void) => void,
48 + setListener: (
49 + target: EventTarget | ReactScopeMethods,
50 + callback: ?(Event) => void,
51 + ) => void,
52 |};
53
54 let hookLog: Array<HookLogEntry> = [];
packages/react-dom/src/client/ReactDOMHostConfig.js
+24 -13
@@ -9,6 +9,15 @@
9
10 import type {TopLevelType} from 'legacy-events/TopLevelEventTypes';
11 import type {RootType} from './ReactDOMRoot';
12 +import type {
13 + ReactDOMEventResponder,
14 + ReactDOMEventResponderInstance,
15 + ReactDOMFundamentalComponentInstance,
16 + ReactDOMListener,
17 + ReactDOMListenerEvent,
18 + ReactDOMListenerMap,
19 +} from '../shared/ReactDOMTypes';
20 +import type {ReactScopeMethods} from 'shared/ReactTypes';
21
22 import {
23 precacheFiberNode,
@@ -49,14 +58,6 @@ import {
58 } from '../shared/HTMLNodeType';
59 import dangerousStyleValue from '../shared/dangerousStyleValue';
60
52 -import type {
53 - ReactDOMEventResponder,
54 - ReactDOMEventResponderInstance,
55 - ReactDOMFundamentalComponentInstance,
56 - ReactDOMListener,
57 - ReactDOMListenerEvent,
58 - ReactDOMListenerMap,
59 -} from '../shared/ReactDOMTypes';
61 import {
62 mountEventResponder,
63 unmountEventResponder,
@@ -69,6 +70,7 @@ import {
70 enableDeprecatedFlareAPI,
71 enableFundamentalAPI,
72 enableUseEventAPI,
73 + enableScopeAPI,
74 } from 'shared/ReactFeatureFlags';
75 import {HostComponent} from 'react-reconciler/src/ReactWorkTags';
76 import {
@@ -79,10 +81,13 @@ import {
81 isManagedDOMElement,
82 isValidEventTarget,
83 listenToTopLevelEvent,
84 + attachListenerToManagedDOMElement,
85 detachListenerFromManagedDOMElement,
83 - attachListenerFromManagedDOMElement,
84 - detachTargetEventListener,
86 attachTargetEventListener,
87 + detachTargetEventListener,
88 + isReactScope,
89 + attachListenerToReactScope,
90 + detachListenerFromReactScope,
91 } from '../events/DOMModernPluginEventSystem';
92 import {getListenerMapForElement} from '../events/DOMEventListenerMap';
93 import {TOP_BEFORE_BLUR, TOP_AFTER_BLUR} from '../events/DOMTopLevelEventTypes';
@@ -1159,7 +1164,9 @@ export function mountEventListener(listener: ReactDOMListener): void {
1164 if (enableUseEventAPI) {
1165 const {target} = listener;
1166 if (isManagedDOMElement(target)) {
1162 - attachListenerFromManagedDOMElement(listener);
1167 + attachListenerToManagedDOMElement(listener);
1168 + } else if (enableScopeAPI && isReactScope(target)) {
1169 + attachListenerToReactScope(listener);
1170 } else {
1171 attachTargetEventListener(listener);
1172 }
@@ -1171,6 +1178,8 @@ export function unmountEventListener(listener: ReactDOMListener): void {
1178 const {target} = listener;
1179 if (isManagedDOMElement(target)) {
1180 detachListenerFromManagedDOMElement(listener);
1181 + } else if (enableScopeAPI && isReactScope(target)) {
1182 + detachListenerFromReactScope(listener);
1183 } else {
1184 detachTargetEventListener(listener);
1185 }
@@ -1178,13 +1187,15 @@ export function unmountEventListener(listener: ReactDOMListener): void {
1187 }
1188
1189 export function validateEventListenerTarget(
1181 - target: EventTarget,
1190 + target: EventTarget | ReactScopeMethods,
1191 listener: ?(Event) => void,
1192 ): boolean {
1193 if (enableUseEventAPI) {
1194 if (
1195 target != null &&
1187 - (isManagedDOMElement(target) || isValidEventTarget(target))
1196 + (isManagedDOMElement(target) ||
1197 + isValidEventTarget(target) ||
1198 + isReactScope(target))
1199 ) {
1200 if (listener == null || typeof listener === 'function') {
1201 return true;
packages/react-dom/src/events/DOMModernPluginEventSystem.js
+93 -22
@@ -14,7 +14,7 @@ import type {
14 ElementListenerMapEntry,
15 } from '../events/DOMEventListenerMap';
16 import type {EventSystemFlags} from 'legacy-events/EventSystemFlags';
17 -import type {EventPriority} from 'shared/ReactTypes';
17 +import type {EventPriority, ReactScopeMethods} from 'shared/ReactTypes';
18 import type {Fiber} from 'react-reconciler/src/ReactFiber';
19 import type {PluginModule} from 'legacy-events/PluginModuleType';
20 import type {
@@ -142,8 +142,11 @@ const emptyDispatchConfigForCustomEvents: CustomDispatchConfig = {
142
143 const isArray = Array.isArray;
144
145 -// $FlowFixMe: Flow struggles with this pattern
146 -const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
145 +// TODO: we should remove the FlowFixMes and the casting to figure out how to make
146 +// these patterns work properly.
147 +// $FlowFixMe: Flow struggles with this pattern, so we also have to cast it.
148 +const PossiblyWeakMap = ((typeof WeakMap === 'function' ? WeakMap : Map): any);
149 +
150 // $FlowFixMe: Flow cannot handle polymorphic WeakMaps
151 export const eventTargetEventListenerStore: WeakMap<
152 EventTarget,
@@ -153,6 +156,15 @@ export const eventTargetEventListenerStore: WeakMap<
156 >,
157 > = new PossiblyWeakMap();
158
159 +// $FlowFixMe: Flow cannot handle polymorphic WeakMaps
160 +export const reactScopeListenerStore: WeakMap<
161 + ReactScopeMethods,
162 + Map<
163 + DOMTopLevelEventType,
164 + {bubbled: Set<ReactDOMListener>, captured: Set<ReactDOMListener>},
165 + >,
166 +> = new PossiblyWeakMap();
167 +
168 function dispatchEventsForPlugins(
169 topLevelType: DOMTopLevelEventType,
170 eventSystemFlags: EventSystemFlags,
@@ -306,12 +318,20 @@ function isMatchingRootContainer(
318 );
319 }
320
309 -export function isManagedDOMElement(target: EventTarget): boolean {
321 +export function isManagedDOMElement(
322 + target: EventTarget | ReactScopeMethods,
323 +): boolean {
324 return getClosestInstanceFromNode(((target: any): Node)) !== null;
325 }
326
313 -export function isValidEventTarget(target: EventTarget): boolean {
314 - return typeof target.addEventListener === 'function';
327 +export function isValidEventTarget(
328 + target: EventTarget | ReactScopeMethods,
329 +): boolean {
330 + return typeof (target: any).addEventListener === 'function';
331 +}
332 +
333 +export function isReactScope(target: EventTarget | ReactScopeMethods): boolean {
334 + return typeof (target: any).getChildContextValues === 'function';
335 }
336
337 export function dispatchEventForPluginEventSystem(
@@ -446,18 +466,16 @@ function addEventTypeToDispatchConfig(type: DOMTopLevelEventType): void {
466 }
467 }
468
449 -export function attachListenerFromManagedDOMElement(
469 +export function attachListenerToManagedDOMElement(
470 listener: ReactDOMListener,
471 ): void {
472 const {event, target} = listener;
473 const {passive, priority, type} = event;
454 - const possibleManagedTarget = ((target: any): Element);
455 - let containerEventTarget = target;
456 - if (getClosestInstanceFromNode(possibleManagedTarget)) {
457 - containerEventTarget = getNearestRootOrPortalContainer(
458 - possibleManagedTarget,
459 - );
460 - }
474 +
475 + const managedTargetElement = ((target: any): Element);
476 + const containerEventTarget = getNearestRootOrPortalContainer(
477 + managedTargetElement,
478 + );
479 const listenerMap = getListenerMapForElement(containerEventTarget);
480 // Add the event listener to the target container (falling back to
481 // the target if we didn't find one).
@@ -469,11 +487,11 @@ export function attachListenerFromManagedDOMElement(
487 priority,
488 );
489 // Get the internal listeners Set from the target instance.
472 - let listeners = getListenersFromTarget(target);
490 + let listeners = getListenersFromTarget(managedTargetElement);
491 // If we don't have any listeners, then we need to init them.
492 if (listeners === null) {
493 listeners = new Set();
476 - initListenersSet(target, listeners);
494 + initListenersSet(managedTargetElement, listeners);
495 }
496 // Add our listener to the listeners Set.
497 listeners.add(listener);
@@ -485,8 +503,9 @@ export function detachListenerFromManagedDOMElement(
503 listener: ReactDOMListener,
504 ): void {
505 const {target} = listener;
506 + const managedTargetElement = ((target: any): Element);
507 // Get the internal listeners Set from the target instance.
489 - const listeners = getListenersFromTarget(target);
508 + const listeners = getListenersFromTarget(managedTargetElement);
509 if (listeners !== null) {
510 // Remove out listener from the listeners Set.
511 listeners.delete(listener);
@@ -496,13 +515,21 @@ export function detachListenerFromManagedDOMElement(
515 export function attachTargetEventListener(listener: ReactDOMListener): void {
516 const {event, target} = listener;
517 const {capture, passive, priority, type} = event;
499 - const listenerMap = getListenerMapForElement(target);
518 + const eventTarget = ((target: any): EventTarget);
519 + const listenerMap = getListenerMapForElement(eventTarget);
520 // Add the event listener to the TargetEvent object.
501 - listenToTopLevelEvent(type, target, listenerMap, passive, priority, capture);
502 - let eventTypeMap = eventTargetEventListenerStore.get(target);
521 + listenToTopLevelEvent(
522 + type,
523 + eventTarget,
524 + listenerMap,
525 + passive,
526 + priority,
527 + capture,
528 + );
529 + let eventTypeMap = eventTargetEventListenerStore.get(eventTarget);
530 if (eventTypeMap === undefined) {
531 eventTypeMap = new Map();
505 - eventTargetEventListenerStore.set(target, eventTypeMap);
532 + eventTargetEventListenerStore.set(eventTarget, eventTypeMap);
533 }
534 // Get the listeners by the event type
535 let listeners = eventTypeMap.get(type);
@@ -523,7 +550,51 @@ export function attachTargetEventListener(listener: ReactDOMListener): void {
550 export function detachTargetEventListener(listener: ReactDOMListener): void {
551 const {event, target} = listener;
552 const {capture, type} = event;
526 - const eventTypeMap = eventTargetEventListenerStore.get(target);
553 + const validEventTarget = ((target: any): EventTarget);
554 + const eventTypeMap = eventTargetEventListenerStore.get(validEventTarget);
555 + if (eventTypeMap !== undefined) {
556 + const listeners = eventTypeMap.get(type);
557 + if (listeners !== undefined) {
558 + // Remove out listener from the listeners Set.
559 + if (capture) {
560 + listeners.captured.delete(listener);
561 + } else {
562 + listeners.bubbled.delete(listener);
563 + }
564 + }
565 + }
566 +}
567 +
568 +export function attachListenerToReactScope(listener: ReactDOMListener): void {
569 + const {event, target} = listener;
570 + const {capture, type} = event;
571 + const reactScope = ((target: any): ReactScopeMethods);
572 + let eventTypeMap = reactScopeListenerStore.get(reactScope);
573 + if (eventTypeMap === undefined) {
574 + eventTypeMap = new Map();
575 + reactScopeListenerStore.set(reactScope, eventTypeMap);
576 + }
577 + // Get the listeners by the event type
578 + let listeners = eventTypeMap.get(type);
579 + if (listeners === undefined) {
580 + listeners = {captured: new Set(), bubbled: new Set()};
581 + eventTypeMap.set(type, listeners);
582 + }
583 + // Add our listener to the listeners Set.
584 + if (capture) {
585 + listeners.captured.add(listener);
586 + } else {
587 + listeners.bubbled.add(listener);
588 + }
589 + // Finally, add the event to our known event types list.
590 + addEventTypeToDispatchConfig(type);
591 +}
592 +
593 +export function detachListenerFromReactScope(listener: ReactDOMListener): void {
594 + const {event, target} = listener;
595 + const {capture, type} = event;
596 + const reactScope = ((target: any): ReactScopeMethods);
597 + const eventTypeMap = reactScopeListenerStore.get(reactScope);
598 if (eventTypeMap !== undefined) {
599 const listeners = eventTypeMap.get(type);
600 if (listeners !== undefined) {
packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js
+133
@@ -2327,6 +2327,139 @@ describe('DOMModernPluginEventSystem', () => {
2327 document.body.removeChild(container2);
2328 },
2329 );
2330 +
2331 + describe('Compatibility with Scopes API', () => {
2332 + beforeEach(() => {
2333 + jest.resetModules();
2334 + ReactFeatureFlags = require('shared/ReactFeatureFlags');
2335 + ReactFeatureFlags.enableModernEventSystem = true;
2336 + ReactFeatureFlags.enableUseEventAPI = true;
2337 + ReactFeatureFlags.enableScopeAPI = true;
2338 +
2339 + React = require('react');
2340 + ReactDOM = require('react-dom');
2341 + Scheduler = require('scheduler');
2342 + ReactDOMServer = require('react-dom/server');
2343 + });
2344 +
2345 + it('handle propagation of click events on a scope', () => {
2346 + const buttonRef = React.createRef();
2347 + const log = [];
2348 + const onClick = jest.fn(e =>
2349 + log.push(['bubble', e.currentTarget]),
2350 + );
2351 + const onClickCapture = jest.fn(e =>
2352 + log.push(['capture', e.currentTarget]),
2353 + );
2354 + const TestScope = React.unstable_createScope();
2355 +
2356 + function Test() {
2357 + const click = ReactDOM.unstable_useEvent('click');
2358 + const clickCapture = ReactDOM.unstable_useEvent('click', {
2359 + capture: true,
2360 + });
2361 + const scopeRef = React.useRef(null);
2362 +
2363 + React.useEffect(() => {
2364 + click.setListener(scopeRef.current, onClick);
2365 + clickCapture.setListener(scopeRef.current, onClickCapture);
2366 + });
2367 +
2368 + return (
2369 + <TestScope ref={scopeRef}>
2370 + <button ref={buttonRef} />
2371 + </TestScope>
2372 + );
2373 + }
2374 +
2375 + ReactDOM.render(<Test />, container);
2376 + Scheduler.unstable_flushAll();
2377 +
2378 + const buttonElement = buttonRef.current;
2379 + dispatchClickEvent(buttonElement);
2380 +
2381 + expect(onClick).toHaveBeenCalledTimes(1);
2382 + expect(onClickCapture).toHaveBeenCalledTimes(1);
2383 + expect(log).toEqual([
2384 + ['capture', buttonElement],
2385 + ['bubble', buttonElement],
2386 + ]);
2387 + });
2388 +
2389 + it('handle mixed propagation of click events on a scope', () => {
2390 + const buttonRef = React.createRef();
2391 + const divRef = React.createRef();
2392 + const log = [];
2393 + const onClick = jest.fn(e =>
2394 + log.push(['bubble', e.currentTarget]),
2395 + );
2396 + const onClickCapture = jest.fn(e =>
2397 + log.push(['capture', e.currentTarget]),
2398 + );
2399 + const TestScope = React.unstable_createScope();
2400 +
2401 + function Test() {
2402 + const click = ReactDOM.unstable_useEvent('click');
2403 + const clickCapture = ReactDOM.unstable_useEvent('click', {
2404 + capture: true,
2405 + });
2406 + const scopeRef = React.useRef(null);
2407 +
2408 + React.useEffect(() => {
2409 + click.setListener(scopeRef.current, onClick);
2410 + clickCapture.setListener(scopeRef.current, onClickCapture);
2411 + click.setListener(buttonRef.current, onClick);
2412 + clickCapture.setListener(buttonRef.current, onClickCapture);
2413 + });
2414 +
2415 + return (
2416 + <TestScope ref={scopeRef}>
2417 + <button ref={buttonRef}>
2418 + <div
2419 + ref={divRef}
2420 + onClick={onClick}
2421 + onClickCapture={onClickCapture}>
2422 + Click me!
2423 + </div>
2424 + </button>
2425 + </TestScope>
2426 + );
2427 + }
2428 +
2429 + ReactDOM.render(<Test />, container);
2430 + Scheduler.unstable_flushAll();
2431 +
2432 + const buttonElement = buttonRef.current;
2433 + dispatchClickEvent(buttonElement);
2434 +
2435 + expect(onClick).toHaveBeenCalledTimes(2);
2436 + expect(onClickCapture).toHaveBeenCalledTimes(2);
2437 + expect(log).toEqual([
2438 + ['capture', buttonElement],
2439 + ['capture', buttonElement],
2440 + ['bubble', buttonElement],
2441 + ['bubble', buttonElement],
2442 + ]);
2443 +
2444 + log.length = 0;
2445 + onClick.mockClear();
2446 + onClickCapture.mockClear();
2447 +
2448 + const divElement = divRef.current;
2449 + dispatchClickEvent(divElement);
2450 +
2451 + expect(onClick).toHaveBeenCalledTimes(3);
2452 + expect(onClickCapture).toHaveBeenCalledTimes(3);
2453 + expect(log).toEqual([
2454 + ['capture', buttonElement],
2455 + ['capture', buttonElement],
2456 + ['capture', divElement],
2457 + ['bubble', divElement],
2458 + ['bubble', buttonElement],
2459 + ['bubble', buttonElement],
2460 + ]);
2461 + });
2462 + });
2463 });
2464 },
2465 );
packages/react-dom/src/events/accumulateTwoPhaseListeners.js
+36 -4
@@ -11,13 +11,19 @@ import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes';
11 import type {EventSystemFlags} from 'legacy-events/EventSystemFlags';
12 import type {ReactSyntheticEvent} from 'legacy-events/ReactSyntheticEventType';
13
14 -import {HostComponent} from 'react-reconciler/src/ReactWorkTags';
15 -import {enableUseEventAPI} from 'shared/ReactFeatureFlags';
14 +import {
15 + HostComponent,
16 + ScopeComponent,
17 +} from 'react-reconciler/src/ReactWorkTags';
18 +import {enableUseEventAPI, enableScopeAPI} from 'shared/ReactFeatureFlags';
19
20 import getListener from 'legacy-events/getListener';
21 import {getListenersFromTarget} from '../client/ReactDOMComponentTree';
22 import {IS_TARGET_EVENT_ONLY} from 'legacy-events/EventSystemFlags';
20 -import {eventTargetEventListenerStore} from './DOMModernPluginEventSystem';
23 +import {
24 + eventTargetEventListenerStore,
25 + reactScopeListenerStore,
26 +} from './DOMModernPluginEventSystem';
27
28 export default function accumulateTwoPhaseListeners(
29 event: ReactSyntheticEvent,
@@ -76,11 +82,13 @@ export default function accumulateTwoPhaseListeners(
82 // usual two phase accumulation using the React fiber tree to pick up
83 // all relevant useEvent and on* prop events.
84 let node = event._targetInst;
85 + let lastHostComponent;
86
87 // Accumulate all instances and listeners via the target -> root path.
88 while (node !== null) {
82 - // We only care for listeners that are on HostComponents (i.e. <div>)
89 + // Handle listeners that are on HostComponents (i.e. <div>)
90 if (node.tag === HostComponent) {
91 + lastHostComponent = node.stateNode;
92 // For useEvent listenrs
93 if (enableUseEventAPI && accumulateUseEventListeners) {
94 // useEvent event listeners
@@ -127,6 +135,30 @@ export default function accumulateTwoPhaseListeners(
135 dispatchInstances.push(node);
136 }
137 }
138 + } else if (enableScopeAPI && node.tag === ScopeComponent) {
139 + const reactScope = node.stateNode.methods;
140 + const eventTypeMap = reactScopeListenerStore.get(reactScope);
141 + if (eventTypeMap !== undefined) {
142 + const type = ((event.type: any): DOMTopLevelEventType);
143 + const listeners = eventTypeMap.get(type);
144 + if (listeners !== undefined) {
145 + const captureListeners = Array.from(listeners.captured);
146 + const bubbleListeners = Array.from(listeners.bubbled);
147 +
148 + for (let i = 0; i < captureListeners.length; i++) {
149 + const listener = captureListeners[i];
150 + const {callback} = listener;
151 + dispatchListeners.unshift(callback);
152 + dispatchInstances.unshift(((lastHostComponent: any): Element));
153 + }
154 + for (let i = 0; i < bubbleListeners.length; i++) {
155 + const listener = bubbleListeners[i];
156 + const {callback} = listener;
157 + dispatchListeners.push(callback);
158 + dispatchInstances.push(((lastHostComponent: any): Element));
159 + }
160 + }
161 + }
162 }
163 node = node.return;
164 }
packages/react-dom/src/shared/ReactDOMTypes.js
+6 -2
@@ -12,6 +12,7 @@ import type {
12 ReactEventResponder,
13 ReactEventResponderInstance,
14 EventPriority,
15 + ReactScopeMethods,
16 } from 'shared/ReactTypes';
17 import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes';
18
@@ -86,12 +87,15 @@ export type ReactDOMListenerEvent = {|
87
88 export type ReactDOMListenerMap = {|
89 clear: () => void,
89 - setListener: (target: EventTarget, callback: ?(Event) => void) => void,
90 + setListener: (
91 + target: EventTarget | ReactScopeMethods,
92 + callback: ?(Event) => void,
93 + ) => void,
94 |};
95
96 export type ReactDOMListener = {|
97 callback: Event => void,
98 destroy: Node => void,
99 event: ReactDOMListenerEvent,
96 - target: EventTarget,
100 + target: EventTarget | ReactScopeMethods,
101 |};
packages/react-reconciler/src/ReactFiberHooks.js
+10 -3
@@ -14,6 +14,7 @@ import type {
14 ReactEventResponder,
15 ReactContext,
16 ReactEventResponderListener,
17 + ReactScopeMethods,
18 } from 'shared/ReactTypes';
19 import type {Fiber} from './ReactFiber';
20 import type {ExpirationTime} from './ReactFiberExpirationTime';
@@ -1651,7 +1652,7 @@ function validateNotInFunctionRender(): boolean {
1652 function createReactListener(
1653 event: ReactListenerEvent,
1654 callback: Event => void,
1654 - target: EventTarget,
1655 + target: EventTarget | ReactScopeMethods,
1656 destroy: Node => void,
1657 ): ReactListener {
1658 return {
@@ -1665,7 +1666,10 @@ function createReactListener(
1666 function mountEventListener(event: ReactListenerEvent): ReactListenerMap {
1667 if (enableUseEventAPI) {
1668 const hook = mountWorkInProgressHook();
1668 - const listenerMap: Map<EventTarget, ReactListener> = new Map();
1669 + const listenerMap: Map<
1670 + EventTarget | ReactScopeMethods,
1671 + ReactListener,
1672 + > = new Map();
1673 const rootContainerInstance = getRootHostContainer();
1674
1675 // Register the event to the current root to ensure event
@@ -1700,7 +1704,10 @@ function mountEventListener(event: ReactListenerEvent): ReactListenerMap {
1704
1705 const reactListenerMap: ReactListenerMap = {
1706 clear,
1703 - setListener(target: EventTarget, callback: ?(Event) => void): void {
1707 + setListener(
1708 + target: EventTarget | ReactScopeMethods,
1709 + callback: ?(Event) => void,
1710 + ): void {
1711 if (
1712 validateNotInFunctionRender() &&
1713 validateEventListenerTarget(target, callback)