Skip to content

Commit

Permalink
Trigger events when a transition starts after transition.ready
Browse files Browse the repository at this point in the history
  • Loading branch information
sebmarkbage committed Jan 10, 2025
1 parent 0bf1f39 commit 6e23a51
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 3 deletions.
33 changes: 32 additions & 1 deletion packages/react-reconciler/src/ReactFiberCommitWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ import {
addMarkerIncompleteCallbackToPendingTransition,
addMarkerCompleteCallbackToPendingTransition,
retryDehydratedSuspenseBoundary,
scheduleViewTransitionEvent,
} from './ReactFiberWorkLoop';
import {
HasEffect as HookHasEffect,
Expand Down Expand Up @@ -649,6 +650,7 @@ function commitAppearingPairViewTransitions(placement: Fiber): void {
if (child.tag === OffscreenComponent && child.memoizedState === null) {
// This tree was already hidden so we skip it.
} else {
commitAppearingPairViewTransitions(child);
if (
child.tag === ViewTransitionComponent &&
(child.flags & ViewTransitionNamedStatic) !== NoFlags
Expand Down Expand Up @@ -682,7 +684,6 @@ function commitAppearingPairViewTransitions(placement: Fiber): void {
}
}
}
commitAppearingPairViewTransitions(child);
}
child = child.sibling;
}
Expand All @@ -701,12 +702,18 @@ function commitEnterViewTransitions(placement: Fiber): void {
false,
);
if (!inViewport) {
// TODO: If this was part of a pair we will still run the onShare callback.
// Revert the transition names. This boundary is not in the viewport
// so we won't bother animating it.
restoreViewTransitionOnHostInstances(placement.child, false);
// TODO: Should we still visit the children in case a named one was in the viewport?
} else {
commitAppearingPairViewTransitions(placement);

const state: ViewTransitionState = placement.stateNode;
if (!state.paired) {
scheduleViewTransitionEvent(placement, props.onEnter);
}
}
} else if ((placement.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
let child = placement.child;
Expand Down Expand Up @@ -764,6 +771,9 @@ function commitDeletedPairViewTransitions(
const oldinstance: ViewTransitionState = child.stateNode;
const newInstance: ViewTransitionState = pair;
newInstance.paired = oldinstance;
// Note: If the other side ends up outside the viewport, we'll still run this.
// Therefore it's possible for onShare to be called with only an old snapshot.
scheduleViewTransitionEvent(child, props.onShare);
}
// Delete the entry so that we know when we've found all of them
// and can stop searching (size reaches zero).
Expand Down Expand Up @@ -811,9 +821,16 @@ function commitExitViewTransitions(
// Delete the entry so that we know when we've found all of them
// and can stop searching (size reaches zero).
appearingViewTransitions.delete(name);
// Note: If the other side ends up outside the viewport, we'll still run this.
// Therefore it's possible for onShare to be called with only an old snapshot.
scheduleViewTransitionEvent(deletion, props.onShare);
} else {
scheduleViewTransitionEvent(deletion, props.onExit);
}
// Look for more pairs deeper in the tree.
commitDeletedPairViewTransitions(deletion, appearingViewTransitions);
} else {
scheduleViewTransitionEvent(deletion, props.onExit);
}
} else if ((deletion.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
let child = deletion.child;
Expand Down Expand Up @@ -1118,6 +1135,8 @@ function measureNestedViewTransitions(changedParent: Fiber): void {
child.memoizedState,
false,
);
const props: ViewTransitionProps = child.memoizedProps;
scheduleViewTransitionEvent(child, props.onLayout);
}
} else if ((child.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
measureNestedViewTransitions(child);
Expand Down Expand Up @@ -3075,6 +3094,8 @@ function commitAfterMutationEffectsOnFiber(
(Placement | Update | ChildDeletion | ContentReset | Visibility)) !==
NoFlags
) {
const wasMutated = (finishedWork.flags & Update) !== NoFlags;

const prevContextChanged = viewTransitionContextChanged;
const prevCancelableChildren = viewTransitionCancelableChildren;
viewTransitionContextChanged = false;
Expand Down Expand Up @@ -3103,7 +3124,17 @@ function commitAfterMutationEffectsOnFiber(
);
viewTransitionCancelableChildren = prevCancelableChildren;
}
// 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 {
const props: ViewTransitionProps = finishedWork.memoizedProps;
scheduleViewTransitionEvent(
finishedWork,
wasMutated || viewTransitionContextChanged
? props.onUpdate
: props.onLayout,
);

// If this boundary did update, we cannot cancel its children so those are dropped.
viewTransitionCancelableChildren = prevCancelableChildren;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ export type ViewTransitionProps = {
name?: string,
className?: string,
children?: ReactNodeList,
onEnter?: (instance: ViewTransitionInstance) => void,
onExit?: (instance: ViewTransitionInstance) => void,
onLayout?: (instance: ViewTransitionInstance) => void,
onShare?: (instance: ViewTransitionInstance) => void,
onUpdate?: (instance: ViewTransitionInstance) => void,
};

export type ViewTransitionState = {
Expand Down
48 changes: 46 additions & 2 deletions packages/react-reconciler/src/ReactFiberWorkLoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@ import type {
TransitionAbort,
} from './ReactFiberTracingMarkerComponent';
import type {OffscreenInstance} from './ReactFiberActivityComponent';
import type {Resource} from './ReactFiberConfig';
import type {Resource, ViewTransitionInstance} from './ReactFiberConfig';
import type {RootState} from './ReactFiberRoot';
import type {ViewTransitionState} from './ReactFiberViewTransitionComponent';
import {
getViewTransitionName,
type ViewTransitionState,
} from './ReactFiberViewTransitionComponent';

import {
enableCreateEventHandleAPI,
Expand Down Expand Up @@ -95,6 +98,7 @@ import {
resolveUpdatePriority,
trackSchedulerEvent,
startViewTransition,
createViewTransitionInstance,
} from './ReactFiberConfig';

import {createWorkInProgress, resetWorkInProgress} from './ReactFiber';
Expand Down Expand Up @@ -649,6 +653,7 @@ let pendingEffectsRemainingLanes: Lanes = NoLanes;
let pendingEffectsRenderEndTime: number = -0; // Profiling-only
let pendingPassiveTransitions: Array<Transition> | null = null;
let pendingRecoverableErrors: null | Array<CapturedValue<mixed>> = null;
let pendingViewTransitionEvents: Array<() => void> | null = null;
let pendingDidIncludeRenderPhaseUpdate: boolean = false;
let pendingSuspendedCommitReason: SuspendedCommitReason = IMMEDIATE_COMMIT; // Profiling-only

Expand Down Expand Up @@ -797,6 +802,27 @@ export function requestDeferredLane(): Lane {
return workInProgressDeferredLane;
}

export function scheduleViewTransitionEvent(
fiber: Fiber,
callback: ?(instance: ViewTransitionInstance) => void,
): void {
if (enableViewTransition) {
if (callback != null) {
const state: ViewTransitionState = fiber.stateNode;
let instance = state.ref;
if (instance === null) {
instance = state.ref = createViewTransitionInstance(
getViewTransitionName(fiber.memoizedProps, state),
);
}
if (pendingViewTransitionEvents === null) {
pendingViewTransitionEvents = [];
}
pendingViewTransitionEvents.push(callback.bind(null, instance));
}
}
}

export function peekDeferredLane(): Lane {
return workInProgressDeferredLane;
}
Expand Down Expand Up @@ -3322,6 +3348,9 @@ function commitRoot(
pendingEffectsRemainingLanes = remainingLanes;
pendingPassiveTransitions = transitions;
pendingRecoverableErrors = recoverableErrors;
if (enableViewTransition) {
pendingViewTransitionEvents = null;
}
pendingDidIncludeRenderPhaseUpdate = didIncludeRenderPhaseUpdate;
if (enableProfilerTimer) {
pendingEffectsRenderEndTime = completedRenderEndTime;
Expand Down Expand Up @@ -3673,6 +3702,21 @@ function flushSpawnedWork(): void {
}
}

if (enableViewTransition) {
// We should now be after the startViewTransition's .ready call which is late enough
// to start animating any pseudo-elements. We do this before flushing any passive
// effects or spawned sync work since this is still part of the previous commit.
// Even though conceptually it's like its own task between layout effets and passive.
const pendingEvents = pendingViewTransitionEvents;
if (pendingEvents !== null) {
pendingViewTransitionEvents = null;
for (let i = 0; i < pendingEvents.length; i++) {
const viewTransitionEvent = pendingEvents[i];
viewTransitionEvent();
}
}
}

// If the passive effects are the result of a discrete render, flush them
// synchronously at the end of the current task so that the result is
// immediately observable. Otherwise, we assume that they are not
Expand Down

0 comments on commit 6e23a51

Please sign in to comment.