Skip to content

Commit

Permalink
feat: add steps support (#144)
Browse files Browse the repository at this point in the history
* feat: add steps support

* test: add test case

* delete useless default gapDegree

Co-authored-by: yangpj17 <yangpj17@chianunicom.com>
  • Loading branch information
yykoypj and yangpj17 authored Jun 29, 2022
1 parent 1ed17d7 commit 80ea029
Show file tree
Hide file tree
Showing 5 changed files with 213 additions and 28 deletions.
3 changes: 3 additions & 0 deletions docs/demo/steps.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## steps

<code src="../examples/steps.tsx">
76 changes: 76 additions & 0 deletions docs/examples/steps.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import * as React from 'react';
import { useState } from 'react';
import { Circle } from 'rc-progress';

const Example = () => {

const [percent, setPercent] = useState<number>(30);
const [strokeWidth, setStrokeWidth] = useState<number>(20);
const [steps, setSteps] = useState<number>(5);
const [space, setSpace] = useState<number>(4);


return (
<div>
<div>
percent: <input
id='range'
type='range'
min='0'
max='100'
value={percent}
style={{ width: 300 }}
onChange={(e) => setPercent(parseInt(e.target.value))} />
</div>
<div>
strokeWidth: <input
id='range'
type='range'
min='0'
max='30'
value={strokeWidth}
style={{ width: 300 }}
onChange={(e) => setStrokeWidth(parseInt(e.target.value))} />
</div>
<div>
steps: <input
id='range'
type='range'
min='0'
max='15'
value={steps}
style={{ width: 300 }}
onChange={(e) => setSteps(parseInt(e.target.value))} />
</div>
<div>
space: <input
id='range'
type='range'
min='0'
max='15'
value={space}
style={{ width: 300 }}
onChange={(e) => setSpace(parseInt(e.target.value))} />
</div>
<h3>Circle Progress:</h3>
<div>percent: {percent}% </div>
<div>strokeWidth: {strokeWidth}px</div>
<div>steps: {steps}</div>
<div>space: {space}px</div>

<div style={{ width: 100 }}>
<Circle
percent={percent}
strokeWidth={strokeWidth}
steps={{
count: steps,
space: space,
}}
strokeColor={'red'}
/>
</div>
</div>
);
};

export default Example;
111 changes: 84 additions & 27 deletions src/Circle.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';
import classNames from 'classnames';
import { useTransitionDuration, defaultProps } from './common';
import { defaultProps, useTransitionDuration } from './common';
import type { ProgressProps } from './interface';
import useId from './hooks/useId';

Expand All @@ -16,20 +16,19 @@ function toArray<T>(value: T | T[]): T[] {
const VIEW_BOX_SIZE = 100;

const getCircleStyle = (
radius: number,
perimeter: number,
perimeterWithoutGap: number,
offset: number,
percent: number,
rotateDeg: number,
gapDegree,
gapPosition: ProgressProps['gapPosition'] | undefined,
strokeColor: string | Record<string, string>,
gapDegree = 0,
gapPosition: ProgressProps['gapPosition'],
strokeLinecap: ProgressProps['strokeLinecap'],
strokeWidth,
stepSpace = 0,
) => {
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 =
gapDegree === 0
? 0
Expand All @@ -45,7 +44,7 @@ const getCircleStyle = (
// 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
// when percent is small enough (<= 1%), keep smallest value to avoid it's disappearance
if (strokeDashoffset >= perimeterWithoutGap) {
strokeDashoffset = perimeterWithoutGap - 0.01;
}
Expand All @@ -54,7 +53,7 @@ const getCircleStyle = (
return {
stroke: typeof strokeColor === 'string' ? strokeColor : undefined,
strokeDasharray: `${perimeterWithoutGap}px ${perimeter}`,
strokeDashoffset,
strokeDashoffset: strokeDashoffset + stepSpace,
transform: `rotate(${rotateDeg + offsetDeg + positionDeg}deg)`,
transformOrigin: '50% 50%',
transition:
Expand All @@ -66,9 +65,10 @@ const getCircleStyle = (
const Circle: React.FC<ProgressProps> = ({
id,
prefixCls,
steps,
strokeWidth,
trailWidth,
gapDegree,
gapDegree = 0,
gapPosition,
trailColor,
strokeLinecap,
Expand All @@ -81,14 +81,21 @@ const Circle: React.FC<ProgressProps> = ({
const mergedId = useId(id);
const gradientId = `${mergedId}-gradient`;
const radius = VIEW_BOX_SIZE / 2 - strokeWidth / 2;
const perimeter = Math.PI * 2 * radius;
const rotateDeg = gapDegree > 0 ? 90 + gapDegree / 2 : -90;
const perimeterWithoutGap = perimeter * ((360 - gapDegree) / 360);
const { count: stepCount, space: stepSpace } =
typeof steps === 'object' ? steps : { count: steps, space: 2 };

const circleStyle = getCircleStyle(
radius,
perimeter,
perimeterWithoutGap,
0,
100,
trailColor,
rotateDeg,
gapDegree,
gapPosition,
trailColor,
strokeLinecap,
strokeWidth,
);
Expand All @@ -105,12 +112,14 @@ const Circle: React.FC<ProgressProps> = ({
const color = strokeColorList[index] || strokeColorList[strokeColorList.length - 1];
const stroke = color && typeof color === 'object' ? `url(#${gradientId})` : undefined;
const circleStyleForStack = getCircleStyle(
radius,
perimeter,
perimeterWithoutGap,
stackPtg,
ptg,
color,
rotateDeg,
gapDegree,
gapPosition,
color,
strokeLinecap,
strokeWidth,
);
Expand All @@ -132,7 +141,7 @@ const Circle: React.FC<ProgressProps> = ({
// React will call the ref callback with the DOM element when the component mounts,
// and call it with `null` when it unmounts.
// Refs are guaranteed to be up-to-date before componentDidMount or componentDidUpdate fires.

paths[index] = elem;
}}
/>
Expand All @@ -141,6 +150,52 @@ const Circle: React.FC<ProgressProps> = ({
.reverse();
};

const getStepStokeList = () => {
// only show the first percent when pass steps
const current = Math.round(stepCount * (percentList[0] / 100));
const stepPtg = 100 / stepCount;

let stackPtg = 0;
return new Array(stepCount).fill(null).map((_, index) => {
const color = index <= current - 1 ? strokeColorList[0] : trailColor;
const stroke = color && typeof color === 'object' ? `url(#${gradientId})` : undefined;
const circleStyleForStack = getCircleStyle(
perimeter,
perimeterWithoutGap,
stackPtg,
stepPtg,
rotateDeg,
gapDegree,
gapPosition,
color,
'butt',
strokeWidth,
stepSpace,
);
stackPtg +=
((perimeterWithoutGap - circleStyleForStack.strokeDashoffset + stepSpace) * 100) /
perimeterWithoutGap;

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={1}
style={circleStyleForStack}
ref={(elem) => {
paths[index] = elem;
}}
/>
);
});
};

return (
<svg
className={classNames(`${prefixCls}-circle`, className)}
Expand All @@ -160,17 +215,19 @@ const Circle: React.FC<ProgressProps> = ({
</linearGradient>
</defs>
)}
<circle
className={`${prefixCls}-circle-trail`}
r={radius}
cx={VIEW_BOX_SIZE / 2}
cy={VIEW_BOX_SIZE / 2}
stroke={trailColor}
strokeLinecap={strokeLinecap}
strokeWidth={trailWidth || strokeWidth}
style={circleStyle}
/>
{getStokeList()}
{!stepCount && (
<circle
className={`${prefixCls}-circle-trail`}
r={radius}
cx={VIEW_BOX_SIZE / 2}
cy={VIEW_BOX_SIZE / 2}
stroke={trailColor}
strokeLinecap={strokeLinecap}
strokeWidth={trailWidth || strokeWidth}
style={circleStyle}
/>
)}
{stepCount ? getStepStokeList() : getStokeList()}
</svg>
);
};
Expand Down
1 change: 1 addition & 0 deletions src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface ProgressProps {
gapPosition?: GapPositionType;
transition?: string;
onClick?: React.MouseEventHandler;
steps?: number | { count: number; space: number };
}

export type BaseStrokeColorType = string | Record<string, string>;
Expand Down
50 changes: 49 additions & 1 deletion tests/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// eslint-disable-next-line max-classes-per-file
import React from 'react';
import { mount } from 'enzyme';
import { Line, Circle } from '../src';
import { Circle, Line } from '../src';

describe('Progress', () => {
describe('Line', () => {
Expand Down Expand Up @@ -51,6 +51,7 @@ describe('Progress', () => {
return <Circle percent={percent} strokeWidth="1" />;
}
}

const circle = mount(<Demo />);
expect(circle.state().percent).toBe('0');
circle.setState({ percent: '30' });
Expand Down Expand Up @@ -164,6 +165,52 @@ describe('Progress', () => {
wrapper.find('.line-target').at(0).simulate('click');
expect(onClick).toHaveBeenCalledTimes(2);
});

it('should steps works with no error', () => {
const steps = 4;
const percent = 35;
const wrapper = mount(
<Circle
steps={steps}
percent={percent}
strokeColor="red"
trailColor="grey"
strokeWidth={20}
/>,
);

expect(wrapper.find('.rc-progress-circle-path')).toHaveLength(steps);
expect(wrapper.find('.rc-progress-circle-path').at(0).getDOMNode().style.cssText).toContain(
'stroke: red;',
);
expect(wrapper.find('.rc-progress-circle-path').at(1).getDOMNode().style.cssText).toContain(
'stroke: grey;',
);

wrapper.setProps({
strokeColor: {
'0%': '#108ee9',
'100%': '#87d068',
},
});
expect(wrapper.find('.rc-progress-circle-path').at(0).props().stroke).toContain('url(');
});
it('should steps works with gap', () => {
const wrapper = mount(
<Circle
steps={{ space: 2, count: 5 }}
gapDegree={60}
percent={50}
strokeColor="red"
trailColor="grey"
strokeWidth={20}
/>,
);
expect(wrapper.find('.rc-progress-circle-path')).toHaveLength(5);
expect(wrapper.find('.rc-progress-circle-path').at(0).getDOMNode().style.cssText).toContain(
'transform: rotate(120deg);',
);
});
});

it('should support percentage array changes', () => {
Expand All @@ -189,6 +236,7 @@ describe('Progress', () => {
);
}
}

const circle = mount(<Demo />);
expect(circle.find(Circle).props().percent).toEqual([40, 40]);
circle.setState({ subPathsCount: 4 });
Expand Down

1 comment on commit 80ea029

@vercel
Copy link

@vercel vercel bot commented on 80ea029 Jun 29, 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.