diff --git a/devtools/client/shared/test/browser.ini b/devtools/client/shared/test/browser.ini index 48db269651f0..2043127232e0 100644 --- a/devtools/client/shared/test/browser.ini +++ b/devtools/client/shared/test/browser.ini @@ -124,6 +124,7 @@ skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts [browser_html_tooltip_offset.js] [browser_html_tooltip_variable-height.js] [browser_html_tooltip_width-auto.js] +[browser_html_tooltip_xul-wrapper.js] [browser_inplace-editor-01.js] [browser_inplace-editor-02.js] [browser_inplace-editor_autocomplete_01.js] diff --git a/devtools/client/shared/test/browser_html_tooltip-01.js b/devtools/client/shared/test/browser_html_tooltip-01.js index b85850abdef7..1d5304e99ba2 100644 --- a/devtools/client/shared/test/browser_html_tooltip-01.js +++ b/devtools/client/shared/test/browser_html_tooltip-01.js @@ -9,7 +9,7 @@ */ const HTML_NS = "http://www.w3.org/1999/xhtml"; -const TEST_WINDOW_URI = `data:text/xml;charset=UTF-8, +const TEST_URI = `data:text/xml;charset=UTF-8, `; -const TEST_PAGE_URI = `data:text/xml;charset=UTF-8, - - - - - test1 - - `; - const {HTMLTooltip} = require("devtools/client/shared/widgets/HTMLTooltip"); loadHelperScript("helper_html_tooltip.js"); +let useXulWrapper; + function getTooltipContent(doc) { let div = doc.createElementNS(HTML_NS, "div"); div.style.height = "50px"; @@ -44,18 +36,20 @@ function getTooltipContent(doc) { } add_task(function* () { - info("Test showing a basic tooltip in XUL document using "); - yield testTooltipForUri(TEST_WINDOW_URI); + let [,, doc] = yield createHost("bottom", TEST_URI); - info("Test showing a basic tooltip in XUL document using "); - yield testTooltipForUri(TEST_PAGE_URI); + info("Run tests for a Tooltip without using a XUL panel"); + useXulWrapper = false; + yield runTests(doc); + + info("Run tests for a Tooltip with a XUL panel"); + useXulWrapper = true; + yield runTests(doc); }); -function* testTooltipForUri(uri) { - let tab = yield addTab("about:blank"); - let [,, doc] = yield createHost("bottom", uri); - - let tooltip = new HTMLTooltip({doc}, {}); +function* runTests(doc) { + yield addTab("about:blank"); + let tooltip = new HTMLTooltip({doc}, {useXulWrapper}); info("Set tooltip content"); tooltip.setContent(getTooltipContent(doc), {width: 100, height: 50}); @@ -90,5 +84,5 @@ function* testTooltipForUri(uri) { yield waitForReflow(tooltip); is(tooltip.isVisible(), false, "Tooltip is not visible"); - yield removeTab(tab); + tooltip.destroy(); } diff --git a/devtools/client/shared/test/browser_html_tooltip-02.js b/devtools/client/shared/test/browser_html_tooltip-02.js index 2359667d9517..500e76fe6976 100644 --- a/devtools/client/shared/test/browser_html_tooltip-02.js +++ b/devtools/client/shared/test/browser_html_tooltip-02.js @@ -27,21 +27,33 @@ const TEST_URI = `data:text/xml;charset=UTF-8, const {HTMLTooltip} = require("devtools/client/shared/widgets/HTMLTooltip"); loadHelperScript("helper_html_tooltip.js"); +let useXulWrapper; + add_task(function* () { yield addTab("about:blank"); let [,, doc] = yield createHost("bottom", TEST_URI); + info("Run tests for a Tooltip without using a XUL panel"); + useXulWrapper = false; + yield runTests(doc); + + info("Run tests for a Tooltip with a XUL panel"); + useXulWrapper = true; + yield runTests(doc); +}); + +function* runTests(doc) { yield testClickInTooltipContent(doc); yield testConsumeOutsideClicksFalse(doc); yield testConsumeOutsideClicksTrue(doc); yield testClickInOuterIframe(doc); yield testClickInInnerIframe(doc); -}); +} function* testClickInTooltipContent(doc) { info("Test a tooltip is not closed when clicking inside itself"); - let tooltip = new HTMLTooltip({doc}, {}); + let tooltip = new HTMLTooltip({doc}, {useXulWrapper}); tooltip.setContent(getTooltipContent(doc), {width: 100, height: 50}); yield showTooltip(tooltip, doc.getElementById("box1")); @@ -57,7 +69,7 @@ function* testConsumeOutsideClicksFalse(doc) { info("Test closing a tooltip via click with consumeOutsideClicks: false"); let box4 = doc.getElementById("box4"); - let tooltip = new HTMLTooltip({doc}, {consumeOutsideClicks: false}); + let tooltip = new HTMLTooltip({doc}, {consumeOutsideClicks: false, useXulWrapper}); tooltip.setContent(getTooltipContent(doc), {width: 100, height: 50}); yield showTooltip(tooltip, doc.getElementById("box1")); @@ -80,7 +92,7 @@ function* testConsumeOutsideClicksTrue(doc) { let box4clicks = 0; box4.addEventListener("click", () => box4clicks++); - let tooltip = new HTMLTooltip({doc}, {consumeOutsideClicks: true}); + let tooltip = new HTMLTooltip({doc}, {consumeOutsideClicks: true, useXulWrapper}); tooltip.setContent(getTooltipContent(doc), {width: 100, height: 50}); yield showTooltip(tooltip, doc.getElementById("box1")); @@ -98,7 +110,7 @@ function* testClickInOuterIframe(doc) { info("Test clicking an iframe outside of the tooltip closes the tooltip"); let frame = doc.getElementById("frame"); - let tooltip = new HTMLTooltip({doc}); + let tooltip = new HTMLTooltip({doc}, {useXulWrapper}); tooltip.setContent(getTooltipContent(doc), {width: 100, height: 50}); yield showTooltip(tooltip, doc.getElementById("box1")); @@ -113,7 +125,7 @@ function* testClickInOuterIframe(doc) { function* testClickInInnerIframe(doc) { info("Test clicking an iframe inside the tooltip content does not close the tooltip"); - let tooltip = new HTMLTooltip({doc}, {consumeOutsideClicks: false}); + let tooltip = new HTMLTooltip({doc}, {consumeOutsideClicks: false, useXulWrapper}); let iframe = doc.createElementNS(HTML_NS, "iframe"); iframe.style.width = "100px"; diff --git a/devtools/client/shared/test/browser_html_tooltip-03.js b/devtools/client/shared/test/browser_html_tooltip-03.js index 5eb64bb85a89..03a362b12d8d 100644 --- a/devtools/client/shared/test/browser_html_tooltip-03.js +++ b/devtools/client/shared/test/browser_html_tooltip-03.js @@ -31,14 +31,26 @@ const TEST_URI = `data:text/xml;charset=UTF-8, const {HTMLTooltip} = require("devtools/client/shared/widgets/HTMLTooltip"); loadHelperScript("helper_html_tooltip.js"); +let useXulWrapper; + add_task(function* () { yield addTab("about:blank"); let [, , doc] = yield createHost("bottom", TEST_URI); + info("Run tests for a Tooltip without using a XUL panel"); + useXulWrapper = false; + yield runTests(doc); + + info("Run tests for a Tooltip with a XUL panel"); + useXulWrapper = true; + yield runTests(doc); +}); + +function* runTests(doc) { yield testNoAutoFocus(doc); yield testAutoFocus(doc); yield testAutoFocusPreservesFocusChange(doc); -}); +} function* testNoAutoFocus(doc) { yield focusNode(doc, "#box4-input"); @@ -126,7 +138,7 @@ function blurNode(doc, selector) { * tooltip content will be ready. */ function* createTooltip(doc, autofocus) { - let tooltip = new HTMLTooltip({doc}, {autofocus}); + let tooltip = new HTMLTooltip({doc}, {autofocus, useXulWrapper}); let div = doc.createElementNS(HTML_NS, "div"); div.classList.add("tooltip-content"); div.style.height = "50px"; diff --git a/devtools/client/shared/test/browser_html_tooltip-04.js b/devtools/client/shared/test/browser_html_tooltip-04.js index 16b1654167a8..9202012bfabe 100644 --- a/devtools/client/shared/test/browser_html_tooltip-04.js +++ b/devtools/client/shared/test/browser_html_tooltip-04.js @@ -40,7 +40,7 @@ add_task(function* () { let [,, doc] = yield createHost("bottom", TEST_URI); info("Create HTML tooltip"); - let tooltip = new HTMLTooltip({doc}, {}); + let tooltip = new HTMLTooltip({doc}, {useXulWrapper: false}); let div = doc.createElementNS(HTML_NS, "div"); div.style.height = "100%"; tooltip.setContent(div, {width: TOOLTIP_WIDTH, height: TOOLTIP_HEIGHT}); diff --git a/devtools/client/shared/test/browser_html_tooltip-05.js b/devtools/client/shared/test/browser_html_tooltip-05.js index e5c4d291b7e5..f695c34a60e8 100644 --- a/devtools/client/shared/test/browser_html_tooltip-05.js +++ b/devtools/client/shared/test/browser_html_tooltip-05.js @@ -32,12 +32,11 @@ const TOOLTIP_WIDTH = 200; 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("Create HTML tooltip"); - let tooltip = new HTMLTooltip({doc}, {}); + let tooltip = new HTMLTooltip({doc}, {useXulWrapper: false}); let div = doc.createElementNS(HTML_NS, "div"); div.style.height = "100%"; tooltip.setContent(div, {width: TOOLTIP_WIDTH, height: TOOLTIP_HEIGHT}); @@ -57,7 +56,7 @@ add_task(function* () { 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: 150, width}; checkTooltipGeometry(tooltip, box1, expectedTooltipGeometry); yield hideTooltip(tooltip); diff --git a/devtools/client/shared/test/browser_html_tooltip_arrow-01.js b/devtools/client/shared/test/browser_html_tooltip_arrow-01.js index 3169cb3cda9d..b6c77b0d9177 100644 --- a/devtools/client/shared/test/browser_html_tooltip_arrow-01.js +++ b/devtools/client/shared/test/browser_html_tooltip_arrow-01.js @@ -49,6 +49,8 @@ const TEST_URI = `data:text/xml;charset=UTF-8, const {HTMLTooltip} = require("devtools/client/shared/widgets/HTMLTooltip"); loadHelperScript("helper_html_tooltip.js"); +let useXulWrapper; + add_task(function* () { // Force the toolbox to be 200px high; yield pushPref("devtools.toolbox.footer.height", 200); @@ -56,8 +58,18 @@ add_task(function* () { yield addTab("about:blank"); let [,, doc] = yield createHost("bottom", TEST_URI); + info("Run tests for a Tooltip without using a XUL panel"); + useXulWrapper = false; + yield runTests(doc); + + info("Run tests for a Tooltip with a XUL panel"); + useXulWrapper = true; + yield runTests(doc); +}); + +function* runTests(doc) { info("Create HTML tooltip"); - let tooltip = new HTMLTooltip({doc}, {type: "arrow"}); + let tooltip = new HTMLTooltip({doc}, {type: "arrow", useXulWrapper}); let div = doc.createElementNS(HTML_NS, "div"); div.style.height = "35px"; tooltip.setContent(div, {width: 200, height: 35}); @@ -91,4 +103,6 @@ add_task(function* () { yield hideTooltip(tooltip); } -}); + + tooltip.destroy(); +} diff --git a/devtools/client/shared/test/browser_html_tooltip_arrow-02.js b/devtools/client/shared/test/browser_html_tooltip_arrow-02.js index b1954d5ab69c..79e120c4e206 100644 --- a/devtools/client/shared/test/browser_html_tooltip_arrow-02.js +++ b/devtools/client/shared/test/browser_html_tooltip_arrow-02.js @@ -43,15 +43,26 @@ const TEST_URI = `data:text/xml;charset=UTF-8, const {HTMLTooltip} = require("devtools/client/shared/widgets/HTMLTooltip"); loadHelperScript("helper_html_tooltip.js"); +let useXulWrapper; + 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("Run tests for a Tooltip without using a XUL panel"); + useXulWrapper = false; + yield runTests(doc); + + info("Run tests for a Tooltip with a XUL panel"); + useXulWrapper = true; + yield runTests(doc); +}); + +function* runTests(doc) { info("Create HTML tooltip"); - let tooltip = new HTMLTooltip({doc}, {type: "arrow"}); + let tooltip = new HTMLTooltip({doc}, {type: "arrow", useXulWrapper}); let div = doc.createElementNS(HTML_NS, "div"); div.style.height = "35px"; tooltip.setContent(div, {width: 200, height: 35}); @@ -84,4 +95,4 @@ add_task(function* () { "The tooltip arrow remains inside the tooltip panel horizontally"); yield hideTooltip(tooltip); } -}); +} diff --git a/devtools/client/shared/test/browser_html_tooltip_consecutive-show.js b/devtools/client/shared/test/browser_html_tooltip_consecutive-show.js index b2c4480f89e4..6dc431c81026 100644 --- a/devtools/client/shared/test/browser_html_tooltip_consecutive-show.js +++ b/devtools/client/shared/test/browser_html_tooltip_consecutive-show.js @@ -34,7 +34,6 @@ function getTooltipContent(doc) { } add_task(function* () { - yield addTab("about:blank"); let [,, doc] = yield createHost("bottom", TEST_URI); let box1 = doc.getElementById("box1"); @@ -44,7 +43,7 @@ add_task(function* () { let width = 100, height = 50; - let tooltip = new HTMLTooltip({doc}, {}); + let tooltip = new HTMLTooltip({doc}, {useXulWrapper: false}); tooltip.setContent(getTooltipContent(doc), {width, height}); info("Show the tooltip on each of the 4 hbox, without calling hide in between"); diff --git a/devtools/client/shared/test/browser_html_tooltip_offset.js b/devtools/client/shared/test/browser_html_tooltip_offset.js index 30e5b550b996..f85b2627fdec 100644 --- a/devtools/client/shared/test/browser_html_tooltip_offset.js +++ b/devtools/client/shared/test/browser_html_tooltip_offset.js @@ -30,7 +30,6 @@ 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"); @@ -40,7 +39,7 @@ add_task(function* () { let box3 = doc.getElementById("box3"); let box4 = doc.getElementById("box4"); - let tooltip = new HTMLTooltip({doc}, {}); + let tooltip = new HTMLTooltip({doc}, {useXulWrapper: false}); let div = doc.createElementNS(HTML_NS, "div"); div.style.height = "100px"; diff --git a/devtools/client/shared/test/browser_html_tooltip_variable-height.js b/devtools/client/shared/test/browser_html_tooltip_variable-height.js index 2e2c52cb5e3e..9d0df7d2116a 100644 --- a/devtools/client/shared/test/browser_html_tooltip_variable-height.js +++ b/devtools/client/shared/test/browser_html_tooltip_variable-height.js @@ -29,6 +29,8 @@ const TOOLTIP_HEIGHT = 50; const {HTMLTooltip} = require("devtools/client/shared/widgets/HTMLTooltip"); loadHelperScript("helper_html_tooltip.js"); +let useXulWrapper; + add_task(function* () { // Force the toolbox to be 400px tall => 50px for each box. yield pushPref("devtools.toolbox.footer.height", 400); @@ -36,7 +38,17 @@ add_task(function* () { yield addTab("about:blank"); let [,, doc] = yield createHost("bottom", TEST_URI); - let tooltip = new HTMLTooltip({doc}, {}); + info("Run tests for a Tooltip without using a XUL panel"); + useXulWrapper = false; + yield runTests(doc); + + info("Run tests for a Tooltip with a XUL panel"); + useXulWrapper = true; + yield runTests(doc); +}); + +function* runTests(doc) { + let tooltip = new HTMLTooltip({doc}, {useXulWrapper}); info("Set tooltip content 50px tall, but request a container 200px tall"); let tooltipContent = doc.createElementNS(HTML_NS, "div"); tooltipContent.style.cssText = "height: " + TOOLTIP_HEIGHT + "px; background: red;"; @@ -71,4 +83,4 @@ add_task(function* () { EventUtils.synthesizeMouse(tooltip.container, 100, CONTAINER_HEIGHT + 10, {}, doc.defaultView); yield onHidden; -}); +} diff --git a/devtools/client/shared/test/browser_html_tooltip_width-auto.js b/devtools/client/shared/test/browser_html_tooltip_width-auto.js index 2e80d77d903b..f2e811345e3e 100644 --- a/devtools/client/shared/test/browser_html_tooltip_width-auto.js +++ b/devtools/client/shared/test/browser_html_tooltip_width-auto.js @@ -25,11 +25,23 @@ const TEST_URI = `data:text/xml;charset=UTF-8, const {HTMLTooltip} = require("devtools/client/shared/widgets/HTMLTooltip"); loadHelperScript("helper_html_tooltip.js"); +let useXulWrapper; + add_task(function* () { yield addTab("about:blank"); let [,, doc] = yield createHost("bottom", TEST_URI); - let tooltip = new HTMLTooltip({doc}, {}); + info("Run tests for a Tooltip without using a XUL panel"); + useXulWrapper = false; + yield runTests(doc); + + info("Run tests for a Tooltip with a XUL panel"); + useXulWrapper = true; + yield runTests(doc); +}); + +function* runTests(doc) { + let tooltip = new HTMLTooltip({doc}, {useXulWrapper}); info("Create tooltip content width to 150px"); let tooltipContent = doc.createElementNS(HTML_NS, "div"); tooltipContent.style.cssText = "height: 100%; width: 150px; background: red;"; @@ -44,4 +56,4 @@ add_task(function* () { is(panelRect.width, 150, "Tooltip panel has the expected width."); yield hideTooltip(tooltip); -}); +} diff --git a/devtools/client/shared/test/browser_html_tooltip_xul-wrapper.js b/devtools/client/shared/test/browser_html_tooltip_xul-wrapper.js new file mode 100644 index 000000000000..2dff0b72fcce --- /dev/null +++ b/devtools/client/shared/test/browser_html_tooltip_xul-wrapper.js @@ -0,0 +1,78 @@ +/* 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 overflow out of the toolbox when using a XUL panel wrapper. + */ + +const HTML_NS = "http://www.w3.org/1999/xhtml"; +const TEST_URI = `data:text/xml;charset=UTF-8, + + + + + test1 + test2 + test3 + test4 + + `; + +const {HTMLTooltip} = require("devtools/client/shared/widgets/HTMLTooltip"); +loadHelperScript("helper_html_tooltip.js"); + +// The test toolbox will be 200px tall, the anchors are 50px tall, therefore, the maximum +// tooltip height that could fit in the toolbox is 150px. Setting 160px, the tooltip will +// either have to overflow or to be resized. +const TOOLTIP_HEIGHT = 160; +const TOOLTIP_WIDTH = 200; + +add_task(function* () { + // Force the toolbox to be 200px high; + yield pushPref("devtools.toolbox.footer.height", 200); + + let [, win, doc] = yield createHost("bottom", TEST_URI); + + info("Resizing window to have some space below the window."); + let originalWidth = win.top.outerWidth; + let originalHeight = win.top.outerHeight; + win.top.resizeBy(0, -100); + + info("Create HTML tooltip"); + let tooltip = new HTMLTooltip({doc}, {useXulWrapper: true}); + let div = doc.createElementNS(HTML_NS, "div"); + div.style.height = "200px"; + div.style.background = "red"; + tooltip.setContent(div, {width: TOOLTIP_WIDTH, height: TOOLTIP_HEIGHT}); + + let box1 = doc.getElementById("box1"); + + // Above box1: check that the tooltip can overflow onto the content page. + info("Display the tooltip above box1."); + yield showTooltip(tooltip, box1, {position: "top"}); + checkTooltip(tooltip, "top", TOOLTIP_HEIGHT); + yield hideTooltip(tooltip); + + // Below box1: check that the tooltip can overflow out of the browser window. + info("Display the tooltip below box1."); + yield showTooltip(tooltip, box1, {position: "bottom"}); + checkTooltip(tooltip, "bottom", TOOLTIP_HEIGHT); + yield hideTooltip(tooltip); + + is(tooltip.isVisible(), false, "Tooltip is not visible"); + + info("Restore original window dimensions."); + win.top.resizeTo(originalWidth, originalHeight); +}); + +function checkTooltip(tooltip, position, height) { + is(tooltip.position, position, "Actual tooltip position is " + position); + let rect = tooltip.container.getBoundingClientRect(); + is(rect.height, height, "Actual tooltip height is " + height); + // Testing the actual left/top offsets is not relevant here as it is handled by the XUL + // panel. +} diff --git a/devtools/client/shared/widgets/HTMLTooltip.js b/devtools/client/shared/widgets/HTMLTooltip.js index 109c09a16130..f87aaea2e113 100644 --- a/devtools/client/shared/widgets/HTMLTooltip.js +++ b/devtools/client/shared/widgets/HTMLTooltip.js @@ -8,6 +8,8 @@ const EventEmitter = require("devtools/shared/event-emitter"); const {TooltipToggle} = require("devtools/client/shared/widgets/tooltip/TooltipToggle"); +const {listenOnce} = require("devtools/shared/async-utils"); +const {Task} = require("devtools/shared/task"); const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; const XHTML_NS = "http://www.w3.org/1999/xhtml"; @@ -49,8 +51,9 @@ const EXTRA_BORDER = { * * @param {DOMRect} anchorRect * Bounding rectangle for the anchor, relative to the tooltip document. - * @param {DOMRect} docRect - * Bounding rectange for the tooltip document owner. + * @param {DOMRect} viewportRect + * Bounding rectangle for the viewport. top/left can be different from 0 if some + * space should not be used by tooltips (for instance OS toolbars, taskbars etc.). * @param {Number} height * Preferred height for the tooltip. * @param {String} pos @@ -61,15 +64,18 @@ const EXTRA_BORDER = { * - {String} computedPosition: Can differ from the preferred position depending * on the available height). "top" or "bottom" */ -const calculateVerticalPosition = function (anchorRect, docRect, height, pos, offset) { +const calculateVerticalPosition = +function (anchorRect, viewportRect, height, pos, offset) { let {TOP, BOTTOM} = POSITION; let {top: anchorTop, height: anchorHeight} = anchorRect; - let {bottom: docBottom} = docRect; + + // Translate to the available viewport space before calculating dimensions and position. + anchorTop -= viewportRect.top; // Calculate available space for the tooltip. let availableTop = anchorTop; - let availableBottom = docBottom - (anchorTop + anchorHeight); + let availableBottom = viewportRect.height - (anchorTop + anchorHeight); // Find POSITION let keepPosition = false; @@ -90,6 +96,9 @@ const calculateVerticalPosition = function (anchorRect, docRect, height, pos, of // Calculate TOP. let top = pos === TOP ? anchorTop - height - offset : anchorTop + anchorHeight + offset; + // Translate back to absolute coordinates by re-including viewport top margin. + top += viewportRect.top; + return {top, height, computedPosition: pos}; }; @@ -100,8 +109,9 @@ const calculateVerticalPosition = function (anchorRect, docRect, height, pos, of * * @param {DOMRect} anchorRect * Bounding rectangle for the anchor, relative to the tooltip document. - * @param {DOMRect} docRect - * Bounding rectange for the tooltip document owner. + * @param {DOMRect} viewportRect + * Bounding rectangle for the viewport. top/left can be different from 0 if some + * space should not be used by tooltips (for instance OS toolbars, taskbars etc.). * @param {Number} width * Preferred width for the tooltip. * @return {Object} @@ -109,18 +119,20 @@ const calculateVerticalPosition = function (anchorRect, docRect, height, pos, of * - {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, width, type, offset) { +const calculateHorizontalPosition = +function (anchorRect, viewportRect, width, type, offset) { let {left: anchorLeft, width: anchorWidth} = anchorRect; - let {right: docRight} = docRect; + + // Translate to the available viewport space before calculating dimensions and position. + anchorLeft -= viewportRect.left; // Calculate WIDTH. - let availableWidth = docRight; - width = Math.min(width, availableWidth); + width = Math.min(width, viewportRect.width); // 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 + offset, docRight - width); + let left = Math.min(anchorLeft + offset, viewportRect.width - width); // Calculate ARROW LEFT (tooltip's LEFT might be updated) let arrowLeft; @@ -141,6 +153,9 @@ const calculateHorizontalPosition = function (anchorRect, docRect, width, type, arrowLeft = Math.max(arrowLeft, 0); } + // Translate back to absolute coordinates by re-including viewport left margin. + left += viewportRect.left; + return {left, width, arrowLeft}; }; @@ -177,15 +192,25 @@ const getRelativeRect = function (node, relativeTo) { * - {Boolean} consumeOutsideClicks * Defaults to true. The tooltip is closed when clicking outside. * Should this event be stopped and consumed or not. + * - {Boolean} useXulWrapper + * Defaults to true. If the tooltip is hosted in a XUL document, use a XUL panel + * in order to use all the screen viewport available. */ -function HTMLTooltip(toolbox, - {type = "normal", autofocus = false, consumeOutsideClicks = true} = {}) { +function HTMLTooltip(toolbox, { + type = "normal", + autofocus = false, + consumeOutsideClicks = true, + useXulWrapper = true, + } = {}) { EventEmitter.decorate(this); this.doc = toolbox.doc; this.type = type; this.autofocus = autofocus; this.consumeOutsideClicks = consumeOutsideClicks; + this.useXulWrapper = useXulWrapper; + + this._position = null; // Use the topmost window to listen for click events to close the tooltip this.topWindow = this.doc.defaultView.top; @@ -198,7 +223,20 @@ function HTMLTooltip(toolbox, this.container = this._createContainer(); - if (this._isXUL()) { + if (this._isXUL() && this.useXulWrapper) { + // When using a XUL panel as the wrapper, the actual markup for the tooltip is as + // follows : + // + //
+ //
+ 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);