From 2b1c1df35410f2801b9a4c4095c7518d62585830 Mon Sep 17 00:00:00 2001 From: Muhammad Hur Ali Date: Tue, 18 Oct 2022 01:07:59 +0500 Subject: [PATCH] fix: progress bar animated value (#3414) --- example/src/Examples/ProgressBarExample.tsx | 44 ++++++++++++++-- src/components/ProgressBar.tsx | 25 ++++++++- src/components/__tests__/ProgressBar.test.js | 55 +++++++++++++------- 3 files changed, 101 insertions(+), 23 deletions(-) diff --git a/example/src/Examples/ProgressBarExample.tsx b/example/src/Examples/ProgressBarExample.tsx index cfd36040ae..ab40fed65a 100644 --- a/example/src/Examples/ProgressBarExample.tsx +++ b/example/src/Examples/ProgressBarExample.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { View, StyleSheet } from 'react-native'; +import { View, StyleSheet, Animated } from 'react-native'; import { Button, @@ -8,21 +8,46 @@ import { MD2Colors, MD3Colors, useTheme, + ProgressBarProps, } from 'react-native-paper'; import ScreenWrapper from '../ScreenWrapper'; +class ClassProgressBar extends React.Component { + constructor(props: ProgressBarProps) { + super(props); + } + + render() { + return ; + } +} + +const AnimatedProgressBar = Animated.createAnimatedComponent(ClassProgressBar); + const ProgressBarExample = () => { const [visible, setVisible] = React.useState(true); const [progress, setProgress] = React.useState(0.3); - const { isV3 } = useTheme(); + const theme = useTheme(); + const { isV3 } = theme; + const { current: progressBarValue } = React.useRef(new Animated.Value(0)); + + const runCustomAnimation = () => { + progressBarValue.setValue(0); + Animated.timing(progressBarValue, { + toValue: 1, + duration: 2000, + useNativeDriver: false, + }).start(); + }; return ( - + + Default ProgressBar @@ -63,6 +88,16 @@ const ProgressBarExample = () => { style={styles.customHeight} /> + + + ProgressBar with animated value + + ); }; @@ -79,6 +114,9 @@ const styles = StyleSheet.create({ customHeight: { height: 20, }, + progressBar: { + height: 15, + }, }); export default ProgressBarExample; diff --git a/src/components/ProgressBar.tsx b/src/components/ProgressBar.tsx index 5b0fa17290..6b4f6cf40f 100644 --- a/src/components/ProgressBar.tsx +++ b/src/components/ProgressBar.tsx @@ -16,8 +16,14 @@ import { withTheme } from '../core/theming'; import type { Theme } from '../types'; export type Props = React.ComponentPropsWithRef & { + /** + * Animated value (between 0 and 1). This tells the progress bar to rely on this value to animate it. + * Note: It should not be used in parallel with the `progress` prop. + */ + animatedValue?: number; /** * Progress value (between 0 and 1). + * Note: It should not be used in parallel with the `animatedValue` prop. */ progress?: number; /** @@ -69,6 +75,7 @@ const ProgressBar = ({ progress = 0, visible = true, theme, + animatedValue, ...rest }: Props) => { const { current: timer } = React.useRef( @@ -92,6 +99,10 @@ const ProgressBar = ({ isInteraction: false, }).start(); + if (animatedValue && animatedValue >= 0) { + return; + } + // Animate progress bar if (indeterminate) { if (!indeterminateAnimation.current) { @@ -116,7 +127,13 @@ const ProgressBar = ({ isInteraction: false, }).start(); } - }, [scale, timer, progress, indeterminate, fade]); + /** + * We shouldn't add @param animatedValue to the + * deps array, to avoid the unnecessary loop. + * We can only check if the prop is passed initially, + * and we do early return. + */ + }, [fade, scale, indeterminate, timer, progress]); const stopAnimation = React.useCallback(() => { // Stop indeterminate animation @@ -137,6 +154,12 @@ const ProgressBar = ({ else stopAnimation(); }, [visible, startAnimation, stopAnimation]); + React.useEffect(() => { + if (animatedValue && animatedValue >= 0) { + timer.setValue(animatedValue); + } + }, [animatedValue, timer]); + React.useEffect(() => { // Start animation the very first time when previously the width was unclear if (visible && prevWidth === 0) { diff --git a/src/components/__tests__/ProgressBar.test.js b/src/components/__tests__/ProgressBar.test.js index c7ee5d96c1..2c0b8009bb 100644 --- a/src/components/__tests__/ProgressBar.test.js +++ b/src/components/__tests__/ProgressBar.test.js @@ -1,8 +1,7 @@ import * as React from 'react'; -import { View } from 'react-native'; +import { Animated } from 'react-native'; -import { act } from '@testing-library/react-native'; -import renderer from 'react-test-renderer'; +import { render } from '@testing-library/react-native'; import ProgressBar from '../ProgressBar.tsx'; @@ -14,37 +13,55 @@ const layoutEvent = { }, }; +const a11Role = 'progressbar'; + +class ClassProgressBar extends React.Component { + constructor(props) { + super(props); + } + + render() { + return ; + } +} + +const AnimatedProgressBar = Animated.createAnimatedComponent(ClassProgressBar); + +it('renders progress bar with animated value', () => { + const tree = render(); + + const props = tree.getByRole(a11Role).props; + props.onLayout(layoutEvent); + + tree.update(); + + expect(tree.container.props['animatedValue']).toBe(0.4); +}); + it('renders progress bar with specific progress', () => { - const tree = renderer.create(); - act(() => { - tree.root.findByType(View).props.onLayout(layoutEvent); - }); + const tree = render(); + tree.getByRole(a11Role).props.onLayout(layoutEvent); expect(tree.toJSON()).toMatchSnapshot(); }); it('renders hidden progress bar', () => { - const tree = renderer.create(); - act(() => { - tree.root.findByType(View).props.onLayout(layoutEvent); - }); + const tree = render(); + tree.getByRole(a11Role).props.onLayout(layoutEvent); expect(tree.toJSON()).toMatchSnapshot(); }); it('renders colored progress bar', () => { - const tree = renderer.create(); - act(() => { - tree.root.findByType(View).props.onLayout(layoutEvent); - }); + const tree = render(); + tree.getByRole(a11Role).props.onLayout(layoutEvent); expect(tree.toJSON()).toMatchSnapshot(); }); it('renders indeterminate progress bar', () => { - const tree = renderer.create(); - act(() => { - tree.root.findByType(View).props.onLayout(layoutEvent); - }); + const tree = render(); + tree.getByRole(a11Role).props.onLayout(layoutEvent); + expect(tree.toJSON()).toMatchSnapshot(); });