import { CSSProperties, ReactNode, useMemo, useRef, useState, useEffect, } from 'react' import classNames from 'classnames' import { CommonComponentProps } from '../../utils/types' import { Icon } from '../icon/Icon' import './Rate.scss' import { useEvent, useSelectorId, useStrike } from '../../use' import { PAN_START, PAN_MOVE, TAP, StrikePanEvent } from '../../strike' import { getBoundingClientRect } from '../../utils/dom' export interface RateProps extends CommonComponentProps { className?: string style?: CSSProperties children?: ReactNode value?: number defaultValue?: number allowHalf?: boolean allowClear?: boolean count?: number size?: number | string spacing?: number | string icon?: ReactNode voidIcon?: ReactNode color?: string voidColor?: string disabled?: boolean onChange?: (value: number) => void } export function Rate(props: RateProps) { const { className, style, children, value, defaultValue, allowHalf, allowClear = true, count = 5, size, spacing, icon, voidIcon, color, voidColor, disabled, onChange, ...restProps } = props const rateId = useSelectorId() const [innerValue, setInnerValue] = useState(() => value ?? defaultValue ?? 0) // 受控 useEffect(() => { if (value != null) { setInnerValue(value) } }, [value]) const itemsBoundary = useRef<[number, number][]>([]) const panStartLeft = useRef(0) const setValueByCurrent = (current: number) => { if (current === innerValue) { return } // 非受控 if (value == null) { setInnerValue(current) } onChange?.(current) } const handlePanstart = useEvent(() => { if (disabled) { return } getBoundingClientRect('#' + rateId, (rect) => { panStartLeft.current = rect.left }) itemsBoundary.current = [] Array(count) .fill(0) .map((_, index) => { getBoundingClientRect('#' + rateId + '_' + index, (rect) => { itemsBoundary.current[index] = [rect.left, rect.right] }) }) }) const handlePanChange = ({ x }: StrikePanEvent) => { if (disabled) { return } const offsetX = x - panStartLeft.current const boundaries = itemsBoundary.current if (offsetX < 0) { setValueByCurrent(0) return } for (let i = boundaries.length - 1; i >= 0; i--) { const [left, right] = boundaries[i] if (x >= left) { setValueByCurrent(i + (allowHalf && x < (right + left) / 2 ? 0.5 : 1)) return } } } const handlePanmove = useEvent((event: StrikePanEvent) => { handlePanChange(event) }) const handleTap = useEvent((event: StrikePanEvent) => { handlePanChange(event) }) const rateBinding = useStrike( (strike) => { strike.on(PAN_START, handlePanstart) strike.on(PAN_MOVE, handlePanmove) strike.on(TAP, handleTap) }, { pan: true, direction: 'horizontal', lockDirection: true, tap: true, } ) const rateClass = classNames( 's-rate', { 's-rate-disabled': disabled, }, className ) const rateStyle = { fontSize: size, ...style, } return (
{Array(count) .fill(0) .map((_, index) => { const itemValue = index + 1 const diff = itemValue - innerValue return (
{voidIcon ?? }
{diff < 1 && (
0 ? (innerValue % 1) + 'em' : '', color: color, }} > {icon ?? }
)}
) })}
) } export default Rate