From 4fbbae8afa62b61c84cf1ca0cd7a1f5a413f59df Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Sat, 6 Apr 2019 07:51:21 +0100 Subject: [PATCH] Add full TouchHitTarget hit slop (experimental event API) to ReactDOM (#15308) --- packages/events/EventTypes.js | 6 +- packages/react-art/src/ReactARTHostConfig.js | 26 +- .../src/client/ReactDOMHostConfig.js | 134 +++++-- .../src/events/DOMEventResponderSystem.js | 31 +- .../src/server/ReactPartialRenderer.js | 24 ++ packages/react-events/src/Hover.js | 5 +- packages/react-events/src/Press.js | 1 + .../__tests__/TouchHitTarget-test.internal.js | 344 +++++++++++++++++- .../src/ReactFabricHostConfig.js | 26 +- .../src/ReactNativeHostConfig.js | 24 +- .../src/createReactNoop.js | 84 +++-- .../src/ReactFiberBeginWork.js | 33 +- .../src/ReactFiberCommitWork.js | 45 ++- .../src/ReactFiberCompleteWork.js | 21 +- .../ReactFiberEvents-test-internal.js | 252 +++++-------- .../src/forks/ReactFiberHostConfig.custom.js | 5 + .../src/ReactTestHostConfig.js | 79 +++- packages/shared/HostConfigWithNoHydration.js | 2 + .../shared/HostConfigWithNoPersistence.js | 1 + 19 files changed, 848 insertions(+), 295 deletions(-) diff --git a/packages/events/EventTypes.js b/packages/events/EventTypes.js index ab5c51db640a0..6f1f5e3b4a631 100644 --- a/packages/events/EventTypes.js +++ b/packages/events/EventTypes.js @@ -34,7 +34,11 @@ export type ResponderContext = { parentTarget: Element | Document, ) => boolean, isTargetWithinEventComponent: (Element | Document) => boolean, - isPositionWithinTouchHitTarget: (x: number, y: number) => boolean, + isPositionWithinTouchHitTarget: ( + doc: Document, + x: number, + y: number, + ) => boolean, addRootEventTypes: ( document: Document, rootEventTypes: Array, diff --git a/packages/react-art/src/ReactARTHostConfig.js b/packages/react-art/src/ReactARTHostConfig.js index 347693416591f..ef5b5914bfd34 100644 --- a/packages/react-art/src/ReactARTHostConfig.js +++ b/packages/react-art/src/ReactARTHostConfig.js @@ -443,15 +443,31 @@ export function handleEventComponent( eventResponder: ReactEventResponder, rootContainerInstance: Container, internalInstanceHandle: Object, -) { - // TODO: add handleEventComponent implementation +): void { + throw new Error('Not yet implemented.'); +} + +export function getEventTargetChildElement( + type: Symbol | number, + props: Props, +): null { + throw new Error('Not yet implemented.'); } export function handleEventTarget( type: Symbol | number, props: Props, - parentInstance: Container, + rootContainerInstance: Container, internalInstanceHandle: Object, -) { - // TODO: add handleEventTarget implementation +): boolean { + throw new Error('Not yet implemented.'); +} + +export function commitEventTarget( + type: Symbol | number, + props: Props, + instance: Instance, + parentInstance: Instance, +): void { + throw new Error('Not yet implemented.'); } diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js index ef96ee8f944bc..27b00f7da913b 100644 --- a/packages/react-dom/src/client/ReactDOMHostConfig.js +++ b/packages/react-dom/src/client/ReactDOMHostConfig.js @@ -33,7 +33,7 @@ import { isEnabled as ReactBrowserEventEmitterIsEnabled, setEnabled as ReactBrowserEventEmitterSetEnabled, } from '../events/ReactBrowserEventEmitter'; -import {getChildNamespace} from '../shared/DOMNamespaces'; +import {Namespaces, getChildNamespace} from '../shared/DOMNamespaces'; import { ELEMENT_NODE, TEXT_NODE, @@ -46,6 +46,7 @@ import dangerousStyleValue from '../shared/dangerousStyleValue'; import type {DOMContainer} from './ReactDOM'; import type {ReactEventResponder} from 'shared/ReactTypes'; import {REACT_EVENT_TARGET_TOUCH_HIT} from 'shared/ReactSymbols'; +import {canUseDOM} from 'shared/ExecutionEnvironment'; export type Type = string; export type Props = { @@ -57,6 +58,23 @@ export type Props = { style?: { display?: string, }, + bottom?: null | number, + left?: null | number, + right?: null | number, + top?: null | number, +}; +export type EventTargetChildElement = { + type: string, + props: null | { + style?: { + position?: string, + zIndex?: number, + bottom?: string, + left?: string, + right?: string, + top?: string, + }, + }, }; export type Container = Element | Document; export type Instance = Element; @@ -70,7 +88,6 @@ type HostContextDev = { eventData: null | {| isEventComponent?: boolean, isEventTarget?: boolean, - eventTargetType?: null | Symbol | number, |}, }; type HostContextProd = string; @@ -86,6 +103,8 @@ import { } from 'shared/ReactFeatureFlags'; import warning from 'shared/warning'; +const {html: HTML_NAMESPACE} = Namespaces; + // Intentionally not named imports because Rollup would // use dynamic dispatch for CommonJS interop named imports. const { @@ -190,7 +209,6 @@ export function getChildHostContextForEventComponent( const eventData = { isEventComponent: true, isEventTarget: false, - eventTargetType: null, }; return {namespace, ancestorInfo, eventData}; } @@ -204,17 +222,24 @@ export function getChildHostContextForEventTarget( if (__DEV__) { const parentHostContextDev = ((parentHostContext: any): HostContextDev); const {namespace, ancestorInfo} = parentHostContextDev; - warning( - parentHostContextDev.eventData === null || - !parentHostContextDev.eventData.isEventComponent || - type !== REACT_EVENT_TARGET_TOUCH_HIT, - 'validateDOMNesting: cannot not be a direct child of an event component. ' + - 'Ensure is a direct child of a DOM element.', - ); + if (type === REACT_EVENT_TARGET_TOUCH_HIT) { + warning( + parentHostContextDev.eventData === null || + !parentHostContextDev.eventData.isEventComponent, + 'validateDOMNesting: cannot not be a direct child of an event component. ' + + 'Ensure is a direct child of a DOM element.', + ); + const parentNamespace = parentHostContextDev.namespace; + if (parentNamespace !== HTML_NAMESPACE) { + throw new Error( + ' was used in an unsupported DOM namespace. ' + + 'Ensure the is used in an HTML namespace.', + ); + } + } const eventData = { isEventComponent: false, isEventTarget: true, - eventTargetType: type, }; return {namespace, ancestorInfo, eventData}; } @@ -249,16 +274,6 @@ export function createInstance( if (__DEV__) { // TODO: take namespace into account when validating. const hostContextDev = ((hostContext: any): HostContextDev); - if (enableEventAPI) { - const eventData = hostContextDev.eventData; - if (eventData !== null) { - warning( - !eventData.isEventTarget || - eventData.eventTargetType !== REACT_EVENT_TARGET_TOUCH_HIT, - 'Warning: validateDOMNesting: must not have any children.', - ); - } - } validateDOMNesting(type, null, hostContextDev.ancestorInfo); if ( typeof props.children === 'string' || @@ -365,25 +380,12 @@ export function createTextInstance( if (enableEventAPI) { const eventData = hostContextDev.eventData; if (eventData !== null) { - warning( - eventData === null || - !eventData.isEventTarget || - eventData.eventTargetType !== REACT_EVENT_TARGET_TOUCH_HIT, - 'Warning: validateDOMNesting: must not have any children.', - ); warning( !eventData.isEventComponent, 'validateDOMNesting: React event components cannot have text DOM nodes as children. ' + 'Wrap the child text "%s" in an element.', text, ); - warning( - !eventData.isEventTarget || - eventData.eventTargetType === REACT_EVENT_TARGET_TOUCH_HIT, - 'validateDOMNesting: React event targets cannot have text DOM nodes as children. ' + - 'Wrap the child text "%s" in an element.', - text, - ); } } } @@ -899,16 +901,74 @@ export function handleEventComponent( } } +export function getEventTargetChildElement( + type: Symbol | number, + props: Props, +): null | EventTargetChildElement { + if (enableEventAPI) { + if (type === REACT_EVENT_TARGET_TOUCH_HIT) { + const {bottom, left, right, top} = props; + + if (!bottom && !left && !right && !top) { + return null; + } + return { + type: 'div', + props: { + style: { + position: 'absolute', + zIndex: -1, + bottom: bottom ? `-${bottom}px` : '0px', + left: left ? `-${left}px` : '0px', + right: right ? `-${right}px` : '0px', + top: top ? `-${top}px` : '0px', + }, + }, + }; + } + } + return null; +} + export function handleEventTarget( type: Symbol | number, props: Props, - parentInstance: Container, + rootContainerInstance: Container, internalInstanceHandle: Object, +): boolean { + return false; +} + +export function commitEventTarget( + type: Symbol | number, + props: Props, + instance: Instance, + parentInstance: Instance, ): void { if (enableEventAPI) { - // Touch target hit slop handling if (type === REACT_EVENT_TARGET_TOUCH_HIT) { - // TODO + if (__DEV__ && canUseDOM) { + // This is done at DEV time because getComputedStyle will + // typically force a style recalculation and force a layout, + // reflow -– both of which are sync are expensive. + const computedStyles = window.getComputedStyle(parentInstance); + const position = computedStyles.getPropertyValue('position'); + warning( + position !== '' && position !== 'static', + ' inserts an empty absolutely positioned
. ' + + 'This requires its parent DOM node to be positioned too, but the ' + + 'parent DOM node was found to have the style "position" set to ' + + 'either no value, or a value of "static". Try using a "position" ' + + 'value of "relative".', + ); + warning( + computedStyles.getPropertyValue('zIndex') !== '', + ' inserts an empty
with "z-index" of "-1". ' + + 'This requires its parent DOM node to have a "z-index" great than "-1",' + + 'but the parent DOM node was found to no "z-index" value set.' + + ' Try using a "z-index" value of "0" or greater.', + ); + } } } } diff --git a/packages/react-dom/src/events/DOMEventResponderSystem.js b/packages/react-dom/src/events/DOMEventResponderSystem.js index 7630d8d7b427f..5f12b84a1c5dc 100644 --- a/packages/react-dom/src/events/DOMEventResponderSystem.js +++ b/packages/react-dom/src/events/DOMEventResponderSystem.js @@ -17,7 +17,10 @@ import { PASSIVE_NOT_SUPPORTED, } from 'events/EventSystemFlags'; import type {AnyNativeEvent} from 'events/PluginModuleType'; -import {EventComponent} from 'shared/ReactWorkTags'; +import { + EventComponent, + EventTarget as EventTargetWorkTag, +} from 'shared/ReactWorkTags'; import type { ReactEventResponder, ReactEventResponderEventType, @@ -110,7 +113,31 @@ const eventResponderContext: ResponderContext = { eventsWithStopPropagation.add(eventObject); } }, - isPositionWithinTouchHitTarget(x: number, y: number): boolean { + isPositionWithinTouchHitTarget(doc: Document, x: number, y: number): boolean { + // This isn't available in some environments (JSDOM) + if (typeof doc.elementFromPoint !== 'function') { + return false; + } + const target = doc.elementFromPoint(x, y); + if (target === null) { + return false; + } + const childFiber = getClosestInstanceFromNode(target); + if (childFiber === null) { + return false; + } + const parentFiber = childFiber.return; + if (parentFiber !== null && parentFiber.tag === EventTargetWorkTag) { + const parentNode = ((target.parentNode: any): Element); + // TODO find another way to do this without using the + // expensive getBoundingClientRect. + const {left, top, right, bottom} = parentNode.getBoundingClientRect(); + // Check if the co-ords intersect with the target element's rect. + if (x > left && y > top && x < right && y < bottom) { + return false; + } + return true; + } return false; }, isTargetWithinEventComponent(target: Element | Document): boolean { diff --git a/packages/react-dom/src/server/ReactPartialRenderer.js b/packages/react-dom/src/server/ReactPartialRenderer.js index 7eecd8dd41b03..c493c0398ce5a 100644 --- a/packages/react-dom/src/server/ReactPartialRenderer.js +++ b/packages/react-dom/src/server/ReactPartialRenderer.js @@ -39,6 +39,7 @@ import { REACT_MEMO_TYPE, REACT_EVENT_COMPONENT_TYPE, REACT_EVENT_TARGET_TYPE, + REACT_EVENT_TARGET_TOUCH_HIT, } from 'shared/ReactSymbols'; import { @@ -1168,6 +1169,29 @@ class ReactDOMServerRenderer { case REACT_EVENT_COMPONENT_TYPE: case REACT_EVENT_TARGET_TYPE: { if (enableEventAPI) { + if ( + elementType.$$typeof === REACT_EVENT_TARGET_TYPE && + elementType.type === REACT_EVENT_TARGET_TOUCH_HIT + ) { + const props = nextElement.props; + const bottom = props.bottom || 0; + const left = props.left || 0; + const right = props.right || 0; + const top = props.top || 0; + + if (bottom === 0 && left === 0 && right === 0 && top === 0) { + return ''; + } + let topString = top ? `-${top}px` : '0px'; + let leftString = left ? `-${left}px` : '0px'; + let rightString = right ? `-${right}px` : '0x'; + let bottomString = bottom ? `-${bottom}px` : '0px'; + + return ( + `
` + ); + } const nextChildren = toArray( ((nextChild: any): ReactElement).props.children, ); diff --git a/packages/react-events/src/Hover.js b/packages/react-events/src/Hover.js index 2addb3185e519..0668880268476 100644 --- a/packages/react-events/src/Hover.js +++ b/packages/react-events/src/Hover.js @@ -196,7 +196,7 @@ const HoverResponder = { props: HoverProps, state: HoverState, ): void { - const {type, nativeEvent} = event; + const {type, target, nativeEvent} = event; switch (type) { /** @@ -218,6 +218,7 @@ const HoverResponder = { } if ( context.isPositionWithinTouchHitTarget( + target.ownerDocument, (nativeEvent: any).x, (nativeEvent: any).y, ) @@ -244,6 +245,7 @@ const HoverResponder = { if (state.isInHitSlop) { if ( !context.isPositionWithinTouchHitTarget( + target.ownerDocument, (nativeEvent: any).x, (nativeEvent: any).y, ) @@ -254,6 +256,7 @@ const HoverResponder = { } else if ( state.isHovered && context.isPositionWithinTouchHitTarget( + target.ownerDocument, (nativeEvent: any).x, (nativeEvent: any).y, ) diff --git a/packages/react-events/src/Press.js b/packages/react-events/src/Press.js index ab2750d6271b8..2615826b16c45 100644 --- a/packages/react-events/src/Press.js +++ b/packages/react-events/src/Press.js @@ -259,6 +259,7 @@ const PressResponder = { nativeEvent.button === 2 || // Ignore pressing on hit slop area with mouse context.isPositionWithinTouchHitTarget( + target.ownerDocument, (nativeEvent: any).x, (nativeEvent: any).y, ) diff --git a/packages/react-events/src/__tests__/TouchHitTarget-test.internal.js b/packages/react-events/src/__tests__/TouchHitTarget-test.internal.js index d30a6c92c2735..e0bb517518be3 100644 --- a/packages/react-events/src/__tests__/TouchHitTarget-test.internal.js +++ b/packages/react-events/src/__tests__/TouchHitTarget-test.internal.js @@ -16,6 +16,7 @@ let ReactFeatureFlags; let EventComponent; let ReactTestRenderer; let ReactDOM; +let ReactDOMServer; let ReactSymbols; let ReactEvents; let TouchHitTarget; @@ -58,6 +59,11 @@ function initReactDOM() { ReactDOM = require('react-dom'); } +function initReactDOMServer() { + init(); + ReactDOMServer = require('react-dom/server'); +} + describe('TouchHitTarget', () => { describe('NoopRenderer', () => { beforeEach(() => { @@ -94,9 +100,7 @@ describe('TouchHitTarget', () => { expect(() => { ReactNoop.render(); expect(Scheduler).toFlushWithoutYielding(); - }).toWarnDev( - 'Warning: validateDOMNesting: must not have any children.', - ); + }).toWarnDev('Warning: Event targets should not have children.'); const Test2 = () => ( @@ -109,9 +113,7 @@ describe('TouchHitTarget', () => { expect(() => { ReactNoop.render(); expect(Scheduler).toFlushWithoutYielding(); - }).toWarnDev( - 'Warning: validateDOMNesting: must not have any children.', - ); + }).toWarnDev('Warning: Event targets should not have children.'); // Should render without warnings const Test3 = () => ( @@ -181,9 +183,7 @@ describe('TouchHitTarget', () => { expect(() => { root.update(); expect(Scheduler).toFlushWithoutYielding(); - }).toWarnDev( - 'Warning: validateDOMNesting: must not have any children.', - ); + }).toWarnDev('Warning: Event targets should not have children.'); const Test2 = () => ( @@ -196,9 +196,7 @@ describe('TouchHitTarget', () => { expect(() => { root.update(); expect(Scheduler).toFlushWithoutYielding(); - }).toWarnDev( - 'Warning: validateDOMNesting: must not have any children.', - ); + }).toWarnDev('Warning: Event targets should not have children.'); // Should render without warnings const Test3 = () => ( @@ -269,9 +267,7 @@ describe('TouchHitTarget', () => { expect(() => { ReactDOM.render(, container); expect(Scheduler).toFlushWithoutYielding(); - }).toWarnDev( - 'Warning: validateDOMNesting: must not have any children.', - ); + }).toWarnDev('Warning: Event targets should not have children.'); const Test2 = () => ( @@ -284,9 +280,7 @@ describe('TouchHitTarget', () => { expect(() => { ReactDOM.render(, container); expect(Scheduler).toFlushWithoutYielding(); - }).toWarnDev( - 'Warning: validateDOMNesting: must not have any children.', - ); + }).toWarnDev('Warning: Event targets should not have children.'); // Should render without warnings const Test3 = () => ( @@ -318,5 +312,319 @@ describe('TouchHitTarget', () => { 'Ensure is a direct child of a DOM element.', ); }); + + it('should render a conditional TouchHitTarget correctly (false -> true)', () => { + let cond = false; + + const Test = () => ( + +
+ {cond ? null : ( + + )} +
+
+ ); + + const container = document.createElement('div'); + ReactDOM.render(, container); + expect(Scheduler).toFlushWithoutYielding(); + expect(container.innerHTML).toBe( + '
', + ); + + cond = true; + ReactDOM.render(, container); + expect(Scheduler).toFlushWithoutYielding(); + expect(container.innerHTML).toBe('
'); + }); + + it('should render a conditional TouchHitTarget correctly (true -> false)', () => { + let cond = true; + + const Test = () => ( + +
+ {cond ? null : ( + + )} +
+
+ ); + + const container = document.createElement('div'); + ReactDOM.render(, container); + expect(Scheduler).toFlushWithoutYielding(); + expect(container.innerHTML).toBe('
'); + + cond = false; + ReactDOM.render(, container); + expect(Scheduler).toFlushWithoutYielding(); + expect(container.innerHTML).toBe( + '
', + ); + }); + + it('should render a conditional TouchHitTarget hit slop correctly (false -> true)', () => { + let cond = false; + + const Test = () => ( + +
+ {cond ? ( + + ) : ( + + )} +
+
+ ); + + const container = document.createElement('div'); + ReactDOM.render(, container); + expect(Scheduler).toFlushWithoutYielding(); + expect(container.innerHTML).toBe( + '
', + ); + + cond = true; + ReactDOM.render(, container); + expect(Scheduler).toFlushWithoutYielding(); + expect(container.innerHTML).toBe('
'); + }); + + it('should render a conditional TouchHitTarget hit slop correctly (true -> false)', () => { + let cond = true; + + const Test = () => ( + +
+ Random span 1 + {cond ? ( + + ) : ( + + )} + Random span 2 +
+
+ ); + + const container = document.createElement('div'); + ReactDOM.render(, container); + expect(Scheduler).toFlushWithoutYielding(); + expect(container.innerHTML).toBe( + '
Random span 1Random span 2
', + ); + + cond = false; + ReactDOM.render(, container); + expect(Scheduler).toFlushWithoutYielding(); + expect(container.innerHTML).toBe( + '
Random span 1
Random span 2
', + ); + }); + + it('should update TouchHitTarget hit slop values correctly (false -> true)', () => { + let cond = false; + + const Test = () => ( + +
+ Random span 1 + {cond ? ( + + ) : ( + + )} + Random span 2 +
+
+ ); + + const container = document.createElement('div'); + ReactDOM.render(, container); + expect(Scheduler).toFlushWithoutYielding(); + expect(container.innerHTML).toBe( + '
Random span 1
Random span 2
', + ); + + cond = true; + ReactDOM.render(, container); + expect(Scheduler).toFlushWithoutYielding(); + expect(container.innerHTML).toBe( + '
Random span 1
Random span 2
', + ); + }); + + it('should update TouchHitTarget hit slop values correctly (true -> false)', () => { + let cond = true; + + const Test = () => ( + +
+ Random span 1 + {cond ? ( + + ) : ( + + )} + Random span 2 +
+
+ ); + + const container = document.createElement('div'); + ReactDOM.render(, container); + expect(Scheduler).toFlushWithoutYielding(); + expect(container.innerHTML).toBe( + '
Random span 1
Random span 2
', + ); + + cond = false; + ReactDOM.render(, container); + expect(Scheduler).toFlushWithoutYielding(); + expect(container.innerHTML).toBe( + '
Random span 1
Random span 2
', + ); + }); + + it('should hydrate TouchHitTarget hit slop elements correcty', () => { + const Test = () => ( + +
+ +
+
+ ); + + const container = document.createElement('div'); + container.innerHTML = '
'; + ReactDOM.hydrate(, container); + expect(Scheduler).toFlushWithoutYielding(); + expect(container.innerHTML).toBe('
'); + + const Test2 = () => ( + +
+ +
+
+ ); + + const container2 = document.createElement('div'); + container2.innerHTML = + '
'; + ReactDOM.hydrate(, container2); + expect(Scheduler).toFlushWithoutYielding(); + expect(container2.innerHTML).toBe( + '
', + ); + }); + + it('should hydrate TouchHitTarget hit slop elements correcty and patch them', () => { + const Test = () => ( + +
+ +
+
+ ); + + const container = document.createElement('div'); + container.innerHTML = '
'; + expect(() => { + ReactDOM.hydrate(, container); + expect(Scheduler).toFlushWithoutYielding(); + }).toWarnDev( + 'Warning: Expected server HTML to contain a matching
in
.', + {withoutStack: true}, + ); + expect(Scheduler).toFlushWithoutYielding(); + expect(container.innerHTML).toBe( + '
', + ); + }); + }); + + describe('ReactDOMServer', () => { + beforeEach(() => { + initReactDOMServer(); + EventComponent = createReactEventComponent(); + TouchHitTarget = ReactEvents.TouchHitTarget; + }); + + it('should not warn when a TouchHitTarget is used correctly', () => { + const Test = () => ( + +
+ +
+
+ ); + + const output = ReactDOMServer.renderToString(); + expect(output).toBe('
'); + }); + + it('should render a TouchHitTarget with hit slop values', () => { + const Test = () => ( + +
+ +
+
+ ); + + let output = ReactDOMServer.renderToString(); + expect(output).toBe( + '
', + ); + + const Test2 = () => ( + +
+ +
+
+ ); + + output = ReactDOMServer.renderToString(); + expect(output).toBe( + '
', + ); + + const Test3 = () => ( + +
+ +
+
+ ); + + output = ReactDOMServer.renderToString(); + expect(output).toBe( + '
', + ); + }); }); }); diff --git a/packages/react-native-renderer/src/ReactFabricHostConfig.js b/packages/react-native-renderer/src/ReactFabricHostConfig.js index c2ccde544cb98..5f55444bff482 100644 --- a/packages/react-native-renderer/src/ReactFabricHostConfig.js +++ b/packages/react-native-renderer/src/ReactFabricHostConfig.js @@ -438,15 +438,31 @@ export function handleEventComponent( eventResponder: ReactEventResponder, rootContainerInstance: Container, internalInstanceHandle: Object, -) { - // TODO: add handleEventComponent implementation +): void { + throw new Error('Not yet implemented.'); +} + +export function getEventTargetChildElement( + type: Symbol | number, + props: Props, +): null { + throw new Error('Not yet implemented.'); } export function handleEventTarget( type: Symbol | number, props: Props, - parentInstance: Container, + rootContainerInstance: Container, internalInstanceHandle: Object, -) { - // TODO: add handleEventTarget implementation +): boolean { + throw new Error('Not yet implemented.'); +} + +export function commitEventTarget( + type: Symbol | number, + props: Props, + instance: Instance, + parentInstance: Instance, +): void { + throw new Error('Not yet implemented.'); } diff --git a/packages/react-native-renderer/src/ReactNativeHostConfig.js b/packages/react-native-renderer/src/ReactNativeHostConfig.js index f4b24a1c39f4b..55fe1ba868b0e 100644 --- a/packages/react-native-renderer/src/ReactNativeHostConfig.js +++ b/packages/react-native-renderer/src/ReactNativeHostConfig.js @@ -498,14 +498,30 @@ export function handleEventComponent( rootContainerInstance: Container, internalInstanceHandle: Object, ) { - // TODO: add handleEventComponent implementation + throw new Error('Not yet implemented.'); +} + +export function getEventTargetChildElement( + type: Symbol | number, + props: Props, +): null { + throw new Error('Not yet implemented.'); } export function handleEventTarget( type: Symbol | number, props: Props, - parentInstance: Container, + rootContainerInstance: Container, internalInstanceHandle: Object, -) { - // TODO: add handleEventTarget implementation +): boolean { + throw new Error('Not yet implemented.'); +} + +export function commitEventTarget( + type: Symbol | number, + props: Props, + instance: Instance, + parentInstance: Instance, +): void { + throw new Error('Not yet implemented.'); } diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index 55198b6ba43ad..de42992a6bb2d 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -33,12 +33,32 @@ import ReactSharedInternals from 'shared/ReactSharedInternals'; import warningWithoutStack from 'shared/warningWithoutStack'; import {enableEventAPI} from 'shared/ReactFeatureFlags'; +type EventTargetChildElement = { + type: string, + props: null | { + style?: { + position?: string, + bottom?: string, + left?: string, + right?: string, + top?: string, + }, + }, +}; type Container = { rootID: string, children: Array, pendingChildren: Array, }; -type Props = {prop: any, hidden: boolean, children?: mixed}; +type Props = { + prop: any, + hidden: boolean, + children?: mixed, + bottom?: null | number, + left?: null | number, + right?: null | number, + top?: null | number, +}; type Instance = {| type: string, id: number, @@ -299,12 +319,6 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { rootContainerInstance: Container, hostContext: HostContext, ): Instance { - if (__DEV__ && enableEventAPI) { - warning( - hostContext !== EVENT_TOUCH_HIT_TARGET_CONTEXT, - 'validateDOMNesting: must not have any children.', - ); - } if (type === 'errorInCompletePhase') { throw new Error('Error in host config.'); } @@ -379,22 +393,12 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { internalInstanceHandle: Object, ): TextInstance { if (__DEV__ && enableEventAPI) { - warning( - hostContext !== EVENT_TOUCH_HIT_TARGET_CONTEXT, - 'validateDOMNesting: must not have any children.', - ); warning( hostContext !== EVENT_COMPONENT_CONTEXT, 'validateDOMNesting: React event components cannot have text DOM nodes as children. ' + 'Wrap the child text "%s" in an element.', text, ); - warning( - hostContext !== EVENT_TARGET_CONTEXT, - 'validateDOMNesting: React event targets cannot have text DOM nodes as children. ' + - 'Wrap the child text "%s" in an element.', - text, - ); } if (hostContext === UPPERCASE_CONTEXT) { text = text.toUpperCase(); @@ -431,15 +435,51 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { // NO-OP }, + getEventTargetChildElement( + type: Symbol | number, + props: Props, + ): null | EventTargetChildElement { + if (enableEventAPI) { + if (type === REACT_EVENT_TARGET_TOUCH_HIT) { + const {bottom, left, right, top} = props; + + if (!bottom && !left && !right && !top) { + return null; + } + return { + type: 'div', + props: { + style: { + position: 'absolute', + zIndex: -1, + bottom: bottom ? `-${bottom}px` : '0px', + left: left ? `-${left}px` : '0px', + right: right ? `-${right}px` : '0px', + top: top ? `-${top}px` : '0px', + }, + }, + }; + } + } + return null; + }, + handleEventTarget( type: Symbol | number, props: Props, - parentInstance: Container, + rootContainerInstance: Container, internalInstanceHandle: Object, - ) { - if (type === REACT_EVENT_TARGET_TOUCH_HIT) { - // TODO - } + ): boolean { + return false; + }, + + commitEventTarget( + type: Symbol | number, + props: Props, + instance: Instance, + parentInstance: Instance, + ): void { + // NO-OP }, }; diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 2832021950f69..3c66c38e229d8 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -96,6 +96,7 @@ import { registerSuspenseInstanceRetry, } from './ReactFiberHostConfig'; import type {SuspenseInstance} from './ReactFiberHostConfig'; +import {getEventTargetChildElement} from './ReactFiberHostConfig'; import {shouldSuspend} from './ReactFiberReconciler'; import { pushHostContext, @@ -1988,15 +1989,33 @@ function updateEventComponent(current, workInProgress, renderExpirationTime) { } function updateEventTarget(current, workInProgress, renderExpirationTime) { + const type = workInProgress.type.type; const nextProps = workInProgress.pendingProps; - let nextChildren = nextProps.children; + const eventTargetChild = getEventTargetChildElement(type, nextProps); - reconcileChildren( - current, - workInProgress, - nextChildren, - renderExpirationTime, - ); + if (__DEV__) { + warning( + nextProps.children == null, + 'Event targets should not have children.', + ); + } + if (eventTargetChild !== null) { + const child = (workInProgress.child = createFiberFromTypeAndProps( + eventTargetChild.type, + null, + eventTargetChild.props, + null, + workInProgress.mode, + renderExpirationTime, + )); + child.return = workInProgress; + + if (current === null || current.child === null) { + child.effectTag = Placement; + } + } else { + reconcileChildren(current, workInProgress, null, renderExpirationTime); + } pushHostContextForEventTarget(workInProgress); return workInProgress.child; } diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index a3e479c20ad62..dfba03702199d 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -28,6 +28,7 @@ import { enableSchedulerTracing, enableProfilerTimer, enableSuspenseServerRenderer, + enableEventAPI, } from 'shared/ReactFeatureFlags'; import { FunctionComponent, @@ -43,6 +44,7 @@ import { IncompleteClassComponent, MemoComponent, SimpleMemoComponent, + EventTarget, } from 'shared/ReactWorkTags'; import { invokeGuardedCallback, @@ -90,6 +92,7 @@ import { hideTextInstance, unhideInstance, unhideTextInstance, + commitEventTarget, } from './ReactFiberHostConfig'; import { captureCommitPhaseError, @@ -299,6 +302,7 @@ function commitBeforeMutationLifeCycles( case HostText: case HostPortal: case IncompleteClassComponent: + case EventTarget: // Nothing to do for these component types return; default: { @@ -585,6 +589,7 @@ function commitLifeCycles( } case SuspenseComponent: case IncompleteClassComponent: + case EventTarget: break; default: { invariant( @@ -817,7 +822,8 @@ function commitContainer(finishedWork: Fiber) { switch (finishedWork.tag) { case ClassComponent: case HostComponent: - case HostText: { + case HostText: + case EventTarget: { return; } case HostRoot: @@ -955,17 +961,18 @@ function commitPlacement(finishedWork: Fiber): void { let node: Fiber = finishedWork; while (true) { if (node.tag === HostComponent || node.tag === HostText) { + const stateNode = node.stateNode; if (before) { if (isContainer) { - insertInContainerBefore(parent, node.stateNode, before); + insertInContainerBefore(parent, stateNode, before); } else { - insertBefore(parent, node.stateNode, before); + insertBefore(parent, stateNode, before); } } else { if (isContainer) { - appendChildToContainer(parent, node.stateNode); + appendChildToContainer(parent, stateNode); } else { - appendChild(parent, node.stateNode); + appendChild(parent, stateNode); } } } else if (node.tag === HostPortal) { @@ -1195,6 +1202,34 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void { commitTextUpdate(textInstance, oldText, newText); return; } + case EventTarget: { + if (enableEventAPI) { + const type = finishedWork.type.type; + const props = finishedWork.memoizedProps; + const instance = finishedWork.stateNode; + let parentInstance = null; + + let node = finishedWork.return; + // Traverse up the fiber tree until we find the parent host node. + while (node !== null) { + if (node.tag === HostComponent) { + parentInstance = node.stateNode; + break; + } else if (node.tag === HostRoot) { + parentInstance = node.stateNode.containerInfo; + break; + } + node = node.return; + } + invariant( + parentInstance !== null, + 'This should have a parent host component initialized. This error is likely ' + + 'caused by a bug in React. Please file an issue.', + ); + commitEventTarget(type, props, instance, parentInstance); + } + return; + } case HostRoot: { return; } diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index abd125d3acb78..457a6b7289a30 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -784,18 +784,15 @@ function completeWork( if (enableEventAPI) { popHostContext(workInProgress); const type = workInProgress.type.type; - let node = workInProgress.return; - let parentHostInstance = null; - // Traverse up the fiber tree till we find a host component fiber - while (node !== null) { - if (node.tag === HostComponent) { - parentHostInstance = node.stateNode; - break; - } - node = node.return; - } - if (parentHostInstance !== null) { - handleEventTarget(type, newProps, parentHostInstance, workInProgress); + const rootContainerInstance = getRootHostContainer(); + const shouldUpdate = handleEventTarget( + type, + newProps, + rootContainerInstance, + workInProgress, + ); + if (shouldUpdate) { + markUpdate(workInProgress); } } break; diff --git a/packages/react-reconciler/src/__tests__/ReactFiberEvents-test-internal.js b/packages/react-reconciler/src/__tests__/ReactFiberEvents-test-internal.js index 73dcc8d4a01e5..5516f783731a7 100644 --- a/packages/react-reconciler/src/__tests__/ReactFiberEvents-test-internal.js +++ b/packages/react-reconciler/src/__tests__/ReactFiberEvents-test-internal.js @@ -127,9 +127,9 @@ describe('ReactFiberEvents', () => { it('should render a simple event component with a single event target', () => { const Test = () => ( - -
Hello world
-
+
+ Hello world +
); @@ -148,10 +148,7 @@ describe('ReactFiberEvents', () => { expect(() => { ReactNoop.render(); expect(Scheduler).toFlushWithoutYielding(); - }).toWarnDev( - 'Warning: validateDOMNesting: React event targets cannot have text DOM nodes as children. ' + - 'Wrap the child text "Hello world" in an element.', - ); + }).toWarnDev('Warning: Event targets should not have children.'); }); it('should warn when an event target has a direct text child #2', () => { @@ -167,19 +164,15 @@ describe('ReactFiberEvents', () => { expect(() => { ReactNoop.render(); expect(Scheduler).toFlushWithoutYielding(); - }).toWarnDev( - 'Warning: validateDOMNesting: React event targets cannot have text DOM nodes as children. ' + - 'Wrap the child text "Hello world" in an element.', - ); + }).toWarnDev('Warning: Event targets should not have children.'); }); it('should not warn if an event target is not a direct child of an event component', () => { const Test = () => (
- - Child 1 - + + Child 1
); @@ -207,9 +200,7 @@ describe('ReactFiberEvents', () => { expect(() => { ReactNoop.render(); expect(Scheduler).toFlushWithoutYielding(); - }).toWarnDev( - 'Warning: validateDOMNesting: React event targets must not have event components as children.', - ); + }).toWarnDev('Warning: Event targets should not have children.'); }); it('should handle event components correctly with error boundaries', () => { @@ -219,11 +210,9 @@ describe('ReactFiberEvents', () => { const Test = () => ( - - - - - + + + ); @@ -268,11 +257,9 @@ describe('ReactFiberEvents', () => { const Parent = () => ( - -
- -
-
+
+ +
); @@ -321,9 +308,7 @@ describe('ReactFiberEvents', () => { const Parent = () => ( - - - + ); @@ -341,7 +326,7 @@ describe('ReactFiberEvents', () => { }); expect(Scheduler).toFlushWithoutYielding(); }).toWarnDev( - 'Warning: validateDOMNesting: React event targets cannot have text DOM nodes as children. ' + + 'Warning: validateDOMNesting: React event components cannot have text DOM nodes as children. ' + 'Wrap the child text "Text!" in an element.', ); }); @@ -355,11 +340,7 @@ describe('ReactFiberEvents', () => { _updateCounter = updateCounter; if (counter === 1) { - return ( - -
Child
-
- ); + return 123; } return ( @@ -370,18 +351,20 @@ describe('ReactFiberEvents', () => { } const Parent = () => ( - - +
+ - - + +
); ReactNoop.render(); expect(Scheduler).toFlushWithoutYielding(); expect(ReactNoop).toMatchRenderedOutput(
- Child - 0 +
+ Child - 0 +
, ); @@ -390,9 +373,7 @@ describe('ReactFiberEvents', () => { _updateCounter(counter => counter + 1); }); expect(Scheduler).toFlushWithoutYielding(); - }).toWarnDev( - 'Warning: validateDOMNesting: React event targets must not have event components as children.', - ); + }).toWarnDev('Warning: Event targets should not have children.'); }); it('should error with a component stack contains the names of the event components and event targets', () => { @@ -404,11 +385,9 @@ describe('ReactFiberEvents', () => { const Test = () => ( - - - - - + + + ); @@ -437,7 +416,6 @@ describe('ReactFiberEvents', () => { expect(componentStackMessage.includes('ErrorComponent')).toBe(true); expect(componentStackMessage.includes('span')).toBe(true); - expect(componentStackMessage.includes('TestEventTarget')).toBe(true); expect(componentStackMessage.includes('TestEventComponent')).toBe(true); expect(componentStackMessage.includes('Test')).toBe(true); expect(componentStackMessage.includes('Wrapper')).toBe(true); @@ -498,9 +476,9 @@ describe('ReactFiberEvents', () => { it('should render a simple event component with a single event target', () => { const Test = () => ( - -
Hello world
-
+
+ Hello world +
); @@ -511,9 +489,8 @@ describe('ReactFiberEvents', () => { const Test2 = () => ( - - I am now a span - + + I am now a span ); @@ -533,10 +510,7 @@ describe('ReactFiberEvents', () => { expect(() => { root.update(); expect(Scheduler).toFlushWithoutYielding(); - }).toWarnDev( - 'Warning: validateDOMNesting: React event targets cannot have text DOM nodes as children. ' + - 'Wrap the child text "Hello world" in an element.', - ); + }).toWarnDev('Warning: Event targets should not have children.'); }); it('should warn when an event target has a direct text child #2', () => { @@ -553,19 +527,15 @@ describe('ReactFiberEvents', () => { expect(() => { root.update(); expect(Scheduler).toFlushWithoutYielding(); - }).toWarnDev( - 'Warning: validateDOMNesting: React event targets cannot have text DOM nodes as children. ' + - 'Wrap the child text "Hello world" in an element.', - ); + }).toWarnDev('Warning: Event targets should not have children.'); }); it('should not warn if an event target is not a direct child of an event component', () => { const Test = () => (
- - Child 1 - + + Child 1
); @@ -595,9 +565,7 @@ describe('ReactFiberEvents', () => { expect(() => { root.update(); expect(Scheduler).toFlushWithoutYielding(); - }).toWarnDev( - 'Warning: validateDOMNesting: React event targets must not have event components as children.', - ); + }).toWarnDev('Warning: Event targets should not have children.'); }); it('should handle event components correctly with error boundaries', () => { @@ -607,11 +575,9 @@ describe('ReactFiberEvents', () => { const Test = () => ( - - - - - + + + ); @@ -620,7 +586,7 @@ describe('ReactFiberEvents', () => { error: null, }; - componentDidCatch(error, errStack) { + componentDidCatch(error) { this.setState({ error, }); @@ -657,11 +623,9 @@ describe('ReactFiberEvents', () => { const Parent = () => ( - -
- -
-
+
+ +
); @@ -710,9 +674,7 @@ describe('ReactFiberEvents', () => { const Parent = () => ( - - - + ); @@ -730,7 +692,7 @@ describe('ReactFiberEvents', () => { _updateCounter(counter => counter + 1); }); }).toWarnDev( - 'Warning: validateDOMNesting: React event targets cannot have text DOM nodes as children. ' + + 'Warning: validateDOMNesting: React event components cannot have text DOM nodes as children. ' + 'Wrap the child text "Text!" in an element.', ); }); @@ -744,11 +706,7 @@ describe('ReactFiberEvents', () => { _updateCounter = updateCounter; if (counter === 1) { - return ( - -
Child
-
- ); + return 123; } return ( @@ -759,11 +717,11 @@ describe('ReactFiberEvents', () => { } const Parent = () => ( - - +
+ - - + +
); const root = ReactTestRenderer.create(null); @@ -771,7 +729,9 @@ describe('ReactFiberEvents', () => { expect(Scheduler).toFlushWithoutYielding(); expect(root).toMatchRenderedOutput(
- Child - 0 +
+ Child - 0 +
, ); @@ -780,9 +740,7 @@ describe('ReactFiberEvents', () => { _updateCounter(counter => counter + 1); }); expect(Scheduler).toFlushWithoutYielding(); - }).toWarnDev( - 'Warning: validateDOMNesting: React event targets must not have event components as children.', - ); + }).toWarnDev('Warning: Event targets should not have children.'); }); it('should error with a component stack contains the names of the event components and event targets', () => { @@ -794,11 +752,9 @@ describe('ReactFiberEvents', () => { const Test = () => ( - - - - - + + + ); @@ -828,7 +784,6 @@ describe('ReactFiberEvents', () => { expect(componentStackMessage.includes('ErrorComponent')).toBe(true); expect(componentStackMessage.includes('span')).toBe(true); - expect(componentStackMessage.includes('TestEventTarget')).toBe(true); expect(componentStackMessage.includes('TestEventComponent')).toBe(true); expect(componentStackMessage.includes('Test')).toBe(true); expect(componentStackMessage.includes('Wrapper')).toBe(true); @@ -888,9 +843,9 @@ describe('ReactFiberEvents', () => { it('should render a simple event component with a single event target', () => { const Test = () => ( - -
Hello world
-
+
+ Hello world +
); @@ -901,9 +856,8 @@ describe('ReactFiberEvents', () => { const Test2 = () => ( - - I am now a span - + + I am now a span ); @@ -923,10 +877,7 @@ describe('ReactFiberEvents', () => { const container = document.createElement('div'); ReactDOM.render(, container); expect(Scheduler).toFlushWithoutYielding(); - }).toWarnDev( - 'Warning: validateDOMNesting: React event targets cannot have text DOM nodes as children. ' + - 'Wrap the child text "Hello world" in an element.', - ); + }).toWarnDev('Warning: Event targets should not have children.'); }); it('should warn when an event target has a direct text child #2', () => { @@ -943,19 +894,15 @@ describe('ReactFiberEvents', () => { const container = document.createElement('div'); ReactDOM.render(, container); expect(Scheduler).toFlushWithoutYielding(); - }).toWarnDev( - 'Warning: validateDOMNesting: React event targets cannot have text DOM nodes as children. ' + - 'Wrap the child text "Hello world" in an element.', - ); + }).toWarnDev('Warning: Event targets should not have children.'); }); it('should not warn if an event target is not a direct child of an event component', () => { const Test = () => (
- - Child 1 - + + Child 1
); @@ -981,9 +928,7 @@ describe('ReactFiberEvents', () => { const container = document.createElement('div'); ReactDOM.render(, container); expect(Scheduler).toFlushWithoutYielding(); - }).toWarnDev( - 'Warning: validateDOMNesting: React event targets must not have event components as children.', - ); + }).toWarnDev('Warning: Event targets should not have children.'); }); it('should handle event components correctly with error boundaries', () => { @@ -993,11 +938,9 @@ describe('ReactFiberEvents', () => { const Test = () => ( - - - - - + + + ); @@ -1043,11 +986,9 @@ describe('ReactFiberEvents', () => { const Parent = () => ( - -
- -
-
+
+ +
); @@ -1087,9 +1028,7 @@ describe('ReactFiberEvents', () => { const Parent = () => ( - - - + ); @@ -1103,7 +1042,7 @@ describe('ReactFiberEvents', () => { }); expect(Scheduler).toFlushWithoutYielding(); }).toWarnDev( - 'Warning: validateDOMNesting: React event targets cannot have text DOM nodes as children. ' + + 'Warning: validateDOMNesting: React event components cannot have text DOM nodes as children. ' + 'Wrap the child text "Text!" in an element.', ); }); @@ -1117,11 +1056,7 @@ describe('ReactFiberEvents', () => { _updateCounter = updateCounter; if (counter === 1) { - return ( - -
Child
-
- ); + return 123; } return ( @@ -1132,25 +1067,25 @@ describe('ReactFiberEvents', () => { } const Parent = () => ( - - +
+ - - + +
); const container = document.createElement('div'); ReactDOM.render(, container); - expect(container.innerHTML).toBe('
Child - 0
'); + expect(container.innerHTML).toBe( + '
Child - 0
', + ); expect(() => { ReactTestUtils.act(() => { _updateCounter(counter => counter + 1); }); expect(Scheduler).toFlushWithoutYielding(); - }).toWarnDev( - 'Warning: validateDOMNesting: React event targets must not have event components as children.', - ); + }).toWarnDev('Warning: Event targets should not have children.'); }); it('should error with a component stack contains the names of the event components and event targets', () => { @@ -1162,11 +1097,9 @@ describe('ReactFiberEvents', () => { const Test = () => ( - - - - - + + + ); @@ -1195,7 +1128,6 @@ describe('ReactFiberEvents', () => { expect(componentStackMessage.includes('ErrorComponent')).toBe(true); expect(componentStackMessage.includes('span')).toBe(true); - expect(componentStackMessage.includes('TestEventTarget')).toBe(true); expect(componentStackMessage.includes('TestEventComponent')).toBe(true); expect(componentStackMessage.includes('Test')).toBe(true); expect(componentStackMessage.includes('Wrapper')).toBe(true); @@ -1222,9 +1154,9 @@ describe('ReactFiberEvents', () => { it('should render a simple event component with a single event target', () => { const Test = () => ( - -
Hello world
-
+
+ Hello world +
); diff --git a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js index d1f38c65d22a3..9a571b168f4bc 100644 --- a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js +++ b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js @@ -65,6 +65,8 @@ export const supportsPersistence = $$$hostConfig.supportsPersistence; export const supportsHydration = $$$hostConfig.supportsHydration; export const handleEventComponent = $$$hostConfig.handleEventComponent; export const handleEventTarget = $$$hostConfig.handleEventTarget; +export const getEventTargetChildElement = + $$$hostConfig.getEventTargetChildElement; // ------------------- // Mutation @@ -84,6 +86,9 @@ export const hideInstance = $$$hostConfig.hideInstance; export const hideTextInstance = $$$hostConfig.hideTextInstance; export const unhideInstance = $$$hostConfig.unhideInstance; export const unhideTextInstance = $$$hostConfig.unhideTextInstance; +export const commitTouchHitTargetUpdate = + $$$hostConfig.commitTouchHitTargetUpdate; +export const commitEventTarget = $$$hostConfig.commitEventTarget; // ------------------- // Persistence diff --git a/packages/react-test-renderer/src/ReactTestHostConfig.js b/packages/react-test-renderer/src/ReactTestHostConfig.js index 8020282394c64..cb9dceec361b8 100644 --- a/packages/react-test-renderer/src/ReactTestHostConfig.js +++ b/packages/react-test-renderer/src/ReactTestHostConfig.js @@ -14,6 +14,18 @@ import {REACT_EVENT_TARGET_TOUCH_HIT} from 'shared/ReactSymbols'; import {enableEventAPI} from 'shared/ReactFeatureFlags'; +type EventTargetChildElement = { + type: string, + props: null | { + style?: { + position?: string, + bottom?: string, + left?: string, + right?: string, + top?: string, + }, + }, +}; export type Type = string; export type Props = Object; export type Container = {| @@ -170,12 +182,6 @@ export function createInstance( hostContext: Object, internalInstanceHandle: Object, ): Instance { - if (__DEV__ && enableEventAPI) { - warning( - hostContext !== EVENT_TOUCH_HIT_TARGET_CONTEXT, - 'validateDOMNesting: must not have any children.', - ); - } return { type, props, @@ -233,10 +239,6 @@ export function createTextInstance( internalInstanceHandle: Object, ): TextInstance { if (__DEV__ && enableEventAPI) { - warning( - hostContext !== EVENT_TOUCH_HIT_TARGET_CONTEXT, - 'validateDOMNesting: must not have any children.', - ); warning( hostContext !== EVENT_COMPONENT_CONTEXT, 'validateDOMNesting: React event components cannot have text DOM nodes as children. ' + @@ -329,17 +331,62 @@ export function handleEventComponent( eventResponder: ReactEventResponder, rootContainerInstance: Container, internalInstanceHandle: Object, -) { - // TODO: add handleEventComponent implementation +): void { + // noop +} + +export function getEventTargetChildElement( + type: Symbol | number, + props: Props, +): null | EventTargetChildElement { + if (enableEventAPI) { + if (type === REACT_EVENT_TARGET_TOUCH_HIT) { + const {bottom, left, right, top} = props; + + if (!bottom && !left && !right && !top) { + return null; + } + return { + type: 'div', + props: { + style: { + position: 'absolute', + zIndex: -1, + bottom: bottom ? `-${bottom}px` : '0px', + left: left ? `-${left}px` : '0px', + right: right ? `-${right}px` : '0px', + top: top ? `-${top}px` : '0px', + }, + }, + }; + } + } + return null; } export function handleEventTarget( type: Symbol | number, props: Props, - parentInstance: Container, + rootContainerInstance: Container, internalInstanceHandle: Object, -) { - if (type === REACT_EVENT_TARGET_TOUCH_HIT) { - // TODO +): boolean { + if (enableEventAPI) { + if (type === REACT_EVENT_TARGET_TOUCH_HIT) { + // In DEV we do a computed style check on the position to ensure + // the parent host component is correctly position in the document. + if (__DEV__) { + return true; + } + } } + return false; +} + +export function commitEventTarget( + type: Symbol | number, + props: Props, + instance: Instance, + parentInstance: Instance, +): void { + // noop } diff --git a/packages/shared/HostConfigWithNoHydration.js b/packages/shared/HostConfigWithNoHydration.js index 1be5f0b8a987d..adc976a849bac 100644 --- a/packages/shared/HostConfigWithNoHydration.js +++ b/packages/shared/HostConfigWithNoHydration.js @@ -47,3 +47,5 @@ export const didNotFindHydratableContainerSuspenseInstance = shim; export const didNotFindHydratableInstance = shim; export const didNotFindHydratableTextInstance = shim; export const didNotFindHydratableSuspenseInstance = shim; +export const canHydrateTouchHitTargetInstance = shim; +export const hydrateTouchHitTargetInstance = shim; diff --git a/packages/shared/HostConfigWithNoPersistence.js b/packages/shared/HostConfigWithNoPersistence.js index d5f84cf43fd6d..9646c6a11f48b 100644 --- a/packages/shared/HostConfigWithNoPersistence.js +++ b/packages/shared/HostConfigWithNoPersistence.js @@ -30,3 +30,4 @@ export const finalizeContainerChildren = shim; export const replaceContainerChildren = shim; export const cloneHiddenInstance = shim; export const cloneHiddenTextInstance = shim; +export const cloneHiddenTouchHitTargetInstance = shim;