Skip to content
Open
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
25 changes: 14 additions & 11 deletions fixtures/view-transition/src/components/Page.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,17 +171,20 @@ export default function Page({url, navigate}) {
}}>
<h1>{!show ? 'A' + counter : 'B'}</h1>
</ViewTransition>
{show ? (
<div>
{a}
{b}
</div>
) : (
<div>
{b}
{a}
</div>
)}
{
// Using url instead of renderedUrl here lets us only update this on commit.
url === '/?b' ? (
<div>
{a}
{b}
</div>
) : (
<div>
{b}
{a}
</div>
)
}
<ViewTransition>
{show ? (
<div>hello{exclamation}</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -711,7 +711,11 @@ function measureViewTransitionHostInstancesRecursive(
}
viewTransitionCancelableChildren.push(
instance,
oldName,
viewTransitionHostInstanceIdx === 0
? oldName
: // If we have multiple Host Instances below, we add a suffix to the name to give
// each one a unique name.
oldName + '_' + viewTransitionHostInstanceIdx,
child.memoizedProps,
);
}
Expand Down
144 changes: 82 additions & 62 deletions packages/react-reconciler/src/ReactFiberGestureScheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,10 @@ import type {GestureOptions} from 'shared/ReactTypes';
import type {GestureTimeline, RunningViewTransition} from './ReactFiberConfig';
import type {TransitionTypes} from 'react/src/ReactTransitionType';

import {
GestureLane,
includesBlockingLane,
includesTransitionLane,
} from './ReactFiberLane';
import {GestureLane, markRootFinished, NoLane, NoLanes} from './ReactFiberLane';
import {ensureRootIsScheduled} from './ReactFiberRootScheduler';
import {getCurrentGestureOffset, stopViewTransition} from './ReactFiberConfig';
import {pingGestureRoot, restartGestureRoot} from './ReactFiberWorkLoop';

// This type keeps track of any scheduled or active gestures.
export type ScheduledGesture = {
Expand All @@ -28,6 +25,7 @@ export type ScheduledGesture = {
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.
committing: boolean, // If the gesture was released in a committed state and should actually commit.
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 Down Expand Up @@ -55,6 +53,7 @@ export function scheduleGesture(
rangeEnd: 100, // Uninitialized
types: null,
running: null,
committing: false,
prev: prev,
next: null,
};
Expand Down Expand Up @@ -122,77 +121,98 @@ export function cancelScheduledGesture(
): void {
gesture.count--;
if (gesture.count === 0) {
// Delete the scheduled gesture from the pending queue.
deleteScheduledGesture(root, gesture);
// If the end state is closer to the end than the beginning then we commit into the
// end state before reverting back (or applying a new Transition).
// Otherwise we just revert back and don't commit.
let shouldCommit: boolean;
const finalOffset = getCurrentGestureOffset(gesture.provider);
const rangeStart = gesture.rangeStart;
const rangeEnd = gesture.rangeEnd;
if (rangeStart < rangeEnd) {
shouldCommit = finalOffset > rangeStart + (rangeEnd - rangeStart) / 2;
} else {
shouldCommit = finalOffset < rangeEnd + (rangeStart - rangeEnd) / 2;
}
// TODO: If we're currently rendering this gesture, we need to restart the render
// on a different gesture or cancel the render..
// TODO: We might want to pause the View Transition at this point since you should
// no longer be able to update the position of anything but it might be better to
// just commit the gesture state.
const runningTransition = gesture.running;
if (runningTransition !== null) {
const pendingLanesExcludingGestureLane = root.pendingLanes & ~GestureLane;
if (
includesBlockingLane(pendingLanesExcludingGestureLane) ||
includesTransitionLane(pendingLanesExcludingGestureLane)
) {
// If we have pending work we schedule the gesture to be stopped at the next commit.
// This ensures that we don't snap back to the previous state until we have
// had a chance to commit any resulting updates.
const existing = root.stoppingGestures;
if (existing !== null) {
gesture.next = existing;
existing.prev = gesture;
if (runningTransition !== null && shouldCommit) {
// If we are going to commit this gesture in its to state, we need to wait to
// stop it until it commits. We should now schedule a render at the gesture
// lane to actually commit it.
gesture.committing = true;
if (root.pendingGestures === gesture) {
// Ping the root given the new state. This is similar to pingSuspendedRoot.
// This will either schedule the gesture lane to be committed possibly from its current state.
pingGestureRoot(root);
}
} else {
// If we're not going to commit this gesture we can stop the View Transition
// right away and delete the scheduled gesture from the pending queue.
if (gesture.prev === null) {
if (root.pendingGestures === gesture) {
// This was the currently rendering gesture.
root.pendingGestures = gesture.next;
let remainingLanes = root.pendingLanes;
if (root.pendingGestures === null) {
// Gestures don't clear their lanes while the gesture is still active but it
// might not be scheduled to do any more renders and so we shouldn't schedule
// any more gesture lane work until a new gesture is scheduled.
remainingLanes &= ~GestureLane;
}
markRootFinished(
root,
GestureLane,
remainingLanes,
NoLane,
NoLane,
NoLanes,
);
// If we had a currently rendering gesture we need to now reset the gesture lane to
// now render the next gesture or cancel if there's no more gestures in the queue.
restartGestureRoot(root);
}
root.stoppingGestures = gesture;
} else {
gesture.running = null;
// If there's no work scheduled so we can stop the View Transition right away.
stopViewTransition(runningTransition);
if (runningTransition !== null) {
stopViewTransition(runningTransition);
}
} else {
// This was not the current gesture so it doesn't affect the current render.
gesture.prev.next = gesture.next;
if (gesture.next !== null) {
gesture.next.prev = gesture.prev;
}
gesture.prev = null;
gesture.next = null;
}
}
}
}

export function deleteScheduledGesture(
root: FiberRoot,
gesture: ScheduledGesture,
): void {
if (gesture.prev === null) {
if (root.pendingGestures === gesture) {
root.pendingGestures = gesture.next;
if (root.pendingGestures === null) {
// Gestures don't clear their lanes while the gesture is still active but it
// might not be scheduled to do any more renders and so we shouldn't schedule
// any more gesture lane work until a new gesture is scheduled.
root.pendingLanes &= ~GestureLane;
}
}
if (root.stoppingGestures === gesture) {
// This should not really happen the way we use it now but just in case we start.
root.stoppingGestures = gesture.next;
}
} else {
gesture.prev.next = gesture.next;
if (gesture.next !== null) {
gesture.next.prev = gesture.prev;
export function stopCommittedGesture(root: FiberRoot) {
// The top was just committed. We can delete it from the queue
// and stop its View Transition now.
const committedGesture = root.pendingGestures;
if (committedGesture !== null) {
// Mark it as no longer committing and should no longer be included in rerenders.
committedGesture.committing = false;
const nextGesture = committedGesture.next;
if (nextGesture === null) {
// Gestures don't clear their lanes while the gesture is still active but it
// might not be scheduled to do any more renders and so we shouldn't schedule
// any more gesture lane work until a new gesture is scheduled.
root.pendingLanes &= ~GestureLane;
} else {
nextGesture.prev = null;
}
gesture.prev = null;
gesture.next = null;
}
}

export function stopCompletedGestures(root: FiberRoot) {
let gesture = root.stoppingGestures;
root.stoppingGestures = null;
while (gesture !== null) {
if (gesture.running !== null) {
stopViewTransition(gesture.running);
gesture.running = null;
root.pendingGestures = nextGesture;
const runningTransition = committedGesture.running;
if (runningTransition !== null) {
committedGesture.running = null;
stopViewTransition(runningTransition);
}
const nextGesture = gesture.next;
gesture.next = null;
gesture.prev = null;
gesture = nextGesture;
}
}
2 changes: 1 addition & 1 deletion packages/react-reconciler/src/ReactFiberHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -1384,7 +1384,7 @@ function updateReducerImpl<S, A>(
// ScheduledGesture.
const scheduledGesture = update.gesture;
if (scheduledGesture !== null) {
if (scheduledGesture.count === 0) {
if (scheduledGesture.count === 0 && !scheduledGesture.committing) {
// This gesture has already been cancelled. We can clean up this update.
update = update.next;
continue;
Expand Down
1 change: 0 additions & 1 deletion packages/react-reconciler/src/ReactFiberRoot.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,6 @@ function FiberRootNode(

if (enableGestureTransition) {
this.pendingGestures = null;
this.stoppingGestures = null;
this.gestureClone = null;
}

Expand Down
Loading
Loading