From 36f0cf26e011eaa43e0f631dae6475ed1045bc2b Mon Sep 17 00:00:00 2001 From: Yamin Yassin Date: Sat, 30 May 2026 00:28:29 +0100 Subject: [PATCH] Fix inherited animated text color on native --- .../benchmarks/perf/mocks/AnimatedNode.js | 20 +++++++ .../benchmarks/perf/mocks/react-native.js | 4 +- packages/benchmarks/perf/rollup.config.mjs | 4 ++ .../src/native/modules/useStyleProps.js | 14 ++++- .../src/native/react-native/AnimatedNode.js | 11 ++++ .../src/native/react-native/index.js | 1 + .../Libraries/Animated/nodes/AnimatedNode.js | 20 +++++++ .../tests/__mocks__/react-native/index.js | 6 +- .../tests/html/html-test.native.js | 60 +++++++++++++++++++ 9 files changed, 136 insertions(+), 4 deletions(-) create mode 100644 packages/benchmarks/perf/mocks/AnimatedNode.js create mode 100644 packages/react-strict-dom/src/native/react-native/AnimatedNode.js create mode 100644 packages/react-strict-dom/tests/__mocks__/react-native/Libraries/Animated/nodes/AnimatedNode.js diff --git a/packages/benchmarks/perf/mocks/AnimatedNode.js b/packages/benchmarks/perf/mocks/AnimatedNode.js new file mode 100644 index 00000000..36f6ed3d --- /dev/null +++ b/packages/benchmarks/perf/mocks/AnimatedNode.js @@ -0,0 +1,20 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +const animatedNodeMarker = Symbol('AnimatedNode'); + +export function createAnimatedNode(config) { + return Object.defineProperty({ ...config }, animatedNodeMarker, { + value: true + }); +} + +export default class AnimatedNode { + static [Symbol.hasInstance](value) { + return Boolean(value?.[animatedNodeMarker]); + } +} diff --git a/packages/benchmarks/perf/mocks/react-native.js b/packages/benchmarks/perf/mocks/react-native.js index 596969b4..5f5e0ca7 100644 --- a/packages/benchmarks/perf/mocks/react-native.js +++ b/packages/benchmarks/perf/mocks/react-native.js @@ -9,6 +9,8 @@ * React Native mock for Node.js benchmarks */ +import { createAnimatedNode } from './AnimatedNode'; + export const AccessibilityInfo = { addEventListener: () => ({ remove: () => {} @@ -25,7 +27,7 @@ export const Animated = { Text: 'Animated.Text', Value: () => { return { - interpolate: (value) => value + interpolate: (value) => createAnimatedNode(value) }; }, timing: () => { diff --git a/packages/benchmarks/perf/rollup.config.mjs b/packages/benchmarks/perf/rollup.config.mjs index 0b6c82e5..dcb681d3 100644 --- a/packages/benchmarks/perf/rollup.config.mjs +++ b/packages/benchmarks/perf/rollup.config.mjs @@ -43,6 +43,10 @@ const config = [ __dirname, './mocks/ViewNativeComponent.js' ) + }, + { + find: 'react-native/Libraries/Animated/nodes/AnimatedNode', + replacement: path.resolve(__dirname, './mocks/AnimatedNode.js') } ] }), diff --git a/packages/react-strict-dom/src/native/modules/useStyleProps.js b/packages/react-strict-dom/src/native/modules/useStyleProps.js index 698be171..8616d73c 100644 --- a/packages/react-strict-dom/src/native/modules/useStyleProps.js +++ b/packages/react-strict-dom/src/native/modules/useStyleProps.js @@ -8,8 +8,11 @@ */ import type { CustomProperties, Style } from '../../types/styles'; -import type { ReactNativeProps } from '../../types/renderer.native'; -import type { ReactNativeStyle } from '../../types/renderer.native'; +import type { + ReactNativeProps, + ReactNativeStyle, + ReactNativeStyleValue +} from '../../types/renderer.native'; import * as css from '../css'; import * as ReactNative from '../react-native'; @@ -76,6 +79,10 @@ function resolveUnitlessLineHeight(style: ReactNativeStyle): ReactNativeStyle { return style; } +function isAnimatedNode(value: ?ReactNativeStyleValue): boolean { + return value instanceof ReactNative.AnimatedNode; +} + /** * Produces the relevant React Native props to implement the given styles, and any * inheritable text styles that may be required. @@ -208,6 +215,9 @@ export function useStyleProps( val = inheritedValue; } if (val != null) { + if (isAnimatedNode(val)) { + styleProps.animated = true; + } hasInheritableStyle = true; inheritableStyle[key] = val; styleProps.style[key] = val; diff --git a/packages/react-strict-dom/src/native/react-native/AnimatedNode.js b/packages/react-strict-dom/src/native/react-native/AnimatedNode.js new file mode 100644 index 00000000..b736d506 --- /dev/null +++ b/packages/react-strict-dom/src/native/react-native/AnimatedNode.js @@ -0,0 +1,11 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + */ + +import AnimatedNode from 'react-native/Libraries/Animated/nodes/AnimatedNode'; +export { AnimatedNode }; diff --git a/packages/react-strict-dom/src/native/react-native/index.js b/packages/react-strict-dom/src/native/react-native/index.js index 1fe99bb0..8b2258d7 100644 --- a/packages/react-strict-dom/src/native/react-native/index.js +++ b/packages/react-strict-dom/src/native/react-native/index.js @@ -17,6 +17,7 @@ export { Text, TextInput } from 'react-native'; +export { AnimatedNode } from './AnimatedNode'; export { LayoutConformance } from './LayoutConformance'; export { TextAncestorContext } from './TextAncestorContext'; export { ViewNativeComponent } from './ViewNativeComponent'; diff --git a/packages/react-strict-dom/tests/__mocks__/react-native/Libraries/Animated/nodes/AnimatedNode.js b/packages/react-strict-dom/tests/__mocks__/react-native/Libraries/Animated/nodes/AnimatedNode.js new file mode 100644 index 00000000..36f6ed3d --- /dev/null +++ b/packages/react-strict-dom/tests/__mocks__/react-native/Libraries/Animated/nodes/AnimatedNode.js @@ -0,0 +1,20 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +const animatedNodeMarker = Symbol('AnimatedNode'); + +export function createAnimatedNode(config) { + return Object.defineProperty({ ...config }, animatedNodeMarker, { + value: true + }); +} + +export default class AnimatedNode { + static [Symbol.hasInstance](value) { + return Boolean(value?.[animatedNodeMarker]); + } +} diff --git a/packages/react-strict-dom/tests/__mocks__/react-native/index.js b/packages/react-strict-dom/tests/__mocks__/react-native/index.js index 01aadcd7..17c0ae09 100644 --- a/packages/react-strict-dom/tests/__mocks__/react-native/index.js +++ b/packages/react-strict-dom/tests/__mocks__/react-native/index.js @@ -5,6 +5,8 @@ * LICENSE file in the root directory of this source tree. */ +import { createAnimatedNode } from './Libraries/Animated/nodes/AnimatedNode'; + export const AccessibilityInfo = { addEventListener: jest.fn().mockReturnValue({ remove: jest.fn() }), isReduceMotionEnabled: jest.fn().mockReturnValue(Promise.resolve(false)) @@ -19,7 +21,9 @@ export const Animated = { Text: 'Animated.Text', Value: jest.fn(() => { return { - interpolate: jest.fn().mockImplementation((value) => value) + interpolate: jest + .fn() + .mockImplementation((value) => createAnimatedNode(value)) }; }), timing: jest.fn(() => { diff --git a/packages/react-strict-dom/tests/html/html-test.native.js b/packages/react-strict-dom/tests/html/html-test.native.js index 21981bc3..73dcd848 100644 --- a/packages/react-strict-dom/tests/html/html-test.native.js +++ b/packages/react-strict-dom/tests/html/html-test.native.js @@ -593,6 +593,66 @@ describe(' (native polyfills)', () => { expect(getFontSize(secondSecond)).toBe(1.5 * 3 * 16); }); + test('inherited color from transitioned parent', () => { + const styles = css.create({ + container: { + padding: '32px', + transitionProperty: 'backgroundColor, color', + transitionDuration: '300ms', + backgroundColor: { + default: 'blue', + ':active': 'darkblue' + }, + color: { + default: 'white', + ':active': 'green' + } + }, + text: { + fontSize: '24px', + color: 'inherit' + } + }); + + let root; + act(() => { + root = create( + + Alert Banner + + ); + }); + + let button = root.toJSON(); + let span = button.children[0]; + expect(span.type).toBe('Text'); + expect(span.props.style.color).toBe('white'); + + act(() => { + button.props.onPointerDown(); + }); + + button = root.toJSON(); + span = button.children[0]; + expect(span.type).toBe('Animated.Text'); + expect(span.props.style.color).toEqual({ + inputRange: [0, 1], + outputRange: ['white', 'green'] + }); + + act(() => { + button.props.onPointerUp(); + }); + + button = root.toJSON(); + span = button.children[0]; + expect(span.type).toBe('Animated.Text'); + expect(span.props.style.color).toEqual({ + inputRange: [0, 1], + outputRange: ['green', 'white'] + }); + }); + test('inherited lineHeight (unitless)', () => { const styles = css.create({ root: {