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 https://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:
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.
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 https://codesandbox.io/s/drag-number-input-z2rnj?file=/src/App.js.
Fork it & make improvements to it. Till then ✌️.
Top comments (3)
Change:
to:
to prevent zeroing out the input on next drag
You forgot to remove mousemove listener on mouse release:
const onMouseUp = () => {
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
setStartPos(0);
};
Very nice start but you'll have to do some math if you want a default value other than 0!