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