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
--- 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.