ranger icon indicating copy to clipboard operation
ranger copied to clipboard

Is there a way to click on a Track to set a new Range value ?

Open Marco-exports opened this issue 4 years ago • 5 comments

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 !

Marco-exports avatar Nov 28 '20 17:11 Marco-exports

Good examples of click-to-move slider:

https://material-ui.com/components/slider/

https://react-range.netlify.app/?path=/story/range--two-thumbs

Marco-exports avatar Nov 28 '20 17:11 Marco-exports

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>

Marco-exports avatar Dec 17 '20 15:12 Marco-exports

I'd like to see this added as well

scottshuffler avatar Jan 07 '21 13:01 scottshuffler

I also find it tedious to click on the track bar instead of picking the thumb. A must have imho.

jackblackCH avatar Feb 02 '22 12:02 jackblackCH

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>
    );
};

alexboffey avatar Sep 20 '22 16:09 alexboffey

I came up with this (currently unfinished idea) :

  • use concept from https://github.com/TanStack/ranger/issues/21#issuecomment-1252618108
  • bind a onPointerMove handler on my track to handlePointerMove
  • 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], extract onChange from its getHandleProps and 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>
  );
}

airtonix avatar Jan 14 '23 04:01 airtonix

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.

rkulinski avatar Dec 05 '23 19:12 rkulinski