DEV Community

tmns
tmns

Posted on

useSelectInnerText: Cross-browser solution for selecting inner text

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"
    />
  )
}
Enter fullscreen mode Exit fullscreen mode

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:

Trying and failing to select all inner text of input in Safari

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"
    />
  )
}
Enter fullscreen mode Exit fullscreen mode

And with that, we get a reliable select all behavior, even in Safari:

Succeeding to select all inner text of input 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];
}
Enter fullscreen mode Exit fullscreen mode

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"
    />
  )
}
Enter fullscreen mode Exit fullscreen mode

πŸͺ

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)