/
githubmr
/
facebook-react
Обзор
Документация
Войти
/
githubmr
/
facebook-react
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
main
packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
6 418 строк
214 KB
Sebastian "Sebbie" Silbermann
Release Fragment refs to Canary (#34720)
07 окт 2025, 07:24
Не верифицирован
07 окт 2025, 07:24
a4eb2df
Код
Авторство
О чём код?
/** * Copyright (c) Meta Platforms, Inc. and 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 {ReactContext, ReactScopeInstance} from 'shared/ReactTypes'; import type {AncestorInfoDev} from './validateDOMNesting'; import type {FormStatus} from 'react-dom-bindings/src/shared/ReactDOMFormActions'; import type { CrossOriginEnum, PreloadImplOptions, PreloadModuleImplOptions, PreinitStyleOptions, PreinitScriptOptions, PreinitModuleScriptOptions, } from 'react-dom/src/shared/ReactDOMTypes'; import type {TransitionTypes} from 'react/src/ReactTransitionType'; import {NotPending} from '../shared/ReactDOMFormActions'; import {setSrcObject} from './ReactDOMSrcObject'; import {getCurrentRootHostContainer} from 'react-reconciler/src/ReactFiberHostContext'; import {runWithFiberInDEV} from 'react-reconciler/src/ReactCurrentFiber'; import hasOwnProperty from 'shared/hasOwnProperty'; import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion'; import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols'; export { setCurrentUpdatePriority, getCurrentUpdatePriority, resolveUpdatePriority, } from './ReactDOMUpdatePriority'; import { precacheFiberNode, updateFiberProps, getFiberCurrentPropsFromNode, getInstanceFromNode, getClosestInstanceFromNode, getFiberFromScopeInstance, getInstanceFromNode as getInstanceFromNodeDOMTree, isContainerMarkedAsRoot, detachDeletedInstance, getResourcesFromRoot, isMarkedHoistable, markNodeAsHoistable, isOwnedInstance, } from './ReactDOMComponentTree'; import { traverseFragmentInstance, getFragmentParentHostFiber, getInstanceFromHostFiber, isFiberFollowing, isFiberPreceding, getFragmentInstanceSiblings, traverseFragmentInstanceDeeply, fiberIsPortaledIntoHost, isFiberContainedByFragment, isFragmentContainedByFiber, } from 'react-reconciler/src/ReactFiberTreeReflection'; import {compareDocumentPositionForEmptyFragment} from 'shared/ReactDOMFragmentRefShared'; export {detachDeletedInstance}; import {hasRole} from './DOMAccessibilityRoles'; import { setInitialProperties, updateProperties, hydrateProperties, hydrateText, diffHydratedProperties, getPropsFromElement, diffHydratedText, trapClickOnNonInteractiveElement, } from './ReactDOMComponent'; import {hydrateInput} from './ReactDOMInput'; import {hydrateTextarea} from './ReactDOMTextarea'; import {hydrateSelect} from './ReactDOMSelect'; import {getSelectionInformation, restoreSelection} from './ReactInputSelection'; import setTextContent from './setTextContent'; import { validateDOMNesting, validateTextNesting, updatedAncestorInfoDev, } from './validateDOMNesting'; import { isEnabled as ReactBrowserEventEmitterIsEnabled, setEnabled as ReactBrowserEventEmitterSetEnabled, } from '../events/ReactDOMEventListener'; import {SVG_NAMESPACE, MATH_NAMESPACE} from './DOMNamespaces'; import { ELEMENT_NODE, TEXT_NODE, COMMENT_NODE, DOCUMENT_NODE, DOCUMENT_TYPE_NODE, DOCUMENT_FRAGMENT_NODE, } from './HTMLNodeType'; import { flushEventReplaying, retryIfBlockedOn, } from '../events/ReactDOMEventReplaying'; import { enableCreateEventHandleAPI, enableScopeAPI, enableTrustedTypesIntegration, disableLegacyMode, enableMoveBefore, disableCommentsAsDOMContainers, enableSuspenseyImages, enableSrcObject, enableViewTransition, enableHydrationChangeEvent, enableFragmentRefsScrollIntoView, enableProfilerTimer, } from 'shared/ReactFeatureFlags'; import { HostComponent, HostHoistable, HostText, HostSingleton, } from 'react-reconciler/src/ReactWorkTags'; import {listenToAllSupportedEvents} from '../events/DOMPluginEventSystem'; import {validateLinkPropsForStyleResource} from '../shared/ReactDOMResourceValidation'; import escapeSelectorAttributeValueInsideDoubleQuotes from './escapeSelectorAttributeValueInsideDoubleQuotes'; import {flushSyncWork as flushSyncWorkOnAllRoots} from 'react-reconciler/src/ReactFiberWorkLoop'; import {requestFormReset as requestFormResetOnFiber} from 'react-reconciler/src/ReactFiberHooks'; import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals'; export {default as rendererVersion} from 'shared/ReactVersion'; import noop from 'shared/noop'; import estimateBandwidth from './estimateBandwidth'; export const rendererPackageName = 'react-dom'; export const extraDevToolsConfig = null; export type Type = string; export type Props = { autoFocus?: boolean, children?: mixed, disabled?: boolean, hidden?: boolean, suppressHydrationWarning?: boolean, dangerouslySetInnerHTML?: mixed, style?: { display?: string, viewTransitionName?: string, 'view-transition-name'?: string, viewTransitionClass?: string, 'view-transition-class'?: string, margin?: string, marginTop?: string, 'margin-top'?: string, marginBottom?: string, 'margin-bottom'?: string, ... }, bottom?: null | number, left?: null | number, right?: null | number, top?: null | number, is?: string, size?: number, value?: string, defaultValue?: string, checked?: boolean, defaultChecked?: boolean, multiple?: boolean, src?: string | Blob | MediaSource | MediaStream, // TODO: Response srcSet?: string, loading?: 'eager' | 'lazy', onLoad?: (event: any) => void, ... }; type RawProps = { [string]: mixed, }; export type EventTargetChildElement = { type: string, props: null | { style?: { position?: string, zIndex?: number, bottom?: string, left?: string, right?: string, top?: string, ... }, ... }, ... }; export type Container = | interface extends Element {_reactRootContainer?: FiberRoot} | interface extends Document {_reactRootContainer?: FiberRoot} | interface extends DocumentFragment {_reactRootContainer?: FiberRoot}; export type Instance = Element; export type TextInstance = Text; declare class ActivityInterface extends Comment {} declare class SuspenseInterface extends Comment { _reactRetry: void | (() => void); } export type ActivityInstance = ActivityInterface; export type SuspenseInstance = SuspenseInterface; type FormStateMarkerInstance = Comment; export type HydratableInstance = | Instance | TextInstance | ActivityInstance | SuspenseInstance | FormStateMarkerInstance; export type PublicInstance = Element | Text; export type HostContextDev = { context: HostContextProd, ancestorInfo: AncestorInfoDev, }; type HostContextProd = HostContextNamespace; export type HostContext = HostContextDev | HostContextProd; export type UpdatePayload = Array<mixed>; export type ChildSet = void; // Unused export type TimeoutHandle = TimeoutID; export type NoTimeout = -1; export type RendererInspectionConfig = $ReadOnly<{}>; export type TransitionStatus = FormStatus; export type ViewTransitionInstance = { name: string, group: mixin$Animatable, imagePair: mixin$Animatable, old: mixin$Animatable, new: mixin$Animatable, }; type SelectionInformation = { focusedElem: null | HTMLElement, selectionRange: mixed, }; const SUPPRESS_HYDRATION_WARNING = 'suppressHydrationWarning'; const ACTIVITY_START_DATA = '&'; const ACTIVITY_END_DATA = '/&'; const SUSPENSE_START_DATA = '$'; const SUSPENSE_END_DATA = '/$'; const SUSPENSE_PENDING_START_DATA = '$?'; const SUSPENSE_QUEUED_START_DATA = '$~'; const SUSPENSE_FALLBACK_START_DATA = '$!'; const PREAMBLE_CONTRIBUTION_HTML = 'html'; const PREAMBLE_CONTRIBUTION_BODY = 'body'; const PREAMBLE_CONTRIBUTION_HEAD = 'head'; const FORM_STATE_IS_MATCHING = 'F!'; const FORM_STATE_IS_NOT_MATCHING = 'F'; const DOCUMENT_READY_STATE_LOADING = 'loading'; const STYLE = 'style'; opaque type HostContextNamespace = 0 | 1 | 2; export const HostContextNamespaceNone: HostContextNamespace = 0; const HostContextNamespaceSvg: HostContextNamespace = 1; const HostContextNamespaceMath: HostContextNamespace = 2; let eventsEnabled: ?boolean = null; let selectionInformation: null | SelectionInformation = null; export * from 'react-reconciler/src/ReactFiberConfigWithNoPersistence'; function getOwnerDocumentFromRootContainer( rootContainerElement: Element | Document | DocumentFragment, ): Document { return rootContainerElement.nodeType === DOCUMENT_NODE ? (rootContainerElement: any) : rootContainerElement.ownerDocument; } export function getRootHostContext( rootContainerInstance: Container, ): HostContext { let type; let context: HostContextProd; const nodeType = rootContainerInstance.nodeType; switch (nodeType) { case DOCUMENT_NODE: case DOCUMENT_FRAGMENT_NODE: { type = nodeType === DOCUMENT_NODE ? '#document' : '#fragment'; const root = (rootContainerInstance: any).documentElement; if (root) { const namespaceURI = root.namespaceURI; context = namespaceURI ? getOwnHostContext(namespaceURI) : HostContextNamespaceNone; } else { context = HostContextNamespaceNone; } break; } default: { const container: any = !disableCommentsAsDOMContainers && nodeType === COMMENT_NODE ? rootContainerInstance.parentNode : rootContainerInstance; type = container.tagName; const namespaceURI = container.namespaceURI; if (!namespaceURI) { switch (type) { case 'svg': context = HostContextNamespaceSvg; break; case 'math': context = HostContextNamespaceMath; break; default: context = HostContextNamespaceNone; break; } } else { const ownContext = getOwnHostContext(namespaceURI); context = getChildHostContextProd(ownContext, type); } break; } } if (__DEV__) { const validatedTag = type.toLowerCase(); const ancestorInfo = updatedAncestorInfoDev(null, validatedTag); return {context, ancestorInfo}; } return context; } function getOwnHostContext(namespaceURI: string): HostContextNamespace { switch (namespaceURI) { case SVG_NAMESPACE: return HostContextNamespaceSvg; case MATH_NAMESPACE: return HostContextNamespaceMath; default: return HostContextNamespaceNone; } } function getChildHostContextProd( parentNamespace: HostContextNamespace, type: string, ): HostContextNamespace { if (parentNamespace === HostContextNamespaceNone) { // No (or default) parent namespace: potential entry point. switch (type) { case 'svg': return HostContextNamespaceSvg; case 'math': return HostContextNamespaceMath; default: return HostContextNamespaceNone; } } if (parentNamespace === HostContextNamespaceSvg && type === 'foreignObject') { // We're leaving SVG. return HostContextNamespaceNone; } // By default, pass namespace below. return parentNamespace; } export function getChildHostContext( parentHostContext: HostContext, type: string, ): HostContext { if (__DEV__) { const parentHostContextDev = ((parentHostContext: any): HostContextDev); const context = getChildHostContextProd(parentHostContextDev.context, type); const ancestorInfo = updatedAncestorInfoDev( parentHostContextDev.ancestorInfo, type, ); return {context, ancestorInfo}; } const parentNamespace = ((parentHostContext: any): HostContextProd); return getChildHostContextProd(parentNamespace, type); } export function getPublicInstance(instance: Instance): Instance { return instance; } export function prepareForCommit(containerInfo: Container): Object | null { eventsEnabled = ReactBrowserEventEmitterIsEnabled(); selectionInformation = getSelectionInformation(containerInfo); 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, containerInfo); ReactBrowserEventEmitterSetEnabled(eventsEnabled); eventsEnabled = null; selectionInformation = null; } export function createHoistableInstance( type: string, props: Props, rootContainerInstance: Container, internalInstanceHandle: Object, ): Instance { const ownerDocument = getOwnerDocumentFromRootContainer( rootContainerInstance, ); const domElement: Instance = ownerDocument.createElement(type); precacheFiberNode(internalInstanceHandle, domElement); updateFiberProps(domElement, props); setInitialProperties(domElement, type, props); markNodeAsHoistable(domElement); return domElement; } let didWarnScriptTags = false; const warnedUnknownTags: { [key: string]: boolean, } = { // There are working polyfills for <dialog>. Let people use it. dialog: true, // Electron ships a custom <webview> tag to display external web content in // an isolated frame and process. // This tag is not present in non Electron environments such as JSDom which // is often used for testing purposes. // @see https://electronjs.org/docs/api/webview-tag webview: true, }; export function createInstance( type: string, props: Props, rootContainerInstance: Container, hostContext: HostContext, internalInstanceHandle: Object, ): Instance { let hostContextProd: HostContextProd; if (__DEV__) { // TODO: take namespace into account when validating. const hostContextDev: HostContextDev = (hostContext: any); validateDOMNesting(type, hostContextDev.ancestorInfo); hostContextProd = hostContextDev.context; } else { hostContextProd = (hostContext: any); } const ownerDocument = getOwnerDocumentFromRootContainer( rootContainerInstance, ); let domElement: Instance; switch (hostContextProd) { case HostContextNamespaceSvg: domElement = ownerDocument.createElementNS(SVG_NAMESPACE, type); break; case HostContextNamespaceMath: domElement = ownerDocument.createElementNS(MATH_NAMESPACE, type); break; default: switch (type) { case 'svg': { domElement = ownerDocument.createElementNS(SVG_NAMESPACE, type); break; } case 'math': { domElement = ownerDocument.createElementNS(MATH_NAMESPACE, type); break; } case 'script': { // Create the script via .innerHTML so its "parser-inserted" flag is // set to true and it does not execute const div = ownerDocument.createElement('div'); if (__DEV__) { if (enableTrustedTypesIntegration && !didWarnScriptTags) { console.error( 'Encountered a script tag while rendering React component. ' + 'Scripts inside React components are never executed when rendering ' + 'on the client. Consider using template tag instead ' + '(https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template).', ); didWarnScriptTags = true; } } div.innerHTML = '<script><' + '/script>'; // This is guaranteed to yield a script element. const firstChild = ((div.firstChild: any): HTMLScriptElement); domElement = div.removeChild(firstChild); break; } case 'select': { if (typeof props.is === 'string') { domElement = ownerDocument.createElement('select', {is: props.is}); } else { // Separate else branch instead of using `props.is || undefined` above because of a Firefox bug. // See discussion in https://github.com/facebook/react/pull/6896 // and discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1276240 domElement = ownerDocument.createElement('select'); } if (props.multiple) { domElement.multiple = true; } else if (props.size) { // Setting a size greater than 1 causes a select to behave like `multiple=true`, where // it is possible that no option is selected. // // This is only necessary when a select in "single selection mode". domElement.size = props.size; } break; } default: { if (typeof props.is === 'string') { domElement = ownerDocument.createElement(type, {is: props.is}); } else { // Separate else branch instead of using `props.is || undefined` above because of a Firefox bug. // See discussion in https://github.com/facebook/react/pull/6896 // and discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1276240 domElement = ownerDocument.createElement(type); } if (__DEV__) { if (type.indexOf('-') === -1) { // We're not SVG/MathML and we don't have a dash, so we're not a custom element // Even if you use `is`, these should be of known type and lower case. if (type !== type.toLowerCase()) { console.error( '<%s /> is using incorrect casing. ' + 'Use PascalCase for React components, ' + 'or lowercase for HTML elements.', type, ); } if ( // $FlowFixMe[method-unbinding] Object.prototype.toString.call(domElement) === '[object HTMLUnknownElement]' && !hasOwnProperty.call(warnedUnknownTags, type) ) { warnedUnknownTags[type] = true; console.error( 'The tag <%s> is unrecognized in this browser. ' + 'If you meant to render a React component, start its name with ' + 'an uppercase letter.', type, ); } } } } } } precacheFiberNode(internalInstanceHandle, domElement); updateFiberProps(domElement, props); return domElement; } export function cloneMutableInstance( instance: Instance, keepChildren: boolean, ): Instance { return instance.cloneNode(keepChildren); } export function appendInitialChild( parentInstance: Instance, child: Instance | TextInstance, ): void { // Note: This should not use moveBefore() because initial are appended while disconnected. parentInstance.appendChild(child); } export function finalizeInitialChildren( domElement: Instance, type: string, props: Props, hostContext: HostContext, ): boolean { setInitialProperties(domElement, type, props); switch (type) { case 'button': case 'input': case 'select': case 'textarea': return !!props.autoFocus; case 'img': return true; default: return false; } } export function finalizeHydratedChildren( domElement: Instance, type: string, props: Props, hostContext: HostContext, ): boolean { // TOOD: Consider unifying this with hydrateInstance. if (!enableHydrationChangeEvent) { return false; } switch (type) { case 'input': case 'select': case 'textarea': case 'img': return true; default: return false; } } export function shouldSetTextContent(type: string, props: Props): boolean { return ( type === 'textarea' || type === 'noscript' || typeof props.children === 'string' || typeof props.children === 'number' || typeof props.children === 'bigint' || (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); const ancestor = hostContextDev.ancestorInfo.current; if (ancestor != null) { validateTextNesting( text, ancestor.tag, hostContextDev.ancestorInfo.implicitRootScope, ); } } const textNode: TextInstance = getOwnerDocumentFromRootContainer( rootContainerInstance, ).createTextNode(text); precacheFiberNode(internalInstanceHandle, textNode); return textNode; } export function cloneMutableTextInstance( textInstance: TextInstance, ): TextInstance { return textInstance.cloneNode(false); } let currentPopstateTransitionEvent: Event | null = null; export function shouldAttemptEagerTransition(): boolean { const event = window.event; if (event && event.type === 'popstate') { // This is a popstate event. Attempt to render any transition during this // event synchronously. Unless we already attempted during this event. if (event === currentPopstateTransitionEvent) { // We already attempted to render this popstate transition synchronously. // Any subsequent attempts must have happened as the result of a derived // update, like startTransition inside useEffect, or useDV. Switch back to // the default behavior for all remaining transitions during the current // popstate event. return false; } else { // Cache the current event in case a derived transition is scheduled. // (Refer to previous branch.) currentPopstateTransitionEvent = event; return true; } } // We're not inside a popstate event. currentPopstateTransitionEvent = null; return false; } let schedulerEvent: void | Event = undefined; export function trackSchedulerEvent(): void { schedulerEvent = window.event; } export function resolveEventType(): null | string { const event = window.event; return event && event !== schedulerEvent ? event.type : null; } export function resolveEventTimeStamp(): number { const event = window.event; return event && event !== schedulerEvent ? event.timeStamp : -1.1; } 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 = -1; const localPromise = typeof Promise === 'function' ? Promise : undefined; const localRequestAnimationFrame = typeof requestAnimationFrame === 'function' ? requestAnimationFrame : scheduleTimeout; export {getClosestInstanceFromNode as getInstanceFromNode}; 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; } // ------------------- // Microtasks // ------------------- export const supportsMicrotasks = true; export const scheduleMicrotask: any = typeof queueMicrotask === 'function' ? queueMicrotask : typeof localPromise !== 'undefined' ? callback => localPromise.resolve(null).then(callback).catch(handleErrorInNextTick) : scheduleTimeout; // TODO: Determine the best fallback here. function handleErrorInNextTick(error: any) { 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. switch (type) { case 'button': case 'input': case 'select': case 'textarea': if (newProps.autoFocus) { ((domElement: any): | HTMLButtonElement | HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement).focus(); } return; case 'img': { // The technique here is to assign the src or srcSet property to cause the browser // to issue a new load event. If it hasn't loaded yet it'll fire whenever the load actually completes. // If it has already loaded we missed it so the second load will still be the first one that executes // any associated onLoad props. // Even if we have srcSet we prefer to reassign src. The reason is that Firefox does not trigger a new // load event when only srcSet is assigned. Chrome will trigger a load event if either is assigned so we // only need to assign one. And Safari just never triggers a new load event which means this technique // is already a noop regardless of which properties are assigned. We should revisit if browsers update // this heuristic in the future. if (newProps.src) { const src = (newProps: any).src; if (enableSrcObject && typeof src === 'object') { // For object src, we can't just set the src again to the same blob URL because it might have // already revoked if it loaded before this. However, we can create a new blob URL and set that. // This is relatively cheap since the blob is already in memory but this might cause some // duplicated work. // TODO: We could maybe detect if load hasn't fired yet and if so reuse the URL. try { setSrcObject(domElement, type, src); return; } catch (x) { // If URL.createObjectURL() errors, it was probably some other object type // that should be toString:ed instead, so we just fall-through to the normal // path. } } ((domElement: any): HTMLImageElement).src = src; } else if (newProps.srcSet) { ((domElement: any): HTMLImageElement).srcset = (newProps: any).srcSet; } return; } } } export function commitHydratedInstance( domElement: Instance, type: string, props: Props, internalInstanceHandle: Object, ): void { if (!enableHydrationChangeEvent) { return; } // This fires in the commit phase if a hydrated instance needs to do further // work in the commit phase. Similar to commitMount. However, this should not // do things that would've already happened such as set auto focus since that // would steal focus. It's only scheduled if finalizeHydratedChildren returns // true. switch (type) { case 'input': { hydrateInput( domElement, props.value, props.defaultValue, props.checked, props.defaultChecked, ); break; } case 'select': { hydrateSelect( domElement, props.value, props.defaultValue, props.multiple, ); break; } case 'textarea': hydrateTextarea(domElement, props.value, props.defaultValue); break; case 'img': // TODO: Should we replay onLoad events? break; } } export function commitUpdate( domElement: Instance, type: string, oldProps: Props, newProps: Props, internalInstanceHandle: Object, ): void { // Diff and update the properties. updateProperties(domElement, type, oldProps, newProps); // Update the props handle so that we know which props are the ones with // with current event handlers. updateFiberProps(domElement, newProps); } export function resetTextContent(domElement: Instance): void { setTextContent(domElement, ''); } export function commitTextUpdate( textInstance: TextInstance, oldText: string, newText: string, ): void { textInstance.nodeValue = newText; } const supportsMoveBefore = // $FlowFixMe[prop-missing]: We're doing the feature detection here. enableMoveBefore && typeof window !== 'undefined' && typeof window.Element.prototype.moveBefore === 'function'; export function appendChild( parentInstance: Instance, child: Instance | TextInstance, ): void { if (supportsMoveBefore && child.parentNode !== null) { // $FlowFixMe[prop-missing]: We've checked this with supportsMoveBefore. parentInstance.moveBefore(child, null); } else { parentInstance.appendChild(child); } } function warnForReactChildrenConflict(container: Container): void { if (__DEV__) { if ((container: any).__reactWarnedAboutChildrenConflict) { return; } const props = getFiberCurrentPropsFromNode(container); if (props !== null) { const fiber = getInstanceFromNode(container); if (fiber !== null) { if ( typeof props.children === 'string' || typeof props.children === 'number' ) { (container: any).__reactWarnedAboutChildrenConflict = true; // Run the warning with the Fiber of the container for context of where the children are specified. // We could also maybe use the Portal. The current execution context is the child being added. runWithFiberInDEV(fiber, () => { console.error( 'Cannot use a ref on a React element as a container to `createRoot` or `createPortal` ' + 'if that element also sets "children" text content using React. It should be a leaf with no children. ' + "Otherwise it's ambiguous which children should be used.", ); }); } else if (props.dangerouslySetInnerHTML != null) { (container: any).__reactWarnedAboutChildrenConflict = true; runWithFiberInDEV(fiber, () => { console.error( 'Cannot use a ref on a React element as a container to `createRoot` or `createPortal` ' + 'if that element also sets "dangerouslySetInnerHTML" using React. It should be a leaf with no children. ' + "Otherwise it's ambiguous which children should be used.", ); }); } } } } } export function appendChildToContainer( container: Container, child: Instance | TextInstance, ): void { if (__DEV__) { warnForReactChildrenConflict(container); } let parentNode: DocumentFragment | Element; if (container.nodeType === DOCUMENT_NODE) { parentNode = (container: any).body; } else if ( !disableCommentsAsDOMContainers && container.nodeType === COMMENT_NODE ) { parentNode = (container.parentNode: any); if (supportsMoveBefore && child.parentNode !== null) { // $FlowFixMe[prop-missing]: We've checked this with supportsMoveBefore. parentNode.moveBefore(child, container); } else { parentNode.insertBefore(child, container); } return; } else if (container.nodeName === 'HTML') { parentNode = (container.ownerDocument.body: any); } else { parentNode = (container: any); } if (supportsMoveBefore && child.parentNode !== null) { // $FlowFixMe[prop-missing]: We've checked this with supportsMoveBefore. parentNode.moveBefore(child, null); } else { 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 | ActivityInstance, ): void { if (supportsMoveBefore && child.parentNode !== null) { // $FlowFixMe[prop-missing]: We've checked this with supportsMoveBefore. parentInstance.moveBefore(child, beforeChild); } else { parentInstance.insertBefore(child, beforeChild); } } export function insertInContainerBefore( container: Container, child: Instance | TextInstance, beforeChild: Instance | TextInstance | SuspenseInstance | ActivityInstance, ): void { if (__DEV__) { warnForReactChildrenConflict(container); } let parentNode: DocumentFragment | Element; if (container.nodeType === DOCUMENT_NODE) { parentNode = (container: any).body; } else if ( !disableCommentsAsDOMContainers && container.nodeType === COMMENT_NODE ) { parentNode = (container.parentNode: any); } else if (container.nodeName === 'HTML') { parentNode = (container.ownerDocument.body: any); } else { parentNode = (container: any); } if (supportsMoveBefore && child.parentNode !== null) { // $FlowFixMe[prop-missing]: We've checked this with supportsMoveBefore. parentNode.moveBefore(child, beforeChild); } else { parentNode.insertBefore(child, beforeChild); } } export function isSingletonScope(type: string): boolean { return type === 'head'; } 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[prop-missing]: 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 | ActivityInstance, ): void { parentInstance.removeChild(child); } export function removeChildFromContainer( container: Container, child: Instance | TextInstance | SuspenseInstance | ActivityInstance, ): void { let parentNode: DocumentFragment | Element; if (container.nodeType === DOCUMENT_NODE) { parentNode = (container: any).body; } else if ( !disableCommentsAsDOMContainers && container.nodeType === COMMENT_NODE ) { parentNode = (container.parentNode: any); } else if (container.nodeName === 'HTML') { parentNode = (container.ownerDocument.body: any); } else { parentNode = (container: any); } parentNode.removeChild(child); } function clearHydrationBoundary( parentInstance: Instance, hydrationInstance: SuspenseInstance | ActivityInstance, ): void { let node: Node = hydrationInstance; // 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 || data === ACTIVITY_END_DATA) { if (depth === 0) { parentInstance.removeChild(nextNode); // Retry if any event replaying was blocked on this. retryIfBlockedOn(hydrationInstance); return; } else { depth--; } } else if ( data === SUSPENSE_START_DATA || data === SUSPENSE_PENDING_START_DATA || data === SUSPENSE_QUEUED_START_DATA || data === SUSPENSE_FALLBACK_START_DATA || data === ACTIVITY_START_DATA ) { depth++; } else if (data === PREAMBLE_CONTRIBUTION_HTML) { // If a preamble contribution marker is found within the bounds of this boundary, // then it contributed to the html tag and we need to reset it. const ownerDocument = parentInstance.ownerDocument; const documentElement: Element = (ownerDocument.documentElement: any); releaseSingletonInstance(documentElement); } else if (data === PREAMBLE_CONTRIBUTION_HEAD) { const ownerDocument = parentInstance.ownerDocument; const head: Element = (ownerDocument.head: any); releaseSingletonInstance(head); // We need to clear the head because this is the only singleton that can have children that // were part of this boundary but are not inside this boundary. clearHead(head); } else if (data === PREAMBLE_CONTRIBUTION_BODY) { const ownerDocument = parentInstance.ownerDocument; const body: Element = (ownerDocument.body: any); releaseSingletonInstance(body); } } // $FlowFixMe[incompatible-type] we bail out when we get a null node = nextNode; } while (node); // TODO: Warn, we didn't find the end comment boundary. // Retry if any event replaying was blocked on this. retryIfBlockedOn(hydrationInstance); } export function clearActivityBoundary( parentInstance: Instance, activityInstance: ActivityInstance, ): void { clearHydrationBoundary(parentInstance, activityInstance); } export function clearSuspenseBoundary( parentInstance: Instance, suspenseInstance: SuspenseInstance, ): void { clearHydrationBoundary(parentInstance, suspenseInstance); } function clearHydrationBoundaryFromContainer( container: Container, hydrationInstance: SuspenseInstance | ActivityInstance, ): void { let parentNode: DocumentFragment | Element; if (container.nodeType === DOCUMENT_NODE) { parentNode = (container: any).body; } else if ( !disableCommentsAsDOMContainers && container.nodeType === COMMENT_NODE ) { parentNode = (container.parentNode: any); } else if (container.nodeName === 'HTML') { parentNode = (container.ownerDocument.body: any); } else { parentNode = (container: any); } clearHydrationBoundary(parentNode, hydrationInstance); // Retry if any event replaying was blocked on this. retryIfBlockedOn(container); } export function clearActivityBoundaryFromContainer( container: Container, activityInstance: ActivityInstance, ): void { clearHydrationBoundaryFromContainer(container, activityInstance); } export function clearSuspenseBoundaryFromContainer( container: Container, suspenseInstance: SuspenseInstance, ): void { clearHydrationBoundaryFromContainer(container, suspenseInstance); } function hideOrUnhideDehydratedBoundary( suspenseInstance: SuspenseInstance | ActivityInstance, isHidden: boolean, ) { let node: Node = suspenseInstance; // Unhide all nodes within this suspense boundary. let depth = 0; do { const nextNode = node.nextSibling; if (node.nodeType === ELEMENT_NODE) { const instance = ((node: any): HTMLElement & {_stashedDisplay?: string}); if (isHidden) { instance._stashedDisplay = instance.style.display; instance.style.display = 'none'; } else { instance.style.display = instance._stashedDisplay || ''; if (instance.getAttribute('style') === '') { instance.removeAttribute('style'); } } } else if (node.nodeType === TEXT_NODE) { const textNode = ((node: any): Text & {_stashedText?: string}); if (isHidden) { textNode._stashedText = textNode.nodeValue; textNode.nodeValue = ''; } else { textNode.nodeValue = textNode._stashedText || ''; } } if (nextNode && nextNode.nodeType === COMMENT_NODE) { const data = ((nextNode: any).data: string); if (data === SUSPENSE_END_DATA) { if (depth === 0) { return; } else { depth--; } } else if ( data === SUSPENSE_START_DATA || data === SUSPENSE_PENDING_START_DATA || data === SUSPENSE_QUEUED_START_DATA || data === SUSPENSE_FALLBACK_START_DATA ) { depth++; } // TODO: Should we hide preamble contribution in this case? } // $FlowFixMe[incompatible-type] we bail out when we get a null node = nextNode; } while (node); } export function hideDehydratedBoundary( suspenseInstance: SuspenseInstance, ): void { hideOrUnhideDehydratedBoundary(suspenseInstance, true); } 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; // $FlowFixMe[method-unbinding] if (typeof style.setProperty === 'function') { style.setProperty('display', 'none', 'important'); } else { style.display = 'none'; } } export function hideTextInstance(textInstance: TextInstance): void { textInstance.nodeValue = ''; } export function unhideDehydratedBoundary( dehydratedInstance: SuspenseInstance | ActivityInstance, ): void { hideOrUnhideDehydratedBoundary(dehydratedInstance, false); } 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 = display == null || typeof display === 'boolean' ? '' : // The value would've errored already if it wasn't safe. // eslint-disable-next-line react-internal/safe-string-coercion ('' + display).trim(); } export function unhideTextInstance( textInstance: TextInstance, text: string, ): void { textInstance.nodeValue = text; } function warnForBlockInsideInline(instance: HTMLElement) { if (__DEV__) { let nextNode = instance.firstChild; outer: while (nextNode != null) { let node: Node = nextNode; if ( node.nodeType === ELEMENT_NODE && getComputedStyle((node: any)).display === 'block' ) { const fiber = getInstanceFromNode(node) || getInstanceFromNode(instance); runWithFiberInDEV( fiber, (parentTag: string, childTag: string) => { console.error( "You're about to start a <ViewTransition> around a display: inline " + 'element <%s>, which itself has a display: block element <%s> inside it. ' + 'This might trigger a bug in Safari which causes the View Transition to ' + 'be skipped with a duplicate name error.\n' + 'https://bugs.webkit.org/show_bug.cgi?id=290923', parentTag.toLocaleLowerCase(), childTag.toLocaleLowerCase(), ); }, instance.tagName, (node: any).tagName, ); break; } if (node.firstChild != null) { nextNode = node.firstChild; continue; } if (node === instance) { break; } while (node.nextSibling == null) { if (node.parentNode == null || node.parentNode === instance) { break; } node = node.parentNode; } nextNode = node.nextSibling; } } } function countClientRects(rects: Array<ClientRect>): number { if (rects.length === 1) { return 1; } // Count non-zero rects. let count = 0; for (let i = 0; i < rects.length; i++) { const rect = rects[i]; if (rect.width > 0 && rect.height > 0) { count++; } } return count; } export function applyViewTransitionName( instance: Instance, name: string, className: ?string, ): void { instance = ((instance: any): HTMLElement); // $FlowFixMe[prop-missing] instance.style.viewTransitionName = name; if (className != null) { // $FlowFixMe[prop-missing] instance.style.viewTransitionClass = className; } const computedStyle = getComputedStyle(instance); if (computedStyle.display === 'inline') { // WebKit has a bug where assigning a name to display: inline elements errors // if they have display: block children. We try to work around this bug in the // simple case by converting it automatically to display: inline-block. // https://bugs.webkit.org/show_bug.cgi?id=290923 const rects = instance.getClientRects(); if ( // $FlowFixMe[incompatible-call] countClientRects(rects) === 1 ) { // If the instance has a single client rect, that means that it can be // expressed as a display: inline-block or block. // This will cause layout thrash but we live with it since inline view transitions // are unusual. const style = instance.style; // If there's literally only one rect, then it's likely on a single line like an // inline-block. If it's multiple rects but all but one of them are empty it's // likely because it's a single block that caused a line break. style.display = rects.length === 1 ? 'inline-block' : 'block'; // Margin doesn't apply to inline so should be zero. However, padding top/bottom // applies to inline-block positioning which we can offset by setting the margin // to the negative padding to get it back into original position. style.marginTop = '-' + computedStyle.paddingTop; style.marginBottom = '-' + computedStyle.paddingBottom; } else { // This case cannot be easily fixed if it has blocks but it's also fine if // it doesn't have blocks. So we only warn in DEV about this being an issue. warnForBlockInsideInline(instance); } } } export function restoreViewTransitionName( instance: Instance, props: Props, ): void { instance = ((instance: any): HTMLElement); const style = instance.style; const styleProp = props[STYLE]; const viewTransitionName = styleProp != null ? styleProp.hasOwnProperty('viewTransitionName') ? styleProp.viewTransitionName : styleProp.hasOwnProperty('view-transition-name') ? styleProp['view-transition-name'] : null : null; // $FlowFixMe[prop-missing] style.viewTransitionName = viewTransitionName == null || typeof viewTransitionName === 'boolean' ? '' : // The value would've errored already if it wasn't safe. // eslint-disable-next-line react-internal/safe-string-coercion ('' + viewTransitionName).trim(); const viewTransitionClass = styleProp != null ? styleProp.hasOwnProperty('viewTransitionClass') ? styleProp.viewTransitionClass : styleProp.hasOwnProperty('view-transition-class') ? styleProp['view-transition-class'] : null : null; // $FlowFixMe[prop-missing] style.viewTransitionClass = viewTransitionClass == null || typeof viewTransitionClass === 'boolean' ? '' : // The value would've errored already if it wasn't safe. // eslint-disable-next-line react-internal/safe-string-coercion ('' + viewTransitionClass).trim(); if (style.display === 'inline-block') { // We might have overridden the style. Reset it to what it should be. if (styleProp == null) { style.display = style.margin = ''; } else { const display = styleProp.display; style.display = display == null || typeof display === 'boolean' ? '' : display; const margin = styleProp.margin; if (margin != null) { style.margin = margin; } else { const marginTop = styleProp.hasOwnProperty('marginTop') ? styleProp.marginTop : styleProp['margin-top']; style.marginTop = marginTop == null || typeof marginTop === 'boolean' ? '' : marginTop; const marginBottom = styleProp.hasOwnProperty('marginBottom') ? styleProp.marginBottom : styleProp['margin-bottom']; style.marginBottom = marginBottom == null || typeof marginBottom === 'boolean' ? '' : marginBottom; } } } } export function cancelViewTransitionName( instance: Instance, oldName: string, props: Props, ): void { // To cancel the "new" state and paint this instance as part of the parent, all we have to do // is remove the view-transition-name before we exit startViewTransition. restoreViewTransitionName(instance, props); // There isn't a way to cancel an "old" state but what we can do is hide it by animating it. // Since it is already removed from the old state of the parent, this technique only works // if the parent also isn't transitioning. Therefore we should only cancel the root most // ViewTransitions. const documentElement = instance.ownerDocument.documentElement; if (documentElement !== null) { documentElement.animate( {opacity: [0, 0], pointerEvents: ['none', 'none']}, // $FlowFixMe[incompatible-call] { duration: 0, fill: 'forwards', pseudoElement: '::view-transition-group(' + oldName + ')', }, ); } } export function cancelRootViewTransitionName(rootContainer: Container): void { const documentElement: null | HTMLElement = rootContainer.nodeType === DOCUMENT_NODE ? (rootContainer: any).documentElement : rootContainer.ownerDocument.documentElement; if ( !disableCommentsAsDOMContainers && rootContainer.nodeType === COMMENT_NODE ) { if (__DEV__) { console.warn( 'Cannot cancel root view transition on a comment node. All view transitions will be globally scoped.', ); } return; } if ( documentElement !== null && // $FlowFixMe[prop-missing] documentElement.style.viewTransitionName === '' ) { // $FlowFixMe[prop-missing] documentElement.style.viewTransitionName = 'none'; documentElement.animate( {opacity: [0, 0], pointerEvents: ['none', 'none']}, // $FlowFixMe[incompatible-call] { duration: 0, fill: 'forwards', pseudoElement: '::view-transition-group(root)', }, ); // By default the root ::view-transition selector captures all pointer events, // which means nothing gets interactive. We want to let whatever is not animating // remain interactive during the transition. To do that, we set the size to nothing // so that the transition doesn't capture any clicks. We don't set pointer-events // on this one as that would apply to all running transitions. This lets animations // that are running to block clicks so that they don't end up incorrectly hitting // whatever is below the animation. documentElement.animate( {width: [0, 0], height: [0, 0]}, // $FlowFixMe[incompatible-call] { duration: 0, fill: 'forwards', pseudoElement: '::view-transition', }, ); } } export function restoreRootViewTransitionName(rootContainer: Container): void { let containerInstance: Instance; if (rootContainer.nodeType === DOCUMENT_NODE) { containerInstance = (rootContainer: any).body; } else if (rootContainer.nodeName === 'HTML') { containerInstance = (rootContainer.ownerDocument.body: any); } else { // If the container is not the whole document, then we ideally should probably // clone the whole document outside of the React too. containerInstance = (rootContainer: any); } if ( !disableCommentsAsDOMContainers && containerInstance.nodeType === COMMENT_NODE ) { return; } if ( // $FlowFixMe[prop-missing] containerInstance.style.viewTransitionName === 'root' ) { // If we moved the root view transition name to the container in a gesture // we need to restore it now. containerInstance.style.viewTransitionName = ''; } const documentElement: null | HTMLElement = containerInstance.ownerDocument.documentElement; if ( documentElement !== null && // $FlowFixMe[prop-missing] documentElement.style.viewTransitionName === 'none' ) { // $FlowFixMe[prop-missing] documentElement.style.viewTransitionName = ''; } } function getComputedTransform(style: CSSStyleDeclaration): string { // Gets the merged transform of all the short hands. const computedStyle: any = style; let transform: string = computedStyle.transform; if (transform === 'none') { transform = ''; } const scale: string = computedStyle.scale; if (scale !== 'none' && scale !== '') { const parts = scale.split(' '); transform = (parts.length === 3 ? 'scale3d' : 'scale') + '(' + parts.join(', ') + ') ' + transform; } const rotate: string = computedStyle.rotate; if (rotate !== 'none' && rotate !== '') { const parts = rotate.split(' '); if (parts.length === 1) { transform = 'rotate(' + parts[0] + ') ' + transform; } else if (parts.length === 2) { transform = 'rotate' + parts[0].toUpperCase() + '(' + parts[1] + ') ' + transform; } else { transform = 'rotate3d(' + parts.join(', ') + ') ' + transform; } } const translate: string = computedStyle.translate; if (translate !== 'none' && translate !== '') { const parts = translate.split(' '); transform = (parts.length === 3 ? 'translate3d' : 'translate') + '(' + parts.join(', ') + ') ' + transform; } return transform; } function moveOutOfViewport( originalStyle: CSSStyleDeclaration, element: HTMLElement, ): void { // Apply a transform that safely puts the whole element outside the viewport // while still letting it paint its "old" state to a snapshot. const transform = getComputedTransform(originalStyle); // Clear the long form properties. // $FlowFixMe element.style.translate = 'none'; // $FlowFixMe element.style.scale = 'none'; // $FlowFixMe element.style.rotate = 'none'; // Apply a translate to move it way out of the viewport. This is applied first // so that it is in the coordinate space of the parent and not after applying // other transforms. That's why we need to merge the long form properties. // TODO: Ideally we'd adjust for the parent's rotate/scale. Otherwise when // we move back the ::view-transition-group we might overshoot or undershoot. element.style.transform = 'translate(-20000px, -20000px) ' + transform; } function moveOldFrameIntoViewport(keyframe: any): void { // In the resulting View Transition Animation, the first frame will be offset. const computedTransform: ?string = keyframe.transform; if (computedTransform != null) { let transform = computedTransform === 'none' ? '' : computedTransform; transform = 'translate(20000px, 20000px) ' + transform; keyframe.transform = transform; } } export function cloneRootViewTransitionContainer( rootContainer: Container, ): Instance { // This implies that we're not going to animate the root document but instead // the clone so we first clear the name of the root container. const documentElement: null | HTMLElement = rootContainer.nodeType === DOCUMENT_NODE ? (rootContainer: any).documentElement : rootContainer.ownerDocument.documentElement; if ( documentElement !== null && // $FlowFixMe[prop-missing] documentElement.style.viewTransitionName === '' ) { // $FlowFixMe[prop-missing] documentElement.style.viewTransitionName = 'none'; } let containerInstance: HTMLElement; if (rootContainer.nodeType === DOCUMENT_NODE) { containerInstance = (rootContainer: any).body; } else if (rootContainer.nodeName === 'HTML') { containerInstance = (rootContainer.ownerDocument.body: any); } else if ( !disableCommentsAsDOMContainers && rootContainer.nodeType === COMMENT_NODE ) { throw new Error( 'Cannot use a startGestureTransition() with a comment node root.', ); } else { // If the container is not the whole document, then we ideally should probably // clone the whole document outside of the React too. containerInstance = (rootContainer: any); } const containerParent = containerInstance.parentNode; if (containerParent === null) { throw new Error( 'Cannot use a startGestureTransition() on a detached root.', ); } const clone: HTMLElement = containerInstance.cloneNode(false); const computedStyle = getComputedStyle(containerInstance); if ( computedStyle.position === 'absolute' || computedStyle.position === 'fixed' ) { // If the style is already absolute, we don't have to do anything because it'll appear // in the same place. } else { // Otherwise we need to absolutely position the clone in the same location as the original. let positionedAncestor: HTMLElement = containerParent; while ( positionedAncestor.parentNode != null && positionedAncestor.parentNode.nodeType !== DOCUMENT_NODE ) { if (getComputedStyle(positionedAncestor).position !== 'static') { break; } // $FlowFixMe: This is refined. positionedAncestor = positionedAncestor.parentNode; } const positionedAncestorStyle: any = positionedAncestor.style; const containerInstanceStyle: any = containerInstance.style; // Clear the transform while we're measuring since it affects the bounding client rect. const prevAncestorTranslate = positionedAncestorStyle.translate; const prevAncestorScale = positionedAncestorStyle.scale; const prevAncestorRotate = positionedAncestorStyle.rotate; const prevAncestorTransform = positionedAncestorStyle.transform; const prevTranslate = containerInstanceStyle.translate; const prevScale = containerInstanceStyle.scale; const prevRotate = containerInstanceStyle.rotate; const prevTransform = containerInstanceStyle.transform; positionedAncestorStyle.translate = 'none'; positionedAncestorStyle.scale = 'none'; positionedAncestorStyle.rotate = 'none'; positionedAncestorStyle.transform = 'none'; containerInstanceStyle.translate = 'none'; containerInstanceStyle.scale = 'none'; containerInstanceStyle.rotate = 'none'; containerInstanceStyle.transform = 'none'; const ancestorRect = positionedAncestor.getBoundingClientRect(); const rect = containerInstance.getBoundingClientRect(); const cloneStyle = clone.style; cloneStyle.position = 'absolute'; cloneStyle.top = rect.top - ancestorRect.top + 'px'; cloneStyle.left = rect.left - ancestorRect.left + 'px'; cloneStyle.width = rect.width + 'px'; cloneStyle.height = rect.height + 'px'; cloneStyle.margin = '0px'; cloneStyle.boxSizing = 'border-box'; positionedAncestorStyle.translate = prevAncestorTranslate; positionedAncestorStyle.scale = prevAncestorScale; positionedAncestorStyle.rotate = prevAncestorRotate; positionedAncestorStyle.transform = prevAncestorTransform; containerInstanceStyle.translate = prevTranslate; containerInstanceStyle.scale = prevScale; containerInstanceStyle.rotate = prevRotate; containerInstanceStyle.transform = prevTransform; } // For this transition the container will act as the root. Nothing outside of it should // be affected anyway. This lets us transition from the cloned container to the original. // $FlowFixMe[prop-missing] clone.style.viewTransitionName = 'root'; // Move out of the viewport so that it's still painted for the snapshot but is not visible // for the frame where the snapshot happens. moveOutOfViewport(computedStyle, clone); // Insert the clone after the root container as a sibling. This may inject a body // as the next sibling of an existing body. document.body will still point to the // first one and any id selectors will still find the first one. That's why it's // important that it's after the existing node. containerInstance.parentNode.insertBefore( clone, containerInstance.nextSibling, ); return clone; } export function removeRootViewTransitionClone( rootContainer: Container, clone: Instance, ): void { let containerInstance: Instance; if (rootContainer.nodeType === DOCUMENT_NODE) { containerInstance = (rootContainer: any).body; } else if (rootContainer.nodeName === 'HTML') { containerInstance = (rootContainer.ownerDocument.body: any); } else { // If the container is not the whole document, then we ideally should probably // clone the whole document outside of the React too. containerInstance = (rootContainer: any); } const containerParent = containerInstance.parentNode; if (containerParent === null) { throw new Error( 'Cannot use a startGestureTransition() on a detached root.', ); } // We assume that the clone is still within the same parent. containerParent.removeChild(clone); // Now the root is on the containerInstance itself until we call restoreRootViewTransitionName. containerInstance.style.viewTransitionName = 'root'; } export type InstanceMeasurement = { rect: ClientRect | DOMRect, abs: boolean, // is absolutely positioned clip: boolean, // is a clipping parent view: boolean, // is in viewport bounds }; function createMeasurement( rect: ClientRect | DOMRect, computedStyle: CSSStyleDeclaration, element: Element, ): InstanceMeasurement { const ownerWindow = element.ownerDocument.defaultView; return { rect: rect, abs: // Absolutely positioned instances don't contribute their size to the parent. computedStyle.position === 'absolute' || computedStyle.position === 'fixed', clip: // If a ViewTransition boundary acts as a clipping parent group we should // always mark it to animate if its children do so that we can clip them. // This doesn't actually have any effect yet until browsers implement // layered capture and nested view transitions. computedStyle.clipPath !== 'none' || computedStyle.overflow !== 'visible' || computedStyle.filter !== 'none' || computedStyle.mask !== 'none' || computedStyle.mask !== 'none' || computedStyle.borderRadius !== '0px', view: // If the instance was within the bounds of the viewport. We don't care as // much about if it was fully occluded because then it can still pop out. rect.bottom >= 0 && rect.right >= 0 && rect.top <= ownerWindow.innerHeight && rect.left <= ownerWindow.innerWidth, }; } export function measureInstance(instance: Instance): InstanceMeasurement { const rect = instance.getBoundingClientRect(); const computedStyle = getComputedStyle(instance); return createMeasurement(rect, computedStyle, instance); } export function measureClonedInstance(instance: Instance): InstanceMeasurement { const measuredRect = instance.getBoundingClientRect(); // Adjust the DOMRect based on the translate that put it outside the viewport. // TODO: This might not be completely correct if the parent also has a transform. const rect = new DOMRect( measuredRect.x + 20000, measuredRect.y + 20000, measuredRect.width, measuredRect.height, ); const computedStyle = getComputedStyle(instance); return createMeasurement(rect, computedStyle, instance); } export function wasInstanceInViewport( measurement: InstanceMeasurement, ): boolean { return measurement.view; } export function hasInstanceChanged( oldMeasurement: InstanceMeasurement, newMeasurement: InstanceMeasurement, ): boolean { // Note: This is not guaranteed from the same instance in the case that the Instance of the // ViewTransition swaps out but it's still the same ViewTransition instance. if (newMeasurement.clip) { // If we're a clipping parent, we always animate if any of our children do so that we can clip // them. This doesn't yet until browsers implement layered capture and nested view transitions. return true; } const oldRect = oldMeasurement.rect; const newRect = newMeasurement.rect; return ( oldRect.y !== newRect.y || oldRect.x !== newRect.x || oldRect.height !== newRect.height || oldRect.width !== newRect.width ); } export function hasInstanceAffectedParent( oldMeasurement: InstanceMeasurement, newMeasurement: InstanceMeasurement, ): boolean { // Note: This is not guaranteed from the same instance in the case that the Instance of the // ViewTransition swaps out but it's still the same ViewTransition instance. // If the instance has resized, it might have affected the parent layout. if (newMeasurement.abs) { // Absolutely positioned elements don't affect the parent layout, unless they // previously were not absolutely positioned. return !oldMeasurement.abs; } const oldRect = oldMeasurement.rect; const newRect = newMeasurement.rect; return oldRect.height !== newRect.height || oldRect.width !== newRect.width; } function cancelAllViewTransitionAnimations(scope: Element) { // In Safari, we need to manually cancel all manually start animations // or it'll block or interfer with future transitions. // $FlowFixMe[prop-missing] const animations = scope.getAnimations({subtree: true}); for (let i = 0; i < animations.length; i++) { const anim = animations[i]; const effect: KeyframeEffect = (anim.effect: any); // $FlowFixMe const pseudo: ?string = effect.pseudoElement; if ( pseudo != null && pseudo.startsWith('::view-transition') && effect.target === scope ) { anim.cancel(); } } } // How long to wait for new fonts to load before just committing anyway. // This freezes the screen. It needs to be short enough that it doesn't cause too much of // an issue when it's a new load and slow, yet long enough that you have a chance to load // it. Otherwise we wait for no reason. The assumption here is that you likely have // either cached the font or preloaded it earlier. // This timeout is also used for Suspensey Images when they're blocking a View Transition. const SUSPENSEY_FONT_AND_IMAGE_TIMEOUT = 500; function customizeViewTransitionError( error: Object, ignoreAbort: boolean, ): mixed { if (typeof error === 'object' && error !== null) { switch (error.name) { case 'TimeoutError': { // We assume that the only reason a Timeout can happen is because the Navigation // promise. We expect any other work to either be fast or have a timeout (fonts). if (__DEV__) { // eslint-disable-next-line react-internal/prod-error-codes return new Error( 'A ViewTransition timed out because a Navigation stalled. ' + 'This can happen if a Navigation is blocked on React itself. ' + "Such as if it's resolved inside useEffect. " + 'This can be solved by moving the resolution to useLayoutEffect.', {cause: error}, ); } break; } case 'AbortError': { if (ignoreAbort) { return null; } if (__DEV__) { // eslint-disable-next-line react-internal/prod-error-codes return new Error( 'A ViewTransition was aborted early. This might be because you have ' + 'other View Transition libraries on the page and only one can run at ' + "a time. To avoid this, use only React's built-in <ViewTransition> " + 'to coordinate.', {cause: error}, ); } break; } case 'InvalidStateError': { if ( error.message === 'View transition was skipped because document visibility state is hidden.' || error.message === 'Skipping view transition because document visibility state has become hidden.' || error.message === 'Skipping view transition because viewport size changed.' || // Chrome uses a generic error message instead of specific reasons. It will log a // more specific reason in the console but the user might not look there. // Some of these errors are important to surface like duplicate name errors but // it's too noisy for unactionable cases like the document was hidden. Therefore, // we hide all of them and hopefully it surfaces in another browser. error.message === 'Transition was aborted because of invalid state' ) { // Skip logging this. This is not considered an error. return null; } break; } } } return error; } /** @noinline */ function forceLayout(ownerDocument: Document) { // This function exists to trick minifiers to not remove this unused member expression. return (ownerDocument.documentElement: any).clientHeight; } function waitForImageToLoad(this: HTMLImageElement, resolve: () => void) { // TODO: Use decode() instead of the load event here once the fix in // https://issues.chromium.org/issues/420748301 has propagated fully. this.addEventListener('load', resolve); this.addEventListener('error', resolve); } export function startViewTransition( suspendedState: null | SuspendedState, rootContainer: Container, transitionTypes: null | TransitionTypes, mutationCallback: () => void, layoutCallback: () => void, afterMutationCallback: () => void, spawnedWorkCallback: () => void, passiveCallback: () => mixed, errorCallback: mixed => void, blockedCallback: string => void, // Profiling-only finishedAnimation: () => void, // Profiling-only ): null | RunningViewTransition { const ownerDocument: Document = rootContainer.nodeType === DOCUMENT_NODE ? (rootContainer: any) : rootContainer.ownerDocument; try { // $FlowFixMe[prop-missing] const transition = ownerDocument.startViewTransition({ update() { // Note: We read the existence of a pending navigation before we apply the // mutations. That way we're not waiting on a navigation that we spawned // from this update. Only navigations that started before this commit. const ownerWindow = ownerDocument.defaultView; const pendingNavigation = ownerWindow.navigation && ownerWindow.navigation.transition; // $FlowFixMe[prop-missing] const previousFontLoadingStatus = ownerDocument.fonts.status; mutationCallback(); const blockingPromises: Array<Promise<any>> = []; if (previousFontLoadingStatus === 'loaded') { // Force layout calculation to trigger font loading. forceLayout(ownerDocument); if ( // $FlowFixMe[prop-missing] ownerDocument.fonts.status === 'loading' ) { // The mutation lead to new fonts being loaded. We should wait on them before continuing. // This avoids waiting for potentially unrelated fonts that were already loading before. // Either in an earlier transition or as part of a sync optimistic state. This doesn't // include preloads that happened earlier. blockingPromises.push(ownerDocument.fonts.ready); } } const blockingIndexSnapshot = blockingPromises.length; if (suspendedState !== null) { // Suspend on any images that still haven't loaded and are in the viewport. const suspenseyImages = suspendedState.suspenseyImages; let imgBytes = 0; for (let i = 0; i < suspenseyImages.length; i++) { const suspenseyImage = suspenseyImages[i]; if (!suspenseyImage.complete) { const rect = suspenseyImage.getBoundingClientRect(); const inViewport = rect.bottom > 0 && rect.right > 0 && rect.top < ownerWindow.innerHeight && rect.left < ownerWindow.innerWidth; if (inViewport) { imgBytes += estimateImageBytes(suspenseyImage); if (imgBytes > estimatedBytesWithinLimit) { // We don't think we'll be able to download all the images within // the timeout. Give up. Rewind to only block on fonts, if any. blockingPromises.length = blockingIndexSnapshot; break; } const loadingImage = new Promise( waitForImageToLoad.bind(suspenseyImage), ); blockingPromises.push(loadingImage); } } } } if (blockingPromises.length > 0) { if (enableProfilerTimer) { const blockedReason = blockingIndexSnapshot > 0 ? blockingPromises.length > blockingIndexSnapshot ? 'Waiting on Fonts and Images' : 'Waiting on Fonts' : 'Waiting on Images'; blockedCallback(blockedReason); } const blockingReady = Promise.race([ Promise.all(blockingPromises), new Promise(resolve => setTimeout(resolve, SUSPENSEY_FONT_AND_IMAGE_TIMEOUT), ), ]).then(layoutCallback, layoutCallback); const allReady = pendingNavigation ? Promise.allSettled([pendingNavigation.finished, blockingReady]) : blockingReady; return allReady.then(afterMutationCallback, afterMutationCallback); } layoutCallback(); if (pendingNavigation) { return pendingNavigation.finished.then( afterMutationCallback, afterMutationCallback, ); } else { afterMutationCallback(); } }, types: transitionTypes, }); // $FlowFixMe[prop-missing] ownerDocument.__reactViewTransition = transition; const readyCallback = () => { const documentElement: Element = (ownerDocument.documentElement: any); // Loop through all View Transition Animations. // $FlowFixMe[prop-missing] const animations = documentElement.getAnimations({subtree: true}); for (let i = 0; i < animations.length; i++) { const animation = animations[i]; const effect: KeyframeEffect = (animation.effect: any); // $FlowFixMe const pseudoElement: ?string = effect.pseudoElement; if ( pseudoElement != null && pseudoElement.startsWith('::view-transition') ) { const keyframes = effect.getKeyframes(); // Next, we're going to try to optimize this animation in case the auto-generated // width/height keyframes are unnecessary. let width; let height; let unchangedDimensions = true; for (let j = 0; j < keyframes.length; j++) { const keyframe = keyframes[j]; const w = keyframe.width; if (width === undefined) { width = w; } else if (width !== w) { unchangedDimensions = false; break; } const h = keyframe.height; if (height === undefined) { height = h; } else if (height !== h) { unchangedDimensions = false; break; } // We're clearing the keyframes in case we are going to apply the optimization. delete keyframe.width; delete keyframe.height; if (keyframe.transform === 'none') { delete keyframe.transform; } } if ( unchangedDimensions && width !== undefined && height !== undefined ) { // Replace the keyframes with ones that don't animate the width/height. // $FlowFixMe effect.setKeyframes(keyframes); // Read back the new animation to see what the underlying width/height of the pseudo-element was. const computedStyle = getComputedStyle( // $FlowFixMe effect.target, // $FlowFixMe effect.pseudoElement, ); if ( computedStyle.width !== width || computedStyle.height !== height ) { // Oops. Turns out that the pseudo-element had a different width/height so we need to let it // be overridden. Add it back. const first = keyframes[0]; first.width = width; first.height = height; const last = keyframes[keyframes.length - 1]; last.width = width; last.height = height; // $FlowFixMe effect.setKeyframes(keyframes); } } } } spawnedWorkCallback(); }; const handleError = (error: mixed) => { // $FlowFixMe[prop-missing] if (ownerDocument.__reactViewTransition === transition) { // $FlowFixMe[prop-missing] ownerDocument.__reactViewTransition = null; } try { error = customizeViewTransitionError(error, false); if (error !== null) { errorCallback(error); } } finally { // Continue the reset of the work. // If the error happened in the snapshot phase before the update callback // was invoked, then we need to first finish the mutation and layout phases. // If they're already invoked it's still safe to call them due the status check. mutationCallback(); layoutCallback(); // Skip afterMutationCallback() since we're not animating. spawnedWorkCallback(); if (enableProfilerTimer) { finishedAnimation(); } } }; transition.ready.then(readyCallback, handleError); transition.finished.finally(() => { cancelAllViewTransitionAnimations((ownerDocument.documentElement: any)); // $FlowFixMe[prop-missing] if (ownerDocument.__reactViewTransition === transition) { // $FlowFixMe[prop-missing] ownerDocument.__reactViewTransition = null; } if (enableProfilerTimer) { finishedAnimation(); } passiveCallback(); }); return transition; } catch (x) { // We use the error as feature detection. // The only thing that should throw is if startViewTransition is missing // or if it doesn't accept the object form. Other errors are async. // I.e. it's before the View Transitions v2 spec. We only support View // Transitions v2 otherwise we fallback to not animating to ensure that // we're not animating with the wrong animation mapped. // Flush remaining work synchronously. mutationCallback(); layoutCallback(); // Skip afterMutationCallback(). We don't need it since we're not animating. if (enableProfilerTimer) { finishedAnimation(); } spawnedWorkCallback(); // Skip passiveCallback(). Spawned work will schedule a task. return null; } } export type RunningViewTransition = { skipTransition(): void, ... }; function mergeTranslate(translateA: ?string, translateB: ?string): string { if (!translateA || translateA === 'none') { return translateB || ''; } if (!translateB || translateB === 'none') { // $FlowFixMe[constant-condition] return translateA || ''; } const partsA = translateA.split(' '); const partsB = translateB.split(' '); let i; let result = ''; for (i = 0; i < partsA.length && i < partsB.length; i++) { if (i > 0) { result += ' '; } result += 'calc(' + partsA[i] + ' + ' + partsB[i] + ')'; } for (; i < partsA.length; i++) { result += ' ' + partsA[i]; } for (; i < partsB.length; i++) { result += ' ' + partsB[i]; } return result; } function animateGesture( keyframes: any, targetElement: Element, pseudoElement: string, timeline: GestureTimeline, customTimelineCleanup: Array<() => void>, rangeStart: number, rangeEnd: number, moveFirstFrameIntoViewport: boolean, moveAllFramesIntoViewport: boolean, ) { let width; let height; let unchangedDimensions = true; for (let i = 0; i < keyframes.length; i++) { const keyframe = keyframes[i]; // Delete any easing since we always apply linear easing to gestures. delete keyframe.easing; delete keyframe.computedOffset; const w = keyframe.width; if (width === undefined) { width = w; } else if (width !== w) { unchangedDimensions = false; } const h = keyframe.height; if (height === undefined) { height = h; } else if (height !== h) { unchangedDimensions = false; } // Chrome returns "auto" for width/height which is not a valid value to // animate to. Similarly, transform: "none" is actually lack of transform. if (keyframe.width === 'auto') { delete keyframe.width; } if (keyframe.height === 'auto') { delete keyframe.height; } if (keyframe.transform === 'none') { delete keyframe.transform; } if (moveAllFramesIntoViewport) { if (keyframe.transform == null) { // If a transform is not explicitly specified to override the auto // generated one on the pseudo element, then we need to adjust it to // put it back into the viewport. We don't know the offset relative to // the screen so instead we use the translate prop to do a relative // adjustment. // TODO: If the "transform" was manually overridden on the pseudo // element itself and no longer the auto generated one, then we shouldn't // adjust it. I'm not sure how to detect this. if (keyframe.translate == null || keyframe.translate === '') { // TODO: If there's a CSS rule targeting translate on the pseudo element // already we need to merge it. const elementTranslate: ?string = (getComputedStyle( targetElement, pseudoElement, ): any).translate; keyframe.translate = mergeTranslate( elementTranslate, '20000px 20000px', ); } else { keyframe.translate = mergeTranslate( keyframe.translate, '20000px 20000px', ); } } } } if (moveFirstFrameIntoViewport) { // If this is the generated animation that does a FLIP matrix translation // from the old position, we need to adjust it from the out of viewport // position. If this is going from old to new it only applies to first // keyframe. Otherwise it applies to every keyframe. moveOldFrameIntoViewport(keyframes[0]); } if (unchangedDimensions && width !== undefined && height !== undefined) { // Read the underlying width/height of the pseudo-element. The previous animation // should have already been cancelled so we should observe the underlying element. const computedStyle = getComputedStyle(targetElement, pseudoElement); if (computedStyle.width === width && computedStyle.height === height) { for (let i = 0; i < keyframes.length; i++) { const keyframe = keyframes[i]; delete keyframe.width; delete keyframe.height; } } } // TODO: Reverse the reverse if the original direction is reverse. const reverse = rangeStart > rangeEnd; if (timeline instanceof AnimationTimeline) { // Native Timeline // $FlowFixMe[incompatible-call] targetElement.animate(keyframes, { pseudoElement: pseudoElement, // Set the timeline to the current gesture timeline to drive the updates. timeline: timeline, // We reset all easing functions to linear so that it feels like you // have direct impact on the transition and to avoid double bouncing // from scroll bouncing. easing: 'linear', // We fill in both direction for overscroll. fill: 'both', // TODO: Should we preserve the fill instead? // We play all gestures in reverse, except if we're in reverse direction // in which case we need to play it in reverse of the reverse. direction: reverse ? 'normal' : 'reverse', // Range start needs to be higher than range end. If it goes in reverse // we reverse the whole animation below. rangeStart: (reverse ? rangeEnd : rangeStart) + '%', rangeEnd: (reverse ? rangeStart : rangeEnd) + '%', }); } else { // Custom Timeline // $FlowFixMe[incompatible-call] const animation = targetElement.animate(keyframes, { pseudoElement: pseudoElement, // We reset all easing functions to linear so that it feels like you // have direct impact on the transition and to avoid double bouncing // from scroll bouncing. easing: 'linear', // We fill in both direction for overscroll. fill: 'both', // TODO: Should we preserve the fill instead? // We play all gestures in reverse, except if we're in reverse direction // in which case we need to play it in reverse of the reverse. direction: reverse ? 'normal' : 'reverse', // We set the delay and duration to represent the span of the range. delay: reverse ? rangeEnd : rangeStart, duration: reverse ? rangeStart - rangeEnd : rangeEnd - rangeStart, }); // Let the custom timeline take control of driving the animation. const cleanup = timeline.animate(animation); if (cleanup) { customTimelineCleanup.push(cleanup); } } } export function startGestureTransition( suspendedState: null | SuspendedState, rootContainer: Container, timeline: GestureTimeline, rangeStart: number, rangeEnd: number, transitionTypes: null | TransitionTypes, mutationCallback: () => void, animateCallback: () => void, errorCallback: mixed => void, finishedAnimation: () => void, // Profiling-only ): null | RunningViewTransition { const ownerDocument: Document = rootContainer.nodeType === DOCUMENT_NODE ? (rootContainer: any) : rootContainer.ownerDocument; try { // Force layout before we start the Transition. This works around a bug in Safari // if one of the clones end up being a stylesheet that isn't loaded or uncached. // https://bugs.webkit.org/show_bug.cgi?id=290146 forceLayout(ownerDocument); // $FlowFixMe[prop-missing] const transition = ownerDocument.startViewTransition({ update: mutationCallback, types: transitionTypes, }); // $FlowFixMe[prop-missing] ownerDocument.__reactViewTransition = transition; const customTimelineCleanup: Array<() => void> = []; // Cleanup Animations started in a CustomTimeline const readyCallback = () => { const documentElement: Element = (ownerDocument.documentElement: any); // Loop through all View Transition Animations. // $FlowFixMe[prop-missing] const animations = documentElement.getAnimations({subtree: true}); // First do a pass to collect all known group and new items so we can look // up if they exist later. const foundGroups: Set<string> = new Set(); const foundNews: Set<string> = new Set(); // Collect the longest duration of any view-transition animation including delay. let longestDuration = 0; for (let i = 0; i < animations.length; i++) { const effect: KeyframeEffect = (animations[i].effect: any); // $FlowFixMe const pseudoElement: ?string = effect.pseudoElement; if (pseudoElement == null) { } else if (pseudoElement.startsWith('::view-transition')) { const timing = effect.getTiming(); const duration = // $FlowFixMe[prop-missing] typeof timing.duration === 'number' ? timing.duration : 0; // TODO: Consider interation count higher than 1. // $FlowFixMe[prop-missing] // $FlowFixMe[unsafe-addition] const durationWithDelay = timing.delay + duration; if (durationWithDelay > longestDuration) { longestDuration = durationWithDelay; } if (pseudoElement.startsWith('::view-transition-group')) { foundGroups.add(pseudoElement.slice(23)); } else if (pseudoElement.startsWith('::view-transition-new')) { // TODO: This is not really a sufficient detection because if the new // pseudo element might exist but have animations disabled on it. foundNews.add(pseudoElement.slice(21)); } } } const durationToRangeMultipler = (rangeEnd - rangeStart) / longestDuration; for (let i = 0; i < animations.length; i++) { const anim = animations[i]; if (anim.playState !== 'running') { continue; } const effect: KeyframeEffect = (anim.effect: any); // $FlowFixMe const pseudoElement: ?string = effect.pseudoElement; if ( pseudoElement != null && pseudoElement.startsWith('::view-transition') && effect.target === documentElement ) { // Ideally we could mutate the existing animation but unfortunately // the mutable APIs seem less tested and therefore are lacking or buggy. // Therefore we create a new animation instead. anim.cancel(); let isGeneratedGroupAnim = false; let isExitGroupAnim = false; if (pseudoElement.startsWith('::view-transition-group')) { const groupName = pseudoElement.slice(23); if (foundNews.has(groupName)) { // If this has both "new" and "old" state we expect this to be an auto-generated // animation that started outside the viewport. We need to adjust this first frame // to be inside the viewport. // $FlowFixMe[prop-missing] const animationName: ?string = anim.animationName; isGeneratedGroupAnim = animationName != null && // $FlowFixMe[prop-missing] animationName.startsWith('-ua-view-transition-group-anim-'); } else { // If this has only an "old" state then the pseudo element will be outside // the viewport. If any keyframes don't override "transform" we need to // adjust them. isExitGroupAnim = true; } // TODO: If this has only an old state and no new state, } // Adjust the range based on how long the animation would've ran as time based. // Since we're running animations in reverse from how they normally would run, // therefore the timing is from the rangeEnd to the start. const timing = effect.getTiming(); const duration = // $FlowFixMe[prop-missing] typeof timing.duration === 'number' ? timing.duration : 0; let adjustedRangeStart = // $FlowFixMe[unsafe-addition] // $FlowFixMe[prop-missing] rangeEnd - (duration + timing.delay) * durationToRangeMultipler; let adjustedRangeEnd = rangeEnd - // $FlowFixMe[prop-missing] // $FlowFixMe[unsafe-arithmetic] timing.delay * durationToRangeMultipler; if ( timing.direction === 'reverse' || timing.direction === 'alternate-reverse' ) { // This animation was originally in reverse so we have to play it in flipped range. const temp = adjustedRangeStart; adjustedRangeStart = adjustedRangeEnd; adjustedRangeEnd = temp; } animateGesture( effect.getKeyframes(), // $FlowFixMe: Always documentElement atm. effect.target, pseudoElement, timeline, customTimelineCleanup, adjustedRangeStart, adjustedRangeEnd, isGeneratedGroupAnim, isExitGroupAnim, ); if (pseudoElement.startsWith('::view-transition-old')) { const groupName = pseudoElement.slice(21); if (!foundGroups.has(groupName) && !foundNews.has(groupName)) { foundGroups.add(groupName); // We haven't seen any group animation with this name. Since the old // state was outside the viewport we need to put it back. Since we // can't programmatically target the element itself, we use an // animation to adjust it. // This usually happens for exit animations where the element has // the old position. // If we also have a "new" state then we skip this because it means // someone manually disabled the auto-generated animation. We need to // treat the old state as having the position of the "new" state which // will happen by default. const pseudoElementName = '::view-transition-group' + groupName; animateGesture( [{}, {}], // $FlowFixMe: Always documentElement atm. effect.target, pseudoElementName, timeline, customTimelineCleanup, rangeStart, rangeEnd, false, true, // We let the helper apply the translate ); } } } } // View Transitions with ScrollTimeline has a quirk where they end if the // ScrollTimeline ever reaches 100% but that doesn't mean we're done because // you can swipe back again. We can prevent this by adding a paused Animation // that never stops. This seems to keep all running Animations alive until // we explicitly abort (or something forces the View Transition to cancel). // $FlowFixMe[incompatible-call] const blockingAnim = documentElement.animate([{}, {}], { pseudoElement: '::view-transition', duration: 1, }); blockingAnim.pause(); animateCallback(); }; // In Chrome, "new" animations are not ready in the ready callback. We have to wait // until requestAnimationFrame before we can observe them through getAnimations(). // However, in Safari, that would cause a flicker because we're applying them late. // TODO: Think of a feature detection for this instead. const readyForAnimations = navigator.userAgent.indexOf('Chrome') !== -1 ? () => requestAnimationFrame(readyCallback) : readyCallback; const handleError = (error: mixed) => { // $FlowFixMe[prop-missing] if (ownerDocument.__reactViewTransition === transition) { // $FlowFixMe[prop-missing] ownerDocument.__reactViewTransition = null; } try { error = customizeViewTransitionError(error, true); if (error !== null) { errorCallback(error); } } finally { // Continue the reset of the work. // If the error happened in the snapshot phase before the update callback // was invoked, then we need to first finish the mutation and layout phases. // If they're already invoked it's still safe to call them due the status check. mutationCallback(); // Skip readyCallback() and go straight to animateCallbck() since we're not animating. // animateCallback() is still required to restore states. animateCallback(); if (enableProfilerTimer) { finishedAnimation(); } } }; transition.ready.then(readyForAnimations, handleError); transition.finished.finally(() => { cancelAllViewTransitionAnimations((ownerDocument.documentElement: any)); for (let i = 0; i < customTimelineCleanup.length; i++) { const cleanup = customTimelineCleanup[i]; cleanup(); } // $FlowFixMe[prop-missing] if (ownerDocument.__reactViewTransition === transition) { // $FlowFixMe[prop-missing] ownerDocument.__reactViewTransition = null; } if (enableProfilerTimer) { // Signal that the Transition was unable to continue. We do that here // instead of when we stop the running View Transition to ensure that // we cover cases when something else stops it early. finishedAnimation(); } }); return transition; } catch (x) { // We use the error as feature detection. // The only thing that should throw is if startViewTransition is missing // or if it doesn't accept the object form. Other errors are async. // I.e. it's before the View Transitions v2 spec. We only support View // Transitions v2 otherwise we fallback to not animating to ensure that // we're not animating with the wrong animation mapped. // Run through the sequence to put state back into a consistent state. mutationCallback(); animateCallback(); if (enableProfilerTimer) { finishedAnimation(); } return null; } } export function stopViewTransition(transition: RunningViewTransition) { transition.skipTransition(); } interface ViewTransitionPseudoElementType extends mixin$Animatable { _scope: HTMLElement; _selector: string; getComputedStyle(): CSSStyleDeclaration; } function ViewTransitionPseudoElement( this: ViewTransitionPseudoElementType, pseudo: string, name: string, ) { // TODO: Get the owner document from the root container. this._scope = (document.documentElement: any); this._selector = '::view-transition-' + pseudo + '(' + name + ')'; } // $FlowFixMe[prop-missing] ViewTransitionPseudoElement.prototype.animate = function ( this: ViewTransitionPseudoElementType, keyframes: Keyframe[] | PropertyIndexedKeyframes | null, options?: number | KeyframeAnimationOptions, ): Animation { const opts: any = typeof options === 'number' ? { duration: options, } : Object.assign( (// $FlowFixMe[prop-missing] {}: KeyframeAnimationOptions), options, ); opts.pseudoElement = this._selector; // TODO: Handle multiple child instances. return this._scope.animate(keyframes, opts); }; // $FlowFixMe[prop-missing] ViewTransitionPseudoElement.prototype.getAnimations = function ( this: ViewTransitionPseudoElementType, options?: GetAnimationsOptions, ): Animation[] { const scope = this._scope; const selector = this._selector; const animations = scope.getAnimations( // $FlowFixMe[prop-missing] {subtree: true}, ); const result = []; for (let i = 0; i < animations.length; i++) { const effect: null | { target?: Element, pseudoElement?: string, ... } = (animations[i].effect: any); // TODO: Handle multiple child instances. if ( effect !== null && effect.target === scope && effect.pseudoElement === selector ) { result.push(animations[i]); } } return result; }; // $FlowFixMe[prop-missing] ViewTransitionPseudoElement.prototype.getComputedStyle = function ( this: ViewTransitionPseudoElementType, ): CSSStyleDeclaration { const scope = this._scope; const selector = this._selector; return getComputedStyle(scope, selector); }; export function createViewTransitionInstance( name: string, ): ViewTransitionInstance { return { name: name, group: new (ViewTransitionPseudoElement: any)('group', name), imagePair: new (ViewTransitionPseudoElement: any)('image-pair', name), old: new (ViewTransitionPseudoElement: any)('old', name), new: new (ViewTransitionPseudoElement: any)('new', name), }; } interface CustomTimeline { currentTime: number; animate(animation: Animation): void | (() => void); } export type GestureTimeline = AnimationTimeline | CustomTimeline; export function getCurrentGestureOffset(timeline: GestureTimeline): number { const time = timeline.currentTime; if (time === null) { throw new Error( 'Cannot start a gesture with a disconnected AnimationTimeline.', ); } return typeof time === 'number' ? time : time.value; } type StoredEventListener = { type: string, listener: EventListener, optionsOrUseCapture: void | EventListenerOptionsOrUseCapture, }; export type FragmentInstanceType = { _fragmentFiber: Fiber, _eventListeners: null | Array<StoredEventListener>, _observers: null | Set<IntersectionObserver | ResizeObserver>, addEventListener( type: string, listener: EventListener, optionsOrUseCapture?: EventListenerOptionsOrUseCapture, ): void, removeEventListener( type: string, listener: EventListener, optionsOrUseCapture?: EventListenerOptionsOrUseCapture, ): void, dispatchEvent(event: Event): boolean, focus(focusOptions?: FocusOptions): void, focusLast(focusOptions?: FocusOptions): void, blur(): void, observeUsing(observer: IntersectionObserver | ResizeObserver): void, unobserveUsing(observer: IntersectionObserver | ResizeObserver): void, getClientRects(): Array<DOMRect>, getRootNode(getRootNodeOptions?: { composed: boolean, }): Document | ShadowRoot | FragmentInstanceType, compareDocumentPosition(otherNode: Instance): number, scrollIntoView(alignToTop?: boolean): void, }; function FragmentInstance(this: FragmentInstanceType, fragmentFiber: Fiber) { this._fragmentFiber = fragmentFiber; this._eventListeners = null; this._observers = null; } // $FlowFixMe[prop-missing] FragmentInstance.prototype.addEventListener = function ( this: FragmentInstanceType, type: string, listener: EventListener, optionsOrUseCapture?: EventListenerOptionsOrUseCapture, ): void { if (this._eventListeners === null) { this._eventListeners = []; } const listeners = this._eventListeners; // Element.addEventListener will only apply uniquely new event listeners by default. Since we // need to collect the listeners to apply to appended children, we track them ourselves and use // custom equality check for the options. const isNewEventListener = indexOfEventListener(listeners, type, listener, optionsOrUseCapture) === -1; if (isNewEventListener) { listeners.push({type, listener, optionsOrUseCapture}); traverseFragmentInstance( this._fragmentFiber, addEventListenerToChild, type, listener, optionsOrUseCapture, ); } this._eventListeners = listeners; }; function addEventListenerToChild( child: Fiber, type: string, listener: EventListener, optionsOrUseCapture?: EventListenerOptionsOrUseCapture, ): boolean { const instance = getInstanceFromHostFiber<Instance>(child); instance.addEventListener(type, listener, optionsOrUseCapture); return false; } // $FlowFixMe[prop-missing] FragmentInstance.prototype.removeEventListener = function ( this: FragmentInstanceType, type: string, listener: EventListener, optionsOrUseCapture?: EventListenerOptionsOrUseCapture, ): void { const listeners = this._eventListeners; if (listeners === null) { return; } if (typeof listeners !== 'undefined' && listeners.length > 0) { traverseFragmentInstance( this._fragmentFiber, removeEventListenerFromChild, type, listener, optionsOrUseCapture, ); const index = indexOfEventListener( listeners, type, listener, optionsOrUseCapture, ); if (this._eventListeners !== null) { this._eventListeners.splice(index, 1); } } }; function removeEventListenerFromChild( child: Fiber, type: string, listener: EventListener, optionsOrUseCapture?: EventListenerOptionsOrUseCapture, ): boolean { const instance = getInstanceFromHostFiber<Instance>(child); instance.removeEventListener(type, listener, optionsOrUseCapture); return false; } function normalizeListenerOptions( opts: ?EventListenerOptionsOrUseCapture, ): string { if (opts == null) { return '0'; } if (typeof opts === 'boolean') { return `c=${opts ? '1' : '0'}`; } return `c=${opts.capture ? '1' : '0'}&o=${opts.once ? '1' : '0'}&p=${opts.passive ? '1' : '0'}`; } function indexOfEventListener( eventListeners: Array<StoredEventListener>, type: string, listener: EventListener, optionsOrUseCapture: void | EventListenerOptionsOrUseCapture, ): number { for (let i = 0; i < eventListeners.length; i++) { const item = eventListeners[i]; if ( item.type === type && item.listener === listener && normalizeListenerOptions(item.optionsOrUseCapture) === normalizeListenerOptions(optionsOrUseCapture) ) { return i; } } return -1; } // $FlowFixMe[prop-missing] FragmentInstance.prototype.dispatchEvent = function ( this: FragmentInstanceType, event: Event, ): boolean { const parentHostFiber = getFragmentParentHostFiber(this._fragmentFiber); if (parentHostFiber === null) { return true; } const parentHostInstance = getInstanceFromHostFiber<Instance>(parentHostFiber); const eventListeners = this._eventListeners; if ( (eventListeners !== null && eventListeners.length > 0) || !event.bubbles ) { const temp = document.createTextNode(''); if (eventListeners) { for (let i = 0; i < eventListeners.length; i++) { const {type, listener, optionsOrUseCapture} = eventListeners[i]; temp.addEventListener(type, listener, optionsOrUseCapture); } } parentHostInstance.appendChild(temp); const cancelable = temp.dispatchEvent(event); if (eventListeners) { for (let i = 0; i < eventListeners.length; i++) { const {type, listener, optionsOrUseCapture} = eventListeners[i]; temp.removeEventListener(type, listener, optionsOrUseCapture); } } parentHostInstance.removeChild(temp); return cancelable; } else { return parentHostInstance.dispatchEvent(event); } }; // $FlowFixMe[prop-missing] FragmentInstance.prototype.focus = function ( this: FragmentInstanceType, focusOptions?: FocusOptions, ): void { traverseFragmentInstanceDeeply( this._fragmentFiber, setFocusOnFiberIfFocusable, focusOptions, ); }; function setFocusOnFiberIfFocusable( fiber: Fiber, focusOptions?: FocusOptions, ): boolean { const instance = getInstanceFromHostFiber<Instance>(fiber); return setFocusIfFocusable(instance, focusOptions); } // $FlowFixMe[prop-missing] FragmentInstance.prototype.focusLast = function ( this: FragmentInstanceType, focusOptions?: FocusOptions, ): void { const children: Array<Fiber> = []; traverseFragmentInstanceDeeply( this._fragmentFiber, collectChildren, children, ); for (let i = children.length - 1; i >= 0; i--) { const child = children[i]; if (setFocusOnFiberIfFocusable(child, focusOptions)) { break; } } }; function collectChildren(child: Fiber, collection: Array<Fiber>): boolean { collection.push(child); return false; } // $FlowFixMe[prop-missing] FragmentInstance.prototype.blur = function (this: FragmentInstanceType): void { // TODO: When we have a parent element reference, we can skip traversal if the fragment's parent // does not contain document.activeElement traverseFragmentInstance( this._fragmentFiber, blurActiveElementWithinFragment, ); }; function blurActiveElementWithinFragment(child: Fiber): boolean { // TODO: We can get the activeElement from the parent outside of the loop when we have a reference. const instance = getInstanceFromHostFiber<Instance>(child); const ownerDocument = instance.ownerDocument; if (instance === ownerDocument.activeElement) { // $FlowFixMe[prop-missing] instance.blur(); return true; } return false; } // $FlowFixMe[prop-missing] FragmentInstance.prototype.observeUsing = function ( this: FragmentInstanceType, observer: IntersectionObserver | ResizeObserver, ): void { if (this._observers === null) { this._observers = new Set(); } this._observers.add(observer); traverseFragmentInstance(this._fragmentFiber, observeChild, observer); }; function observeChild( child: Fiber, observer: IntersectionObserver | ResizeObserver, ) { const instance = getInstanceFromHostFiber<Instance>(child); observer.observe(instance); return false; } // $FlowFixMe[prop-missing] FragmentInstance.prototype.unobserveUsing = function ( this: FragmentInstanceType, observer: IntersectionObserver | ResizeObserver, ): void { const observers = this._observers; if (observers === null || !observers.has(observer)) { if (__DEV__) { console.error( 'You are calling unobserveUsing() with an observer that is not being observed with this fragment ' + 'instance. First attach the observer with observeUsing()', ); } } else { observers.delete(observer); traverseFragmentInstance(this._fragmentFiber, unobserveChild, observer); } }; function unobserveChild( child: Fiber, observer: IntersectionObserver | ResizeObserver, ) { const instance = getInstanceFromHostFiber<Instance>(child); observer.unobserve(instance); return false; } // $FlowFixMe[prop-missing] FragmentInstance.prototype.getClientRects = function ( this: FragmentInstanceType, ): Array<DOMRect> { const rects: Array<DOMRect> = []; traverseFragmentInstance(this._fragmentFiber, collectClientRects, rects); return rects; }; function collectClientRects(child: Fiber, rects: Array<DOMRect>): boolean { const instance = getInstanceFromHostFiber<Instance>(child); // $FlowFixMe[method-unbinding] rects.push.apply(rects, instance.getClientRects()); return false; } // $FlowFixMe[prop-missing] FragmentInstance.prototype.getRootNode = function ( this: FragmentInstanceType, getRootNodeOptions?: {composed: boolean}, ): Document | ShadowRoot | FragmentInstanceType { const parentHostFiber = getFragmentParentHostFiber(this._fragmentFiber); if (parentHostFiber === null) { return this; } const parentHostInstance = getInstanceFromHostFiber<Instance>(parentHostFiber); const rootNode = // $FlowFixMe[incompatible-cast] Flow expects Node (parentHostInstance.getRootNode(getRootNodeOptions): Document | ShadowRoot); return rootNode; }; // $FlowFixMe[prop-missing] FragmentInstance.prototype.compareDocumentPosition = function ( this: FragmentInstanceType, otherNode: Instance, ): number { const parentHostFiber = getFragmentParentHostFiber(this._fragmentFiber); if (parentHostFiber === null) { return Node.DOCUMENT_POSITION_DISCONNECTED; } const children: Array<Fiber> = []; traverseFragmentInstance(this._fragmentFiber, collectChildren, children); const parentHostInstance = getInstanceFromHostFiber<Instance>(parentHostFiber); if (children.length === 0) { return compareDocumentPositionForEmptyFragment( this._fragmentFiber, parentHostInstance, otherNode, getInstanceFromHostFiber, ); } const firstElement = getInstanceFromHostFiber<Instance>(children[0]); const lastElement = getInstanceFromHostFiber<Instance>( children[children.length - 1], ); // If the fragment has been portaled into another host instance, we need to // our best guess is to use the parent of the child instance, rather than // the fiber tree host parent. const firstInstance = getInstanceFromHostFiber<Instance>(children[0]); const parentHostInstanceFromDOM = fiberIsPortaledIntoHost(this._fragmentFiber) ? (firstInstance.parentElement: ?Instance) : parentHostInstance; if (parentHostInstanceFromDOM == null) { return Node.DOCUMENT_POSITION_DISCONNECTED; } // Check if first and last element are actually in the expected document position // before relying on them as source of truth for other contained elements const firstElementIsContained = parentHostInstanceFromDOM.compareDocumentPosition(firstElement) & Node.DOCUMENT_POSITION_CONTAINED_BY; const lastElementIsContained = parentHostInstanceFromDOM.compareDocumentPosition(lastElement) & Node.DOCUMENT_POSITION_CONTAINED_BY; const firstResult = firstElement.compareDocumentPosition(otherNode); const lastResult = lastElement.compareDocumentPosition(otherNode); const otherNodeIsFirstOrLastChild = (firstElementIsContained && firstElement === otherNode) || (lastElementIsContained && lastElement === otherNode); const otherNodeIsFirstOrLastChildDisconnected = (!firstElementIsContained && firstElement === otherNode) || (!lastElementIsContained && lastElement === otherNode); const otherNodeIsWithinFirstOrLastChild = firstResult & Node.DOCUMENT_POSITION_CONTAINED_BY || lastResult & Node.DOCUMENT_POSITION_CONTAINED_BY; const otherNodeIsBetweenFirstAndLastChildren = firstElementIsContained && lastElementIsContained && firstResult & Node.DOCUMENT_POSITION_FOLLOWING && lastResult & Node.DOCUMENT_POSITION_PRECEDING; let result = Node.DOCUMENT_POSITION_DISCONNECTED; if ( otherNodeIsFirstOrLastChild || otherNodeIsWithinFirstOrLastChild || otherNodeIsBetweenFirstAndLastChildren ) { result = Node.DOCUMENT_POSITION_CONTAINED_BY; } else if (otherNodeIsFirstOrLastChildDisconnected) { // otherNode has been portaled into another container result = Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC; } else { result = firstResult; } if ( result & Node.DOCUMENT_POSITION_DISCONNECTED || result & Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC ) { return result; } // Now that we have the result from the DOM API, we double check it matches // the state of the React tree. If it doesn't, we have a case of portaled or // otherwise injected elements and we return DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC. const documentPositionMatchesFiberPosition = validateDocumentPositionWithFiberTree( result, this._fragmentFiber, children[0], children[children.length - 1], otherNode, ); if (documentPositionMatchesFiberPosition) { return result; } return Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC; }; function validateDocumentPositionWithFiberTree( documentPosition: number, fragmentFiber: Fiber, precedingBoundaryFiber: Fiber, followingBoundaryFiber: Fiber, otherNode: Instance, ): boolean { const otherFiber = getClosestInstanceFromNode(otherNode); if (documentPosition & Node.DOCUMENT_POSITION_CONTAINED_BY) { return ( !!otherFiber && isFiberContainedByFragment(otherFiber, fragmentFiber) ); } if (documentPosition & Node.DOCUMENT_POSITION_CONTAINS) { if (otherFiber === null) { // otherFiber could be null if its the document or body element const ownerDocument = otherNode.ownerDocument; return otherNode === ownerDocument || otherNode === ownerDocument.body; } return isFragmentContainedByFiber(fragmentFiber, otherFiber); } if (documentPosition & Node.DOCUMENT_POSITION_PRECEDING) { return ( !!otherFiber && (otherFiber === precedingBoundaryFiber || isFiberPreceding(precedingBoundaryFiber, otherFiber)) ); } if (documentPosition & Node.DOCUMENT_POSITION_FOLLOWING) { return ( !!otherFiber && (otherFiber === followingBoundaryFiber || isFiberFollowing(followingBoundaryFiber, otherFiber)) ); } return false; } if (enableFragmentRefsScrollIntoView) { // $FlowFixMe[prop-missing] FragmentInstance.prototype.scrollIntoView = function ( this: FragmentInstanceType, alignToTop?: boolean, ): void { if (typeof alignToTop === 'object') { throw new Error( 'FragmentInstance.scrollIntoView() does not support ' + 'scrollIntoViewOptions. Use the alignToTop boolean instead.', ); } // First, get the children nodes const children: Array<Fiber> = []; traverseFragmentInstance(this._fragmentFiber, collectChildren, children); const resolvedAlignToTop = alignToTop !== false; // If there are no children, we can use the parent and siblings to determine a position if (children.length === 0) { const hostSiblings = getFragmentInstanceSiblings(this._fragmentFiber); const targetFiber = resolvedAlignToTop ? hostSiblings[1] || hostSiblings[0] || getFragmentParentHostFiber(this._fragmentFiber) : hostSiblings[0] || hostSiblings[1]; if (targetFiber === null) { if (__DEV__) { console.warn( 'You are attempting to scroll a FragmentInstance that has no ' + 'children, siblings, or parent. No scroll was performed.', ); } return; } const target = getInstanceFromHostFiber<Instance>(targetFiber); target.scrollIntoView(alignToTop); return; } let i = resolvedAlignToTop ? children.length - 1 : 0; while (i !== (resolvedAlignToTop ? -1 : children.length)) { const child = children[i]; const instance = getInstanceFromHostFiber<Instance>(child); instance.scrollIntoView(alignToTop); i += resolvedAlignToTop ? -1 : 1; } }; } export function createFragmentInstance( fragmentFiber: Fiber, ): FragmentInstanceType { return new (FragmentInstance: any)(fragmentFiber); } export function updateFragmentInstanceFiber( fragmentFiber: Fiber, instance: FragmentInstanceType, ): void { instance._fragmentFiber = fragmentFiber; } export function commitNewChildToFragmentInstance( childInstance: Instance, fragmentInstance: FragmentInstanceType, ): void { const eventListeners = fragmentInstance._eventListeners; if (eventListeners !== null) { for (let i = 0; i < eventListeners.length; i++) { const {type, listener, optionsOrUseCapture} = eventListeners[i]; childInstance.addEventListener(type, listener, optionsOrUseCapture); } } if (fragmentInstance._observers !== null) { fragmentInstance._observers.forEach(observer => { observer.observe(childInstance); }); } } export function deleteChildFromFragmentInstance( childElement: Instance, fragmentInstance: FragmentInstanceType, ): void { const eventListeners = fragmentInstance._eventListeners; if (eventListeners !== null) { for (let i = 0; i < eventListeners.length; i++) { const {type, listener, optionsOrUseCapture} = eventListeners[i]; childElement.removeEventListener(type, listener, optionsOrUseCapture); } } } export function clearContainer(container: Container): void { const nodeType = container.nodeType; if (nodeType === DOCUMENT_NODE) { clearContainerSparingly(container); } else if (nodeType === ELEMENT_NODE) { switch (container.nodeName) { case 'HEAD': case 'HTML': case 'BODY': clearContainerSparingly(container); return; default: { container.textContent = ''; } } } } function clearContainerSparingly(container: Node) { let node; let nextNode: ?Node = container.firstChild; if (nextNode && nextNode.nodeType === DOCUMENT_TYPE_NODE) { nextNode = nextNode.nextSibling; } while (nextNode) { node = nextNode; nextNode = nextNode.nextSibling; switch (node.nodeName) { case 'HTML': case 'HEAD': case 'BODY': { const element: Element = (node: any); clearContainerSparingly(element); // If these singleton instances had previously been rendered with React they // may still hold on to references to the previous fiber tree. We detatch them // prospectively to reset them to a baseline starting state since we cannot create // new instances. detachDeletedInstance(element); continue; } // Script tags are retained to avoid an edge case bug. Normally scripts will execute if they // are ever inserted into the DOM. However when streaming if a script tag is opened but not // yet closed some browsers create and insert the script DOM Node but the script cannot execute // yet until the closing tag is parsed. If something causes React to call clearContainer while // this DOM node is in the document but not yet executable the DOM node will be removed from the // document and when the script closing tag comes in the script will not end up running. This seems // to happen in Chrome/Firefox but not Safari at the moment though this is not necessarily specified // behavior so it could change in future versions of browsers. While leaving all scripts is broader // than strictly necessary this is the least amount of additional code to avoid this breaking // edge case. // // Style tags are retained because they may likely come from 3rd party scripts and extensions case 'SCRIPT': case 'STYLE': { continue; } // Stylesheet tags are retained because they may likely come from 3rd party scripts and extensions case 'LINK': { if (((node: any): HTMLLinkElement).rel.toLowerCase() === 'stylesheet') { continue; } } } container.removeChild(node); } return; } function clearHead(head: Element): void { let node = head.firstChild; while (node) { const nextNode = node.nextSibling; const nodeName = node.nodeName; if ( isMarkedHoistable(node) || nodeName === 'SCRIPT' || nodeName === 'STYLE' || (nodeName === 'LINK' && ((node: any): HTMLLinkElement).rel.toLowerCase() === 'stylesheet') ) { // retain these nodes } else { head.removeChild(node); } node = nextNode; } return; } // Making this so we can eventually move all of the instance caching to the commit phase. // Currently this is only used to associate fiber and props to instances for hydrating // HostSingletons. The reason we need it here is we only want to make this binding on commit // because only one fiber can own the instance at a time and render can fail/restart export function bindInstance( instance: Instance, props: Props, internalInstanceHandle: mixed, ) { precacheFiberNode((internalInstanceHandle: any), instance); updateFiberProps(instance, props); } // ------------------- // Hydration // ------------------- export const supportsHydration = true; export function canHydrateInstance( instance: HydratableInstance, type: string, props: Props, inRootOrSingleton: boolean, ): null | Instance { while (instance.nodeType === ELEMENT_NODE) { const element: Element = (instance: any); const anyProps = (props: any); if (element.nodeName.toLowerCase() !== type.toLowerCase()) { if (!inRootOrSingleton) { // Usually we error for mismatched tags. if (element.nodeName === 'INPUT' && (element: any).type === 'hidden') { // If we have extra hidden inputs, we don't mismatch. This allows us to embed // extra form data in the original form. } else { return null; } } // In root or singleton parents we skip past mismatched instances. } else if (!inRootOrSingleton) { // Match if (type === 'input' && (element: any).type === 'hidden') { if (__DEV__) { checkAttributeStringCoercion(anyProps.name, 'name'); } const name = anyProps.name == null ? null : '' + anyProps.name; if ( anyProps.type !== 'hidden' || element.getAttribute('name') !== name ) { // Skip past hidden inputs unless that's what we're looking for. This allows us // embed extra form data in the original form. } else { return element; } } else { return element; } } else if (isMarkedHoistable(element)) { // We've already claimed this as a hoistable which isn't hydrated this way so we skip past it. } else { // We have an Element with the right type. // We are going to try to exclude it if we can definitely identify it as a hoisted Node or if // we can guess that the node is likely hoisted or was inserted by a 3rd party script or browser extension // using high entropy attributes for certain types. This technique will fail for strange insertions like // extension prepending <div> in the <body> but that already breaks before and that is an edge case. switch (type) { // case 'title': //We assume all titles are matchable. You should only have one in the Document, at least in a hoistable scope // and if you are a HostComponent with type title we must either be in an <svg> context or this title must have an `itemProp` prop. case 'meta': { // The only way to opt out of hoisting meta tags is to give it an itemprop attribute. We assume there will be // not 3rd party meta tags that are prepended, accepting the cases where this isn't true because meta tags // are usually only functional for SSR so even in a rare case where we did bind to an injected tag the runtime // implications are minimal if (!element.hasAttribute('itemprop')) { // This is a Hoistable break; } return element; } case 'link': { // Links come in many forms and we do expect 3rd parties to inject them into <head> / <body>. We exclude known resources // and then use high-entroy attributes like href which are almost always used and almost always unique to filter out unlikely // matches. const rel = element.getAttribute('rel'); if (rel === 'stylesheet' && element.hasAttribute('data-precedence')) { // This is a stylesheet resource break; } else if ( rel !== anyProps.rel || element.getAttribute('href') !== (anyProps.href == null || anyProps.href === '' ? null : anyProps.href) || element.getAttribute('crossorigin') !== (anyProps.crossOrigin == null ? null : anyProps.crossOrigin) || element.getAttribute('title') !== (anyProps.title == null ? null : anyProps.title) ) { // rel + href should usually be enough to uniquely identify a link however crossOrigin can vary for rel preconnect // and title could vary for rel alternate break; } return element; } case 'style': { // Styles are hard to match correctly. We can exclude known resources but otherwise we accept the fact that a non-hoisted style tags // in <head> or <body> are likely never going to be unmounted given their position in the document and the fact they likely hold global styles if (element.hasAttribute('data-precedence')) { // This is a style resource break; } return element; } case 'script': { // Scripts are a little tricky, we exclude known resources and then similar to links try to use high-entropy attributes // to reject poor matches. One challenge with scripts are inline scripts. We don't attempt to check text content which could // in theory lead to a hydration error later if a 3rd party injected an inline script before the React rendered nodes. // Falling back to client rendering if this happens should be seemless though so we will try this hueristic and revisit later // if we learn it is problematic const srcAttr = element.getAttribute('src'); if ( srcAttr !== (anyProps.src == null ? null : anyProps.src) || element.getAttribute('type') !== (anyProps.type == null ? null : anyProps.type) || element.getAttribute('crossorigin') !== (anyProps.crossOrigin == null ? null : anyProps.crossOrigin) ) { // This script is for a different src/type/crossOrigin. It may be a script resource // or it may just be a mistmatch if ( srcAttr && element.hasAttribute('async') && !element.hasAttribute('itemprop') ) { // This is an async script resource break; } } return element; } default: { // We have excluded the most likely cases of mismatch between hoistable tags, 3rd party script inserted tags, // and browser extension inserted tags. While it is possible this is not the right match it is a decent hueristic // that should work in the vast majority of cases. return element; } } } const nextInstance = getNextHydratableSibling(element); if (nextInstance === null) { break; } instance = nextInstance; } // This is a suspense boundary or Text node or we got the end. // Suspense Boundaries are never expected to be injected by 3rd parties. If we see one it should be matched // and this is a hydration error. // Text Nodes are also not expected to be injected by 3rd parties. This is less of a guarantee for <body> // but it seems reasonable and conservative to reject this as a hydration error as well return null; } export function canHydrateTextInstance( instance: HydratableInstance, text: string, inRootOrSingleton: boolean, ): null | TextInstance { // Empty strings are not parsed by HTML so there won't be a correct match here. if (text === '') return null; while (instance.nodeType !== TEXT_NODE) { if ( instance.nodeType === ELEMENT_NODE && instance.nodeName === 'INPUT' && (instance: any).type === 'hidden' ) { // If we have extra hidden inputs, we don't mismatch. This allows us to // embed extra form data in the original form. } else if (!inRootOrSingleton) { return null; } const nextInstance = getNextHydratableSibling(instance); if (nextInstance === null) { return null; } instance = nextInstance; } // This has now been refined to a text node. return ((instance: any): TextInstance); } function canHydrateHydrationBoundary( instance: HydratableInstance, inRootOrSingleton: boolean, ): null | SuspenseInstance | ActivityInstance { while (instance.nodeType !== COMMENT_NODE) { if ( instance.nodeType === ELEMENT_NODE && instance.nodeName === 'INPUT' && (instance: any).type === 'hidden' ) { // If we have extra hidden inputs, we don't mismatch. This allows us to // embed extra form data in the original form. } else if (!inRootOrSingleton) { return null; } const nextInstance = getNextHydratableSibling(instance); if (nextInstance === null) { return null; } instance = nextInstance; } // This has now been refined to a hydration boundary node. return (instance: any); } export function canHydrateActivityInstance( instance: HydratableInstance, inRootOrSingleton: boolean, ): null | ActivityInstance { const hydratableInstance = canHydrateHydrationBoundary( instance, inRootOrSingleton, ); if ( hydratableInstance !== null && hydratableInstance.data === ACTIVITY_START_DATA ) { return (hydratableInstance: any); } return null; } export function canHydrateSuspenseInstance( instance: HydratableInstance, inRootOrSingleton: boolean, ): null | SuspenseInstance { const hydratableInstance = canHydrateHydrationBoundary( instance, inRootOrSingleton, ); if ( hydratableInstance !== null && hydratableInstance.data !== ACTIVITY_START_DATA ) { return (hydratableInstance: any); } return null; } export function isSuspenseInstancePending(instance: SuspenseInstance): boolean { return ( instance.data === SUSPENSE_PENDING_START_DATA || instance.data === SUSPENSE_QUEUED_START_DATA ); } export function isSuspenseInstanceFallback( instance: SuspenseInstance, ): boolean { return ( instance.data === SUSPENSE_FALLBACK_START_DATA || (instance.data === SUSPENSE_PENDING_START_DATA && instance.ownerDocument.readyState !== DOCUMENT_READY_STATE_LOADING) ); } export function getSuspenseInstanceFallbackErrorDetails( instance: SuspenseInstance, ): { digest: ?string, message?: string, stack?: string, componentStack?: string, } { const dataset = instance.nextSibling && ((instance.nextSibling: any): HTMLElement).dataset; let digest, message, stack, componentStack; if (dataset) { digest = dataset.dgst; if (__DEV__) { message = dataset.msg; stack = dataset.stck; componentStack = dataset.cstck; } } if (__DEV__) { return { message, digest, stack, componentStack, }; } else { // Object gets DCE'd if constructed in tail position and matches callsite destructuring return { digest, }; } } export function registerSuspenseInstanceRetry( instance: SuspenseInstance, callback: () => void, ) { const ownerDocument = instance.ownerDocument; if (instance.data === SUSPENSE_QUEUED_START_DATA) { // The Fizz runtime has already queued this boundary for reveal. We wait for it // to be revealed and then retries. instance._reactRetry = callback; } else if ( // The Fizz runtime must have put this boundary into client render or complete // state after the render finished but before it committed. We need to call the // callback now rather than wait instance.data !== SUSPENSE_PENDING_START_DATA || // The boundary is still in pending status but the document has finished loading // before we could register the event handler that would have scheduled the retry // on load so we call teh callback now. ownerDocument.readyState !== DOCUMENT_READY_STATE_LOADING ) { callback(); } else { // We're still in pending status and the document is still loading so we attach // a listener to the document load even and expose the retry on the instance for // the Fizz runtime to trigger if it ends up resolving this boundary const listener = () => { callback(); ownerDocument.removeEventListener('DOMContentLoaded', listener); }; ownerDocument.addEventListener('DOMContentLoaded', listener); instance._reactRetry = listener; } } export function canHydrateFormStateMarker( instance: HydratableInstance, inRootOrSingleton: boolean, ): null | FormStateMarkerInstance { while (instance.nodeType !== COMMENT_NODE) { if (!inRootOrSingleton) { return null; } const nextInstance = getNextHydratableSibling(instance); if (nextInstance === null) { return null; } instance = nextInstance; } const nodeData = (instance: any).data; if ( nodeData === FORM_STATE_IS_MATCHING || nodeData === FORM_STATE_IS_NOT_MATCHING ) { const markerInstance: FormStateMarkerInstance = (instance: any); return markerInstance; } return null; } export function isFormStateMarkerMatching( markerInstance: FormStateMarkerInstance, ): boolean { return markerInstance.data === FORM_STATE_IS_MATCHING; } function getNextHydratable(node: ?Node) { // Skip non-hydratable nodes. for (; node != null; node = ((node: any): Node).nextSibling) { const nodeType = node.nodeType; if (nodeType === ELEMENT_NODE || nodeType === TEXT_NODE) { break; } if (nodeType === COMMENT_NODE) { const data = (node: any).data; if ( data === SUSPENSE_START_DATA || data === SUSPENSE_FALLBACK_START_DATA || data === SUSPENSE_PENDING_START_DATA || data === SUSPENSE_QUEUED_START_DATA || data === ACTIVITY_START_DATA || data === FORM_STATE_IS_MATCHING || data === FORM_STATE_IS_NOT_MATCHING ) { break; } if (data === SUSPENSE_END_DATA || data === ACTIVITY_END_DATA) { return null; } } } return (node: any); } export function getNextHydratableSibling( instance: HydratableInstance, ): null | HydratableInstance { return getNextHydratable(instance.nextSibling); } export function getFirstHydratableChild( parentInstance: Instance, ): null | HydratableInstance { return getNextHydratable(parentInstance.firstChild); } export function getFirstHydratableChildWithinContainer( parentContainer: Container, ): null | HydratableInstance { let parentElement: Element; switch (parentContainer.nodeType) { case DOCUMENT_NODE: parentElement = (parentContainer: any).body; break; default: { if (parentContainer.nodeName === 'HTML') { parentElement = (parentContainer: any).ownerDocument.body; } else { parentElement = (parentContainer: any); } } } return getNextHydratable(parentElement.firstChild); } export function getFirstHydratableChildWithinActivityInstance( parentInstance: ActivityInstance, ): null | HydratableInstance { return getNextHydratable(parentInstance.nextSibling); } export function getFirstHydratableChildWithinSuspenseInstance( parentInstance: SuspenseInstance, ): null | HydratableInstance { return getNextHydratable(parentInstance.nextSibling); } // If it were possible to have more than one scope singleton in a DOM tree // we would need to model this as a stack but since you can only have one <head> // and head is the only singleton that is a scope in DOM we can get away with // tracking this as a single value. let previousHydratableOnEnteringScopedSingleton: null | HydratableInstance = null; export function getFirstHydratableChildWithinSingleton( type: string, singletonInstance: Instance, currentHydratableInstance: null | HydratableInstance, ): null | HydratableInstance { if (isSingletonScope(type)) { previousHydratableOnEnteringScopedSingleton = currentHydratableInstance; return getNextHydratable(singletonInstance.firstChild); } else { return currentHydratableInstance; } } export function getNextHydratableSiblingAfterSingleton( type: string, currentHydratableInstance: null | HydratableInstance, ): null | HydratableInstance { if (isSingletonScope(type)) { const previousHydratableInstance = previousHydratableOnEnteringScopedSingleton; previousHydratableOnEnteringScopedSingleton = null; return previousHydratableInstance; } else { return currentHydratableInstance; } } export function describeHydratableInstanceForDevWarnings( instance: HydratableInstance, ): string | {type: string, props: $ReadOnly<Props>} { // Reverse engineer a pseudo react-element from hydratable instance if (instance.nodeType === ELEMENT_NODE) { // Reverse engineer a set of props that can print for dev warnings return { type: instance.nodeName.toLowerCase(), props: getPropsFromElement((instance: any)), }; } else if (instance.nodeType === COMMENT_NODE) { if (instance.data === ACTIVITY_START_DATA) { return { type: 'Activity', props: {}, }; } return { type: 'Suspense', props: {}, }; } else { return instance.nodeValue; } } export function validateHydratableInstance( type: string, props: Props, hostContext: HostContext, ): boolean { if (__DEV__) { // TODO: take namespace into account when validating. const hostContextDev: HostContextDev = (hostContext: any); return validateDOMNesting(type, hostContextDev.ancestorInfo); } return true; } export function hydrateInstance( instance: Instance, type: string, props: Props, hostContext: HostContext, internalInstanceHandle: Object, ): boolean { precacheFiberNode(internalInstanceHandle, instance); // TODO: Possibly defer this until the commit phase where all the events // get attached. updateFiberProps(instance, props); return hydrateProperties(instance, type, props, hostContext); } // Returns a Map of properties that were different on the server. export function diffHydratedPropsForDevWarnings( instance: Instance, type: string, props: Props, hostContext: HostContext, ): null | $ReadOnly<Props> { return diffHydratedProperties(instance, type, props, hostContext); } export function validateHydratableTextInstance( text: string, hostContext: HostContext, ): boolean { if (__DEV__) { const hostContextDev = ((hostContext: any): HostContextDev); const ancestor = hostContextDev.ancestorInfo.current; if (ancestor != null) { return validateTextNesting( text, ancestor.tag, hostContextDev.ancestorInfo.implicitRootScope, ); } } return true; } export function hydrateTextInstance( textInstance: TextInstance, text: string, internalInstanceHandle: Object, parentInstanceProps: null | Props, ): boolean { precacheFiberNode(internalInstanceHandle, textInstance); return hydrateText(textInstance, text, parentInstanceProps); } // Returns the server text if it differs from the client. export function diffHydratedTextForDevWarnings( textInstance: TextInstance, text: string, parentProps: null | Props, ): null | string { if ( parentProps === null || parentProps[SUPPRESS_HYDRATION_WARNING] !== true ) { return diffHydratedText(textInstance, text); } return null; } export function hydrateActivityInstance( activityInstance: ActivityInstance, internalInstanceHandle: Object, ) { precacheFiberNode(internalInstanceHandle, activityInstance); } export function hydrateSuspenseInstance( suspenseInstance: SuspenseInstance, internalInstanceHandle: Object, ) { precacheFiberNode(internalInstanceHandle, suspenseInstance); } function getNextHydratableInstanceAfterHydrationBoundary( hydrationInstance: SuspenseInstance | ActivityInstance, ): null | HydratableInstance { let node = hydrationInstance.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 || data === ACTIVITY_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 || data === SUSPENSE_QUEUED_START_DATA || data === ACTIVITY_START_DATA ) { depth++; } } node = node.nextSibling; } // TODO: Warn, we didn't find the end comment boundary. return null; } export function getNextHydratableInstanceAfterActivityInstance( activityInstance: ActivityInstance, ): null | HydratableInstance { return getNextHydratableInstanceAfterHydrationBoundary(activityInstance); } export function getNextHydratableInstanceAfterSuspenseInstance( suspenseInstance: SuspenseInstance, ): null | HydratableInstance { return getNextHydratableInstanceAfterHydrationBoundary(suspenseInstance); } // 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 getParentHydrationBoundary( targetInstance: Node, ): null | SuspenseInstance | ActivityInstance { 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 || data === SUSPENSE_QUEUED_START_DATA || data === ACTIVITY_START_DATA ) { if (depth === 0) { return ((node: any): SuspenseInstance | ActivityInstance); } else { depth--; } } else if (data === SUSPENSE_END_DATA || data === ACTIVITY_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 commitHydratedActivityInstance( activityInstance: ActivityInstance, ): void { // Retry if any event replaying was blocked on this. retryIfBlockedOn(activityInstance); } export function commitHydratedSuspenseInstance( suspenseInstance: SuspenseInstance, ): void { // Retry if any event replaying was blocked on this. retryIfBlockedOn(suspenseInstance); } export function flushHydrationEvents(): void { if (enableHydrationChangeEvent) { flushEventReplaying(); } } export function shouldDeleteUnhydratedTailInstances( parentType: string, ): boolean { return parentType !== 'form' && parentType !== 'button'; } // ------------------- // Test Selectors // ------------------- 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 HostHoistable: case HostSingleton: 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, focusOptions?: FocusOptions, ): 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); // $FlowFixMe[method-unbinding] (element.focus || HTMLElement.prototype.focus).call(element, focusOptions); } finally { element.removeEventListener('focus', handleFocus); } return didFocus; } type RectRatio = { ratio: number, rect: BoundingRect, }; export function setupIntersectionObserver( targets: Array<Instance>, callback: ObserveVisibleRectsCallback, options?: IntersectionObserverOptions, ): { disconnect: () => void, observe: (instance: Instance) => void, unobserve: (instance: Instance) => void, } { const rectRatioCache: Map<Instance, RectRatio> = new Map(); targets.forEach(target => { rectRatioCache.set(target, { rect: getBoundingRect(target), ratio: 0, }); }); const handleIntersection = (entries: Array<IntersectionObserverEntry>) => { 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)); }, }; } export function requestPostPaintCallback(callback: (time: number) => void) { localRequestAnimationFrame(() => { localRequestAnimationFrame(time => callback(time)); }); } // ------------------- // Singletons // ------------------- export const supportsSingletons = true; export function isHostSingletonType(type: string): boolean { return type === 'html' || type === 'head' || type === 'body'; } export function resolveSingletonInstance( type: string, props: Props, rootContainerInstance: Container, hostContext: HostContext, validateDOMNestingDev: boolean, ): Instance { if (__DEV__) { const hostContextDev = ((hostContext: any): HostContextDev); if (validateDOMNestingDev) { validateDOMNesting(type, hostContextDev.ancestorInfo); } } const ownerDocument = getOwnerDocumentFromRootContainer( rootContainerInstance, ); switch (type) { case 'html': { const documentElement = ownerDocument.documentElement; if (!documentElement) { throw new Error( 'React expected an <html> element (document.documentElement) to exist in the Document but one was' + ' not found. React never removes the documentElement for any Document it renders into so' + ' the cause is likely in some other script running on this page.', ); } return documentElement; } case 'head': { const head = ownerDocument.head; if (!head) { throw new Error( 'React expected a <head> element (document.head) to exist in the Document but one was' + ' not found. React never removes the head for any Document it renders into so' + ' the cause is likely in some other script running on this page.', ); } return head; } case 'body': { const body = ownerDocument.body; if (!body) { throw new Error( 'React expected a <body> element (document.body) to exist in the Document but one was' + ' not found. React never removes the body for any Document it renders into so' + ' the cause is likely in some other script running on this page.', ); } return body; } default: { throw new Error( 'resolveSingletonInstance was called with an element type that is not supported. This is a bug in React.', ); } } } export function acquireSingletonInstance( type: string, props: Props, instance: Instance, internalInstanceHandle: Object, ): void { if (__DEV__) { if ( // If this instance is the container then it is invalid to acquire it as a singleton however // the DOM nesting validation will already warn for this and the message below isn't semantically // aligned with the actual fix you need to make so we omit the warning in this case !isContainerMarkedAsRoot(instance) && // If this instance isn't the root but is currently owned by a different HostSingleton instance then // we we need to warn that you are rendering more than one singleton at a time. getInstanceFromNodeDOMTree(instance) ) { const tagName = instance.tagName.toLowerCase(); console.error( 'You are mounting a new %s component when a previous one has not first unmounted. It is an' + ' error to render more than one %s component at a time and attributes and children of these' + ' components will likely fail in unpredictable ways. Please only render a single instance of' + ' <%s> and if you need to mount a new one, ensure any previous ones have unmounted first.', tagName, tagName, tagName, ); } switch (type) { case 'html': case 'head': case 'body': { break; } default: { console.error( 'acquireSingletonInstance was called with an element type that is not supported. This is a bug in React.', ); } } } const attributes = instance.attributes; while (attributes.length) { instance.removeAttributeNode(attributes[0]); } setInitialProperties(instance, type, props); precacheFiberNode(internalInstanceHandle, instance); updateFiberProps(instance, props); } export function releaseSingletonInstance(instance: Instance): void { const attributes = instance.attributes; while (attributes.length) { instance.removeAttributeNode(attributes[0]); } detachDeletedInstance(instance); } // ------------------- // Resources // ------------------- export const supportsResources = true; type HoistableTagType = 'link' | 'meta' | 'title'; type TResource< T: 'stylesheet' | 'style' | 'script' | 'void', S: null | {...}, > = { type: T, instance: null | Instance, count: number, state: S, }; type StylesheetResource = TResource<'stylesheet', StylesheetState>; type StyleTagResource = TResource<'style', null>; type StyleResource = StyleTagResource | StylesheetResource; type ScriptResource = TResource<'script', null>; type VoidResource = TResource<'void', null>; export type Resource = StyleResource | ScriptResource | VoidResource; type LoadingState = number; const NotLoaded = /* */ 0b000; const Loaded = /* */ 0b001; const Errored = /* */ 0b010; const Settled = /* */ 0b011; const Inserted = /* */ 0b100; type StylesheetState = { loading: LoadingState, preload: null | HTMLLinkElement, }; type StyleTagProps = { 'data-href': string, 'data-precedence': string, [string]: mixed, }; type StylesheetProps = { rel: 'stylesheet', href: string, 'data-precedence': string, [string]: mixed, }; type ScriptProps = { src: string, async: true, [string]: mixed, }; type PreloadProps = { rel: 'preload', href: ?string, [string]: mixed, }; type PreloadModuleProps = { rel: 'modulepreload', href: string, [string]: mixed, }; export type RootResources = { hoistableStyles: Map<string, StyleResource>, hoistableScripts: Map<string, ScriptResource>, }; export function prepareToCommitHoistables() { tagCaches = null; } // global collections of Resources const preloadPropsMap: Map<string, PreloadProps | PreloadModuleProps> = new Map(); const preconnectsSet: Set<string> = new Set(); export type HoistableRoot = Document | ShadowRoot; // getRootNode is missing from IE and old jsdom versions export function getHoistableRoot(container: Container): HoistableRoot { // $FlowFixMe[method-unbinding] return typeof container.getRootNode === 'function' ? /* $FlowFixMe[incompatible-cast] Flow types this as returning a `Node`, * but it's either a `Document` or `ShadowRoot`. */ (container.getRootNode(): Document | ShadowRoot) : container.nodeType === DOCUMENT_NODE ? // $FlowFixMe[incompatible-cast] We've constrained this to be a Document which satisfies the return type (container: Document) : container.ownerDocument; } function getCurrentResourceRoot(): null | HoistableRoot { const currentContainer = getCurrentRootHostContainer(); return currentContainer ? getHoistableRoot(currentContainer) : null; } function getDocumentFromRoot(root: HoistableRoot): Document { return root.ownerDocument || root; } const previousDispatcher = ReactDOMSharedInternals.d; /* ReactDOMCurrentDispatcher */ ReactDOMSharedInternals.d /* ReactDOMCurrentDispatcher */ = { f /* flushSyncWork */: disableLegacyMode ? flushSyncWork : previousDispatcher.f /* flushSyncWork */, r: requestFormReset, D /* prefetchDNS */: prefetchDNS, C /* preconnect */: preconnect, L /* preload */: preload, m /* preloadModule */: preloadModule, X /* preinitScript */: preinitScript, S /* preinitStyle */: preinitStyle, M /* preinitModuleScript */: preinitModuleScript, }; function flushSyncWork() { if (disableLegacyMode) { const previousWasRendering = previousDispatcher.f(); /* flushSyncWork */ const wasRendering = flushSyncWorkOnAllRoots(); // Since multiple dispatchers can flush sync work during a single flushSync call // we need to return true if any of them were rendering. return previousWasRendering || wasRendering; } else { throw new Error( 'flushSyncWork should not be called from builds that support legacy mode. This is a bug in React.', ); } } function requestFormReset(form: HTMLFormElement) { const formInst = getInstanceFromNodeDOMTree(form); if ( formInst !== null && formInst.tag === HostComponent && formInst.type === 'form' ) { requestFormResetOnFiber(formInst); } else { // This form was either not rendered by this React renderer (or it's an // invalid type). Try the next one. // // The last implementation in the sequence will throw an error. previousDispatcher.r(/* requestFormReset */ form); } } // We expect this to get inlined. It is a function mostly to communicate the special nature of // how we resolve the HoistableRoot for ReactDOM.pre*() methods. Because we support calling // these methods outside of render there is no way to know which Document or ShadowRoot is 'scoped' // and so we have to fall back to something universal. Currently we just refer to the global document. // This is notable because nowhere else in ReactDOM do we actually reference the global document or window // because we may be rendering inside an iframe. const globalDocument = typeof document === 'undefined' ? null : document; function getGlobalDocument(): ?Document { return globalDocument; } function preconnectAs( rel: 'preconnect' | 'dns-prefetch', href: string, crossOrigin: ?CrossOriginEnum, ) { const ownerDocument = getGlobalDocument(); if (ownerDocument && typeof href === 'string' && href) { const limitedEscapedHref = escapeSelectorAttributeValueInsideDoubleQuotes(href); let key = `link[rel="${rel}"][href="${limitedEscapedHref}"]`; if (typeof crossOrigin === 'string') { key += `[crossorigin="${crossOrigin}"]`; } if (!preconnectsSet.has(key)) { preconnectsSet.add(key); const preconnectProps = {rel, crossOrigin, href}; if (null === ownerDocument.querySelector(key)) { const instance = ownerDocument.createElement('link'); setInitialProperties(instance, 'link', preconnectProps); markNodeAsHoistable(instance); (ownerDocument.head: any).appendChild(instance); } } } } function prefetchDNS(href: string) { previousDispatcher.D(/* prefetchDNS */ href); preconnectAs('dns-prefetch', href, null); } function preconnect(href: string, crossOrigin?: ?CrossOriginEnum) { previousDispatcher.C(/* preconnect */ href, crossOrigin); preconnectAs('preconnect', href, crossOrigin); } function preload(href: string, as: string, options?: ?PreloadImplOptions) { previousDispatcher.L(/* preload */ href, as, options); const ownerDocument = getGlobalDocument(); if (ownerDocument && href && as) { let preloadSelector = `link[rel="preload"][as="${escapeSelectorAttributeValueInsideDoubleQuotes( as, )}"]`; if (as === 'image') { if (options && options.imageSrcSet) { preloadSelector += `[imagesrcset="${escapeSelectorAttributeValueInsideDoubleQuotes( options.imageSrcSet, )}"]`; if (typeof options.imageSizes === 'string') { preloadSelector += `[imagesizes="${escapeSelectorAttributeValueInsideDoubleQuotes( options.imageSizes, )}"]`; } } else { preloadSelector += `[href="${escapeSelectorAttributeValueInsideDoubleQuotes( href, )}"]`; } } else { preloadSelector += `[href="${escapeSelectorAttributeValueInsideDoubleQuotes( href, )}"]`; } // Some preloads are keyed under their selector. This happens when the preload is for // an arbitrary type. Other preloads are keyed under the resource key they represent a preload for. // Here we figure out which key to use to determine if we have a preload already. let key = preloadSelector; switch (as) { case 'style': key = getStyleKey(href); break; case 'script': key = getScriptKey(href); break; } if (!preloadPropsMap.has(key)) { const preloadProps = Object.assign( ({ rel: 'preload', // There is a bug in Safari where imageSrcSet is not respected on preload links // so we omit the href here if we have imageSrcSet b/c safari will load the wrong image. // This harms older browers that do not support imageSrcSet by making their preloads not work // but this population is shrinking fast and is already small so we accept this tradeoff. href: as === 'image' && options && options.imageSrcSet ? undefined : href, as, }: PreloadProps), options, ); preloadPropsMap.set(key, preloadProps); if (null === ownerDocument.querySelector(preloadSelector)) { if ( as === 'style' && ownerDocument.querySelector(getStylesheetSelectorFromKey(key)) ) { // We already have a stylesheet for this key. We don't need to preload it. return; } else if ( as === 'script' && ownerDocument.querySelector(getScriptSelectorFromKey(key)) ) { // We already have a stylesheet for this key. We don't need to preload it. return; } const instance = ownerDocument.createElement('link'); setInitialProperties(instance, 'link', preloadProps); markNodeAsHoistable(instance); (ownerDocument.head: any).appendChild(instance); } } } } function preloadModule(href: string, options?: ?PreloadModuleImplOptions) { previousDispatcher.m(/* preloadModule */ href, options); const ownerDocument = getGlobalDocument(); if (ownerDocument && href) { const as = options && typeof options.as === 'string' ? options.as : 'script'; const preloadSelector = `link[rel="modulepreload"][as="${escapeSelectorAttributeValueInsideDoubleQuotes( as, )}"][href="${escapeSelectorAttributeValueInsideDoubleQuotes(href)}"]`; // Some preloads are keyed under their selector. This happens when the preload is for // an arbitrary type. Other preloads are keyed under the resource key they represent a preload for. // Here we figure out which key to use to determine if we have a preload already. let key = preloadSelector; switch (as) { case 'audioworklet': case 'paintworklet': case 'serviceworker': case 'sharedworker': case 'worker': case 'script': { key = getScriptKey(href); break; } } if (!preloadPropsMap.has(key)) { const props: PreloadModuleProps = Object.assign( ({ rel: 'modulepreload', href, }: PreloadModuleProps), options, ); preloadPropsMap.set(key, props); if (null === ownerDocument.querySelector(preloadSelector)) { switch (as) { case 'audioworklet': case 'paintworklet': case 'serviceworker': case 'sharedworker': case 'worker': case 'script': { if (ownerDocument.querySelector(getScriptSelectorFromKey(key))) { return; } } } const instance = ownerDocument.createElement('link'); setInitialProperties(instance, 'link', props); markNodeAsHoistable(instance); (ownerDocument.head: any).appendChild(instance); } } } } function preinitStyle( href: string, precedence: ?string, options?: ?PreinitStyleOptions, ) { previousDispatcher.S(/* preinitStyle */ href, precedence, options); const ownerDocument = getGlobalDocument(); if (ownerDocument && href) { const styles = getResourcesFromRoot(ownerDocument).hoistableStyles; const key = getStyleKey(href); precedence = precedence || 'default'; // Check if this resource already exists let resource = styles.get(key); if (resource) { // We can early return. The resource exists and there is nothing // more to do return; } const state = { loading: NotLoaded, preload: null, }; // Attempt to hydrate instance from DOM let instance: null | Instance = ownerDocument.querySelector( getStylesheetSelectorFromKey(key), ); if (instance) { state.loading = Loaded | Inserted; } else { // Construct a new instance and insert it const stylesheetProps = Object.assign( ({ rel: 'stylesheet', href, 'data-precedence': precedence, }: StylesheetProps), options, ); const preloadProps = preloadPropsMap.get(key); if (preloadProps) { adoptPreloadPropsForStylesheet(stylesheetProps, preloadProps); } const link = (instance = ownerDocument.createElement('link')); markNodeAsHoistable(link); setInitialProperties(link, 'link', stylesheetProps); (link: any)._p = new Promise((resolve, reject) => { link.onload = resolve; link.onerror = reject; }); link.addEventListener('load', () => { state.loading |= Loaded; }); link.addEventListener('error', () => { state.loading |= Errored; }); state.loading |= Inserted; insertStylesheet(instance, precedence, ownerDocument); } // Construct a Resource and cache it resource = { type: 'stylesheet', instance, count: 1, state, }; styles.set(key, resource); return; } } function preinitScript(src: string, options?: ?PreinitScriptOptions) { previousDispatcher.X(/* preinitScript */ src, options); const ownerDocument = getGlobalDocument(); if (ownerDocument && src) { const scripts = getResourcesFromRoot(ownerDocument).hoistableScripts; const key = getScriptKey(src); // Check if this resource already exists let resource = scripts.get(key); if (resource) { // We can early return. The resource exists and there is nothing // more to do return; } // Attempt to hydrate instance from DOM let instance: null | Instance = ownerDocument.querySelector( getScriptSelectorFromKey(key), ); if (!instance) { // Construct a new instance and insert it const scriptProps = Object.assign( ({ src, async: true, }: ScriptProps), options, ); // Adopt certain preload props const preloadProps = preloadPropsMap.get(key); if (preloadProps) { adoptPreloadPropsForScript(scriptProps, preloadProps); } instance = ownerDocument.createElement('script'); markNodeAsHoistable(instance); setInitialProperties(instance, 'link', scriptProps); (ownerDocument.head: any).appendChild(instance); } // Construct a Resource and cache it resource = { type: 'script', instance, count: 1, state: null, }; scripts.set(key, resource); return; } } function preinitModuleScript( src: string, options?: ?PreinitModuleScriptOptions, ) { previousDispatcher.M(/* preinitModuleScript */ src, options); const ownerDocument = getGlobalDocument(); if (ownerDocument && src) { const scripts = getResourcesFromRoot(ownerDocument).hoistableScripts; const key = getScriptKey(src); // Check if this resource already exists let resource = scripts.get(key); if (resource) { // We can early return. The resource exists and there is nothing // more to do return; } // Attempt to hydrate instance from DOM let instance: null | Instance = ownerDocument.querySelector( getScriptSelectorFromKey(key), ); if (!instance) { // Construct a new instance and insert it const scriptProps = Object.assign( ({ src, async: true, type: 'module', }: ScriptProps), options, ); // Adopt certain preload props const preloadProps = preloadPropsMap.get(key); if (preloadProps) { adoptPreloadPropsForScript(scriptProps, preloadProps); } instance = ownerDocument.createElement('script'); markNodeAsHoistable(instance); setInitialProperties(instance, 'link', scriptProps); (ownerDocument.head: any).appendChild(instance); } // Construct a Resource and cache it resource = { type: 'script', instance, count: 1, state: null, }; scripts.set(key, resource); return; } } type StyleTagQualifyingProps = { href: string, precedence: string, [string]: mixed, }; type StylesheetQualifyingProps = { rel: 'stylesheet', href: string, precedence: string, [string]: mixed, }; // This function is called in begin work and we should always have a currentDocument set export function getResource( type: string, currentProps: any, pendingProps: any, currentResource: null | Resource, ): null | Resource { const resourceRoot = getCurrentResourceRoot(); if (!resourceRoot) { throw new Error( '"resourceRoot" was expected to exist. This is a bug in React.', ); } switch (type) { case 'meta': case 'title': { return null; } case 'style': { if ( typeof pendingProps.precedence === 'string' && typeof pendingProps.href === 'string' ) { const key = getStyleKey(pendingProps.href); const styles = getResourcesFromRoot(resourceRoot).hoistableStyles; let resource = styles.get(key); if (!resource) { resource = { type: 'style', instance: null, count: 0, state: null, }; styles.set(key, resource); } return resource; } return { type: 'void', instance: null, count: 0, state: null, }; } case 'link': { if ( pendingProps.rel === 'stylesheet' && typeof pendingProps.href === 'string' && typeof pendingProps.precedence === 'string' ) { const qualifiedProps: StylesheetQualifyingProps = pendingProps; const key = getStyleKey(qualifiedProps.href); const styles = getResourcesFromRoot(resourceRoot).hoistableStyles; let resource = styles.get(key); if (!resource) { // We asserted this above but Flow can't figure out that the type satisfies const ownerDocument = getDocumentFromRoot(resourceRoot); resource = ({ type: 'stylesheet', instance: null, count: 0, state: { loading: NotLoaded, preload: null, }, }: StylesheetResource); styles.set(key, resource); const instance = ownerDocument.querySelector( getStylesheetSelectorFromKey(key), ); if (instance) { const loadingState: ?Promise<mixed> = (instance: any)._p; if (loadingState) { // This instance is inserted as part of a boundary reveal and is not yet // loaded } else { // This instance is already loaded resource.instance = instance; resource.state.loading = Loaded | Inserted; } } if (!preloadPropsMap.has(key)) { const preloadProps = preloadPropsFromStylesheet(qualifiedProps); preloadPropsMap.set(key, preloadProps); if (!instance) { preloadStylesheet( ownerDocument, key, preloadProps, resource.state, ); } } } if (currentProps && currentResource === null) { // This node was previously an Instance type and is becoming a Resource type // For now we error because we don't support flavor changes let diff = ''; if (__DEV__) { diff = ` - ${describeLinkForResourceErrorDEV(currentProps)} + ${describeLinkForResourceErrorDEV(pendingProps)}`; } throw new Error( 'Expected <link> not to update to be updated to a stylesheet with precedence.' + ' Check the `rel`, `href`, and `precedence` props of this component.' + ' Alternatively, check whether two different <link> components render in the same slot or share the same key.' + diff, ); } return resource; } else { if (currentProps && currentResource !== null) { // This node was previously a Resource type and is becoming an Instance type // For now we error because we don't support flavor changes let diff = ''; if (__DEV__) { diff = ` - ${describeLinkForResourceErrorDEV(currentProps)} + ${describeLinkForResourceErrorDEV(pendingProps)}`; } throw new Error( 'Expected stylesheet with precedence to not be updated to a different kind of <link>.' + ' Check the `rel`, `href`, and `precedence` props of this component.' + ' Alternatively, check whether two different <link> components render in the same slot or share the same key.' + diff, ); } return null; } } case 'script': { const async = pendingProps.async; const src = pendingProps.src; if ( typeof src === 'string' && async && typeof async !== 'function' && typeof async !== 'symbol' ) { const key = getScriptKey(src); const scripts = getResourcesFromRoot(resourceRoot).hoistableScripts; let resource = scripts.get(key); if (!resource) { resource = { type: 'script', instance: null, count: 0, state: null, }; scripts.set(key, resource); } return resource; } return { type: 'void', instance: null, count: 0, state: null, }; } default: { throw new Error( `getResource encountered a type it did not expect: "${type}". this is a bug in React.`, ); } } } function describeLinkForResourceErrorDEV(props: any) { if (__DEV__) { let describedProps = 0; let description = '<link'; if (typeof props.rel === 'string') { describedProps++; description += ` rel="${props.rel}"`; } else if (hasOwnProperty.call(props, 'rel')) { describedProps++; description += ` rel="${ props.rel === null ? 'null' : 'invalid type ' + typeof props.rel }"`; } if (typeof props.href === 'string') { describedProps++; description += ` href="${props.href}"`; } else if (hasOwnProperty.call(props, 'href')) { describedProps++; description += ` href="${ props.href === null ? 'null' : 'invalid type ' + typeof props.href }"`; } if (typeof props.precedence === 'string') { describedProps++; description += ` precedence="${props.precedence}"`; } else if (hasOwnProperty.call(props, 'precedence')) { describedProps++; description += ` precedence={${ props.precedence === null ? 'null' : 'invalid type ' + typeof props.precedence }}`; } if (Object.getOwnPropertyNames(props).length > describedProps) { description += ' ...'; } description += ' />'; return description; } return ''; } function styleTagPropsFromRawProps( rawProps: StyleTagQualifyingProps, ): StyleTagProps { return { ...rawProps, 'data-href': rawProps.href, 'data-precedence': rawProps.precedence, href: null, precedence: null, }; } function getStyleKey(href: string) { const limitedEscapedHref = escapeSelectorAttributeValueInsideDoubleQuotes(href); return `href="${limitedEscapedHref}"`; } function getStyleTagSelector(href: string) { const limitedEscapedHref = escapeSelectorAttributeValueInsideDoubleQuotes(href); return `style[data-href~="${limitedEscapedHref}"]`; } function getStylesheetSelectorFromKey(key: string) { return `link[rel="stylesheet"][${key}]`; } function getPreloadStylesheetSelectorFromKey(key: string) { return `link[rel="preload"][as="style"][${key}]`; } function stylesheetPropsFromRawProps( rawProps: StylesheetQualifyingProps, ): StylesheetProps { return { ...rawProps, 'data-precedence': rawProps.precedence, precedence: null, }; } function preloadStylesheet( ownerDocument: Document, key: string, preloadProps: PreloadProps, state: StylesheetState, ) { const preloadEl = ownerDocument.querySelector( getPreloadStylesheetSelectorFromKey(key), ); if (preloadEl) { // If we find a preload already it was SSR'd and we won't have an actual // loading state to track. For now we will just assume it is loaded state.loading = Loaded; } else { const instance = ownerDocument.createElement('link'); state.preload = instance; instance.addEventListener('load', () => (state.loading |= Loaded)); instance.addEventListener('error', () => (state.loading |= Errored)); setInitialProperties(instance, 'link', preloadProps); markNodeAsHoistable(instance); (ownerDocument.head: any).appendChild(instance); } } function preloadPropsFromStylesheet( props: StylesheetQualifyingProps, ): PreloadProps { return { rel: 'preload', as: 'style', href: props.href, crossOrigin: props.crossOrigin, integrity: props.integrity, media: props.media, hrefLang: props.hrefLang, referrerPolicy: props.referrerPolicy, }; } function getScriptKey(src: string): string { const limitedEscapedSrc = escapeSelectorAttributeValueInsideDoubleQuotes(src); return `[src="${limitedEscapedSrc}"]`; } function getScriptSelectorFromKey(key: string): string { return 'script[async]' + key; } export function acquireResource( hoistableRoot: HoistableRoot, resource: Resource, props: any, ): null | Instance { resource.count++; if (resource.instance === null) { switch (resource.type) { case 'style': { const qualifiedProps: StyleTagQualifyingProps = props; // Attempt to hydrate instance from DOM let instance: null | Instance = hoistableRoot.querySelector( getStyleTagSelector(qualifiedProps.href), ); if (instance) { resource.instance = instance; markNodeAsHoistable(instance); return instance; } const styleProps = styleTagPropsFromRawProps(props); const ownerDocument = getDocumentFromRoot(hoistableRoot); instance = ownerDocument.createElement('style'); markNodeAsHoistable(instance); setInitialProperties(instance, 'style', styleProps); // TODO: `style` does not have loading state for tracking insertions. I // guess because these aren't suspensey? Not sure whether this is a // factoring smell. // resource.state.loading |= Inserted; insertStylesheet(instance, qualifiedProps.precedence, hoistableRoot); resource.instance = instance; return instance; } case 'stylesheet': { // This typing is enforce by `getResource`. If we change the logic // there for what qualifies as a stylesheet resource we need to ensure // this cast still makes sense; const qualifiedProps: StylesheetQualifyingProps = props; const key = getStyleKey(qualifiedProps.href); // Attempt to hydrate instance from DOM let instance: null | Instance = hoistableRoot.querySelector( getStylesheetSelectorFromKey(key), ); if (instance) { resource.state.loading |= Inserted; resource.instance = instance; markNodeAsHoistable(instance); return instance; } const stylesheetProps = stylesheetPropsFromRawProps(props); const preloadProps = preloadPropsMap.get(key); if (preloadProps) { adoptPreloadPropsForStylesheet(stylesheetProps, preloadProps); } // Construct and insert a new instance const ownerDocument = getDocumentFromRoot(hoistableRoot); instance = ownerDocument.createElement('link'); markNodeAsHoistable(instance); const linkInstance: HTMLLinkElement = (instance: any); (linkInstance: any)._p = new Promise((resolve, reject) => { linkInstance.onload = resolve; linkInstance.onerror = reject; }); setInitialProperties(instance, 'link', stylesheetProps); resource.state.loading |= Inserted; insertStylesheet(instance, qualifiedProps.precedence, hoistableRoot); resource.instance = instance; return instance; } case 'script': { // This typing is enforce by `getResource`. If we change the logic // there for what qualifies as a stylesheet resource we need to ensure // this cast still makes sense; const borrowedScriptProps: ScriptProps = props; const key = getScriptKey(borrowedScriptProps.src); // Attempt to hydrate instance from DOM let instance: null | Instance = hoistableRoot.querySelector( getScriptSelectorFromKey(key), ); if (instance) { resource.instance = instance; markNodeAsHoistable(instance); return instance; } let scriptProps = borrowedScriptProps; const preloadProps = preloadPropsMap.get(key); if (preloadProps) { scriptProps = {...borrowedScriptProps}; adoptPreloadPropsForScript(scriptProps, preloadProps); } // Construct and insert a new instance const ownerDocument = getDocumentFromRoot(hoistableRoot); instance = ownerDocument.createElement('script'); markNodeAsHoistable(instance); setInitialProperties(instance, 'link', scriptProps); (ownerDocument.head: any).appendChild(instance); resource.instance = instance; return instance; } case 'void': { return null; } default: { throw new Error( `acquireResource encountered a resource type it did not expect: "${resource.type}". this is a bug in React.`, ); } } } else { // In the case of stylesheets, they might have already been assigned an // instance during `suspendResource`. But that doesn't mean they were // inserted, because the commit might have been interrupted. So we need to // check now. // // The other resource types are unaffected because they are not // yet suspensey. // // TODO: This is a bit of a code smell. Consider refactoring how // `suspendResource` and `acquireResource` work together. The idea is that // `suspendResource` does all the same stuff as `acquireResource` except // for the insertion. if ( resource.type === 'stylesheet' && (resource.state.loading & Inserted) === NotLoaded ) { const qualifiedProps: StylesheetQualifyingProps = props; const instance: Instance = resource.instance; resource.state.loading |= Inserted; insertStylesheet(instance, qualifiedProps.precedence, hoistableRoot); } } return resource.instance; } export function releaseResource(resource: Resource): void { resource.count--; } function insertStylesheet( instance: Element, precedence: string, root: HoistableRoot, ): void { const nodes = root.querySelectorAll( 'link[rel="stylesheet"][data-precedence],style[data-precedence]', ); const last = nodes.length ? nodes[nodes.length - 1] : null; let prior = last; for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; // $FlowFixMe[prop-missing] const nodePrecedence = node.dataset.precedence; if (nodePrecedence === precedence) { prior = node; } else if (prior !== last) { break; } } if (prior) { // We get the prior from the document so we know it is in the tree. // We also know that links can't be the topmost Node so the parentNode // must exist. ((prior.parentNode: any): Node).insertBefore(instance, prior.nextSibling); } else { const parent = root.nodeType === DOCUMENT_NODE ? ((((root: any): Document).head: any): Element) : ((root: any): ShadowRoot); parent.insertBefore(instance, parent.firstChild); } } function adoptPreloadPropsForStylesheet( stylesheetProps: StylesheetProps, preloadProps: PreloadProps | PreloadModuleProps, ): void { if (stylesheetProps.crossOrigin == null) stylesheetProps.crossOrigin = preloadProps.crossOrigin; if (stylesheetProps.referrerPolicy == null) stylesheetProps.referrerPolicy = preloadProps.referrerPolicy; if (stylesheetProps.title == null) stylesheetProps.title = preloadProps.title; } function adoptPreloadPropsForScript( scriptProps: ScriptProps, preloadProps: PreloadProps | PreloadModuleProps, ): void { if (scriptProps.crossOrigin == null) scriptProps.crossOrigin = preloadProps.crossOrigin; if (scriptProps.referrerPolicy == null) scriptProps.referrerPolicy = preloadProps.referrerPolicy; if (scriptProps.integrity == null) scriptProps.integrity = preloadProps.integrity; } type KeyedTagCache = Map<string, Array<Element>>; type DocumentTagCaches = Map<Document, KeyedTagCache>; let tagCaches: null | DocumentTagCaches = null; export function hydrateHoistable( hoistableRoot: HoistableRoot, type: HoistableTagType, props: any, internalInstanceHandle: Object, ): Instance { const ownerDocument = getDocumentFromRoot(hoistableRoot); let instance: ?Instance = null; getInstance: switch (type) { case 'title': { instance = ownerDocument.getElementsByTagName('title')[0]; if ( !instance || isOwnedInstance(instance) || instance.namespaceURI === SVG_NAMESPACE || instance.hasAttribute('itemprop') ) { instance = ownerDocument.createElement(type); (ownerDocument.head: any).insertBefore( instance, ownerDocument.querySelector('head > title'), ); } setInitialProperties(instance, type, props); precacheFiberNode(internalInstanceHandle, instance); markNodeAsHoistable(instance); return instance; } case 'link': { const cache = getHydratableHoistableCache('link', 'href', ownerDocument); const key = type + (props.href || ''); const maybeNodes = cache.get(key); if (maybeNodes) { const nodes = maybeNodes; for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; if ( node.getAttribute('href') !== (props.href == null || props.href === '' ? null : props.href) || node.getAttribute('rel') !== (props.rel == null ? null : props.rel) || node.getAttribute('title') !== (props.title == null ? null : props.title) || node.getAttribute('crossorigin') !== (props.crossOrigin == null ? null : props.crossOrigin) ) { // mismatch, try the next node; continue; } instance = node; nodes.splice(i, 1); break getInstance; } } instance = ownerDocument.createElement(type); setInitialProperties(instance, type, props); (ownerDocument.head: any).appendChild(instance); break; } case 'meta': { const cache = getHydratableHoistableCache( 'meta', 'content', ownerDocument, ); const key = type + (props.content || ''); const maybeNodes = cache.get(key); if (maybeNodes) { const nodes = maybeNodes; for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; // We coerce content to string because it is the most likely one to // use a `toString` capable value. For the rest we just do identity match // passing non-strings here is not really valid anyway. if (__DEV__) { checkAttributeStringCoercion(props.content, 'content'); } if ( node.getAttribute('content') !== (props.content == null ? null : '' + props.content) || node.getAttribute('name') !== (props.name == null ? null : props.name) || node.getAttribute('property') !== (props.property == null ? null : props.property) || node.getAttribute('http-equiv') !== (props.httpEquiv == null ? null : props.httpEquiv) || node.getAttribute('charset') !== (props.charSet == null ? null : props.charSet) ) { // mismatch, try the next node; continue; } instance = node; nodes.splice(i, 1); break getInstance; } } instance = ownerDocument.createElement(type); setInitialProperties(instance, type, props); (ownerDocument.head: any).appendChild(instance); break; } default: throw new Error( `getNodesForType encountered a type it did not expect: "${type}". This is a bug in React.`, ); } // This node is a match precacheFiberNode(internalInstanceHandle, instance); markNodeAsHoistable(instance); return instance; } function getHydratableHoistableCache( type: HoistableTagType, keyAttribute: string, ownerDocument: Document, ): KeyedTagCache { let cache: KeyedTagCache; let caches: DocumentTagCaches; if (tagCaches === null) { cache = new Map(); caches = tagCaches = new Map(); caches.set(ownerDocument, cache); } else { caches = tagCaches; const maybeCache = caches.get(ownerDocument); if (!maybeCache) { cache = new Map(); caches.set(ownerDocument, cache); } else { cache = maybeCache; } } if (cache.has(type)) { // We use type as a special key that signals that this cache has been seeded for this type return cache; } // Mark this cache as seeded for this type cache.set(type, (null: any)); const nodes = ownerDocument.getElementsByTagName(type); for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; if ( !isOwnedInstance(node) && (type !== 'link' || node.getAttribute('rel') !== 'stylesheet') && node.namespaceURI !== SVG_NAMESPACE ) { const nodeKey = node.getAttribute(keyAttribute) || ''; const key = type + nodeKey; const existing = cache.get(key); if (existing) { existing.push(node); } else { cache.set(key, [node]); } } } return cache; } export function mountHoistable( hoistableRoot: HoistableRoot, type: HoistableTagType, instance: Instance, ): void { const ownerDocument = getDocumentFromRoot(hoistableRoot); (ownerDocument.head: any).insertBefore( instance, type === 'title' ? ownerDocument.querySelector('head > title') : null, ); } export function unmountHoistable(instance: Instance): void { (instance.parentNode: any).removeChild(instance); } export function isHostHoistableType( type: string, props: RawProps, hostContext: HostContext, ): boolean { let outsideHostContainerContext: boolean; let hostContextProd: HostContextProd; if (__DEV__) { const hostContextDev: HostContextDev = (hostContext: any); // We can only render resources when we are not within the host container context outsideHostContainerContext = !hostContextDev.ancestorInfo.containerTagInScope; hostContextProd = hostContextDev.context; } else { hostContextProd = (hostContext: any); } // Global opt out of hoisting for anything in SVG Namespace or anything with an itemProp inside an itemScope if (hostContextProd === HostContextNamespaceSvg || props.itemProp != null) { if (__DEV__) { if ( outsideHostContainerContext && props.itemProp != null && (type === 'meta' || type === 'title' || type === 'style' || type === 'link' || type === 'script') ) { console.error( 'Cannot render a <%s> outside the main document if it has an `itemProp` prop. `itemProp` suggests the tag belongs to an' + ' `itemScope` which can appear anywhere in the DOM. If you were intending for React to hoist this <%s> remove the `itemProp` prop.' + ' Otherwise, try moving this tag into the <head> or <body> of the Document.', type, type, ); } } return false; } switch (type) { case 'meta': case 'title': { return true; } case 'style': { if ( typeof props.precedence !== 'string' || typeof props.href !== 'string' || props.href === '' ) { if (__DEV__) { if (outsideHostContainerContext) { console.error( 'Cannot render a <style> outside the main document without knowing its precedence and a unique href key.' + ' React can hoist and deduplicate <style> tags if you provide a `precedence` prop along with an `href` prop that' + ' does not conflict with the `href` values used in any other hoisted <style> or <link rel="stylesheet" ...> tags. ' + ' Note that hoisting <style> tags is considered an advanced feature that most will not use directly.' + ' Consider moving the <style> tag to the <head> or consider adding a `precedence="default"` and `href="some unique resource identifier"`.', ); } } return false; } return true; } case 'link': { if ( typeof props.rel !== 'string' || typeof props.href !== 'string' || props.href === '' || props.onLoad || props.onError ) { if (__DEV__) { if ( props.rel === 'stylesheet' && typeof props.precedence === 'string' ) { validateLinkPropsForStyleResource(props); } if (outsideHostContainerContext) { if ( typeof props.rel !== 'string' || typeof props.href !== 'string' || props.href === '' ) { console.error( 'Cannot render a <link> outside the main document without a `rel` and `href` prop.' + ' Try adding a `rel` and/or `href` prop to this <link> or moving the link into the <head> tag', ); } else if (props.onError || props.onLoad) { console.error( 'Cannot render a <link> with onLoad or onError listeners outside the main document.' + ' Try removing onLoad={...} and onError={...} or moving it into the root <head> tag or' + ' somewhere in the <body>.', ); } } } return false; } switch (props.rel) { case 'stylesheet': { const {precedence, disabled} = props; if (__DEV__) { if (typeof precedence !== 'string') { if (outsideHostContainerContext) { console.error( 'Cannot render a <link rel="stylesheet" /> outside the main document without knowing its precedence.' + ' Consider adding precedence="default" or moving it into the root <head> tag.', ); } } } return typeof precedence === 'string' && disabled == null; } default: { return true; } } } case 'script': { const isAsync = props.async && typeof props.async !== 'function' && typeof props.async !== 'symbol'; if ( !isAsync || props.onLoad || props.onError || !props.src || typeof props.src !== 'string' ) { if (__DEV__) { if (outsideHostContainerContext) { if (!isAsync) { console.error( 'Cannot render a sync or defer <script> outside the main document without knowing its order.' + ' Try adding async="" or moving it into the root <head> tag.', ); } else if (props.onLoad || props.onError) { console.error( 'Cannot render a <script> with onLoad or onError listeners outside the main document.' + ' Try removing onLoad={...} and onError={...} or moving it into the root <head> tag or' + ' somewhere in the <body>.', ); } else { console.error( 'Cannot render a <script> outside the main document without `async={true}` and a non-empty `src` prop.' + ' Ensure there is a valid `src` and either make the script async or move it into the root <head> tag or' + ' somewhere in the <body>.', ); } } } return false; } return true; } case 'noscript': case 'template': { if (__DEV__) { if (outsideHostContainerContext) { console.error( 'Cannot render <%s> outside the main document. Try moving it into the root <head> tag.', type, ); } } return false; } } return false; } export function maySuspendCommit(type: Type, props: Props): boolean { if (!enableSuspenseyImages && !enableViewTransition) { return false; } // Suspensey images are the default, unless you opt-out of with either // loading="lazy" or onLoad={...} which implies you're ok waiting. return ( type === 'img' && props.src != null && props.src !== '' && props.onLoad == null && props.loading !== 'lazy' ); } export function maySuspendCommitOnUpdate( type: Type, oldProps: Props, newProps: Props, ): boolean { return ( maySuspendCommit(type, newProps) && (newProps.src !== oldProps.src || newProps.srcSet !== oldProps.srcSet) ); } export function maySuspendCommitInSyncRender( type: Type, props: Props, ): boolean { // TODO: Allow sync lanes to suspend too with an opt-in. return false; } export function mayResourceSuspendCommit(resource: Resource): boolean { return ( resource.type === 'stylesheet' && (resource.state.loading & Inserted) === NotLoaded ); } export function preloadInstance( instance: Instance, type: Type, props: Props, ): boolean { // We don't need to preload Suspensey images because the browser will // load them early once we set the src. // If we return true here, we'll still get a suspendInstance call in the // pre-commit phase to determine if we still need to decode the image or // if was dropped from cache. This just avoids rendering Suspense fallback. return !!(instance: any).complete; } export function preloadResource(resource: Resource): boolean { if ( resource.type === 'stylesheet' && (resource.state.loading & Settled) === NotLoaded ) { // Return false to indicate this resource should suspend return false; } // Return true to indicate this resource should not suspend return true; } export opaque type SuspendedState = { stylesheets: null | Map<StylesheetResource, HoistableRoot>, count: number, // suspensey css and active view transitions imgCount: number, // suspensey images pending to load imgBytes: number, // number of bytes we estimate needing to download suspenseyImages: Array<HTMLImageElement>, // instances of suspensey images (whether loaded or not) waitingForImages: boolean, // false when we're no longer blocking on images waitingForViewTransition: boolean, unsuspend: null | (() => void), }; export function startSuspendingCommit(): SuspendedState { return { stylesheets: null, count: 0, imgCount: 0, imgBytes: 0, suspenseyImages: [], waitingForImages: true, waitingForViewTransition: false, // We use a noop function when we begin suspending because if possible we want the // waitfor step to finish synchronously. If it doesn't we'll return a function to // provide the actual unsuspend function and that will get completed when the count // hits zero or it will get cancelled if the root starts new work. unsuspend: noop, }; } function estimateImageBytes(instance: HTMLImageElement): number { const width: number = instance.width || 100; const height: number = instance.height || 100; const pixelRatio: number = typeof devicePixelRatio === 'number' ? devicePixelRatio : 1; const pixelsToDownload = width * height * pixelRatio; const AVERAGE_BYTE_PER_PIXEL = 0.25; return pixelsToDownload * AVERAGE_BYTE_PER_PIXEL; } export function suspendInstance( state: SuspendedState, instance: Instance, type: Type, props: Props, ): void { if (!enableSuspenseyImages && !enableViewTransition) { return; } if ( // $FlowFixMe[prop-missing] typeof instance.decode === 'function' ) { // If this browser supports decode() API, we use it to suspend waiting on the image. // The loading should have already started at this point, so it should be enough to // just call decode() which should also wait for the data to finish loading. state.imgCount++; // Estimate the byte size that we're about to download based on the width/height // specified in the props. This is best practice to know ahead of time but if it's // unspecified we'll fallback to a guess of 100x100 pixels. if (!(instance: any).complete) { state.imgBytes += estimateImageBytes((instance: any)); state.suspenseyImages.push((instance: any)); } const ping = onUnsuspendImg.bind(state); // $FlowFixMe[prop-missing] instance.decode().then(ping, ping); } } export function suspendResource( state: SuspendedState, hoistableRoot: HoistableRoot, resource: Resource, props: any, ): void { if (resource.type === 'stylesheet') { if (typeof props.media === 'string') { // If we don't currently match media we avoid suspending on this resource // and let it insert on the mutation path if (matchMedia(props.media).matches === false) { return; } } if ((resource.state.loading & Inserted) === NotLoaded) { if (resource.instance === null) { const qualifiedProps: StylesheetQualifyingProps = props; const key = getStyleKey(qualifiedProps.href); // Attempt to hydrate instance from DOM let instance: null | Instance = hoistableRoot.querySelector( getStylesheetSelectorFromKey(key), ); if (instance) { // If this instance has a loading state it came from the Fizz runtime. // If there is not loading state it is assumed to have been server rendered // as part of the preamble and therefore synchronously loaded. It could have // errored however which we still do not yet have a means to detect. For now // we assume it is loaded. const maybeLoadingState: ?Promise<mixed> = (instance: any)._p; if ( maybeLoadingState !== null && typeof maybeLoadingState === 'object' && // $FlowFixMe[method-unbinding] typeof maybeLoadingState.then === 'function' ) { const loadingState = maybeLoadingState; state.count++; const ping = onUnsuspend.bind(state); loadingState.then(ping, ping); } resource.state.loading |= Inserted; resource.instance = instance; markNodeAsHoistable(instance); return; } const ownerDocument = getDocumentFromRoot(hoistableRoot); const stylesheetProps = stylesheetPropsFromRawProps(props); const preloadProps = preloadPropsMap.get(key); if (preloadProps) { adoptPreloadPropsForStylesheet(stylesheetProps, preloadProps); } // Construct and insert a new instance instance = ownerDocument.createElement('link'); markNodeAsHoistable(instance); const linkInstance: HTMLLinkElement = (instance: any); // This Promise is a loading state used by the Fizz runtime. We need this incase there is a race // between this resource being rendered on the client and being rendered with a late completed boundary. (linkInstance: any)._p = new Promise((resolve, reject) => { linkInstance.onload = resolve; linkInstance.onerror = reject; }); setInitialProperties(instance, 'link', stylesheetProps); resource.instance = instance; } if (state.stylesheets === null) { state.stylesheets = new Map(); } state.stylesheets.set(resource, hoistableRoot); const preloadEl = resource.state.preload; if (preloadEl && (resource.state.loading & Settled) === NotLoaded) { state.count++; const ping = onUnsuspend.bind(state); preloadEl.addEventListener('load', ping); preloadEl.addEventListener('error', ping); } } } } export function suspendOnActiveViewTransition( state: SuspendedState, rootContainer: Container, ): void { const ownerDocument = rootContainer.nodeType === DOCUMENT_NODE ? rootContainer : rootContainer.ownerDocument; // $FlowFixMe[prop-missing] const activeViewTransition = ownerDocument.__reactViewTransition; if (activeViewTransition == null) { return; } state.count++; state.waitingForViewTransition = true; const ping = onUnsuspend.bind(state); activeViewTransition.finished.then(ping, ping); } const SUSPENSEY_STYLESHEET_TIMEOUT = 60000; const SUSPENSEY_IMAGE_TIMEOUT = 800; const SUSPENSEY_IMAGE_TIME_ESTIMATE = 500; let estimatedBytesWithinLimit: number = 0; export function waitForCommitToBeReady( state: SuspendedState, timeoutOffset: number, ): null | ((() => void) => () => void) { if (state.stylesheets && state.count === 0) { // We are not currently blocked but we have not inserted all stylesheets. // If this insertion happens and loads or errors synchronously then we can // avoid suspending the commit. To do this we check the count again immediately after insertSuspendedStylesheets(state, state.stylesheets); } // We need to check the count again because the inserted stylesheets may have led to new // tasks to wait on. if (state.count > 0 || state.imgCount > 0) { return commit => { // We almost never want to show content before its styles have loaded. But // eventually we will give up and allow unstyled content. So this number is // somewhat arbitrary — big enough that you'd only reach it under // extreme circumstances. // TODO: Figure out what the browser engines do during initial page load and // consider aligning our behavior with that. const stylesheetTimer = setTimeout(() => { if (state.stylesheets) { insertSuspendedStylesheets(state, state.stylesheets); } if (state.unsuspend) { const unsuspend = state.unsuspend; state.unsuspend = null; unsuspend(); } }, SUSPENSEY_STYLESHEET_TIMEOUT + timeoutOffset); if (state.imgBytes > 0 && estimatedBytesWithinLimit === 0) { // Estimate how many bytes we can download in 500ms. const mbps = estimateBandwidth(); estimatedBytesWithinLimit = mbps * 125 * SUSPENSEY_IMAGE_TIME_ESTIMATE; } // If we have more images to download than we expect to fit in the timeout, then // don't wait for images longer than 50ms. The 50ms lets us still do decoding and // hitting caches if it turns out that they're already in the HTTP cache. const imgTimeout = state.imgBytes > estimatedBytesWithinLimit ? 50 : SUSPENSEY_IMAGE_TIMEOUT; const imgTimer = setTimeout(() => { // We're no longer blocked on images. If CSS resolves after this we can commit. state.waitingForImages = false; if (state.count === 0) { if (state.stylesheets) { insertSuspendedStylesheets(state, state.stylesheets); } if (state.unsuspend) { const unsuspend = state.unsuspend; state.unsuspend = null; unsuspend(); } } }, imgTimeout + timeoutOffset); state.unsuspend = commit; return () => { state.unsuspend = null; clearTimeout(stylesheetTimer); clearTimeout(imgTimer); }; }; } return null; } export function getSuspendedCommitReason( state: SuspendedState, rootContainer: Container, ): null | string { if (state.waitingForViewTransition) { return 'Waiting for the previous Animation'; } if (state.count > 0) { if (state.imgCount > 0) { return 'Suspended on CSS and Images'; } return 'Suspended on CSS'; } if (state.imgCount === 1) { return 'Suspended on an Image'; } if (state.imgCount > 0) { return 'Suspended on Images'; } return null; } function checkIfFullyUnsuspended(state: SuspendedState) { if (state.count === 0 && (state.imgCount === 0 || !state.waitingForImages)) { if (state.stylesheets) { // If we haven't actually inserted the stylesheets yet we need to do so now before starting the commit. // The reason we do this after everything else has finished is because we want to have all the stylesheets // load synchronously right before mutating. Ideally the new styles will cause a single recalc only on the // new tree. When we filled up stylesheets we only inlcuded stylesheets with matching media attributes so we // wait for them to load before actually continuing. We expect this to increase the count above zero insertSuspendedStylesheets(state, state.stylesheets); } else if (state.unsuspend) { const unsuspend = state.unsuspend; state.unsuspend = null; unsuspend(); } } } function onUnsuspend(this: SuspendedState) { this.count--; checkIfFullyUnsuspended(this); } function onUnsuspendImg(this: SuspendedState) { this.imgCount--; checkIfFullyUnsuspended(this); } // We use a value that is type distinct from precedence to track which one is last. // This ensures there is no collision with user defined precedences. Normally we would // just track this in module scope but since the precedences are tracked per HoistableRoot // we need to associate it to something other than a global scope hence why we try to // colocate it with the map of precedences in the first place const LAST_PRECEDENCE = null; // This is typecast to non-null because it will always be set before read. // it is important that this not be used except when the stack guarantees it exists. // Currentlyt his is only during insertSuspendedStylesheet. let precedencesByRoot: Map< HoistableRoot, Map<string | typeof LAST_PRECEDENCE, Instance>, > = (null: any); function insertSuspendedStylesheets( state: SuspendedState, resources: Map<StylesheetResource, HoistableRoot>, ): void { // We need to clear this out so we don't try to reinsert after the stylesheets have loaded state.stylesheets = null; if (state.unsuspend === null) { // The suspended commit was cancelled. We don't need to insert any stylesheets. return; } // Temporarily increment count. we don't want any synchronously loaded stylesheets to try to unsuspend // before we finish inserting all stylesheets. state.count++; precedencesByRoot = new Map(); resources.forEach(insertStylesheetIntoRoot, state); precedencesByRoot = (null: any); // We can remove our temporary count and if we're still at zero we can unsuspend. // If we are in the synchronous phase before deciding if the commit should suspend and this // ends up hitting the unsuspend path it will just invoke the noop unsuspend. onUnsuspend.call(state); } function insertStylesheetIntoRoot( this: SuspendedState, root: HoistableRoot, resource: StylesheetResource, map: Map<StylesheetResource, HoistableRoot>, ) { if (resource.state.loading & Inserted) { // This resource was inserted by another root committing. we don't need to insert it again return; } let last; let precedences = precedencesByRoot.get(root); if (!precedences) { precedences = new Map(); precedencesByRoot.set(root, precedences); const nodes = root.querySelectorAll( 'link[data-precedence],style[data-precedence]', ); for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; if ( node.nodeName === 'LINK' || // We omit style tags with media="not all" because they are not in the right position // and will be hoisted by the Fizz runtime imminently. node.getAttribute('media') !== 'not all' ) { precedences.set( // $FlowFixMe[prop-missing] node.dataset.precedence, node, ); last = node; } } if (last) { precedences.set(LAST_PRECEDENCE, last); } } else { last = precedences.get(LAST_PRECEDENCE); } // We only call this after we have constructed an instance so we assume it here const instance: HTMLLinkElement = (resource.instance: any); // We will always have a precedence for stylesheet instances const precedence: string = (instance.getAttribute('data-precedence'): any); const prior = precedences.get(precedence) || last; if (prior === last) { precedences.set(LAST_PRECEDENCE, instance); } precedences.set(precedence, instance); this.count++; const onComplete = onUnsuspend.bind(this); instance.addEventListener('load', onComplete); instance.addEventListener('error', onComplete); if (prior) { (prior.parentNode: any).insertBefore(instance, prior.nextSibling); } else { const parent = root.nodeType === DOCUMENT_NODE ? ((((root: any): Document).head: any): Element) : ((root: any): ShadowRoot); parent.insertBefore(instance, parent.firstChild); } resource.state.loading |= Inserted; } export const NotPendingTransition: TransitionStatus = NotPending; export const HostTransitionContext: ReactContext<TransitionStatus> = { $$typeof: REACT_CONTEXT_TYPE, Provider: (null: any), Consumer: (null: any), _currentValue: NotPendingTransition, _currentValue2: NotPendingTransition, _threadCount: 0, }; export type FormInstance = HTMLFormElement; export function resetFormInstance(form: FormInstance): void { form.reset(); }