@samitouri / QOS-React / commits / 11ee82df45

[Events] Make passiveness and priority non-configurable (#19807)

Dan Abramov committed Sep 14, 2020 at 13:54 UTC 11ee82df4572fff2bbe82fef5fc98281d977b072
10 files changed +161 -480
packages/react-dom/src/client/ReactDOMComponentTree.js
+5 -16
@@ -20,7 +20,6 @@ import type {
20 SuspenseInstance,
21 Props,
22 } from './ReactDOMHostConfig';
23 -import type {DOMEventName} from '../events/DOMEventNames';
23
24 import {
25 HostComponent,
@@ -44,16 +43,6 @@ const internalEventHandlersKey = '__reactEvents$' + randomKey;
43 const internalEventHandlerListenersKey = '__reactListeners$' + randomKey;
44 const internalEventHandlesSetKey = '__reactHandles$' + randomKey;
45
47 -export type ElementListenerMap = Map<
48 - DOMEventName | string,
49 - ElementListenerMapEntry | null,
50 ->;
51 -
52 -export type ElementListenerMapEntry = {
53 - passive: void | boolean,
54 - listener: any => void,
55 -};
56 -
46 export function precacheFiberNode(
47 hostInst: Fiber,
48 node: Instance | TextInstance | SuspenseInstance | ReactScopeInstance,
@@ -207,12 +196,12 @@ export function updateFiberProps(
196 (node: any)[internalPropsKey] = props;
197 }
198
210 -export function getEventListenerMap(node: EventTarget): ElementListenerMap {
211 - let elementListenerMap = (node: any)[internalEventHandlersKey];
212 - if (elementListenerMap === undefined) {
213 - elementListenerMap = (node: any)[internalEventHandlersKey] = new Map();
199 +export function getEventListenerSet(node: EventTarget): Set<string> {
200 + let elementListenerSet = (node: any)[internalEventHandlersKey];
201 + if (elementListenerSet === undefined) {
202 + elementListenerSet = (node: any)[internalEventHandlersKey] = new Set();
203 }
215 - return elementListenerMap;
204 + return elementListenerSet;
205 }
206
207 export function getFiberFromScopeInstance(
packages/react-dom/src/client/ReactDOMEventHandle.js
+69 -104
@@ -8,13 +8,12 @@
8 */
9
10 import type {DOMEventName} from '../events/DOMEventNames';
11 -import type {EventPriority, ReactScopeInstance} from 'shared/ReactTypes';
11 +import type {ReactScopeInstance} from 'shared/ReactTypes';
12 import type {
13 ReactDOMEventHandle,
14 ReactDOMEventHandleListener,
15 } from '../shared/ReactDOMTypes';
16
17 -import {getEventPriorityForListenerSystem} from '../events/DOMEventProperties';
17 import {allNativeEvents} from '../events/EventRegistry';
18 import {
19 getClosestInstanceFromNode,
@@ -25,10 +24,7 @@ import {
24 addEventHandleToTarget,
25 } from './ReactDOMComponentTree';
26 import {ELEMENT_NODE, COMMENT_NODE} from '../shared/HTMLNodeType';
28 -import {
29 - listenToNativeEvent,
30 - addEventTypeToDispatchConfig,
31 -} from '../events/DOMPluginEventSystem';
27 +import {listenToNativeEvent} from '../events/DOMPluginEventSystem';
28
29 import {HostRoot, HostPortal} from 'react-reconciler/src/ReactWorkTags';
30 import {IS_EVENT_HANDLE_NON_MANAGED_NODE} from '../events/EventSystemFlags';
@@ -42,8 +38,6 @@ import invariant from 'shared/invariant';
38
39 type EventHandleOptions = {|
40 capture?: boolean,
45 - passive?: boolean,
46 - priority?: EventPriority,
41 |};
42
43 function getNearestRootOrPortalContainer(node: Fiber): null | Element {
@@ -82,76 +76,76 @@ function createEventHandleListener(
76 function registerEventOnNearestTargetContainer(
77 targetFiber: Fiber,
78 domEventName: DOMEventName,
85 - isPassiveListener: boolean | void,
86 - listenerPriority: EventPriority | void,
79 isCapturePhaseListener: boolean,
80 targetElement: Element | null,
81 ): void {
90 - // If it is, find the nearest root or portal and make it
91 - // our event handle target container.
92 - let targetContainer = getNearestRootOrPortalContainer(targetFiber);
93 - if (targetContainer === null) {
94 - invariant(
95 - false,
96 - 'ReactDOM.createEventHandle: setListener called on an target ' +
97 - 'that did not have a corresponding root. This is likely a bug in React.',
82 + if (!enableEagerRootListeners) {
83 + // If it is, find the nearest root or portal and make it
84 + // our event handle target container.
85 + let targetContainer = getNearestRootOrPortalContainer(targetFiber);
86 + if (targetContainer === null) {
87 + if (__DEV__) {
88 + console.error(
89 + 'ReactDOM.createEventHandle: setListener called on an target ' +
90 + 'that did not have a corresponding root. This is likely a bug in React.',
91 + );
92 + }
93 + return;
94 + }
95 + if (targetContainer.nodeType === COMMENT_NODE) {
96 + targetContainer = ((targetContainer.parentNode: any): Element);
97 + }
98 + listenToNativeEvent(
99 + domEventName,
100 + isCapturePhaseListener,
101 + targetContainer,
102 + targetElement,
103 );
104 }
100 - if (targetContainer.nodeType === COMMENT_NODE) {
101 - targetContainer = ((targetContainer.parentNode: any): Element);
102 - }
103 - listenToNativeEvent(
104 - domEventName,
105 - isCapturePhaseListener,
106 - targetContainer,
107 - targetElement,
108 - isPassiveListener,
109 - listenerPriority,
110 - );
105 }
106
107 function registerReactDOMEvent(
108 target: EventTarget | ReactScopeInstance,
109 domEventName: DOMEventName,
116 - isPassiveListener: boolean | void,
110 isCapturePhaseListener: boolean,
118 - listenerPriority: EventPriority | void,
111 ): void {
112 // Check if the target is a DOM element.
113 if ((target: any).nodeType === ELEMENT_NODE) {
122 - const targetElement = ((target: any): Element);
123 - // Check if the DOM element is managed by React.
124 - const targetFiber = getClosestInstanceFromNode(targetElement);
125 - if (targetFiber === null) {
126 - invariant(
127 - false,
128 - 'ReactDOM.createEventHandle: setListener called on an element ' +
129 - 'target that is not managed by React. Ensure React rendered the DOM element.',
114 + if (!enableEagerRootListeners) {
115 + const targetElement = ((target: any): Element);
116 + // Check if the DOM element is managed by React.
117 + const targetFiber = getClosestInstanceFromNode(targetElement);
118 + if (targetFiber === null) {
119 + if (__DEV__) {
120 + console.error(
121 + 'ReactDOM.createEventHandle: setListener called on an element ' +
122 + 'target that is not managed by React. Ensure React rendered the DOM element.',
123 + );
124 + }
125 + return;
126 + }
127 + registerEventOnNearestTargetContainer(
128 + targetFiber,
129 + domEventName,
130 + isCapturePhaseListener,
131 + targetElement,
132 );
133 }
132 - registerEventOnNearestTargetContainer(
133 - targetFiber,
134 - domEventName,
135 - isPassiveListener,
136 - listenerPriority,
137 - isCapturePhaseListener,
138 - targetElement,
139 - );
134 } else if (enableScopeAPI && isReactScope(target)) {
141 - const scopeTarget = ((target: any): ReactScopeInstance);
142 - const targetFiber = getFiberFromScopeInstance(scopeTarget);
143 - if (targetFiber === null) {
144 - // Scope is unmounted, do not proceed.
145 - return;
135 + if (!enableEagerRootListeners) {
136 + const scopeTarget = ((target: any): ReactScopeInstance);
137 + const targetFiber = getFiberFromScopeInstance(scopeTarget);
138 + if (targetFiber === null) {
139 + // Scope is unmounted, do not proceed.
140 + return;
141 + }
142 + registerEventOnNearestTargetContainer(
143 + targetFiber,
144 + domEventName,
145 + isCapturePhaseListener,
146 + null,
147 + );
148 }
147 - registerEventOnNearestTargetContainer(
148 - targetFiber,
149 - domEventName,
150 - isPassiveListener,
151 - listenerPriority,
152 - isCapturePhaseListener,
153 - null,
154 - );
149 } else if (isValidEventTarget(target)) {
150 const eventTarget = ((target: any): EventTarget);
151 // These are valid event targets, but they are also
@@ -161,8 +155,6 @@ function registerReactDOMEvent(
155 isCapturePhaseListener,
156 eventTarget,
157 null,
164 - isPassiveListener,
165 - listenerPriority,
158 IS_EVENT_HANDLE_NON_MANAGED_NODE,
159 );
160 } else {
@@ -181,46 +173,27 @@ export function createEventHandle(
173 if (enableCreateEventHandleAPI) {
174 const domEventName = ((type: any): DOMEventName);
175
184 - if (enableEagerRootListeners) {
185 - // We cannot support arbitrary native events with eager root listeners
186 - // because the eager strategy relies on knowing the whole list ahead of time.
187 - // If we wanted to support this, we'd have to add code to keep track
188 - // (or search) for all portal and root containers, and lazily add listeners
189 - // to them whenever we see a previously unknown event. This seems like a lot
190 - // of complexity for something we don't even have a particular use case for.
191 - // Unfortunately, the downside of this invariant is that *removing* a native
192 - // event from the list of known events has now become a breaking change for
193 - // any code relying on the createEventHandle API.
194 - invariant(
195 - allNativeEvents.has(domEventName) ||
196 - domEventName === 'beforeblur' ||
197 - domEventName === 'afterblur',
198 - 'Cannot call unstable_createEventHandle with "%s", as it is not an event known to React.',
199 - domEventName,
200 - );
201 - }
176 + // We cannot support arbitrary native events with eager root listeners
177 + // because the eager strategy relies on knowing the whole list ahead of time.
178 + // If we wanted to support this, we'd have to add code to keep track
179 + // (or search) for all portal and root containers, and lazily add listeners
180 + // to them whenever we see a previously unknown event. This seems like a lot
181 + // of complexity for something we don't even have a particular use case for.
182 + // Unfortunately, the downside of this invariant is that *removing* a native
183 + // event from the list of known events has now become a breaking change for
184 + // any code relying on the createEventHandle API.
185 + invariant(
186 + allNativeEvents.has(domEventName),
187 + 'Cannot call unstable_createEventHandle with "%s", as it is not an event known to React.',
188 + domEventName,
189 + );
190
191 let isCapturePhaseListener = false;
204 - let isPassiveListener = undefined; // Undefined means to use the browser default
205 - let listenerPriority;
206 -
192 if (options != null) {
193 const optionsCapture = options.capture;
209 - const optionsPassive = options.passive;
210 - const optionsPriority = options.priority;
211 -
194 if (typeof optionsCapture === 'boolean') {
195 isCapturePhaseListener = optionsCapture;
196 }
215 - if (typeof optionsPassive === 'boolean') {
216 - isPassiveListener = optionsPassive;
217 - }
218 - if (typeof optionsPriority === 'number') {
219 - listenerPriority = optionsPriority;
220 - }
221 - }
222 - if (listenerPriority === undefined) {
223 - listenerPriority = getEventPriorityForListenerSystem(domEventName);
197 }
198
199 const eventHandle = (
@@ -234,15 +207,7 @@ export function createEventHandle(
207 );
208 if (!doesTargetHaveEventHandle(target, eventHandle)) {
209 addEventHandleToTarget(target, eventHandle);
237 - registerReactDOMEvent(
238 - target,
239 - domEventName,
240 - isPassiveListener,
241 - isCapturePhaseListener,
242 - listenerPriority,
243 - );
244 - // Add the event to our known event types list.
245 - addEventTypeToDispatchConfig(domEventName);
210 + registerReactDOMEvent(target, domEventName, isCapturePhaseListener);
211 }
212 const listener = createEventHandleListener(
213 domEventName,
packages/react-dom/src/events/DOMEventProperties.js
+5 -1
@@ -89,6 +89,10 @@ const otherDiscreteEvents: Array<DOMEventName> = [
89 ];
90
91 if (enableCreateEventHandleAPI) {
92 + // Special case: these two events don't have on* React handler
93 + // and are only accessible via the createEventHandle API.
94 + topLevelEventsToReactNames.set('beforeblur', null);
95 + topLevelEventsToReactNames.set('afterblur', null);
96 otherDiscreteEvents.push('beforeblur', 'afterblur');
97 }
98
@@ -202,7 +206,7 @@ export function getEventPriorityForListenerSystem(
206 if (__DEV__) {
207 console.warn(
208 'The event "%s" provided to createEventHandle() does not have a known priority type.' +
205 - ' It is recommended to provide a "priority" option to specify a priority.',
209 + ' This is likely a bug in React.',
210 type,
211 );
212 }
packages/react-dom/src/events/DOMPluginEventSystem.js
+36 -86
@@ -19,8 +19,6 @@ import type {
19 KnownReactSyntheticEvent,
20 ReactSyntheticEvent,
21 } from './ReactSyntheticEventType';
22 -import type {ElementListenerMapEntry} from '../client/ReactDOMComponentTree';
23 -import type {EventPriority} from 'shared/ReactTypes';
22 import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
23
24 import {registrationNameDependencies, allNativeEvents} from './EventRegistry';
@@ -41,7 +39,7 @@ import {
39 import getEventTarget from './getEventTarget';
40 import {
41 getClosestInstanceFromNode,
44 - getEventListenerMap,
42 + getEventListenerSet,
43 getEventHandlerListeners,
44 } from '../client/ReactDOMComponentTree';
45 import {COMMENT_NODE} from '../shared/HTMLNodeType';
@@ -69,7 +67,6 @@ import {
67 addEventBubbleListenerWithPassiveFlag,
68 addEventCaptureListenerWithPassiveFlag,
69 } from './EventListener';
72 -import {topLevelEventsToReactNames} from './DOMEventProperties';
70 import * as BeforeInputEventPlugin from './plugins/BeforeInputEventPlugin';
71 import * as ChangeEventPlugin from './plugins/ChangeEventPlugin';
72 import * as EnterLeaveEventPlugin from './plugins/EnterLeaveEventPlugin';
@@ -296,36 +293,24 @@ function dispatchEventsForPlugins(
293 processDispatchQueue(dispatchQueue, eventSystemFlags);
294 }
295
299 -function shouldUpgradeListener(
300 - listenerEntry: void | ElementListenerMapEntry,
301 - passive: void | boolean,
302 -): boolean {
303 - return (
304 - listenerEntry !== undefined && listenerEntry.passive === true && !passive
305 - );
306 -}
307 -
296 export function listenToNonDelegatedEvent(
297 domEventName: DOMEventName,
298 targetElement: Element,
299 ): void {
300 const isCapturePhaseListener = false;
313 - const listenerMap = getEventListenerMap(targetElement);
314 - const listenerMapKey = getListenerMapKey(
301 + const listenerSet = getEventListenerSet(targetElement);
302 + const listenerSetKey = getListenerSetKey(
303 domEventName,
304 isCapturePhaseListener,
305 );
318 - const listenerEntry = ((listenerMap.get(
319 - listenerMapKey,
320 - ): any): ElementListenerMapEntry | void);
321 - if (listenerEntry === undefined) {
322 - const listener = addTrappedEventListener(
306 + if (!listenerSet.has(listenerSetKey)) {
307 + addTrappedEventListener(
308 targetElement,
309 domEventName,
310 IS_NON_DELEGATED,
311 isCapturePhaseListener,
312 );
328 - listenerMap.set(listenerMapKey, {passive: false, listener});
313 + listenerSet.add(listenerSetKey);
314 }
315 }
316
@@ -369,8 +354,6 @@ export function listenToNativeEvent(
354 isCapturePhaseListener: boolean,
355 rootContainerElement: EventTarget,
356 targetElement: Element | null,
372 - isPassiveListener?: boolean,
373 - listenerPriority?: EventPriority,
357 eventSystemFlags?: EventSystemFlags = 0,
358 ): void {
359 let target = rootContainerElement;
@@ -384,21 +367,6 @@ export function listenToNativeEvent(
367 ) {
368 target = (rootContainerElement: any).ownerDocument;
369 }
387 - if (enablePassiveEventIntervention && isPassiveListener === undefined) {
388 - // Browsers introduced an intervention, making these events
389 - // passive by default on document. React doesn't bind them
390 - // to document anymore, but changing this now would undo
391 - // the performance wins from the change. So we emulate
392 - // the existing behavior manually on the roots now.
393 - // https://github.com/facebook/react/issues/19651
394 - if (
395 - domEventName === 'touchstart' ||
396 - domEventName === 'touchmove' ||
397 - domEventName === 'wheel'
398 - ) {
399 - isPassiveListener = true;
400 - }
401 - }
370 // If the event can be delegated (or is capture phase), we can
371 // register it to the root container. Otherwise, we should
372 // register the event to the target element and mark it as
@@ -423,42 +391,24 @@ export function listenToNativeEvent(
391 eventSystemFlags |= IS_NON_DELEGATED;
392 target = targetElement;
393 }
426 - const listenerMap = getEventListenerMap(target);
427 - const listenerMapKey = getListenerMapKey(
394 + const listenerSet = getEventListenerSet(target);
395 + const listenerSetKey = getListenerSetKey(
396 domEventName,
397 isCapturePhaseListener,
398 );
431 - const listenerEntry = ((listenerMap.get(
432 - listenerMapKey,
433 - ): any): ElementListenerMapEntry | void);
434 - const shouldUpgrade = shouldUpgradeListener(listenerEntry, isPassiveListener);
435 -
399 // If the listener entry is empty or we should upgrade, then
400 // we need to trap an event listener onto the target.
438 - if (listenerEntry === undefined || shouldUpgrade) {
439 - // If we should upgrade, then we need to remove the existing trapped
440 - // event listener for the target container.
441 - if (shouldUpgrade) {
442 - removeEventListener(
443 - target,
444 - domEventName,
445 - ((listenerEntry: any): ElementListenerMapEntry).listener,
446 - isCapturePhaseListener,
447 - );
448 - }
401 + if (!listenerSet.has(listenerSetKey)) {
402 if (isCapturePhaseListener) {
403 eventSystemFlags |= IS_CAPTURE_PHASE;
404 }
452 - const listener = addTrappedEventListener(
405 + addTrappedEventListener(
406 target,
407 domEventName,
408 eventSystemFlags,
409 isCapturePhaseListener,
457 - false,
458 - isPassiveListener,
459 - listenerPriority,
410 );
461 - listenerMap.set(listenerMapKey, {passive: isPassiveListener, listener});
411 + listenerSet.add(listenerSetKey);
412 }
413 }
414
@@ -480,11 +430,13 @@ export function listenToReactEvent(
430 const isPolyfillEventPlugin = dependenciesLength !== 1;
431
432 if (isPolyfillEventPlugin) {
483 - const listenerMap = getEventListenerMap(rootContainerElement);
484 - // For optimization, we register plugins on the listener map, so we
485 - // don't need to check each of their dependencies each time.
486 - if (!listenerMap.has(reactEvent)) {
487 - listenerMap.set(reactEvent, null);
433 + const listenerSet = getEventListenerSet(rootContainerElement);
434 + // When eager listeners are off, this Set has a dual purpose: it both
435 + // captures which native listeners we registered (e.g. "click__bubble")
436 + // and *React* lazy listeners (e.g. "onClick") so we don't do extra checks.
437 + // This second usage does not exist in the eager mode.
438 + if (!listenerSet.has(reactEvent)) {
439 + listenerSet.add(reactEvent);
440 for (let i = 0; i < dependenciesLength; i++) {
441 listenToNativeEvent(
442 dependencies[i],
@@ -520,19 +472,29 @@ function addTrappedEventListener(
472 eventSystemFlags: EventSystemFlags,
473 isCapturePhaseListener: boolean,
474 isDeferredListenerForLegacyFBSupport?: boolean,
523 - isPassiveListener?: boolean,
524 - listenerPriority?: EventPriority,
525 -): any => void {
475 +) {
476 let listener = createEventListenerWrapperWithPriority(
477 targetContainer,
478 domEventName,
479 eventSystemFlags,
530 - listenerPriority,
480 );
481 // If passive option is not supported, then the event will be
482 // active and not passive.
534 - if (isPassiveListener === true && !passiveBrowserEventsSupported) {
535 - isPassiveListener = false;
483 + let isPassiveListener = undefined;
484 + if (enablePassiveEventIntervention && passiveBrowserEventsSupported) {
485 + // Browsers introduced an intervention, making these events
486 + // passive by default on document. React doesn't bind them
487 + // to document anymore, but changing this now would undo
488 + // the performance wins from the change. So we emulate
489 + // the existing behavior manually on the roots now.
490 + // https://github.com/facebook/react/issues/19651
491 + if (
492 + domEventName === 'touchstart' ||
493 + domEventName === 'touchmove' ||
494 + domEventName === 'wheel'
495 + ) {
496 + isPassiveListener = true;
497 + }
498 }
499
500 targetContainer =
@@ -564,6 +526,7 @@ function addTrappedEventListener(
526 return originalListener.apply(this, p);
527 };
528 }
529 + // TODO: There are too many combinations here. Consolidate them.
530 if (isCapturePhaseListener) {
531 if (isPassiveListener !== undefined) {
532 unsubscribeListener = addEventCaptureListenerWithPassiveFlag(
@@ -595,7 +558,6 @@ function addTrappedEventListener(
558 );
559 }
560 }
598 - return unsubscribeListener;
561 }
562
563 function deferClickToDocumentForLegacyFBSupport(
@@ -1085,19 +1047,7 @@ export function accumulateEventHandleNonManagedNodeListeners(
1047 }
1048 }
1049
1088 -export function addEventTypeToDispatchConfig(type: DOMEventName): void {
1089 - const reactName = topLevelEventsToReactNames.get(type);
1090 - // If we don't have a reactName, then we're dealing with
1091 - // an event type that React does not know about (i.e. a custom event).
1092 - // We need to register an event config for this or the SimpleEventPlugin
1093 - // will not appropriately provide a SyntheticEvent, so we use out empty
1094 - // dispatch config for custom events.
1095 - if (reactName === undefined) {
1096 - topLevelEventsToReactNames.set(type, null);
1097 - }
1098 -}
1099 -
1100 -export function getListenerMapKey(
1050 +export function getListenerSetKey(
1051 domEventName: DOMEventName,
1052 capture: boolean,
1053 ): string {
packages/react-dom/src/events/EventRegistry.js
+8 -5
@@ -9,10 +9,15 @@
9
10 import type {DOMEventName} from './DOMEventNames';
11
12 -import {enableEagerRootListeners} from 'shared/ReactFeatureFlags';
12 +import {enableCreateEventHandleAPI} from 'shared/ReactFeatureFlags';
13
14 export const allNativeEvents: Set<DOMEventName> = new Set();
15
16 +if (enableCreateEventHandleAPI) {
17 + allNativeEvents.add('beforeblur');
18 + allNativeEvents.add('afterblur');
19 +}
20 +
21 /**
22 * Mapping from registration name to event name
23 */
@@ -60,9 +65,7 @@ export function registerDirectEvent(
65 }
66 }
67
63 - if (enableEagerRootListeners) {
64 - for (let i = 0; i < dependencies.length; i++) {
65 - allNativeEvents.add(dependencies[i]);
66 - }
68 + for (let i = 0; i < dependencies.length; i++) {
69 + allNativeEvents.add(dependencies[i]);
70 }
71 }
packages/react-dom/src/events/ReactDOMEventListener.js
+1 -6
@@ -8,7 +8,6 @@
8 */
9
10 import type {AnyNativeEvent} from '../events/PluginModuleType';
11 -import type {EventPriority} from 'shared/ReactTypes';
11 import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
12 import type {Container, SuspenseInstance} from '../client/ReactDOMHostConfig';
13 import type {DOMEventName} from '../events/DOMEventNames';
@@ -96,12 +95,8 @@ export function createEventListenerWrapperWithPriority(
95 targetContainer: EventTarget,
96 domEventName: DOMEventName,
97 eventSystemFlags: EventSystemFlags,
99 - priority?: EventPriority,
98 ): Function {
101 - const eventPriority =
102 - priority === undefined
103 - ? getEventPriorityForPluginSystem(domEventName)
104 - : priority;
99 + const eventPriority = getEventPriorityForPluginSystem(domEventName);
100 let listenerWrapper;
101 switch (eventPriority) {
102 case DiscreteEvent:
packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js
+23 -213
@@ -1949,106 +1949,6 @@ describe('DOMPluginEventSystem', () => {
1949 expect(log).toEqual([{counter: 1}]);
1950 });
1951
1952 - // @gate experimental
1953 - it('should correctly work for a basic "click" listener that upgrades', () => {
1954 - const clickEvent = jest.fn();
1955 - const buttonRef = React.createRef();
1956 - const button2Ref = React.createRef();
1957 - const setClick1 = ReactDOM.unstable_createEventHandle('click', {
1958 - passive: false,
1959 - });
1960 - const setClick2 = ReactDOM.unstable_createEventHandle('click', {
1961 - passive: true,
1962 - });
1963 -
1964 - function Test2() {
1965 - React.useEffect(() => {
1966 - return setClick1(button2Ref.current, clickEvent);
1967 - });
1968 -
1969 - return <button ref={button2Ref}>Click me!</button>;
1970 - }
1971 -
1972 - function Test({extra}) {
1973 - React.useEffect(() => {
1974 - return setClick2(buttonRef.current, clickEvent);
1975 - });
1976 -
1977 - return (
1978 - <>
1979 - <button ref={buttonRef}>Click me!</button>
1980 - {extra && <Test2 />}
1981 - </>
1982 - );
1983 - }
1984 -
1985 - ReactDOM.render(<Test />, container);
1986 - Scheduler.unstable_flushAll();
1987 -
1988 - let button = buttonRef.current;
1989 - dispatchClickEvent(button);
1990 - expect(clickEvent).toHaveBeenCalledTimes(1);
1991 -
1992 - ReactDOM.render(<Test extra={true} />, container);
1993 - Scheduler.unstable_flushAll();
1994 -
1995 - clickEvent.mockClear();
1996 -
1997 - button = button2Ref.current;
1998 - dispatchClickEvent(button);
1999 - expect(clickEvent).toHaveBeenCalledTimes(1);
2000 - });
2001 -
2002 - // @gate experimental
2003 - it('should correctly work for a basic "click" listener that upgrades #2', () => {
2004 - const clickEvent = jest.fn();
2005 - const buttonRef = React.createRef();
2006 - const button2Ref = React.createRef();
2007 - const setClick1 = ReactDOM.unstable_createEventHandle('click', {
2008 - passive: false,
2009 - });
2010 - const setClick2 = ReactDOM.unstable_createEventHandle('click', {
2011 - passive: undefined,
2012 - });
2013 -
2014 - function Test2() {
2015 - React.useEffect(() => {
2016 - return setClick1(button2Ref.current, clickEvent);
2017 - });
2018 -
2019 - return <button ref={button2Ref}>Click me!</button>;
2020 - }
2021 -
2022 - function Test({extra}) {
2023 - React.useEffect(() => {
2024 - return setClick2(buttonRef.current, clickEvent);
2025 - });
2026 -
2027 - return (
2028 - <>
2029 - <button ref={buttonRef}>Click me!</button>
2030 - {extra && <Test2 />}
2031 - </>
2032 - );
2033 - }
2034 -
2035 - ReactDOM.render(<Test />, container);
2036 - Scheduler.unstable_flushAll();
2037 -
2038 - let button = buttonRef.current;
2039 - dispatchClickEvent(button);
2040 - expect(clickEvent).toHaveBeenCalledTimes(1);
2041 -
2042 - ReactDOM.render(<Test extra={true} />, container);
2043 - Scheduler.unstable_flushAll();
2044 -
2045 - clickEvent.mockClear();
2046 -
2047 - button = button2Ref.current;
2048 - dispatchClickEvent(button);
2049 - expect(clickEvent).toHaveBeenCalledTimes(1);
2050 - });
2051 -
1952 // @gate experimental
1953 it('should correctly work for a basic "click" window listener', () => {
1954 const log = [];
@@ -2391,109 +2291,15 @@ describe('DOMPluginEventSystem', () => {
2291 });
2292
2293 // @gate experimental
2394 - it('handles propagation of custom user events', () => {
2395 - const buttonRef = React.createRef();
2396 - const divRef = React.createRef();
2397 - const log = [];
2398 - const onCustomEvent = jest.fn(e =>
2399 - log.push(['bubble', e.currentTarget]),
2294 + it('does not support custom user events', () => {
2295 + // With eager listeners, supporting custom events via this API doesn't make sense
2296 + // because we can't know a full list of them ahead of time. Let's check we throw
2297 + // since otherwise we'd end up with inconsistent behavior, like no portal bubbling.
2298 + expect(() => {
2299 + ReactDOM.unstable_createEventHandle('custom-event');
2300 + }).toThrow(
2301 + 'Cannot call unstable_createEventHandle with "custom-event", as it is not an event known to React.',
2302 );
2401 - const onCustomEventCapture = jest.fn(e =>
2402 - log.push(['capture', e.currentTarget]),
2403 - );
2404 -
2405 - let setCustomEventHandle;
2406 - if (gate(flags => flags.enableEagerRootListeners)) {
2407 - // With eager listeners, supporting custom events via this API doesn't make sense
2408 - // because we can't know a full list of them ahead of time. Let's check we throw
2409 - // since otherwise we'd end up with inconsistent behavior, like no portal bubbling.
2410 - expect(() => {
2411 - setCustomEventHandle = ReactDOM.unstable_createEventHandle(
2412 - 'custom-event',
2413 - );
2414 - }).toThrow(
2415 - 'Cannot call unstable_createEventHandle with "custom-event", as it is not an event known to React.',
2416 - );
2417 - } else {
2418 - // Test that we get a warning when we don't provide an explicit priority
2419 - expect(() => {
2420 - setCustomEventHandle = ReactDOM.unstable_createEventHandle(
2421 - 'custom-event',
2422 - );
2423 - }).toWarnDev(
2424 - 'Warning: The event "custom-event" provided to createEventHandle() does not have a known priority type. ' +
2425 - 'It is recommended to provide a "priority" option to specify a priority.',
2426 - {withoutStack: true},
2427 - );
2428 -
2429 - setCustomEventHandle = ReactDOM.unstable_createEventHandle(
2430 - 'custom-event',
2431 - {
2432 - priority: 0, // Discrete
2433 - },
2434 - );
2435 -
2436 - const setCustomCaptureHandle = ReactDOM.unstable_createEventHandle(
2437 - 'custom-event',
2438 - {
2439 - capture: true,
2440 - priority: 0, // Discrete
2441 - },
2442 - );
2443 -
2444 - const Test = () => {
2445 - React.useEffect(() => {
2446 - const clearCustom1 = setCustomEventHandle(
2447 - buttonRef.current,
2448 - onCustomEvent,
2449 - );
2450 - const clearCustom2 = setCustomCaptureHandle(
2451 - buttonRef.current,
2452 - onCustomEventCapture,
2453 - );
2454 - const clearCustom3 = setCustomEventHandle(
2455 - divRef.current,
2456 - onCustomEvent,
2457 - );
2458 - const clearCustom4 = setCustomCaptureHandle(
2459 - divRef.current,
2460 - onCustomEventCapture,
2461 - );
2462 -
2463 - return () => {
2464 - clearCustom1();
2465 - clearCustom2();
2466 - clearCustom3();
2467 - clearCustom4();
2468 - };
2469 - });
2470 -
2471 - return (
2472 - <button ref={buttonRef}>
2473 - <div ref={divRef}>Click me!</div>
2474 - </button>
2475 - );
2476 - };
2477 -
2478 - ReactDOM.render(<Test />, container);
2479 - Scheduler.unstable_flushAll();
2480 -
2481 - const buttonElement = buttonRef.current;
2482 - dispatchEvent(buttonElement, 'custom-event');
2483 - expect(onCustomEvent).toHaveBeenCalledTimes(1);
2484 - expect(onCustomEventCapture).toHaveBeenCalledTimes(1);
2485 - expect(log[0]).toEqual(['capture', buttonElement]);
2486 - expect(log[1]).toEqual(['bubble', buttonElement]);
2487 -
2488 - const divElement = divRef.current;
2489 - dispatchEvent(divElement, 'custom-event');
2490 - expect(onCustomEvent).toHaveBeenCalledTimes(3);
2491 - expect(onCustomEventCapture).toHaveBeenCalledTimes(3);
2492 - expect(log[2]).toEqual(['capture', buttonElement]);
2493 - expect(log[3]).toEqual(['capture', divElement]);
2494 - expect(log[4]).toEqual(['bubble', divElement]);
2495 - expect(log[5]).toEqual(['bubble', buttonElement]);
2496 - }
2303 });
2304
2305 // @gate experimental
@@ -3211,12 +3017,14 @@ describe('DOMPluginEventSystem', () => {
3017 });
3018
3019 // @gate experimental
3214 - it('should be able to register non-passive handlers for events affected by the intervention', () => {
3020 + it('should be able to register handlers for events affected by the intervention', () => {
3021 const rootContainer = document.createElement('div');
3022 container.appendChild(rootContainer);
3023
3024 + const allEvents = [];
3025 const defaultPreventedEvents = [];
3026 const handler = e => {
3027 + allEvents.push(e.type);
3028 if (e.defaultPrevented) defaultPreventedEvents.push(e.type);
3029 };
3030
@@ -3227,15 +3035,11 @@ describe('DOMPluginEventSystem', () => {
3035 const ref = React.createRef();
3036 const setTouchStart = ReactDOM.unstable_createEventHandle(
3037 'touchstart',
3230 - {passive: false},
3038 );
3039 const setTouchMove = ReactDOM.unstable_createEventHandle(
3040 'touchmove',
3234 - {passive: false},
3041 );
3236 - const setWheel = ReactDOM.unstable_createEventHandle('wheel', {
3237 - passive: false,
3238 - });
3042 + const setWheel = ReactDOM.unstable_createEventHandle('wheel');
3043
3044 function Component() {
3045 React.useEffect(() => {
@@ -3264,11 +3068,17 @@ describe('DOMPluginEventSystem', () => {
3068 dispatchEvent(ref.current, 'touchmove');
3069 dispatchEvent(ref.current, 'wheel');
3070
3267 - expect(defaultPreventedEvents).toEqual([
3268 - 'touchstart',
3269 - 'touchmove',
3270 - 'wheel',
3271 - ]);
3071 + expect(allEvents).toEqual(['touchstart', 'touchmove', 'wheel']);
3072 + // These events are passive by default, so we can't preventDefault.
3073 + if (gate(flags => flags.enablePassiveEventIntervention)) {
3074 + expect(defaultPreventedEvents).toEqual([]);
3075 + } else {
3076 + expect(defaultPreventedEvents).toEqual([
3077 + 'touchstart',
3078 + 'touchmove',
3079 + 'wheel',
3080 + ]);
3081 + }
3082 });
3083 });
3084 });
packages/react-dom/src/events/plugins/SelectEventPlugin.js
+4 -4
@@ -22,7 +22,7 @@ import {registerTwoPhaseEvent} from '../EventRegistry';
22 import getActiveElement from '../../client/getActiveElement';
23 import {
24 getNodeFromInstance,
25 - getEventListenerMap,
25 + getEventListenerSet,
26 } from '../../client/ReactDOMComponentTree';
27 import {hasSelectionCapabilities} from '../../client/ReactInputSelection';
28 import {DOCUMENT_NODE} from '../../shared/HTMLNodeType';
@@ -154,7 +154,7 @@ function extractEvents(
154 targetContainer: EventTarget,
155 ) {
156 if (!enableEagerRootListeners) {
157 - const eventListenerMap = getEventListenerMap(targetContainer);
157 + const eventListenerSet = getEventListenerSet(targetContainer);
158 // Track whether all listeners exists for this plugin. If none exist, we do
159 // not extract events. See #3639.
160 if (
@@ -163,8 +163,8 @@ function extractEvents(
163 // event attached from the onChange plugin and we don't expose an
164 // onSelectionChange event from React.
165 domEventName !== 'selectionchange' &&
166 - !eventListenerMap.has('onSelect') &&
167 - !eventListenerMap.has('onSelectCapture')
166 + !eventListenerSet.has('onSelect') &&
167 + !eventListenerSet.has('onSelectCapture')
168 ) {
169 return;
170 }
packages/react-interactions/events/src/dom/create-event-handle/Focus.js
+10 -43
@@ -37,32 +37,6 @@ const isMac =
37 ? /^Mac/.test(window.navigator.platform)
38 : false;
39
40 -const canUseDOM: boolean = !!(
41 - typeof window !== 'undefined' &&
42 - typeof window.document !== 'undefined' &&
43 - typeof window.document.createElement !== 'undefined'
44 -);
45 -
46 -let passiveBrowserEventsSupported = false;
47 -
48 -// Check if browser support events with passive listeners
49 -// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Safely_detecting_option_support
50 -if (canUseDOM) {
51 - try {
52 - const options = {};
53 - // $FlowFixMe: Ignore Flow complaining about needing a value
54 - Object.defineProperty(options, 'passive', {
55 - get: function() {
56 - passiveBrowserEventsSupported = true;
57 - },
58 - });
59 - window.addEventListener('test', options, options);
60 - window.removeEventListener('test', options, options);
61 - } catch (e) {
62 - passiveBrowserEventsSupported = false;
63 - }
64 -}
65 -
40 const hasPointerEvents =
41 typeof window !== 'undefined' && window.PointerEvent != null;
42
@@ -78,20 +52,13 @@ const globalFocusVisibleEvents = hasPointerEvents
52 'touchend',
53 ];
54
81 -const passiveObject = {passive: true};
82 -const passiveObjectWithPriority = {passive: true, priority: 0};
83 -
55 // Global state for tracking focus visible and emulation of mouse
56 let isGlobalFocusVisible = true;
57 let hasTrackedGlobalFocusVisible = false;
58
59 function trackGlobalFocusVisible() {
60 globalFocusVisibleEvents.forEach(type => {
90 - window.addEventListener(
91 - type,
92 - handleGlobalFocusVisibleEvent,
93 - passiveBrowserEventsSupported ? {capture: true, passive: true} : true,
94 - );
61 + window.addEventListener(type, handleGlobalFocusVisibleEvent, true);
62 });
63 }
64
@@ -171,9 +138,9 @@ function setFocusVisibleListeners(
138
139 function useFocusVisibleInputHandles() {
140 return [
174 - useEvent('mousedown', passiveObject),
175 - useEvent(hasPointerEvents ? 'pointerdown' : 'touchstart', passiveObject),
176 - useEvent('keydown', passiveObject),
141 + useEvent('mousedown'),
142 + useEvent(hasPointerEvents ? 'pointerdown' : 'touchstart'),
143 + useEvent('keydown'),
144 ];
145 }
146
@@ -200,8 +167,8 @@ export function useFocus(
167 const stateRef = useRef<null | {isFocused: boolean, isFocusVisible: boolean}>(
168 {isFocused: false, isFocusVisible: false},
169 );
203 - const focusHandle = useEvent('focusin', passiveObjectWithPriority);
204 - const blurHandle = useEvent('focusout', passiveObjectWithPriority);
170 + const focusHandle = useEvent('focusin');
171 + const blurHandle = useEvent('focusout');
172 const focusVisibleHandles = useFocusVisibleInputHandles();
173
174 useLayoutEffect(() => {
@@ -297,10 +264,10 @@ export function useFocusWithin<T>(
264 const stateRef = useRef<null | {isFocused: boolean, isFocusVisible: boolean}>(
265 {isFocused: false, isFocusVisible: false},
266 );
300 - const focusHandle = useEvent('focusin', passiveObjectWithPriority);
301 - const blurHandle = useEvent('focusout', passiveObjectWithPriority);
302 - const afterBlurHandle = useEvent('afterblur', passiveObject);
303 - const beforeBlurHandle = useEvent('beforeblur', passiveObject);
267 + const focusHandle = useEvent('focusin');
268 + const blurHandle = useEvent('focusout');
269 + const afterBlurHandle = useEvent('afterblur');
270 + const beforeBlurHandle = useEvent('beforeblur');
271 const focusVisibleHandles = useFocusVisibleInputHandles();
272
273 const useFocusWithinRef = useCallback(
packages/react-interactions/events/src/dom/create-event-handle/useEvent.js
-2
@@ -25,8 +25,6 @@ export default function useEvent(
25 event: string,
26 options?: {|
27 capture?: boolean,
28 - passive?: boolean,
29 - priority?: 0 | 1 | 2,
28 |},
29 ): UseEventHandle {
30 const handleRef = useRef<UseEventHandle | null>(null);