Bug 1461522 - Add a mechanism to allow updating an HTMLTooltip's size and position; r?jdescottes draft
authorBrian Birtles <birtles@gmail.com>
Thu, 28 Jun 2018 15:13:05 +0900
changeset 813903 10cc3bee686a361edbb3f655ac6216631631f6d5
parent 813902 2d76ea92eb15af36da25884682778c6667fe9b7b
child 813904 0f4176c8ad2339903ac4c1f766ae447449e8e6b7
push id115042
push userbbirtles@mozilla.com
push dateWed, 04 Jul 2018 04:36:27 +0000
reviewersjdescottes
bugs1461522
milestone63.0a1
Bug 1461522 - Add a mechanism to allow updating an HTMLTooltip's size and position; r?jdescottes MozReview-Commit-ID: 4SDxlTTFp8E
devtools/client/shared/test/browser.ini
devtools/client/shared/test/browser_html_tooltip_resize.js
devtools/client/shared/widgets/tooltip/HTMLTooltip.js
--- a/devtools/client/shared/test/browser.ini
+++ b/devtools/client/shared/test/browser.ini
@@ -140,16 +140,17 @@ skip-if = e10s # Bug 1221911, bug 122228
 [browser_html_tooltip_arrow-01.js]
 [browser_html_tooltip_arrow-02.js]
 [browser_html_tooltip_consecutive-show.js]
 [browser_html_tooltip_doorhanger-01.js]
 [browser_html_tooltip_doorhanger-02.js]
 [browser_html_tooltip_height-auto.js]
 [browser_html_tooltip_hover.js]
 [browser_html_tooltip_offset.js]
+[browser_html_tooltip_resize.js]
 [browser_html_tooltip_rtl.js]
 [browser_html_tooltip_variable-height.js]
 [browser_html_tooltip_width-auto.js]
 [browser_html_tooltip_xul-wrapper.js]
 [browser_html_tooltip_zoom.js]
 [browser_inplace-editor-01.js]
 [browser_inplace-editor-02.js]
 [browser_inplace-editor_autoclose_parentheses.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_resize.js
@@ -0,0 +1,77 @@
+/* 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 resized.
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = CHROME_URL_ROOT + "doc_html_tooltip.xul";
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+const TOOLBOX_WIDTH = 500;
+
+add_task(async function() {
+  await pushPref("devtools.toolbox.sidebar.width", TOOLBOX_WIDTH);
+
+  // Open the host on the right so that the doorhangers hang right.
+  const [,, doc] = await createHost("right", TEST_URI);
+
+  info("Test resizing of a tooltip");
+
+  const tooltip =
+    new HTMLTooltip(doc, { useXulWrapper: true, type: "doorhanger" });
+  const div = doc.createElementNS(HTML_NS, "div");
+  div.textContent = "tooltip";
+  div.style.cssText = "width: 100px; height: 40px";
+  tooltip.setContent(div);
+
+  const box1 = doc.getElementById("box1");
+
+  await showTooltip(tooltip, box1, { position: "top" });
+
+  // Get the original position of the panel and arrow.
+  const originalPanelBounds =
+    tooltip.panel.getBoxQuads({ relativeTo: doc })[0].getBounds();
+  const originalArrowBounds =
+    tooltip.arrow.getBoxQuads({ relativeTo: doc })[0].getBounds();
+
+  // Resize the content
+  div.style.cssText = "width: 200px; height: 30px";
+  tooltip.updateContainerBounds(box1, { position: "top" });
+
+  // The panel should have moved 100px to the left and 10px down
+  const updatedPanelBounds =
+    tooltip.panel.getBoxQuads({ relativeTo: doc })[0].getBounds();
+
+  const panelXMovement = `panel left: ${originalPanelBounds.left}->` +
+    updatedPanelBounds.left;
+  ok(Math.round(updatedPanelBounds.left - originalPanelBounds.left) === -100,
+     `Panel should have moved 100px to the left (actual: ${panelXMovement})`);
+
+  const panelYMovement = `panel top: ${originalPanelBounds.top}->` +
+    updatedPanelBounds.top;
+  ok(Math.round(updatedPanelBounds.top - originalPanelBounds.top) === 10,
+     `Panel should have moved 10px down (actual: ${panelYMovement})`);
+
+  // The arrow should be in the same position
+  const updatedArrowBounds =
+    tooltip.arrow.getBoxQuads({ relativeTo: doc })[0].getBounds();
+
+  const arrowXMovement = `arrow left: ${originalArrowBounds.left}->` +
+    updatedArrowBounds.left;
+  ok(Math.round(updatedArrowBounds.left - originalArrowBounds.left) === 0,
+     `Arrow should not have moved (actual: ${arrowXMovement})`);
+
+  const arrowYMovement = `arrow top: ${originalArrowBounds.top}->` +
+    updatedArrowBounds.top;
+  ok(Math.round(updatedArrowBounds.top - originalArrowBounds.top) === 0,
+     `Arrow should not have moved (actual: ${arrowYMovement})`);
+
+  await hideTooltip(tooltip);
+  tooltip.destroy();
+});
--- a/devtools/client/shared/widgets/tooltip/HTMLTooltip.js
+++ b/devtools/client/shared/widgets/tooltip/HTMLTooltip.js
@@ -417,28 +417,76 @@ HTMLTooltip.prototype = {
 
   /**
    * Show the tooltip next to the provided anchor element. A preferred position
    * can be set. The event "shown" will be fired after the tooltip is displayed.
    *
    * @param {Element} anchor
    *        The reference element with which the tooltip should be aligned
    * @param {Object} options
-   *        Settings for positioning the tooltip.
+   *        Optional settings for positioning the tooltip.
    * @param {String} options.position
    *        Optional, possible values: top|bottom
    *        If layout permits, the tooltip will be displayed on top/bottom
    *        of the anchor. If omitted, the tooltip will be displayed where
    *        more space is available.
    * @param {Number} options.x
    *        Optional, horizontal offset between the anchor and the tooltip.
    * @param {Number} options.y
    *        Optional, vertical offset between the anchor and the tooltip.
    */
-  async show(anchor, {position, x = 0, y = 0} = {}) {
+  async show(anchor, options) {
+    const { left, top } = this._updateContainerBounds(anchor, options);
+
+    if (this.useXulWrapper) {
+      await this._showXulWrapperAt(left, top);
+    } else {
+      this.container.style.left = left + "px";
+      this.container.style.top = top + "px";
+    }
+
+    this.container.classList.add("tooltip-visible");
+
+    // Keep a pointer on the focused element to refocus it when hiding the tooltip.
+    this._focusedElement = this.doc.activeElement;
+
+    this.doc.defaultView.clearTimeout(this.attachEventsTimer);
+    this.attachEventsTimer = this.doc.defaultView.setTimeout(() => {
+      if (this.autofocus) {
+        this.focus();
+      }
+      // Update the top window reference each time in case the host changes.
+      this.topWindow = this._getTopWindow();
+      this.topWindow.addEventListener("click", this._onClick, true);
+      this.emit("shown");
+    }, 0);
+  },
+
+  /**
+   * Recalculate the dimensions and position of the tooltip in response to
+   * changes to its content.
+   *
+   * Parameters are identical to show().
+   */
+  updateContainerBounds(anchor, options) {
+    if (!this.isVisible()) {
+      return;
+    }
+
+    const { left, top } = this._updateContainerBounds(anchor, options);
+
+    if (this.useXulWrapper) {
+      this._moveXulWrapperTo(left, top);
+    } else {
+      this.container.style.left = left + "px";
+      this.container.style.top = top + "px";
+    }
+  },
+
+  _updateContainerBounds(anchor, {position, x = 0, y = 0} = {}) {
     // Get anchor geometry
     let anchorRect = getRelativeRect(anchor, this.doc);
     if (this.useXulWrapper) {
       anchorRect = this._convertToScreenRect(anchorRect);
     }
 
     const { viewportRect, windowRect } = this._getBoundingRects();
 
@@ -539,38 +587,17 @@ HTMLTooltip.prototype = {
 
     // If the preferred height is set to Infinity, the tooltip container should grow based
     // on its content's height and use as much height as possible.
     this.container.classList.toggle("tooltip-flexible-height",
       this.preferredHeight === Infinity);
 
     this.container.style.height = height + "px";
 
-    if (this.useXulWrapper) {
-      await this._showXulWrapperAt(left, top);
-    } else {
-      this.container.style.left = left + "px";
-      this.container.style.top = top + "px";
-    }
-
-    this.container.classList.add("tooltip-visible");
-
-    // Keep a pointer on the focused element to refocus it when hiding the tooltip.
-    this._focusedElement = this.doc.activeElement;
-
-    this.doc.defaultView.clearTimeout(this.attachEventsTimer);
-    this.attachEventsTimer = this.doc.defaultView.setTimeout(() => {
-      if (this.autofocus) {
-        this.focus();
-      }
-      // Update the top window reference each time in case the host changes.
-      this.topWindow = this._getTopWindow();
-      this.topWindow.addEventListener("click", this._onClick, true);
-      this.emit("shown");
-    }, 0);
+    return { left, top };
   },
 
   /**
    * Calculate the following boundary rectangles:
    *
    * - Viewport rect: This is the region that limits the tooltip dimensions.
    *   When using a XUL panel wrapper, the tooltip will be able to use the whole
    *   screen (excluding space reserved by the OS for toolbars etc.) and hence
@@ -845,16 +872,21 @@ HTMLTooltip.prototype = {
   _showXulWrapperAt: function(left, top) {
     this.xulPanelWrapper.addEventListener("popuphidden", this._onXulPanelHidden);
     const onPanelShown = listenOnce(this.xulPanelWrapper, "popupshown");
     const zoom = getCurrentZoom(this.xulPanelWrapper);
     this.xulPanelWrapper.openPopupAtScreen(left * zoom, top * zoom, false);
     return onPanelShown;
   },
 
+  _moveXulWrapperTo: function(left, top) {
+    const zoom = getCurrentZoom(this.xulPanelWrapper);
+    this.xulPanelWrapper.moveTo(left * zoom, top * zoom);
+  },
+
   _hideXulWrapper: function() {
     this.xulPanelWrapper.removeEventListener("popuphidden", this._onXulPanelHidden);
 
     if (this.xulPanelWrapper.state === "closed") {
       // XUL panel is already closed, resolve immediately.
       return Promise.resolve();
     }