Bug 1461522 - Add a mechanism to allow updating an HTMLTooltip's size and position; r?jdescottes
MozReview-Commit-ID: 4SDxlTTFp8E
--- 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();
}