Skip to content

Commit

Permalink
refactor: Progress circle (#141)
Browse files Browse the repository at this point in the history
* refactor: use circle instead of path

* fix: gapDegree

* fix: gapPosition

* fix: gapPosition default value

* chore: VIEW_BOX_SIZE variable

* fix: strokeLinecap square make percent not correct

* fix: percent accuracy issue when >98% or <2%

Reduce half value of storkeWidth when strokeLinecap="round"

* test: update snapshot

* chore: upgrade devDeps

* chore: improve ts type

* fix: test case
  • Loading branch information
afc163 authored May 8, 2022
1 parent 2ea3828 commit 703ce38
Show file tree
Hide file tree
Showing 11 changed files with 373 additions and 613 deletions.
2 changes: 1 addition & 1 deletion docs/examples/fast-progress.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class App extends React.Component<ProgressProps, any> {
this.restart = this.restart.bind(this);
}

private tm: number;
private tm: NodeJS.Timeout;

componentDidMount() {
this.increase();
Expand Down
31 changes: 20 additions & 11 deletions docs/examples/simple.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ class Example extends React.Component<ProgressProps, any> {
constructor(props) {
super(props);
this.state = {
percent: 30,
percent: 96,
color: '#3FC7FA',
};
this.changeState = this.changeState.bind(this);
Expand All @@ -23,19 +23,25 @@ class Example extends React.Component<ProgressProps, any> {
};

changeIncrease() {
let percent = this.state.percent + 10;
if (percent > 100) {
percent = 100;
}
this.setState({ percent });
this.setState(({ percent }) => {
if (percent > 100) {
percent = 100;
}
return {
percent: percent + 1,
};
});
};

changeReduce() {
let percent = this.state.percent - 10;
if (percent < 0) {
percent = 0;
}
this.setState({ percent });
this.setState(({ percent }) => {
if (percent < 0) {
percent = 0;
}
return {
percent: percent - 1,
};
});
};

render() {
Expand Down Expand Up @@ -63,6 +69,9 @@ class Example extends React.Component<ProgressProps, any> {
<div style={circleContainerStyle}>
<Circle percent={percent} strokeWidth={6} strokeLinecap="round" strokeColor={color} />
</div>
<div style={circleContainerStyle}>
<Circle percent={percent} strokeWidth={6} strokeLinecap="butt" strokeColor={color} />
</div>
<div style={circleContainerStyle}>
<Circle percent={percent} strokeWidth={6} strokeLinecap="square" strokeColor={color} />
</div>
Expand Down
16 changes: 9 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"lint": "eslint src/ --ext .ts,.tsx,.jsx,.js",
"prettier": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"",
"test": "father test",
"tsc": "tsc --noEmit",
"coverage": "father test --coverage",
"now-build": "npm run docs:build"
},
Expand All @@ -47,22 +48,23 @@
},
"devDependencies": {
"@types/classnames": "^2.2.9",
"@types/jest": "^26.0.0",
"@types/react": "^16.9.2",
"@types/react-dom": "^16.9.0",
"@types/jest": "^27.5.0",
"@types/react": "^18.0.9",
"@types/react-dom": "^18.0.3",
"@umijs/fabric": "^2.0.0",
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.7",
"cross-env": "^7.0.0",
"dumi": "^1.1.0",
"enzyme": "^3.1.1",
"enzyme-adapter-react-16": "^1.0.1",
"enzyme-to-json": "^3.1.2",
"eslint": "^7.6.0",
"eslint": "^7.1.0",
"father": "^2.29.6",
"glob": "^7.1.6",
"glob": "^8.0.1",
"np": "^7.2.0",
"prettier": "^2.1.1",
"react": "^16.9.0",
"react-dom": "^16.9.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"typescript": "^4.0.2"
}
}
153 changes: 82 additions & 71 deletions src/Circle.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from 'react';
import classNames from 'classnames';
import { useTransitionDuration, defaultProps } from './common';
import type { ProgressProps, GapPositionType } from './interface';
import type { ProgressProps } from './interface';
import useId from './hooks/useId';

function stripPercentToNumber(percent: string) {
Expand All @@ -13,56 +13,52 @@ function toArray<T>(value: T | T[]): T[] {
return Array.isArray(mergedValue) ? mergedValue : [mergedValue];
}

function getPathStyles(
const VIEW_BOX_SIZE = 100;

const getCircleStyle = (
radius: number,
offset: number,
percent: number,
strokeColor: string | Record<string, string>,
strokeWidth: number,
gapDegree = 0,
gapPosition: GapPositionType,
) {
const radius = 50 - strokeWidth / 2;
let beginPositionX = 0;
let beginPositionY = -radius;
let endPositionX = 0;
let endPositionY = -2 * radius;
switch (gapPosition) {
case 'left':
beginPositionX = -radius;
beginPositionY = 0;
endPositionX = 2 * radius;
endPositionY = 0;
break;
case 'right':
beginPositionX = radius;
beginPositionY = 0;
endPositionX = -2 * radius;
endPositionY = 0;
break;
case 'bottom':
beginPositionY = radius;
endPositionY = 2 * radius;
break;
default:
gapPosition: ProgressProps['gapPosition'],
strokeLinecap: ProgressProps['strokeLinecap'],
strokeWidth,
) => {
const rotateDeg = gapDegree > 0 ? 90 + gapDegree / 2 : -90;
const perimeter = Math.PI * 2 * radius;
const perimeterWithoutGap = perimeter * ((360 - gapDegree) / 360);
const offsetDeg = (offset / 100) * 360 * ((360 - gapDegree) / 360);

const positionDeg = {
bottom: 0,
top: 180,
left: 90,
right: -90,
}[gapPosition];

let strokeDashoffset = ((100 - percent) / 100) * perimeterWithoutGap;
// Fix percent accuracy when strokeLinecap is round
// https://github.com/ant-design/ant-design/issues/35009
if (strokeLinecap === 'round' && percent !== 100) {
strokeDashoffset += strokeWidth / 2;
// when percent is small enough (<= 1%), keep smallest value to avoid it's disapperance
if (strokeDashoffset >= perimeterWithoutGap) {
strokeDashoffset = perimeterWithoutGap - 0.01;
}
}
const pathString = `M 50,50 m ${beginPositionX},${beginPositionY}
a ${radius},${radius} 0 1 1 ${endPositionX},${-endPositionY}
a ${radius},${radius} 0 1 1 ${-endPositionX},${endPositionY}`;
const len = Math.PI * 2 * radius;

const pathStyle = {
return {
stroke: typeof strokeColor === 'string' ? strokeColor : undefined,
strokeDasharray: `${(percent / 100) * (len - gapDegree)}px ${len}px`,
strokeDashoffset: `-${gapDegree / 2 + (offset / 100) * (len - gapDegree)}px`,
strokeDasharray: `${perimeterWithoutGap}px ${perimeter}`,
strokeDashoffset,
transform: `rotate(${rotateDeg + offsetDeg + positionDeg}deg)`,
transformOrigin: '50% 50%',
transition:
'stroke-dashoffset .3s ease 0s, stroke-dasharray .3s ease 0s, stroke .3s, stroke-width .06s ease .3s, opacity .3s ease 0s', // eslint-disable-line
'stroke-dashoffset .3s ease 0s, stroke-dasharray .3s ease 0s, stroke .3s, stroke-width .06s ease .3s, opacity .3s ease 0s',
fillOpacity: 0,
};

return {
pathString,
pathStyle,
};
}
};

const Circle: React.FC<ProgressProps> = ({
id,
Expand All @@ -80,16 +76,18 @@ const Circle: React.FC<ProgressProps> = ({
...restProps
}) => {
const mergedId = useId(id);

const gradientId = `${mergedId}-gradient`;
const radius = VIEW_BOX_SIZE / 2 - strokeWidth / 2;

const { pathString, pathStyle } = getPathStyles(
const circleStyle = getCircleStyle(
radius,
0,
100,
trailColor,
strokeWidth,
gapDegree,
gapPosition,
strokeLinecap,
strokeWidth,
);
const percentList = toArray(percent);
const strokeColorList = toArray(strokeColor);
Expand All @@ -99,32 +97,44 @@ const Circle: React.FC<ProgressProps> = ({

const getStokeList = () => {
let stackPtg = 0;
return percentList.map((ptg, index) => {
const color = strokeColorList[index] || strokeColorList[strokeColorList.length - 1];
const stroke = color && typeof color === 'object' ? `url(#${gradientId})` : '';
const pathStyles = getPathStyles(stackPtg, ptg, color, strokeWidth, gapDegree, gapPosition);
stackPtg += ptg;
return (
<path
key={index}
className={`${prefixCls}-circle-path`}
d={pathStyles.pathString}
stroke={stroke}
strokeLinecap={strokeLinecap}
strokeWidth={strokeWidth}
opacity={ptg === 0 ? 0 : 1}
fillOpacity="0"
style={pathStyles.pathStyle}
ref={paths[index]}
/>
);
});
return percentList
.map((ptg, index) => {
const color = strokeColorList[index] || strokeColorList[strokeColorList.length - 1];
const stroke = color && typeof color === 'object' ? `url(#${gradientId})` : undefined;
const circleStyleForStack = getCircleStyle(
radius,
stackPtg,
ptg,
color,
gapDegree,
gapPosition,
strokeLinecap,
strokeWidth,
);
stackPtg += ptg;
return (
<circle
key={index}
className={`${prefixCls}-circle-path`}
r={radius}
cx={VIEW_BOX_SIZE / 2}
cy={VIEW_BOX_SIZE / 2}
stroke={stroke}
strokeLinecap={strokeLinecap}
strokeWidth={strokeWidth}
opacity={ptg === 0 ? 0 : 1}
style={circleStyleForStack}
ref={paths[index]}
/>
);
})
.reverse();
};

return (
<svg
className={classNames(`${prefixCls}-circle`, className)}
viewBox="0 0 100 100"
viewBox={`0 0 ${VIEW_BOX_SIZE} ${VIEW_BOX_SIZE}`}
style={style}
id={id}
{...restProps}
Expand All @@ -140,16 +150,17 @@ const Circle: React.FC<ProgressProps> = ({
</linearGradient>
</defs>
)}
<path
<circle
className={`${prefixCls}-circle-trail`}
d={pathString}
r={radius}
cx={VIEW_BOX_SIZE / 2}
cy={VIEW_BOX_SIZE / 2}
stroke={trailColor}
strokeLinecap={strokeLinecap}
strokeWidth={trailWidth || strokeWidth}
fillOpacity="0"
style={pathStyle}
style={circleStyle}
/>
{getStokeList().reverse()}
{getStokeList()}
</svg>
);
};
Expand Down
2 changes: 1 addition & 1 deletion src/Line.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from 'react';
import classNames from 'classnames';
import { useTransitionDuration, defaultProps } from './common';
import { ProgressProps } from './interface';
import type { ProgressProps } from './interface';

const Line: React.FC<ProgressProps> = ({
className,
Expand Down
5 changes: 3 additions & 2 deletions src/common.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useRef, useEffect } from 'react';
import { ProgressProps } from './interface';
import type { ProgressProps } from './interface';

export const defaultProps: Partial<ProgressProps> = {
className: '',
Expand All @@ -11,6 +11,7 @@ export const defaultProps: Partial<ProgressProps> = {
style: {},
trailColor: '#D9D9D9',
trailWidth: 1,
gapPosition: 'bottom',
};

export const useTransitionDuration = (percentList: number[]) => {
Expand All @@ -21,7 +22,7 @@ export const useTransitionDuration = (percentList: number[]) => {
const now = Date.now();
let updated = false;

Object.keys(paths).forEach(key => {
Object.keys(paths).forEach((key) => {
const path = paths[key].current;
if (!path) {
return;
Expand Down
5 changes: 2 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import Line from './Line';
import Circle from './Circle';
import { ProgressProps } from './interface';

export { Line, Circle, ProgressProps };

export type { ProgressProps } from './interface';
export { Line, Circle };
export default {
Line,
Circle,
Expand Down
Loading

1 comment on commit 703ce38

@vercel
Copy link

@vercel vercel bot commented on 703ce38 May 8, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.