9

I have a form where whenever one of the fields is focused (i.e., the user has clicked or tabbed into it), a div with extra information about that field is shown. Pretty simple: onFocus sets display: none; on this info pane, and onBlur removes it.

I only want these events to fire when clicking other elements on the same page, but they also fire when switching to another window or tab. Super annoying to see content come and go on the page every time you <Alt>-<Tab>.

Is there any way for JS to distinguish between these two kinds of blur events?


EDIT: I made a codepen to illustrate the problem. Open it up, click on the text input field, then alt-tab to another window to see some of the text disappear.

Here is the code in question:

<input id="foo" />

<p>
  Lorem ipsum dolor <span id="bar">sit amet consectetur</span>
</p>
.hidden {
  display: none;
}
const inputField = document.getElementById('foo')
const hiddenSpan = document.getElementById('bar')

inputField.addEventListener('focus', () => hiddenSpan.classList.add('hidden'))
inputField.addEventListener('blur', () => hiddenSpan.classList.remove('hidden'))
Ryan Lue
  • 916
  • 10
  • 29

2 Answers2

6

Credit to Reddit user /u/jcunews1 for this answer: Use document.activeElement to determine if the focus has actually left the target element.

const inputField = document.getElementById('foo');
const hiddenSpan = document.getElementById('bar');

inputField.addEventListener('focus', () => hiddenSpan.classList.add('hidden'));
inputField.addEventListener('blur', (event) => {
  if (event.target !== document.activeElement) { // This is where the magic happens!
    hiddenSpan.classList.remove('hidden');
  }
});
.hidden {
  display: none;
}
<input id="foo" />

<p>
  Lorem ipsum dolor <span id="bar">sit amet consectetur</span>
</p>
Ryan Lue
  • 916
  • 10
  • 29
3

The intended behavior when the window is blurred is to preserve the input's focus.

Investigation reveals that when a window is de-selected, a blur event is first sent to the active element, followed by a blur event to the window.

These two blur events happen in rapid succession, so you can take advantage of this to re-focus the input when appropriate:

  • record the time of the last input blur
  • intercept window blur event
  • if time between input blur and window blur is small, re-focus input

Working example:

  const inputField = document.getElementById('input');
  const inputSpan = document.getElementById('bar');
  const windowSpan = document.getElementById('window-span');

  let inputBlurTime = 0;

  inputField.addEventListener('focus', () => {
    inputSpan.innerHTML = ' FOCUSED';
  });

  inputField.addEventListener('blur', evt => {
    inputSpan.innerHTML = ' BLURRED';
    inputBlurTime = new Date().getTime();
  });

  window.addEventListener('focus', () => {
    windowSpan.innerHTML = ' FOCUSED';
  });

  window.addEventListener('blur', () => {
    windowSpan.innerHTML = ' BLURRED';
    const windowBlurTime = new Date().getTime();
    if (windowBlurTime - inputBlurTime < 100) {
      windowSpan.innerHTML += ' (re-focusing input)';
      inputField.focus();
      inputSpan.innerHTML = ' RE-FOCUSED';
    }
  });
<h4>Preserve Input Focus on Window Blur</h4>
<input id="input" />

<p>Input <span id="bar">INIT</span></p>
<p>Window <span id="window-span">INIT</span></p>
terrymorse
  • 6,771
  • 1
  • 21
  • 27
  • This is an excellent observation and workaround. Unfortunately, it didn't work for me because you don't know for sure what kind of blur it is until 100ms later (I actually experienced quite a bit of variability in latency between the element/window blur events, between 75ms and 110ms), and this latency leads to jitter in my UI effects. – Ryan Lue May 12 '20 at 17:12