177 lines
4.8 KiB
TypeScript
Raw Normal View History

2022-05-12 02:45:43 +08:00
import { useRef, useState, useMemo, useCallback, useEffect } from 'react'
2022-05-29 17:53:27 +08:00
import { cx } from '@emotion/css'
2022-05-12 02:45:43 +08:00
2022-03-13 14:40:38 +08:00
const Slider = ({
value,
min,
max,
onChange,
onlyCallOnChangeAfterDragEnded = false,
orientation = 'horizontal',
2022-05-29 17:53:27 +08:00
alwaysShowThumb = false,
2022-03-13 14:40:38 +08:00
}: {
value: number
min: number
max: number
onChange: (value: number) => void
onlyCallOnChangeAfterDragEnded?: boolean
orientation?: 'horizontal' | 'vertical'
2022-05-29 17:53:27 +08:00
alwaysShowTrack?: boolean
alwaysShowThumb?: boolean
2022-03-13 14:40:38 +08:00
}) => {
const sliderRef = useRef<HTMLInputElement>(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(
2022-05-29 17:53:27 +08:00
(pointer: { x: number; y: number }) => {
2022-03-13 14:40:38 +08:00
if (!sliderRef?.current) return 0
2022-05-29 17:53:27 +08:00
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
2022-03-13 14:40:38 +08:00
if (newValue < min) return min
if (newValue > max) return max
return newValue
},
2022-05-29 17:53:27 +08:00
[sliderRef, max, min, orientation]
2022-03-13 14:40:38 +08:00
)
/**
* Handle slider click event
*/
const handleClick = useCallback(
2022-05-29 17:53:27 +08:00
(e: React.MouseEvent<HTMLDivElement>) =>
onChange(getNewValue({ x: e.clientX, y: e.clientY })),
2022-03-13 14:40:38 +08:00
[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
2022-05-29 17:53:27 +08:00
const newValue = getNewValue({ x: e.clientX, y: e.clientY })
2022-03-13 14:40:38 +08:00
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
*/
2022-05-29 17:53:27 +08:00
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])
2022-03-13 14:40:38 +08:00
return (
<div
2022-05-29 17:53:27 +08:00
className={cx(
'group relative flex items-center',
orientation === 'horizontal' && 'h-2',
orientation === 'vertical' && 'h-full w-2 flex-col'
)}
2022-03-13 14:40:38 +08:00
ref={sliderRef}
onClick={handleClick}
>
{/* Track */}
2022-05-29 17:53:27 +08:00
<div
className={cx(
2022-10-28 20:29:04 +08:00
'absolute overflow-hidden rounded-full bg-black/10 bg-opacity-10 dark:bg-white/10',
orientation === 'horizontal' && 'h-[3px] w-full',
orientation === 'vertical' && 'h-full w-[3px]'
2022-05-29 17:53:27 +08:00
)}
2022-10-28 20:29:04 +08:00
>
{/* Passed track */}
<div
className={cx(
'bg-black dark:bg-white',
orientation === 'horizontal' && 'h-full rounded-r-full',
orientation === 'vertical' && 'bottom-0 w-full rounded-t-full'
)}
style={usedTrackStyle}
></div>
</div>
2022-03-13 14:40:38 +08:00
{/* Thumb */}
<div
2022-05-12 02:45:43 +08:00
className={cx(
2022-10-28 20:29:04 +08:00
'absolute flex h-2 w-2 items-center justify-center rounded-full bg-black bg-opacity-20 transition-opacity dark:bg-white',
2022-05-29 17:53:27 +08:00
isDragging || alwaysShowThumb
? 'opacity-100'
: 'opacity-0 group-hover:opacity-100',
2022-10-28 20:29:04 +08:00
orientation === 'horizontal' && '-translate-x-1',
orientation === 'vertical' && 'translate-y-1'
2022-03-13 14:40:38 +08:00
)}
style={thumbStyle}
onClick={e => e.stopPropagation()}
onPointerDown={handlePointerDown}
2022-10-28 20:29:04 +08:00
></div>
2022-03-13 14:40:38 +08:00
</div>
)
}
export default Slider