@@ -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