Determine if user clicked outside shadow dom

2019-06-16 10:59发布

I'm trying to implement a dropdown which you can click outside to close. The dropdown is part of a custom date input and is encapsulated inside the input's shadow DOM.

I want to write something like:

window.addEventListener('mousedown', function (evt) {
  if (!componentNode.contains(evt.target)) {
    closeDropdown();
  }
});

however, the event is retargeted, so evt.target is always the outside the element. There are multiple shadow boundaries that the event will cross before reaching the window, so there seems to be no way of actually knowing if the user clicked inside my component or not.

Note: I'm not using polymer anywhere -- I need an answer which applies to generic shadow DOM, not a polymer specific hack.

3条回答
走好不送
2楼-- · 2019-06-16 11:36

Another option is to check the event cursor offsets against the target element:

listener(event) {
    const { top, right, bottom, left } = targetElement.getBoundingClientRect();
    const { pageX, pageY } = event;
    const isInside = pageX >= left && pageX <= right && pageY >= top && pageY <= bottom;
}
查看更多
SAY GOODBYE
3楼-- · 2019-06-16 11:59

You can try using the path property of the event object. Haven't found a actual reference for it and MDN doesn't yet have a page for it. HTML5Rocks has a small section about it in there shadow dom tutorials though. As such I do not know the compatibility of this across browsers.

Found the W3 Spec about event paths, not sure if this is meant exactly for the Event.path property or not, but it is the closest reference I could find.

If anyone knows an actual spec reference to Event.path (if the linked spec page isn't already it) feel free to edit it in.

It holds the path the event went through. It will contain elements that are in a shadow dom. The first element in the list ( path[0] ) should be the element that was actually clicked on. Note you will need to call contains from the shadow dom reference, eg shadowRoot.contains(e.path[0]) or some sub element within your shadow dom.

Demo: Click menu to expand, clicking anywhere except on the menu items will close menu.

var host = document.querySelector('#host');
var root = host.createShadowRoot();
d = document.createElement("div");
d.id = "shadowdiv";

d.innerHTML = `
  <div id="menu">
    <div class="menu-item menu-toggle">Menu</div>
    <div class="menu-item">Item 1</div>
    <div class="menu-item">Item 2</div>
    <div class="menu-item">Item 3</div>
  </div>
  <div id="other">Other shadow element</div>
`;
var menuToggle = d.querySelector(".menu-toggle");
var menu = d.querySelector("#menu");
menuToggle.addEventListener("click",function(e){
  menu.classList.toggle("active");
});
root.appendChild(d)

//Use document instead of window
document.addEventListener("click",function(e){
  if(!menu.contains(e.path[0])){
    menu.classList.remove("active");
  }
});
#host::shadow #menu{
  height:24px;
  width:150px;
  transition:height 1s;
  overflow:hidden;
  background:black;
  color:white;
}
#host::shadow #menu.active {
  height:300px;
}
#host::shadow #menu .menu-item {
  height:24px;
  text-align:center;
  line-height:24px;
}

#host::shadow #other {
  position:absolute;
  right:100px;
  top:0px;
  background:yellow;
  width:100px;
  height:32px;
  font-size:12px;
  padding:4px;
}
<div id="host"></div>

查看更多
叼着烟拽天下
4楼-- · 2019-06-16 12:01

The event.target of the shadowRoot would be the host element. To close a <select> element within shadowDOM if event.target is not host element you can use if (evt.target !== hostElement), then call .blur() on hostElement

var input = document.querySelector("input");
var shadow = input.createShadowRoot();
var template = document.querySelector("template");
var clone = document.importNode(template.content, true);
shadow.appendChild(clone);

window.addEventListener("mousedown", function (evt) {
  if (evt.target !== input) {
    input.blur();
  }
});
<input type="date" />
<template>
  <select>
    <option value="1999">1999</option>
    <option value="2000">2000</option>
  </select>
</template>

查看更多
登录 后发表回答