diff --git a/src/plugins/plugin.tooltip.js b/src/plugins/plugin.tooltip.js index b39681ce2ca..10a7cd9450c 100644 --- a/src/plugins/plugin.tooltip.js +++ b/src/plugins/plugin.tooltip.js @@ -641,7 +641,28 @@ export class Tooltip extends Element { let properties; let tooltipItems = []; - if (!active.length) { + if (active.length) { + tooltipItems = this._createItems(options); + } + + // Hide the tooltip when nothing is active or every item was filtered + // out, so content callbacks are not invoked with an empty item array + if (!tooltipItems.length) { + if (active.length) { + // Every item was filtered out: clear the content so the previous + // body is not drawn with the cleared label colors while fading out, + // and anchor the caret so an identical event is not a position change + this.title = []; + this.beforeBody = []; + this.body = []; + this.afterBody = []; + this.footer = []; + const position = positioners[options.position].call(this, active, this._eventPosition); + if (position !== false) { + this.caretX = position.x; + this.caretY = position.y; + } + } if (this.opacity !== 0) { properties = { opacity: 0 @@ -649,7 +670,6 @@ export class Tooltip extends Element { } } else { const position = positioners[options.position].call(this, active, this._eventPosition); - tooltipItems = this._createItems(options); this.title = this.getTitle(tooltipItems, options); this.beforeBody = this.getBeforeBody(tooltipItems, options); diff --git a/test/specs/plugin.tooltip.tests.js b/test/specs/plugin.tooltip.tests.js index 94ae45b7246..4873cef54f8 100644 --- a/test/specs/plugin.tooltip.tests.js +++ b/test/specs/plugin.tooltip.tests.js @@ -831,6 +831,112 @@ describe('Plugin.Tooltip', function() { })); }); + it('should hide the tooltip and not run content callbacks when all items are filtered out', async function() { + var titleCallback = jasmine.createSpy('titleCallback'); + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30] + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + }, + options: { + plugins: { + tooltip: { + mode: 'index', + filter: function() { + return false; + }, + callbacks: { + title: titleCallback + } + } + } + } + }); + + var meta = chart.getDatasetMeta(0); + var point = meta.data[1]; + + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + + expect(titleCallback).not.toHaveBeenCalled(); + expect(chart.tooltip.opacity).toBe(0); + }); + + it('should stay drawable while fading out after all items become filtered', async function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30] + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + }, + options: { + plugins: { + tooltip: { + mode: 'index', + filter: function(item) { + return item.dataIndex !== 2; + } + } + } + } + }); + + var meta = chart.getDatasetMeta(0); + + // Show the tooltip on an accepted item, then move to a filtered one + await jasmine.triggerMouseEvent(chart, 'mousemove', meta.data[1]); + expect(chart.tooltip.body.length).toBe(1); + await jasmine.triggerMouseEvent(chart, 'mousemove', meta.data[2]); + + expect(chart.tooltip.body.length).toBe(0); + + // Drawing mid fade-out must not throw with the cleared label colors + chart.tooltip.opacity = 0.5; + expect(function() { + chart.tooltip.draw(chart.ctx); + }).not.toThrow(); + }); + + it('should not report a position change for repeated events on filtered items', async function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30] + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + }, + options: { + plugins: { + tooltip: { + mode: 'index', + filter: function() { + return false; + } + } + } + } + }); + + var meta = chart.getDatasetMeta(0); + var point = meta.data[1]; + + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + + var updateSpy = spyOn(chart.tooltip, 'update').and.callThrough(); + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + + expect(updateSpy).not.toHaveBeenCalled(); + }); + it('should set the caretPadding based on a config setting', async function() { var chart = window.acquireChart({ type: 'line',