@samitouri / QOS-React / commits / 017d6f14b7

Experimental Event API: add `rootEventTypes` support to event responders (#15475)

* Adds rootEventTypes

Dominic Gannaway committed Apr 23, 2019 at 19:55 UTC 017d6f14b75d4e05a438e34fe0967d730ce24185
6 files changed +141 -8
packages/react-dom/src/client/ReactDOMComponent.js
+1 -1
@@ -1302,7 +1302,7 @@ export function listenToEventResponderEventTypes(
1302 if (__DEV__) {
1303 warning(
1304 typeof targetEventType === 'object' && targetEventType !== null,
1305 - 'Event Responder: invalid entry in targetEventTypes array. ' +
1305 + 'Event Responder: invalid entry in event types array. ' +
1306 'Entry must be string or an object. Instead, got %s.',
1307 targetEventType,
1308 );
packages/react-dom/src/client/ReactDOMHostConfig.js
+13 -4
@@ -34,6 +34,7 @@ import {
34 setEnabled as ReactBrowserEventEmitterSetEnabled,
35 } from '../events/ReactBrowserEventEmitter';
36 import {Namespaces, getChildNamespace} from '../shared/DOMNamespaces';
37 +import {addRootEventTypesForComponentInstance} from '../events/DOMEventResponderSystem';
38 import {
39 ELEMENT_NODE,
40 TEXT_NODE,
@@ -906,10 +907,18 @@ export function updateEventComponent(
907 if (enableEventAPI) {
908 const rootContainerInstance = ((eventComponentInstance.rootInstance: any): Container);
909 const rootElement = rootContainerInstance.ownerDocument;
909 - listenToEventResponderEventTypes(
910 - eventComponentInstance.responder.targetEventTypes,
911 - rootElement,
912 - );
910 + const responder = eventComponentInstance.responder;
911 + const {rootEventTypes, targetEventTypes} = responder;
912 + if (targetEventTypes !== undefined) {
913 + listenToEventResponderEventTypes(targetEventTypes, rootElement);
914 + }
915 + if (rootEventTypes !== undefined) {
916 + addRootEventTypesForComponentInstance(
917 + eventComponentInstance,
918 + rootEventTypes,
919 + );
920 + listenToEventResponderEventTypes(rootEventTypes, rootElement);
921 + }
922 }
923 }
924
packages/react-dom/src/events/DOMEventResponderSystem.js
+61 -2
@@ -206,9 +206,20 @@ const eventResponderContext: ReactResponderContext = {
206 rootEventComponentInstances,
207 );
208 }
209 - rootEventComponentInstances.add(
210 - ((currentInstance: any): ReactEventComponentInstance),
209 + const componentInstance = ((currentInstance: any): ReactEventComponentInstance);
210 + let rootEventTypesSet = componentInstance.rootEventTypes;
211 + if (rootEventTypesSet === null) {
212 + rootEventTypesSet = componentInstance.rootEventTypes = new Set();
213 + }
214 + invariant(
215 + !rootEventTypesSet.has(topLevelEventType),
216 + 'addRootEventTypes() found a duplicate root event ' +
217 + 'type of "%s". This might be because the event type exists in the event responder "rootEventTypes" ' +
218 + 'array or because of a previous addRootEventTypes() using this root event type.',
219 + rootEventType,
220 );
221 + rootEventTypesSet.add(topLevelEventType);
222 + rootEventComponentInstances.add(componentInstance);
223 }
224 },
225 removeRootEventTypes(
@@ -222,6 +233,11 @@ const eventResponderContext: ReactResponderContext = {
233 let rootEventComponents = rootEventTypesToEventComponentInstances.get(
234 topLevelEventType,
235 );
236 + let rootEventTypesSet = ((currentInstance: any): ReactEventComponentInstance)
237 + .rootEventTypes;
238 + if (rootEventTypesSet !== null) {
239 + rootEventTypesSet.delete(topLevelEventType);
240 + }
241 if (rootEventComponents !== undefined) {
242 rootEventComponents.delete(
243 ((currentInstance: any): ReactEventComponentInstance),
@@ -636,6 +652,20 @@ export function unmountEventResponder(
652 if (responder.onOwnershipChange !== undefined) {
653 ownershipChangeListeners.delete(eventComponentInstance);
654 }
655 + const rootEventTypesSet = eventComponentInstance.rootEventTypes;
656 + if (rootEventTypesSet !== null) {
657 + const rootEventTypes = Array.from(rootEventTypesSet);
658 +
659 + for (let i = 0; i < rootEventTypes.length; i++) {
660 + const topLevelEventType = rootEventTypes[i];
661 + let rootEventComponentInstances = rootEventTypesToEventComponentInstances.get(
662 + topLevelEventType,
663 + );
664 + if (rootEventComponentInstances !== undefined) {
665 + rootEventComponentInstances.delete(eventComponentInstance);
666 + }
667 + }
668 + }
669 }
670
671 function validateResponderContext(): void {
@@ -671,3 +701,32 @@ export function dispatchEventForResponderEventSystem(
701 }
702 }
703 }
704 +
705 +export function addRootEventTypesForComponentInstance(
706 + eventComponentInstance: ReactEventComponentInstance,
707 + rootEventTypes: Array<ReactEventResponderEventType>,
708 +): void {
709 + for (let i = 0; i < rootEventTypes.length; i++) {
710 + const rootEventType = rootEventTypes[i];
711 + const topLevelEventType =
712 + typeof rootEventType === 'string' ? rootEventType : rootEventType.name;
713 + let rootEventComponentInstances = rootEventTypesToEventComponentInstances.get(
714 + topLevelEventType,
715 + );
716 + if (rootEventComponentInstances === undefined) {
717 + rootEventComponentInstances = new Set();
718 + rootEventTypesToEventComponentInstances.set(
719 + topLevelEventType,
720 + rootEventComponentInstances,
721 + );
722 + }
723 + let rootEventTypesSet = eventComponentInstance.rootEventTypes;
724 + if (rootEventTypesSet === null) {
725 + rootEventTypesSet = eventComponentInstance.rootEventTypes = new Set();
726 + }
727 + rootEventTypesSet.add(topLevelEventType);
728 + rootEventComponentInstances.add(
729 + ((eventComponentInstance: any): ReactEventComponentInstance),
730 + );
731 + }
732 +}
packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js
+62
@@ -16,6 +16,7 @@ let ReactSymbols;
16
17 function createReactEventComponent(
18 targetEventTypes,
19 + rootEventTypes,
20 createInitialState,
21 onEvent,
22 onEventCapture,
@@ -26,6 +27,7 @@ function createReactEventComponent(
27 ) {
28 const testEventResponder = {
29 targetEventTypes,
30 + rootEventTypes,
31 createInitialState,
32 onEvent,
33 onEventCapture,
@@ -90,6 +92,7 @@ describe('DOMEventResponderSystem', () => {
92 const ClickEventComponent = createReactEventComponent(
93 ['click'],
94 undefined,
95 + undefined,
96 (event, context, props) => {
97 eventResponderFiredCount++;
98 eventLog.push({
@@ -163,6 +166,7 @@ describe('DOMEventResponderSystem', () => {
166 const ClickEventComponent = createReactEventComponent(
167 ['click'],
168 undefined,
169 + undefined,
170 (event, context, props) => {
171 eventLog.push({
172 name: event.type,
@@ -217,6 +221,7 @@ describe('DOMEventResponderSystem', () => {
221 const ClickEventComponent = createReactEventComponent(
222 ['click'],
223 undefined,
224 + undefined,
225 (event, context, props) => {
226 eventResponderFiredCount++;
227 eventLog.push({
@@ -288,6 +293,7 @@ describe('DOMEventResponderSystem', () => {
293 const ClickEventComponentA = createReactEventComponent(
294 ['click'],
295 undefined,
296 + undefined,
297 (event, context, props) => {
298 eventLog.push(`A [bubble]`);
299 },
@@ -299,6 +305,7 @@ describe('DOMEventResponderSystem', () => {
305 const ClickEventComponentB = createReactEventComponent(
306 ['click'],
307 undefined,
308 + undefined,
309 (event, context, props) => {
310 eventLog.push(`B [bubble]`);
311 },
@@ -336,6 +343,7 @@ describe('DOMEventResponderSystem', () => {
343 const ClickEventComponent = createReactEventComponent(
344 ['click'],
345 undefined,
346 + undefined,
347 (event, context, props) => {
348 eventLog.push(`${props.name} [bubble]`);
349 },
@@ -377,6 +385,7 @@ describe('DOMEventResponderSystem', () => {
385 const ClickEventComponent = createReactEventComponent(
386 ['click'],
387 undefined,
388 + undefined,
389 (event, context, props) => {
390 eventLog.push(`${props.name} [bubble]`);
391 },
@@ -413,6 +422,7 @@ describe('DOMEventResponderSystem', () => {
422 const ClickEventComponent = createReactEventComponent(
423 ['click'],
424 undefined,
425 + undefined,
426 (event, context, props) => {
427 if (props.onMagicClick) {
428 const syntheticEvent = {
@@ -505,6 +515,7 @@ describe('DOMEventResponderSystem', () => {
515 const LongPressEventComponent = createReactEventComponent(
516 ['click'],
517 undefined,
518 + undefined,
519 (event, context, props) => {
520 handleEvent(event, context, props, 'bubble');
521 },
@@ -551,6 +562,7 @@ describe('DOMEventResponderSystem', () => {
562 undefined,
563 undefined,
564 undefined,
565 + undefined,
566 (event, context, props, state) => {},
567 () => {
568 onUnmountFired++;
@@ -573,6 +585,7 @@ describe('DOMEventResponderSystem', () => {
585
586 const EventComponent = createReactEventComponent(
587 [],
588 + undefined,
589 () => ({
590 incrementAmount: 5,
591 }),
@@ -603,6 +616,7 @@ describe('DOMEventResponderSystem', () => {
616 const EventComponent = createReactEventComponent(
617 ['click'],
618 undefined,
619 + undefined,
620 (event, context, props, state) => {
621 ownershipGained = context.requestOwnership();
622 },
@@ -641,6 +655,7 @@ describe('DOMEventResponderSystem', () => {
655 const EventComponent = createReactEventComponent(
656 ['click'],
657 undefined,
658 + undefined,
659 (event, context, props, state) => {
660 queryResult = Array.from(
661 context.getEventTargetsFromTarget(event.target),
@@ -696,6 +711,7 @@ describe('DOMEventResponderSystem', () => {
711 const EventComponent = createReactEventComponent(
712 ['click'],
713 undefined,
714 + undefined,
715 (event, context, props, state) => {
716 queryResult = context.getEventTargetsFromTarget(
717 event.target,
@@ -743,6 +759,7 @@ describe('DOMEventResponderSystem', () => {
759 const EventComponent = createReactEventComponent(
760 ['click'],
761 undefined,
762 + undefined,
763 (event, context, props, state) => {
764 queryResult = context.getEventTargetsFromTarget(
765 event.target,
@@ -795,6 +812,7 @@ describe('DOMEventResponderSystem', () => {
812 const EventComponent = createReactEventComponent(
813 ['click'],
814 undefined,
815 + undefined,
816 (event, context, props, state) => {
817 queryResult = context.getEventTargetsFromTarget(
818 event.target,
@@ -855,4 +873,48 @@ describe('DOMEventResponderSystem', () => {
873 ]);
874 expect(queryResult3).toEqual([]);
875 });
876 +
877 + it('the event responder root listeners should fire on a root click event', () => {
878 + let eventResponderFiredCount = 0;
879 + let eventLog = [];
880 +
881 + const ClickEventComponent = createReactEventComponent(
882 + undefined,
883 + ['click'],
884 + undefined,
885 + undefined,
886 + undefined,
887 + event => {
888 + eventResponderFiredCount++;
889 + eventLog.push({
890 + name: event.type,
891 + passive: event.passive,
892 + passiveSupported: event.passiveSupported,
893 + phase: 'root',
894 + });
895 + },
896 + );
897 +
898 + const Test = () => (
899 + <ClickEventComponent>
900 + <button>Click me!</button>
901 + </ClickEventComponent>
902 + );
903 +
904 + ReactDOM.render(<Test />, container);
905 + expect(container.innerHTML).toBe('<button>Click me!</button>');
906 +
907 + // Clicking the button should trigger the event responder onEvent() twice
908 + dispatchClickEvent(document.body);
909 + expect(eventResponderFiredCount).toBe(1);
910 + expect(eventLog.length).toBe(1);
911 + expect(eventLog).toEqual([
912 + {
913 + name: 'click',
914 + passive: false,
915 + passiveSupported: false,
916 + phase: 'root',
917 + },
918 + ]);
919 + });
920 });
packages/react-reconciler/src/ReactFiberCompleteWork.js
+1
@@ -816,6 +816,7 @@ function completeWork(
816 context: null,
817 props: newProps,
818 responder,
819 + rootEventTypes: null,
820 rootInstance: rootContainerInstance,
821 state: responderState,
822 };
packages/shared/ReactTypes.js
+3 -1
@@ -86,7 +86,8 @@ export type ReactEventResponderEventType =
86 | {name: string, passive?: boolean, capture?: boolean};
87
88 export type ReactEventResponder = {
89 - targetEventTypes: Array<ReactEventResponderEventType>,
89 + targetEventTypes?: Array<ReactEventResponderEventType>,
90 + rootEventTypes?: Array<ReactEventResponderEventType>,
91 createInitialState?: (props: null | Object) => Object,
92 stopLocalPropagation: boolean,
93 onEvent?: (
@@ -123,6 +124,7 @@ export type ReactEventComponentInstance = {|
124 context: null | Object,
125 props: null | Object,
126 responder: ReactEventResponder,
127 + rootEventTypes: null | Set<string>,
128 rootInstance: mixed,
129 state: null | Object,
130 |};