nandorojo / moti

🐼 The React Native (+ Web) animation library, powered by Reanimated 3.
https://moti.fyi
MIT License
3.9k stars 120 forks source link

[moti] reduceMotion transition config is lost on timing, delay or repeated transitions. #351

Open breakingrobot opened 1 month ago

breakingrobot commented 1 month ago

Is there an existing issue for this?

Do you want this issue prioritized?

Current Behavior

MotiView depending on which transition config is sent through props will not pass reduceMotion correctly.

reduceMotion is broken while using:

Expected Behavior

MotiView should respect the reduceMotion config passed through transition with any type or repeating and delaying props.

Steps To Reproduce

  1. Create a MotiView Component with transition={{ type: 'timing' }}
    
    import { View as MotiView } from 'moti';
    import { ReduceMotion } from 'react-native-reanimated';

<MotiView style={{ backgroundColor: 'rgba(255, 0, 0, 0.3)', }} from={{ backgroundColor: 'rgba(255, 0, 0, 0.3)', }} animate={{ backgroundColor: colorAnimation, }} transition={{ type: 'timing', duration: 3000, reduceMotion: ReduceMotion.Never, }} />

2. Create a MotiView Component of transition `type: 'spring'` with loop or delaying transition config:
```ts
import { View as MotiView } from 'moti';
import { ReduceMotion } from 'react-native-reanimated';

<MotiView
  style={{ position: 'relative', width: 250, height: 250, backgroundColor: 'rgba(255, 0, 0, 0.3)' }}
  from={{
    rotate: '0deg',
    scale: 1,
  }}
  animate={{
    rotate: '360deg',
    scale: scaleAnimation,
  }}
  transition={{
    type: 'spring',
    duration: 3000,
    loop: true,
    reduceMotion: ReduceMotion.Never,
  }}
/>

### Versions

```markdown
- Moti: 0.29.0
- Reanimated: 3.11.0
- React Native: 0.74.1

Screenshots

Here, the red circle should rotate and loop even with ReduceMotion.Never

Capture d’écran 2024-05-28 à 13 41 18

Reproduction

import { View as MotiView } from 'moti';
import { View } from 'react-native'
import { useEffect, useState } from 'react';
import { ReduceMotion } from 'react-native-reanimated';

const DebugMoti = () => {

  const [colorAnimation, setColorAnimation] = useState('rgba(255, 0, 0, 0.3)');
  const [scaleAnimation, setScaleAnimation] = useState(1);

  useEffect(() => {
    setColorAnimation('rgba(0, 0, 255, 0.3)');
    setScaleAnimation(Math.random() * 2 + 1);
  }, []);

  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <MotiView
        style={{ position: 'relative', width: 250, height: 250 }}
        from={{
          rotate: '0deg',
          scale: 1,
        }}
        animate={{
          rotate: '360deg',
          scale: scaleAnimation,
        }}
        transition={{
          type: 'timing',
          duration: 3000,
          loop: true,
          reduceMotion: ReduceMotion.Never,
        }}
      >
        <View style={{ width: 250, height: 250, backgroundColor: 'green' }} />
        <MotiView
          style={{
            position: 'absolute',
            top: 0,
            left: 0,
            width: 250,
            height: 250,
            backgroundColor: 'rgba(255, 0, 0, 0.3)',
          }}
          from={{
            backgroundColor: 'rgba(255, 0, 0, 0.3)',
          }}
          animate={{
            backgroundColor: colorAnimation,
          }}
          transition={{
            type: 'sprint',
            duration: 3000,
            loop: true,
            reduceMotion: ReduceMotion.Never,
          }}
        />
      </MotiView>
    </View>
  );
};

export default DebugMoti;

Workaround

I have written a bit of a patch-package that fixes the problem but it is not perfect in any way.

diff --git a/src/core/use-motify.ts b/src/core/use-motify.ts
index 5bbcd8f..bebb725 100644
--- a/src/core/use-motify.ts
+++ b/src/core/use-motify.ts
@@ -13,7 +13,7 @@ import {
   withDelay,
   withRepeat,
   withSequence,
-  runOnJS,
+  runOnJS, ReduceMotion,
 } from 'react-native-reanimated'
 import type {
   WithDecayConfig,
@@ -165,6 +165,7 @@ function animationConfig<Animate>(
   // debug({ loop, key, repeatCount, animationType })

   let config = {}
+  let reduceMotion = ReduceMotion.System;
   // so sad, but fix it later :(
   let animation = (...props: any): any => props

@@ -177,12 +178,20 @@ function animationConfig<Animate>(
       (transition?.[key] as WithTimingConfig | undefined)?.easing ??
       (transition as WithTimingConfig | undefined)?.easing

+    const timingReduceMotion =
+      (transition?.[key] as WithTimingConfig | undefined)?.reduceMotion ??
+      (transition as WithTimingConfig | undefined)?.reduceMotion
+
     if (easing) {
       config['easing'] = easing
     }
     if (duration != null) {
       config['duration'] = duration
     }
+    if (reduceMotion) {
+      reduceMotion = timingReduceMotion ?? reduceMotion
+      config['reduceMotion'] = reduceMotion
+    }
     animation = withTiming
   } else if (animationType === 'spring') {
     animation = withSpring
@@ -191,6 +200,10 @@ function animationConfig<Animate>(
       const styleSpecificConfig = transition?.[key]?.[configKey]
       const transitionConfigForKey = transition?.[configKey]

+      if (configKey === 'reduceMotion') {
+        reduceMotion = transitionConfigForKey || styleSpecificConfig
+      }
+
       if (styleSpecificConfig != null) {
         config[configKey] = styleSpecificConfig
       } else if (transitionConfigForKey != null) {
@@ -212,6 +225,10 @@ function animationConfig<Animate>(
       const styleSpecificConfig = transition?.[key]?.[configKey]
       const transitionConfigForKey = transition?.[configKey]

+      if (configKey === 'reduceMotion') {
+        reduceMotion = transitionConfigForKey || styleSpecificConfig
+      }
+
       if (styleSpecificConfig != null) {
         config[configKey] = styleSpecificConfig
       } else if (transitionConfigForKey != null) {
@@ -227,6 +244,7 @@ function animationConfig<Animate>(
   return {
     animation,
     config,
+    reduceMotion,
     repeatReverse,
     repeatCount,
     shouldRepeat: !!repeatCount,
@@ -260,6 +278,7 @@ const getSequenceArray = (
     if (shouldPush) {
       let stepDelay = delayMs
       let stepValue = step
+      let stepReduceMotion = ReduceMotion.System;
       let stepConfig = Object.assign({}, config)
       let stepAnimation = animation as
         | typeof withTiming
@@ -272,13 +291,14 @@ const getSequenceArray = (
         delete stepTransition.delay
         delete stepTransition.value

-        const { config: inlineStepConfig, animation } = animationConfig(
+        const { config: inlineStepConfig, animation, reduceMotion } = animationConfig(
           sequenceKey,
-          stepTransition
+          stepTransition,
         )

         stepConfig = Object.assign({}, stepConfig, inlineStepConfig)
         stepAnimation = animation
+        stepReduceMotion = reduceMotion

         if (step.delay != null) {
           stepDelay = step.delay
@@ -304,7 +324,7 @@ const getSequenceArray = (
         }
       )
       if (stepDelay != null) {
-        sequence.push(withDelay(stepDelay, sequenceValue))
+        sequence.push(withDelay(stepDelay, sequenceValue, stepReduceMotion))
       } else {
         sequence.push(sequenceValue)
       }
@@ -467,7 +487,7 @@ export function useMotify<Animate>({
         value = value.value
       }

-      const { animation, config, shouldRepeat, repeatCount, repeatReverse } =
+      const { animation, config, reduceMotion, shouldRepeat, repeatCount, repeatReverse } =
         animationConfig(key, transition)

       const callback: (
@@ -543,7 +563,9 @@ export function useMotify<Animate>({
                   finalValue = withRepeat(
                     finalValue,
                     repeatCount,
-                    repeatReverse
+                    repeatReverse,
+                    callback,
+                    reduceMotion
                   )
                 }
                 transform[transformKey] = finalValue
@@ -572,10 +594,10 @@ export function useMotify<Animate>({

               let finalValue = animation(transformValue, config, callback)
               if (shouldRepeat) {
-                finalValue = withRepeat(finalValue, repeatCount, repeatReverse)
+                finalValue = withRepeat(finalValue, repeatCount, repeatReverse, undefined, reduceMotion)
               }
               if (delayMs != null) {
-                transform[transformKey] = withDelay(delayMs, finalValue)
+                transform[transformKey] = withDelay(delayMs, finalValue, reduceMotion)
               } else {
                 transform[transformKey] = finalValue
               }
@@ -602,7 +624,7 @@ export function useMotify<Animate>({
         )
         let finalValue = withSequence(sequence[0], ...sequence.slice(1))
         if (shouldRepeat) {
-          finalValue = withRepeat(finalValue, repeatCount, repeatReverse)
+          finalValue = withRepeat(finalValue, repeatCount, repeatReverse, undefined, reduceMotion)
         }

         if (isTransform(key)) {
@@ -634,10 +656,10 @@ export function useMotify<Animate>({
         const transform = {}
         let finalValue = animation(value, config, callback)
         if (shouldRepeat) {
-          finalValue = withRepeat(finalValue, repeatCount, repeatReverse)
+          finalValue = withRepeat(finalValue, repeatCount, repeatReverse, undefined, reduceMotion)
         }
         if (delayMs != null) {
-          transform[key] = withDelay(delayMs, finalValue)
+          transform[key] = withDelay(delayMs, finalValue, reduceMotion)
         } else {
           transform[key] = finalValue
         }
@@ -651,11 +673,11 @@ export function useMotify<Animate>({
           let finalValue = animation(value, config, callback)

           if (shouldRepeat) {
-            finalValue = withRepeat(finalValue, repeatCount, repeatReverse)
+            finalValue = withRepeat(finalValue, repeatCount, repeatReverse, undefined, reduceMotion)
           }

           if (delayMs != null) {
-            final[key][innerStyleKey] = withDelay(delayMs, finalValue)
+            final[key][innerStyleKey] = withDelay(delayMs, finalValue, reduceMotion)
           } else {
             final[key][innerStyleKey] = finalValue
           }
@@ -663,11 +685,11 @@ export function useMotify<Animate>({
       } else {
         let finalValue = animation(value, config, callback)
         if (shouldRepeat) {
-          finalValue = withRepeat(finalValue, repeatCount, repeatReverse)
+          finalValue = withRepeat(finalValue, repeatCount, repeatReverse, undefined, reduceMotion)
         }

         if (delayMs != null && typeof delayMs === 'number') {
-          final[key] = withDelay(delayMs, finalValue)
+          final[key] = withDelay(delayMs, finalValue, reduceMotion)
         } else {
           final[key] = finalValue
         }