diff --git a/fixtures/view-transition/src/components/Page.css b/fixtures/view-transition/src/components/Page.css index 06100a53e8c..63ae718e740 100644 --- a/fixtures/view-transition/src/components/Page.css +++ b/fixtures/view-transition/src/components/Page.css @@ -1,12 +1,3 @@ -.roboto-font { - font-family: "Roboto", serif; - font-optical-sizing: auto; - font-weight: 100; - font-style: normal; - font-variation-settings: - "wdth" 100; -} - .swipe-recognizer { width: 300px; background: #eee; diff --git a/fixtures/view-transition/src/components/Page.js b/fixtures/view-transition/src/components/Page.js index 6227e9ebc5f..2a3cacf0a54 100644 --- a/fixtures/view-transition/src/components/Page.js +++ b/fixtures/view-transition/src/components/Page.js @@ -4,6 +4,7 @@ import React, { Activity, useLayoutEffect, useEffect, + useInsertionEffect, useState, useId, useOptimistic, @@ -41,6 +42,26 @@ const b = ( ); function Component() { + // Test inserting fonts with style tags using useInsertionEffect. This is not recommended but + // used to test that gestures etc works with useInsertionEffect so that stylesheet based + // libraries can be properly supported. + useInsertionEffect(() => { + const style = document.createElement('style'); + style.textContent = ` + .roboto-font { + font-family: "Roboto", serif; + font-optical-sizing: auto; + font-weight: 100; + font-style: normal; + font-variation-settings: + "wdth" 100; + } + `; + document.head.appendChild(style); + return () => { + document.head.removeChild(style); + }; + }, []); return ( { + animation1.cancel(); + animation2.cancel(); + }; + } + + function onGestureTransition( + timeline, + {rangeStart, rangeEnd}, + viewTransition, + types + ) { + const keyframes = [ + {rotate: '0deg', transformOrigin: '30px 8px'}, + {rotate: '360deg', transformOrigin: '30px 8px'}, + ]; + const reverse = rangeStart > rangeEnd; + if (timeline instanceof AnimationTimeline) { + // Native Timeline + const options = { + timeline: timeline, + direction: reverse ? 'normal' : 'reverse', + rangeStart: (reverse ? rangeEnd : rangeStart) + '%', + rangeEnd: (reverse ? rangeStart : rangeEnd) + '%', + }; + const animation1 = viewTransition.old.animate(keyframes, options); + const animation2 = viewTransition.new.animate(keyframes, options); + return () => { + animation1.cancel(); + animation2.cancel(); + }; + } else { + // Custom Timeline + const options = { + direction: reverse ? 'normal' : 'reverse', + // We set the delay and duration to represent the span of the range. + delay: reverse ? rangeEnd : rangeStart, + duration: reverse ? rangeStart - rangeEnd : rangeEnd - rangeStart, + }; + const animation1 = viewTransition.old.animate(keyframes, options); + const animation2 = viewTransition.new.animate(keyframes, options); + // Let the custom timeline take control of driving the animations. + const cleanup1 = timeline.animate(animation1); + const cleanup2 = timeline.animate(animation2); + return () => { + cleanup1(); + cleanup2(); + }; + } } function swipeAction() { @@ -131,7 +201,10 @@ export default function Page({url, navigate}) { ); const exclamation = ( - +
!
diff --git a/packages/react-art/src/ReactFiberConfigART.js b/packages/react-art/src/ReactFiberConfigART.js index ca2745beea5..50873af6da0 100644 --- a/packages/react-art/src/ReactFiberConfigART.js +++ b/packages/react-art/src/ReactFiberConfigART.js @@ -549,6 +549,13 @@ export function startGestureTransition() { export function stopViewTransition(transition: RunningViewTransition) {} +export function addViewTransitionFinishedListener( + transition: RunningViewTransition, + callback: () => void, +) { + callback(); +} + export type ViewTransitionInstance = null | {name: string, ...}; export function createViewTransitionInstance( diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index 66e46961d81..15e6bf7be34 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -607,10 +607,32 @@ export function createInstance( return domElement; } +let didWarnForClone = false; + export function cloneMutableInstance( instance: Instance, keepChildren: boolean, ): Instance { + if (__DEV__) { + // Warn for problematic + const tagName = instance.tagName; + switch (tagName) { + case 'VIDEO': + case 'IFRAME': + if (!didWarnForClone) { + didWarnForClone = true; + // TODO: Once we have the ability to avoid cloning the root, suggest an absolutely + // positioned ViewTransition instead as the solution. + console.warn( + 'startGestureTransition() required cloning a <%s> element since it exists in ' + + 'both states of the gesture. This can be problematic since it will load it twice ' + + 'Try removing or hiding it with in the optimistic state.', + tagName.toLowerCase(), + ); + } + break; + } + } return instance.cloneNode(keepChildren); } @@ -2337,6 +2359,7 @@ export function startViewTransition( export type RunningViewTransition = { skipTransition(): void, + finished: Promise, ... }; @@ -2372,6 +2395,7 @@ function animateGesture( targetElement: Element, pseudoElement: string, timeline: GestureTimeline, + viewTransitionAnimations: Array, customTimelineCleanup: Array<() => void>, rangeStart: number, rangeEnd: number, @@ -2464,7 +2488,7 @@ function animateGesture( if (timeline instanceof AnimationTimeline) { // Native Timeline // $FlowFixMe[incompatible-call] - targetElement.animate(keyframes, { + const animation = targetElement.animate(keyframes, { pseudoElement: pseudoElement, // Set the timeline to the current gesture timeline to drive the updates. timeline: timeline, @@ -2482,6 +2506,7 @@ function animateGesture( rangeStart: (reverse ? rangeEnd : rangeStart) + '%', rangeEnd: (reverse ? rangeStart : rangeEnd) + '%', }); + viewTransitionAnimations.push(animation); } else { // Custom Timeline // $FlowFixMe[incompatible-call] @@ -2554,8 +2579,10 @@ export function startGestureTransition( // $FlowFixMe const pseudoElement: ?string = effect.pseudoElement; if (pseudoElement == null) { - } else if (pseudoElement.startsWith('::view-transition')) { - viewTransitionAnimations.push(animations[i]); + } else if ( + pseudoElement.startsWith('::view-transition') && + effect.target === documentElement + ) { const timing = effect.getTiming(); const duration = // $FlowFixMe[prop-missing] @@ -2648,6 +2675,7 @@ export function startGestureTransition( effect.target, pseudoElement, timeline, + viewTransitionAnimations, customTimelineCleanup, adjustedRangeStart, adjustedRangeEnd, @@ -2675,6 +2703,7 @@ export function startGestureTransition( effect.target, pseudoElementName, timeline, + viewTransitionAnimations, customTimelineCleanup, rangeStart, rangeEnd, @@ -2696,6 +2725,7 @@ export function startGestureTransition( duration: 1, }); blockingAnim.pause(); + viewTransitionAnimations.push(blockingAnim); animateCallback(); }; // In Chrome, "new" animations are not ready in the ready callback. We have to wait @@ -2777,6 +2807,13 @@ export function stopViewTransition(transition: RunningViewTransition) { transition.skipTransition(); } +export function addViewTransitionFinishedListener( + transition: RunningViewTransition, + callback: () => void, +) { + transition.finished.finally(callback); +} + interface ViewTransitionPseudoElementType extends mixin$Animatable { _scope: HTMLElement; _selector: string; diff --git a/packages/react-native-renderer/src/ReactFiberConfigNative.js b/packages/react-native-renderer/src/ReactFiberConfigNative.js index 89da4108fc9..fcf356776c2 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigNative.js +++ b/packages/react-native-renderer/src/ReactFiberConfigNative.js @@ -713,6 +713,13 @@ export function startGestureTransition( export function stopViewTransition(transition: RunningViewTransition) {} +export function addViewTransitionFinishedListener( + transition: RunningViewTransition, + callback: () => void, +) { + callback(); +} + export type ViewTransitionInstance = null | {name: string, ...}; export function createViewTransitionInstance( diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index db69232d129..79d1e1a8c9b 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -888,6 +888,13 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { stopViewTransition(transition: RunningViewTransition) {}, + addViewTransitionFinishedListener( + transition: RunningViewTransition, + callback: () => void, + ) { + callback(); + }, + createViewTransitionInstance(name: string): ViewTransitionInstance { return null; }, diff --git a/packages/react-reconciler/src/ReactFiberApplyGesture.js b/packages/react-reconciler/src/ReactFiberApplyGesture.js index f18e33b57ba..32e59395845 100644 --- a/packages/react-reconciler/src/ReactFiberApplyGesture.js +++ b/packages/react-reconciler/src/ReactFiberApplyGesture.js @@ -43,6 +43,7 @@ import { } from './ReactFiberMutationTracking'; import { MutationMask, + Placement, Update, ContentReset, NoFlags, @@ -52,6 +53,14 @@ import { AffectedParentLayout, } from './ReactFiberFlags'; import { + HasEffect as HookHasEffect, + Insertion as HookInsertion, +} from './ReactHookEffectTags'; +import { + FunctionComponent, + ForwardRef, + MemoComponent, + SimpleMemoComponent, HostComponent, HostHoistable, HostSingleton, @@ -72,6 +81,10 @@ import { pushViewTransitionCancelableScope, popViewTransitionCancelableScope, } from './ReactFiberCommitViewTransitions'; +import { + commitHookEffectListMount, + commitHookEffectListUnmount, +} from './ReactFiberCommitEffects'; import { getViewTransitionName, getViewTransitionClassName, @@ -82,8 +95,7 @@ import { enableComponentPerformanceTrack, } from 'shared/ReactFeatureFlags'; import {trackAnimatingTask} from './ReactProfilerTimer'; - -let didWarnForRootClone = false; +import {scheduleGestureTransitionEvent} from './ReactFiberWorkLoop'; // Used during the apply phase to track whether a parent ViewTransition component // might have been affected by any mutations / relayouts below. @@ -280,6 +292,7 @@ function applyAppearingPairViewTransition(child: Fiber): void { if (clones !== null) { applyViewTransitionToClones(name, className, clones, child); } + scheduleGestureTransitionEvent(child, props.onGestureShare); } } } @@ -310,6 +323,11 @@ function applyExitViewTransition(placement: Fiber): void { if (clones !== null) { applyViewTransitionToClones(name, className, clones, placement); } + if (state.paired) { + scheduleGestureTransitionEvent(placement, props.onGestureShare); + } else { + scheduleGestureTransitionEvent(placement, props.onGestureExit); + } } } @@ -371,9 +389,10 @@ function recursivelyInsertNew( if ( visitPhase === INSERT_APPEARING_PAIR && parentViewTransition === null && - (parentFiber.subtreeFlags & ViewTransitionNamedStatic) === NoFlags + (parentFiber.subtreeFlags & (ViewTransitionNamedStatic | Placement)) === + NoFlags ) { - // We're just searching for pairs but we have reached the end. + // We're just searching for pairs or insertion effects but we have reached the end. return; } let child = parentFiber.child; @@ -395,6 +414,28 @@ function recursivelyInsertNewFiber( visitPhase: VisitPhase, ): void { switch (finishedWork.tag) { + case FunctionComponent: + case ForwardRef: + case MemoComponent: + case SimpleMemoComponent: { + recursivelyInsertNew( + finishedWork, + hostParentClone, + parentViewTransition, + visitPhase, + ); + if (finishedWork.flags & Update) { + // Insertion Effects are mounted temporarily during the rendering of the snapshot. + // This does not affect cloned Offscreen content since those would've been mounted + // while inside the offscreen tree already. + // Note that because we are mounting a clone of the DOM tree and the previous DOM + // tree remains mounted during the snapshot, we can't unmount any previous insertion + // effects. This can lead to conflicts but that is similar to what can happen with + // conflicts for two mounted Activity boundaries. + commitHookEffectListMount(HookInsertion | HookHasEffect, finishedWork); + } + break; + } case HostHoistable: { if (supportsResources) { // TODO: Hoistables should get optimistically inserted and then removed. @@ -990,17 +1031,6 @@ export function insertDestinationClones( // we cancel the root view transition name. const needsClone = detectMutationOrInsertClones(finishedWork); if (needsClone) { - if (__DEV__) { - if (!didWarnForRootClone) { - didWarnForRootClone = true; - console.warn( - 'startGestureTransition() 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 Gesture Transition.', - ); - } - } // Clone the whole root const rootClone = cloneRootViewTransitionContainer(root.containerInfo); root.gestureClone = rootClone; @@ -1032,6 +1062,40 @@ function measureExitViewTransitions(placement: Fiber): void { } } +function recursivelyRestoreNew( + finishedWork: Fiber, + nearestMountedAncestor: Fiber, +): void { + // There has to be move a Placement AND an Update flag somewhere below for this + // pass to be relevant since we only apply insertion effects for new components here. + if (((Placement | Update) & finishedWork.subtreeFlags) !== NoFlags) { + let child = finishedWork.child; + while (child !== null) { + recursivelyRestoreNew(child, nearestMountedAncestor); + child = child.sibling; + } + } + switch (finishedWork.tag) { + case FunctionComponent: + case ForwardRef: + case MemoComponent: + case SimpleMemoComponent: { + const current = finishedWork.alternate; + if (current === null && finishedWork.flags & Update) { + // Insertion Effects are mounted temporarily during the rendering of the snapshot. + // We have now already takes a snapshot of the inserted state so we can now unmount + // them to get back into the original state before starting the animation. + commitHookEffectListUnmount( + HookInsertion | HookHasEffect, + finishedWork, + nearestMountedAncestor, + ); + } + break; + } + } +} + function recursivelyApplyViewTransitions(parentFiber: Fiber) { const deletions = parentFiber.deletions; if (deletions !== null) { @@ -1048,7 +1112,13 @@ function recursivelyApplyViewTransitions(parentFiber: Fiber) { // If we have mutations or if this is a newly inserted tree, clone as we go. let child = parentFiber.child; while (child !== null) { - applyViewTransitionsOnFiber(child); + const current = child.alternate; + if (current === null) { + measureExitViewTransitions(child); + recursivelyRestoreNew(child, parentFiber); + } else { + applyViewTransitionsOnFiber(child, current); + } child = child.sibling; } } else { @@ -1059,14 +1129,7 @@ function recursivelyApplyViewTransitions(parentFiber: Fiber) { } } -function applyViewTransitionsOnFiber(finishedWork: Fiber) { - const current = finishedWork.alternate; - if (current === null) { - measureExitViewTransitions(finishedWork); - return; - } - - const flags = finishedWork.flags; +function applyViewTransitionsOnFiber(finishedWork: Fiber, current: Fiber) { // 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. @@ -1076,12 +1139,18 @@ function applyViewTransitionsOnFiber(finishedWork: Fiber) { break; } case OffscreenComponent: { - if (flags & Visibility) { - const newState: OffscreenState | null = finishedWork.memoizedState; - const isHidden = newState !== null; - if (!isHidden) { + const newState: OffscreenState | null = finishedWork.memoizedState; + const isHidden = newState !== null; + const wasHidden = current.memoizedState !== null; + if (!isHidden) { + if (wasHidden) { measureExitViewTransitions(finishedWork); - } else if (current !== null && current.memoizedState === null) { + recursivelyRestoreNew(finishedWork, finishedWork); + } else { + recursivelyApplyViewTransitions(finishedWork); + } + } else { + if (!wasHidden) { // Was previously mounted as visible but is now hidden. commitEnterViewTransitions(current, true); } @@ -1123,7 +1192,8 @@ function applyViewTransitionsOnFiber(finishedWork: Fiber) { // TODO: If this doesn't end up canceled, because a parent animates, // then we should probably issue an event since this instance is part of it. } else { - // TODO: Schedule gesture events. + const props: ViewTransitionProps = finishedWork.memoizedProps; + scheduleGestureTransitionEvent(finishedWork, props.onGestureUpdate); // If this boundary did update, we cannot cancel its children so those are dropped. popViewTransitionCancelableScope(prevCancelableChildren); } diff --git a/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js b/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js index 64940c31957..a9edc0c84d2 100644 --- a/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js +++ b/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js @@ -34,7 +34,10 @@ import { hasInstanceAffectedParent, wasInstanceInViewport, } from './ReactFiberConfig'; -import {scheduleViewTransitionEvent} from './ReactFiberWorkLoop'; +import { + scheduleViewTransitionEvent, + scheduleGestureTransitionEvent, +} from './ReactFiberWorkLoop'; import { getViewTransitionName, getViewTransitionClassName, @@ -312,7 +315,7 @@ export function commitEnterViewTransitions( if (!state.paired) { if (gesture) { - // TODO: Schedule gesture events. + scheduleGestureTransitionEvent(placement, props.onGestureEnter); } else { scheduleViewTransitionEvent(placement, props.onEnter); } @@ -848,7 +851,7 @@ export function measureNestedViewTransitions( // Nothing changed. } else { if (gesture) { - // TODO: Schedule gesture events. + scheduleGestureTransitionEvent(child, props.onGestureUpdate); } else { scheduleViewTransitionEvent(child, props.onUpdate); } diff --git a/packages/react-reconciler/src/ReactFiberConfigWithNoMutation.js b/packages/react-reconciler/src/ReactFiberConfigWithNoMutation.js index 09e64b0aa08..79cf3990a72 100644 --- a/packages/react-reconciler/src/ReactFiberConfigWithNoMutation.js +++ b/packages/react-reconciler/src/ReactFiberConfigWithNoMutation.js @@ -54,6 +54,7 @@ export const startViewTransition = shim; export type RunningViewTransition = null; export const startGestureTransition = shim; export const stopViewTransition = shim; +export const addViewTransitionFinishedListener = shim; export type ViewTransitionInstance = null | {name: string, ...}; export const createViewTransitionInstance = shim; export type GestureTimeline = any; diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 5bc334364ad..b03f5eff159 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -9,7 +9,11 @@ import {REACT_STRICT_MODE_TYPE} from 'shared/ReactSymbols'; -import type {Wakeable, Thenable} from 'shared/ReactTypes'; +import type { + Wakeable, + Thenable, + GestureOptionsRequired, +} from 'shared/ReactTypes'; import type {Fiber, FiberRoot} from './ReactInternalTypes'; import type {Lanes, Lane} from './ReactFiberLane'; import type {ActivityState} from './ReactFiberActivityComponent'; @@ -26,6 +30,7 @@ import type { Resource, ViewTransitionInstance, RunningViewTransition, + GestureTimeline, SuspendedState, } from './ReactFiberConfig'; import type {RootState} from './ReactFiberRoot'; @@ -115,6 +120,7 @@ import { startViewTransition, startGestureTransition, stopViewTransition, + addViewTransitionFinishedListener, createViewTransitionInstance, flushHydrationEvents, } from './ReactFiberConfig'; @@ -728,8 +734,9 @@ let pendingEffectsRenderEndTime: number = -0; // Profiling-only let pendingPassiveTransitions: Array | null = null; let pendingRecoverableErrors: null | Array> = null; let pendingViewTransition: null | RunningViewTransition = null; -let pendingViewTransitionEvents: Array<(types: Array) => void> | null = - null; +let pendingViewTransitionEvents: Array< + (types: Array) => void | (() => void), +> | null = null; let pendingTransitionTypes: null | TransitionTypes = null; let pendingDidIncludeRenderPhaseUpdate: boolean = false; let pendingSuspendedCommitReason: SuspendedCommitReason = null; // Profiling-only @@ -894,7 +901,10 @@ export function requestDeferredLane(): Lane { export function scheduleViewTransitionEvent( fiber: Fiber, - callback: ?(instance: ViewTransitionInstance, types: Array) => void, + callback: ?( + instance: ViewTransitionInstance, + types: Array, + ) => void | (() => void), ): void { if (enableViewTransition) { if (callback != null) { @@ -913,6 +923,42 @@ export function scheduleViewTransitionEvent( } } +export function scheduleGestureTransitionEvent( + fiber: Fiber, + callback: ?( + timeline: GestureTimeline, + options: GestureOptionsRequired, + instance: ViewTransitionInstance, + types: Array, + ) => void | (() => void), +): void { + if (enableGestureTransition) { + if (callback != null) { + const applyingGesture = pendingEffectsRoot.pendingGestures; + if (applyingGesture !== null) { + const state: ViewTransitionState = fiber.stateNode; + let instance = state.ref; + if (instance === null) { + instance = state.ref = createViewTransitionInstance( + getViewTransitionName(fiber.memoizedProps, state), + ); + } + const timeline = applyingGesture.provider; + const options = { + rangeStart: applyingGesture.rangeStart, + rangeEnd: applyingGesture.rangeEnd, + }; + if (pendingViewTransitionEvents === null) { + pendingViewTransitionEvents = []; + } + pendingViewTransitionEvents.push( + callback.bind(null, timeline, options, instance), + ); + } + } + } +} + export function peekDeferredLane(): Lane { return workInProgressDeferredLane; } @@ -4102,6 +4148,7 @@ function flushSpawnedWork(): void { pendingEffectsStatus = NO_PENDING_EFFECTS; + const committedViewTransition = pendingViewTransition; pendingViewTransition = null; // The view transition has now fully started. // Tell Scheduler to yield at the end of the frame, so the browser has an @@ -4221,9 +4268,14 @@ function flushSpawnedWork(): void { // Normalize the type. This is lazily created only for events. pendingTypes = []; } - for (let i = 0; i < pendingEvents.length; i++) { - const viewTransitionEvent = pendingEvents[i]; - viewTransitionEvent(pendingTypes); + if (committedViewTransition !== null) { + for (let i = 0; i < pendingEvents.length; i++) { + const viewTransitionEvent = pendingEvents[i]; + const cleanup = viewTransitionEvent(pendingTypes); + if (cleanup !== undefined) { + addViewTransitionFinishedListener(committedViewTransition, cleanup); + } + } } } } @@ -4352,6 +4404,8 @@ function applyGestureOnRoot( startAnimating(pendingEffectsLanes); } + pendingViewTransitionEvents = null; + const prevTransition = ReactSharedInternals.T; ReactSharedInternals.T = null; const previousPriority = getCurrentUpdatePriority(); @@ -4476,6 +4530,35 @@ function flushGestureAnimations(): void { ReactSharedInternals.T = prevTransition; } + if (enableViewTransition) { + // We should now be after the startGestureTransition's .ready call which is late enough + // to start animating any pseudo-elements. We have also already applied any adjustments + // we do to the built-in animations which can now be read by the refs. + const pendingEvents = pendingViewTransitionEvents; + let pendingTypes = pendingTransitionTypes; + pendingTransitionTypes = null; + if (pendingEvents !== null) { + pendingViewTransitionEvents = null; + if (pendingTypes === null) { + // Normalize the type. This is lazily created only for events. + pendingTypes = []; + } + const appliedGesture = root.pendingGestures; + if (appliedGesture !== null) { + const runningTransition = appliedGesture.running; + if (runningTransition !== null) { + for (let i = 0; i < pendingEvents.length; i++) { + const viewTransitionEvent = pendingEvents[i]; + const cleanup = viewTransitionEvent(pendingTypes); + if (cleanup !== undefined) { + addViewTransitionFinishedListener(runningTransition, cleanup); + } + } + } + } + } + } + if (enableProfilerTimer && enableComponentPerformanceTrack) { finalizeRender(lanes, commitEndTime); } diff --git a/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js index a6c6613eb39..1785fa9aaec 100644 --- a/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js +++ b/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js @@ -162,6 +162,8 @@ export const hasInstanceAffectedParent = $$$config.hasInstanceAffectedParent; export const startViewTransition = $$$config.startViewTransition; export const startGestureTransition = $$$config.startGestureTransition; export const stopViewTransition = $$$config.stopViewTransition; +export const addViewTransitionFinishedListener = + $$$config.addViewTransitionFinishedListener; export const getCurrentGestureOffset = $$$config.getCurrentGestureOffset; export const createViewTransitionInstance = $$$config.createViewTransitionInstance; diff --git a/packages/react-test-renderer/src/ReactFiberConfigTestHost.js b/packages/react-test-renderer/src/ReactFiberConfigTestHost.js index e18523bc04e..6b04a36d297 100644 --- a/packages/react-test-renderer/src/ReactFiberConfigTestHost.js +++ b/packages/react-test-renderer/src/ReactFiberConfigTestHost.js @@ -459,6 +459,13 @@ export function startGestureTransition( export function stopViewTransition(transition: RunningViewTransition) {} +export function addViewTransitionFinishedListener( + transition: RunningViewTransition, + callback: () => void, +) { + callback(); +} + export type ViewTransitionInstance = null | {name: string, ...}; export function createViewTransitionInstance( diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index 65ed43c063c..e58c36f0a0c 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -290,6 +290,11 @@ export type ViewTransitionClass = | string | ViewTransitionClassPerType; +export type GestureOptionsRequired = { + rangeStart: number, + rangeEnd: number, +}; + export type ViewTransitionProps = { name?: string, children?: ReactNodeList, @@ -298,10 +303,46 @@ export type ViewTransitionProps = { exit?: ViewTransitionClass, share?: ViewTransitionClass, update?: ViewTransitionClass, - onEnter?: (instance: ViewTransitionInstance, types: Array) => void, - onExit?: (instance: ViewTransitionInstance, types: Array) => void, - onShare?: (instance: ViewTransitionInstance, types: Array) => void, - onUpdate?: (instance: ViewTransitionInstance, types: Array) => void, + onEnter?: ( + instance: ViewTransitionInstance, + types: Array, + ) => void | (() => void), + onExit?: ( + instance: ViewTransitionInstance, + types: Array, + ) => void | (() => void), + onShare?: ( + instance: ViewTransitionInstance, + types: Array, + ) => void | (() => void), + onUpdate?: ( + instance: ViewTransitionInstance, + types: Array, + ) => void | (() => void), + onGestureEnter?: ( + timeline: GestureProvider, + options: GestureOptionsRequired, + instance: ViewTransitionInstance, + types: Array, + ) => void | (() => void), + onGestureExit?: ( + timeline: GestureProvider, + options: GestureOptionsRequired, + instance: ViewTransitionInstance, + types: Array, + ) => void | (() => void), + onGestureShare?: ( + timeline: GestureProvider, + options: GestureOptionsRequired, + instance: ViewTransitionInstance, + types: Array, + ) => void | (() => void), + onGestureUpdate?: ( + timeline: GestureProvider, + options: GestureOptionsRequired, + instance: ViewTransitionInstance, + types: Array, + ) => void | (() => void), }; export type ActivityProps = {