Skip to content

Commit a6aae6e

Browse files
committed
fix: scroll command
1 parent c73b545 commit a6aae6e

2 files changed

Lines changed: 95 additions & 5 deletions

File tree

example/src/App.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
Switch,
99
Alert,
1010
} from 'react-native';
11-
import { useState } from 'react';
11+
import { useState, useRef } from 'react';
1212
import { AgentKitProvider, useAgentKitStatus } from 'react-native-agentkit';
1313

1414
function StatusBadge() {
@@ -35,6 +35,8 @@ function DemoApp() {
3535
const [email, setEmail] = useState('');
3636
const [agreed, setAgreed] = useState(false);
3737

38+
const scrollRef = useRef<any>(null);
39+
3840
const handleSubmit = () => {
3941
Alert.alert(
4042
'Submitted!',
@@ -44,6 +46,7 @@ function DemoApp() {
4446

4547
return (
4648
<ScrollView
49+
ref={scrollRef}
4750
testID="main-scroll"
4851
style={styles.scrollView}
4952
contentContainerStyle={styles.scrollContent}

src/Introspector.ts

Lines changed: 91 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -193,14 +193,101 @@ function resolveActions(
193193
/**
194194
* Extract handlers from props for the ActionExecutor.
195195
*/
196-
function extractHandlers(props: Record<string, any>): ElementHandlers {
197-
return {
196+
function extractHandlers(
197+
props: Record<string, any>,
198+
fiber: FiberNode,
199+
type: ElementType
200+
): ElementHandlers {
201+
const handlers: ElementHandlers = {
198202
onPress: props.onPress,
199203
onLongPress: props.onLongPress,
200204
onChangeText: props.onChangeText,
201205
onValueChange: props.onValueChange,
202-
// scrollTo is attached via ref, we'll handle that separately
203206
};
207+
208+
// Attach scrollTo for scroll-type elements.
209+
// The host fiber (RCTScrollView) stateNode is a raw native view that
210+
// doesn't have scrollTo. The scrollTo lives on the composite ScrollView
211+
// component instance, accessible via the ref on an ancestor fiber.
212+
if (type === 'scroll') {
213+
const scrollRef = findScrollRef(fiber);
214+
if (scrollRef && typeof scrollRef.scrollTo === 'function') {
215+
handlers.scrollTo = (opts) => scrollRef.scrollTo(opts);
216+
}
217+
}
218+
219+
return handlers;
220+
}
221+
222+
/**
223+
* Walk up ancestor fibers from a host ScrollView node to find a ref
224+
* that exposes `scrollTo`. In React Native, the composite `ScrollView`
225+
* component is typically 1-4 levels above the host `RCTScrollView` fiber.
226+
*/
227+
function findScrollRef(fiber: FiberNode): any {
228+
let current: FiberNode | null = fiber;
229+
let depth = 0;
230+
231+
while (current && depth < 8) {
232+
// Check refs on composite components (the ScrollView JS component)
233+
if (current.ref) {
234+
const ref =
235+
typeof current.ref === 'function'
236+
? null // callback refs — can't read synchronously
237+
: current.ref?.current ?? current.ref;
238+
239+
if (ref) {
240+
// Direct scrollTo on the ref (most common path)
241+
if (typeof ref.scrollTo === 'function') {
242+
return ref;
243+
}
244+
// Some wrappers use getNativeScrollRef
245+
if (typeof ref.getNativeScrollRef === 'function') {
246+
const inner = ref.getNativeScrollRef();
247+
if (inner && typeof inner.scrollTo === 'function') {
248+
return inner;
249+
}
250+
}
251+
// Or getScrollResponder
252+
if (typeof ref.getScrollResponder === 'function') {
253+
const responder = ref.getScrollResponder();
254+
if (responder && typeof responder.scrollTo === 'function') {
255+
return responder;
256+
}
257+
}
258+
}
259+
}
260+
261+
// Also check stateNode for class component instances
262+
if (current.stateNode && current.tag !== HOST_COMPONENT) {
263+
const inst = current.stateNode;
264+
if (typeof inst.scrollTo === 'function') {
265+
return inst;
266+
}
267+
if (typeof inst.getNativeScrollRef === 'function') {
268+
const inner = inst.getNativeScrollRef();
269+
if (inner && typeof inner.scrollTo === 'function') {
270+
return inner;
271+
}
272+
}
273+
if (typeof inst.getScrollResponder === 'function') {
274+
const responder = inst.getScrollResponder();
275+
if (responder && typeof responder.scrollTo === 'function') {
276+
return responder;
277+
}
278+
}
279+
}
280+
281+
current = current.return;
282+
depth++;
283+
}
284+
285+
// Last resort: check the original fiber's stateNode directly
286+
if (fiber.stateNode && typeof fiber.stateNode.scrollTo === 'function') {
287+
return fiber.stateNode;
288+
}
289+
290+
return null;
204291
}
205292

206293
/**
@@ -397,7 +484,7 @@ function traverseFiber(
397484
accessibilityHint: props.accessibilityHint || props['aria-hint'],
398485
},
399486
ref: { current: fiber.stateNode },
400-
handlers: extractHandlers(props),
487+
handlers: extractHandlers(props, fiber, type),
401488
};
402489

403490
elements.push(element);

0 commit comments

Comments
 (0)