42

I have a div on a page that shows some info about a particular category (Image, Name etc).

When I click on the edit image it puts the category into edit mode which allows me to update the name. As you can see from the image below it shows that "Soup" is currently in edit mode, the others are in normal view mode. This all works as expected with the cancel / save buttons doing everything right. (I tried adding an image but wouldn't let me, need more love)

However once in edit mode if I click anywhere else on the page (Outside of the div) the expected result would be that the soup category would go back to view mode. Upon an event firing of some sort, this should also allow me to ask if they wanted to save changes.

So what I then decided to do is create an blur event on the "soups" parent div. This works as expected if I click anywhere on the page, however if I click on the inner element of the div it also causes the parents blur event to be fired, thus causing the category to go back to view mode.

So, is there a way to prevent the parent div from firing the blur event if any one of its children receive focus?

<div tabindex="-1" onblur="alert('outer')">
    <input type="text" value="Soup" />
</div>

I just wrote the code without a compiler so not sure if that even works but with that hopefully you get the idea.

I'm using Knockout.js to update the GUI on the fly but that shouldn't effect this answer I wouldn't have thought.

Anders
  • 7,431
  • 6
  • 42
  • 76
Thwaitesy
  • 1,093
  • 2
  • 11
  • 14
  • [Related question](http://stackoverflow.com/questions/121499/when-onblur-occurs-how-can-i-find-out-which-element-focus-went-to) – jbabey Aug 23 '12 at 13:12

3 Answers3

53

I faced the same issue. This what worked for me.

 handleBlur(event) {
    // if the blur was because of outside focus
    // currentTarget is the parent element, relatedTarget is the clicked element
    if (!event.currentTarget.contains(event.relatedTarget)) {
        .....
    }
}

Enjoy :)

Yahel Yechieli
  • 559
  • 4
  • 4
  • With some editing, I got this to do what I wanted; Had to handle cases where relatedTarget === null. Still seems a more appropriate answer than using mousedown. – Sean Clarke Apr 15 '21 at 18:34
49

I've had to tackle this problem before. I am not sure if it is the best solution, but it is what I ended up using.

Since the click event fires after the blur, there is no (cross-browser, reliable) way to tell what element is gaining focus.

Mousedown, however, fires before blur. This means that you can set some flag in the mousedown of your children elements, and interrogate that flag in the blur of your parent.

Working example: http://jsfiddle.net/L5Cts/

Note that you will also have to handle keydown (and check for tab/shift-tab) if you want to also catch blurs caused by the keyboard.

jbabey
  • 42,963
  • 11
  • 66
  • 94
  • 7
    Sir, you are a gentleman and a scholar. I've been trying different solutions to this problem in different situations for over a year. Never realized the timing of all the events, thanks a million. – Bryan Rayner Aug 29 '15 at 14:38
  • I've been trying to find some sort of official definition of event order/precedence to see if this can be relied on. This is an old question, but if the answer is still true this solution might work just due to coincidence: https://stackoverflow.com/a/282271/2382483. Id love to find a really solid solution to this, but everything seems to be a work around or hack. – Rob Allsopp Nov 29 '17 at 18:10
  • This example, while for React, can be adapted for pretty well any use. It uses focus on the new element and blur on the original, and uses a timer to cancel the blur event when focus fires. https://medium.com/@jessebeach/dealing-with-focus-and-blur-in-a-composite-widget-in-react-90d3c3b49a9b – IceWarrior353 Sep 26 '18 at 21:35
  • This may be a hack, but I use a setTimeout on the blur and a clearTimeout on the child click. – FranCarstens Feb 22 '19 at 15:01
26

I don't think there is any guarantee mousedown will happen before the focus events in all browsers, so a better way to handle this might be to use evt.relatedTarget. For the focusin event, the eventTarget property is a reference to the element that is currently losing focus. You can check if that element is a descendant of the parent, and if its not, you know focus is entering the parent from the outside. For the focusout event, relatedTarget is a reference to the element that is currently receiving focus. Use the same logic to determine if focus is fully leaving the parent:

const parent = document.getElementById('parent');

parent.addEventListener('focusin', e => {
    const enteringParent = !parent.contains(e.relatedTarget);

    if (enteringParent) {
        // do things in response to focus on any child of the parent or the parent itself
    }
});

parent.addEventListener('focusout', e => {
    const leavingParent = !parent.contains(e.relatedTarget);

    if (leavingParent) {
        // do things in response to fully leaving the parent element and all of its children
    }
});
Rob Allsopp
  • 2,775
  • 3
  • 26
  • 45
  • 4
    react doesn't allow the use of focusin or focusout :/ – WouldBeNerd May 30 '19 at 03:31
  • too bad, at least vuejs does – ilrein Nov 07 '19 at 20:19
  • 5
    @WouldBeNerd The synthetic onBlur / onFocus events in React do bubble (like their native cousins focusout / fousin) and therefore you can just use those in React: `
    { if (!event.currentTarget.contains(event.relatedTarget)) doStuff(); }} />` And btw you _can_ use native events in React; you just have to get a `ref` and call `addEventListener` (which can be wrapped inside a [hook](https://github.com/teetotum/react-panoply/blob/master/src/useEvent/index.js))
    – Martin Jun 30 '20 at 17:53