且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

当调用onBlur时,Firefox中relatedTarget为null

更新时间:2023-11-30 22:02:40

问题是按住SHIFT + TAB键,则会看到relatedTarget已按预期设置,因为在这种情况下该按钮确实获得了焦点.

我将运行代码以将div隐藏一段时间,以使click处理程序有机会先运行.

在下面的示例中,我实现了此建议,并添加了一些日志记录,以使查看工作情况更加容易:

 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> 

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?

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>