import { isUndefined } from 'lodash';
import React, { useEffect, useRef, useState } from 'react';
import { flushSync } from 'react-dom';
import { useAsync, useId } from '@react-devui/hooks';
import {
CheckCircleFilled,
CheckOutlined,
CloseCircleFilled,
CloseOutlined,
DCustomIcon,
ExclamationOutlined,
WarningFilled,
} from '@react-devui/icons';
import { checkNodeExist, getClassName, getUID } from '@react-devui/utils';
import { registerComponentMate } from '../../utils';
import { useComponentConfig, usePrefixConfig } from '../root';
function ease(k: number) {
return 0.5 * (1 - Math.cos(Math.PI * k));
}
export interface DProgressProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {
dPercent?: number;
dType?: 'line' | 'circle' | 'dashboard';
dStatus?: 'success' | 'warning' | 'error' | 'process';
dWave?: boolean;
dLineCap?: 'butt' | 'round' | 'square' | 'inherit';
dLinearGradient?: React.ReactElement<React.SVGProps<SVGLinearGradientElement>>;
dGapDegree?: number;
dLabel?: React.ReactNode;
dSize?: number;
dLineWidth?: number;
}
const { COMPONENT_NAME } = registerComponentMate({ COMPONENT_NAME: 'DProgress' as const });
export function DProgress(props: DProgressProps): JSX.Element | null {
const {
dPercent = 0,
dType = 'line',
dStatus,
dWave = false,
dLineCap = 'round',
dLinearGradient,
dGapDegree = 0.55 * Math.PI,
dLabel,
dSize,
dLineWidth = 8,
...restProps
} = useComponentConfig(COMPONENT_NAME, props);
const dPrefix = usePrefixConfig();
const progressRef = useRef<HTMLDivElement>(null);
const dataRef = useRef<{
waveLoopFn?: (startTime: number) => void;
transitionLoopFn?: (startTime: number, startPercent: number) => void;
clearWaveTid?: () => void;
clearTransitionTid?: () => void;
}>({});
const async = useAsync();
const uniqueId = useId();
const gradientId = `${dPrefix}progress-${uniqueId}`;
const status = isUndefined(dStatus) && dPercent === 100 ? 'success' : dStatus;
const theme = status === 'success' ? 'success' : status === 'warning' ? 'warning' : status === 'error' ? 'danger' : 'primary';
const [percent, setPercent] = useState(dPercent);
const [wavePercent, setWavePercent] = useState(0);
const valuenowStroke = isUndefined(dLinearGradient) ? `var(--${dPrefix}color-${theme})` : `url(#${gradientId})`;
let waveGradient: React.ReactNode = null;
const [lineWidth, setLineWidth] = useState(0);
useEffect(() => {
if (progressRef.current) {
const observer = new ResizeObserver(() => {
flushSync(() => {
if (progressRef.current) {
const el = progressRef.current.querySelector(`[data-progress-svg="${uniqueId}"]`);
if (el) {
setLineWidth(el.clientWidth);
}
}
});
});
observer.observe(progressRef.current);
return () => {
observer.disconnect();
};
}
}, [uniqueId]);
const drawLine = () => {
const lineProps = {
x1: dLineCap === 'butt' ? 0 : dLineWidth / 2,
y1: '50%',
y2: '50%',
strokeWidth: dLineWidth,
strokeLinecap: dLineCap,
};
const waveWidth = Math.max((lineWidth * (percent / 100)) / 20, 12);
const waveX2 = lineProps.x1 + (dLineCap === 'butt' ? lineWidth : lineWidth - dLineWidth) * (wavePercent / 100);
const waveX1 = Math.max(waveX2 - waveWidth, lineProps.x1);
const id = getUID();
waveGradient = (
<linearGradient id={id} x1={waveX1} y1="50%" x2={waveX2} y2="50%" gradientUnits="userSpaceOnUse">
<stop offset="0%" stopColor="#fff" stopOpacity={0} />
<stop offset="80%" stopColor="#fff" stopOpacity={0.12} />
<stop offset="100%" stopColor="#fff" stopOpacity={0.16} />
</linearGradient>
);
return (
<>
<line
{...lineProps}
x2={dLineCap === 'butt' ? lineWidth : lineWidth - dLineWidth / 2}
stroke={`var(--${dPrefix}background-color-indicator)`}
/>
{percent > 0 && (
<line
{...lineProps}
x2={lineProps.x1 + (dLineCap === 'butt' ? lineWidth : lineWidth - dLineWidth) * (percent / 100)}
stroke={valuenowStroke}
/>
)}
{dWave && wavePercent > 0 && <line {...lineProps} x1={waveX1} x2={waveX2} stroke={`url(#${id})`} />}
</>
);
};
const arcSize = dSize ?? 120;
const getCoordinate = (angle: number) => {
const c = arcSize / 2;
const r = arcSize / 2 - dLineWidth;
return [c + Math.sin(angle) * r, dLineWidth + (r - Math.cos(angle) * r)];
};
const drawArc = (arc: { startAngle: number; endAngle: number; stroke: string }): React.ReactNode => {
const r = arcSize / 2 - dLineWidth;
const angle = arc.endAngle - arc.startAngle;
return angle === 2 * Math.PI ? (
<>
{drawArc({ ...arc, startAngle: 0, endAngle: 1 * Math.PI })}
{drawArc({ ...arc, startAngle: 1 * Math.PI, endAngle: 2 * Math.PI })}
</>
) : (
<path
d={`M ${getCoordinate(arc.startAngle).join()}A ${r} ${r} 0 ${angle > Math.PI ? 1 : 0} 1 ${getCoordinate(arc.endAngle).join()}`}
fill="none"
stroke={arc.stroke}
strokeWidth={dLineWidth}
strokeLinecap={dLineCap}
></path>
);
};
const drawArcWave = (arc: { startAngle: number; endAngle: number }) => {
const [x1, y1] = getCoordinate(arc.startAngle);
const [x2, y2] = getCoordinate(arc.endAngle);
const id = getUID();
waveGradient = (
<linearGradient id={id} x1={x1} y1={y1} x2={x2} y2={y2} gradientUnits="userSpaceOnUse">
<stop offset="0%" stopColor="#fff" stopOpacity={0} />
<stop offset="80%" stopColor="#fff" stopOpacity={0.12} />
<stop offset="100%" stopColor="#fff" stopOpacity={0.16} />
</linearGradient>
);
return dWave && wavePercent > 0 && drawArc({ ...arc, stroke: `url(#${id})` });
};
const drawCircle = () => {
const waveEndAngle = 2 * Math.PI * (wavePercent / 100);
const waveStartAngle = Math.max(waveEndAngle - (2 * Math.PI) / 20, 0);
return (
<>
{drawArc({ startAngle: 0, endAngle: 2 * Math.PI, stroke: `var(--${dPrefix}background-color-indicator)` })}
{percent > 0 && drawArc({ startAngle: 0, endAngle: 2 * Math.PI * (percent / 100), stroke: valuenowStroke })}
{drawArcWave({ startAngle: waveStartAngle, endAngle: waveEndAngle })}
</>
);
};
const drawDashboard = () => {
const endAngle = Math.PI - dGapDegree / 2;
const startAngle = -endAngle;
const waveEndAngle = startAngle + (endAngle - startAngle) * (wavePercent / 100);
const waveStartAngle = Math.max(waveEndAngle - (2 * Math.PI) / 20, startAngle);
return (
<>
{drawArc({
startAngle,
endAngle,
stroke: `var(--${dPrefix}background-color-indicator)`,
})}
{percent > 0 &&
drawArc({
startAngle,
endAngle: startAngle + (endAngle - startAngle) * (percent / 100),
stroke: valuenowStroke,
})}
{drawArcWave({ startAngle: waveStartAngle, endAngle: waveEndAngle })}
</>
);
};
dataRef.current.waveLoopFn = (startTime) => {
const time = window.performance.now();
const elapsed = Math.min((time - startTime) / 1500, 1);
const speed = ease(elapsed);
if (wavePercent < percent) {
setWavePercent(percent * speed);
dataRef.current.clearWaveTid = async.requestAnimationFrame(() => {
dataRef.current.waveLoopFn?.(startTime);
});
} else {
setWavePercent(0);
dataRef.current.clearWaveTid = async.setTimeout(() => {
dataRef.current.waveLoopFn?.(window.performance.now());
}, 2000);
}
};
useEffect(() => {
dataRef.current.clearWaveTid?.();
if (dWave) {
dataRef.current.waveLoopFn?.(window.performance.now());
}
}, [dWave]);
dataRef.current.transitionLoopFn = (startTime, startPercent) => {
const time = window.performance.now();
const elapsed = Math.min((time - startTime) / 300, 1);
const speed = ease(elapsed);
const newPercent = startPercent + (dPercent - startPercent) * speed;
setPercent(newPercent);
if (newPercent !== dPercent) {
dataRef.current.clearTransitionTid = async.requestAnimationFrame(() => {
dataRef.current.transitionLoopFn?.(startTime, startPercent);
});
}
};
useEffect(() => {
dataRef.current.clearTransitionTid?.();
dataRef.current.transitionLoopFn?.(window.performance.now(), percent);
}, [dPercent]);
const node = dType === 'line' ? drawLine() : dType === 'circle' ? drawCircle() : drawDashboard();
return (
<div
{...restProps}
ref={progressRef}
className={getClassName(restProps.className, `${dPrefix}progress`, `${dPrefix}progress--${dType}`)}
style={{
...restProps.style,
width: dType === 'line' && isUndefined(dSize) ? '100%' : undefined,
}}
role="progressbar"
aria-valuenow={dPercent}
aria-valuemin={0}
aria-valuemax={100}
>
<DCustomIcon
viewBox={dType === 'line' ? `0 0 ${lineWidth} ${dLineWidth}` : `0 0 ${arcSize} ${arcSize}`}
data-progress-svg={uniqueId}
dSize={dType === 'line' ? ['100%', dLineWidth] : arcSize}
>
<defs>
{waveGradient}
{!isUndefined(dLinearGradient) && React.cloneElement(dLinearGradient, { id: gradientId })}
</defs>
{node}
</DCustomIcon>
{dLabel !== false && (
<div
className={getClassName(`${dPrefix}progress__label`, {
[`${dPrefix}progress__label--${status}`]: status,
})}
>
{checkNodeExist(dLabel)
? dLabel
: status === 'success'
? React.createElement(dType === 'line' ? CheckCircleFilled : CheckOutlined, { className: `${dPrefix}progress__label-icon` })
: status === 'warning'
? React.createElement(dType === 'line' ? WarningFilled : ExclamationOutlined, { className: `${dPrefix}progress__label-icon` })
: status === 'error'
? React.createElement(dType === 'line' ? CloseCircleFilled : CloseOutlined, { className: `${dPrefix}progress__label-icon` })
: `${dPercent}%`}
</div>
)}
</div>
);
}