Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions draftlogs/7357_fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Fix click event handling for plots in shadow DOM elements [[#7357](https://github.com/plotly/plotly.js/pull/7357)]
44 changes: 26 additions & 18 deletions src/components/dragelement/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice cleanup. 🏆

}
}

Expand Down
26 changes: 26 additions & 0 deletions test/jasmine/assets/create_shadow_graph_div.js
Original file line number Diff line number Diff line change
@@ -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;
};
8 changes: 5 additions & 3 deletions test/jasmine/assets/destroy_graph_div.js
Original file line number Diff line number Diff line change
@@ -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);
});
};
7 changes: 6 additions & 1 deletion test/jasmine/assets/mouse_event.js
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
57 changes: 54 additions & 3 deletions test/jasmine/tests/click_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I slightly loosened this test, then this whole file passed locally. I had to look it up, apparently "1" as the second arg means a range of 10^-1, ie a span of 0.1, ie +/- 0.05, whereas the default is +/- 0.005 and I was getting errors of ~0.007.


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);
});
Expand Down Expand Up @@ -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);
Expand Down