[react-interactions] Upgrade passive event listeners to active listeners (#17513)
Dominic Gannaway committed
Dec 4, 2019 at 19:30 UTC
acfe4b21b2621da3618f9096c8be06b32d6247ec
8 files changed
+145
-114
packages/react-dom/src/client/ReactDOMComponent.js
+35
-11
@@ -14,7 +14,6 @@ import warning from 'shared/warning';
14
import {canUseDOM} from 'shared/ExecutionEnvironment';
15
import warningWithoutStack from 'shared/warningWithoutStack';
16
import endsWith from 'shared/endsWith';
17
-import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes';
17
import {setListenToResponderEventTypes} from '../events/DOMEventResponderSystem';
18
19
import {
@@ -63,9 +62,12 @@ import {
62
import {
63
listenTo,
64
trapBubbledEvent,
66
- getListeningSetForElement,
65
+ getListenerMapForElement,
66
} from '../events/ReactBrowserEventEmitter';
68
-import {trapEventForResponderEventSystem} from '../events/ReactDOMEventListener.js';
67
+import {
68
+ addResponderEventSystemEvent,
69
+ removeActiveResponderEventSystemEvent,
70
+} from '../events/ReactDOMEventListener.js';
71
import {mediaEventTypes} from '../events/DOMTopLevelEventTypes';
72
import {
73
createDangerousStringForStyles,
@@ -1307,12 +1309,12 @@ export function restoreControlledState(
1309
1310
export function listenToEventResponderEventTypes(
1311
eventTypes: Array<string>,
1310
- element: Element | Document,
1312
+ document: Document,
1313
): void {
1314
if (enableFlareAPI) {
1313
- // Get the listening Set for this element. We use this to track
1315
+ // Get the listening Map for this element. We use this to track
1316
// what events we're listening to.
1315
- const listeningSet = getListeningSetForElement(element);
1317
+ const listenerMap = getListenerMapForElement(document);
1318
1319
// Go through each target event type of the event responder
1320
for (let i = 0, length = eventTypes.length; i < length; ++i) {
@@ -1322,13 +1324,35 @@ export function listenToEventResponderEventTypes(
1324
const targetEventType = isPassive
1325
? eventType
1326
: eventType.substring(0, eventType.length - 7);
1325
- if (!listeningSet.has(eventKey)) {
1326
- trapEventForResponderEventSystem(
1327
- element,
1328
- ((targetEventType: any): DOMTopLevelEventType),
1327
+ if (!listenerMap.has(eventKey)) {
1328
+ if (isPassive) {
1329
+ const activeKey = targetEventType + '_active';
1330
+ // If we have an active event listener, do not register
1331
+ // a passive event listener. We use the same active event
1332
+ // listener.
1333
+ if (listenerMap.has(activeKey)) {
1334
+ continue;
1335
+ }
1336
+ } else {
1337
+ // If we have a passive event listener, remove the
1338
+ // existing passive event listener before we add the
1339
+ // active event listener.
1340
+ const passiveKey = targetEventType + '_passive';
1341
+ const passiveListener = listenerMap.get(passiveKey);
1342
+ if (passiveListener != null) {
1343
+ removeActiveResponderEventSystemEvent(
1344
+ document,
1345
+ targetEventType,
1346
+ passiveListener,
1347
+ );
1348
+ }
1349
+ }
1350
+ const eventListener = addResponderEventSystemEvent(
1351
+ document,
1352
+ targetEventType,
1353
isPassive,
1354
);
1331
- listeningSet.add(eventKey);
1355
+ listenerMap.set(eventKey, eventListener);
1356
}
1357
}
1358
}
packages/react-dom/src/events/DOMEventResponderSystem.js
+28
-11
@@ -290,7 +290,6 @@ function createDOMResponderEvent(
290
nativeEvent: AnyNativeEvent,
291
nativeEventTarget: Element | Document,
292
passive: boolean,
293
- passiveSupported: boolean,
293
): ReactDOMResponderEvent {
294
const {buttons, pointerType} = (nativeEvent: any);
295
let eventPointerType = '';
@@ -308,7 +307,6 @@ function createDOMResponderEvent(
307
return {
308
nativeEvent: nativeEvent,
309
passive,
311
- passiveSupported,
310
pointerType: eventPointerType,
311
target: nativeEventTarget,
312
type: topLevelType,
@@ -318,9 +316,11 @@ function createDOMResponderEvent(
316
function responderEventTypesContainType(
317
eventTypes: Array<string>,
318
type: string,
319
+ isPassive: boolean,
320
): boolean {
321
for (let i = 0, len = eventTypes.length; i < len; i++) {
323
- if (eventTypes[i] === type) {
322
+ const eventType = eventTypes[i];
323
+ if (eventType === type || (!isPassive && eventType === type + '_active')) {
324
return true;
325
}
326
}
@@ -330,11 +330,16 @@ function responderEventTypesContainType(
330
function validateResponderTargetEventTypes(
331
eventType: string,
332
responder: ReactDOMEventResponder,
333
+ isPassive: boolean,
334
): boolean {
335
const {targetEventTypes} = responder;
336
// Validate the target event type exists on the responder
337
if (targetEventTypes !== null) {
337
- return responderEventTypesContainType(targetEventTypes, eventType);
338
+ return responderEventTypesContainType(
339
+ targetEventTypes,
340
+ eventType,
341
+ isPassive,
342
+ );
343
}
344
return false;
345
}
@@ -349,7 +354,6 @@ function traverseAndHandleEventResponderInstances(
354
const isPassiveEvent = (eventSystemFlags & IS_PASSIVE) !== 0;
355
const isPassiveSupported = (eventSystemFlags & PASSIVE_NOT_SUPPORTED) === 0;
356
const isPassive = isPassiveEvent || !isPassiveSupported;
352
- const eventType = isPassive ? topLevelType : topLevelType + '_active';
357
358
// Trigger event responders in this order:
359
// - Bubble target responder phase
@@ -361,7 +365,6 @@ function traverseAndHandleEventResponderInstances(
365
nativeEvent,
366
nativeEventTarget,
367
isPassiveEvent,
364
- isPassiveSupported,
368
);
369
let node = targetFiber;
370
let insidePortal = false;
@@ -381,7 +384,11 @@ function traverseAndHandleEventResponderInstances(
384
const {props, responder, state} = responderInstance;
385
if (
386
!visitedResponders.has(responder) &&
384
- validateResponderTargetEventTypes(eventType, responder) &&
387
+ validateResponderTargetEventTypes(
388
+ topLevelType,
389
+ responder,
390
+ isPassive,
391
+ ) &&
392
(!insidePortal || responder.targetPortalPropagation)
393
) {
394
visitedResponders.add(responder);
@@ -401,10 +408,20 @@ function traverseAndHandleEventResponderInstances(
408
node = node.return;
409
}
410
// Root phase
404
- const rootEventResponderInstances = rootEventTypesToEventResponderInstances.get(
405
- eventType,
406
- );
407
- if (rootEventResponderInstances !== undefined) {
411
+ const passive = rootEventTypesToEventResponderInstances.get(topLevelType);
412
+ const rootEventResponderInstances = [];
413
+ if (passive !== undefined) {
414
+ rootEventResponderInstances.push(...Array.from(passive));
415
+ }
416
+ if (!isPassive) {
417
+ const active = rootEventTypesToEventResponderInstances.get(
418
+ topLevelType + '_active',
419
+ );
420
+ if (active !== undefined) {
421
+ rootEventResponderInstances.push(...Array.from(active));
422
+ }
423
+ }
424
+ if (rootEventResponderInstances.length > 0) {
425
const responderInstances = Array.from(rootEventResponderInstances);
426
427
for (let i = 0; i < responderInstances.length; i++) {
packages/react-dom/src/events/ReactBrowserEventEmitter.js
+17
-17
@@ -86,22 +86,22 @@ import isEventSupported from './isEventSupported';
86
*/
87
88
const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
89
-const elementListeningSets:
89
+const elementListenerMap:
90
| WeakMap
91
| Map<
92
Document | Element | Node,
93
- Set<DOMTopLevelEventType | string>,
93
+ Map<DOMTopLevelEventType | string, null | (any => void)>,
94
> = new PossiblyWeakMap();
95
96
-export function getListeningSetForElement(
96
+export function getListenerMapForElement(
97
element: Document | Element | Node,
98
-): Set<DOMTopLevelEventType | string> {
99
- let listeningSet = elementListeningSets.get(element);
100
- if (listeningSet === undefined) {
101
- listeningSet = new Set();
102
- elementListeningSets.set(element, listeningSet);
98
+): Map<DOMTopLevelEventType | string, null | (any => void)> {
99
+ let listenerMap = elementListenerMap.get(element);
100
+ if (listenerMap === undefined) {
101
+ listenerMap = new Map();
102
+ elementListenerMap.set(element, listenerMap);
103
}
104
- return listeningSet;
104
+ return listenerMap;
105
}
106
107
/**
@@ -129,7 +129,7 @@ export function listenTo(
129
registrationName: string,
130
mountAt: Document | Element | Node,
131
): void {
132
- const listeningSet = getListeningSetForElement(mountAt);
132
+ const listeningSet = getListenerMapForElement(mountAt);
133
const dependencies = registrationNameDependencies[registrationName];
134
135
for (let i = 0; i < dependencies.length; i++) {
@@ -141,9 +141,9 @@ export function listenTo(
141
export function listenToTopLevel(
142
topLevelType: DOMTopLevelEventType,
143
mountAt: Document | Element | Node,
144
- listeningSet: Set<DOMTopLevelEventType | string>,
144
+ listenerMap: Map<DOMTopLevelEventType | string, null | (any => void)>,
145
): void {
146
- if (!listeningSet.has(topLevelType)) {
146
+ if (!listenerMap.has(topLevelType)) {
147
switch (topLevelType) {
148
case TOP_SCROLL:
149
trapCapturedEvent(TOP_SCROLL, mountAt);
@@ -154,8 +154,8 @@ export function listenToTopLevel(
154
trapCapturedEvent(TOP_BLUR, mountAt);
155
// We set the flag for a single dependency later in this function,
156
// but this ensures we mark both as attached rather than just one.
157
- listeningSet.add(TOP_BLUR);
158
- listeningSet.add(TOP_FOCUS);
157
+ listenerMap.set(TOP_BLUR, null);
158
+ listenerMap.set(TOP_FOCUS, null);
159
break;
160
case TOP_CANCEL:
161
case TOP_CLOSE:
@@ -178,7 +178,7 @@ export function listenToTopLevel(
178
}
179
break;
180
}
181
- listeningSet.add(topLevelType);
181
+ listenerMap.set(topLevelType, null);
182
}
183
}
184
@@ -186,12 +186,12 @@ export function isListeningToAllDependencies(
186
registrationName: string,
187
mountAt: Document | Element,
188
): boolean {
189
- const listeningSet = getListeningSetForElement(mountAt);
189
+ const listenerMap = getListenerMapForElement(mountAt);
190
const dependencies = registrationNameDependencies[registrationName];
191
192
for (let i = 0; i < dependencies.length; i++) {
193
const dependency = dependencies[i];
194
- if (!listeningSet.has(dependency)) {
194
+ if (!listenerMap.has(dependency)) {
195
return false;
196
}
197
}
packages/react-dom/src/events/ReactDOMEventListener.js
+49
-32
@@ -211,42 +211,59 @@ export function trapCapturedEvent(
211
trapEventForPluginEventSystem(element, topLevelType, true);
212
}
213
214
-export function trapEventForResponderEventSystem(
215
- element: Document | Element | Node,
216
- topLevelType: DOMTopLevelEventType,
214
+export function addResponderEventSystemEvent(
215
+ document: Document,
216
+ topLevelType: string,
217
passive: boolean,
218
-): void {
219
- if (enableFlareAPI) {
220
- const rawEventName = getRawEventName(topLevelType);
221
- let eventFlags = RESPONDER_EVENT_SYSTEM;
222
-
223
- // If passive option is not supported, then the event will be
224
- // active and not passive, but we flag it as using not being
225
- // supported too. This way the responder event plugins know,
226
- // and can provide polyfills if needed.
227
- if (passive) {
228
- if (passiveBrowserEventsSupported) {
229
- eventFlags |= IS_PASSIVE;
230
- } else {
231
- eventFlags |= IS_ACTIVE;
232
- eventFlags |= PASSIVE_NOT_SUPPORTED;
233
- passive = false;
234
- }
235
- } else {
236
- eventFlags |= IS_ACTIVE;
237
- }
238
- // Check if interactive and wrap in discreteUpdates
239
- const listener = dispatchEvent.bind(null, topLevelType, eventFlags);
218
+): any => void {
219
+ let eventFlags = RESPONDER_EVENT_SYSTEM;
220
+
221
+ // If passive option is not supported, then the event will be
222
+ // active and not passive, but we flag it as using not being
223
+ // supported too. This way the responder event plugins know,
224
+ // and can provide polyfills if needed.
225
+ if (passive) {
226
if (passiveBrowserEventsSupported) {
241
- addEventCaptureListenerWithPassiveFlag(
242
- element,
243
- rawEventName,
244
- listener,
245
- passive,
246
- );
227
+ eventFlags |= IS_PASSIVE;
228
} else {
248
- addEventCaptureListener(element, rawEventName, listener);
229
+ eventFlags |= IS_ACTIVE;
230
+ eventFlags |= PASSIVE_NOT_SUPPORTED;
231
+ passive = false;
232
}
233
+ } else {
234
+ eventFlags |= IS_ACTIVE;
235
+ }
236
+ // Check if interactive and wrap in discreteUpdates
237
+ const listener = dispatchEvent.bind(
238
+ null,
239
+ ((topLevelType: any): DOMTopLevelEventType),
240
+ eventFlags,
241
+ );
242
+ if (passiveBrowserEventsSupported) {
243
+ addEventCaptureListenerWithPassiveFlag(
244
+ document,
245
+ topLevelType,
246
+ listener,
247
+ passive,
248
+ );
249
+ } else {
250
+ addEventCaptureListener(document, topLevelType, listener);
251
+ }
252
+ return listener;
253
+}
254
+
255
+export function removeActiveResponderEventSystemEvent(
256
+ document: Document,
257
+ topLevelType: string,
258
+ listener: any => void,
259
+) {
260
+ if (passiveBrowserEventsSupported) {
261
+ document.removeEventListener(topLevelType, listener, {
262
+ capture: true,
263
+ passive: false,
264
+ });
265
+ } else {
266
+ document.removeEventListener(topLevelType, listener, true);
267
}
268
}
269
packages/react-dom/src/events/ReactDOMEventReplaying.js
+16
-24
@@ -30,10 +30,10 @@ import {
30
} from 'react-reconciler/reflection';
31
import {
32
attemptToDispatchEvent,
33
- trapEventForResponderEventSystem,
33
+ addResponderEventSystemEvent,
34
} from './ReactDOMEventListener';
35
import {
36
- getListeningSetForElement,
36
+ getListenerMapForElement,
37
listenToTopLevel,
38
} from './ReactBrowserEventEmitter';
39
import {
@@ -214,41 +214,33 @@ export function isReplayableDiscreteEvent(
214
function trapReplayableEvent(
215
topLevelType: DOMTopLevelEventType,
216
document: Document,
217
- listeningSet: Set<DOMTopLevelEventType | string>,
217
+ listenerMap: Map<DOMTopLevelEventType | string, null | (any => void)>,
218
) {
219
- listenToTopLevel(topLevelType, document, listeningSet);
219
+ listenToTopLevel(topLevelType, document, listenerMap);
220
if (enableFlareAPI) {
221
// Trap events for the responder system.
222
- const passiveEventKey =
223
- unsafeCastDOMTopLevelTypeToString(topLevelType) + '_passive';
224
- if (!listeningSet.has(passiveEventKey)) {
225
- trapEventForResponderEventSystem(document, topLevelType, true);
226
- listeningSet.add(passiveEventKey);
227
- }
228
- // TODO: This listens to all events as active which might have
229
- // undesirable effects. It's also unnecessary to have both
230
- // passive and active listeners. Instead, we could start with
231
- // a passive and upgrade it to an active one if needed.
232
- // For replaying purposes the active is never needed since we
233
- // currently don't preventDefault.
234
- const activeEventKey =
235
- unsafeCastDOMTopLevelTypeToString(topLevelType) + '_active';
236
- if (!listeningSet.has(activeEventKey)) {
237
- trapEventForResponderEventSystem(document, topLevelType, false);
238
- listeningSet.add(activeEventKey);
222
+ const topLevelTypeString = unsafeCastDOMTopLevelTypeToString(topLevelType);
223
+ const passiveEventKey = topLevelTypeString + '_passive';
224
+ if (!listenerMap.has(passiveEventKey)) {
225
+ const listener = addResponderEventSystemEvent(
226
+ document,
227
+ topLevelTypeString,
228
+ true,
229
+ );
230
+ listenerMap.set(passiveEventKey, listener);
231
}
232
}
233
}
234
235
export function eagerlyTrapReplayableEvents(document: Document) {
244
- const listeningSet = getListeningSetForElement(document);
236
+ const listenerMap = getListenerMapForElement(document);
237
// Discrete
238
discreteReplayableEvents.forEach(topLevelType => {
247
- trapReplayableEvent(topLevelType, document, listeningSet);
239
+ trapReplayableEvent(topLevelType, document, listenerMap);
240
});
241
// Continuous
242
continuousReplayableEvents.forEach(topLevelType => {
251
- trapReplayableEvent(topLevelType, document, listeningSet);
243
+ trapReplayableEvent(topLevelType, document, listenerMap);
244
});
245
}
246
packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js
-17
@@ -156,7 +156,6 @@ describe('DOMEventResponderSystem', () => {
156
eventLog.push({
157
name: event.type,
158
passive: event.passive,
159
- passiveSupported: event.passiveSupported,
159
phase: 'bubble',
160
});
161
},
@@ -185,7 +184,6 @@ describe('DOMEventResponderSystem', () => {
184
{
185
name: 'click',
186
passive: false,
188
- passiveSupported: false,
187
phase: 'bubble',
188
},
189
]);
@@ -216,7 +214,6 @@ describe('DOMEventResponderSystem', () => {
214
eventLog.push({
215
name: event.type,
216
passive: event.passive,
219
- passiveSupported: event.passiveSupported,
217
phase: 'bubble',
218
});
219
},
@@ -242,7 +239,6 @@ describe('DOMEventResponderSystem', () => {
239
{
240
name: 'click',
241
passive: true,
245
- passiveSupported: true,
242
phase: 'bubble',
243
},
244
]);
@@ -260,7 +256,6 @@ describe('DOMEventResponderSystem', () => {
256
eventLog.push({
257
name: event.type,
258
passive: event.passive,
263
- passiveSupported: event.passiveSupported,
259
phase: 'bubble',
260
});
261
},
@@ -296,7 +291,6 @@ describe('DOMEventResponderSystem', () => {
291
{
292
name: 'click',
293
passive: false,
299
- passiveSupported: false,
294
phase: 'bubble',
295
},
296
]);
@@ -327,7 +321,6 @@ describe('DOMEventResponderSystem', () => {
321
{
322
name: 'click',
323
passive: false,
330
- passiveSupported: false,
324
phase: 'bubble',
325
},
326
]);
@@ -634,7 +627,6 @@ describe('DOMEventResponderSystem', () => {
627
eventLog.push({
628
name: event.type,
629
passive: event.passive,
637
- passiveSupported: event.passiveSupported,
630
phase: 'root',
631
});
632
},
@@ -656,7 +648,6 @@ describe('DOMEventResponderSystem', () => {
648
{
649
name: 'click',
650
passive: false,
659
- passiveSupported: false,
651
phase: 'root',
652
},
653
]);
@@ -675,7 +666,6 @@ describe('DOMEventResponderSystem', () => {
666
eventLog.push({
667
name: event.type,
668
passive: event.passive,
678
- passiveSupported: event.passiveSupported,
669
});
670
},
671
});
@@ -687,7 +677,6 @@ describe('DOMEventResponderSystem', () => {
677
eventLog.push({
678
name: event.type,
679
passive: event.passive,
690
- passiveSupported: event.passiveSupported,
680
});
681
},
682
});
@@ -717,12 +706,10 @@ describe('DOMEventResponderSystem', () => {
706
{
707
name: 'click',
708
passive: false,
720
- passiveSupported: false,
709
},
710
{
711
name: 'click',
712
passive: false,
725
- passiveSupported: true,
713
},
714
]);
715
});
@@ -739,7 +726,6 @@ describe('DOMEventResponderSystem', () => {
726
eventLog.push({
727
name: event.type,
728
passive: event.passive,
742
- passiveSupported: event.passiveSupported,
729
});
730
},
731
});
@@ -751,7 +737,6 @@ describe('DOMEventResponderSystem', () => {
737
eventLog.push({
738
name: event.type,
739
passive: event.passive,
754
- passiveSupported: event.passiveSupported,
740
});
741
},
742
});
@@ -778,12 +763,10 @@ describe('DOMEventResponderSystem', () => {
763
{
764
name: 'click',
765
passive: false,
781
- passiveSupported: false,
766
},
767
{
768
name: 'click',
769
passive: false,
786
- passiveSupported: true,
770
},
771
]);
772
packages/react-interactions/events/README.md
-1
@@ -28,7 +28,6 @@ type ResponderEvent = {|
28
pointerType: string,
29
type: string,
30
passive: boolean,
31
- passiveSupported: boolean,
31
|};
32
33
type CustomEvent = {
packages/shared/ReactDOMTypes.js
-1
@@ -27,7 +27,6 @@ export type PointerType =
27
export type ReactDOMResponderEvent = {
28
nativeEvent: AnyNativeEvent,
29
passive: boolean,
30
- passiveSupported: boolean,
30
pointerType: PointerType,
31
target: Element | Document,
32
type: string,