diff --git a/devtools/client/shared/test/browser.ini b/devtools/client/shared/test/browser.ini index 1267ad9120ec..66cee4e6ba96 100644 --- a/devtools/client/shared/test/browser.ini +++ b/devtools/client/shared/test/browser.ini @@ -138,6 +138,7 @@ skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts [browser_html_tooltip_arrow-01.js] [browser_html_tooltip_arrow-02.js] [browser_html_tooltip_consecutive-show.js] +[browser_html_tooltip_height-auto.js] [browser_html_tooltip_hover.js] [browser_html_tooltip_offset.js] [browser_html_tooltip_rtl.js] diff --git a/devtools/client/shared/test/browser_html_tooltip_height-auto.js b/devtools/client/shared/test/browser_html_tooltip_height-auto.js new file mode 100644 index 000000000000..4b9ab41253ba --- /dev/null +++ b/devtools/client/shared/test/browser_html_tooltip_height-auto.js @@ -0,0 +1,67 @@ +/* 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 content can automatically calculate its height based on + * content. + */ + +const HTML_NS = "http://www.w3.org/1999/xhtml"; +const TEST_URI = CHROME_URL_ROOT + "doc_html_tooltip.xul"; + +const {HTMLTooltip} = + require("devtools/client/shared/widgets/tooltip/HTMLTooltip"); +loadHelperScript("helper_html_tooltip.js"); + +let useXulWrapper; + +add_task(async function() { + await addTab("about:blank"); + const [,, doc] = await createHost("bottom", TEST_URI); + + info("Run tests for a Tooltip without using a XUL panel"); + useXulWrapper = false; + await runTests(doc); + + info("Run tests for a Tooltip with a XUL panel"); + useXulWrapper = true; + await runTests(doc); +}); + +async function runTests(doc) { + const tooltip = new HTMLTooltip(doc, {useXulWrapper}); + info("Create tooltip content height to 150px"); + const tooltipContent = doc.createElementNS(HTML_NS, "div"); + tooltipContent.style.cssText = + "width: 300px; height: 150px; background: red;"; + + info("Set tooltip content using width:auto and height:auto"); + tooltip.setContent(tooltipContent); + + info("Show the tooltip and check the tooltip panel dimensions."); + await showTooltip(tooltip, doc.getElementById("box1")); + + let panelRect = tooltip.panel.getBoundingClientRect(); + is(panelRect.width, 300, "Tooltip panel has the expected width."); + is(panelRect.height, 150, "Tooltip panel has the expected width."); + + await hideTooltip(tooltip); + + info("Set tooltip content using fixed width and height:auto"); + tooltipContent.style.cssText = + "width: auto; height: 200px; background: red;"; + tooltip.setContent(tooltipContent, { width: 400 }); + + info("Show the tooltip and check the tooltip panel height."); + await showTooltip(tooltip, doc.getElementById("box1")); + + panelRect = tooltip.panel.getBoundingClientRect(); + is(panelRect.height, 200, "Tooltip panel has the expected width."); + + await hideTooltip(tooltip); + + tooltip.destroy(); +} diff --git a/devtools/client/shared/widgets/tooltip/EventTooltipHelper.js b/devtools/client/shared/widgets/tooltip/EventTooltipHelper.js index f025b1756403..87b9eb2cf2bc 100644 --- a/devtools/client/shared/widgets/tooltip/EventTooltipHelper.js +++ b/devtools/client/shared/widgets/tooltip/EventTooltipHelper.js @@ -197,7 +197,10 @@ EventTooltip.prototype = { this._addContentListeners(header); } - this._tooltip.setContent(this.container, {width: CONTAINER_WIDTH}); + this._tooltip.setContent( + this.container, + {width: CONTAINER_WIDTH, height: Infinity} + ); this._tooltip.on("hidden", this.destroy); }, diff --git a/devtools/client/shared/widgets/tooltip/HTMLTooltip.js b/devtools/client/shared/widgets/tooltip/HTMLTooltip.js index 03687d352b70..d08a215218f4 100644 --- a/devtools/client/shared/widgets/tooltip/HTMLTooltip.js +++ b/devtools/client/shared/widgets/tooltip/HTMLTooltip.js @@ -255,6 +255,8 @@ function HTMLTooltip(toolboxDoc, { this.autofocus = autofocus; this.consumeOutsideClicks = consumeOutsideClicks; this.useXulWrapper = this._isXUL() && useXulWrapper; + this.preferredWidth = "auto"; + this.preferredHeight = "auto"; // The top window is used to attach click event listeners to close the tooltip if the // user clicks on the content page. @@ -326,11 +328,21 @@ HTMLTooltip.prototype = { * @param {Object} * - {Number} width: preferred width for the tooltip container. If not specified * the tooltip container will be measured before being displayed, and the - * measured width will be used as preferred width. - * - {Number} height: optional, preferred height for the tooltip container. If - * not specified, the tooltip will be able to use all the height available. + * measured width will be used as the preferred width. + * - {Number} height: preferred height for the tooltip container. If + * not specified the tooltip container will be measured before being + * displayed, and the measured height will be used as the preferred + * height. + * + * For tooltips whose content height may change while being + * displayed, the special value Infinity may be used to produce + * a flexible container that accommodates resizing content. Note, + * however, that when used in combination with the XUL wrapper the + * unfilled part of this container will consume all mouse events + * making content behind this area inaccessible until the tooltip is + * dismissed. */ - setContent: function(content, {width = "auto", height = Infinity} = {}) { + setContent: function(content, {width = "auto", height = "auto"} = {}) { this.preferredWidth = width; this.preferredHeight = height; @@ -366,28 +378,21 @@ HTMLTooltip.prototype = { // Get viewport size const viewportRect = this._getViewportRect(); - const themeHeight = EXTRA_HEIGHT[this.type] + 2 * EXTRA_BORDER[this.type]; - const preferredHeight = this.preferredHeight + themeHeight; - - const {top, height, computedPosition} = - calculateVerticalPosition(anchorRect, viewportRect, preferredHeight, position, y); - - this._position = computedPosition; - // Apply height before measuring the content width (if width="auto"). - const isTop = computedPosition === POSITION.TOP; - this.container.classList.toggle("tooltip-top", isTop); - this.container.classList.toggle("tooltip-bottom", !isTop); - - // If the preferred height is set to Infinity, the tooltip container should grow based - // on its content's height and use as much height as possible. - this.container.classList.toggle("tooltip-flexible-height", - this.preferredHeight === Infinity); - - this.container.style.height = height + "px"; - + // Calculate the horizonal position and width let preferredWidth; + // Record the height too since it might save us from having to look it up + // later. + let measuredHeight; if (this.preferredWidth === "auto") { - preferredWidth = this._measureContainerWidth(); + // Reset any styles that constrain the dimensions we want to calculate. + this.container.style.width = "auto"; + if (this.preferredHeight === "auto") { + this.container.style.height = "auto"; + } + ({ + width: preferredWidth, + height: measuredHeight, + } = this._measureContainerSize()); } else { const themeWidth = 2 * EXTRA_BORDER[this.type]; preferredWidth = this.preferredWidth + themeWidth; @@ -398,12 +403,47 @@ HTMLTooltip.prototype = { const {left, width, arrowLeft} = calculateHorizontalPosition( anchorRect, viewportRect, preferredWidth, this.type, x, isRtl); - this.container.style.width = width + "px"; + // If we constrained the width, then any measured height we have is no + // longer valid. + if (measuredHeight && width !== preferredWidth) { + measuredHeight = undefined; + } + // Apply width and arrow positioning + this.container.style.width = width + "px"; if (this.type === TYPE.ARROW) { this.arrow.style.left = arrowLeft + "px"; } + // Calculate the vertical position and height + let preferredHeight; + if (this.preferredHeight === "auto") { + if (measuredHeight) { + this.container.style.height = "auto"; + preferredHeight = measuredHeight; + } else { + ({ height: preferredHeight } = this._measureContainerSize()); + } + } else { + const themeHeight = EXTRA_HEIGHT[this.type] + 2 * EXTRA_BORDER[this.type]; + preferredHeight = this.preferredHeight + themeHeight; + } + + const {top, height, computedPosition} = + calculateVerticalPosition(anchorRect, viewportRect, preferredHeight, position, y); + + this._position = computedPosition; + const isTop = computedPosition === POSITION.TOP; + this.container.classList.toggle("tooltip-top", isTop); + this.container.classList.toggle("tooltip-bottom", !isTop); + + // If the preferred height is set to Infinity, the tooltip container should grow based + // on its content's height and use as much height as possible. + this.container.classList.toggle("tooltip-flexible-height", + this.preferredHeight === Infinity); + + this.container.style.height = height + "px"; + if (this.useXulWrapper) { await this._showXulWrapperAt(left, top); } else { @@ -455,7 +495,7 @@ HTMLTooltip.prototype = { return this.doc.documentElement.getBoundingClientRect(); }, - _measureContainerWidth: function() { + _measureContainerSize: function() { const xulParent = this.container.parentNode; if (this.useXulWrapper && !this.isVisible()) { // Move the container out of the XUL Panel to measure it. @@ -463,15 +503,19 @@ HTMLTooltip.prototype = { } this.container.classList.add("tooltip-hidden"); - this.container.style.width = "auto"; - const width = this.container.getBoundingClientRect().width; + // Set either of the tooltip-top or tooltip-bottom styles so that we get an + // accurate height. We're assuming that the two styles will be symmetrical + // and that we will clear this as necessary later. + this.container.classList.add("tooltip-top"); + this.container.classList.remove("tooltip-bottom"); + const { width, height } = this.container.getBoundingClientRect(); this.container.classList.remove("tooltip-hidden"); if (this.useXulWrapper && !this.isVisible()) { xulParent.appendChild(this.container); } - return width; + return { width, height }; }, /**