import React from 'react' import { useEvent } from './useEvent' import useThumb from './useThumb' type Props = { step: number; range: [number, number]; minimumRange: number; minimumValue: number; maximumValue: number; slideOnTap?: boolean; crossingAllowed?: boolean; onValueChange?: (range: [number, number]) => boolean | void; } /** Handle the state of a range slider */ const useRange = ({ step, range: propValue, minimumRange, minimumValue, maximumValue, slideOnTap, onValueChange, crossingAllowed }: Props) => { const [minProp, maxProp] = propValue const minRef = React.useRef(minProp) const maxRef = React.useRef(maxProp) // When updating the props, we immediately apply the change to the refs in order to have the correct values in useThumb // Using a useEffect would make the computation happen too late. Comparing the values between render is the only way. // With this code, changing the props will overwrite the internal state, which is likely the expected behavior. const memory = React.useRef<{ min: number, max: number, minValue: number, maxValue: number }>({ min: minimumValue, max: maximumValue, minValue: minProp, maxValue: maxProp }) if (memory.current.min !== minimumValue || memory.current.minValue !== minProp) { memory.current.min = minimumValue memory.current.minValue = minProp minRef.current = Math.max(minimumValue, minProp) } if (memory.current.max !== maximumValue || memory.current.maxValue !== maxProp) { memory.current.max = maximumValue memory.current.maxValue = maxProp maxRef.current = Math.min(maximumValue, maxProp) } const onMinChange = useEvent((min: number) => { minRef.current = min return onValueChange?.([min, maxRef.current].sort((a, b) => a - b) as [number, number]) }) const onMaxChange = useEvent((max: number) => { maxRef.current = max return onValueChange?.([minRef.current, max].sort((a, b) => a - b) as [number, number]) }) // Min value thumb const { updateValue: updateMinValue, canMove: canMoveMin, value: minValue } = useThumb({ minimumValue, maximumValue: Math.max(minimumValue, maxRef.current - minimumRange), value: minProp, step, slideOnTap, onValueChange: onMinChange }) // Max value thumb const { updateValue: updateMaxValue, canMove: canMoveMax, value: maxValue } = useThumb({ minimumValue: Math.min(maximumValue, minRef.current + minimumRange), maximumValue, value: maxProp, step, slideOnTap, onValueChange: onMaxChange }) const range = React.useMemo(() => [minValue, maxValue].sort((a, b) => a - b) as [number, number], [maxValue, minValue]) const currentThumb = React.useRef<'min' | 'max'>() // Method to update the lower or higher bound according to which one is the closest const updateClosestValue = useEvent((value: number, state: 'press' | 'release' | 'drag') => { let isMinClosest = false // When moving a thumb, we don't want to let it cross the other thumb if (currentThumb.current && !crossingAllowed) isMinClosest = currentThumb.current === 'min' else if (!currentThumb.current) isMinClosest = Math.abs(value - minValue) < Math.abs(value - maxValue) || (minValue === maxValue && value < minValue) // if the current thumb is the min, we keep it as long as it's below the max else if (currentThumb.current === 'min') isMinClosest = value <= maxValue // Otherwise, if we hold the max thumb, we switch only if the value is below the min else isMinClosest = value < minValue // We update the state accordingly isMinClosest ? updateMinValue(value) : updateMaxValue(value) const newThumb = isMinClosest ? 'min' : 'max' // We set the thumb being currently moved // When the 2 thumbs cross, we set the other thumb to the max possible value if (state === 'drag' && newThumb !== currentThumb.current) isMinClosest ? updateMaxValue(minValue) : updateMinValue(maxValue) currentThumb.current = state === 'release' ? undefined : newThumb // We release the thumb, or keep maintaining it return isMinClosest ? [value, maxValue] : [minValue, value] }) const canMove = useEvent((value: number) => { return canMoveMax(value) || canMoveMin(value) }) return { updateMinValue, updateMaxValue, updateClosestValue, canMove, range } } export default useRange