UNPKG

@re-flex/ui

Version:
255 lines (254 loc) 9.56 kB
import React from "react"; const getBoundingClientRect = (element) => { const rect = element.getBoundingClientRect(); return { left: Math.ceil(rect.left), top: Math.ceil(rect.bottom), width: Math.ceil(rect.width), height: Math.ceil(rect.height), }; }; const sortNumList = (arr) => [...arr].sort((a, b) => Number(a) - Number(b)); const useGetLatest = (val) => { const ref = React.useRef(val); ref.current = val; return React.useCallback(() => ref.current, []); }; const linearInterpolator = { getPercentageForValue: (val, min, max) => { return Math.max(0, Math.min(100, ((val - min) / (max - min)) * 100)); }, getValueForClient: (client, trackDims, min, max, mode) => { const { left, width, top, height } = trackDims; const percentageValue = mode === "vertical" ? (top - client) / height : (client - left) / width; const value = (max - min) * percentageValue; return value + min; }, }; function useRanger({ trackElRef, interpolator = linearInterpolator, tickSize = 10, values, min, max, ticks: controlledTicks, steps, onChange, onDrag, stepSize, mode, }) { const [activeHandleIndex, setActiveHandleIndex] = React.useState(); const [tempValues, setTempValues] = React.useState(); const getLatest = useGetLatest({ activeHandleIndex, onChange, onDrag, values, tempValues, }); const axis = mode === "vertical" ? "clientY" : "clientX"; const getValueForClient = React.useCallback((client) => { if (!trackElRef.current) return; const trackDims = getBoundingClientRect(trackElRef.current); return interpolator.getValueForClient(client, trackDims, min, max, mode); }, [interpolator, max, min, trackElRef]); const getNextStep = React.useCallback((val, direction) => { if (steps) { let currIndex = steps.indexOf(val); let nextIndex = currIndex + direction; if (nextIndex >= 0 && nextIndex < steps.length) { return steps[nextIndex]; } else { return val; } } else { let nextVal = val + stepSize * direction; if (nextVal >= min && nextVal <= max) { return nextVal; } else { return val; } } }, [max, min, stepSize, steps]); const roundToStep = React.useCallback((val) => { let left = min; let right = max; if (steps) { steps.forEach((step) => { if (step <= val && step > left) { left = step; } if (step >= val && step < right) { right = step; } }); } else { while (left < val && left + stepSize < val) { left += stepSize; } right = Math.min(left + stepSize, max); } if (val - left < right - val) { return left; } return right; }, [max, min, stepSize, steps]); const handleDrag = React.useCallback((e) => { const { activeHandleIndex, onDrag } = getLatest(); let client = 0; if (e instanceof TouchEvent) { client = e.changedTouches[0][axis]; } else { client = e[axis]; } const newValue = getValueForClient(client); const newRoundedValue = roundToStep(newValue); const newValues = [ ...values.slice(0, activeHandleIndex), newRoundedValue, ...values.slice(activeHandleIndex + 1), ]; if (onDrag) { onDrag(newValues); } else { setTempValues(newValues); } }, [getLatest, getValueForClient, roundToStep, values]); const handleKeyDown = React.useCallback((e, i) => { const { values, onChange = (e) => { } } = getLatest(); if (e.keyCode === 37 || e.keyCode === 39) { setActiveHandleIndex(i); const direction = e.keyCode === 37 ? -1 : 1; const newValue = getNextStep(values[i], direction); const newValues = [ ...values.slice(0, i), newValue, ...values.slice(i + 1), ]; const sortedValues = sortNumList(newValues); onChange(sortedValues); } }, [getLatest, getNextStep]); const handlePress = React.useCallback((e, i) => { setActiveHandleIndex(i); const handleRelease = () => { const { tempValues, values, onChange = (e) => { }, onDrag = () => { }, } = getLatest(); document.removeEventListener("mousemove", handleDrag); document.removeEventListener("touchmove", handleDrag); document.removeEventListener("mouseup", handleRelease); document.removeEventListener("touchend", handleRelease); const sortedValues = sortNumList(tempValues || values); onChange(sortedValues); onDrag(sortedValues); setActiveHandleIndex(null); setTempValues(undefined); }; document.addEventListener("mousemove", handleDrag); document.addEventListener("touchmove", handleDrag); document.addEventListener("mouseup", handleRelease); document.addEventListener("touchend", handleRelease); }, [getLatest, handleDrag]); const getPercentageForValue = React.useCallback((val) => interpolator.getPercentageForValue(val, min, max), [interpolator, max, min]); const ticks = React.useMemo(() => { let ticks = controlledTicks || steps; if (!ticks) { ticks = [min]; while (ticks[ticks.length - 1] < max - tickSize) { ticks.push(ticks[ticks.length - 1] + tickSize); } ticks.push(max); } return ticks.map((value, i) => ({ value, getTickProps: ({ key = i, style = {}, ...rest } = {}) => ({ key, style: { [mode === "vertical" ? "bottom" : "left"]: `${getPercentageForValue(value)}%`, ...style, }, ...rest, }), })); }, [controlledTicks, getPercentageForValue, max, min, steps, tickSize, ,]); const segments = React.useMemo(() => { const sortedValues = sortNumList(tempValues || values); return [...sortedValues, max].map((value, i) => ({ value, getSegmentProps: ({ key = i, style = {}, ...rest } = {}) => { const from = getPercentageForValue(sortedValues[i - 1] ? sortedValues[i - 1] : min); const to = getPercentageForValue(value) - from; return { key, style: { [mode === "vertical" ? "bottom" : "left"]: `${from}%`, [mode === "vertical" ? "height" : "width"]: `${to}%`, ...style, }, ...rest, }; }, })); }, [getPercentageForValue, max, min, tempValues, values]); const handles = React.useMemo(() => (tempValues || values).map((value, i) => ({ value, active: i === activeHandleIndex, getHandleProps: ({ key = i, onKeyDown, onMouseDown, onTouchStart, } = {}) => ({ key, onKeyDown: (e) => { e.persist(); handleKeyDown(e, i); if (onKeyDown) onKeyDown(e); }, onMouseDown: (e) => { e.persist(); handlePress(e, i); if (onMouseDown) onMouseDown(e); }, onTouchStart: (e) => { e.persist(); handlePress(e, i); if (onTouchStart) onTouchStart(e); }, style: { [mode === "vertical" ? "left" : "bottom"]: "50%", [mode === "vertical" ? "bottom" : "left"]: `${getPercentageForValue(value)}%`, zIndex: i === activeHandleIndex ? "1" : "0", }, }), })), [ activeHandleIndex, getPercentageForValue, handleKeyDown, handlePress, min, max, tempValues, values, ]); const onClickTrack = (event) => { if (event.currentTarget.getAttribute("role") === "button" || event.currentTarget.classList.value.includes("reflex-slider-thumb")) return; event.persist(); const client = event.type === "touchmove" ? event.changedTouches[0][axis] : event[axis]; const trackDims = getBoundingClientRect(trackElRef.current); const value = interpolator.getValueForClient(client, trackDims, min, max, mode); if (values.length === 2) { const [findChangingValueIndex, preValue] = values .map((val, i) => [i, Math.abs(value - val)]) .sort((a, b) => (a[1] < b[1] ? -1 : 1))[0]; values[findChangingValueIndex] = value; onChange(values); } else { onChange([value]); } }; return { activeHandleIndex, ticks, segments, handles, onClickTrack, }; } export default useRanger;