introduce Fantom.scrollTo (#48907)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48907

changelog: [internal]

Introduce new function `Fantom.scrollTo`, which will fake a scroll to particular position.
Calling the method will call onScroll event using codepath that iOS uses. It will also set C++ state so the new scroll position is observable from JavaScript.

Reviewed By: javache

Differential Revision: D68554703

fbshipit-source-id: 2fc71e96836a03ec343053ceed85764c4bc2f5c7
This commit is contained in:
Samuel Susla
2025-01-25 14:58:28 -08:00
committed by Facebook GitHub Bot
parent 84b8e6a531
commit aa5760837c
4 changed files with 131 additions and 0 deletions
@@ -473,4 +473,108 @@ describe('Fantom', () => {
y: 2,
});
});
describe('scrollTo', () => {
it('throws error if called on node that is not scroll view', () => {
const root = Fantom.createRoot();
let maybeNode;
Fantom.runTask(() => {
root.render(
<View
ref={node => {
maybeNode = node;
}}
/>,
);
});
const element = ensureInstance(maybeNode, ReactNativeElement);
expect(() => {
Fantom.runOnUIThread(() => {
Fantom.scrollTo(element, {
x: 0,
y: 1,
});
});
}).toThrow(
'Exception in HostFunction: scrollTo() can only be called on <ScrollView />',
);
});
it('delivers onScroll event and affects position of elements on screen', () => {
const root = Fantom.createRoot();
let maybeScrollViewNode;
let maybeNode;
const onScroll = jest.fn();
Fantom.runTask(() => {
root.render(
<ScrollView
onScroll={event => {
onScroll(event.nativeEvent);
}}
ref={node => {
maybeScrollViewNode = node;
}}>
<View
style={{width: 1, height: 2, top: 3}}
ref={node => {
maybeNode = node;
}}
/>
</ScrollView>,
);
});
const scrollViewElement = ensureInstance(
maybeScrollViewNode,
ReactNativeElement,
);
Fantom.runOnUIThread(() => {
Fantom.scrollTo(scrollViewElement, {
x: 0,
y: 1,
});
});
Fantom.runWorkLoop();
expect(onScroll).toHaveBeenCalledTimes(1);
const viewElement = ensureInstance(maybeNode, ReactNativeElement);
let rect;
viewElement.measure((x, y, width, height, pageX, pageY) => {
rect = {
x,
y,
width,
height,
pageX,
pageY,
};
});
expect(rect).toEqual({
x: 0,
y: 3,
width: 1,
height: 2,
pageY: 2,
pageX: 0,
});
const boundingClientRect = viewElement.getBoundingClientRect();
expect(boundingClientRect.x).toBe(0);
expect(boundingClientRect.y).toBe(2);
expect(boundingClientRect.width).toBe(1);
expect(boundingClientRect.height).toBe(2);
root.destroy();
});
});
});
+11
View File
@@ -167,6 +167,16 @@ function dispatchNativeEvent(
);
}
function scrollTo(
node: ReactNativeElement,
options: {x: number, y: number, zoomScale?: number},
) {
const shadowNode = getShadowNode(node);
NativeFantom.scrollTo(shadowNode, options);
}
export const unstable_benchmark = Benchmark;
type FantomConstants = $ReadOnly<{
isRunningFromCI: boolean,
}>;
@@ -255,4 +265,5 @@ export default {
createRoot,
dispatchNativeEvent,
unstable_benchmark: Benchmark,
scrollTo,
};
@@ -10465,6 +10465,11 @@ export enum NativeEventCategory {
Discrete = 3,
Continuous = 4,
}
export type ScrollOptions = {
x: number,
y: number,
zoomScale?: number,
};
interface Spec extends TurboModule {
startSurface: (
surfaceId: number,
@@ -10480,6 +10485,7 @@ interface Spec extends TurboModule {
category?: NativeEventCategory,
isUnique?: boolean
) => void;
scrollTo: (shadowNode: mixed, options: ScrollOptions) => void;
getMountingManagerLogs: (surfaceId: number) => Array<string>;
flushMessageQueue: () => void;
flushEventQueue: () => void;
@@ -51,6 +51,12 @@ export enum NativeEventCategory {
Continuous = 4,
}
export type ScrollOptions = {
x: number,
y: number,
zoomScale?: number,
};
interface Spec extends TurboModule {
startSurface: (
surfaceId: number,
@@ -66,6 +72,10 @@ interface Spec extends TurboModule {
category?: NativeEventCategory,
isUnique?: boolean,
) => void;
scrollTo: (
shadowNode: mixed /* ShadowNode */,
options: ScrollOptions,
) => void;
getMountingManagerLogs: (surfaceId: number) => Array<string>;
flushMessageQueue: () => void;
flushEventQueue: () => void;