Skip to content

Instantly share code, notes, and snippets.

@pstoica
Last active September 9, 2024 05:14
Show Gist options
  • Select an option

  • Save pstoica/4323d3e6e37e8a23dd59 to your computer and use it in GitHub Desktop.

Select an option

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>
);
}
@neaucode

neaucode commented Feb 16, 2017

Copy link
Copy Markdown

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

nickbouton commented Feb 19, 2017

Copy link
Copy Markdown

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

@ericbeijer

Copy link
Copy Markdown

Thanks, works great!

@shanielh

shanielh commented Jun 5, 2017

Copy link
Copy Markdown

Thanks!

@mcroba

mcroba commented Aug 21, 2017

Copy link
Copy Markdown

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

@bmancini42

Copy link
Copy Markdown

This is great! Thanks for sharing

@abobwhite

Copy link
Copy Markdown

Worked great for me! Thanks!

@emirdeliz

Copy link
Copy Markdown

Thanks, works great!

@vikamirror

Copy link
Copy Markdown

Thank you sooooo much! This is just what I needed

@AlexanderLyon

Copy link
Copy Markdown

Exactly what I was looking for thank you!

@yunay

yunay commented May 10, 2018

Copy link
Copy Markdown

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

Mrwaite commented Aug 15, 2018

Copy link
Copy Markdown

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

@mockey-jockey

Copy link
Copy Markdown

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

@blackjacques

Copy link
Copy Markdown

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

@aeolusheath

Copy link
Copy Markdown

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

@oreporan

Copy link
Copy Markdown

halleluja!

@jason90929

Copy link
Copy Markdown

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

@shawnhutchins

Copy link
Copy Markdown

Awesome! Thanks!

@Iam-Locy

Iam-Locy commented Mar 1, 2020

Copy link
Copy Markdown

@neaumusic Thank you, it helped a lot.

@bchoddny

bchoddny commented Mar 14, 2020

Copy link
Copy Markdown

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

pstoica commented Mar 14, 2020

Copy link
Copy Markdown
Author

@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
Copy Markdown

Thanks! You save my day!

@sunnymui

sunnymui commented May 28, 2020

Copy link
Copy Markdown

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
Copy Markdown

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

@HermanNygaard

Copy link
Copy Markdown

@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

pstoica commented Mar 12, 2021

Copy link
Copy Markdown
Author

@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
Copy Markdown

You're my hero!

@gattonero1052

Copy link
Copy Markdown

That helps a lot!

@diegohaz

Copy link
Copy Markdown

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

@YogeshR6

YogeshR6 commented Sep 9, 2024

Copy link
Copy Markdown

@diegohaz I was look for this for so long, thanks a lot. Was working with emoji pickers and for some reason document.activeElement does not work when I click on the picker itself and closes the pop over even though it is a part of the div, but using event.relatedTarget works perfectly fine now.

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