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]

Reply via email to