mattermost-mobile/app/components/animated_number/index.tsx
Rahim Rahman 84d320afcf
test: more meaningful AnimatedNumber component test (#8309)
* better unit test for animated_number

* updated test

* update test

* clean up based on comments
2024-11-21 10:30:30 -07:00

153 lines
5.6 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {Animated, Easing, type LayoutChangeEvent, type StyleProp, Text, type TextStyle, View} from 'react-native';
interface Props {
animateToNumber: number;
fontStyle?: StyleProp<TextStyle>;
animationDuration?: number;
easing?: number;
}
const NUMBERS = Array(10).fill(null).map((_, i) => i);
const usePrevious = (value: number) => {
const ref = React.useRef<number>(value);
React.useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
};
const AnimatedNumber = ({
animateToNumber,
animationDuration = 1400,
fontStyle,
easing = 1.2,
}: Props) => {
const previousNumber = usePrevious(animateToNumber);
const [numberHeight, setNumberHeight] = useState(0);
const animateToNumberString = String(Math.abs(animateToNumber));
const previousNumberString = useMemo(() => {
// comparing previousNumber and animateToNumber gets trickier when the number of digits changes and the length differs.
// By padding the previousNumber with 0s, or slicing the previousNumber to match the length of animateToNumber
// we can compare them digit by digit
const _previousNumberString = String(Math.abs(previousNumber)).padStart(
animateToNumberString.length,
'0',
);
if (_previousNumberString.length > animateToNumberString.length) {
_previousNumberString.slice(_previousNumberString.length - animateToNumberString.length);
}
return _previousNumberString;
}, [animateToNumberString, previousNumber]);
const animations = useMemo(() => {
const animateToNumberArray = Array.from(animateToNumberString, Number);
if (!numberHeight) {
return animateToNumberArray.map(() => new Animated.Value(0));
}
const previousNumberArray = Array.from(previousNumberString, Number);
return animateToNumberArray.map((_, index) => {
const useDigit = previousNumberArray[index] === animateToNumberArray[index] ? animateToNumberArray[index] : previousNumberArray[index];
return new Animated.Value(-1 * (numberHeight * useDigit));
});
}, [animateToNumberString, numberHeight, previousNumberString]);
useEffect(() => {
if (!numberHeight) {
return;
}
const animateToNumberArray = Array.from(animateToNumberString, Number);
animations.forEach((animation, index) => {
Animated.timing(animation, {
toValue: -1 * (numberHeight * animateToNumberArray[index]),
duration: animationDuration,
useNativeDriver: true,
easing: Easing.elastic(easing),
}).start();
});
}, [
animateToNumberString,
animationDuration,
animations,
easing,
numberHeight,
]);
const setButtonLayout = useCallback((e: LayoutChangeEvent) => {
setNumberHeight(e.nativeEvent.layout.height);
}, []);
return (
<>
{numberHeight !== 0 && (
<View
style={{flexDirection: 'row'}}
testID='animation-number-main'
>
{animateToNumber < 0 && (
<Text style={[fontStyle, {height: numberHeight}]}>{'-'}</Text>
)}
{Array.from(animateToNumberString, Number).map((_, index) => {
const useIndex = animateToNumberString.length - 1 - index;
return (
<View
key={useIndex}
style={{height: numberHeight, overflow: 'hidden'}}
>
<Animated.View
style={{
transform: [
{
translateY: animations[index],
},
],
}}
testID={`animated-number-view-${useIndex}`}
>
{NUMBERS.map((number, i) => (
<View
key={`${NUMBERS.length - 1 - i}`}
style={{flexDirection: 'row'}}
>
<Text
style={[fontStyle, {height: numberHeight}]}
testID={`text-${useIndex}-${number}`}
>
{number}
</Text>
</View>
))}
</Animated.View>
</View>
);
})}
</View>
)}
{numberHeight === 0 &&
<Text
style={[fontStyle]}
onLayout={setButtonLayout}
testID={'no-animation-number'}
>
{animateToNumberString}
</Text>
}
</>
);
};
export default AnimatedNumber;