Просмотр исходного кода

feat: [progress] Auto-fill gradient colors & custom colors according to progress (#1105)

* feat: Auto-fill gradient colours & custom colours according to progress
Signed-off-by: uiuing <[email protected]>
Co-authored-by: pointhalo <[email protected]>
uiuing 3 лет назад
Родитель
Сommit
3deb4e9712

+ 146 - 1
content/feedback/progress/index-en-US.md

@@ -241,6 +241,150 @@ import { Progress } from '@douyinfe/semi-ui';
 );
 ```
 
+### Customise the progress bar color
+
+The color of a specific `percent` can be customised by setting the `stroke` property
+
+```jsx live=true
+import React, { useState } from 'react';
+import { Progress, Button } from '@douyinfe/semi-ui';
+import { IconChevronLeft, IconChevronRight } from '@douyinfe/semi-icons';
+
+() => {
+    const [percent, setPercent] = useState(10);
+    const strokeArr = [
+        { percent: 20, color: 'red' },
+        { percent: 40, color: 'orange-9' },
+        { percent: 60, color: 'light-green-8' },
+        { percent: 80, color: 'hsla(125, 50%, 46% / 1)' }
+    ];
+    return (
+        <>
+            <div>
+                <Progress
+                    percent={percent}
+                    stroke={strokeArr}
+                    showInfo
+                    type="circle"
+                    width={100}
+                    aria-label="disk usage"
+                />
+                <Progress
+                    percent={percent}
+                    stroke={strokeArr}
+                    showInfo
+                    style={{ margin: '20px 0 10px' }}
+                    aria-label="disk usage"
+                />
+            </div>
+            <Button
+                icon={<IconChevronLeft />}
+                theme="light"
+                onClick={() => {
+                    setPercent(percent - 10);
+                }}
+                disabled={percent === 0}
+            />
+            <Button
+                icon={<IconChevronRight />}
+                theme="light"
+                onClick={() => {
+                    setPercent(percent + 10);
+                }}
+                disabled={percent === 100}
+            />
+        </>
+    );
+};
+```
+
+### Auto-completion of colour intervals
+
+The gradient can be generated by setting the `strokeGradient` property to `true`, automatically fill the colour interval.
+
+```jsx live=true
+import React, { useEffect, useState } from 'react';
+import { Space, Progress, Button } from '@douyinfe/semi-ui';
+import { IconChevronLeft, IconChevronRight } from '@douyinfe/semi-icons';
+
+() => {
+    const [percent, setPercent] = useState(65);
+    const [percentInterval, setPercentInterval] = useState(0);
+    useEffect(() => {
+        setTimeout(
+            () => {
+                setPercentInterval(percentInterval > 100 ? 0 : percentInterval + 3);
+            },
+            percentInterval === 0 || percentInterval > 100 ? 1200 : 290 - (percentInterval % 50) * 3
+        );
+    }, [percentInterval]);
+    const strokeArr = [
+        { percent: 0, color: 'rgb(249, 57, 32)' },
+        { percent: 50, color: '#46259E' },
+        { percent: 100, color: 'hsla(125, 50%, 46% / 1)' },
+    ];
+    const strokeArrReverse = [
+        { percent: 0, color: 'hsla(125, 50%, 46% / 1)' },
+        { percent: 50, color: '#46259E' },
+        { percent: 100, color: 'rgb(249, 57, 32)' },
+    ];
+    return (
+        <>
+            <Space spacing={20}>
+                <div>
+                    <Progress
+                        percent={percentInterval}
+                        stroke={strokeArr}
+                        strokeGradient={true}
+                        showInfo
+                        type="circle"
+                        width={100}
+                        aria-label="file download speed"
+                    />
+                </div>
+                <div>
+                    <Progress
+                        percent={percentInterval}
+                        stroke={strokeArrReverse}
+                        strokeGradient={true}
+                        showInfo
+                        type="circle"
+                        width={100}
+                        aria-label="file download speed"
+                    />
+                </div>
+            </Space>
+            <div style={{ width: '100%', margin: '20px 0 10px' }}>
+                <Progress
+                    percent={percent}
+                    stroke={strokeArr}
+                    strokeGradient={true}
+                    showInfo
+                    size="large"
+                    aria-label="file download speed"
+                />
+            </div>
+            <Button
+                icon={<IconChevronLeft />}
+                theme="light"
+                onClick={() => {
+                    setPercent(percent - 5);
+                }}
+                disabled={percent === 0}
+            />
+            <Button
+                icon={<IconChevronRight />}
+                theme="light"
+                onClick={() => {
+                    setPercent(percent + 5);
+                }}
+                disabled={percent === 100}
+            />
+        </>
+    );
+};
+```
+
 ## API Reference
 
 | PROPERTIES | Instructions | Type | Default |
@@ -256,7 +400,8 @@ import { Progress } from '@douyinfe/semi-ui';
 | percent | percentage of progress | number |  |
 | showInfo | Whether to display the middle text in the circular progress bar, and whether to display the text on the right side of the bar-shaped progress bar | boolean | false |
 | size | size, optional `default`, `small` (only type=circle is effective), `large` (only type=line is effective) | string | 'default' |
-| stroke | Fill color of progress bar | string | 'var(--semi-color-success)' |
+| stroke | Fill color of progress bar, When of type `Array<{percent:number; color:string }>`, the `color` parameter supports the color types: `'Hex'` &#124; `'Hsl'` &#124; `'Hsla'` &#124; `'Rgb'` &#124; `'Rgba'` &#124; `'Semi Design Tokens'` | string &#124; Array<{percent:number; color:string }> | 'var(--semi-color-success)' |
+| strokeGradient | Whether to automatically generate gradient colors to fill color intervals, requires `stroke` to set at least one color interval | boolean | false |
 | strokeLinecap | round corner `round`/square corner `square` (only effective in type='circle' mode) | string | 'round' |
 | strokeWidth | When type is `line`, this property controls the height of the progress bar; when type is `circle`, this property controls the width of the progress bar | number | 4 |
 | style | style | CSSProperties |  |

+ 150 - 4
content/feedback/progress/index.md

@@ -7,6 +7,7 @@ icon: doc-progress
 width: 60%
 brief: 用于展示用户操作的当前进度和状态,一般在操作耗时较长时使用。也可用来表示任务/对象的完成度
 ---
+
 ## 代码演示
 
 ### 如何引入
@@ -261,6 +262,150 @@ import { Progress } from '@douyinfe/semi-ui';
 );
 ```
 
+### 自定义进度条颜色
+
+可通过设置 `stroke` 属性,自定义具体 `percent` 的颜色
+
+```jsx live=true
+import React, { useState } from 'react';
+import { Progress, Button } from '@douyinfe/semi-ui';
+import { IconChevronLeft, IconChevronRight } from '@douyinfe/semi-icons';
+
+() => {
+    const [percent, setPercent] = useState(10);
+    const strokeArr = [
+        { percent: 20, color: 'red' },
+        { percent: 40, color: 'orange-9' },
+        { percent: 60, color: 'light-green-8' },
+        { percent: 80, color: 'hsla(125, 50%, 46% / 1)' }
+    ];
+    return (
+        <>
+            <div>
+                <Progress
+                    percent={percent}
+                    stroke={strokeArr}
+                    showInfo
+                    type="circle"
+                    width={100}
+                    aria-label="disk usage"
+                />
+                <Progress
+                    percent={percent}
+                    stroke={strokeArr}
+                    showInfo
+                    style={{ margin: '20px 0 10px' }}
+                    aria-label="disk usage"
+                />
+            </div>
+            <Button
+                icon={<IconChevronLeft />}
+                theme="light"
+                onClick={() => {
+                    setPercent(percent - 10);
+                }}
+                disabled={percent === 0}
+            />
+            <Button
+                icon={<IconChevronRight />}
+                theme="light"
+                onClick={() => {
+                    setPercent(percent + 10);
+                }}
+                disabled={percent === 100}
+            />
+        </>
+    );
+};
+```
+
+### 自动补齐颜色区间
+
+可通过设置 `strokeGradient` 属性,属性为 `true` 时自动补齐颜色区间,生成渐变色
+
+```jsx live=true
+import React, { useEffect, useState } from 'react';
+import { Space, Progress, Button } from '@douyinfe/semi-ui';
+import { IconChevronLeft, IconChevronRight } from '@douyinfe/semi-icons';
+
+() => {
+    const [percent, setPercent] = useState(65);
+    const [percentInterval, setPercentInterval] = useState(0);
+    useEffect(() => {
+        setTimeout(
+            () => {
+                setPercentInterval(percentInterval > 100 ? 0 : percentInterval + 3);
+            },
+            percentInterval === 0 || percentInterval > 100 ? 1200 : 290 - (percentInterval % 50) * 3
+        );
+    }, [percentInterval]);
+    const strokeArr = [
+        { percent: 0, color: 'rgb(249, 57, 32)' },
+        { percent: 50, color: '#46259E' },
+        { percent: 100, color: 'hsla(125, 50%, 46% / 1)' },
+    ];
+    const strokeArrReverse = [
+        { percent: 0, color: 'hsla(125, 50%, 46% / 1)' },
+        { percent: 50, color: '#46259E' },
+        { percent: 100, color: 'rgb(249, 57, 32)' },
+    ];
+    return (
+        <>
+            <Space spacing={20}>
+                <div>
+                    <Progress
+                        percent={percentInterval}
+                        stroke={strokeArr}
+                        strokeGradient={true}
+                        showInfo
+                        type="circle"
+                        width={100}
+                        aria-label="file download speed"
+                    />
+                </div>
+                <div>
+                    <Progress
+                        percent={percentInterval}
+                        stroke={strokeArrReverse}
+                        strokeGradient={true}
+                        showInfo
+                        type="circle"
+                        width={100}
+                        aria-label="file download speed"
+                    />
+                </div>
+            </Space>
+            <div style={{ width: '100%', margin: '20px 0 10px' }}>
+                <Progress
+                    percent={percent}
+                    stroke={strokeArr}
+                    strokeGradient={true}
+                    showInfo
+                    size="large"
+                    aria-label="file download speed"
+                />
+            </div>
+            <Button
+                icon={<IconChevronLeft />}
+                theme="light"
+                onClick={() => {
+                    setPercent(percent - 5);
+                }}
+                disabled={percent === 0}
+            />
+            <Button
+                icon={<IconChevronRight />}
+                theme="light"
+                onClick={() => {
+                    setPercent(percent + 5);
+                }}
+                disabled={percent === 100}
+            />
+        </>
+    );
+};
+```
+
 ## API 参考
 
 | 属性 | 说明 | 类型 | 默认值 |
@@ -276,7 +421,8 @@ import { Progress } from '@douyinfe/semi-ui';
 | percent | 进度百分比 | number |  |
 | showInfo | 环形进度条是否显示中间文本,条状进度条后右侧是否显示文本 | boolean | false |
 | size | 尺寸,可选`default`、`small`(仅 type=circle 生效)、`large`(仅 type=line 生效) | string | 'default' |
-| stroke | 进度条填充色 | string | 'var(--semi-color-success)' |
+| stroke | 进度条填充色,类型为 `Array<{percent:number; color:string }>` 时,`color` 参数支持颜色类型:`'Hex'` &#124; `'Hsl'` &#124; `'Hsla'` &#124; `'Rgb'` &#124; `'Rgba'` &#124; `'Semi Design Tokens'` | string &#124; Array<{percent:number; color:string }> | 'var(--semi-color-success)' |
+| strokeGradient | 是否自动生成渐变色补齐区间颜色,需要 `stroke` 设置至少一个颜色区间 | boolean | false |
 | strokeLinecap | 圆角`round`/方角`square`(仅在 type='circle'模式下生效) | string | 'round' |
 | strokeWidth | type 为`line`时,该属性控制进度条高度; type 为`circle`时,该属性控制进度条宽度 | number | 4 |
 | style | 样式 | CSSProperties |  |
@@ -303,12 +449,12 @@ import { Progress } from '@douyinfe/semi-ui';
 <Progress aria-label='Percent of file downloaded' percent={80} />
 
 // usage of aria-valuetext
-<Progress aria-label='Percent of disk usage' percent={80} aria-valuetext="Step 2: Copying files... "/> 
+<Progress aria-label='Percent of disk usage' percent={80} aria-valuetext="Step 2: Copying files... "/>
 ```
 
-
 ## 文案规范
-- 如果进度条过程复杂,或者有很长的等待时间,可以使用帮助文本来做说明。这样可以让用户知道正在发生的进度进展
+
+-   如果进度条过程复杂,或者有很长的等待时间,可以使用帮助文本来做说明。这样可以让用户知道正在发生的进度进展
 
 ## 设计变量
 

+ 2 - 1
packages/semi-foundation/progress/constants.ts

@@ -7,12 +7,13 @@ const cssClasses = {
 const strings = {
     types: ['line', 'circle'],
     DEFAULT_TYPE: 'line',
+    STROKE_DEFAULT: 'var(--semi-color-success)',
     strokeLineCap: ['square', 'round'],
     DEFAULT_LINECAP: 'round',
     sizes: ['default', 'small', 'large'],
     DEFAULT_SIZE: 'default',
     directions: ['vertical', 'horizontal'],
-    DEFAULT_DIRECTION: 'horizontal'
+    DEFAULT_DIRECTION: 'horizontal',
 };
 
 const numbers = {};

+ 242 - 0
packages/semi-foundation/progress/generates.ts

@@ -0,0 +1,242 @@
+import { strings } from './constants';
+
+// type ColorType = 'Hex' | 'Hsl' | 'Hsla' | 'Rgb' | 'Rgba' | 'Semi Design Tokens';
+
+type Generate = {
+    startColor: string;
+    endColor: string;
+    size: number;
+};
+
+type StrokeSet = { percent: number; color: string };
+
+type StrokeArr = Array<StrokeSet>;
+
+function generateColor(s: StrokeArr, percent: number, gradient: boolean): string | undefined {
+    try {
+        const gradientColorArr = generate(s, percent, gradient);
+        if (gradientColorArr.length !== 0) return gradientColorArr;
+    } catch (e) {
+        return undefined;
+    }
+    return undefined;
+}
+
+function generate(s: StrokeArr, percent: number, gradient: boolean): string | undefined {
+    s.sort((a, b) => a.percent - b.percent);
+    if (s[0].percent > percent) {
+        return strings.STROKE_DEFAULT;
+    }
+    const endS = s[s.length - 1];
+    if (endS.percent < percent) {
+        return formatToHex(endS.color);
+    }
+    for (const [index, item] of s.entries()) {
+        if (item.percent === percent) {
+            return formatToHex(item.color);
+        }
+        if (percent > item.percent) continue;
+        const oldItem = s[index - 1];
+        if (!gradient) {
+            return formatToHex(oldItem.color);
+        }
+        return generateGradients(
+            {
+                startColor: formatToHex(oldItem.color),
+                endColor: formatToHex(item.color),
+                size: item.percent - oldItem.percent - 1,
+            },
+            percent - oldItem.percent - 1
+        ) as string;
+    }
+    return undefined;
+}
+
+function generateGradients(g: Generate, index: number | undefined): Array<string> | string {
+    const { startColor, endColor, size } = g;
+    const sA = startColor.split('');
+    const eA = endColor.split('');
+    const rC = [parseInt(`${sA[1]}${sA[2]}`, 16), parseInt(`${eA[1]}${eA[2]}`, 16)];
+    const gC = [parseInt(`${sA[3]}${sA[4]}`, 16), parseInt(`${eA[3]}${eA[4]}`, 16)];
+    const bC = [parseInt(`${sA[5]}${sA[6]}`, 16), parseInt(`${eA[5]}${eA[6]}`, 16)];
+    const aC = [parseInt(`${sA[7]}${sA[8]}`, 16), parseInt(`${eA[7]}${eA[8]}`, 16)];
+    const rStep = (rC[0] - rC[1]) / (size + 1);
+    const gStep = (gC[0] - gC[1]) / (size + 1);
+    const bStep = (bC[0] - bC[1]) / (size + 1);
+    const aStep = (aC[0] - aC[1]) / (size + 1);
+    function tHex(i: number) {
+        const rS = Math.round(rC[0] - rStep * (i + 1)).toString(16);
+        const gS = Math.round(gC[0] - gStep * (i + 1)).toString(16);
+        const bS = Math.round(bC[0] - bStep * (i + 1)).toString(16);
+        const h = `${padTwo(rS)}${padTwo(gS)}${padTwo(bS)}`;
+        const t = Math.floor(aStep * (i + 1) + aC[1]).toString(16);
+        return toHex.Hex(`#${h}`, t);
+    }
+    function padTwo(s: string) {
+        if (s.length === 1) {
+            return `0${s}`;
+        }
+        if (s.length === 0) {
+            return '00';
+        }
+        return s;
+    }
+    if (typeof index === 'undefined') {
+        const gradientColorArr = [startColor];
+        for (let i = 0; i < size; i += 1) {
+            gradientColorArr.push(tHex(i));
+        }
+        return gradientColorArr;
+    }
+    return tHex(index);
+}
+
+// Resolve the colour type contained within `ColorType` to Hex
+function formatToHex(color: string): string | undefined {
+    color = color.trim().toLowerCase();
+    // Hex
+    if (REG_S.hex.test(color)) {
+        return toHex.Hex(color, undefined);
+    }
+    // Hsl or Hsla
+    if (REG_S.hslA.test(color)) {
+        return toHex.Hex(toHex.HslA(color), undefined);
+    }
+    // Rgb or Rgba
+    if (REG_S.rgbA.test(color)) {
+        return toHex.Hex(toHex.RgbA(color), undefined);
+    }
+    // Semi Design Tokens
+    if (REG_S.semiDesignTokens.test(color)) {
+        if (SEMI_DESIGN_TOKENS.ALONG.indexOf(color) !== -1) {
+            return toHex.SemiDesignToken(color);
+        }
+        if (SEMI_DESIGN_TOKENS.SEQUENCE.indexOf(color) !== -1) {
+            return toHex.SemiDesignToken(`${color}-5`);
+        }
+        return toHex.SemiDesignToken(`${color}`);
+    }
+    return undefined;
+}
+
+const toHex = {
+    Hex(color: string, transparency: string | undefined): string {
+        color = color.replace('#', '');
+        if (color.length === 8) return `#${color}`;
+        if (color.length === 6) return `#${color}${transparency || 'ff'}`;
+        if (color.length === 3) {
+            color = color
+                .split('')
+                .map(c => c + c)
+                .join('');
+        }
+        return `#${color}${transparency || 'ff'}`;
+    },
+    SemiDesignToken(color: string): string | undefined {
+        // ! Only produces effects when used, the conditions for running need to occur after the real DOM is rendered
+        if (typeof window === 'undefined') {
+            return undefined;
+        }
+        const variable = getComputedStyle(document.body).getPropertyValue(`--semi-${color}`);
+        if (variable === '') return undefined;
+        const rgba = `rgba(${variable}, 1)`;
+        return toHex.RgbA(rgba);
+    },
+    HslA(color: string): string {
+        const hsla = REG_S.hslA.exec(color);
+        const h = parseInt(hsla[2]);
+        const s = parseInt(hsla[3]) / 100;
+        const l = parseInt(hsla[4]) / 100;
+        const a = hsla[5];
+        const c = (1 - Math.abs(2 * l - 1)) * s,
+            x = c * (1 - Math.abs(((h / 60) % 2) - 1)),
+            m = l - c / 2;
+        let r: string | number = 0,
+            g: string | number = 0,
+            b: string | number = 0;
+        if (0 <= h && h < 60) {
+            r = c;
+            g = x;
+            b = 0;
+        } else if (60 <= h && h < 120) {
+            r = x;
+            g = c;
+            b = 0;
+        } else if (120 <= h && h < 180) {
+            r = 0;
+            g = c;
+            b = x;
+        } else if (180 <= h && h < 240) {
+            r = 0;
+            g = x;
+            b = c;
+        } else if (240 <= h && h < 300) {
+            r = x;
+            g = 0;
+            b = c;
+        } else if (300 <= h && h < 360) {
+            r = c;
+            g = 0;
+            b = x;
+        }
+        r = Math.round((r + m) * 255).toString(16);
+        g = Math.round((g + m) * 255).toString(16);
+        b = Math.round((b + m) * 255).toString(16);
+        return toHex.utils.pAL(r, g, b, a);
+    },
+    RgbA(color: string): string {
+        const rgba = REG_S.rgbA.exec(color);
+        const r = parseInt(rgba[2], 10).toString(16),
+            g = parseInt(rgba[3], 10).toString(16),
+            b = parseInt(rgba[4], 10).toString(16),
+            a = rgba[5];
+        return toHex.utils.pAL(r, g, b, a);
+    },
+    utils: {
+        pAL(r: string, g: string, b: string, a: string) {
+            if (r.length == 1) r = '0' + r;
+            if (g.length == 1) g = '0' + g;
+            if (b.length == 1) b = '0' + b;
+            if (typeof a !== 'undefined') {
+                a = Math.round(parseInt(a) * 255).toString(16);
+                if (a.length == 1) a = '0' + a;
+                return '#' + r + g + b + a;
+            }
+            return '#' + r + g + b;
+        },
+    },
+};
+
+const REG_S = {
+    hex: /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/,
+    hslA: /(hsl)a?\(\s*?(\d+),?\s*?(\d+)%,?\s*?(\d+)%,?\s*?\/?(\s*?[\d.]+)?\s*?\)/,
+    rgbA: /(rgb)a?\(\s*?(\d+),?\s*?(\d+),?\s*?(\d+),?\s*?\/?(\s*?[\d.]+)?\s*?\)/,
+    semiDesignTokens: /(\w+)?-?(\w+)-?(\d)?/,
+};
+
+// From src/components/palette.js
+const SEMI_DESIGN_TOKENS = {
+    // No sequence
+    ALONG:["black", "white"],
+    // Sequence: 0-9
+    SEQUENCE:[
+        "amber",
+        "blue",
+        "cyan",
+        "green",
+        "grey",
+        "indigo",
+        "light-blue",
+        "light-green",
+        "lime",
+        "orange",
+        "pink",
+        "purple",
+        "red",
+        "teal",
+        "violet",
+        "yellow"
+    ]
+};
+
+export { generateColor, StrokeArr };

+ 80 - 11
packages/semi-ui/progress/__test__/progress.test.js

@@ -6,7 +6,6 @@ import Progress from '../index';
 const getProgress = (props = {}) => mount(<Progress {...props} />);
 
 describe('Progress', () => {
-
     it('percent pass invalid value like NaN', () => {
         const p = getProgress({ percent: 30 });
         function testNaN() {
@@ -34,7 +33,6 @@ describe('Progress', () => {
         }, 500);
     });
 
-
     it('classname & style', () => {
         const p = getProgress({ className: 'test', color: 'red' });
         const node = p.find(`.${BASE_CLASS_PREFIX}-progress`);
@@ -49,10 +47,76 @@ describe('Progress', () => {
         let props = {
             stroke: '#fc8800',
             size: 'small',
-            orbitStroke: '#f93920'
+            orbitStroke: '#f93920',
+        };
+        const p = getProgress(props);
+        expect(p.exists('.semi-progress-large'));
+    });
+
+    it('Gradient Accuracy [strokeGradient true & stroke type is Array]', () => {
+        let props = {
+            stroke: [
+                {
+                    percent: 50,
+                    color: '#fff',
+                },
+                {
+                    percent: 52,
+                    color: 'rgba(0, 0, 0, 0)',
+                },
+            ],
+            strokeGradient: true,
+            percent: 51,
+            type: 'circle',
+        };
+        const p = getProgress(props);
+        const _stroke = p
+            .find('.semi-progress-circle-ring-inner')
+            .at(0)
+            .getDOMNode()
+            .getAttribute('stroke');
+        expect(_stroke).toEqual('#8080807f');
+    });
+
+    it('Gradient Accuracy [strokeGradient false & stroke type is Array]', () => {
+        let props = {
+            stroke: [
+                {
+                    percent: 3,
+                    color: '#fff',
+                },
+            ],
+            percent: 90,
+            type: 'circle',
         };
         const p = getProgress(props);
-        expect(p.exists('.semi-progress-large'))
+        const _stroke = p
+            .find('.semi-progress-circle-ring-inner')
+            .at(0)
+            .getDOMNode()
+            .getAttribute('stroke');
+        expect(_stroke).toEqual('#ffffffff');
+    });
+
+    it('Gradient Compatibility [strokeGradient true & stroke type is Array]', () => {
+        let props = {
+            stroke: [
+                { percent: 0, color: 'red' },
+                { percent: 10, color: '#b2140c' },
+                { percent: 50, color: 'rgb(0, 99, 167)' },
+                { percent: 100, color: 'hsla(125, 50%, 46% / 1)' },
+            ],
+            strokeGradient: true,
+            percent: 55,
+            type: 'circle',
+        };
+        const p = getProgress(props);
+        const _stroke = p
+            .find('.semi-progress-circle-ring-inner')
+            .at(0)
+            .getDOMNode()
+            .getAttribute('stroke');
+        expect(_stroke).toEqual('#066b9dff');
     });
 
     it('direction', () => {
@@ -79,7 +143,10 @@ describe('Progress', () => {
             strokeWidth: 10,
         };
         const p = getProgress(props);
-        let firstCircle = p.find('circle').at(0).getDOMNode();
+        let firstCircle = p
+            .find('circle')
+            .at(0)
+            .getDOMNode();
         expect(firstCircle.getAttribute('stroke-linecap')).toEqual('square');
         expect(firstCircle.getAttribute('stroke-width')).toEqual('10');
     });
@@ -87,10 +154,13 @@ describe('Progress', () => {
     it('width', () => {
         let props = {
             width: 120,
-            type: 'circle'
-        }
+            type: 'circle',
+        };
         const p = getProgress(props);
-        let svgRing = p.find('.semi-progress-circle-ring').at(0).getDOMNode();
+        let svgRing = p
+            .find('.semi-progress-circle-ring')
+            .at(0)
+            .getDOMNode();
         expect(svgRing.getAttribute('width')).toEqual('120');
     });
 
@@ -98,8 +168,8 @@ describe('Progress', () => {
         let props = {
             motion: false,
             percent: 70,
-            showInfo: true
-        }
+            showInfo: true,
+        };
         const p = getProgress(props);
         expect(p.find('.semi-progress-line-text').text()).toEqual('70%');
         p.setProps({ percent: 80 });
@@ -124,4 +194,3 @@ describe('Progress', () => {
         expect(minp.find('.semi-progress-line-text').text()).toEqual('0%');
     });
 });
-

+ 87 - 26
packages/semi-ui/progress/index.tsx

@@ -5,6 +5,7 @@ import { cssClasses, strings } from '@douyinfe/semi-foundation/progress/constant
 import '@douyinfe/semi-foundation/progress/progress.scss';
 import { Animation } from '@douyinfe/semi-animation';
 import { Motion } from '../_base/base';
+import { generateColor, StrokeArr } from '@douyinfe/semi-foundation/progress/generates';
 
 const prefixCls = cssClasses.PREFIX;
 
@@ -21,7 +22,8 @@ export interface ProgressProps {
     percent?: number;
     showInfo?: boolean;
     size?: 'default' | 'small' | 'large';
-    stroke?: string;
+    stroke?: string | StrokeArr;
+    strokeGradient?: boolean;
     strokeLinecap?: 'round' | 'square';
     strokeWidth?: number;
     style?: React.CSSProperties;
@@ -48,7 +50,16 @@ class Progress extends Component<ProgressProps, ProgressState> {
         scale: PropTypes.number,
         showInfo: PropTypes.bool,
         size: PropTypes.oneOf(strings.sizes),
-        stroke: PropTypes.string,
+        stroke: PropTypes.oneOfType([
+            PropTypes.string,
+            PropTypes.arrayOf(
+                PropTypes.shape({
+                    percent: PropTypes.number,
+                    color: PropTypes.string,
+                })
+            ),
+        ]),
+        strokeGradient: PropTypes.bool,
         strokeLinecap: PropTypes.oneOf(strings.strokeLineCap),
         strokeWidth: PropTypes.number,
         style: PropTypes.object,
@@ -65,7 +76,8 @@ class Progress extends Component<ProgressProps, ProgressState> {
         percent: 0,
         showInfo: false,
         size: strings.DEFAULT_SIZE,
-        stroke: 'var(--semi-color-success)',
+        stroke: strings.STROKE_DEFAULT,
+        strokeGradient: false,
         strokeLinecap: strings.DEFAULT_LINECAP,
         strokeWidth: 4,
         style: {},
@@ -80,7 +92,7 @@ class Progress extends Component<ProgressProps, ProgressState> {
         super(props);
         this._mounted = true;
         this.state = {
-            percentNumber: this.props.percent // Specially used for animation of numbers
+            percentNumber: this.props.percent, // Specially used for animation of numbers
         };
     }
     componentDidUpdate(prevProps: ProgressProps): void {
@@ -98,14 +110,17 @@ class Progress extends Component<ProgressProps, ProgressState> {
             if (this.animation && this.animation.destroy) {
                 this.animation.destroy();
             }
-            this.animation = new Animation({
-                from: { value: prevProps.percent },
-                to: { value: this.props.percent }
-            }, {
-                // easing: 'cubic-bezier(0, .68, .3, 1)'
-                easing: 'linear',
-                duration: 300
-            });
+            this.animation = new Animation(
+                {
+                    from: { value: prevProps.percent },
+                    to: { value: this.props.percent },
+                },
+                {
+                    // easing: 'cubic-bezier(0, .68, .3, 1)'
+                    easing: 'linear',
+                    duration: 300,
+                }
+            );
             this.animation.on('frame', (props: any) => {
                 // prevent setState while component is unmounted but this timer is called
                 if (this._mounted === false) {
@@ -132,7 +147,20 @@ class Progress extends Component<ProgressProps, ProgressState> {
     }
 
     renderCircleProgress(): ReactNode {
-        const { strokeLinecap, style, className, strokeWidth, format, size, stroke, showInfo, percent, orbitStroke, id } = this.props;
+        const {
+            strokeLinecap,
+            style,
+            className,
+            strokeWidth,
+            format,
+            size,
+            stroke,
+            strokeGradient,
+            showInfo,
+            percent,
+            orbitStroke,
+            id,
+        } = this.props;
         const ariaLabel = this.props['aria-label'];
         const ariaLabelledBy = this.props['aria-labelledby'];
         const ariaValueText = this.props['aria-valuetext'];
@@ -140,7 +168,7 @@ class Progress extends Component<ProgressProps, ProgressState> {
         const classNames = {
             wrapper: cls(`${prefixCls}-circle`, className),
             svg: cls(`${prefixCls}-circle-ring`),
-            circle: cls(`${prefixCls}-circle-ring-inner`)
+            circle: cls(`${prefixCls}-circle-ring-inner`),
         };
         const perc = this.calcPercent(percent);
         const percNumber = this.calcPercent(percentNumber);
@@ -149,9 +177,12 @@ class Progress extends Component<ProgressProps, ProgressState> {
         if (this.props.width) {
             width = this.props.width;
         } else {
-            size === strings.DEFAULT_SIZE ? width = 72 : width = 24;
+            size === strings.DEFAULT_SIZE ? (width = 72) : (width = 24);
         }
 
+        // parse stroke & generate gradients
+        const _stroke = this.selectStroke(stroke, percent, strokeGradient);
+
         // cx, cy is circle center
         const cy = width / 2;
         const cx = width / 2;
@@ -165,10 +196,10 @@ class Progress extends Component<ProgressProps, ProgressState> {
         return (
             <div
                 id={id}
-                className={classNames.wrapper} 
-                style={style} 
-                role='progressbar' 
-                aria-valuemin={0} 
+                className={classNames.wrapper}
+                style={style}
+                role="progressbar"
+                aria-valuemin={0}
                 aria-valuemax={100}
                 aria-valuenow={percNumber}
                 aria-labelledby={ariaLabelledBy}
@@ -195,14 +226,14 @@ class Progress extends Component<ProgressProps, ProgressState> {
                         strokeDasharray={strokeDasharray}
                         strokeLinecap={strokeLinecap}
                         fill="transparent"
-                        stroke={stroke}
+                        stroke={_stroke}
                         r={radius}
                         cx={cx}
                         cy={cy}
                         aria-hidden
                     />
                 </svg>
-                {showInfo && size !== 'small' ? (<span className={`${prefixCls}-circle-text`}>{text}</span>) : null}
+                {showInfo && size !== 'small' ? <span className={`${prefixCls}-circle-text`}>{text}</span> : null}
             </div>
         );
     }
@@ -219,8 +250,31 @@ class Progress extends Component<ProgressProps, ProgressState> {
         return perc;
     }
 
+    selectStroke(stroke: string | StrokeArr, percent: number, strokeGradient): string {
+        if (typeof stroke === 'string') {
+            return stroke;
+        }
+        const color = generateColor(stroke, percent, strokeGradient);
+        if (typeof color !== 'undefined') {
+            return color;
+        }
+        return strings.STROKE_DEFAULT;
+    }
+
     renderLineProgress(): ReactNode {
-        const { className, style, stroke, direction, format, showInfo, size, percent, orbitStroke, id } = this.props;
+        const {
+            className,
+            style,
+            stroke,
+            strokeGradient,
+            direction,
+            format,
+            showInfo,
+            size,
+            percent,
+            orbitStroke,
+            id,
+        } = this.props;
         const ariaLabel = this.props['aria-label'];
         const ariaLabelledBy = this.props['aria-labelledby'];
         const ariaValueText = this.props['aria-valuetext'];
@@ -238,8 +292,11 @@ class Progress extends Component<ProgressProps, ProgressState> {
         const perc = this.calcPercent(percent);
         const percNumber = this.calcPercent(percentNumber);
 
+        // parse stroke & generate gradients
+        const _stroke = this.selectStroke(stroke, percent, strokeGradient);
+
         const innerStyle: Record<string, any> = {
-            background: stroke
+            background: _stroke,
         };
         if (direction === strings.DEFAULT_DIRECTION) {
             innerStyle.width = `${perc}%`;
@@ -254,7 +311,7 @@ class Progress extends Component<ProgressProps, ProgressState> {
                 id={id}
                 className={progressWrapperCls}
                 style={style}
-                role='progressbar'
+                role="progressbar"
                 aria-valuemin={0}
                 aria-valuemax={100}
                 aria-valuenow={perc}
@@ -262,7 +319,11 @@ class Progress extends Component<ProgressProps, ProgressState> {
                 aria-label={ariaLabel}
                 aria-valuetext={ariaValueText}
             >
-                <div className={progressTrackCls} style={orbitStroke ? { backgroundColor: orbitStroke } : {}} aria-hidden>
+                <div
+                    className={progressTrackCls}
+                    style={orbitStroke ? { backgroundColor: orbitStroke } : {}}
+                    aria-hidden
+                >
                     <div className={innerCls} style={innerStyle} aria-hidden />
                 </div>
                 {showInfo ? <div className={`${prefixCls}-line-text`}>{text}</div> : null}
@@ -280,4 +341,4 @@ class Progress extends Component<ProgressProps, ProgressState> {
     }
 }
 
-export default Progress;
+export default Progress;