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

This commit is contained in:
Dominic Gannaway 2019-04-06 07:51:21 +01:00 committed by GitHub
parent 958b6173fd
commit 4fbbae8afa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 848 additions and 295 deletions

View File

@ -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<ReactEventResponderEventType>,

View File

@ -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.');
}

View File

@ -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: <TouchHitTarget> cannot not be a direct child of an event component. ' +
'Ensure <TouchHitTarget> is a direct child of a DOM element.',
);
if (type === REACT_EVENT_TARGET_TOUCH_HIT) {
warning(
parentHostContextDev.eventData === null ||
!parentHostContextDev.eventData.isEventComponent,
'validateDOMNesting: <TouchHitTarget> cannot not be a direct child of an event component. ' +
'Ensure <TouchHitTarget> is a direct child of a DOM element.',
);
const parentNamespace = parentHostContextDev.namespace;
if (parentNamespace !== HTML_NAMESPACE) {
throw new Error(
'<TouchHitTarget> was used in an unsupported DOM namespace. ' +
'Ensure the <TouchHitTarget> 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: <TouchHitTarget> 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: <TouchHitTarget> 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',
'<TouchHitTarget> inserts an empty absolutely positioned <div>. ' +
'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') !== '',
'<TouchHitTarget> inserts an empty <div> 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.',
);
}
}
}
}

View File

@ -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 {

View File

@ -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 (
`<div style="position:absolute;z-index:-1;bottom:` +
`${bottomString};left:${leftString};right:${rightString};top:${topString}"></div>`
);
}
const nextChildren = toArray(
((nextChild: any): ReactElement).props.children,
);

View File

@ -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,
)

View File

@ -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,
)

View File

@ -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(<Test />);
expect(Scheduler).toFlushWithoutYielding();
}).toWarnDev(
'Warning: validateDOMNesting: <TouchHitTarget> must not have any children.',
);
}).toWarnDev('Warning: Event targets should not have children.');
const Test2 = () => (
<EventComponent>
@ -109,9 +113,7 @@ describe('TouchHitTarget', () => {
expect(() => {
ReactNoop.render(<Test2 />);
expect(Scheduler).toFlushWithoutYielding();
}).toWarnDev(
'Warning: validateDOMNesting: <TouchHitTarget> 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(<Test />);
expect(Scheduler).toFlushWithoutYielding();
}).toWarnDev(
'Warning: validateDOMNesting: <TouchHitTarget> must not have any children.',
);
}).toWarnDev('Warning: Event targets should not have children.');
const Test2 = () => (
<EventComponent>
@ -196,9 +196,7 @@ describe('TouchHitTarget', () => {
expect(() => {
root.update(<Test2 />);
expect(Scheduler).toFlushWithoutYielding();
}).toWarnDev(
'Warning: validateDOMNesting: <TouchHitTarget> 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(<Test />, container);
expect(Scheduler).toFlushWithoutYielding();
}).toWarnDev(
'Warning: validateDOMNesting: <TouchHitTarget> must not have any children.',
);
}).toWarnDev('Warning: Event targets should not have children.');
const Test2 = () => (
<EventComponent>
@ -284,9 +280,7 @@ describe('TouchHitTarget', () => {
expect(() => {
ReactDOM.render(<Test2 />, container);
expect(Scheduler).toFlushWithoutYielding();
}).toWarnDev(
'Warning: validateDOMNesting: <TouchHitTarget> 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 <TouchHitTarget> is a direct child of a DOM element.',
);
});
it('should render a conditional TouchHitTarget correctly (false -> true)', () => {
let cond = false;
const Test = () => (
<EventComponent>
<div>
{cond ? null : (
<TouchHitTarget top={10} left={10} right={10} bottom={10} />
)}
</div>
</EventComponent>
);
const container = document.createElement('div');
ReactDOM.render(<Test />, container);
expect(Scheduler).toFlushWithoutYielding();
expect(container.innerHTML).toBe(
'<div><div style="position: absolute; z-index: -1; bottom: -10px; ' +
'left: -10px; right: -10px; top: -10px;"></div></div>',
);
cond = true;
ReactDOM.render(<Test />, container);
expect(Scheduler).toFlushWithoutYielding();
expect(container.innerHTML).toBe('<div></div>');
});
it('should render a conditional TouchHitTarget correctly (true -> false)', () => {
let cond = true;
const Test = () => (
<EventComponent>
<div>
{cond ? null : (
<TouchHitTarget top={10} left={10} right={10} bottom={10} />
)}
</div>
</EventComponent>
);
const container = document.createElement('div');
ReactDOM.render(<Test />, container);
expect(Scheduler).toFlushWithoutYielding();
expect(container.innerHTML).toBe('<div></div>');
cond = false;
ReactDOM.render(<Test />, container);
expect(Scheduler).toFlushWithoutYielding();
expect(container.innerHTML).toBe(
'<div><div style="position: absolute; z-index: -1; bottom: -10px; ' +
'left: -10px; right: -10px; top: -10px;"></div></div>',
);
});
it('should render a conditional TouchHitTarget hit slop correctly (false -> true)', () => {
let cond = false;
const Test = () => (
<EventComponent>
<div>
{cond ? (
<TouchHitTarget />
) : (
<TouchHitTarget top={10} left={10} right={10} bottom={10} />
)}
</div>
</EventComponent>
);
const container = document.createElement('div');
ReactDOM.render(<Test />, container);
expect(Scheduler).toFlushWithoutYielding();
expect(container.innerHTML).toBe(
'<div><div style="position: absolute; z-index: -1; bottom: -10px; ' +
'left: -10px; right: -10px; top: -10px;"></div></div>',
);
cond = true;
ReactDOM.render(<Test />, container);
expect(Scheduler).toFlushWithoutYielding();
expect(container.innerHTML).toBe('<div></div>');
});
it('should render a conditional TouchHitTarget hit slop correctly (true -> false)', () => {
let cond = true;
const Test = () => (
<EventComponent>
<div>
<span>Random span 1</span>
{cond ? (
<TouchHitTarget />
) : (
<TouchHitTarget top={10} left={10} right={10} bottom={10} />
)}
<span>Random span 2</span>
</div>
</EventComponent>
);
const container = document.createElement('div');
ReactDOM.render(<Test />, container);
expect(Scheduler).toFlushWithoutYielding();
expect(container.innerHTML).toBe(
'<div><span>Random span 1</span><span>Random span 2</span></div>',
);
cond = false;
ReactDOM.render(<Test />, container);
expect(Scheduler).toFlushWithoutYielding();
expect(container.innerHTML).toBe(
'<div><span>Random span 1</span><div style="position: absolute; z-index: -1; bottom: -10px; ' +
'left: -10px; right: -10px; top: -10px;"></div><span>Random span 2</span></div>',
);
});
it('should update TouchHitTarget hit slop values correctly (false -> true)', () => {
let cond = false;
const Test = () => (
<EventComponent>
<div>
<span>Random span 1</span>
{cond ? (
<TouchHitTarget top={10} left={null} right={10} bottom={10} />
) : (
<TouchHitTarget
top={undefined}
left={20}
right={null}
bottom={0}
/>
)}
<span>Random span 2</span>
</div>
</EventComponent>
);
const container = document.createElement('div');
ReactDOM.render(<Test />, container);
expect(Scheduler).toFlushWithoutYielding();
expect(container.innerHTML).toBe(
'<div><span>Random span 1</span><div style="position: absolute; z-index: -1; bottom: 0px; ' +
'left: -20px; right: 0px; top: 0px;"></div><span>Random span 2</span></div>',
);
cond = true;
ReactDOM.render(<Test />, container);
expect(Scheduler).toFlushWithoutYielding();
expect(container.innerHTML).toBe(
'<div><span>Random span 1</span><div style="position: absolute; z-index: -1; bottom: 0px; ' +
'left: -20px; right: 0px; top: 0px;"></div><span>Random span 2</span></div>',
);
});
it('should update TouchHitTarget hit slop values correctly (true -> false)', () => {
let cond = true;
const Test = () => (
<EventComponent>
<div>
<span>Random span 1</span>
{cond ? (
<TouchHitTarget top={10} left={null} right={10} bottom={10} />
) : (
<TouchHitTarget
top={undefined}
left={20}
right={null}
bottom={0}
/>
)}
<span>Random span 2</span>
</div>
</EventComponent>
);
const container = document.createElement('div');
ReactDOM.render(<Test />, container);
expect(Scheduler).toFlushWithoutYielding();
expect(container.innerHTML).toBe(
'<div><span>Random span 1</span><div style="position: absolute; z-index: -1; bottom: -10px; ' +
'left: 0px; right: -10px; top: -10px;"></div><span>Random span 2</span></div>',
);
cond = false;
ReactDOM.render(<Test />, container);
expect(Scheduler).toFlushWithoutYielding();
expect(container.innerHTML).toBe(
'<div><span>Random span 1</span><div style="position: absolute; z-index: -1; bottom: -10px; ' +
'left: 0px; right: -10px; top: -10px;"></div><span>Random span 2</span></div>',
);
});
it('should hydrate TouchHitTarget hit slop elements correcty', () => {
const Test = () => (
<EventComponent>
<div>
<TouchHitTarget />
</div>
</EventComponent>
);
const container = document.createElement('div');
container.innerHTML = '<div></div>';
ReactDOM.hydrate(<Test />, container);
expect(Scheduler).toFlushWithoutYielding();
expect(container.innerHTML).toBe('<div></div>');
const Test2 = () => (
<EventComponent>
<div>
<TouchHitTarget top={10} left={10} right={10} bottom={10} />
</div>
</EventComponent>
);
const container2 = document.createElement('div');
container2.innerHTML =
'<div><div style="position:absolute;z-index:-1;bottom:-10px;left:-10px;right:-10px;top:-10px"></div></div>';
ReactDOM.hydrate(<Test2 />, container2);
expect(Scheduler).toFlushWithoutYielding();
expect(container2.innerHTML).toBe(
'<div><div style="position:absolute;z-index:-1;bottom:-10px;left:-10px;right:-10px;top:-10px"></div></div>',
);
});
it('should hydrate TouchHitTarget hit slop elements correcty and patch them', () => {
const Test = () => (
<EventComponent>
<div>
<TouchHitTarget top={10} left={10} right={10} bottom={10} />
</div>
</EventComponent>
);
const container = document.createElement('div');
container.innerHTML = '<div></div>';
expect(() => {
ReactDOM.hydrate(<Test />, container);
expect(Scheduler).toFlushWithoutYielding();
}).toWarnDev(
'Warning: Expected server HTML to contain a matching <div> in <div>.',
{withoutStack: true},
);
expect(Scheduler).toFlushWithoutYielding();
expect(container.innerHTML).toBe(
'<div><div style="position: absolute; z-index: -1; bottom: -10px; ' +
'left: -10px; right: -10px; top: -10px;"></div></div>',
);
});
});
describe('ReactDOMServer', () => {
beforeEach(() => {
initReactDOMServer();
EventComponent = createReactEventComponent();
TouchHitTarget = ReactEvents.TouchHitTarget;
});
it('should not warn when a TouchHitTarget is used correctly', () => {
const Test = () => (
<EventComponent>
<div>
<TouchHitTarget />
</div>
</EventComponent>
);
const output = ReactDOMServer.renderToString(<Test />);
expect(output).toBe('<div></div>');
});
it('should render a TouchHitTarget with hit slop values', () => {
const Test = () => (
<EventComponent>
<div>
<TouchHitTarget top={10} left={10} right={10} bottom={10} />
</div>
</EventComponent>
);
let output = ReactDOMServer.renderToString(<Test />);
expect(output).toBe(
'<div><div style="position:absolute;z-index:-1;bottom:-10px;left:-10px;right:-10px;top:-10px"></div></div>',
);
const Test2 = () => (
<EventComponent>
<div>
<TouchHitTarget top={null} left={undefined} right={0} bottom={10} />
</div>
</EventComponent>
);
output = ReactDOMServer.renderToString(<Test2 />);
expect(output).toBe(
'<div><div style="position:absolute;z-index:-1;bottom:-10px;left:0px;right:0x;top:0px"></div></div>',
);
const Test3 = () => (
<EventComponent>
<div>
<TouchHitTarget top={1} left={2} right={3} bottom={4} />
</div>
</EventComponent>
);
output = ReactDOMServer.renderToString(<Test3 />);
expect(output).toBe(
'<div><div style="position:absolute;z-index:-1;bottom:-4px;left:-2px;right:-3px;top:-1px"></div></div>',
);
});
});
});

View File

@ -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.');
}

View File

@ -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.');
}

View File

@ -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<Instance | TextInstance>,
pendingChildren: Array<Instance | TextInstance>,
};
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: <TouchHitTarget> 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: <TouchHitTarget> 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
},
};

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;

View File

@ -127,9 +127,9 @@ describe('ReactFiberEvents', () => {
it('should render a simple event component with a single event target', () => {
const Test = () => (
<EventComponent>
<EventTarget>
<div>Hello world</div>
</EventTarget>
<div>
Hello world<EventTarget />
</div>
</EventComponent>
);
@ -148,10 +148,7 @@ describe('ReactFiberEvents', () => {
expect(() => {
ReactNoop.render(<Test />);
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(<Test />);
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 = () => (
<EventComponent>
<div>
<EventTarget>
<span>Child 1</span>
</EventTarget>
<EventTarget />
<span>Child 1</span>
</div>
</EventComponent>
);
@ -207,9 +200,7 @@ describe('ReactFiberEvents', () => {
expect(() => {
ReactNoop.render(<Test />);
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 = () => (
<EventComponent>
<EventTarget>
<span>
<ErrorComponent />
</span>
</EventTarget>
<span>
<ErrorComponent />
</span>
</EventComponent>
);
@ -268,11 +257,9 @@ describe('ReactFiberEvents', () => {
const Parent = () => (
<EventComponent>
<EventTarget>
<div>
<Child />
</div>
</EventTarget>
<div>
<Child />
</div>
</EventComponent>
);
@ -321,9 +308,7 @@ describe('ReactFiberEvents', () => {
const Parent = () => (
<EventComponent>
<EventTarget>
<Child />
</EventTarget>
<Child />
</EventComponent>
);
@ -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 (
<EventComponent>
<div>Child</div>
</EventComponent>
);
return <EventTarget>123</EventTarget>;
}
return (
@ -370,18 +351,20 @@ describe('ReactFiberEvents', () => {
}
const Parent = () => (
<EventComponent>
<EventTarget>
<div>
<EventComponent>
<Child />
</EventTarget>
</EventComponent>
</EventComponent>
</div>
);
ReactNoop.render(<Parent />);
expect(Scheduler).toFlushWithoutYielding();
expect(ReactNoop).toMatchRenderedOutput(
<div>
<span>Child - 0</span>
<div>
<span>Child - 0</span>
</div>
</div>,
);
@ -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 = () => (
<EventComponent>
<EventTarget>
<span>
<ErrorComponent />
</span>
</EventTarget>
<span>
<ErrorComponent />
</span>
</EventComponent>
);
@ -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 = () => (
<EventComponent>
<EventTarget>
<div>Hello world</div>
</EventTarget>
<div>
Hello world<EventTarget />
</div>
</EventComponent>
);
@ -511,9 +489,8 @@ describe('ReactFiberEvents', () => {
const Test2 = () => (
<EventComponent>
<EventTarget>
<span>I am now a span</span>
</EventTarget>
<EventTarget />
<span>I am now a span</span>
</EventComponent>
);
@ -533,10 +510,7 @@ describe('ReactFiberEvents', () => {
expect(() => {
root.update(<Test />);
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(<Test />);
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 = () => (
<EventComponent>
<div>
<EventTarget>
<span>Child 1</span>
</EventTarget>
<EventTarget />
<span>Child 1</span>
</div>
</EventComponent>
);
@ -595,9 +565,7 @@ describe('ReactFiberEvents', () => {
expect(() => {
root.update(<Test />);
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 = () => (
<EventComponent>
<EventTarget>
<span>
<ErrorComponent />
</span>
</EventTarget>
<span>
<ErrorComponent />
</span>
</EventComponent>
);
@ -620,7 +586,7 @@ describe('ReactFiberEvents', () => {
error: null,
};
componentDidCatch(error, errStack) {
componentDidCatch(error) {
this.setState({
error,
});
@ -657,11 +623,9 @@ describe('ReactFiberEvents', () => {
const Parent = () => (
<EventComponent>
<EventTarget>
<div>
<Child />
</div>
</EventTarget>
<div>
<Child />
</div>
</EventComponent>
);
@ -710,9 +674,7 @@ describe('ReactFiberEvents', () => {
const Parent = () => (
<EventComponent>
<EventTarget>
<Child />
</EventTarget>
<Child />
</EventComponent>
);
@ -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 (
<EventComponent>
<div>Child</div>
</EventComponent>
);
return <EventTarget>123</EventTarget>;
}
return (
@ -759,11 +717,11 @@ describe('ReactFiberEvents', () => {
}
const Parent = () => (
<EventComponent>
<EventTarget>
<div>
<EventComponent>
<Child />
</EventTarget>
</EventComponent>
</EventComponent>
</div>
);
const root = ReactTestRenderer.create(null);
@ -771,7 +729,9 @@ describe('ReactFiberEvents', () => {
expect(Scheduler).toFlushWithoutYielding();
expect(root).toMatchRenderedOutput(
<div>
<span>Child - 0</span>
<div>
<span>Child - 0</span>
</div>
</div>,
);
@ -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 = () => (
<EventComponent>
<EventTarget>
<span>
<ErrorComponent />
</span>
</EventTarget>
<span>
<ErrorComponent />
</span>
</EventComponent>
);
@ -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 = () => (
<EventComponent>
<EventTarget>
<div>Hello world</div>
</EventTarget>
<div>
Hello world<EventTarget />
</div>
</EventComponent>
);
@ -901,9 +856,8 @@ describe('ReactFiberEvents', () => {
const Test2 = () => (
<EventComponent>
<EventTarget>
<span>I am now a span</span>
</EventTarget>
<EventTarget />
<span>I am now a span</span>
</EventComponent>
);
@ -923,10 +877,7 @@ describe('ReactFiberEvents', () => {
const container = document.createElement('div');
ReactDOM.render(<Test />, 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(<Test />, 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 = () => (
<EventComponent>
<div>
<EventTarget>
<span>Child 1</span>
</EventTarget>
<EventTarget />
<span>Child 1</span>
</div>
</EventComponent>
);
@ -981,9 +928,7 @@ describe('ReactFiberEvents', () => {
const container = document.createElement('div');
ReactDOM.render(<Test />, 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 = () => (
<EventComponent>
<EventTarget>
<span>
<ErrorComponent />
</span>
</EventTarget>
<span>
<ErrorComponent />
</span>
</EventComponent>
);
@ -1043,11 +986,9 @@ describe('ReactFiberEvents', () => {
const Parent = () => (
<EventComponent>
<EventTarget>
<div>
<Child />
</div>
</EventTarget>
<div>
<Child />
</div>
</EventComponent>
);
@ -1087,9 +1028,7 @@ describe('ReactFiberEvents', () => {
const Parent = () => (
<EventComponent>
<EventTarget>
<Child />
</EventTarget>
<Child />
</EventComponent>
);
@ -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 (
<EventComponent>
<div>Child</div>
</EventComponent>
);
return <EventTarget>123</EventTarget>;
}
return (
@ -1132,25 +1067,25 @@ describe('ReactFiberEvents', () => {
}
const Parent = () => (
<EventComponent>
<EventTarget>
<div>
<EventComponent>
<Child />
</EventTarget>
</EventComponent>
</EventComponent>
</div>
);
const container = document.createElement('div');
ReactDOM.render(<Parent />, container);
expect(container.innerHTML).toBe('<div><span>Child - 0</span></div>');
expect(container.innerHTML).toBe(
'<div><div><span>Child - 0</span></div></div>',
);
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 = () => (
<EventComponent>
<EventTarget>
<span>
<ErrorComponent />
</span>
</EventTarget>
<span>
<ErrorComponent />
</span>
</EventComponent>
);
@ -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 = () => (
<EventComponent>
<EventTarget>
<div>Hello world</div>
</EventTarget>
<div>
Hello world<EventTarget />
</div>
</EventComponent>
);

View File

@ -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

View File

@ -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: <TouchHitTarget> 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: <TouchHitTarget> 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
}

View File

@ -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;

View File

@ -30,3 +30,4 @@ export const finalizeContainerChildren = shim;
export const replaceContainerChildren = shim;
export const cloneHiddenInstance = shim;
export const cloneHiddenTextInstance = shim;
export const cloneHiddenTouchHitTargetInstance = shim;