With further experimentation, and some help from @Keith in a comment, I've solved almost all my problems.
I've forked the codepen in my question and made these changes in the fork: https://codepen.io/micahrl/pen/GRjwqdO.
Checking input validity
@Keith pointed me to validity.badInput
(https://developer.mozilla.org/en-US/docs/Web/API/ValidityState/badInput). With this, I can differentiate between empty input, where a user types something then deletes it, and bad input, where the user attempts to add a non-numeric character.
That means I add this to the beginning of setNumWrapper()
:
if (event.target.value === "") {
if (event.target.validity.badInput) {
// Set the text box and number to the old value - ignore the bad input
inputRef.current.value = String(num);
setNum(num);
} else {
// The data in the text box was deleted - set everything to empty
inputRef.current.value = "";
setNum(NaN);
}
return;
}
I also have to make an inputRef
with useRef()
, and set it on the <input>
element.
This solves #5 most of #1 (but see below for one remaining minor problem).
Trimming leading zeroes
All I had to do for this was use that inputRef
to set the value in the <input>
element at the end of setNumWrapper()
:
inputRef.current.value = String(newNum);
The <input>
element's value is always a string anyway, and casting the number to a string removed leading zeroes, if there were any.
Remaining problem: invalid input is allowed if the text box is empty
When the text box is empty, the user can type non-numeric characters into it, and setNumWrapper()
doesn't even fire. If you put a console.log()
at the top of setNumWrapper()
, it won't print anything to the log if the user types a letter, but it will print to the log if the user types a number.
This means I cannot use setNumWrapper()
to solve this problem.
However, it's also relatively minor. On mobile, the number keypad comes up, preventing non-numeric input. On the desktop nothing stops the user from typing letters with their keyboard, but for my app, it's clear that only numbers are allowed, so this is good enough for now.
Still, if there's a way to fix this, I'd be curious to hear about it.