@samitouri / QOS-React / commits / 8658611b6c

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 });