@samitouri / QOS-React / commits / 4fbbae8afa

Add full TouchHitTarget hit slop (experimental event API) to ReactDOM (#15308)

Dominic Gannaway committed Apr 6, 2019 at 07:51 UTC 4fbbae8afa62b61c84cf1ca0cd7a1f5a413f59df
19 files changed +848 -295
packages/events/EventTypes.js
+5 -1
@@ -34,7 +34,11 @@ export type ResponderContext = {
34 parentTarget: Element | Document,
35 ) => boolean,
36 isTargetWithinEventComponent: (Element | Document) => boolean,
37 - isPositionWithinTouchHitTarget: (x: number, y: number) => boolean,
37 + isPositionWithinTouchHitTarget: (
38 + doc: Document,
39 + x: number,
40 + y: number,
41 + ) => boolean,
42 addRootEventTypes: (
43 document: Document,
44 rootEventTypes: Array<ReactEventResponderEventType>,
packages/react-art/src/ReactARTHostConfig.js
+21 -5
@@ -443,15 +443,31 @@ export function handleEventComponent(
443 eventResponder: ReactEventResponder,
444 rootContainerInstance: Container,
445 internalInstanceHandle: Object,
446 -) {
447 - // TODO: add handleEventComponent implementation
446 +): void {
447 + throw new Error('Not yet implemented.');
448 +}
449 +
450 +export function getEventTargetChildElement(
451 + type: Symbol | number,
452 + props: Props,
453 +): null {
454 + throw new Error('Not yet implemented.');
455 }
456
457 export function handleEventTarget(
458 type: Symbol | number,
459 props: Props,
453 - parentInstance: Container,
460 + rootContainerInstance: Container,
461 internalInstanceHandle: Object,
455 -) {
456 - // TODO: add handleEventTarget implementation
462 +): boolean {
463 + throw new Error('Not yet implemented.');
464 +}
465 +
466 +export function commitEventTarget(
467 + type: Symbol | number,
468 + props: Props,
469 + instance: Instance,
470 + parentInstance: Instance,
471 +): void {
472 + throw new Error('Not yet implemented.');
473 }
packages/react-dom/src/client/ReactDOMHostConfig.js
+97 -37
@@ -33,7 +33,7 @@ import {
33 isEnabled as ReactBrowserEventEmitterIsEnabled,
34 setEnabled as ReactBrowserEventEmitterSetEnabled,
35 } from '../events/ReactBrowserEventEmitter';
36 -import {getChildNamespace} from '../shared/DOMNamespaces';
36 +import {Namespaces, getChildNamespace} from '../shared/DOMNamespaces';
37 import {
38 ELEMENT_NODE,
39 TEXT_NODE,
@@ -46,6 +46,7 @@ import dangerousStyleValue from '../shared/dangerousStyleValue';
46 import type {DOMContainer} from './ReactDOM';
47 import type {ReactEventResponder} from 'shared/ReactTypes';
48 import {REACT_EVENT_TARGET_TOUCH_HIT} from 'shared/ReactSymbols';
49 +import {canUseDOM} from 'shared/ExecutionEnvironment';
50
51 export type Type = string;
52 export type Props = {
@@ -57,6 +58,23 @@ export type Props = {
58 style?: {
59 display?: string,
60 },
61 + bottom?: null | number,
62 + left?: null | number,
63 + right?: null | number,
64 + top?: null | number,
65 +};
66 +export type EventTargetChildElement = {
67 + type: string,
68 + props: null | {
69 + style?: {
70 + position?: string,
71 + zIndex?: number,
72 + bottom?: string,
73 + left?: string,
74 + right?: string,
75 + top?: string,
76 + },
77 + },
78 };
79 export type Container = Element | Document;
80 export type Instance = Element;
@@ -70,7 +88,6 @@ type HostContextDev = {
88 eventData: null | {|
89 isEventComponent?: boolean,
90 isEventTarget?: boolean,
73 - eventTargetType?: null | Symbol | number,
91 |},
92 };
93 type HostContextProd = string;
@@ -86,6 +103,8 @@ import {
103 } from 'shared/ReactFeatureFlags';
104 import warning from 'shared/warning';
105
106 +const {html: HTML_NAMESPACE} = Namespaces;
107 +
108 // Intentionally not named imports because Rollup would
109 // use dynamic dispatch for CommonJS interop named imports.
110 const {
@@ -190,7 +209,6 @@ export function getChildHostContextForEventComponent(
209 const eventData = {
210 isEventComponent: true,
211 isEventTarget: false,
193 - eventTargetType: null,
212 };
213 return {namespace, ancestorInfo, eventData};
214 }
@@ -204,17 +222,24 @@ export function getChildHostContextForEventTarget(
222 if (__DEV__) {
223 const parentHostContextDev = ((parentHostContext: any): HostContextDev);
224 const {namespace, ancestorInfo} = parentHostContextDev;
207 - warning(
208 - parentHostContextDev.eventData === null ||
209 - !parentHostContextDev.eventData.isEventComponent ||
210 - type !== REACT_EVENT_TARGET_TOUCH_HIT,
211 - 'validateDOMNesting: <TouchHitTarget> cannot not be a direct child of an event component. ' +
212 - 'Ensure <TouchHitTarget> is a direct child of a DOM element.',
213 - );
225 + if (type === REACT_EVENT_TARGET_TOUCH_HIT) {
226 + warning(
227 + parentHostContextDev.eventData === null ||
228 + !parentHostContextDev.eventData.isEventComponent,
229 + 'validateDOMNesting: <TouchHitTarget> cannot not be a direct child of an event component. ' +
230 + 'Ensure <TouchHitTarget> is a direct child of a DOM element.',
231 + );
232 + const parentNamespace = parentHostContextDev.namespace;
233 + if (parentNamespace !== HTML_NAMESPACE) {
234 + throw new Error(
235 + '<TouchHitTarget> was used in an unsupported DOM namespace. ' +
236 + 'Ensure the <TouchHitTarget> is used in an HTML namespace.',
237 + );
238 + }
239 + }
240 const eventData = {
241 isEventComponent: false,
242 isEventTarget: true,
217 - eventTargetType: type,
243 };
244 return {namespace, ancestorInfo, eventData};
245 }
@@ -249,16 +274,6 @@ export function createInstance(
274 if (__DEV__) {
275 // TODO: take namespace into account when validating.
276 const hostContextDev = ((hostContext: any): HostContextDev);
252 - if (enableEventAPI) {
253 - const eventData = hostContextDev.eventData;
254 - if (eventData !== null) {
255 - warning(
256 - !eventData.isEventTarget ||
257 - eventData.eventTargetType !== REACT_EVENT_TARGET_TOUCH_HIT,
258 - 'Warning: validateDOMNesting: <TouchHitTarget> must not have any children.',
259 - );
260 - }
261 - }
277 validateDOMNesting(type, null, hostContextDev.ancestorInfo);
278 if (
279 typeof props.children === 'string' ||
@@ -365,25 +380,12 @@ export function createTextInstance(
380 if (enableEventAPI) {
381 const eventData = hostContextDev.eventData;
382 if (eventData !== null) {
368 - warning(
369 - eventData === null ||
370 - !eventData.isEventTarget ||
371 - eventData.eventTargetType !== REACT_EVENT_TARGET_TOUCH_HIT,
372 - 'Warning: validateDOMNesting: <TouchHitTarget> must not have any children.',
373 - );
383 warning(
384 !eventData.isEventComponent,
385 'validateDOMNesting: React event components cannot have text DOM nodes as children. ' +
386 'Wrap the child text "%s" in an element.',
387 text,
388 );
380 - warning(
381 - !eventData.isEventTarget ||
382 - eventData.eventTargetType === REACT_EVENT_TARGET_TOUCH_HIT,
383 - 'validateDOMNesting: React event targets cannot have text DOM nodes as children. ' +
384 - 'Wrap the child text "%s" in an element.',
385 - text,
386 - );
389 }
390 }
391 }
@@ -899,16 +901,74 @@ export function handleEventComponent(
901 }
902 }
903
904 +export function getEventTargetChildElement(
905 + type: Symbol | number,
906 + props: Props,
907 +): null | EventTargetChildElement {
908 + if (enableEventAPI) {
909 + if (type === REACT_EVENT_TARGET_TOUCH_HIT) {
910 + const {bottom, left, right, top} = props;
911 +
912 + if (!bottom && !left && !right && !top) {
913 + return null;
914 + }
915 + return {
916 + type: 'div',
917 + props: {
918 + style: {
919 + position: 'absolute',
920 + zIndex: -1,
921 + bottom: bottom ? `-${bottom}px` : '0px',
922 + left: left ? `-${left}px` : '0px',
923 + right: right ? `-${right}px` : '0px',
924 + top: top ? `-${top}px` : '0px',
925 + },
926 + },
927 + };
928 + }
929 + }
930 + return null;
931 +}
932 +
933 export function handleEventTarget(
934 type: Symbol | number,
935 props: Props,
905 - parentInstance: Container,
936 + rootContainerInstance: Container,
937 internalInstanceHandle: Object,
938 +): boolean {
939 + return false;
940 +}
941 +
942 +export function commitEventTarget(
943 + type: Symbol | number,
944 + props: Props,
945 + instance: Instance,
946 + parentInstance: Instance,
947 ): void {
948 if (enableEventAPI) {
909 - // Touch target hit slop handling
949 if (type === REACT_EVENT_TARGET_TOUCH_HIT) {
911 - // TODO
950 + if (__DEV__ && canUseDOM) {
951 + // This is done at DEV time because getComputedStyle will
952 + // typically force a style recalculation and force a layout,
953 + // reflow -– both of which are sync are expensive.
954 + const computedStyles = window.getComputedStyle(parentInstance);
955 + const position = computedStyles.getPropertyValue('position');
956 + warning(
957 + position !== '' && position !== 'static',
958 + '<TouchHitTarget> inserts an empty absolutely positioned <div>. ' +
959 + 'This requires its parent DOM node to be positioned too, but the ' +
960 + 'parent DOM node was found to have the style "position" set to ' +
961 + 'either no value, or a value of "static". Try using a "position" ' +
962 + 'value of "relative".',
963 + );
964 + warning(
965 + computedStyles.getPropertyValue('zIndex') !== '',
966 + '<TouchHitTarget> inserts an empty <div> with "z-index" of "-1". ' +
967 + 'This requires its parent DOM node to have a "z-index" great than "-1",' +
968 + 'but the parent DOM node was found to no "z-index" value set.' +
969 + ' Try using a "z-index" value of "0" or greater.',
970 + );
971 + }
972 }
973 }
974 }
packages/react-dom/src/events/DOMEventResponderSystem.js
+29 -2
@@ -17,7 +17,10 @@ import {
17 PASSIVE_NOT_SUPPORTED,
18 } from 'events/EventSystemFlags';
19 import type {AnyNativeEvent} from 'events/PluginModuleType';
20 -import {EventComponent} from 'shared/ReactWorkTags';
20 +import {
21 + EventComponent,
22 + EventTarget as EventTargetWorkTag,
23 +} from 'shared/ReactWorkTags';
24 import type {
25 ReactEventResponder,
26 ReactEventResponderEventType,
@@ -110,7 +113,31 @@ const eventResponderContext: ResponderContext = {
113 eventsWithStopPropagation.add(eventObject);
114 }
115 },
113 - isPositionWithinTouchHitTarget(x: number, y: number): boolean {
116 + isPositionWithinTouchHitTarget(doc: Document, x: number, y: number): boolean {
117 + // This isn't available in some environments (JSDOM)
118 + if (typeof doc.elementFromPoint !== 'function') {
119 + return false;
120 + }
121 + const target = doc.elementFromPoint(x, y);
122 + if (target === null) {
123 + return false;
124 + }
125 + const childFiber = getClosestInstanceFromNode(target);
126 + if (childFiber === null) {
127 + return false;
128 + }
129 + const parentFiber = childFiber.return;
130 + if (parentFiber !== null && parentFiber.tag === EventTargetWorkTag) {
131 + const parentNode = ((target.parentNode: any): Element);
132 + // TODO find another way to do this without using the
133 + // expensive getBoundingClientRect.
134 + const {left, top, right, bottom} = parentNode.getBoundingClientRect();
135 + // Check if the co-ords intersect with the target element's rect.
136 + if (x > left && y > top && x < right && y < bottom) {
137 + return false;
138 + }
139 + return true;
140 + }
141 return false;
142 },
143 isTargetWithinEventComponent(target: Element | Document): boolean {
packages/react-dom/src/server/ReactPartialRenderer.js
+24
@@ -39,6 +39,7 @@ import {
39 REACT_MEMO_TYPE,
40 REACT_EVENT_COMPONENT_TYPE,
41 REACT_EVENT_TARGET_TYPE,
42 + REACT_EVENT_TARGET_TOUCH_HIT,
43 } from 'shared/ReactSymbols';
44
45 import {
@@ -1168,6 +1169,29 @@ class ReactDOMServerRenderer {
1169 case REACT_EVENT_COMPONENT_TYPE:
1170 case REACT_EVENT_TARGET_TYPE: {
1171 if (enableEventAPI) {
1172 + if (
1173 + elementType.$$typeof === REACT_EVENT_TARGET_TYPE &&
1174 + elementType.type === REACT_EVENT_TARGET_TOUCH_HIT
1175 + ) {
1176 + const props = nextElement.props;
1177 + const bottom = props.bottom || 0;
1178 + const left = props.left || 0;
1179 + const right = props.right || 0;
1180 + const top = props.top || 0;
1181 +
1182 + if (bottom === 0 && left === 0 && right === 0 && top === 0) {
1183 + return '';
1184 + }
1185 + let topString = top ? `-${top}px` : '0px';
1186 + let leftString = left ? `-${left}px` : '0px';
1187 + let rightString = right ? `-${right}px` : '0x';
1188 + let bottomString = bottom ? `-${bottom}px` : '0px';
1189 +
1190 + return (
1191 + `<div style="position:absolute;z-index:-1;bottom:` +
1192 + `${bottomString};left:${leftString};right:${rightString};top:${topString}"></div>`
1193 + );
1194 + }
1195 const nextChildren = toArray(
1196 ((nextChild: any): ReactElement).props.children,
1197 );
packages/react-events/src/Hover.js
+4 -1
@@ -196,7 +196,7 @@ const HoverResponder = {
196 props: HoverProps,
197 state: HoverState,
198 ): void {
199 - const {type, nativeEvent} = event;
199 + const {type, target, nativeEvent} = event;
200
201 switch (type) {
202 /**
@@ -218,6 +218,7 @@ const HoverResponder = {
218 }
219 if (
220 context.isPositionWithinTouchHitTarget(
221 + target.ownerDocument,
222 (nativeEvent: any).x,
223 (nativeEvent: any).y,
224 )
@@ -244,6 +245,7 @@ const HoverResponder = {
245 if (state.isInHitSlop) {
246 if (
247 !context.isPositionWithinTouchHitTarget(
248 + target.ownerDocument,
249 (nativeEvent: any).x,
250 (nativeEvent: any).y,
251 )
@@ -254,6 +256,7 @@ const HoverResponder = {
256 } else if (
257 state.isHovered &&
258 context.isPositionWithinTouchHitTarget(
259 + target.ownerDocument,
260 (nativeEvent: any).x,
261 (nativeEvent: any).y,
262 )
packages/react-events/src/Press.js
+1
@@ -259,6 +259,7 @@ const PressResponder = {
259 nativeEvent.button === 2 ||
260 // Ignore pressing on hit slop area with mouse
261 context.isPositionWithinTouchHitTarget(
262 + target.ownerDocument,
263 (nativeEvent: any).x,
264 (nativeEvent: any).y,
265 )
packages/react-events/src/__tests__/TouchHitTarget-test.internal.js
+326 -18
@@ -16,6 +16,7 @@ let ReactFeatureFlags;
16 let EventComponent;
17 let ReactTestRenderer;
18 let ReactDOM;
19 +let ReactDOMServer;
20 let ReactSymbols;
21 let ReactEvents;
22 let TouchHitTarget;
@@ -58,6 +59,11 @@ function initReactDOM() {
59 ReactDOM = require('react-dom');
60 }
61
62 +function initReactDOMServer() {
63 + init();
64 + ReactDOMServer = require('react-dom/server');
65 +}
66 +
67 describe('TouchHitTarget', () => {
68 describe('NoopRenderer', () => {
69 beforeEach(() => {
@@ -94,9 +100,7 @@ describe('TouchHitTarget', () => {
100 expect(() => {
101 ReactNoop.render(<Test />);
102 expect(Scheduler).toFlushWithoutYielding();
97 - }).toWarnDev(
98 - 'Warning: validateDOMNesting: <TouchHitTarget> must not have any children.',
99 - );
103 + }).toWarnDev('Warning: Event targets should not have children.');
104
105 const Test2 = () => (
106 <EventComponent>
@@ -109,9 +113,7 @@ describe('TouchHitTarget', () => {
113 expect(() => {
114 ReactNoop.render(<Test2 />);
115 expect(Scheduler).toFlushWithoutYielding();
112 - }).toWarnDev(
113 - 'Warning: validateDOMNesting: <TouchHitTarget> must not have any children.',
114 - );
116 + }).toWarnDev('Warning: Event targets should not have children.');
117
118 // Should render without warnings
119 const Test3 = () => (
@@ -181,9 +183,7 @@ describe('TouchHitTarget', () => {
183 expect(() => {
184 root.update(<Test />);
185 expect(Scheduler).toFlushWithoutYielding();
184 - }).toWarnDev(
185 - 'Warning: validateDOMNesting: <TouchHitTarget> must not have any children.',
186 - );
186 + }).toWarnDev('Warning: Event targets should not have children.');
187
188 const Test2 = () => (
189 <EventComponent>
@@ -196,9 +196,7 @@ describe('TouchHitTarget', () => {
196 expect(() => {
197 root.update(<Test2 />);
198 expect(Scheduler).toFlushWithoutYielding();
199 - }).toWarnDev(
200 - 'Warning: validateDOMNesting: <TouchHitTarget> must not have any children.',
201 - );
199 + }).toWarnDev('Warning: Event targets should not have children.');
200
201 // Should render without warnings
202 const Test3 = () => (
@@ -269,9 +267,7 @@ describe('TouchHitTarget', () => {
267 expect(() => {
268 ReactDOM.render(<Test />, container);
269 expect(Scheduler).toFlushWithoutYielding();
272 - }).toWarnDev(
273 - 'Warning: validateDOMNesting: <TouchHitTarget> must not have any children.',
274 - );
270 + }).toWarnDev('Warning: Event targets should not have children.');
271
272 const Test2 = () => (
273 <EventComponent>
@@ -284,9 +280,7 @@ describe('TouchHitTarget', () => {
280 expect(() => {
281 ReactDOM.render(<Test2 />, container);
282 expect(Scheduler).toFlushWithoutYielding();
287 - }).toWarnDev(
288 - 'Warning: validateDOMNesting: <TouchHitTarget> must not have any children.',
289 - );
283 + }).toWarnDev('Warning: Event targets should not have children.');
284
285 // Should render without warnings
286 const Test3 = () => (
@@ -318,5 +312,319 @@ describe('TouchHitTarget', () => {
312 'Ensure <TouchHitTarget> is a direct child of a DOM element.',
313 );
314 });
315 +
316 + it('should render a conditional TouchHitTarget correctly (false -> true)', () => {
317 + let cond = false;
318 +
319 + const Test = () => (
320 + <EventComponent>
321 + <div>
322 + {cond ? null : (
323 + <TouchHitTarget top={10} left={10} right={10} bottom={10} />
324 + )}
325 + </div>
326 + </EventComponent>
327 + );
328 +
329 + const container = document.createElement('div');
330 + ReactDOM.render(<Test />, container);
331 + expect(Scheduler).toFlushWithoutYielding();
332 + expect(container.innerHTML).toBe(
333 + '<div><div style="position: absolute; z-index: -1; bottom: -10px; ' +
334 + 'left: -10px; right: -10px; top: -10px;"></div></div>',
335 + );
336 +
337 + cond = true;
338 + ReactDOM.render(<Test />, container);
339 + expect(Scheduler).toFlushWithoutYielding();
340 + expect(container.innerHTML).toBe('<div></div>');
341 + });
342 +
343 + it('should render a conditional TouchHitTarget correctly (true -> false)', () => {
344 + let cond = true;
345 +
346 + const Test = () => (
347 + <EventComponent>
348 + <div>
349 + {cond ? null : (
350 + <TouchHitTarget top={10} left={10} right={10} bottom={10} />
351 + )}
352 + </div>
353 + </EventComponent>
354 + );
355 +
356 + const container = document.createElement('div');
357 + ReactDOM.render(<Test />, container);
358 + expect(Scheduler).toFlushWithoutYielding();
359 + expect(container.innerHTML).toBe('<div></div>');
360 +
361 + cond = false;
362 + ReactDOM.render(<Test />, container);
363 + expect(Scheduler).toFlushWithoutYielding();
364 + expect(container.innerHTML).toBe(
365 + '<div><div style="position: absolute; z-index: -1; bottom: -10px; ' +
366 + 'left: -10px; right: -10px; top: -10px;"></div></div>',
367 + );
368 + });
369 +
370 + it('should render a conditional TouchHitTarget hit slop correctly (false -> true)', () => {
371 + let cond = false;
372 +
373 + const Test = () => (
374 + <EventComponent>
375 + <div>
376 + {cond ? (
377 + <TouchHitTarget />
378 + ) : (
379 + <TouchHitTarget top={10} left={10} right={10} bottom={10} />
380 + )}
381 + </div>
382 + </EventComponent>
383 + );
384 +
385 + const container = document.createElement('div');
386 + ReactDOM.render(<Test />, container);
387 + expect(Scheduler).toFlushWithoutYielding();
388 + expect(container.innerHTML).toBe(
389 + '<div><div style="position: absolute; z-index: -1; bottom: -10px; ' +
390 + 'left: -10px; right: -10px; top: -10px;"></div></div>',
391 + );
392 +
393 + cond = true;
394 + ReactDOM.render(<Test />, container);
395 + expect(Scheduler).toFlushWithoutYielding();
396 + expect(container.innerHTML).toBe('<div></div>');
397 + });
398 +
399 + it('should render a conditional TouchHitTarget hit slop correctly (true -> false)', () => {
400 + let cond = true;
401 +
402 + const Test = () => (
403 + <EventComponent>
404 + <div>
405 + <span>Random span 1</span>
406 + {cond ? (
407 + <TouchHitTarget />
408 + ) : (
409 + <TouchHitTarget top={10} left={10} right={10} bottom={10} />
410 + )}
411 + <span>Random span 2</span>
412 + </div>
413 + </EventComponent>
414 + );
415 +
416 + const container = document.createElement('div');
417 + ReactDOM.render(<Test />, container);
418 + expect(Scheduler).toFlushWithoutYielding();
419 + expect(container.innerHTML).toBe(
420 + '<div><span>Random span 1</span><span>Random span 2</span></div>',
421 + );
422 +
423 + cond = false;
424 + ReactDOM.render(<Test />, container);
425 + expect(Scheduler).toFlushWithoutYielding();
426 + expect(container.innerHTML).toBe(
427 + '<div><span>Random span 1</span><div style="position: absolute; z-index: -1; bottom: -10px; ' +
428 + 'left: -10px; right: -10px; top: -10px;"></div><span>Random span 2</span></div>',
429 + );
430 + });
431 +
432 + it('should update TouchHitTarget hit slop values correctly (false -> true)', () => {
433 + let cond = false;
434 +
435 + const Test = () => (
436 + <EventComponent>
437 + <div>
438 + <span>Random span 1</span>
439 + {cond ? (
440 + <TouchHitTarget top={10} left={null} right={10} bottom={10} />
441 + ) : (
442 + <TouchHitTarget
443 + top={undefined}
444 + left={20}
445 + right={null}
446 + bottom={0}
447 + />
448 + )}
449 + <span>Random span 2</span>
450 + </div>
451 + </EventComponent>
452 + );
453 +
454 + const container = document.createElement('div');
455 + ReactDOM.render(<Test />, container);
456 + expect(Scheduler).toFlushWithoutYielding();
457 + expect(container.innerHTML).toBe(
458 + '<div><span>Random span 1</span><div style="position: absolute; z-index: -1; bottom: 0px; ' +
459 + 'left: -20px; right: 0px; top: 0px;"></div><span>Random span 2</span></div>',
460 + );
461 +
462 + cond = true;
463 + ReactDOM.render(<Test />, container);
464 + expect(Scheduler).toFlushWithoutYielding();
465 + expect(container.innerHTML).toBe(
466 + '<div><span>Random span 1</span><div style="position: absolute; z-index: -1; bottom: 0px; ' +
467 + 'left: -20px; right: 0px; top: 0px;"></div><span>Random span 2</span></div>',
468 + );
469 + });
470 +
471 + it('should update TouchHitTarget hit slop values correctly (true -> false)', () => {
472 + let cond = true;
473 +
474 + const Test = () => (
475 + <EventComponent>
476 + <div>
477 + <span>Random span 1</span>
478 + {cond ? (
479 + <TouchHitTarget top={10} left={null} right={10} bottom={10} />
480 + ) : (
481 + <TouchHitTarget
482 + top={undefined}
483 + left={20}
484 + right={null}
485 + bottom={0}
486 + />
487 + )}
488 + <span>Random span 2</span>
489 + </div>
490 + </EventComponent>
491 + );
492 +
493 + const container = document.createElement('div');
494 + ReactDOM.render(<Test />, container);
495 + expect(Scheduler).toFlushWithoutYielding();
496 + expect(container.innerHTML).toBe(
497 + '<div><span>Random span 1</span><div style="position: absolute; z-index: -1; bottom: -10px; ' +
498 + 'left: 0px; right: -10px; top: -10px;"></div><span>Random span 2</span></div>',
499 + );
500 +
501 + cond = false;
502 + ReactDOM.render(<Test />, container);
503 + expect(Scheduler).toFlushWithoutYielding();
504 + expect(container.innerHTML).toBe(
505 + '<div><span>Random span 1</span><div style="position: absolute; z-index: -1; bottom: -10px; ' +
506 + 'left: 0px; right: -10px; top: -10px;"></div><span>Random span 2</span></div>',
507 + );
508 + });
509 +
510 + it('should hydrate TouchHitTarget hit slop elements correcty', () => {
511 + const Test = () => (
512 + <EventComponent>
513 + <div>
514 + <TouchHitTarget />
515 + </div>
516 + </EventComponent>
517 + );
518 +
519 + const container = document.createElement('div');
520 + container.innerHTML = '<div></div>';
521 + ReactDOM.hydrate(<Test />, container);
522 + expect(Scheduler).toFlushWithoutYielding();
523 + expect(container.innerHTML).toBe('<div></div>');
524 +
525 + const Test2 = () => (
526 + <EventComponent>
527 + <div>
528 + <TouchHitTarget top={10} left={10} right={10} bottom={10} />
529 + </div>
530 + </EventComponent>
531 + );
532 +
533 + const container2 = document.createElement('div');
534 + container2.innerHTML =
535 + '<div><div style="position:absolute;z-index:-1;bottom:-10px;left:-10px;right:-10px;top:-10px"></div></div>';
536 + ReactDOM.hydrate(<Test2 />, container2);
537 + expect(Scheduler).toFlushWithoutYielding();
538 + expect(container2.innerHTML).toBe(
539 + '<div><div style="position:absolute;z-index:-1;bottom:-10px;left:-10px;right:-10px;top:-10px"></div></div>',
540 + );
541 + });
542 +
543 + it('should hydrate TouchHitTarget hit slop elements correcty and patch them', () => {
544 + const Test = () => (
545 + <EventComponent>
546 + <div>
547 + <TouchHitTarget top={10} left={10} right={10} bottom={10} />
548 + </div>
549 + </EventComponent>
550 + );
551 +
552 + const container = document.createElement('div');
553 + container.innerHTML = '<div></div>';
554 + expect(() => {
555 + ReactDOM.hydrate(<Test />, container);
556 + expect(Scheduler).toFlushWithoutYielding();
557 + }).toWarnDev(
558 + 'Warning: Expected server HTML to contain a matching <div> in <div>.',
559 + {withoutStack: true},
560 + );
561 + expect(Scheduler).toFlushWithoutYielding();
562 + expect(container.innerHTML).toBe(
563 + '<div><div style="position: absolute; z-index: -1; bottom: -10px; ' +
564 + 'left: -10px; right: -10px; top: -10px;"></div></div>',
565 + );
566 + });
567 + });
568 +
569 + describe('ReactDOMServer', () => {
570 + beforeEach(() => {
571 + initReactDOMServer();
572 + EventComponent = createReactEventComponent();
573 + TouchHitTarget = ReactEvents.TouchHitTarget;
574 + });
575 +
576 + it('should not warn when a TouchHitTarget is used correctly', () => {
577 + const Test = () => (
578 + <EventComponent>
579 + <div>
580 + <TouchHitTarget />
581 + </div>
582 + </EventComponent>
583 + );
584 +
585 + const output = ReactDOMServer.renderToString(<Test />);
586 + expect(output).toBe('<div></div>');
587 + });
588 +
589 + it('should render a TouchHitTarget with hit slop values', () => {
590 + const Test = () => (
591 + <EventComponent>
592 + <div>
593 + <TouchHitTarget top={10} left={10} right={10} bottom={10} />
594 + </div>
595 + </EventComponent>
596 + );
597 +
598 + let output = ReactDOMServer.renderToString(<Test />);
599 + expect(output).toBe(
600 + '<div><div style="position:absolute;z-index:-1;bottom:-10px;left:-10px;right:-10px;top:-10px"></div></div>',
601 + );
602 +
603 + const Test2 = () => (
604 + <EventComponent>
605 + <div>
606 + <TouchHitTarget top={null} left={undefined} right={0} bottom={10} />
607 + </div>
608 + </EventComponent>
609 + );
610 +
611 + output = ReactDOMServer.renderToString(<Test2 />);
612 + expect(output).toBe(
613 + '<div><div style="position:absolute;z-index:-1;bottom:-10px;left:0px;right:0x;top:0px"></div></div>',
614 + );
615 +
616 + const Test3 = () => (
617 + <EventComponent>
618 + <div>
619 + <TouchHitTarget top={1} left={2} right={3} bottom={4} />
620 + </div>
621 + </EventComponent>
622 + );
623 +
624 + output = ReactDOMServer.renderToString(<Test3 />);
625 + expect(output).toBe(
626 + '<div><div style="position:absolute;z-index:-1;bottom:-4px;left:-2px;right:-3px;top:-1px"></div></div>',
627 + );
628 + });
629 });
630 });
packages/react-native-renderer/src/ReactFabricHostConfig.js
+21 -5
@@ -438,15 +438,31 @@ export function handleEventComponent(
438 eventResponder: ReactEventResponder,
439 rootContainerInstance: Container,
440 internalInstanceHandle: Object,
441 -) {
442 - // TODO: add handleEventComponent implementation
441 +): void {
442 + throw new Error('Not yet implemented.');
443 +}
444 +
445 +export function getEventTargetChildElement(
446 + type: Symbol | number,
447 + props: Props,
448 +): null {
449 + throw new Error('Not yet implemented.');
450 }
451
452 export function handleEventTarget(
453 type: Symbol | number,
454 props: Props,
448 - parentInstance: Container,
455 + rootContainerInstance: Container,
456 internalInstanceHandle: Object,
450 -) {
451 - // TODO: add handleEventTarget implementation
457 +): boolean {
458 + throw new Error('Not yet implemented.');
459 +}
460 +
461 +export function commitEventTarget(
462 + type: Symbol | number,
463 + props: Props,
464 + instance: Instance,
465 + parentInstance: Instance,
466 +): void {
467 + throw new Error('Not yet implemented.');
468 }
packages/react-native-renderer/src/ReactNativeHostConfig.js
+20 -4
@@ -498,14 +498,30 @@ export function handleEventComponent(
498 rootContainerInstance: Container,
499 internalInstanceHandle: Object,
500 ) {
501 - // TODO: add handleEventComponent implementation
501 + throw new Error('Not yet implemented.');
502 +}
503 +
504 +export function getEventTargetChildElement(
505 + type: Symbol | number,
506 + props: Props,
507 +): null {
508 + throw new Error('Not yet implemented.');
509 }
510
511 export function handleEventTarget(
512 type: Symbol | number,
513 props: Props,
507 - parentInstance: Container,
514 + rootContainerInstance: Container,
515 internalInstanceHandle: Object,
509 -) {
510 - // TODO: add handleEventTarget implementation
516 +): boolean {
517 + throw new Error('Not yet implemented.');
518 +}
519 +
520 +export function commitEventTarget(
521 + type: Symbol | number,
522 + props: Props,
523 + instance: Instance,
524 + parentInstance: Instance,
525 +): void {
526 + throw new Error('Not yet implemented.');
527 }
packages/react-noop-renderer/src/createReactNoop.js
+62 -22
@@ -33,12 +33,32 @@ import ReactSharedInternals from 'shared/ReactSharedInternals';
33 import warningWithoutStack from 'shared/warningWithoutStack';
34 import {enableEventAPI} from 'shared/ReactFeatureFlags';
35
36 +type EventTargetChildElement = {
37 + type: string,
38 + props: null | {
39 + style?: {
40 + position?: string,
41 + bottom?: string,
42 + left?: string,
43 + right?: string,
44 + top?: string,
45 + },
46 + },
47 +};
48 type Container = {
49 rootID: string,
50 children: Array<Instance | TextInstance>,
51 pendingChildren: Array<Instance | TextInstance>,
52 };
41 -type Props = {prop: any, hidden: boolean, children?: mixed};
53 +type Props = {
54 + prop: any,
55 + hidden: boolean,
56 + children?: mixed,
57 + bottom?: null | number,
58 + left?: null | number,
59 + right?: null | number,
60 + top?: null | number,
61 +};
62 type Instance = {|
63 type: string,
64 id: number,
@@ -299,12 +319,6 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
319 rootContainerInstance: Container,
320 hostContext: HostContext,
321 ): Instance {
302 - if (__DEV__ && enableEventAPI) {
303 - warning(
304 - hostContext !== EVENT_TOUCH_HIT_TARGET_CONTEXT,
305 - 'validateDOMNesting: <TouchHitTarget> must not have any children.',
306 - );
307 - }
322 if (type === 'errorInCompletePhase') {
323 throw new Error('Error in host config.');
324 }
@@ -379,22 +393,12 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
393 internalInstanceHandle: Object,
394 ): TextInstance {
395 if (__DEV__ && enableEventAPI) {
382 - warning(
383 - hostContext !== EVENT_TOUCH_HIT_TARGET_CONTEXT,
384 - 'validateDOMNesting: <TouchHitTarget> must not have any children.',
385 - );
396 warning(
397 hostContext !== EVENT_COMPONENT_CONTEXT,
398 'validateDOMNesting: React event components cannot have text DOM nodes as children. ' +
399 'Wrap the child text "%s" in an element.',
400 text,
401 );
392 - warning(
393 - hostContext !== EVENT_TARGET_CONTEXT,
394 - 'validateDOMNesting: React event targets cannot have text DOM nodes as children. ' +
395 - 'Wrap the child text "%s" in an element.',
396 - text,
397 - );
402 }
403 if (hostContext === UPPERCASE_CONTEXT) {
404 text = text.toUpperCase();
@@ -431,15 +435,51 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
435 // NO-OP
436 },
437
438 + getEventTargetChildElement(
439 + type: Symbol | number,
440 + props: Props,
441 + ): null | EventTargetChildElement {
442 + if (enableEventAPI) {
443 + if (type === REACT_EVENT_TARGET_TOUCH_HIT) {
444 + const {bottom, left, right, top} = props;
445 +
446 + if (!bottom && !left && !right && !top) {
447 + return null;
448 + }
449 + return {
450 + type: 'div',
451 + props: {
452 + style: {
453 + position: 'absolute',
454 + zIndex: -1,
455 + bottom: bottom ? `-${bottom}px` : '0px',
456 + left: left ? `-${left}px` : '0px',
457 + right: right ? `-${right}px` : '0px',
458 + top: top ? `-${top}px` : '0px',
459 + },
460 + },
461 + };
462 + }
463 + }
464 + return null;
465 + },
466 +
467 handleEventTarget(
468 type: Symbol | number,
469 props: Props,
437 - parentInstance: Container,
470 + rootContainerInstance: Container,
471 internalInstanceHandle: Object,
439 - ) {
440 - if (type === REACT_EVENT_TARGET_TOUCH_HIT) {
441 - // TODO
442 - }
472 + ): boolean {
473 + return false;
474 + },
475 +
476 + commitEventTarget(
477 + type: Symbol | number,
478 + props: Props,
479 + instance: Instance,
480 + parentInstance: Instance,
481 + ): void {
482 + // NO-OP
483 },
484 };
485
packages/react-reconciler/src/ReactFiberBeginWork.js
+26 -7
@@ -96,6 +96,7 @@ import {
96 registerSuspenseInstanceRetry,
97 } from './ReactFiberHostConfig';
98 import type {SuspenseInstance} from './ReactFiberHostConfig';
99 +import {getEventTargetChildElement} from './ReactFiberHostConfig';
100 import {shouldSuspend} from './ReactFiberReconciler';
101 import {
102 pushHostContext,
@@ -1988,15 +1989,33 @@ function updateEventComponent(current, workInProgress, renderExpirationTime) {
1989 }
1990
1991 function updateEventTarget(current, workInProgress, renderExpirationTime) {
1992 + const type = workInProgress.type.type;
1993 const nextProps = workInProgress.pendingProps;
1992 - let nextChildren = nextProps.children;
1994 + const eventTargetChild = getEventTargetChildElement(type, nextProps);
1995
1994 - reconcileChildren(
1995 - current,
1996 - workInProgress,
1997 - nextChildren,
1998 - renderExpirationTime,
1999 - );
1996 + if (__DEV__) {
1997 + warning(
1998 + nextProps.children == null,
1999 + 'Event targets should not have children.',
2000 + );
2001 + }
2002 + if (eventTargetChild !== null) {
2003 + const child = (workInProgress.child = createFiberFromTypeAndProps(
2004 + eventTargetChild.type,
2005 + null,
2006 + eventTargetChild.props,
2007 + null,
2008 + workInProgress.mode,
2009 + renderExpirationTime,
2010 + ));
2011 + child.return = workInProgress;
2012 +
2013 + if (current === null || current.child === null) {
2014 + child.effectTag = Placement;
2015 + }
2016 + } else {
2017 + reconcileChildren(current, workInProgress, null, renderExpirationTime);
2018 + }
2019 pushHostContextForEventTarget(workInProgress);
2020 return workInProgress.child;
2021 }
packages/react-reconciler/src/ReactFiberCommitWork.js
+40 -5
@@ -28,6 +28,7 @@ import {
28 enableSchedulerTracing,
29 enableProfilerTimer,
30 enableSuspenseServerRenderer,
31 + enableEventAPI,
32 } from 'shared/ReactFeatureFlags';
33 import {
34 FunctionComponent,
@@ -43,6 +44,7 @@ import {
44 IncompleteClassComponent,
45 MemoComponent,
46 SimpleMemoComponent,
47 + EventTarget,
48 } from 'shared/ReactWorkTags';
49 import {
50 invokeGuardedCallback,
@@ -90,6 +92,7 @@ import {
92 hideTextInstance,
93 unhideInstance,
94 unhideTextInstance,
95 + commitEventTarget,
96 } from './ReactFiberHostConfig';
97 import {
98 captureCommitPhaseError,
@@ -299,6 +302,7 @@ function commitBeforeMutationLifeCycles(
302 case HostText:
303 case HostPortal:
304 case IncompleteClassComponent:
305 + case EventTarget:
306 // Nothing to do for these component types
307 return;
308 default: {
@@ -585,6 +589,7 @@ function commitLifeCycles(
589 }
590 case SuspenseComponent:
591 case IncompleteClassComponent:
592 + case EventTarget:
593 break;
594 default: {
595 invariant(
@@ -817,7 +822,8 @@ function commitContainer(finishedWork: Fiber) {
822 switch (finishedWork.tag) {
823 case ClassComponent:
824 case HostComponent:
820 - case HostText: {
825 + case HostText:
826 + case EventTarget: {
827 return;
828 }
829 case HostRoot:
@@ -955,17 +961,18 @@ function commitPlacement(finishedWork: Fiber): void {
961 let node: Fiber = finishedWork;
962 while (true) {
963 if (node.tag === HostComponent || node.tag === HostText) {
964 + const stateNode = node.stateNode;
965 if (before) {
966 if (isContainer) {
960 - insertInContainerBefore(parent, node.stateNode, before);
967 + insertInContainerBefore(parent, stateNode, before);
968 } else {
962 - insertBefore(parent, node.stateNode, before);
969 + insertBefore(parent, stateNode, before);
970 }
971 } else {
972 if (isContainer) {
966 - appendChildToContainer(parent, node.stateNode);
973 + appendChildToContainer(parent, stateNode);
974 } else {
968 - appendChild(parent, node.stateNode);
975 + appendChild(parent, stateNode);
976 }
977 }
978 } else if (node.tag === HostPortal) {
@@ -1195,6 +1202,34 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
1202 commitTextUpdate(textInstance, oldText, newText);
1203 return;
1204 }
1205 + case EventTarget: {
1206 + if (enableEventAPI) {
1207 + const type = finishedWork.type.type;
1208 + const props = finishedWork.memoizedProps;
1209 + const instance = finishedWork.stateNode;
1210 + let parentInstance = null;
1211 +
1212 + let node = finishedWork.return;
1213 + // Traverse up the fiber tree until we find the parent host node.
1214 + while (node !== null) {
1215 + if (node.tag === HostComponent) {
1216 + parentInstance = node.stateNode;
1217 + break;
1218 + } else if (node.tag === HostRoot) {
1219 + parentInstance = node.stateNode.containerInfo;
1220 + break;
1221 + }
1222 + node = node.return;
1223 + }
1224 + invariant(
1225 + parentInstance !== null,
1226 + 'This should have a parent host component initialized. This error is likely ' +
1227 + 'caused by a bug in React. Please file an issue.',
1228 + );
1229 + commitEventTarget(type, props, instance, parentInstance);
1230 + }
1231 + return;
1232 + }
1233 case HostRoot: {
1234 return;
1235 }
packages/react-reconciler/src/ReactFiberCompleteWork.js
+9 -12
@@ -784,18 +784,15 @@ function completeWork(
784 if (enableEventAPI) {
785 popHostContext(workInProgress);
786 const type = workInProgress.type.type;
787 - let node = workInProgress.return;
788 - let parentHostInstance = null;
789 - // Traverse up the fiber tree till we find a host component fiber
790 - while (node !== null) {
791 - if (node.tag === HostComponent) {
792 - parentHostInstance = node.stateNode;
793 - break;
794 - }
795 - node = node.return;
796 - }
797 - if (parentHostInstance !== null) {
798 - handleEventTarget(type, newProps, parentHostInstance, workInProgress);
787 + const rootContainerInstance = getRootHostContainer();
788 + const shouldUpdate = handleEventTarget(
789 + type,
790 + newProps,
791 + rootContainerInstance,
792 + workInProgress,
793 + );
794 + if (shouldUpdate) {
795 + markUpdate(workInProgress);
796 }
797 }
798 break;
packages/react-reconciler/src/__tests__/ReactFiberEvents-test-internal.js
+92 -160
@@ -127,9 +127,9 @@ describe('ReactFiberEvents', () => {
127 it('should render a simple event component with a single event target', () => {
128 const Test = () => (
129 <EventComponent>
130 - <EventTarget>
131 - <div>Hello world</div>
132 - </EventTarget>
130 + <div>
131 + Hello world<EventTarget />
132 + </div>
133 </EventComponent>
134 );
135
@@ -148,10 +148,7 @@ describe('ReactFiberEvents', () => {
148 expect(() => {
149 ReactNoop.render(<Test />);
150 expect(Scheduler).toFlushWithoutYielding();
151 - }).toWarnDev(
152 - 'Warning: validateDOMNesting: React event targets cannot have text DOM nodes as children. ' +
153 - 'Wrap the child text "Hello world" in an element.',
154 - );
151 + }).toWarnDev('Warning: Event targets should not have children.');
152 });
153
154 it('should warn when an event target has a direct text child #2', () => {
@@ -167,19 +164,15 @@ describe('ReactFiberEvents', () => {
164 expect(() => {
165 ReactNoop.render(<Test />);
166 expect(Scheduler).toFlushWithoutYielding();
170 - }).toWarnDev(
171 - 'Warning: validateDOMNesting: React event targets cannot have text DOM nodes as children. ' +
172 - 'Wrap the child text "Hello world" in an element.',
173 - );
167 + }).toWarnDev('Warning: Event targets should not have children.');
168 });
169
170 it('should not warn if an event target is not a direct child of an event component', () => {
171 const Test = () => (
172 <EventComponent>
173 <div>
180 - <EventTarget>
181 - <span>Child 1</span>
182 - </EventTarget>
174 + <EventTarget />
175 + <span>Child 1</span>
176 </div>
177 </EventComponent>
178 );
@@ -207,9 +200,7 @@ describe('ReactFiberEvents', () => {
200 expect(() => {
201 ReactNoop.render(<Test />);
202 expect(Scheduler).toFlushWithoutYielding();
210 - }).toWarnDev(
211 - 'Warning: validateDOMNesting: React event targets must not have event components as children.',
212 - );
203 + }).toWarnDev('Warning: Event targets should not have children.');
204 });
205
206 it('should handle event components correctly with error boundaries', () => {
@@ -219,11 +210,9 @@ describe('ReactFiberEvents', () => {
210
211 const Test = () => (
212 <EventComponent>
222 - <EventTarget>
223 - <span>
224 - <ErrorComponent />
225 - </span>
226 - </EventTarget>
213 + <span>
214 + <ErrorComponent />
215 + </span>
216 </EventComponent>
217 );
218
@@ -268,11 +257,9 @@ describe('ReactFiberEvents', () => {
257
258 const Parent = () => (
259 <EventComponent>
271 - <EventTarget>
272 - <div>
273 - <Child />
274 - </div>
275 - </EventTarget>
260 + <div>
261 + <Child />
262 + </div>
263 </EventComponent>
264 );
265
@@ -321,9 +308,7 @@ describe('ReactFiberEvents', () => {
308
309 const Parent = () => (
310 <EventComponent>
324 - <EventTarget>
325 - <Child />
326 - </EventTarget>
311 + <Child />
312 </EventComponent>
313 );
314
@@ -341,7 +326,7 @@ describe('ReactFiberEvents', () => {
326 });
327 expect(Scheduler).toFlushWithoutYielding();
328 }).toWarnDev(
344 - 'Warning: validateDOMNesting: React event targets cannot have text DOM nodes as children. ' +
329 + 'Warning: validateDOMNesting: React event components cannot have text DOM nodes as children. ' +
330 'Wrap the child text "Text!" in an element.',
331 );
332 });
@@ -355,11 +340,7 @@ describe('ReactFiberEvents', () => {
340 _updateCounter = updateCounter;
341
342 if (counter === 1) {
358 - return (
359 - <EventComponent>
360 - <div>Child</div>
361 - </EventComponent>
362 - );
343 + return <EventTarget>123</EventTarget>;
344 }
345
346 return (
@@ -370,18 +351,20 @@ describe('ReactFiberEvents', () => {
351 }
352
353 const Parent = () => (
373 - <EventComponent>
374 - <EventTarget>
354 + <div>
355 + <EventComponent>
356 <Child />
376 - </EventTarget>
377 - </EventComponent>
357 + </EventComponent>
358 + </div>
359 );
360
361 ReactNoop.render(<Parent />);
362 expect(Scheduler).toFlushWithoutYielding();
363 expect(ReactNoop).toMatchRenderedOutput(
364 <div>
384 - <span>Child - 0</span>
365 + <div>
366 + <span>Child - 0</span>
367 + </div>
368 </div>,
369 );
370
@@ -390,9 +373,7 @@ describe('ReactFiberEvents', () => {
373 _updateCounter(counter => counter + 1);
374 });
375 expect(Scheduler).toFlushWithoutYielding();
393 - }).toWarnDev(
394 - 'Warning: validateDOMNesting: React event targets must not have event components as children.',
395 - );
376 + }).toWarnDev('Warning: Event targets should not have children.');
377 });
378
379 it('should error with a component stack contains the names of the event components and event targets', () => {
@@ -404,11 +385,9 @@ describe('ReactFiberEvents', () => {
385
386 const Test = () => (
387 <EventComponent>
407 - <EventTarget>
408 - <span>
409 - <ErrorComponent />
410 - </span>
411 - </EventTarget>
388 + <span>
389 + <ErrorComponent />
390 + </span>
391 </EventComponent>
392 );
393
@@ -437,7 +416,6 @@ describe('ReactFiberEvents', () => {
416
417 expect(componentStackMessage.includes('ErrorComponent')).toBe(true);
418 expect(componentStackMessage.includes('span')).toBe(true);
440 - expect(componentStackMessage.includes('TestEventTarget')).toBe(true);
419 expect(componentStackMessage.includes('TestEventComponent')).toBe(true);
420 expect(componentStackMessage.includes('Test')).toBe(true);
421 expect(componentStackMessage.includes('Wrapper')).toBe(true);
@@ -498,9 +476,9 @@ describe('ReactFiberEvents', () => {
476 it('should render a simple event component with a single event target', () => {
477 const Test = () => (
478 <EventComponent>
501 - <EventTarget>
502 - <div>Hello world</div>
503 - </EventTarget>
479 + <div>
480 + Hello world<EventTarget />
481 + </div>
482 </EventComponent>
483 );
484
@@ -511,9 +489,8 @@ describe('ReactFiberEvents', () => {
489
490 const Test2 = () => (
491 <EventComponent>
514 - <EventTarget>
515 - <span>I am now a span</span>
516 - </EventTarget>
492 + <EventTarget />
493 + <span>I am now a span</span>
494 </EventComponent>
495 );
496
@@ -533,10 +510,7 @@ describe('ReactFiberEvents', () => {
510 expect(() => {
511 root.update(<Test />);
512 expect(Scheduler).toFlushWithoutYielding();
536 - }).toWarnDev(
537 - 'Warning: validateDOMNesting: React event targets cannot have text DOM nodes as children. ' +
538 - 'Wrap the child text "Hello world" in an element.',
539 - );
513 + }).toWarnDev('Warning: Event targets should not have children.');
514 });
515
516 it('should warn when an event target has a direct text child #2', () => {
@@ -553,19 +527,15 @@ describe('ReactFiberEvents', () => {
527 expect(() => {
528 root.update(<Test />);
529 expect(Scheduler).toFlushWithoutYielding();
556 - }).toWarnDev(
557 - 'Warning: validateDOMNesting: React event targets cannot have text DOM nodes as children. ' +
558 - 'Wrap the child text "Hello world" in an element.',
559 - );
530 + }).toWarnDev('Warning: Event targets should not have children.');
531 });
532
533 it('should not warn if an event target is not a direct child of an event component', () => {
534 const Test = () => (
535 <EventComponent>
536 <div>
566 - <EventTarget>
567 - <span>Child 1</span>
568 - </EventTarget>
537 + <EventTarget />
538 + <span>Child 1</span>
539 </div>
540 </EventComponent>
541 );
@@ -595,9 +565,7 @@ describe('ReactFiberEvents', () => {
565 expect(() => {
566 root.update(<Test />);
567 expect(Scheduler).toFlushWithoutYielding();
598 - }).toWarnDev(
599 - 'Warning: validateDOMNesting: React event targets must not have event components as children.',
600 - );
568 + }).toWarnDev('Warning: Event targets should not have children.');
569 });
570
571 it('should handle event components correctly with error boundaries', () => {
@@ -607,11 +575,9 @@ describe('ReactFiberEvents', () => {
575
576 const Test = () => (
577 <EventComponent>
610 - <EventTarget>
611 - <span>
612 - <ErrorComponent />
613 - </span>
614 - </EventTarget>
578 + <span>
579 + <ErrorComponent />
580 + </span>
581 </EventComponent>
582 );
583
@@ -620,7 +586,7 @@ describe('ReactFiberEvents', () => {
586 error: null,
587 };
588
623 - componentDidCatch(error, errStack) {
589 + componentDidCatch(error) {
590 this.setState({
591 error,
592 });
@@ -657,11 +623,9 @@ describe('ReactFiberEvents', () => {
623
624 const Parent = () => (
625 <EventComponent>
660 - <EventTarget>
661 - <div>
662 - <Child />
663 - </div>
664 - </EventTarget>
626 + <div>
627 + <Child />
628 + </div>
629 </EventComponent>
630 );
631
@@ -710,9 +674,7 @@ describe('ReactFiberEvents', () => {
674
675 const Parent = () => (
676 <EventComponent>
713 - <EventTarget>
714 - <Child />
715 - </EventTarget>
677 + <Child />
678 </EventComponent>
679 );
680
@@ -730,7 +692,7 @@ describe('ReactFiberEvents', () => {
692 _updateCounter(counter => counter + 1);
693 });
694 }).toWarnDev(
733 - 'Warning: validateDOMNesting: React event targets cannot have text DOM nodes as children. ' +
695 + 'Warning: validateDOMNesting: React event components cannot have text DOM nodes as children. ' +
696 'Wrap the child text "Text!" in an element.',
697 );
698 });
@@ -744,11 +706,7 @@ describe('ReactFiberEvents', () => {
706 _updateCounter = updateCounter;
707
708 if (counter === 1) {
747 - return (
748 - <EventComponent>
749 - <div>Child</div>
750 - </EventComponent>
751 - );
709 + return <EventTarget>123</EventTarget>;
710 }
711
712 return (
@@ -759,11 +717,11 @@ describe('ReactFiberEvents', () => {
717 }
718
719 const Parent = () => (
762 - <EventComponent>
763 - <EventTarget>
720 + <div>
721 + <EventComponent>
722 <Child />
765 - </EventTarget>
766 - </EventComponent>
723 + </EventComponent>
724 + </div>
725 );
726
727 const root = ReactTestRenderer.create(null);
@@ -771,7 +729,9 @@ describe('ReactFiberEvents', () => {
729 expect(Scheduler).toFlushWithoutYielding();
730 expect(root).toMatchRenderedOutput(
731 <div>
774 - <span>Child - 0</span>
732 + <div>
733 + <span>Child - 0</span>
734 + </div>
735 </div>,
736 );
737
@@ -780,9 +740,7 @@ describe('ReactFiberEvents', () => {
740 _updateCounter(counter => counter + 1);
741 });
742 expect(Scheduler).toFlushWithoutYielding();
783 - }).toWarnDev(
784 - 'Warning: validateDOMNesting: React event targets must not have event components as children.',
785 - );
743 + }).toWarnDev('Warning: Event targets should not have children.');
744 });
745
746 it('should error with a component stack contains the names of the event components and event targets', () => {
@@ -794,11 +752,9 @@ describe('ReactFiberEvents', () => {
752
753 const Test = () => (
754 <EventComponent>
797 - <EventTarget>
798 - <span>
799 - <ErrorComponent />
800 - </span>
801 - </EventTarget>
755 + <span>
756 + <ErrorComponent />
757 + </span>
758 </EventComponent>
759 );
760
@@ -828,7 +784,6 @@ describe('ReactFiberEvents', () => {
784
785 expect(componentStackMessage.includes('ErrorComponent')).toBe(true);
786 expect(componentStackMessage.includes('span')).toBe(true);
831 - expect(componentStackMessage.includes('TestEventTarget')).toBe(true);
787 expect(componentStackMessage.includes('TestEventComponent')).toBe(true);
788 expect(componentStackMessage.includes('Test')).toBe(true);
789 expect(componentStackMessage.includes('Wrapper')).toBe(true);
@@ -888,9 +843,9 @@ describe('ReactFiberEvents', () => {
843 it('should render a simple event component with a single event target', () => {
844 const Test = () => (
845 <EventComponent>
891 - <EventTarget>
892 - <div>Hello world</div>
893 - </EventTarget>
846 + <div>
847 + Hello world<EventTarget />
848 + </div>
849 </EventComponent>
850 );
851
@@ -901,9 +856,8 @@ describe('ReactFiberEvents', () => {
856
857 const Test2 = () => (
858 <EventComponent>
904 - <EventTarget>
905 - <span>I am now a span</span>
906 - </EventTarget>
859 + <EventTarget />
860 + <span>I am now a span</span>
861 </EventComponent>
862 );
863
@@ -923,10 +877,7 @@ describe('ReactFiberEvents', () => {
877 const container = document.createElement('div');
878 ReactDOM.render(<Test />, container);
879 expect(Scheduler).toFlushWithoutYielding();
926 - }).toWarnDev(
927 - 'Warning: validateDOMNesting: React event targets cannot have text DOM nodes as children. ' +
928 - 'Wrap the child text "Hello world" in an element.',
929 - );
880 + }).toWarnDev('Warning: Event targets should not have children.');
881 });
882
883 it('should warn when an event target has a direct text child #2', () => {
@@ -943,19 +894,15 @@ describe('ReactFiberEvents', () => {
894 const container = document.createElement('div');
895 ReactDOM.render(<Test />, container);
896 expect(Scheduler).toFlushWithoutYielding();
946 - }).toWarnDev(
947 - 'Warning: validateDOMNesting: React event targets cannot have text DOM nodes as children. ' +
948 - 'Wrap the child text "Hello world" in an element.',
949 - );
897 + }).toWarnDev('Warning: Event targets should not have children.');
898 });
899
900 it('should not warn if an event target is not a direct child of an event component', () => {
901 const Test = () => (
902 <EventComponent>
903 <div>
956 - <EventTarget>
957 - <span>Child 1</span>
958 - </EventTarget>
904 + <EventTarget />
905 + <span>Child 1</span>
906 </div>
907 </EventComponent>
908 );
@@ -981,9 +928,7 @@ describe('ReactFiberEvents', () => {
928 const container = document.createElement('div');
929 ReactDOM.render(<Test />, container);
930 expect(Scheduler).toFlushWithoutYielding();
984 - }).toWarnDev(
985 - 'Warning: validateDOMNesting: React event targets must not have event components as children.',
986 - );
931 + }).toWarnDev('Warning: Event targets should not have children.');
932 });
933
934 it('should handle event components correctly with error boundaries', () => {
@@ -993,11 +938,9 @@ describe('ReactFiberEvents', () => {
938
939 const Test = () => (
940 <EventComponent>
996 - <EventTarget>
997 - <span>
998 - <ErrorComponent />
999 - </span>
1000 - </EventTarget>
941 + <span>
942 + <ErrorComponent />
943 + </span>
944 </EventComponent>
945 );
946
@@ -1043,11 +986,9 @@ describe('ReactFiberEvents', () => {
986
987 const Parent = () => (
988 <EventComponent>
1046 - <EventTarget>
1047 - <div>
1048 - <Child />
1049 - </div>
1050 - </EventTarget>
989 + <div>
990 + <Child />
991 + </div>
992 </EventComponent>
993 );
994
@@ -1087,9 +1028,7 @@ describe('ReactFiberEvents', () => {
1028
1029 const Parent = () => (
1030 <EventComponent>
1090 - <EventTarget>
1091 - <Child />
1092 - </EventTarget>
1031 + <Child />
1032 </EventComponent>
1033 );
1034
@@ -1103,7 +1042,7 @@ describe('ReactFiberEvents', () => {
1042 });
1043 expect(Scheduler).toFlushWithoutYielding();
1044 }).toWarnDev(
1106 - 'Warning: validateDOMNesting: React event targets cannot have text DOM nodes as children. ' +
1045 + 'Warning: validateDOMNesting: React event components cannot have text DOM nodes as children. ' +
1046 'Wrap the child text "Text!" in an element.',
1047 );
1048 });
@@ -1117,11 +1056,7 @@ describe('ReactFiberEvents', () => {
1056 _updateCounter = updateCounter;
1057
1058 if (counter === 1) {
1120 - return (
1121 - <EventComponent>
1122 - <div>Child</div>
1123 - </EventComponent>
1124 - );
1059 + return <EventTarget>123</EventTarget>;
1060 }
1061
1062 return (
@@ -1132,25 +1067,25 @@ describe('ReactFiberEvents', () => {
1067 }
1068
1069 const Parent = () => (
1135 - <EventComponent>
1136 - <EventTarget>
1070 + <div>
1071 + <EventComponent>
1072 <Child />
1138 - </EventTarget>
1139 - </EventComponent>
1073 + </EventComponent>
1074 + </div>
1075 );
1076
1077 const container = document.createElement('div');
1078 ReactDOM.render(<Parent />, container);
1144 - expect(container.innerHTML).toBe('<div><span>Child - 0</span></div>');
1079 + expect(container.innerHTML).toBe(
1080 + '<div><div><span>Child - 0</span></div></div>',
1081 + );
1082
1083 expect(() => {
1084 ReactTestUtils.act(() => {
1085 _updateCounter(counter => counter + 1);
1086 });
1087 expect(Scheduler).toFlushWithoutYielding();
1151 - }).toWarnDev(
1152 - 'Warning: validateDOMNesting: React event targets must not have event components as children.',
1153 - );
1088 + }).toWarnDev('Warning: Event targets should not have children.');
1089 });
1090
1091 it('should error with a component stack contains the names of the event components and event targets', () => {
@@ -1162,11 +1097,9 @@ describe('ReactFiberEvents', () => {
1097
1098 const Test = () => (
1099 <EventComponent>
1165 - <EventTarget>
1166 - <span>
1167 - <ErrorComponent />
1168 - </span>
1169 - </EventTarget>
1100 + <span>
1101 + <ErrorComponent />
1102 + </span>
1103 </EventComponent>
1104 );
1105
@@ -1195,7 +1128,6 @@ describe('ReactFiberEvents', () => {
1128
1129 expect(componentStackMessage.includes('ErrorComponent')).toBe(true);
1130 expect(componentStackMessage.includes('span')).toBe(true);
1198 - expect(componentStackMessage.includes('TestEventTarget')).toBe(true);
1131 expect(componentStackMessage.includes('TestEventComponent')).toBe(true);
1132 expect(componentStackMessage.includes('Test')).toBe(true);
1133 expect(componentStackMessage.includes('Wrapper')).toBe(true);
@@ -1222,9 +1154,9 @@ describe('ReactFiberEvents', () => {
1154 it('should render a simple event component with a single event target', () => {
1155 const Test = () => (
1156 <EventComponent>
1225 - <EventTarget>
1226 - <div>Hello world</div>
1227 - </EventTarget>
1157 + <div>
1158 + Hello world<EventTarget />
1159 + </div>
1160 </EventComponent>
1161 );
1162
packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
+5
@@ -65,6 +65,8 @@ export const supportsPersistence = $$$hostConfig.supportsPersistence;
65 export const supportsHydration = $$$hostConfig.supportsHydration;
66 export const handleEventComponent = $$$hostConfig.handleEventComponent;
67 export const handleEventTarget = $$$hostConfig.handleEventTarget;
68 +export const getEventTargetChildElement =
69 + $$$hostConfig.getEventTargetChildElement;
70
71 // -------------------
72 // Mutation
@@ -84,6 +86,9 @@ export const hideInstance = $$$hostConfig.hideInstance;
86 export const hideTextInstance = $$$hostConfig.hideTextInstance;
87 export const unhideInstance = $$$hostConfig.unhideInstance;
88 export const unhideTextInstance = $$$hostConfig.unhideTextInstance;
89 +export const commitTouchHitTargetUpdate =
90 + $$$hostConfig.commitTouchHitTargetUpdate;
91 +export const commitEventTarget = $$$hostConfig.commitEventTarget;
92
93 // -------------------
94 // Persistence
packages/react-test-renderer/src/ReactTestHostConfig.js
+63 -16
@@ -14,6 +14,18 @@ import {REACT_EVENT_TARGET_TOUCH_HIT} from 'shared/ReactSymbols';
14
15 import {enableEventAPI} from 'shared/ReactFeatureFlags';
16
17 +type EventTargetChildElement = {
18 + type: string,
19 + props: null | {
20 + style?: {
21 + position?: string,
22 + bottom?: string,
23 + left?: string,
24 + right?: string,
25 + top?: string,
26 + },
27 + },
28 +};
29 export type Type = string;
30 export type Props = Object;
31 export type Container = {|
@@ -170,12 +182,6 @@ export function createInstance(
182 hostContext: Object,
183 internalInstanceHandle: Object,
184 ): Instance {
173 - if (__DEV__ && enableEventAPI) {
174 - warning(
175 - hostContext !== EVENT_TOUCH_HIT_TARGET_CONTEXT,
176 - 'validateDOMNesting: <TouchHitTarget> must not have any children.',
177 - );
178 - }
185 return {
186 type,
187 props,
@@ -233,10 +239,6 @@ export function createTextInstance(
239 internalInstanceHandle: Object,
240 ): TextInstance {
241 if (__DEV__ && enableEventAPI) {
236 - warning(
237 - hostContext !== EVENT_TOUCH_HIT_TARGET_CONTEXT,
238 - 'validateDOMNesting: <TouchHitTarget> must not have any children.',
239 - );
242 warning(
243 hostContext !== EVENT_COMPONENT_CONTEXT,
244 'validateDOMNesting: React event components cannot have text DOM nodes as children. ' +
@@ -329,17 +331,62 @@ export function handleEventComponent(
331 eventResponder: ReactEventResponder,
332 rootContainerInstance: Container,
333 internalInstanceHandle: Object,
332 -) {
333 - // TODO: add handleEventComponent implementation
334 +): void {
335 + // noop
336 +}
337 +
338 +export function getEventTargetChildElement(
339 + type: Symbol | number,
340 + props: Props,
341 +): null | EventTargetChildElement {
342 + if (enableEventAPI) {
343 + if (type === REACT_EVENT_TARGET_TOUCH_HIT) {
344 + const {bottom, left, right, top} = props;
345 +
346 + if (!bottom && !left && !right && !top) {
347 + return null;
348 + }
349 + return {
350 + type: 'div',
351 + props: {
352 + style: {
353 + position: 'absolute',
354 + zIndex: -1,
355 + bottom: bottom ? `-${bottom}px` : '0px',
356 + left: left ? `-${left}px` : '0px',
357 + right: right ? `-${right}px` : '0px',
358 + top: top ? `-${top}px` : '0px',
359 + },
360 + },
361 + };
362 + }
363 + }
364 + return null;
365 }
366
367 export function handleEventTarget(
368 type: Symbol | number,
369 props: Props,
339 - parentInstance: Container,
370 + rootContainerInstance: Container,
371 internalInstanceHandle: Object,
341 -) {
342 - if (type === REACT_EVENT_TARGET_TOUCH_HIT) {
343 - // TODO
372 +): boolean {
373 + if (enableEventAPI) {
374 + if (type === REACT_EVENT_TARGET_TOUCH_HIT) {
375 + // In DEV we do a computed style check on the position to ensure
376 + // the parent host component is correctly position in the document.
377 + if (__DEV__) {
378 + return true;
379 + }
380 + }
381 }
382 + return false;
383 +}
384 +
385 +export function commitEventTarget(
386 + type: Symbol | number,
387 + props: Props,
388 + instance: Instance,
389 + parentInstance: Instance,
390 +): void {
391 + // noop
392 }
packages/shared/HostConfigWithNoHydration.js
+2
@@ -47,3 +47,5 @@ export const didNotFindHydratableContainerSuspenseInstance = shim;
47 export const didNotFindHydratableInstance = shim;
48 export const didNotFindHydratableTextInstance = shim;
49 export const didNotFindHydratableSuspenseInstance = shim;
50 +export const canHydrateTouchHitTargetInstance = shim;
51 +export const hydrateTouchHitTargetInstance = shim;
packages/shared/HostConfigWithNoPersistence.js
+1
@@ -30,3 +30,4 @@ export const finalizeContainerChildren = shim;
30 export const replaceContainerChildren = shim;
31 export const cloneHiddenInstance = shim;
32 export const cloneHiddenTextInstance = shim;
33 +export const cloneHiddenTouchHitTargetInstance = shim;