Browsers do not implement the specification order
The specification's event order disagrees strongly with most browser's event order. (Check updated date of this article in case it's changed!)
According to the spec, the order is as follows. Let A be the element with focus. Let B be the element to be focused.
- A is about to lose focus (A.focusout)
- B is about to gain focus (B.focusin)
- A has lost focus (A.blur)
- B has gained focus (B.focus)
Most browsers will order as follows:
- A loses focus (A.blur)
- A loses focus, repeat (A.focusout)
- B gains focus (B.focus)
- B gains focus repeat (B.focusin)
Why the repeats? Because focusin and focusout bubble, while focus and blur do not.
This is an unfortunate state of affairs because there is no interleaving events in most browsers. Interleaving blur/focus events would allow for simplified logic, aka an easier time programming, in certain circumstances.
A false negative in tracking group focus
Let's say you want to track combined focus of a few elements. You have a groupFocus
boolean that tracks whether any group elements are focused, e.g., groupFocus = a.focus || b.focus
.
At first you might think to do this by changing state on blur
and focus
events. E.g., a.onblur => a.focus = false
, b.onfocus => b.focus = true
, etc. This would lead to a brief false-negative after a.onblur
flips a.focus
to false. Both a
and b
are briefly in a false
focus state. Eventually b.onfocus
will run, but until then, any calculations based on groupFocus
will lead to unexpected behaviors.
Spec order could fix this
One solution to this problem is via the spec's ordering, and using focusin
and blur
. They are ordered in a way that preserves the groupFocus
boolean in the expected way. After b.onfocusin
both a
and b
are true. Then, after a.blur
, a
is false but b
stays true. Thus, groupFocus
remains true the whole time.
FocusEvent.relatedTarget currently fixes this
For normal events there are usually two targets attached to the event object. event.currentTarget
is the element that the event handler is attached to, and event.target
is the element that triggered the event. So a parent can catch a bubbling event from a child interaction. In such a situation, the parent is the currentTarget
and the child is the target
.
For focus events there's an additional target, event.relatedTarget
. The related target corresponds to the "secondary" target being interacted with. On blur, it's the "other element" that's receiving the focus. On focus, it's the "other element" that is losing the focus.
With this, you can fix the false negative. On focusout you can check the relatedTarget
. If it's in the group, groupFocus
is true
, otherwise, groupFocus
is false.
React blur and focus
React blur and focus do bubble. This likely means they are actually using focusout and focusin internally, but I haven't confirmed. The real takeaway here is that you can setup a onBlur
or onFocus
on a parent element with the expectation of catching the event on bubble up.
For example, you can set an onBlur
on a group wrapper div (with tabindex
as well) and then catch any blurs. You can then check for containment: e.currentTarget.contains(e.relatedTarget)
. If it does, groupFocus
is still true, otherwise false
.
The future I hope for
I hope the browsers will implement focus event ordering as per the spec. This would mean there are meaningful ordering differences between all the events. React (and other frameworks) could then support all four focus event types.