@samitouri / QOS-React-2 / commits / 1160b37691

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,