You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@echarts.apache.org by zi...@apache.org on 2026/04/08 08:35:44 UTC
(echarts) 01/01: fix(tooltip): hide appendTo HTML tooltip on scroll
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 <ji...@guandata.com>
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: commits-unsubscribe@echarts.apache.org
For additional commands, e-mail: commits-help@echarts.apache.org