diff --git a/fixtures/view-transition/src/components/Page.js b/fixtures/view-transition/src/components/Page.js index 962ef168b4c..f91dc44e58d 100644 --- a/fixtures/view-transition/src/components/Page.js +++ b/fixtures/view-transition/src/components/Page.js @@ -82,8 +82,12 @@ export default function Page({url, navigate}) { {rotate: '0deg', transformOrigin: '30px 8px'}, {rotate: '360deg', transformOrigin: '30px 8px'}, ]; - viewTransition.old.animate(keyframes, 250); - viewTransition.new.animate(keyframes, 250); + const animation1 = viewTransition.old.animate(keyframes, 250); + const animation2 = viewTransition.new.animate(keyframes, 250); + return () => { + animation1.cancel(); + animation2.cancel(); + }; } function onGestureTransition( @@ -105,8 +109,12 @@ export default function Page({url, navigate}) { rangeStart: (reverse ? rangeEnd : rangeStart) + '%', rangeEnd: (reverse ? rangeStart : rangeEnd) + '%', }; - viewTransition.old.animate(keyframes, options); - viewTransition.new.animate(keyframes, options); + const animation1 = viewTransition.old.animate(keyframes, options); + const animation2 = viewTransition.new.animate(keyframes, options); + return () => { + animation1.cancel(); + animation2.cancel(); + }; } else { // Custom Timeline const options = { @@ -120,11 +128,10 @@ export default function Page({url, navigate}) { // Let the custom timeline take control of driving the animations. const cleanup1 = timeline.animate(animation1); const cleanup2 = timeline.animate(animation2); - // TODO: Support returning a clean up function from ViewTransition events. - // return () => { - // cleanup1(); - // cleanup2(); - // }; + return () => { + cleanup1(); + cleanup2(); + }; } } 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 4d3bb0248b1..6a07839d37d 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -2337,6 +2337,7 @@ export function startViewTransition( export type RunningViewTransition = { skipTransition(): void, + finished: Promise, ... }; @@ -2784,6 +2785,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/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 206bf797d60..b03f5eff159 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -120,6 +120,7 @@ import { startViewTransition, startGestureTransition, stopViewTransition, + addViewTransitionFinishedListener, createViewTransitionInstance, flushHydrationEvents, } from './ReactFiberConfig'; @@ -733,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 @@ -899,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) { @@ -925,7 +930,7 @@ export function scheduleGestureTransitionEvent( options: GestureOptionsRequired, instance: ViewTransitionInstance, types: Array, - ) => void, + ) => void | (() => void), ): void { if (enableGestureTransition) { if (callback != null) { @@ -4143,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 @@ -4262,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); + } + } } } } @@ -4532,9 +4543,18 @@ function flushGestureAnimations(): 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); + 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); + } + } + } } } } 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 2596f02c995..e58c36f0a0c 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -303,34 +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 | (() => void), onGestureExit?: ( timeline: GestureProvider, options: GestureOptionsRequired, instance: ViewTransitionInstance, types: Array, - ) => void, + ) => void | (() => void), onGestureShare?: ( timeline: GestureProvider, options: GestureOptionsRequired, instance: ViewTransitionInstance, types: Array, - ) => void, + ) => void | (() => void), onGestureUpdate?: ( timeline: GestureProvider, options: GestureOptionsRequired, instance: ViewTransitionInstance, types: Array, - ) => void, + ) => void | (() => void), }; export type ActivityProps = {