diff --git a/fixtures/view-transition/src/components/Page.js b/fixtures/view-transition/src/components/Page.js
index d12ab56203..d7acc93c0c 100644
--- a/fixtures/view-transition/src/components/Page.js
+++ b/fixtures/view-transition/src/components/Page.js
@@ -2,6 +2,8 @@ import React, {
unstable_ViewTransition as ViewTransition,
unstable_Activity as Activity,
unstable_useSwipeTransition as useSwipeTransition,
+ useEffect,
+ useState,
} from 'react';
import SwipeRecognizer from './SwipeRecognizer';
@@ -53,6 +55,13 @@ export default function Page({url, navigate}) {
navigate(show ? '/?a' : '/?b');
}
+ const [counter, setCounter] = useState(0);
+
+ useEffect(() => {
+ const timer = setInterval(() => setCounter(c => c + 1), 1000);
+ return () => clearInterval(timer);
+ }, []);
+
const exclamation = (
!
@@ -76,7 +85,7 @@ export default function Page({url, navigate}) {
'navigation-back': transitions['slide-right'],
'navigation-forward': transitions['slide-left'],
}}>
- {!show ? 'A' : 'B'}
+ {!show ? 'A' + counter : 'B' + counter}
{show ? (
diff --git a/packages/react-art/src/ReactFiberConfigART.js b/packages/react-art/src/ReactFiberConfigART.js
index dc746b0e17..1ca506d022 100644
--- a/packages/react-art/src/ReactFiberConfigART.js
+++ b/packages/react-art/src/ReactFiberConfigART.js
@@ -302,6 +302,10 @@ export function createInstance(type, props, internalInstanceHandle) {
return instance;
}
+export function cloneMutableInstance(instance, keepChildren) {
+ return instance;
+}
+
export function createTextInstance(
text,
rootContainerInstance,
@@ -310,6 +314,10 @@ export function createTextInstance(
return text;
}
+export function cloneMutableTextInstance(textInstance) {
+ return textInstance;
+}
+
export function finalizeInitialChildren(domElement, type, props) {
return false;
}
@@ -475,6 +483,14 @@ export function restoreRootViewTransitionName(rootContainer) {
// Noop
}
+export function cloneRootViewTransitionContainer(rootContainer) {
+ throw new Error('Not implemented.');
+}
+
+export function removeRootViewTransitionClone(rootContainer, clone) {
+ throw new Error('Not implemented.');
+}
+
export type InstanceMeasurement = null;
export function measureInstance(instance) {
diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
index 2b288efe46..ba79b08c47 100644
--- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
@@ -544,6 +544,13 @@ export function createInstance(
return domElement;
}
+export function cloneMutableInstance(
+ instance: Instance,
+ keepChildren: boolean,
+): Instance {
+ return instance.cloneNode(keepChildren);
+}
+
export function appendInitialChild(
parentInstance: Instance,
child: Instance | TextInstance,
@@ -609,6 +616,12 @@ export function createTextInstance(
return textNode;
}
+export function cloneMutableTextInstance(
+ textInstance: TextInstance,
+): TextInstance {
+ return textInstance.cloneNode(false);
+}
+
let currentPopstateTransitionEvent: Event | null = null;
export function shouldAttemptEagerTransition(): boolean {
const event = window.event;
@@ -1207,10 +1220,24 @@ export function cancelRootViewTransitionName(rootContainer: Container): void {
}
export function restoreRootViewTransitionName(rootContainer: Container): void {
+ let containerInstance: Instance;
+ if (rootContainer.nodeType === DOCUMENT_NODE) {
+ containerInstance = (rootContainer: any).body;
+ } else if (rootContainer.nodeName === 'HTML') {
+ containerInstance = (rootContainer.ownerDocument.body: any);
+ } else {
+ // If the container is not the whole document, then we ideally should probably
+ // clone the whole document outside of the React too.
+ containerInstance = (rootContainer: any);
+ }
+ // $FlowFixMe[prop-missing]
+ if (containerInstance.style.viewTransitionName === 'root') {
+ // If we moved the root view transition name to the container in a gesture
+ // we need to restore it now.
+ containerInstance.style.viewTransitionName = '';
+ }
const documentElement: null | HTMLElement =
- rootContainer.nodeType === DOCUMENT_NODE
- ? (rootContainer: any).documentElement
- : rootContainer.ownerDocument.documentElement;
+ containerInstance.ownerDocument.documentElement;
if (
documentElement !== null &&
// $FlowFixMe[prop-missing]
@@ -1221,6 +1248,226 @@ export function restoreRootViewTransitionName(rootContainer: Container): void {
}
}
+function getComputedTransform(style: CSSStyleDeclaration): string {
+ // Gets the merged transform of all the short hands.
+ const computedStyle: any = style;
+ let transform: string = computedStyle.transform;
+ if (transform === 'none') {
+ transform = '';
+ }
+ const scale: string = computedStyle.scale;
+ if (scale !== 'none' && scale !== '') {
+ const parts = scale.split(' ');
+ transform =
+ (parts.length === 3 ? 'scale3d' : 'scale') +
+ '(' +
+ parts.join(', ') +
+ ') ' +
+ transform;
+ }
+ const rotate: string = computedStyle.rotate;
+ if (rotate !== 'none' && rotate !== '') {
+ const parts = rotate.split(' ');
+ if (parts.length === 1) {
+ transform = 'rotate(' + parts[0] + ') ' + transform;
+ } else if (parts.length === 2) {
+ transform =
+ 'rotate' + parts[0].toUpperCase() + '(' + parts[1] + ') ' + transform;
+ } else {
+ transform = 'rotate3d(' + parts.join(', ') + ') ' + transform;
+ }
+ }
+ const translate: string = computedStyle.translate;
+ if (translate !== 'none' && translate !== '') {
+ const parts = translate.split(' ');
+ transform =
+ (parts.length === 3 ? 'translate3d' : 'translate') +
+ '(' +
+ parts.join(', ') +
+ ') ' +
+ transform;
+ }
+ return transform;
+}
+
+function moveOutOfViewport(
+ originalStyle: CSSStyleDeclaration,
+ element: HTMLElement,
+): void {
+ // Apply a transform that safely puts the whole element outside the viewport
+ // while still letting it paint its "old" state to a snapshot.
+ const transform = getComputedTransform(originalStyle);
+ // Clear the long form properties.
+ // $FlowFixMe
+ element.style.translate = 'none';
+ // $FlowFixMe
+ element.style.scale = 'none';
+ // $FlowFixMe
+ element.style.rotate = 'none';
+ // Apply a translate to move it way out of the viewport. This is applied first
+ // so that it is in the coordinate space of the parent and not after applying
+ // other transforms. That's why we need to merge the long form properties.
+ // TODO: Ideally we'd adjust for the parent's rotate/scale. Otherwise when
+ // we move back the ::view-transition-group we might overshoot or undershoot.
+ element.style.transform = 'translate(-20000px, -20000px) ' + transform;
+}
+
+function moveOldFrameIntoViewport(keyframe: any): void {
+ // In the resulting View Transition Animation, the first frame will be offset.
+ const computedTransform: ?string = keyframe.transform;
+ if (computedTransform != null) {
+ let transform = computedTransform === 'none' ? '' : computedTransform;
+ transform = 'translate(20000px, 20000px) ' + transform;
+ keyframe.transform = transform;
+ }
+}
+
+export function cloneRootViewTransitionContainer(
+ rootContainer: Container,
+): Instance {
+ // This implies that we're not going to animate the root document but instead
+ // the clone so we first clear the name of the root container.
+ const documentElement: null | HTMLElement =
+ rootContainer.nodeType === DOCUMENT_NODE
+ ? (rootContainer: any).documentElement
+ : rootContainer.ownerDocument.documentElement;
+ if (
+ documentElement !== null &&
+ // $FlowFixMe[prop-missing]
+ documentElement.style.viewTransitionName === ''
+ ) {
+ // $FlowFixMe[prop-missing]
+ documentElement.style.viewTransitionName = 'none';
+ }
+
+ let containerInstance: HTMLElement;
+ if (rootContainer.nodeType === DOCUMENT_NODE) {
+ containerInstance = (rootContainer: any).body;
+ } else if (rootContainer.nodeName === 'HTML') {
+ containerInstance = (rootContainer.ownerDocument.body: any);
+ } else {
+ // If the container is not the whole document, then we ideally should probably
+ // clone the whole document outside of the React too.
+ containerInstance = (rootContainer: any);
+ }
+
+ const containerParent = containerInstance.parentNode;
+ if (containerParent === null) {
+ throw new Error('Cannot use a useSwipeTransition() in a detached root.');
+ }
+
+ const clone: HTMLElement = containerInstance.cloneNode(false);
+
+ const computedStyle = getComputedStyle(containerInstance);
+
+ if (
+ computedStyle.position === 'absolute' ||
+ computedStyle.position === 'fixed'
+ ) {
+ // If the style is already absolute, we don't have to do anything because it'll appear
+ // in the same place.
+ } else {
+ // Otherwise we need to absolutely position the clone in the same location as the original.
+ let positionedAncestor: HTMLElement = containerParent;
+ while (
+ positionedAncestor.parentNode != null &&
+ positionedAncestor.parentNode.nodeType !== DOCUMENT_NODE
+ ) {
+ if (getComputedStyle(positionedAncestor).position !== 'static') {
+ break;
+ }
+ // $FlowFixMe: This is refined.
+ positionedAncestor = positionedAncestor.parentNode;
+ }
+
+ const positionedAncestorStyle: any = positionedAncestor.style;
+ const containerInstanceStyle: any = containerInstance.style;
+ // Clear the transform while we're measuring since it affects the bounding client rect.
+ const prevAncestorTranslate = positionedAncestorStyle.translate;
+ const prevAncestorScale = positionedAncestorStyle.scale;
+ const prevAncestorRotate = positionedAncestorStyle.rotate;
+ const prevAncestorTransform = positionedAncestorStyle.transform;
+ const prevTranslate = containerInstanceStyle.translate;
+ const prevScale = containerInstanceStyle.scale;
+ const prevRotate = containerInstanceStyle.rotate;
+ const prevTransform = containerInstanceStyle.transform;
+ positionedAncestorStyle.translate = 'none';
+ positionedAncestorStyle.scale = 'none';
+ positionedAncestorStyle.rotate = 'none';
+ positionedAncestorStyle.transform = 'none';
+ containerInstanceStyle.translate = 'none';
+ containerInstanceStyle.scale = 'none';
+ containerInstanceStyle.rotate = 'none';
+ containerInstanceStyle.transform = 'none';
+
+ const ancestorRect = positionedAncestor.getBoundingClientRect();
+ const rect = containerInstance.getBoundingClientRect();
+
+ const cloneStyle = clone.style;
+ cloneStyle.position = 'absolute';
+ cloneStyle.top = rect.top - ancestorRect.top + 'px';
+ cloneStyle.left = rect.left - ancestorRect.left + 'px';
+ cloneStyle.width = rect.width + 'px';
+ cloneStyle.height = rect.height + 'px';
+ cloneStyle.margin = '0px';
+ cloneStyle.boxSizing = 'border-box';
+
+ positionedAncestorStyle.translate = prevAncestorTranslate;
+ positionedAncestorStyle.scale = prevAncestorScale;
+ positionedAncestorStyle.rotate = prevAncestorRotate;
+ positionedAncestorStyle.transform = prevAncestorTransform;
+ containerInstanceStyle.translate = prevTranslate;
+ containerInstanceStyle.scale = prevScale;
+ containerInstanceStyle.rotate = prevRotate;
+ containerInstanceStyle.transform = prevTransform;
+ }
+
+ // For this transition the container will act as the root. Nothing outside of it should
+ // be affected anyway. This lets us transition from the cloned container to the original.
+ // $FlowFixMe[prop-missing]
+ clone.style.viewTransitionName = 'root';
+
+ // Move out of the viewport so that it's still painted for the snapshot but is not visible
+ // for the frame where the snapshot happens.
+ moveOutOfViewport(computedStyle, clone);
+
+ // Insert the clone after the root container as a sibling. This may inject a body
+ // as the next sibling of an existing body. document.body will still point to the
+ // first one and any id selectors will still find the first one. That's why it's
+ // important that it's after the existing node.
+ containerInstance.parentNode.insertBefore(
+ clone,
+ containerInstance.nextSibling,
+ );
+
+ return clone;
+}
+
+export function removeRootViewTransitionClone(
+ rootContainer: Container,
+ clone: Instance,
+): void {
+ let containerInstance: Instance;
+ if (rootContainer.nodeType === DOCUMENT_NODE) {
+ containerInstance = (rootContainer: any).body;
+ } else if (rootContainer.nodeName === 'HTML') {
+ containerInstance = (rootContainer.ownerDocument.body: any);
+ } else {
+ // If the container is not the whole document, then we ideally should probably
+ // clone the whole document outside of the React too.
+ containerInstance = (rootContainer: any);
+ }
+ const containerParent = containerInstance.parentNode;
+ if (containerParent === null) {
+ throw new Error('Cannot use a useSwipeTransition() in a detached root.');
+ }
+ // We assume that the clone is still within the same parent.
+ containerParent.removeChild(clone);
+
+ // Now the root is on the containerInstance itself until we call restoreRootViewTransitionName.
+ containerInstance.style.viewTransitionName = 'root';
+}
+
export type InstanceMeasurement = {
rect: ClientRect | DOMRect,
abs: boolean, // is absolutely positioned
@@ -1417,8 +1664,127 @@ export type RunningGestureTransition = {
...
};
+function mergeTranslate(translateA: ?string, translateB: ?string): string {
+ if (!translateA || translateA === 'none') {
+ return translateB || '';
+ }
+ if (!translateB || translateB === 'none') {
+ return translateA || '';
+ }
+ const partsA = translateA.split(' ');
+ const partsB = translateB.split(' ');
+ let i;
+ let result = '';
+ for (i = 0; i < partsA.length && i < partsB.length; i++) {
+ if (i > 0) {
+ result += ' ';
+ }
+ result += 'calc(' + partsA[i] + ' + ' + partsB[i] + ')';
+ }
+ for (; i < partsA.length; i++) {
+ result += ' ' + partsA[i];
+ }
+ for (; i < partsB.length; i++) {
+ result += ' ' + partsB[i];
+ }
+ return result;
+}
+
+function animateGesture(
+ keyframes: any,
+ targetElement: Element,
+ pseudoElement: string,
+ timeline: AnimationTimeline,
+ rangeStart: number,
+ rangeEnd: number,
+ moveFirstFrameIntoViewport: boolean,
+ moveAllFramesIntoViewport: boolean,
+) {
+ for (let i = 0; i < keyframes.length; i++) {
+ const keyframe = keyframes[i];
+ // Delete any easing since we always apply linear easing to gestures.
+ delete keyframe.easing;
+ delete keyframe.computedOffset;
+ // Chrome returns "auto" for width/height which is not a valid value to
+ // animate to. Similarly, transform: "none" is actually lack of transform.
+ if (keyframe.width === 'auto') {
+ delete keyframe.width;
+ }
+ if (keyframe.height === 'auto') {
+ delete keyframe.height;
+ }
+ if (keyframe.transform === 'none') {
+ delete keyframe.transform;
+ }
+ if (moveAllFramesIntoViewport) {
+ if (keyframe.transform == null) {
+ // If a transform is not explicitly specified to override the auto
+ // generated one on the pseudo element, then we need to adjust it to
+ // put it back into the viewport. We don't know the offset relative to
+ // the screen so instead we use the translate prop to do a relative
+ // adjustment.
+ // TODO: If the "transform" was manually overridden on the pseudo
+ // element itself and no longer the auto generated one, then we shouldn't
+ // adjust it. I'm not sure how to detect this.
+ if (keyframe.translate == null || keyframe.translate === '') {
+ // TODO: If there's a CSS rule targeting translate on the pseudo element
+ // already we need to merge it.
+ const elementTranslate: ?string = (getComputedStyle(
+ targetElement,
+ pseudoElement,
+ ): any).translate;
+ keyframe.translate = mergeTranslate(
+ elementTranslate,
+ '20000px 20000px',
+ );
+ } else {
+ keyframe.translate = mergeTranslate(
+ keyframe.translate,
+ '20000px 20000px',
+ );
+ }
+ }
+ }
+ }
+ if (moveFirstFrameIntoViewport) {
+ // If this is the generated animation that does a FLIP matrix translation
+ // from the old position, we need to adjust it from the out of viewport
+ // position. If this is going from old to new it only applies to first
+ // keyframe. Otherwise it applies to every keyframe.
+ moveOldFrameIntoViewport(keyframes[0]);
+ }
+ const reverse = rangeStart > rangeEnd;
+ const anim = targetElement.animate(keyframes, {
+ pseudoElement: pseudoElement,
+ // Set the timeline to the current gesture timeline to drive the updates.
+ timeline: timeline,
+ // We reset all easing functions to linear so that it feels like you
+ // have direct impact on the transition and to avoid double bouncing
+ // from scroll bouncing.
+ easing: 'linear',
+ // We fill in both direction for overscroll.
+ fill: 'both',
+ // Range start needs to be higher than range end. If it goes in reverse
+ // we reverse the whole animation below.
+ rangeStart: (reverse ? rangeEnd : rangeStart) + '%',
+ rangeEnd: (reverse ? rangeStart : rangeEnd) + '%',
+ });
+ if (!reverse) {
+ // We play all gestures in reverse, except if we're in reverse direction
+ // in which case we need to play it in reverse of the reverse.
+ anim.reverse();
+ // In Safari, there's a bug where the starting position isn't immediately
+ // picked up from the ScrollTimeline for one frame.
+ // $FlowFixMe[cannot-resolve-name]
+ anim.currentTime = CSS.percent(100);
+ }
+}
+
export function startGestureTransition(
rootContainer: Container,
+ timeline: GestureTimeline,
+ rangeStart: number,
+ rangeEnd: number,
transitionTypes: null | TransitionTypes,
mutationCallback: () => void,
animateCallback: () => void,
@@ -1435,26 +1801,136 @@ export function startGestureTransition(
});
// $FlowFixMe[prop-missing]
ownerDocument.__reactViewTransition = transition;
- let blockingAnim = null;
- const readyCallback = () => {
+ const readyCallback = (x: any) => {
+ const documentElement: Element = (ownerDocument.documentElement: any);
+ // Loop through all View Transition Animations.
+ const animations = documentElement.getAnimations({subtree: true});
+ // First do a pass to collect all known group and new items so we can look
+ // up if they exist later.
+ const foundGroups: Set = new Set();
+ const foundNews: Set = new Set();
+ for (let i = 0; i < animations.length; i++) {
+ // $FlowFixMe
+ const pseudoElement: ?string = animations[i].effect.pseudoElement;
+ if (pseudoElement == null) {
+ } else if (pseudoElement.startsWith('::view-transition-group')) {
+ foundGroups.add(pseudoElement.slice(23));
+ } else if (pseudoElement.startsWith('::view-transition-new')) {
+ // TODO: This is not really a sufficient detection because if the new
+ // pseudo element might exist but have animations disabled on it.
+ foundNews.add(pseudoElement.slice(21));
+ }
+ }
+ for (let i = 0; i < animations.length; i++) {
+ const anim = animations[i];
+ const effect: KeyframeEffect = (anim.effect: any);
+ // $FlowFixMe
+ const pseudoElement: ?string = effect.pseudoElement;
+ if (
+ pseudoElement != null &&
+ pseudoElement.startsWith('::view-transition')
+ ) {
+ // Ideally we could mutate the existing animation but unfortunately
+ // the mutable APIs seem less tested and therefore are lacking or buggy.
+ // Therefore we create a new animation instead.
+ anim.cancel();
+ let isGeneratedGroupAnim = false;
+ let isExitGroupAnim = false;
+ if (pseudoElement.startsWith('::view-transition-group')) {
+ const groupName = pseudoElement.slice(23);
+ if (foundNews.has(groupName)) {
+ // If this has both "new" and "old" state we expect this to be an auto-generated
+ // animation that started outside the viewport. We need to adjust this first frame
+ // to be inside the viewport.
+ // $FlowFixMe[prop-missing]
+ const animationName: ?string = anim.animationName;
+ isGeneratedGroupAnim =
+ animationName != null &&
+ // $FlowFixMe[prop-missing]
+ animationName.startsWith('-ua-view-transition-group-anim-');
+ } else {
+ // If this has only an "old" state then the pseudo element will be outside
+ // the viewport. If any keyframes don't override "transform" we need to
+ // adjust them.
+ isExitGroupAnim = true;
+ }
+ // TODO: If this has only an old state and no new state,
+ }
+ animateGesture(
+ effect.getKeyframes(),
+ // $FlowFixMe: Always documentElement atm.
+ effect.target,
+ pseudoElement,
+ timeline,
+ rangeStart,
+ rangeEnd,
+ isGeneratedGroupAnim,
+ isExitGroupAnim,
+ );
+ if (pseudoElement.startsWith('::view-transition-old')) {
+ const groupName = pseudoElement.slice(21);
+ if (!foundGroups.has(groupName) && !foundNews.has(groupName)) {
+ foundGroups.add(groupName);
+ // We haven't seen any group animation with this name. Since the old
+ // state was outside the viewport we need to put it back. Since we
+ // can't programmatically target the element itself, we use an
+ // animation to adjust it.
+ // This usually happens for exit animations where the element has
+ // the old position.
+ // If we also have a "new" state then we skip this because it means
+ // someone manually disabled the auto-generated animation. We need to
+ // treat the old state as having the position of the "new" state which
+ // will happen by default.
+ const pseudoElementName = '::view-transition-group' + groupName;
+ animateGesture(
+ [{}, {}],
+ // $FlowFixMe: Always documentElement atm.
+ effect.target,
+ pseudoElementName,
+ timeline,
+ rangeStart,
+ rangeEnd,
+ false,
+ true, // We let the helper apply the translate
+ );
+ }
+ }
+ }
+ }
// View Transitions with ScrollTimeline has a quirk where they end if the
// ScrollTimeline ever reaches 100% but that doesn't mean we're done because
// you can swipe back again. We can prevent this by adding a paused Animation
// that never stops. This seems to keep all running Animations alive until
// we explicitly abort (or something forces the View Transition to cancel).
- const documentElement: Element = (ownerDocument.documentElement: any);
- blockingAnim = documentElement.animate([{}, {}], {
+ const blockingAnim = documentElement.animate([{}, {}], {
pseudoElement: '::view-transition',
duration: 1,
});
blockingAnim.pause();
animateCallback();
};
- transition.ready.then(readyCallback, readyCallback);
+ // In Chrome, "new" animations are not ready in the ready callback. We have to wait
+ // until requestAnimationFrame before we can observe them through getAnimations().
+ // However, in Safari, that would cause a flicker because we're applying them late.
+ // TODO: Think of a feature detection for this instead.
+ const readyForAnimations =
+ navigator.userAgent.indexOf('Chrome') !== -1
+ ? () => requestAnimationFrame(readyCallback)
+ : readyCallback;
+ transition.ready.then(readyForAnimations, readyCallback);
transition.finished.then(() => {
- if (blockingAnim !== null) {
- // In Safari, we need to manually clear this or it'll block future transitions.
- blockingAnim.cancel();
+ // In Safari, we need to manually cancel all manually start animations
+ // or it'll block future transitions.
+ const documentElement: Element = (ownerDocument.documentElement: any);
+ const animations = documentElement.getAnimations({subtree: true});
+ for (let i = 0; i < animations.length; i++) {
+ const anim = animations[i];
+ const effect: KeyframeEffect = (anim.effect: any);
+ // $FlowFixMe
+ const pseudo: ?string = effect.pseudoElement;
+ if (pseudo != null && pseudo.startsWith('::view-transition')) {
+ anim.cancel();
+ }
}
// $FlowFixMe[prop-missing]
if (ownerDocument.__reactViewTransition === transition) {
diff --git a/packages/react-native-renderer/src/ReactFiberConfigNative.js b/packages/react-native-renderer/src/ReactFiberConfigNative.js
index 4a9064c82a..5f1b5583a1 100644
--- a/packages/react-native-renderer/src/ReactFiberConfigNative.js
+++ b/packages/react-native-renderer/src/ReactFiberConfigNative.js
@@ -165,6 +165,13 @@ export function createInstance(
return ((component: any): Instance);
}
+export function cloneMutableInstance(
+ instance: Instance,
+ keepChildren: boolean,
+): Instance {
+ throw new Error('Not yet implemented.');
+}
+
export function createTextInstance(
text: string,
rootContainerInstance: Container,
@@ -189,6 +196,12 @@ export function createTextInstance(
return tag;
}
+export function cloneMutableTextInstance(
+ textInstance: TextInstance,
+): TextInstance {
+ throw new Error('Not yet implemented.');
+}
+
export function finalizeInitialChildren(
parentInstance: Instance,
type: string,
@@ -558,6 +571,19 @@ export function restoreRootViewTransitionName(rootContainer: Container): void {
// Not yet implemented
}
+export function cloneRootViewTransitionContainer(
+ rootContainer: Container,
+): Instance {
+ throw new Error('Not implemented.');
+}
+
+export function removeRootViewTransitionClone(
+ rootContainer: Container,
+ clone: Instance,
+): void {
+ throw new Error('Not implemented.');
+}
+
export type InstanceMeasurement = null;
export function measureInstance(instance: Instance): InstanceMeasurement {
@@ -601,6 +627,9 @@ export type RunningGestureTransition = null;
export function startGestureTransition(
rootContainer: Container,
+ timeline: GestureTimeline,
+ rangeStart: number,
+ rangeEnd: number,
transitionTypes: null | TransitionTypes,
mutationCallback: () => void,
animateCallback: () => void,
diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js
index e392ad7b0c..4978dccf2a 100644
--- a/packages/react-noop-renderer/src/createReactNoop.js
+++ b/packages/react-noop-renderer/src/createReactNoop.js
@@ -453,6 +453,10 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
return inst;
},
+ cloneMutableInstance(instance: Instance, keepChildren: boolean): Instance {
+ throw new Error('Not yet implemented.');
+ },
+
appendInitialChild(
parentInstance: Instance,
child: Instance | TextInstance,
@@ -504,6 +508,10 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
return inst;
},
+ cloneMutableTextInstance(textInstance: TextInstance): TextInstance {
+ throw new Error('Not yet implemented.');
+ },
+
scheduleTimeout: setTimeout,
cancelTimeout: clearTimeout,
noTimeout: -1,
@@ -761,6 +769,17 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
restoreRootViewTransitionName(rootContainer: Container): void {},
+ cloneRootViewTransitionContainer(rootContainer: Container): Instance {
+ throw new Error('Not yet implemented.');
+ },
+
+ removeRootViewTransitionClone(
+ rootContainer: Container,
+ clone: Instance,
+ ): void {
+ throw new Error('Not implemented.');
+ },
+
measureInstance(instance: Instance): InstanceMeasurement {
return null;
},
@@ -796,6 +815,9 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
startGestureTransition(
rootContainer: Container,
+ timeline: GestureTimeline,
+ rangeStart: number,
+ rangeEnd: number,
transitionTypes: null | TransitionTypes,
mutationCallback: () => void,
animateCallback: () => void,
diff --git a/packages/react-reconciler/src/ReactFiberApplyGesture.js b/packages/react-reconciler/src/ReactFiberApplyGesture.js
index 46af2ca430..6847a2426c 100644
--- a/packages/react-reconciler/src/ReactFiberApplyGesture.js
+++ b/packages/react-reconciler/src/ReactFiberApplyGesture.js
@@ -9,10 +9,327 @@
import type {Fiber, FiberRoot} from './ReactInternalTypes';
+import type {Instance, TextInstance} from './ReactFiberConfig';
+
+import type {OffscreenState} from './ReactFiberActivityComponent';
+
import {
+ cloneMutableInstance,
+ cloneMutableTextInstance,
+ cloneRootViewTransitionContainer,
+ removeRootViewTransitionClone,
cancelRootViewTransitionName,
restoreRootViewTransitionName,
+ appendChild,
+ commitUpdate,
+ commitTextUpdate,
+ resetTextContent,
+ supportsResources,
+ supportsSingletons,
+ unhideInstance,
+ unhideTextInstance,
} from './ReactFiberConfig';
+import {
+ popMutationContext,
+ pushMutationContext,
+ viewTransitionMutationContext,
+} from './ReactFiberMutationTracking';
+import {
+ MutationMask,
+ Update,
+ ContentReset,
+ NoFlags,
+ Visibility,
+} from './ReactFiberFlags';
+import {
+ HostComponent,
+ HostHoistable,
+ HostSingleton,
+ HostText,
+ HostPortal,
+ OffscreenComponent,
+ ViewTransitionComponent,
+} from './ReactWorkTags';
+
+let didWarnForRootClone = false;
+
+function detectMutationOrInsertClones(finishedWork: Fiber): boolean {
+ return true;
+}
+
+let unhideHostChildren = false;
+
+function recursivelyInsertClonesFromExistingTree(
+ parentFiber: Fiber,
+ hostParentClone: Instance,
+): void {
+ let child = parentFiber.child;
+ while (child !== null) {
+ switch (child.tag) {
+ case HostComponent: {
+ const instance: Instance = child.stateNode;
+ // If we have no mutations in this subtree, we just need to make a deep clone.
+ const clone: Instance = cloneMutableInstance(instance, true);
+ appendChild(hostParentClone, clone);
+ // TODO: We may need to transfer some DOM state such as scroll position
+ // for the deep clones.
+ // TODO: If there's a manual view-transition-name inside the clone we
+ // should ideally remove it from the original and then restore it in mutation
+ // phase. Otherwise it leads to duplicate names.
+ if (unhideHostChildren) {
+ unhideInstance(clone, child.memoizedProps);
+ }
+ break;
+ }
+ case HostText: {
+ const textInstance: TextInstance = child.stateNode;
+ if (textInstance === null) {
+ throw new Error(
+ 'This should have a text node initialized. This error is likely ' +
+ 'caused by a bug in React. Please file an issue.',
+ );
+ }
+ const clone = cloneMutableTextInstance(textInstance);
+ appendChild(hostParentClone, clone);
+ if (unhideHostChildren) {
+ unhideTextInstance(clone, child.memoizedProps);
+ }
+ break;
+ }
+ case HostPortal: {
+ // TODO: Consider what should happen to Portals. For now we exclude them.
+ break;
+ }
+ case OffscreenComponent: {
+ const newState: OffscreenState | null = child.memoizedState;
+ const isHidden = newState !== null;
+ if (!isHidden) {
+ // Only insert clones if this tree is going to be visible. No need to
+ // clone invisible content.
+ // TODO: If this is visible but detached it should still be cloned.
+ // Since there was no mutation to this node, it couldn't have changed
+ // visibility so we don't need to update unhideHostChildren here.
+ recursivelyInsertClonesFromExistingTree(child, hostParentClone);
+ }
+ break;
+ }
+ case ViewTransitionComponent:
+ const prevMutationContext = pushMutationContext();
+ // TODO: If this was already cloned by a previous pass we can reuse those clones.
+ recursivelyInsertClonesFromExistingTree(child, hostParentClone);
+ // TODO: Do we need to track whether this should have a name applied?
+ // child.flags |= Update;
+ popMutationContext(prevMutationContext);
+ break;
+ default: {
+ recursivelyInsertClonesFromExistingTree(child, hostParentClone);
+ break;
+ }
+ }
+ child = child.sibling;
+ }
+}
+
+function recursivelyInsertClones(
+ parentFiber: Fiber,
+ hostParentClone: Instance,
+) {
+ const deletions = parentFiber.deletions;
+ if (deletions !== null) {
+ for (let i = 0; i < deletions.length; i++) {
+ // const childToDelete = deletions[i];
+ // TODO
+ }
+ }
+
+ if (
+ parentFiber.alternate === null ||
+ (parentFiber.subtreeFlags & MutationMask) !== NoFlags
+ ) {
+ // If we have mutations or if this is a newly inserted tree, clone as we go.
+ let child = parentFiber.child;
+ while (child !== null) {
+ insertDestinationClonesOfFiber(child, hostParentClone);
+ child = child.sibling;
+ }
+ } else {
+ // Once we reach a subtree with no more mutations we can bail out.
+ // However, we must still insert deep clones of the HostComponents.
+ recursivelyInsertClonesFromExistingTree(parentFiber, hostParentClone);
+ }
+}
+
+function insertDestinationClonesOfFiber(
+ finishedWork: Fiber,
+ hostParentClone: Instance,
+) {
+ const current = finishedWork.alternate;
+ const flags = finishedWork.flags;
+ // The effect flag should be checked *after* we refine the type of fiber,
+ // because the fiber tag is more specific. An exception is any flag related
+ // to reconciliation, because those can be set on all fiber types.
+ switch (finishedWork.tag) {
+ case HostHoistable: {
+ if (supportsResources) {
+ // TODO: Hoistables should get optimistically inserted and then removed.
+ recursivelyInsertClones(finishedWork, hostParentClone);
+ break;
+ }
+ // Fall through
+ }
+ case HostSingleton: {
+ if (supportsSingletons) {
+ recursivelyInsertClones(finishedWork, hostParentClone);
+ if (__DEV__) {
+ // We cannot apply mutations to Host Singletons since by definition
+ // they cannot be cloned. Therefore we warn in DEV if this commit
+ // had any effect.
+ if (flags & Update) {
+ if (current === null) {
+ console.error(
+ 'useSwipeTransition() caused something to render a new <%s>. ' +
+ 'This is not possible in the current implementation. ' +
+ "Make sure that the swipe doesn't mount any new <%s> elements.",
+ finishedWork.type,
+ finishedWork.type,
+ );
+ } else {
+ const newProps = finishedWork.memoizedProps;
+ const oldProps = current.memoizedProps;
+ const instance = finishedWork.stateNode;
+ const type = finishedWork.type;
+ const prev = pushMutationContext();
+
+ try {
+ // Since we currently don't have a separate diffing algorithm for
+ // individual properties, the Update flag can be a false positive.
+ // We have to apply the new props first o detect any mutations and
+ // then revert them.
+ commitUpdate(instance, type, oldProps, newProps, finishedWork);
+ if (viewTransitionMutationContext) {
+ console.error(
+ 'useSwipeTransition() caused something to mutate <%s>. ' +
+ 'This is not possible in the current implementation. ' +
+ "Make sure that the swipe doesn't update any state which " +
+ 'causes <%s> to change.',
+ finishedWork.type,
+ finishedWork.type,
+ );
+ }
+ // Revert
+ commitUpdate(instance, type, newProps, oldProps, finishedWork);
+ } finally {
+ popMutationContext(prev);
+ }
+ }
+ }
+ }
+ break;
+ }
+ // Fall through
+ }
+ case HostComponent: {
+ const instance: Instance = finishedWork.stateNode;
+ if (current === null) {
+ // For insertions we don't need to clone. It's already new state node.
+ // TODO: Do we need to visit it for ViewTransitions though?
+ appendChild(hostParentClone, instance);
+ } else {
+ let clone: Instance;
+ if (finishedWork.child === null) {
+ // This node is terminal. We still do a deep clone in case this has user
+ // inserted content, text content or dangerouslySetInnerHTML.
+ clone = cloneMutableInstance(instance, true);
+ if (finishedWork.flags & ContentReset) {
+ resetTextContent(clone);
+ }
+ } else {
+ // If we have children we'll clone them as we walk the tree so we just
+ // do a shallow clone here.
+ clone = cloneMutableInstance(instance, false);
+ }
+
+ if (flags & Update) {
+ const newProps = finishedWork.memoizedProps;
+ const oldProps = current.memoizedProps;
+ const type = finishedWork.type;
+ // Apply the delta to the clone.
+ commitUpdate(clone, type, oldProps, newProps, finishedWork);
+ }
+
+ if (unhideHostChildren) {
+ unhideHostChildren = false;
+ recursivelyInsertClones(finishedWork, clone);
+ appendChild(hostParentClone, clone);
+ unhideHostChildren = true;
+ unhideInstance(clone, finishedWork.memoizedProps);
+ } else {
+ recursivelyInsertClones(finishedWork, clone);
+ appendChild(hostParentClone, clone);
+ }
+ }
+ break;
+ }
+ case HostText: {
+ const textInstance: TextInstance = finishedWork.stateNode;
+ if (textInstance === null) {
+ throw new Error(
+ 'This should have a text node initialized. This error is likely ' +
+ 'caused by a bug in React. Please file an issue.',
+ );
+ }
+ if (current === null) {
+ // For insertions we don't need to clone. It's already new state node.
+ appendChild(hostParentClone, textInstance);
+ } else {
+ const clone = cloneMutableTextInstance(textInstance);
+ if (flags & Update) {
+ const newText: string = finishedWork.memoizedProps;
+ const oldText: string = current.memoizedProps;
+ commitTextUpdate(clone, newText, oldText);
+ }
+ appendChild(hostParentClone, clone);
+ if (unhideHostChildren) {
+ unhideTextInstance(clone, finishedWork.memoizedProps);
+ }
+ }
+ break;
+ }
+ case HostPortal: {
+ // TODO: Consider what should happen to Portals. For now we exclude them.
+ break;
+ }
+ case OffscreenComponent: {
+ const newState: OffscreenState | null = finishedWork.memoizedState;
+ const isHidden = newState !== null;
+ if (!isHidden) {
+ // Only insert clones if this tree is going to be visible. No need to
+ // clone invisible content.
+ // TODO: If this is visible but detached it should still be cloned.
+ const prevUnhide = unhideHostChildren;
+ unhideHostChildren = prevUnhide || (flags & Visibility) !== NoFlags;
+ recursivelyInsertClones(finishedWork, hostParentClone);
+ unhideHostChildren = prevUnhide;
+ }
+ break;
+ }
+ case ViewTransitionComponent:
+ const prevMutationContext = pushMutationContext();
+ // TODO: If this was already cloned by a previous pass we can reuse those clones.
+ recursivelyInsertClones(finishedWork, hostParentClone);
+ if (viewTransitionMutationContext) {
+ // Track that this boundary had a mutation and therefore needs to animate
+ // whether it resized or not.
+ finishedWork.flags |= Update;
+ }
+ popMutationContext(prevMutationContext);
+ break;
+ default: {
+ recursivelyInsertClones(finishedWork, hostParentClone);
+ break;
+ }
+ }
+}
// Clone View Transition boundaries that have any mutations or might have had their
// layout affected by child insertions.
@@ -20,7 +337,30 @@ export function insertDestinationClones(
root: FiberRoot,
finishedWork: Fiber,
): void {
- // TODO
+ unhideHostChildren = false;
+ // We'll either not transition the root, or we'll transition the clone. Regardless
+ // we cancel the root view transition name.
+ const needsClone = detectMutationOrInsertClones(finishedWork);
+ if (needsClone) {
+ if (__DEV__) {
+ if (!didWarnForRootClone) {
+ didWarnForRootClone = true;
+ console.warn(
+ 'useSwipeTransition() caused something to mutate or relayout the root. ' +
+ 'This currently requires a clone of the whole document. Make sure to ' +
+ 'add a directly around an absolutely positioned DOM node ' +
+ 'to minimize the impact of any changes caused by the Swipe Transition.',
+ );
+ }
+ }
+ // Clone the whole root
+ const rootClone = cloneRootViewTransitionContainer(root.containerInfo);
+ root.gestureClone = rootClone;
+ recursivelyInsertClones(finishedWork, rootClone);
+ } else {
+ root.gestureClone = null;
+ cancelRootViewTransitionName(root.containerInfo);
+ }
}
// Revert insertions and apply view transition names to the "new" (current) state.
@@ -28,8 +368,12 @@ export function applyDepartureTransitions(
root: FiberRoot,
finishedWork: Fiber,
): void {
+ const rootClone = root.gestureClone;
+ if (rootClone !== null) {
+ root.gestureClone = null;
+ removeRootViewTransitionClone(root.containerInfo, rootClone);
+ }
// TODO
- cancelRootViewTransitionName(root.containerInfo);
}
// Revert transition names and start/adjust animations on the started View Transition.
diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js
index f5d0987393..b9ea97bf56 100644
--- a/packages/react-reconciler/src/ReactFiberCommitWork.js
+++ b/packages/react-reconciler/src/ReactFiberCommitWork.js
@@ -247,7 +247,6 @@ import {
restoreNestedViewTransitions,
measureUpdateViewTransition,
measureNestedViewTransitions,
- resetShouldStartViewTransition,
resetAppearingViewTransitions,
trackAppearingViewTransition,
viewTransitionCancelableChildren,
@@ -290,8 +289,6 @@ export function commitBeforeMutationEffects(
focusedInstanceHandle = prepareForCommit(root.containerInfo);
shouldFireAfterActiveInstanceBlur = false;
- resetShouldStartViewTransition();
-
const isViewTransitionEligible =
enableViewTransition &&
includesOnlyViewTransitionEligibleLanes(committedLanes);
diff --git a/packages/react-reconciler/src/ReactFiberConfigWithNoMutation.js b/packages/react-reconciler/src/ReactFiberConfigWithNoMutation.js
index 430ad02abc..64b67491aa 100644
--- a/packages/react-reconciler/src/ReactFiberConfigWithNoMutation.js
+++ b/packages/react-reconciler/src/ReactFiberConfigWithNoMutation.js
@@ -20,6 +20,8 @@ function shim(...args: any): empty {
// Mutation (when unsupported)
export const supportsMutation = false;
+export const cloneMutableInstance = shim;
+export const cloneMutableTextInstance = shim;
export const appendChild = shim;
export const appendChildToContainer = shim;
export const commitTextUpdate = shim;
@@ -40,6 +42,8 @@ export const restoreViewTransitionName = shim;
export const cancelViewTransitionName = shim;
export const cancelRootViewTransitionName = shim;
export const restoreRootViewTransitionName = shim;
+export const cloneRootViewTransitionContainer = shim;
+export const removeRootViewTransitionClone = shim;
export type InstanceMeasurement = null;
export const measureInstance = shim;
export const wasInstanceInViewport = shim;
diff --git a/packages/react-reconciler/src/ReactFiberRoot.js b/packages/react-reconciler/src/ReactFiberRoot.js
index 41401bac2b..1e1ac1cab2 100644
--- a/packages/react-reconciler/src/ReactFiberRoot.js
+++ b/packages/react-reconciler/src/ReactFiberRoot.js
@@ -101,6 +101,7 @@ function FiberRootNode(
if (enableSwipeTransition) {
this.pendingGestures = null;
this.stoppingGestures = null;
+ this.gestureClone = null;
}
this.incompleteTransitions = new Map();
diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js
index bf611f7327..f8d4fa03a0 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js
@@ -228,6 +228,7 @@ import {
invokePassiveEffectUnmountInDEV,
accumulateSuspenseyCommit,
} from './ReactFiberCommitWork';
+import {resetShouldStartViewTransition} from './ReactFiberCommitViewTransitions';
import {shouldStartViewTransition} from './ReactFiberCommitViewTransitions';
import {
insertDestinationClones,
@@ -3449,6 +3450,8 @@ function commitRoot(
}
}
+ resetShouldStartViewTransition();
+
// The commit phase is broken into several sub-phases. We do a separate pass
// of the effect list for each phase: all mutation effects come before all
// layout effects, and so on.
@@ -3900,6 +3903,11 @@ function commitGestureOnRoot(
finishedGesture.running = startGestureTransition(
root.containerInfo,
+ finishedGesture.provider,
+ finishedGesture.rangeCurrent,
+ finishedGesture.direction
+ ? finishedGesture.rangeNext
+ : finishedGesture.rangePrevious,
pendingTransitionTypes,
flushGestureMutations,
flushGestureAnimations,
diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js
index be5e9a5c0d..1452746089 100644
--- a/packages/react-reconciler/src/ReactInternalTypes.js
+++ b/packages/react-reconciler/src/ReactInternalTypes.js
@@ -26,6 +26,7 @@ import type {Lane, Lanes, LaneMap} from './ReactFiberLane';
import type {RootTag} from './ReactRootTags';
import type {
Container,
+ Instance,
TimeoutHandle,
NoTimeout,
SuspenseInstance,
@@ -286,6 +287,7 @@ type BaseFiberRootProperties = {
// enableSwipeTransition only
pendingGestures: null | ScheduledGesture,
stoppingGestures: null | ScheduledGesture,
+ gestureClone: null | Instance,
};
// The following attributes are only used by DevTools and are only present in DEV builds.
diff --git a/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js
index 9569b4a896..e978249e18 100644
--- a/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js
+++ b/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js
@@ -56,10 +56,12 @@ export const getChildHostContext = $$$config.getChildHostContext;
export const prepareForCommit = $$$config.prepareForCommit;
export const resetAfterCommit = $$$config.resetAfterCommit;
export const createInstance = $$$config.createInstance;
+export const cloneMutableInstance = $$$config.cloneMutableInstance;
export const appendInitialChild = $$$config.appendInitialChild;
export const finalizeInitialChildren = $$$config.finalizeInitialChildren;
export const shouldSetTextContent = $$$config.shouldSetTextContent;
export const createTextInstance = $$$config.createTextInstance;
+export const cloneMutableTextInstance = $$$config.cloneMutableTextInstance;
export const scheduleTimeout = $$$config.scheduleTimeout;
export const cancelTimeout = $$$config.cancelTimeout;
export const noTimeout = $$$config.noTimeout;
@@ -141,6 +143,10 @@ export const cancelRootViewTransitionName =
$$$config.cancelRootViewTransitionName;
export const restoreRootViewTransitionName =
$$$config.restoreRootViewTransitionName;
+export const cloneRootViewTransitionContainer =
+ $$$config.cloneRootViewTransitionContainer;
+export const removeRootViewTransitionClone =
+ $$$config.removeRootViewTransitionClone;
export const measureInstance = $$$config.measureInstance;
export const wasInstanceInViewport = $$$config.wasInstanceInViewport;
export const hasInstanceChanged = $$$config.hasInstanceChanged;
diff --git a/packages/react-test-renderer/src/ReactFiberConfigTestHost.js b/packages/react-test-renderer/src/ReactFiberConfigTestHost.js
index 48939e9ff4..bb18e44e55 100644
--- a/packages/react-test-renderer/src/ReactFiberConfigTestHost.js
+++ b/packages/react-test-renderer/src/ReactFiberConfigTestHost.js
@@ -172,6 +172,21 @@ export function createInstance(
};
}
+export function cloneMutableInstance(
+ instance: Instance,
+ keepChildren: boolean,
+): Instance {
+ return {
+ type: instance.type,
+ props: instance.props,
+ isHidden: instance.isHidden,
+ children: keepChildren ? instance.children : [],
+ internalInstanceHandle: null,
+ rootContainerInstance: instance.rootContainerInstance,
+ tag: 'INSTANCE',
+ };
+}
+
export function appendInitialChild(
parentInstance: Instance,
child: Instance | TextInstance,
@@ -210,6 +225,16 @@ export function createTextInstance(
};
}
+export function cloneMutableTextInstance(
+ textInstance: TextInstance,
+): TextInstance {
+ return {
+ text: textInstance.text,
+ isHidden: textInstance.isHidden,
+ tag: 'TEXT',
+ };
+}
+
let currentUpdatePriority: EventPriority = NoEventPriority;
export function setCurrentUpdatePriority(newPriority: EventPriority): void {
currentUpdatePriority = newPriority;
@@ -337,6 +362,27 @@ export function restoreRootViewTransitionName(rootContainer: Container): void {
// Noop
}
+export function cloneRootViewTransitionContainer(
+ rootContainer: Container,
+): Instance {
+ return {
+ type: 'ROOT',
+ props: {},
+ isHidden: false,
+ children: [],
+ internalInstanceHandle: null,
+ rootContainerInstance: rootContainer,
+ tag: 'INSTANCE',
+ };
+}
+
+export function removeRootViewTransitionClone(
+ rootContainer: Container,
+ clone: Instance,
+): void {
+ // Noop since it was never inserted anywhere.
+}
+
export type InstanceMeasurement = null;
export function measureInstance(instance: Instance): InstanceMeasurement {
@@ -379,6 +425,9 @@ export type RunningGestureTransition = null;
export function startGestureTransition(
rootContainer: Container,
+ timeline: GestureTimeline,
+ rangeStart: number,
+ rangeEnd: number,
transitionTypes: null | TransitionTypes,
mutationCallback: () => void,
animateCallback: () => void,
diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json
index 0722eba56d..9db8a9cb89 100644
--- a/scripts/error-codes/codes.json
+++ b/scripts/error-codes/codes.json
@@ -536,5 +536,6 @@
"548": "Finished rendering the gesture lane but there were no pending gestures. React should not have started a render in this case. This is a bug in React.",
"549": "Cannot start a gesture with a disconnected AnimationTimeline.",
"550": "useSwipeTransition is not yet supported in react-art.",
- "551": "useSwipeTransition is not yet supported in React Native."
+ "551": "useSwipeTransition is not yet supported in React Native.",
+ "552": "Cannot use a useSwipeTransition() in a detached root."
}