Skip to content

Commit

Permalink
fix: progress bar animated value (#3414)
Browse files Browse the repository at this point in the history
  • Loading branch information
hurali97 authored Oct 17, 2022
1 parent 06831c9 commit 2b1c1df
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 23 deletions.
44 changes: 41 additions & 3 deletions example/src/Examples/ProgressBarExample.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react';
import { View, StyleSheet } from 'react-native';
import { View, StyleSheet, Animated } from 'react-native';

import {
Button,
Expand All @@ -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 <ProgressBar {...this.props} />;
}
}

const AnimatedProgressBar = Animated.createAnimatedComponent(ClassProgressBar);

const ProgressBarExample = () => {
const [visible, setVisible] = React.useState<boolean>(true);
const [progress, setProgress] = React.useState<number>(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 (
<ScreenWrapper contentContainerStyle={styles.container}>
<Button onPress={() => setVisible(!visible)}>Toggle visible</Button>
<Button onPress={() => setVisible(!visible)}>Toggle visibility</Button>
<Button onPress={() => setProgress(Math.random())}>
Random progress
</Button>
<Button onPress={runCustomAnimation}>Toggle animation</Button>

<View style={styles.row}>
<Paragraph>Default ProgressBar </Paragraph>
Expand Down Expand Up @@ -63,6 +88,16 @@ const ProgressBarExample = () => {
style={styles.customHeight}
/>
</View>

<View style={styles.row}>
<Paragraph>ProgressBar with animated value</Paragraph>
<AnimatedProgressBar
style={styles.progressBar}
animatedValue={progressBarValue}
color={MD2Colors.green200}
theme={theme}
/>
</View>
</ScreenWrapper>
);
};
Expand All @@ -79,6 +114,9 @@ const styles = StyleSheet.create({
customHeight: {
height: 20,
},
progressBar: {
height: 15,
},
});

export default ProgressBarExample;
25 changes: 24 additions & 1 deletion src/components/ProgressBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,14 @@ import { withTheme } from '../core/theming';
import type { Theme } from '../types';

export type Props = React.ComponentPropsWithRef<typeof View> & {
/**
* 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;
/**
Expand Down Expand Up @@ -69,6 +75,7 @@ const ProgressBar = ({
progress = 0,
visible = true,
theme,
animatedValue,
...rest
}: Props) => {
const { current: timer } = React.useRef<Animated.Value>(
Expand All @@ -92,6 +99,10 @@ const ProgressBar = ({
isInteraction: false,
}).start();

if (animatedValue && animatedValue >= 0) {
return;
}

// Animate progress bar
if (indeterminate) {
if (!indeterminateAnimation.current) {
Expand All @@ -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
Expand All @@ -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) {
Expand Down
55 changes: 36 additions & 19 deletions src/components/__tests__/ProgressBar.test.js
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -14,37 +13,55 @@ const layoutEvent = {
},
};

const a11Role = 'progressbar';

class ClassProgressBar extends React.Component {
constructor(props) {
super(props);
}

render() {
return <ProgressBar {...this.props} />;
}
}

const AnimatedProgressBar = Animated.createAnimatedComponent(ClassProgressBar);

it('renders progress bar with animated value', () => {
const tree = render(<AnimatedProgressBar animatedValue={0.2} />);

const props = tree.getByRole(a11Role).props;
props.onLayout(layoutEvent);

tree.update(<AnimatedProgressBar animatedValue={0.4} />);

expect(tree.container.props['animatedValue']).toBe(0.4);
});

it('renders progress bar with specific progress', () => {
const tree = renderer.create(<ProgressBar progress={0.2} />);
act(() => {
tree.root.findByType(View).props.onLayout(layoutEvent);
});
const tree = render(<ProgressBar progress={0.2} />);
tree.getByRole(a11Role).props.onLayout(layoutEvent);

expect(tree.toJSON()).toMatchSnapshot();
});

it('renders hidden progress bar', () => {
const tree = renderer.create(<ProgressBar progress={0.2} visible={false} />);
act(() => {
tree.root.findByType(View).props.onLayout(layoutEvent);
});
const tree = render(<ProgressBar progress={0.2} visible={false} />);
tree.getByRole(a11Role).props.onLayout(layoutEvent);

expect(tree.toJSON()).toMatchSnapshot();
});

it('renders colored progress bar', () => {
const tree = renderer.create(<ProgressBar progress={0.2} color="red" />);
act(() => {
tree.root.findByType(View).props.onLayout(layoutEvent);
});
const tree = render(<ProgressBar progress={0.2} color="red" />);
tree.getByRole(a11Role).props.onLayout(layoutEvent);

expect(tree.toJSON()).toMatchSnapshot();
});

it('renders indeterminate progress bar', () => {
const tree = renderer.create(<ProgressBar indeterminate />);
act(() => {
tree.root.findByType(View).props.onLayout(layoutEvent);
});
const tree = render(<ProgressBar indeterminate />);
tree.getByRole(a11Role).props.onLayout(layoutEvent);

expect(tree.toJSON()).toMatchSnapshot();
});

0 comments on commit 2b1c1df

Please sign in to comment.