Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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