diff --git a/site/site.config.mjs b/site/site.config.mjs index 95b2ce272..377e6f697 100644 --- a/site/site.config.mjs +++ b/site/site.config.mjs @@ -323,7 +323,7 @@ export default { title: 'Card 卡片', name: 'card', path: '/react/components/card', - component: () => import('tdesign-react/card/card.md') + component: () => import('tdesign-react/card/card.md'), }, { title: 'Collapse 折叠面板', @@ -397,6 +397,12 @@ export default { path: '/react/components/watermark', component: () => import('tdesign-react/watermark/watermark.md'), }, + { + title: 'Rate 评分', + name: 'rate', + path: '/react/components/rate', + component: () => import('tdesign-react/rate/rate.md'), + }, ], }, { diff --git a/src/index.ts b/src/index.ts index 529528340..6f61ee4bb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -56,3 +56,4 @@ export * from './range-input'; export * from './watermark'; export * from './space'; export * from './jumper'; +export * from './rate'; diff --git a/src/rate/Rate.tsx b/src/rate/Rate.tsx new file mode 100644 index 000000000..b1c3a498f --- /dev/null +++ b/src/rate/Rate.tsx @@ -0,0 +1,148 @@ +import React, { MouseEvent, useState, useCallback } from 'react'; +import Tooltip from '../tooltip'; +import { TdRateProps } from './type'; +import useConfig from '../_util/useConfig'; +import useControlled from '../hooks/useControlled'; +import { rateDefaultProps } from './defaultProps'; + +const getFilledStarSvg = (size) => ( + + + +); + +const getFilledHalfStarSvg = (size) => ( + + + + + + + + + +); + +export type RateProps = TdRateProps; +const Rate = (props: RateProps) => { + const { + allowHalf, // 是否允许半选 + color, // 评分图标的颜色,样式中默认为 #ED7B2F。一个值表示设置选中高亮的五角星颜色,两个值表示分别设置 选中高亮的五角星颜色 和 未选中暗灰的五角星颜色。示例:['#ED7B2F', '#999999'] + count, // 评分的数量 + disabled, // 是否禁用评分 + gap, // 评分图标的间距 + showText, // 是否显示对应的辅助文字 + size, // 评分图标的大小,示例:`20` + texts, // 自定义评分等级对应的辅助文字。组件内置默认值为:['极差', '失望', '一般', '满意', '惊喜']。自定义值示例:['1分', '2分', '3分', '4分', '5分'] + onChange, + } = props; + const [starValue = 0, setStarValue] = useControlled(props, 'value', onChange); + + const [hoverValue = undefined, setHoverValue] = useState(undefined); + const rootRef = React.useRef(null); + const { classPrefix } = useConfig(); + + const getStarValue = (event: MouseEvent, index: number) => { + if (allowHalf) { + const rootNode = rootRef.current; + const { left } = rootNode.getBoundingClientRect(); + const firstStar = rootNode.firstChild as HTMLElement; + const { width } = firstStar.getBoundingClientRect(); + const { clientX } = event; + const starMiddle = width * (index - 0.5) + gap * (index - 1); + if (clientX - left < starMiddle) { + return index - 0.5; + } + if (clientX - left >= starMiddle) { + return index; + } + } else { + return index; + } + }; + + const mouseEnterHandler = (event: MouseEvent, index: number) => { + setHoverValue(getStarValue(event, index)); + }; + + const mouseLeaveHandler = () => { + setHoverValue(undefined); + }; + + const clickHandler = (event: MouseEvent, index: number) => { + setStarValue(getStarValue(event, index)); + }; + + const getStarStyle = useCallback( + (index: number, count: number, displayValue: number): React.CSSProperties => { + const filledColor = Array.isArray(color) ? color[0] : color || '#ED7B2F'; + const defaultColor = Array.isArray(color) ? color[1] : '#E7E7E7'; + return { + marginRight: index < count - 1 ? gap : '', + width: size, + height: size, + color: index < displayValue ? filledColor : defaultColor, + }; + }, + [size, color, gap], + ); + + const getStar = useCallback( + (allowHalf: boolean, index: number, displayValue: number) => { + if (allowHalf && index + 0.5 === displayValue) { + return getFilledHalfStarSvg(size); + } + if (index >= displayValue) { + return getFilledStarSvg(size); + } + if (index < displayValue) { + return getFilledStarSvg(size); + } + }, + [size], + ); + const displayValue = hoverValue || starValue; + return ( +
!disabled && mouseLeaveHandler()}> + {[...Array(count)].map((_, index) => + showText ? ( + +
!disabled && mouseEnterHandler(event, index + 1)} + onClick={(event) => !disabled && clickHandler(event, index + 1)} + style={getStarStyle(index, count, displayValue)} + className={`${classPrefix}-rate__wrapper`} + > + {getStar(allowHalf, index, displayValue)} +
+
+ ) : ( +
!disabled && mouseEnterHandler(event, index + 1)} + onClick={(event) => !disabled && clickHandler(event, index + 1)} + style={getStarStyle(index, count, displayValue)} + className={`${classPrefix}-rate__wrapper`} + > + {getStar(allowHalf, index, displayValue)} +
+ ), + )} + {showText &&
{texts[displayValue - 1]}
} +
+ ); +}; + +Rate.displayName = 'Rate'; +Rate.defaultProps = rateDefaultProps; + +export default Rate; diff --git a/src/rate/__tests__/__snapshots__/rate.test.tsx.snap b/src/rate/__tests__/__snapshots__/rate.test.tsx.snap new file mode 100644 index 000000000..b44b7b117 --- /dev/null +++ b/src/rate/__tests__/__snapshots__/rate.test.tsx.snap @@ -0,0 +1,1208 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Rate 组件测试 count 1`] = ` +
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+`; + +exports[`Rate 组件测试 create 1`] = ` +
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+`; + +exports[`base.jsx 1`] = ` + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+`; + +exports[`mulit.jsx 1`] = ` + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+`; + +exports[`size.jsx 1`] = ` + +
+
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+
+
+`; + +exports[`status.jsx 1`] = ` + +
+
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + + + + + + + +
+
+
+
+
+`; + +exports[`texts.jsx 1`] = ` + +
+
+
+ + + +
+
+
+
+ + + +
+
+
+
+ + + +
+
+
+
+ + + +
+
+
+
+ + + +
+
+
+ 满意 +
+
+
+`; diff --git a/src/rate/__tests__/rate.test.tsx b/src/rate/__tests__/rate.test.tsx new file mode 100644 index 000000000..6b6d79581 --- /dev/null +++ b/src/rate/__tests__/rate.test.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { testExamples, render, fireEvent } from '@test/utils'; +import Rate from '../Rate'; + +// 测试组件代码 Example 快照 +testExamples(__dirname); + +describe('Rate 组件测试', () => { + // 测试渲染 + test('create', async () => { + const { container } = render(); + expect(container.firstChild.classList.contains('t-rate')).toBeTruthy(); + expect(document.querySelectorAll('.t-rate__wrapper')).toHaveLength(5); + expect(container).toMatchSnapshot(); + }); + // 点击测试 + test('onChange', async () => { + const clickFn = jest.fn(); + render(); + fireEvent.click(document.querySelector('.t-rate__wrapper')); + expect(clickFn).toBeCalledTimes(1); + expect(clickFn).toBeCalledWith(1); + }); + // disable测试 + test('disable', async () => { + const clickFn = jest.fn(); + render(); + fireEvent.click(document.querySelector('.t-rate__wrapper')); + expect(clickFn).toBeCalledTimes(0); + }); + // 数量测试 + test('count', async () => { + const { container } = render(); + expect(document.querySelectorAll('.t-rate__wrapper')).toHaveLength(10); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/src/rate/_example/base.jsx b/src/rate/_example/base.jsx new file mode 100644 index 000000000..7e790676c --- /dev/null +++ b/src/rate/_example/base.jsx @@ -0,0 +1,6 @@ +import React from 'react'; +import { Rate } from 'tdesign-react'; + +export default function BasicRate() { + return ; +} diff --git a/src/rate/_example/mulit.jsx b/src/rate/_example/mulit.jsx new file mode 100644 index 000000000..0e0defb21 --- /dev/null +++ b/src/rate/_example/mulit.jsx @@ -0,0 +1,6 @@ +import React from 'react'; +import { Rate } from 'tdesign-react'; + +export default function BasicRate() { + return ; +} diff --git a/src/rate/_example/size.jsx b/src/rate/_example/size.jsx new file mode 100644 index 000000000..6138f29ad --- /dev/null +++ b/src/rate/_example/size.jsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { Space, Rate } from 'tdesign-react'; + +export default function BasicRate() { + return ( + + + + + + ); +} diff --git a/src/rate/_example/status.jsx b/src/rate/_example/status.jsx new file mode 100644 index 000000000..9d4268dfd --- /dev/null +++ b/src/rate/_example/status.jsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { Space, Rate } from 'tdesign-react'; + +export default function BasicRate() { + return ( + + + + + + ); +} diff --git a/src/rate/_example/texts.jsx b/src/rate/_example/texts.jsx new file mode 100644 index 000000000..d6f725cd1 --- /dev/null +++ b/src/rate/_example/texts.jsx @@ -0,0 +1,6 @@ +import React from 'react'; +import { Rate } from 'tdesign-react'; + +export default function BasicRate() { + return ; +} diff --git a/src/rate/defaultProps.ts b/src/rate/defaultProps.ts index b74b85c45..b7427516a 100644 --- a/src/rate/defaultProps.ts +++ b/src/rate/defaultProps.ts @@ -8,9 +8,10 @@ export const rateDefaultProps: TdRateProps = { defaultValue: 0, allowHalf: false, color: '#ED7B2F', + disabled: false, + size: '20', count: 5, gap: 6, showText: false, - texts: [], - value: 0, + texts: ['极差', '失望', '一般', '满意', '惊喜'], }; diff --git a/src/rate/index.ts b/src/rate/index.ts new file mode 100644 index 000000000..6787af2b3 --- /dev/null +++ b/src/rate/index.ts @@ -0,0 +1,9 @@ +import _Rate from './Rate'; + +import './style/index.js'; + +export type { RateProps } from './Rate'; +export * from './type'; + +export const Rate = _Rate; +export default Rate; diff --git a/src/rate/rate.md b/src/rate/rate.md index 419871d69..7a2bd2323 100644 --- a/src/rate/rate.md +++ b/src/rate/rate.md @@ -8,7 +8,7 @@ className | String | - | 类名 | N style | Object | - | 样式,TS 类型:`React.CSSProperties` | N allowHalf | Boolean | false | 是否允许半选 | N -color | String / Array | '#ED7B2F' | 评分图标的颜色,样式中默认为 #ED7B2F。一个值表示设置选中高亮的五角星颜色,示例:[选中颜色]。数组则表示分别设置 选中高亮的五角星颜色 和 未选中暗灰的五角星颜色,[选中颜色,未选中颜色]。示例:['#ED7B2F', '#E3E6EB']。TS 类型:`string | Array` | N +color | String / Array | '#ED7B2F' | 评分图标的颜色,样式中默认为 #ED7B2F。一个值表示设置选中高亮的五角星颜色,两个值表示分别设置 选中高亮的五角星颜色 和 未选中暗灰的五角星颜色。示例:['#ED7B2F', '#999999']。TS 类型:`string | Array` | N count | Number | 5 | 评分的数量 | N disabled | Boolean | false | 是否禁用评分 | N gap | Number | 6 | 评分图标的间距 | N diff --git a/src/rate/style/css.js b/src/rate/style/css.js new file mode 100644 index 000000000..6a9a4b132 --- /dev/null +++ b/src/rate/style/css.js @@ -0,0 +1 @@ +import './index.css'; diff --git a/src/rate/style/index.js b/src/rate/style/index.js new file mode 100644 index 000000000..9c1a0fb38 --- /dev/null +++ b/src/rate/style/index.js @@ -0,0 +1 @@ +import '../../_common/style/web/components/rate/_index.less'; diff --git a/src/rate/type.ts b/src/rate/type.ts index fa65fd2d6..2495a6da7 100644 --- a/src/rate/type.ts +++ b/src/rate/type.ts @@ -11,7 +11,7 @@ export interface TdRateProps { */ allowHalf?: boolean; /** - * 评分图标的颜色,样式中默认为 #ED7B2F。一个值表示设置选中高亮的五角星颜色,示例:[选中颜色]。数组则表示分别设置 选中高亮的五角星颜色 和 未选中暗灰的五角星颜色,[选中颜色,未选中颜色]。示例:['#ED7B2F', '#E3E6EB'] + * 评分图标的颜色,样式中默认为 #ED7B2F。一个值表示设置选中高亮的五角星颜色,两个值表示分别设置 选中高亮的五角星颜色 和 未选中暗灰的五角星颜色。示例:['#ED7B2F', '#999999'] * @default '#ED7B2F' */ color?: string | Array;