Is there a way to click on a Track to set a new Range value ?
Hello,
We have switched to "react-ranger" from "tajo/react-range", in order to work with -- and for compatibility -- with Hooks...
Our web pages use many range sliders, and our users find it tedious to mouse-click and drag a button...
Is there a way to "click" on the Track, so that it will change the value -- and move the button ?
Thanks !
Good examples of click-to-move slider:
https://material-ui.com/components/slider/
https://react-range.netlify.app/?path=/story/range--two-thumbs
After a few variations of trying to use "Ref" hooks inside the react_ranger code, I finally found a way to implement this click-move feature from outside the hook:
export default function Slider(props) {
const [width, setWidth] = React.useState(0)
... various UseEffect functions ...
React.useEffect(() => {
let elem = document.getElementById("tracked")
const coords = elem.getBoundingClientRect()
setWidth(Math.ceil(coords.width))
}, [])
const showClick= (e) => {
e.preventDefault()
var x = e.nativeEvent.offsetX
if(values.length === 1){setValues([Math.round(x / width * 100)])}
}
... and finally ...
return (
<div id="tracked" className={'track'} {...getTrackProps()} onClick={showClick}>
{segments.map(({ getSegmentProps }, i) => (<div {...getSegmentProps()} index={i} />))}
{handles.map(({ value, getHandleProps }) => (
<button className={'slideButton'} {...getHandleProps()} onClick={e => e.stopPropagation()}>
<div className={'handle'}>{value}</div>
</button>
))}
</div>
I'd like to see this added as well
I also find it tedious to click on the track bar instead of picking the thumb. A must have imho.
Thanks for the example above @Marco-exports 🙏
I've taken it and expanded on it to support multi handle rangers too, I cant be the only one who needed this use case!
import clsx from "clsx";
import { useRef } from "react";
import { RangerOptions, useRanger } from "react-ranger";
export type SliderProps = {
min?: number;
max?: number;
stepSize?: number;
values: number[];
onChange: RangerOptions["onChange"];
colorClassName?: string;
label: string;
labelHidden?: boolean;
};
export const Slider: React.FC<SliderProps> = ({ min = 1, max = 100, stepSize = 5, values, onChange, colorClassName = "bg-ui-base-text-secondary", label, labelHidden = false }) => {
const trackRef = useRef<HTMLDivElement>(null);
const { getTrackProps, handles, segments } = useRanger({
min,
max,
stepSize,
values,
onChange,
});
const handleTrackOnClick = (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
if (!onChange || !trackRef.current) {
return;
}
const clickPosition = e.clientX - trackRef.current.getBoundingClientRect().left;
const trackWidth = trackRef.current.getBoundingClientRect().width;
const x = Math.max(min, Math.round((clickPosition / trackWidth) * max));
const closestCurrentValue = values.sort((a, b) => Math.abs(a - x) - Math.abs(b - x))[0];
const nextValues = values
.filter((v) => v !== closestCurrentValue)
.concat(x)
.sort((a, b) => a - b);
onChange(nextValues);
};
return (
<label>
<span className={clsx("input__label", labelHidden && "hidden")}>{label}</span>
<div
{...getTrackProps({
className: "rounded bg-ui-base-3 h-[0.3rem] w-full shadow-sm cursor-pointer",
id: "tracked",
onClick: handleTrackOnClick,
ref: trackRef,
})}
>
{segments.map(({ getSegmentProps }, i) => {
// Only render segments with values
if (i === values.length || (values.length > 1 && i === 0)) {
return null;
}
return (
<div
{...getSegmentProps({
className: `${colorClassName} h-full rounded`,
})}
key={`${label}-slider-segment-${i}`}
/>
);
})}
{handles.map(({ getHandleProps }, i) => (
<div key={`${label}-slider-handle-${i}`}>
<button
{...getHandleProps({
className: `${colorClassName} w-xs h-xs rounded-full shadow-md`,
})}
/>
</div>
))}
</div>
</label>
);
};
I came up with this (currently unfinished idea) :
- use concept from https://github.com/TanStack/ranger/issues/21#issuecomment-1252618108
- bind a
onPointerMovehandler on my track tohandlePointerMove - unfinished concept for handlePointerMove:
- on pointerMove, test if mouse buttons are down (verify how this works on touch screens)
- if no touching/no buttons, bail here
- use location of touch/drag to find nearest
handles[number], extractonChangefrom itsgetHandlePropsand run that.
This way you can click and drag on the track and it moves the nearest handle to where ever your pointer is dragging.
function RangeInput({
name,
values,
min = 1,
max = 100,
stepSize = 1,
showTicks,
className,
onChange,
}: {
name: string;
min?: number;
max?: number;
values: string[];
stepSize?: number;
className?: string;
showTicks?: boolean;
onChange: (value: string[]) => void;
}) {
const [numberValues, setNumberValues] = useState(() =>
(values || []).map((value) => parseInt(value))
);
const trackRef = useRef<HTMLDivElement>(null);
const handleChange = useCallback(
(values: number[]) => {
setNumberValues(values);
onChange(values.map((value) => value.toString()));
},
[onChange]
);
const { getTrackProps, ticks, segments, handles } = useRanger({
min,
max,
stepSize,
values: numberValues,
onChange: handleChange,
onDrag: handleChange,
});
const handleTrackOnClick = useCallback(
(event: React.MouseEvent<HTMLElement>) => {
event.preventDefault();
if (!onChange || !trackRef.current) {
return;
}
const clickPosition =
event.clientX - trackRef.current.getBoundingClientRect().left;
const trackWidth = trackRef.current.getBoundingClientRect().width;
const x = Math.max(min, Math.round((clickPosition / trackWidth) * max));
const closestCurrentValue = numberValues.sort(
(a, b) => Math.abs(a - x) - Math.abs(b - x)
)[0];
const nextValues = numberValues
.filter((v) => v !== closestCurrentValue)
.concat(x)
.sort((a, b) => a - b);
setNumberValues(() => {
return nextValues;
});
handleChange(nextValues);
},
[handleChange, max, min, numberValues, onChange]
);
return (
<div
className={classnames("flex w-full h-4 my-2 items-center", className)}
ref={trackRef}
>
<div
{...getTrackProps()}
className={classnames("block w-full h-1 bg-gray-200 cursor-pointer")}
onClick={handleTrackOnClick}
// TODO: this will make the handle move, but which one?
// use position to find closest handles[number], extract props from
// its getHandleProps() and run the onChange handler from that
// onPointerMove={(event) => {
//
// if (event.buttons > 0) handleTrackOnClick(event);
// }}
>
{showTicks &&
ticks.map(({ value, getTickProps }) => (
<div className="h-2" {...getTickProps()} key={value}>
<div>{value}</div>
</div>
))}
{segments.map(({ getSegmentProps }, index) => (
<div
{...getSegmentProps({})}
className={classnames("h-1", ["bg-blue-300", "bg-blue-100"][index])}
key={`${name}-slider-segment-${index}`}
/>
))}
{handles.map(({ value, active, getHandleProps }, index) => (
<div
{...getHandleProps()}
className="flex items-center justify-center"
key={`${name}-slider-handle-${index}`}
>
<div
className={classnames(
"absolute",
"flex items-center justify-center",
"rounded-full min-w-8 h-8 px-4",
"transition-all",
"text-white bg-blue-500",
active && "font-bold ring"
)}
style={{
transform: active
? `translateY(-50%) scale(1.1)`
: "translateY(0) scale(0.9)",
}}
>
{value}
</div>
</div>
))}
</div>
</div>
);
}
I see this is referring to the old version. Please take a look if your case is supported in new version. If not please open new issue or submit a pull request.