Skip to content

Correctly assign click event target in shadow dom #7357

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Feb 17, 2025
Merged
Show file tree
Hide file tree
Changes from all 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