Bug 1266456 - part6: HTMLTooltip show() now accepts x,y offsets;r=bgrins draft
authorJulian Descottes <jdescottes@mozilla.com>
Mon, 20 Jun 2016 21:07:08 +0200
changeset 381153 e694a6ba60d0c7f068a84a27b3a5db6882f054bd
parent 381152 3ddfdc6ca484e051c3e2291c62e0af2367d8df3a
child 381154 fc6e4cbe7247bd144ec978c14a4d435cc6aedb78
push id21409
push userjdescottes@mozilla.com
push dateFri, 24 Jun 2016 14:54:55 +0000
reviewersbgrins
bugs1266456
milestone50.0a1
Bug 1266456 - part6: HTMLTooltip show() now accepts x,y offsets;r=bgrins The autocomplete popup is displayed relatively to an anchor, but will be shifted on the x-axis in order to be aligned with the word being completed. To support this, the HTMLTooltip show() method now accepts x and y offsets. MozReview-Commit-ID: 1cR3XFPdcVy
devtools/client/shared/test/browser.ini
devtools/client/shared/test/browser_html_tooltip-04.js
devtools/client/shared/test/browser_html_tooltip-05.js
devtools/client/shared/test/browser_html_tooltip_offset.js
devtools/client/shared/test/helper_html_tooltip.js
devtools/client/shared/widgets/HTMLTooltip.js
--- a/devtools/client/shared/test/browser.ini
+++ b/devtools/client/shared/test/browser.ini
@@ -116,16 +116,17 @@ skip-if = e10s # Bug 1221911, bug 122228
 [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_html_tooltip_consecutive-show.js]
+[browser_html_tooltip_offset.js]
 [browser_html_tooltip_variable-height.js]
 [browser_html_tooltip_width-auto.js]
 [browser_inplace-editor-01.js]
 [browser_inplace-editor-02.js]
 [browser_inplace-editor_autocomplete_01.js]
 [browser_inplace-editor_autocomplete_02.js]
 [browser_inplace-editor_autocomplete_offset.js]
 [browser_inplace-editor_maxwidth.js]
--- a/devtools/client/shared/test/browser_html_tooltip-04.js
+++ b/devtools/client/shared/test/browser_html_tooltip-04.js
@@ -54,55 +54,55 @@ add_task(function* () {
   // box1: Can only fit below box1
   info("Display the tooltip on box1.");
   yield showTooltip(tooltip, box1);
   let expectedTooltipGeometry = {position: "bottom", height, width};
   checkTooltipGeometry(tooltip, box1, expectedTooltipGeometry);
   yield hideTooltip(tooltip);
 
   info("Try to display the tooltip on top of box1.");
-  yield showTooltip(tooltip, box1, "top");
+  yield showTooltip(tooltip, box1, {position: "top"});
   expectedTooltipGeometry = {position: "bottom", height, width};
   checkTooltipGeometry(tooltip, box1, expectedTooltipGeometry);
   yield hideTooltip(tooltip);
 
   // box2: Can fit above or below, will default to bottom, more height
   // available.
   info("Try to display the tooltip on box2.");
   yield showTooltip(tooltip, box2);
   expectedTooltipGeometry = {position: "bottom", height, width};
   checkTooltipGeometry(tooltip, box2, expectedTooltipGeometry);
   yield hideTooltip(tooltip);
 
   info("Try to display the tooltip on top of box2.");
-  yield showTooltip(tooltip, box2, "top");
+  yield showTooltip(tooltip, box2, {position: "top"});
   expectedTooltipGeometry = {position: "top", height, width};
   checkTooltipGeometry(tooltip, box2, expectedTooltipGeometry);
   yield hideTooltip(tooltip);
 
   // box3: Can fit above or below, will default to top, more height available.
   info("Try to display the tooltip on box3.");
   yield showTooltip(tooltip, box3);
   expectedTooltipGeometry = {position: "top", height, width};
   checkTooltipGeometry(tooltip, box3, expectedTooltipGeometry);
   yield hideTooltip(tooltip);
 
   info("Try to display the tooltip on bottom of box3.");
-  yield showTooltip(tooltip, box3, "bottom");
+  yield showTooltip(tooltip, box3, {position: "bottom"});
   expectedTooltipGeometry = {position: "bottom", height, width};
   checkTooltipGeometry(tooltip, box3, expectedTooltipGeometry);
   yield hideTooltip(tooltip);
 
   // box4: Can only fit above box4
   info("Display the tooltip on box4.");
   yield showTooltip(tooltip, box4);
   expectedTooltipGeometry = {position: "top", height, width};
   checkTooltipGeometry(tooltip, box4, expectedTooltipGeometry);
   yield hideTooltip(tooltip);
 
   info("Try to display the tooltip on bottom of box4.");
-  yield showTooltip(tooltip, box4, "bottom");
+  yield showTooltip(tooltip, box4, {position: "bottom"});
   expectedTooltipGeometry = {position: "top", height, width};
   checkTooltipGeometry(tooltip, box4, expectedTooltipGeometry);
   yield hideTooltip(tooltip);
 
   is(tooltip.isVisible(), false, "Tooltip is not visible");
 });
--- a/devtools/client/shared/test/browser_html_tooltip-05.js
+++ b/devtools/client/shared/test/browser_html_tooltip-05.js
@@ -66,43 +66,43 @@ add_task(function* () {
   // height of 100px.
   info("Try to display the tooltip on box2.");
   yield showTooltip(tooltip, box2);
   expectedTooltipGeometry = {position: "bottom", height: 100, width};
   checkTooltipGeometry(tooltip, box2, expectedTooltipGeometry);
   yield hideTooltip(tooltip);
 
   info("Try to display the tooltip on top of box2.");
-  yield showTooltip(tooltip, box2, "top");
+  yield showTooltip(tooltip, box2, {position: "top"});
   expectedTooltipGeometry = {position: "bottom", height: 100, width};
   checkTooltipGeometry(tooltip, box2, expectedTooltipGeometry);
   yield hideTooltip(tooltip);
 
   // box3: Can not fit above or below box3, default to top with a reduced height
   // of 100px.
   info("Try to display the tooltip on box3.");
   yield showTooltip(tooltip, box3);
   expectedTooltipGeometry = {position: "top", height: 100, width};
   checkTooltipGeometry(tooltip, box3, expectedTooltipGeometry);
   yield hideTooltip(tooltip);
 
   info("Try to display the tooltip on bottom of box3.");
-  yield showTooltip(tooltip, box3, "bottom");
+  yield showTooltip(tooltip, box3, {position: "bottom"});
   expectedTooltipGeometry = {position: "top", height: 100, width};
   checkTooltipGeometry(tooltip, box3, expectedTooltipGeometry);
   yield hideTooltip(tooltip);
 
   // box4: Can not fit above or below box4, default to top with a reduced height
   // of 150px.
   info("Display the tooltip on box4.");
   yield showTooltip(tooltip, box4);
   expectedTooltipGeometry = {position: "top", height: 150, width};
   checkTooltipGeometry(tooltip, box4, expectedTooltipGeometry);
   yield hideTooltip(tooltip);
 
   info("Try to display the tooltip on bottom of box4.");
-  yield showTooltip(tooltip, box4, "bottom");
+  yield showTooltip(tooltip, box4, {position: "bottom"});
   expectedTooltipGeometry = {position: "top", height: 150, width};
   checkTooltipGeometry(tooltip, box4, expectedTooltipGeometry);
   yield hideTooltip(tooltip);
 
   is(tooltip.isVisible(), false, "Tooltip is not visible");
 });
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_offset.js
@@ -0,0 +1,100 @@
+/* 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 can be displayed with vertical and horizontal offsets.
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+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/tooltips.css"?>
+  <window
+    xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+    htmlns="http://www.w3.org/1999/xhtml"
+    title="Tooltip test">
+    <vbox flex="1">
+      <hbox id="box1" flex="1">test1</hbox>
+      <hbox id="box2" flex="1">test2</hbox>
+      <hbox id="box3" flex="1">test3</hbox>
+      <hbox id="box4" flex="1">test4</hbox>
+    </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("Test a tooltip is not closed when clicking inside itself");
+
+  let box1 = doc.getElementById("box1");
+  let box2 = doc.getElementById("box2");
+  let box3 = doc.getElementById("box3");
+  let box4 = doc.getElementById("box4");
+
+  let tooltip = new HTMLTooltip({doc}, {});
+
+  let div = doc.createElementNS(HTML_NS, "div");
+  div.style.height = "100px";
+  div.style.boxSizing = "border-box";
+  div.textContent = "tooltip";
+  tooltip.setContent(div, {width: 50, height: 100});
+
+  info("Display the tooltip on box1.");
+  yield showTooltip(tooltip, box1, {x: 5, y: 10});
+
+  let panelRect = tooltip.container.getBoundingClientRect();
+  let anchorRect = box1.getBoundingClientRect();
+
+  // Tooltip will be displayed below box1
+  is(panelRect.top, anchorRect.bottom + 10, "Tooltip top has 10px offset");
+  is(panelRect.left, anchorRect.left + 5, "Tooltip left has 5px offset");
+  is(panelRect.height, 100, "Tooltip height is at 100px as expected");
+
+  info("Display the tooltip on box2.");
+  yield showTooltip(tooltip, box2, {x: 5, y: 10});
+
+  panelRect = tooltip.container.getBoundingClientRect();
+  anchorRect = box2.getBoundingClientRect();
+
+  // Tooltip will be displayed below box2, but can't be fully displayed because of the
+  // offset
+  is(panelRect.top, anchorRect.bottom + 10, "Tooltip top has 10px offset");
+  is(panelRect.left, anchorRect.left + 5, "Tooltip left has 5px offset");
+  is(panelRect.height, 90, "Tooltip height is only 90px");
+
+  info("Display the tooltip on box3.");
+  yield showTooltip(tooltip, box3, {x: 5, y: 10});
+
+  panelRect = tooltip.container.getBoundingClientRect();
+  anchorRect = box3.getBoundingClientRect();
+
+  // Tooltip will be displayed above box3, but can't be fully displayed because of the
+  // offset
+  is(panelRect.bottom, anchorRect.top - 10, "Tooltip bottom is 10px above anchor");
+  is(panelRect.left, anchorRect.left + 5, "Tooltip left has 5px offset");
+  is(panelRect.height, 90, "Tooltip height is only 90px");
+
+  info("Display the tooltip on box4.");
+  yield showTooltip(tooltip, box4, {x: 5, y: 10});
+
+  panelRect = tooltip.container.getBoundingClientRect();
+  anchorRect = box4.getBoundingClientRect();
+
+  // Tooltip will be displayed above box4
+  is(panelRect.bottom, anchorRect.top - 10, "Tooltip bottom is 10px above anchor");
+  is(panelRect.left, anchorRect.left + 5, "Tooltip left has 5px offset");
+  is(panelRect.height, 100, "Tooltip height is at 100px as expected");
+
+  yield hideTooltip(tooltip);
+
+  tooltip.destroy();
+});
--- a/devtools/client/shared/test/helper_html_tooltip.js
+++ b/devtools/client/shared/test/helper_html_tooltip.js
@@ -11,24 +11,23 @@
 /**
  * Display an existing HTMLTooltip on an anchor. After the tooltip "shown"
  * event has been fired a reflow will be triggered.
  *
  * @param {HTMLTooltip} tooltip
  *        The tooltip instance to display
  * @param {Node} anchor
  *        The anchor that should be used to display the tooltip
- * @param {String} position
- *        The preferred display position ("top", "bottom")
+ * @param {Object} see HTMLTooltip:show documentation
  * @return {Promise} promise that resolves when "shown" has been fired, reflow
  *         and repaint done.
  */
-function* showTooltip(tooltip, anchor, position) {
+function* showTooltip(tooltip, anchor, {position, x, y} = {}) {
   let onShown = tooltip.once("shown");
-  tooltip.show(anchor, {position});
+  tooltip.show(anchor, {position, x, y});
   yield onShown;
   return waitForReflow(tooltip);
 }
 
 /**
  * Hide an existing HTMLTooltip. After the tooltip "hidden" event has been fired
  * a reflow will be triggered.
  *
--- a/devtools/client/shared/widgets/HTMLTooltip.js
+++ b/devtools/client/shared/widgets/HTMLTooltip.js
@@ -46,86 +46,86 @@ const EXTRA_BORDER = {
  * Calculate the vertical position & offsets to use for the tooltip. Will attempt to
  * respect the provided height and position preferences, unless the available height
  * prevents this.
  *
  * @param {DOMRect} anchorRect
  *        Bounding rectangle for the anchor, relative to the tooltip document.
  * @param {DOMRect} docRect
  *        Bounding rectange for the tooltip document owner.
- * @param {Number} preferredHeight
+ * @param {Number} height
  *        Preferred height for the tooltip.
  * @param {String} pos
  *        Preferred position for the tooltip. Possible values: "top" or "bottom".
  * @return {Object}
  *         - {Number} top: the top offset for the tooltip.
  *         - {Number} height: the height to use for the tooltip container.
  *         - {String} computedPosition: Can differ from the preferred position depending
  *           on the available height). "top" or "bottom"
  */
-const calculateVerticalPosition = function (anchorRect, docRect, preferredHeight, pos) {
+const calculateVerticalPosition = function (anchorRect, docRect, height, pos, offset) {
   let {TOP, BOTTOM} = POSITION;
 
   let {top: anchorTop, height: anchorHeight} = anchorRect;
   let {bottom: docBottom} = docRect;
 
   // Calculate available space for the tooltip.
   let availableTop = anchorTop;
   let availableBottom = docBottom - (anchorTop + anchorHeight);
 
   // Find POSITION
   let keepPosition = false;
   if (pos === TOP) {
-    keepPosition = availableTop >= preferredHeight;
+    keepPosition = availableTop >= height + offset;
   } else if (pos === BOTTOM) {
-    keepPosition = availableBottom >= preferredHeight;
+    keepPosition = availableBottom >= height + offset;
   }
   if (!keepPosition) {
     pos = availableTop > availableBottom ? TOP : BOTTOM;
   }
 
   // Calculate HEIGHT.
   let availableHeight = pos === TOP ? availableTop : availableBottom;
-  let height = Math.min(preferredHeight, availableHeight);
+  height = Math.min(height, availableHeight - offset);
   height = Math.floor(height);
 
   // Calculate TOP.
-  let top = pos === TOP ? anchorTop - height : anchorTop + anchorHeight;
+  let top = pos === TOP ? anchorTop - height - offset : anchorTop + anchorHeight + offset;
 
   return {top, height, computedPosition: pos};
 };
 
 /**
  * Calculate the vertical position & offsets to use for the tooltip. Will attempt to
  * respect the provided height and position preferences, unless the available height
  * prevents this.
  *
  * @param {DOMRect} anchorRect
  *        Bounding rectangle for the anchor, relative to the tooltip document.
  * @param {DOMRect} docRect
  *        Bounding rectange for the tooltip document owner.
- * @param {Number} preferredWidth
+ * @param {Number} width
  *        Preferred width for the tooltip.
  * @return {Object}
  *         - {Number} left: the left offset for the tooltip.
  *         - {Number} width: the width to use for the tooltip container.
  *         - {Number} arrowLeft: the left offset to use for the arrow element.
  */
-const calculateHorizontalPosition = function (anchorRect, docRect, preferredWidth, type) {
+const calculateHorizontalPosition = function (anchorRect, docRect, width, type, offset) {
   let {left: anchorLeft, width: anchorWidth} = anchorRect;
   let {right: docRight} = docRect;
 
   // Calculate WIDTH.
   let availableWidth = docRight;
-  let width = Math.min(preferredWidth, availableWidth);
+  width = Math.min(width, availableWidth);
 
   // 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);
+  let left = Math.min(anchorLeft + offset, 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 (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.
@@ -252,42 +252,44 @@ HTMLTooltip.prototype = {
    *
    * @param {Element} anchor
    *        The reference element with which the tooltip should be aligned
    * @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.
+   *        - {Number} x: optional, horizontal offset between the anchor and the tooltip
+   *        - {Number} y: optional, vertical offset between the anchor and the tooltip
    */
-  show: function (anchor, {position} = {}) {
+  show: function (anchor, {position, x = 0, y = 0} = {}) {
     // Get anchor geometry
     let anchorRect = getRelativeRect(anchor, this.doc);
     // Get document geometry
     let docRect = this.doc.documentElement.getBoundingClientRect();
 
     let themeHeight = EXTRA_HEIGHT[this.type] + 2 * EXTRA_BORDER[this.type];
     let preferredHeight = this.preferredHeight + themeHeight;
 
     let {top, height, computedPosition} =
-      calculateVerticalPosition(anchorRect, docRect, preferredHeight, position);
+      calculateVerticalPosition(anchorRect, docRect, preferredHeight, position, y);
 
     // Apply height and top information before measuring the content width (if "auto").
     let isTop = computedPosition === POSITION.TOP;
     this.container.classList.toggle("tooltip-top", isTop);
     this.container.classList.toggle("tooltip-bottom", !isTop);
     this.container.style.height = height + "px";
     this.container.style.top = top + "px";
 
     let themeWidth = 2 * EXTRA_BORDER[this.type];
     let preferredWidth = this.preferredWidth === "auto" ?
       this._measureContainerWidth() : this.preferredWidth + themeWidth;
 
     let {left, width, arrowLeft} =
-      calculateHorizontalPosition(anchorRect, docRect, preferredWidth, this.type);
+      calculateHorizontalPosition(anchorRect, docRect, preferredWidth, this.type, x);
 
     this.container.style.width = width + "px";
     this.container.style.left = left + "px";
 
     if (this.type === TYPE.ARROW) {
       this.arrow.style.left = arrowLeft + "px";
     }
 
@@ -301,16 +303,18 @@ HTMLTooltip.prototype = {
       this._maybeFocusTooltip();
       this.topWindow.addEventListener("click", this._onClick, true);
       this.emit("shown");
     }, 0);
   },
 
   _measureContainerWidth: function () {
     this.container.classList.add("tooltip-hidden");
+    this.container.style.left = "0px";
+    this.container.style.width = "auto";
     let width = this.container.getBoundingClientRect().width;
     this.container.classList.remove("tooltip-hidden");
     return width;
   },
 
   /**
    * Hide the current tooltip. The event "hidden" will be fired when the tooltip
    * is hidden.