diff --git a/packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js b/packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js
index 9db64ffb80bb..adeaf7e485cf 100644
--- a/packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js
+++ b/packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js
@@ -15,7 +15,8 @@ import type {HostInstance} from 'react-native';
import ensureInstance from '../../../src/private/__tests__/utilities/ensureInstance';
import * as Fantom from '@react-native/fantom';
-import {createRef, memo, useEffect, useMemo, useState} from 'react';
+import * as React from 'react';
+import {Component, createRef, memo, useEffect, useMemo, useState} from 'react';
import {Animated, View, useAnimatedValue} from 'react-native';
import {allowStyleProp} from 'react-native/Libraries/Animated/NativeAnimatedAllowlist';
import ReactNativeElement from 'react-native/src/private/webapis/dom/nodes/ReactNativeElement';
@@ -75,6 +76,122 @@ test('animated opacity', () => {
);
});
+// ScrollView's ref is the host instance, so it resolves directly (sanity check
+// that the fix doesn't regress it).
+test('animated opacity on Animated.ScrollView', () => {
+ let _opacity;
+ let _opacityAnimation;
+
+ function MyApp() {
+ const opacity = useAnimatedValue(1);
+ _opacity = opacity;
+ return (
+
+
+
+ );
+ }
+
+ const root = Fantom.createRoot();
+ Fantom.runTask(() => {
+ root.render();
+ });
+
+ Fantom.runTask(() => {
+ _opacityAnimation = Animated.timing(_opacity, {
+ toValue: 0,
+ duration: 30,
+ useNativeDriver: true,
+ }).start();
+ });
+ Fantom.unstable_produceFramesForDuration(30);
+ Fantom.runTask(() => {
+ _opacityAnimation?.stop();
+ });
+
+ expect(
+ JSON.stringify(root.getRenderedOutput({props: ['opacity']}).toJSON()),
+ ).toContain('"opacity":"0"');
+});
+
+test('animated opacity on Animated.FlatList', () => {
+ let _opacity;
+ let _opacityAnimation;
+
+ function MyApp() {
+ const opacity = useAnimatedValue(1);
+ _opacity = opacity;
+ return (
+ }
+ renderItem={() => null}
+ style={{opacity}}
+ />
+ );
+ }
+
+ const root = Fantom.createRoot();
+ Fantom.runTask(() => {
+ root.render();
+ });
+
+ Fantom.runTask(() => {
+ _opacityAnimation = Animated.timing(_opacity, {
+ toValue: 0,
+ duration: 30,
+ useNativeDriver: true,
+ }).start();
+ });
+ Fantom.unstable_produceFramesForDuration(30);
+ Fantom.runTask(() => {
+ _opacityAnimation?.stop();
+ });
+
+ expect(
+ JSON.stringify(root.getRenderedOutput({props: ['opacity']}).toJSON()),
+ ).toContain('"opacity":"0"');
+});
+
+// A class composite uses the findShadowNodeByTag fallback path in #connectShadowNode.
+test('animated opacity on a class composite wrapping a host', () => {
+ let _opacity;
+ let _opacityAnimation;
+
+ class HostWrapper extends Component<{style?: $FlowFixMe}> {
+ render(): React.Node {
+ return ;
+ }
+ }
+ const AnimatedHostWrapper = Animated.createAnimatedComponent(HostWrapper);
+
+ function MyApp() {
+ const opacity = useAnimatedValue(1);
+ _opacity = opacity;
+ return ;
+ }
+
+ const root = Fantom.createRoot();
+ Fantom.runTask(() => {
+ root.render();
+ });
+
+ Fantom.runTask(() => {
+ _opacityAnimation = Animated.timing(_opacity, {
+ toValue: 0,
+ duration: 30,
+ useNativeDriver: true,
+ }).start();
+ });
+ Fantom.unstable_produceFramesForDuration(30);
+ Fantom.runTask(() => {
+ _opacityAnimation?.stop();
+ });
+
+ expect(root.getRenderedOutput({props: ['opacity']}).toJSX()).toEqual(
+ ,
+ );
+});
+
test('animate layout props', () => {
const viewRef = createRef();
allowStyleProp('height');
diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedProps.js b/packages/react-native/Libraries/Animated/nodes/AnimatedProps.js
index a29bd5ed5a55..6581c150f437 100644
--- a/packages/react-native/Libraries/Animated/nodes/AnimatedProps.js
+++ b/packages/react-native/Libraries/Animated/nodes/AnimatedProps.js
@@ -15,6 +15,7 @@ import type {AnimatedStyleAllowlist} from './AnimatedStyle';
import NativeAnimatedHelper from '../../../src/private/animated/NativeAnimatedHelper';
import * as ReactNativeFeatureFlags from '../../../src/private/featureflags/ReactNativeFeatureFlags';
+import {getFabricUIManager} from '../../ReactNative/FabricUIManager';
import {findNodeHandle} from '../../ReactNative/RendererProxy';
import {getNodeFromPublicInstance} from '../../ReactPrivate/ReactNativePrivateInterface';
import flattenStyle from '../../StyleSheet/flattenStyle';
@@ -298,8 +299,31 @@ export default class AnimatedProps extends AnimatedNode {
}
invariant(this.__isNative, 'Expected node to be marked as "native"');
- // $FlowExpectedError[incompatible-type] - target.instance may be an HTMLElement but we need ReactNativeElement for Fabric
- const shadowNode = getNodeFromPublicInstance(target.instance);
+ // Host components and ScrollView (whose ref is the host instance) resolve a
+ // shadow node directly; FlatList/SectionList are class composites that expose
+ // the host via getNativeScrollRef().
+ // $FlowFixMe[unclear-type] - Legacy instance assumptions.
+ const instance: any = target.instance;
+ const candidates = [instance, instance?.getNativeScrollRef?.()];
+ let shadowNode = null;
+ for (const candidate of candidates) {
+ if (candidate == null) {
+ continue;
+ }
+ shadowNode = getNodeFromPublicInstance(candidate);
+ if (shadowNode != null) {
+ break;
+ }
+ }
+ // Any other class composite: resolve from the host tag #connectAnimatedView
+ // already found via findNodeHandle (the lookup runs on the native side).
+ const connectedViewTag = target.connectedViewTag;
+ if (shadowNode == null && connectedViewTag != null) {
+ shadowNode =
+ getFabricUIManager()?.findShadowNodeByTag_DEPRECATED?.(
+ connectedViewTag,
+ );
+ }
if (shadowNode == null) {
return;
}