Skip to content
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

Allow passing range option to useSwipeTransition #32412

Merged
merged 2 commits into from
Feb 21, 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
5 changes: 5 additions & 0 deletions packages/react-art/src/ReactFiberConfigART.js
Original file line number Diff line number Diff line change
Expand Up @@ -510,8 +510,13 @@ export function createViewTransitionInstance(

export type GestureTimeline = null;

export function getCurrentGestureOffset(provider: GestureTimeline): number {
throw new Error('useSwipeTransition is not yet supported in react-art.');
}

export function subscribeToGestureDirection(
provider: GestureTimeline,
currentOffset: number,
directionCallback: (direction: boolean) => void,
): () => void {
throw new Error('useSwipeTransition is not yet supported in react-art.');
Expand Down
32 changes: 17 additions & 15 deletions packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -1480,17 +1480,21 @@ export function createViewTransitionInstance(

export type GestureTimeline = AnimationTimeline; // TODO: More provider types.

export function subscribeToGestureDirection(
provider: GestureTimeline,
directionCallback: (direction: boolean) => void,
): () => void {
export function getCurrentGestureOffset(provider: GestureTimeline): number {
const time = provider.currentTime;
if (time === null) {
throw new Error(
'Cannot start a gesture with a disconnected AnimationTimeline.',
);
}
const startTime = typeof time === 'number' ? time : time.value;
return typeof time === 'number' ? time : time.value;
}

export function subscribeToGestureDirection(
provider: GestureTimeline,
currentOffset: number,
directionCallback: (direction: boolean) => void,
): () => void {
if (
typeof ScrollTimeline === 'function' &&
provider instanceof ScrollTimeline
Expand All @@ -1500,11 +1504,10 @@ export function subscribeToGestureDirection(
const scrollCallback = () => {
const newTime = provider.currentTime;
if (newTime !== null) {
directionCallback(
typeof newTime === 'number'
? newTime > startTime
: newTime.value > startTime,
);
const newValue = typeof newTime === 'number' ? newTime : newTime.value;
if (newValue !== currentOffset) {
directionCallback(newValue > currentOffset);
}
}
};
element.addEventListener('scroll', scrollCallback, false);
Expand All @@ -1517,11 +1520,10 @@ export function subscribeToGestureDirection(
const rafCallback = () => {
const newTime = provider.currentTime;
if (newTime !== null) {
directionCallback(
typeof newTime === 'number'
? newTime > startTime
: newTime.value > startTime,
);
const newValue = typeof newTime === 'number' ? newTime : newTime.value;
if (newValue !== currentOffset) {
directionCallback(newValue > currentOffset);
}
}
callbackID = requestAnimationFrame(rafCallback);
};
Expand Down
5 changes: 5 additions & 0 deletions packages/react-native-renderer/src/ReactFiberConfigNative.js
Original file line number Diff line number Diff line change
Expand Up @@ -607,8 +607,13 @@ export function createViewTransitionInstance(

export type GestureTimeline = null;

export function getCurrentGestureOffset(provider: GestureTimeline): number {
throw new Error('useSwipeTransition is not yet supported in React Native.');
}

export function subscribeToGestureDirection(
provider: GestureTimeline,
currentOffset: number,
directionCallback: (direction: boolean) => void,
): () => void {
throw new Error('useSwipeTransition is not yet supported in React Native.');
Expand Down
5 changes: 5 additions & 0 deletions packages/react-noop-renderer/src/createReactNoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -796,8 +796,13 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
return null;
},

getCurrentGestureOffset(provider: GestureTimeline): number {
return 0;
},

subscribeToGestureDirection(
provider: GestureTimeline,
currentOffset: number,
directionCallback: (direction: boolean) => void,
): () => void {
return () => {};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,5 @@ export const startViewTransition = shim;
export type ViewTransitionInstance = null | {name: string, ...};
export const createViewTransitionInstance = shim;
export type GestureTimeline = any;
export const getCurrentGestureOffset = shim;
export const subscribeToGestureDirection = shim;
55 changes: 36 additions & 19 deletions packages/react-reconciler/src/ReactFiberGestureScheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ export type ScheduledGesture = {
provider: GestureTimeline,
count: number, // The number of times this same provider has been started.
direction: boolean, // false = previous, true = next
rangePrevious: number, // The end along the timeline where the previous state is reached.
rangeCurrent: number, // The starting offset along the timeline.
rangeNext: number, // The end along the timeline where the next state is reached.
cancel: () => void, // Cancel the subscription to direction change.
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 @@ -28,6 +31,9 @@ export function scheduleGesture(
root: FiberRoot,
provider: GestureTimeline,
initialDirection: boolean,
rangePrevious: number,
rangeCurrent: number,
rangeNext: number,
): ScheduledGesture {
let prev = root.gestures;
while (prev !== null) {
Expand All @@ -42,32 +48,43 @@ export function scheduleGesture(
}
prev = next;
}
const isFlippedDirection = rangePrevious > rangeNext;
// Add new instance to the end of the queue.
const cancel = subscribeToGestureDirection(provider, (direction: boolean) => {
if (gesture.direction !== direction) {
gesture.direction = direction;
if (gesture.prev === null && root.gestures !== gesture) {
// This gesture is not in the schedule, meaning it was already rendered.
// We need to rerender in the new direction. Insert it into the first slot
// in case other gestures are queued after the on-going one.
const existing = root.gestures;
gesture.next = existing;
if (existing !== null) {
existing.prev = gesture;
const cancel = subscribeToGestureDirection(
provider,
rangeCurrent,
(direction: boolean) => {
if (isFlippedDirection) {
direction = !direction;
}
if (gesture.direction !== direction) {
gesture.direction = direction;
if (gesture.prev === null && root.gestures !== gesture) {
// This gesture is not in the schedule, meaning it was already rendered.
// We need to rerender in the new direction. Insert it into the first slot
// in case other gestures are queued after the on-going one.
const existing = root.gestures;
gesture.next = existing;
if (existing !== null) {
existing.prev = gesture;
}
root.gestures = gesture;
// Schedule the lane on the root. The Fibers will already be marked as
// long as the gesture is active on that Hook.
root.pendingLanes |= GestureLane;
ensureRootIsScheduled(root);
}
root.gestures = gesture;
// Schedule the lane on the root. The Fibers will already be marked as
// long as the gesture is active on that Hook.
root.pendingLanes |= GestureLane;
ensureRootIsScheduled(root);
// TODO: If we're currently rendering this gesture, we need to restart it.
}
// TODO: If we're currently rendering this gesture, we need to restart it.
}
});
},
);
const gesture: ScheduledGesture = {
provider: provider,
count: 1,
direction: initialDirection,
rangePrevious: rangePrevious,
rangeCurrent: rangeCurrent,
rangeNext: rangeNext,
cancel: cancel,
prev: prev,
next: null,
Expand Down
39 changes: 38 additions & 1 deletion packages/react-reconciler/src/ReactFiberHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
Awaited,
StartGesture,
GestureProvider,
GestureOptions,
} from 'shared/ReactTypes';
import type {
Fiber,
Expand All @@ -35,6 +36,7 @@ import {
NotPendingTransition as NoPendingHostTransition,
setCurrentUpdatePriority,
getCurrentUpdatePriority,
getCurrentGestureOffset,
} from './ReactFiberConfig';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import {
Expand Down Expand Up @@ -3988,6 +3990,7 @@ function startGesture(
fiber: Fiber,
queue: SwipeTransitionUpdateQueue,
gestureProvider: GestureProvider,
gestureOptions?: GestureOptions,
): () => void {
const root = enqueueGestureRender(fiber);
if (root === null) {
Expand All @@ -3998,10 +4001,44 @@ function startGesture(
};
}
const gestureTimeline: GestureTimeline = gestureProvider;
const currentOffset = getCurrentGestureOffset(gestureTimeline);
const range = gestureOptions && gestureOptions.range;
const rangePrevious: number = range ? range[0] : 0; // If no range is provider we assume it's the starting point of the range.
const rangeCurrent: number = range ? range[1] : currentOffset;
const rangeNext: number = range ? range[2] : 100; // If no range is provider we assume it's the starting point of the range.
if (__DEV__) {
if (
(rangePrevious > rangeCurrent && rangeNext > rangeCurrent) ||
(rangePrevious < rangeCurrent && rangeNext < rangeCurrent)
) {
console.error(
'The range of a gesture needs "previous" and "next" to be on either side of ' +
'the "current" offset. Both cannot be above current and both cannot be below current.',
);
}
}
const isFlippedDirection = rangePrevious > rangeNext;
const initialDirection =
// If a range is specified we can imply initial direction if it's not the current
// value such as if the gesture starts after it has already moved.
currentOffset < rangeCurrent
? isFlippedDirection
: currentOffset > rangeCurrent
? !isFlippedDirection
: // Otherwise, look for an explicit option.
gestureOptions && gestureOptions.direction === 'next'
? true
: gestureOptions && gestureOptions.direction === 'previous'
? false
: // If no option is specified, imply from the values specified.
queue.initialDirection;
Comment on lines +4024 to +4034
Copy link
Contributor

Choose a reason for hiding this comment

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

The nested ternaries are tough to reason about here. Might be more readable in if blocks.

Took some stepping through to realize we only look for the gestureOptions.direction or queue.initialDirection if the currentOffset is the rangeCurrent. I had expected an explicit direction object to override when available. But I guess it makes sense to base off movement because even a one-way gesture needs to be able to reset if its pulled back the other way.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah I see from the next PR that the movement may have started before the scroll event. Which the currentOffset can represent here.

const scheduledGesture = scheduleGesture(
root,
gestureTimeline,
queue.initialDirection,
initialDirection,
rangePrevious,
rangeCurrent,
rangeNext,
);
// Add this particular instance to the queue.
// We add multiple of the same timeline even if they get batched so
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ export const wasInstanceInViewport = $$$config.wasInstanceInViewport;
export const hasInstanceChanged = $$$config.hasInstanceChanged;
export const hasInstanceAffectedParent = $$$config.hasInstanceAffectedParent;
export const startViewTransition = $$$config.startViewTransition;
export const getCurrentGestureOffset = $$$config.getCurrentGestureOffset;
export const subscribeToGestureDirection =
$$$config.subscribeToGestureDirection;
export const createViewTransitionInstance =
Expand Down
5 changes: 5 additions & 0 deletions packages/react-test-renderer/src/ReactFiberConfigTestHost.js
Original file line number Diff line number Diff line change
Expand Up @@ -393,8 +393,13 @@ export function getInstanceFromNode(mockNode: Object): Object | null {

export type GestureTimeline = null;

export function getCurrentGestureOffset(provider: GestureTimeline): number {
return 0;
}

export function subscribeToGestureDirection(
provider: GestureTimeline,
currentOffset: number,
directionCallback: (direction: boolean) => void,
): () => void {
return () => {};
Expand Down
10 changes: 9 additions & 1 deletion packages/shared/ReactTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,15 @@ export type ReactFormState<S, ReferenceId> = [
// renderer supports it.
export type GestureProvider = any;

export type StartGesture = (gestureProvider: GestureProvider) => () => void;
export type StartGesture = (
gestureProvider: GestureProvider,
gestureOptions: GestureOptions,
) => () => void;

export type GestureOptions = {
direction?: 'previous' | 'next',
range?: [/*previous*/ number, /*current*/ number, /*next*/ number],
};

export type Awaited<T> = T extends null | void
? T // special case for `null | undefined` when not in `--strictNullChecks` mode
Expand Down
Loading