From 55aa8d25793cd1b3595fe01787b47aee045becad Mon Sep 17 00:00:00 2001 From: Nicolas Gallagher Date: Tue, 8 Jul 2025 11:31:40 -0700 Subject: [PATCH] Remove use of Animated.delay and sequence for CSS transition polyfill We can use the `delay` field in the config for `timing` and `spring` rather than using `Animated.delay` and `Animated.sequence`. This should help with animation sequencing and avoid depending on `onAnimationComplete` events firing. --- apps/examples/src/components/App.js | 1 + .../src/native/modules/useStyleTransition.js | 20 +++++----- .../tests/__mocks__/react-native/index.js | 10 ++++- .../tests/html-test.native.js | 40 +++++++++++++++++-- 4 files changed, 56 insertions(+), 15 deletions(-) diff --git a/apps/examples/src/components/App.js b/apps/examples/src/components/App.js index 5dc7ab9b..311de906 100644 --- a/apps/examples/src/components/App.js +++ b/apps/examples/src/components/App.js @@ -731,6 +731,7 @@ const styles = css.create({ }, transitionOpacity: { backgroundColor: 'red', + transitionDelay: '250ms', transitionDuration: '0.5s', transitionProperty: 'opacity', transitionTimingFunction: 'ease' diff --git a/packages/react-strict-dom/src/native/modules/useStyleTransition.js b/packages/react-strict-dom/src/native/modules/useStyleTransition.js index 829b823b..f5b87dc0 100644 --- a/packages/react-strict-dom/src/native/modules/useStyleTransition.js +++ b/packages/react-strict-dom/src/native/modules/useStyleTransition.js @@ -195,6 +195,7 @@ function transitionStyleHasChanged( function getAnimation( animatedValue: ReactNative.Animated.Value, + delay: number, duration: number, timingFunction: string | null, shouldUseNativeDriver: boolean @@ -210,6 +211,7 @@ function getAnimation( `spring() timing function of "${timingFunction}" is missing closing parenthesis.` ); return ReactNative.Animated.timing(animatedValue, { + delay, duration, easing: getEasingFunction(null), toValue: 1, @@ -244,6 +246,7 @@ function getAnimation( } return ReactNative.Animated.spring(animatedValue, { + delay, damping, mass, stiffness, @@ -254,6 +257,7 @@ function getAnimation( } return ReactNative.Animated.timing(animatedValue, { + delay, duration, easing: getEasingFunction(timingFunction), toValue: 1, @@ -324,15 +328,13 @@ export function useStyleTransition(style: ReactNativeStyle): ReactNativeStyle { const { delay, duration, timingFunction, shouldUseNativeDriver } = transitionMetadataRef.current; - const animation = ReactNative.Animated.sequence([ - ReactNative.Animated.delay(delay), - getAnimation( - animatedValue, - duration, - timingFunction, - shouldUseNativeDriver - ) - ]); + const animation = getAnimation( + animatedValue, + delay, + duration, + timingFunction, + shouldUseNativeDriver + ); animation.start(); return () => { 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 84645556..e6bf3e28 100644 --- a/packages/react-strict-dom/tests/__mocks__/react-native/index.js +++ b/packages/react-strict-dom/tests/__mocks__/react-native/index.js @@ -19,7 +19,10 @@ export const Animated = { }), timing: jest.fn(() => { return { - start: jest.fn() + start: jest.fn((callback) => { + callback && callback(); + }), + stop: jest.fn() }; }), delay: jest.fn(), @@ -33,7 +36,10 @@ export const Animated = { }), spring: jest.fn(() => { return { - start: jest.fn() + start: jest.fn((callback) => { + callback && callback(); + }), + stop: jest.fn() }; }) }; diff --git a/packages/react-strict-dom/tests/html-test.native.js b/packages/react-strict-dom/tests/html-test.native.js index 9284397e..27ff635e 100644 --- a/packages/react-strict-dom/tests/html-test.native.js +++ b/packages/react-strict-dom/tests/html-test.native.js @@ -622,7 +622,7 @@ describe('', () => { expect(console.warn).not.toHaveBeenCalledWith( expect.stringContaining('React Strict DOM') ); - expect(Animated.sequence).not.toHaveBeenCalled(); + expect(Animated.timing).not.toHaveBeenCalled(); expect(root.toJSON()).toMatchSnapshot('default'); }); @@ -638,14 +638,14 @@ describe('', () => { expect(console.error).not.toHaveBeenCalledWith( expect.stringContaining('React Strict DOM') ); - expect(Animated.sequence).toHaveBeenCalled(); + expect(Animated.timing).toHaveBeenCalled(); expect(root.toJSON()).toMatchSnapshot('red to green'); - Animated.sequence.mockClear(); + Animated.timing.mockClear(); act(() => { root.update(); }); - expect(Animated.sequence).toHaveBeenCalled(); + expect(Animated.timing).toHaveBeenCalled(); expect(root.toJSON()).toMatchSnapshot('green to blue'); }); @@ -664,6 +664,14 @@ describe('', () => { }); expect(root.toJSON()).toMatchSnapshot('end'); expect(Easing.inOut).toHaveBeenCalled(); + expect(Animated.timing).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + delay: 200, + duration: 2000, + useNativeDriver: false + }) + ); }); test('opacity transition', () => { @@ -679,6 +687,14 @@ describe('', () => { }); expect(root.toJSON()).toMatchSnapshot('end'); expect(Easing.in).toHaveBeenCalled(); + expect(Animated.timing).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + delay: 50, + duration: 1000, + useNativeDriver: true + }) + ); }); test('transform transition', () => { @@ -698,6 +714,14 @@ describe('', () => { }); expect(root.toJSON()).toMatchSnapshot('end'); expect(Easing.out).toHaveBeenCalled(); + expect(Animated.timing).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + delay: 0, + duration: 1000, + useNativeDriver: true + }) + ); }); test('width transition', () => { @@ -713,6 +737,14 @@ describe('', () => { }); expect(root.toJSON()).toMatchSnapshot('end'); expect(Easing.out).toHaveBeenCalled(); + expect(Animated.timing).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + delay: 0, + duration: 500, + useNativeDriver: false + }) + ); }); test('cubic-bezier() timing function', () => {