137 lines
5.4 KiB
TypeScript
137 lines
5.4 KiB
TypeScript
'use client'
|
|
|
|
import * as React from 'react'
|
|
import * as SliderPrimitive from '@radix-ui/react-slider'
|
|
|
|
import { ny } from '@/lib/utils'
|
|
|
|
interface SliderProps extends React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> {
|
|
showSteps?: 'none' | 'half' | 'full'
|
|
formatLabel?: (value: number) => string
|
|
formatLabelSide?: string
|
|
}
|
|
|
|
const Slider = React.forwardRef<
|
|
React.ElementRef<typeof SliderPrimitive.Root>,
|
|
SliderProps
|
|
>(({ className, showSteps = 'none', formatLabel, formatLabelSide = 'top', ...props }, ref) => {
|
|
const { min = 0, max = 100, step = 1, orientation = 'horizontal', value, defaultValue, onValueChange } = props
|
|
const [hoveredThumbIndex, setHoveredThumbIndex] = React.useState<boolean>(false)
|
|
const numberOfSteps = Math.floor((max - min) / step)
|
|
const stepLines = Array.from({ length: numberOfSteps }, (_, index) => index * step + min)
|
|
|
|
const initialValue = Array.isArray(value) ? value : (Array.isArray(defaultValue) ? defaultValue : [min, max])
|
|
const [localValues, setLocalValues] = React.useState<number[]>(initialValue)
|
|
|
|
React.useEffect(() => {
|
|
if (!isEqual(value, localValues))
|
|
setLocalValues(Array.isArray(value) ? value : (Array.isArray(defaultValue) ? defaultValue : [min, max]))
|
|
}, [min, max, value])
|
|
|
|
const handleValueChange = (newValues: number[]) => {
|
|
setLocalValues(newValues)
|
|
if (onValueChange)
|
|
onValueChange(newValues)
|
|
}
|
|
|
|
function isEqual(array1: number[] | undefined, array2: number[] | undefined) {
|
|
array1 = array1 ?? []
|
|
array2 = array2 ?? []
|
|
|
|
if (array1.length !== array2.length)
|
|
return false
|
|
|
|
for (let i = 0; i < array1.length; i++) {
|
|
if (array1[i] !== array2[i])
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
return (
|
|
<SliderPrimitive.Root
|
|
ref={ref}
|
|
className={ny(
|
|
'relative flex cursor-pointer touch-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
|
orientation === 'horizontal' ? 'w-full items-center' : 'h-full justify-center',
|
|
className,
|
|
)}
|
|
min={min}
|
|
max={max}
|
|
step={step}
|
|
value={localValues}
|
|
onValueChange={value => handleValueChange(value)}
|
|
{...props}
|
|
onFocus={() => setHoveredThumbIndex(true)}
|
|
onBlur={() => setHoveredThumbIndex(false)}
|
|
>
|
|
<SliderPrimitive.Track className={ny(
|
|
'bg-primary/20 relative grow overflow-hidden rounded-full',
|
|
orientation === 'horizontal' ? 'h-1.5 w-full' : 'h-full w-1.5',
|
|
)}
|
|
>
|
|
<SliderPrimitive.Range className={ny(
|
|
'bg-primary absolute',
|
|
orientation === 'horizontal' ? 'h-full' : 'w-full',
|
|
)}
|
|
/>
|
|
{showSteps !== undefined && showSteps !== 'none' && stepLines.map((value, index) => {
|
|
if (value === min || value === max)
|
|
return null
|
|
|
|
const positionPercentage = ((value - min) / (max - min)) * 100
|
|
const adjustedPosition = 50 + (positionPercentage - 50) * 0.96
|
|
return (
|
|
<div
|
|
key={index}
|
|
className={ny(
|
|
{ 'w-0.5 h-2': orientation !== 'vertical', 'w-2 h-0.5': orientation === 'vertical' },
|
|
'bg-muted-foreground absolute',
|
|
{
|
|
'left-1': orientation === 'vertical' && showSteps === 'half',
|
|
'top-1': orientation !== 'vertical' && showSteps === 'half',
|
|
'left-0': orientation === 'vertical' && showSteps === 'full',
|
|
'top-0': orientation !== 'vertical' && showSteps === 'full',
|
|
'-translate-x-1/2': orientation !== 'vertical',
|
|
'-translate-y-1/2': orientation === 'vertical',
|
|
},
|
|
)}
|
|
style={{
|
|
[orientation === 'vertical' ? 'bottom' : 'left']: `${adjustedPosition}%`,
|
|
}}
|
|
/>
|
|
)
|
|
})}
|
|
|
|
</SliderPrimitive.Track>
|
|
{localValues.map((numberStep, index) => (
|
|
<SliderPrimitive.Thumb
|
|
key={index}
|
|
className={ny(
|
|
'border-primary/50 bg-background focus-visible:ring-ring block size-4 rounded-full border shadow transition-colors focus-visible:outline-none focus-visible:ring-1',
|
|
)}
|
|
>
|
|
{hoveredThumbIndex && formatLabel && (
|
|
<div
|
|
className={ny(
|
|
{ 'bottom-8 left-1/2 -translate-x-1/2': formatLabelSide === 'top' },
|
|
{ 'top-8 left-1/2 -translate-x-1/2': formatLabelSide === 'bottom' },
|
|
{ 'right-8 -translate-y-1/4': formatLabelSide === 'left' },
|
|
{ 'left-8 -translate-y-1/4': formatLabelSide === 'right' },
|
|
'bg-popover text-popover-foreground absolute z-30 w-max items-center justify-items-center rounded-md border px-2 py-1 text-center shadow-sm',
|
|
)}
|
|
>
|
|
{formatLabel(numberStep)}
|
|
</div>
|
|
)}
|
|
</SliderPrimitive.Thumb>
|
|
))}
|
|
</SliderPrimitive.Root>
|
|
)
|
|
})
|
|
|
|
Slider.displayName = SliderPrimitive.Root.displayName
|
|
|
|
export { Slider }
|