TanStack / ranger

🤖 Headless utilities for building range and multi-range sliders in React, Preact, Solid, Vue, Svelte and Angular
MIT License
753 stars 63 forks source link

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

Closed Marco-exports closed 9 months ago

Marco-exports commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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>
scottshuffler commented 3 years ago

I'd like to see this added as well

jackblackCH commented 2 years ago

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

alexboffey commented 2 years ago

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>
    );
};
airtonix commented 1 year ago

I came up with this (currently unfinished idea) :

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>
  );
}
rkulinski commented 9 months ago

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.

minecrawler commented 1 month ago

@rkulinski how is it supported in the newer version? I cannot find an example of how to click on the track to move a handle to that point (or the closest step)