├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .npmignore ├── .prettierrc.json ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── README_v1.md ├── UPGRADING.md ├── demo ├── .env ├── .gitignore ├── README.md ├── package.json ├── public │ ├── codesandbox │ │ └── base-examples.js │ ├── favicon-256x256.png │ ├── images │ │ ├── CircularProgressbarWithChildren.png │ │ ├── animated-progressbar.gif │ │ └── circular-progressbar-examples.png │ ├── index.html │ └── manifest.json ├── src │ ├── AnimatedProgressProvider.tsx │ ├── App.css │ ├── App.test.tsx │ ├── App.tsx │ ├── ChangingProgressProvider.tsx │ ├── Demo.tsx │ ├── ProgressProvider.tsx │ ├── index.css │ ├── index.tsx │ ├── react-app-env.d.ts │ └── serviceWorker.ts ├── tsconfig.json └── yarn.lock ├── jest.config.json ├── package.json ├── rollup.config.js ├── src ├── CircularProgressbar.tsx ├── CircularProgressbarWithChildren.tsx ├── Path.tsx ├── buildStyles.ts ├── constants.ts ├── index.ts ├── styles.css └── types.ts ├── test ├── CircularProgressbar.test.tsx ├── CircularProgressbarWithChildren.test.tsx ├── buildStyles.test.ts └── setupTests.ts ├── tsconfig.json └── yarn.lock /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## 🐛 Bug report 11 | 12 | ### Summary of issue 13 | 14 | Please describe what the problem is - this will help me be able to track down the issue. If applicable, add screenshots to help explain your problem. 15 | 16 | ### Reproducible example 17 | 18 | Fork this Codesandbox and modify it to demonstrate the issue you're having: https://codesandbox.io/s/reactcircularprogressbar-issue-template-3zm3j 19 | 20 | ### Your environment 21 | 22 | * What browser version were you using? 23 | * What version of react-circular-progressbar are you using? 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## 🚀 Feature request 11 | 12 | ### Problem description 13 | 14 | Describe what you'd like to be able to accomplish. Including a rough sketch or screenshot can be helpful. 15 | 16 | ### Suggested solution (optional) 17 | 18 | Suggest a solution that would solve the problem you have. For example, this may involve adding a new prop, changing an existing prop, or adding a new exported component. 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | 4 | # distribution folder 5 | /dist 6 | 7 | # coverage 8 | /coverage 9 | 10 | # rollup-plugin-typescript2 11 | .rpt2_cache 12 | 13 | .DS_Store 14 | 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | .idea 19 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | demo 3 | src 4 | assets 5 | test 6 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "printWidth": 100, 4 | "singleQuote": true, 5 | "trailingComma": "all" 6 | } 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | - lts/* 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.2.0 (2025-02-23) 2 | 3 | - Support all major versions of react 4 | 5 | ## 2.1.0 (2022-06-26) 6 | 7 | - Support React 18.x in package.json 8 | 9 | ## 2.0.4 (2021-03-26) 10 | 11 | - Support React 17.x in package.json 12 | 13 | ## 2.0.3 14 | 15 | - Remove other synthetic react imports (i.e. "import React from 'React'") [#124] 16 | - Upgrade React to 16.9.0 in /demo [#113] 17 | 18 | ## 2.0.2 19 | 20 | - Import react with "\* as React" to prevent the need to use allowSyntheticDefaultExports/esModuleInterop in consumers (issue #110) [#112] 21 | 22 | ## 2.0.1 23 | 24 | - Fix vertical centering of CircularProgressbarWithChildren [#96] 25 | 26 | ## 2.0.0 27 | 28 | - Add buildStyles utility, and make CircularProgressbar a named import [#86] 29 | - Add wrapper component [#87] 30 | - Remove initialAnimation prop in favor of percentage being controlled externally [#88] 31 | - Replace props.percentage with props.value, and add minValue and maxValue [#89] 32 | - Update docs for v2.0.0 [#90] 33 | 34 | ## 1.2.1 35 | 36 | - Use Rollup to build package [#83] 37 | - Extract Path component into separate file [#84] 38 | 39 | ## 1.2.0 40 | 41 | - Add props.circleRatio to enable partial diameter "car speedometer" style [#80] 42 | 43 | ## 1.1.0 44 | 45 | - Convert project to Typescript and improve demo setup [#77] 46 | - Remove prop-types dependency [#78] 47 | 48 | 1.1.0 now uses Typescript! 49 | 50 | There should not be any breaking changes to the public JS interface. However, the slight discrepancy in typing may cause type errors when switching from 1.0 using DefinitelyTyped. Runtime prop-types checking is also now removed in [#78]. 51 | 52 | ## 1.0.0 53 | 54 | We're at 1.0.0! 🎉 Thank you to all the contributors and issue creators. 55 | 56 | - Add text prop and remove textForPercentage and classForPercentage props [#61] 57 | 58 | ## 0.8.1 59 | 60 | - Use styles.root style hook properly [#60] 61 | 62 | ## 0.8.0 63 | 64 | - Check in build files to `/dist` and enable importing styles from `dist/styles.css` [#40][#45] 65 | 66 | ## 0.7.0 67 | 68 | - Add `styles` prop for customizing inline styles [#42] 69 | 70 | ## 0.6.0 71 | 72 | - Add `counterClockwise` prop for having progressbar go in opposite direction [#39] 73 | 74 | ## 0.5.0 75 | 76 | - Add `classes` prop for customizing svg element classNames [#25] 77 | 78 | ## 0.4.1 79 | 80 | - Don't render when textForPercentage is falsy 81 | 82 | ## 0.4.0 83 | 84 | - Add `background` prop, fix black circle issue for upgrading without new CSS 85 | 86 | ## 0.3.0 87 | 88 | - Support custom background colors and added `backgroundPadding` prop [#23] 89 | 90 | ## 0.2.2 91 | 92 | - Allow react 16 as a peerDependency 93 | 94 | ## 0.2.1 95 | 96 | - Restrict percentages to be between 0 and 100 97 | - Fix "undefined" className when it's unset 98 | 99 | ## 0.2.0 100 | 101 | - Adds `className` prop to customize component styles 102 | 103 | ## 0.1.5 104 | 105 | - Fixes 'calling PropTypes validators directly' issue caused by prop-types package by upgrading to 15.5.10+ 106 | 107 | ## 0.1.4 108 | 109 | - Fixes React PropTypes import warning by using prop-types package 110 | - Upgrades devDependencies 111 | 112 | ## 0.1.3 113 | 114 | - Fix errors when component is unmounted immediately [#1] 115 | 116 | ## 0.1.2 117 | 118 | - Tweak initialAnimation behavior 119 | - Fix package.json repo URL 120 | 121 | ## 0.1.1 122 | 123 | - Remove unused dependencies 124 | 125 | ## 0.1.0 126 | 127 | - Initial working version 128 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | Thanks for your interest in helping out! If you'd like to make a change to react-circular-progressbar master, it's preferable to first validate your change by writing what you plan to do in a new [Github issue](https://github.com/kevinsqi/react-circular-progressbar/issues). 4 | 5 | To make your own changes: 6 | 7 | 1. Fork the repo. 8 | 2. Go to `/demo` and view the README to see how to run the demo. 9 | 3. Make changes to `/src` and they'll be applied immediately to the running demo. You can use this to prototype, but you probably don't need to commit the changes to `/demo` when creating a PR. 10 | 4. Add tests for your new changes and make sure they're passing with `yarn test`. 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Kevin Qi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Circular Progressbar 2 | 3 | [![npm version](https://badge.fury.io/js/react-circular-progressbar.svg)](https://www.npmjs.com/package/react-circular-progressbar) 4 | [![Bundle size](https://img.shields.io/bundlephobia/min/react-circular-progressbar.svg)](https://bundlephobia.com/result?p=react-circular-progressbar) 5 | 6 | A circular progressbar component, built with SVG and extensively customizable. [**Try it out on CodeSandbox**](https://codesandbox.io/s/vymm4oln6y). 7 | 8 | animated progressbar progressbar examples 9 | 10 | ## Version 2.0.0 is out! 👋 11 | 12 | **New features:** 13 | 14 | - Use `import { CircularProgressbarWithChildren }` in order to [put arbitrary JSX inside the component](/README.md#adding-arbitrary-text-or-content-inside-the-progressbar). 15 | - Use `import { buildStyles }` to make it easier to [customize styles](/README.md#using-the-styles-prop). 16 | - Use `props.minValue` and `props.maxValue` to specify a range other than 0-100. 17 | 18 | **Breaking changes:** if you're upgrading from an older version, take a look at [UPGRADING.md](/UPGRADING.md) for instructions on how to migrate. 19 | 20 | Documentation for v1.x.x will still be available at [README_v1.md](/README_v1.md). 21 | 22 | ## Installation 23 | 24 | Install with yarn: 25 | 26 | ```bash 27 | yarn add react-circular-progressbar 28 | ``` 29 | 30 | or npm: 31 | 32 | ```bash 33 | npm install --save react-circular-progressbar 34 | ``` 35 | 36 | ## Usage 37 | 38 | Import the component and default styles: 39 | 40 | ```javascript 41 | import { CircularProgressbar } from 'react-circular-progressbar'; 42 | import 'react-circular-progressbar/dist/styles.css'; 43 | ``` 44 | 45 | **Note**: Importing CSS requires a CSS loader (if you're using create-react-app, this is already set up for you). If you don't have a CSS loader, you can copy [styles.css](src/styles.css) into your project instead. 46 | 47 | Now you can use the component: 48 | 49 | ```jsx 50 | const percentage = 66; 51 | 52 | ; 53 | ``` 54 | 55 | If your values are not in percentages, you can adjust `minValue` and `maxValue` to select the scale you want: 56 | 57 | ```jsx 58 | const value = 0.66; 59 | 60 | ; 61 | ``` 62 | 63 | The progressbar is designed to fill the width of its container. You can size the progressbar by sizing its container: 64 | 65 | ```jsx 66 |
67 | 68 |
69 | ``` 70 | 71 | This makes the progressbar work well with responsive designs and grid systems. 72 | 73 | 74 | ## Props 75 | 76 | [**Take a look at the CodeSandbox**](https://codesandbox.io/s/vymm4oln6y) for interactive examples on how to use these props. 77 | 78 | ℹ️ Version 1.0.0 removed the `classForPercentage` and `textForPercentage` props in favor of `className` and `text` props. Version 2.0.0 replaces `percentage` with `value` and removes the `initialAnimation` prop. Take a look at [UPGRADING.md](/UPGRADING.md) for instructions on how to migrate. 79 | 80 | | Name | Description | 81 | | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 82 | | `value` | Completion value of the progressbar, from `minValue` to `maxValue`. Required. | 83 | | `minValue` | Minimum value of the progressbar. Default: `0`. | 84 | | `maxValue` | Maximum value of the progressbar. Default: `100`. | 85 | | `className` | Classes to apply to the svg element. Default: `''`. | 86 | | `text` | Text to display inside progressbar. Default: `''`. | 87 | | `strokeWidth` | Width of circular line relative to total width of component, a value from 0-100. Default: `8`. | 88 | | `background` | Whether to display background color. Default: `false`. | 89 | | `backgroundPadding` | Padding between background circle and path/trail relative to total width of component. Only used if `background` is `true`. Default: `0`. | 90 | | `counterClockwise` | Whether to rotate progressbar in counterclockwise direction. Default: `false`. | 91 | | `circleRatio` | Number from 0-1 representing ratio of the full circle diameter the progressbar should use. Default: `1`. | 92 | | `classes` | Object allowing overrides of classNames of each svg subcomponent (root, trail, path, text, background). Enables styling with react-jss. See [this PR](https://github.com/kevinsqi/react-circular-progressbar/pull/25) for more detail. | 93 | | `styles` | Object allowing customization of styles of each svg subcomponent (root, trail, path, text, background). | 94 | 95 | ## Theming (customizing styles) 96 | 97 | Use CSS or inline styles to customize the styling - the default CSS is a good starting point, but you can override it as needed. 98 | 99 | #### Using the `styles` prop 100 | 101 | You can use the `styles` prop to customize each part of the progressbar (the root svg, path, trail, text, and background). This uses the native `style` prop for each subcomponent, so you can use any CSS properties here, not just the ones mentioned below. 102 | 103 | As a convenience, you can use `buildStyles` to configure the most common style changes: 104 | 105 | ```jsx 106 | import { CircularProgressbar, buildStyles } from 'react-circular-progressbar'; 107 | 108 | const percentage = 66; 109 | 110 | ; 136 | ``` 137 | 138 | `buildStyles` is a shorthand, but you can also build the `styles` object yourself. It's an object with `root`, `path`, `trail`, `text`, and `background` properties, which are each a set of inline styles to apply to the relevant SVG subcomponent. Here's the equivalent set of styles as above, without using `buildStyles`: 139 | 140 | ```jsx 141 | 182 | ``` 183 | 184 | However, you're not limited to the CSS properties shown above—you have the full set of SVG CSS properties available to you when you use `prop.styles`. 185 | 186 | See the [CodeSandbox examples](https://codesandbox.io/s/vymm4oln6y) for a live example on how to customize styles. 187 | 188 | #### Using CSS 189 | 190 | You can also customize styles with CSS. There are equivalent CSS hooks for the root, path, trail, text, and background of the progressbar. 191 | 192 | If you're importing the default styles, you can override the defaults like this: 193 | 194 | ```jsx 195 | import 'react-circular-progressbar/dist/styles.css'; 196 | import './custom.css'; 197 | ``` 198 | 199 | ```css 200 | // custom.css 201 | .CircularProgressbar-path { 202 | stroke: red; 203 | } 204 | .CircularProgressbar-trail { 205 | stroke: gray; 206 | } 207 | .CircularProgressbar-text { 208 | fill: yellow; 209 | } 210 | .CircularProgressbar-background { 211 | fill: green; 212 | } 213 | ``` 214 | 215 | ## Adding arbitrary text or content inside the progressbar 216 | 217 | If you want to add multiple lines of text or images within the progressbar, you can overlay it on top of a regular `` using absolute positioning. `react-circular-progressbar` ships with a `CircularProgressbarWithChildren` component which makes it easy to do that by using JSX children: 218 | 219 | ```jsx 220 | import { CircularProgressbarWithChildren } from 'react-circular-progressbar'; 221 | 222 | 223 | {/* Put any JSX content in here that you'd like. It'll be vertically and horizonally centered. */} 224 | doge 225 |
226 | 66% mate 227 |
228 |
; 229 | ``` 230 | 231 | CircularProgressbarWithChildren example 232 | 233 | `CircularProgressbarWithChildren` has all the same props as `CircularProgressbar` - you can use it the exact same way otherwise. 234 | 235 | ## Animating text 236 | 237 | If you want to animate the text as well as the path, you'll need to transition the `value` prop from one value to another using a third-party animation library like `react-move` and an easing library like `d3-ease`. 238 | 239 | You can use a render prop wrapper like **[AnimatedProgressProvider.js inside this Codesandbox](https://codesandbox.io/s/vymm4oln6y)** to help manage the transitioning value, and use it like this: 240 | 241 | ```jsx 242 | import { easeQuadInOut } from 'd3-ease'; 243 | 244 | 250 | {(value) => { 251 | const roundedValue = Math.round(value); 252 | return ( 253 | 260 | ); 261 | }} 262 | ; 263 | ``` 264 | 265 | ## Animating progressbar upon component mount or upon visible 266 | 267 | **Upon component mount** 268 | 269 | In order to trigger the default CSS animation on mount, you'll need to change `props.value` from 0 to your desired value with a `setTimeout` in `componentDidMount`. You can use a wrapper component to help manage this like [ProgressProvider.js in this Codesandbox](https://codesandbox.io/s/0zk372m7l). Then you can do: 270 | 271 | ```jsx 272 | 273 | {(value) => } 274 | 275 | ``` 276 | 277 | **Upon visible** 278 | 279 | To animate the progressbar only when it becomes visible (e.g. if it's below the fold), you can use something like `react-visibility-sensor` which detects whether the component is visible or not. [Here's a Codesandbox example](https://codesandbox.io/s/81wzmm8n00). 280 | 281 | ## Fixing text centering in Internet Explorer (IE) 282 | 283 | Because the `dominant-baseline` CSS property does not work in IE, the text may not be centered in IE. 284 | 285 | The **recommended way to fix this** is to instead of using `props.text`, use `CircularProgressbarWithChildren` and put your text in `props.children`, [as described here](/README.md#adding-arbitrary-text-or-content-inside-the-progressbar). 286 | 287 | However, you can also work around this by setting the `text` prop to be a `` element and then adjusting the `dy` vertical offset, like so: 288 | 289 | ```jsx 290 | // Use feature or browser detection to determine if IE 291 | const needDominantBaselineFix = ... 292 | 293 | {percentage}} 296 | /> 297 | ``` 298 | 299 | [See this Codesandbox example](https://codesandbox.io/s/x8o1zx7j4) to see this in action. 300 | 301 | ## Advanced usage 302 | 303 | - [Applying a gradient to the progressbar](https://github.com/kevinsqi/react-circular-progressbar/issues/31#issuecomment-338216925) 304 | - [Creating a dashboard/speedometer style progressbar](https://github.com/kevinsqi/react-circular-progressbar/issues/49) 305 | 306 | ## Supported platforms 307 | 308 | react-circular-progressbar does not work with React Native, because React Native does not support `` out of the box. 309 | 310 | ## Contributing 311 | 312 | Take a look at [CONTRIBUTING.md](/CONTRIBUTING.md) to see how to help contribute to react-circular-progressbar. 313 | 314 | ## License 315 | 316 | [MIT](/LICENSE) 317 | -------------------------------------------------------------------------------- /README_v1.md: -------------------------------------------------------------------------------- 1 | # React Circular Progressbar 2 | 3 | [![npm version](https://badge.fury.io/js/react-circular-progressbar.svg)](https://www.npmjs.com/package/react-circular-progressbar) 4 | [![Build Status](https://travis-ci.org/kevinsqi/react-circular-progressbar.svg?branch=master)](https://travis-ci.org/kevinsqi/react-circular-progressbar) 5 | [![Bundle size](https://img.shields.io/bundlephobia/min/react-circular-progressbar.svg)](https://bundlephobia.com/result?p=react-circular-progressbar) 6 | 7 | A circular progressbar component, built with SVG and extensively customizable. [**Try it out on CodeSandbox**](https://codesandbox.io/s/3023r61611). 8 | 9 | animated progressbar progressbar examples 10 | 11 | ## Installation 12 | 13 | Install with yarn: 14 | 15 | ```bash 16 | yarn add react-circular-progressbar 17 | ``` 18 | 19 | or npm: 20 | 21 | ```bash 22 | npm install --save react-circular-progressbar 23 | ``` 24 | 25 | ## Usage 26 | 27 | Import the component: 28 | 29 | ```javascript 30 | import CircularProgressbar from 'react-circular-progressbar'; 31 | ``` 32 | 33 | If you have a CSS loader configured, you can import the stylesheet: 34 | 35 | ```javascript 36 | import 'react-circular-progressbar/dist/styles.css'; 37 | ``` 38 | 39 | If not, you can copy [styles.css](src/styles.css) into your project instead, and include `` in your ``. 40 | 41 | Now you can use the component: 42 | 43 | ```jsx 44 | const percentage = 66; 45 | 46 | ; 47 | ``` 48 | 49 | ## Props 50 | 51 | [**Take a look at the CodeSandbox**](https://codesandbox.io/s/3023r61611) for interactive examples on how to use these props. 52 | 53 | | Name | Description | 54 | | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 55 | | `percentage` | Numeric percentage to display, from 0-100. Required. | 56 | | `className` | Classes to apply to the svg element. Default: `''`. | 57 | | `text` | Text to display inside progressbar. Default: `null`. | 58 | | `strokeWidth` | Width of circular line as a percentage relative to total width of component. Default: `8`. | 59 | | `background` | Whether to display background color. Default: `false`. | 60 | | `backgroundPadding` | Padding between background and edge of svg as a percentage relative to total width of component. Default: `null`. | 61 | | `initialAnimation` | Toggle whether to animate progress starting from 0% on initial mount. Default: `false`. | 62 | | `counterClockwise` | Toggle whether to rotate progressbar in counterclockwise direction. Default: `false`. | 63 | | `circleRatio` | Number from 0-1 representing ratio of the full circle diameter the progressbar should use. Default: `1`. | 64 | | `classes` | Object allowing overrides of classNames of each svg subcomponent (root, trail, path, text, background). Enables styling with react-jss. See [this PR](https://github.com/kevinsqi/react-circular-progressbar/pull/25) for more detail. | 65 | | `styles` | Object allowing customization of styles of each svg subcomponent (root, trail, path, text, background). | 66 | 67 | Version 1.0.0 removed the `classForPercentage` and `textForPercentage` props in favor of the newer `className` and `text` props. Take a look at the [migration guide](/UPGRADING.md) for instructions on how to migrate. 68 | 69 | ## Theming (customizing styles) 70 | 71 | Use CSS or inline styles to customize the styling - the default CSS is a good starting point, but you can override it as needed. 72 | 73 | #### Using the `styles` prop 74 | 75 | You can use the `styles` prop to customize the inline styles of each subcomponent of the progressbar (the root svg, path, trail, text, and background). This uses the native `style` prop for each subcomponent, so you can use any CSS properties here, not just the ones mentioned below. 76 | 77 | ```jsx 78 | 111 | ``` 112 | 113 | See the [CodeSandbox examples](https://codesandbox.io/s/3023r61611) for a live example on how to customize styles. 114 | 115 | #### Using CSS 116 | 117 | You can also customize styles with CSS. There are equivalent CSS hooks for the root, path, trail, text, and background of the progressbar. 118 | 119 | If you're importing the default styles, you can override the defaults like this: 120 | 121 | ```jsx 122 | import 'react-circular-progressbar/dist/styles.css'; 123 | import './custom.css'; 124 | ``` 125 | 126 | ```css 127 | // custom.css 128 | .CircularProgressbar-path { 129 | stroke: red; 130 | } 131 | .CircularProgressbar-trail { 132 | stroke: gray; 133 | } 134 | .CircularProgressbar-text { 135 | fill: yellow; 136 | } 137 | .CircularProgressbar-background { 138 | fill: green; 139 | } 140 | ``` 141 | 142 | ## Customizing the text/content inside progressbar 143 | 144 | If you want to add images or multiple lines of text within the progressbar, the suggested approach is to overlay it on top of a regular `` using absolute positioning - this gives you the ability to put arbitrary HTML content in there. You can create your own wrapper component to make this easy to work with. 145 | 146 | [**Here's a Codesandbox demo**](https://codesandbox.io/s/qlr7w0rm29) 147 | 148 | ## Customizing animation and animating text 149 | 150 | You can adjust the animation characteristics using CSS or the `styles` prop: 151 | 152 | ``` 153 | 160 | ``` 161 | 162 | [See this Codesandbox example](https://codesandbox.io/s/x29rxrr4kw) to see how the transition can be customized. 163 | 164 | If you want to animate the text as well, you can! You'll instead control the `percentage` prop using a third-party animation library, like react-move. [**See a Codesandbox example here on how to do that**](https://codesandbox.io/s/m5xq9ozo3j). 165 | 166 | ## Fixing text centering in Internet Explorer (IE) 167 | 168 | Because the `dominant-baseline` CSS property does not work in IE, the percentage text may not be centered. 169 | 170 | A solid cross-browser way to fix this is to use [this approach for overlaying arbitrary content inside the progressbar](https://github.com/kevinsqi/react-circular-progressbar#customizing-the-textcontent-inside-progressbar). 171 | 172 | However, if you don't want to do that, you can also work around this by setting the `text` prop to be a `` element and then adjusting the `dy` vertical offset, like so: 173 | 174 | ```jsx 175 | // Use feature or browser detection to determine if IE 176 | const needDominantBaselineFix = ... 177 | 178 | {percentage}} 181 | /> 182 | ``` 183 | 184 | [See this Codesandbox example](https://codesandbox.io/s/x8o1zx7j4) to see this in action. 185 | 186 | ## Advanced usage 187 | 188 | - [Delaying the animation until the progressbar is visible](https://github.com/kevinsqi/react-circular-progressbar/issues/64) 189 | - [Using a different value range than 0-100](https://codesandbox.io/s/6z64omwv3n) 190 | - [Rotating the progressbar by some degree](https://github.com/kevinsqi/react-circular-progressbar/issues/38) 191 | - [Applying a gradient to the progressbar](https://github.com/kevinsqi/react-circular-progressbar/issues/31#issuecomment-338216925) 192 | - [Customizing the background](https://github.com/kevinsqi/react-circular-progressbar/issues/21#issuecomment-336613160) 193 | - [Creating a countdown timer](https://github.com/kevinsqi/react-circular-progressbar/issues/52) 194 | - [Creating a dashboard/speedometer style progressbar](https://github.com/kevinsqi/react-circular-progressbar/issues/49) 195 | 196 | ## Supported platforms 197 | 198 | react-circular-progressbar does not work with React Native, because React Native does not support `` out of the box. 199 | 200 | ## Contributing 201 | 202 | Take a look at [CONTRIBUTING.md](/CONTRIBUTING.md) to see how to help contribute to react-circular-progressbar. 203 | 204 | ## License 205 | 206 | [MIT](/LICENSE) 207 | -------------------------------------------------------------------------------- /UPGRADING.md: -------------------------------------------------------------------------------- 1 | # Migration guide 2 | 3 | ## 1.x.x to 2.0.0 4 | 5 | **Use a named import to import `CircularProgressbar`** 6 | 7 | Before: 8 | 9 | ```jsx 10 | import CircularProgressbar from 'react-circular-progressbar'; 11 | ``` 12 | 13 | After: 14 | 15 | ```jsx 16 | import { CircularProgressbar } from 'react-circular-progressbar'; 17 | ``` 18 | 19 | **Use `props.value` instead of `props.percentage`** 20 | 21 | Before: 22 | 23 | ```jsx 24 | 25 | ``` 26 | 27 | After: 28 | 29 | ```jsx 30 | 31 | ``` 32 | 33 | **Replace `props.initialAnimation` with a setTimeout value transition** 34 | 35 | This is only applicable if you're using `props.initialAnimation`, which is removed in v2.0.0. Instead, you must trigger the animation by changing `value` from one value to another yourself. 36 | 37 | Before: 38 | 39 | ```jsx 40 | 41 | ``` 42 | 43 | After: 44 | 45 | In order to trigger the default CSS animation on mount, you'll need to change `props.value` from 0 to your desired value with a `setTimeout` in `componentDidMount`. You can use a wrapper component to help manage this like [ProgressProvider.js in this Codesandbox](https://codesandbox.io/s/0zk372m7l). Then you can do: 46 | 47 | ```jsx 48 | 49 | {(value) => } 50 | 51 | ``` 52 | 53 | ## 0.x.x to 1.0.0 54 | 55 | The main breaking change is the replacement of the `classForPercentage` prop with `className`, and the `textForPercentage` prop with `text` [#61]. 56 | 57 | **How to migrate**: 58 | 59 | Previously, the text would by default display as "{yourPercentage}%". With 1.0, if you want to display that text, you need to supply the `text` prop explicitly: 60 | 61 | ```jsx 62 | const percentage = 60; 63 | 64 | ; 65 | ``` 66 | 67 | If you had customized either `classForPercentage` or `textForPercentage` functions before 1.0, you can reuse the same function for `className` and `text`. But instead of passing the function as a prop, you now need to pass the called function's value. 68 | 69 | So if your pre-1.0 usage looked like this: 70 | 71 | ```jsx 72 | function customClassForPercentage(percentage) { 73 | if (percentage < 50) { 74 | return 'red'; 75 | } else { 76 | return 'blue'; 77 | } 78 | } 79 | 80 | function customTextForPercentage(percentage) { 81 | if (percentage === 100) { 82 | return `${percentage}!!!`; 83 | } else { 84 | return `${percentage}%`; 85 | } 86 | } 87 | 88 | const percentage = 60; 89 | 90 | ; 95 | ``` 96 | 97 | ...you can make a small change to make it work in 1.0 by using the new props and calling the functions instead: 98 | 99 | ```jsx 100 | 105 | ``` 106 | -------------------------------------------------------------------------------- /demo/.env: -------------------------------------------------------------------------------- 1 | # This is used to skip the error where 2 | # e.g. babel-jest is a sub-dependency in the parent dir 3 | # but is also used by react-scripts: 4 | # "There might be a problem with the project dependency tree" 5 | SKIP_PREFLIGHT_CHECK=true 6 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # react-circular-progressbar demo site 2 | 3 | ## Developing 4 | 5 | In the parent react-circular-progressbar directory, run: 6 | 7 | ``` 8 | yarn link 9 | yarn start 10 | ``` 11 | 12 | In this repo, run: 13 | 14 | ``` 15 | yarn link react-circular-progressbar 16 | yarn start 17 | ``` 18 | 19 | The demo site will be running at [localhost:3000](http://localhost:3000). Now, any changes that are made to react-circular-progressbar source will be live-reloaded on the demo site. 20 | 21 | ## Upgrading version 22 | 23 | ``` 24 | yarn upgrade react-circular-progressbar@../ 25 | ``` 26 | 27 | ## Deploying 28 | 29 | This site is hosted on github pages at https://www.kevinqi.com/react-circular-progressbar. Deploy new updates by running in this directory: 30 | 31 | ``` 32 | yarn run deploy 33 | ``` 34 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "homepage": "https://www.kevinqi.com/react-circular-progressbar", 4 | "version": "0.1.0", 5 | "private": true, 6 | "dependencies": { 7 | "@types/classnames": "^2.2.7", 8 | "@types/d3-ease": "^1.0.8", 9 | "@types/jest": "24.0.11", 10 | "@types/node": "11.13.6", 11 | "@types/react": "^18.0.0", 12 | "@types/react-dom": "^18.0.0", 13 | "classnames": "^2.2.6", 14 | "d3-ease": "^1.0.5", 15 | "gh-pages": "^2.0.1", 16 | "lodash": "^4.17.15", 17 | "react": "^18.0.0", 18 | "react-circular-progressbar": "../", 19 | "react-dom": "^18.0.0", 20 | "react-move": "^5.2.1", 21 | "react-scripts": "^5.0.0", 22 | "typescript": "3.4.4" 23 | }, 24 | "scripts": { 25 | "predeploy": "npm run build", 26 | "deploy": "gh-pages -d build", 27 | "start": "react-scripts start", 28 | "build": "react-scripts build", 29 | "test": "react-scripts test", 30 | "eject": "react-scripts eject" 31 | }, 32 | "eslintConfig": { 33 | "extends": "react-app" 34 | }, 35 | "browserslist": [ 36 | ">0.2%", 37 | "not dead", 38 | "not ie <= 11", 39 | "not op_mini all" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /demo/public/codesandbox/base-examples.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | 4 | // Import react-circular-progressbar module and styles 5 | import { 6 | CircularProgressbar, 7 | CircularProgressbarWithChildren, 8 | buildStyles, 9 | } from 'react-circular-progressbar'; 10 | import 'react-circular-progressbar/dist/styles.css'; 11 | 12 | // Animation 13 | import { easeQuadInOut } from 'd3-ease'; 14 | import AnimatedProgressProvider from './AnimatedProgressProvider'; 15 | import ChangingProgressProvider from './ChangingProgressProvider'; 16 | 17 | // Radial separators 18 | import RadialSeparators from './RadialSeparators'; 19 | 20 | const percentage = 66; 21 | 22 | const App = () => ( 23 |
24 |

react-circular-progressbar examples

25 |

26 | 27 | View Github docs 28 | 29 |

30 | 31 |

Common style customizations

32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 46 | 47 | 48 | 57 | 58 | 59 | 68 | 69 | 70 | 77 | 78 | 79 |

Animation

80 | 81 | 82 | {(percentage) => } 83 | 84 | 85 | 86 | 87 | {(percentage) => ( 88 | 95 | )} 96 | 97 | 98 | 99 | 100 | {(percentage) => ( 101 | 108 | )} 109 | 110 | 111 | 112 | 119 | {(value) => { 120 | const roundedValue = Math.round(value); 121 | return ( 122 | 129 | ); 130 | }} 131 | 132 | 133 | 134 |

Other use cases

135 | 136 | 137 | {/* Put any JSX content in here that you'd like. It'll be vertically and horizonally centered. */} 138 | doge 143 |
144 | 66% mate 145 |
146 |
147 |
148 | 149 | 157 | {/* Foreground path */} 158 | 165 | 166 | 167 | 168 | 176 | {/* 177 | Width here needs to be (100 - 2 * strokeWidth)% 178 | in order to fit exactly inside the outer progressbar. 179 | */} 180 |
181 | 187 |
188 |
189 |
190 | 191 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 216 | 217 | 218 | 226 | 235 | 236 | 237 | 238 | 239 | {(value) => ( 240 | 250 | )} 251 | 252 | 253 |
254 | ); 255 | 256 | function Example(props) { 257 | return ( 258 |
259 |
260 |
261 |
{props.children}
262 |
263 |

{props.label}

264 |

{props.description}

265 |
266 |
267 |
268 | ); 269 | } 270 | 271 | render(, document.getElementById('root')); 272 | -------------------------------------------------------------------------------- /demo/public/favicon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinsqi/react-circular-progressbar/48568ff0eef553182eef57a7c9f9e0b9486ff170/demo/public/favicon-256x256.png -------------------------------------------------------------------------------- /demo/public/images/CircularProgressbarWithChildren.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinsqi/react-circular-progressbar/48568ff0eef553182eef57a7c9f9e0b9486ff170/demo/public/images/CircularProgressbarWithChildren.png -------------------------------------------------------------------------------- /demo/public/images/animated-progressbar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinsqi/react-circular-progressbar/48568ff0eef553182eef57a7c9f9e0b9486ff170/demo/public/images/animated-progressbar.gif -------------------------------------------------------------------------------- /demo/public/images/circular-progressbar-examples.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinsqi/react-circular-progressbar/48568ff0eef553182eef57a7c9f9e0b9486ff170/demo/public/images/circular-progressbar-examples.png -------------------------------------------------------------------------------- /demo/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | react-circular-progressbar: a circular progress indicator component 23 | 24 | 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /demo/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /demo/src/AnimatedProgressProvider.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Animate } from 'react-move'; 3 | 4 | type Props = { 5 | duration: number; 6 | easingFunction: Function; 7 | valueStart: number; 8 | valueEnd: number; 9 | children: (value: number) => React.ReactElement; 10 | }; 11 | 12 | type State = { 13 | isAnimated: boolean; 14 | }; 15 | 16 | class AnimatedProgressProvider extends React.Component { 17 | state = { 18 | isAnimated: false, 19 | }; 20 | 21 | static defaultProps = { 22 | valueStart: 0, 23 | }; 24 | 25 | componentDidMount() { 26 | this.setState({ 27 | isAnimated: true, 28 | }); 29 | } 30 | 31 | render() { 32 | return ( 33 | ({ 35 | value: this.props.valueStart, 36 | })} 37 | update={() => ({ 38 | value: [this.state.isAnimated ? this.props.valueEnd : this.props.valueStart], 39 | timing: { 40 | duration: this.props.duration * 1000, 41 | ease: this.props.easingFunction, 42 | }, 43 | })} 44 | > 45 | {({ value }) => this.props.children(value)} 46 | 47 | ); 48 | } 49 | } 50 | 51 | export default AnimatedProgressProvider; 52 | -------------------------------------------------------------------------------- /demo/src/App.css: -------------------------------------------------------------------------------- 1 | .bg-yellow { 2 | background-color: #f8e8d5; 3 | } 4 | 5 | /* bootstrap overrides */ 6 | a { 7 | color: #3e98c7; 8 | } 9 | a:hover { 10 | text-decoration: none; 11 | } 12 | 13 | /* demo style overrides */ 14 | .CircularProgressbar.incomplete .CircularProgressbar-path { 15 | stroke: #f66; 16 | } 17 | .CircularProgressbar.incomplete .CircularProgressbar-text { 18 | fill: #f66; 19 | } 20 | .CircularProgressbar.complete .CircularProgressbar-path { 21 | stroke: #99f; 22 | } 23 | .CircularProgressbar.complete .CircularProgressbar-text { 24 | fill: #99f; 25 | } 26 | -------------------------------------------------------------------------------- /demo/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /demo/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Demo from './Demo'; 3 | 4 | // Stylesheets 5 | import 'react-circular-progressbar/dist/styles.css'; 6 | import './App.css'; 7 | 8 | class App extends Component { 9 | render() { 10 | return ( 11 |
12 | 13 |
14 | ); 15 | } 16 | } 17 | 18 | export default App; 19 | -------------------------------------------------------------------------------- /demo/src/ChangingProgressProvider.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type Props = { 4 | values: number[]; 5 | interval: number; 6 | children: (value: number) => React.ReactNode; 7 | }; 8 | 9 | type State = { 10 | valuesIndex: number; 11 | }; 12 | 13 | class ChangingProgressProvider extends React.Component { 14 | static defaultProps = { 15 | interval: 1000, 16 | }; 17 | 18 | state = { 19 | valuesIndex: 0, 20 | }; 21 | 22 | componentDidMount() { 23 | setInterval(() => { 24 | this.setState({ 25 | valuesIndex: (this.state.valuesIndex + 1) % this.props.values.length, 26 | }); 27 | }, this.props.interval); 28 | } 29 | 30 | render() { 31 | return this.props.children(this.props.values[this.state.valuesIndex]); 32 | } 33 | } 34 | 35 | export default ChangingProgressProvider; 36 | -------------------------------------------------------------------------------- /demo/src/Demo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | CircularProgressbar, 4 | CircularProgressbarWithChildren, 5 | buildStyles, 6 | } from 'react-circular-progressbar'; 7 | import classNames from 'classnames'; 8 | import { easeQuadInOut } from 'd3-ease'; 9 | 10 | // Custom progressbar wrappers 11 | import AnimatedProgressProvider from './AnimatedProgressProvider'; 12 | import ChangingProgressProvider from './ChangingProgressProvider'; 13 | import ProgressProvider from './ProgressProvider'; 14 | 15 | const GITHUB_URL = 'https://github.com/kevinsqi/react-circular-progressbar'; 16 | const CODESANDBOX_EXAMPLES_URL = 'https://codesandbox.io/s/vymm4oln6y'; 17 | 18 | const Code: React.FunctionComponent> = (props) => ( 19 | 20 | ); 21 | 22 | const Example: React.FunctionComponent<{ 23 | description: React.ReactNode; 24 | children: React.ReactNode; 25 | }> = ({ description, children }) => ( 26 |
27 |
28 |
{children}
29 |
30 |

{description}

31 |
32 | ); 33 | 34 | function Demo() { 35 | const [showAllExamples, setShowAllExamples] = React.useState(false); 36 | 37 | return ( 38 |
39 |
40 |
41 |
42 |

react-circular-progressbar

43 |

A circular progress indicator component

44 |
45 |
46 |
47 | 48 |
49 |
50 | 51 | {(value) => ( 52 | 59 | )} 60 | 61 |
62 |
63 | 64 |
65 |
66 |
67 |

Built with SVG and styled with plain CSS.

68 |
69 | 70 | 73 | Customize props.text, props.styles, and{' '} 74 | props.className based on value. 75 | 76 | } 77 | > 78 | 79 | {(value) => ( 80 | 85 | )} 86 | 87 | 88 | 89 | 92 | Customize props.strokeWidth and make it go counterclockwise with{' '} 93 | props.counterClockwise. 94 | 95 | } 96 | > 97 | 98 | {(value) => ( 99 | 109 | )} 110 | 111 | 112 | 113 | 116 | Use props.background to give it an inverted style. 117 | 118 | } 119 | > 120 | 140 | 141 | 142 | 145 | Use a library like react-move to ease props.value if you want to animate 146 | text. 147 | 148 | } 149 | > 150 | 156 | {(value) => { 157 | const roundedValue = Math.round(value); 158 | return ( 159 | 164 | ); 165 | }} 166 | 167 | 168 | 169 | 172 | Want that car speedometer look? Use props.circleRatio and CSS rotation. 173 | 174 | } 175 | > 176 | 177 | {(value) => ( 178 | 189 | )} 190 | 191 | 192 | 193 | 196 | Need custom content? Use CircularProgressbarWithChildren to add arbitrary 197 | centered HTML. 198 | 199 | } 200 | > 201 | 202 | doge 207 |
208 | 66% mate 209 |
210 |
211 |
212 | 213 | {showAllExamples ? ( 214 | 215 | 218 | circleRatio  219 | counterClockwise  220 | background 221 | 222 | } 223 | > 224 | 236 | 237 | 238 | ) : ( 239 |
240 | 246 |
247 | )} 248 | 249 | 254 |
255 | 256 |
257 |
258 |

Installation

259 |
260 |

Install with yarn or npm:

261 |

262 | yarn add react-circular-progressbar 263 |

264 | 265 | View docs on Github 266 | 267 |
268 |
269 |
270 | Built by @kevinsqi 271 |
272 |
273 |
274 |
275 | ); 276 | } 277 | 278 | export default Demo; 279 | -------------------------------------------------------------------------------- /demo/src/ProgressProvider.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type Props = { 4 | valueStart: number; 5 | valueEnd: number; 6 | children: (value: number) => React.ReactNode; 7 | }; 8 | 9 | type State = { 10 | value: number; 11 | }; 12 | 13 | class ProgressProvider extends React.Component { 14 | timeout: number | undefined = undefined; 15 | 16 | state = { 17 | value: this.props.valueStart, 18 | }; 19 | 20 | static defaultProps = { 21 | valueStart: 0, 22 | }; 23 | 24 | componentDidMount() { 25 | this.timeout = window.setTimeout(() => { 26 | this.setState({ 27 | value: this.props.valueEnd, 28 | }); 29 | }, 0); 30 | } 31 | 32 | componentWillUnmount() { 33 | window.clearTimeout(this.timeout); 34 | } 35 | 36 | render() { 37 | return this.props.children(this.state.value); 38 | } 39 | } 40 | 41 | export default ProgressProvider; 42 | -------------------------------------------------------------------------------- /demo/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 5 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 13 | monospace; 14 | } 15 | -------------------------------------------------------------------------------- /demo/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: https://bit.ly/CRA-PWA 12 | serviceWorker.unregister(); 13 | -------------------------------------------------------------------------------- /demo/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /demo/src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | type Config = { 24 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 25 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 26 | }; 27 | 28 | export function register(config?: Config) { 29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL( 32 | (process as { env: { [key: string]: string } }).env.PUBLIC_URL, 33 | window.location.href 34 | ); 35 | if (publicUrl.origin !== window.location.origin) { 36 | // Our service worker won't work if PUBLIC_URL is on a different origin 37 | // from what our page is served on. This might happen if a CDN is used to 38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 39 | return; 40 | } 41 | 42 | window.addEventListener('load', () => { 43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 44 | 45 | if (isLocalhost) { 46 | // This is running on localhost. Let's check if a service worker still exists or not. 47 | checkValidServiceWorker(swUrl, config); 48 | 49 | // Add some additional logging to localhost, pointing developers to the 50 | // service worker/PWA documentation. 51 | navigator.serviceWorker.ready.then(() => { 52 | console.log( 53 | 'This web app is being served cache-first by a service ' + 54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 55 | ); 56 | }); 57 | } else { 58 | // Is not localhost. Just register service worker 59 | registerValidSW(swUrl, config); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | function registerValidSW(swUrl: string, config?: Config) { 66 | navigator.serviceWorker 67 | .register(swUrl) 68 | .then(registration => { 69 | registration.onupdatefound = () => { 70 | const installingWorker = registration.installing; 71 | if (installingWorker == null) { 72 | return; 73 | } 74 | installingWorker.onstatechange = () => { 75 | if (installingWorker.state === 'installed') { 76 | if (navigator.serviceWorker.controller) { 77 | // At this point, the updated precached content has been fetched, 78 | // but the previous service worker will still serve the older 79 | // content until all client tabs are closed. 80 | console.log( 81 | 'New content is available and will be used when all ' + 82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 83 | ); 84 | 85 | // Execute callback 86 | if (config && config.onUpdate) { 87 | config.onUpdate(registration); 88 | } 89 | } else { 90 | // At this point, everything has been precached. 91 | // It's the perfect time to display a 92 | // "Content is cached for offline use." message. 93 | console.log('Content is cached for offline use.'); 94 | 95 | // Execute callback 96 | if (config && config.onSuccess) { 97 | config.onSuccess(registration); 98 | } 99 | } 100 | } 101 | }; 102 | }; 103 | }) 104 | .catch(error => { 105 | console.error('Error during service worker registration:', error); 106 | }); 107 | } 108 | 109 | function checkValidServiceWorker(swUrl: string, config?: Config) { 110 | // Check if the service worker can be found. If it can't reload the page. 111 | fetch(swUrl) 112 | .then(response => { 113 | // Ensure service worker exists, and that we really are getting a JS file. 114 | const contentType = response.headers.get('content-type'); 115 | if ( 116 | response.status === 404 || 117 | (contentType != null && contentType.indexOf('javascript') === -1) 118 | ) { 119 | // No service worker found. Probably a different app. Reload the page. 120 | navigator.serviceWorker.ready.then(registration => { 121 | registration.unregister().then(() => { 122 | window.location.reload(); 123 | }); 124 | }); 125 | } else { 126 | // Service worker found. Proceed as normal. 127 | registerValidSW(swUrl, config); 128 | } 129 | }) 130 | .catch(() => { 131 | console.log( 132 | 'No internet connection found. App is running in offline mode.' 133 | ); 134 | }); 135 | } 136 | 137 | export function unregister() { 138 | if ('serviceWorker' in navigator) { 139 | navigator.serviceWorker.ready.then(registration => { 140 | registration.unregister(); 141 | }); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "preserve" 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "rootDir": "test", 3 | "transform": { 4 | "^.+\\.(t|j)sx?$": "ts-jest" 5 | }, 6 | "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", 7 | "moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"], 8 | "coverageDirectory": "/../coverage", 9 | "setupFilesAfterEnv": ["/setupTests.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-circular-progressbar", 3 | "version": "2.2.0", 4 | "description": "A circular progress indicator component", 5 | "author": "Kevin Qi ", 6 | "main": "dist/index.js", 7 | "module": "dist/index.esm.js", 8 | "types": "dist/index.d.ts", 9 | "style": "dist/styles.css", 10 | "files": [ 11 | "dist" 12 | ], 13 | "repository": "https://github.com/kevinsqi/react-circular-progressbar.git", 14 | "license": "MIT", 15 | "keywords": [ 16 | "progressbar", 17 | "react", 18 | "react-component", 19 | "svg" 20 | ], 21 | "scripts": { 22 | "build": "npm-run-all clean build:css build:js", 23 | "build:css": "postcss src/styles.css --use autoprefixer -d dist/ --no-map", 24 | "build:js": "rollup -c", 25 | "clean": "rimraf dist", 26 | "format": "prettier --write 'src/**/*' 'demo/src/**/*'", 27 | "prepare": "npm-run-all clean build", 28 | "start": "npm-run-all --parallel start:css start:js", 29 | "start:css": "postcss src/styles.css --use autoprefixer -d dist/ --no-map --watch", 30 | "start:js": "rollup -c -w", 31 | "test": "jest --config jest.config.json --coverage" 32 | }, 33 | "devDependencies": { 34 | "@types/enzyme": "^3.9.1", 35 | "@types/enzyme-adapter-react-16": "^1.0.5", 36 | "@types/jest": "^25.0.0", 37 | "@types/react": "^16.8.14", 38 | "autoprefixer": "^9.5.1", 39 | "enzyme": "^3.9.0", 40 | "enzyme-adapter-react-16": "^1.12.1", 41 | "jest": "^25.0.0", 42 | "npm-run-all": "^4.1.5", 43 | "postcss-cli": "^6.1.2", 44 | "prettier": "^1.17.0", 45 | "react": "^16.8.6", 46 | "react-dom": "^16.8.6", 47 | "rimraf": "^2.3.4", 48 | "rollup": "^1.10.1", 49 | "rollup-plugin-typescript2": "^0.21.0", 50 | "ts-jest": "^25.0.0", 51 | "typescript": "^3.4.4" 52 | }, 53 | "dependencies": {}, 54 | "peerDependencies": { 55 | "react": ">=0.14.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2'; 2 | 3 | import pkg from './package.json'; 4 | 5 | export default [ 6 | { 7 | input: 'src/index.ts', 8 | external: [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.peerDependencies || {})], 9 | output: [ 10 | { 11 | file: pkg.main, 12 | format: 'cjs', 13 | sourcemap: true, 14 | }, 15 | { 16 | file: pkg.module, 17 | format: 'es', 18 | sourcemap: true, 19 | }, 20 | ], 21 | plugins: [ 22 | typescript({ 23 | typescript: require('typescript'), 24 | }), 25 | ], 26 | }, 27 | ]; 28 | -------------------------------------------------------------------------------- /src/CircularProgressbar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { 4 | VIEWBOX_WIDTH, 5 | VIEWBOX_HEIGHT, 6 | VIEWBOX_HEIGHT_HALF, 7 | VIEWBOX_CENTER_X, 8 | VIEWBOX_CENTER_Y, 9 | } from './constants'; 10 | import Path from './Path'; 11 | import { CircularProgressbarDefaultProps, CircularProgressbarProps } from './types'; 12 | 13 | class CircularProgressbar extends React.Component { 14 | static defaultProps: CircularProgressbarDefaultProps = { 15 | background: false, 16 | backgroundPadding: 0, 17 | circleRatio: 1, 18 | classes: { 19 | root: 'CircularProgressbar', 20 | trail: 'CircularProgressbar-trail', 21 | path: 'CircularProgressbar-path', 22 | text: 'CircularProgressbar-text', 23 | background: 'CircularProgressbar-background', 24 | }, 25 | counterClockwise: false, 26 | className: '', 27 | maxValue: 100, 28 | minValue: 0, 29 | strokeWidth: 8, 30 | styles: { 31 | root: {}, 32 | trail: {}, 33 | path: {}, 34 | text: {}, 35 | background: {}, 36 | }, 37 | text: '', 38 | }; 39 | 40 | getBackgroundPadding() { 41 | if (!this.props.background) { 42 | // Don't add padding if not displaying background 43 | return 0; 44 | } 45 | return this.props.backgroundPadding; 46 | } 47 | 48 | getPathRadius() { 49 | // The radius of the path is defined to be in the middle, so in order for the path to 50 | // fit perfectly inside the 100x100 viewBox, need to subtract half the strokeWidth 51 | return VIEWBOX_HEIGHT_HALF - this.props.strokeWidth / 2 - this.getBackgroundPadding(); 52 | } 53 | 54 | // Ratio of path length to trail length, as a value between 0 and 1 55 | getPathRatio() { 56 | const { value, minValue, maxValue } = this.props; 57 | const boundedValue = Math.min(Math.max(value, minValue), maxValue); 58 | return (boundedValue - minValue) / (maxValue - minValue); 59 | } 60 | 61 | render() { 62 | const { 63 | circleRatio, 64 | className, 65 | classes, 66 | counterClockwise, 67 | styles, 68 | strokeWidth, 69 | text, 70 | } = this.props; 71 | 72 | const pathRadius = this.getPathRadius(); 73 | const pathRatio = this.getPathRatio(); 74 | 75 | return ( 76 | 82 | {this.props.background ? ( 83 | 90 | ) : null} 91 | 92 | 100 | 101 | 109 | 110 | {text ? ( 111 | 117 | {text} 118 | 119 | ) : null} 120 | 121 | ); 122 | } 123 | } 124 | 125 | export default CircularProgressbar; 126 | -------------------------------------------------------------------------------- /src/CircularProgressbarWithChildren.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import CircularProgressbar from './CircularProgressbar'; 4 | import { CircularProgressbarWrapperProps } from './types'; 5 | 6 | type CircularProgressbarWithChildrenProps = CircularProgressbarWrapperProps & { 7 | children?: React.ReactNode; 8 | }; 9 | 10 | // This is a wrapper around CircularProgressbar that allows passing children, 11 | // which will be vertically and horizontally centered inside the progressbar automatically. 12 | function CircularProgressbarWithChildren(props: CircularProgressbarWithChildrenProps) { 13 | const { children, ...circularProgressbarProps } = props; 14 | 15 | return ( 16 |
17 | {/* Has an extra div wrapper because otherwise, adding content after 18 | this progressbar is spaced weirdly. */} 19 |
20 | {/* Progressbar is not positioned absolutely, so that it can establish 21 | intrinsic size for props.children's content. */} 22 | 23 | 24 | {/* Children are positioned absolutely, and height adapts to the 25 | progressbar's intrinsic size. It appears below the progressbar, 26 | but negative margin moves it back up. */} 27 | {props.children ? ( 28 |
41 | {props.children} 42 |
43 | ) : null} 44 |
45 |
46 | ); 47 | } 48 | 49 | export default CircularProgressbarWithChildren; 50 | -------------------------------------------------------------------------------- /src/Path.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { VIEWBOX_CENTER_X, VIEWBOX_CENTER_Y } from './constants'; 3 | 4 | function Path({ 5 | className, 6 | counterClockwise, 7 | dashRatio, 8 | pathRadius, 9 | strokeWidth, 10 | style, 11 | }: { 12 | className?: string; 13 | counterClockwise: boolean; 14 | dashRatio: number; 15 | pathRadius: number; 16 | strokeWidth: number; 17 | style?: object; 18 | }) { 19 | return ( 20 | 30 | ); 31 | } 32 | 33 | // SVG path description specifies how the path should be drawn 34 | function getPathDescription({ 35 | pathRadius, 36 | counterClockwise, 37 | }: { 38 | pathRadius: number; 39 | counterClockwise: boolean; 40 | }) { 41 | const radius = pathRadius; 42 | const rotation = counterClockwise ? 1 : 0; 43 | 44 | // Move to center of canvas 45 | // Relative move to top canvas 46 | // Relative arc to bottom of canvas 47 | // Relative arc to top of canvas 48 | return ` 49 | M ${VIEWBOX_CENTER_X},${VIEWBOX_CENTER_Y} 50 | m 0,-${radius} 51 | a ${radius},${radius} ${rotation} 1 1 0,${2 * radius} 52 | a ${radius},${radius} ${rotation} 1 1 0,-${2 * radius} 53 | `; 54 | } 55 | 56 | function getDashStyle({ 57 | counterClockwise, 58 | dashRatio, 59 | pathRadius, 60 | }: { 61 | counterClockwise: boolean; 62 | dashRatio: number; 63 | pathRadius: number; 64 | }) { 65 | const diameter = Math.PI * 2 * pathRadius; 66 | const gapLength = (1 - dashRatio) * diameter; 67 | 68 | return { 69 | // Have dash be full diameter, and gap be full diameter 70 | strokeDasharray: `${diameter}px ${diameter}px`, 71 | // Shift dash backward by gapLength, so gap starts appearing at correct distance 72 | strokeDashoffset: `${counterClockwise ? -gapLength : gapLength}px`, 73 | }; 74 | } 75 | 76 | export default Path; 77 | -------------------------------------------------------------------------------- /src/buildStyles.ts: -------------------------------------------------------------------------------- 1 | import { CircularProgressbarStyles } from './types'; 2 | 3 | export default function buildStyles({ 4 | rotation, 5 | strokeLinecap, 6 | textColor, 7 | textSize, 8 | pathColor, 9 | pathTransition, 10 | pathTransitionDuration, 11 | trailColor, 12 | backgroundColor, 13 | }: { 14 | rotation?: number; // Number of turns, 0-1 15 | strokeLinecap?: any; 16 | textColor?: string; 17 | textSize?: string | number; 18 | pathColor?: string; 19 | pathTransition?: string; 20 | pathTransitionDuration?: number; // Measured in seconds 21 | trailColor?: string; 22 | backgroundColor?: string; 23 | }): CircularProgressbarStyles { 24 | const rotationTransform = rotation == null ? undefined : `rotate(${rotation}turn)`; 25 | const rotationTransformOrigin = rotation == null ? undefined : 'center center'; 26 | 27 | return { 28 | root: {}, 29 | path: removeUndefinedValues({ 30 | stroke: pathColor, 31 | strokeLinecap: strokeLinecap, 32 | transform: rotationTransform, 33 | transformOrigin: rotationTransformOrigin, 34 | transition: pathTransition, 35 | transitionDuration: pathTransitionDuration == null ? undefined : `${pathTransitionDuration}s`, 36 | }), 37 | trail: removeUndefinedValues({ 38 | stroke: trailColor, 39 | strokeLinecap: strokeLinecap, 40 | transform: rotationTransform, 41 | transformOrigin: rotationTransformOrigin, 42 | }), 43 | text: removeUndefinedValues({ 44 | fill: textColor, 45 | fontSize: textSize, 46 | }), 47 | background: removeUndefinedValues({ 48 | fill: backgroundColor, 49 | }), 50 | }; 51 | } 52 | 53 | function removeUndefinedValues(obj: { [key: string]: any }) { 54 | Object.keys(obj).forEach((key: string) => { 55 | if (obj[key] == null) { 56 | delete obj[key]; 57 | } 58 | }); 59 | return obj; 60 | } 61 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const VIEWBOX_WIDTH = 100; 2 | export const VIEWBOX_HEIGHT = 100; 3 | export const VIEWBOX_HEIGHT_HALF = 50; 4 | export const VIEWBOX_CENTER_X = 50; 5 | export const VIEWBOX_CENTER_Y = 50; 6 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import CircularProgressbar from './CircularProgressbar'; 2 | import CircularProgressbarWithChildren from './CircularProgressbarWithChildren'; 3 | import buildStyles from './buildStyles'; 4 | 5 | export { CircularProgressbar, CircularProgressbarWithChildren, buildStyles }; 6 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /* 2 | * react-circular-progressbar styles 3 | * All of the styles in this file are configurable! 4 | */ 5 | 6 | .CircularProgressbar { 7 | /* 8 | * This fixes an issue where the CircularProgressbar svg has 9 | * 0 width inside a "display: flex" container, and thus not visible. 10 | */ 11 | width: 100%; 12 | /* 13 | * This fixes a centering issue with CircularProgressbarWithChildren: 14 | * https://github.com/kevinsqi/react-circular-progressbar/issues/94 15 | */ 16 | vertical-align: middle; 17 | } 18 | 19 | .CircularProgressbar .CircularProgressbar-path { 20 | stroke: #3e98c7; 21 | stroke-linecap: round; 22 | transition: stroke-dashoffset 0.5s ease 0s; 23 | } 24 | 25 | .CircularProgressbar .CircularProgressbar-trail { 26 | stroke: #d6d6d6; 27 | /* Used when trail is not full diameter, i.e. when props.circleRatio is set */ 28 | stroke-linecap: round; 29 | } 30 | 31 | .CircularProgressbar .CircularProgressbar-text { 32 | fill: #3e98c7; 33 | font-size: 20px; 34 | dominant-baseline: middle; 35 | text-anchor: middle; 36 | } 37 | 38 | .CircularProgressbar .CircularProgressbar-background { 39 | fill: #d6d6d6; 40 | } 41 | 42 | /* 43 | * Sample background styles. Use these with e.g.: 44 | * 45 | * 50 | */ 51 | .CircularProgressbar.CircularProgressbar-inverted .CircularProgressbar-background { 52 | fill: #3e98c7; 53 | } 54 | 55 | .CircularProgressbar.CircularProgressbar-inverted .CircularProgressbar-text { 56 | fill: #fff; 57 | } 58 | 59 | .CircularProgressbar.CircularProgressbar-inverted .CircularProgressbar-path { 60 | stroke: #fff; 61 | } 62 | 63 | .CircularProgressbar.CircularProgressbar-inverted .CircularProgressbar-trail { 64 | stroke: transparent; 65 | } 66 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export type CircularProgressbarStyles = { 4 | root?: React.CSSProperties; 5 | trail?: React.CSSProperties; 6 | path?: React.CSSProperties; 7 | text?: React.CSSProperties; 8 | background?: React.CSSProperties; 9 | }; 10 | 11 | export type CircularProgressbarDefaultProps = { 12 | background: boolean; 13 | backgroundPadding: number; 14 | circleRatio: number; 15 | classes: { 16 | root: string; 17 | trail: string; 18 | path: string; 19 | text: string; 20 | background: string; 21 | }; 22 | className: string; 23 | counterClockwise: boolean; 24 | maxValue: number; 25 | minValue: number; 26 | strokeWidth: number; 27 | styles: CircularProgressbarStyles; 28 | text: string; 29 | }; 30 | 31 | // These are used for any CircularProgressbar wrapper components that can safely 32 | // ignore default props. 33 | export type CircularProgressbarWrapperProps = { 34 | background?: boolean; 35 | backgroundPadding?: number; 36 | circleRatio?: number; 37 | classes?: { 38 | root: string; 39 | trail: string; 40 | path: string; 41 | text: string; 42 | background: string; 43 | }; 44 | className?: string; 45 | counterClockwise?: boolean; 46 | maxValue?: number; 47 | minValue?: number; 48 | strokeWidth?: number; 49 | styles?: CircularProgressbarStyles; 50 | text?: string; 51 | value: number; 52 | }; 53 | 54 | export type CircularProgressbarProps = CircularProgressbarDefaultProps & { 55 | value: number; 56 | }; 57 | -------------------------------------------------------------------------------- /test/CircularProgressbar.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { mount, shallow, ReactWrapper } from 'enzyme'; 3 | 4 | import { CircularProgressbar } from '../src/index'; 5 | 6 | function getExpectedStrokeDashoffset({ 7 | percentage, 8 | strokeWidth, 9 | }: { 10 | percentage: number; 11 | strokeWidth: number; 12 | }) { 13 | const radius = 50 - strokeWidth / 2; 14 | const diameter = 2 * radius * Math.PI; 15 | const expectedGapLength = (1 - percentage / 100) * diameter; 16 | return `${expectedGapLength}px`; 17 | } 18 | 19 | function expectPathPercentageToEqual( 20 | wrapper: ReactWrapper, 21 | percentage: number, 22 | ) { 23 | const strokeWidth = wrapper.props().strokeWidth; 24 | expect( 25 | wrapper 26 | .find('.CircularProgressbar-path') 27 | .hostNodes() 28 | .prop('style')!.strokeDashoffset, 29 | ).toEqual(getExpectedStrokeDashoffset({ percentage, strokeWidth })); 30 | } 31 | 32 | describe('', () => { 33 | test('SVG rendered to DOM', () => { 34 | const wrapper = shallow(); 35 | expect(wrapper.find('svg').exists()).toBe(true); 36 | }); 37 | describe('props.strokeWidth', () => { 38 | test('Applies to path', () => { 39 | const wrapper = shallow(); 40 | expect(wrapper.find('.CircularProgressbar-path').prop('strokeWidth')).toEqual(2); 41 | }); 42 | }); 43 | describe('props.className', () => { 44 | test('Applies to SVG', () => { 45 | const wrapper = shallow(); 46 | expect(wrapper.find('svg').prop('className')).toContain('my-custom-class'); 47 | }); 48 | }); 49 | describe('props.text', () => { 50 | test('Does not render when blank', () => { 51 | const wrapper = shallow(); 52 | expect(wrapper.find('.CircularProgressbar-text').exists()).toEqual(false); 53 | }); 54 | test('Renders the correct string', () => { 55 | const percentage = 50; 56 | const wrapper = shallow(); 57 | expect(wrapper.find('.CircularProgressbar-text').text()).toEqual('50%'); 58 | }); 59 | }); 60 | describe('props.value', () => { 61 | test('Renders correct path', () => { 62 | const percentage = 30; 63 | const wrapper = mount(); 64 | 65 | expectPathPercentageToEqual(wrapper, percentage); 66 | 67 | const expectedRadius = 50; 68 | const expectedArcto = `a ${expectedRadius},${expectedRadius}`; 69 | expect( 70 | wrapper 71 | .find('.CircularProgressbar-path') 72 | .hostNodes() 73 | .prop('d'), 74 | ).toContain(expectedArcto); 75 | }); 76 | test('Value is bounded by minValue', () => { 77 | const percentage = -30; 78 | const wrapper = mount(); 79 | 80 | expectPathPercentageToEqual(wrapper, 0); 81 | }); 82 | test('Value is bounded by maxValue', () => { 83 | const percentage = 130; 84 | const wrapper = mount(); 85 | 86 | expectPathPercentageToEqual(wrapper, 100); 87 | }); 88 | }); 89 | describe('props.minValue, props.maxValue', () => { 90 | test('Positive min/max', () => { 91 | const wrapper = mount(); 92 | 93 | expectPathPercentageToEqual(wrapper, 25); 94 | }); 95 | test('Negative to positive min/max', () => { 96 | const wrapper = mount(); 97 | 98 | expectPathPercentageToEqual(wrapper, 50); 99 | }); 100 | test('Negative min/max', () => { 101 | const wrapper = mount(); 102 | 103 | expectPathPercentageToEqual(wrapper, 50); 104 | }); 105 | }); 106 | describe('props.counterClockwise', () => { 107 | test('Reverses dashoffset', () => { 108 | const clockwise = mount(); 109 | const counterClockwise = mount(); 110 | 111 | // Counterclockwise should have the negative dashoffset of clockwise 112 | expect( 113 | `-${ 114 | clockwise 115 | .find('.CircularProgressbar-path') 116 | .hostNodes() 117 | .prop('style')!.strokeDashoffset 118 | }`, 119 | ).toEqual( 120 | counterClockwise 121 | .find('.CircularProgressbar-path') 122 | .hostNodes() 123 | .prop('style')!.strokeDashoffset, 124 | ); 125 | }); 126 | }); 127 | describe('props.circleRatio', () => { 128 | test('Default full diameter', () => { 129 | const percentage = 25; 130 | const wrapper = mount(); 131 | 132 | expectPathPercentageToEqual(wrapper, percentage); 133 | }); 134 | 135 | test('Correct path and trail lengths', () => { 136 | const percentage = 25; 137 | const circleRatio = 0.8; 138 | const wrapper = mount(); 139 | 140 | // Path offset should be scaled 141 | expectPathPercentageToEqual(wrapper, percentage * circleRatio); 142 | 143 | // Trail offset should be scaled 144 | expect( 145 | wrapper 146 | .find('.CircularProgressbar-trail') 147 | .hostNodes() 148 | .prop('style')!.strokeDashoffset, 149 | ).toEqual( 150 | getExpectedStrokeDashoffset({ 151 | percentage: 100 * circleRatio, 152 | strokeWidth: wrapper.props().strokeWidth, 153 | }), 154 | ); 155 | }); 156 | }); 157 | describe('props.styles', () => { 158 | test('Style customizations applied to all subcomponents', () => { 159 | const percentage = 50; 160 | const wrapper = shallow( 161 | , 173 | ); 174 | expect(wrapper.find('.CircularProgressbar').prop('style')!.stroke).toEqual('#000000'); 175 | expect(wrapper.find('.CircularProgressbar-trail').prop('style')!.stroke).toEqual('#111111'); 176 | expect(wrapper.find('.CircularProgressbar-path').prop('style')!.stroke).toEqual('#222222'); 177 | expect(wrapper.find('.CircularProgressbar-text').prop('style')!.stroke).toEqual('#333333'); 178 | expect(wrapper.find('.CircularProgressbar-background').prop('style')!.stroke).toEqual( 179 | '#444444', 180 | ); 181 | }); 182 | }); 183 | describe('props.background', () => { 184 | test('Background does not render when prop is false', () => { 185 | const wrapper = shallow(); 186 | expect(wrapper.find('.CircularProgressbar-background').exists()).toEqual(false); 187 | }); 188 | test('Renders a with correct radius', () => { 189 | const wrapper = shallow(); 190 | expect(wrapper.find('.CircularProgressbar-background').exists()).toBe(true); 191 | expect(wrapper.find('.CircularProgressbar-background').type()).toEqual('circle'); 192 | expect(wrapper.find('.CircularProgressbar-background').prop('r')).toEqual(50); 193 | }); 194 | }); 195 | describe('props.classes', () => { 196 | test('Has default values', () => { 197 | const wrapper = mount(); 198 | expect( 199 | wrapper 200 | .find('.CircularProgressbar') 201 | .hostNodes() 202 | .type(), 203 | ).toEqual('svg'); 204 | expect( 205 | wrapper 206 | .find('.CircularProgressbar-path') 207 | .hostNodes() 208 | .type(), 209 | ).toEqual('path'); 210 | expect( 211 | wrapper 212 | .find('.CircularProgressbar-trail') 213 | .hostNodes() 214 | .type(), 215 | ).toEqual('path'); 216 | expect( 217 | wrapper 218 | .find('.CircularProgressbar-text') 219 | .hostNodes() 220 | .type(), 221 | ).toEqual('text'); 222 | }); 223 | test('Prop overrides work', () => { 224 | const wrapper = mount( 225 | , 237 | ); 238 | 239 | // Assert default classes don't exist 240 | expect(wrapper.find('.CircularProgressbar').exists()).toBe(false); 241 | expect(wrapper.find('.CircularProgressbar-path').exists()).toBe(false); 242 | expect(wrapper.find('.CircularProgressbar-trail').exists()).toBe(false); 243 | expect(wrapper.find('.CircularProgressbar-text').exists()).toBe(false); 244 | expect(wrapper.find('.CircularProgressbar-background').exists()).toBe(false); 245 | 246 | // Assert override classes do exist 247 | expect( 248 | wrapper 249 | .find('.someRootClass') 250 | .hostNodes() 251 | .type(), 252 | ).toEqual('svg'); 253 | expect( 254 | wrapper 255 | .find('.somePathClass') 256 | .hostNodes() 257 | .type(), 258 | ).toEqual('path'); 259 | expect( 260 | wrapper 261 | .find('.someTrailClass') 262 | .hostNodes() 263 | .type(), 264 | ).toEqual('path'); 265 | expect( 266 | wrapper 267 | .find('.someTextClass') 268 | .hostNodes() 269 | .type(), 270 | ).toEqual('text'); 271 | expect( 272 | wrapper 273 | .find('.someBackgroundClass') 274 | .hostNodes() 275 | .type(), 276 | ).toEqual('circle'); 277 | }); 278 | }); 279 | }); 280 | -------------------------------------------------------------------------------- /test/CircularProgressbarWithChildren.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { mount, shallow } from 'enzyme'; 3 | 4 | import { CircularProgressbarWithChildren } from '../src/index'; 5 | 6 | describe('', () => { 7 | test('SVG rendered to DOM', () => { 8 | const wrapper = mount(); 9 | 10 | expect(wrapper.find('svg').exists()).toEqual(true); 11 | expect(wrapper.find('[data-test-id="CircularProgressbar"]').exists()).toEqual(true); 12 | expect(wrapper.find('[data-test-id="CircularProgressbarWithChildren"]').exists()).toEqual(true); 13 | }); 14 | 15 | test('Forwards all CircularProgressbar props except children', () => { 16 | const props = { 17 | background: true, 18 | backgroundPadding: 5, 19 | circleRatio: 0.5, 20 | classes: { 21 | background: 'background', 22 | path: 'path', 23 | root: 'root', 24 | text: 'text', 25 | trail: 'trail', 26 | }, 27 | className: 'johnny', 28 | counterClockwise: false, 29 | minValue: 0, 30 | maxValue: 100, 31 | value: 50, 32 | strokeWidth: 2, 33 | styles: {}, 34 | text: '50%', 35 | }; 36 | const wrapper = shallow( 37 | 38 |
Hello
39 |
, 40 | ); 41 | 42 | expect(wrapper.find('CircularProgressbar').props()).toEqual(props); 43 | }); 44 | 45 | describe('props.children', () => { 46 | test('No children', () => { 47 | const wrapper = mount(); 48 | 49 | expect( 50 | wrapper.find('[data-test-id="CircularProgressbarWithChildren__children"]').exists(), 51 | ).toEqual(false); 52 | }); 53 | 54 | test('Renders child content', () => { 55 | const wrapper = mount( 56 | 57 |
Hello
58 |
, 59 | ); 60 | 61 | expect( 62 | wrapper.find('[data-test-id="CircularProgressbarWithChildren__children"]').exists(), 63 | ).toEqual(true); 64 | expect(wrapper.find('#hello').exists()).toEqual(true); 65 | }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /test/buildStyles.test.ts: -------------------------------------------------------------------------------- 1 | import { buildStyles } from '../src/index'; 2 | 3 | describe('buildStyles', () => { 4 | test('Builds empty set', () => { 5 | const result = buildStyles({}); 6 | 7 | expect(result).toEqual({ 8 | root: {}, 9 | path: {}, 10 | trail: {}, 11 | text: {}, 12 | background: {}, 13 | }); 14 | }); 15 | test('Builds subset of CSS properties', () => { 16 | const result = buildStyles({ 17 | textSize: 12, 18 | }); 19 | 20 | expect(result).toEqual({ 21 | root: {}, 22 | path: {}, 23 | trail: {}, 24 | text: { 25 | fontSize: 12, 26 | }, 27 | background: {}, 28 | }); 29 | }); 30 | 31 | test('Builds full set of CSS properties', () => { 32 | const result = buildStyles({ 33 | rotation: 0.25, 34 | strokeLinecap: 'butt', 35 | textSize: '16px', 36 | pathTransitionDuration: 0.5, 37 | pathColor: `#000`, 38 | textColor: '#f88', 39 | trailColor: '#d6d6d6', 40 | backgroundColor: '#3e98c7', 41 | }); 42 | 43 | expect(result).toEqual({ 44 | root: {}, 45 | path: { 46 | stroke: `#000`, 47 | strokeLinecap: 'butt', 48 | transitionDuration: '0.5s', 49 | transform: 'rotate(0.25turn)', 50 | transformOrigin: 'center center', 51 | }, 52 | trail: { 53 | stroke: '#d6d6d6', 54 | strokeLinecap: 'butt', 55 | transform: 'rotate(0.25turn)', 56 | transformOrigin: 'center center', 57 | }, 58 | text: { 59 | fill: '#f88', 60 | fontSize: '16px', 61 | }, 62 | background: { 63 | fill: '#3e98c7', 64 | }, 65 | }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /test/setupTests.ts: -------------------------------------------------------------------------------- 1 | import Enzyme from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | 4 | Enzyme.configure({ adapter: new Adapter() }); 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "declarationDir": "dist", 5 | "esModuleInterop": true, 6 | "jsx": "react", 7 | "lib": ["es2015", "dom"], 8 | "module": "es6", 9 | "noImplicitAny": true, 10 | "outDir": "./dist", 11 | "removeComments": true, 12 | "sourceMap": true, 13 | "strict": true, 14 | "target": "es5" 15 | }, 16 | "include": ["src"], 17 | "exclude": ["node_modules", "dist"] 18 | } 19 | --------------------------------------------------------------------------------