introduce convenience method Fantom.scrollTo (#50011)

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

changelog: [internal]

Majority of time, you want to simply scroll. This diff introduces a convenience method Fantom.scrollTo.

Previously, you would have to write:
```
  Fantom.runOnUIThread(() => {
    Fantom.enqueueScrollEvent(element, {
      x: 0,
      y: 60,
    });
  });
  Fantom.runWorkLoop();
```

Now, you can just do:
```
 Fantom.scrollTo(element, {
    x: 0,
    y: 60,
  });
```

Reviewed By: rubennorte

Differential Revision: D71113680

fbshipit-source-id: aed49a2f12be8ab30be549235cc7a2b3e35faadb
This commit is contained in:
Samuel Susla
2025-03-13 07:42:32 -07:00
committed by Facebook GitHub Bot
parent b275a318a6
commit 65ffc588fb
3 changed files with 202 additions and 116 deletions
@@ -638,6 +638,106 @@ describe('Fantom', () => {
});
});
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.enqueueScrollEvent(element, {
x: 0,
y: 1,
});
});
}).toThrow(
'Exception in HostFunction: enqueueScrollEvent() 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.scrollTo(scrollViewElement, {
x: 0,
y: 1,
});
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();
});
});
describe('flushAllNativeEvents', () => {
it('calls events in the event queue', () => {
const root = Fantom.createRoot();
+44 -1
View File
@@ -229,14 +229,56 @@ function dispatchNativeEvent(
runWorkLoop();
}
export type ScrollEventOptions = {
x: number,
y: number,
zoomScale?: number,
};
function enqueueScrollEvent(
node: ReactNativeElement,
options: {x: number, y: number, zoomScale?: number},
options: ScrollEventOptions,
) {
const shadowNode = getNativeNodeReference(node);
NativeFantom.enqueueScrollEvent(shadowNode, options);
}
/**
* Scrolls the specified ScrollView node to the given coordinates on the UI thread.
* The call is immediately observable unlike `Fantom.enqueueScrollEvent` where the
* event is queued and not processed.
*
* @example
* ```
* const root = Fantom.createRoot();
* let maybeScrollViewNode;
*
* Fantom.runTask(() => {
* root.render(
* <ScrollView
* ref={node => {
* maybeScrollViewNode = node;
* }} />
* <ScrollViewContent />
* </ScrollView>,
* );
* });
*
* const element = ensureInstance(maybeScrollViewNode, ReactNativeElement);
*
* Fantom.scrollTo(element, {x: 0, y: 20});
*
* // Assert that changes from Fantom.scrollTo are in effect.
* ```
*/
function scrollTo(node: ReactNativeElement, options: ScrollEventOptions) {
runOnUIThread(() => {
enqueueScrollEvent(node, options);
});
runWorkLoop();
}
function enqueueModalSizeUpdate(
node: ReactNativeElement,
size: {width: number, height: number},
@@ -347,5 +389,6 @@ export default {
enqueueModalSizeUpdate,
unstable_benchmark: Benchmark,
enqueueScrollEvent,
scrollTo,
saveJSMemoryHeapSnapshot,
};
@@ -49,13 +49,10 @@ test('basic culling', () => {
const element = ensureInstance(maybeNode, ReactNativeElement);
Fantom.runOnUIThread(() => {
Fantom.enqueueScrollEvent(element, {
x: 0,
y: 60,
});
Fantom.scrollTo(element, {
x: 0,
y: 60,
});
Fantom.runWorkLoop();
expect(root.takeMountingManagerLogs()).toEqual([
'Remove {type: "View", parentNativeID: (N/A), index: 0, nativeID: "child"}',
@@ -65,13 +62,10 @@ test('basic culling', () => {
'Update {type: "ScrollView", nativeID: (N/A)}',
]);
Fantom.runOnUIThread(() => {
Fantom.enqueueScrollEvent(element, {
x: 0,
y: 0,
});
Fantom.scrollTo(element, {
x: 0,
y: 0,
});
Fantom.runWorkLoop();
expect(root.takeMountingManagerLogs()).toEqual([
'Update {type: "ScrollView", nativeID: (N/A)}',
@@ -132,26 +126,21 @@ test('recursive culling', () => {
const element = ensureInstance(maybeNode, ReactNativeElement);
// === Scroll down to the edge of child AA ===
Fantom.runOnUIThread(() => {
Fantom.enqueueScrollEvent(element, {
x: 0,
y: 30,
});
Fantom.scrollTo(element, {
x: 0,
y: 30,
});
Fantom.runWorkLoop();
expect(root.takeMountingManagerLogs()).toEqual([
'Update {type: "ScrollView", nativeID: (N/A)}',
]);
// === Scroll down past child AA ===
Fantom.runOnUIThread(() => {
Fantom.enqueueScrollEvent(element, {
x: 0,
y: 36,
});
Fantom.scrollTo(element, {
x: 0,
y: 36,
});
Fantom.runWorkLoop();
expect(root.takeMountingManagerLogs()).toEqual([
'Update {type: "ScrollView", nativeID: (N/A)}',
@@ -160,13 +149,10 @@ test('recursive culling', () => {
]);
// === Scroll down past child AB ===
Fantom.runOnUIThread(() => {
Fantom.enqueueScrollEvent(element, {
x: 0,
y: 51,
});
Fantom.scrollTo(element, {
x: 0,
y: 51,
});
Fantom.runWorkLoop();
expect(root.takeMountingManagerLogs()).toEqual([
'Update {type: "ScrollView", nativeID: (N/A)}',
@@ -175,13 +161,10 @@ test('recursive culling', () => {
]);
// === Scroll down past element A ===
Fantom.runOnUIThread(() => {
Fantom.enqueueScrollEvent(element, {
x: 0,
y: 56,
});
Fantom.scrollTo(element, {
x: 0,
y: 56,
});
Fantom.runWorkLoop();
expect(root.takeMountingManagerLogs()).toEqual([
'Update {type: "ScrollView", nativeID: (N/A)}',
@@ -190,13 +173,10 @@ test('recursive culling', () => {
]);
// Scroll element B into viewport. Just child BA should be created.
Fantom.runOnUIThread(() => {
Fantom.enqueueScrollEvent(element, {
x: 0,
y: 155,
});
Fantom.scrollTo(element, {
x: 0,
y: 155,
});
Fantom.runWorkLoop();
expect(root.takeMountingManagerLogs()).toEqual([
'Update {type: "ScrollView", nativeID: (N/A)}',
@@ -207,13 +187,10 @@ test('recursive culling', () => {
]);
// Scroll child BA into viewport.
Fantom.runOnUIThread(() => {
Fantom.enqueueScrollEvent(element, {
x: 0,
y: 165,
});
Fantom.scrollTo(element, {
x: 0,
y: 165,
});
Fantom.runWorkLoop();
expect(root.takeMountingManagerLogs()).toEqual([
'Update {type: "ScrollView", nativeID: (N/A)}',
@@ -222,13 +199,10 @@ test('recursive culling', () => {
]);
// Scroll back to start
Fantom.runOnUIThread(() => {
Fantom.enqueueScrollEvent(element, {
x: 0,
y: 0,
});
Fantom.scrollTo(element, {
x: 0,
y: 0,
});
Fantom.runWorkLoop();
expect(root.takeMountingManagerLogs()).toEqual([
'Update {type: "ScrollView", nativeID: (N/A)}',
@@ -247,13 +221,10 @@ test('recursive culling', () => {
]);
// Scroll past element A
Fantom.runOnUIThread(() => {
Fantom.enqueueScrollEvent(element, {
x: 0,
y: 85,
});
Fantom.scrollTo(element, {
x: 0,
y: 85,
});
Fantom.runWorkLoop();
expect(root.takeMountingManagerLogs()).toEqual([
'Update {type: "ScrollView", nativeID: (N/A)}',
@@ -303,13 +274,10 @@ test('recursive culling when initial offset is negative', () => {
const element = ensureInstance(maybeNode, ReactNativeElement);
Fantom.runOnUIThread(() => {
Fantom.enqueueScrollEvent(element, {
x: 0,
y: 0,
});
Fantom.scrollTo(element, {
x: 0,
y: 0,
});
Fantom.runWorkLoop();
expect(root.takeMountingManagerLogs()).toEqual([
'Update {type: "ScrollView", nativeID: (N/A)}',
@@ -373,13 +341,10 @@ test('deep nesting', () => {
const element = ensureInstance(maybeNode, ReactNativeElement);
Fantom.runOnUIThread(() => {
Fantom.enqueueScrollEvent(element, {
x: 0,
y: 40,
});
Fantom.scrollTo(element, {
x: 0,
y: 40,
});
Fantom.runWorkLoop();
expect(root.takeMountingManagerLogs()).toEqual([
'Update {type: "ScrollView", nativeID: (N/A)}',
@@ -391,13 +356,10 @@ test('deep nesting', () => {
'Insert {type: "View", parentNativeID: (N/A), index: 1, nativeID: "element B"}',
]);
Fantom.runOnUIThread(() => {
Fantom.enqueueScrollEvent(element, {
x: 0,
y: 150,
});
Fantom.scrollTo(element, {
x: 0,
y: 150,
});
Fantom.runWorkLoop();
expect(root.takeMountingManagerLogs()).toEqual([
'Update {type: "ScrollView", nativeID: (N/A)}',
@@ -542,13 +504,10 @@ test('initial render', () => {
const element = ensureInstance(maybeNode, ReactNativeElement);
Fantom.runOnUIThread(() => {
Fantom.enqueueScrollEvent(element, {
x: 0,
y: 100,
});
Fantom.scrollTo(element, {
x: 0,
y: 100,
});
Fantom.runWorkLoop();
expect(root.takeMountingManagerLogs()).toEqual([
'Update {type: "ScrollView", nativeID: (N/A)}',
@@ -621,13 +580,10 @@ test('basic culling smaller ScrollView', () => {
const element = ensureInstance(maybeNode, ReactNativeElement);
Fantom.runOnUIThread(() => {
Fantom.enqueueScrollEvent(element, {
x: 0,
y: 11,
});
Fantom.scrollTo(element, {
x: 0,
y: 11,
});
Fantom.runWorkLoop();
expect(root.takeMountingManagerLogs()).toEqual([
'Remove {type: "View", parentNativeID: (N/A), index: 0, nativeID: "element 1"}',
@@ -691,13 +647,10 @@ test('culling with transform move', () => {
const element = ensureInstance(maybeNode, ReactNativeElement);
Fantom.runOnUIThread(() => {
Fantom.enqueueScrollEvent(element, {
x: 0,
y: 1,
});
Fantom.scrollTo(element, {
x: 0,
y: 1,
});
Fantom.runWorkLoop();
expect(root.takeMountingManagerLogs()).toEqual([
'Update {type: "ScrollView", nativeID: (N/A)}',
@@ -743,13 +696,10 @@ test('culling with recursive transform move', () => {
const element = ensureInstance(maybeNode, ReactNativeElement);
Fantom.runOnUIThread(() => {
Fantom.enqueueScrollEvent(element, {
x: 0,
y: 1,
});
Fantom.scrollTo(element, {
x: 0,
y: 1,
});
Fantom.runWorkLoop();
expect(root.takeMountingManagerLogs()).toEqual([
'Update {type: "ScrollView", nativeID: (N/A)}',
@@ -794,13 +744,10 @@ test('culling with transform scale', () => {
const element = ensureInstance(maybeNode, ReactNativeElement);
Fantom.runOnUIThread(() => {
Fantom.enqueueScrollEvent(element, {
x: 0,
y: 121,
});
Fantom.scrollTo(element, {
x: 0,
y: 121,
});
Fantom.runWorkLoop();
expect(root.takeMountingManagerLogs()).toEqual([
'Remove {type: "View", parentNativeID: (N/A), index: 0, nativeID: "child"}',
@@ -874,15 +821,11 @@ test('view flattening with culling', () => {
const element = ensureInstance(maybeNode, ReactNativeElement);
Fantom.runOnUIThread(() => {
Fantom.enqueueScrollEvent(element, {
x: 0,
y: 60,
});
Fantom.scrollTo(element, {
x: 0,
y: 60,
});
Fantom.runWorkLoop();
expect(root.takeMountingManagerLogs()).toEqual([
'Update {type: "ScrollView", nativeID: (N/A)}',
'Create {type: "View", nativeID: "child"}',