From 3aa417586a22c12841516dea5e81ce14927b9e03 Mon Sep 17 00:00:00 2001 From: Julian Descottes Date: Wed, 6 Jul 2016 14:50:44 +0200 Subject: [PATCH] Bug 1267403 - HTMLTooltip: add useXulWrapper option when displayed in a XUL document;r=ochameau The HTMLTooltip supports an additional configuration parameter "useXulWrapper". When set to true, if the tooltip is displayed in a XUL document, a XUL panel will be used as an additional container for the tooltip. This allows the tooltip to be displayed anywhere on the screen and can be useful when displayed in small toolboxes. MozReview-Commit-ID: 63kv4vAeW5R --HG-- extra : source : fc4d902ff01ee92a5b6742d44286e5feaaba1500 extra : intermediate-source : 126f43ff3be5505920946a77ad82401c6bbaebef extra : histedit_source : 863888c014723f7e95742079395497ba1a30aa36%2C13ba9aaf80acb96c587739c767c20a8f0f6a9a5a --- devtools/client/shared/test/browser.ini | 1 + .../shared/test/browser_html_tooltip-01.js | 36 ++- .../shared/test/browser_html_tooltip-02.js | 24 +- .../shared/test/browser_html_tooltip-03.js | 16 +- .../shared/test/browser_html_tooltip-04.js | 2 +- .../shared/test/browser_html_tooltip-05.js | 5 +- .../test/browser_html_tooltip_arrow-01.js | 18 +- .../test/browser_html_tooltip_arrow-02.js | 17 +- .../browser_html_tooltip_consecutive-show.js | 3 +- .../test/browser_html_tooltip_offset.js | 3 +- .../browser_html_tooltip_variable-height.js | 16 +- .../test/browser_html_tooltip_width-auto.js | 16 +- .../test/browser_html_tooltip_xul-wrapper.js | 78 +++++++ devtools/client/shared/widgets/HTMLTooltip.js | 218 +++++++++++++++--- devtools/client/themes/tooltips.css | 18 +- 15 files changed, 388 insertions(+), 83 deletions(-) create mode 100644 devtools/client/shared/test/browser_html_tooltip_xul-wrapper.js 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);