diff --git a/packages/react-native/Libraries/Animated/__tests__/AnimatedProps-test.js b/packages/react-native/Libraries/Animated/__tests__/AnimatedProps-test.js new file mode 100644 index 00000000000..dd9f574e93a --- /dev/null +++ b/packages/react-native/Libraries/Animated/__tests__/AnimatedProps-test.js @@ -0,0 +1,31 @@ +/** + * 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 + * @format + * @oncall react_native + */ + +import AnimatedProps from '../nodes/AnimatedProps'; + +describe('AnimatedProps', () => { + function getValue(inputProps: {[string]: mixed}) { + const animatedProps = new AnimatedProps(inputProps, jest.fn()); + return animatedProps.__getValue(); + } + + it('returns original `style` if it has no nodes', () => { + const style = {color: 'red'}; + expect(getValue({style}).style).toBe(style); + }); + + it('returns original `style` for invalid style values', () => { + const values = [undefined, null, function () {}, true, 123, 'foo']; + for (const value of values) { + expect(getValue({style: value})).toEqual({style: value}); + } + }); +}); diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedProps.js b/packages/react-native/Libraries/Animated/nodes/AnimatedProps.js index e0b642fd64f..90d240dd2ac 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedProps.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedProps.js @@ -33,10 +33,14 @@ function createAnimatedProps( const value = inputProps[key]; if (key === 'style') { - const node = new AnimatedStyle(value); - nodeKeys.push(key); - nodes.push(node); - props[key] = node; + const node = AnimatedStyle.from(value); + if (node == null) { + props[key] = value; + } else { + nodeKeys.push(key); + nodes.push(node); + props[key] = node; + } } else if (value instanceof AnimatedNode) { const node = value; nodeKeys.push(key); diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedStyle.js b/packages/react-native/Libraries/Animated/nodes/AnimatedStyle.js index bf08605e5f4..455884bfaaf 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedStyle.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedStyle.js @@ -38,7 +38,7 @@ function createAnimatedStyle( const node = ReactNativeFeatureFlags.shouldUseAnimatedObjectForTransform() ? AnimatedObject.from(value) : // $FlowFixMe[incompatible-call] - `value` is mixed. - new AnimatedTransform(value); + AnimatedTransform.from(value); if (node == null) { if (keepUnanimatedValues) { style[key] = value; @@ -77,19 +77,36 @@ export default class AnimatedStyle extends AnimatedWithChildren { _inputStyle: any; _style: {[string]: any}; - constructor(inputStyle: any) { - super(); - this._inputStyle = inputStyle; + /** + * Creates an `AnimatedStyle` if `value` contains `AnimatedNode` instances. + * Otherwise, returns `null`. + */ + static from(inputStyle: any): ?AnimatedStyle { + const flatStyle = flattenStyle(inputStyle); + if (flatStyle == null) { + return null; + } const [nodeKeys, nodes, style] = createAnimatedStyle( - // NOTE: This null check should not be necessary, but the types are not - // strong nor enforced as of this writing. This check should be hoisted - // to instantiation sites. - flattenStyle(inputStyle) ?? {}, + flatStyle, Platform.OS !== 'web', ); + if (nodes.length === 0) { + return null; + } + return new AnimatedStyle(nodeKeys, nodes, style, inputStyle); + } + + constructor( + nodeKeys: $ReadOnlyArray, + nodes: $ReadOnlyArray, + style: {[string]: any}, + inputStyle: any, + ) { + super(); this.#nodeKeys = nodeKeys; this.#nodes = nodes; this._style = style; + this._inputStyle = inputStyle; } __getValue(): Object | Array { diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedTransform.js b/packages/react-native/Libraries/Animated/nodes/AnimatedTransform.js index c0284d28c01..67da5be13a2 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedTransform.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedTransform.js @@ -26,37 +26,58 @@ type Transform = { | {[string]: number | string | T}, }; +function flatAnimatedNodes( + transforms: $ReadOnlyArray>, +): Array { + const nodes = []; + for (let ii = 0, length = transforms.length; ii < length; ii++) { + const transform = transforms[ii]; + // There should be exactly one property in `transform`. + for (const key in transform) { + const value = transform[key]; + if (value instanceof AnimatedNode) { + nodes.push(value); + } + } + } + return nodes; +} + export default class AnimatedTransform extends AnimatedWithChildren { // NOTE: For potentially historical reasons, some operations only operate on // the first level of AnimatedNode instances. This optimizes that bevavior. - #shallowNodes: $ReadOnlyArray; + #nodes: $ReadOnlyArray; _transforms: $ReadOnlyArray>; - constructor(transforms: $ReadOnlyArray>) { - super(); - this._transforms = transforms; - - const shallowNodes = []; - // NOTE: This check should not be necessary, but the types are not enforced - // as of this writing. This check should be hoisted to instantiation sites. - if (Array.isArray(transforms)) { - for (let ii = 0, length = transforms.length; ii < length; ii++) { - const transform = transforms[ii]; - // There should be exactly one property in `transform`. - for (const key in transform) { - const value = transform[key]; - if (value instanceof AnimatedNode) { - shallowNodes.push(value); - } - } - } + /** + * Creates an `AnimatedTransform` if `transforms` contains `AnimatedNode` + * instances. Otherwise, returns `null`. + */ + static from(transforms: $ReadOnlyArray>): ?AnimatedTransform { + const nodes = flatAnimatedNodes( + // NOTE: This check should not be necessary, but the types are not + // enforced as of this writing. This check should be hoisted to + // instantiation sites. + Array.isArray(transforms) ? transforms : [], + ); + if (nodes.length === 0) { + return null; } - this.#shallowNodes = shallowNodes; + return new AnimatedTransform(nodes, transforms); + } + + constructor( + nodes: $ReadOnlyArray, + transforms: $ReadOnlyArray>, + ) { + super(); + this.#nodes = nodes; + this._transforms = transforms; } __makeNative(platformConfig: ?PlatformConfig) { - const nodes = this.#shallowNodes; + const nodes = this.#nodes; for (let ii = 0, length = nodes.length; ii < length; ii++) { const node = nodes[ii]; node.__makeNative(platformConfig); @@ -77,7 +98,7 @@ export default class AnimatedTransform extends AnimatedWithChildren { } __attach(): void { - const nodes = this.#shallowNodes; + const nodes = this.#nodes; for (let ii = 0, length = nodes.length; ii < length; ii++) { const node = nodes[ii]; node.__addChild(this); @@ -85,7 +106,7 @@ export default class AnimatedTransform extends AnimatedWithChildren { } __detach(): void { - const nodes = this.#shallowNodes; + const nodes = this.#nodes; for (let ii = 0, length = nodes.length; ii < length; ii++) { const node = nodes[ii]; node.__removeChild(this); diff --git a/packages/react-native/Libraries/LogBox/UI/__tests__/__snapshots__/LogBoxInspectorSourceMapStatus-test.js.snap b/packages/react-native/Libraries/LogBox/UI/__tests__/__snapshots__/LogBoxInspectorSourceMapStatus-test.js.snap index bbc3ed47a0b..c186cd3a4e4 100644 --- a/packages/react-native/Libraries/LogBox/UI/__tests__/__snapshots__/LogBoxInspectorSourceMapStatus-test.js.snap +++ b/packages/react-native/Libraries/LogBox/UI/__tests__/__snapshots__/LogBoxInspectorSourceMapStatus-test.js.snap @@ -35,12 +35,18 @@ exports[`LogBoxInspectorSourceMapStatus should render for failed 1`] = ` } } style={ - Object { - "height": 14, - "marginEnd": 4, - "tintColor": "rgba(243, 83, 105, 1)", - "width": 16, - } + Array [ + Object { + "height": 14, + "marginEnd": 4, + "tintColor": "rgba(255, 255, 255, 0.4)", + "width": 16, + }, + Object { + "tintColor": "rgba(243, 83, 105, 1)", + }, + null, + ] } /> , + nodes: $ReadOnlyArray, + style: { [string]: any }, + inputStyle: any + ): void; __getValue(): Object | Array; __getAnimatedValue(): Object; __attach(): void; @@ -1034,7 +1040,11 @@ exports[`public API should not change unintentionally Libraries/Animated/nodes/A }; declare export default class AnimatedTransform extends AnimatedWithChildren { _transforms: $ReadOnlyArray>; - constructor(transforms: $ReadOnlyArray>): void; + static from(transforms: $ReadOnlyArray>): ?AnimatedTransform; + constructor( + nodes: $ReadOnlyArray, + transforms: $ReadOnlyArray> + ): void; __makeNative(platformConfig: ?PlatformConfig): void; __getValue(): $ReadOnlyArray>; __getAnimatedValue(): $ReadOnlyArray>;