import React, { RefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react' import useTheme from '../use-theme' import withDefaults from '../utils/with-defaults' import useDrag, { DraggingEvent } from '../utils/use-drag' import useCurrentState from '../utils/use-current-state' import SliderDot from './slider-dot' import SliderMark from './slider-mark' interface Props { value?: number initialValue?: number step?: number max?: number min?: number disabled?: boolean showMarkers?: boolean onChange?: (val: number) => void className?: string } const defaultProps = { initialValue: 0, step: 1, min: 0, max: 100, disabled: false, showMarkers: false, className: '' } type NativeAttrs = Omit, keyof Props> export type SliderProps = Props & typeof defaultProps & NativeAttrs const getRefWidth = (elementRef: RefObject | null): number => { if (!elementRef || !elementRef.current) return 0 const rect = elementRef.current.getBoundingClientRect() return rect.width || rect.right - rect.left } const getValue = ( max: number, min: number, step: number, offsetX: number, railWidth: number ): number => { if (offsetX < 0) return min if (offsetX > railWidth) return max const widthForEachStep = (railWidth / (max - min)) * step if (widthForEachStep <= 0) return min const slideDistance = Math.round(offsetX / widthForEachStep) * step + min return Number.isInteger(slideDistance) ? slideDistance : Number.parseFloat(slideDistance.toFixed(1)) } const Slider: React.FC> = ({ disabled, step, max, min, initialValue, value: customValue, onChange, className, showMarkers, ...props }) => { const theme = useTheme() const [value, setValue] = useState(initialValue) const [, setSliderWidth, sideWidthRef] = useCurrentState(0) const [, setLastDargOffset, lastDargOffsetRef] = useCurrentState(0) const [isClick, setIsClick] = useState(false) const sliderRef = useRef(null) const dotRef = useRef(null) const currentRatio = useMemo(() => ((value - min) / (max - min)) * 100, [value, max, min]) const setLastOffsetManually = (val: number) => { const width = getRefWidth(sliderRef) const shouldOffset = ((val - min) / (max - min)) * width setLastDargOffset(shouldOffset) } const updateValue = useCallback( (offset: number) => { const currentValue = getValue(max, min, step, offset, sideWidthRef.current) setValue(currentValue) onChange && onChange(currentValue) }, [max, min, step, sideWidthRef] ) const dragHandler = (event: DraggingEvent) => { if (disabled) return const currentOffset = event.currentX - event.startX const offset = currentOffset + lastDargOffsetRef.current updateValue(offset) } const dragStartHandler = () => { setIsClick(false) setSliderWidth(getRefWidth(sliderRef)) } const dragEndHandler = (event: DraggingEvent) => { if (disabled) return const offset = event.currentX - event.startX const currentOffset = offset + lastDargOffsetRef.current const boundOffset = currentOffset < 0 ? 0 : Math.min(currentOffset, sideWidthRef.current) setLastDargOffset(boundOffset) } const clickHandler = (event: React.MouseEvent) => { if (disabled) return if (!sliderRef || !sliderRef.current) return setIsClick(true) setSliderWidth(getRefWidth(sliderRef)) const clickOffset = event.clientX - sliderRef.current.getBoundingClientRect().x setLastDargOffset(clickOffset) updateValue(clickOffset) } useDrag(dotRef, dragHandler, dragStartHandler, dragEndHandler) useEffect(() => { if (customValue === undefined) return if (customValue === value) return setValue(customValue) }, [customValue, value]) useEffect(() => { initialValue && setLastOffsetManually(initialValue) }, []) return (
{value} {showMarkers && }
) } export default withDefaults(Slider, defaultProps)