Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

relatedTarget is null in Firefox when onBlur is called

I'm trying to create a help message that will disappear when the user either clicks the toggle button to display the help message or clicks away by clicking elsewhere on the page. The solution appears to be to look at the relatedTarget property of the onblur event and prevent the onblur handler from running when the relatedTarget is the button to toggle the help message. This seems to work in Chrome, but in Firefox and Safari, the relatedTarget property is set to the container div rather than the button, which makes it impossible to distinguish between the toggle button click and a "click away".

I've created a simple demonstrator that illustrates the problem:

let openState = false;
let contentDiv = document.getElementById("show-hide-content");
let accordionDiv = document.getElementById("accordion");
let showHideButton = document.getElementById("show-hide-toggle");
function setOpenState(state) {
    openState = state;
    if(openState) {
        contentDiv.style.visibility = "visible";
        contentDiv.focus();
    }
    else {
        contentDiv.style.visibility = "hidden";
    }
}

function toggleVisibility(override) {
    if (typeof override === "boolean") {
        setOpenState(override);
    }
    else {
        setOpenState(!openState);
    }
}

function buttonClickHandler(event) {
    toggleVisibility();
}

function contentBlurHandler(event) {
    if(!accordionDiv.contains(event.relatedTarget)) {
        toggleVisibility(false);
    }
}

showHideButton.onclick = buttonClickHandler;
contentDiv.onblur = contentBlurHandler;
.parent {
    display: flex;
    width: 100vw;
    height: 100vh;
    flex-direction: row;
    background-color: #409958;
}

.accordion {
    background-color: #28a7c9;
}

.show-hide-content {
    visibility: hidden;
    background-color: #c91e63;
}
<h1>Show/Hide Test</h1>
<div class="parent">
  <div class="drawer">
    <div class="accordion" id="accordion">
      <button class="show-hide-toggle" id="show-hide-toggle">Show/Hide</button>
      <div class="show-hide-content" id="show-hide-content" tabindex="-1">
        <p>This is some content that should be shown or hidden depending on the toggle.</p>
      </div>
    </div>
    <div class="spacer">
    </div>
  </div>
</div>

This code works correctly in Chrome. However, in Firefox, clicking the "Show/Hide" button displays the hidden content, but doesn't hide it when the button is clicked again. As far as I can tell, this is because the onblur handler is hiding the div, and then the onclick handler is toggling it open again, even though I'm checking onblur.relatedTarget to ensure that it's not anywhere inside the drawer.

What is the correct way to detect click-away in Firefox?

like image 897
quanticle Avatar asked Jan 29 '26 19:01

quanticle


1 Answers

The problem is that clicking the <button> doesn't focus it on some browser/OS combinations (notably on macOS except in Chrome), so onblur's event.relatedTarget is null as nothing on the page receives focus. If you SHIFT+TAB from the <div id="show-hide-content">, you'll see that relatedTarget is set as you expect, as the button does receive focus in this scenario.

I would run the code to hide the div off a small timeout, to give the click handler a chance to run first.

In the example below I implemented this suggestion and added some logging to make it easier to see what's going on:

let openState = false;
let contentDiv = document.getElementById("show-hide-content");
let accordionDiv = document.getElementById("accordion");
let showHideButton = document.getElementById("show-hide-toggle");
function setOpenState(state) {
    openState = state;
    if(openState) {
        contentDiv.style.visibility = "visible";
        contentDiv.focus();
    }
    else {
        contentDiv.style.visibility = "hidden";
    }
}

function buttonClickHandler(event) {
    console.log("click, setting openState to", !openState);
    setOpenState(!openState);
}

function contentBlurHandler(event) {
    console.log("blur, relatedTarget is", event.relatedTarget);

    setTimeout(function() {
        console.log("blur, after timeout, openState is", openState);
        setOpenState(false);
    }, 100);
}

showHideButton.onclick = buttonClickHandler;
showHideButton.onmousedown = function() {console.log("button mousedown"); };
contentDiv.onblur = contentBlurHandler;
showHideButton.onfocus = function(ev) { console.log("button onfocus"); }
.parent {
    display: flex;
    width: 100vw;
    height: 100vh;
    flex-direction: row;
    background-color: #409958;
}

.accordion {
    background-color: #28a7c9;
}

.show-hide-content {
    visibility: hidden;
    background-color: #c91e63;
}
<h1>Show/Hide Test</h1>
<div class="parent">
  <div class="drawer">
    <div class="accordion" id="accordion">
      <button class="show-hide-toggle" id="show-hide-toggle">Show/Hide</button>
      <div class="show-hide-content" id="show-hide-content" tabindex="-1">
        <p>This is some content that should be shown or hidden depending on the toggle.</p>
      </div>
    </div>
    <div class="spacer">
    </div>
  </div>
</div>
like image 155
Nickolay Avatar answered Jan 31 '26 07:01

Nickolay