How to change numeric input by dragging in React?

How to change numeric input by dragging in React?

ยท

3 min read

The above GIF is one of the interactions that I love about Figma . To change the position of an item, I do not have to use a keyboard at all. Just a mouse is enough.

So in this article we are going to create a numeric input field whose value can be changed by dragging on its label from scratch. We needed this for Graftini because we think a visual tool should not require keyboard for most interactions.

If you are eager to see the code and try it out for yourself, then jump to CodeSandbox at codesandbox.io/s/drag-number-input-z2rnj.

Let us first create a simple input

We can start by creating an input which reads from and writes the values to a state. If you have already created forms this should be simple enough to understand.

function Input() {
  const [value, setValue] = useState(0);

 const onInputChange = useCallback(
    (ev) => setValue(parseInt(ev.target.value, 10)),
    []
  );

  return (
    <input
        value={value}
        onChange={onInputChange}
        style={{
          padding: 8,
        }}
     />
  );
}

We are creating a state which stores the current value of the input. This state will be updated when the input changes via the keyboard.

Now we need a label that fits right in with the input

We need the label to be an anchor on which the mouse interactions can be added. The input itself cannot be the anchor because then it will ruin how input fields normally are expected to behave. The code that captures that idea can look like:

<div
    style={{
      display: "flex",
      border: "1px solid #CCC",
      alignItems: "center",
      borderRadius: 4,
      fontFamily: "sans-serif",
      width: 300,
    }}
>
   <span
      style={{
        padding: 8,
        color: "gray",
        cursor: "ew-resize",
        userSelect: "none",
      }}
    >
      Count
    </span>
   <input
      value={value}
      onChange={onInputChange}
      style={{
        flex: 1,
        padding: 8,
        border: "none",
        outline: "none",
      }}
   />
</div>

The above code is just visual cosmetics. You can make it to look however you see fit. Now the input should look something like: image.png

Adding mouse interactions on the label

We will extract the label into its own component to make it easier to write & understand the code. Then we will add three mouse interactions in it. One on the label itself and two on the document. Why? We will discuss it along side the code.

function DragLabel({ value, setValue }) {
  // We are creating a snapshot of the values when the drag starts
  // because the [value] will itself change & we need the original
  // [value] to calculate during a drag.
  const [snapshot, setSnapshot] = useState(value);

  // This captures the starting position of the drag and is used to 
  // calculate the diff in positions of the cursor.
  const [startVal, setStartVal] = useState(0);

  // Start the drag to change operation when the mouse button is down.
  const onStart = useCallback(
    (event) => {
      setStartVal(event.clientX);
      setSnapshot(value);
    },
    [value]
  );

  // We use document events to update and end the drag operation
  // because the mouse may not be present over the label during
  // the operation..
  useEffect(() => {
    // Only change the value if the drag was actually started.
    const onUpdate = (event) => {
      if (startVal) {
        setValue(event.clientX - snapshot);
      }
    };

    // Stop the drag operation now.
    const onEnd = () => {
      setStartVal(0);
    };

    document.addEventListener("mousemove", onUpdate);
    document.addEventListener("mouseup", onEnd);
    return () => {
      document.removeEventListener("mousemove", onUpdate);
      document.removeEventListener("mouseup", onEnd);
    };
  }, [startVal, setValue, snapshot]);

  return (
    <span
      onMouseDown={onStart}
      style={{
        padding: 8,
        color: "gray",
        cursor: "ew-resize",
        userSelect: "none",
      }}
    >
      Count
    </span>
  );
}

Now try running it up and voila ๐ŸŽ‰๐ŸŽŠ you have your own drag to change numeric input.

2021-06-29 16.27.09.gif

It looks awesome doesn't it? Though the cursor during the operation does not look good. This could be something that you can fix. ๐Ÿ˜‹

The full code is at CodeSandbox for you to try it out codesandbox.io/s/drag-number-input-z2rnj?fi...

Fork it & make improvements to it. Till then โœŒ๏ธ.

ย