Skip to content

Instantly share code, notes, and snippets.

@pstoica
Last active August 1, 2023 21:00
Show Gist options
  • Star 98 You must be signed in to star a gist
  • Fork 16 You must be signed in to fork a gist
  • Save pstoica/4323d3e6e37e8a23dd59 to your computer and use it in GitHub Desktop.
Save pstoica/4323d3e6e37e8a23dd59 to your computer and use it in GitHub Desktop.
onBlur for entire react element
function OnBlurComponent({ onBlur }) {
const handleBlur = (e) => {
const currentTarget = e.currentTarget;
// Check the newly focused element in the next tick of the event loop
setTimeout(() => {
// Check if the new activeElement is a child of the original container
if (!currentTarget.contains(document.activeElement)) {
// You can invoke a callback or add custom logic here
onBlur();
}
}, 0);
};
return (
<div tabIndex="1" onBlur={handleBlur}>
Hello <input type="text" value="world" />
</div>
);
}
@djskinner
Copy link

For anyone else who is wondering, this answer gives a good explanation of why it is necessary. Thanks for the snippet!

@neaumusic
Copy link

neaumusic commented Feb 16, 2017

This doesn't seem to work on mac, since document.activeElement is body unless it's an element (input) that can receive keystrokes

The solution for me was to use onMouseDown, which fires before onBlur (click and presumable onMouseUp fire afterwards)
Tested this on iPhone and onMouseDown triggers

@nickbouton
Copy link

nickbouton commented Feb 19, 2017

Works fine on Chrome/OS X for me, thanks for this @pstoica.

@ericbeijer
Copy link

Thanks, works great!

@shanielh
Copy link

shanielh commented Jun 5, 2017

Thanks!

@mcroba
Copy link

mcroba commented Aug 21, 2017

@neaumusic you might have forgotten to add the tabIndex on the div

@bmancini42
Copy link

This is great! Thanks for sharing

@abobwhite
Copy link

Worked great for me! Thanks!

@emirdeliz
Copy link

Thanks, works great!

@vikamirror
Copy link

Thank you sooooo much! This is just what I needed

@AlexanderLyon
Copy link

Exactly what I was looking for thank you!

@yunay
Copy link

yunay commented May 10, 2018

I just had this warning when I tried this:
Warning: This synthetic event is reused for performance reasons. If you're seeing this, you're accessing the method currentTarget on a released/nullified synthetic event. This is a no-op function. If you must keep the original synthetic event around, use event.persist(). See https://fb.me/react-event-pooling for more information.

@Mrwaite
Copy link

Mrwaite commented Aug 15, 2018

Thanks, solved the problem that has been bothering me for a long time.

@mockey-jockey
Copy link

Good mind ....thanks a lot you saved me more time..

@blackjacques
Copy link

Awesome! Surprising that we still need such a kluge in 2018!

@aeolusheath
Copy link

omg !! serveral years later , it still works!!! big thanks

@oreporan
Copy link

halleluja!

@jason90929
Copy link

My code works! But I still don't know why

@shawnhutchins
Copy link

Awesome! Thanks!

@Iam-Locy
Copy link

Iam-Locy commented Mar 1, 2020

@neaumusic Thank you, it helped a lot.

@bchoddny
Copy link

bchoddny commented Mar 14, 2020

Any reason, why the onblur event is on div, instead of on input element? Does it gives any advantage of placing blur event handler on one over the other?
Why would we require a set timeout event?

Thanks.

@pstoica
Copy link
Author

pstoica commented Mar 14, 2020

@bchoddny:

  • onBlur bubbles up; put it on the outermost element needed. The input is there to show you can still use children with their own onBlur.
  • setTimeout(fn, 0) runs afterwards in the event loop so that document.activeElement refers to the newly focused element. If it's a child, we're still in the container element.

@IvanNazarov
Copy link

Thanks! You save my day!

@sunnymui
Copy link

sunnymui commented May 28, 2020

Super useful! Just spent a day going through tons of pages on blur and focus to figure out an annoying bug that was preventing my onclick handlers in a child component from firing. I was using it for a dropdown component based on the details/summary html element to remove the open attribute on blur so only one dropdown menu would show at a time, but the blur event goes first, adding the open attribute, and triggering a rerender before the child component's onclicks could fire.

This also helps me out because one of the dropdowns I needed to keep it open when they clicked within it because there were some input interactions in the dropdown menu. Also had to add tabindex=0 to my dropdown element so the focus info would pass correctly.

The other thing was it wouldn't work for me until I set the timeout to a longer time, around 50-100ms. Not sure exactly why, but I'm guessing it's because React doesn't trigger renders immediately after changes, instead batching the changes then updating all at once. I think that extra time makes it wait long enough for that to happen before triggering my blur handler.

Here's what my the details element in my render function looked like:

 <details
      className={`DetailSummaryDropdown`}
      ref={dropDownRef}
      tabIndex='0'
      onBlur={(e) => {
          const currentTarget = e.currentTarget;
          // blur happens before click, preventing any click events in children from firing due to rerender from state change
          // so wait a tick for child component events to fire before changing open state and causing rerender
          window.setTimeout(() => {
            if (!currentTarget.contains(document.activeElement)) {
              dropDownRef.current.removeAttribute("open");
            }
          }, 100);
      }}
>

Anyway, just sharing my frustrations in case anyone else finds it helpful

@StanielPetrov
Copy link

OMG, you are a genius! Thank you so much for sharing this!

@HermanNygaard
Copy link

@pstoica
Thanks for this! Just a heads up, I think you forgot to change the callback onBlur={onBlur} to onBlur={handleBlur} in the newest revision:

return (
    <div tabIndex="1" onBlur={handleBlur}>
      Hello <input type="text" value="world" />
    </div>
  );

@pstoica
Copy link
Author

pstoica commented Mar 12, 2021

@pstoica
Thanks for this! Just a heads up, I think you forgot to change the callback onBlur={onBlur} to onBlur={handleBlur} in the newest revision:

return (
    <div tabIndex="1" onBlur={handleBlur}>
      Hello <input type="text" value="world" />
    </div>
  );

glad it helped. thanks for catching that, updated!

@AshlandWest
Copy link

You're my hero!

@gattonero1052
Copy link

That helps a lot!

@diegohaz
Copy link

You can also use event.relatedTarget to get the next active element on blur if you don't care about IE 11.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment