Event API: ensure event keys are unique + add validation (#15501)
Dominic Gannaway committed
Apr 26, 2019 at 08:00 UTC
8658611b6c0beb6ae23a88d71fce154640ec47fd
5 files changed
+287
-70
packages/events/EventSystemFlags.js
+2
-1
@@ -13,4 +13,5 @@ export const PLUGIN_EVENT_SYSTEM = 1;
13
export const RESPONDER_EVENT_SYSTEM = 1 << 1;
14
export const IS_PASSIVE = 1 << 2;
15
export const IS_ACTIVE = 1 << 3;
16
-export const PASSIVE_NOT_SUPPORTED = 1 << 4;
16
+export const IS_CAPTURE = 1 << 4;
17
+export const PASSIVE_NOT_SUPPORTED = 1 << 5;
packages/react-dom/src/client/ReactDOMComponent.js
+9
-7
@@ -15,7 +15,10 @@ import {canUseDOM} from 'shared/ExecutionEnvironment';
15
import warningWithoutStack from 'shared/warningWithoutStack';
16
import type {ReactEventResponderEventType} from 'shared/ReactTypes';
17
import type {DOMTopLevelEventType} from 'events/TopLevelEventTypes';
18
-import {setListenToResponderEventTypes} from '../events/DOMEventResponderSystem';
18
+import {
19
+ setListenToResponderEventTypes,
20
+ generateListeningKey,
21
+} from '../events/DOMEventResponderSystem';
22
23
import {
24
getValueForAttribute,
@@ -1320,12 +1323,11 @@ export function listenToEventResponderEventTypes(
1323
capture = targetEventConfigObject.capture;
1324
}
1325
}
1323
- // Create a unique name for this event, plus its properties. We'll
1324
- // use this to ensure we don't listen to the same event with the same
1325
- // properties again.
1326
- const passiveKey = passive ? '_passive' : '_active';
1327
- const captureKey = capture ? '_capture' : '';
1328
- const listeningName = `${topLevelType}${passiveKey}${captureKey}`;
1326
+ const listeningName = generateListeningKey(
1327
+ topLevelType,
1328
+ passive,
1329
+ capture,
1330
+ );
1331
if (!listeningSet.has(listeningName)) {
1332
trapEventForResponderEventSystem(
1333
element,
packages/react-dom/src/events/DOMEventResponderSystem.js
+142
-62
@@ -9,6 +9,7 @@
9
import {
10
type EventSystemFlags,
11
IS_PASSIVE,
12
+ IS_CAPTURE,
13
PASSIVE_NOT_SUPPORTED,
14
} from 'events/EventSystemFlags';
15
import type {AnyNativeEvent} from 'events/PluginModuleType';
@@ -73,7 +74,7 @@ const rootEventTypesToEventComponentInstances: Map<
74
> = new Map();
75
const targetEventTypeCached: Map<
76
Array<ReactEventResponderEventType>,
76
- Set<DOMTopLevelEventType>,
77
+ Set<string>,
78
> = new Map();
79
const ownershipChangeListeners: Set<ReactEventComponentInstance> = new Set();
80
const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
@@ -235,32 +236,8 @@ const eventResponderContext: ReactResponderContext = {
236
listenToResponderEventTypesImpl(rootEventTypes, activeDocument);
237
for (let i = 0; i < rootEventTypes.length; i++) {
238
const rootEventType = rootEventTypes[i];
238
- const topLevelEventType =
239
- typeof rootEventType === 'string' ? rootEventType : rootEventType.name;
240
- let rootEventComponentInstances = rootEventTypesToEventComponentInstances.get(
241
- topLevelEventType,
242
- );
243
- if (rootEventComponentInstances === undefined) {
244
- rootEventComponentInstances = new Set();
245
- rootEventTypesToEventComponentInstances.set(
246
- topLevelEventType,
247
- rootEventComponentInstances,
248
- );
249
- }
250
- const componentInstance = ((currentInstance: any): ReactEventComponentInstance);
251
- let rootEventTypesSet = componentInstance.rootEventTypes;
252
- if (rootEventTypesSet === null) {
253
- rootEventTypesSet = componentInstance.rootEventTypes = new Set();
254
- }
255
- invariant(
256
- !rootEventTypesSet.has(topLevelEventType),
257
- 'addRootEventTypes() found a duplicate root event ' +
258
- 'type of "%s". This might be because the event type exists in the event responder "rootEventTypes" ' +
259
- 'array or because of a previous addRootEventTypes() using this root event type.',
260
- rootEventType,
261
- );
262
- rootEventTypesSet.add(topLevelEventType);
263
- rootEventComponentInstances.add(componentInstance);
239
+ const eventComponentInstance = ((currentInstance: any): ReactEventComponentInstance);
240
+ registerRootEventType(rootEventType, eventComponentInstance);
241
}
242
},
243
removeRootEventTypes(
@@ -269,15 +246,37 @@ const eventResponderContext: ReactResponderContext = {
246
validateResponderContext();
247
for (let i = 0; i < rootEventTypes.length; i++) {
248
const rootEventType = rootEventTypes[i];
272
- const topLevelEventType =
273
- typeof rootEventType === 'string' ? rootEventType : rootEventType.name;
249
+ let name = rootEventType;
250
+ let capture = false;
251
+ let passive = true;
252
+
253
+ if (typeof rootEventType !== 'string') {
254
+ const targetEventConfigObject = ((rootEventType: any): {
255
+ name: string,
256
+ passive?: boolean,
257
+ capture?: boolean,
258
+ });
259
+ name = targetEventConfigObject.name;
260
+ if (targetEventConfigObject.passive !== undefined) {
261
+ passive = targetEventConfigObject.passive;
262
+ }
263
+ if (targetEventConfigObject.capture !== undefined) {
264
+ capture = targetEventConfigObject.capture;
265
+ }
266
+ }
267
+
268
+ const listeningName = generateListeningKey(
269
+ ((name: any): string),
270
+ passive,
271
+ capture,
272
+ );
273
let rootEventComponents = rootEventTypesToEventComponentInstances.get(
275
- topLevelEventType,
274
+ listeningName,
275
);
276
let rootEventTypesSet = ((currentInstance: any): ReactEventComponentInstance)
277
.rootEventTypes;
278
if (rootEventTypesSet !== null) {
280
- rootEventTypesSet.delete(topLevelEventType);
279
+ rootEventTypesSet.delete(listeningName);
280
}
281
if (rootEventComponents !== undefined) {
282
rootEventComponents.delete(
@@ -476,14 +475,15 @@ function createResponderEvent(
475
topLevelType: string,
476
nativeEvent: AnyNativeEvent,
477
nativeEventTarget: Element | Document,
479
- eventSystemFlags: EventSystemFlags,
478
+ passive: boolean,
479
+ passiveSupported: boolean,
480
): ReactResponderEvent {
481
const responderEvent = {
482
nativeEvent: nativeEvent,
483
target: nativeEventTarget,
484
type: topLevelType,
485
- passive: (eventSystemFlags & IS_PASSIVE) !== 0,
486
- passiveSupported: (eventSystemFlags & PASSIVE_NOT_SUPPORTED) === 0,
485
+ passive,
486
+ passiveSupported,
487
};
488
if (__DEV__) {
489
Object.freeze(responderEvent);
@@ -529,16 +529,37 @@ export function processEventQueue(): void {
529
530
function getTargetEventTypesSet(
531
eventTypes: Array<ReactEventResponderEventType>,
532
-): Set<DOMTopLevelEventType> {
532
+): Set<string> {
533
let cachedSet = targetEventTypeCached.get(eventTypes);
534
535
if (cachedSet === undefined) {
536
cachedSet = new Set();
537
for (let i = 0; i < eventTypes.length; i++) {
538
const eventType = eventTypes[i];
539
- const topLevelEventType =
540
- typeof eventType === 'string' ? eventType : eventType.name;
541
- cachedSet.add(((topLevelEventType: any): DOMTopLevelEventType));
539
+ let name = eventType;
540
+ let capture = false;
541
+ let passive = true;
542
+
543
+ if (typeof eventType !== 'string') {
544
+ const targetEventConfigObject = ((eventType: any): {
545
+ name: string,
546
+ passive?: boolean,
547
+ capture?: boolean,
548
+ });
549
+ name = targetEventConfigObject.name;
550
+ if (targetEventConfigObject.passive !== undefined) {
551
+ passive = targetEventConfigObject.passive;
552
+ }
553
+ if (targetEventConfigObject.capture !== undefined) {
554
+ capture = targetEventConfigObject.capture;
555
+ }
556
+ }
557
+ const listeningName = generateListeningKey(
558
+ ((name: any): string),
559
+ passive,
560
+ capture,
561
+ );
562
+ cachedSet.add(listeningName);
563
}
564
targetEventTypeCached.set(eventTypes, cachedSet);
565
}
@@ -546,7 +567,7 @@ function getTargetEventTypesSet(
567
}
568
569
function getTargetEventResponderInstances(
549
- topLevelType: DOMTopLevelEventType,
570
+ listeningName: string,
571
targetFiber: null | Fiber,
572
): Array<ReactEventComponentInstance> {
573
const eventResponderInstances = [];
@@ -560,7 +581,7 @@ function getTargetEventResponderInstances(
581
// Validate the target event type exists on the responder
582
if (targetEventTypes !== undefined) {
583
const targetEventTypesSet = getTargetEventTypesSet(targetEventTypes);
563
- if (targetEventTypesSet.has(topLevelType)) {
584
+ if (targetEventTypesSet.has(listeningName)) {
585
eventResponderInstances.push(eventComponentInstance);
586
}
587
}
@@ -571,11 +592,11 @@ function getTargetEventResponderInstances(
592
}
593
594
function getRootEventResponderInstances(
574
- topLevelType: DOMTopLevelEventType,
595
+ listeningName: string,
596
): Array<ReactEventComponentInstance> {
597
const eventResponderInstances = [];
598
const rootEventInstances = rootEventTypesToEventComponentInstances.get(
578
- topLevelType,
599
+ listeningName,
600
);
601
if (rootEventInstances !== undefined) {
602
const rootEventComponentInstances = Array.from(rootEventInstances);
@@ -618,20 +639,30 @@ function traverseAndHandleEventResponderInstances(
639
nativeEventTarget: EventTarget,
640
eventSystemFlags: EventSystemFlags,
641
): void {
642
+ const isPassiveEvent = (eventSystemFlags & IS_PASSIVE) !== 0;
643
+ const isCaptureEvent = (eventSystemFlags & IS_CAPTURE) !== 0;
644
+ const isPassiveSupported = (eventSystemFlags & PASSIVE_NOT_SUPPORTED) === 0;
645
+ const listeningName = generateListeningKey(
646
+ ((topLevelType: any): string),
647
+ isPassiveEvent || !isPassiveSupported,
648
+ isCaptureEvent,
649
+ );
650
+
651
// Trigger event responders in this order:
652
// - Capture target phase
653
// - Bubble target phase
654
// - Root phase
655
656
const targetEventResponderInstances = getTargetEventResponderInstances(
627
- topLevelType,
657
+ listeningName,
658
targetFiber,
659
);
660
const responderEvent = createResponderEvent(
661
((topLevelType: any): string),
662
nativeEvent,
663
((nativeEventTarget: any): Element | Document),
634
- eventSystemFlags,
664
+ isPassiveEvent,
665
+ isPassiveSupported,
666
);
667
const propagatedEventResponders: Set<ReactEventResponder> = new Set();
668
let length = targetEventResponderInstances.length;
@@ -684,7 +715,7 @@ function traverseAndHandleEventResponderInstances(
715
}
716
// Root phase
717
const rootEventResponderInstances = getRootEventResponderInstances(
687
- topLevelType,
718
+ listeningName,
719
);
720
length = rootEventResponderInstances.length;
721
if (length > 0) {
@@ -835,25 +866,74 @@ export function addRootEventTypesForComponentInstance(
866
): void {
867
for (let i = 0; i < rootEventTypes.length; i++) {
868
const rootEventType = rootEventTypes[i];
838
- const topLevelEventType =
839
- typeof rootEventType === 'string' ? rootEventType : rootEventType.name;
840
- let rootEventComponentInstances = rootEventTypesToEventComponentInstances.get(
841
- topLevelEventType,
842
- );
843
- if (rootEventComponentInstances === undefined) {
844
- rootEventComponentInstances = new Set();
845
- rootEventTypesToEventComponentInstances.set(
846
- topLevelEventType,
847
- rootEventComponentInstances,
848
- );
869
+ registerRootEventType(rootEventType, eventComponentInstance);
870
+ }
871
+}
872
+
873
+function registerRootEventType(
874
+ rootEventType: ReactEventResponderEventType,
875
+ eventComponentInstance: ReactEventComponentInstance,
876
+): void {
877
+ let name = rootEventType;
878
+ let capture = false;
879
+ let passive = true;
880
+
881
+ if (typeof rootEventType !== 'string') {
882
+ const targetEventConfigObject = ((rootEventType: any): {
883
+ name: string,
884
+ passive?: boolean,
885
+ capture?: boolean,
886
+ });
887
+ name = targetEventConfigObject.name;
888
+ if (targetEventConfigObject.passive !== undefined) {
889
+ passive = targetEventConfigObject.passive;
890
}
850
- let rootEventTypesSet = eventComponentInstance.rootEventTypes;
851
- if (rootEventTypesSet === null) {
852
- rootEventTypesSet = eventComponentInstance.rootEventTypes = new Set();
891
+ if (targetEventConfigObject.capture !== undefined) {
892
+ capture = targetEventConfigObject.capture;
893
}
854
- rootEventTypesSet.add(topLevelEventType);
855
- rootEventComponentInstances.add(
856
- ((eventComponentInstance: any): ReactEventComponentInstance),
894
+ }
895
+
896
+ const listeningName = generateListeningKey(
897
+ ((name: any): string),
898
+ passive,
899
+ capture,
900
+ );
901
+ let rootEventComponentInstances = rootEventTypesToEventComponentInstances.get(
902
+ listeningName,
903
+ );
904
+ if (rootEventComponentInstances === undefined) {
905
+ rootEventComponentInstances = new Set();
906
+ rootEventTypesToEventComponentInstances.set(
907
+ listeningName,
908
+ rootEventComponentInstances,
909
);
910
}
911
+ let rootEventTypesSet = eventComponentInstance.rootEventTypes;
912
+ if (rootEventTypesSet === null) {
913
+ rootEventTypesSet = eventComponentInstance.rootEventTypes = new Set();
914
+ }
915
+ invariant(
916
+ !rootEventTypesSet.has(listeningName),
917
+ 'addRootEventTypes() found a duplicate root event ' +
918
+ 'type of "%s". This might be because the event type exists in the event responder "rootEventTypes" ' +
919
+ 'array or because of a previous addRootEventTypes() using this root event type.',
920
+ name,
921
+ );
922
+ rootEventTypesSet.add(listeningName);
923
+ rootEventComponentInstances.add(
924
+ ((eventComponentInstance: any): ReactEventComponentInstance),
925
+ );
926
+}
927
+
928
+export function generateListeningKey(
929
+ topLevelType: string,
930
+ passive: boolean,
931
+ capture: boolean,
932
+): string {
933
+ // Create a unique name for this event, plus its properties. We'll
934
+ // use this to ensure we don't listen to the same event with the same
935
+ // properties again.
936
+ const passiveKey = passive ? '_passive' : '_active';
937
+ const captureKey = capture ? '_capture' : '';
938
+ return `${topLevelType}${passiveKey}${captureKey}`;
939
}
packages/react-dom/src/events/ReactDOMEventListener.js
+4
@@ -22,6 +22,7 @@ import {
22
RESPONDER_EVENT_SYSTEM,
23
IS_PASSIVE,
24
IS_ACTIVE,
25
+ IS_CAPTURE,
26
PASSIVE_NOT_SUPPORTED,
27
} from 'events/EventSystemFlags';
28
@@ -189,6 +190,9 @@ export function trapEventForResponderEventSystem(
190
} else {
191
eventFlags |= IS_ACTIVE;
192
}
193
+ if (capture) {
194
+ eventFlags |= IS_CAPTURE;
195
+ }
196
// Check if interactive and wrap in interactiveUpdates
197
const listener = dispatchEvent.bind(null, topLevelType, eventFlags);
198
addEventListener(element, rawEventName, listener, {
packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js
+130
@@ -757,4 +757,134 @@ describe('DOMEventResponderSystem', () => {
757
758
expect(log).toEqual([false, true, false]);
759
});
760
+
761
+ it('the event responder target listeners should correctly fire for only their events', () => {
762
+ let clickEventComponent1Fired = 0;
763
+ let clickEventComponent2Fired = 0;
764
+ let eventLog = [];
765
+ const buttonRef = React.createRef();
766
+
767
+ const ClickEventComponent1 = createReactEventComponent(
768
+ [{name: 'click', passive: false, capture: false}],
769
+ undefined,
770
+ undefined,
771
+ event => {
772
+ clickEventComponent1Fired++;
773
+ eventLog.push({
774
+ name: event.type,
775
+ passive: event.passive,
776
+ passiveSupported: event.passiveSupported,
777
+ });
778
+ },
779
+ );
780
+
781
+ const ClickEventComponent2 = createReactEventComponent(
782
+ [{name: 'click', passive: true, capture: false}],
783
+ undefined,
784
+ undefined,
785
+ event => {
786
+ clickEventComponent2Fired++;
787
+ eventLog.push({
788
+ name: event.type,
789
+ passive: event.passive,
790
+ passiveSupported: event.passiveSupported,
791
+ });
792
+ },
793
+ );
794
+
795
+ const Test = () => (
796
+ <ClickEventComponent1>
797
+ <ClickEventComponent2>
798
+ <button ref={buttonRef}>Click me!</button>
799
+ </ClickEventComponent2>
800
+ </ClickEventComponent1>
801
+ );
802
+
803
+ ReactDOM.render(<Test />, container);
804
+
805
+ let buttonElement = buttonRef.current;
806
+ dispatchClickEvent(buttonElement);
807
+
808
+ expect(clickEventComponent1Fired).toBe(1);
809
+ expect(clickEventComponent2Fired).toBe(1);
810
+ expect(eventLog.length).toBe(2);
811
+ expect(eventLog).toEqual([
812
+ {
813
+ name: 'click',
814
+ passive: false,
815
+ passiveSupported: false,
816
+ },
817
+ {
818
+ name: 'click',
819
+ passive: false,
820
+ passiveSupported: true,
821
+ },
822
+ ]);
823
+ });
824
+
825
+ it('the event responder root listeners should correctly fire for only their events', () => {
826
+ let clickEventComponent1Fired = 0;
827
+ let clickEventComponent2Fired = 0;
828
+ let eventLog = [];
829
+
830
+ const ClickEventComponent1 = createReactEventComponent(
831
+ undefined,
832
+ [{name: 'click', passive: false, capture: false}],
833
+ undefined,
834
+ undefined,
835
+ undefined,
836
+ event => {
837
+ clickEventComponent1Fired++;
838
+ eventLog.push({
839
+ name: event.type,
840
+ passive: event.passive,
841
+ passiveSupported: event.passiveSupported,
842
+ });
843
+ },
844
+ );
845
+
846
+ const ClickEventComponent2 = createReactEventComponent(
847
+ undefined,
848
+ [{name: 'click', passive: true, capture: false}],
849
+ undefined,
850
+ undefined,
851
+ undefined,
852
+ event => {
853
+ clickEventComponent2Fired++;
854
+ eventLog.push({
855
+ name: event.type,
856
+ passive: event.passive,
857
+ passiveSupported: event.passiveSupported,
858
+ });
859
+ },
860
+ );
861
+
862
+ const Test = () => (
863
+ <ClickEventComponent1>
864
+ <ClickEventComponent2>
865
+ <button>Click me!</button>
866
+ </ClickEventComponent2>
867
+ </ClickEventComponent1>
868
+ );
869
+
870
+ ReactDOM.render(<Test />, container);
871
+
872
+ dispatchClickEvent(document.body);
873
+
874
+ expect(clickEventComponent1Fired).toBe(1);
875
+ expect(clickEventComponent2Fired).toBe(1);
876
+ expect(eventLog.length).toBe(2);
877
+ expect(eventLog).toEqual([
878
+ {
879
+ name: 'click',
880
+ passive: false,
881
+ passiveSupported: false,
882
+ },
883
+ {
884
+ name: 'click',
885
+ passive: false,
886
+ passiveSupported: true,
887
+ },
888
+ ]);
889
+ });
890
});