Skip to content

Commit 80ea029

Browse files
yykoypjyangpj17
andauthored
feat: add steps support (#144)
* feat: add steps support * test: add test case * delete useless default gapDegree Co-authored-by: yangpj17 <[email protected]>
1 parent 1ed17d7 commit 80ea029

File tree

5 files changed

+213
-28
lines changed

5 files changed

+213
-28
lines changed

docs/demo/steps.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
## steps
2+
3+
<code src="../examples/steps.tsx">

docs/examples/steps.tsx

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import * as React from 'react';
2+
import { useState } from 'react';
3+
import { Circle } from 'rc-progress';
4+
5+
const Example = () => {
6+
7+
const [percent, setPercent] = useState<number>(30);
8+
const [strokeWidth, setStrokeWidth] = useState<number>(20);
9+
const [steps, setSteps] = useState<number>(5);
10+
const [space, setSpace] = useState<number>(4);
11+
12+
13+
return (
14+
<div>
15+
<div>
16+
percent: <input
17+
id='range'
18+
type='range'
19+
min='0'
20+
max='100'
21+
value={percent}
22+
style={{ width: 300 }}
23+
onChange={(e) => setPercent(parseInt(e.target.value))} />
24+
</div>
25+
<div>
26+
strokeWidth: <input
27+
id='range'
28+
type='range'
29+
min='0'
30+
max='30'
31+
value={strokeWidth}
32+
style={{ width: 300 }}
33+
onChange={(e) => setStrokeWidth(parseInt(e.target.value))} />
34+
</div>
35+
<div>
36+
steps: <input
37+
id='range'
38+
type='range'
39+
min='0'
40+
max='15'
41+
value={steps}
42+
style={{ width: 300 }}
43+
onChange={(e) => setSteps(parseInt(e.target.value))} />
44+
</div>
45+
<div>
46+
space: <input
47+
id='range'
48+
type='range'
49+
min='0'
50+
max='15'
51+
value={space}
52+
style={{ width: 300 }}
53+
onChange={(e) => setSpace(parseInt(e.target.value))} />
54+
</div>
55+
<h3>Circle Progress:</h3>
56+
<div>percent: {percent}% </div>
57+
<div>strokeWidth: {strokeWidth}px</div>
58+
<div>steps: {steps}</div>
59+
<div>space: {space}px</div>
60+
61+
<div style={{ width: 100 }}>
62+
<Circle
63+
percent={percent}
64+
strokeWidth={strokeWidth}
65+
steps={{
66+
count: steps,
67+
space: space,
68+
}}
69+
strokeColor={'red'}
70+
/>
71+
</div>
72+
</div>
73+
);
74+
};
75+
76+
export default Example;

src/Circle.tsx

Lines changed: 84 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as React from 'react';
22
import classNames from 'classnames';
3-
import { useTransitionDuration, defaultProps } from './common';
3+
import { defaultProps, useTransitionDuration } from './common';
44
import type { ProgressProps } from './interface';
55
import useId from './hooks/useId';
66

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

1818
const getCircleStyle = (
19-
radius: number,
19+
perimeter: number,
20+
perimeterWithoutGap: number,
2021
offset: number,
2122
percent: number,
23+
rotateDeg: number,
24+
gapDegree,
25+
gapPosition: ProgressProps['gapPosition'] | undefined,
2226
strokeColor: string | Record<string, string>,
23-
gapDegree = 0,
24-
gapPosition: ProgressProps['gapPosition'],
2527
strokeLinecap: ProgressProps['strokeLinecap'],
2628
strokeWidth,
29+
stepSpace = 0,
2730
) => {
28-
const rotateDeg = gapDegree > 0 ? 90 + gapDegree / 2 : -90;
29-
const perimeter = Math.PI * 2 * radius;
30-
const perimeterWithoutGap = perimeter * ((360 - gapDegree) / 360);
3131
const offsetDeg = (offset / 100) * 360 * ((360 - gapDegree) / 360);
32-
3332
const positionDeg =
3433
gapDegree === 0
3534
? 0
@@ -45,7 +44,7 @@ const getCircleStyle = (
4544
// https://github.com/ant-design/ant-design/issues/35009
4645
if (strokeLinecap === 'round' && percent !== 100) {
4746
strokeDashoffset += strokeWidth / 2;
48-
// when percent is small enough (<= 1%), keep smallest value to avoid it's disapperance
47+
// when percent is small enough (<= 1%), keep smallest value to avoid it's disappearance
4948
if (strokeDashoffset >= perimeterWithoutGap) {
5049
strokeDashoffset = perimeterWithoutGap - 0.01;
5150
}
@@ -54,7 +53,7 @@ const getCircleStyle = (
5453
return {
5554
stroke: typeof strokeColor === 'string' ? strokeColor : undefined,
5655
strokeDasharray: `${perimeterWithoutGap}px ${perimeter}`,
57-
strokeDashoffset,
56+
strokeDashoffset: strokeDashoffset + stepSpace,
5857
transform: `rotate(${rotateDeg + offsetDeg + positionDeg}deg)`,
5958
transformOrigin: '50% 50%',
6059
transition:
@@ -66,9 +65,10 @@ const getCircleStyle = (
6665
const Circle: React.FC<ProgressProps> = ({
6766
id,
6867
prefixCls,
68+
steps,
6969
strokeWidth,
7070
trailWidth,
71-
gapDegree,
71+
gapDegree = 0,
7272
gapPosition,
7373
trailColor,
7474
strokeLinecap,
@@ -81,14 +81,21 @@ const Circle: React.FC<ProgressProps> = ({
8181
const mergedId = useId(id);
8282
const gradientId = `${mergedId}-gradient`;
8383
const radius = VIEW_BOX_SIZE / 2 - strokeWidth / 2;
84+
const perimeter = Math.PI * 2 * radius;
85+
const rotateDeg = gapDegree > 0 ? 90 + gapDegree / 2 : -90;
86+
const perimeterWithoutGap = perimeter * ((360 - gapDegree) / 360);
87+
const { count: stepCount, space: stepSpace } =
88+
typeof steps === 'object' ? steps : { count: steps, space: 2 };
8489

8590
const circleStyle = getCircleStyle(
86-
radius,
91+
perimeter,
92+
perimeterWithoutGap,
8793
0,
8894
100,
89-
trailColor,
95+
rotateDeg,
9096
gapDegree,
9197
gapPosition,
98+
trailColor,
9299
strokeLinecap,
93100
strokeWidth,
94101
);
@@ -105,12 +112,14 @@ const Circle: React.FC<ProgressProps> = ({
105112
const color = strokeColorList[index] || strokeColorList[strokeColorList.length - 1];
106113
const stroke = color && typeof color === 'object' ? `url(#${gradientId})` : undefined;
107114
const circleStyleForStack = getCircleStyle(
108-
radius,
115+
perimeter,
116+
perimeterWithoutGap,
109117
stackPtg,
110118
ptg,
111-
color,
119+
rotateDeg,
112120
gapDegree,
113121
gapPosition,
122+
color,
114123
strokeLinecap,
115124
strokeWidth,
116125
);
@@ -132,7 +141,7 @@ const Circle: React.FC<ProgressProps> = ({
132141
// React will call the ref callback with the DOM element when the component mounts,
133142
// and call it with `null` when it unmounts.
134143
// Refs are guaranteed to be up-to-date before componentDidMount or componentDidUpdate fires.
135-
144+
136145
paths[index] = elem;
137146
}}
138147
/>
@@ -141,6 +150,52 @@ const Circle: React.FC<ProgressProps> = ({
141150
.reverse();
142151
};
143152

153+
const getStepStokeList = () => {
154+
// only show the first percent when pass steps
155+
const current = Math.round(stepCount * (percentList[0] / 100));
156+
const stepPtg = 100 / stepCount;
157+
158+
let stackPtg = 0;
159+
return new Array(stepCount).fill(null).map((_, index) => {
160+
const color = index <= current - 1 ? strokeColorList[0] : trailColor;
161+
const stroke = color && typeof color === 'object' ? `url(#${gradientId})` : undefined;
162+
const circleStyleForStack = getCircleStyle(
163+
perimeter,
164+
perimeterWithoutGap,
165+
stackPtg,
166+
stepPtg,
167+
rotateDeg,
168+
gapDegree,
169+
gapPosition,
170+
color,
171+
'butt',
172+
strokeWidth,
173+
stepSpace,
174+
);
175+
stackPtg +=
176+
((perimeterWithoutGap - circleStyleForStack.strokeDashoffset + stepSpace) * 100) /
177+
perimeterWithoutGap;
178+
179+
return (
180+
<circle
181+
key={index}
182+
className={`${prefixCls}-circle-path`}
183+
r={radius}
184+
cx={VIEW_BOX_SIZE / 2}
185+
cy={VIEW_BOX_SIZE / 2}
186+
stroke={stroke}
187+
// strokeLinecap={strokeLinecap}
188+
strokeWidth={strokeWidth}
189+
opacity={1}
190+
style={circleStyleForStack}
191+
ref={(elem) => {
192+
paths[index] = elem;
193+
}}
194+
/>
195+
);
196+
});
197+
};
198+
144199
return (
145200
<svg
146201
className={classNames(`${prefixCls}-circle`, className)}
@@ -160,17 +215,19 @@ const Circle: React.FC<ProgressProps> = ({
160215
</linearGradient>
161216
</defs>
162217
)}
163-
<circle
164-
className={`${prefixCls}-circle-trail`}
165-
r={radius}
166-
cx={VIEW_BOX_SIZE / 2}
167-
cy={VIEW_BOX_SIZE / 2}
168-
stroke={trailColor}
169-
strokeLinecap={strokeLinecap}
170-
strokeWidth={trailWidth || strokeWidth}
171-
style={circleStyle}
172-
/>
173-
{getStokeList()}
218+
{!stepCount && (
219+
<circle
220+
className={`${prefixCls}-circle-trail`}
221+
r={radius}
222+
cx={VIEW_BOX_SIZE / 2}
223+
cy={VIEW_BOX_SIZE / 2}
224+
stroke={trailColor}
225+
strokeLinecap={strokeLinecap}
226+
strokeWidth={trailWidth || strokeWidth}
227+
style={circleStyle}
228+
/>
229+
)}
230+
{stepCount ? getStepStokeList() : getStokeList()}
174231
</svg>
175232
);
176233
};

src/interface.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export interface ProgressProps {
1313
gapPosition?: GapPositionType;
1414
transition?: string;
1515
onClick?: React.MouseEventHandler;
16+
steps?: number | { count: number; space: number };
1617
}
1718

1819
export type BaseStrokeColorType = string | Record<string, string>;

tests/index.spec.js

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// eslint-disable-next-line max-classes-per-file
33
import React from 'react';
44
import { mount } from 'enzyme';
5-
import { Line, Circle } from '../src';
5+
import { Circle, Line } from '../src';
66

77
describe('Progress', () => {
88
describe('Line', () => {
@@ -51,6 +51,7 @@ describe('Progress', () => {
5151
return <Circle percent={percent} strokeWidth="1" />;
5252
}
5353
}
54+
5455
const circle = mount(<Demo />);
5556
expect(circle.state().percent).toBe('0');
5657
circle.setState({ percent: '30' });
@@ -164,6 +165,52 @@ describe('Progress', () => {
164165
wrapper.find('.line-target').at(0).simulate('click');
165166
expect(onClick).toHaveBeenCalledTimes(2);
166167
});
168+
169+
it('should steps works with no error', () => {
170+
const steps = 4;
171+
const percent = 35;
172+
const wrapper = mount(
173+
<Circle
174+
steps={steps}
175+
percent={percent}
176+
strokeColor="red"
177+
trailColor="grey"
178+
strokeWidth={20}
179+
/>,
180+
);
181+
182+
expect(wrapper.find('.rc-progress-circle-path')).toHaveLength(steps);
183+
expect(wrapper.find('.rc-progress-circle-path').at(0).getDOMNode().style.cssText).toContain(
184+
'stroke: red;',
185+
);
186+
expect(wrapper.find('.rc-progress-circle-path').at(1).getDOMNode().style.cssText).toContain(
187+
'stroke: grey;',
188+
);
189+
190+
wrapper.setProps({
191+
strokeColor: {
192+
'0%': '#108ee9',
193+
'100%': '#87d068',
194+
},
195+
});
196+
expect(wrapper.find('.rc-progress-circle-path').at(0).props().stroke).toContain('url(');
197+
});
198+
it('should steps works with gap', () => {
199+
const wrapper = mount(
200+
<Circle
201+
steps={{ space: 2, count: 5 }}
202+
gapDegree={60}
203+
percent={50}
204+
strokeColor="red"
205+
trailColor="grey"
206+
strokeWidth={20}
207+
/>,
208+
);
209+
expect(wrapper.find('.rc-progress-circle-path')).toHaveLength(5);
210+
expect(wrapper.find('.rc-progress-circle-path').at(0).getDOMNode().style.cssText).toContain(
211+
'transform: rotate(120deg);',
212+
);
213+
});
167214
});
168215

169216
it('should support percentage array changes', () => {
@@ -189,6 +236,7 @@ describe('Progress', () => {
189236
);
190237
}
191238
}
239+
192240
const circle = mount(<Demo />);
193241
expect(circle.find(Circle).props().percent).toEqual([40, 40]);
194242
circle.setState({ subPathsCount: 4 });

0 commit comments

Comments
 (0)