mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
1025 lines
29 KiB
JavaScript
1025 lines
29 KiB
JavaScript
/**
|
||
* 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 * as Scheduler from 'scheduler';
|
||
|
||
import {precacheFiberNode, updateFiberProps} from './ReactDOMComponentTree';
|
||
import {
|
||
createElement,
|
||
createTextNode,
|
||
setInitialProperties,
|
||
diffProperties,
|
||
updateProperties,
|
||
diffHydratedProperties,
|
||
diffHydratedText,
|
||
trapClickOnNonInteractiveElement,
|
||
warnForUnmatchedText,
|
||
warnForDeletedHydratableElement,
|
||
warnForDeletedHydratableText,
|
||
warnForInsertedHydratedElement,
|
||
warnForInsertedHydratedText,
|
||
listenToEventResponderEventTypes,
|
||
} from './ReactDOMComponent';
|
||
import {getSelectionInformation, restoreSelection} from './ReactInputSelection';
|
||
import setTextContent from './setTextContent';
|
||
import {validateDOMNesting, updatedAncestorInfo} from './validateDOMNesting';
|
||
import {
|
||
isEnabled as ReactBrowserEventEmitterIsEnabled,
|
||
setEnabled as ReactBrowserEventEmitterSetEnabled,
|
||
} from '../events/ReactBrowserEventEmitter';
|
||
import {Namespaces, getChildNamespace} from '../shared/DOMNamespaces';
|
||
import {addRootEventTypesForComponentInstance} from '../events/DOMEventResponderSystem';
|
||
import {
|
||
ELEMENT_NODE,
|
||
TEXT_NODE,
|
||
COMMENT_NODE,
|
||
DOCUMENT_NODE,
|
||
DOCUMENT_FRAGMENT_NODE,
|
||
} from '../shared/HTMLNodeType';
|
||
import dangerousStyleValue from '../shared/dangerousStyleValue';
|
||
|
||
import type {DOMContainer} from './ReactDOM';
|
||
import type {
|
||
ReactDOMEventResponder,
|
||
ReactDOMEventComponentInstance,
|
||
} from 'shared/ReactDOMTypes';
|
||
import {
|
||
mountEventResponder,
|
||
unmountEventResponder,
|
||
} from '../events/DOMEventResponderSystem';
|
||
import {REACT_EVENT_TARGET_TOUCH_HIT} from 'shared/ReactSymbols';
|
||
import {canUseDOM} from 'shared/ExecutionEnvironment';
|
||
|
||
export type Type = string;
|
||
export type Props = {
|
||
autoFocus?: boolean,
|
||
children?: mixed,
|
||
hidden?: boolean,
|
||
suppressHydrationWarning?: boolean,
|
||
dangerouslySetInnerHTML?: mixed,
|
||
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;
|
||
export type TextInstance = Text;
|
||
export type SuspenseInstance = Comment & {_reactRetry?: () => void};
|
||
export type HydratableInstance = Instance | TextInstance | SuspenseInstance;
|
||
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;
|
||
export type UpdatePayload = Array<mixed>;
|
||
export type ChildSet = void; // Unused
|
||
export type TimeoutHandle = TimeoutID;
|
||
export type NoTimeout = -1;
|
||
|
||
import {
|
||
enableSuspenseServerRenderer,
|
||
enableEventAPI,
|
||
} 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 {
|
||
unstable_now: now,
|
||
unstable_scheduleCallback: scheduleDeferredCallback,
|
||
unstable_shouldYield: shouldYield,
|
||
unstable_cancelCallback: cancelDeferredCallback,
|
||
} = Scheduler;
|
||
|
||
export {now, scheduleDeferredCallback, shouldYield, cancelDeferredCallback};
|
||
|
||
let SUPPRESS_HYDRATION_WARNING;
|
||
if (__DEV__) {
|
||
SUPPRESS_HYDRATION_WARNING = 'suppressHydrationWarning';
|
||
}
|
||
|
||
const SUSPENSE_START_DATA = '$';
|
||
const SUSPENSE_END_DATA = '/$';
|
||
const SUSPENSE_PENDING_START_DATA = '$?';
|
||
const SUSPENSE_FALLBACK_START_DATA = '$!';
|
||
|
||
const STYLE = 'style';
|
||
|
||
let eventsEnabled: ?boolean = null;
|
||
let selectionInformation: ?mixed = null;
|
||
|
||
function shouldAutoFocusHostComponent(type: string, props: Props): boolean {
|
||
switch (type) {
|
||
case 'button':
|
||
case 'input':
|
||
case 'select':
|
||
case 'textarea':
|
||
return !!props.autoFocus;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
export * from 'shared/HostConfigWithNoPersistence';
|
||
|
||
export function getRootHostContext(
|
||
rootContainerInstance: Container,
|
||
): HostContext {
|
||
let type;
|
||
let namespace;
|
||
const nodeType = rootContainerInstance.nodeType;
|
||
switch (nodeType) {
|
||
case DOCUMENT_NODE:
|
||
case DOCUMENT_FRAGMENT_NODE: {
|
||
type = nodeType === DOCUMENT_NODE ? '#document' : '#fragment';
|
||
let root = (rootContainerInstance: any).documentElement;
|
||
namespace = root ? root.namespaceURI : getChildNamespace(null, '');
|
||
break;
|
||
}
|
||
default: {
|
||
const container: any =
|
||
nodeType === COMMENT_NODE
|
||
? rootContainerInstance.parentNode
|
||
: rootContainerInstance;
|
||
const ownNamespace = container.namespaceURI || null;
|
||
type = container.tagName;
|
||
namespace = getChildNamespace(ownNamespace, type);
|
||
break;
|
||
}
|
||
}
|
||
if (__DEV__) {
|
||
const validatedTag = type.toLowerCase();
|
||
const ancestorInfo = updatedAncestorInfo(null, validatedTag);
|
||
return {namespace, ancestorInfo, eventData: null};
|
||
}
|
||
return namespace;
|
||
}
|
||
|
||
export function getChildHostContext(
|
||
parentHostContext: HostContext,
|
||
type: string,
|
||
rootContainerInstance: Container,
|
||
): HostContext {
|
||
if (__DEV__) {
|
||
const parentHostContextDev = ((parentHostContext: any): HostContextDev);
|
||
const namespace = getChildNamespace(parentHostContextDev.namespace, type);
|
||
const ancestorInfo = updatedAncestorInfo(
|
||
parentHostContextDev.ancestorInfo,
|
||
type,
|
||
);
|
||
return {namespace, ancestorInfo, eventData: null};
|
||
}
|
||
const parentNamespace = ((parentHostContext: any): HostContextProd);
|
||
return getChildNamespace(parentNamespace, type);
|
||
}
|
||
|
||
export function getChildHostContextForEventComponent(
|
||
parentHostContext: HostContext,
|
||
): HostContext {
|
||
if (__DEV__) {
|
||
const parentHostContextDev = ((parentHostContext: any): HostContextDev);
|
||
const {namespace, ancestorInfo} = parentHostContextDev;
|
||
warning(
|
||
parentHostContextDev.eventData === null ||
|
||
!parentHostContextDev.eventData.isEventTarget,
|
||
'validateDOMNesting: React event targets must not have event components as children.',
|
||
);
|
||
const eventData = {
|
||
isEventComponent: true,
|
||
isEventTarget: false,
|
||
};
|
||
return {namespace, ancestorInfo, eventData};
|
||
}
|
||
return parentHostContext;
|
||
}
|
||
|
||
export function getChildHostContextForEventTarget(
|
||
parentHostContext: HostContext,
|
||
type: Symbol | number,
|
||
): HostContext {
|
||
if (__DEV__) {
|
||
const parentHostContextDev = ((parentHostContext: any): HostContextDev);
|
||
const {namespace, ancestorInfo} = parentHostContextDev;
|
||
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,
|
||
};
|
||
return {namespace, ancestorInfo, eventData};
|
||
}
|
||
return parentHostContext;
|
||
}
|
||
|
||
export function getPublicInstance(instance: Instance): * {
|
||
return instance;
|
||
}
|
||
|
||
export function prepareForCommit(containerInfo: Container): void {
|
||
eventsEnabled = ReactBrowserEventEmitterIsEnabled();
|
||
selectionInformation = getSelectionInformation();
|
||
ReactBrowserEventEmitterSetEnabled(false);
|
||
}
|
||
|
||
export function resetAfterCommit(containerInfo: Container): void {
|
||
restoreSelection(selectionInformation);
|
||
selectionInformation = null;
|
||
ReactBrowserEventEmitterSetEnabled(eventsEnabled);
|
||
eventsEnabled = null;
|
||
}
|
||
|
||
export function createInstance(
|
||
type: string,
|
||
props: Props,
|
||
rootContainerInstance: Container,
|
||
hostContext: HostContext,
|
||
internalInstanceHandle: Object,
|
||
): Instance {
|
||
let parentNamespace: string;
|
||
if (__DEV__) {
|
||
// TODO: take namespace into account when validating.
|
||
const hostContextDev = ((hostContext: any): HostContextDev);
|
||
validateDOMNesting(type, null, hostContextDev.ancestorInfo);
|
||
if (
|
||
typeof props.children === 'string' ||
|
||
typeof props.children === 'number'
|
||
) {
|
||
const string = '' + props.children;
|
||
const ownAncestorInfo = updatedAncestorInfo(
|
||
hostContextDev.ancestorInfo,
|
||
type,
|
||
);
|
||
validateDOMNesting(null, string, ownAncestorInfo);
|
||
}
|
||
parentNamespace = hostContextDev.namespace;
|
||
} else {
|
||
parentNamespace = ((hostContext: any): HostContextProd);
|
||
}
|
||
const domElement: Instance = createElement(
|
||
type,
|
||
props,
|
||
rootContainerInstance,
|
||
parentNamespace,
|
||
);
|
||
precacheFiberNode(internalInstanceHandle, domElement);
|
||
updateFiberProps(domElement, props);
|
||
return domElement;
|
||
}
|
||
|
||
export function appendInitialChild(
|
||
parentInstance: Instance,
|
||
child: Instance | TextInstance,
|
||
): void {
|
||
parentInstance.appendChild(child);
|
||
}
|
||
|
||
export function finalizeInitialChildren(
|
||
domElement: Instance,
|
||
type: string,
|
||
props: Props,
|
||
rootContainerInstance: Container,
|
||
hostContext: HostContext,
|
||
): boolean {
|
||
setInitialProperties(domElement, type, props, rootContainerInstance);
|
||
return shouldAutoFocusHostComponent(type, props);
|
||
}
|
||
|
||
export function prepareUpdate(
|
||
domElement: Instance,
|
||
type: string,
|
||
oldProps: Props,
|
||
newProps: Props,
|
||
rootContainerInstance: Container,
|
||
hostContext: HostContext,
|
||
): null | Array<mixed> {
|
||
if (__DEV__) {
|
||
const hostContextDev = ((hostContext: any): HostContextDev);
|
||
if (
|
||
typeof newProps.children !== typeof oldProps.children &&
|
||
(typeof newProps.children === 'string' ||
|
||
typeof newProps.children === 'number')
|
||
) {
|
||
const string = '' + newProps.children;
|
||
const ownAncestorInfo = updatedAncestorInfo(
|
||
hostContextDev.ancestorInfo,
|
||
type,
|
||
);
|
||
validateDOMNesting(null, string, ownAncestorInfo);
|
||
}
|
||
}
|
||
return diffProperties(
|
||
domElement,
|
||
type,
|
||
oldProps,
|
||
newProps,
|
||
rootContainerInstance,
|
||
);
|
||
}
|
||
|
||
export function shouldSetTextContent(type: string, props: Props): boolean {
|
||
return (
|
||
type === 'textarea' ||
|
||
type === 'option' ||
|
||
type === 'noscript' ||
|
||
typeof props.children === 'string' ||
|
||
typeof props.children === 'number' ||
|
||
(typeof props.dangerouslySetInnerHTML === 'object' &&
|
||
props.dangerouslySetInnerHTML !== null &&
|
||
props.dangerouslySetInnerHTML.__html != null)
|
||
);
|
||
}
|
||
|
||
export function shouldDeprioritizeSubtree(type: string, props: Props): boolean {
|
||
return !!props.hidden;
|
||
}
|
||
|
||
export function createTextInstance(
|
||
text: string,
|
||
rootContainerInstance: Container,
|
||
hostContext: HostContext,
|
||
internalInstanceHandle: Object,
|
||
): TextInstance {
|
||
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,
|
||
);
|
||
}
|
||
}
|
||
}
|
||
const textNode: TextInstance = createTextNode(text, rootContainerInstance);
|
||
precacheFiberNode(internalInstanceHandle, textNode);
|
||
return textNode;
|
||
}
|
||
|
||
export const isPrimaryRenderer = true;
|
||
// This initialization code may run even on server environments
|
||
// if a component just imports ReactDOM (e.g. for findDOMNode).
|
||
// Some environments might not have setTimeout or clearTimeout.
|
||
export const scheduleTimeout =
|
||
typeof setTimeout === 'function' ? setTimeout : (undefined: any);
|
||
export const cancelTimeout =
|
||
typeof clearTimeout === 'function' ? clearTimeout : (undefined: any);
|
||
export const noTimeout = -1;
|
||
|
||
// -------------------
|
||
// Mutation
|
||
// -------------------
|
||
|
||
export const supportsMutation = true;
|
||
|
||
export function commitMount(
|
||
domElement: Instance,
|
||
type: string,
|
||
newProps: Props,
|
||
internalInstanceHandle: Object,
|
||
): void {
|
||
// Despite the naming that might imply otherwise, this method only
|
||
// fires if there is an `Update` effect scheduled during mounting.
|
||
// This happens if `finalizeInitialChildren` returns `true` (which it
|
||
// does to implement the `autoFocus` attribute on the client). But
|
||
// there are also other cases when this might happen (such as patching
|
||
// up text content during hydration mismatch). So we'll check this again.
|
||
if (shouldAutoFocusHostComponent(type, newProps)) {
|
||
((domElement: any):
|
||
| HTMLButtonElement
|
||
| HTMLInputElement
|
||
| HTMLSelectElement
|
||
| HTMLTextAreaElement).focus();
|
||
}
|
||
}
|
||
|
||
export function commitUpdate(
|
||
domElement: Instance,
|
||
updatePayload: Array<mixed>,
|
||
type: string,
|
||
oldProps: Props,
|
||
newProps: Props,
|
||
internalInstanceHandle: Object,
|
||
): void {
|
||
// Update the props handle so that we know which props are the ones with
|
||
// with current event handlers.
|
||
updateFiberProps(domElement, newProps);
|
||
// Apply the diff to the DOM node.
|
||
updateProperties(domElement, updatePayload, type, oldProps, newProps);
|
||
}
|
||
|
||
export function resetTextContent(domElement: Instance): void {
|
||
setTextContent(domElement, '');
|
||
}
|
||
|
||
export function commitTextUpdate(
|
||
textInstance: TextInstance,
|
||
oldText: string,
|
||
newText: string,
|
||
): void {
|
||
textInstance.nodeValue = newText;
|
||
}
|
||
|
||
export function appendChild(
|
||
parentInstance: Instance,
|
||
child: Instance | TextInstance,
|
||
): void {
|
||
parentInstance.appendChild(child);
|
||
}
|
||
|
||
export function appendChildToContainer(
|
||
container: DOMContainer,
|
||
child: Instance | TextInstance,
|
||
): void {
|
||
let parentNode;
|
||
if (container.nodeType === COMMENT_NODE) {
|
||
parentNode = (container.parentNode: any);
|
||
parentNode.insertBefore(child, container);
|
||
} else {
|
||
parentNode = container;
|
||
parentNode.appendChild(child);
|
||
}
|
||
// This container might be used for a portal.
|
||
// If something inside a portal is clicked, that click should bubble
|
||
// through the React tree. However, on Mobile Safari the click would
|
||
// never bubble through the *DOM* tree unless an ancestor with onclick
|
||
// event exists. So we wouldn't see it and dispatch it.
|
||
// This is why we ensure that non React root containers have inline onclick
|
||
// defined.
|
||
// https://github.com/facebook/react/issues/11918
|
||
const reactRootContainer = container._reactRootContainer;
|
||
if (
|
||
(reactRootContainer === null || reactRootContainer === undefined) &&
|
||
parentNode.onclick === null
|
||
) {
|
||
// TODO: This cast may not be sound for SVG, MathML or custom elements.
|
||
trapClickOnNonInteractiveElement(((parentNode: any): HTMLElement));
|
||
}
|
||
}
|
||
|
||
export function insertBefore(
|
||
parentInstance: Instance,
|
||
child: Instance | TextInstance,
|
||
beforeChild: Instance | TextInstance | SuspenseInstance,
|
||
): void {
|
||
parentInstance.insertBefore(child, beforeChild);
|
||
}
|
||
|
||
export function insertInContainerBefore(
|
||
container: Container,
|
||
child: Instance | TextInstance,
|
||
beforeChild: Instance | TextInstance | SuspenseInstance,
|
||
): void {
|
||
if (container.nodeType === COMMENT_NODE) {
|
||
(container.parentNode: any).insertBefore(child, beforeChild);
|
||
} else {
|
||
container.insertBefore(child, beforeChild);
|
||
}
|
||
}
|
||
|
||
export function removeChild(
|
||
parentInstance: Instance,
|
||
child: Instance | TextInstance | SuspenseInstance,
|
||
): void {
|
||
parentInstance.removeChild(child);
|
||
}
|
||
|
||
export function removeChildFromContainer(
|
||
container: Container,
|
||
child: Instance | TextInstance | SuspenseInstance,
|
||
): void {
|
||
if (container.nodeType === COMMENT_NODE) {
|
||
(container.parentNode: any).removeChild(child);
|
||
} else {
|
||
container.removeChild(child);
|
||
}
|
||
}
|
||
|
||
export function clearSuspenseBoundary(
|
||
parentInstance: Instance,
|
||
suspenseInstance: SuspenseInstance,
|
||
): void {
|
||
let node = suspenseInstance;
|
||
// Delete all nodes within this suspense boundary.
|
||
// There might be nested nodes so we need to keep track of how
|
||
// deep we are and only break out when we're back on top.
|
||
let depth = 0;
|
||
do {
|
||
let nextNode = node.nextSibling;
|
||
parentInstance.removeChild(node);
|
||
if (nextNode && nextNode.nodeType === COMMENT_NODE) {
|
||
let data = ((nextNode: any).data: string);
|
||
if (data === SUSPENSE_END_DATA) {
|
||
if (depth === 0) {
|
||
parentInstance.removeChild(nextNode);
|
||
return;
|
||
} else {
|
||
depth--;
|
||
}
|
||
} else if (
|
||
data === SUSPENSE_START_DATA ||
|
||
data === SUSPENSE_PENDING_START_DATA ||
|
||
data === SUSPENSE_FALLBACK_START_DATA
|
||
) {
|
||
depth++;
|
||
}
|
||
}
|
||
node = nextNode;
|
||
} while (node);
|
||
// TODO: Warn, we didn't find the end comment boundary.
|
||
}
|
||
|
||
export function clearSuspenseBoundaryFromContainer(
|
||
container: Container,
|
||
suspenseInstance: SuspenseInstance,
|
||
): void {
|
||
if (container.nodeType === COMMENT_NODE) {
|
||
clearSuspenseBoundary((container.parentNode: any), suspenseInstance);
|
||
} else if (container.nodeType === ELEMENT_NODE) {
|
||
clearSuspenseBoundary((container: any), suspenseInstance);
|
||
} else {
|
||
// Document nodes should never contain suspense boundaries.
|
||
}
|
||
}
|
||
|
||
export function hideInstance(instance: Instance): void {
|
||
// TODO: Does this work for all element types? What about MathML? Should we
|
||
// pass host context to this method?
|
||
instance = ((instance: any): HTMLElement);
|
||
const style = instance.style;
|
||
if (typeof style.setProperty === 'function') {
|
||
style.setProperty('display', 'none', 'important');
|
||
} else {
|
||
style.display = 'none';
|
||
}
|
||
}
|
||
|
||
export function hideTextInstance(textInstance: TextInstance): void {
|
||
textInstance.nodeValue = '';
|
||
}
|
||
|
||
export function unhideInstance(instance: Instance, props: Props): void {
|
||
instance = ((instance: any): HTMLElement);
|
||
const styleProp = props[STYLE];
|
||
const display =
|
||
styleProp !== undefined &&
|
||
styleProp !== null &&
|
||
styleProp.hasOwnProperty('display')
|
||
? styleProp.display
|
||
: null;
|
||
instance.style.display = dangerousStyleValue('display', display);
|
||
}
|
||
|
||
export function unhideTextInstance(
|
||
textInstance: TextInstance,
|
||
text: string,
|
||
): void {
|
||
textInstance.nodeValue = text;
|
||
}
|
||
|
||
// -------------------
|
||
// Hydration
|
||
// -------------------
|
||
|
||
export const supportsHydration = true;
|
||
|
||
export function canHydrateInstance(
|
||
instance: HydratableInstance,
|
||
type: string,
|
||
props: Props,
|
||
): null | Instance {
|
||
if (
|
||
instance.nodeType !== ELEMENT_NODE ||
|
||
type.toLowerCase() !== instance.nodeName.toLowerCase()
|
||
) {
|
||
return null;
|
||
}
|
||
// This has now been refined to an element node.
|
||
return ((instance: any): Instance);
|
||
}
|
||
|
||
export function canHydrateTextInstance(
|
||
instance: HydratableInstance,
|
||
text: string,
|
||
): null | TextInstance {
|
||
if (text === '' || instance.nodeType !== TEXT_NODE) {
|
||
// Empty strings are not parsed by HTML so there won't be a correct match here.
|
||
return null;
|
||
}
|
||
// This has now been refined to a text node.
|
||
return ((instance: any): TextInstance);
|
||
}
|
||
|
||
export function canHydrateSuspenseInstance(
|
||
instance: HydratableInstance,
|
||
): null | SuspenseInstance {
|
||
if (instance.nodeType !== COMMENT_NODE) {
|
||
// Empty strings are not parsed by HTML so there won't be a correct match here.
|
||
return null;
|
||
}
|
||
// This has now been refined to a suspense node.
|
||
return ((instance: any): SuspenseInstance);
|
||
}
|
||
|
||
export function isSuspenseInstancePending(instance: SuspenseInstance) {
|
||
return instance.data === SUSPENSE_PENDING_START_DATA;
|
||
}
|
||
|
||
export function isSuspenseInstanceFallback(instance: SuspenseInstance) {
|
||
return instance.data === SUSPENSE_FALLBACK_START_DATA;
|
||
}
|
||
|
||
export function registerSuspenseInstanceRetry(
|
||
instance: SuspenseInstance,
|
||
callback: () => void,
|
||
) {
|
||
instance._reactRetry = callback;
|
||
}
|
||
|
||
export function getNextHydratableSibling(
|
||
instance: HydratableInstance,
|
||
): null | HydratableInstance {
|
||
let node = instance.nextSibling;
|
||
// Skip non-hydratable nodes.
|
||
while (
|
||
node &&
|
||
node.nodeType !== ELEMENT_NODE &&
|
||
node.nodeType !== TEXT_NODE &&
|
||
(!enableSuspenseServerRenderer ||
|
||
node.nodeType !== COMMENT_NODE ||
|
||
((node: any).data !== SUSPENSE_START_DATA &&
|
||
(node: any).data !== SUSPENSE_PENDING_START_DATA &&
|
||
(node: any).data !== SUSPENSE_FALLBACK_START_DATA))
|
||
) {
|
||
node = node.nextSibling;
|
||
}
|
||
return (node: any);
|
||
}
|
||
|
||
export function getFirstHydratableChild(
|
||
parentInstance: Container | Instance,
|
||
): null | HydratableInstance {
|
||
let next = parentInstance.firstChild;
|
||
// Skip non-hydratable nodes.
|
||
while (
|
||
next &&
|
||
next.nodeType !== ELEMENT_NODE &&
|
||
next.nodeType !== TEXT_NODE &&
|
||
(!enableSuspenseServerRenderer ||
|
||
next.nodeType !== COMMENT_NODE ||
|
||
((next: any).data !== SUSPENSE_START_DATA &&
|
||
(next: any).data !== SUSPENSE_FALLBACK_START_DATA &&
|
||
(next: any).data !== SUSPENSE_PENDING_START_DATA))
|
||
) {
|
||
next = next.nextSibling;
|
||
}
|
||
return (next: any);
|
||
}
|
||
|
||
export function hydrateInstance(
|
||
instance: Instance,
|
||
type: string,
|
||
props: Props,
|
||
rootContainerInstance: Container,
|
||
hostContext: HostContext,
|
||
internalInstanceHandle: Object,
|
||
): null | Array<mixed> {
|
||
precacheFiberNode(internalInstanceHandle, instance);
|
||
// TODO: Possibly defer this until the commit phase where all the events
|
||
// get attached.
|
||
updateFiberProps(instance, props);
|
||
let parentNamespace: string;
|
||
if (__DEV__) {
|
||
const hostContextDev = ((hostContext: any): HostContextDev);
|
||
parentNamespace = hostContextDev.namespace;
|
||
} else {
|
||
parentNamespace = ((hostContext: any): HostContextProd);
|
||
}
|
||
return diffHydratedProperties(
|
||
instance,
|
||
type,
|
||
props,
|
||
parentNamespace,
|
||
rootContainerInstance,
|
||
);
|
||
}
|
||
|
||
export function hydrateTextInstance(
|
||
textInstance: TextInstance,
|
||
text: string,
|
||
internalInstanceHandle: Object,
|
||
): boolean {
|
||
precacheFiberNode(internalInstanceHandle, textInstance);
|
||
return diffHydratedText(textInstance, text);
|
||
}
|
||
|
||
export function getNextHydratableInstanceAfterSuspenseInstance(
|
||
suspenseInstance: SuspenseInstance,
|
||
): null | HydratableInstance {
|
||
let node = suspenseInstance.nextSibling;
|
||
// Skip past all nodes within this suspense boundary.
|
||
// There might be nested nodes so we need to keep track of how
|
||
// deep we are and only break out when we're back on top.
|
||
let depth = 0;
|
||
while (node) {
|
||
if (node.nodeType === COMMENT_NODE) {
|
||
let data = ((node: any).data: string);
|
||
if (data === SUSPENSE_END_DATA) {
|
||
if (depth === 0) {
|
||
return getNextHydratableSibling((node: any));
|
||
} else {
|
||
depth--;
|
||
}
|
||
} else if (data === SUSPENSE_START_DATA) {
|
||
depth++;
|
||
}
|
||
}
|
||
node = node.nextSibling;
|
||
}
|
||
// TODO: Warn, we didn't find the end comment boundary.
|
||
return null;
|
||
}
|
||
|
||
export function didNotMatchHydratedContainerTextInstance(
|
||
parentContainer: Container,
|
||
textInstance: TextInstance,
|
||
text: string,
|
||
) {
|
||
if (__DEV__) {
|
||
warnForUnmatchedText(textInstance, text);
|
||
}
|
||
}
|
||
|
||
export function didNotMatchHydratedTextInstance(
|
||
parentType: string,
|
||
parentProps: Props,
|
||
parentInstance: Instance,
|
||
textInstance: TextInstance,
|
||
text: string,
|
||
) {
|
||
if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) {
|
||
warnForUnmatchedText(textInstance, text);
|
||
}
|
||
}
|
||
|
||
export function didNotHydrateContainerInstance(
|
||
parentContainer: Container,
|
||
instance: HydratableInstance,
|
||
) {
|
||
if (__DEV__) {
|
||
if (instance.nodeType === ELEMENT_NODE) {
|
||
warnForDeletedHydratableElement(parentContainer, (instance: any));
|
||
} else if (instance.nodeType === COMMENT_NODE) {
|
||
// TODO: warnForDeletedHydratableSuspenseBoundary
|
||
} else {
|
||
warnForDeletedHydratableText(parentContainer, (instance: any));
|
||
}
|
||
}
|
||
}
|
||
|
||
export function didNotHydrateInstance(
|
||
parentType: string,
|
||
parentProps: Props,
|
||
parentInstance: Instance,
|
||
instance: HydratableInstance,
|
||
) {
|
||
if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) {
|
||
if (instance.nodeType === ELEMENT_NODE) {
|
||
warnForDeletedHydratableElement(parentInstance, (instance: any));
|
||
} else if (instance.nodeType === COMMENT_NODE) {
|
||
// TODO: warnForDeletedHydratableSuspenseBoundary
|
||
} else {
|
||
warnForDeletedHydratableText(parentInstance, (instance: any));
|
||
}
|
||
}
|
||
}
|
||
|
||
export function didNotFindHydratableContainerInstance(
|
||
parentContainer: Container,
|
||
type: string,
|
||
props: Props,
|
||
) {
|
||
if (__DEV__) {
|
||
warnForInsertedHydratedElement(parentContainer, type, props);
|
||
}
|
||
}
|
||
|
||
export function didNotFindHydratableContainerTextInstance(
|
||
parentContainer: Container,
|
||
text: string,
|
||
) {
|
||
if (__DEV__) {
|
||
warnForInsertedHydratedText(parentContainer, text);
|
||
}
|
||
}
|
||
|
||
export function didNotFindHydratableContainerSuspenseInstance(
|
||
parentContainer: Container,
|
||
) {
|
||
if (__DEV__) {
|
||
// TODO: warnForInsertedHydratedSupsense(parentContainer);
|
||
}
|
||
}
|
||
|
||
export function didNotFindHydratableInstance(
|
||
parentType: string,
|
||
parentProps: Props,
|
||
parentInstance: Instance,
|
||
type: string,
|
||
props: Props,
|
||
) {
|
||
if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) {
|
||
warnForInsertedHydratedElement(parentInstance, type, props);
|
||
}
|
||
}
|
||
|
||
export function didNotFindHydratableTextInstance(
|
||
parentType: string,
|
||
parentProps: Props,
|
||
parentInstance: Instance,
|
||
text: string,
|
||
) {
|
||
if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) {
|
||
warnForInsertedHydratedText(parentInstance, text);
|
||
}
|
||
}
|
||
|
||
export function didNotFindHydratableSuspenseInstance(
|
||
parentType: string,
|
||
parentProps: Props,
|
||
parentInstance: Instance,
|
||
) {
|
||
if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) {
|
||
// TODO: warnForInsertedHydratedSuspense(parentInstance);
|
||
}
|
||
}
|
||
|
||
export function mountEventComponent(
|
||
eventComponentInstance: ReactDOMEventComponentInstance,
|
||
): void {
|
||
if (enableEventAPI) {
|
||
const rootContainerInstance = ((eventComponentInstance.rootInstance: any): Container);
|
||
const doc = rootContainerInstance.ownerDocument;
|
||
const documentBody = doc.body || doc;
|
||
const responder = eventComponentInstance.responder;
|
||
const {
|
||
rootEventTypes,
|
||
targetEventTypes,
|
||
} = ((responder: any): ReactDOMEventResponder);
|
||
if (targetEventTypes !== undefined) {
|
||
listenToEventResponderEventTypes(targetEventTypes, documentBody);
|
||
}
|
||
if (rootEventTypes !== undefined) {
|
||
addRootEventTypesForComponentInstance(
|
||
eventComponentInstance,
|
||
rootEventTypes,
|
||
);
|
||
listenToEventResponderEventTypes(rootEventTypes, documentBody);
|
||
}
|
||
mountEventResponder(eventComponentInstance);
|
||
}
|
||
}
|
||
|
||
export function updateEventComponent(
|
||
eventComponentInstance: ReactDOMEventComponentInstance,
|
||
): void {
|
||
// NO-OP, why might use this in the future
|
||
}
|
||
|
||
export function unmountEventComponent(
|
||
eventComponentInstance: ReactDOMEventComponentInstance,
|
||
): void {
|
||
if (enableEventAPI) {
|
||
// TODO stop listening to targetEventTypes
|
||
unmountEventResponder(eventComponentInstance);
|
||
}
|
||
}
|
||
|
||
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,
|
||
pointerEvents: null,
|
||
bottom: bottom ? `-${bottom}px` : '0px',
|
||
left: left ? `-${left}px` : '0px',
|
||
right: right ? `-${right}px` : '0px',
|
||
top: top ? `-${top}px` : '0px',
|
||
},
|
||
hydrateTouchHitTarget: true,
|
||
suppressHydrationWarning: true,
|
||
},
|
||
};
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
export function handleEventTarget(
|
||
type: Symbol | number,
|
||
props: Props,
|
||
rootContainerInstance: Container,
|
||
internalInstanceHandle: Object,
|
||
): boolean {
|
||
if (
|
||
__DEV__ &&
|
||
type === REACT_EVENT_TARGET_TOUCH_HIT &&
|
||
(props.left || props.right || props.top || props.bottom)
|
||
) {
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
export function commitEventTarget(
|
||
type: Symbol | number,
|
||
props: Props,
|
||
instance: Instance,
|
||
parentInstance: Instance,
|
||
): void {
|
||
if (enableEventAPI) {
|
||
if (type === REACT_EVENT_TARGET_TOUCH_HIT) {
|
||
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('z-index') !== '',
|
||
'<TouchHitTarget> inserts an empty <div> with "z-index" of "-1". ' +
|
||
'This requires its parent DOM node to have a "z-index" greater 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.',
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|