palantir / plottable

:bar_chart: A library of modular chart components built on D3
http://plottablejs.org/
MIT License
2.97k stars 221 forks source link

Plot interactions don't work inside Polymer elements when using shadow DOM mode #3350

Open jameswex opened 7 years ago

jameswex commented 7 years ago

I have a plottable v3 chart inside of a Polymer 1.x component and have interactions set up on the chart (click and pointer interactions). Polymer by default uses shady DOM, and in this mode the click interactions work as intended (see https://www.polymer-project.org/1.0/docs/devguide/settings for details and how to set the DOM rendering mode).

But, if I set Polymer to use the shadow DOM mode then the click interactions no longer work. I am running into this because I am embedding this Polymer element in a jupyter notebook through the declarativewidgets plugin (https://github.com/jupyter-widgets/declarativewidgets) which hard-codes shadow DOM mode.

Stepping through the plottable code, the interactions fail because of the Translator.prototype.isInside method. It checks if the event.target element is contained in the click-enabled component's root element (which seems to be the element that plottable is rendering the chart to).

With shady DOM, when clicking on an interactive chart element, the event.target is the correct svg element in the chart, so isInside returns true. With shadow DOM, the event.target is the Polymer component's top-level HTML element, so isInside returns false. This happens because of shadow DOM event retargeting (https://www.polymer-project.org/2.0/docs/devguide/shadow-dom#event-retargeting).

If I change the plottable Translator.prototype.isInside method to use event.path[0] or event.composedPath()[0] (which is the actual clicked svg element in the chart) instead of event.target, then the interactions work correctly in both case, but I'm not sure of the support of path and composedPath() across all browsers. I am working in the latest Chrome for reference.

hellochar commented 7 years ago

Thanks for the detailed report @jameswex , cool to hear about Plottable being used inside Polymer! I couldn't find any docs on event.path - is it polymer specific? I'm wary of adding shadow DOM specific features since I don't know of a good way to unit test it/make it part of the build (admittedly I know little about Polymer/shadow DOM). Perhaps there's another property or different strategy to solve the problem?

jameswex commented 7 years ago

event.path may be a chrome-specific implementation detail. I think event.composedPath() is the actual spec'd method. I see event.composedPath() mentioned both in the polymer link above about event retargeting and also in the dom event spec (https://dom.spec.whatwg.org/#dom-event-composedpath)

stijnkoopal commented 7 years ago

For people interested in a monkey patch for 3.4.1:

Plottable.Utils.Translator.isEventInside = (component, e) =>
  Plottable.Utils.DOM.contains(component.root().rootElement().node(), e.composedPath()[0]);
stijnkoopal commented 7 years ago

For 3.5.4 you also require the following:

Plottable.Utils.DOM.getHtmlElementAncestors = elem => {
  const elems = [];
  while (elem && elem instanceof HTMLElement) {
    elems.push(elem);
    elem = elem.offsetParent || elem.parentElement || elem.parentNode;
  }
  return elems;
};
stephanwlee commented 5 years ago

Sorry for rebooting this old thread.

This issue seems to be a general webcomponent issue[1]. As you have remarked, an event listener attached at document cannot access shadowDom via event.target which I think is by design.

Instead of the monkey patches, would it be possible to allow modification to _eventTarget? https://github.com/palantir/plottable/blob/88158e2883f7e03bbf2ca5499e0a8ec9fd65cc94/src/dispatchers/dispatcher.ts#L24

Because the constructor is private, I can't even subclass the Mouse/Touch/Key dispatchers :( https://github.com/palantir/plottable/blob/88158e2883f7e03bbf2ca5499e0a8ec9fd65cc94/src/dispatchers/mouseDispatcher.ts#L50

[1]:

(function() {
  class Hello extends HTMLElement {
    constructor() {
      super();
      const shadow = this.attachShadow({mode: 'open'});
      const button = document.createElement('button');
      button.innerText = "click me";
      shadow.appendChild(button);
    }
  }

  window.customElements.define('hello-world', Hello);
  const el = document.createElement("hello-world");
  document.body.appendChild(el);
  document.addEventListener('click', (e) => {
    // clicking on a button does not give e.target of button. Instead, it returns `<hello-world>`
    console.log('click!', e.target);
  });
})();