Skip to content

Support addTransitionType in startGestureTransition #32792

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion fixtures/view-transition/src/components/Page.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, {
unstable_addTransitionType as addTransitionType,
unstable_ViewTransition as ViewTransition,
unstable_Activity as Activity,
useLayoutEffect,
Expand Down Expand Up @@ -113,7 +114,12 @@ export default function Page({url, navigate}) {
<div className="swipe-recognizer">
<SwipeRecognizer
action={swipeAction}
gesture={optimisticNavigate}
gesture={direction => {
addTransitionType(
direction === 'left' ? 'navigation-forward' : 'navigation-back'
);
optimisticNavigate(direction);
}}
direction={show ? 'left' : 'right'}>
<button
className="button"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import type {
PreinitScriptOptions,
PreinitModuleScriptOptions,
} from 'react-dom/src/shared/ReactDOMTypes';
import type {TransitionTypes} from 'react/src/ReactTransitionType.js';
import type {TransitionTypes} from 'react/src/ReactTransitionType';

import {NotPending} from '../shared/ReactDOMFormActions';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
*/

import type {InspectorData, TouchedViewDataAtPoint} from './ReactNativeTypes';
import type {TransitionTypes} from 'react/src/ReactTransitionType.js';
import type {TransitionTypes} from 'react/src/ReactTransitionType';

// Modules provided by RN:
import {
Expand Down
2 changes: 1 addition & 1 deletion packages/react-noop-renderer/src/createReactNoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import type {UpdateQueue} from 'react-reconciler/src/ReactFiberClassUpdateQueue'
import type {ReactNodeList} from 'shared/ReactTypes';
import type {RootTag} from 'react-reconciler/src/ReactRootTags';
import type {EventPriority} from 'react-reconciler/src/ReactEventPriorities';
import type {TransitionTypes} from 'react/src/ReactTransitionType.js';
import type {TransitionTypes} from 'react/src/ReactTransitionType';

import * as Scheduler from 'scheduler/unstable_mock';
import {REACT_FRAGMENT_TYPE, REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
Expand Down
16 changes: 16 additions & 0 deletions packages/react-reconciler/src/ReactFiberGestureScheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import type {FiberRoot} from './ReactInternalTypes';
import type {GestureOptions} from 'shared/ReactTypes';
import type {GestureTimeline, RunningViewTransition} from './ReactFiberConfig';
import type {TransitionTypes} from 'react/src/ReactTransitionType';

import {
GestureLane,
Expand All @@ -25,6 +26,7 @@ export type ScheduledGesture = {
count: number, // The number of times this same provider has been started.
rangeStart: number, // The percentage along the timeline where the "current" state starts.
rangeEnd: number, // The percentage along the timeline where the "destination" state is reached.
types: null | TransitionTypes, // Any addTransitionType call made during startGestureTransition.
running: null | RunningViewTransition, // Used to cancel the running transition after we're done.
prev: null | ScheduledGesture, // The previous scheduled gesture in the queue for this root.
next: null | ScheduledGesture, // The next scheduled gesture in the queue for this root.
Expand All @@ -51,6 +53,7 @@ export function scheduleGesture(
count: 0,
rangeStart: 0, // Uninitialized
rangeEnd: 100, // Uninitialized
types: null,
running: null,
prev: prev,
next: null,
Expand All @@ -68,6 +71,7 @@ export function startScheduledGesture(
root: FiberRoot,
gestureTimeline: GestureTimeline,
gestureOptions: ?GestureOptions,
transitionTypes: null | TransitionTypes,
): null | ScheduledGesture {
const rangeStart =
gestureOptions && gestureOptions.rangeStart != null
Expand All @@ -87,6 +91,18 @@ export function startScheduledGesture(
// Update the options.
prev.rangeStart = rangeStart;
prev.rangeEnd = rangeEnd;
if (transitionTypes !== null) {
let scheduledTypes = prev.types;
if (scheduledTypes === null) {
scheduledTypes = prev.types = [];
}
for (let i = 0; i < transitionTypes.length; i++) {
const transitionType = transitionTypes[i];
if (scheduledTypes.indexOf(transitionType) === -1) {
scheduledTypes.push(transitionType);
}
}
}
return prev;
}
const next = prev.next;
Expand Down
10 changes: 9 additions & 1 deletion packages/react-reconciler/src/ReactFiberTransition.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {StackCursor} from './ReactFiberStack';
import type {Cache, SpawnedCachePool} from './ReactFiberCacheComponent';
import type {Transition} from 'react/src/ReactStartTransition';
import type {ScheduledGesture} from './ReactFiberGestureScheduler';
import type {TransitionTypes} from 'react/src/ReactTransitionType';

import {
enableTransitionTracing,
Expand Down Expand Up @@ -112,13 +113,15 @@ if (enableGestureTransition) {
transition: Transition,
provider: GestureProvider,
options: ?GestureOptions,
transitionTypes: null | TransitionTypes,
): () => void {
let cancel = null;
if (prevOnStartGestureTransitionFinish !== null) {
cancel = prevOnStartGestureTransitionFinish(
transition,
provider,
options,
transitionTypes,
);
}
// For every root that has work scheduled, check if there's a ScheduledGesture
Expand All @@ -131,7 +134,12 @@ if (enableGestureTransition) {
// that it's conceptually started globally.
let root = firstScheduledRoot;
while (root !== null) {
const scheduledGesture = startScheduledGesture(root, provider, options);
const scheduledGesture = startScheduledGesture(
root,
provider,
options,
transitionTypes,
);
if (scheduledGesture !== null) {
cancel = chainGestureCancellation(root, scheduledGesture, cancel);
}
Expand Down
5 changes: 2 additions & 3 deletions packages/react-reconciler/src/ReactFiberWorkLoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import {
getViewTransitionName,
type ViewTransitionState,
} from './ReactFiberViewTransitionComponent';
import type {TransitionTypes} from 'react/src/ReactTransitionType.js';
import type {TransitionTypes} from 'react/src/ReactTransitionType';

import {
enableCreateEventHandleAPI,
Expand Down Expand Up @@ -3925,8 +3925,7 @@ function commitGestureOnRoot(
setCurrentUpdatePriority(previousPriority);
ReactSharedInternals.T = prevTransition;
}
// TODO: Collect transition types.
pendingTransitionTypes = null;
pendingTransitionTypes = finishedGesture.types;
pendingEffectsStatus = PENDING_GESTURE_MUTATION_PHASE;

pendingViewTransition = finishedGesture.running = startGestureTransition(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
*/

import type {ReactContext} from 'shared/ReactTypes';
import type {TransitionTypes} from 'react/src/ReactTransitionType.js';
import type {TransitionTypes} from 'react/src/ReactTransitionType';

import isArray from 'shared/isArray';
import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
Expand Down
12 changes: 10 additions & 2 deletions packages/react/src/ReactSharedInternalsClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,20 @@ import {
enableGestureTransition,
} from 'shared/ReactFeatureFlags';

type onStartTransitionFinish = (Transition, mixed) => void;
type onStartGestureTransitionFinish = (
Transition,
GestureProvider,
?GestureOptions,
transitionTypes: null | TransitionTypes,
) => () => void;

export type SharedStateClient = {
H: null | Dispatcher, // ReactCurrentDispatcher for Hooks
A: null | AsyncDispatcher, // ReactCurrentCache for Cache
T: null | Transition, // ReactCurrentBatchConfig for Transitions
S: null | ((Transition, mixed) => void), // onStartTransitionFinish
G: null | ((Transition, GestureProvider, ?GestureOptions) => () => void), // onStartGestureTransitionFinish
S: null | onStartTransitionFinish,
G: null | onStartGestureTransitionFinish,
V: null | TransitionTypes, // Pending Transition Types for the Next Transition

// DEV-only
Expand Down
11 changes: 11 additions & 0 deletions packages/react/src/ReactStartTransition.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ import {
enableGestureTransition,
} from 'shared/ReactFeatureFlags';

import {
pendingGestureTransitionTypes,
pushPendingGestureTransitionTypes,
popPendingGestureTransitionTypes,
} from './ReactTransitionType';

import reportGlobalError from 'shared/reportGlobalError';

export type Transition = {
Expand Down Expand Up @@ -105,6 +111,8 @@ export function startGestureTransition(
}
ReactSharedInternals.T = currentTransition;

const prevTransitionTypes = pushPendingGestureTransitionTypes();

try {
const returnValue = scope();
if (__DEV__) {
Expand All @@ -118,17 +126,20 @@ export function startGestureTransition(
);
}
}
const transitionTypes = pendingGestureTransitionTypes;
const onStartGestureTransitionFinish = ReactSharedInternals.G;
if (onStartGestureTransitionFinish !== null) {
return onStartGestureTransitionFinish(
currentTransition,
provider,
options,
transitionTypes,
);
}
} catch (error) {
reportGlobalError(error);
} finally {
popPendingGestureTransitionTypes(prevTransitionTypes);
ReactSharedInternals.T = prevTransition;
}
return function cancelGesture() {
Expand Down
46 changes: 40 additions & 6 deletions packages/react/src/ReactTransitionType.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,51 @@
*/

import ReactSharedInternals from 'shared/ReactSharedInternals';
import {enableViewTransition} from 'shared/ReactFeatureFlags';
import {
enableViewTransition,
enableGestureTransition,
} from 'shared/ReactFeatureFlags';

export type TransitionTypes = Array<string>;

// This one is only available synchronously so we don't need to use ReactSharedInternals
// for this state. Instead, we track it in isomorphic and pass it to the renderer.
export let pendingGestureTransitionTypes: null | TransitionTypes = null;

export function pushPendingGestureTransitionTypes(): null | TransitionTypes {
const prev = pendingGestureTransitionTypes;
pendingGestureTransitionTypes = null;
return prev;
}

export function popPendingGestureTransitionTypes(
prev: null | TransitionTypes,
): void {
pendingGestureTransitionTypes = prev;
}

export function addTransitionType(type: string): void {
if (enableViewTransition) {
const pendingTransitionTypes: null | TransitionTypes =
ReactSharedInternals.V;
if (pendingTransitionTypes === null) {
ReactSharedInternals.V = [type];
} else if (pendingTransitionTypes.indexOf(type) === -1) {
let pendingTransitionTypes: null | TransitionTypes;
if (
enableGestureTransition &&
ReactSharedInternals.T !== null &&
ReactSharedInternals.T.gesture !== null
) {
// We're inside a startGestureTransition which is always sync.
pendingTransitionTypes = pendingGestureTransitionTypes;
if (pendingTransitionTypes === null) {
pendingTransitionTypes = pendingGestureTransitionTypes = [];
}
} else {
// Otherwise we're either inside a synchronous startTransition
// or in the async gap of one, which we track globally.
pendingTransitionTypes = ReactSharedInternals.V;
if (pendingTransitionTypes === null) {
pendingTransitionTypes = ReactSharedInternals.V = [];
}
}
if (pendingTransitionTypes.indexOf(type) === -1) {
pendingTransitionTypes.push(type);
}
}
Expand Down
Loading