+ //
+ this.xulPanelWrapper = this._createXulPanelWrapper();
+ let inner = this.doc.createElementNS(XHTML_NS, "div");
+ inner.classList.add("tooltip-xul-wrapper-inner");
+
+ this.doc.documentElement.appendChild(this.xulPanelWrapper);
+ this.xulPanelWrapper.appendChild(inner);
+ inner.appendChild(this.container);
+ } else if (this._isXUL()) {
this.doc.documentElement.appendChild(this.container);
} else {
// In non-XUL context the container is ready to use as is.
@@ -224,6 +262,13 @@ HTMLTooltip.prototype = {
return this.container.querySelector(".tooltip-arrow");
},
+ /**
+ * Retrieve the displayed position used for the tooltip. Null if the tooltip is hidden.
+ */
+ get position() {
+ return this.isVisible() ? this._position : null;
+ },
+
/**
* Set the tooltip content element. The preferred width/height should also be
* specified here.
@@ -260,39 +305,53 @@ HTMLTooltip.prototype = {
* - {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, x = 0, y = 0} = {}) {
+ show: Task.async(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();
+ if (this.useXulWrapper) {
+ anchorRect = this._convertToScreenRect(anchorRect);
+ }
+
+ // Get viewport size
+ let viewportRect = this._getViewportRect();
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, y);
+ calculateVerticalPosition(anchorRect, viewportRect, preferredHeight, position, y);
- // Apply height and top information before measuring the content width (if "auto").
+ this._position = computedPosition;
+ // Apply height before measuring the content width (if width="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 preferredWidth;
+ if (this.preferredWidth === "auto") {
+ preferredWidth = this._measureContainerWidth();
+ } else {
+ let themeWidth = 2 * EXTRA_BORDER[this.type];
+ preferredWidth = this.preferredWidth + themeWidth;
+ }
let {left, width, arrowLeft} =
- calculateHorizontalPosition(anchorRect, docRect, preferredWidth, this.type, x);
+ calculateHorizontalPosition(anchorRect, viewportRect, 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";
}
+ if (this.useXulWrapper) {
+ 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.
@@ -304,14 +363,53 @@ HTMLTooltip.prototype = {
this.topWindow.addEventListener("click", this._onClick, true);
this.emit("shown");
}, 0);
+ }),
+
+ /**
+ * Calculate the rect of the viewport that limits the tooltip dimensions. When using a
+ * XUL panel wrapper, the viewport will be able to use the whole screen (excluding space
+ * reserved by the OS for toolbars etc.). Otherwise, the viewport is limited to the
+ * tooltip's document.
+ *
+ * @return {Object} DOMRect-like object with the Number properties: top, right, bottom,
+ * left, width, height
+ */
+ _getViewportRect: function () {
+ if (this.useXulWrapper) {
+ // availLeft/Top are the coordinates first pixel available on the screen for
+ // applications (excluding space dedicated for OS toolbars, menus etc...)
+ // availWidth/Height are the dimensions available to applications excluding all
+ // the OS reserved space
+ let {availLeft, availTop, availHeight, availWidth} = this.doc.defaultView.screen;
+ return {
+ top: availTop,
+ right: availLeft + availWidth,
+ bottom: availTop + availHeight,
+ left: availLeft,
+ width: availWidth,
+ height: availHeight,
+ };
+ }
+
+ return this.doc.documentElement.getBoundingClientRect();
},
_measureContainerWidth: function () {
+ let xulParent = this.container.parentNode;
+ if (this.useXulWrapper && !this.isVisible()) {
+ // Move the container out of the XUL Panel to measure it.
+ this.doc.documentElement.appendChild(this.container);
+ }
+
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");
+
+ if (this.useXulWrapper && !this.isVisible()) {
+ xulParent.appendChild(this.container);
+ }
+
return width;
},
@@ -319,7 +417,7 @@ HTMLTooltip.prototype = {
* Hide the current tooltip. The event "hidden" will be fired when the tooltip
* is hidden.
*/
- hide: function () {
+ hide: Task.async(function* () {
this.doc.defaultView.clearTimeout(this.attachEventsTimer);
if (!this.isVisible()) {
return;
@@ -327,6 +425,10 @@ HTMLTooltip.prototype = {
this.topWindow.removeEventListener("click", this._onClick, true);
this.container.classList.remove("tooltip-visible");
+ if (this.useXulWrapper) {
+ yield this._hideXulWrapper();
+ }
+
this.emit("hidden");
let tooltipHasFocus = this.container.contains(this.doc.activeElement);
@@ -334,7 +436,7 @@ HTMLTooltip.prototype = {
this._focusedElement.focus();
this._focusedElement = null;
}
- },
+ }),
/**
* Check if the tooltip is currently displayed.
@@ -351,6 +453,9 @@ HTMLTooltip.prototype = {
destroy: function () {
this.hide();
this.container.remove();
+ if (this.xulPanelWrapper) {
+ this.xulPanelWrapper.remove();
+ }
},
_createContainer: function () {
@@ -407,13 +512,6 @@ HTMLTooltip.prototype = {
return false;
},
- /**
- * Check if the tooltip's owner document is a XUL document.
- */
- _isXUL: function () {
- return this.doc.documentElement.namespaceURI === XUL_NS;
- },
-
/**
* If the tootlip is configured to autofocus and a focusable element can be found,
* focus it.
@@ -427,4 +525,52 @@ HTMLTooltip.prototype = {
focusableElement.focus();
}
},
+
+ /**
+ * Check if the tooltip's owner document is a XUL document.
+ */
+ _isXUL: function () {
+ return this.doc.documentElement.namespaceURI === XUL_NS;
+ },
+
+ _createXulPanelWrapper: function () {
+ let panel = this.doc.createElementNS(XUL_NS, "panel");
+
+ // XUL panel is only a way to display DOM elements outside of the document viewport,
+ // so disable all features that impact the behavior.
+ panel.setAttribute("animate", false);
+ panel.setAttribute("consumeoutsideclicks", false);
+ panel.setAttribute("noautofocus", true);
+ panel.setAttribute("ignorekeys", true);
+
+ panel.setAttribute("level", "float");
+ panel.setAttribute("class", "tooltip-xul-wrapper");
+
+ return panel;
+ },
+
+ _showXulWrapperAt: function (left, top) {
+ let onPanelShown = listenOnce(this.xulPanelWrapper, "popupshown");
+ this.xulPanelWrapper.openPopupAtScreen(left, top, false);
+ return onPanelShown;
+ },
+
+ _hideXulWrapper: function () {
+ let onPanelHidden = listenOnce(this.xulPanelWrapper, "popuphidden");
+ this.xulPanelWrapper.hidePopup();
+ return onPanelHidden;
+ },
+
+ /**
+ * Convert from coordinates relative to the tooltip's document, to coordinates relative
+ * to the "available" screen. By "available" we mean the screen, excluding the OS bars
+ * display on screen edges.
+ */
+ _convertToScreenRect: function ({left, top, width, height}) {
+ // mozInnerScreenX/Y are the coordinates of the top left corner of the window's
+ // viewport, excluding chrome UI.
+ left += this.doc.defaultView.mozInnerScreenX;
+ top += this.doc.defaultView.mozInnerScreenY;
+ return {top, right: left + width, bottom: top + height, left, width, height};
+ },
};
diff --git a/devtools/client/themes/tooltips.css b/devtools/client/themes/tooltips.css
index d90dc8e70c09..c493d91e5f5a 100644
--- a/devtools/client/themes/tooltips.css
+++ b/devtools/client/themes/tooltips.css
@@ -109,6 +109,17 @@
overflow: hidden;
}
+.tooltip-xul-wrapper {
+ -moz-appearance: none;
+ background: transparent;
+ overflow: visible;
+ border-style: none;
+}
+
+.tooltip-xul-wrapper .tooltip-container {
+ position: absolute;
+}
+
.tooltip-top {
flex-direction: column;
}
@@ -137,6 +148,11 @@
filter: drop-shadow(0 3px 4px var(--theme-tooltip-shadow));
}
+.tooltip-xul-wrapper .tooltip-container[type="arrow"] {
+ /* When displayed in a XUL panel the drop shadow would be abruptly cut by the panel */
+ filter: none;
+}
+
.tooltip-container[type="arrow"] > .tooltip-panel {
position: relative;
flex-grow: 0;
@@ -265,7 +281,7 @@
.event-tooltip-content-box {
display: none;
- height: 54px;
+ height: 100px;
overflow: hidden;
margin-inline-end: 0;
border: 1px solid var(--theme-splitter-color);