Skip to content

Commit 93f761f

Browse files
authored
Merge pull request #7357 from plotly/alex/shadow-click
Correctly assign click event target in shadow dom
2 parents 24478b5 + 89d6404 commit 93f761f

File tree

6 files changed

+118
-25
lines changed

6 files changed

+118
-25
lines changed

draftlogs/7357_fix.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Fix click event handling for plots in shadow DOM elements [[#7357](https://github.com/plotly/plotly.js/pull/7357)]

src/components/dragelement/index.js

+26-18
Original file line numberDiff line numberDiff line change
@@ -221,30 +221,38 @@ dragElement.init = function init(options) {
221221
if(gd._dragged) {
222222
if(options.doneFn) options.doneFn();
223223
} else {
224-
if(options.clickFn) options.clickFn(numClicks, initialEvent);
224+
// If you're in a shadow DOM the target here gets pushed
225+
// up to the container in the main DOM. (why only here? IDK)
226+
// Don't make an event at all, just an object that looks like one,
227+
// since the shadow DOM puts restrictions on what can go in the event,
228+
// but copy as much as possible since it will be passed on to
229+
// plotly_click handlers
230+
var clickEvent;
231+
if (initialEvent.target === initialTarget) {
232+
clickEvent = initialEvent;
233+
} else {
234+
clickEvent = {
235+
target: initialTarget,
236+
srcElement: initialTarget,
237+
toElement: initialTarget
238+
};
239+
Object.keys(initialEvent)
240+
.concat(Object.keys(initialEvent.__proto__))
241+
.forEach(k => {
242+
var v = initialEvent[k];
243+
if (!clickEvent[k] && (typeof v !== 'function')) {
244+
clickEvent[k] = v;
245+
}
246+
});
247+
}
248+
if(options.clickFn) options.clickFn(numClicks, clickEvent);
225249

226250
// If we haven't dragged, this should be a click. But because of the
227251
// coverSlip changing the element, the natural system might not generate one,
228252
// so we need to make our own. But right clicks don't normally generate
229253
// click events, only contextmenu events, which happen on mousedown.
230254
if(!rightClick) {
231-
var e2;
232-
233-
try {
234-
e2 = new MouseEvent('click', e);
235-
} catch(err) {
236-
var offset = pointerOffset(e);
237-
e2 = document.createEvent('MouseEvents');
238-
e2.initMouseEvent('click',
239-
e.bubbles, e.cancelable,
240-
e.view, e.detail,
241-
e.screenX, e.screenY,
242-
offset[0], offset[1],
243-
e.ctrlKey, e.altKey, e.shiftKey, e.metaKey,
244-
e.button, e.relatedTarget);
245-
}
246-
247-
initialTarget.dispatchEvent(e2);
255+
initialTarget.dispatchEvent(new MouseEvent('click', e));
248256
}
249257
}
250258

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
'use strict';
2+
3+
module.exports = function createShadowGraphDiv() {
4+
var container = document.createElement('div');
5+
container.id = 'shadowcontainer';
6+
document.body.appendChild(container);
7+
var root = container.attachShadow({mode: 'open'});
8+
var gd = document.createElement('div');
9+
gd.id = 'graph2';
10+
root.appendChild(gd);
11+
12+
// force the shadow container to be at position 0,0 no matter what
13+
container.style.position = 'fixed';
14+
container.style.left = 0;
15+
container.style.top = 0;
16+
17+
var style = document.createElement('style');
18+
root.appendChild(style);
19+
20+
for (var plotlyStyle of document.querySelectorAll('[id^="plotly.js-"]')) {
21+
for (var rule of plotlyStyle.sheet.rules) {
22+
style.sheet.insertRule(rule.cssText);
23+
}
24+
}
25+
return gd;
26+
};
+5-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
'use strict';
22

33
module.exports = function destroyGraphDiv() {
4-
var gd = document.getElementById('graph');
5-
6-
if(gd) document.body.removeChild(gd);
4+
// remove both plain graphs and shadow DOM graph containers
5+
['graph', 'shadowcontainer'].forEach(function(id) {
6+
var el = document.getElementById(id);
7+
if(el) document.body.removeChild(el);
8+
});
79
};

test/jasmine/assets/mouse_event.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,12 @@ module.exports = function(type, x, y, opts) {
3333
fullOpts.shiftKey = opts.shiftKey;
3434
}
3535

36-
var el = (opts && opts.element) || document.elementFromPoint(x, y);
36+
var shadowContainer = document.getElementById('shadowcontainer');
37+
var elementRoot = (opts && opts.elementRoot) || (
38+
shadowContainer ? shadowContainer.shadowRoot : document
39+
);
40+
41+
var el = (opts && opts.element) || elementRoot.elementFromPoint(x, y);
3742
var ev;
3843

3944
if(type === 'scroll' || type === 'wheel') {

test/jasmine/tests/click_test.js

+54-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ var DBLCLICKDELAY = require('../../../src/plot_api/plot_config').dfltConfig.doub
77
var d3Select = require('../../strict-d3').select;
88
var d3SelectAll = require('../../strict-d3').selectAll;
99
var createGraphDiv = require('../assets/create_graph_div');
10+
var createShadowGraphDiv = require('../assets/create_shadow_graph_div');
1011
var destroyGraphDiv = require('../assets/destroy_graph_div');
1112

1213
var mouseEvent = require('../assets/mouse_event');
@@ -1059,17 +1060,17 @@ describe('Test click interactions:', function() {
10591060
width: 600,
10601061
height: 600
10611062
}).then(function() {
1062-
expect(gd.layout.xaxis.range).toBeCloseToArray([1, 2.068]);
1063+
expect(gd.layout.xaxis.range).toBeCloseToArray([1, 2.068], 1);
10631064

10641065
return doubleClick(300, 300);
10651066
})
10661067
.then(function() {
1067-
expect(gd.layout.xaxis.range).toBeCloseToArray([-0.2019, 3.249]);
1068+
expect(gd.layout.xaxis.range).toBeCloseToArray([-0.2019, 3.249], 1);
10681069

10691070
return doubleClick(300, 300);
10701071
})
10711072
.then(function() {
1072-
expect(gd.layout.xaxis.range).toBeCloseToArray([1, 2.068]);
1073+
expect(gd.layout.xaxis.range).toBeCloseToArray([1, 2.068], 1);
10731074
})
10741075
.then(done, done.fail);
10751076
});
@@ -1164,6 +1165,56 @@ describe('Test click interactions:', function() {
11641165
});
11651166
});
11661167

1168+
describe('Click events in Shadow DOM', function() {
1169+
afterEach(destroyGraphDiv);
1170+
1171+
function fig() {
1172+
var x = [];
1173+
var y = [];
1174+
for (var i = 0; i <= 20; i++) {
1175+
for (var j = 0; j <= 20; j++) {
1176+
x.push(i);
1177+
y.push(j);
1178+
}
1179+
}
1180+
return {
1181+
data: [{x: x, y: y, mode: 'markers'}],
1182+
layout: {
1183+
width: 400,
1184+
height: 400,
1185+
margin: {l: 100, r: 100, t: 100, b: 100},
1186+
xaxis: {range: [0, 20]},
1187+
yaxis: {range: [0, 20]},
1188+
}
1189+
};
1190+
}
1191+
1192+
it('should select the same point in regular and shadow DOM', function(done) {
1193+
var clickData;
1194+
var clickX = 120;
1195+
var clickY = 150;
1196+
var expectedX = 2; // counts up 1 every 10px from 0 at 100px
1197+
var expectedY = 15; // counts down 1 every 10px from 20 at 100px
1198+
1199+
function check(gd) {
1200+
gd.on('plotly_click', function(event) { clickData = event; });
1201+
click(clickX, clickY);
1202+
expect(clickData.points.length).toBe(1);
1203+
var pt = clickData.points[0];
1204+
expect(pt.x).toBe(expectedX);
1205+
expect(pt.y).toBe(expectedY);
1206+
clickData = null;
1207+
}
1208+
1209+
Plotly.newPlot(createGraphDiv(), fig())
1210+
.then(check)
1211+
.then(destroyGraphDiv)
1212+
.then(function() { return Plotly.newPlot(createShadowGraphDiv(), fig()) })
1213+
.then(check)
1214+
.then(done, done.fail);
1215+
});
1216+
});
1217+
11671218

11681219
describe('dragbox', function() {
11691220
afterEach(destroyGraphDiv);

0 commit comments

Comments
 (0)