Skip to content

Commit

Permalink
feat(statistic): new component Statistic
Browse files Browse the repository at this point in the history
  • Loading branch information
HaixingOoO committed Nov 9, 2023
1 parent 55bddb0 commit 3b77b11
Show file tree
Hide file tree
Showing 19 changed files with 729 additions and 0 deletions.
8 changes: 8 additions & 0 deletions site/site.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,14 @@ export const docs = [
component: () => import('tdesign-react/skeleton/skeleton.md'),
componentEn: () => import('tdesign-react/skeleton/skeleton.en-US.md'),
},
{
title: 'Statistic 统计数值',
titleEn: 'Statistic',
name: 'statistic',
path: '/react/components/statistic',
component: () => import('tdesign-react/statistic/statistic.md'),
componentEn: () => import('tdesign-react/statistic/statistic.en-US.md'),
},
{
title: 'Swiper 轮播框',
titleEn: 'Swiper',
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,4 @@ export * from './rate';
export * from './link';
export * from './guide';
export * from './back-top';
export * from './statistic';
175 changes: 175 additions & 0 deletions src/statistic/Statistic.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
import isNumber from 'lodash/isNumber';
import isFunction from 'lodash/isFunction';
import {
ArrowTriangleDownFilledIcon as TDArrowTriangleDownFilledIcon,
ArrowTriangleUpFilledIcon as TDArrowTriangleUpFilledIcon,
} from 'tdesign-icons-react';
import { TdStatisticProps } from './type';
import { statisticDefaultProps } from './defaultProps';
import { StyledProps } from '../common';
import useConfig from '../hooks/useConfig';
import useGlobalIcon from '../hooks/useGlobalIcon';
import useDefaultProps from '../hooks/useDefaultProps';

import Skeleton from '../skeleton';
import Tween from './tween';

export interface StatisticProps extends TdStatisticProps, StyledProps {}

export interface StatisticRef {
start: (from?: number, to?: number) => void;
}

const Statistic = forwardRef<StatisticRef, StatisticProps>((props, ref) => {
const {
animation,
animationStart,
color,
decimalPlaces,
extra,
format,
loading,
prefix,
separator,
suffix,
title,
trend,
trendPlacement,
unit,
value,
} = useDefaultProps<StatisticProps>(props, statisticDefaultProps);
const { classPrefix } = useConfig();
const { ArrowTriangleUpFilledIcon } = useGlobalIcon({ ArrowTriangleUpFilledIcon: TDArrowTriangleUpFilledIcon });
const { ArrowTriangleDownFilledIcon } = useGlobalIcon({
ArrowTriangleDownFilledIcon: TDArrowTriangleDownFilledIcon,
});

/**
* init value
*/
const [innerValue, setInnerValue] = useState(animation?.valueFrom ?? value);
const numberValue = useMemo(() => (isNumber(value) ? value : 0), [value]);

const tween = useRef(null);

const start = (from: number = animation?.valueFrom ?? 0, to: number = numberValue) => {
if (from !== to) {
tween.current = new Tween({
from: {
value: from,
},
to: {
value: to,
},
duration: props.animation.duration,
onUpdate: (keys) => {
setInnerValue(keys.value);
},
onFinish: () => {
setInnerValue(to);
},
});
tween.current?.start();
}
};

const formatValue = useMemo(() => {
// eslint-disable-next-line no-underscore-dangle
let _value: number | undefined | string = innerValue;

if (isFunction(format)) {
return format(_value);
}
const options = {
minimumFractionDigits: decimalPlaces || 0,
maximumFractionDigits: decimalPlaces || 20,
useGrouping: !!separator,
};
// replace的替换的方案仅能应对大部分地区
_value = _value.toLocaleString(undefined, options).replace(/,|,/g, separator);

return _value;
}, [innerValue, decimalPlaces, separator, format]);

const COLOR_MAP = {
blue: 'var(--td-brand-color)',
red: 'var(--td-error-color)',
orange: 'var(--td-warning-color)',
green: 'var(--td-success-color)',
};

const valueStyle = useMemo(
() => ({
color: COLOR_MAP[color] || color,
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[color],
);

useEffect(() => {
animation && animationStart && start();
return () => {
if (tween.current) {
tween.current.stop();
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

useEffect(() => {
if (animationStart && animation && !tween.current) {
start();
}
return () => {
if (tween.current) {
tween.current.stop();
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [animationStart]);

useEffect(() => {
if (tween.current) {
tween.current?.stop();
tween.current = null;
}
setInnerValue(value);
if (animationStart && animation) {
start();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value]);

useImperativeHandle(ref, () => ({
start,
}));

const trendIcons = {
increase: <ArrowTriangleUpFilledIcon />,
decrease: <ArrowTriangleDownFilledIcon />,
};

const trendIcon = trend ? trendIcons[trend] : null;

const prefixRender = prefix || (trendIcon && trendPlacement !== 'right' ? trendIcon : null);
const suffixRender = suffix || (trendIcon && trendPlacement === 'right' ? trendIcon : null);

return (
<div className={`${classPrefix}-statistic`}>
{title && <div className={`${classPrefix}-statistic-title`}>{title}</div>}
<Skeleton animation="gradient" theme="text" loading={!!loading}>
<div className={`${classPrefix}-statistic-content`} style={valueStyle}>
{prefixRender && <span className={`${classPrefix}-statistic-content-prefix`}>{prefixRender}</span>}
<span className={`${classPrefix}-statistic-content-value`}>{formatValue}</span>
{unit && <span className={`${classPrefix}-statistic-content-unit`}>{unit}</span>}
{suffixRender && <span className={`${classPrefix}-statistic-content-suffix`}>{suffixRender}</span>}
</div>
</Skeleton>
{extra && <div className={`${classPrefix}-statistic-extra`}>{extra}</div>}
</div>
);
});

Statistic.displayName = 'Statistic';
export default Statistic;
126 changes: 126 additions & 0 deletions src/statistic/__tests__/statistic.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import React from 'react';
import { render, fireEvent, waitFor } from '@test/utils';
import { vi } from 'vitest';
import { ArrowTriangleDownFilledIcon, ArrowTriangleUpFilledIcon } from 'tdesign-icons-react';
import Statistic from '../index';

beforeEach(() => {
vi.useFakeTimers({ shouldAdvanceTime: true });
});

afterEach(() => {
vi.runOnlyPendingTimers();
vi.useRealTimers();
});

describe('Statistic 组件测试', () => {
/**
* props
*/

test('props', () => {
render(<Statistic title="Total Assets" value={82.76} unit="%" trend="increase" color="black" />);

expect(document.querySelector('.t-statistic-title')).toHaveTextContent('Total Assets');

expect(document.querySelector('.t-statistic-content-unit')).toHaveTextContent('%');
});

/**
* color
*/

const COLOR_MAP = {
black: 'black',
blue: 'var(--td-brand-color)',
red: 'var(--td-error-color)',
orange: 'var(--td-warning-color)',
green: 'var(--td-success-color)',
};
const colors = ['black', 'blue', 'red', 'orange', 'green'] as const;
colors.forEach((color) => {
test('color', () => {
render(<Statistic title="Total Assets" value={82.76} unit="%" trend="increase" color={color} />);

expect(document.querySelector('.t-statistic-content')).toHaveStyle(`color: ${COLOR_MAP[color]}`);
});
});

/**
* trend
*/

test('trend', () => {
render(
<div>
<Statistic title="Total Assets" value={82.76} unit="%" trend="increase" />
<Statistic title="Total Assets" value={82.76} unit="%" trend="decrease" />
</div>,
);

const { container: upIcon } = render(<ArrowTriangleUpFilledIcon />);
const { container: downIcon } = render(<ArrowTriangleDownFilledIcon />);

expect(upIcon).toBeInTheDocument();
expect(downIcon).toBeInTheDocument();
});

test('trendPlacement left', () => {
render(<Statistic title="Total Assets" value={82.76} unit="%" trend="decrease" />);

expect(document.querySelector('.t-statistic-content-prefix')).toBeInTheDocument();
});

test('trendPlacement right', () => {
render(<Statistic title="Total Assets" value={82.76} unit="%" trend="decrease" trendPlacement="right" />);

expect(document.querySelector('.t-statistic-content-suffix')).toBeInTheDocument();
});

/**
* loading
*/

test('loading', () => {
render(<Statistic title="Total Assets" value={82.76} loading />);

expect(document.querySelector('.t-statistic-title')).toHaveTextContent('Total Assets');

expect(document.querySelector('.t-skeleton__row')).toBeInTheDocument();
});

/**
* Start
*/

test('Start Function', async () => {
const TestDom = () => {
const [start, setStart] = React.useState(false);

return (
<>
<button id="button" onClick={() => setStart(true)}></button>
<Statistic
title="Total Assets"
value={82.76}
animation={{
valueFrom: 0,
duration: 2000,
}}
format={(value) => +value.toFixed(2)}
animationStart={start}
/>
</>
);
};
render(<TestDom />);

fireEvent.click(document.querySelector('#button'));

vi.advanceTimersByTime(2000);

await waitFor(() => {
expect(document.querySelector('.t-statistic-content-value')).toHaveTextContent('82.76');
});
});
});
33 changes: 33 additions & 0 deletions src/statistic/_example/animation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react';
import { Space, Button, Statistic } from 'tdesign-react';
import type { StatisticRef } from '../index';

const AnimationStatistic = () => {
const [start, setStart] = React.useState(false);
const [value, setValue] = React.useState(56.32);
const statisticRef = React.useRef<StatisticRef>();

return (
<Space direction="vertical">
<Space>
<Button onClick={() => setStart(true)}>Start</Button>
<Button onClick={() => setValue(98.12)}>Update value</Button>
<Button onClick={() => statisticRef.current?.start()}>refs</Button>
</Space>
<Statistic
ref={statisticRef}
title="Total Assets"
suffix="%"
value={value}
animation={{
valueFrom: 0,
duration: 2000,
}}
decimalPlaces={2}
animationStart={start}
/>
</Space>
);
};

export default AnimationStatistic;
11 changes: 11 additions & 0 deletions src/statistic/_example/base.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from 'react';
import { Space, Statistic } from 'tdesign-react';

const BaseStatistic = () => (
<Space size={100}>
<Statistic title="Total Assets" value={82.76} unit="%" trend="increase" />
<Statistic title="Total Assets" value={82.76} unit="USD" trend="increase" />
</Space>
);

export default BaseStatistic;
14 changes: 14 additions & 0 deletions src/statistic/_example/color.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react';
import { Space, Statistic } from 'tdesign-react';

const ColorStatistic = () => (
<Space>
<Statistic title="Total Assets" value={82.76} unit="%" trend="increase" color="black" />
<Statistic title="Total Assets" value={82.76} unit="%" trend="increase" color="blue" />
<Statistic title="Total Assets" value={82.76} unit="%" trend="increase" color="red" />
<Statistic title="Total Assets" value={82.76} unit="%" trend="increase" color="orange" />
<Statistic title="Total Assets" value={82.76} unit="%" trend="increase" color="green" />
</Space>
);

export default ColorStatistic;
Loading

0 comments on commit 3b77b11

Please sign in to comment.