diff --git a/Libraries/Animated/__tests__/Animated-web-test.js b/Libraries/Animated/__tests__/Animated-web-test.js new file mode 100644 index 00000000000000..2fccade525c107 --- /dev/null +++ b/Libraries/Animated/__tests__/Animated-web-test.js @@ -0,0 +1,198 @@ +/** + * 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. + * + * @format + * @oncall react_native + */ + +const StyleSheet = require('../../StyleSheet/StyleSheet'); +let Animated = require('../Animated').default; +let AnimatedProps = require('../nodes/AnimatedProps').default; + +jest.mock('../../Utilities/Platform', () => { + return {OS: 'web'}; +}); + +describe('Animated tests', () => { + beforeEach(() => { + jest.resetModules(); + }); + + describe('Animated', () => { + it('works end to end', () => { + const anim = new Animated.Value(0); + const translateAnim = anim.interpolate({ + inputRange: [0, 1], + outputRange: [100, 200], + }); + + const callback = jest.fn(); + + const node = new AnimatedProps( + { + style: { + backgroundColor: 'red', + opacity: anim, + transform: [ + { + translate: [translateAnim, translateAnim], + }, + { + translateX: translateAnim, + }, + {scale: anim}, + ], + shadowOffset: { + width: anim, + height: anim, + }, + }, + }, + callback, + ); + + expect(node.__getValue()).toEqual({ + style: [ + { + backgroundColor: 'red', + opacity: anim, + shadowOffset: { + width: anim, + height: anim, + }, + transform: [ + {translate: [translateAnim, translateAnim]}, + {translateX: translateAnim}, + {scale: anim}, + ], + }, + { + opacity: 0, + transform: [{translate: [100, 100]}, {translateX: 100}, {scale: 0}], + shadowOffset: { + width: 0, + height: 0, + }, + }, + ], + }); + + expect(anim.__getChildren().length).toBe(0); + + node.__attach(); + + expect(anim.__getChildren().length).toBe(3); + + anim.setValue(0.5); + + expect(callback).toBeCalled(); + + expect(node.__getValue()).toEqual({ + style: [ + { + backgroundColor: 'red', + opacity: anim, + shadowOffset: { + width: anim, + height: anim, + }, + transform: [ + {translate: [translateAnim, translateAnim]}, + {translateX: translateAnim}, + {scale: anim}, + ], + }, + { + opacity: 0.5, + transform: [ + {translate: [150, 150]}, + {translateX: 150}, + {scale: 0.5}, + ], + shadowOffset: { + width: 0.5, + height: 0.5, + }, + }, + ], + }); + + node.__detach(); + expect(anim.__getChildren().length).toBe(0); + + anim.setValue(1); + expect(callback.mock.calls.length).toBe(1); + }); + + /** + * The behavior matters when the input style is a mix of values + * from StyleSheet.create and an inline style with an animation + */ + it('does not discard initial style', () => { + const value1 = new Animated.Value(1); + const scale = value1.interpolate({ + inputRange: [0, 1], + outputRange: [1, 2], + }); + const callback = jest.fn(); + const node = new AnimatedProps( + { + style: [ + styles.red, + { + transform: [ + { + scale, + }, + ], + }, + ], + }, + callback, + ); + + expect(node.__getValue()).toEqual({ + style: [ + [ + styles.red, + { + transform: [{scale}], + }, + ], + { + transform: [{scale: 2}], + }, + ], + }); + + node.__attach(); + expect(callback.mock.calls.length).toBe(0); + value1.setValue(0.5); + expect(callback.mock.calls.length).toBe(1); + expect(node.__getValue()).toEqual({ + style: [ + [ + styles.red, + { + transform: [{scale}], + }, + ], + { + transform: [{scale: 1.5}], + }, + ], + }); + + node.__detach(); + }); + }); +}); + +const styles = StyleSheet.create({ + red: { + backgroundColor: 'red', + }, +}); diff --git a/Libraries/Animated/nodes/AnimatedStyle.js b/Libraries/Animated/nodes/AnimatedStyle.js index 7cb11bb1b55679..31291968f16e15 100644 --- a/Libraries/Animated/nodes/AnimatedStyle.js +++ b/Libraries/Animated/nodes/AnimatedStyle.js @@ -13,24 +13,52 @@ import type {PlatformConfig} from '../AnimatedPlatformConfig'; import flattenStyle from '../../StyleSheet/flattenStyle'; +import Platform from '../../Utilities/Platform'; import NativeAnimatedHelper from '../NativeAnimatedHelper'; import AnimatedNode from './AnimatedNode'; import AnimatedTransform from './AnimatedTransform'; import AnimatedWithChildren from './AnimatedWithChildren'; +function createAnimatedStyle(inputStyle: any): Object { + const style = flattenStyle(inputStyle); + const animatedStyles: any = {}; + for (const key in style) { + const value = style[key]; + if (key === 'transform') { + animatedStyles[key] = new AnimatedTransform(value); + } else if (value instanceof AnimatedNode) { + animatedStyles[key] = value; + } else if (value && !Array.isArray(value) && typeof value === 'object') { + animatedStyles[key] = createAnimatedStyle(value); + } + } + return animatedStyles; +} + +function createStyleWithAnimatedTransform(inputStyle: any): Object { + let style = flattenStyle(inputStyle) || ({}: {[string]: any}); + + if (style.transform) { + style = { + ...style, + transform: new AnimatedTransform(style.transform), + }; + } + return style; +} + export default class AnimatedStyle extends AnimatedWithChildren { + _inputStyle: any; _style: Object; constructor(style: any) { super(); - style = flattenStyle(style) || ({}: {[string]: any}); - if (style.transform) { - style = { - ...style, - transform: new AnimatedTransform(style.transform), - }; + if (Platform.OS === 'web') { + this._inputStyle = style; + this._style = createAnimatedStyle(style); + } else { + this._style = createStyleWithAnimatedTransform(style); } - this._style = style; } // Recursively get values for nested styles (like iOS's shadowOffset) @@ -50,7 +78,11 @@ export default class AnimatedStyle extends AnimatedWithChildren { return updatedStyle; } - __getValue(): Object { + __getValue(): Object | Array { + if (Platform.OS === 'web') { + return [this._inputStyle, this._walkStyleAndGetValues(this._style)]; + } + return this._walkStyleAndGetValues(this._style); }