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;