Was the cursor moving one position to the left of the last character?
Interestingly, when using onKeyUp (the release of the key) rather than onKeyDown the issue seems to go away. I've listed that solution followed by a couple other examples with explanations below.
Solution
import { useRef, useState } from "react";
const MyComponent = () => {
const [value, setValue] = useState("1234");
const ref = useRef(null);
const onKeyUp = (event) => {
if (event.key === "ArrowLeft") {
const textInput = ref.current;
const len = value.length;
textInput.setSelectionRange(len, len);
textInput.focus();
}
};
return (
<>
<input
onChange={({ target }) => setValue(target.value)}
value={value}
ref={ref}
type={"text"}
/>
<input onKeyUp={onKeyUp} type={"text"} />
</>
);
};
export default MyComponent;
https://codesandbox.io/s/cursor-end-of-input-onkeyup-x6ekke
Explanation
My guess is that because onKeyUp naturally follows onKeyDown, when we receive the onKeyDown event in React, per your example code, the following sequence occurs (or generally speaking something like this is happening):
Inside onKeyDown...
Our cursor is moved to the very end of the text input.
ref.current?.setSelectionRange(value.length, value.length)
The text input receives focus.
ref.current?.focus()
Then, the release of the left arrow key causes onKeyUp event to run in the DOM (we haven't done anything to handle this in React) while the focus is now on the text input as a result of step 2 above. The default behavior pressing/releasing the left arrow key while the input has focus is to move the cursor one position to the left, placing it one position from the end of the input text.
Other Examples/Solutions
If you stick with the use of onKeyDown, here are a couple other examples.
event.preventDefault()
const onKeyDown = (event) => {
if (event.key === "ArrowLeft") {
event.preventDefault();
const textInput = ref.current;
const len = value.length;
textInput.setSelectionRange(len, len);
textInput.focus();
}
};
setTimeout()
const onKeyDown = (event) => {
if (event.key === "ArrowLeft") {
setTimeout(() => {
const textInput = ref.current;
const len = value.length;
textInput.setSelectionRange(len, len);
textInput.focus();
}, 0);
}
};
https://codesandbox.io/s/cursor-end-of-input-example-h1yrdr
My guess is that these workarounds effectively block the browser from firing the native key down and up events altogether or delay our handler from running until after the native events have fired, respectively.