Repository: callstack/react-native-paper
Pull request #735 (2019-01-18 08:55:29)

feat: ProgressBar component
### Motivation

Currently, the progress bar differs on every platform. This component is a drop-in for the native `ProgressBar` and follows the guidelines from https://material.io/design/components/progress-indicators.html

### Test plan

See examples app.


callstack-bot: Hey @jaulz, thank you for your pull request 🤗. The documentation from this branch can be viewed [here](https://6890-71323749-gh.circle-artifacts.com/0/docs/index.html). Please remember to update Typescript types if you changed API.
jaulz: @Trancever thanks for the feedback. Could you please have another look? 😊 
Trancever: @jaulz I also pushed one commit with updated typescript types, cause they were lost during merge I think.
jaulz: @Trancever oops, sorry, but thanks!
Trancever: @satya164 Should we consider this change as breaking? We changed `animating` prop to `visible`.. I haven't though about this earlier :/
deyanskiq: satya164 any update?
Trancever: @deyanskiq This is breaking change so we are going to introduce it with 3.0 version, but I can't say exactly when it's going to happen.
deyanskiq: I have trouble changing the height of the ProgressBar. This breaking change is related to my problem right?
Trancever: @deyanskiq This is breaking change, cause we slightly change props to better align with existing components, but yeah, after this is merged, you should be able to change height via `style` prop. If you can't wait for it, you can copy and paste this component into your project and use it :)
deyanskiq: @Trancever Applying height property to style set height only to the background object. Is there a way to apply height to the front object too. Screenshot here: https://prnt.sc/mxrcqg
jaulz: @deyanskiq here you go...
deyanskiq: Great thank you. You helped other devs as well.
jaulz: @Trancever do you think it would make sense to have a 2.0 and a 3.0 version with the subtle change of the property name?
Trancever: @jaulz We are going to release 3.0 just after we merge this PR with Menu component. Shouldn't take much time, probably it will happen this week.
jaulz: @Trancever great news, thanks for the update 😊 
Trancever: @jaulz Could you change target branch to 3.0? Once it's done we will merge this :) 
jaulz: @Trancever changed 😊 

Patch:
diff --git a/example/src/Examples/ProgressBarExample.js b/example/src/Examples/ProgressBarExample.js
index 860c7eb72..71b4d3228 100644
--- a/example/src/Examples/ProgressBarExample.js
+++ b/example/src/Examples/ProgressBarExample.js
@@ -3,6 +3,7 @@
 import * as React from 'react';
 import { View, StyleSheet } from 'react-native';
 import {
+  Button,
   ProgressBar,
   Paragraph,
   Colors,
@@ -16,43 +17,73 @@ type Props = {
 
 type State = {
   progress: number,
+  visible: boolean,
 };
 
 class ProgressBarExample extends React.Component<Props, State> {
   static title = 'Progress Bar';
 
   state = {
-    progress: 0,
+    progress: 0.3,
+    visible: true,
   };
 
-  componentDidMount() {
-    this._interval = setInterval(
-      () =>
-        this.setState(state => ({
-          progress: state.progress < 1 ? state.progress + 0.01 : 0,
-        })),
-      16
-    );
-  }
-
-  componentWillUnmount() {
-    clearInterval(this._interval);
-  }
-
-  _interval: IntervalID;
-
   render() {
     const {
       theme: {
         colors: { background },
       },
     } = this.props;
+
     return (
       <View style={[styles.container, { backgroundColor: background }]}>
-        <Paragraph>ProgressBar primary color</Paragraph>
-        <ProgressBar progress={this.state.progress} />
-        <Paragraph>ProgressBar custom color</Paragraph>
-        <ProgressBar progress={this.state.progress} color={Colors.red800} />
+        <Button onPress={() => this.setState({ visible: !this.state.visible })}>
+          Toggle visible
+        </Button>
+        <Button onPress={() => this.setState({ progress: Math.random() })}>
+          Random progress
+        </Button>
+
+        <View style={styles.row}>
+          <Paragraph>Default ProgressBar </Paragraph>
+          <ProgressBar
+            progress={this.state.progress}
+            visible={this.state.visible}
+          />
+        </View>
+
+        <View style={styles.row}>
+          <Paragraph>Indeterminate ProgressBar</Paragraph>
+          <ProgressBar indeterminate visible={this.state.visible} />
+        </View>
+
+        <View style={styles.row}>
+          <Paragraph>ProgressBar with custom color</Paragraph>
+          <ProgressBar
+            progress={this.state.progress}
+            visible={this.state.visible}
+            color={Colors.red800}
+          />
+        </View>
+
+        <View style={styles.row}>
+          <Paragraph>ProgressBar with custom background color</Paragraph>
+          <ProgressBar
+            progress={this.state.progress}
+            visible={this.state.visible}
+            color={Colors.red800}
+            style={{ backgroundColor: Colors.teal500 }}
+          />
+        </View>
+
+        <View style={styles.row}>
+          <Paragraph>ProgressBar with custom height</Paragraph>
+          <ProgressBar
+            progress={this.state.progress}
+            visible={this.state.visible}
+            style={{ height: 20 }}
+          />
+        </View>
       </View>
     );
   }
@@ -62,6 +93,10 @@ const styles = StyleSheet.create({
     flex: 1,
     padding: 16,
   },
+
+  row: {
+    marginVertical: 10,
+  },
 });
 
 export default withTheme(ProgressBarExample);
diff --git a/src/components/ProgressBar.js b/src/components/ProgressBar.js
new file mode 100644
index 000000000..612ca8c18
--- /dev/null
+++ b/src/components/ProgressBar.js
@@ -0,0 +1,234 @@
+/* @flow */
+
+import * as React from 'react';
+import { Animated, Platform, StyleSheet, View } from 'react-native';
+import setColor from 'color';
+import { withTheme } from '../core/theming';
+import type { Theme } from '../types';
+
+type Props = {|
+  /**
+   * Progress value (between 0 and 1).
+   */
+  progress?: number,
+  /**
+   * Color of the progress bar. The background color will be calculated based on this but you can change it by passing `backgroundColor` to `style` prop.
+   */
+  color?: string,
+  /**
+   * If the progress bar will show indeterminate progress.
+   */
+  indeterminate?: boolean,
+  /**
+   * Whether to show the ProgressBar (true, the default) or hide it (false).
+   */
+  visible?: boolean,
+  style?: any,
+  /**
+   * @optional
+   */
+  theme: Theme,
+|};
+
+type State = {
+  width: number,
+  fade: Animated.Value,
+  timer: Animated.Value,
+};
+
+const INDETERMINATE_DURATION = 2000;
+const INDETERMINATE_MAX_WIDTH = 0.6;
+
+/**
+ * Progress bar is an indicator used to present progress of some activity in the app.
+ *
+ * <div class="screenshots">
+ *   <img src="screenshots/progress-bar.png" />
+ * </div>
+ *
+ * ## Usage
+ * ```js
+ * import * as React from 'react';
+ * import { ProgressBar, Colors } from 'react-native-paper';
+ *
+ * const MyComponent = () => (
+ *   <ProgressBar progress={0.5} color={Colors.red800} />
+ * );
+ *
+ * export default MyComponent;
+ * ```
+ */
+class ProgressBar extends React.Component<Props, State> {
+  static defaultProps = {
+    visible: true,
+    progress: 0,
+  };
+
+  state = {
+    width: 0,
+    timer: new Animated.Value(0),
+    fade: new Animated.Value(0),
+  };
+
+  indeterminateAnimation = null;
+
+  componentDidUpdate() {
+    const { visible } = this.props;
+
+    if (visible) {
+      this._startAnimation();
+    } else {
+      this._stopAnimation();
+    }
+  }
+
+  _onLayout = event => {
+    const { visible } = this.props;
+    const { width: previousWidth } = this.state;
+
+    this.setState({ width: event.nativeEvent.layout.width }, () => {
+      // Start animation the very first time when previously the width was unclear
+      if (visible && previousWidth === 0) {
+        this._startAnimation();
+      }
+    });
+  };
+
+  _startAnimation() {
+    const { indeterminate, progress } = this.props;
+    const { fade, timer } = this.state;
+
+    // Show progress bar
+    Animated.timing(fade, {
+      duration: 200,
+      toValue: 1,
+      useNativeDriver: true,
+      isInteraction: false,
+    }).start();
+
+    // Animate progress bar
+    if (indeterminate) {
+      if (!this.indeterminateAnimation) {
+        this.indeterminateAnimation = Animated.timing(timer, {
+          duration: INDETERMINATE_DURATION,
+          toValue: 1,
+          // Animated.loop does not work if useNativeDriver is true on web
+          useNativeDriver: Platform.OS !== 'web',
+          isInteraction: false,
+        });
+      }
+
+      // Reset timer to the beginning
+      timer.setValue(0);
+
+      // $FlowFixMe
+      Animated.loop(this.indeterminateAnimation).start();
+    } else {
+      Animated.timing(timer, {
+        duration: 200,
+        // $FlowFixMe
+        toValue: progress,
+        useNativeDriver: true,
+        isInteraction: false,
+      }).start();
+    }
+  }
+
+  _stopAnimation() {
+    const { fade } = this.state;
+
+    // Stop indeterminate animation
+    if (this.indeterminateAnimation) {
+      this.indeterminateAnimation.stop();
+    }
+
+    Animated.timing(fade, {
+      duration: 200,
+      toValue: 0,
+      useNativeDriver: true,
+      isInteraction: false,
+    }).start();
+  }
+
+  render() {
+    const { color, indeterminate, style, theme } = this.props;
+    const { fade, timer, width } = this.state;
+    const tintColor = color || theme.colors.primary;
+    const trackTintColor = setColor(tintColor)
+      .alpha(0.38)
+      .rgb()
+      .string();
+
+    return (
+      <View onLayout={this._onLayout}>
+        <Animated.View
+          style={[
+            styles.container,
+            { backgroundColor: trackTintColor, opacity: fade },
+            style,
+          ]}
+        >
+          <Animated.View
+            style={[
+              styles.progressBar,
+              {
+                backgroundColor: tintColor,
+                width,
+                transform: [
+                  {
+                    translateX: timer.interpolate(
+                      indeterminate
+                        ? {
+                            inputRange: [0, 0.5, 1],
+                            outputRange: [
+                              -0.5 * width,
+                              -0.5 * INDETERMINATE_MAX_WIDTH * width,
+                              0.7 * width,
+                            ],
+                          }
+                        : {
+                            inputRange: [0, 1],
+                            outputRange: [-0.5 * width, 0],
+                          }
+                    ),
+                  },
+                  {
+                    // Workaround for workaround for https://github.com/facebook/react-native/issues/6278
+                    scaleX: timer.interpolate(
+                      indeterminate
+                        ? {
+                            inputRange: [0, 0.5, 1],
+                            outputRange: [
+                              0.0001,
+                              INDETERMINATE_MAX_WIDTH,
+                              0.0001,
+                            ],
+                          }
+                        : {
+                            inputRange: [0, 1],
+                            outputRange: [0.0001, 1],
+                          }
+                    ),
+                  },
+                ],
+              },
+            ]}
+          />
+        </Animated.View>
+      </View>
+    );
+  }
+}
+
+const styles = StyleSheet.create({
+  container: {
+    height: 4,
+    overflow: 'hidden',
+  },
+
+  progressBar: {
+    flex: 1,
+  },
+});
+
+export default withTheme(ProgressBar);
diff --git a/src/components/ProgressBar/ProgressBar.js b/src/components/ProgressBar/ProgressBar.js
deleted file mode 100644
index ba71aa4c8..000000000
--- a/src/components/ProgressBar/ProgressBar.js
+++ /dev/null
@@ -1,71 +0,0 @@
-/* @flow */
-
-import * as React from 'react';
-import { StyleSheet } from 'react-native';
-import setColor from 'color';
-import ProgressBarComponent from './ProgressBarComponent';
-import { withTheme } from '../../core/theming';
-import type { Theme } from '../../types';
-
-type Props = {|
-  /**
-   * Progress value (between 0 and 1).
-   */
-  progress: number,
-  /**
-   * Color of the progress bar.
-   */
-  color?: string,
-  style?: any,
-  /**
-   * @optional
-   */
-  theme: Theme,
-|};
-
-/**
- * Progress bar is an indicator used to present progress of some activity in the app.
- *
- * <div class="screenshots">
- *   <img src="screenshots/progress-bar.png" />
- * </div>
- *
- * ## Usage
- * ```js
- * import * as React from 'react';
- * import { ProgressBar, Colors } from 'react-native-paper';
- *
- * const MyComponent = () => (
- *   <ProgressBar progress={0.5} color={Colors.red800} />
- * );
- *
- * export default MyComponent;
- * ```
- */
-class ProgressBar extends React.Component<Props> {
-  render() {
-    const { progress, color, style, theme } = this.props;
-    const tintColor = color || theme.colors.primary;
-    const trackTintColor = setColor(tintColor)
-      .alpha(0.38)
-      .rgb()
-      .string();
-
-    return (
-      <ProgressBarComponent
-        progress={progress}
-        progressTintColor={tintColor}
-        style={[styles.progressBarHeight, style]}
-        trackTintColor={trackTintColor}
-      />
-    );
-  }
-}
-
-const styles = StyleSheet.create({
-  progressBarHeight: {
-    paddingVertical: 10,
-  },
-});
-
-export default withTheme(ProgressBar);
diff --git a/src/components/ProgressBar/ProgressBarComponent.android.js b/src/components/ProgressBar/ProgressBarComponent.android.js
deleted file mode 100644
index c72b04da0..000000000
--- a/src/components/ProgressBar/ProgressBarComponent.android.js
+++ /dev/null
@@ -1,18 +0,0 @@
-/* @flow */
-
-import * as React from 'react';
-import { ProgressBarAndroid } from 'react-native';
-
-export default function BaseProgressBarAndroid({
-  progressTintColor,
-  ...rest
-}: *) {
-  return (
-    <ProgressBarAndroid
-      {...rest}
-      styleAttr="Horizontal"
-      indeterminate={false}
-      color={progressTintColor}
-    />
-  );
-}
diff --git a/src/components/ProgressBar/ProgressBarComponent.ios.js b/src/components/ProgressBar/ProgressBarComponent.ios.js
deleted file mode 100644
index fc3ff179c..000000000
--- a/src/components/ProgressBar/ProgressBarComponent.ios.js
+++ /dev/null
@@ -1,3 +0,0 @@
-/* @flow */
-
-export { ProgressViewIOS as default } from 'react-native';
diff --git a/src/components/ProgressBar/ProgressBarComponent.js b/src/components/ProgressBar/ProgressBarComponent.js
deleted file mode 100644
index 9e5330823..000000000
--- a/src/components/ProgressBar/ProgressBarComponent.js
+++ /dev/null
@@ -1,1 +0,0 @@
-export { ProgressBar as default } from 'react-native';
diff --git a/src/components/ProgressBar/ProgressBarComponent.macos.js b/src/components/ProgressBar/ProgressBarComponent.macos.js
deleted file mode 100644
index fc3ff179c..000000000
--- a/src/components/ProgressBar/ProgressBarComponent.macos.js
+++ /dev/null
@@ -1,3 +0,0 @@
-/* @flow */
-
-export { ProgressViewIOS as default } from 'react-native';
diff --git a/src/components/ProgressBar/ProgressBarComponent.windows.js b/src/components/ProgressBar/ProgressBarComponent.windows.js
deleted file mode 100644
index 2c6cc2221..000000000
--- a/src/components/ProgressBar/ProgressBarComponent.windows.js
+++ /dev/null
@@ -1,3 +0,0 @@
-/* @flow */
-
-export { View as default } from 'react-native';
diff --git a/src/index.js b/src/index.js
index 3c404a11f..ff6c203db 100644
--- a/src/index.js
+++ b/src/index.js
@@ -36,7 +36,7 @@ export { default as IconButton } from './components/IconButton';
 export { default as Menu } from './components/Menu/Menu';
 export { default as Modal } from './components/Modal';
 export { default as Portal } from './components/Portal/Portal';
-export { default as ProgressBar } from './components/ProgressBar/ProgressBar';
+export { default as ProgressBar } from './components/ProgressBar';
 export { default as RadioButton } from './components/RadioButton';
 export { default as Searchbar } from './components/Searchbar';
 export { default as Snackbar } from './components/Snackbar';
diff --git a/typings/components/ProgressBar.d.ts b/typings/components/ProgressBar.d.ts
index 9b22cf2d7..f5bea2ba9 100644
--- a/typings/components/ProgressBar.d.ts
+++ b/typings/components/ProgressBar.d.ts
@@ -2,8 +2,8 @@ import * as React from 'react';
 import { ThemeShape } from '../types';
 
 export interface ProgressBarProps {
-  progress: number;
-  animating?: boolean;
+  progress?: number;
+  visible?: boolean;
   indeterminate?: boolean;
   color?: string;
   style?: any;


Test patch:
diff --git a/src/components/__tests__/ProgressBar.test.js b/src/components/__tests__/ProgressBar.test.js
new file mode 100644
index 000000000..cfb57387c
--- /dev/null
+++ b/src/components/__tests__/ProgressBar.test.js
@@ -0,0 +1,44 @@
+/* @flow */
+
+import * as React from 'react';
+import { View } from 'react-native';
+import renderer from 'react-test-renderer';
+import ProgressBar from '../ProgressBar';
+
+jest.useFakeTimers();
+
+const layoutEvent = {
+  nativeEvent: {
+    layout: {
+      width: 100,
+    },
+  },
+};
+
+it('renders progress bar with specific progress', () => {
+  const tree = renderer.create(<ProgressBar progress={0.2} />);
+  tree.root.findByType(View).props.onLayout(layoutEvent);
+
+  expect(tree.toJSON()).toMatchSnapshot();
+});
+
+it('renders hidden progress bar', () => {
+  const tree = renderer.create(<ProgressBar progress={0.2} visible={false} />);
+  tree.root.findByType(View).props.onLayout(layoutEvent);
+
+  expect(tree.toJSON()).toMatchSnapshot();
+});
+
+it('renders colored progress bar', () => {
+  const tree = renderer.create(<ProgressBar progress={0.2} color="red" />);
+  tree.root.findByType(View).props.onLayout(layoutEvent);
+
+  expect(tree.toJSON()).toMatchSnapshot();
+});
+
+it('renders indeterminate progress bar', () => {
+  const tree = renderer.create(<ProgressBar indeterminate />);
+  tree.root.findByType(View).props.onLayout(layoutEvent);
+
+  expect(tree.toJSON()).toMatchSnapshot();
+});
diff --git a/src/components/__tests__/__snapshots__/ProgressBar.test.js.snap b/src/components/__tests__/__snapshots__/ProgressBar.test.js.snap
new file mode 100644
index 000000000..1e39cf634
--- /dev/null
+++ b/src/components/__tests__/__snapshots__/ProgressBar.test.js.snap
@@ -0,0 +1,141 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders colored progress bar 1`] = `
+<View
+  onLayout={[Function]}
+>
+  <View
+    style={
+      Object {
+        "backgroundColor": "rgba(255, 0, 0, 0.38)",
+        "height": 4,
+        "opacity": 0,
+        "overflow": "hidden",
+      }
+    }
+  >
+    <View
+      style={
+        Object {
+          "backgroundColor": "red",
+          "flex": 1,
+          "transform": Array [
+            Object {
+              "translateX": -50,
+            },
+            Object {
+              "scaleX": 0.0001,
+            },
+          ],
+          "width": 100,
+        }
+      }
+    />
+  </View>
+</View>
+`;
+
+exports[`renders hidden progress bar 1`] = `
+<View
+  onLayout={[Function]}
+>
+  <View
+    style={
+      Object {
+        "backgroundColor": "rgba(98, 0, 238, 0.38)",
+        "height": 4,
+        "opacity": 0,
+        "overflow": "hidden",
+      }
+    }
+  >
+    <View
+      style={
+        Object {
+          "backgroundColor": "#6200ee",
+          "flex": 1,
+          "transform": Array [
+            Object {
+              "translateX": -50,
+            },
+            Object {
+              "scaleX": 0.0001,
+            },
+          ],
+          "width": 100,
+        }
+      }
+    />
+  </View>
+</View>
+`;
+
+exports[`renders indeterminate progress bar 1`] = `
+<View
+  onLayout={[Function]}
+>
+  <View
+    style={
+      Object {
+        "backgroundColor": "rgba(98, 0, 238, 0.38)",
+        "height": 4,
+        "opacity": 0,
+        "overflow": "hidden",
+      }
+    }
+  >
+    <View
+      style={
+        Object {
+          "backgroundColor": "#6200ee",
+          "flex": 1,
+          "transform": Array [
+            Object {
+              "translateX": -50,
+            },
+            Object {
+              "scaleX": 0.0001,
+            },
+          ],
+          "width": 100,
+        }
+      }
+    />
+  </View>
+</View>
+`;
+
+exports[`renders progress bar with specific progress 1`] = `
+<View
+  onLayout={[Function]}
+>
+  <View
+    style={
+      Object {
+        "backgroundColor": "rgba(98, 0, 238, 0.38)",
+        "height": 4,
+        "opacity": 0,
+        "overflow": "hidden",
+      }
+    }
+  >
+    <View
+      style={
+        Object {
+          "backgroundColor": "#6200ee",
+          "flex": 1,
+          "transform": Array [
+            Object {
+              "translateX": -50,
+            },
+            Object {
+              "scaleX": 0.0001,
+            },
+          ],
+          "width": 100,
+        }
+      }
+    />
+  </View>
+</View>
+`;