Bug 1270111 - Part 1: TooltipToggle doesn't hide the tooltip when mouse is over, shows and hides after a short delay. r=jdescottes
authorJarda Snajdr <jsnajdr@gmail.com>
Fri, 27 May 2016 05:09:00 +0200
changeset 338491 c79906974f052c8060195d4d81e067f3bb4cd338
parent 338490 640f046e0e79d18aa0d4c0efac7e9f5d365f09a0
child 338492 e682705064327ff3a67efa75485a31a6ebf763e0
push id6249
push userjlund@mozilla.com
push dateMon, 01 Aug 2016 13:59:36 +0000
treeherdermozilla-beta@bad9d4f5bf7e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjdescottes
bugs1270111
milestone49.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1270111 - Part 1: TooltipToggle doesn't hide the tooltip when mouse is over, shows and hides after a short delay. r=jdescottes
devtools/client/shadereditor/shadereditor.js
devtools/client/shared/widgets/Tooltip.js
devtools/client/shared/widgets/tooltip/TooltipToggle.js
--- a/devtools/client/shadereditor/shadereditor.js
+++ b/devtools/client/shadereditor/shadereditor.js
@@ -590,17 +590,19 @@ var ShadersEditorsView = {
   _onMarkerMouseOver: function (line, node, messages) {
     if (node._markerErrorsTooltip) {
       return;
     }
 
     let tooltip = node._markerErrorsTooltip = new Tooltip(document);
     tooltip.defaultOffsetX = GUTTER_ERROR_PANEL_OFFSET_X;
     tooltip.setTextContent({ messages: messages });
-    tooltip.startTogglingOnHover(node, () => true, GUTTER_ERROR_PANEL_DELAY);
+    tooltip.startTogglingOnHover(node, () => true, {
+      toggleDelay: GUTTER_ERROR_PANEL_DELAY
+    });
   },
 
   /**
    * Removes all the gutter markers and line classes from the editor.
    */
   _cleanEditor: function (type) {
     this._getEditor(type).then(editor => {
       editor.removeAllMarkers("errors");
--- a/devtools/client/shared/widgets/Tooltip.js
+++ b/devtools/client/shared/widgets/Tooltip.js
@@ -338,16 +338,25 @@ Tooltip.prototype = {
 
     this.doc = null;
 
     this.panel.remove();
     this.panel = null;
   },
 
   /**
+   * Returns the outer container node (that includes the arrow etc.). Happens
+   * to be identical to this.panel here, can be different element in other
+   * Tooltip implementations.
+   */
+  get container() {
+    return this.panel;
+  },
+
+  /**
    * Set the content of this tooltip. Will first empty the tooltip and then
    * append the new content element.
    * Consider using one of the set<type>Content() functions instead.
    * @param {node} content
    *        A node that can be appended in the tooltip XUL element
    */
   set content(content) {
     if (this.content == content) {
--- a/devtools/client/shared/widgets/tooltip/TooltipToggle.js
+++ b/devtools/client/shared/widgets/tooltip/TooltipToggle.js
@@ -3,33 +3,36 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const {Task} = require("devtools/shared/task");
 
-const DEFAULT_SHOW_DELAY = 50;
+const DEFAULT_TOGGLE_DELAY = 50;
 
 /**
  * Tooltip helper designed to show/hide the tooltip when the mouse hovers over
  * particular nodes.
  *
  * This works by tracking mouse movements on a base container node (baseNode)
  * and showing the tooltip when the mouse stops moving. A callback can be
  * provided to the start() method to know whether or not the node being
  * hovered over should indeed receive the tooltip.
  */
 function TooltipToggle(tooltip) {
   this.tooltip = tooltip;
   this.win = tooltip.doc.defaultView;
 
   this._onMouseMove = this._onMouseMove.bind(this);
   this._onMouseLeave = this._onMouseLeave.bind(this);
+
+  this._onTooltipMouseOver = this._onTooltipMouseOver.bind(this);
+  this._onTooltipMouseOut = this._onTooltipMouseOut.bind(this);
 }
 
 module.exports.TooltipToggle = TooltipToggle;
 
 TooltipToggle.prototype = {
   /**
    * Start tracking mouse movements on the provided baseNode to show the
    * tooltip.
@@ -51,73 +54,91 @@ TooltipToggle.prototype = {
    *        A function that accepts a node argument and that checks if a tooltip
    *        should be displayed. Possible return values are:
    *        - false (or a falsy value) if the tooltip should not be displayed
    *        - true if the tooltip should be displayed
    *        - a DOM node to display the tooltip on the returned anchor
    *        The function can also return a promise that will resolve to one of
    *        the values listed above.
    *        If omitted, the tooltip will be shown everytime.
-   * @param {Number} showDelay
-   *        An optional delay that will be observed before showing the tooltip.
-   *        Defaults to DEFAULT_SHOW_DELAY.
+   * @param {Object} options
+            Set of optional arguments:
+   *        - {Number} toggleDelay
+   *          An optional delay (in ms) that will be observed before showing
+   *          and before hiding the tooltip. Defaults to DEFAULT_TOGGLE_DELAY.
+   *        - {Boolean} interactive
+   *          If enabled, the tooltip is not hidden when mouse leaves the
+   *          target element and enters the tooltip. Allows the tooltip
+   *          content to be interactive.
    */
-  start: function (baseNode, targetNodeCb, showDelay = DEFAULT_SHOW_DELAY) {
+  start: function (baseNode, targetNodeCb,
+                   {toggleDelay = DEFAULT_TOGGLE_DELAY, interactive = false} = {}) {
     this.stop();
 
     if (!baseNode) {
       // Calling tool is in the process of being destroyed.
       return;
     }
 
     this._baseNode = baseNode;
-    this._showDelay = showDelay;
     this._targetNodeCb = targetNodeCb || (() => true);
+    this._toggleDelay = toggleDelay;
+    this._interactive = interactive;
 
-    baseNode.addEventListener("mousemove", this._onMouseMove, false);
-    baseNode.addEventListener("mouseleave", this._onMouseLeave, false);
+    baseNode.addEventListener("mousemove", this._onMouseMove);
+    baseNode.addEventListener("mouseleave", this._onMouseLeave);
+
+    if (this._interactive) {
+      this.tooltip.container.addEventListener("mouseover", this._onTooltipMouseOver);
+      this.tooltip.container.addEventListener("mouseout", this._onTooltipMouseOut);
+    }
   },
 
   /**
    * If the start() function has been used previously, and you want to get rid
    * of this behavior, then call this function to remove the mouse movement
    * tracking
    */
   stop: function () {
     this.win.clearTimeout(this.toggleTimer);
 
     if (!this._baseNode) {
       return;
     }
 
-    this._baseNode.removeEventListener("mousemove", this._onMouseMove, false);
-    this._baseNode.removeEventListener("mouseleave", this._onMouseLeave, false);
+    this._baseNode.removeEventListener("mousemove", this._onMouseMove);
+    this._baseNode.removeEventListener("mouseleave", this._onMouseLeave);
+
+    if (this._interactive) {
+      this.tooltip.container.removeEventListener("mouseover", this._onTooltipMouseOver);
+      this.tooltip.container.removeEventListener("mouseout", this._onTooltipMouseOut);
+    }
 
     this._baseNode = null;
     this._targetNodeCb = null;
     this._lastHovered = null;
   },
 
   _onMouseMove: function (event) {
     if (event.target !== this._lastHovered) {
-      this.tooltip.hide();
       this._lastHovered = event.target;
 
       this.win.clearTimeout(this.toggleTimer);
       this.toggleTimer = this.win.setTimeout(() => {
+        this.tooltip.hide();
         this.isValidHoverTarget(event.target).then(target => {
           if (target === null) {
             return;
           }
           this.tooltip.show(target);
         }, reason => {
           console.error("isValidHoverTarget rejected with unexpected reason:");
           console.error(reason);
         });
-      }, this._showDelay);
+      }, this._toggleDelay);
     }
   },
 
   /**
    * Is the given target DOMNode a valid node for toggling the tooltip on hover.
    * This delegates to the user-defined _targetNodeCb callback.
    * @return {Promise} a promise that will resolve the anchor to use for the
    *         tooltip or null if no valid target was found.
@@ -127,17 +148,30 @@ TooltipToggle.prototype = {
     if (res) {
       return res.nodeName ? res : target;
     }
 
     return null;
   }),
 
   _onMouseLeave: function () {
+    this._lastHovered = null;
     this.win.clearTimeout(this.toggleTimer);
-    this._lastHovered = null;
-    this.tooltip.hide();
+    this.toggleTimer = this.win.setTimeout(() => {
+      this.tooltip.hide();
+    }, this._toggleDelay);
+  },
+
+  _onTooltipMouseOver() {
+    this.win.clearTimeout(this.toggleTimer);
+  },
+
+  _onTooltipMouseOut() {
+    this.win.clearTimeout(this.toggleTimer);
+    this.toggleTimer = this.win.setTimeout(() => {
+      this.tooltip.hide();
+    }, this._toggleDelay);
   },
 
   destroy: function () {
     this.stop();
   }
 };