Adds more scaffolding for experimental event API (#15112)

* Adds more scaffolding for experimental event API
This commit is contained in:
Dominic Gannaway
2019-03-20 11:20:17 +00:00
committed by GitHub
parent daeda44d8f
commit b83e01cade
19 changed files with 1066 additions and 279 deletions
+5 -1
View File
@@ -340,6 +340,10 @@ export function getChildHostContext() {
return NO_CONTEXT;
}
export function getChildHostContextForEvent() {
return NO_CONTEXT;
}
export const scheduleTimeout = setTimeout;
export const cancelTimeout = clearTimeout;
export const noTimeout = -1;
@@ -440,7 +444,7 @@ export function handleEventComponent(
}
export function handleEventTarget(
type: string,
type: Symbol | number,
props: Props,
internalInstanceHandle: Object,
) {
+82 -5
View File
@@ -44,6 +44,12 @@ import dangerousStyleValue from '../shared/dangerousStyleValue';
import type {DOMContainer} from './ReactDOM';
import type {ReactEventResponder} from 'shared/ReactTypes';
import {
REACT_EVENT_COMPONENT_TYPE,
REACT_EVENT_TARGET_TYPE,
REACT_EVENT_TARGET_TOUCH_HIT,
} from 'shared/ReactSymbols';
import getElementFromTouchHitTarget from 'shared/getElementFromTouchHitTarget';
export type Type = string;
export type Props = {
@@ -65,6 +71,10 @@ export type PublicInstance = Element | Text;
type HostContextDev = {
namespace: string,
ancestorInfo: mixed,
eventData: null | {|
isEventComponent?: boolean,
isEventTarget?: boolean,
|},
};
type HostContextProd = string;
export type HostContext = HostContextDev | HostContextProd;
@@ -73,7 +83,11 @@ export type ChildSet = void; // Unused
export type TimeoutHandle = TimeoutID;
export type NoTimeout = -1;
import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags';
import {
enableSuspenseServerRenderer,
enableEventAPI,
} from 'shared/ReactFeatureFlags';
import warning from 'shared/warning';
// Intentionally not named imports because Rollup would
// use dynamic dispatch for CommonJS interop named imports.
@@ -142,7 +156,7 @@ export function getRootHostContext(
if (__DEV__) {
const validatedTag = type.toLowerCase();
const ancestorInfo = updatedAncestorInfo(null, validatedTag);
return {namespace, ancestorInfo};
return {namespace, ancestorInfo, eventData: null};
}
return namespace;
}
@@ -159,12 +173,42 @@ export function getChildHostContext(
parentHostContextDev.ancestorInfo,
type,
);
return {namespace, ancestorInfo};
return {namespace, ancestorInfo, eventData: null};
}
const parentNamespace = ((parentHostContext: any): HostContextProd);
return getChildNamespace(parentNamespace, type);
}
export function getChildHostContextForEvent(
parentHostContext: HostContext,
type: Symbol | number,
): HostContext {
if (__DEV__) {
const parentHostContextDev = ((parentHostContext: any): HostContextDev);
const {namespace, ancestorInfo} = parentHostContextDev;
let eventData = null;
if (type === REACT_EVENT_COMPONENT_TYPE) {
eventData = {
isEventComponent: true,
isEventTarget: false,
};
} else if (type === REACT_EVENT_TARGET_TYPE) {
warning(
parentHostContextDev.eventData !== null &&
parentHostContextDev.eventData.isEventComponent,
'validateDOMNesting: React event targets must be direct children of event components.',
);
eventData = {
isEventComponent: false,
isEventTarget: true,
};
}
return {namespace, ancestorInfo, eventData};
}
return parentHostContext;
}
export function getPublicInstance(instance: Instance): * {
return instance;
}
@@ -296,6 +340,23 @@ export function createTextInstance(
if (__DEV__) {
const hostContextDev = ((hostContext: any): HostContextDev);
validateDOMNesting(null, text, hostContextDev.ancestorInfo);
if (enableEventAPI) {
const eventData = hostContextDev.eventData;
if (eventData !== null) {
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,
'validateDOMNesting: React event targets cannot have text DOM nodes as children. ' +
'Wrap the child text "%s" in an element.',
text,
);
}
}
}
const textNode: TextInstance = createTextNode(text, rootContainerInstance);
precacheFiberNode(internalInstanceHandle, textNode);
@@ -804,9 +865,25 @@ export function handleEventComponent(
}
export function handleEventTarget(
type: string,
type: Symbol | number,
props: Props,
internalInstanceHandle: Object,
) {
// TODO: add handleEventTarget implementation
// Touch target hit slop handling
if (type === REACT_EVENT_TARGET_TOUCH_HIT) {
// Validates that there is a single element
const element = getElementFromTouchHitTarget(internalInstanceHandle);
if (element !== null) {
// We update the event target state node to be that of the element.
// We can then diff this entry to determine if we need to add the
// hit slop element, or change the dimensions of the hit slop.
const lastElement = internalInstanceHandle.stateNode;
if (lastElement !== element) {
internalInstanceHandle.stateNode = element;
// TODO: Create the hit slop element and attach it to the element
} else {
// TODO: Diff the left, top, right, bottom props
}
}
}
}
@@ -283,6 +283,14 @@ export function getChildHostContext(
}
}
export function getChildHostContextForEvent(
parentHostContext: HostContext,
type: Symbol | number,
) {
// TODO: add getChildHostContextForEvent implementation
return parentHostContext;
}
export function getPublicInstance(instance: Instance): * {
return instance.canonical;
}
@@ -428,7 +436,7 @@ export function handleEventComponent(
}
export function handleEventTarget(
type: string,
type: Symbol | number,
props: Props,
internalInstanceHandle: Object,
) {
@@ -206,6 +206,14 @@ export function getChildHostContext(
}
}
export function getChildHostContextForEvent(
parentHostContext: HostContext,
type: Symbol | number,
) {
// TODO: add getChildHostContextForEvent implementation
return parentHostContext;
}
export function getPublicInstance(instance: Instance): * {
return instance;
}
@@ -487,7 +495,7 @@ export function handleEventComponent(
}
export function handleEventTarget(
type: string,
type: Symbol | number,
props: Props,
internalInstanceHandle: Object,
) {
+60 -1
View File
@@ -21,8 +21,18 @@ import type {ReactNodeList} from 'shared/ReactTypes';
import * as Scheduler from 'scheduler/unstable_mock';
import {createPortal} from 'shared/ReactPortal';
import expect from 'expect';
import {REACT_FRAGMENT_TYPE, REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
import {
REACT_FRAGMENT_TYPE,
REACT_ELEMENT_TYPE,
REACT_EVENT_COMPONENT_TYPE,
REACT_EVENT_TARGET_TYPE,
REACT_EVENT_TARGET_TOUCH_HIT,
} from 'shared/ReactSymbols';
import warningWithoutStack from 'shared/warningWithoutStack';
import warning from 'shared/warning';
import getElementFromTouchHitTarget from 'shared/getElementFromTouchHitTarget';
import {enableEventAPI} from 'shared/ReactFeatureFlags';
// for .act's return value
type Thenable = {
@@ -54,6 +64,8 @@ type HostContext = Object;
const NO_CONTEXT = {};
const UPPERCASE_CONTEXT = {};
const EVENT_COMPONENT_CONTEXT = {};
const EVENT_TARGET_CONTEXT = {};
const UPDATE_SIGNAL = {};
if (__DEV__) {
Object.freeze(NO_CONTEXT);
@@ -250,6 +262,24 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
return NO_CONTEXT;
},
getChildHostContextForEvent(
parentHostContext: HostContext,
type: Symbol | number,
) {
if (__DEV__ && enableEventAPI) {
if (type === REACT_EVENT_COMPONENT_TYPE) {
return EVENT_COMPONENT_CONTEXT;
} else if (type === REACT_EVENT_TARGET_TYPE) {
warning(
parentHostContext === EVENT_COMPONENT_CONTEXT,
'validateDOMNesting: React event targets must be direct children of event components.',
);
return EVENT_TARGET_CONTEXT;
}
}
return parentHostContext;
},
getPublicInstance(instance) {
return instance;
},
@@ -333,6 +363,20 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
hostContext: Object,
internalInstanceHandle: Object,
): TextInstance {
if (__DEV__ && enableEventAPI) {
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();
}
@@ -363,6 +407,21 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
isPrimaryRenderer: true,
supportsHydration: false,
handleEventComponent() {
// NO-OP
},
handleEventTarget(
type: Symbol | number,
props: Props,
internalInstanceHandle: Object,
) {
if (type === REACT_EVENT_TARGET_TOUCH_HIT) {
// Validates that there is a single element
getElementFromTouchHitTarget(internalInstanceHandle);
}
},
};
const hostConfig = useMutation
+63 -2
View File
@@ -8,7 +8,13 @@
*/
import type {ReactElement, Source} from 'shared/ReactElementType';
import type {ReactFragment, ReactPortal, RefObject} from 'shared/ReactTypes';
import type {
ReactFragment,
ReactPortal,
RefObject,
ReactEventComponent,
ReactEventTarget,
} from 'shared/ReactTypes';
import type {WorkTag} from 'shared/ReactWorkTags';
import type {TypeOfMode} from './ReactTypeOfMode';
import type {SideEffectTag} from 'shared/ReactSideEffectTags';
@@ -19,7 +25,7 @@ import type {HookType} from './ReactFiberHooks';
import invariant from 'shared/invariant';
import warningWithoutStack from 'shared/warningWithoutStack';
import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
import {enableProfilerTimer, enableEventAPI} from 'shared/ReactFeatureFlags';
import {NoEffect} from 'shared/ReactSideEffectTags';
import {
IndeterminateComponent,
@@ -38,6 +44,8 @@ import {
FunctionComponent,
MemoComponent,
LazyComponent,
EventComponent,
EventTarget,
} from 'shared/ReactWorkTags';
import getComponentName from 'shared/getComponentName';
@@ -60,6 +68,8 @@ import {
REACT_SUSPENSE_TYPE,
REACT_MEMO_TYPE,
REACT_LAZY_TYPE,
REACT_EVENT_COMPONENT_TYPE,
REACT_EVENT_TARGET_TYPE,
} from 'shared/ReactSymbols';
let hasBadMapPolyfill;
@@ -503,6 +513,28 @@ export function createFiberFromTypeAndProps(
fiberTag = LazyComponent;
resolvedType = null;
break getTag;
case REACT_EVENT_COMPONENT_TYPE:
if (enableEventAPI) {
return createFiberFromEventComponent(
type,
pendingProps,
mode,
expirationTime,
key,
);
}
break;
case REACT_EVENT_TARGET_TYPE:
if (enableEventAPI) {
return createFiberFromEventTarget(
type,
pendingProps,
mode,
expirationTime,
key,
);
}
break;
}
}
let info = '';
@@ -581,6 +613,35 @@ export function createFiberFromFragment(
return fiber;
}
export function createFiberFromEventComponent(
eventComponent: ReactEventComponent,
pendingProps: any,
mode: TypeOfMode,
expirationTime: ExpirationTime,
key: null | string,
): Fiber {
const fiber = createFiber(EventComponent, pendingProps, key, mode);
fiber.elementType = eventComponent;
fiber.type = eventComponent;
fiber.stateNode = new Map();
fiber.expirationTime = expirationTime;
return fiber;
}
export function createFiberFromEventTarget(
eventTarget: ReactEventTarget,
pendingProps: any,
mode: TypeOfMode,
expirationTime: ExpirationTime,
key: null | string,
): Fiber {
const fiber = createFiber(EventTarget, pendingProps, key, mode);
fiber.elementType = eventTarget;
fiber.type = eventTarget;
fiber.expirationTime = expirationTime;
return fiber;
}
function createFiberFromProfiler(
pendingProps: any,
mode: TypeOfMode,
+52 -1
View File
@@ -35,6 +35,8 @@ import {
SimpleMemoComponent,
LazyComponent,
IncompleteClassComponent,
EventComponent,
EventTarget,
} from 'shared/ReactWorkTags';
import {
NoEffect,
@@ -52,6 +54,7 @@ import {
debugRenderPhaseSideEffectsForStrictMode,
enableProfilerTimer,
enableSuspenseServerRenderer,
enableEventAPI,
} from 'shared/ReactFeatureFlags';
import invariant from 'shared/invariant';
import shallowEqual from 'shared/shallowEqual';
@@ -93,7 +96,11 @@ import {
registerSuspenseInstanceRetry,
} from './ReactFiberHostConfig';
import type {SuspenseInstance} from './ReactFiberHostConfig';
import {pushHostContext, pushHostContainer} from './ReactFiberHostContext';
import {
pushHostContext,
pushHostContainer,
pushHostContextForEvent,
} from './ReactFiberHostContext';
import {
pushProvider,
propagateContextChange,
@@ -1943,6 +1950,34 @@ function updateContextConsumer(
return workInProgress.child;
}
function updateEventComponent(current, workInProgress, renderExpirationTime) {
const nextProps = workInProgress.pendingProps;
let nextChildren = nextProps.children;
reconcileChildren(
current,
workInProgress,
nextChildren,
renderExpirationTime,
);
pushHostContextForEvent(workInProgress);
return workInProgress.child;
}
function updateEventTarget(current, workInProgress, renderExpirationTime) {
const nextProps = workInProgress.pendingProps;
let nextChildren = nextProps.children;
reconcileChildren(
current,
workInProgress,
nextChildren,
renderExpirationTime,
);
pushHostContextForEvent(workInProgress);
return workInProgress.child;
}
export function markWorkInProgressReceivedUpdate() {
didReceiveUpdate = true;
}
@@ -2259,6 +2294,22 @@ function beginWork(
}
break;
}
case EventComponent: {
if (enableEventAPI) {
return updateEventComponent(
current,
workInProgress,
renderExpirationTime,
);
}
break;
}
case EventTarget: {
if (enableEventAPI) {
return updateEventTarget(current, workInProgress, renderExpirationTime);
}
break;
}
}
invariant(
false,
+2 -16
View File
@@ -43,7 +43,6 @@ import {
IncompleteClassComponent,
MemoComponent,
SimpleMemoComponent,
EventTarget,
} from 'shared/ReactWorkTags';
import {
invokeGuardedCallback,
@@ -91,7 +90,6 @@ import {
hideTextInstance,
unhideInstance,
unhideTextInstance,
handleEventTarget,
} from './ReactFiberHostConfig';
import {
captureCommitPhaseError,
@@ -301,7 +299,6 @@ function commitBeforeMutationLifeCycles(
case HostText:
case HostPortal:
case IncompleteClassComponent:
case EventTarget:
// Nothing to do for these component types
return;
default: {
@@ -588,7 +585,6 @@ function commitLifeCycles(
}
case SuspenseComponent:
case IncompleteClassComponent:
case EventTarget:
break;
default: {
invariant(
@@ -819,12 +815,8 @@ function commitContainer(finishedWork: Fiber) {
}
switch (finishedWork.tag) {
case ClassComponent: {
return;
}
case HostComponent: {
return;
}
case ClassComponent:
case HostComponent:
case HostText: {
return;
}
@@ -1216,12 +1208,6 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
case IncompleteClassComponent: {
return;
}
case EventTarget: {
const newProps = finishedWork.memoizedProps;
const type = finishedWork.type.type;
handleEventTarget(type, newProps, finishedWork);
return;
}
default: {
invariant(
false,
+16 -5
View File
@@ -66,6 +66,7 @@ import {
appendChildToContainerChildSet,
finalizeContainerChildren,
handleEventComponent,
handleEventTarget,
} from './ReactFiberHostConfig';
import {
getRootHostContainer,
@@ -85,7 +86,10 @@ import {
skipPastDehydratedSuspenseInstance,
popHydrationState,
} from './ReactFiberHydrationContext';
import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags';
import {
enableSuspenseServerRenderer,
enableEventAPI,
} from 'shared/ReactFeatureFlags';
function markUpdate(workInProgress: Fiber) {
// Tag the fiber with an update effect. This turns a Placement into
@@ -766,13 +770,20 @@ function completeWork(
break;
}
case EventComponent: {
const rootContainerInstance = getRootHostContainer();
const responder = workInProgress.type.responder;
handleEventComponent(responder, rootContainerInstance, workInProgress);
if (enableEventAPI) {
popHostContext(workInProgress);
const rootContainerInstance = getRootHostContainer();
const responder = workInProgress.type.responder;
handleEventComponent(responder, rootContainerInstance, workInProgress);
}
break;
}
case EventTarget: {
markUpdate(workInProgress);
if (enableEventAPI) {
popHostContext(workInProgress);
const type = workInProgress.type.type;
handleEventTarget(type, newProps, workInProgress);
}
break;
}
default:
+22 -1
View File
@@ -13,7 +13,11 @@ import type {Container, HostContext} from './ReactFiberHostConfig';
import invariant from 'shared/invariant';
import {getChildHostContext, getRootHostContext} from './ReactFiberHostConfig';
import {
getChildHostContext,
getRootHostContext,
getChildHostContextForEvent,
} from './ReactFiberHostConfig';
import {createCursor, push, pop} from './ReactFiberStack';
declare class NoContextT {}
@@ -92,6 +96,22 @@ function pushHostContext(fiber: Fiber): void {
push(contextStackCursor, nextContext, fiber);
}
function pushHostContextForEvent(fiber: Fiber): void {
const context: HostContext = requiredContext(contextStackCursor.current);
const eventTypeof = fiber.type.$$typeof;
const nextContext = getChildHostContextForEvent(context, eventTypeof);
// Don't push this Fiber's context unless it's unique.
if (context === nextContext) {
return;
}
// Track the context and the Fiber that provided it.
// This enables us to pop only Fibers that provide unique contexts.
push(contextFiberStackCursor, fiber, fiber);
push(contextStackCursor, nextContext, fiber);
}
function popHostContext(fiber: Fiber): void {
// Do not pop unless this Fiber provided the current context.
// pushHostContext() only pushes Fibers that provide unique contexts.
@@ -110,4 +130,5 @@ export {
popHostContext,
pushHostContainer,
pushHostContext,
pushHostContextForEvent,
};
@@ -0,0 +1,361 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
* @jest-environment node
*/
'use strict';
let React;
let ReactNoop;
let Scheduler;
let ReactFeatureFlags;
let EventComponent;
let ReactTestRenderer;
let EventTarget;
let ReactEvents;
const noOpResponder = {
targetEventTypes: [],
handleEvent() {},
};
function createReactEventComponent() {
return {
$$typeof: Symbol.for('react.event_component'),
props: null,
responder: noOpResponder,
};
}
function init() {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.enableEventAPI = true;
React = require('react');
Scheduler = require('scheduler');
ReactEvents = require('react-events');
}
function initNoopRenderer() {
init();
ReactNoop = require('react-noop-renderer');
}
function initTestRenderer() {
init();
ReactTestRenderer = require('react-test-renderer');
}
// This is a new feature in Fiber so I put it in its own test file. It could
// probably move to one of the other test files once it is official.
describe('ReactTopLevelText', () => {
describe('NoopRenderer', () => {
beforeEach(() => {
initNoopRenderer();
EventComponent = createReactEventComponent();
EventTarget = ReactEvents.TouchHitTarget;
});
it('should render a simple event component with a single child', () => {
const Test = () => (
<EventComponent>
<div>Hello world</div>
</EventComponent>
);
ReactNoop.render(<Test />);
expect(Scheduler).toFlushWithoutYielding();
expect(ReactNoop).toMatchRenderedOutput(<div>Hello world</div>);
});
it('should warn when an event component has a direct text child', () => {
const Test = () => <EventComponent>Hello world</EventComponent>;
expect(() => {
ReactNoop.render(<Test />);
expect(Scheduler).toFlushWithoutYielding();
}).toWarnDev(
'Warning: validateDOMNesting: React event components cannot have text DOM nodes as children. ' +
'Wrap the child text "Hello world" in an element.',
);
});
it('should warn when an event component has a direct text child #2', () => {
const ChildWrapper = () => 'Hello world';
const Test = () => (
<EventComponent>
<ChildWrapper />
</EventComponent>
);
expect(() => {
ReactNoop.render(<Test />);
expect(Scheduler).toFlushWithoutYielding();
}).toWarnDev(
'Warning: validateDOMNesting: React event components cannot have text DOM nodes as children. ' +
'Wrap the child text "Hello world" in an element.',
);
});
it('should render a simple event component with a single event target', () => {
const Test = () => (
<EventComponent>
<EventTarget>
<div>Hello world</div>
</EventTarget>
</EventComponent>
);
ReactNoop.render(<Test />);
expect(Scheduler).toFlushWithoutYielding();
expect(ReactNoop).toMatchRenderedOutput(<div>Hello world</div>);
});
it('should warn when an event target has a direct text child', () => {
const Test = () => (
<EventComponent>
<EventTarget>Hello world</EventTarget>
</EventComponent>
);
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.',
'Warning: <TouchHitTarget> must have a single DOM element as a child. Found no children.',
]);
});
it('should warn when an event target has a direct text child #2', () => {
const ChildWrapper = () => 'Hello world';
const Test = () => (
<EventComponent>
<EventTarget>
<ChildWrapper />
</EventTarget>
</EventComponent>
);
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.',
'Warning: <TouchHitTarget> must have a single DOM element as a child. Found no children.',
]);
});
it('should warn when an event target has more than one child', () => {
const Test = () => (
<EventComponent>
<EventTarget>
<span>Child 1</span>
<span>Child 2</span>
</EventTarget>
</EventComponent>
);
expect(() => {
ReactNoop.render(<Test />);
expect(Scheduler).toFlushWithoutYielding();
}).toWarnDev(
'Warning: <TouchHitTarget> must only have a single DOM element as a child. Found many children.',
);
});
it('should warn if an event target is not a direct child of an event component', () => {
const Test = () => (
<EventComponent>
<div>
<EventTarget>
<span>Child 1</span>
</EventTarget>
</div>
</EventComponent>
);
expect(() => {
ReactNoop.render(<Test />);
expect(Scheduler).toFlushWithoutYielding();
}).toWarnDev(
'Warning: validateDOMNesting: React event targets must be direct children of event components.',
);
});
});
describe('TestRenderer', () => {
beforeEach(() => {
initTestRenderer();
EventComponent = createReactEventComponent();
EventTarget = ReactEvents.TouchHitTarget;
});
it('should render a simple event component with a single child', () => {
const Test = () => (
<EventComponent>
<div>Hello world</div>
</EventComponent>
);
const root = ReactTestRenderer.create(null);
root.update(<Test />);
expect(Scheduler).toFlushWithoutYielding();
expect(root).toMatchRenderedOutput(<div>Hello world</div>);
});
it('should warn when an event component has a direct text child', () => {
const Test = () => <EventComponent>Hello world</EventComponent>;
const root = ReactTestRenderer.create(null);
expect(() => {
root.update(<Test />);
expect(Scheduler).toFlushWithoutYielding();
}).toWarnDev(
'Warning: validateDOMNesting: React event components cannot have text DOM nodes as children. ' +
'Wrap the child text "Hello world" in an element.',
);
});
it('should warn when an event component has a direct text child #2', () => {
const ChildWrapper = () => 'Hello world';
const Test = () => (
<EventComponent>
<ChildWrapper />
</EventComponent>
);
const root = ReactTestRenderer.create(null);
expect(() => {
root.update(<Test />);
expect(Scheduler).toFlushWithoutYielding();
}).toWarnDev(
'Warning: validateDOMNesting: React event components cannot have text DOM nodes as children. ' +
'Wrap the child text "Hello world" in an element.',
);
});
it('should render a simple event component with a single event target', () => {
const Test = () => (
<EventComponent>
<EventTarget>
<div>Hello world</div>
</EventTarget>
</EventComponent>
);
const root = ReactTestRenderer.create(null);
root.update(<Test />);
expect(Scheduler).toFlushWithoutYielding();
expect(root).toMatchRenderedOutput(<div>Hello world</div>);
const Test2 = () => (
<EventComponent>
<EventTarget>
<span>I am now a span</span>
</EventTarget>
</EventComponent>
);
root.update(<Test2 />);
expect(Scheduler).toFlushWithoutYielding();
expect(root).toMatchRenderedOutput(<span>I am now a span</span>);
});
it('should warn when an event target has a direct text child', () => {
const Test = () => (
<EventComponent>
<EventTarget>Hello world</EventTarget>
</EventComponent>
);
const root = ReactTestRenderer.create(null);
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.',
'Warning: <TouchHitTarget> must have a single DOM element as a child. Found no children.',
]);
});
it('should warn when an event target has a direct text child #2', () => {
const ChildWrapper = () => 'Hello world';
const Test = () => (
<EventComponent>
<EventTarget>
<ChildWrapper />
</EventTarget>
</EventComponent>
);
const root = ReactTestRenderer.create(null);
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.',
'Warning: <TouchHitTarget> must have a single DOM element as a child. Found no children.',
]);
});
it('should warn when an event target has more than one child', () => {
const Test = () => (
<EventComponent>
<EventTarget>
<span>Child 1</span>
<span>Child 2</span>
</EventTarget>
</EventComponent>
);
const root = ReactTestRenderer.create(null);
expect(() => {
root.update(<Test />);
expect(Scheduler).toFlushWithoutYielding();
}).toWarnDev(
'Warning: <TouchHitTarget> must only have a single DOM element as a child. Found many children.',
);
// This should not fire a warning, as this is now valid.
const Test2 = () => (
<EventComponent>
<EventTarget>
<span>Child 1</span>
</EventTarget>
</EventComponent>
);
root.update(<Test2 />);
expect(Scheduler).toFlushWithoutYielding();
expect(root).toMatchRenderedOutput(<span>Child 1</span>);
});
it('should warn if an event target is not a direct child of an event component', () => {
const Test = () => (
<EventComponent>
<div>
<EventTarget>
<span>Child 1</span>
</EventTarget>
</div>
</EventComponent>
);
const root = ReactTestRenderer.create(null);
expect(() => {
root.update(<Test />);
expect(Scheduler).toFlushWithoutYielding();
}).toWarnDev(
'Warning: validateDOMNesting: React event targets must be direct children of event components.',
);
});
});
});
@@ -31,6 +31,9 @@ describe('ReactFiberHostContext', () => {
getChildHostContext: function() {
return null;
},
getChildHostContextForEvent: function() {
return null;
},
shouldSetTextContent: function() {
return false;
},
@@ -41,6 +41,8 @@ export opaque type NoTimeout = mixed; // eslint-disable-line no-undef
export const getPublicInstance = $$$hostConfig.getPublicInstance;
export const getRootHostContext = $$$hostConfig.getRootHostContext;
export const getChildHostContext = $$$hostConfig.getChildHostContext;
export const getChildHostContextForEvent =
$$$hostConfig.getChildHostContextForEvent;
export const prepareForCommit = $$$hostConfig.prepareForCommit;
export const resetAfterCommit = $$$hostConfig.resetAfterCommit;
export const createInstance = $$$hostConfig.createInstance;
+47 -2
View File
@@ -10,6 +10,14 @@
import warning from 'shared/warning';
import type {ReactEventResponder} from 'shared/ReactTypes';
import {
REACT_EVENT_COMPONENT_TYPE,
REACT_EVENT_TARGET_TYPE,
REACT_EVENT_TARGET_TOUCH_HIT,
} from 'shared/ReactSymbols';
import getElementFromTouchHitTarget from 'shared/getElementFromTouchHitTarget';
import {enableEventAPI} from 'shared/ReactFeatureFlags';
export type Type = string;
export type Props = Object;
@@ -42,6 +50,8 @@ export type NoTimeout = -1;
export * from 'shared/HostConfigWithNoPersistence';
export * from 'shared/HostConfigWithNoHydration';
const EVENT_COMPONENT_CONTEXT = {};
const EVENT_TARGET_CONTEXT = {};
const NO_CONTEXT = {};
const UPDATE_SIGNAL = {};
if (__DEV__) {
@@ -117,6 +127,24 @@ export function getChildHostContext(
return NO_CONTEXT;
}
export function getChildHostContextForEvent(
parentHostContext: HostContext,
type: Symbol | number,
): HostContext {
if (__DEV__ && enableEventAPI) {
if (type === REACT_EVENT_COMPONENT_TYPE) {
return EVENT_COMPONENT_CONTEXT;
} else if (type === REACT_EVENT_TARGET_TYPE) {
warning(
parentHostContext === EVENT_COMPONENT_CONTEXT,
'validateDOMNesting: React event targets must be direct children of event components.',
);
return EVENT_TARGET_CONTEXT;
}
}
return NO_CONTEXT;
}
export function prepareForCommit(containerInfo: Container): void {
// noop
}
@@ -188,6 +216,20 @@ export function createTextInstance(
hostContext: Object,
internalInstanceHandle: Object,
): TextInstance {
if (__DEV__ && enableEventAPI) {
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,
);
}
return {
text,
isHidden: false,
@@ -272,9 +314,12 @@ export function handleEventComponent(
}
export function handleEventTarget(
type: string,
type: Symbol | number,
props: Props,
internalInstanceHandle: Object,
) {
// TODO: add handleEventTarget implementation
if (type === REACT_EVENT_TARGET_TOUCH_HIT) {
// Validates that there is a single element
getElementFromTouchHitTarget(internalInstanceHandle);
}
}
+1 -1
View File
@@ -47,7 +47,7 @@ export const REACT_SUSPENSE_TYPE = hasSymbol
export const REACT_MEMO_TYPE = hasSymbol ? Symbol.for('react.memo') : 0xead3;
export const REACT_LAZY_TYPE = hasSymbol ? Symbol.for('react.lazy') : 0xead4;
export const REACT_EVENT_COMPONENT_TYPE = hasSymbol
? Symbol.for('react.event')
? Symbol.for('react.event_component')
: 0xead5;
export const REACT_EVENT_TARGET_TYPE = hasSymbol
? Symbol.for('react.event_target')
+7 -3
View File
@@ -14,7 +14,7 @@ export type ReactNode =
| ReactFragment
| ReactProvider<any>
| ReactConsumer<any>
| ReactEvent
| ReactEventComponent
| ReactEventTarget;
export type ReactEmpty = null | void | boolean;
@@ -81,13 +81,17 @@ export type RefObject = {|
current: any,
|};
export type ReactEventResponderEventType =
| string
| {name: string, passive?: boolean, capture?: boolean};
export type ReactEventResponder = {
targetEventTypes: Array<string>,
targetEventTypes: Array<ReactEventResponderEventType>,
createInitialState?: (props: Object) => Object,
handleEvent: (context: Object, props: Object, state: Object) => void,
};
export type ReactEvent = {|
export type ReactEventComponent = {|
$$typeof: Symbol | number,
props: null | Object,
responder: ReactEventResponder,
@@ -27,6 +27,7 @@ export const enableStableConcurrentModeAPIs = false;
export const warnAboutShorthandPropertyCollision = false;
export const enableSchedulerDebugging = false;
export const warnAboutDeprecatedSetNativeProps = false;
export const enableEventAPI = false;
// Only used in www builds.
export function addUserTimingListener() {
@@ -0,0 +1,85 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {Fiber} from 'react-reconciler/src/ReactFiber';
import {HostComponent} from 'shared/ReactWorkTags';
import warning from 'shared/warning';
type HostContext = Object;
type TextInstance =
| Text
| {|
text: string,
id: number,
hidden: boolean,
context: HostContext,
|};
type Instance =
| Element
| {|
type: string,
id: number,
children: Array<Instance | TextInstance>,
text: string | null,
prop: any,
hidden: boolean,
context: HostContext,
|};
export default function getElementFromTouchHitTarget(
targetFiber: Fiber,
): null | Instance {
// Traverse through child fibers and find the first host components
let node = targetFiber.child;
let hostComponent = null;
while (node !== null) {
if (node.tag === HostComponent) {
if (__DEV__) {
if (hostComponent === null) {
hostComponent = node.stateNode;
} else {
warning(
false,
'<TouchHitTarget> must only have a single DOM element as a child. ' +
'Found many children.',
);
}
while (node !== null) {
if (node === targetFiber) {
return hostComponent;
} else if (node.sibling !== null) {
node = node.sibling;
break;
}
node = node.return;
}
} else {
return node.stateNode;
}
} else if (node.child !== null) {
node = node.child;
} else if (node.sibling !== null) {
node = node.sibling;
} else {
break;
}
}
if (__DEV__) {
warning(
hostComponent !== null,
'<TouchHitTarget> must have a single DOM element as a child. ' +
'Found no children.',
);
}
return hostComponent;
}
File diff suppressed because it is too large Load Diff