Skip to content

Commit 017d6f1

Browse files
authored
Experimental Event API: add rootEventTypes support to event responders (#15475)
* Adds rootEventTypes
1 parent 784ebd8 commit 017d6f1

File tree

6 files changed

+141
-8
lines changed

6 files changed

+141
-8
lines changed

packages/react-dom/src/client/ReactDOMComponent.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1302,7 +1302,7 @@ export function listenToEventResponderEventTypes(
13021302
if (__DEV__) {
13031303
warning(
13041304
typeof targetEventType === 'object' && targetEventType !== null,
1305-
'Event Responder: invalid entry in targetEventTypes array. ' +
1305+
'Event Responder: invalid entry in event types array. ' +
13061306
'Entry must be string or an object. Instead, got %s.',
13071307
targetEventType,
13081308
);

packages/react-dom/src/client/ReactDOMHostConfig.js

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
setEnabled as ReactBrowserEventEmitterSetEnabled,
3535
} from '../events/ReactBrowserEventEmitter';
3636
import {Namespaces, getChildNamespace} from '../shared/DOMNamespaces';
37+
import {addRootEventTypesForComponentInstance} from '../events/DOMEventResponderSystem';
3738
import {
3839
ELEMENT_NODE,
3940
TEXT_NODE,
@@ -906,10 +907,18 @@ export function updateEventComponent(
906907
if (enableEventAPI) {
907908
const rootContainerInstance = ((eventComponentInstance.rootInstance: any): Container);
908909
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+
}
913922
}
914923
}
915924

packages/react-dom/src/events/DOMEventResponderSystem.js

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,9 +206,20 @@ const eventResponderContext: ReactResponderContext = {
206206
rootEventComponentInstances,
207207
);
208208
}
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,
211220
);
221+
rootEventTypesSet.add(topLevelEventType);
222+
rootEventComponentInstances.add(componentInstance);
212223
}
213224
},
214225
removeRootEventTypes(
@@ -222,6 +233,11 @@ const eventResponderContext: ReactResponderContext = {
222233
let rootEventComponents = rootEventTypesToEventComponentInstances.get(
223234
topLevelEventType,
224235
);
236+
let rootEventTypesSet = ((currentInstance: any): ReactEventComponentInstance)
237+
.rootEventTypes;
238+
if (rootEventTypesSet !== null) {
239+
rootEventTypesSet.delete(topLevelEventType);
240+
}
225241
if (rootEventComponents !== undefined) {
226242
rootEventComponents.delete(
227243
((currentInstance: any): ReactEventComponentInstance),
@@ -636,6 +652,20 @@ export function unmountEventResponder(
636652
if (responder.onOwnershipChange !== undefined) {
637653
ownershipChangeListeners.delete(eventComponentInstance);
638654
}
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+
}
639669
}
640670

641671
function validateResponderContext(): void {
@@ -671,3 +701,32 @@ export function dispatchEventForResponderEventSystem(
671701
}
672702
}
673703
}
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

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ let ReactSymbols;
1616

1717
function createReactEventComponent(
1818
targetEventTypes,
19+
rootEventTypes,
1920
createInitialState,
2021
onEvent,
2122
onEventCapture,
@@ -26,6 +27,7 @@ function createReactEventComponent(
2627
) {
2728
const testEventResponder = {
2829
targetEventTypes,
30+
rootEventTypes,
2931
createInitialState,
3032
onEvent,
3133
onEventCapture,
@@ -90,6 +92,7 @@ describe('DOMEventResponderSystem', () => {
9092
const ClickEventComponent = createReactEventComponent(
9193
['click'],
9294
undefined,
95+
undefined,
9396
(event, context, props) => {
9497
eventResponderFiredCount++;
9598
eventLog.push({
@@ -163,6 +166,7 @@ describe('DOMEventResponderSystem', () => {
163166
const ClickEventComponent = createReactEventComponent(
164167
['click'],
165168
undefined,
169+
undefined,
166170
(event, context, props) => {
167171
eventLog.push({
168172
name: event.type,
@@ -217,6 +221,7 @@ describe('DOMEventResponderSystem', () => {
217221
const ClickEventComponent = createReactEventComponent(
218222
['click'],
219223
undefined,
224+
undefined,
220225
(event, context, props) => {
221226
eventResponderFiredCount++;
222227
eventLog.push({
@@ -288,6 +293,7 @@ describe('DOMEventResponderSystem', () => {
288293
const ClickEventComponentA = createReactEventComponent(
289294
['click'],
290295
undefined,
296+
undefined,
291297
(event, context, props) => {
292298
eventLog.push(`A [bubble]`);
293299
},
@@ -299,6 +305,7 @@ describe('DOMEventResponderSystem', () => {
299305
const ClickEventComponentB = createReactEventComponent(
300306
['click'],
301307
undefined,
308+
undefined,
302309
(event, context, props) => {
303310
eventLog.push(`B [bubble]`);
304311
},
@@ -336,6 +343,7 @@ describe('DOMEventResponderSystem', () => {
336343
const ClickEventComponent = createReactEventComponent(
337344
['click'],
338345
undefined,
346+
undefined,
339347
(event, context, props) => {
340348
eventLog.push(`${props.name} [bubble]`);
341349
},
@@ -377,6 +385,7 @@ describe('DOMEventResponderSystem', () => {
377385
const ClickEventComponent = createReactEventComponent(
378386
['click'],
379387
undefined,
388+
undefined,
380389
(event, context, props) => {
381390
eventLog.push(`${props.name} [bubble]`);
382391
},
@@ -413,6 +422,7 @@ describe('DOMEventResponderSystem', () => {
413422
const ClickEventComponent = createReactEventComponent(
414423
['click'],
415424
undefined,
425+
undefined,
416426
(event, context, props) => {
417427
if (props.onMagicClick) {
418428
const syntheticEvent = {
@@ -505,6 +515,7 @@ describe('DOMEventResponderSystem', () => {
505515
const LongPressEventComponent = createReactEventComponent(
506516
['click'],
507517
undefined,
518+
undefined,
508519
(event, context, props) => {
509520
handleEvent(event, context, props, 'bubble');
510521
},
@@ -551,6 +562,7 @@ describe('DOMEventResponderSystem', () => {
551562
undefined,
552563
undefined,
553564
undefined,
565+
undefined,
554566
(event, context, props, state) => {},
555567
() => {
556568
onUnmountFired++;
@@ -573,6 +585,7 @@ describe('DOMEventResponderSystem', () => {
573585

574586
const EventComponent = createReactEventComponent(
575587
[],
588+
undefined,
576589
() => ({
577590
incrementAmount: 5,
578591
}),
@@ -603,6 +616,7 @@ describe('DOMEventResponderSystem', () => {
603616
const EventComponent = createReactEventComponent(
604617
['click'],
605618
undefined,
619+
undefined,
606620
(event, context, props, state) => {
607621
ownershipGained = context.requestOwnership();
608622
},
@@ -641,6 +655,7 @@ describe('DOMEventResponderSystem', () => {
641655
const EventComponent = createReactEventComponent(
642656
['click'],
643657
undefined,
658+
undefined,
644659
(event, context, props, state) => {
645660
queryResult = Array.from(
646661
context.getEventTargetsFromTarget(event.target),
@@ -696,6 +711,7 @@ describe('DOMEventResponderSystem', () => {
696711
const EventComponent = createReactEventComponent(
697712
['click'],
698713
undefined,
714+
undefined,
699715
(event, context, props, state) => {
700716
queryResult = context.getEventTargetsFromTarget(
701717
event.target,
@@ -743,6 +759,7 @@ describe('DOMEventResponderSystem', () => {
743759
const EventComponent = createReactEventComponent(
744760
['click'],
745761
undefined,
762+
undefined,
746763
(event, context, props, state) => {
747764
queryResult = context.getEventTargetsFromTarget(
748765
event.target,
@@ -795,6 +812,7 @@ describe('DOMEventResponderSystem', () => {
795812
const EventComponent = createReactEventComponent(
796813
['click'],
797814
undefined,
815+
undefined,
798816
(event, context, props, state) => {
799817
queryResult = context.getEventTargetsFromTarget(
800818
event.target,
@@ -855,4 +873,48 @@ describe('DOMEventResponderSystem', () => {
855873
]);
856874
expect(queryResult3).toEqual([]);
857875
});
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+
});
858920
});

packages/react-reconciler/src/ReactFiberCompleteWork.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,7 @@ function completeWork(
816816
context: null,
817817
props: newProps,
818818
responder,
819+
rootEventTypes: null,
819820
rootInstance: rootContainerInstance,
820821
state: responderState,
821822
};

packages/shared/ReactTypes.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ export type ReactEventResponderEventType =
8686
| {name: string, passive?: boolean, capture?: boolean};
8787

8888
export type ReactEventResponder = {
89-
targetEventTypes: Array<ReactEventResponderEventType>,
89+
targetEventTypes?: Array<ReactEventResponderEventType>,
90+
rootEventTypes?: Array<ReactEventResponderEventType>,
9091
createInitialState?: (props: null | Object) => Object,
9192
stopLocalPropagation: boolean,
9293
onEvent?: (
@@ -123,6 +124,7 @@ export type ReactEventComponentInstance = {|
123124
context: null | Object,
124125
props: null | Object,
125126
responder: ReactEventResponder,
127+
rootEventTypes: null | Set<string>,
126128
rootInstance: mixed,
127129
state: null | Object,
128130
|};

0 commit comments

Comments
 (0)