Skip to content
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
47 changes: 46 additions & 1 deletion fixtures/view-transition/src/components/Page.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,48 @@ export default function Page({url, navigate}) {
viewTransition.new.animate(keyframes, 250);
}

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) + '%',
};
viewTransition.old.animate(keyframes, options);
viewTransition.new.animate(keyframes, options);
} 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);
// TODO: Support returning a clean up function from ViewTransition events.
// return () => {
// cleanup1();
// cleanup2();
// };
}
}

function swipeAction() {
navigate(show ? '/?a' : '/?b');
}
Expand Down Expand Up @@ -131,7 +173,10 @@ export default function Page({url, navigate}) {
);

const exclamation = (
<ViewTransition name="exclamation" onShare={onTransition}>
<ViewTransition
name="exclamation"
onShare={onTransition}
onGestureShare={onGestureTransition}>
<span>
<div>!</div>
</span>
Expand Down
10 changes: 9 additions & 1 deletion packages/react-reconciler/src/ReactFiberApplyGesture.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ import {
enableComponentPerformanceTrack,
} from 'shared/ReactFeatureFlags';
import {trackAnimatingTask} from './ReactProfilerTimer';
import {scheduleGestureTransitionEvent} from './ReactFiberWorkLoop';

let didWarnForRootClone = false;

Expand Down Expand Up @@ -280,6 +281,7 @@ function applyAppearingPairViewTransition(child: Fiber): void {
if (clones !== null) {
applyViewTransitionToClones(name, className, clones, child);
}
scheduleGestureTransitionEvent(child, props.onGestureShare);
}
}
}
Expand Down Expand Up @@ -310,6 +312,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);
}
}
}

Expand Down Expand Up @@ -1123,7 +1130,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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ import {
hasInstanceAffectedParent,
wasInstanceInViewport,
} from './ReactFiberConfig';
import {scheduleViewTransitionEvent} from './ReactFiberWorkLoop';
import {
scheduleViewTransitionEvent,
scheduleGestureTransitionEvent,
} from './ReactFiberWorkLoop';
import {
getViewTransitionName,
getViewTransitionClassName,
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
}
Expand Down
65 changes: 64 additions & 1 deletion packages/react-reconciler/src/ReactFiberWorkLoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -26,6 +30,7 @@ import type {
Resource,
ViewTransitionInstance,
RunningViewTransition,
GestureTimeline,
SuspendedState,
} from './ReactFiberConfig';
import type {RootState} from './ReactFiberRoot';
Expand Down Expand Up @@ -913,6 +918,42 @@ export function scheduleViewTransitionEvent(
}
}

export function scheduleGestureTransitionEvent(
fiber: Fiber,
callback: ?(
timeline: GestureTimeline,
options: GestureOptionsRequired,
instance: ViewTransitionInstance,
types: Array<string>,
) => 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;
}
Expand Down Expand Up @@ -4352,6 +4393,8 @@ function applyGestureOnRoot(
startAnimating(pendingEffectsLanes);
}

pendingViewTransitionEvents = null;

const prevTransition = ReactSharedInternals.T;
ReactSharedInternals.T = null;
const previousPriority = getCurrentUpdatePriority();
Expand Down Expand Up @@ -4476,6 +4519,26 @@ 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 = [];
}
for (let i = 0; i < pendingEvents.length; i++) {
const viewTransitionEvent = pendingEvents[i];
viewTransitionEvent(pendingTypes);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This calls user code. Does it need to be wrapped in error handling? Didn't see where else that might be happening.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these are similar to onRecoverableError. I don't think they're handled properly in the existing one in flushSpawnedWork is right neither.

The way these phases flush is that they keep mutable state so that they can pick up and flush remaining phases if one is missed. So if this errors, then it errors the view transition which then flushes the remaining work which flushes the previous work but it doesn't flush this again so it probably misses anything after this. That means that bugs in React code might not be properly handled.

However, to ensure that other callbacks are handled, these should probably be wrapped in try/catch for each callback so that they're independently handled. Same thing for onRecoverableError.

There's also a question of what the default priority and execution context should be for these.

}
}
}

if (enableProfilerTimer && enableComponentPerformanceTrack) {
finalizeRender(lanes, commitEndTime);
}
Expand Down
29 changes: 29 additions & 0 deletions packages/shared/ReactTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,11 @@ export type ViewTransitionClass =
| string
| ViewTransitionClassPerType;

export type GestureOptionsRequired = {
rangeStart: number,
rangeEnd: number,
};

export type ViewTransitionProps = {
name?: string,
children?: ReactNodeList,
Expand All @@ -302,6 +307,30 @@ export type ViewTransitionProps = {
onExit?: (instance: ViewTransitionInstance, types: Array<string>) => void,
onShare?: (instance: ViewTransitionInstance, types: Array<string>) => void,
onUpdate?: (instance: ViewTransitionInstance, types: Array<string>) => void,
onGestureEnter?: (
timeline: GestureProvider,
options: GestureOptionsRequired,
instance: ViewTransitionInstance,
types: Array<string>,
) => void,
onGestureExit?: (
timeline: GestureProvider,
options: GestureOptionsRequired,
instance: ViewTransitionInstance,
types: Array<string>,
) => void,
onGestureShare?: (
timeline: GestureProvider,
options: GestureOptionsRequired,
instance: ViewTransitionInstance,
types: Array<string>,
) => void,
onGestureUpdate?: (
timeline: GestureProvider,
options: GestureOptionsRequired,
instance: ViewTransitionInstance,
types: Array<string>,
) => void,
};

export type ActivityProps = {
Expand Down
Loading