Event API: Add responder allowMultipleHostChildren flag (#15646)
Dominic Gannaway committed
May 16, 2019 at 19:03 UTC
1160b37691d32c9bf385b7e7d7a106adce05c7df
11 files changed
+366
-243
packages/react-dom/src/events/DOMEventResponderSystem.js
+34
-79
@@ -16,8 +16,6 @@ import {
16
EventComponent,
17
EventTarget as EventTargetWorkTag,
18
HostComponent,
19
- SuspenseComponent,
20
- Fragment,
19
} from 'shared/ReactWorkTags';
20
import type {
21
ReactEventResponder,
@@ -34,6 +32,10 @@ import warning from 'shared/warning';
32
import {enableEventAPI} from 'shared/ReactFeatureFlags';
33
import {invokeGuardedCallbackAndCatchFirstError} from 'shared/ReactErrorUtils';
34
import invariant from 'shared/invariant';
35
+import {
36
+ isFiberSuspenseAndTimedOut,
37
+ getSuspenseFallbackChild,
38
+} from 'react-reconciler/src/ReactFiberEvents';
39
40
import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree';
41
@@ -351,50 +353,11 @@ const eventResponderContext: ReactResponderContext = {
353
validateResponderContext();
354
const focusableElements = [];
355
const eventComponentInstance = ((currentInstance: any): ReactEventComponentInstance);
354
- let node = ((eventComponentInstance.currentFiber: any): Fiber).child;
355
-
356
- while (node !== null) {
357
- if (node.tag === SuspenseComponent) {
358
- const suspendedChild = isFiberSuspenseAndTimedOut(node)
359
- ? getSuspenseFallbackChild(node)
360
- : getSuspenseChild(node);
361
- if (suspendedChild !== null) {
362
- node = suspendedChild;
363
- continue;
364
- }
365
- } else {
366
- if (isFiberHostComponentFocusable(node)) {
367
- focusableElements.push(node.stateNode);
368
- } else {
369
- const child = node.child;
356
+ const child = ((eventComponentInstance.currentFiber: any): Fiber).child;
357
371
- if (child !== null) {
372
- node = child;
373
- continue;
374
- }
375
- }
376
- }
377
- const sibling = node.sibling;
378
-
379
- if (sibling !== null) {
380
- node = sibling;
381
- continue;
382
- }
383
- let parent;
384
- if (isFiberSuspenseChild(node)) {
385
- parent = getSuspenseFiberFromChild(node);
386
- } else {
387
- parent = node.return;
388
- if (parent === null) {
389
- break;
390
- }
391
- }
392
- if (parent.stateNode === currentInstance) {
393
- break;
394
- }
395
- node = parent.sibling;
358
+ if (child !== null) {
359
+ collectFocusableElements(child, focusableElements);
360
}
397
-
361
return focusableElements;
362
},
363
getActiveDocument,
@@ -455,6 +418,33 @@ const eventResponderContext: ReactResponderContext = {
418
},
419
};
420
421
+function collectFocusableElements(
422
+ node: Fiber,
423
+ focusableElements: Array<HTMLElement>,
424
+): void {
425
+ if (isFiberSuspenseAndTimedOut(node)) {
426
+ const fallbackChild = getSuspenseFallbackChild(node);
427
+ if (fallbackChild !== null) {
428
+ collectFocusableElements(fallbackChild, focusableElements);
429
+ }
430
+ } else {
431
+ if (isFiberHostComponentFocusable(node)) {
432
+ focusableElements.push(node.stateNode);
433
+ } else {
434
+ const child = node.child;
435
+
436
+ if (child !== null) {
437
+ collectFocusableElements(child, focusableElements);
438
+ }
439
+ }
440
+ }
441
+ const sibling = node.sibling;
442
+
443
+ if (sibling !== null) {
444
+ collectFocusableElements(sibling, focusableElements);
445
+ }
446
+}
447
+
448
function isTargetWithinEventComponent(target: Element | Document): boolean {
449
validateResponderContext();
450
if (target != null) {
@@ -605,41 +595,6 @@ export function processEventQueue(): void {
595
}
596
}
597
608
-function isFiberSuspenseAndTimedOut(fiber: Fiber): boolean {
609
- return fiber.tag === SuspenseComponent && fiber.memoizedState !== null;
610
-}
611
-
612
-function isFiberSuspenseChild(fiber: Fiber | null): boolean {
613
- if (fiber === null) {
614
- return false;
615
- }
616
- const parent = fiber.return;
617
- if (parent !== null && parent.tag === Fragment) {
618
- const grandParent = parent.return;
619
-
620
- if (
621
- grandParent !== null &&
622
- grandParent.tag === SuspenseComponent &&
623
- grandParent.stateNode !== null
624
- ) {
625
- return true;
626
- }
627
- }
628
- return false;
629
-}
630
-
631
-function getSuspenseFiberFromChild(fiber: Fiber): Fiber {
632
- return ((((fiber.return: any): Fiber).return: any): Fiber);
633
-}
634
-
635
-function getSuspenseFallbackChild(fiber: Fiber): Fiber | null {
636
- return ((((fiber.child: any): Fiber).sibling: any): Fiber).child;
637
-}
638
-
639
-function getSuspenseChild(fiber: Fiber): Fiber | null {
640
- return (((fiber.child: any): Fiber): Fiber).child;
641
-}
642
-
598
function getTargetEventTypesSet(
599
eventTypes: Array<ReactEventResponderEventType>,
600
): Set<string> {
packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js
+225
-164
@@ -13,7 +13,7 @@ let React;
13
let ReactFeatureFlags;
14
let ReactDOM;
15
16
-function createReactEventComponent(
16
+function createReactEventComponent({
17
targetEventTypes,
18
rootEventTypes,
19
createInitialState,
@@ -24,7 +24,8 @@ function createReactEventComponent(
24
onUnmount,
25
onOwnershipChange,
26
stopLocalPropagation,
27
-) {
27
+ allowMultipleHostChildren,
28
+}) {
29
const testEventResponder = {
30
targetEventTypes,
31
rootEventTypes,
@@ -36,6 +37,7 @@ function createReactEventComponent(
37
onUnmount,
38
onOwnershipChange,
39
stopLocalPropagation: stopLocalPropagation || false,
40
+ allowMultipleHostChildren: allowMultipleHostChildren || false,
41
};
42
43
return {
@@ -81,11 +83,9 @@ describe('DOMEventResponderSystem', () => {
83
let eventLog = [];
84
const buttonRef = React.createRef();
85
84
- const ClickEventComponent = createReactEventComponent(
85
- ['click'],
86
- undefined,
87
- undefined,
88
- (event, context, props) => {
86
+ const ClickEventComponent = createReactEventComponent({
87
+ targetEventTypes: ['click'],
88
+ onEvent: (event, context, props) => {
89
eventResponderFiredCount++;
90
eventLog.push({
91
name: event.type,
@@ -94,7 +94,7 @@ describe('DOMEventResponderSystem', () => {
94
phase: 'bubble',
95
});
96
},
97
- (event, context, props) => {
97
+ onEventCapture: (event, context, props) => {
98
eventResponderFiredCount++;
99
eventLog.push({
100
name: event.type,
@@ -103,7 +103,7 @@ describe('DOMEventResponderSystem', () => {
103
phase: 'capture',
104
});
105
},
106
- );
106
+ });
107
108
const Test = () => (
109
<ClickEventComponent>
@@ -155,11 +155,9 @@ describe('DOMEventResponderSystem', () => {
155
let eventLog = [];
156
const buttonRef = React.createRef();
157
158
- const ClickEventComponent = createReactEventComponent(
159
- ['click'],
160
- undefined,
161
- undefined,
162
- (event, context, props) => {
158
+ const ClickEventComponent = createReactEventComponent({
159
+ targetEventTypes: ['click'],
160
+ onEvent: (event, context, props) => {
161
eventLog.push({
162
name: event.type,
163
passive: event.passive,
@@ -167,7 +165,7 @@ describe('DOMEventResponderSystem', () => {
165
phase: 'bubble',
166
});
167
},
170
- (event, context, props) => {
168
+ onEventCapture: (event, context, props) => {
169
eventLog.push({
170
name: event.type,
171
passive: event.passive,
@@ -175,7 +173,7 @@ describe('DOMEventResponderSystem', () => {
173
phase: 'capture',
174
});
175
},
178
- );
176
+ });
177
178
const Test = () => (
179
<ClickEventComponent>
@@ -210,11 +208,9 @@ describe('DOMEventResponderSystem', () => {
208
let eventLog = [];
209
const buttonRef = React.createRef();
210
213
- const ClickEventComponent = createReactEventComponent(
214
- ['click'],
215
- undefined,
216
- undefined,
217
- (event, context, props) => {
211
+ const ClickEventComponent = createReactEventComponent({
212
+ targetEventTypes: ['click'],
213
+ onEvent: (event, context, props) => {
214
eventResponderFiredCount++;
215
eventLog.push({
216
name: event.type,
@@ -223,7 +219,7 @@ describe('DOMEventResponderSystem', () => {
219
phase: 'bubble',
220
});
221
},
226
- (event, context, props) => {
222
+ onEventCapture: (event, context, props) => {
223
eventResponderFiredCount++;
224
eventLog.push({
225
name: event.type,
@@ -232,7 +228,7 @@ describe('DOMEventResponderSystem', () => {
228
phase: 'capture',
229
});
230
},
235
- );
231
+ });
232
233
const Test = () => (
234
<ClickEventComponent>
@@ -282,29 +278,25 @@ describe('DOMEventResponderSystem', () => {
278
let eventLog = [];
279
const buttonRef = React.createRef();
280
285
- const ClickEventComponentA = createReactEventComponent(
286
- ['click'],
287
- undefined,
288
- undefined,
289
- (event, context, props) => {
281
+ const ClickEventComponentA = createReactEventComponent({
282
+ targetEventTypes: ['click'],
283
+ onEvent: (event, context, props) => {
284
eventLog.push(`A [bubble]`);
285
},
292
- (event, context, props) => {
286
+ onEventCapture: (event, context, props) => {
287
eventLog.push(`A [capture]`);
288
},
295
- );
289
+ });
290
297
- const ClickEventComponentB = createReactEventComponent(
298
- ['click'],
299
- undefined,
300
- undefined,
301
- (event, context, props) => {
291
+ const ClickEventComponentB = createReactEventComponent({
292
+ targetEventTypes: ['click'],
293
+ onEvent: (event, context, props) => {
294
eventLog.push(`B [bubble]`);
295
},
304
- (event, context, props) => {
296
+ onEventCapture: (event, context, props) => {
297
eventLog.push(`B [capture]`);
298
},
307
- );
299
+ });
300
301
const Test = () => (
302
<ClickEventComponentA>
@@ -332,21 +324,16 @@ describe('DOMEventResponderSystem', () => {
324
let eventLog = [];
325
const buttonRef = React.createRef();
326
335
- const ClickEventComponent = createReactEventComponent(
336
- ['click'],
337
- undefined,
338
- undefined,
339
- (event, context, props) => {
327
+ const ClickEventComponent = createReactEventComponent({
328
+ targetEventTypes: ['click'],
329
+ onEvent: (event, context, props) => {
330
eventLog.push(`${props.name} [bubble]`);
331
},
342
- (event, context, props) => {
332
+ onEventCapture: (event, context, props) => {
333
eventLog.push(`${props.name} [capture]`);
334
},
345
- undefined,
346
- undefined,
347
- undefined,
348
- false,
349
- );
335
+ stopLocalPropagation: false,
336
+ });
337
338
const Test = () => (
339
<ClickEventComponent name="A">
@@ -374,22 +361,16 @@ describe('DOMEventResponderSystem', () => {
361
let eventLog = [];
362
const buttonRef = React.createRef();
363
377
- const ClickEventComponent = createReactEventComponent(
378
- ['click'],
379
- undefined,
380
- undefined,
381
- (event, context, props) => {
364
+ const ClickEventComponent = createReactEventComponent({
365
+ targetEventTypes: ['click'],
366
+ onEvent: (event, context, props) => {
367
eventLog.push(`${props.name} [bubble]`);
368
},
384
- (event, context, props) => {
369
+ onEventCapture: (event, context, props) => {
370
eventLog.push(`${props.name} [capture]`);
371
},
387
- undefined,
388
- undefined,
389
- undefined,
390
- undefined,
391
- true,
392
- );
372
+ stopLocalPropagation: true,
373
+ });
374
375
const Test = () => (
376
<ClickEventComponent name="A">
@@ -412,11 +393,9 @@ describe('DOMEventResponderSystem', () => {
393
let eventLog = [];
394
const buttonRef = React.createRef();
395
415
- const ClickEventComponent = createReactEventComponent(
416
- ['click'],
417
- undefined,
418
- undefined,
419
- (event, context, props) => {
396
+ const ClickEventComponent = createReactEventComponent({
397
+ targetEventTypes: ['click'],
398
+ onEvent: (event, context, props) => {
399
if (props.onMagicClick) {
400
const syntheticEvent = {
401
target: event.target,
@@ -429,7 +408,7 @@ describe('DOMEventResponderSystem', () => {
408
});
409
}
410
},
432
- (event, context, props) => {
411
+ onEventCapture: (event, context, props) => {
412
if (props.onMagicClick) {
413
const syntheticEvent = {
414
target: event.target,
@@ -442,7 +421,7 @@ describe('DOMEventResponderSystem', () => {
421
});
422
}
423
},
445
- );
424
+ });
425
426
function handleMagicEvent(e) {
427
eventLog.push('magic event fired', e.type, e.phase);
@@ -510,17 +489,15 @@ describe('DOMEventResponderSystem', () => {
489
}, 500);
490
}
491
513
- const LongPressEventComponent = createReactEventComponent(
514
- ['click'],
515
- undefined,
516
- undefined,
517
- (event, context, props) => {
492
+ const LongPressEventComponent = createReactEventComponent({
493
+ targetEventTypes: ['click'],
494
+ onEvent: (event, context, props) => {
495
handleEvent(event, context, props, 'bubble');
496
},
520
- (event, context, props) => {
497
+ onEventCapture: (event, context, props) => {
498
handleEvent(event, context, props, 'capture');
499
},
523
- );
500
+ });
501
502
function log(msg) {
503
eventLog.push(msg);
@@ -555,17 +532,12 @@ describe('DOMEventResponderSystem', () => {
532
it('the event responder onMount() function should fire', () => {
533
let onMountFired = 0;
534
558
- const EventComponent = createReactEventComponent(
559
- [],
560
- undefined,
561
- undefined,
562
- undefined,
563
- undefined,
564
- undefined,
565
- () => {
535
+ const EventComponent = createReactEventComponent({
536
+ targetEventTypes: [],
537
+ onMount: () => {
538
onMountFired++;
539
},
568
- );
540
+ });
541
542
const Test = () => (
543
<EventComponent>
@@ -580,18 +552,12 @@ describe('DOMEventResponderSystem', () => {
552
it('the event responder onUnmount() function should fire', () => {
553
let onUnmountFired = 0;
554
583
- const EventComponent = createReactEventComponent(
584
- [],
585
- undefined,
586
- undefined,
587
- undefined,
588
- undefined,
589
- undefined,
590
- undefined,
591
- () => {
555
+ const EventComponent = createReactEventComponent({
556
+ targetEventTypes: [],
557
+ onUnmount: () => {
558
onUnmountFired++;
559
},
594
- );
560
+ });
561
562
const Test = () => (
563
<EventComponent>
@@ -607,20 +573,15 @@ describe('DOMEventResponderSystem', () => {
573
it('the event responder onUnmount() function should fire with state', () => {
574
let counter = 0;
575
610
- const EventComponent = createReactEventComponent(
611
- [],
612
- undefined,
613
- () => ({
576
+ const EventComponent = createReactEventComponent({
577
+ targetEventTypes: [],
578
+ createInitialState: () => ({
579
incrementAmount: 5,
580
}),
616
- undefined,
617
- undefined,
618
- undefined,
619
- undefined,
620
- (context, props, state) => {
581
+ onUnmount: (context, props, state) => {
582
counter += state.incrementAmount;
583
},
623
- );
584
+ });
585
586
const Test = () => (
587
<EventComponent>
@@ -638,21 +599,15 @@ describe('DOMEventResponderSystem', () => {
599
let ownershipGained = false;
600
const buttonRef = React.createRef();
601
641
- const EventComponent = createReactEventComponent(
642
- ['click'],
643
- undefined,
644
- undefined,
645
- (event, context, props, state) => {
602
+ const EventComponent = createReactEventComponent({
603
+ targetEventTypes: ['click'],
604
+ onEvent: (event, context, props, state) => {
605
ownershipGained = context.requestGlobalOwnership();
606
},
648
- undefined,
649
- undefined,
650
- undefined,
651
- undefined,
652
- () => {
607
+ onOwnershipChange: () => {
608
onOwnershipChangeFired++;
609
},
655
- );
610
+ });
611
612
const Test = () => (
613
<EventComponent>
@@ -675,13 +630,9 @@ describe('DOMEventResponderSystem', () => {
630
let eventResponderFiredCount = 0;
631
let eventLog = [];
632
678
- const ClickEventComponent = createReactEventComponent(
679
- undefined,
680
- ['click'],
681
- undefined,
682
- undefined,
683
- undefined,
684
- event => {
633
+ const ClickEventComponent = createReactEventComponent({
634
+ rootEventTypes: ['click'],
635
+ onRootEvent: event => {
636
eventResponderFiredCount++;
637
eventLog.push({
638
name: event.type,
@@ -690,7 +641,7 @@ describe('DOMEventResponderSystem', () => {
641
phase: 'root',
642
});
643
},
693
- );
644
+ });
645
646
const Test = () => (
647
<ClickEventComponent>
@@ -720,17 +671,16 @@ describe('DOMEventResponderSystem', () => {
671
const divRef = React.createRef();
672
const log = [];
673
723
- const EventComponent = createReactEventComponent(
724
- ['pointerout'],
725
- undefined,
726
- undefined,
727
- (event, context) => {
674
+ const EventComponent = createReactEventComponent({
675
+ targetEventTypes: ['pointerout'],
676
+ onEvent: (event, context) => {
677
const isWithin = context.isTargetWithinEventResponderScope(
678
event.nativeEvent.relatedTarget,
679
);
680
log.push(isWithin);
681
},
733
- );
682
+ allowMultipleHostChildren: true,
683
+ });
684
685
const Test = () => (
686
<EventComponent>
@@ -769,11 +719,9 @@ describe('DOMEventResponderSystem', () => {
719
let eventLog = [];
720
const buttonRef = React.createRef();
721
772
- const ClickEventComponent1 = createReactEventComponent(
773
- [{name: 'click', passive: false, capture: false}],
774
- undefined,
775
- undefined,
776
- event => {
722
+ const ClickEventComponent1 = createReactEventComponent({
723
+ targetEventTypes: [{name: 'click', passive: false, capture: false}],
724
+ onEvent: event => {
725
clickEventComponent1Fired++;
726
eventLog.push({
727
name: event.type,
@@ -781,13 +729,11 @@ describe('DOMEventResponderSystem', () => {
729
passiveSupported: event.passiveSupported,
730
});
731
},
784
- );
732
+ });
733
786
- const ClickEventComponent2 = createReactEventComponent(
787
- [{name: 'click', passive: true, capture: false}],
788
- undefined,
789
- undefined,
790
- event => {
734
+ const ClickEventComponent2 = createReactEventComponent({
735
+ targetEventTypes: [{name: 'click', passive: true, capture: false}],
736
+ onEvent: event => {
737
clickEventComponent2Fired++;
738
eventLog.push({
739
name: event.type,
@@ -795,7 +741,7 @@ describe('DOMEventResponderSystem', () => {
741
passiveSupported: event.passiveSupported,
742
});
743
},
798
- );
744
+ });
745
746
const Test = () => (
747
<ClickEventComponent1>
@@ -832,13 +778,9 @@ describe('DOMEventResponderSystem', () => {
778
let clickEventComponent2Fired = 0;
779
let eventLog = [];
780
835
- const ClickEventComponent1 = createReactEventComponent(
836
- undefined,
837
- [{name: 'click', passive: false, capture: false}],
838
- undefined,
839
- undefined,
840
- undefined,
841
- event => {
781
+ const ClickEventComponent1 = createReactEventComponent({
782
+ rootEventTypes: [{name: 'click', passive: false, capture: false}],
783
+ onRootEvent: event => {
784
clickEventComponent1Fired++;
785
eventLog.push({
786
name: event.type,
@@ -846,15 +788,11 @@ describe('DOMEventResponderSystem', () => {
788
passiveSupported: event.passiveSupported,
789
});
790
},
849
- );
791
+ });
792
851
- const ClickEventComponent2 = createReactEventComponent(
852
- undefined,
853
- [{name: 'click', passive: true, capture: false}],
854
- undefined,
855
- undefined,
856
- undefined,
857
- event => {
793
+ const ClickEventComponent2 = createReactEventComponent({
794
+ rootEventTypes: [{name: 'click', passive: true, capture: false}],
795
+ onRootEvent: event => {
796
clickEventComponent2Fired++;
797
eventLog.push({
798
name: event.type,
@@ -862,7 +800,7 @@ describe('DOMEventResponderSystem', () => {
800
passiveSupported: event.passiveSupported,
801
});
802
},
865
- );
803
+ });
804
805
const Test = () => (
806
<ClickEventComponent1>
@@ -896,13 +834,9 @@ describe('DOMEventResponderSystem', () => {
834
});
835
836
it('the event responder system should warn on accessing invalid properties', () => {
899
- const ClickEventComponent = createReactEventComponent(
900
- undefined,
901
- ['click'],
902
- undefined,
903
- undefined,
904
- undefined,
905
- (event, context, props) => {
837
+ const ClickEventComponent = createReactEventComponent({
838
+ rootEventTypes: ['click'],
839
+ onRootEvent: (event, context, props) => {
840
const syntheticEvent = {
841
target: event.target,
842
type: 'click',
@@ -912,7 +846,7 @@ describe('DOMEventResponderSystem', () => {
846
discrete: true,
847
});
848
},
915
- );
849
+ });
850
851
let handler;
852
const Test = () => (
@@ -989,4 +923,131 @@ describe('DOMEventResponderSystem', () => {
923
924
expect(container.innerHTML).toBe('<button>Click me!</button>');
925
});
926
+
927
+ it('should warn if multiple host components are detected without allowMultipleHostChildren', () => {
928
+ const EventComponent = createReactEventComponent({
929
+ targetEventTypes: [],
930
+ onEvent: () => {},
931
+ allowMultipleHostChildren: false,
932
+ });
933
+
934
+ const Test = () => (
935
+ <EventComponent>
936
+ <div />
937
+ <div />
938
+ </EventComponent>
939
+ );
940
+
941
+ expect(() => {
942
+ ReactDOM.render(<Test />, container);
943
+ }).toWarnDev(
944
+ 'Warning: A "<TestEventComponent>" event component cannot contain multiple host children.',
945
+ );
946
+
947
+ function Component() {
948
+ return <div />;
949
+ }
950
+
951
+ const Test2 = () => (
952
+ <EventComponent>
953
+ <div />
954
+ <Component />
955
+ </EventComponent>
956
+ );
957
+
958
+ expect(() => {
959
+ ReactDOM.render(<Test2 />, container);
960
+ }).toWarnDev(
961
+ 'Warning: A "<TestEventComponent>" event component cannot contain multiple host children.',
962
+ );
963
+ });
964
+
965
+ it('should handle suspended nodes correctly when detecting host components without allowMultipleHostChildren', () => {
966
+ const EventComponent = createReactEventComponent({
967
+ targetEventTypes: [],
968
+ onEvent: () => {},
969
+ allowMultipleHostChildren: false,
970
+ });
971
+
972
+ function SuspendedComponent() {
973
+ throw Promise.resolve();
974
+ }
975
+
976
+ function Component() {
977
+ return (
978
+ <React.Fragment>
979
+ <div />
980
+ <SuspendedComponent />
981
+ </React.Fragment>
982
+ );
983
+ }
984
+
985
+ const Test = () => (
986
+ <EventComponent>
987
+ <React.Suspense fallback={<div>Loading...</div>}>
988
+ <Component />
989
+ </React.Suspense>
990
+ </EventComponent>
991
+ );
992
+
993
+ ReactDOM.render(<Test />, container);
994
+
995
+ function Component2() {
996
+ return (
997
+ <React.Fragment>
998
+ <SuspendedComponent />
999
+ </React.Fragment>
1000
+ );
1001
+ }
1002
+
1003
+ const Test2 = () => (
1004
+ <EventComponent>
1005
+ <React.Suspense
1006
+ fallback={
1007
+ <React.Fragment>
1008
+ <div />
1009
+ <div />
1010
+ </React.Fragment>
1011
+ }>
1012
+ <Component2 />
1013
+ </React.Suspense>
1014
+ </EventComponent>
1015
+ );
1016
+
1017
+ expect(() => {
1018
+ ReactDOM.render(<Test2 />, container);
1019
+ }).toWarnDev(
1020
+ 'Warning: A "<TestEventComponent>" event component cannot contain multiple host children.',
1021
+ );
1022
+ });
1023
+
1024
+ it('should not warn if multiple host components are detected with allowMultipleHostChildren', () => {
1025
+ const EventComponent = createReactEventComponent({
1026
+ targetEventTypes: [],
1027
+ onEvent: () => {},
1028
+ allowMultipleHostChildren: true,
1029
+ });
1030
+
1031
+ const Test = () => (
1032
+ <EventComponent>
1033
+ <div />
1034
+ <div />
1035
+ </EventComponent>
1036
+ );
1037
+
1038
+ ReactDOM.render(<Test />, container);
1039
+
1040
+ function Component() {
1041
+ return <div />;
1042
+ }
1043
+
1044
+ const Test2 = () => (
1045
+ <EventComponent>
1046
+ <div />
1047
+ <Component />
1048
+ </EventComponent>
1049
+ );
1050
+
1051
+ ReactDOM.render(<Test2 />, container);
1052
+ });
1053
});
packages/react-events/src/Drag.js
+1
@@ -95,6 +95,7 @@ const DragResponder = {
95
y: 0,
96
};
97
},
98
+ allowMultipleHostChildren: false,
99
stopLocalPropagation: true,
100
onEvent(
101
event: ReactResponderEvent,
packages/react-events/src/Focus.js
+1
@@ -222,6 +222,7 @@ const FocusResponder = {
222
pointerType: '',
223
};
224
},
225
+ allowMultipleHostChildren: false,
226
stopLocalPropagation: true,
227
onEvent(
228
event: ReactResponderEvent,
packages/react-events/src/FocusScope.js
+2
@@ -55,6 +55,8 @@ const FocusScopeResponder = {
55
currentFocusedNode: null,
56
};
57
},
58
+ allowMultipleHostChildren: true,
59
+ stopLocalPropagation: false,
60
onEvent(
61
event: ReactResponderEvent,
62
context: ReactResponderContext,
packages/react-events/src/Hover.js
+1
@@ -247,6 +247,7 @@ const HoverResponder = {
247
ignoreEmulatedMouseEvents: false,
248
};
249
},
250
+ allowMultipleHostChildren: false,
251
stopLocalPropagation: true,
252
onEvent(
253
event: ReactResponderEvent,
packages/react-events/src/Press.js
+1
@@ -605,6 +605,7 @@ const PressResponder = {
605
allowPressReentry: false,
606
};
607
},
608
+ allowMultipleHostChildren: false,
609
stopLocalPropagation: true,
610
onEvent(
611
event: ReactResponderEvent,
packages/react-events/src/Swipe.js
+1
@@ -99,6 +99,7 @@ const SwipeResponder = {
99
y: 0,
100
};
101
},
102
+ allowMultipleHostChildren: false,
103
stopLocalPropagation: true,
104
onEvent(
105
event: ReactResponderEvent,
packages/react-reconciler/src/ReactFiberCompleteWork.js
+13
@@ -95,6 +95,9 @@ import {
95
enableEventAPI,
96
} from 'shared/ReactFeatureFlags';
97
import {markRenderEventTime, renderDidSuspend} from './ReactFiberScheduler';
98
+import {getEventComponentHostChildrenCount} from './ReactFiberEvents';
99
+import getComponentName from 'shared/getComponentName';
100
+import warning from 'shared/warning';
101
102
function markUpdate(workInProgress: Fiber) {
103
// Tag the fiber with an update effect. This turns a Placement into
@@ -811,6 +814,16 @@ function completeWork(
814
815
if (eventComponentInstance === null) {
816
let responderState = null;
817
+ if (__DEV__ && !responder.allowMultipleHostChildren) {
818
+ const hostChildrenCount = getEventComponentHostChildrenCount(
819
+ workInProgress,
820
+ );
821
+ warning(
822
+ (hostChildrenCount || 0) < 2,
823
+ 'A "<%s>" event component cannot contain multiple host children.',
824
+ getComponentName(workInProgress.type),
825
+ );
826
+ }
827
if (responder.createInitialState !== undefined) {
828
responderState = responder.createInitialState(newProps);
829
}
packages/react-reconciler/src/ReactFiberEvents.js
new
+86
@@ -0,0 +1,86 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import type {Fiber} from './ReactFiber';
11
+
12
+import {
13
+ HostComponent,
14
+ HostText,
15
+ HostPortal,
16
+ SuspenseComponent,
17
+ Fragment,
18
+} from 'shared/ReactWorkTags';
19
+
20
+export function isFiberSuspenseAndTimedOut(fiber: Fiber): boolean {
21
+ return fiber.tag === SuspenseComponent && fiber.memoizedState !== null;
22
+}
23
+
24
+export function getSuspenseFallbackChild(fiber: Fiber): Fiber | null {
25
+ return ((((fiber.child: any): Fiber).sibling: any): Fiber).child;
26
+}
27
+
28
+export function isFiberSuspenseTimedOutChild(fiber: Fiber | null): boolean {
29
+ if (fiber === null) {
30
+ return false;
31
+ }
32
+ const parent = fiber.return;
33
+ if (parent !== null && parent.tag === Fragment) {
34
+ const grandParent = parent.return;
35
+
36
+ if (
37
+ grandParent !== null &&
38
+ grandParent.tag === SuspenseComponent &&
39
+ grandParent.stateNode !== null
40
+ ) {
41
+ return true;
42
+ }
43
+ }
44
+ return false;
45
+}
46
+
47
+export function getSuspenseFiberFromTimedOutChild(fiber: Fiber): Fiber {
48
+ return ((((fiber.return: any): Fiber).return: any): Fiber);
49
+}
50
+
51
+export function getEventComponentHostChildrenCount(
52
+ eventComponentFiber: Fiber,
53
+): ?number {
54
+ if (__DEV__) {
55
+ let hostChildrenCount = 0;
56
+ const getHostChildrenCount = node => {
57
+ if (isFiberSuspenseAndTimedOut(node)) {
58
+ const fallbackChild = getSuspenseFallbackChild(node);
59
+ if (fallbackChild !== null) {
60
+ getHostChildrenCount(fallbackChild);
61
+ }
62
+ } else if (
63
+ node.tag === HostComponent ||
64
+ node.tag === HostText ||
65
+ node.tag === HostPortal
66
+ ) {
67
+ hostChildrenCount++;
68
+ } else {
69
+ const child = node.child;
70
+ if (child !== null) {
71
+ getHostChildrenCount(child);
72
+ }
73
+ }
74
+ const sibling = node.sibling;
75
+ if (sibling !== null) {
76
+ getHostChildrenCount(sibling);
77
+ }
78
+ };
79
+
80
+ if (eventComponentFiber.child !== null) {
81
+ getHostChildrenCount(eventComponentFiber.child);
82
+ }
83
+
84
+ return hostChildrenCount;
85
+ }
86
+}
packages/shared/ReactTypes.js
+1
@@ -89,6 +89,7 @@ export type ReactEventResponder = {
89
targetEventTypes?: Array<ReactEventResponderEventType>,
90
rootEventTypes?: Array<ReactEventResponderEventType>,
91
createInitialState?: (props: null | Object) => Object,
92
+ allowMultipleHostChildren: boolean,
93
stopLocalPropagation: boolean,
94
onEvent?: (
95
event: ReactResponderEvent,