ovangle ovangle - 6 months ago 36
Javascript Question

Determine if user clicked outside shadow dom

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.

Answer

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>

Comments