import { useRef, useState, useMemo, useCallback, useEffect } from 'react' import { cx } from '@emotion/css' const Slider = ({ value, min, max, onChange, onlyCallOnChangeAfterDragEnded = false, orientation = 'horizontal', alwaysShowThumb = false, }: { value: number min: number max: number onChange: (value: number) => void onlyCallOnChangeAfterDragEnded?: boolean orientation?: 'horizontal' | 'vertical' alwaysShowTrack?: boolean alwaysShowThumb?: boolean }) => { const sliderRef = useRef(null) const [isDragging, setIsDragging] = useState(false) const [draggingValue, setDraggingValue] = useState(value) const memoedValue = useMemo( () => isDragging && onlyCallOnChangeAfterDragEnded ? draggingValue : value, [isDragging, draggingValue, value, onlyCallOnChangeAfterDragEnded] ) /** * Get the value of the slider based on the position of the pointer */ const getNewValue = useCallback( (pointer: { x: number; y: number }) => { if (!sliderRef?.current) return 0 const slider = sliderRef.current.getBoundingClientRect() const newValue = orientation === 'horizontal' ? ((pointer.x - slider.x) / slider.width) * max : ((slider.height - (pointer.y - slider.y)) / slider.height) * max if (newValue < min) return min if (newValue > max) return max return newValue }, [sliderRef, max, min, orientation] ) /** * Handle slider click event */ const handleClick = useCallback( (e: React.MouseEvent) => onChange(getNewValue({ x: e.clientX, y: e.clientY })), [getNewValue, onChange] ) /** * Handle pointer down event */ const handlePointerDown = () => { setIsDragging(true) } /** * Handle pointer move events */ useEffect(() => { const handlePointerMove = (e: { clientX: number; clientY: number }) => { if (!isDragging) return const newValue = getNewValue({ x: e.clientX, y: e.clientY }) onlyCallOnChangeAfterDragEnded ? setDraggingValue(newValue) : onChange(newValue) } document.addEventListener('pointermove', handlePointerMove) return () => { document.removeEventListener('pointermove', handlePointerMove) } }, [ isDragging, onChange, setDraggingValue, onlyCallOnChangeAfterDragEnded, getNewValue, ]) /** * Handle pointer up events */ useEffect(() => { const handlePointerUp = () => { if (!isDragging) return setIsDragging(false) if (onlyCallOnChangeAfterDragEnded) { onChange(draggingValue) } } document.addEventListener('pointerup', handlePointerUp) return () => { document.removeEventListener('pointerup', handlePointerUp) } }, [ isDragging, setIsDragging, onlyCallOnChangeAfterDragEnded, draggingValue, onChange, ]) /** * Track and thumb styles */ const usedTrackStyle = useMemo(() => { const percentage = `${(memoedValue / max) * 100}%` return orientation === 'horizontal' ? { width: percentage } : { height: percentage } }, [max, memoedValue, orientation]) const thumbStyle = useMemo(() => { const percentage = `${(memoedValue / max) * 100}%` return orientation === 'horizontal' ? { left: percentage } : { bottom: percentage } }, [max, memoedValue, orientation]) return (
{/* Track */}
{/* Passed track */}
{/* Thumb */}
e.stopPropagation()} onPointerDown={handlePointerDown} >
) } export default Slider