This is an automated email from the ASF dual-hosted git repository. zisen pushed a commit to branch fix/tooltip-hide-on-scroll-appendto in repository https://gitbox.apache.org/repos/asf/echarts.git
commit 82c73b37a624d65afaa4aca24e0afb6aa74ecc1d Author: jinzisen <[email protected]> AuthorDate: Wed Apr 8 16:34:47 2026 +0800 fix(tooltip): hide appendTo HTML tooltip on scroll Add an auto hide-on-scroll path for HTML tooltips appended outside chart containers, preventing stale tooltips during page/touch scrolling. Also add a regression test page and runTest action to cover appendToBody behavior. Made-with: Cursor --- .gitignore | 2 + src/component/tooltip/TooltipModel.ts | 9 ++ src/component/tooltip/TooltipView.ts | 78 +++++++++++++++++ test/runTest/actions/tooltip-hideOnScroll.json | 13 +++ test/tooltip-hideOnScroll.html | 114 +++++++++++++++++++++++++ 5 files changed, 216 insertions(+) diff --git a/.gitignore b/.gitignore index acc8c8bcb..49dc3d46c 100644 --- a/.gitignore +++ b/.gitignore @@ -212,4 +212,6 @@ todo /renderers.d.ts /features.js /features.d.ts +/dist/echarts.js +/dist/echarts.js.map *.tgz diff --git a/src/component/tooltip/TooltipModel.ts b/src/component/tooltip/TooltipModel.ts index f48d64144..c7f4fafb0 100644 --- a/src/component/tooltip/TooltipModel.ts +++ b/src/component/tooltip/TooltipModel.ts @@ -71,6 +71,12 @@ export interface TooltipOption extends CommonTooltipOption<TopLevelFormatterPara */ appendTo?: ((chartContainer: HTMLElement) => HTMLElement | undefined | null) | string | HTMLElement + /** + * Hide tooltip on document scroll/touch scroll. + * `'auto'` enables this behavior when tooltip is appended outside chart container. + */ + hideOnScroll?: boolean | 'auto' + /** * Specify the class name of tooltip element * Only available when renderMode is html @@ -112,6 +118,9 @@ class TooltipModel extends ComponentModel<TooltipOption> { renderMode: 'auto', // 'auto' | 'html' | 'richText' + // Hide tooltip while page or container scrolls. + hideOnScroll: 'auto', + // whether restraint content inside viewRect. // If renderMode: 'richText', default true. // If renderMode: 'html', defaults to `false` (for backward compat). diff --git a/src/component/tooltip/TooltipView.ts b/src/component/tooltip/TooltipView.ts index 11d965f8e..32a218f00 100644 --- a/src/component/tooltip/TooltipView.ts +++ b/src/component/tooltip/TooltipView.ts @@ -160,6 +160,10 @@ class TooltipView extends ComponentView { private _lastDataByCoordSys: DataByCoordSys[]; private _cbParamsList: TooltipCallbackDataParams[]; + private _hideOnScrollListener: EventListener; + private _hideOnScrollAttached: boolean = false; + private _hideOnScrollDocument: Document | null; + private _hideOnScrollWindow: Window | null; init(ecModel: GlobalModel, api: ExtensionAPI) { if (env.node || !api.getDom()) { @@ -199,6 +203,7 @@ class TooltipView extends ComponentView { tooltipContent.setEnterable(tooltipModel.get('enterable')); this._initGlobalListener(); + this._updateHideOnScrollListener(); this._keepShow(); @@ -237,6 +242,78 @@ class TooltipView extends ComponentView { ); } + private _isHideOnScrollEnabled() { + const tooltipModel = this._tooltipModel; + if (!tooltipModel || this._renderMode === 'richText') { + return false; + } + + const hideOnScroll = tooltipModel.get('hideOnScroll'); + if (hideOnScroll === true) { + return true; + } + if (hideOnScroll === false) { + return false; + } + + // Auto mode: enable only when tooltip content is appended outside chart container. + return !!(tooltipModel.get('appendToBody', true) || tooltipModel.get('appendTo', true)); + } + + private _updateHideOnScrollListener() { + if (!this._isHideOnScrollEnabled()) { + this._removeHideOnScrollListener(); + return; + } + if (this._hideOnScrollAttached) { + return; + } + + const apiDom = this._api && this._api.getDom(); + const doc = apiDom && apiDom.ownerDocument; + if (!doc) { + return; + } + + const win = doc.defaultView; + if (!win) { + return; + } + const listener = this._hideOnScrollListener || (this._hideOnScrollListener = () => { + const tooltipContent = this._tooltipContent; + if (!tooltipContent || !tooltipContent.isShow()) { + return; + } + if (this._tooltipModel && this._tooltipModel.get('triggerOn') === 'none') { + return; + } + this._hide(bind(this._api.dispatchAction, this._api)); + }); + + doc.addEventListener('scroll', listener, true); + doc.addEventListener('touchmove', listener, true); + win.addEventListener('scroll', listener, true); + this._hideOnScrollDocument = doc; + this._hideOnScrollWindow = win; + this._hideOnScrollAttached = true; + } + + private _removeHideOnScrollListener() { + if (!this._hideOnScrollAttached || !this._hideOnScrollListener) { + return; + } + + this._hideOnScrollDocument + && this._hideOnScrollDocument.removeEventListener('scroll', this._hideOnScrollListener, true); + this._hideOnScrollDocument + && this._hideOnScrollDocument.removeEventListener('touchmove', this._hideOnScrollListener, true); + this._hideOnScrollWindow + && this._hideOnScrollWindow.removeEventListener('scroll', this._hideOnScrollListener, true); + this._hideOnScrollAttached = false; + this._hideOnScrollDocument = null; + this._hideOnScrollWindow = null; + } + private _keepShow() { const tooltipModel = this._tooltipModel; const ecModel = this._ecModel; @@ -1048,6 +1125,7 @@ class TooltipView extends ComponentView { return; } clear(this, '_updatePosition'); + this._removeHideOnScrollListener(); this._tooltipContent.dispose(); globalListener.unregister('itemTooltip', api); diff --git a/test/runTest/actions/tooltip-hideOnScroll.json b/test/runTest/actions/tooltip-hideOnScroll.json new file mode 100644 index 000000000..cd9880c92 --- /dev/null +++ b/test/runTest/actions/tooltip-hideOnScroll.json @@ -0,0 +1,13 @@ +[{ + "name": "Action 1", + "ops": [ + { + "type": "screenshot-auto", + "time": 1400, + "delay": 400 + } + ], + "scrollY": 0, + "scrollX": 0, + "timestamp": 1712563200000 +}] diff --git a/test/tooltip-hideOnScroll.html b/test/tooltip-hideOnScroll.html new file mode 100644 index 000000000..8f35e199d --- /dev/null +++ b/test/tooltip-hideOnScroll.html @@ -0,0 +1,114 @@ +<!DOCTYPE html> +<!-- +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +--> +<html> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <script src="lib/simpleRequire.js"></script> + <script src="lib/config.js"></script> + <script src="lib/facePrint.js"></script> + <script src="lib/testHelper.js"></script> + <link rel="stylesheet" href="lib/reset.css" /> +</head> +<body> +<style> + .chart { + height: 260px; + margin: 12px 0; + } +</style> +<div id="main-append-to-body" class="chart"></div> +<div id="main-default-append" class="chart"></div> +<script> +require(['echarts'], function (echarts) { + function createOption(appendToBody) { + return { + tooltip: { + trigger: 'item', + appendToBody: appendToBody + }, + xAxis: { type: 'category', data: ['a', 'b', 'c'] }, + yAxis: { type: 'value' }, + series: [{ + type: 'line', + symbolSize: 16, + data: [12, 20, 15] + }] + }; + } + + function interceptHideTip(chart) { + var hideTipCount = 0; + var rawDispatchAction = chart.dispatchAction; + chart.dispatchAction = function (payload) { + if (payload && payload.type === 'hideTip') { + hideTipCount++; + } + return rawDispatchAction.call(this, payload); + }; + return function () { + return hideTipCount; + }; + } + + var chartAppendToBody = testHelper.create(echarts, 'main-append-to-body', { + title: [ + 'appendToBody: true', + 'dispatch scroll event should auto hide tooltip' + ], + option: createOption(true), + height: 260 + }); + var chartDefault = testHelper.create(echarts, 'main-default-append', { + title: [ + 'appendToBody: false', + 'dispatch scroll event should keep tooltip logic unchanged' + ], + option: createOption(false), + height: 260 + }); + + var getHideTipCountAppendToBody = interceptHideTip(chartAppendToBody); + var getHideTipCountDefault = interceptHideTip(chartDefault); + + setTimeout(function () { + chartAppendToBody.dispatchAction({ type: 'showTip', seriesIndex: 0, dataIndex: 0 }); + chartDefault.dispatchAction({ type: 'showTip', seriesIndex: 0, dataIndex: 0 }); + + setTimeout(function () { + var evt = new Event('scroll', { bubbles: true, cancelable: true }); + document.dispatchEvent(evt); + + setTimeout(function () { + if (getHideTipCountAppendToBody() <= 0) { + throw new Error('Expected hideTip when appendToBody=true and document scrolls.'); + } + if (getHideTipCountDefault() > 0) { + throw new Error('Unexpected hideTip when appendToBody=false in auto mode.'); + } + // Helps visual check in local manual run. + facePrint('tooltip hideOnScroll check passed'); + }, 100); + }, 100); + }, 100); +}); +</script> +</body> +</html> --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
