Bug 1267401 - part3: Create arrow style for HTML tooltips;f=bgrins draft
authorJulian Descottes <jdescottes@mozilla.com>
Mon, 16 May 2016 17:53:19 +0200
changeset 369840 b7a8d5e682dbd7640b5b24ef8b93e3abc6a7b99e
parent 369839 a6f00eb92e596ba93fcb2be80fb4d514ba12d168
child 521630 d8cabf6c86bf0ec7cee5387aca7be0a8ed982968
push id18928
push userjdescottes@mozilla.com
push dateMon, 23 May 2016 20:51:03 +0000
bugs1267401
milestone49.0a1
Bug 1267401 - part3: Create arrow style for HTML tooltips;f=bgrins MozReview-Commit-ID: Bp2RylafolP
devtools/client/shared/test/browser.ini
devtools/client/shared/test/browser_html_tooltip_arrow-01.js
devtools/client/shared/test/browser_html_tooltip_arrow-02.js
devtools/client/shared/widgets/HTMLTooltip.js
devtools/client/shared/widgets/tooltip-frame.xhtml
devtools/client/themes/common.css
devtools/client/themes/variables.css
--- a/devtools/client/shared/test/browser.ini
+++ b/devtools/client/shared/test/browser.ini
@@ -112,16 +112,18 @@ skip-if = e10s # Bug 1221911, bug 122228
 skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
 [browser_graphs-16.js]
 skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
 [browser_html_tooltip-01.js]
 [browser_html_tooltip-02.js]
 [browser_html_tooltip-03.js]
 [browser_html_tooltip-04.js]
 [browser_html_tooltip-05.js]
+[browser_html_tooltip_arrow-01.js]
+[browser_html_tooltip_arrow-02.js]
 [browser_inplace-editor-01.js]
 [browser_inplace-editor-02.js]
 [browser_inplace-editor_maxwidth.js]
 [browser_key_shortcuts.js]
 [browser_layoutHelpers.js]
 skip-if = e10s # Layouthelpers test should not run in a content page.
 [browser_layoutHelpers-getBoxQuads.js]
 skip-if = e10s # Layouthelpers test should not run in a content page.
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_arrow-01.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+
+"use strict";
+
+/**
+ * Test the HTMLTooltip "arrow" type on small anchors. The arrow should remain
+ * aligned with the anchors as much as possible
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const getAnchor = function (position) {
+  return `<html:div class="anchor" style="width:10px;
+                                          height: 10px;
+                                          position: absolute;
+                                          background: red;
+                                          ${position}"></html:div>`;
+};
+
+const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+  <?xml-stylesheet href="chrome://global/skin/global.css"?>
+  <?xml-stylesheet href="chrome://devtools/skin/common.css"?>
+  <?xml-stylesheet href="chrome://devtools/skin/light-theme.css"?>
+
+  <window class="theme-light"
+          xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+          xmlns:html="http://www.w3.org/1999/xhtml"
+          title="Tooltip test">
+    <vbox flex="1" style="position: relative">
+      ${getAnchor("top: 0; left: 0;")}
+      ${getAnchor("top: 0; left: 25px;")}
+      ${getAnchor("top: 0; left: 50px;")}
+      ${getAnchor("top: 0; left: 75px;")}
+      ${getAnchor("bottom: 0; left: 0;")}
+      ${getAnchor("bottom: 0; left: 25px;")}
+      ${getAnchor("bottom: 0; left: 50px;")}
+      ${getAnchor("bottom: 0; left: 75px;")}
+      ${getAnchor("bottom: 0; right: 0;")}
+      ${getAnchor("bottom: 0; right: 25px;")}
+      ${getAnchor("bottom: 0; right: 50px;")}
+      ${getAnchor("bottom: 0; right: 75px;")}
+      ${getAnchor("top: 0; right: 0;")}
+      ${getAnchor("top: 0; right: 25px;")}
+      ${getAnchor("top: 0; right: 50px;")}
+      ${getAnchor("top: 0; right: 75px;")}
+    </vbox>
+  </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+add_task(function* () {
+  // Force the toolbox to be 200px high;
+  yield pushPref("devtools.toolbox.footer.height", 200);
+
+  yield addTab("about:blank");
+  let [,, doc] = yield createHost("bottom", TEST_URI);
+
+  info("Create HTML tooltip");
+  let tooltip = new HTMLTooltip({doc}, {type: "arrow"});
+  let div = doc.createElementNS(HTML_NS, "div");
+  div.style.height = "100%";
+  yield tooltip.setContent(div, 200, 35);
+
+  let {right: docRight} = doc.documentElement.getBoundingClientRect();
+
+  let elements = [...doc.querySelectorAll(".anchor")];
+  for (let el of elements) {
+    info("Display the tooltip on an anchor.");
+    yield showTooltip(tooltip, el);
+
+    let arrow = tooltip.arrow;
+    ok(arrow, "Tooltip has an arrow");
+
+    // Get the geometry of the anchor, the tooltip frame & arrow.
+    let arrowBounds = arrow.getBoxQuads({relativeTo: doc})[0].bounds;
+    let frameBounds = tooltip.frame.getBoxQuads({relativeTo: doc})[0].bounds;
+    let anchorBounds = el.getBoxQuads({relativeTo: doc})[0].bounds;
+
+    let intersects = arrowBounds.left <= anchorBounds.right &&
+                     arrowBounds.right >= anchorBounds.left;
+    let isBlockedByViewport = arrowBounds.left == 0 ||
+                              arrowBounds.right == docRight;
+    ok(intersects || isBlockedByViewport,
+      "Tooltip arrow is aligned with the anchor, or stuck on viewport's edge.");
+
+    let isInFrame = arrowBounds.left >= frameBounds.left &&
+                    arrowBounds.right <= frameBounds.right;
+    ok(isInFrame,
+      "The tooltip arrow remains inside the tooltip frame horizontally");
+
+    yield hideTooltip(tooltip);
+  }
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_arrow-02.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+
+"use strict";
+
+/**
+ * Test the HTMLTooltip "arrow" type on wide anchors. The arrow should remain
+ * aligned with the anchors as much as possible
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const getAnchor = function (position) {
+  return `<html:div class="anchor" style="height: 5px;
+                                          position: absolute;
+                                          background: red;
+                                          ${position}"></html:div>`;
+};
+
+const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+  <?xml-stylesheet href="chrome://global/skin/global.css"?>
+  <?xml-stylesheet href="chrome://devtools/skin/common.css"?>
+  <?xml-stylesheet href="chrome://devtools/skin/light-theme.css"?>
+
+  <window class="theme-light"
+          xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+          xmlns:html="http://www.w3.org/1999/xhtml"
+          title="Tooltip test">
+    <vbox flex="1" style="position: relative">
+      ${getAnchor("top:    0; left: 0; width: 50px;")}
+      ${getAnchor("top: 10px; left: 0; width: 100px;")}
+      ${getAnchor("top: 20px; left: 0; width: 150px;")}
+      ${getAnchor("top: 30px; left: 0; width: 200px;")}
+      ${getAnchor("top: 40px; left: 0; width: 250px;")}
+      ${getAnchor("top: 50px; left: 100px; width: 250px;")}
+      ${getAnchor("top: 100px; width:  50px; right: 0;")}
+      ${getAnchor("top: 110px; width: 100px; right: 0;")}
+      ${getAnchor("top: 120px; width: 150px; right: 0;")}
+      ${getAnchor("top: 130px; width: 200px; right: 0;")}
+      ${getAnchor("top: 140px; width: 250px; right: 0;")}
+    </vbox>
+  </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+add_task(function* () {
+  // Force the toolbox to be 200px high;
+  yield pushPref("devtools.toolbox.footer.height", 200);
+
+  yield addTab("about:blank");
+  let [,, doc] = yield createHost("bottom", TEST_URI);
+
+  info("Create HTML tooltip");
+  let tooltip = new HTMLTooltip({doc}, {type: "arrow"});
+  let div = doc.createElementNS(HTML_NS, "div");
+  div.style.height = "100%";
+  yield tooltip.setContent(div, 200, 35);
+
+  let {right: docRight} = doc.documentElement.getBoundingClientRect();
+
+  let elements = [...doc.querySelectorAll(".anchor")];
+  for (let el of elements) {
+    info("Display the tooltip on an anchor.");
+    yield showTooltip(tooltip, el);
+
+    let arrow = tooltip.arrow;
+    ok(arrow, "Tooltip has an arrow");
+
+    // Get the geometry of the anchor, the tooltip frame & arrow.
+    let arrowBounds = arrow.getBoxQuads({relativeTo: doc})[0].bounds;
+    let frameBounds = tooltip.frame.getBoxQuads({relativeTo: doc})[0].bounds;
+    let anchorBounds = el.getBoxQuads({relativeTo: doc})[0].bounds;
+
+    let intersects = arrowBounds.left <= anchorBounds.right &&
+                     arrowBounds.right >= anchorBounds.left;
+    let isBlockedByViewport = arrowBounds.left == 0 ||
+                              arrowBounds.right == docRight;
+    ok(intersects || isBlockedByViewport,
+      "Tooltip arrow is aligned with the anchor, or stuck on viewport's edge.");
+
+    let isInFrame = arrowBounds.left >= frameBounds.left &&
+                    arrowBounds.right <= frameBounds.right;
+    ok(isInFrame,
+      "The tooltip arrow remains inside the tooltip frame horizontally");
+    yield hideTooltip(tooltip);
+  }
+});
--- a/devtools/client/shared/widgets/HTMLTooltip.js
+++ b/devtools/client/shared/widgets/HTMLTooltip.js
@@ -8,24 +8,54 @@
 
 const EventEmitter = require("devtools/shared/event-emitter");
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 const XHTML_NS = "http://www.w3.org/1999/xhtml";
 
 const IFRAME_URL = "chrome://devtools/content/shared/widgets/tooltip-frame.xhtml";
 const IFRAME_CONTAINER_ID = "tooltip-iframe-container";
 
+const POSITION = {
+  TOP: "top",
+  BOTTOM: "bottom",
+};
+
+module.exports.POSITION = POSITION;
+
+const TYPE = {
+  NORMAL: "normal",
+  ARROW: "arrow",
+};
+
+module.exports.TYPE = TYPE;
+
+const ARROW_WIDTH = 32;
+
+// Default offset between the tooltip's left edge and the tooltip arrow.
+const ARROW_OFFSET = 20;
+
+const EXTRA_HEIGHT = {
+  "normal": 0,
+  // The arrow is 16px tall, but merges on 3px with the panel border
+  "arrow": 13,
+};
+
+const EXTRA_BORDER = {
+  "normal": 0,
+  "arrow": 3,
+};
+
 /**
  * The HTMLTooltip can display HTML content in a tooltip popup.
  *
  * @param {Toolbox} toolbox
  *        The devtools toolbox, needed to get the devtools main window.
  * @param {Object}
  *        - {String} type
- *          Display type of the tooltip. Possible values: "normal"
+ *          Display type of the tooltip. Possible values: "normal", "arrow"
  *        - {Boolean} autofocus
  *          Defaults to true. Should the tooltip be focused when opening it.
  *        - {Boolean} consumeOutsideClicks
  *          Defaults to true. The tooltip is closed when clicking outside.
  *          Should this event be stopped and consumed or not.
  */
 function HTMLTooltip(toolbox,
   {type = "normal", autofocus = true, consumeOutsideClicks = true} = {}) {
@@ -43,53 +73,84 @@ function HTMLTooltip(toolbox,
 
   this.container = this._createContainer();
 
   // Promise that will resolve when the container can be filled with content.
   this.containerReady = new Promise(resolve => {
     if (this._isXUL()) {
       // In XUL context, load a placeholder document in the iframe container.
       let onLoad = () => {
-        this.container.removeEventListener("load", onLoad, true);
+        this.frame.removeEventListener("load", onLoad, true);
         resolve();
       };
 
-      this.container.addEventListener("load", onLoad, true);
-      this.container.setAttribute("src", IFRAME_URL);
+      this.frame.addEventListener("load", onLoad, true);
+      this.frame.setAttribute("src", IFRAME_URL);
+      this.doc.querySelector("window").appendChild(this.container);
     } else {
       // In non-XUL context the container is ready to use as is.
+      this.doc.body.appendChild(this.container);
       resolve();
     }
   });
 }
 
 module.exports.HTMLTooltip = HTMLTooltip;
 
 HTMLTooltip.prototype = {
-  position: {
-    TOP: "top",
-    BOTTOM: "bottom",
+  /**
+   * The tooltip frame is the child of the tooltip container that will only
+   * contain the tooltip content (and not the arrow or any other tooltip styling
+   * element).
+   * In XUL contexts, this is an iframe. In non XUL contexts this is a div,
+   * which also happens to be the tooltip.panel property.
+   */
+  get frame() {
+    return this.container.querySelector(".tooltip-panel");
+  },
+
+  /**
+   * The tooltip panel is the parentNode of the tooltip content provided in
+   * setContent().
+   */
+  get panel() {
+    if (!this._isXUL()) {
+      return this.frame;
+    }
+    // In XUL context, the content is wrapped in an iframe.
+    let win = this.frame.contentWindow.wrappedJSObject;
+    return win.document.getElementById(IFRAME_CONTAINER_ID);
+  },
+
+  /**
+   * The arrow element. Might be null depending on the tooltip type.
+   */
+  get arrow() {
+    return this.container.querySelector(".tooltip-arrow");
   },
 
   /**
    * Set the tooltip content element. The preferred width/height should also be
    * specified here.
    *
    * @param {Element} content
    *        The tooltip content, should be a HTML element.
    * @param {Number} width
    *        Preferred width for the tooltip container
    * @param {Number} height
    *        Preferred height for the tooltip container
    * @return {Promise} a promise that will resolve when the content has been
    *         added in the tooltip container.
    */
   setContent: function (content, width, height) {
-    this.preferredWidth = width;
-    this.preferredHeight = height;
+    let themeHeight = EXTRA_HEIGHT[this.type] + 2 * EXTRA_BORDER[this.type];
+    let themeWidth = 2 * EXTRA_BORDER[this.type];
+
+    this.preferredWidth = width + themeWidth;
+    this.preferredHeight = height + themeHeight;
 
     return this.containerReady.then(() => {
       this.panel.innerHTML = "";
       this.panel.appendChild(content);
     });
   },
 
   /**
@@ -101,96 +162,90 @@ HTMLTooltip.prototype = {
    * @param {Object}
    *        - {String} position: optional, possible values: top|bottom
    *          If layout permits, the tooltip will be displayed on top/bottom
    *          of the anchor. If ommitted, the tooltip will be displayed where
    *          more space is available.
    */
   show: function (anchor, {position} = {}) {
     this.containerReady.then(() => {
-      let {top, left, width, height} = this._findBestPosition(anchor, position);
+      let computedPosition = this._findBestPosition(anchor, position);
+
+      let isTop = computedPosition.position === POSITION.TOP;
+      this.container.classList.toggle("tooltip-top", isTop);
+      this.container.classList.toggle("tooltip-bottom", !isTop);
 
-      if (this._isXUL()) {
-        this.container.setAttribute("width", width);
-        this.container.setAttribute("height", height);
-      } else {
-        this.container.style.width = width + "px";
-        this.container.style.height = height + "px";
+      this.container.style.width = computedPosition.width + "px";
+      this.container.style.height = computedPosition.height + "px";
+      this.container.style.top = computedPosition.top + "px";
+      this.container.style.left = computedPosition.left + "px";
+
+      if (this.type === TYPE.ARROW) {
+        this.arrow.style.left = computedPosition.arrowLeft + "px";
       }
 
-      this.container.style.top = top + "px";
-      this.container.style.left = left + "px";
-      this.container.style.display = "block";
-
-      if (this.autofocus) {
-        this.container.focus();
-      }
+      this.container.classList.add("tooltip-visible");
 
       this.attachEventsTimer = this.doc.defaultView.setTimeout(() => {
+        if (this.autofocus) {
+          this.frame.focus();
+        }
         this.topWindow.addEventListener("click", this._onClick, true);
         this.emit("shown");
       }, 0);
     });
   },
 
   /**
    * Hide the current tooltip. The event "hidden" will be fired when the tooltip
    * is hidden.
    */
   hide: function () {
     this.doc.defaultView.clearTimeout(this.attachEventsTimer);
 
     if (this.isVisible()) {
       this.topWindow.removeEventListener("click", this._onClick, true);
-      this.container.style.display = "none";
+      this.container.classList.remove("tooltip-visible");
       this.emit("hidden");
     }
   },
 
-  get panel() {
-    if (this._isXUL()) {
-      // In XUL context, we are wrapping the HTML content in an iframe.
-      let win = this.container.contentWindow.wrappedJSObject;
-      return win.document.getElementById(IFRAME_CONTAINER_ID);
-    }
-    return this.container;
-  },
-
   /**
    * Check if the tooltip is currently displayed.
    * @return {Boolean} true if the tooltip is visible
    */
   isVisible: function () {
-    let win = this.doc.defaultView;
-    return win.getComputedStyle(this.container).display != "none";
+    return this.container.classList.contains("tooltip-visible");
   },
 
   /**
    * Destroy the tooltip instance. Hide the tooltip if displayed, remove the
    * tooltip container from the document.
    */
   destroy: function () {
     this.hide();
     this.container.remove();
   },
 
   _createContainer: function () {
-    let container;
+    let container = this.doc.createElementNS(XHTML_NS, "div");
+    container.setAttribute("type", this.type);
+    container.classList.add("tooltip-container");
+
+    let html;
     if (this._isXUL()) {
-      container = this.doc.createElementNS(XHTML_NS, "iframe");
-      container.classList.add("devtools-tooltip-iframe");
-      this.doc.querySelector("window").appendChild(container);
+      html = '<iframe class="devtools-tooltip-iframe tooltip-panel"></iframe>';
     } else {
-      container = this.doc.createElementNS(XHTML_NS, "div");
-      this.doc.body.appendChild(container);
+      html = '<div class="tooltip-panel theme-body"></div>';
     }
 
-    container.classList.add("theme-body");
-    container.classList.add("devtools-htmltooltip-container");
-
+    if (this.type === TYPE.ARROW) {
+      html += '<div class="tooltip-arrow"></div>';
+    }
+    container.innerHTML = html;
     return container;
   },
 
   _onClick: function (e) {
     if (this._isInTooltipContainer(e.target)) {
       return;
     }
 
@@ -216,76 +271,104 @@ HTMLTooltip.prototype = {
       win = win.parent;
       if (win === contentWindow) {
         return true;
       }
     }
     return false;
   },
 
+  /**
+   * Calculates the best possible position to display the tooltip near the
+   * provided anchor. An optional position can be provided, but will be
+   * respected only if it doesn't force the tooltip to be resized.
+   *
+   * If the tooltip has to be resized, the position will be wherever the most
+   * space is available.
+   *
+   */
   _findBestPosition: function (anchor, position) {
-    let top, left;
-    let {TOP, BOTTOM} = this.position;
+    let {TOP, BOTTOM} = POSITION;
 
-    let {left: anchorLeft, top: anchorTop, height: anchorHeight}
-      = this._getRelativeRect(anchor, this.doc);
+    // Get anchor geometry
+    let {
+      left: anchorLeft, top: anchorTop,
+      height: anchorHeight, width: anchorWidth
+    } = this._getRelativeRect(anchor, this.doc);
 
+    // Get document geometry
     let {bottom: docBottom, right: docRight} =
       this.doc.documentElement.getBoundingClientRect();
 
-    let height = this.preferredHeight;
-    // Check if the popup can fit above the anchor.
+    // Calculate available space for the tooltip.
     let availableTop = anchorTop;
-    let fitsAbove = availableTop >= height;
-    // Check if the popup can fit below the anchor.
-    let availableBelow = docBottom - (anchorTop + anchorHeight);
-    let fitsBelow = availableBelow >= height;
+    let availableBottom = docBottom - (anchorTop + anchorHeight);
 
-    let isPositionSuitable = (fitsAbove && position === TOP)
-      || (fitsBelow && position === BOTTOM);
-    if (!isPositionSuitable) {
-      // If the preferred position does not fit the preferred height,
-      // pick the position offering the most height.
-      position = availableTop > availableBelow ? TOP : BOTTOM;
+    // Find POSITION
+    let keepPosition = false;
+    if (position === TOP) {
+      keepPosition = availableTop >= this.preferredHeight;
+    } else if (position === BOTTOM) {
+      keepPosition = availableBottom >= this.preferredHeight;
+    }
+    if (!keepPosition) {
+      position = availableTop > availableBottom ? TOP : BOTTOM;
     }
 
-    // Calculate height, capped by the maximum height available.
-    height = Math.min(height, Math.max(availableTop, availableBelow));
-    top = position === TOP ? anchorTop - height : anchorTop + anchorHeight;
+    // Calculate HEIGHT.
+    let availableHeight = position === TOP ? availableTop : availableBottom;
+    let height = Math.min(this.preferredHeight, availableHeight);
+    height = Math.floor(height);
 
+    // Calculate TOP.
+    let top = position === TOP ? anchorTop - height : anchorTop + anchorHeight;
+
+    // Calculate WIDTH.
     let availableWidth = docRight;
     let width = Math.min(this.preferredWidth, availableWidth);
 
-    // By default, align the tooltip's left edge with the anchor left edge.
-    if (anchorLeft + width <= docRight) {
-      left = anchorLeft;
-    } else {
-      // If the tooltip cannot fit, shift to the left just enough to fit.
-      left = docRight - width;
+    // Calculate LEFT.
+    // By default the tooltip is aligned with the anchor left edge. Unless this
+    // makes it overflow the viewport, in which case is shifts to the left.
+    let left = Math.min(anchorLeft, docRight - width);
+
+    // Calculate ARROW LEFT (tooltip's LEFT might be updated)
+    let arrowLeft;
+    // Arrow style tooltips may need to be shifted to the left
+    if (this.type === TYPE.ARROW) {
+      let arrowCenter = left + ARROW_OFFSET + ARROW_WIDTH / 2;
+      let anchorCenter = anchorLeft + anchorWidth / 2;
+      // If the anchor is too narrow, align the arrow and the anchor center.
+      if (arrowCenter > anchorCenter) {
+        left = Math.max(0, left - (arrowCenter - anchorCenter));
+      }
+      // Arrow's feft offset relative to the anchor.
+      arrowLeft = Math.min(ARROW_OFFSET, (anchorWidth - ARROW_WIDTH) / 2) | 0;
+      // Translate the coordinate to tooltip container
+      arrowLeft += anchorLeft - left;
+      // Make sure the arrow remains in the tooltip container.
+      arrowLeft = Math.min(arrowLeft, width - ARROW_WIDTH);
+      arrowLeft = Math.max(arrowLeft, 0);
     }
 
-    return {top, left, width, height};
+    return {top, left, width, height, position, arrowLeft};
   },
 
   /**
    * Get the bounding client rectangle for a given node, relative to a custom
    * reference element (instead of the default for getBoundingClientRect which
    * is always the element's ownerDocument).
    */
   _getRelativeRect: function (node, relativeTo) {
     // Width and Height can be taken from the rect.
     let {width, height} = node.getBoundingClientRect();
 
-    // Find the smallest top/left coordinates from all quads.
-    let top = Infinity, left = Infinity;
-    let quads = node.getBoxQuads({relativeTo: relativeTo});
-    for (let quad of quads) {
-      top = Math.min(top, quad.bounds.top);
-      left = Math.min(left, quad.bounds.left);
-    }
+    let quads = node.getBoxQuads({relativeTo});
+    let top = quads[0].bounds.top;
+    let left = quads[0].bounds.left;
 
     // Compute right and bottom coordinates using the rest of the data.
     let right = left + width;
     let bottom = top + height;
 
     return {top, right, bottom, left, width, height};
   },
 
--- a/devtools/client/shared/widgets/tooltip-frame.xhtml
+++ b/devtools/client/shared/widgets/tooltip-frame.xhtml
@@ -10,15 +10,16 @@
   <script type="application/javascript;version=1.8" src="chrome://devtools/content/shared/theme-switching.js"/>
   <style>
     html, body, #tooltip-iframe-container {
       margin: 0;
       padding: 0;
       width: 100%;
       height: 100%;
       overflow: hidden;
+      color: var(--theme-body-color);
     }
   </style>
 </head>
 <body role="application" class="theme-body">
   <div id="tooltip-iframe-container"></div>
 </body>
 </html>
--- a/devtools/client/themes/common.css
+++ b/devtools/client/themes/common.css
@@ -241,20 +241,98 @@
   background-position: 0 0, 10px 10px;
 }
 
 .devtools-tooltip-iframe {
   border: none;
   background: transparent;
 }
 
-.devtools-htmltooltip-container {
+.tooltip-container {
   display: none;
   position: fixed;
   z-index: 9999;
+  display: none;
+  background: transparent;
+}
+
+.tooltip-panel{
+  background-color: var(--theme-tooltip-background);
+}
+
+.tooltip-visible {
+  display: block;
+}
+
+.tooltip-container[type="normal"] > .tooltip-panel {
+  height: 100%;
+  width: 100%;
+}
+
+/* Tooltip : arrow style */
+
+.tooltip-container[type="arrow"] {
+  filter: drop-shadow(0 3px 4px var(--theme-tooltip-shadow));
+}
+
+.tooltip-container[type="arrow"] > .tooltip-panel {
+  position: absolute;
+  box-sizing: border-box;
+  height: calc(100% - 13px);
+  width: 100%;
+
+  border: 3px solid var(--theme-tooltip-border);
+  border-radius: 5px;
+}
+
+.tooltip-top[type="arrow"] .tooltip-panel {
+  top: 0;
+}
+
+.tooltip-bottom[type="arrow"] .tooltip-panel {
+  bottom: 0;
+}
+
+.tooltip-arrow {
+  position: absolute;
+  height: 16px;
+  width: 32px;
+  overflow: hidden;
+}
+
+.tooltip-top .tooltip-arrow {
+  bottom: 0;
+}
+
+.tooltip-bottom .tooltip-arrow {
+  top: 0;
+}
+
+.tooltip-arrow:before {
+  content: "";
+  position: absolute;
+  width: 21px;
+  height: 21px;
+  margin-left: 4px;
+  background: linear-gradient(-45deg,
+    var(--theme-tooltip-background) 50%, transparent 50%);
+  border-color: var(--theme-tooltip-border);
+  border-style: solid;
+  border-width: 0px 3px 3px 0px;
+  border-radius: 3px;
+}
+
+.tooltip-bottom .tooltip-arrow:before {
+  margin-top: 4px;
+  transform: rotate(225deg);
+}
+
+.tooltip-top .tooltip-arrow:before {
+  margin-top: -12px;
+  transform: rotate(45deg);
 }
 
 /* links to source code, like displaying `myfile.js:45` */
 
 .devtools-source-link {
   font-family: var(--monospace-font-family);
   color: var(--theme-highlight-blue);
   cursor: pointer;
--- a/devtools/client/themes/variables.css
+++ b/devtools/client/themes/variables.css
@@ -58,16 +58,21 @@
   --theme-graphs-red: #e57180;
   --theme-graphs-grey: #cccccc;
   --theme-graphs-full-red: #f00;
   --theme-graphs-full-blue: #00f;
 
   /* Images */
   --theme-pane-collapse-image: url(chrome://devtools/skin/images/pane-collapse.svg);
   --theme-pane-expand-image: url(chrome://devtools/skin/images/pane-expand.svg);
+
+  /* Tooltips */
+  --theme-tooltip-border: #d9e1e8;
+  --theme-tooltip-background: rgba(255, 255, 255, .9);
+  --theme-tooltip-shadow: rgba(155, 155, 155, 0.26);
 }
 
 :root.theme-dark {
   --theme-body-background: #393f4c;
   --theme-sidebar-background: #393f4c;
   --theme-contrast-background: #ffb35b;
 
   --theme-tab-toolbar-background: #272b35;
@@ -109,16 +114,21 @@
   --theme-graphs-red: #eb5368;
   --theme-graphs-grey: #757873;
   --theme-graphs-full-red: #f00;
   --theme-graphs-full-blue: #00f;
 
   /* Images */
   --theme-pane-collapse-image: url(chrome://devtools/skin/images/pane-collapse.svg);
   --theme-pane-expand-image: url(chrome://devtools/skin/images/pane-expand.svg);
+
+  /* Tooltips */
+  --theme-tooltip-border: #434850;
+  --theme-tooltip-background: rgba(19, 28, 38, .9);
+  --theme-tooltip-shadow: rgba(25, 25, 25, 0.76);
 }
 
 :root.theme-firebug {
   --theme-body-background: #fcfcfc;
   --theme-sidebar-background: #fcfcfc;
   --theme-contrast-background: #e6b064;
 
   --theme-tab-toolbar-background: #ebeced;