Skip to content

Commit

Permalink
Avoid recursive event replay loops (#8738)
Browse files Browse the repository at this point in the history
* chart._lastEvent = null while processing onHover

* Pass replay flag to external tooltip

* Add test for replay

* cc
  • Loading branch information
kurkle authored Mar 27, 2021
1 parent 396cbcb commit b2c7baf
Show file tree
Hide file tree
Showing 5 changed files with 65 additions and 12 deletions.
15 changes: 9 additions & 6 deletions src/core/core.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -1075,8 +1075,7 @@ class Chart {
*/
_handleEvent(e, replay) {
const me = this;
const lastActive = me._active || [];
const options = me.options;
const {_active: lastActive = [], options} = me;
const hoverOptions = options.hover;

// If the event is replayed from `update`, we should evaluate with the final positions.
Expand All @@ -1096,14 +1095,16 @@ class Chart {

let active = [];
let changed = false;
let lastEvent = null;

// Find Active Elements for hover and tooltips
if (e.type === 'mouseout') {
me._lastEvent = null;
} else {
if (e.type !== 'mouseout') {
active = me.getElementsAtEventForMode(e, hoverOptions.mode, hoverOptions, useFinalPosition);
me._lastEvent = e.type === 'click' ? me._lastEvent : e;
lastEvent = e.type === 'click' ? me._lastEvent : e;
}
// Set _lastEvent to null while we are processing the event handlers.
// This prevents recursion if the handler calls chart.update()
me._lastEvent = null;

// Invoke onHover hook
callCallback(options.onHover || hoverOptions.onHover, [e, active, me], me);
Expand All @@ -1120,6 +1121,8 @@ class Chart {
me._updateHoverStyles(active, lastActive, replay);
}

me._lastEvent = lastEvent;

return changed;
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/plugins/plugin.tooltip.js
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,7 @@ export class Tooltip extends Element {
return tooltipItems;
}

update(changed) {
update(changed, replay) {
const me = this;
const options = me.options.setContext(me.getContext());
const active = me._active;
Expand Down Expand Up @@ -574,7 +574,7 @@ export class Tooltip extends Element {
}

if (changed && options.external) {
options.external.call(me, {chart: me._chart, tooltip: me});
options.external.call(me, {chart: me._chart, tooltip: me, replay});
}
}

Expand Down Expand Up @@ -1023,7 +1023,7 @@ export class Tooltip extends Element {
y: e.y
};

me.update(true);
me.update(true, replay);
}
}

Expand Down
50 changes: 50 additions & 0 deletions test/fixtures/controller.doughnut/event-replay.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
function drawMousePoint(ctx, center) {
ctx.beginPath();
ctx.arc(center.x, center.y, 8, 0, Math.PI * 2);
ctx.fillStyle = 'yellow';
ctx.fill();
}

const canvas = document.createElement('canvas');
canvas.width = 512;
canvas.height = 512;
const ctx = canvas.getContext('2d');

module.exports = {
config: {
type: 'pie',
data: {
datasets: [{
backgroundColor: ['red', 'green', 'blue'],
hoverBackgroundColor: 'black',
data: [1, 1, 1]
}]
}
},
options: {
canvas: {
width: 512,
height: 512
},
async run(chart) {
ctx.drawImage(chart.canvas, 0, 0, 256, 256);

const arc = chart.getDatasetMeta(0).data[0];
const center = arc.getCenterPoint();
await jasmine.triggerMouseEvent(chart, 'mousemove', arc);
drawMousePoint(chart.ctx, center);
ctx.drawImage(chart.canvas, 256, 0, 256, 256);

chart.toggleDataVisibility(0);
chart.update();
drawMousePoint(chart.ctx, center);
ctx.drawImage(chart.canvas, 0, 256, 256, 256);

await jasmine.triggerMouseEvent(chart, 'mouseout', arc);
ctx.drawImage(chart.canvas, 256, 256, 256, 256);

Chart.helpers.clearCanvas(chart.canvas);
chart.ctx.drawImage(canvas, 0, 0);
}
}
};
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions test/specs/plugin.tooltip.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -917,7 +917,7 @@ describe('Plugin.Tooltip', function() {

// First dispatch change event, should update tooltip
await jasmine.triggerMouseEvent(chart, 'mousemove', firstPoint);
expect(tooltip.update).toHaveBeenCalledWith(true);
expect(tooltip.update).toHaveBeenCalledWith(true, undefined);

// Reset calls
tooltip.update.calls.reset();
Expand Down Expand Up @@ -980,15 +980,15 @@ describe('Plugin.Tooltip', function() {

// First dispatch change event, should update tooltip
await jasmine.triggerMouseEvent(chart, 'mousemove', firstPoint);
expect(tooltip.update).toHaveBeenCalledWith(true);
expect(tooltip.update).toHaveBeenCalledWith(true, undefined);

// Reset calls
tooltip.update.calls.reset();

// Second dispatch change event (same event), should update tooltip
// because position mode is 'nearest'
await jasmine.triggerMouseEvent(chart, 'mousemove', secondPoint);
expect(tooltip.update).toHaveBeenCalledWith(true);
expect(tooltip.update).toHaveBeenCalledWith(true, undefined);
});

describe('positioners', function() {
Expand Down

0 comments on commit b2c7baf

Please sign in to comment.