diff --git a/README.md b/README.md index 77c284d..276b894 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,12 @@ export default () => ( top the gap position: can be `top`, `bottom`, `left`, or `right`. + + loading + Boolean + false + If it is true the indeterminate progress will be enabled. + diff --git a/docs/demo/loading.md b/docs/demo/loading.md new file mode 100644 index 0000000..73d6cae --- /dev/null +++ b/docs/demo/loading.md @@ -0,0 +1,8 @@ +--- +title: loading +nav: + title: Demo + path: /demo +--- + + diff --git a/docs/examples/loading.tsx b/docs/examples/loading.tsx new file mode 100644 index 0000000..b4948d0 --- /dev/null +++ b/docs/examples/loading.tsx @@ -0,0 +1,13 @@ +import * as React from 'react'; +import { Line, Circle } from 'rc-progress'; + +const Loading = () => { + return ( +
+ + +
+ ); +}; + +export default Loading; diff --git a/src/Circle/index.tsx b/src/Circle/index.tsx index db090f7..1d4cdd5 100644 --- a/src/Circle/index.tsx +++ b/src/Circle/index.tsx @@ -5,6 +5,7 @@ import type { ProgressProps } from '../interface'; import useId from '../hooks/useId'; import PtgCircle from './PtgCircle'; import { VIEW_BOX_SIZE, getCircleStyle } from './util'; +import getIndeterminateCircle from '../utils/getIndeterminateCircle'; function toArray(value: T | T[]): T[] { const mergedValue = value ?? []; @@ -26,6 +27,7 @@ const Circle: React.FC = (props) => { className, strokeColor, percent, + loading, ...restProps } = { ...defaultProps, @@ -51,6 +53,10 @@ const Circle: React.FC = (props) => { >; const isConicGradient = gradient && typeof gradient === 'object'; const mergedStrokeLinecap = isConicGradient ? 'butt' : strokeLinecap; + const { indeterminateStyleProps, indeterminateStyleAnimation } = getIndeterminateCircle({ + id: mergedId, + loading, + }); const circleStyle = getCircleStyle( perimeter, @@ -94,7 +100,7 @@ const Circle: React.FC = (props) => { radius={radius} prefixCls={prefixCls} gradientId={gradientId} - style={circleStyleForStack} + style={{ ...circleStyleForStack, ...indeterminateStyleProps }} strokeLinecap={mergedStrokeLinecap} strokeWidth={strokeWidth} gapDegree={gapDegree} @@ -180,6 +186,7 @@ const Circle: React.FC = (props) => { /> )} {stepCount ? getStepStokeList() : getStokeList()} + {indeterminateStyleAnimation} ); }; diff --git a/src/Line.tsx b/src/Line.tsx index 259aa3a..605c779 100644 --- a/src/Line.tsx +++ b/src/Line.tsx @@ -2,9 +2,12 @@ import * as React from 'react'; import classNames from 'classnames'; import { useTransitionDuration, defaultProps } from './common'; import type { ProgressProps } from './interface'; +import getIndeterminateLine from './utils/getIndeterminateLine'; +import useId from './hooks/useId'; const Line: React.FC = (props) => { const { + id, className, percent, prefixCls, @@ -15,12 +18,15 @@ const Line: React.FC = (props) => { trailColor, trailWidth, transition, + loading, ...restProps } = { ...defaultProps, ...props, }; + const mergedId = useId(id); + // eslint-disable-next-line no-param-reassign delete restProps.gapPosition; const percentList = Array.isArray(percent) ? percent : [percent]; @@ -34,6 +40,14 @@ const Line: React.FC = (props) => { L ${strokeLinecap === 'round' ? right : 100},${center}`; const viewBoxString = `0 0 100 ${strokeWidth}`; let stackPtg = 0; + const { indeterminateStyleProps, indeterminateStyleAnimation } = getIndeterminateLine({ + id: mergedId, + loading, + percent: percentList[0], + strokeLinecap, + strokeWidth, + }); + return ( = (props) => { transition: transition || 'stroke-dashoffset 0.3s ease 0s, stroke-dasharray .3s ease 0s, stroke 0.3s linear', + ...indeterminateStyleProps, }; const color = strokeColorList[index] || strokeColorList[strokeColorList.length - 1]; stackPtg += ptg; @@ -93,6 +108,7 @@ const Line: React.FC = (props) => { /> ); })} + {indeterminateStyleAnimation} ); }; diff --git a/src/common.ts b/src/common.ts index 5bd0a57..5d320fa 100644 --- a/src/common.ts +++ b/src/common.ts @@ -10,6 +10,7 @@ export const defaultProps: Partial = { trailColor: '#D9D9D9', trailWidth: 1, gapPosition: 'bottom', + loading: false, }; export const useTransitionDuration = (): SVGPathElement[] => { diff --git a/src/interface.ts b/src/interface.ts index a5c3770..e4b9b90 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -14,6 +14,7 @@ export interface ProgressProps { transition?: string; onClick?: React.MouseEventHandler; steps?: number | { count: number; gap: number }; + loading?: boolean; } export type StrokeColorObject = Record; diff --git a/src/utils/getIndeterminateCircle.tsx b/src/utils/getIndeterminateCircle.tsx new file mode 100644 index 0000000..704bec0 --- /dev/null +++ b/src/utils/getIndeterminateCircle.tsx @@ -0,0 +1,32 @@ +import React from 'react'; + +interface IndeterminateOption { + id: string; + loading: boolean; +} + +export default ({ id, loading }: IndeterminateOption) => { + if (!loading) { + return { + indeterminateStyleProps: {}, + indeterminateStyleAnimation: null, + }; + } + + const animationName = `${id}-indeterminate-animate`; + + return { + indeterminateStyleProps: { + transform: 'rotate(0deg)', + animation: `${animationName} 1s linear infinite`, + }, + indeterminateStyleAnimation: ( + + ), + }; +}; diff --git a/src/utils/getIndeterminateLine.tsx b/src/utils/getIndeterminateLine.tsx new file mode 100644 index 0000000..cb5be60 --- /dev/null +++ b/src/utils/getIndeterminateLine.tsx @@ -0,0 +1,38 @@ +import type { StrokeLinecapType } from '@/interface'; +import React from 'react'; + +interface IndeterminateOption { + id: string; + loading: boolean; + percent: number; + strokeLinecap: StrokeLinecapType; + strokeWidth: number; +} + +export default (options: IndeterminateOption) => { + const { id, percent, strokeLinecap, strokeWidth, loading } = options; + if (!loading) { + return { + indeterminateStyleProps: {}, + indeterminateStyleAnimation: null, + }; + } + const animationName = `${id}-indeterminate-animate`; + const strokeDashOffset = 100 - (percent + (strokeLinecap === 'round' ? strokeWidth : 0)); + + return { + indeterminateStyleProps: { + strokeDasharray: `${percent} 100`, + animation: `${animationName} .6s linear alternate infinite`, + strokeDashoffset: 0, + }, + indeterminateStyleAnimation: ( + + ), + }; +}; diff --git a/tests/__snapshots__/index.spec.js.snap b/tests/__snapshots__/index.spec.js.snap index 6d96e51..fd65bcd 100644 --- a/tests/__snapshots__/index.spec.js.snap +++ b/tests/__snapshots__/index.spec.js.snap @@ -24,7 +24,7 @@ exports[` 1`] = ` stroke="#2db7f5" stroke-linecap="butt" stroke-width="1" - style="stroke-dasharray: 20px, 100px; stroke-dashoffset: -0px; transition: stroke-dashoffset 0.3s ease 0s, stroke-dasharray .3s ease 0s, stroke 0.3s linear; transition-duration: .3s, .3s, .3s, .06s;" + style="stroke-dasharray: 20px, 100px; stroke-dashoffset: -0px; transition: stroke-dashoffset 0.3s ease 0s, stroke-dasharray .3s ease 0s, stroke 0.3s linear; transition-duration: 0s, 0s;" />
@@ -50,7 +50,7 @@ exports[` 1`] = ` stroke="#2db7f5" stroke-linecap="round" stroke-width="1" - style="stroke-dasharray: 19.8px, 100px; stroke-dashoffset: -0px; transition: stroke-dashoffset 0.3s ease 0s, stroke-dasharray .3s ease 0s, stroke 0.3s linear; transition-duration: .3s, .3s, .3s, .06s;" + style="stroke-dasharray: 19.8px, 100px; stroke-dashoffset: -0px; transition: stroke-dashoffset 0.3s ease 0s, stroke-dasharray .3s ease 0s, stroke 0.3s linear; transition-duration: 0s, 0s;" />
@@ -76,7 +76,7 @@ exports[` 1`] = ` stroke="#2db7f5" stroke-linecap="square" stroke-width="1" - style="stroke-dasharray: 19.9px, 100px; stroke-dashoffset: -0px; transition: stroke-dashoffset 0.3s ease 0s, stroke-dasharray .3s ease 0s, stroke 0.3s linear; transition-duration: .3s, .3s, .3s, .06s;" + style="stroke-dasharray: 19.9px, 100px; stroke-dashoffset: -0px; transition: stroke-dashoffset 0.3s ease 0s, stroke-dasharray .3s ease 0s, stroke 0.3s linear; transition-duration: 0s, 0s;" /> diff --git a/tests/indeterminate.spec.tsx b/tests/indeterminate.spec.tsx new file mode 100644 index 0000000..380a098 --- /dev/null +++ b/tests/indeterminate.spec.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { Circle, Line } from '../src'; +import { render } from '@testing-library/react'; + +describe('(Circle | Line).indeterminate', () => { + describe('Line', () => { + it('should render indeterminate style', () => { + const { container, rerender } = render(); + const line: HTMLElement = container.querySelector('.rc-progress-line-path'); + + expect(line.style.animation).toContain('indeterminate-animate'); + + rerender(); + + expect(line.style.animation).not.toContain('indeterminate-animate'); + }); + + it('should render indeterminate with percent and rerennder without it', () => { + const { container, rerender } = render(); + const line: HTMLElement = container.querySelector('.rc-progress-line-path'); + + expect(line.style.animation).toContain('indeterminate-animate'); + expect(line.style.strokeDasharray).toEqual('20 100'); + + rerender(); + + expect(line.style.animation).not.toContain('indeterminate-animate'); + expect(line.style.strokeDasharray).not.toEqual('20 100'); + }); + }); + + describe('Circle', () => { + it('should render indeterminate style', () => { + const { container, rerender } = render(); + const circle: HTMLElement = container.querySelector('.rc-progress-circle-path'); + + expect(circle.style.animation).toContain('indeterminate-animate'); + + rerender(); + + expect(circle.style.animation).not.toContain('indeterminate-animate'); + }); + + it('should rerender indeterminate with percent Circle', () => { + const { container, rerender } = render(); + const circle: HTMLElement = container.querySelector('.rc-progress-circle-path'); + + expect(circle.style.animation).toContain('indeterminate-animate'); + expect(circle.style.transform).toEqual('rotate(0deg)'); + + rerender(); + + expect(circle.style.animation).not.toContain('indeterminate-animate'); + expect(circle.style.transform).not.toEqual('rotate(0deg)'); + }); + }); +});