main
js 6,891 lines 230 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 import type {DOMEventName} from '../events/DOMEventNames';
11 import type {Fiber, FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
12 import type {
13 BoundingRect,
14 IntersectionObserverOptions,
15 ObserveVisibleRectsCallback,
16 } from 'react-reconciler/src/ReactTestSelectors';
17 import type {ReactContext, ReactScopeInstance} from 'shared/ReactTypes';
18 import type {AncestorInfoDev} from './validateDOMNesting';
19 import type {FormStatus} from 'react-dom-bindings/src/shared/ReactDOMFormActions';
20 import type {
21 CrossOriginEnum,
22 PreloadImplOptions,
23 PreloadModuleImplOptions,
24 PreinitStyleOptions,
25 PreinitScriptOptions,
26 PreinitModuleScriptOptions,
27 } from 'react-dom/src/shared/ReactDOMTypes';
28 import type {TransitionTypes} from 'react/src/ReactTransitionType';
29
30 import {NotPending} from '../shared/ReactDOMFormActions';
31
32 import {setSrcObject} from './ReactDOMSrcObject';
33
34 import {getCurrentRootHostContainer} from 'react-reconciler/src/ReactFiberHostContext';
35 import {runWithFiberInDEV} from 'react-reconciler/src/ReactCurrentFiber';
36
37 import hasOwnProperty from 'shared/hasOwnProperty';
38 import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion';
39 import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
40
41 export {
42 setCurrentUpdatePriority,
43 getCurrentUpdatePriority,
44 resolveUpdatePriority,
45 } from './ReactDOMUpdatePriority';
46 import {
47 precacheFiberNode,
48 updateFiberProps,
49 getFiberCurrentPropsFromNode,
50 getInstanceFromNode,
51 getClosestInstanceFromNode,
52 getFiberFromScopeInstance,
53 getInstanceFromNode as getInstanceFromNodeDOMTree,
54 isContainerMarkedAsRoot,
55 detachDeletedInstance,
56 getResourcesFromRoot,
57 isMarkedHoistable,
58 markNodeAsHoistable,
59 markNodeAsPendingLoad,
60 clearPendingLoadOnNode,
61 isNodePendingLoad,
62 isOwnedInstance,
63 } from './ReactDOMComponentTree';
64 import {
65 traverseFragmentInstancesAndTextInstances,
66 getFragmentParentInstanceOrContainerFiber,
67 getInstanceFromHostFiber,
68 isFiberFollowing,
69 isFiberPreceding,
70 getFragmentInstanceOrTextInstanceSiblings,
71 traverseFragmentInstancesAndTextInstancesDeeply,
72 fiberIsPortaledIntoHost,
73 getFragmentPortalContainerInfo,
74 isFiberContainedByFragment,
75 isFragmentContainedByFiber,
76 } from 'react-reconciler/src/ReactFiberTreeReflection';
77 import {compareDocumentPositionForEmptyFragment} from 'shared/ReactDOMFragmentRefShared';
78
79 export {detachDeletedInstance};
80 import {hasRole} from './DOMAccessibilityRoles';
81 import type {SingletonType} from './ReactDOMComponent';
82 import {
83 setInitialProperties,
84 updateProperties,
85 clearSingletonProperties,
86 hydrateProperties,
87 hydrateText,
88 diffHydratedProperties,
89 getPropsFromElement,
90 diffHydratedText,
91 trapClickOnNonInteractiveElement,
92 clearClickListener,
93 } from './ReactDOMComponent';
94 import {hydrateInput} from './ReactDOMInput';
95 import {hydrateTextarea} from './ReactDOMTextarea';
96 import {hydrateSelect} from './ReactDOMSelect';
97 import {getSelectionInformation, restoreSelection} from './ReactInputSelection';
98 import setTextContent from './setTextContent';
99 import {
100 validateDOMNesting,
101 validateTextNesting,
102 updatedAncestorInfoDev,
103 } from './validateDOMNesting';
104 import {
105 isEnabled as ReactBrowserEventEmitterIsEnabled,
106 setEnabled as ReactBrowserEventEmitterSetEnabled,
107 } from '../events/ReactDOMEventListener';
108 import {SVG_NAMESPACE, MATH_NAMESPACE} from './DOMNamespaces';
109 import {
110 ELEMENT_NODE,
111 TEXT_NODE,
112 COMMENT_NODE,
113 DOCUMENT_NODE,
114 DOCUMENT_TYPE_NODE,
115 DOCUMENT_FRAGMENT_NODE,
116 } from './HTMLNodeType';
117
118 import {
119 flushEventReplaying,
120 retryIfBlockedOn,
121 } from '../events/ReactDOMEventReplaying';
122
123 import {
124 enableCreateEventHandleAPI,
125 enableScopeAPI,
126 enableTrustedTypesIntegration,
127 disableLegacyMode,
128 enableMoveBefore,
129 disableCommentsAsDOMContainers,
130 enableSuspenseyImages,
131 enableSrcObject,
132 enableViewTransition,
133 enableHydrationChangeEvent,
134 enableFragmentRefsScrollIntoView,
135 enableProfilerTimer,
136 enableFragmentRefsInstanceHandles,
137 enableFragmentRefsTextNodes,
138 } from 'shared/ReactFeatureFlags';
139 import {
140 HostComponent,
141 HostHoistable,
142 HostText,
143 HostSingleton,
144 } from 'react-reconciler/src/ReactWorkTags';
145 import {listenToAllSupportedEvents} from '../events/DOMPluginEventSystem';
146 import {validateLinkPropsForStyleResource} from '../shared/ReactDOMResourceValidation';
147 import escapeSelectorAttributeValueInsideDoubleQuotes from './escapeSelectorAttributeValueInsideDoubleQuotes';
148 import {flushSyncWork as flushSyncWorkOnAllRoots} from 'react-reconciler/src/ReactFiberWorkLoop';
149 import {requestFormReset as requestFormResetOnFiber} from 'react-reconciler/src/ReactFiberHooks';
150
151 import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';
152
153 export {default as rendererVersion} from 'shared/ReactVersion';
154
155 import noop from 'shared/noop';
156 import estimateBandwidth from './estimateBandwidth';
157
158 export const rendererPackageName = 'react-dom';
159 export const extraDevToolsConfig = null;
160
161 export type Type = string;
162 export type Props = {
163 autoFocus?: boolean,
164 children?: mixed,
165 disabled?: boolean,
166 hidden?: boolean,
167 suppressHydrationWarning?: boolean,
168 dangerouslySetInnerHTML?: mixed,
169 style?: {
170 display?: string,
171 viewTransitionName?: string,
172 'view-transition-name'?: string,
173 viewTransitionClass?: string,
174 'view-transition-class'?: string,
175 margin?: string,
176 marginTop?: string,
177 'margin-top'?: string,
178 marginBottom?: string,
179 'margin-bottom'?: string,
180 ...
181 },
182 bottom?: null | number,
183 left?: null | number,
184 right?: null | number,
185 top?: null | number,
186 is?: string,
187 size?: number,
188 value?: string,
189 defaultValue?: string,
190 checked?: boolean,
191 defaultChecked?: boolean,
192 multiple?: boolean,
193 type?: string,
194 src?: string | Blob | MediaSource | MediaStream, // TODO: Response
195 srcSet?: string,
196 loading?: 'eager' | 'lazy',
197 onLoad?: (event: any) => void,
198 ...
199 };
200 type RawProps = {
201 [string]: mixed,
202 };
203 export type EventTargetChildElement = {
204 type: string,
205 props: null | {
206 style?: {
207 position?: string,
208 zIndex?: number,
209 bottom?: string,
210 left?: string,
211 right?: string,
212 top?: string,
213 ...
214 },
215 ...
216 },
217 ...
218 };
219
220 export type Container =
221 | interface extends Element {_reactRootContainer?: FiberRoot}
222 | interface extends Document {_reactRootContainer?: FiberRoot}
223 | interface extends DocumentFragment {_reactRootContainer?: FiberRoot};
224 export type Instance = Element;
225 export type TextInstance = Text;
226
227 type InstanceWithFragmentHandles = Instance & {
228 reactFragments?: Set<FragmentInstanceType>,
229 };
230 type HostNodeWithFragmentHandles = (Instance | TextInstance) & {
231 reactFragments?: Set<FragmentInstanceType>,
232 };
233
234 declare class ActivityInterface extends Comment {}
235 declare class SuspenseInterface extends Comment {
236 _reactRetry: void | (() => void);
237 }
238
239 export type ActivityInstance = ActivityInterface;
240 export type SuspenseInstance = SuspenseInterface;
241
242 type FormStateMarkerInstance = Comment;
243 export type HydratableInstance =
244 | Instance
245 | TextInstance
246 | ActivityInstance
247 | SuspenseInstance
248 | FormStateMarkerInstance;
249 export type PublicInstance = Element | Text;
250 export type HostContextDev = {
251 context: HostContextProd,
252 ancestorInfo: AncestorInfoDev,
253 };
254 type HostContextProd = HostContextNamespace;
255 export type HostContext = HostContextDev | HostContextProd;
256 export type UpdatePayload = Array<mixed>;
257 export type ChildSet = void; // Unused
258 export type TimeoutHandle = TimeoutID;
259 export type NoTimeout = -1;
260 export type RendererInspectionConfig = $ReadOnly<{}>;
261
262 export type TransitionStatus = FormStatus;
263
264 export type ViewTransitionInstance = {
265 name: string,
266 group: mixin$Animatable,
267 imagePair: mixin$Animatable,
268 old: mixin$Animatable,
269 new: mixin$Animatable,
270 };
271
272 type SelectionInformation = {
273 focusedElem: null | HTMLElement,
274 selectionRange: mixed,
275 };
276
277 const SUPPRESS_HYDRATION_WARNING = 'suppressHydrationWarning';
278
279 const ACTIVITY_START_DATA = '&';
280 const ACTIVITY_END_DATA = '/&';
281 const SUSPENSE_START_DATA = '$';
282 const SUSPENSE_END_DATA = '/$';
283 const SUSPENSE_PENDING_START_DATA = '$?';
284 const SUSPENSE_QUEUED_START_DATA = '$~';
285 const SUSPENSE_FALLBACK_START_DATA = '$!';
286 const PREAMBLE_CONTRIBUTION_HTML = 'html';
287 const PREAMBLE_CONTRIBUTION_BODY = 'body';
288 const PREAMBLE_CONTRIBUTION_HEAD = 'head';
289 const FORM_STATE_IS_MATCHING = 'F!';
290 const FORM_STATE_IS_NOT_MATCHING = 'F';
291
292 const DOCUMENT_READY_STATE_LOADING = 'loading';
293
294 const STYLE = 'style';
295
296 opaque type HostContextNamespace = 0 | 1 | 2;
297 export const HostContextNamespaceNone: HostContextNamespace = 0;
298 const HostContextNamespaceSvg: HostContextNamespace = 1;
299 const HostContextNamespaceMath: HostContextNamespace = 2;
300
301 let eventsEnabled: ?boolean = null;
302 let selectionInformation: null | SelectionInformation = null;
303
304 export * from 'react-reconciler/src/ReactFiberConfigWithNoPersistence';
305
306 function getOwnerDocumentFromRootContainer(
307 rootContainerElement: Element | Document | DocumentFragment,
308 ): Document {
309 return rootContainerElement.nodeType === DOCUMENT_NODE
310 ? (rootContainerElement as any)
311 : rootContainerElement.ownerDocument;
312 }
313
314 export function getRootHostContext(
315 rootContainerInstance: Container,
316 ): HostContext {
317 let type;
318 let context: HostContextProd;
319 const nodeType = rootContainerInstance.nodeType;
320 switch (nodeType) {
321 case DOCUMENT_NODE:
322 case DOCUMENT_FRAGMENT_NODE: {
323 type = nodeType === DOCUMENT_NODE ? '#document' : '#fragment';
324 const root = (rootContainerInstance as any).documentElement;
325 if (root) {
326 const namespaceURI = root.namespaceURI;
327 context = namespaceURI
328 ? getOwnHostContext(namespaceURI)
329 : HostContextNamespaceNone;
330 } else {
331 context = HostContextNamespaceNone;
332 }
333 break;
334 }
335 default: {
336 const container: any =
337 !disableCommentsAsDOMContainers && nodeType === COMMENT_NODE
338 ? rootContainerInstance.parentNode
339 : rootContainerInstance;
340 type = container.tagName;
341 const namespaceURI = container.namespaceURI;
342 if (!namespaceURI) {
343 switch (type) {
344 case 'svg':
345 context = HostContextNamespaceSvg;
346 break;
347 case 'math':
348 context = HostContextNamespaceMath;
349 break;
350 default:
351 context = HostContextNamespaceNone;
352 break;
353 }
354 } else {
355 const ownContext = getOwnHostContext(namespaceURI);
356 context = getChildHostContextProd(ownContext, type);
357 }
358 break;
359 }
360 }
361 if (__DEV__) {
362 const validatedTag = type.toLowerCase();
363 const ancestorInfo = updatedAncestorInfoDev(null, validatedTag);
364 return {context, ancestorInfo};
365 }
366 return context;
367 }
368
369 function getOwnHostContext(namespaceURI: string): HostContextNamespace {
370 switch (namespaceURI) {
371 case SVG_NAMESPACE:
372 return HostContextNamespaceSvg;
373 case MATH_NAMESPACE:
374 return HostContextNamespaceMath;
375 default:
376 return HostContextNamespaceNone;
377 }
378 }
379
380 function getChildHostContextProd(
381 parentNamespace: HostContextNamespace,
382 type: string,
383 ): HostContextNamespace {
384 if (parentNamespace === HostContextNamespaceNone) {
385 // No (or default) parent namespace: potential entry point.
386 switch (type) {
387 case 'svg':
388 return HostContextNamespaceSvg;
389 case 'math':
390 return HostContextNamespaceMath;
391 default:
392 return HostContextNamespaceNone;
393 }
394 }
395 if (parentNamespace === HostContextNamespaceSvg && type === 'foreignObject') {
396 // We're leaving SVG.
397 return HostContextNamespaceNone;
398 }
399 // By default, pass namespace below.
400 return parentNamespace;
401 }
402
403 export function getChildHostContext(
404 parentHostContext: HostContext,
405 type: string,
406 ): HostContext {
407 if (__DEV__) {
408 const parentHostContextDev = parentHostContext as any as HostContextDev;
409 const context = getChildHostContextProd(parentHostContextDev.context, type);
410 const ancestorInfo = updatedAncestorInfoDev(
411 parentHostContextDev.ancestorInfo,
412 type,
413 );
414 return {context, ancestorInfo};
415 }
416 const parentNamespace = parentHostContext as any as HostContextProd;
417 return getChildHostContextProd(parentNamespace, type);
418 }
419
420 export function getPublicInstance(instance: Instance): Instance {
421 return instance;
422 }
423
424 export function prepareForCommit(containerInfo: Container): Object | null {
425 eventsEnabled = ReactBrowserEventEmitterIsEnabled();
426 selectionInformation = getSelectionInformation(containerInfo);
427 let activeInstance = null;
428 if (enableCreateEventHandleAPI) {
429 const focusedElem = selectionInformation.focusedElem;
430 if (focusedElem !== null) {
431 activeInstance = getClosestInstanceFromNode(focusedElem);
432 }
433 }
434 ReactBrowserEventEmitterSetEnabled(false);
435 return activeInstance;
436 }
437
438 export function beforeActiveInstanceBlur(internalInstanceHandle: Object): void {
439 if (enableCreateEventHandleAPI) {
440 ReactBrowserEventEmitterSetEnabled(true);
441 dispatchBeforeDetachedBlur(
442 (selectionInformation as any).focusedElem,
443 internalInstanceHandle,
444 );
445 ReactBrowserEventEmitterSetEnabled(false);
446 }
447 }
448
449 export function afterActiveInstanceBlur(): void {
450 if (enableCreateEventHandleAPI) {
451 ReactBrowserEventEmitterSetEnabled(true);
452 dispatchAfterDetachedBlur((selectionInformation as any).focusedElem);
453 ReactBrowserEventEmitterSetEnabled(false);
454 }
455 }
456
457 export function resetAfterCommit(containerInfo: Container): void {
458 restoreSelection(selectionInformation, containerInfo);
459 ReactBrowserEventEmitterSetEnabled(eventsEnabled);
460 eventsEnabled = null;
461 selectionInformation = null;
462 }
463
464 export function createHoistableInstance(
465 type: string,
466 props: Props,
467 rootContainerInstance: Container,
468 internalInstanceHandle: Object,
469 ): Instance {
470 const ownerDocument = getOwnerDocumentFromRootContainer(
471 rootContainerInstance,
472 );
473
474 const domElement: Instance = ownerDocument.createElement(type);
475 precacheFiberNode(internalInstanceHandle, domElement);
476 updateFiberProps(domElement, props);
477 setInitialProperties(domElement, type, props);
478 markNodeAsHoistable(domElement);
479 return domElement;
480 }
481
482 let didWarnScriptTags = false;
483 function isScriptDataBlock(props: Props): boolean {
484 const scriptType = props.type;
485 if (typeof scriptType !== 'string' || scriptType === '') {
486 return false;
487 }
488 const lower = scriptType.toLowerCase();
489 // Special non-MIME keywords recognized by the HTML spec
490 // TODO: May be fine to also not warn about having these types be parsed as "parser-inserted"
491 if (
492 lower === 'module' ||
493 lower === 'importmap' ||
494 lower === 'speculationrules'
495 ) {
496 return false;
497 }
498 // JavaScript MIME types per https://mimesniff.spec.whatwg.org/#javascript-mime-type
499 switch (lower) {
500 case 'application/ecmascript':
501 case 'application/javascript':
502 case 'application/x-ecmascript':
503 case 'application/x-javascript':
504 case 'text/ecmascript':
505 case 'text/javascript':
506 case 'text/javascript1.0':
507 case 'text/javascript1.1':
508 case 'text/javascript1.2':
509 case 'text/javascript1.3':
510 case 'text/javascript1.4':
511 case 'text/javascript1.5':
512 case 'text/jscript':
513 case 'text/livescript':
514 case 'text/x-ecmascript':
515 case 'text/x-javascript':
516 return false;
517 }
518 // Any other non-empty type value means this is a data block
519 return true;
520 }
521 const warnedUnknownTags: {
522 [key: string]: boolean,
523 } = {
524 // There are working polyfills for <dialog>. Let people use it.
525 dialog: true,
526 // Electron ships a custom <webview> tag to display external web content in
527 // an isolated frame and process.
528 // This tag is not present in non Electron environments such as JSDom which
529 // is often used for testing purposes.
530 // @see https://electronjs.org/docs/api/webview-tag
531 webview: true,
532 };
533
534 export function createInstance(
535 type: string,
536 props: Props,
537 rootContainerInstance: Container,
538 hostContext: HostContext,
539 internalInstanceHandle: Object,
540 ): Instance {
541 let hostContextProd: HostContextProd;
542 if (__DEV__) {
543 // TODO: take namespace into account when validating.
544 const hostContextDev: HostContextDev = hostContext as any;
545 validateDOMNesting(type, hostContextDev.ancestorInfo);
546 hostContextProd = hostContextDev.context;
547 } else {
548 hostContextProd = hostContext as any;
549 }
550
551 const ownerDocument = getOwnerDocumentFromRootContainer(
552 rootContainerInstance,
553 );
554
555 let domElement: Instance;
556 switch (hostContextProd) {
557 case HostContextNamespaceSvg:
558 domElement = ownerDocument.createElementNS(SVG_NAMESPACE, type);
559 break;
560 case HostContextNamespaceMath:
561 domElement = ownerDocument.createElementNS(MATH_NAMESPACE, type);
562 break;
563 default:
564 switch (type) {
565 case 'svg': {
566 domElement = ownerDocument.createElementNS(SVG_NAMESPACE, type);
567 break;
568 }
569 case 'math': {
570 domElement = ownerDocument.createElementNS(MATH_NAMESPACE, type);
571 break;
572 }
573 case 'script': {
574 // Create the script via .innerHTML so its "parser-inserted" flag is
575 // set to true and it does not execute
576 const div = ownerDocument.createElement('div');
577 if (__DEV__) {
578 if (
579 enableTrustedTypesIntegration &&
580 !didWarnScriptTags &&
581 // Data block scripts are not executed by UAs anyway so
582 // we don't need to warn: https://html.spec.whatwg.org/multipage/scripting.html#attr-script-type
583 !isScriptDataBlock(props)
584 ) {
585 console.error(
586 'Encountered a script tag while rendering React component. ' +
587 'Scripts inside React components are never executed when rendering ' +
588 'on the client. Consider using template tag instead ' +
589 '(https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template).',
590 );
591 didWarnScriptTags = true;
592 }
593 }
594 div.innerHTML = '<script><' + '/script>';
595 // This is guaranteed to yield a script element.
596 const firstChild = div.firstChild as any as HTMLScriptElement;
597 domElement = div.removeChild(firstChild);
598 break;
599 }
600 case 'select': {
601 if (typeof props.is === 'string') {
602 domElement = ownerDocument.createElement('select', {is: props.is});
603 } else {
604 // Separate else branch instead of using `props.is || undefined` above because of a Firefox bug.
605 // See discussion in https://github.com/facebook/react/pull/6896
606 // and discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1276240
607 domElement = ownerDocument.createElement('select');
608 }
609 if (props.multiple) {
610 domElement.multiple = true;
611 } else if (props.size) {
612 // Setting a size greater than 1 causes a select to behave like `multiple=true`, where
613 // it is possible that no option is selected.
614 //
615 // This is only necessary when a select in "single selection mode".
616 domElement.size = props.size;
617 }
618 break;
619 }
620 default: {
621 if (typeof props.is === 'string') {
622 domElement = ownerDocument.createElement(type, {is: props.is});
623 } else {
624 // Separate else branch instead of using `props.is || undefined` above because of a Firefox bug.
625 // See discussion in https://github.com/facebook/react/pull/6896
626 // and discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1276240
627 domElement = ownerDocument.createElement(type);
628 }
629
630 if (__DEV__) {
631 if (type.indexOf('-') === -1) {
632 // We're not SVG/MathML and we don't have a dash, so we're not a custom element
633 // Even if you use `is`, these should be of known type and lower case.
634 if (type !== type.toLowerCase()) {
635 console.error(
636 '<%s /> is using incorrect casing. ' +
637 'Use PascalCase for React components, ' +
638 'or lowercase for HTML elements.',
639 type,
640 );
641 }
642 if (
643 // $FlowFixMe[method-unbinding]
644 Object.prototype.toString.call(domElement) ===
645 '[object HTMLUnknownElement]' &&
646 !hasOwnProperty.call(warnedUnknownTags, type)
647 ) {
648 warnedUnknownTags[type] = true;
649 console.error(
650 'The tag <%s> is unrecognized in this browser. ' +
651 'If you meant to render a React component, start its name with ' +
652 'an uppercase letter.',
653 type,
654 );
655 }
656 }
657 }
658 }
659 }
660 }
661 precacheFiberNode(internalInstanceHandle, domElement);
662 updateFiberProps(domElement, props);
663 return domElement;
664 }
665
666 let didWarnForClone = false;
667
668 export function cloneMutableInstance(
669 instance: Instance,
670 keepChildren: boolean,
671 ): Instance {
672 if (__DEV__) {
673 // Warn for problematic
674 const tagName = instance.tagName;
675 switch (tagName) {
676 case 'VIDEO':
677 case 'IFRAME':
678 if (!didWarnForClone) {
679 didWarnForClone = true;
680 // TODO: Once we have the ability to avoid cloning the root, suggest an absolutely
681 // positioned ViewTransition instead as the solution.
682 console.warn(
683 'startGestureTransition() required cloning a <%s> element since it exists in ' +
684 'both states of the gesture. This can be problematic since it will load it twice ' +
685 'Try removing or hiding it with <Activity mode="offscreen"> in the optimistic state.',
686 tagName.toLowerCase(),
687 );
688 }
689 break;
690 }
691 }
692 return instance.cloneNode(keepChildren);
693 }
694
695 export function appendInitialChild(
696 parentInstance: Instance,
697 child: Instance | TextInstance,
698 ): void {
699 // Note: This should not use moveBefore() because initial are appended while disconnected.
700 parentInstance.appendChild(child);
701 }
702
703 export function finalizeInitialChildren(
704 domElement: Instance,
705 type: string,
706 props: Props,
707 hostContext: HostContext,
708 ): boolean {
709 setInitialProperties(domElement, type, props);
710 switch (type) {
711 case 'button':
712 case 'input':
713 case 'select':
714 case 'textarea':
715 return !!props.autoFocus;
716 case 'img':
717 return true;
718 default:
719 return false;
720 }
721 }
722
723 export function finalizeHydratedChildren(
724 domElement: Instance,
725 type: string,
726 props: Props,
727 hostContext: HostContext,
728 ): boolean {
729 // TOOD: Consider unifying this with hydrateInstance.
730 if (!enableHydrationChangeEvent) {
731 return false;
732 }
733 switch (type) {
734 case 'input':
735 case 'select':
736 case 'textarea':
737 case 'img':
738 return true;
739 default:
740 return false;
741 }
742 }
743
744 export function shouldSetTextContent(type: string, props: Props): boolean {
745 return (
746 type === 'textarea' ||
747 type === 'noscript' ||
748 typeof props.children === 'string' ||
749 typeof props.children === 'number' ||
750 typeof props.children === 'bigint' ||
751 (typeof props.dangerouslySetInnerHTML === 'object' &&
752 props.dangerouslySetInnerHTML !== null &&
753 props.dangerouslySetInnerHTML.__html != null)
754 );
755 }
756
757 export function createTextInstance(
758 text: string,
759 rootContainerInstance: Container,
760 hostContext: HostContext,
761 internalInstanceHandle: Object,
762 ): TextInstance {
763 if (__DEV__) {
764 const hostContextDev = hostContext as any as HostContextDev;
765 const ancestor = hostContextDev.ancestorInfo.current;
766 if (ancestor != null) {
767 validateTextNesting(
768 text,
769 ancestor.tag,
770 hostContextDev.ancestorInfo.implicitRootScope,
771 );
772 }
773 }
774 const textNode: TextInstance = getOwnerDocumentFromRootContainer(
775 rootContainerInstance,
776 ).createTextNode(text);
777 precacheFiberNode(internalInstanceHandle, textNode);
778 return textNode;
779 }
780
781 export function cloneMutableTextInstance(
782 textInstance: TextInstance,
783 ): TextInstance {
784 return textInstance.cloneNode(false);
785 }
786
787 let currentPopstateTransitionEvent: Event | null = null;
788 export function shouldAttemptEagerTransition(): boolean {
789 const event = window.event;
790 if (event && event.type === 'popstate') {
791 // This is a popstate event. Attempt to render any transition during this
792 // event synchronously. Unless we already attempted during this event.
793 if (event === currentPopstateTransitionEvent) {
794 // We already attempted to render this popstate transition synchronously.
795 // Any subsequent attempts must have happened as the result of a derived
796 // update, like startTransition inside useEffect, or useDV. Switch back to
797 // the default behavior for all remaining transitions during the current
798 // popstate event.
799 return false;
800 } else {
801 // Cache the current event in case a derived transition is scheduled.
802 // (Refer to previous branch.)
803 currentPopstateTransitionEvent = event;
804 return true;
805 }
806 }
807 // We're not inside a popstate event.
808 currentPopstateTransitionEvent = null;
809 return false;
810 }
811
812 let schedulerEvent: void | Event = undefined;
813 export function trackSchedulerEvent(): void {
814 schedulerEvent = window.event;
815 }
816
817 export function resolveEventType(): null | string {
818 const event = window.event;
819 return event && event !== schedulerEvent ? event.type : null;
820 }
821
822 export function resolveEventTimeStamp(): number {
823 const event = window.event;
824 return event && event !== schedulerEvent ? event.timeStamp : -1.1;
825 }
826
827 export const isPrimaryRenderer = true;
828 export const warnsIfNotActing = true;
829 // This initialization code may run even on server environments
830 // if a component just imports ReactDOM (e.g. for findDOMNode).
831 // Some environments might not have setTimeout or clearTimeout.
832 export const scheduleTimeout: any =
833 typeof setTimeout === 'function' ? setTimeout : (undefined as any);
834 export const cancelTimeout: any =
835 typeof clearTimeout === 'function' ? clearTimeout : (undefined as any);
836 export const noTimeout: -1 = -1;
837 const localPromise = typeof Promise === 'function' ? Promise : undefined;
838 const localRequestAnimationFrame =
839 typeof requestAnimationFrame === 'function'
840 ? requestAnimationFrame
841 : scheduleTimeout;
842
843 export {getClosestInstanceFromNode as getInstanceFromNode};
844
845 export function preparePortalMount(portalInstance: Instance): void {
846 listenToAllSupportedEvents(portalInstance);
847 }
848
849 export function prepareScopeUpdate(
850 scopeInstance: ReactScopeInstance,
851 internalInstanceHandle: Object,
852 ): void {
853 if (enableScopeAPI) {
854 precacheFiberNode(internalInstanceHandle, scopeInstance);
855 }
856 }
857
858 export function getInstanceFromScope(
859 scopeInstance: ReactScopeInstance,
860 ): null | Object {
861 if (enableScopeAPI) {
862 return getFiberFromScopeInstance(scopeInstance);
863 }
864 return null;
865 }
866
867 // -------------------
868 // Microtasks
869 // -------------------
870 export const supportsMicrotasks = true;
871 export const scheduleMicrotask: any =
872 typeof queueMicrotask === 'function'
873 ? queueMicrotask
874 : typeof localPromise !== 'undefined'
875 ? callback =>
876 localPromise.resolve(null).then(callback).catch(handleErrorInNextTick)
877 : scheduleTimeout; // TODO: Determine the best fallback here.
878
879 function handleErrorInNextTick(error: any) {
880 setTimeout(() => {
881 throw error;
882 });
883 }
884
885 // -------------------
886 // Mutation
887 // -------------------
888
889 export const supportsMutation = true;
890
891 export function commitMount(
892 domElement: Instance,
893 type: string,
894 newProps: Props,
895 internalInstanceHandle: Object,
896 ): void {
897 // Despite the naming that might imply otherwise, this method only
898 // fires if there is an `Update` effect scheduled during mounting.
899 // This happens if `finalizeInitialChildren` returns `true` (which it
900 // does to implement the `autoFocus` attribute on the client). But
901 // there are also other cases when this might happen (such as patching
902 // up text content during hydration mismatch). So we'll check this again.
903 switch (type) {
904 case 'button':
905 case 'input':
906 case 'select':
907 case 'textarea':
908 if (newProps.autoFocus) {
909 (
910 domElement as any as
911 | HTMLButtonElement
912 | HTMLInputElement
913 | HTMLSelectElement
914 | HTMLTextAreaElement
915 ).focus();
916 }
917 return;
918 case 'img': {
919 // The technique here is to assign the src or srcSet property to cause the browser
920 // to issue a new load event. If it hasn't loaded yet it'll fire whenever the load actually completes.
921 // If it has already loaded we missed it so the second load will still be the first one that executes
922 // any associated onLoad props.
923 // Even if we have srcSet we prefer to reassign src. The reason is that Firefox does not trigger a new
924 // load event when only srcSet is assigned. Chrome will trigger a load event if either is assigned so we
925 // only need to assign one. And Safari just never triggers a new load event which means this technique
926 // is already a noop regardless of which properties are assigned. We should revisit if browsers update
927 // this heuristic in the future.
928 if (newProps.src) {
929 const src = (newProps as any).src;
930 if (enableSrcObject && typeof src === 'object') {
931 // For object src, we can't just set the src again to the same blob URL because it might have
932 // already revoked if it loaded before this. However, we can create a new blob URL and set that.
933 // This is relatively cheap since the blob is already in memory but this might cause some
934 // duplicated work.
935 // TODO: We could maybe detect if load hasn't fired yet and if so reuse the URL.
936 try {
937 setSrcObject(domElement, type, src);
938 return;
939 } catch (x) {
940 // If URL.createObjectURL() errors, it was probably some other object type
941 // that should be toString:ed instead, so we just fall-through to the normal
942 // path.
943 }
944 }
945 (domElement as any as HTMLImageElement).src = src;
946 } else if (newProps.srcSet) {
947 (domElement as any as HTMLImageElement).srcset = (
948 newProps as any
949 ).srcSet;
950 }
951 return;
952 }
953 }
954 }
955
956 export function commitHydratedInstance(
957 domElement: Instance,
958 type: string,
959 props: Props,
960 internalInstanceHandle: Object,
961 ): void {
962 if (!enableHydrationChangeEvent) {
963 return;
964 }
965 // This fires in the commit phase if a hydrated instance needs to do further
966 // work in the commit phase. Similar to commitMount. However, this should not
967 // do things that would've already happened such as set auto focus since that
968 // would steal focus. It's only scheduled if finalizeHydratedChildren returns
969 // true.
970 switch (type) {
971 case 'input': {
972 hydrateInput(
973 domElement,
974 props.value,
975 props.defaultValue,
976 props.checked,
977 props.defaultChecked,
978 );
979 break;
980 }
981 case 'select': {
982 hydrateSelect(
983 domElement,
984 props.value,
985 props.defaultValue,
986 props.multiple,
987 );
988 break;
989 }
990 case 'textarea':
991 hydrateTextarea(domElement, props.value, props.defaultValue);
992 break;
993 case 'img':
994 // TODO: Should we replay onLoad events?
995 break;
996 }
997 }
998
999 export function commitUpdate(
1000 domElement: Instance,
1001 type: string,
1002 oldProps: Props,
1003 newProps: Props,
1004 internalInstanceHandle: Object,
1005 ): void {
1006 // Diff and update the properties.
1007 updateProperties(domElement, type, oldProps, newProps);
1008
1009 // Update the props handle so that we know which props are the ones with
1010 // with current event handlers.
1011 updateFiberProps(domElement, newProps);
1012 }
1013
1014 export function resetTextContent(domElement: Instance): void {
1015 setTextContent(domElement, '');
1016 }
1017
1018 export function commitTextUpdate(
1019 textInstance: TextInstance,
1020 oldText: string,
1021 newText: string,
1022 ): void {
1023 textInstance.nodeValue = newText;
1024 }
1025
1026 const supportsMoveBefore =
1027 // $FlowFixMe[prop-missing]: We're doing the feature detection here.
1028 enableMoveBefore &&
1029 typeof window !== 'undefined' &&
1030 typeof window.Element.prototype.moveBefore === 'function';
1031
1032 export function appendChild(
1033 parentInstance: Instance,
1034 child: Instance | TextInstance,
1035 ): void {
1036 if (supportsMoveBefore && child.parentNode !== null) {
1037 // $FlowFixMe[prop-missing]: We've checked this with supportsMoveBefore.
1038 parentInstance.moveBefore(child, null);
1039 } else {
1040 parentInstance.appendChild(child);
1041 }
1042 }
1043
1044 function warnForReactChildrenConflict(container: Container): void {
1045 if (__DEV__) {
1046 if ((container as any).__reactWarnedAboutChildrenConflict) {
1047 return;
1048 }
1049 const props = getFiberCurrentPropsFromNode(container);
1050 if (props !== null) {
1051 const fiber = getInstanceFromNode(container);
1052 if (fiber !== null) {
1053 if (
1054 typeof props.children === 'string' ||
1055 typeof props.children === 'number'
1056 ) {
1057 (container as any).__reactWarnedAboutChildrenConflict = true;
1058 // Run the warning with the Fiber of the container for context of where the children are specified.
1059 // We could also maybe use the Portal. The current execution context is the child being added.
1060 runWithFiberInDEV(fiber, () => {
1061 console.error(
1062 'Cannot use a ref on a React element as a container to `createRoot` or `createPortal` ' +
1063 'if that element also sets "children" text content using React. It should be a leaf with no children. ' +
1064 "Otherwise it's ambiguous which children should be used.",
1065 );
1066 });
1067 } else if (props.dangerouslySetInnerHTML != null) {
1068 (container as any).__reactWarnedAboutChildrenConflict = true;
1069 runWithFiberInDEV(fiber, () => {
1070 console.error(
1071 'Cannot use a ref on a React element as a container to `createRoot` or `createPortal` ' +
1072 'if that element also sets "dangerouslySetInnerHTML" using React. It should be a leaf with no children. ' +
1073 "Otherwise it's ambiguous which children should be used.",
1074 );
1075 });
1076 }
1077 }
1078 }
1079 }
1080 }
1081
1082 export function appendChildToContainer(
1083 container: Container,
1084 child: Instance | TextInstance,
1085 ): void {
1086 if (__DEV__) {
1087 warnForReactChildrenConflict(container);
1088 }
1089 let parentNode: DocumentFragment | Element;
1090 if (container.nodeType === DOCUMENT_NODE) {
1091 parentNode = (container as any).body;
1092 } else if (
1093 !disableCommentsAsDOMContainers &&
1094 container.nodeType === COMMENT_NODE
1095 ) {
1096 parentNode = container.parentNode as any;
1097 if (supportsMoveBefore && child.parentNode !== null) {
1098 // $FlowFixMe[prop-missing]: We've checked this with supportsMoveBefore.
1099 parentNode.moveBefore(child, container);
1100 } else {
1101 parentNode.insertBefore(child, container);
1102 }
1103 return;
1104 } else if (container.nodeName === 'HTML') {
1105 parentNode = container.ownerDocument.body as any;
1106 } else {
1107 parentNode = container as any;
1108 }
1109 if (supportsMoveBefore && child.parentNode !== null) {
1110 // $FlowFixMe[prop-missing]: We've checked this with supportsMoveBefore.
1111 parentNode.moveBefore(child, null);
1112 } else {
1113 parentNode.appendChild(child);
1114 }
1115
1116 // This container might be used for a portal.
1117 // If something inside a portal is clicked, that click should bubble
1118 // through the React tree. However, on Mobile Safari the click would
1119 // never bubble through the *DOM* tree unless an ancestor with onclick
1120 // event exists. So we wouldn't see it and dispatch it.
1121 // This is why we ensure that non React root containers have inline onclick
1122 // defined.
1123 // https://github.com/facebook/react/issues/11918
1124 const reactRootContainer = container._reactRootContainer;
1125 if (
1126 // $FlowFixMe[invalid-compare]
1127 (reactRootContainer === null || reactRootContainer === undefined) &&
1128 parentNode.onclick === null
1129 ) {
1130 // TODO: This cast may not be sound for SVG, MathML or custom elements.
1131 trapClickOnNonInteractiveElement(parentNode as any as HTMLElement);
1132 }
1133 }
1134
1135 export function insertBefore(
1136 parentInstance: Instance,
1137 child: Instance | TextInstance,
1138 beforeChild: Instance | TextInstance | SuspenseInstance | ActivityInstance,
1139 ): void {
1140 if (supportsMoveBefore && child.parentNode !== null) {
1141 // $FlowFixMe[prop-missing]: We've checked this with supportsMoveBefore.
1142 parentInstance.moveBefore(child, beforeChild);
1143 } else {
1144 parentInstance.insertBefore(child, beforeChild);
1145 }
1146 }
1147
1148 export function insertInContainerBefore(
1149 container: Container,
1150 child: Instance | TextInstance,
1151 beforeChild: Instance | TextInstance | SuspenseInstance | ActivityInstance,
1152 ): void {
1153 if (__DEV__) {
1154 warnForReactChildrenConflict(container);
1155 }
1156 let parentNode: DocumentFragment | Element;
1157 if (container.nodeType === DOCUMENT_NODE) {
1158 parentNode = (container as any).body;
1159 } else if (
1160 !disableCommentsAsDOMContainers &&
1161 container.nodeType === COMMENT_NODE
1162 ) {
1163 parentNode = container.parentNode as any;
1164 } else if (container.nodeName === 'HTML') {
1165 parentNode = container.ownerDocument.body as any;
1166 } else {
1167 parentNode = container as any;
1168 }
1169 if (supportsMoveBefore && child.parentNode !== null) {
1170 // $FlowFixMe[prop-missing]: We've checked this with supportsMoveBefore.
1171 parentNode.moveBefore(child, beforeChild);
1172 } else {
1173 parentNode.insertBefore(child, beforeChild);
1174 }
1175 }
1176
1177 export function isSingletonScope(type: string): boolean {
1178 return type === 'head';
1179 }
1180
1181 function createEvent(type: DOMEventName, bubbles: boolean): Event {
1182 const event = document.createEvent('Event');
1183 event.initEvent(type as any as string, bubbles, false);
1184 return event;
1185 }
1186
1187 function dispatchBeforeDetachedBlur(
1188 target: HTMLElement,
1189 internalInstanceHandle: Object,
1190 ): void {
1191 if (enableCreateEventHandleAPI) {
1192 const event = createEvent('beforeblur', true);
1193 // Dispatch "beforeblur" directly on the target,
1194 // so it gets picked up by the event system and
1195 // can propagate through the React internal tree.
1196 // $FlowFixMe[prop-missing]: internal field
1197 event._detachedInterceptFiber = internalInstanceHandle;
1198 target.dispatchEvent(event);
1199 }
1200 }
1201
1202 function dispatchAfterDetachedBlur(target: HTMLElement): void {
1203 if (enableCreateEventHandleAPI) {
1204 const event = createEvent('afterblur', false);
1205 // So we know what was detached, make the relatedTarget the
1206 // detached target on the "afterblur" event.
1207 (event as any).relatedTarget = target;
1208 // Dispatch the event on the document.
1209 document.dispatchEvent(event);
1210 }
1211 }
1212
1213 export function removeChild(
1214 parentInstance: Instance,
1215 child: Instance | TextInstance | SuspenseInstance | ActivityInstance,
1216 ): void {
1217 parentInstance.removeChild(child);
1218 }
1219
1220 export function removeChildFromContainer(
1221 container: Container,
1222 child: Instance | TextInstance | SuspenseInstance | ActivityInstance,
1223 ): void {
1224 let parentNode: DocumentFragment | Element;
1225 if (container.nodeType === DOCUMENT_NODE) {
1226 parentNode = (container as any).body;
1227 } else if (
1228 !disableCommentsAsDOMContainers &&
1229 container.nodeType === COMMENT_NODE
1230 ) {
1231 parentNode = container.parentNode as any;
1232 } else if (container.nodeName === 'HTML') {
1233 parentNode = container.ownerDocument.body as any;
1234 } else {
1235 parentNode = container as any;
1236 }
1237 parentNode.removeChild(child);
1238 }
1239
1240 function clearHydrationBoundary(
1241 parentInstance: Instance,
1242 hydrationInstance: SuspenseInstance | ActivityInstance,
1243 ): void {
1244 let node: Node = hydrationInstance;
1245 // Delete all nodes within this suspense boundary.
1246 // There might be nested nodes so we need to keep track of how
1247 // deep we are and only break out when we're back on top.
1248 let depth = 0;
1249 do {
1250 const nextNode = node.nextSibling;
1251 parentInstance.removeChild(node);
1252 if (nextNode && nextNode.nodeType === COMMENT_NODE) {
1253 const data = (nextNode as any).data as string;
1254 if (data === SUSPENSE_END_DATA || data === ACTIVITY_END_DATA) {
1255 if (depth === 0) {
1256 parentInstance.removeChild(nextNode);
1257 // Retry if any event replaying was blocked on this.
1258 retryIfBlockedOn(hydrationInstance);
1259 return;
1260 } else {
1261 depth--;
1262 }
1263 } else if (
1264 data === SUSPENSE_START_DATA ||
1265 data === SUSPENSE_PENDING_START_DATA ||
1266 data === SUSPENSE_QUEUED_START_DATA ||
1267 data === SUSPENSE_FALLBACK_START_DATA ||
1268 data === ACTIVITY_START_DATA
1269 ) {
1270 depth++;
1271 } else if (data === PREAMBLE_CONTRIBUTION_HTML) {
1272 // If a preamble contribution marker is found within the bounds of this boundary,
1273 // then it contributed to the html tag and we need to reset it.
1274 const ownerDocument = parentInstance.ownerDocument;
1275 const documentElement: Element = ownerDocument.documentElement as any;
1276 clearSingletonPreambleContribution(documentElement);
1277 } else if (data === PREAMBLE_CONTRIBUTION_HEAD) {
1278 const ownerDocument = parentInstance.ownerDocument;
1279 const head: Element = ownerDocument.head as any;
1280 clearSingletonPreambleContribution(head);
1281 // We need to clear the head because this is the only singleton that can have children that
1282 // were part of this boundary but are not inside this boundary.
1283 clearHead(head);
1284 } else if (data === PREAMBLE_CONTRIBUTION_BODY) {
1285 const ownerDocument = parentInstance.ownerDocument;
1286 const body: Element = ownerDocument.body as any;
1287 clearSingletonPreambleContribution(body);
1288 }
1289 }
1290 // $FlowFixMe[incompatible-type] we bail out when we get a null
1291 node = nextNode;
1292 } while (node);
1293 // TODO: Warn, we didn't find the end comment boundary.
1294 // Retry if any event replaying was blocked on this.
1295 retryIfBlockedOn(hydrationInstance);
1296 }
1297
1298 export function clearActivityBoundary(
1299 parentInstance: Instance,
1300 activityInstance: ActivityInstance,
1301 ): void {
1302 clearHydrationBoundary(parentInstance, activityInstance);
1303 }
1304
1305 export function clearSuspenseBoundary(
1306 parentInstance: Instance,
1307 suspenseInstance: SuspenseInstance,
1308 ): void {
1309 clearHydrationBoundary(parentInstance, suspenseInstance);
1310 }
1311
1312 function clearHydrationBoundaryFromContainer(
1313 container: Container,
1314 hydrationInstance: SuspenseInstance | ActivityInstance,
1315 ): void {
1316 let parentNode: DocumentFragment | Element;
1317 if (container.nodeType === DOCUMENT_NODE) {
1318 parentNode = (container as any).body;
1319 } else if (
1320 !disableCommentsAsDOMContainers &&
1321 container.nodeType === COMMENT_NODE
1322 ) {
1323 parentNode = container.parentNode as any;
1324 } else if (container.nodeName === 'HTML') {
1325 parentNode = container.ownerDocument.body as any;
1326 } else {
1327 parentNode = container as any;
1328 }
1329 clearHydrationBoundary(parentNode, hydrationInstance);
1330 // Retry if any event replaying was blocked on this.
1331 retryIfBlockedOn(container);
1332 }
1333
1334 export function clearActivityBoundaryFromContainer(
1335 container: Container,
1336 activityInstance: ActivityInstance,
1337 ): void {
1338 clearHydrationBoundaryFromContainer(container, activityInstance);
1339 }
1340
1341 export function clearSuspenseBoundaryFromContainer(
1342 container: Container,
1343 suspenseInstance: SuspenseInstance,
1344 ): void {
1345 clearHydrationBoundaryFromContainer(container, suspenseInstance);
1346 }
1347
1348 function hideOrUnhideDehydratedBoundary(
1349 suspenseInstance: SuspenseInstance | ActivityInstance,
1350 isHidden: boolean,
1351 ) {
1352 let node: Node = suspenseInstance;
1353 // Unhide all nodes within this suspense boundary.
1354 let depth = 0;
1355 do {
1356 const nextNode = node.nextSibling;
1357 if (node.nodeType === ELEMENT_NODE) {
1358 const instance = node as any as HTMLElement & {_stashedDisplay?: string};
1359 if (isHidden) {
1360 instance._stashedDisplay = instance.style.display;
1361 instance.style.display = 'none';
1362 } else {
1363 instance.style.display = instance._stashedDisplay || '';
1364 if (instance.getAttribute('style') === '') {
1365 instance.removeAttribute('style');
1366 }
1367 }
1368 } else if (node.nodeType === TEXT_NODE) {
1369 const textNode = node as any as Text & {_stashedText?: string};
1370 if (isHidden) {
1371 textNode._stashedText = textNode.nodeValue;
1372 textNode.nodeValue = '';
1373 } else {
1374 textNode.nodeValue = textNode._stashedText || '';
1375 }
1376 }
1377 if (nextNode && nextNode.nodeType === COMMENT_NODE) {
1378 const data = (nextNode as any).data as string;
1379 if (data === SUSPENSE_END_DATA) {
1380 if (depth === 0) {
1381 return;
1382 } else {
1383 depth--;
1384 }
1385 } else if (
1386 data === SUSPENSE_START_DATA ||
1387 data === SUSPENSE_PENDING_START_DATA ||
1388 data === SUSPENSE_QUEUED_START_DATA ||
1389 data === SUSPENSE_FALLBACK_START_DATA
1390 ) {
1391 depth++;
1392 }
1393 // TODO: Should we hide preamble contribution in this case?
1394 }
1395 // $FlowFixMe[incompatible-type] we bail out when we get a null
1396 node = nextNode;
1397 } while (node);
1398 }
1399
1400 export function hideDehydratedBoundary(
1401 suspenseInstance: SuspenseInstance,
1402 ): void {
1403 hideOrUnhideDehydratedBoundary(suspenseInstance, true);
1404 }
1405
1406 export function hideInstance(instance: Instance): void {
1407 // TODO: Does this work for all element types? What about MathML? Should we
1408 // pass host context to this method?
1409 instance = instance as any as HTMLElement;
1410 const style = instance.style;
1411 // $FlowFixMe[method-unbinding]
1412 if (typeof style.setProperty === 'function') {
1413 style.setProperty('display', 'none', 'important');
1414 } else {
1415 style.display = 'none';
1416 }
1417 }
1418
1419 export function hideTextInstance(textInstance: TextInstance): void {
1420 textInstance.nodeValue = '';
1421 }
1422
1423 export function unhideDehydratedBoundary(
1424 dehydratedInstance: SuspenseInstance | ActivityInstance,
1425 ): void {
1426 hideOrUnhideDehydratedBoundary(dehydratedInstance, false);
1427 }
1428
1429 export function unhideInstance(instance: Instance, props: Props): void {
1430 instance = instance as any as HTMLElement;
1431 const styleProp = props[STYLE];
1432 const display =
1433 styleProp !== undefined &&
1434 // $FlowFixMe[invalid-compare]
1435 styleProp !== null &&
1436 styleProp.hasOwnProperty('display')
1437 ? styleProp.display
1438 : null;
1439 instance.style.display =
1440 display == null || typeof display === 'boolean'
1441 ? ''
1442 : // The value would've errored already if it wasn't safe.
1443 // eslint-disable-next-line react-internal/safe-string-coercion
1444 ('' + display).trim();
1445 }
1446
1447 export function unhideTextInstance(
1448 textInstance: TextInstance,
1449 text: string,
1450 ): void {
1451 textInstance.nodeValue = text;
1452 }
1453
1454 function warnForBlockInsideInline(instance: HTMLElement) {
1455 if (__DEV__) {
1456 let nextNode = instance.firstChild;
1457 outer: while (nextNode != null) {
1458 let node: Node = nextNode;
1459 if (
1460 node.nodeType === ELEMENT_NODE &&
1461 getComputedStyle(node as any).display === 'block'
1462 ) {
1463 const fiber =
1464 getInstanceFromNode(node) || getInstanceFromNode(instance);
1465 runWithFiberInDEV(
1466 fiber,
1467 (parentTag: string, childTag: string) => {
1468 console.error(
1469 "You're about to start a <ViewTransition> around a display: inline " +
1470 'element <%s>, which itself has a display: block element <%s> inside it. ' +
1471 'This might trigger a bug in Safari which causes the View Transition to ' +
1472 'be skipped with a duplicate name error.\n' +
1473 'https://bugs.webkit.org/show_bug.cgi?id=290923',
1474 parentTag.toLocaleLowerCase(),
1475 childTag.toLocaleLowerCase(),
1476 );
1477 },
1478 instance.tagName,
1479 (node as any).tagName,
1480 );
1481 break;
1482 }
1483 if (node.firstChild != null) {
1484 nextNode = node.firstChild;
1485 continue;
1486 }
1487 if (node === instance) {
1488 break;
1489 }
1490 while (node.nextSibling == null) {
1491 if (node.parentNode == null || node.parentNode === instance) {
1492 break;
1493 }
1494 node = node.parentNode;
1495 }
1496 nextNode = node.nextSibling;
1497 }
1498 }
1499 }
1500
1501 function countClientRects(rects: Array<ClientRect>): number {
1502 if (rects.length === 1) {
1503 return 1;
1504 }
1505 // Count non-zero rects.
1506 let count = 0;
1507 for (let i = 0; i < rects.length; i++) {
1508 const rect = rects[i];
1509 if (rect.width > 0 && rect.height > 0) {
1510 count++;
1511 }
1512 }
1513 return count;
1514 }
1515
1516 export function applyViewTransitionName(
1517 instance: Instance,
1518 name: string,
1519 className: ?string,
1520 ): void {
1521 instance = instance as any as HTMLElement;
1522 // If the name isn't valid CSS identifier, base64 encode the name instead.
1523 // This doesn't let you select it in custom CSS selectors but it does work in current
1524 // browsers.
1525 const escapedName =
1526 CSS.escape(name) !== name ? 'r-' + btoa(name).replace(/=/g, '') : name;
1527 // $FlowFixMe[prop-missing]
1528 instance.style.viewTransitionName = escapedName;
1529 if (className != null) {
1530 // $FlowFixMe[prop-missing]
1531 instance.style.viewTransitionClass = className;
1532 }
1533 const computedStyle = getComputedStyle(instance);
1534 if (computedStyle.display === 'inline') {
1535 // WebKit has a bug where assigning a name to display: inline elements errors
1536 // if they have display: block children. We try to work around this bug in the
1537 // simple case by converting it automatically to display: inline-block.
1538 // https://bugs.webkit.org/show_bug.cgi?id=290923
1539 const rects = instance.getClientRects();
1540 if (
1541 // $FlowFixMe[incompatible-type]
1542 countClientRects(rects) === 1
1543 ) {
1544 // If the instance has a single client rect, that means that it can be
1545 // expressed as a display: inline-block or block.
1546 // This will cause layout thrash but we live with it since inline view transitions
1547 // are unusual.
1548 const style = instance.style;
1549 // If there's literally only one rect, then it's likely on a single line like an
1550 // inline-block. If it's multiple rects but all but one of them are empty it's
1551 // likely because it's a single block that caused a line break.
1552 style.display = rects.length === 1 ? 'inline-block' : 'block';
1553 // Margin doesn't apply to inline so should be zero. However, padding top/bottom
1554 // applies to inline-block positioning which we can offset by setting the margin
1555 // to the negative padding to get it back into original position.
1556 style.marginTop = '-' + computedStyle.paddingTop;
1557 style.marginBottom = '-' + computedStyle.paddingBottom;
1558 } else {
1559 // This case cannot be easily fixed if it has blocks but it's also fine if
1560 // it doesn't have blocks. So we only warn in DEV about this being an issue.
1561 warnForBlockInsideInline(instance);
1562 }
1563 }
1564 }
1565
1566 export function restoreViewTransitionName(
1567 instance: Instance,
1568 props: Props,
1569 ): void {
1570 instance = instance as any as HTMLElement;
1571 const style = instance.style;
1572 const styleProp = props[STYLE];
1573 const viewTransitionName =
1574 styleProp != null
1575 ? styleProp.hasOwnProperty('viewTransitionName')
1576 ? styleProp.viewTransitionName
1577 : styleProp.hasOwnProperty('view-transition-name')
1578 ? styleProp['view-transition-name']
1579 : null
1580 : null;
1581 // $FlowFixMe[prop-missing]
1582 style.viewTransitionName =
1583 viewTransitionName == null || typeof viewTransitionName === 'boolean'
1584 ? ''
1585 : // The value would've errored already if it wasn't safe.
1586 // eslint-disable-next-line react-internal/safe-string-coercion
1587 ('' + viewTransitionName).trim();
1588 const viewTransitionClass =
1589 styleProp != null
1590 ? styleProp.hasOwnProperty('viewTransitionClass')
1591 ? styleProp.viewTransitionClass
1592 : styleProp.hasOwnProperty('view-transition-class')
1593 ? styleProp['view-transition-class']
1594 : null
1595 : null;
1596 // $FlowFixMe[prop-missing]
1597 style.viewTransitionClass =
1598 viewTransitionClass == null || typeof viewTransitionClass === 'boolean'
1599 ? ''
1600 : // The value would've errored already if it wasn't safe.
1601 // eslint-disable-next-line react-internal/safe-string-coercion
1602 ('' + viewTransitionClass).trim();
1603 if (style.display === 'inline-block') {
1604 // We might have overridden the style. Reset it to what it should be.
1605 if (styleProp == null) {
1606 style.display = style.margin = '';
1607 } else {
1608 const display = styleProp.display;
1609 style.display =
1610 display == null || typeof display === 'boolean' ? '' : display;
1611 const margin = styleProp.margin;
1612 if (margin != null) {
1613 style.margin = margin;
1614 } else {
1615 const marginTop = styleProp.hasOwnProperty('marginTop')
1616 ? styleProp.marginTop
1617 : styleProp['margin-top'];
1618 style.marginTop =
1619 marginTop == null || typeof marginTop === 'boolean' ? '' : marginTop;
1620 const marginBottom = styleProp.hasOwnProperty('marginBottom')
1621 ? styleProp.marginBottom
1622 : styleProp['margin-bottom'];
1623 style.marginBottom =
1624 marginBottom == null || typeof marginBottom === 'boolean'
1625 ? ''
1626 : marginBottom;
1627 }
1628 }
1629 }
1630 }
1631
1632 export function cancelViewTransitionName(
1633 instance: Instance,
1634 oldName: string,
1635 props: Props,
1636 ): void {
1637 // To cancel the "new" state and paint this instance as part of the parent, all we have to do
1638 // is remove the view-transition-name before we exit startViewTransition.
1639 restoreViewTransitionName(instance, props);
1640 // There isn't a way to cancel an "old" state but what we can do is hide it by animating it.
1641 // Since it is already removed from the old state of the parent, this technique only works
1642 // if the parent also isn't transitioning. Therefore we should only cancel the root most
1643 // ViewTransitions.
1644 const documentElement = instance.ownerDocument.documentElement;
1645 if (documentElement !== null) {
1646 documentElement.animate(
1647 {opacity: [0, 0], pointerEvents: ['none', 'none']},
1648 // $FlowFixMe[incompatible-type]
1649 {
1650 duration: 0,
1651 fill: 'forwards',
1652 pseudoElement: '::view-transition-group(' + oldName + ')',
1653 },
1654 );
1655 }
1656 }
1657
1658 export function cancelRootViewTransitionName(rootContainer: Container): void {
1659 const documentElement: null | HTMLElement =
1660 rootContainer.nodeType === DOCUMENT_NODE
1661 ? (rootContainer as any).documentElement
1662 : rootContainer.ownerDocument.documentElement;
1663
1664 if (
1665 !disableCommentsAsDOMContainers &&
1666 rootContainer.nodeType === COMMENT_NODE
1667 ) {
1668 if (__DEV__) {
1669 console.warn(
1670 'Cannot cancel root view transition on a comment node. All view transitions will be globally scoped.',
1671 );
1672 }
1673 return;
1674 }
1675
1676 if (
1677 documentElement !== null &&
1678 // $FlowFixMe[prop-missing]
1679 documentElement.style.viewTransitionName === ''
1680 ) {
1681 // $FlowFixMe[prop-missing]
1682 documentElement.style.viewTransitionName = 'none';
1683 documentElement.animate(
1684 {opacity: [0, 0], pointerEvents: ['none', 'none']},
1685 // $FlowFixMe[incompatible-type]
1686 {
1687 duration: 0,
1688 fill: 'forwards',
1689 pseudoElement: '::view-transition-group(root)',
1690 },
1691 );
1692 // By default the root ::view-transition selector captures all pointer events,
1693 // which means nothing gets interactive. We want to let whatever is not animating
1694 // remain interactive during the transition. To do that, we set the size to nothing
1695 // so that the transition doesn't capture any clicks. We don't set pointer-events
1696 // on this one as that would apply to all running transitions. This lets animations
1697 // that are running to block clicks so that they don't end up incorrectly hitting
1698 // whatever is below the animation.
1699 documentElement.animate(
1700 {width: [0, 0], height: [0, 0]},
1701 // $FlowFixMe[incompatible-call]
1702 // $FlowFixMe[incompatible-type]
1703 {
1704 duration: 0,
1705 fill: 'forwards',
1706 pseudoElement: '::view-transition',
1707 },
1708 );
1709 }
1710 }
1711
1712 export function restoreRootViewTransitionName(rootContainer: Container): void {
1713 let containerInstance: Instance;
1714 if (rootContainer.nodeType === DOCUMENT_NODE) {
1715 containerInstance = (rootContainer as any).body;
1716 } else if (rootContainer.nodeName === 'HTML') {
1717 containerInstance = rootContainer.ownerDocument.body as any;
1718 } else {
1719 // If the container is not the whole document, then we ideally should probably
1720 // clone the whole document outside of the React too.
1721 containerInstance = rootContainer as any;
1722 }
1723 if (
1724 !disableCommentsAsDOMContainers &&
1725 containerInstance.nodeType === COMMENT_NODE
1726 ) {
1727 return;
1728 }
1729 if (
1730 // $FlowFixMe[prop-missing]
1731 containerInstance.style.viewTransitionName === 'root'
1732 ) {
1733 // If we moved the root view transition name to the container in a gesture
1734 // we need to restore it now.
1735 containerInstance.style.viewTransitionName = '';
1736 }
1737 const documentElement: null | HTMLElement =
1738 containerInstance.ownerDocument.documentElement;
1739 if (
1740 documentElement !== null &&
1741 // $FlowFixMe[prop-missing]
1742 documentElement.style.viewTransitionName === 'none'
1743 ) {
1744 // $FlowFixMe[prop-missing]
1745 documentElement.style.viewTransitionName = '';
1746 }
1747 }
1748
1749 function getComputedTransform(style: CSSStyleDeclaration): string {
1750 // Gets the merged transform of all the short hands.
1751 const computedStyle: any = style;
1752 let transform: string = computedStyle.transform;
1753 if (transform === 'none') {
1754 transform = '';
1755 }
1756 const scale: string = computedStyle.scale;
1757 if (scale !== 'none' && scale !== '') {
1758 const parts = scale.split(' ');
1759 transform =
1760 (parts.length === 3 ? 'scale3d' : 'scale') +
1761 '(' +
1762 parts.join(', ') +
1763 ') ' +
1764 transform;
1765 }
1766 const rotate: string = computedStyle.rotate;
1767 if (rotate !== 'none' && rotate !== '') {
1768 const parts = rotate.split(' ');
1769 if (parts.length === 1) {
1770 transform = 'rotate(' + parts[0] + ') ' + transform;
1771 } else if (parts.length === 2) {
1772 transform =
1773 'rotate' + parts[0].toUpperCase() + '(' + parts[1] + ') ' + transform;
1774 } else {
1775 transform = 'rotate3d(' + parts.join(', ') + ') ' + transform;
1776 }
1777 }
1778 const translate: string = computedStyle.translate;
1779 if (translate !== 'none' && translate !== '') {
1780 const parts = translate.split(' ');
1781 transform =
1782 (parts.length === 3 ? 'translate3d' : 'translate') +
1783 '(' +
1784 parts.join(', ') +
1785 ') ' +
1786 transform;
1787 }
1788 return transform;
1789 }
1790
1791 function moveOutOfViewport(
1792 originalStyle: CSSStyleDeclaration,
1793 element: HTMLElement,
1794 ): void {
1795 // Apply a transform that safely puts the whole element outside the viewport
1796 // while still letting it paint its "old" state to a snapshot.
1797 const transform = getComputedTransform(originalStyle);
1798 // Clear the long form properties.
1799 // $FlowFixMe[prop-missing]
1800 element.style.translate = 'none';
1801 // $FlowFixMe[prop-missing]
1802 element.style.scale = 'none';
1803 // $FlowFixMe[prop-missing]
1804 element.style.rotate = 'none';
1805 // Apply a translate to move it way out of the viewport. This is applied first
1806 // so that it is in the coordinate space of the parent and not after applying
1807 // other transforms. That's why we need to merge the long form properties.
1808 // TODO: Ideally we'd adjust for the parent's rotate/scale. Otherwise when
1809 // we move back the ::view-transition-group we might overshoot or undershoot.
1810 element.style.transform = 'translate(-20000px, -20000px) ' + transform;
1811 }
1812
1813 function moveOldFrameIntoViewport(keyframe: any): void {
1814 // In the resulting View Transition Animation, the first frame will be offset.
1815 const computedTransform: ?string = keyframe.transform;
1816 if (computedTransform != null) {
1817 let transform = computedTransform === 'none' ? '' : computedTransform;
1818 transform = 'translate(20000px, 20000px) ' + transform;
1819 keyframe.transform = transform;
1820 }
1821 }
1822
1823 export function cloneRootViewTransitionContainer(
1824 rootContainer: Container,
1825 ): Instance {
1826 // This implies that we're not going to animate the root document but instead
1827 // the clone so we first clear the name of the root container.
1828 const documentElement: null | HTMLElement =
1829 rootContainer.nodeType === DOCUMENT_NODE
1830 ? (rootContainer as any).documentElement
1831 : rootContainer.ownerDocument.documentElement;
1832 if (
1833 documentElement !== null &&
1834 // $FlowFixMe[prop-missing]
1835 documentElement.style.viewTransitionName === ''
1836 ) {
1837 // $FlowFixMe[prop-missing]
1838 documentElement.style.viewTransitionName = 'none';
1839 }
1840
1841 let containerInstance: HTMLElement;
1842 if (rootContainer.nodeType === DOCUMENT_NODE) {
1843 containerInstance = (rootContainer as any).body;
1844 } else if (rootContainer.nodeName === 'HTML') {
1845 containerInstance = rootContainer.ownerDocument.body as any;
1846 } else if (
1847 !disableCommentsAsDOMContainers &&
1848 rootContainer.nodeType === COMMENT_NODE
1849 ) {
1850 throw new Error(
1851 'Cannot use a startGestureTransition() with a comment node root.',
1852 );
1853 } else {
1854 // If the container is not the whole document, then we ideally should probably
1855 // clone the whole document outside of the React too.
1856 containerInstance = rootContainer as any;
1857 }
1858
1859 const containerParent = containerInstance.parentNode;
1860 if (containerParent === null) {
1861 throw new Error(
1862 'Cannot use a startGestureTransition() on a detached root.',
1863 );
1864 }
1865
1866 const clone: HTMLElement = containerInstance.cloneNode(false);
1867
1868 const computedStyle = getComputedStyle(containerInstance);
1869
1870 if (
1871 computedStyle.position === 'absolute' ||
1872 computedStyle.position === 'fixed'
1873 ) {
1874 // If the style is already absolute, we don't have to do anything because it'll appear
1875 // in the same place.
1876 } else {
1877 // Otherwise we need to absolutely position the clone in the same location as the original.
1878 let positionedAncestor: HTMLElement = containerParent;
1879 while (
1880 positionedAncestor.parentNode != null &&
1881 positionedAncestor.parentNode.nodeType !== DOCUMENT_NODE
1882 ) {
1883 if (getComputedStyle(positionedAncestor).position !== 'static') {
1884 break;
1885 }
1886 // $FlowFixMe[incompatible-type]: This is refined.
1887 positionedAncestor = positionedAncestor.parentNode;
1888 }
1889
1890 const positionedAncestorStyle: any = positionedAncestor.style;
1891 const containerInstanceStyle: any = containerInstance.style;
1892 // Clear the transform while we're measuring since it affects the bounding client rect.
1893 const prevAncestorTranslate = positionedAncestorStyle.translate;
1894 const prevAncestorScale = positionedAncestorStyle.scale;
1895 const prevAncestorRotate = positionedAncestorStyle.rotate;
1896 const prevAncestorTransform = positionedAncestorStyle.transform;
1897 const prevTranslate = containerInstanceStyle.translate;
1898 const prevScale = containerInstanceStyle.scale;
1899 const prevRotate = containerInstanceStyle.rotate;
1900 const prevTransform = containerInstanceStyle.transform;
1901 positionedAncestorStyle.translate = 'none';
1902 positionedAncestorStyle.scale = 'none';
1903 positionedAncestorStyle.rotate = 'none';
1904 positionedAncestorStyle.transform = 'none';
1905 containerInstanceStyle.translate = 'none';
1906 containerInstanceStyle.scale = 'none';
1907 containerInstanceStyle.rotate = 'none';
1908 containerInstanceStyle.transform = 'none';
1909
1910 const ancestorRect = positionedAncestor.getBoundingClientRect();
1911 const rect = containerInstance.getBoundingClientRect();
1912
1913 const cloneStyle = clone.style;
1914 cloneStyle.position = 'absolute';
1915 cloneStyle.top = rect.top - ancestorRect.top + 'px';
1916 cloneStyle.left = rect.left - ancestorRect.left + 'px';
1917 cloneStyle.width = rect.width + 'px';
1918 cloneStyle.height = rect.height + 'px';
1919 cloneStyle.margin = '0px';
1920 cloneStyle.boxSizing = 'border-box';
1921
1922 positionedAncestorStyle.translate = prevAncestorTranslate;
1923 positionedAncestorStyle.scale = prevAncestorScale;
1924 positionedAncestorStyle.rotate = prevAncestorRotate;
1925 positionedAncestorStyle.transform = prevAncestorTransform;
1926 containerInstanceStyle.translate = prevTranslate;
1927 containerInstanceStyle.scale = prevScale;
1928 containerInstanceStyle.rotate = prevRotate;
1929 containerInstanceStyle.transform = prevTransform;
1930 }
1931
1932 // For this transition the container will act as the root. Nothing outside of it should
1933 // be affected anyway. This lets us transition from the cloned container to the original.
1934 // $FlowFixMe[prop-missing]
1935 clone.style.viewTransitionName = 'root';
1936
1937 // Move out of the viewport so that it's still painted for the snapshot but is not visible
1938 // for the frame where the snapshot happens.
1939 moveOutOfViewport(computedStyle, clone);
1940
1941 // Insert the clone after the root container as a sibling. This may inject a body
1942 // as the next sibling of an existing body. document.body will still point to the
1943 // first one and any id selectors will still find the first one. That's why it's
1944 // important that it's after the existing node.
1945 containerInstance.parentNode.insertBefore(
1946 clone,
1947 containerInstance.nextSibling,
1948 );
1949
1950 return clone;
1951 }
1952
1953 export function removeRootViewTransitionClone(
1954 rootContainer: Container,
1955 clone: Instance,
1956 ): void {
1957 let containerInstance: Instance;
1958 if (rootContainer.nodeType === DOCUMENT_NODE) {
1959 containerInstance = (rootContainer as any).body;
1960 } else if (rootContainer.nodeName === 'HTML') {
1961 containerInstance = rootContainer.ownerDocument.body as any;
1962 } else {
1963 // If the container is not the whole document, then we ideally should probably
1964 // clone the whole document outside of the React too.
1965 containerInstance = rootContainer as any;
1966 }
1967 const containerParent = containerInstance.parentNode;
1968 if (containerParent === null) {
1969 throw new Error(
1970 'Cannot use a startGestureTransition() on a detached root.',
1971 );
1972 }
1973 // We assume that the clone is still within the same parent.
1974 containerParent.removeChild(clone);
1975
1976 // Now the root is on the containerInstance itself until we call restoreRootViewTransitionName.
1977 containerInstance.style.viewTransitionName = 'root';
1978 }
1979
1980 export type InstanceMeasurement = {
1981 rect: ClientRect | DOMRect,
1982 abs: boolean, // is absolutely positioned
1983 clip: boolean, // is a clipping parent
1984 view: boolean, // is in viewport bounds
1985 };
1986
1987 function createMeasurement(
1988 rect: ClientRect | DOMRect,
1989 computedStyle: CSSStyleDeclaration,
1990 element: Element,
1991 ): InstanceMeasurement {
1992 const ownerWindow = element.ownerDocument.defaultView;
1993 return {
1994 rect: rect,
1995 abs:
1996 // Absolutely positioned instances don't contribute their size to the parent.
1997 computedStyle.position === 'absolute' ||
1998 computedStyle.position === 'fixed',
1999 clip:
2000 // If a ViewTransition boundary acts as a clipping parent group we should
2001 // always mark it to animate if its children do so that we can clip them.
2002 // This doesn't actually have any effect yet until browsers implement
2003 // layered capture and nested view transitions.
2004 computedStyle.clipPath !== 'none' ||
2005 computedStyle.overflow !== 'visible' ||
2006 computedStyle.filter !== 'none' ||
2007 computedStyle.mask !== 'none' ||
2008 computedStyle.mask !== 'none' ||
2009 computedStyle.borderRadius !== '0px',
2010 view:
2011 // If the instance was within the bounds of the viewport. We don't care as
2012 // much about if it was fully occluded because then it can still pop out.
2013 rect.bottom >= 0 &&
2014 rect.right >= 0 &&
2015 rect.top <= ownerWindow.innerHeight &&
2016 rect.left <= ownerWindow.innerWidth,
2017 };
2018 }
2019
2020 export function measureInstance(instance: Instance): InstanceMeasurement {
2021 const rect = instance.getBoundingClientRect();
2022 const computedStyle = getComputedStyle(instance);
2023 return createMeasurement(rect, computedStyle, instance);
2024 }
2025
2026 export function measureClonedInstance(instance: Instance): InstanceMeasurement {
2027 const measuredRect = instance.getBoundingClientRect();
2028 // Adjust the DOMRect based on the translate that put it outside the viewport.
2029 // TODO: This might not be completely correct if the parent also has a transform.
2030 const rect = new DOMRect(
2031 measuredRect.x + 20000,
2032 measuredRect.y + 20000,
2033 measuredRect.width,
2034 measuredRect.height,
2035 );
2036 const computedStyle = getComputedStyle(instance);
2037 return createMeasurement(rect, computedStyle, instance);
2038 }
2039
2040 export function wasInstanceInViewport(
2041 measurement: InstanceMeasurement,
2042 ): boolean {
2043 return measurement.view;
2044 }
2045
2046 export function hasInstanceChanged(
2047 oldMeasurement: InstanceMeasurement,
2048 newMeasurement: InstanceMeasurement,
2049 ): boolean {
2050 // Note: This is not guaranteed from the same instance in the case that the Instance of the
2051 // ViewTransition swaps out but it's still the same ViewTransition instance.
2052 if (newMeasurement.clip) {
2053 // If we're a clipping parent, we always animate if any of our children do so that we can clip
2054 // them. This doesn't yet until browsers implement layered capture and nested view transitions.
2055 return true;
2056 }
2057 const oldRect = oldMeasurement.rect;
2058 const newRect = newMeasurement.rect;
2059 return (
2060 oldRect.y !== newRect.y ||
2061 oldRect.x !== newRect.x ||
2062 oldRect.height !== newRect.height ||
2063 oldRect.width !== newRect.width
2064 );
2065 }
2066
2067 export function hasInstanceAffectedParent(
2068 oldMeasurement: InstanceMeasurement,
2069 newMeasurement: InstanceMeasurement,
2070 ): boolean {
2071 // Note: This is not guaranteed from the same instance in the case that the Instance of the
2072 // ViewTransition swaps out but it's still the same ViewTransition instance.
2073 // If the instance has resized, it might have affected the parent layout.
2074 if (newMeasurement.abs) {
2075 // Absolutely positioned elements don't affect the parent layout, unless they
2076 // previously were not absolutely positioned.
2077 return !oldMeasurement.abs;
2078 }
2079 const oldRect = oldMeasurement.rect;
2080 const newRect = newMeasurement.rect;
2081 return oldRect.height !== newRect.height || oldRect.width !== newRect.width;
2082 }
2083
2084 // How long to wait for new fonts to load before just committing anyway.
2085 // This freezes the screen. It needs to be short enough that it doesn't cause too much of
2086 // an issue when it's a new load and slow, yet long enough that you have a chance to load
2087 // it. Otherwise we wait for no reason. The assumption here is that you likely have
2088 // either cached the font or preloaded it earlier.
2089 // This timeout is also used for Suspensey Images when they're blocking a View Transition.
2090 const SUSPENSEY_FONT_AND_IMAGE_TIMEOUT = 500;
2091
2092 function customizeViewTransitionError(
2093 error: Object,
2094 ignoreAbort: boolean,
2095 ): mixed {
2096 if (typeof error === 'object' && error !== null) {
2097 switch (error.name) {
2098 case 'TimeoutError': {
2099 // We assume that the only reason a Timeout can happen is because the Navigation
2100 // promise. We expect any other work to either be fast or have a timeout (fonts).
2101 if (__DEV__) {
2102 // eslint-disable-next-line react-internal/prod-error-codes
2103 return new Error(
2104 'A ViewTransition timed out because a Navigation stalled. ' +
2105 'This can happen if a Navigation is blocked on React itself. ' +
2106 "Such as if it's resolved inside useEffect. " +
2107 'This can be solved by moving the resolution to useLayoutEffect.',
2108 {cause: error},
2109 );
2110 }
2111 break;
2112 }
2113 case 'AbortError': {
2114 if (ignoreAbort) {
2115 return null;
2116 }
2117 if (__DEV__) {
2118 // eslint-disable-next-line react-internal/prod-error-codes
2119 return new Error(
2120 'A ViewTransition was aborted early. This might be because you have ' +
2121 'other View Transition libraries on the page and only one can run at ' +
2122 "a time. To avoid this, use only React's built-in <ViewTransition> " +
2123 'to coordinate.',
2124 {cause: error},
2125 );
2126 }
2127 break;
2128 }
2129 case 'InvalidStateError': {
2130 if (
2131 error.message ===
2132 'View transition was skipped because document visibility state is hidden.' ||
2133 error.message ===
2134 'Skipping view transition because document visibility state has become hidden.' ||
2135 error.message ===
2136 'Skipping view transition because viewport size changed.' ||
2137 // Chrome uses a generic error message instead of specific reasons. It will log a
2138 // more specific reason in the console but the user might not look there.
2139 // Some of these errors are important to surface like duplicate name errors but
2140 // it's too noisy for unactionable cases like the document was hidden. Therefore,
2141 // we hide all of them and hopefully it surfaces in another browser.
2142 error.message === 'Transition was aborted because of invalid state'
2143 ) {
2144 // Skip logging this. This is not considered an error.
2145 return null;
2146 }
2147 break;
2148 }
2149 }
2150 }
2151 return error;
2152 }
2153
2154 /** @noinline */
2155 function forceLayout(ownerDocument: Document) {
2156 // This function exists to trick minifiers to not remove this unused member expression.
2157 return (ownerDocument.documentElement as any).clientHeight;
2158 }
2159
2160 function waitForImageToLoad(this: HTMLImageElement, resolve: () => void) {
2161 // TODO: Use decode() instead of the load event here once the fix in
2162 // https://issues.chromium.org/issues/420748301 has propagated fully.
2163 this.addEventListener('load', resolve);
2164 this.addEventListener('error', resolve);
2165 }
2166
2167 export function startViewTransition(
2168 suspendedState: null | SuspendedState,
2169 rootContainer: Container,
2170 transitionTypes: null | TransitionTypes,
2171 mutationCallback: () => void,
2172 layoutCallback: () => void,
2173 afterMutationCallback: () => void,
2174 spawnedWorkCallback: () => void,
2175 passiveCallback: () => mixed,
2176 errorCallback: mixed => void,
2177 blockedCallback: string => void, // Profiling-only
2178 finishedAnimation: () => void, // Profiling-only
2179 ): null | RunningViewTransition {
2180 const ownerDocument: Document =
2181 rootContainer.nodeType === DOCUMENT_NODE
2182 ? (rootContainer as any)
2183 : rootContainer.ownerDocument;
2184 try {
2185 // $FlowFixMe[prop-missing]
2186 const transition = ownerDocument.startViewTransition({
2187 update() {
2188 // Note: We read the existence of a pending navigation before we apply the
2189 // mutations. That way we're not waiting on a navigation that we spawned
2190 // from this update. Only navigations that started before this commit.
2191 const ownerWindow = ownerDocument.defaultView;
2192 const pendingNavigation =
2193 ownerWindow.navigation && ownerWindow.navigation.transition;
2194 // $FlowFixMe[prop-missing]
2195 const previousFontLoadingStatus = ownerDocument.fonts.status;
2196 mutationCallback();
2197 const blockingPromises: Array<Promise<any>> = [];
2198 if (previousFontLoadingStatus === 'loaded') {
2199 // Force layout calculation to trigger font loading.
2200 forceLayout(ownerDocument);
2201 if (
2202 // $FlowFixMe[prop-missing]
2203 ownerDocument.fonts.status === 'loading'
2204 ) {
2205 // The mutation lead to new fonts being loaded. We should wait on them before continuing.
2206 // This avoids waiting for potentially unrelated fonts that were already loading before.
2207 // Either in an earlier transition or as part of a sync optimistic state. This doesn't
2208 // include preloads that happened earlier.
2209 blockingPromises.push(ownerDocument.fonts.ready);
2210 }
2211 }
2212 const blockingIndexSnapshot = blockingPromises.length;
2213 if (suspendedState !== null) {
2214 // Suspend on any images that still haven't loaded and are in the viewport.
2215 const suspenseyImages = suspendedState.suspenseyImages;
2216 let imgBytes = 0;
2217 for (let i = 0; i < suspenseyImages.length; i++) {
2218 const suspenseyImage = suspenseyImages[i];
2219 if (!suspenseyImage.complete) {
2220 const rect = suspenseyImage.getBoundingClientRect();
2221 const inViewport =
2222 rect.bottom > 0 &&
2223 rect.right > 0 &&
2224 rect.top < ownerWindow.innerHeight &&
2225 rect.left < ownerWindow.innerWidth;
2226 if (inViewport) {
2227 imgBytes += estimateImageBytes(suspenseyImage);
2228 if (imgBytes > estimatedBytesWithinLimit) {
2229 // We don't think we'll be able to download all the images within
2230 // the timeout. Give up. Rewind to only block on fonts, if any.
2231 blockingPromises.length = blockingIndexSnapshot;
2232 break;
2233 }
2234 const loadingImage = new Promise(
2235 waitForImageToLoad.bind(suspenseyImage),
2236 );
2237 blockingPromises.push(loadingImage);
2238 }
2239 }
2240 }
2241 }
2242 if (blockingPromises.length > 0) {
2243 if (enableProfilerTimer) {
2244 const blockedReason =
2245 blockingIndexSnapshot > 0
2246 ? blockingPromises.length > blockingIndexSnapshot
2247 ? 'Waiting on Fonts and Images'
2248 : 'Waiting on Fonts'
2249 : 'Waiting on Images';
2250 blockedCallback(blockedReason);
2251 }
2252 const blockingReady = Promise.race([
2253 Promise.all(blockingPromises),
2254 new Promise(resolve =>
2255 setTimeout(resolve, SUSPENSEY_FONT_AND_IMAGE_TIMEOUT),
2256 ),
2257 ]).then(layoutCallback, layoutCallback);
2258 const allReady = pendingNavigation
2259 ? Promise.allSettled([pendingNavigation.finished, blockingReady])
2260 : blockingReady;
2261 return allReady.then(afterMutationCallback, afterMutationCallback);
2262 }
2263 layoutCallback();
2264 if (pendingNavigation) {
2265 return pendingNavigation.finished.then(
2266 afterMutationCallback,
2267 afterMutationCallback,
2268 );
2269 } else {
2270 afterMutationCallback();
2271 }
2272 },
2273 types: transitionTypes,
2274 });
2275 // $FlowFixMe[prop-missing]
2276 ownerDocument.__reactViewTransition = transition;
2277
2278 const viewTransitionAnimations: Array<Animation> = [];
2279
2280 const readyCallback = () => {
2281 const documentElement: Element = ownerDocument.documentElement as any;
2282 // Loop through all View Transition Animations.
2283 // $FlowFixMe[prop-missing]
2284 // $FlowFixMe[incompatible-type]
2285 const animations = documentElement.getAnimations({subtree: true});
2286 for (let i = 0; i < animations.length; i++) {
2287 const animation = animations[i];
2288 const effect: KeyframeEffect = animation.effect as any;
2289 // $FlowFixMe[prop-missing]
2290 const pseudoElement: ?string = effect.pseudoElement;
2291 if (
2292 pseudoElement != null &&
2293 pseudoElement.startsWith('::view-transition')
2294 ) {
2295 viewTransitionAnimations.push(animation);
2296 const keyframes = effect.getKeyframes();
2297 // Next, we're going to try to optimize this animation in case the auto-generated
2298 // width/height keyframes are unnecessary.
2299 let width;
2300 let height;
2301 let unchangedDimensions = true;
2302 for (let j = 0; j < keyframes.length; j++) {
2303 const keyframe = keyframes[j];
2304 const w = keyframe.width;
2305 if (width === undefined) {
2306 width = w;
2307 } else if (width !== w) {
2308 unchangedDimensions = false;
2309 break;
2310 }
2311 const h = keyframe.height;
2312 if (height === undefined) {
2313 height = h;
2314 } else if (height !== h) {
2315 unchangedDimensions = false;
2316 break;
2317 }
2318 // We're clearing the keyframes in case we are going to apply the optimization.
2319 delete keyframe.width;
2320 delete keyframe.height;
2321 if (keyframe.transform === 'none') {
2322 delete keyframe.transform;
2323 }
2324 }
2325 if (
2326 unchangedDimensions &&
2327 width !== undefined &&
2328 height !== undefined
2329 ) {
2330 // Replace the keyframes with ones that don't animate the width/height.
2331 // $FlowFixMe[incompatible-type]
2332 effect.setKeyframes(keyframes);
2333 // Read back the new animation to see what the underlying width/height of the pseudo-element was.
2334 const computedStyle = getComputedStyle(
2335 // $FlowFixMe[incompatible-type]
2336 effect.target,
2337 // $FlowFixMe[prop-missing]
2338 effect.pseudoElement,
2339 );
2340 if (
2341 computedStyle.width !== width ||
2342 computedStyle.height !== height
2343 ) {
2344 // Oops. Turns out that the pseudo-element had a different width/height so we need to let it
2345 // be overridden. Add it back.
2346 const first = keyframes[0];
2347 first.width = width;
2348 first.height = height;
2349 const last = keyframes[keyframes.length - 1];
2350 last.width = width;
2351 last.height = height;
2352 // $FlowFixMe[incompatible-type]
2353 effect.setKeyframes(keyframes);
2354 }
2355 }
2356 }
2357 }
2358 spawnedWorkCallback();
2359 };
2360 const handleError = (error: mixed) => {
2361 // $FlowFixMe[prop-missing]
2362 if (ownerDocument.__reactViewTransition === transition) {
2363 // $FlowFixMe[prop-missing]
2364 ownerDocument.__reactViewTransition = null;
2365 }
2366 try {
2367 error = customizeViewTransitionError(error, false);
2368 if (error !== null) {
2369 errorCallback(error);
2370 }
2371 } finally {
2372 // Continue the reset of the work.
2373 // If the error happened in the snapshot phase before the update callback
2374 // was invoked, then we need to first finish the mutation and layout phases.
2375 // If they're already invoked it's still safe to call them due the status check.
2376 mutationCallback();
2377 layoutCallback();
2378 // Skip afterMutationCallback() since we're not animating.
2379 spawnedWorkCallback();
2380 if (enableProfilerTimer) {
2381 finishedAnimation();
2382 }
2383 }
2384 };
2385 transition.ready.then(readyCallback, handleError);
2386 transition.finished.finally(() => {
2387 for (let i = 0; i < viewTransitionAnimations.length; i++) {
2388 // In Safari, we need to manually cancel all manually started animations
2389 // or it'll block or interfer with future transitions.
2390 // We can't use getAnimations() due to #35336 so we collect them in an array.
2391 viewTransitionAnimations[i].cancel();
2392 }
2393 // $FlowFixMe[prop-missing]
2394 if (ownerDocument.__reactViewTransition === transition) {
2395 // $FlowFixMe[prop-missing]
2396 ownerDocument.__reactViewTransition = null;
2397 }
2398 if (enableProfilerTimer) {
2399 finishedAnimation();
2400 }
2401 passiveCallback();
2402 });
2403 return transition;
2404 } catch (x) {
2405 // We use the error as feature detection.
2406 // The only thing that should throw is if startViewTransition is missing
2407 // or if it doesn't accept the object form. Other errors are async.
2408 // I.e. it's before the View Transitions v2 spec. We only support View
2409 // Transitions v2 otherwise we fallback to not animating to ensure that
2410 // we're not animating with the wrong animation mapped.
2411 // Flush remaining work synchronously.
2412 mutationCallback();
2413 layoutCallback();
2414 // Skip afterMutationCallback(). We don't need it since we're not animating.
2415 if (enableProfilerTimer) {
2416 finishedAnimation();
2417 }
2418 spawnedWorkCallback();
2419 // Skip passiveCallback(). Spawned work will schedule a task.
2420 return null;
2421 }
2422 }
2423
2424 export type RunningViewTransition = {
2425 skipTransition(): void,
2426 finished: Promise<void>,
2427 ...
2428 };
2429
2430 function mergeTranslate(translateA: ?string, translateB: ?string): string {
2431 if (!translateA || translateA === 'none') {
2432 return translateB || '';
2433 }
2434 if (!translateB || translateB === 'none') {
2435 // $FlowFixMe[constant-condition]
2436 return translateA || '';
2437 }
2438 const partsA = translateA.split(' ');
2439 const partsB = translateB.split(' ');
2440 let i;
2441 let result = '';
2442 for (i = 0; i < partsA.length && i < partsB.length; i++) {
2443 if (i > 0) {
2444 result += ' ';
2445 }
2446 result += 'calc(' + partsA[i] + ' + ' + partsB[i] + ')';
2447 }
2448 for (; i < partsA.length; i++) {
2449 result += ' ' + partsA[i];
2450 }
2451 for (; i < partsB.length; i++) {
2452 result += ' ' + partsB[i];
2453 }
2454 return result;
2455 }
2456
2457 function animateGesture(
2458 keyframes: any,
2459 targetElement: Element,
2460 pseudoElement: string,
2461 timeline: GestureTimeline,
2462 viewTransitionAnimations: Array<Animation>,
2463 customTimelineCleanup: Array<() => void>,
2464 rangeStart: number,
2465 rangeEnd: number,
2466 moveFirstFrameIntoViewport: boolean,
2467 moveAllFramesIntoViewport: boolean,
2468 ) {
2469 let width;
2470 let height;
2471 let unchangedDimensions = true;
2472 for (let i = 0; i < keyframes.length; i++) {
2473 const keyframe = keyframes[i];
2474 // Delete any easing since we always apply linear easing to gestures.
2475 delete keyframe.easing;
2476 delete keyframe.computedOffset;
2477 const w = keyframe.width;
2478 if (width === undefined) {
2479 width = w;
2480 } else if (width !== w) {
2481 unchangedDimensions = false;
2482 }
2483 const h = keyframe.height;
2484 if (height === undefined) {
2485 height = h;
2486 } else if (height !== h) {
2487 unchangedDimensions = false;
2488 }
2489 // Chrome returns "auto" for width/height which is not a valid value to
2490 // animate to. Similarly, transform: "none" is actually lack of transform.
2491 if (keyframe.width === 'auto') {
2492 delete keyframe.width;
2493 }
2494 if (keyframe.height === 'auto') {
2495 delete keyframe.height;
2496 }
2497 if (keyframe.transform === 'none') {
2498 delete keyframe.transform;
2499 }
2500 if (moveAllFramesIntoViewport) {
2501 if (keyframe.transform == null) {
2502 // If a transform is not explicitly specified to override the auto
2503 // generated one on the pseudo element, then we need to adjust it to
2504 // put it back into the viewport. We don't know the offset relative to
2505 // the screen so instead we use the translate prop to do a relative
2506 // adjustment.
2507 // TODO: If the "transform" was manually overridden on the pseudo
2508 // element itself and no longer the auto generated one, then we shouldn't
2509 // adjust it. I'm not sure how to detect this.
2510 if (keyframe.translate == null || keyframe.translate === '') {
2511 // TODO: If there's a CSS rule targeting translate on the pseudo element
2512 // already we need to merge it.
2513 const elementTranslate: ?string = (
2514 getComputedStyle(targetElement, pseudoElement) as any
2515 ).translate;
2516 keyframe.translate = mergeTranslate(
2517 elementTranslate,
2518 '20000px 20000px',
2519 );
2520 } else {
2521 keyframe.translate = mergeTranslate(
2522 keyframe.translate,
2523 '20000px 20000px',
2524 );
2525 }
2526 }
2527 }
2528 }
2529 if (moveFirstFrameIntoViewport) {
2530 // If this is the generated animation that does a FLIP matrix translation
2531 // from the old position, we need to adjust it from the out of viewport
2532 // position. If this is going from old to new it only applies to first
2533 // keyframe. Otherwise it applies to every keyframe.
2534 moveOldFrameIntoViewport(keyframes[0]);
2535 }
2536 if (unchangedDimensions && width !== undefined && height !== undefined) {
2537 // Read the underlying width/height of the pseudo-element. The previous animation
2538 // should have already been cancelled so we should observe the underlying element.
2539 const computedStyle = getComputedStyle(targetElement, pseudoElement);
2540 if (computedStyle.width === width && computedStyle.height === height) {
2541 for (let i = 0; i < keyframes.length; i++) {
2542 const keyframe = keyframes[i];
2543 delete keyframe.width;
2544 delete keyframe.height;
2545 }
2546 }
2547 }
2548
2549 // TODO: Reverse the reverse if the original direction is reverse.
2550 const reverse = rangeStart > rangeEnd;
2551 if (timeline instanceof AnimationTimeline) {
2552 // Native Timeline
2553 // $FlowFixMe[incompatible-type]
2554 const animation = targetElement.animate(keyframes, {
2555 pseudoElement: pseudoElement,
2556 // Set the timeline to the current gesture timeline to drive the updates.
2557 timeline: timeline,
2558 // We reset all easing functions to linear so that it feels like you
2559 // have direct impact on the transition and to avoid double bouncing
2560 // from scroll bouncing.
2561 easing: 'linear',
2562 // We fill in both direction for overscroll.
2563 fill: 'both', // TODO: Should we preserve the fill instead?
2564 // We play all gestures in reverse, except if we're in reverse direction
2565 // in which case we need to play it in reverse of the reverse.
2566 direction: reverse ? 'normal' : 'reverse',
2567 // Range start needs to be higher than range end. If it goes in reverse
2568 // we reverse the whole animation below.
2569 rangeStart: (reverse ? rangeEnd : rangeStart) + '%',
2570 rangeEnd: (reverse ? rangeStart : rangeEnd) + '%',
2571 });
2572 viewTransitionAnimations.push(animation);
2573 } else {
2574 // Custom Timeline
2575 // $FlowFixMe[incompatible-type]
2576 const animation = targetElement.animate(keyframes, {
2577 pseudoElement: pseudoElement,
2578 // We reset all easing functions to linear so that it feels like you
2579 // have direct impact on the transition and to avoid double bouncing
2580 // from scroll bouncing.
2581 easing: 'linear',
2582 // We fill in both direction for overscroll.
2583 fill: 'both', // TODO: Should we preserve the fill instead?
2584 // We play all gestures in reverse, except if we're in reverse direction
2585 // in which case we need to play it in reverse of the reverse.
2586 direction: reverse ? 'normal' : 'reverse',
2587 // We set the delay and duration to represent the span of the range.
2588 delay: reverse ? rangeEnd : rangeStart,
2589 duration: reverse ? rangeStart - rangeEnd : rangeEnd - rangeStart,
2590 });
2591 viewTransitionAnimations.push(animation);
2592 // Let the custom timeline take control of driving the animation.
2593 const cleanup = timeline.animate(animation);
2594 if (cleanup) {
2595 customTimelineCleanup.push(cleanup);
2596 }
2597 }
2598 }
2599
2600 export function startGestureTransition(
2601 suspendedState: null | SuspendedState,
2602 rootContainer: Container,
2603 timeline: GestureTimeline,
2604 rangeStart: number,
2605 rangeEnd: number,
2606 transitionTypes: null | TransitionTypes,
2607 mutationCallback: () => void,
2608 animateCallback: () => void,
2609 errorCallback: mixed => void,
2610 finishedAnimation: () => void, // Profiling-only
2611 ): null | RunningViewTransition {
2612 const ownerDocument: Document =
2613 rootContainer.nodeType === DOCUMENT_NODE
2614 ? (rootContainer as any)
2615 : rootContainer.ownerDocument;
2616 try {
2617 // Force layout before we start the Transition. This works around a bug in Safari
2618 // if one of the clones end up being a stylesheet that isn't loaded or uncached.
2619 // https://bugs.webkit.org/show_bug.cgi?id=290146
2620 forceLayout(ownerDocument);
2621 // $FlowFixMe[prop-missing]
2622 const transition = ownerDocument.startViewTransition({
2623 update: mutationCallback,
2624 types: transitionTypes,
2625 });
2626 // $FlowFixMe[prop-missing]
2627 ownerDocument.__reactViewTransition = transition;
2628 const customTimelineCleanup: Array<() => void> = []; // Cleanup Animations started in a CustomTimeline
2629 const viewTransitionAnimations: Array<Animation> = [];
2630 const readyCallback = () => {
2631 const documentElement: Element = ownerDocument.documentElement as any;
2632 // Loop through all View Transition Animations.
2633 // $FlowFixMe[prop-missing]
2634 // $FlowFixMe[incompatible-type]
2635 const animations = documentElement.getAnimations({subtree: true});
2636 // First do a pass to collect all known group and new items so we can look
2637 // up if they exist later.
2638 const foundGroups: Set<string> = new Set();
2639 const foundNews: Set<string> = new Set();
2640 // Collect the longest duration of any view-transition animation including delay.
2641 let longestDuration = 0;
2642 for (let i = 0; i < animations.length; i++) {
2643 const effect: KeyframeEffect = animations[i].effect as any;
2644 // $FlowFixMe[prop-missing]
2645 const pseudoElement: ?string = effect.pseudoElement;
2646 if (pseudoElement == null) {
2647 } else if (
2648 pseudoElement.startsWith('::view-transition') &&
2649 effect.target === documentElement
2650 ) {
2651 const timing = effect.getTiming();
2652 const duration =
2653 // $FlowFixMe[prop-missing]
2654 typeof timing.duration === 'number' ? timing.duration : 0;
2655 // TODO: Consider interation count higher than 1.
2656 // $FlowFixMe[prop-missing]
2657 // $FlowFixMe[unsafe-addition]
2658 const durationWithDelay = timing.delay + duration;
2659 if (durationWithDelay > longestDuration) {
2660 longestDuration = durationWithDelay;
2661 }
2662 if (pseudoElement.startsWith('::view-transition-group')) {
2663 foundGroups.add(pseudoElement.slice(23));
2664 } else if (pseudoElement.startsWith('::view-transition-new')) {
2665 // TODO: This is not really a sufficient detection because if the new
2666 // pseudo element might exist but have animations disabled on it.
2667 foundNews.add(pseudoElement.slice(21));
2668 }
2669 }
2670 }
2671 const durationToRangeMultipler =
2672 (rangeEnd - rangeStart) / longestDuration;
2673 for (let i = 0; i < animations.length; i++) {
2674 const anim = animations[i];
2675 if (anim.playState !== 'running') {
2676 continue;
2677 }
2678 const effect: KeyframeEffect = anim.effect as any;
2679 // $FlowFixMe[prop-missing]
2680 const pseudoElement: ?string = effect.pseudoElement;
2681 if (
2682 pseudoElement != null &&
2683 pseudoElement.startsWith('::view-transition') &&
2684 effect.target === documentElement
2685 ) {
2686 // Ideally we could mutate the existing animation but unfortunately
2687 // the mutable APIs seem less tested and therefore are lacking or buggy.
2688 // Therefore we create a new animation instead.
2689 anim.cancel();
2690 let isGeneratedGroupAnim = false;
2691 let isExitGroupAnim = false;
2692 if (pseudoElement.startsWith('::view-transition-group')) {
2693 const groupName = pseudoElement.slice(23);
2694 if (foundNews.has(groupName)) {
2695 // If this has both "new" and "old" state we expect this to be an auto-generated
2696 // animation that started outside the viewport. We need to adjust this first frame
2697 // to be inside the viewport.
2698 // $FlowFixMe[prop-missing]
2699 const animationName: ?string = anim.animationName;
2700 isGeneratedGroupAnim =
2701 animationName != null &&
2702 // $FlowFixMe[prop-missing]
2703 animationName.startsWith('-ua-view-transition-group-anim-');
2704 } else {
2705 // If this has only an "old" state then the pseudo element will be outside
2706 // the viewport. If any keyframes don't override "transform" we need to
2707 // adjust them.
2708 isExitGroupAnim = true;
2709 }
2710 // TODO: If this has only an old state and no new state,
2711 }
2712 // Adjust the range based on how long the animation would've ran as time based.
2713 // Since we're running animations in reverse from how they normally would run,
2714 // therefore the timing is from the rangeEnd to the start.
2715 const timing = effect.getTiming();
2716 const duration =
2717 // $FlowFixMe[prop-missing]
2718 typeof timing.duration === 'number' ? timing.duration : 0;
2719 let adjustedRangeStart =
2720 // $FlowFixMe[unsafe-addition]
2721 // $FlowFixMe[prop-missing]
2722 rangeEnd - (duration + timing.delay) * durationToRangeMultipler;
2723 let adjustedRangeEnd =
2724 rangeEnd -
2725 // $FlowFixMe[prop-missing]
2726 // $FlowFixMe[unsafe-arithmetic]
2727 timing.delay * durationToRangeMultipler;
2728 if (
2729 timing.direction === 'reverse' ||
2730 timing.direction === 'alternate-reverse'
2731 ) {
2732 // This animation was originally in reverse so we have to play it in flipped range.
2733 const temp = adjustedRangeStart;
2734 adjustedRangeStart = adjustedRangeEnd;
2735 adjustedRangeEnd = temp;
2736 }
2737 animateGesture(
2738 effect.getKeyframes(),
2739 // $FlowFixMe[incompatible-type]: Always documentElement atm.
2740 effect.target,
2741 pseudoElement,
2742 timeline,
2743 viewTransitionAnimations,
2744 customTimelineCleanup,
2745 adjustedRangeStart,
2746 adjustedRangeEnd,
2747 isGeneratedGroupAnim,
2748 isExitGroupAnim,
2749 );
2750 if (pseudoElement.startsWith('::view-transition-old')) {
2751 const groupName = pseudoElement.slice(21);
2752 if (!foundGroups.has(groupName) && !foundNews.has(groupName)) {
2753 foundGroups.add(groupName);
2754 // We haven't seen any group animation with this name. Since the old
2755 // state was outside the viewport we need to put it back. Since we
2756 // can't programmatically target the element itself, we use an
2757 // animation to adjust it.
2758 // This usually happens for exit animations where the element has
2759 // the old position.
2760 // If we also have a "new" state then we skip this because it means
2761 // someone manually disabled the auto-generated animation. We need to
2762 // treat the old state as having the position of the "new" state which
2763 // will happen by default.
2764 const pseudoElementName = '::view-transition-group' + groupName;
2765 animateGesture(
2766 [{}, {}],
2767 // $FlowFixMe[incompatible-type]: Always documentElement atm.
2768 effect.target,
2769 pseudoElementName,
2770 timeline,
2771 viewTransitionAnimations,
2772 customTimelineCleanup,
2773 rangeStart,
2774 rangeEnd,
2775 false,
2776 true, // We let the helper apply the translate
2777 );
2778 }
2779 }
2780 }
2781 }
2782 // View Transitions with ScrollTimeline has a quirk where they end if the
2783 // ScrollTimeline ever reaches 100% but that doesn't mean we're done because
2784 // you can swipe back again. We can prevent this by adding a paused Animation
2785 // that never stops. This seems to keep all running Animations alive until
2786 // we explicitly abort (or something forces the View Transition to cancel).
2787 // $FlowFixMe[incompatible-call]
2788 // $FlowFixMe[incompatible-type]
2789 const blockingAnim = documentElement.animate([{}, {}], {
2790 pseudoElement: '::view-transition',
2791 duration: 1,
2792 });
2793 blockingAnim.pause();
2794 viewTransitionAnimations.push(blockingAnim);
2795 animateCallback();
2796 };
2797 // In Chrome, "new" animations are not ready in the ready callback. We have to wait
2798 // until requestAnimationFrame before we can observe them through getAnimations().
2799 // However, in Safari, that would cause a flicker because we're applying them late.
2800 // TODO: Think of a feature detection for this instead.
2801 const readyForAnimations =
2802 navigator.userAgent.indexOf('Chrome') !== -1
2803 ? () => requestAnimationFrame(readyCallback)
2804 : readyCallback;
2805 const handleError = (error: mixed) => {
2806 // $FlowFixMe[prop-missing]
2807 if (ownerDocument.__reactViewTransition === transition) {
2808 // $FlowFixMe[prop-missing]
2809 ownerDocument.__reactViewTransition = null;
2810 }
2811 try {
2812 error = customizeViewTransitionError(error, true);
2813 if (error !== null) {
2814 errorCallback(error);
2815 }
2816 } finally {
2817 // Continue the reset of the work.
2818 // If the error happened in the snapshot phase before the update callback
2819 // was invoked, then we need to first finish the mutation and layout phases.
2820 // If they're already invoked it's still safe to call them due the status check.
2821 mutationCallback();
2822 // Skip readyCallback() and go straight to animateCallbck() since we're not animating.
2823 // animateCallback() is still required to restore states.
2824 animateCallback();
2825 if (enableProfilerTimer) {
2826 finishedAnimation();
2827 }
2828 }
2829 };
2830 transition.ready.then(readyForAnimations, handleError);
2831 transition.finished.finally(() => {
2832 for (let i = 0; i < viewTransitionAnimations.length; i++) {
2833 // In Safari, we need to manually cancel all manually started animations
2834 // or it'll block or interfer with future transitions.
2835 // We can't use getAnimations() due to #35336 so we collect them in an array.
2836 viewTransitionAnimations[i].cancel();
2837 }
2838 for (let i = 0; i < customTimelineCleanup.length; i++) {
2839 const cleanup = customTimelineCleanup[i];
2840 cleanup();
2841 }
2842 // $FlowFixMe[prop-missing]
2843 if (ownerDocument.__reactViewTransition === transition) {
2844 // $FlowFixMe[prop-missing]
2845 ownerDocument.__reactViewTransition = null;
2846 }
2847 if (enableProfilerTimer) {
2848 // Signal that the Transition was unable to continue. We do that here
2849 // instead of when we stop the running View Transition to ensure that
2850 // we cover cases when something else stops it early.
2851 finishedAnimation();
2852 }
2853 });
2854 return transition;
2855 } catch (x) {
2856 // We use the error as feature detection.
2857 // The only thing that should throw is if startViewTransition is missing
2858 // or if it doesn't accept the object form. Other errors are async.
2859 // I.e. it's before the View Transitions v2 spec. We only support View
2860 // Transitions v2 otherwise we fallback to not animating to ensure that
2861 // we're not animating with the wrong animation mapped.
2862 // Run through the sequence to put state back into a consistent state.
2863 mutationCallback();
2864 animateCallback();
2865 if (enableProfilerTimer) {
2866 finishedAnimation();
2867 }
2868 return null;
2869 }
2870 }
2871
2872 export function stopViewTransition(transition: RunningViewTransition) {
2873 transition.skipTransition();
2874 }
2875
2876 export function addViewTransitionFinishedListener(
2877 transition: RunningViewTransition,
2878 callback: () => void,
2879 ) {
2880 transition.finished.finally(callback);
2881 }
2882
2883 interface ViewTransitionPseudoElementType extends mixin$Animatable {
2884 _scope: HTMLElement;
2885 _selector: string;
2886 getComputedStyle(): CSSStyleDeclaration;
2887 }
2888
2889 function ViewTransitionPseudoElement(
2890 this: ViewTransitionPseudoElementType,
2891 pseudo: string,
2892 name: string,
2893 ) {
2894 // TODO: Get the owner document from the root container.
2895 this._scope = document.documentElement as any;
2896 this._selector = '::view-transition-' + pseudo + '(' + name + ')';
2897 }
2898 // $FlowFixMe[prop-missing]
2899 ViewTransitionPseudoElement.prototype.animate = function (
2900 this: ViewTransitionPseudoElementType,
2901 keyframes: Keyframe[] | PropertyIndexedKeyframes | null,
2902 options?: number | KeyframeAnimationOptions,
2903 ): Animation {
2904 const opts: any =
2905 typeof options === 'number'
2906 ? {
2907 duration: options,
2908 }
2909 : Object.assign(
2910 // $FlowFixMe[prop-missing]
2911 // $FlowFixMe[incompatible-type]
2912 {} as KeyframeAnimationOptions,
2913 options,
2914 );
2915 opts.pseudoElement = this._selector;
2916 // TODO: Handle multiple child instances.
2917 return this._scope.animate(keyframes, opts);
2918 };
2919 // $FlowFixMe[prop-missing]
2920 ViewTransitionPseudoElement.prototype.getAnimations = function (
2921 this: ViewTransitionPseudoElementType,
2922 options?: GetAnimationsOptions,
2923 ): Animation[] {
2924 const scope = this._scope;
2925 const selector = this._selector;
2926 const animations = scope.getAnimations(
2927 // $FlowFixMe[prop-missing]
2928 // $FlowFixMe[incompatible-type]
2929 {subtree: true},
2930 );
2931 const result = [];
2932 for (let i = 0; i < animations.length; i++) {
2933 const effect: null | {
2934 target?: Element,
2935 pseudoElement?: string,
2936 ...
2937 } = animations[i].effect as any;
2938 // TODO: Handle multiple child instances.
2939 if (
2940 effect !== null &&
2941 effect.target === scope &&
2942 effect.pseudoElement === selector
2943 ) {
2944 result.push(animations[i]);
2945 }
2946 }
2947 return result;
2948 };
2949 // $FlowFixMe[prop-missing]
2950 ViewTransitionPseudoElement.prototype.getComputedStyle = function (
2951 this: ViewTransitionPseudoElementType,
2952 ): CSSStyleDeclaration {
2953 const scope = this._scope;
2954 const selector = this._selector;
2955 return getComputedStyle(scope, selector);
2956 };
2957
2958 export function createViewTransitionInstance(
2959 name: string,
2960 ): ViewTransitionInstance {
2961 return {
2962 name: name,
2963 group: new (ViewTransitionPseudoElement as any)('group', name),
2964 imagePair: new (ViewTransitionPseudoElement as any)('image-pair', name),
2965 old: new (ViewTransitionPseudoElement as any)('old', name),
2966 new: new (ViewTransitionPseudoElement as any)('new', name),
2967 };
2968 }
2969
2970 interface CustomTimeline {
2971 currentTime: number;
2972 animate(animation: Animation): void | (() => void);
2973 }
2974
2975 export type GestureTimeline = AnimationTimeline | CustomTimeline;
2976
2977 export function getCurrentGestureOffset(timeline: GestureTimeline): number {
2978 const time = timeline.currentTime;
2979 if (time === null) {
2980 throw new Error(
2981 'Cannot start a gesture with a disconnected AnimationTimeline.',
2982 );
2983 }
2984 return typeof time === 'number' ? time : time.value;
2985 }
2986
2987 type StoredEventListener = {
2988 type: string,
2989 listener: EventListener,
2990 optionsOrUseCapture: void | EventListenerOptionsOrUseCapture,
2991 // When once:true, a wrapper that removes the fragment listener after the
2992 // first fire. Otherwise the same as listener.
2993 attachedListener: EventListener,
2994 };
2995
2996 export type FragmentInstanceType = {
2997 _fragmentFiber: Fiber,
2998 _eventListeners: null | Array<StoredEventListener>,
2999 _observers: null | Set<IntersectionObserver | ResizeObserver>,
3000 addEventListener(
3001 type: string,
3002 listener: EventListener,
3003 optionsOrUseCapture?: EventListenerOptionsOrUseCapture,
3004 ): void,
3005 removeEventListener(
3006 type: string,
3007 listener: EventListener,
3008 optionsOrUseCapture?: EventListenerOptionsOrUseCapture,
3009 ): void,
3010 dispatchEvent(event: Event): boolean,
3011 focus(focusOptions?: FocusOptions): void,
3012 focusLast(focusOptions?: FocusOptions): void,
3013 blur(): void,
3014 observeUsing(observer: IntersectionObserver | ResizeObserver): void,
3015 unobserveUsing(observer: IntersectionObserver | ResizeObserver): void,
3016 getClientRects(): Array<DOMRect>,
3017 getRootNode(getRootNodeOptions?: {
3018 composed: boolean,
3019 }): Document | ShadowRoot | FragmentInstanceType,
3020 compareDocumentPosition(otherNode: Instance): number,
3021 scrollIntoView(alignToTop?: boolean): void,
3022 };
3023
3024 function FragmentInstance(this: FragmentInstanceType, fragmentFiber: Fiber) {
3025 this._fragmentFiber = fragmentFiber;
3026 this._eventListeners = null;
3027 this._observers = null;
3028 }
3029
3030 // $FlowFixMe[prop-missing]
3031 FragmentInstance.prototype.addEventListener = function (
3032 this: FragmentInstanceType,
3033 type: string,
3034 listener: EventListener,
3035 optionsOrUseCapture?: EventListenerOptionsOrUseCapture,
3036 ): void {
3037 if (this._eventListeners === null) {
3038 this._eventListeners = [];
3039 }
3040
3041 const listeners = this._eventListeners;
3042 // Element.addEventListener will only apply uniquely new event listeners by default. Since we
3043 // need to collect the listeners to apply to appended children, we track them ourselves and use
3044 // custom equality check for the options.
3045 const isNewEventListener =
3046 indexOfEventListener(listeners, type, listener, optionsOrUseCapture) === -1;
3047 if (isNewEventListener) {
3048 const fragmentInstance = this;
3049 let attachedListener = listener;
3050 if (isOnceOption(optionsOrUseCapture)) {
3051 // once is fragment-scoped: the first fire on any child removes this
3052 // listener from the fragment and every host child.
3053 attachedListener = function (this: EventTarget, event: Event) {
3054 fragmentInstance.removeEventListener(
3055 type,
3056 listener,
3057 optionsOrUseCapture,
3058 );
3059 if (typeof listener === 'function') {
3060 listener.call(this, event);
3061 } else {
3062 listener.handleEvent(event);
3063 }
3064 };
3065 }
3066 const attachOptions = getAttachOptions(optionsOrUseCapture);
3067 listeners.push({
3068 type,
3069 listener,
3070 optionsOrUseCapture,
3071 attachedListener,
3072 });
3073 traverseFragmentInstancesAndTextInstances(
3074 this._fragmentFiber,
3075 addEventListenerToChild,
3076 type,
3077 attachedListener,
3078 attachOptions,
3079 );
3080 }
3081 this._eventListeners = listeners;
3082 };
3083 function addEventListenerToChild(
3084 child: Fiber,
3085 type: string,
3086 listener: EventListener,
3087 optionsOrUseCapture?: EventListenerOptionsOrUseCapture,
3088 ): boolean {
3089 const instance = getInstanceFromHostFiber<Instance | TextInstance>(child);
3090 instance.addEventListener(type, listener, optionsOrUseCapture);
3091 return false;
3092 }
3093 // $FlowFixMe[prop-missing]
3094 FragmentInstance.prototype.removeEventListener = function (
3095 this: FragmentInstanceType,
3096 type: string,
3097 listener: EventListener,
3098 optionsOrUseCapture?: EventListenerOptionsOrUseCapture,
3099 ): void {
3100 const listeners = this._eventListeners;
3101 if (listeners === null) {
3102 return;
3103 }
3104 const index = indexOfEventListener(
3105 listeners,
3106 type,
3107 listener,
3108 optionsOrUseCapture,
3109 );
3110 if (index === -1) {
3111 return;
3112 }
3113 const {attachedListener, optionsOrUseCapture: storedOptions} =
3114 listeners[index];
3115 const attachOptions = getAttachOptions(storedOptions);
3116 traverseFragmentInstancesAndTextInstances(
3117 this._fragmentFiber,
3118 removeEventListenerFromChild,
3119 type,
3120 attachedListener,
3121 attachOptions,
3122 );
3123 listeners.splice(index, 1);
3124 };
3125 function removeEventListenerFromChild(
3126 child: Fiber,
3127 type: string,
3128 listener: EventListener,
3129 optionsOrUseCapture?: EventListenerOptionsOrUseCapture,
3130 ): boolean {
3131 const instance = getInstanceFromHostFiber<Instance | TextInstance>(child);
3132 instance.removeEventListener(type, listener, optionsOrUseCapture);
3133 return false;
3134 }
3135 function isOnceOption(opts: ?EventListenerOptionsOrUseCapture): boolean {
3136 return opts != null && typeof opts !== 'boolean' && opts.once === true;
3137 }
3138 function getAttachOptions(
3139 opts: void | EventListenerOptionsOrUseCapture,
3140 ): void | EventListenerOptionsOrUseCapture {
3141 // Strip once when attaching to host children; Fragment owns once semantics.
3142 if (opts == null || typeof opts === 'boolean' || opts.once !== true) {
3143 return opts;
3144 }
3145 return {
3146 capture: opts.capture,
3147 passive: opts.passive,
3148 signal: opts.signal,
3149 };
3150 }
3151 function normalizeListenerOptions(
3152 opts: ?EventListenerOptionsOrUseCapture,
3153 ): string {
3154 if (opts == null) {
3155 return 'c=0';
3156 }
3157
3158 if (typeof opts === 'boolean') {
3159 return `c=${opts ? '1' : '0'}`;
3160 }
3161
3162 return `c=${opts.capture ? '1' : '0'}`;
3163 }
3164 function indexOfEventListener(
3165 eventListeners: Array<StoredEventListener>,
3166 type: string,
3167 listener: EventListener,
3168 optionsOrUseCapture: void | EventListenerOptionsOrUseCapture,
3169 ): number {
3170 if (eventListeners.length === 0) {
3171 return -1;
3172 }
3173 const normalizedOptions = normalizeListenerOptions(optionsOrUseCapture);
3174 for (let i = 0; i < eventListeners.length; i++) {
3175 const item = eventListeners[i];
3176 if (
3177 item.type === type &&
3178 item.listener === listener &&
3179 normalizeListenerOptions(item.optionsOrUseCapture) === normalizedOptions
3180 ) {
3181 return i;
3182 }
3183 }
3184 return -1;
3185 }
3186 // $FlowFixMe[prop-missing]
3187 FragmentInstance.prototype.dispatchEvent = function (
3188 this: FragmentInstanceType,
3189 event: Event,
3190 ): boolean {
3191 const parentHostFiber = getFragmentParentInstanceOrContainerFiber(
3192 this._fragmentFiber,
3193 );
3194 if (parentHostFiber === null) {
3195 return true;
3196 }
3197 const parentHostInstance = getInstanceFromHostFiber<Instance | Container>(
3198 parentHostFiber,
3199 );
3200 const eventListeners = this._eventListeners;
3201 if (
3202 (eventListeners !== null && eventListeners.length > 0) ||
3203 !event.bubbles
3204 ) {
3205 // The temporary node stands in for the fragment's position so that its own
3206 // listeners fire before the event propagates to the parent. A Document can
3207 // only hold comments and processing instructions alongside its
3208 // documentElement, so a Text node would be an invalid child there.
3209 const temp =
3210 parentHostInstance.nodeType === DOCUMENT_NODE
3211 ? (parentHostInstance as any as Document).createComment('')
3212 : document.createTextNode('');
3213 if (eventListeners) {
3214 for (let i = 0; i < eventListeners.length; i++) {
3215 const {type, attachedListener, optionsOrUseCapture} = eventListeners[i];
3216 temp.addEventListener(
3217 type,
3218 attachedListener,
3219 getAttachOptions(optionsOrUseCapture),
3220 );
3221 }
3222 }
3223 parentHostInstance.appendChild(temp);
3224 const cancelable = temp.dispatchEvent(event);
3225 if (eventListeners) {
3226 for (let i = 0; i < eventListeners.length; i++) {
3227 const {type, attachedListener, optionsOrUseCapture} = eventListeners[i];
3228 temp.removeEventListener(
3229 type,
3230 attachedListener,
3231 getAttachOptions(optionsOrUseCapture),
3232 );
3233 }
3234 }
3235 parentHostInstance.removeChild(temp);
3236 return cancelable;
3237 } else {
3238 return parentHostInstance.dispatchEvent(event);
3239 }
3240 };
3241 // $FlowFixMe[prop-missing]
3242 FragmentInstance.prototype.focus = function (
3243 this: FragmentInstanceType,
3244 focusOptions?: FocusOptions,
3245 ): void {
3246 traverseFragmentInstancesAndTextInstancesDeeply(
3247 this._fragmentFiber,
3248 setFocusOnFiberIfFocusable,
3249 focusOptions,
3250 );
3251 };
3252 function setFocusOnFiberIfFocusable(
3253 fiber: Fiber,
3254 focusOptions?: FocusOptions,
3255 ): boolean {
3256 if (enableFragmentRefsTextNodes) {
3257 // Skip text nodes - they are not focusable
3258 if (fiber.tag === HostText) {
3259 return false;
3260 }
3261 }
3262 const instance = getInstanceFromHostFiber<Instance>(fiber);
3263 return setFocusIfFocusable(instance, focusOptions);
3264 }
3265 // $FlowFixMe[prop-missing]
3266 FragmentInstance.prototype.focusLast = function (
3267 this: FragmentInstanceType,
3268 focusOptions?: FocusOptions,
3269 ): void {
3270 const children: Array<Fiber> = [];
3271 traverseFragmentInstancesAndTextInstancesDeeply(
3272 this._fragmentFiber,
3273 collectChildren,
3274 children,
3275 );
3276 for (let i = children.length - 1; i >= 0; i--) {
3277 const child = children[i];
3278 if (setFocusOnFiberIfFocusable(child, focusOptions)) {
3279 break;
3280 }
3281 }
3282 };
3283 function collectChildren(child: Fiber, collection: Array<Fiber>): boolean {
3284 collection.push(child);
3285 return false;
3286 }
3287 // $FlowFixMe[prop-missing]
3288 FragmentInstance.prototype.blur = function (this: FragmentInstanceType): void {
3289 const parentHostFiber = getFragmentParentInstanceOrContainerFiber(
3290 this._fragmentFiber,
3291 );
3292 if (parentHostFiber === null) {
3293 return;
3294 }
3295 const parentInstanceOrContainer = getInstanceFromHostFiber<
3296 Instance | Container,
3297 >(parentHostFiber);
3298 // Instance is included in the Container type for DOM.
3299 const ownerDocument = getOwnerDocumentFromRootContainer(
3300 parentInstanceOrContainer,
3301 );
3302 const activeElement = ownerDocument.activeElement;
3303 if (activeElement === null) {
3304 return;
3305 }
3306 traverseFragmentInstancesAndTextInstances(
3307 this._fragmentFiber,
3308 blurActiveElementWithinFragment,
3309 activeElement,
3310 );
3311 };
3312 function blurActiveElementWithinFragment(
3313 child: Fiber,
3314 activeElement: Element,
3315 ): boolean {
3316 // Skip text nodes - they can't be focused
3317 if (enableFragmentRefsTextNodes && child.tag === HostText) {
3318 return false;
3319 }
3320 const instance = getInstanceFromHostFiber<Instance>(child);
3321 if (instance === activeElement || instance.contains(activeElement)) {
3322 // $FlowFixMe[prop-missing]
3323 activeElement.blur();
3324 return true;
3325 }
3326 return false;
3327 }
3328 // $FlowFixMe[prop-missing]
3329 FragmentInstance.prototype.observeUsing = function (
3330 this: FragmentInstanceType,
3331 observer: IntersectionObserver | ResizeObserver,
3332 ): void {
3333 if (__DEV__) {
3334 if (enableFragmentRefsTextNodes) {
3335 let hasText = false;
3336 let hasElement = false;
3337 traverseFragmentInstancesAndTextInstances(
3338 this._fragmentFiber,
3339 (child: Fiber) => {
3340 if (child.tag === HostText) {
3341 hasText = true;
3342 } else {
3343 // Stop traversal, found element
3344 hasElement = true;
3345 return true;
3346 }
3347 return false;
3348 },
3349 );
3350 if (hasText && !hasElement) {
3351 console.error(
3352 'observeUsing() was called on a FragmentInstance with only text children. ' +
3353 'Observers do not work on text nodes.',
3354 );
3355 }
3356 }
3357 }
3358 if (this._observers === null) {
3359 this._observers = new Set();
3360 }
3361 this._observers.add(observer);
3362 traverseFragmentInstancesAndTextInstances(
3363 this._fragmentFiber,
3364 observeChild,
3365 observer,
3366 );
3367 };
3368 function observeChild(
3369 child: Fiber,
3370 observer: IntersectionObserver | ResizeObserver,
3371 ) {
3372 if (enableFragmentRefsTextNodes) {
3373 // Skip text nodes - observers don't work on them
3374 if (child.tag === HostText) {
3375 return false;
3376 }
3377 }
3378 const instance = getInstanceFromHostFiber<Instance>(child);
3379 observer.observe(instance);
3380 return false;
3381 }
3382 // $FlowFixMe[prop-missing]
3383 FragmentInstance.prototype.unobserveUsing = function (
3384 this: FragmentInstanceType,
3385 observer: IntersectionObserver | ResizeObserver,
3386 ): void {
3387 const observers = this._observers;
3388 if (observers === null || !observers.has(observer)) {
3389 if (__DEV__) {
3390 console.error(
3391 'You are calling unobserveUsing() with an observer that is not being observed with this fragment ' +
3392 'instance. First attach the observer with observeUsing()',
3393 );
3394 }
3395 } else {
3396 observers.delete(observer);
3397 traverseFragmentInstancesAndTextInstances(
3398 this._fragmentFiber,
3399 unobserveChild,
3400 observer,
3401 );
3402 }
3403 };
3404 function unobserveChild(
3405 child: Fiber,
3406 observer: IntersectionObserver | ResizeObserver,
3407 ) {
3408 if (enableFragmentRefsTextNodes) {
3409 // Skip text nodes - they were never observed
3410 if (child.tag === HostText) {
3411 return false;
3412 }
3413 }
3414 const instance = getInstanceFromHostFiber<Instance>(child);
3415 observer.unobserve(instance);
3416 return false;
3417 }
3418 // $FlowFixMe[prop-missing]
3419 FragmentInstance.prototype.getClientRects = function (
3420 this: FragmentInstanceType,
3421 ): Array<DOMRect> {
3422 const rects: Array<DOMRect> = [];
3423 traverseFragmentInstancesAndTextInstances(
3424 this._fragmentFiber,
3425 collectClientRects,
3426 rects,
3427 );
3428 return rects;
3429 };
3430 function collectClientRects(child: Fiber, rects: Array<DOMRect>): boolean {
3431 if (enableFragmentRefsTextNodes && child.tag === HostText) {
3432 const textNode: Text = child.stateNode;
3433 const range = textNode.ownerDocument.createRange();
3434 range.selectNodeContents(textNode);
3435 // $FlowFixMe[method-unbinding]
3436 rects.push.apply(rects, range.getClientRects());
3437 } else {
3438 const instance = getInstanceFromHostFiber<Instance>(child);
3439 // $FlowFixMe[method-unbinding]
3440 rects.push.apply(rects, instance.getClientRects());
3441 }
3442 return false;
3443 }
3444 // $FlowFixMe[prop-missing]
3445 FragmentInstance.prototype.getRootNode = function (
3446 this: FragmentInstanceType,
3447 getRootNodeOptions?: {composed: boolean},
3448 ): Document | ShadowRoot | FragmentInstanceType {
3449 const parentHostFiber = getFragmentParentInstanceOrContainerFiber(
3450 this._fragmentFiber,
3451 );
3452 if (parentHostFiber === null) {
3453 return this;
3454 }
3455 const parentHostInstance = getInstanceFromHostFiber<Instance | Container>(
3456 parentHostFiber,
3457 );
3458 const rootNode =
3459 // $FlowFixMe[incompatible-type] Flow expects Node
3460 parentHostInstance.getRootNode(getRootNodeOptions) as Document | ShadowRoot;
3461 return rootNode;
3462 };
3463 // $FlowFixMe[prop-missing]
3464 FragmentInstance.prototype.compareDocumentPosition = function (
3465 this: FragmentInstanceType,
3466 otherNode: Instance,
3467 ): number {
3468 const parentHostFiber = getFragmentParentInstanceOrContainerFiber(
3469 this._fragmentFiber,
3470 );
3471 if (parentHostFiber === null) {
3472 return Node.DOCUMENT_POSITION_DISCONNECTED;
3473 }
3474 const children: Array<Fiber> = [];
3475 traverseFragmentInstancesAndTextInstances(
3476 this._fragmentFiber,
3477 collectChildren,
3478 children,
3479 );
3480 const parentHostInstance = getInstanceFromHostFiber<Instance | Container>(
3481 parentHostFiber,
3482 );
3483
3484 if (children.length === 0) {
3485 // Match non-empty CDP: when portaled, position against the portal
3486 // container rather than the React host parent.
3487 let emptyParentHostInstance = parentHostInstance;
3488 if (fiberIsPortaledIntoHost(this._fragmentFiber)) {
3489 const portalContainer = getFragmentPortalContainerInfo(
3490 this._fragmentFiber,
3491 );
3492 if (portalContainer != null) {
3493 emptyParentHostInstance = portalContainer;
3494 }
3495 }
3496 return compareDocumentPositionForEmptyFragment(
3497 this._fragmentFiber,
3498 emptyParentHostInstance,
3499 otherNode,
3500 getInstanceFromHostFiber,
3501 );
3502 }
3503
3504 const firstNode = getInstanceFromHostFiber<Instance | TextInstance>(
3505 children[0],
3506 );
3507 const lastNode = getInstanceFromHostFiber<Instance | TextInstance>(
3508 children[children.length - 1],
3509 );
3510
3511 // If the fragment has been portaled into another host instance, we need to
3512 // our best guess is to use the parent of the child instance, rather than
3513 // the fiber tree host parent.
3514 const parentHostInstanceFromDOM = fiberIsPortaledIntoHost(this._fragmentFiber)
3515 ? (firstNode.parentElement as ?Instance)
3516 : parentHostInstance;
3517
3518 if (parentHostInstanceFromDOM == null) {
3519 return Node.DOCUMENT_POSITION_DISCONNECTED;
3520 }
3521
3522 // Check if first and last node are actually in the expected document position
3523 // before relying on them as source of truth for other contained nodes
3524 const firstNodeIsContained =
3525 parentHostInstanceFromDOM.compareDocumentPosition(firstNode) &
3526 Node.DOCUMENT_POSITION_CONTAINED_BY;
3527 const lastNodeIsContained =
3528 parentHostInstanceFromDOM.compareDocumentPosition(lastNode) &
3529 Node.DOCUMENT_POSITION_CONTAINED_BY;
3530 const firstResult = firstNode.compareDocumentPosition(otherNode);
3531 const lastResult = lastNode.compareDocumentPosition(otherNode);
3532
3533 const otherNodeIsFirstOrLastChild =
3534 (firstNodeIsContained && firstNode === otherNode) ||
3535 (lastNodeIsContained && lastNode === otherNode);
3536 const otherNodeIsFirstOrLastChildDisconnected =
3537 (!firstNodeIsContained && firstNode === otherNode) ||
3538 (!lastNodeIsContained && lastNode === otherNode);
3539 const otherNodeIsWithinFirstOrLastChild =
3540 firstResult & Node.DOCUMENT_POSITION_CONTAINED_BY ||
3541 lastResult & Node.DOCUMENT_POSITION_CONTAINED_BY;
3542 const otherNodeIsBetweenFirstAndLastChildren =
3543 firstNodeIsContained &&
3544 lastNodeIsContained &&
3545 firstResult & Node.DOCUMENT_POSITION_FOLLOWING &&
3546 lastResult & Node.DOCUMENT_POSITION_PRECEDING;
3547
3548 let result = Node.DOCUMENT_POSITION_DISCONNECTED;
3549 if (
3550 otherNodeIsFirstOrLastChild ||
3551 otherNodeIsWithinFirstOrLastChild ||
3552 otherNodeIsBetweenFirstAndLastChildren
3553 ) {
3554 result = Node.DOCUMENT_POSITION_CONTAINED_BY;
3555 } else if (otherNodeIsFirstOrLastChildDisconnected) {
3556 // otherNode has been portaled into another container
3557 result = Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC;
3558 } else {
3559 result = firstResult;
3560 }
3561
3562 if (
3563 result & Node.DOCUMENT_POSITION_DISCONNECTED ||
3564 result & Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC
3565 ) {
3566 return result;
3567 }
3568
3569 // Now that we have the result from the DOM API, we double check it matches
3570 // the state of the React tree. If it doesn't, we have a case of portaled or
3571 // otherwise injected elements and we return DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC.
3572 const documentPositionMatchesFiberPosition =
3573 validateDocumentPositionWithFiberTree(
3574 result,
3575 this._fragmentFiber,
3576 children[0],
3577 children[children.length - 1],
3578 otherNode,
3579 );
3580 if (documentPositionMatchesFiberPosition) {
3581 return result;
3582 }
3583 return Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC;
3584 };
3585
3586 function validateDocumentPositionWithFiberTree(
3587 documentPosition: number,
3588 fragmentFiber: Fiber,
3589 precedingBoundaryFiber: Fiber,
3590 followingBoundaryFiber: Fiber,
3591 otherNode: Instance,
3592 ): boolean {
3593 const otherFiber = getClosestInstanceFromNode(otherNode);
3594 if (documentPosition & Node.DOCUMENT_POSITION_CONTAINED_BY) {
3595 return (
3596 !!otherFiber && isFiberContainedByFragment(otherFiber, fragmentFiber)
3597 );
3598 }
3599 if (documentPosition & Node.DOCUMENT_POSITION_CONTAINS) {
3600 if (otherFiber === null) {
3601 // otherFiber could be null if its the document, documentElement, or body
3602 const ownerDocument = otherNode.ownerDocument;
3603 return (
3604 (otherNode as Instance | Document) === ownerDocument ||
3605 otherNode === ownerDocument.documentElement ||
3606 otherNode === ownerDocument.body
3607 );
3608 }
3609 return isFragmentContainedByFiber(fragmentFiber, otherFiber);
3610 }
3611 if (documentPosition & Node.DOCUMENT_POSITION_PRECEDING) {
3612 return (
3613 !!otherFiber &&
3614 (otherFiber === precedingBoundaryFiber ||
3615 isFiberPreceding(precedingBoundaryFiber, otherFiber))
3616 );
3617 }
3618 if (documentPosition & Node.DOCUMENT_POSITION_FOLLOWING) {
3619 return (
3620 !!otherFiber &&
3621 (otherFiber === followingBoundaryFiber ||
3622 isFiberFollowing(followingBoundaryFiber, otherFiber))
3623 );
3624 }
3625
3626 return false;
3627 }
3628
3629 function scrollTextNodeIntoView(
3630 textNode: TextInstance,
3631 resolvedAlignToTop: boolean,
3632 ): void {
3633 const range = textNode.ownerDocument.createRange();
3634 range.selectNodeContents(textNode);
3635 const rect = range.getBoundingClientRect();
3636 const scrollY = resolvedAlignToTop
3637 ? window.scrollY + rect.top
3638 : window.scrollY + rect.bottom - window.innerHeight;
3639 window.scrollTo(window.scrollX + rect.left, scrollY);
3640 }
3641
3642 if (enableFragmentRefsScrollIntoView) {
3643 // $FlowFixMe[prop-missing]
3644 FragmentInstance.prototype.scrollIntoView = function (
3645 this: FragmentInstanceType,
3646 alignToTop?: boolean,
3647 ): void {
3648 if (typeof alignToTop === 'object') {
3649 throw new Error(
3650 'FragmentInstance.scrollIntoView() does not support ' +
3651 'scrollIntoViewOptions. Use the alignToTop boolean instead.',
3652 );
3653 }
3654 // First, get the children nodes
3655 const children: Array<Fiber> = [];
3656 traverseFragmentInstancesAndTextInstances(
3657 this._fragmentFiber,
3658 collectChildren,
3659 children,
3660 );
3661
3662 const resolvedAlignToTop = alignToTop !== false;
3663
3664 // If there are no children, we can use the parent and siblings to determine a position
3665 if (children.length === 0) {
3666 const hostSiblings = getFragmentInstanceOrTextInstanceSiblings(
3667 this._fragmentFiber,
3668 );
3669 const targetFiber = resolvedAlignToTop
3670 ? hostSiblings[1] ||
3671 hostSiblings[0] ||
3672 getFragmentParentInstanceOrContainerFiber(this._fragmentFiber)
3673 : hostSiblings[0] || hostSiblings[1];
3674
3675 if (targetFiber === null) {
3676 return;
3677 }
3678 // For text node siblings, use Range API to scroll to their position
3679 if (enableFragmentRefsTextNodes && targetFiber.tag === HostText) {
3680 const textNode = getInstanceFromHostFiber<TextInstance>(targetFiber);
3681 scrollTextNodeIntoView(textNode, resolvedAlignToTop);
3682 return;
3683 }
3684 const target = getInstanceFromHostFiber<Instance | Container>(
3685 targetFiber,
3686 );
3687 // If the parent host fiber is a HostRoot, the target is a Container
3688 // which is not necessarily an Element with a scrollIntoView method.
3689 if (target.nodeType === DOCUMENT_NODE) {
3690 // A Document is always in view.
3691 } else if (target.nodeType === DOCUMENT_FRAGMENT_NODE) {
3692 const fragment = target as any as DocumentFragment;
3693 // ShadowRoot always has a host: https://dom.spec.whatwg.org/#ref-for-concept-documentfragment-host%E2%91%A5
3694 // A generic DocumentFragment doesn't implement this property but conceptually
3695 // host is a nullable Element: https://dom.spec.whatwg.org/#concept-documentfragment-host
3696 const host =
3697 'host' in fragment ? (fragment as any as ShadowRoot).host : null;
3698 if (host !== null) {
3699 // The ShadowRoot's host element marks the position where the
3700 // fragment's content would appear.
3701 host.scrollIntoView(alignToTop);
3702 } else if (__DEV__) {
3703 console.warn(
3704 'You are attempting to scroll a FragmentInstance that is only ' +
3705 'mounted inside a detached DocumentFragment. No scroll was ' +
3706 'performed.',
3707 );
3708 }
3709 return;
3710 } else {
3711 // Narrowed down to Element by nodeType check above, but Flow doesn't know that.
3712 const element = target as any as Element;
3713 element.scrollIntoView(alignToTop);
3714 }
3715 }
3716
3717 let i = resolvedAlignToTop ? children.length - 1 : 0;
3718 while (i !== (resolvedAlignToTop ? -1 : children.length)) {
3719 const child = children[i];
3720 // For text nodes, use Range API to scroll to their position
3721 if (enableFragmentRefsTextNodes && child.tag === HostText) {
3722 const textNode = getInstanceFromHostFiber<TextInstance>(child);
3723 scrollTextNodeIntoView(textNode, resolvedAlignToTop);
3724 i += resolvedAlignToTop ? -1 : 1;
3725 continue;
3726 }
3727 const instance = getInstanceFromHostFiber<Instance>(child);
3728 instance.scrollIntoView(alignToTop);
3729 i += resolvedAlignToTop ? -1 : 1;
3730 }
3731 };
3732 }
3733
3734 function addFragmentHandleToFiber(
3735 child: Fiber,
3736 fragmentInstance: FragmentInstanceType,
3737 ): boolean {
3738 if (enableFragmentRefsInstanceHandles) {
3739 const instance = getInstanceFromHostFiber<Instance | TextInstance>(
3740 child,
3741 ) as any as HostNodeWithFragmentHandles;
3742 addFragmentHandleToInstance(instance, fragmentInstance);
3743 }
3744 return false;
3745 }
3746
3747 function addFragmentHandleToInstance(
3748 instance: HostNodeWithFragmentHandles,
3749 fragmentInstance: FragmentInstanceType,
3750 ): void {
3751 if (enableFragmentRefsInstanceHandles) {
3752 if (instance.reactFragments == null) {
3753 instance.reactFragments = new Set();
3754 }
3755 instance.reactFragments.add(fragmentInstance);
3756 }
3757 }
3758
3759 export function createFragmentInstance(
3760 fragmentFiber: Fiber,
3761 ): FragmentInstanceType {
3762 const fragmentInstance = new (FragmentInstance as any)(fragmentFiber);
3763 if (enableFragmentRefsInstanceHandles) {
3764 traverseFragmentInstancesAndTextInstances(
3765 fragmentFiber,
3766 addFragmentHandleToFiber,
3767 fragmentInstance,
3768 );
3769 }
3770 return fragmentInstance;
3771 }
3772
3773 export function updateFragmentInstanceFiber(
3774 fragmentFiber: Fiber,
3775 instance: FragmentInstanceType,
3776 ): void {
3777 instance._fragmentFiber = fragmentFiber;
3778 }
3779
3780 export function commitNewChildToFragmentInstance(
3781 childInstance: InstanceWithFragmentHandles | Text,
3782 fragmentInstance: FragmentInstanceType,
3783 ): void {
3784 const eventListeners = fragmentInstance._eventListeners;
3785 if (eventListeners !== null) {
3786 for (let i = 0; i < eventListeners.length; i++) {
3787 const {type, attachedListener, optionsOrUseCapture} = eventListeners[i];
3788 childInstance.addEventListener(
3789 type,
3790 attachedListener,
3791 getAttachOptions(optionsOrUseCapture),
3792 );
3793 }
3794 }
3795 // Observers and fragment handles only apply to element children.
3796 if (childInstance.nodeType === TEXT_NODE) {
3797 return;
3798 }
3799 const instance: InstanceWithFragmentHandles = childInstance as any;
3800 if (fragmentInstance._observers !== null) {
3801 fragmentInstance._observers.forEach(observer => {
3802 observer.observe(instance);
3803 });
3804 }
3805 if (enableFragmentRefsInstanceHandles) {
3806 addFragmentHandleToInstance(instance, fragmentInstance);
3807 }
3808 }
3809
3810 export function deleteChildFromFragmentInstance(
3811 childInstance: InstanceWithFragmentHandles | Text,
3812 fragmentInstance: FragmentInstanceType,
3813 ): void {
3814 const eventListeners = fragmentInstance._eventListeners;
3815 if (eventListeners !== null) {
3816 for (let i = 0; i < eventListeners.length; i++) {
3817 const {type, attachedListener, optionsOrUseCapture} = eventListeners[i];
3818 childInstance.removeEventListener(
3819 type,
3820 attachedListener,
3821 getAttachOptions(optionsOrUseCapture),
3822 );
3823 }
3824 }
3825 if (childInstance.nodeType === TEXT_NODE) {
3826 return;
3827 }
3828 const instance: InstanceWithFragmentHandles = childInstance as any;
3829 if (enableFragmentRefsInstanceHandles) {
3830 if (instance.reactFragments != null) {
3831 instance.reactFragments.delete(fragmentInstance);
3832 }
3833 }
3834 }
3835
3836 export function clearContainer(container: Container): void {
3837 const nodeType = container.nodeType;
3838 if (nodeType === DOCUMENT_NODE) {
3839 clearContainerSparingly(container);
3840 } else if (nodeType === ELEMENT_NODE) {
3841 switch (container.nodeName) {
3842 case 'HEAD':
3843 case 'HTML':
3844 case 'BODY':
3845 clearContainerSparingly(container);
3846 return;
3847 default: {
3848 container.textContent = '';
3849 }
3850 }
3851 }
3852 }
3853
3854 function clearContainerSparingly(container: Node) {
3855 let node;
3856 let nextNode: ?Node = container.firstChild;
3857 if (nextNode && nextNode.nodeType === DOCUMENT_TYPE_NODE) {
3858 nextNode = nextNode.nextSibling;
3859 }
3860 while (nextNode) {
3861 node = nextNode;
3862 nextNode = nextNode.nextSibling;
3863 switch (node.nodeName) {
3864 case 'HTML':
3865 case 'HEAD':
3866 case 'BODY': {
3867 const element: Element = node as any;
3868 clearContainerSparingly(element);
3869 // If these singleton instances had previously been rendered with React they
3870 // may still hold on to references to the previous fiber tree. We detatch them
3871 // prospectively to reset them to a baseline starting state since we cannot create
3872 // new instances.
3873 detachDeletedInstance(element);
3874 continue;
3875 }
3876 // Script tags are retained to avoid an edge case bug. Normally scripts will execute if they
3877 // are ever inserted into the DOM. However when streaming if a script tag is opened but not
3878 // yet closed some browsers create and insert the script DOM Node but the script cannot execute
3879 // yet until the closing tag is parsed. If something causes React to call clearContainer while
3880 // this DOM node is in the document but not yet executable the DOM node will be removed from the
3881 // document and when the script closing tag comes in the script will not end up running. This seems
3882 // to happen in Chrome/Firefox but not Safari at the moment though this is not necessarily specified
3883 // behavior so it could change in future versions of browsers. While leaving all scripts is broader
3884 // than strictly necessary this is the least amount of additional code to avoid this breaking
3885 // edge case.
3886 //
3887 // Style tags are retained because they may likely come from 3rd party scripts and extensions
3888 case 'SCRIPT':
3889 case 'STYLE': {
3890 continue;
3891 }
3892 // Stylesheet tags are retained because they may likely come from 3rd party scripts and extensions
3893 case 'LINK': {
3894 if (
3895 (node as any as HTMLLinkElement).rel.toLowerCase() === 'stylesheet'
3896 ) {
3897 continue;
3898 }
3899 }
3900 }
3901 container.removeChild(node);
3902 }
3903 return;
3904 }
3905
3906 function clearHead(head: Element): void {
3907 let node = head.firstChild;
3908 while (node) {
3909 const nextNode = node.nextSibling;
3910 const nodeName = node.nodeName;
3911 if (
3912 isMarkedHoistable(node) ||
3913 nodeName === 'SCRIPT' ||
3914 nodeName === 'STYLE' ||
3915 (nodeName === 'LINK' &&
3916 (node as any as HTMLLinkElement).rel.toLowerCase() === 'stylesheet')
3917 ) {
3918 // retain these nodes
3919 } else {
3920 head.removeChild(node);
3921 }
3922 node = nextNode;
3923 }
3924 return;
3925 }
3926
3927 // Making this so we can eventually move all of the instance caching to the commit phase.
3928 // Currently this is only used to associate fiber and props to instances for hydrating
3929 // HostSingletons. The reason we need it here is we only want to make this binding on commit
3930 // because only one fiber can own the instance at a time and render can fail/restart
3931 export function bindInstance(
3932 instance: Instance,
3933 props: Props,
3934 internalInstanceHandle: mixed,
3935 ) {
3936 precacheFiberNode(internalInstanceHandle as any, instance);
3937 updateFiberProps(instance, props);
3938 }
3939
3940 // -------------------
3941 // Hydration
3942 // -------------------
3943
3944 export const supportsHydration = true;
3945
3946 export function canHydrateInstance(
3947 instance: HydratableInstance,
3948 type: string,
3949 props: Props,
3950 inRootOrSingleton: boolean,
3951 ): null | Instance {
3952 while (instance.nodeType === ELEMENT_NODE) {
3953 const element: Element = instance as any;
3954 const anyProps = props as any;
3955 if (element.nodeName.toLowerCase() !== type.toLowerCase()) {
3956 if (!inRootOrSingleton) {
3957 // Usually we error for mismatched tags.
3958 if (
3959 element.nodeName === 'INPUT' &&
3960 (element as any).type === 'hidden'
3961 ) {
3962 // If we have extra hidden inputs, we don't mismatch. This allows us to embed
3963 // extra form data in the original form.
3964 } else {
3965 return null;
3966 }
3967 }
3968 // In root or singleton parents we skip past mismatched instances.
3969 } else if (!inRootOrSingleton) {
3970 // Match
3971 if (type === 'input' && (element as any).type === 'hidden') {
3972 if (__DEV__) {
3973 checkAttributeStringCoercion(anyProps.name, 'name');
3974 }
3975 const name = anyProps.name == null ? null : '' + anyProps.name;
3976 if (
3977 anyProps.type !== 'hidden' ||
3978 element.getAttribute('name') !== name
3979 ) {
3980 // Skip past hidden inputs unless that's what we're looking for. This allows us
3981 // embed extra form data in the original form.
3982 } else {
3983 return element;
3984 }
3985 } else {
3986 return element;
3987 }
3988 } else if (isMarkedHoistable(element)) {
3989 // We've already claimed this as a hoistable which isn't hydrated this way so we skip past it.
3990 } else {
3991 // We have an Element with the right type.
3992
3993 // We are going to try to exclude it if we can definitely identify it as a hoisted Node or if
3994 // we can guess that the node is likely hoisted or was inserted by a 3rd party script or browser extension
3995 // using high entropy attributes for certain types. This technique will fail for strange insertions like
3996 // extension prepending <div> in the <body> but that already breaks before and that is an edge case.
3997 switch (type) {
3998 // case 'title':
3999 //We assume all titles are matchable. You should only have one in the Document, at least in a hoistable scope
4000 // 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.
4001 case 'meta': {
4002 // The only way to opt out of hoisting meta tags is to give it an itemprop attribute. We assume there will be
4003 // not 3rd party meta tags that are prepended, accepting the cases where this isn't true because meta tags
4004 // are usually only functional for SSR so even in a rare case where we did bind to an injected tag the runtime
4005 // implications are minimal
4006 if (!element.hasAttribute('itemprop')) {
4007 // This is a Hoistable
4008 break;
4009 }
4010 return element;
4011 }
4012 case 'link': {
4013 // Links come in many forms and we do expect 3rd parties to inject them into <head> / <body>. We exclude known resources
4014 // and then use high-entroy attributes like href which are almost always used and almost always unique to filter out unlikely
4015 // matches.
4016 const rel = element.getAttribute('rel');
4017 if (rel === 'stylesheet' && element.hasAttribute('data-precedence')) {
4018 // This is a stylesheet resource
4019 break;
4020 } else if (
4021 rel !== anyProps.rel ||
4022 element.getAttribute('href') !==
4023 (anyProps.href == null || anyProps.href === ''
4024 ? null
4025 : anyProps.href) ||
4026 element.getAttribute('crossorigin') !==
4027 (anyProps.crossOrigin == null ? null : anyProps.crossOrigin) ||
4028 element.getAttribute('title') !==
4029 (anyProps.title == null ? null : anyProps.title)
4030 ) {
4031 // rel + href should usually be enough to uniquely identify a link however crossOrigin can vary for rel preconnect
4032 // and title could vary for rel alternate
4033 break;
4034 }
4035 return element;
4036 }
4037 case 'style': {
4038 // Styles are hard to match correctly. We can exclude known resources but otherwise we accept the fact that a non-hoisted style tags
4039 // 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
4040 if (element.hasAttribute('data-precedence')) {
4041 // This is a style resource
4042 break;
4043 }
4044 return element;
4045 }
4046 case 'script': {
4047 // Scripts are a little tricky, we exclude known resources and then similar to links try to use high-entropy attributes
4048 // to reject poor matches. One challenge with scripts are inline scripts. We don't attempt to check text content which could
4049 // in theory lead to a hydration error later if a 3rd party injected an inline script before the React rendered nodes.
4050 // Falling back to client rendering if this happens should be seemless though so we will try this hueristic and revisit later
4051 // if we learn it is problematic
4052 const srcAttr = element.getAttribute('src');
4053 if (
4054 srcAttr !== (anyProps.src == null ? null : anyProps.src) ||
4055 element.getAttribute('type') !==
4056 (anyProps.type == null ? null : anyProps.type) ||
4057 element.getAttribute('crossorigin') !==
4058 (anyProps.crossOrigin == null ? null : anyProps.crossOrigin)
4059 ) {
4060 // This script is for a different src/type/crossOrigin. It may be a script resource
4061 // or it may just be a mistmatch
4062 if (
4063 srcAttr &&
4064 element.hasAttribute('async') &&
4065 !element.hasAttribute('itemprop')
4066 ) {
4067 // This is an async script resource
4068 break;
4069 }
4070 }
4071 return element;
4072 }
4073 default: {
4074 // We have excluded the most likely cases of mismatch between hoistable tags, 3rd party script inserted tags,
4075 // and browser extension inserted tags. While it is possible this is not the right match it is a decent hueristic
4076 // that should work in the vast majority of cases.
4077 return element;
4078 }
4079 }
4080 }
4081 const nextInstance = getNextHydratableSibling(element);
4082 if (nextInstance === null) {
4083 break;
4084 }
4085 instance = nextInstance;
4086 }
4087 // This is a suspense boundary or Text node or we got the end.
4088 // Suspense Boundaries are never expected to be injected by 3rd parties. If we see one it should be matched
4089 // and this is a hydration error.
4090 // Text Nodes are also not expected to be injected by 3rd parties. This is less of a guarantee for <body>
4091 // but it seems reasonable and conservative to reject this as a hydration error as well
4092 return null;
4093 }
4094
4095 export function canHydrateTextInstance(
4096 instance: HydratableInstance,
4097 text: string,
4098 inRootOrSingleton: boolean,
4099 ): null | TextInstance {
4100 // Empty strings are not parsed by HTML so there won't be a correct match here.
4101 if (text === '') return null;
4102
4103 while (instance.nodeType !== TEXT_NODE) {
4104 if (
4105 instance.nodeType === ELEMENT_NODE &&
4106 instance.nodeName === 'INPUT' &&
4107 (instance as any).type === 'hidden'
4108 ) {
4109 // If we have extra hidden inputs, we don't mismatch. This allows us to
4110 // embed extra form data in the original form.
4111 } else if (!inRootOrSingleton) {
4112 return null;
4113 }
4114 const nextInstance = getNextHydratableSibling(instance);
4115 if (nextInstance === null) {
4116 return null;
4117 }
4118 instance = nextInstance;
4119 }
4120 // This has now been refined to a text node.
4121 return instance as any as TextInstance;
4122 }
4123
4124 function canHydrateHydrationBoundary(
4125 instance: HydratableInstance,
4126 inRootOrSingleton: boolean,
4127 ): null | SuspenseInstance | ActivityInstance {
4128 while (instance.nodeType !== COMMENT_NODE) {
4129 if (
4130 instance.nodeType === ELEMENT_NODE &&
4131 instance.nodeName === 'INPUT' &&
4132 (instance as any).type === 'hidden'
4133 ) {
4134 // If we have extra hidden inputs, we don't mismatch. This allows us to
4135 // embed extra form data in the original form.
4136 } else if (!inRootOrSingleton) {
4137 return null;
4138 }
4139 const nextInstance = getNextHydratableSibling(instance);
4140 if (nextInstance === null) {
4141 return null;
4142 }
4143 instance = nextInstance;
4144 }
4145 // This has now been refined to a hydration boundary node.
4146 return instance as any;
4147 }
4148
4149 export function canHydrateActivityInstance(
4150 instance: HydratableInstance,
4151 inRootOrSingleton: boolean,
4152 ): null | ActivityInstance {
4153 const hydratableInstance = canHydrateHydrationBoundary(
4154 instance,
4155 inRootOrSingleton,
4156 );
4157 if (
4158 hydratableInstance !== null &&
4159 hydratableInstance.data === ACTIVITY_START_DATA
4160 ) {
4161 return hydratableInstance as any;
4162 }
4163 return null;
4164 }
4165
4166 export function canHydrateSuspenseInstance(
4167 instance: HydratableInstance,
4168 inRootOrSingleton: boolean,
4169 ): null | SuspenseInstance {
4170 const hydratableInstance = canHydrateHydrationBoundary(
4171 instance,
4172 inRootOrSingleton,
4173 );
4174 if (
4175 hydratableInstance !== null &&
4176 hydratableInstance.data !== ACTIVITY_START_DATA
4177 ) {
4178 return hydratableInstance as any;
4179 }
4180 return null;
4181 }
4182
4183 export function isSuspenseInstancePending(instance: SuspenseInstance): boolean {
4184 return (
4185 instance.data === SUSPENSE_PENDING_START_DATA ||
4186 instance.data === SUSPENSE_QUEUED_START_DATA
4187 );
4188 }
4189
4190 export function isSuspenseInstanceFallback(
4191 instance: SuspenseInstance,
4192 ): boolean {
4193 return (
4194 instance.data === SUSPENSE_FALLBACK_START_DATA ||
4195 (instance.data === SUSPENSE_PENDING_START_DATA &&
4196 instance.ownerDocument.readyState !== DOCUMENT_READY_STATE_LOADING)
4197 );
4198 }
4199
4200 export function getSuspenseInstanceFallbackErrorDetails(
4201 instance: SuspenseInstance,
4202 ): {
4203 digest: ?string,
4204 message?: string,
4205 stack?: string,
4206 componentStack?: string,
4207 } {
4208 const dataset =
4209 instance.nextSibling &&
4210 (instance.nextSibling as any as HTMLElement).dataset;
4211 let digest, message, stack, componentStack;
4212 if (dataset) {
4213 digest = dataset.dgst;
4214 if (__DEV__) {
4215 message = dataset.msg;
4216 stack = dataset.stck;
4217 componentStack = dataset.cstck;
4218 }
4219 }
4220 if (__DEV__) {
4221 return {
4222 message,
4223 digest,
4224 stack,
4225 componentStack,
4226 };
4227 } else {
4228 // Object gets DCE'd if constructed in tail position and matches callsite destructuring
4229 return {
4230 digest,
4231 };
4232 }
4233 }
4234
4235 export function registerSuspenseInstanceRetry(
4236 instance: SuspenseInstance,
4237 callback: () => void,
4238 ) {
4239 const ownerDocument = instance.ownerDocument;
4240 if (instance.data === SUSPENSE_QUEUED_START_DATA) {
4241 // The Fizz runtime has already queued this boundary for reveal. We wait for it
4242 // to be revealed and then retries.
4243 instance._reactRetry = callback;
4244 } else if (
4245 // The Fizz runtime must have put this boundary into client render or complete
4246 // state after the render finished but before it committed. We need to call the
4247 // callback now rather than wait
4248 instance.data !== SUSPENSE_PENDING_START_DATA ||
4249 // The boundary is still in pending status but the document has finished loading
4250 // before we could register the event handler that would have scheduled the retry
4251 // on load so we call the callback now.
4252 ownerDocument.readyState !== DOCUMENT_READY_STATE_LOADING
4253 ) {
4254 callback();
4255 } else {
4256 // We're still in pending status and the document is still loading so we attach
4257 // a listener to the document load even and expose the retry on the instance for
4258 // the Fizz runtime to trigger if it ends up resolving this boundary
4259 const listener = () => {
4260 callback();
4261 ownerDocument.removeEventListener('DOMContentLoaded', listener);
4262 };
4263 ownerDocument.addEventListener('DOMContentLoaded', listener);
4264 instance._reactRetry = listener;
4265 }
4266 }
4267
4268 export function canHydrateFormStateMarker(
4269 instance: HydratableInstance,
4270 inRootOrSingleton: boolean,
4271 ): null | FormStateMarkerInstance {
4272 while (instance.nodeType !== COMMENT_NODE) {
4273 if (!inRootOrSingleton) {
4274 return null;
4275 }
4276 const nextInstance = getNextHydratableSibling(instance);
4277 if (nextInstance === null) {
4278 return null;
4279 }
4280 instance = nextInstance;
4281 }
4282 const nodeData = (instance as any).data;
4283 if (
4284 nodeData === FORM_STATE_IS_MATCHING ||
4285 nodeData === FORM_STATE_IS_NOT_MATCHING
4286 ) {
4287 const markerInstance: FormStateMarkerInstance = instance as any;
4288 return markerInstance;
4289 }
4290 return null;
4291 }
4292
4293 export function isFormStateMarkerMatching(
4294 markerInstance: FormStateMarkerInstance,
4295 ): boolean {
4296 return markerInstance.data === FORM_STATE_IS_MATCHING;
4297 }
4298
4299 function getNextHydratable(node: ?Node) {
4300 // Skip non-hydratable nodes.
4301 for (; node != null; node = (node as any as Node).nextSibling) {
4302 const nodeType = node.nodeType;
4303 if (nodeType === ELEMENT_NODE || nodeType === TEXT_NODE) {
4304 break;
4305 }
4306 if (nodeType === COMMENT_NODE) {
4307 const data = (node as any).data;
4308 if (
4309 data === SUSPENSE_START_DATA ||
4310 data === SUSPENSE_FALLBACK_START_DATA ||
4311 data === SUSPENSE_PENDING_START_DATA ||
4312 data === SUSPENSE_QUEUED_START_DATA ||
4313 data === ACTIVITY_START_DATA ||
4314 data === FORM_STATE_IS_MATCHING ||
4315 data === FORM_STATE_IS_NOT_MATCHING
4316 ) {
4317 break;
4318 }
4319 if (data === SUSPENSE_END_DATA || data === ACTIVITY_END_DATA) {
4320 return null;
4321 }
4322 }
4323 }
4324 return node as any;
4325 }
4326
4327 export function getNextHydratableSibling(
4328 instance: HydratableInstance,
4329 ): null | HydratableInstance {
4330 return getNextHydratable(instance.nextSibling);
4331 }
4332
4333 export function getFirstHydratableChild(
4334 parentInstance: Instance,
4335 ): null | HydratableInstance {
4336 return getNextHydratable(parentInstance.firstChild);
4337 }
4338
4339 export function getFirstHydratableChildWithinContainer(
4340 parentContainer: Container,
4341 ): null | HydratableInstance {
4342 let parentElement: Element;
4343 switch (parentContainer.nodeType) {
4344 case DOCUMENT_NODE:
4345 parentElement = (parentContainer as any).body;
4346 break;
4347 default: {
4348 if (parentContainer.nodeName === 'HTML') {
4349 parentElement = (parentContainer as any).ownerDocument.body;
4350 } else {
4351 parentElement = parentContainer as any;
4352 }
4353 }
4354 }
4355 return getNextHydratable(parentElement.firstChild);
4356 }
4357
4358 export function getFirstHydratableChildWithinActivityInstance(
4359 parentInstance: ActivityInstance,
4360 ): null | HydratableInstance {
4361 return getNextHydratable(parentInstance.nextSibling);
4362 }
4363
4364 export function getFirstHydratableChildWithinSuspenseInstance(
4365 parentInstance: SuspenseInstance,
4366 ): null | HydratableInstance {
4367 return getNextHydratable(parentInstance.nextSibling);
4368 }
4369
4370 // If it were possible to have more than one scope singleton in a DOM tree
4371 // we would need to model this as a stack but since you can only have one <head>
4372 // and head is the only singleton that is a scope in DOM we can get away with
4373 // tracking this as a single value.
4374 let previousHydratableOnEnteringScopedSingleton: null | HydratableInstance =
4375 null;
4376
4377 export function getFirstHydratableChildWithinSingleton(
4378 type: string,
4379 singletonInstance: Instance,
4380 currentHydratableInstance: null | HydratableInstance,
4381 ): null | HydratableInstance {
4382 if (isSingletonScope(type)) {
4383 previousHydratableOnEnteringScopedSingleton = currentHydratableInstance;
4384 return getNextHydratable(singletonInstance.firstChild);
4385 } else {
4386 return currentHydratableInstance;
4387 }
4388 }
4389
4390 export function getNextHydratableSiblingAfterSingleton(
4391 type: string,
4392 currentHydratableInstance: null | HydratableInstance,
4393 ): null | HydratableInstance {
4394 if (isSingletonScope(type)) {
4395 const previousHydratableInstance =
4396 previousHydratableOnEnteringScopedSingleton;
4397 previousHydratableOnEnteringScopedSingleton = null;
4398 return previousHydratableInstance;
4399 } else {
4400 return currentHydratableInstance;
4401 }
4402 }
4403
4404 export function describeHydratableInstanceForDevWarnings(
4405 instance: HydratableInstance,
4406 ): string | {type: string, props: $ReadOnly<Props>} {
4407 // Reverse engineer a pseudo react-element from hydratable instance
4408 if (instance.nodeType === ELEMENT_NODE) {
4409 // Reverse engineer a set of props that can print for dev warnings
4410 return {
4411 type: instance.nodeName.toLowerCase(),
4412 props: getPropsFromElement(instance as any),
4413 };
4414 } else if (instance.nodeType === COMMENT_NODE) {
4415 if (instance.data === ACTIVITY_START_DATA) {
4416 return {
4417 type: 'Activity',
4418 props: {},
4419 };
4420 }
4421 return {
4422 type: 'Suspense',
4423 props: {},
4424 };
4425 } else {
4426 return instance.nodeValue;
4427 }
4428 }
4429
4430 export function validateHydratableInstance(
4431 type: string,
4432 props: Props,
4433 hostContext: HostContext,
4434 ): boolean {
4435 if (__DEV__) {
4436 // TODO: take namespace into account when validating.
4437 const hostContextDev: HostContextDev = hostContext as any;
4438 return validateDOMNesting(type, hostContextDev.ancestorInfo);
4439 }
4440 return true;
4441 }
4442
4443 export function hydrateInstance(
4444 instance: Instance,
4445 type: string,
4446 props: Props,
4447 hostContext: HostContext,
4448 internalInstanceHandle: Object,
4449 ): boolean {
4450 precacheFiberNode(internalInstanceHandle, instance);
4451 // TODO: Possibly defer this until the commit phase where all the events
4452 // get attached.
4453 updateFiberProps(instance, props);
4454
4455 return hydrateProperties(instance, type, props, hostContext);
4456 }
4457
4458 // Returns a Map of properties that were different on the server.
4459 export function diffHydratedPropsForDevWarnings(
4460 instance: Instance,
4461 type: string,
4462 props: Props,
4463 hostContext: HostContext,
4464 ): null | $ReadOnly<Props> {
4465 return diffHydratedProperties(instance, type, props, hostContext);
4466 }
4467
4468 export function validateHydratableTextInstance(
4469 text: string,
4470 hostContext: HostContext,
4471 ): boolean {
4472 if (__DEV__) {
4473 const hostContextDev = hostContext as any as HostContextDev;
4474 const ancestor = hostContextDev.ancestorInfo.current;
4475 if (ancestor != null) {
4476 return validateTextNesting(
4477 text,
4478 ancestor.tag,
4479 hostContextDev.ancestorInfo.implicitRootScope,
4480 );
4481 }
4482 }
4483 return true;
4484 }
4485
4486 export function hydrateTextInstance(
4487 textInstance: TextInstance,
4488 text: string,
4489 internalInstanceHandle: Object,
4490 parentInstanceProps: null | Props,
4491 ): boolean {
4492 precacheFiberNode(internalInstanceHandle, textInstance);
4493
4494 return hydrateText(textInstance, text, parentInstanceProps);
4495 }
4496
4497 // Returns the server text if it differs from the client.
4498 export function diffHydratedTextForDevWarnings(
4499 textInstance: TextInstance,
4500 text: string,
4501 parentProps: null | Props,
4502 ): null | string {
4503 if (
4504 parentProps === null ||
4505 parentProps[SUPPRESS_HYDRATION_WARNING] !== true
4506 ) {
4507 return diffHydratedText(textInstance, text);
4508 }
4509 return null;
4510 }
4511
4512 export function hydrateActivityInstance(
4513 activityInstance: ActivityInstance,
4514 internalInstanceHandle: Object,
4515 ) {
4516 precacheFiberNode(internalInstanceHandle, activityInstance);
4517 }
4518
4519 export function hydrateSuspenseInstance(
4520 suspenseInstance: SuspenseInstance,
4521 internalInstanceHandle: Object,
4522 ) {
4523 precacheFiberNode(internalInstanceHandle, suspenseInstance);
4524 }
4525
4526 function getNextHydratableInstanceAfterHydrationBoundary(
4527 hydrationInstance: SuspenseInstance | ActivityInstance,
4528 ): null | HydratableInstance {
4529 let node = hydrationInstance.nextSibling;
4530 // Skip past all nodes within this suspense boundary.
4531 // There might be nested nodes so we need to keep track of how
4532 // deep we are and only break out when we're back on top.
4533 let depth = 0;
4534 while (node) {
4535 if (node.nodeType === COMMENT_NODE) {
4536 const data = (node as any).data as string;
4537 if (data === SUSPENSE_END_DATA || data === ACTIVITY_END_DATA) {
4538 if (depth === 0) {
4539 return getNextHydratableSibling(node as any);
4540 } else {
4541 depth--;
4542 }
4543 } else if (
4544 data === SUSPENSE_START_DATA ||
4545 data === SUSPENSE_FALLBACK_START_DATA ||
4546 data === SUSPENSE_PENDING_START_DATA ||
4547 data === SUSPENSE_QUEUED_START_DATA ||
4548 data === ACTIVITY_START_DATA
4549 ) {
4550 depth++;
4551 }
4552 }
4553 node = node.nextSibling;
4554 }
4555 // TODO: Warn, we didn't find the end comment boundary.
4556 return null;
4557 }
4558
4559 export function getNextHydratableInstanceAfterActivityInstance(
4560 activityInstance: ActivityInstance,
4561 ): null | HydratableInstance {
4562 return getNextHydratableInstanceAfterHydrationBoundary(activityInstance);
4563 }
4564
4565 export function getNextHydratableInstanceAfterSuspenseInstance(
4566 suspenseInstance: SuspenseInstance,
4567 ): null | HydratableInstance {
4568 return getNextHydratableInstanceAfterHydrationBoundary(suspenseInstance);
4569 }
4570
4571 // Returns the SuspenseInstance if this node is a direct child of a
4572 // SuspenseInstance. I.e. if its previous sibling is a Comment with
4573 // SUSPENSE_x_START_DATA. Otherwise, null.
4574 export function getParentHydrationBoundary(
4575 targetInstance: Node,
4576 ): null | SuspenseInstance | ActivityInstance {
4577 let node = targetInstance.previousSibling;
4578 // Skip past all nodes within this suspense boundary.
4579 // There might be nested nodes so we need to keep track of how
4580 // deep we are and only break out when we're back on top.
4581 let depth = 0;
4582 while (node) {
4583 if (node.nodeType === COMMENT_NODE) {
4584 const data = (node as any).data as string;
4585 if (
4586 data === SUSPENSE_START_DATA ||
4587 data === SUSPENSE_FALLBACK_START_DATA ||
4588 data === SUSPENSE_PENDING_START_DATA ||
4589 data === SUSPENSE_QUEUED_START_DATA ||
4590 data === ACTIVITY_START_DATA
4591 ) {
4592 if (depth === 0) {
4593 return node as any as SuspenseInstance | ActivityInstance;
4594 } else {
4595 depth--;
4596 }
4597 } else if (data === SUSPENSE_END_DATA || data === ACTIVITY_END_DATA) {
4598 depth++;
4599 }
4600 }
4601 node = node.previousSibling;
4602 }
4603 return null;
4604 }
4605
4606 export function commitHydratedContainer(container: Container): void {
4607 // Retry if any event replaying was blocked on this.
4608 retryIfBlockedOn(container);
4609 }
4610
4611 export function commitHydratedActivityInstance(
4612 activityInstance: ActivityInstance,
4613 ): void {
4614 // Retry if any event replaying was blocked on this.
4615 retryIfBlockedOn(activityInstance);
4616 }
4617
4618 export function commitHydratedSuspenseInstance(
4619 suspenseInstance: SuspenseInstance,
4620 ): void {
4621 // Retry if any event replaying was blocked on this.
4622 retryIfBlockedOn(suspenseInstance);
4623 }
4624
4625 export function flushHydrationEvents(): void {
4626 if (enableHydrationChangeEvent) {
4627 flushEventReplaying();
4628 }
4629 }
4630
4631 export function shouldDeleteUnhydratedTailInstances(
4632 parentType: string,
4633 ): boolean {
4634 return parentType !== 'form' && parentType !== 'button';
4635 }
4636
4637 // -------------------
4638 // Test Selectors
4639 // -------------------
4640
4641 export const supportsTestSelectors = true;
4642
4643 export function findFiberRoot(node: Instance): null | FiberRoot {
4644 const stack = [node];
4645 let index = 0;
4646 while (index < stack.length) {
4647 const current = stack[index++];
4648 if (isContainerMarkedAsRoot(current)) {
4649 return getInstanceFromNodeDOMTree(current) as any as FiberRoot;
4650 }
4651 stack.push(...current.children);
4652 }
4653 return null;
4654 }
4655
4656 export function getBoundingRect(node: Instance): BoundingRect {
4657 const rect = node.getBoundingClientRect();
4658 return {
4659 x: rect.left,
4660 y: rect.top,
4661 width: rect.width,
4662 height: rect.height,
4663 };
4664 }
4665
4666 export function matchAccessibilityRole(node: Instance, role: string): boolean {
4667 if (hasRole(node, role)) {
4668 return true;
4669 }
4670
4671 return false;
4672 }
4673
4674 export function getTextContent(fiber: Fiber): string | null {
4675 switch (fiber.tag) {
4676 case HostHoistable:
4677 case HostSingleton:
4678 case HostComponent:
4679 let textContent = '';
4680 const childNodes = fiber.stateNode.childNodes;
4681 for (let i = 0; i < childNodes.length; i++) {
4682 const childNode = childNodes[i];
4683 if (childNode.nodeType === Node.TEXT_NODE) {
4684 textContent += childNode.textContent;
4685 }
4686 }
4687 return textContent;
4688 case HostText:
4689 return fiber.stateNode.textContent;
4690 }
4691
4692 return null;
4693 }
4694
4695 export function isHiddenSubtree(fiber: Fiber): boolean {
4696 return fiber.tag === HostComponent && fiber.memoizedProps.hidden === true;
4697 }
4698
4699 export function setFocusIfFocusable(
4700 node: Instance,
4701 focusOptions?: FocusOptions,
4702 ): boolean {
4703 // The logic for determining if an element is focusable is kind of complex,
4704 // and since we want to actually change focus anyway- we can just skip it.
4705 // Instead we'll just listen for a "focus" event to verify that focus was set.
4706 //
4707 // We could compare the node to document.activeElement after focus,
4708 // but this would not handle the case where application code managed focus to automatically blur.
4709 const element = node as any as HTMLElement;
4710
4711 // If this element is already the active element, it's focusable and already
4712 // focused. Calling .focus() on it would be a no-op (no focus event fires),
4713 // so we short-circuit here.
4714 if (element.ownerDocument.activeElement === element) {
4715 return true;
4716 }
4717
4718 let didFocus = false;
4719 const handleFocus = () => {
4720 didFocus = true;
4721 };
4722
4723 try {
4724 // Listen on the document in the capture phase so we detect focus even when
4725 // it lands on a different element than the one we called .focus() on. This
4726 // happens with <label> elements (focus delegates to the associated input)
4727 // and shadow hosts with delegatesFocus.
4728 element.ownerDocument.addEventListener('focus', handleFocus, true);
4729 // $FlowFixMe[method-unbinding]
4730 (element.focus || HTMLElement.prototype.focus).call(element, focusOptions);
4731 } finally {
4732 element.ownerDocument.removeEventListener('focus', handleFocus, true);
4733 }
4734
4735 return didFocus;
4736 }
4737
4738 type RectRatio = {
4739 ratio: number,
4740 rect: BoundingRect,
4741 };
4742
4743 export function setupIntersectionObserver(
4744 targets: Array<Instance>,
4745 callback: ObserveVisibleRectsCallback,
4746 options?: IntersectionObserverOptions,
4747 ): {
4748 disconnect: () => void,
4749 observe: (instance: Instance) => void,
4750 unobserve: (instance: Instance) => void,
4751 } {
4752 const rectRatioCache: Map<Instance, RectRatio> = new Map();
4753 targets.forEach(target => {
4754 rectRatioCache.set(target, {
4755 rect: getBoundingRect(target),
4756 ratio: 0,
4757 });
4758 });
4759
4760 const handleIntersection = (entries: Array<IntersectionObserverEntry>) => {
4761 entries.forEach(entry => {
4762 const {boundingClientRect, intersectionRatio, target} = entry;
4763 rectRatioCache.set(target, {
4764 rect: {
4765 x: boundingClientRect.left,
4766 y: boundingClientRect.top,
4767 width: boundingClientRect.width,
4768 height: boundingClientRect.height,
4769 },
4770 ratio: intersectionRatio,
4771 });
4772 });
4773
4774 callback(Array.from(rectRatioCache.values()));
4775 };
4776
4777 const observer = new IntersectionObserver(handleIntersection, options);
4778 targets.forEach(target => {
4779 observer.observe(target as any);
4780 });
4781
4782 return {
4783 disconnect: () => observer.disconnect(),
4784 observe: target => {
4785 rectRatioCache.set(target, {
4786 rect: getBoundingRect(target),
4787 ratio: 0,
4788 });
4789 observer.observe(target as any);
4790 },
4791 unobserve: target => {
4792 rectRatioCache.delete(target);
4793 observer.unobserve(target as any);
4794 },
4795 };
4796 }
4797
4798 export function requestPostPaintCallback(callback: (time: number) => void) {
4799 localRequestAnimationFrame(() => {
4800 localRequestAnimationFrame(time => callback(time));
4801 });
4802 }
4803
4804 // -------------------
4805 // Singletons
4806 // -------------------
4807
4808 export const supportsSingletons = true;
4809
4810 export function isHostSingletonType(type: string): boolean {
4811 return type === 'html' || type === 'head' || type === 'body';
4812 }
4813
4814 export function resolveSingletonInstance(
4815 type: string,
4816 props: Props,
4817 rootContainerInstance: Container,
4818 hostContext: HostContext,
4819 validateDOMNestingDev: boolean,
4820 ): Instance {
4821 if (__DEV__) {
4822 const hostContextDev = hostContext as any as HostContextDev;
4823 if (validateDOMNestingDev) {
4824 validateDOMNesting(type, hostContextDev.ancestorInfo);
4825 }
4826 }
4827 const ownerDocument = getOwnerDocumentFromRootContainer(
4828 rootContainerInstance,
4829 );
4830 switch (type) {
4831 case 'html': {
4832 const documentElement = ownerDocument.documentElement;
4833 if (!documentElement) {
4834 throw new Error(
4835 'React expected an <html> element (document.documentElement) to exist in the Document but one was' +
4836 ' not found. React never removes the documentElement for any Document it renders into so' +
4837 ' the cause is likely in some other script running on this page.',
4838 );
4839 }
4840 return documentElement;
4841 }
4842 case 'head': {
4843 const head = ownerDocument.head;
4844 if (!head) {
4845 throw new Error(
4846 'React expected a <head> element (document.head) to exist in the Document but one was' +
4847 ' not found. React never removes the head for any Document it renders into so' +
4848 ' the cause is likely in some other script running on this page.',
4849 );
4850 }
4851 return head;
4852 }
4853 case 'body': {
4854 const body = ownerDocument.body;
4855 if (!body) {
4856 throw new Error(
4857 'React expected a <body> element (document.body) to exist in the Document but one was' +
4858 ' not found. React never removes the body for any Document it renders into so' +
4859 ' the cause is likely in some other script running on this page.',
4860 );
4861 }
4862 return body;
4863 }
4864 default: {
4865 throw new Error(
4866 'resolveSingletonInstance was called with an element type that is not supported. This is a bug in React.',
4867 );
4868 }
4869 }
4870 }
4871
4872 export function acquireSingletonInstance(
4873 type: string,
4874 props: Props,
4875 instance: Instance,
4876 internalInstanceHandle: Object,
4877 ): void {
4878 if (__DEV__) {
4879 if (
4880 // If this instance is the container then it is invalid to acquire it as a singleton however
4881 // the DOM nesting validation will already warn for this and the message below isn't semantically
4882 // aligned with the actual fix you need to make so we omit the warning in this case
4883 !isContainerMarkedAsRoot(instance) &&
4884 // If this instance isn't the root but is currently owned by a different HostSingleton instance then
4885 // we we need to warn that you are rendering more than one singleton at a time.
4886 getInstanceFromNodeDOMTree(instance)
4887 ) {
4888 const tagName = instance.tagName.toLowerCase();
4889 console.error(
4890 'You are mounting a new %s component when a previous one has not first unmounted. It is an' +
4891 ' error to render more than one %s component at a time and attributes and children of these' +
4892 ' components will likely fail in unpredictable ways. Please only render a single instance of' +
4893 ' <%s> and if you need to mount a new one, ensure any previous ones have unmounted first.',
4894 tagName,
4895 tagName,
4896 tagName,
4897 );
4898 }
4899 switch (type) {
4900 case 'html':
4901 case 'head':
4902 case 'body': {
4903 break;
4904 }
4905 default: {
4906 console.error(
4907 'acquireSingletonInstance was called with an element type that is not supported. This is a bug in React.',
4908 );
4909 }
4910 }
4911 }
4912
4913 const attributes = instance.attributes;
4914 while (attributes.length) {
4915 instance.removeAttributeNode(attributes[0]);
4916 }
4917
4918 setInitialProperties(instance, type, props);
4919 precacheFiberNode(internalInstanceHandle, instance);
4920 updateFiberProps(instance, props);
4921 }
4922
4923 export function releaseSingletonInstance(
4924 instance: Instance,
4925 type: SingletonType,
4926 props: Props,
4927 ): void {
4928 // Remove the attributes and property-backed state owned by this Fiber.
4929 clearSingletonProperties(instance, type, props);
4930
4931 // These properties aren't cleared by updateProperties when their next
4932 // value is null. Normally that is handled by replacing/removing the host
4933 // instance, but a singleton cannot be removed.
4934 // TODO: HostSingleton updates do not currently schedule ContentReset when
4935 // dangerouslySetInnerHTML becomes undefined, so an ordinary update can leave
4936 // the previous HTML in place. This only handles the release path.
4937 if (props.dangerouslySetInnerHTML != null) {
4938 instance.textContent = '';
4939 }
4940 clearClickListener(instance as any as HTMLElement);
4941
4942 // Only remove state that was represented by this Fiber's props. Attributes
4943 // added imperatively while React owned the singleton must be preserved.
4944 detachDeletedInstance(instance);
4945 }
4946
4947 function clearSingletonPreambleContribution(instance: Instance): void {
4948 // This path is only used when clearing a dehydrated boundary that contains a
4949 // Fizz preamble contribution marker. The marker tells us which singleton the
4950 // boundary contributed to, but it does not include the contributed props and
4951 // there is no HostSingleton Fiber to provide them. We therefore cannot tell
4952 // which attributes came from React and which were added imperatively by a
4953 // script or third party. For now, clearing every attribute is an accepted
4954 // edge case.
4955 // TODO: Include the contributed properties in the marker so this cleanup can
4956 // remove only the attributes owned by the boundary.
4957 const attributes = instance.attributes;
4958 while (attributes.length) {
4959 instance.removeAttributeNode(attributes[0]);
4960 }
4961 detachDeletedInstance(instance);
4962 }
4963
4964 // -------------------
4965 // Resources
4966 // -------------------
4967
4968 export const supportsResources = true;
4969
4970 type HoistableTagType = 'link' | 'meta' | 'title';
4971 type TResource<
4972 T: 'stylesheet' | 'style' | 'script' | 'void',
4973 S: null | {...},
4974 > = {
4975 type: T,
4976 instance: null | Instance,
4977 count: number,
4978 state: S,
4979 };
4980 type StylesheetResource = TResource<'stylesheet', StylesheetState>;
4981 type StyleTagResource = TResource<'style', null>;
4982 type StyleResource = StyleTagResource | StylesheetResource;
4983 type ScriptResource = TResource<'script', null>;
4984 type VoidResource = TResource<'void', null>;
4985 export type Resource = StyleResource | ScriptResource | VoidResource;
4986
4987 type LoadingState = number;
4988 const NotLoaded = /* */ 0b000;
4989 const Loaded = /* */ 0b001;
4990 const Errored = /* */ 0b010;
4991 const Settled = /* */ 0b011;
4992 const Inserted = /* */ 0b100;
4993
4994 type StylesheetState = {
4995 loading: LoadingState,
4996 preload: null | HTMLLinkElement,
4997 };
4998
4999 type StyleTagProps = {
5000 'data-href': string,
Showing first 5,000 of 6,891 lines. View raw