Skip to content
Open
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
57 changes: 48 additions & 9 deletions packages/components/progress/Progress.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { forwardRef, isValidElement } from 'react';
import React, { forwardRef, isValidElement, useRef, useState, useEffect, useCallback } from 'react';
import {
CheckCircleFilledIcon as TdCheckCircleFilledIcon,
CheckIcon as TdCheckIcon,
Expand Down Expand Up @@ -55,6 +55,42 @@ const Progress = forwardRef<HTMLDivElement, ProgressProps>((props, ref) => {

const status = !customizeStatus && percentage >= 100 ? 'success' : customizeStatus;

// plump 模式下的 label 位置计算
const innerRef = useRef<HTMLDivElement>(null);
const labelRef = useRef<HTMLDivElement>(null);
const [labelPosition, setLabelPosition] = useState<'inside' | 'outside'>('inside');

const getLabelPosition = useCallback(() => {
if (!innerRef.current || !labelRef.current) return 'inside';

const innerWidth = innerRef.current.offsetWidth; // 填充区域宽度
const labelWidth = labelRef.current.offsetWidth; // 文字实际宽度
const padding = 8; // 左右 padding

return innerWidth >= labelWidth + padding ? 'inside' : 'outside';
}, []);

useEffect(() => {
// 仅在 plump 模式下启用
if (theme !== 'plump') return;

// 初始计算
setLabelPosition(getLabelPosition() as 'inside' | 'outside');

// 如果不支持 ResizeObserver,仅使用初始计算
if (typeof ResizeObserver === 'undefined') return;

const observer = new ResizeObserver(() => {
setLabelPosition(getLabelPosition() as 'inside' | 'outside');
});

if (innerRef.current) {
observer.observe(innerRef.current);
}

return () => observer.disconnect();
}, [theme, percentage, label, getLabelPosition]);

let iconMap = {
success: CheckCircleFilledIcon,
error: CloseCircleFilledIcon,
Expand Down Expand Up @@ -205,29 +241,32 @@ const Progress = forwardRef<HTMLDivElement, ProgressProps>((props, ref) => {
borderRadius: getHeight(),
} as React.CSSProperties;
if (theme === 'plump') {
const PLUMP_SEPARATE = 10;
progressDom = (
<div
ref={ref}
className={classNames(`${classPrefix}-progress__bar`, `${classPrefix}-progress--plump`, {
[`${statusClassName}`]: status,
[`${classPrefix}-progress--over-ten`]: percentage > PLUMP_SEPARATE,
[`${classPrefix}-progress--under-ten`]: percentage <= PLUMP_SEPARATE,
[`${classPrefix}-progress--over-ten`]: labelPosition === 'inside',
[`${classPrefix}-progress--under-ten`]: labelPosition === 'outside',
})}
style={trackStyle}
>
{percentage > PLUMP_SEPARATE ? (
<div className={`${classPrefix}-progress__inner`} style={barStyle}>
{labelPosition === 'inside' ? (
<div ref={innerRef} className={`${classPrefix}-progress__inner`} style={barStyle}>
{label && (
<div className={`${classPrefix}-progress__info`} style={{ color: '#fff' }}>
<div ref={labelRef} className={`${classPrefix}-progress__info`} style={{ color: '#fff' }}>
{isValidElement(label) ? label : `${percentage}%`}
</div>
)}
</div>
) : (
<>
<div className={`${classPrefix}-progress__inner`} style={barStyle}></div>
{getInfoContent()}
<div ref={innerRef} className={`${classPrefix}-progress__inner`} style={barStyle}></div>
{label && (
<div ref={labelRef} className={`${classPrefix}-progress__info`}>
{isValidElement(label) ? label : `${percentage}%`}
</div>
)}
</>
)}
</div>
Expand Down
225 changes: 167 additions & 58 deletions packages/components/progress/__tests__/progress.test.tsx
Original file line number Diff line number Diff line change
@@ -1,58 +1,167 @@
/* eslint-disable prefer-destructuring */
/*
* @Author: Bin
* @Date: 2022-04-07
* @FilePath: /tdesign-react/src/progress/__tests__/progress.test.tsx
*/
import React from 'react';
import { render, waitFor } from '@test/utils';
import Progress from '../Progress';
import { ThemeEnum } from '../type';

describe('Progress 组件测试', () => {
test('render theme', async () => {
const testId = 'progress test theme';
const themes: ThemeEnum[] = ['line', 'plump', 'circle'];
const { getByTestId } = render(
<div data-testid={testId}>
{themes?.map((theme, index) => (
<Progress key={index} strokeWidth={120} theme={theme} size={120} />
))}
</div>,
);

const instance = await waitFor(() => getByTestId(testId));

for (let index = 0; index < themes.length; index++) {
const theme = themes[index];
expect(() => instance.querySelector(`.t-progress--${theme}`)).not.toBe(null);
}
});

test('render size', async () => {
// const testId = 'progress test size';
const sizes: any[] = [
{ name: 'small', size: 72 },
{ name: 'medium', size: 112 },
{ name: 'large', size: 160 },
{ name: 240, size: 240 },
];

const createProgressSizeTest = async (sizeOjb: any) => {
// const sizeOjb = sizes[0];
const testId = `progress test size ${sizeOjb.name}`;
const view = render(
<div data-testid={testId}>
<Progress strokeWidth={'120px'} theme="circle" size={sizeOjb.name} />
</div>,
);
const instance = await waitFor(() => view.getByTestId(testId));
expect(instance.querySelector('.t-progress--circle')).toHaveStyle(`width: ${sizeOjb.size}px`);
};

await createProgressSizeTest(sizes[0]);
await createProgressSizeTest(sizes[1]);
await createProgressSizeTest(sizes[2]);
await createProgressSizeTest(sizes[3]);
});
});
/* eslint-disable prefer-destructuring */
/*
* @Author: Bin
* @Date: 2022-04-07
* @FilePath: /tdesign-react/src/progress/__tests__/progress.test.tsx
*/
import React from 'react';
import { render, waitFor } from '@test/utils';
import { vi } from 'vitest';
import Progress from '../Progress';
import { ThemeEnum } from '../type';

// Mock ResizeObserver
const mockResizeObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(),
unobserve: vi.fn(),
disconnect: vi.fn(),
}));
window.ResizeObserver = mockResizeObserver as any;

// Mock offsetWidth
Object.defineProperties(HTMLElement.prototype, {
offsetWidth: {
get() {
// 根据元素 class 返回不同的宽度
if (this.classList.contains('t-progress__inner')) {
return 200; // 进度条填充区域宽度
}
if (this.classList.contains('t-progress__info')) {
return 30; // label 文字宽度
}
return 0;
},
configurable: true,
},
});

describe('Progress 组件测试', () => {
test('render theme', async () => {
const testId = 'progress test theme';
const themes: ThemeEnum[] = ['line', 'plump', 'circle'];
const { getByTestId } = render(
<div data-testid={testId}>
{themes?.map((theme, index) => (
<Progress key={index} strokeWidth={120} theme={theme} size={120} />
))}
</div>,
);

const instance = await waitFor(() => getByTestId(testId));

for (let index = 0; index < themes.length; index++) {
const theme = themes[index];
expect(() => instance.querySelector(`.t-progress--${theme}`)).not.toBe(null);
}
});

test('render size', async () => {
// const testId = 'progress test size';
const sizes: any[] = [
{ name: 'small', size: 72 },
{ name: 'medium', size: 112 },
{ name: 'large', size: 160 },
{ name: 240, size: 240 },
];

const createProgressSizeTest = async (sizeOjb: any) => {
// const sizeOjb = sizes[0];
const testId = `progress test size ${sizeOjb.name}`;
const view = render(
<div data-testid={testId}>
<Progress strokeWidth={'120px'} theme="circle" size={sizeOjb.name} />
</div>,
);
const instance = await waitFor(() => view.getByTestId(testId));
expect(instance.querySelector('.t-progress--circle')).toHaveStyle(`width: ${sizeOjb.size}px`);
};

await createProgressSizeTest(sizes[0]);
await createProgressSizeTest(sizes[1]);
await createProgressSizeTest(sizes[2]);
await createProgressSizeTest(sizes[3]);
});

describe('plump theme label position', () => {
test('label should be inside when inner width is enough', async () => {
const testId = 'progress plump inside';
const { getByTestId } = render(
<div data-testid={testId}>
<Progress theme="plump" percentage={80} />
</div>,
);

const instance = await waitFor(() => getByTestId(testId));
// inner(200) >= label(30) + 8,label 应在内部
expect(instance.querySelector('.t-progress--over-ten')).toBeTruthy();
});

test('label should be outside when inner width is not enough', async () => {
const testId = 'progress plump outside';
// Mock 小的 inner 宽度
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', {
get() {
if (this.classList.contains('t-progress__inner')) {
return 20; // 小于 label 宽度
}
if (this.classList.contains('t-progress__info')) {
return 30;
}
return 0;
},
configurable: true,
});

const { getByTestId } = render(
<div data-testid={testId}>
<Progress theme="plump" percentage={5} />
</div>,
);

const instance = await waitFor(() => getByTestId(testId));

// inner(20) < label(30) + 8,label 应在外部
expect(instance.querySelector('.t-progress--under-ten')).toBeTruthy();

// 恢复原始 mock
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', {
get() {
if (this.classList.contains('t-progress__inner')) {
return 200;
}
if (this.classList.contains('t-progress__info')) {
return 30;
}
return 0;
},
configurable: true,
});
});

test('label should be hidden when label is false', async () => {
const testId = 'progress plump no label';
const { getByTestId } = render(
<div data-testid={testId}>
<Progress theme="plump" percentage={50} label={false} />
</div>,
);

const instance = await waitFor(() => getByTestId(testId));
// label 为 false 时不显示
expect(instance.querySelector('.t-progress__info')).toBeFalsy();
});

test('custom label should render correctly', async () => {
const testId = 'progress plump custom label';
const customLabel = <span data-testid="custom-label">Custom</span>;
const { getByTestId } = render(
<div data-testid={testId}>
<Progress theme="plump" percentage={50} label={customLabel} />
</div>,
);

const instance = await waitFor(() => getByTestId(testId));
expect(instance.querySelector('[data-testid="custom-label"]')).toBeTruthy();
});
});
});
Loading
Loading