diff --git a/assets/index.less b/assets/index.less index 62ca71c..9d13f1f 100644 --- a/assets/index.less +++ b/assets/index.less @@ -45,11 +45,6 @@ float: right; } - &-first, - &-second { - transition: all .3s; - } - &-focused, &:hover { transform: scale(1.1); } @@ -58,10 +53,11 @@ position: absolute; left: 0; top: 0; - width: 50%; + width: 0%; height: 100%; overflow: hidden; - opacity: 0; + opacity: 1; + color: @rate-star-color; .@{rate-prefix-cls}-rtl & { right: 0; @@ -73,16 +69,6 @@ &-half &-second { opacity: 1; } - - &-half &-first, - &-full &-second { - color: @rate-star-color; - } - - &-half:hover &-first, - &-full:hover &-second { - color: tint(@rate-star-color,30%); - } } } diff --git a/docs/examples/simple.tsx b/docs/examples/simple.tsx index f550b66..9b35ae9 100644 --- a/docs/examples/simple.tsx +++ b/docs/examples/simple.tsx @@ -1,6 +1,6 @@ /* eslint no-console: 0 */ -import React from 'react'; import Rate from 'rc-rate'; +import React from 'react'; import '../../assets/index.less'; function onChange(v: number) { @@ -59,5 +59,14 @@ export default () => ( style={{ fontSize: 50, marginTop: 24 }} character={} /> +

Precision

+ } + /> ); diff --git a/src/Rate.tsx b/src/Rate.tsx index 46602ba..88e7782 100644 --- a/src/Rate.tsx +++ b/src/Rate.tsx @@ -6,7 +6,7 @@ import React from 'react'; import type { StarProps } from './Star'; import Star from './Star'; import useRefs from './useRefs'; -import { getOffsetLeft } from './util'; +import { clamp, getBoundingClientRect, roundValueToPrecision } from './util'; export interface RateProps extends Pick { @@ -32,6 +32,10 @@ export interface RateProps * @default true */ keyboard?: boolean; + /** + * Define the minimum increment value change + */ + precision?: number; } export interface RateRef { @@ -49,9 +53,14 @@ function Rate(props: RateProps, ref: React.Ref) { defaultValue, value: propValue, count = 5, + /** + * allow half star + * @deprecated Since precision has been implemented, `allowHalf` is no longer needed. + */ allowHalf = false, allowClear = true, keyboard = true, + precision = 1, // Display character = '★', @@ -76,6 +85,8 @@ function Rate(props: RateProps, ref: React.Ref) { const [getStarRef, setStarRef] = useRefs(); const rateRef = React.useRef(null); + const mergedPrecision = allowHalf ? 0.5 : precision > 0 ? precision : 1; + const reverse = direction === 'rtl'; // ============================ Ref ============================= const triggerFocus = () => { @@ -99,19 +110,36 @@ function Rate(props: RateProps, ref: React.Ref) { }); const [cleanedValue, setCleanedValue] = useMergedState(null); - const getStarValue = (index: number, x: number) => { - const reverse = direction === 'rtl'; - let starValue = index + 1; - if (allowHalf) { - const starEle = getStarRef(index); - const leftDis = getOffsetLeft(starEle); - const width = starEle.clientWidth; - if (reverse && x - leftDis > width / 2) { - starValue -= 0.5; - } else if (!reverse && x - leftDis < width / 2) { - starValue -= 0.5; - } + const calculatePercentage = (delta: number, left: number, right: number) => { + return (reverse ? right - delta : delta - left) / (right - left); + }; + + const dealWithMergedPrecision = (delta: number) => { + const rootNode = rateRef.current; + const { right, left } = getBoundingClientRect(rootNode); + const percentage = calculatePercentage(delta, left, right); + + let newHover = roundValueToPrecision(count * percentage + mergedPrecision / 2, mergedPrecision); + newHover = clamp(newHover, mergedPrecision, count); + return newHover; + }; + + const getStarValue = (index: number, delta: number) => { + let starValue = index; + + const starEle = getStarRef(index); + const { left, right } = getBoundingClientRect(starEle); + if (!left || !right) return allowHalf ? starValue + 0.5 : starValue + 1; + + const percentage = calculatePercentage(delta, left, right); + let roundedValue = roundValueToPrecision(percentage + mergedPrecision / 2, mergedPrecision); + roundedValue = clamp(roundedValue, mergedPrecision, 1); + starValue += roundedValue; + + if (mergedPrecision > 1) { + return dealWithMergedPrecision(delta); } + return starValue; }; @@ -138,7 +166,7 @@ function Rate(props: RateProps, ref: React.Ref) { const [hoverValue, setHoverValue] = React.useState(null); const onHover = (event: React.MouseEvent, index: number) => { - const nextHoverValue = getStarValue(index, event.pageX); + const nextHoverValue = getStarValue(index, event.clientX); if (nextHoverValue !== cleanedValue) { setHoverValue(nextHoverValue); setCleanedValue(null); @@ -159,7 +187,7 @@ function Rate(props: RateProps, ref: React.Ref) { // =========================== Click ============================ const onClick = (event: React.MouseEvent | React.KeyboardEvent, index: number) => { - const newValue = getStarValue(index, (event as React.MouseEvent).pageX); + const newValue = getStarValue(index, (event as React.MouseEvent).clientX); let isReset = false; if (allowClear) { isReset = newValue === value; @@ -171,8 +199,7 @@ function Rate(props: RateProps, ref: React.Ref) { const onInternalKeyDown: React.KeyboardEventHandler = (event) => { const { keyCode } = event; - const reverse = direction === 'rtl'; - const step = allowHalf ? 0.5 : 1; + const step = mergedPrecision; if (keyboard) { if (keyCode === KeyCode.RIGHT && value < count && !reverse) { diff --git a/src/Star.tsx b/src/Star.tsx index 7243069..ce0fb20 100644 --- a/src/Star.tsx +++ b/src/Star.tsx @@ -1,6 +1,6 @@ -import React from 'react'; -import KeyCode from 'rc-util/lib/KeyCode'; import classNames from 'classnames'; +import KeyCode from 'rc-util/lib/KeyCode'; +import React from 'react'; export interface StarProps { value?: number; @@ -73,7 +73,9 @@ function Star(props: StarProps, ref: React.Ref) { classNameList.add(`${prefixCls}-focused`); } } - + const memoWidth = React.useMemo(() => { + return Math.max(0, Math.min((value - index) * 100, 100)); + }, [index, value]); // >>>>> Node const characterNode = typeof character === 'function' ? character(props) : character; let start: React.ReactNode = ( @@ -88,7 +90,14 @@ function Star(props: StarProps, ref: React.Ref) { aria-setsize={count} tabIndex={disabled ? -1 : 0} > -
{characterNode}
+
+ {characterNode} +
{characterNode}
diff --git a/src/util.ts b/src/util.ts index 1bc62e2..744a4d8 100644 --- a/src/util.ts +++ b/src/util.ts @@ -38,3 +38,29 @@ export function getOffsetLeft(el: HTMLElement) { pos.left += getScroll(w); return pos.left; } + +export const getBoundingClientRect = (element: HTMLElement) => { + return element.getBoundingClientRect(); +}; + +function getDecimalPrecision(value: number) { + const decimalPart = value.toString().split('.')[1]; + return decimalPart ? decimalPart.length : 0; +} + +export function roundValueToPrecision(value: number, precision: number) { + if (value == null) { + return value; + } + + const nearest = Math.round(value / precision) * precision; + return Number(nearest.toFixed(getDecimalPrecision(precision))); +} + +export function clamp( + val: number, + min: number = Number.MIN_SAFE_INTEGER, + max: number = Number.MAX_SAFE_INTEGER, +): number { + return Math.max(min, Math.min(val, max)); +} diff --git a/tests/__snapshots__/simple.spec.js.snap b/tests/__snapshots__/simple.spec.js.snap index 43deb96..9617d0b 100644 --- a/tests/__snapshots__/simple.spec.js.snap +++ b/tests/__snapshots__/simple.spec.js.snap @@ -17,6 +17,7 @@ exports[`rate allowHalf render works 1`] = ` >
@@ -39,6 +40,7 @@ exports[`rate allowHalf render works 1`] = ` >
@@ -61,6 +63,7 @@ exports[`rate allowHalf render works 1`] = ` >
@@ -91,6 +94,7 @@ exports[`rate allowHalf render works in RTL 1`] = ` >
@@ -113,6 +117,7 @@ exports[`rate allowHalf render works in RTL 1`] = ` >
@@ -135,6 +140,7 @@ exports[`rate allowHalf render works in RTL 1`] = ` >
@@ -165,6 +171,7 @@ exports[`rate allowHalf render works more than half 1`] = ` >
@@ -187,6 +194,7 @@ exports[`rate allowHalf render works more than half 1`] = ` >
@@ -209,6 +217,7 @@ exports[`rate allowHalf render works more than half 1`] = ` >
@@ -239,6 +248,7 @@ exports[`rate full render works 1`] = ` >
@@ -261,6 +271,7 @@ exports[`rate full render works 1`] = ` >
@@ -283,6 +294,7 @@ exports[`rate full render works 1`] = ` >
@@ -313,6 +325,7 @@ exports[`rate full render works in RTL 1`] = ` >
@@ -335,6 +348,7 @@ exports[`rate full render works in RTL 1`] = ` >
@@ -357,6 +371,7 @@ exports[`rate full render works in RTL 1`] = ` >
@@ -387,6 +402,7 @@ exports[`rate full render works with character function 1`] = ` >
1
@@ -409,6 +425,7 @@ exports[`rate full render works with character function 1`] = ` >
2
@@ -431,6 +448,7 @@ exports[`rate full render works with character function 1`] = ` >
3
@@ -461,6 +479,7 @@ exports[`rate full render works with character node 1`] = ` >
1
@@ -483,6 +502,7 @@ exports[`rate full render works with character node 1`] = ` >
1
@@ -505,6 +525,7 @@ exports[`rate full render works with character node 1`] = ` >
1
diff --git a/tests/simple.spec.js b/tests/simple.spec.js index 77c6271..b10afc8 100644 --- a/tests/simple.spec.js +++ b/tests/simple.spec.js @@ -1,6 +1,6 @@ -import React from 'react'; -import { render, mount } from 'enzyme'; +import { mount, render } from 'enzyme'; import KeyCode from 'rc-util/lib/KeyCode'; +import React from 'react'; import Rate from '../src'; describe('rate', () => { @@ -319,12 +319,7 @@ describe('rate', () => { const mockChange = jest.fn(); const mockKeyDown = jest.fn(); const wrapper = mount( - + , ); wrapper.simulate('keyDown', { keyCode: KeyCode.LEFT }); expect(mockChange).not.toHaveBeenCalled(); @@ -344,4 +339,21 @@ describe('rate', () => { expect(wrapper.getDOMNode().getAttribute('id')).toBe('myrate'); }); }); + + describe('precision', () => { + it('support precision', () => { + const wrapper = mount(); + + expect(wrapper.find('li > div > .rc-rate-star-first').at(1).prop('style').width).toBe('100%'); + }); + + it('should show the number of stars at the current precision', () => { + const wrapper = mount(); + + const width = parseFloat( + wrapper.find('li > div > .rc-rate-star-first').at(2).prop('style').width, + ); + expect(width).toBeCloseTo(20); + }); + }); });