diff --git a/draftlogs/7357_fix.md b/draftlogs/7357_fix.md new file mode 100644 index 00000000000..73a4044077b --- /dev/null +++ b/draftlogs/7357_fix.md @@ -0,0 +1 @@ +- Fix click event handling for plots in shadow DOM elements [[#7357](https://github.com/plotly/plotly.js/pull/7357)] diff --git a/src/components/dragelement/index.js b/src/components/dragelement/index.js index 1f7c0e4d8d7..16a75982f0c 100644 --- a/src/components/dragelement/index.js +++ b/src/components/dragelement/index.js @@ -221,30 +221,38 @@ dragElement.init = function init(options) { if(gd._dragged) { if(options.doneFn) options.doneFn(); } else { - if(options.clickFn) options.clickFn(numClicks, initialEvent); + // If you're in a shadow DOM the target here gets pushed + // up to the container in the main DOM. (why only here? IDK) + // Don't make an event at all, just an object that looks like one, + // since the shadow DOM puts restrictions on what can go in the event, + // but copy as much as possible since it will be passed on to + // plotly_click handlers + var clickEvent; + if (initialEvent.target === initialTarget) { + clickEvent = initialEvent; + } else { + clickEvent = { + target: initialTarget, + srcElement: initialTarget, + toElement: initialTarget + }; + Object.keys(initialEvent) + .concat(Object.keys(initialEvent.__proto__)) + .forEach(k => { + var v = initialEvent[k]; + if (!clickEvent[k] && (typeof v !== 'function')) { + clickEvent[k] = v; + } + }); + } + if(options.clickFn) options.clickFn(numClicks, clickEvent); // If we haven't dragged, this should be a click. But because of the // coverSlip changing the element, the natural system might not generate one, // so we need to make our own. But right clicks don't normally generate // click events, only contextmenu events, which happen on mousedown. if(!rightClick) { - var e2; - - try { - e2 = new MouseEvent('click', e); - } catch(err) { - var offset = pointerOffset(e); - e2 = document.createEvent('MouseEvents'); - e2.initMouseEvent('click', - e.bubbles, e.cancelable, - e.view, e.detail, - e.screenX, e.screenY, - offset[0], offset[1], - e.ctrlKey, e.altKey, e.shiftKey, e.metaKey, - e.button, e.relatedTarget); - } - - initialTarget.dispatchEvent(e2); + initialTarget.dispatchEvent(new MouseEvent('click', e)); } } diff --git a/test/jasmine/assets/create_shadow_graph_div.js b/test/jasmine/assets/create_shadow_graph_div.js new file mode 100644 index 00000000000..c275bb0fd05 --- /dev/null +++ b/test/jasmine/assets/create_shadow_graph_div.js @@ -0,0 +1,26 @@ +'use strict'; + +module.exports = function createShadowGraphDiv() { + var container = document.createElement('div'); + container.id = 'shadowcontainer'; + document.body.appendChild(container); + var root = container.attachShadow({mode: 'open'}); + var gd = document.createElement('div'); + gd.id = 'graph2'; + root.appendChild(gd); + + // force the shadow container to be at position 0,0 no matter what + container.style.position = 'fixed'; + container.style.left = 0; + container.style.top = 0; + + var style = document.createElement('style'); + root.appendChild(style); + + for (var plotlyStyle of document.querySelectorAll('[id^="plotly.js-"]')) { + for (var rule of plotlyStyle.sheet.rules) { + style.sheet.insertRule(rule.cssText); + } + } + return gd; +}; diff --git a/test/jasmine/assets/destroy_graph_div.js b/test/jasmine/assets/destroy_graph_div.js index a1bd18b9741..a42669ac32e 100644 --- a/test/jasmine/assets/destroy_graph_div.js +++ b/test/jasmine/assets/destroy_graph_div.js @@ -1,7 +1,9 @@ 'use strict'; module.exports = function destroyGraphDiv() { - var gd = document.getElementById('graph'); - - if(gd) document.body.removeChild(gd); + // remove both plain graphs and shadow DOM graph containers + ['graph', 'shadowcontainer'].forEach(function(id) { + var el = document.getElementById(id); + if(el) document.body.removeChild(el); + }); }; diff --git a/test/jasmine/assets/mouse_event.js b/test/jasmine/assets/mouse_event.js index 3b6b0c9f466..ac0dfe0e09c 100644 --- a/test/jasmine/assets/mouse_event.js +++ b/test/jasmine/assets/mouse_event.js @@ -33,7 +33,12 @@ module.exports = function(type, x, y, opts) { fullOpts.shiftKey = opts.shiftKey; } - var el = (opts && opts.element) || document.elementFromPoint(x, y); + var shadowContainer = document.getElementById('shadowcontainer'); + var elementRoot = (opts && opts.elementRoot) || ( + shadowContainer ? shadowContainer.shadowRoot : document + ); + + var el = (opts && opts.element) || elementRoot.elementFromPoint(x, y); var ev; if(type === 'scroll' || type === 'wheel') { diff --git a/test/jasmine/tests/click_test.js b/test/jasmine/tests/click_test.js index 3c255eb546e..db9929cf794 100644 --- a/test/jasmine/tests/click_test.js +++ b/test/jasmine/tests/click_test.js @@ -7,6 +7,7 @@ var DBLCLICKDELAY = require('../../../src/plot_api/plot_config').dfltConfig.doub var d3Select = require('../../strict-d3').select; var d3SelectAll = require('../../strict-d3').selectAll; var createGraphDiv = require('../assets/create_graph_div'); +var createShadowGraphDiv = require('../assets/create_shadow_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); var mouseEvent = require('../assets/mouse_event'); @@ -1059,17 +1060,17 @@ describe('Test click interactions:', function() { width: 600, height: 600 }).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray([1, 2.068]); + expect(gd.layout.xaxis.range).toBeCloseToArray([1, 2.068], 1); return doubleClick(300, 300); }) .then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray([-0.2019, 3.249]); + expect(gd.layout.xaxis.range).toBeCloseToArray([-0.2019, 3.249], 1); return doubleClick(300, 300); }) .then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray([1, 2.068]); + expect(gd.layout.xaxis.range).toBeCloseToArray([1, 2.068], 1); }) .then(done, done.fail); }); @@ -1164,6 +1165,56 @@ describe('Test click interactions:', function() { }); }); +describe('Click events in Shadow DOM', function() { + afterEach(destroyGraphDiv); + + function fig() { + var x = []; + var y = []; + for (var i = 0; i <= 20; i++) { + for (var j = 0; j <= 20; j++) { + x.push(i); + y.push(j); + } + } + return { + data: [{x: x, y: y, mode: 'markers'}], + layout: { + width: 400, + height: 400, + margin: {l: 100, r: 100, t: 100, b: 100}, + xaxis: {range: [0, 20]}, + yaxis: {range: [0, 20]}, + } + }; + } + + it('should select the same point in regular and shadow DOM', function(done) { + var clickData; + var clickX = 120; + var clickY = 150; + var expectedX = 2; // counts up 1 every 10px from 0 at 100px + var expectedY = 15; // counts down 1 every 10px from 20 at 100px + + function check(gd) { + gd.on('plotly_click', function(event) { clickData = event; }); + click(clickX, clickY); + expect(clickData.points.length).toBe(1); + var pt = clickData.points[0]; + expect(pt.x).toBe(expectedX); + expect(pt.y).toBe(expectedY); + clickData = null; + } + + Plotly.newPlot(createGraphDiv(), fig()) + .then(check) + .then(destroyGraphDiv) + .then(function() { return Plotly.newPlot(createShadowGraphDiv(), fig()) }) + .then(check) + .then(done, done.fail); + }); +}); + describe('dragbox', function() { afterEach(destroyGraphDiv);