What's the issue?
Sometimes you have an element containing text that you would like to automatically be selected when a user clicks into it. A typical example of this is an input
, the text of which you want selected in full when clicked so the user can easily delete / replace it.
Sounds straightforward enough, right? Just add an onFocus
event handler that calls e.target.select()
!
function Input() {
const ref = useRef<HTMLInputElement>();
return (
<label htmlFor="quantity">Quantity</label>
<input
ref={ref}
onFocus={(e) => e.target.select()}
name="quantity"
type="text"
/>
)
}
Unfortunately, life isn't all that easy. In Chrome, this works decently. But in Safari, it is very buggy, often not selecting any text at all, or only selecting some of the text. You can observe this behavior below:
But never fear, I've braved the choppy Safari waters before, and my hacky-ness knows no bounds 😁
What can we do?
Perhaps counterintuitively, we will forego onFocus
completely and instead opt for a combination of onClick
and onBlur
.
First, on click, we will not only select the text but also add the target to a ref object that keeps track of whether it's currently focused. This is to allow the user to click again in the same input and remove the "select all" if desired (i.e., if they don't actually want to delete / replace the entire value).
Then, on blur, the target noted above is removed from the ref object so the "select all" works again.
Taking our example from the previous section, this could look something like:
function Input() {
const focusedRef = useRef<string | null>();
return (
<label htmlFor="quantity">Quantity</label>
<input
ref={ref}
onClick={(e) => {
// If our ref object has been set to this input and
// we're getting another event, assume the user wants
// to edit only a piece of the text and early return.
if (focusedRef.current === e.currentTarget.name) return;
// Otherwise, select all and set our ref to the input.
e.currentTarget.select();
focusedRef.current = e.currentTarget.name;
}}
onBlur={() => {
// Simply reset our ref so it can be select all'd again.
focusedRef.current = null;
}}
name="quantity"
type="text"
/>
)
}
And with that, we get a reliable select all behavior, even in Safari:
🥳🥳🥳
What's the catch?
I haven't discovered one yet, but here's the hook 😜
function useSelectInnerText(): [
React.MouseEventHandler<HTMLInputElement>,
React.FocusEventHandler<HTMLInputElement>
] {
const focusedRef = useRef<string | null>(null);
const handleClick = useCallback<React.MouseEventHandler<HTMLInputElement>>((e) => {
if (focusedRef.current === e.currentTarget.name) return;
e.currentTarget.select();
focusedRef.current = e.currentTarget.name;
}, []);
const handleBlur = useCallback<React.FocusEventHandler<HTMLInputElement>>(() => {
focusedRef.current = null;
}, []);
return [handleClick, handleBlur];
}
With that, our previous example becomes:
function Input() {
const [handleClick, handleBlur] = useSelectInnerText();
return (
<label htmlFor="quantity">Quantity</label>
<input
ref={ref}
onClick={handleClick}
onBlur={handleBlur}
name="quantity"
type="text"
/>
)
}
🪝
Conclusion
Once again, Safari brings out my creativity. I owe you so much ol' friend.
Hope this helps you out too! Feel free to leave any feedback / questions in the comments 👋
P.S. I actually discovered this behavior over a year ago but thought it was probably just something temporary that would be fixed. Checked again today and it was still there, so figured I should share my approach.
Top comments (0)