/** * 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 {DOMEventName} from '../events/DOMEventNames'; import type {Fiber, FiberRoot} from 'react-reconciler/src/ReactInternalTypes'; import type { BoundingRect, IntersectionObserverOptions, ObserveVisibleRectsCallback, } from 'react-reconciler/src/ReactTestSelectors'; import type {RootType} from './ReactDOMRoot'; import type {ReactScopeInstance} from 'shared/ReactTypes'; import { precacheFiberNode, updateFiberProps, getClosestInstanceFromNode, getFiberFromScopeInstance, getInstanceFromNode as getInstanceFromNodeDOMTree, isContainerMarkedAsRoot, } from './ReactDOMComponentTree'; export {detachDeletedInstance} from './ReactDOMComponentTree'; import {hasRole} from './DOMAccessibilityRoles'; import { createElement, createTextNode, setInitialProperties, diffProperties, updateProperties, diffHydratedProperties, diffHydratedText, trapClickOnNonInteractiveElement, warnForUnmatchedText, warnForDeletedHydratableElement, warnForDeletedHydratableText, warnForInsertedHydratedElement, warnForInsertedHydratedText, } from './ReactDOMComponent'; import {getSelectionInformation, restoreSelection} from './ReactInputSelection'; import setTextContent from './setTextContent'; import {validateDOMNesting, updatedAncestorInfo} from './validateDOMNesting'; import { isEnabled as ReactBrowserEventEmitterIsEnabled, setEnabled as ReactBrowserEventEmitterSetEnabled, getEventPriority, } from '../events/ReactDOMEventListener'; import {getChildNamespace} from '../shared/DOMNamespaces'; import { ELEMENT_NODE, TEXT_NODE, COMMENT_NODE, DOCUMENT_NODE, DOCUMENT_FRAGMENT_NODE, } from '../shared/HTMLNodeType'; import dangerousStyleValue from '../shared/dangerousStyleValue'; import {REACT_OPAQUE_ID_TYPE} from 'shared/ReactSymbols'; import {retryIfBlockedOn} from '../events/ReactDOMEventReplaying'; import { enableSuspenseServerRenderer, enableCreateEventHandleAPI, enableScopeAPI, } from 'shared/ReactFeatureFlags'; import {HostComponent, HostText} from 'react-reconciler/src/ReactWorkTags'; import {listenToAllSupportedEvents} from '../events/DOMPluginEventSystem'; import {DefaultEventPriority} from 'react-reconciler/src/ReactEventPriorities'; export type Type = string; export type Props = { autoFocus?: boolean, children?: mixed, disabled?: boolean, 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 & {_reactRootContainer?: RootType, ...}) | (Document & {_reactRootContainer?: RootType, ...}); 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, ... }; type HostContextProd = string; export type HostContext = HostContextDev | HostContextProd; export type UpdatePayload = Array; export type ChildSet = void; // Unused export type TimeoutHandle = TimeoutID; export type NoTimeout = -1; export type RendererInspectionConfig = $ReadOnly<{||}>; export opaque type OpaqueIDType = | string | { toString: () => string | void, valueOf: () => string | void, }; type SelectionInformation = {| focusedElem: null | HTMLElement, selectionRange: mixed, |}; 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: null | SelectionInformation = 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 'react-reconciler/src/ReactFiberHostConfigWithNoPersistence'; 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'; const 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}; } 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}; } const parentNamespace = ((parentHostContext: any): HostContextProd); return getChildNamespace(parentNamespace, type); } export function getPublicInstance(instance: Instance): * { return instance; } export function prepareForCommit(containerInfo: Container): Object | null { eventsEnabled = ReactBrowserEventEmitterIsEnabled(); selectionInformation = getSelectionInformation(); let activeInstance = null; if (enableCreateEventHandleAPI) { const focusedElem = selectionInformation.focusedElem; if (focusedElem !== null) { activeInstance = getClosestInstanceFromNode(focusedElem); } } ReactBrowserEventEmitterSetEnabled(false); return activeInstance; } export function beforeActiveInstanceBlur(internalInstanceHandle: Object): void { if (enableCreateEventHandleAPI) { ReactBrowserEventEmitterSetEnabled(true); dispatchBeforeDetachedBlur( (selectionInformation: any).focusedElem, internalInstanceHandle, ); ReactBrowserEventEmitterSetEnabled(false); } } export function afterActiveInstanceBlur(): void { if (enableCreateEventHandleAPI) { ReactBrowserEventEmitterSetEnabled(true); dispatchAfterDetachedBlur((selectionInformation: any).focusedElem); ReactBrowserEventEmitterSetEnabled(false); } } export function resetAfterCommit(containerInfo: Container): void { restoreSelection(selectionInformation); ReactBrowserEventEmitterSetEnabled(eventsEnabled); eventsEnabled = null; selectionInformation = 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 { 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 createTextInstance( text: string, rootContainerInstance: Container, hostContext: HostContext, internalInstanceHandle: Object, ): TextInstance { if (__DEV__) { const hostContextDev = ((hostContext: any): HostContextDev); validateDOMNesting(null, text, hostContextDev.ancestorInfo); } const textNode: TextInstance = createTextNode(text, rootContainerInstance); precacheFiberNode(internalInstanceHandle, textNode); return textNode; } export function getCurrentEventPriority(): * { const currentEvent = window.event; if (currentEvent === undefined) { return DefaultEventPriority; } return getEventPriority(currentEvent.type); } export const isPrimaryRenderer = true; export const warnsIfNotActing = 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: any = typeof setTimeout === 'function' ? setTimeout : (undefined: any); export const cancelTimeout: any = typeof clearTimeout === 'function' ? clearTimeout : (undefined: any); export const noTimeout = -1; // ------------------- // Microtasks // ------------------- export const supportsMicrotasks = true; export const scheduleMicrotask: any = typeof queueMicrotask === 'function' ? queueMicrotask : typeof Promise !== 'undefined' ? callback => Promise.resolve(null) .then(callback) .catch(handleErrorInNextTick) : scheduleTimeout; // TODO: Determine the best fallback here. function handleErrorInNextTick(error) { setTimeout(() => { throw error; }); } // ------------------- // 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, 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: Container, 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); } } function createEvent(type: DOMEventName, bubbles: boolean): Event { const event = document.createEvent('Event'); event.initEvent(((type: any): string), bubbles, false); return event; } function dispatchBeforeDetachedBlur( target: HTMLElement, internalInstanceHandle: Object, ): void { if (enableCreateEventHandleAPI) { const event = createEvent('beforeblur', true); // Dispatch "beforeblur" directly on the target, // so it gets picked up by the event system and // can propagate through the React internal tree. // $FlowFixMe: internal field event._detachedInterceptFiber = internalInstanceHandle; target.dispatchEvent(event); } } function dispatchAfterDetachedBlur(target: HTMLElement): void { if (enableCreateEventHandleAPI) { const event = createEvent('afterblur', false); // So we know what was detached, make the relatedTarget the // detached target on the "afterblur" event. (event: any).relatedTarget = target; // Dispatch the event on the document. document.dispatchEvent(event); } } 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 { const nextNode = node.nextSibling; parentInstance.removeChild(node); if (nextNode && nextNode.nodeType === COMMENT_NODE) { const data = ((nextNode: any).data: string); if (data === SUSPENSE_END_DATA) { if (depth === 0) { parentInstance.removeChild(nextNode); // Retry if any event replaying was blocked on this. retryIfBlockedOn(suspenseInstance); 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. // Retry if any event replaying was blocked on this. retryIfBlockedOn(suspenseInstance); } 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. } // Retry if any event replaying was blocked on this. retryIfBlockedOn(container); } 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; } export function clearContainer(container: Container): void { if (container.nodeType === ELEMENT_NODE) { ((container: any): Element).textContent = ''; } else if (container.nodeType === DOCUMENT_NODE) { const body = ((container: any): Document).body; if (body != null) { body.textContent = ''; } } } // ------------------- // 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; } function getNextHydratable(node) { // Skip non-hydratable nodes. for (; node != null; node = node.nextSibling) { const nodeType = node.nodeType; if (nodeType === ELEMENT_NODE || nodeType === TEXT_NODE) { break; } if (enableSuspenseServerRenderer) { if (nodeType === COMMENT_NODE) { const nodeData = (node: any).data; if ( nodeData === SUSPENSE_START_DATA || nodeData === SUSPENSE_FALLBACK_START_DATA || nodeData === SUSPENSE_PENDING_START_DATA ) { break; } } } } return (node: any); } export function getNextHydratableSibling( instance: HydratableInstance, ): null | HydratableInstance { return getNextHydratable(instance.nextSibling); } export function getFirstHydratableChild( parentInstance: Container | Instance, ): null | HydratableInstance { return getNextHydratable(parentInstance.firstChild); } export function hydrateInstance( instance: Instance, type: string, props: Props, rootContainerInstance: Container, hostContext: HostContext, internalInstanceHandle: Object, ): null | Array { 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 hydrateSuspenseInstance( suspenseInstance: SuspenseInstance, internalInstanceHandle: Object, ) { precacheFiberNode(internalInstanceHandle, suspenseInstance); } 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) { const 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 || data === SUSPENSE_FALLBACK_START_DATA || data === SUSPENSE_PENDING_START_DATA ) { depth++; } } node = node.nextSibling; } // TODO: Warn, we didn't find the end comment boundary. return null; } // Returns the SuspenseInstance if this node is a direct child of a // SuspenseInstance. I.e. if its previous sibling is a Comment with // SUSPENSE_x_START_DATA. Otherwise, null. export function getParentSuspenseInstance( targetInstance: Node, ): null | SuspenseInstance { let node = targetInstance.previousSibling; // 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) { const data = ((node: any).data: string); if ( data === SUSPENSE_START_DATA || data === SUSPENSE_FALLBACK_START_DATA || data === SUSPENSE_PENDING_START_DATA ) { if (depth === 0) { return ((node: any): SuspenseInstance); } else { depth--; } } else if (data === SUSPENSE_END_DATA) { depth++; } } node = node.previousSibling; } return null; } export function commitHydratedContainer(container: Container): void { // Retry if any event replaying was blocked on this. retryIfBlockedOn(container); } export function commitHydratedSuspenseInstance( suspenseInstance: SuspenseInstance, ): void { // Retry if any event replaying was blocked on this. retryIfBlockedOn(suspenseInstance); } export function shouldDeleteUnhydratedTailInstances( parentType: string, ): boolean { return parentType !== 'head' || parentType !== 'body'; } 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: warnForInsertedHydratedSuspense(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 errorHydratingContainer(parentContainer: Container): void { if (__DEV__) { console.error( 'An error occurred during hydration. The server HTML was replaced with client content in <%s>.', parentContainer.nodeName.toLowerCase(), ); } } export function getInstanceFromNode(node: HTMLElement): null | Object { return getClosestInstanceFromNode(node) || null; } let clientId: number = 0; export function makeClientId(): OpaqueIDType { return 'r:' + (clientId++).toString(36); } export function makeClientIdInDEV(warnOnAccessInDEV: () => void): OpaqueIDType { const id = 'r:' + (clientId++).toString(36); return { toString() { warnOnAccessInDEV(); return id; }, valueOf() { warnOnAccessInDEV(); return id; }, }; } export function isOpaqueHydratingObject(value: mixed): boolean { return ( value !== null && typeof value === 'object' && value.$$typeof === REACT_OPAQUE_ID_TYPE ); } export function makeOpaqueHydratingObject( attemptToReadValue: () => void, ): OpaqueIDType { return { $$typeof: REACT_OPAQUE_ID_TYPE, toString: attemptToReadValue, valueOf: attemptToReadValue, }; } export function preparePortalMount(portalInstance: Instance): void { listenToAllSupportedEvents(portalInstance); } export function prepareScopeUpdate( scopeInstance: ReactScopeInstance, internalInstanceHandle: Object, ): void { if (enableScopeAPI) { precacheFiberNode(internalInstanceHandle, scopeInstance); } } export function getInstanceFromScope( scopeInstance: ReactScopeInstance, ): null | Object { if (enableScopeAPI) { return getFiberFromScopeInstance(scopeInstance); } return null; } export const supportsTestSelectors = true; export function findFiberRoot(node: Instance): null | FiberRoot { const stack = [node]; let index = 0; while (index < stack.length) { const current = stack[index++]; if (isContainerMarkedAsRoot(current)) { return ((getInstanceFromNodeDOMTree(current): any): FiberRoot); } stack.push(...current.children); } return null; } export function getBoundingRect(node: Instance): BoundingRect { const rect = node.getBoundingClientRect(); return { x: rect.left, y: rect.top, width: rect.width, height: rect.height, }; } export function matchAccessibilityRole(node: Instance, role: string): boolean { if (hasRole(node, role)) { return true; } return false; } export function getTextContent(fiber: Fiber): string | null { switch (fiber.tag) { case HostComponent: let textContent = ''; const childNodes = fiber.stateNode.childNodes; for (let i = 0; i < childNodes.length; i++) { const childNode = childNodes[i]; if (childNode.nodeType === Node.TEXT_NODE) { textContent += childNode.textContent; } } return textContent; case HostText: return fiber.stateNode.textContent; } return null; } export function isHiddenSubtree(fiber: Fiber): boolean { return fiber.tag === HostComponent && fiber.memoizedProps.hidden === true; } export function setFocusIfFocusable(node: Instance): boolean { // The logic for determining if an element is focusable is kind of complex, // and since we want to actually change focus anyway- we can just skip it. // Instead we'll just listen for a "focus" event to verify that focus was set. // // We could compare the node to document.activeElement after focus, // but this would not handle the case where application code managed focus to automatically blur. let didFocus = false; const handleFocus = () => { didFocus = true; }; const element = ((node: any): HTMLElement); try { element.addEventListener('focus', handleFocus); (element.focus || HTMLElement.prototype.focus).call(element); } finally { element.removeEventListener('focus', handleFocus); } return didFocus; } type RectRatio = { ratio: number, rect: BoundingRect, }; export function setupIntersectionObserver( targets: Array, callback: ObserveVisibleRectsCallback, options?: IntersectionObserverOptions, ): {| disconnect: () => void, observe: (instance: Instance) => void, unobserve: (instance: Instance) => void, |} { const rectRatioCache: Map = new Map(); targets.forEach(target => { rectRatioCache.set(target, { rect: getBoundingRect(target), ratio: 0, }); }); const handleIntersection = (entries: Array) => { entries.forEach(entry => { const {boundingClientRect, intersectionRatio, target} = entry; rectRatioCache.set(target, { rect: { x: boundingClientRect.left, y: boundingClientRect.top, width: boundingClientRect.width, height: boundingClientRect.height, }, ratio: intersectionRatio, }); }); callback(Array.from(rectRatioCache.values())); }; const observer = new IntersectionObserver(handleIntersection, options); targets.forEach(target => { observer.observe((target: any)); }); return { disconnect: () => observer.disconnect(), observe: target => { rectRatioCache.set(target, { rect: getBoundingRect(target), ratio: 0, }); observer.observe((target: any)); }, unobserve: target => { rectRatioCache.delete(target); observer.unobserve((target: any)); }, }; }