useSelectInnerText: Cross-browser solution for selecting inner text
Tags: react, safari, webdev, typescript
Published: 2023-06-17 09:30:23

## 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()`!

```tsx
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:

 
![Trying and failing to select all inner text of input in Safari](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x5gry24inz2zn42ahvep.gif)

But never fear, [I've braved the choppy Safari waters before](https://dev.to/tmns/youtube-embed-with-shadow-rounded-corners-in-all-browsers-a-love-story-5g5f), 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:

```tsx
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:


![Succeeding to select all inner text of input in Safari](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m8sv5in8zl9sknukuar5.gif)

🥳🥳🥳

## What's the catch?

I haven't discovered one yet, but here's the hook 😜

```tsx
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:

```tsx
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._