diff --git a/devtools/client/shared/test/browser.ini b/devtools/client/shared/test/browser.ini
index 66cee4e6ba96..a9900190db4e 100644
--- a/devtools/client/shared/test/browser.ini
+++ b/devtools/client/shared/test/browser.ini
@@ -13,6 +13,8 @@ support-files =
doc_html_tooltip.xul
doc_html_tooltip_arrow-01.xul
doc_html_tooltip_arrow-02.xul
+ doc_html_tooltip_doorhanger-01.xul
+ doc_html_tooltip_doorhanger-02.xul
doc_html_tooltip_hover.xul
doc_html_tooltip_rtl.xul
doc_inplace-editor_autocomplete_offset.xul
@@ -138,6 +140,8 @@ 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_doorhanger-01.js]
+[browser_html_tooltip_doorhanger-02.js]
[browser_html_tooltip_height-auto.js]
[browser_html_tooltip_hover.js]
[browser_html_tooltip_offset.js]
diff --git a/devtools/client/shared/test/browser_html_tooltip_doorhanger-01.js b/devtools/client/shared/test/browser_html_tooltip_doorhanger-01.js
new file mode 100644
index 000000000000..23afab2b7f10
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_doorhanger-01.js
@@ -0,0 +1,91 @@
+/* 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 "doorhanger" type's hang direction. It should hang
+ * towards the middle of the screen.
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = CHROME_URL_ROOT + "doc_html_tooltip_doorhanger-01.xul";
+
+const {HTMLTooltip} =
+ require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+let useXulWrapper;
+
+add_task(async function() {
+ // Force the toolbox to be 200px high;
+ await pushPref("devtools.toolbox.footer.height", 200);
+
+ 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) {
+ info("Create HTML tooltip");
+ const tooltip = new HTMLTooltip(doc, {type: "doorhanger", useXulWrapper});
+ const div = doc.createElementNS(HTML_NS, "div");
+ div.style.width = "200px";
+ div.style.height = "35px";
+ tooltip.setContent(div);
+
+ const docBounds = doc.documentElement.getBoundingClientRect();
+
+ const elements = [...doc.querySelectorAll(".anchor")];
+ for (const el of elements) {
+ info("Display the tooltip on an anchor.");
+ await showTooltip(tooltip, el);
+
+ const arrow = tooltip.arrow;
+ ok(arrow, "Tooltip has an arrow");
+
+ // Get the geometry of the anchor, the tooltip panel & arrow.
+ const anchorBounds = el.getBoxQuads({ relativeTo: doc })[0].getBounds();
+ const panelBounds =
+ tooltip.panel.getBoxQuads({ relativeTo: doc })[0].getBounds();
+ const arrowBounds = arrow.getBoxQuads({ relativeTo: doc })[0].getBounds();
+
+ // Work out which side of the view the anchor is on.
+ const center = bounds => bounds.left + bounds.width / 2;
+ const anchorSide =
+ center(anchorBounds) < center(docBounds)
+ ? "left"
+ : "right";
+
+ // Work out which direction the doorhanger hangs.
+ //
+ // We can do that just by checking which edge of the panel the center of the
+ // arrow is closer to.
+ const panelDirection =
+ center(arrowBounds) - panelBounds.left <
+ panelBounds.right - center(arrowBounds)
+ ? "right"
+ : "left";
+
+ const params =
+ `document: ${docBounds.left}<->${docBounds.right}, ` +
+ `anchor: ${anchorBounds.left}<->${anchorBounds.right}, ` +
+ `panel: ${panelBounds.left}<->${panelBounds.right}, ` +
+ `anchor side: ${anchorSide}, ` +
+ `panel direction: ${panelDirection}`;
+ ok(anchorSide !== panelDirection,
+ `Doorhanger hangs towards center (${params})`);
+
+ await hideTooltip(tooltip);
+ }
+
+ tooltip.destroy();
+}
diff --git a/devtools/client/shared/test/browser_html_tooltip_doorhanger-02.js b/devtools/client/shared/test/browser_html_tooltip_doorhanger-02.js
new file mode 100644
index 000000000000..c3addb6d5b1d
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_doorhanger-02.js
@@ -0,0 +1,72 @@
+/* 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 "doorhanger" type's arrow tip is precisely centered on
+ * the anchor when the anchor is small.
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = CHROME_URL_ROOT + "doc_html_tooltip_doorhanger-02.xul";
+
+const {HTMLTooltip} =
+ require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+let useXulWrapper;
+
+add_task(async function() {
+ // Force the toolbox to be 200px high;
+ await pushPref("devtools.toolbox.footer.height", 200);
+
+ 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) {
+ info("Create HTML tooltip");
+ const tooltip = new HTMLTooltip(doc, {type: "doorhanger", useXulWrapper});
+ const div = doc.createElementNS(HTML_NS, "div");
+ div.style.width = "200px";
+ div.style.height = "35px";
+ tooltip.setContent(div);
+
+ const elements = [...doc.querySelectorAll(".anchor")];
+ for (const el of elements) {
+ info("Display the tooltip on an anchor.");
+ await showTooltip(tooltip, el);
+
+ const arrow = tooltip.arrow;
+ ok(arrow, "Tooltip has an arrow");
+
+ // Get the geometry of the anchor and arrow.
+ const anchorBounds = el.getBoxQuads({ relativeTo: doc })[0].getBounds();
+ const arrowBounds = arrow.getBoxQuads({ relativeTo: doc })[0].getBounds();
+
+ // Compare the centers
+ const center = bounds => bounds.left + bounds.width / 2;
+ const delta = Math.abs(center(anchorBounds) - center(arrowBounds));
+ const describeBounds = bounds =>
+ `${bounds.left}<--[${center(bounds)}]-->${bounds.right}`;
+ const params =
+ `anchor: ${describeBounds(anchorBounds)}, ` +
+ `arrow: ${describeBounds(arrowBounds)}`;
+ ok(delta < 1,
+ `Arrow center is roughly aligned with anchor center (${params})`);
+
+ await hideTooltip(tooltip);
+ }
+
+ tooltip.destroy();
+}
diff --git a/devtools/client/shared/test/doc_html_tooltip_doorhanger-01.xul b/devtools/client/shared/test/doc_html_tooltip_doorhanger-01.xul
new file mode 100644
index 000000000000..2759a0b5a761
--- /dev/null
+++ b/devtools/client/shared/test/doc_html_tooltip_doorhanger-01.xul
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/devtools/client/shared/test/doc_html_tooltip_doorhanger-02.xul b/devtools/client/shared/test/doc_html_tooltip_doorhanger-02.xul
new file mode 100644
index 000000000000..d76fe6775c7d
--- /dev/null
+++ b/devtools/client/shared/test/doc_html_tooltip_doorhanger-02.xul
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/devtools/client/shared/widgets/tooltip/HTMLTooltip.js b/devtools/client/shared/widgets/tooltip/HTMLTooltip.js
index d08a215218f4..ad80d42469d3 100644
--- a/devtools/client/shared/widgets/tooltip/HTMLTooltip.js
+++ b/devtools/client/shared/widgets/tooltip/HTMLTooltip.js
@@ -24,24 +24,41 @@ module.exports.POSITION = POSITION;
const TYPE = {
NORMAL: "normal",
ARROW: "arrow",
+ DOORHANGER: "doorhanger",
};
module.exports.TYPE = TYPE;
-const ARROW_WIDTH = 32;
+const ARROW_WIDTH = {
+ "normal": 0,
+ "arrow": 32,
+ // This is the value calculated for the .tooltip-arrow element in tooltip.css
+ // which includes the arrow width (20px) plus the extra margin added so that
+ // the drop shadow is not cropped (2px each side).
+ "doorhanger": 24,
+};
-// Default offset between the tooltip's left edge and the tooltip arrow.
-const ARROW_OFFSET = 20;
+const ARROW_OFFSET = {
+ "normal": 0,
+ // Default offset between the tooltip's edge and the tooltip arrow.
+ "arrow": 20,
+ // Match other Firefox menus which use 10px from edge (but subtract the 2px
+ // margin included in the ARROW_WIDTH above).
+ "doorhanger": 8,
+};
const EXTRA_HEIGHT = {
"normal": 0,
// The arrow is 16px tall, but merges on 3px with the panel border
"arrow": 13,
+ // The doorhanger arrow is 10px tall, but merges on 1px with the panel border
+ "doorhanger": 9,
};
const EXTRA_BORDER = {
"normal": 0,
"arrow": 3,
+ "doorhanger": 0,
};
/**
@@ -120,12 +137,20 @@ const calculateVerticalPosition = (
* 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 {DOMRect} windowRect
+ * Bounding rectangle for the window. Used to determine which direction
+ * doorhangers should hang.
* @param {Number} width
* Preferred width for the tooltip.
* @param {String} type
* The tooltip type (e.g. "arrow").
* @param {Number} offset
* Horizontal offset in pixels.
+ * @param {Number} borderRadius
+ * The border radius of the panel. This is added to ARROW_OFFSET to
+ * calculate the distance from the edge of the tooltip to the start
+ * of arrow. It is separate from ARROW_OFFSET since it will vary by
+ * platform.
* @param {Boolean} isRtl
* If the anchor is in RTL, the tooltip should be aligned to the right.
* @return {Object}
@@ -136,13 +161,38 @@ const calculateVerticalPosition = (
const calculateHorizontalPosition = (
anchorRect,
viewportRect,
+ windowRect,
width,
type,
offset,
+ borderRadius,
isRtl
) => {
// Which direction should the tooltip go?
- const hangDirection = isRtl ? "left" : "right";
+ //
+ // For tooltips we follow the writing direction but for doorhangers the
+ // guidelines[1] say that,
+ //
+ // "Doorhangers opening on the right side of the view show the directional
+ // arrow on the right.
+ //
+ // Doorhangers opening on the left side of the view show the directional
+ // arrow on the left.
+ //
+ // Never place the directional arrow at the center of doorhangers."
+ //
+ // [1] https://design.firefox.com/photon/components/doorhangers.html#directional-arrow
+ //
+ // So for those we need to check if the anchor is more right or left.
+ let hangDirection;
+ if (type === TYPE.DOORHANGER) {
+ const anchorCenter = anchorRect.left + anchorRect.width / 2;
+ const viewCenter = windowRect.left + windowRect.width / 2;
+ hangDirection = anchorCenter >= viewCenter ? "left" : "right";
+ } else {
+ hangDirection = isRtl ? "left" : "right";
+ }
+
const anchorWidth = anchorRect.width;
// Calculate logical start of anchor relative to the viewport.
@@ -160,12 +210,14 @@ const calculateHorizontalPosition = (
tooltipStart = Math.max(0, tooltipStart);
// Calculate arrow start (tooltip's start might be updated)
- const arrowWidth = type === TYPE.ARROW ? ARROW_WIDTH : 0;
+ const arrowWidth = ARROW_WIDTH[type];
let arrowStart;
- // Arrow style tooltips may need to be shifted
- if (type === TYPE.ARROW) {
+ // Arrow and doorhanger style tooltips may need to be shifted
+ if (type === TYPE.ARROW || type === TYPE.DOORHANGER) {
+ const arrowOffset = ARROW_OFFSET[type] + borderRadius;
+
// Where will the point of the arrow be if we apply the standard offset?
- const arrowCenter = tooltipStart + ARROW_OFFSET + arrowWidth / 2;
+ const arrowCenter = tooltipStart + arrowOffset + arrowWidth / 2;
// How does that compare to the center of the anchor?
const anchorCenter = anchorStart + anchorWidth / 2;
@@ -175,12 +227,12 @@ const calculateHorizontalPosition = (
tooltipStart = Math.max(0, tooltipStart - (arrowCenter - anchorCenter));
}
// Arrow's start offset relative to the anchor.
- arrowStart = Math.min(ARROW_OFFSET, (anchorWidth - arrowWidth) / 2) | 0;
+ arrowStart = Math.min(arrowOffset, (anchorWidth - arrowWidth) / 2) | 0;
// Translate the coordinate to tooltip container
arrowStart += anchorStart - tooltipStart;
// Make sure the arrow remains in the tooltip container.
- arrowStart = Math.min(arrowStart, tooltipWidth - arrowWidth);
- arrowStart = Math.max(arrowStart, 0);
+ arrowStart = Math.min(arrowStart, tooltipWidth - arrowWidth - borderRadius);
+ arrowStart = Math.max(arrowStart, borderRadius);
}
// Convert from logical coordinates to physical
@@ -232,7 +284,8 @@ const getRelativeRect = function(node, relativeTo) {
* The toolbox document to attach the HTMLTooltip popup.
* @param {Object}
* - {String} type
- * Display type of the tooltip. Possible values: "normal", "arrow"
+ * Display type of the tooltip. Possible values: "normal", "arrow", and
+ * "doorhanger".
* - {Boolean} autofocus
* Defaults to false. Should the tooltip be focused when opening it.
* - {Boolean} consumeOutsideClicks
@@ -375,8 +428,7 @@ HTMLTooltip.prototype = {
anchorRect = this._convertToScreenRect(anchorRect);
}
- // Get viewport size
- const viewportRect = this._getViewportRect();
+ const { viewportRect, windowRect } = this._getBoundingRects();
// Calculate the horizonal position and width
let preferredWidth;
@@ -399,9 +451,29 @@ HTMLTooltip.prototype = {
}
const anchorWin = anchor.ownerDocument.defaultView;
- const isRtl = anchorWin.getComputedStyle(anchor).direction === "rtl";
+ const anchorCS = anchorWin.getComputedStyle(anchor);
+ const isRtl = anchorCS.direction === "rtl";
+
+ let borderRadius = 0;
+ if (this.type === TYPE.DOORHANGER) {
+ borderRadius = parseFloat(
+ anchorCS.getPropertyValue("--theme-arrowpanel-border-radius")
+ );
+ if (Number.isNaN(borderRadius)) {
+ borderRadius = 0;
+ }
+ }
+
const {left, width, arrowLeft} = calculateHorizontalPosition(
- anchorRect, viewportRect, preferredWidth, this.type, x, isRtl);
+ anchorRect,
+ viewportRect,
+ windowRect,
+ preferredWidth,
+ this.type,
+ x,
+ borderRadius,
+ isRtl
+ );
// If we constrained the width, then any measured height we have is no
// longer valid.
@@ -411,10 +483,22 @@ HTMLTooltip.prototype = {
// Apply width and arrow positioning
this.container.style.width = width + "px";
- if (this.type === TYPE.ARROW) {
+ if (this.type === TYPE.ARROW || this.type === TYPE.DOORHANGER) {
this.arrow.style.left = arrowLeft + "px";
}
+ // Work out how much vertical margin we have.
+ //
+ // This relies on us having set either .tooltip-top or .tooltip-bottom
+ // and on the margins for both being symmetrical. Fortunately the call to
+ // _measureContainerSize above will set .tooltip-top for us and it also
+ // assumes these styles are symmetrical so this should be ok.
+ const panelWindow = this.panel.ownerDocument.defaultView;
+ const panelComputedStyle = panelWindow.getComputedStyle(this.panel);
+ const verticalMargin =
+ parseFloat(panelComputedStyle.marginTop) +
+ parseFloat(panelComputedStyle.marginBottom);
+
// Calculate the vertical position and height
let preferredHeight;
if (this.preferredHeight === "auto") {
@@ -424,8 +508,12 @@ HTMLTooltip.prototype = {
} else {
({ height: preferredHeight } = this._measureContainerSize());
}
+ preferredHeight += verticalMargin;
} else {
- const themeHeight = EXTRA_HEIGHT[this.type] + 2 * EXTRA_BORDER[this.type];
+ const themeHeight =
+ EXTRA_HEIGHT[this.type] +
+ verticalMargin +
+ 2 * EXTRA_BORDER[this.type];
preferredHeight = this.preferredHeight + themeHeight;
}
@@ -467,22 +555,47 @@ HTMLTooltip.prototype = {
},
/**
- * 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.
+ * Calculate the following boundary rectangles:
*
- * @return {Object} DOMRect-like object with the Number properties: top, right, bottom,
- * left, width, height
+ * - Viewport rect: This is the region that limits the tooltip dimensions.
+ * When using a XUL panel wrapper, the tooltip will be able to use the whole
+ * screen (excluding space reserved by the OS for toolbars etc.) and hence
+ * the result will be in screen coordinates.
+ * Otherwise, the tooltip is limited to the tooltip's document.
+ *
+ * - Window rect: This is the bounds of the view in which the tooltip is
+ * presented. It is reported in the same coordinates as the viewport
+ * rect and is used for determining in which direction a doorhanger-type
+ * tooltip should "hang".
+ * When using the XUL panel wrapper this will be the dimensions of the
+ * window in screen coordinates. Otherwise it will be the same as the
+ * viewport rect.
+ *
+ * @return {Object} An object with the following properties
+ * viewportRect {Object} DOMRect-like object with the Number
+ * properties: top, right, bottom, left, width, height
+ * representing the viewport rect.
+ * windowRect {Object} DOMRect-like object with the Number
+ * properties: top, right, bottom, left, width, height
+ * representing the viewport rect.
*/
- _getViewportRect: function() {
+ _getBoundingRects: function() {
+ let viewportRect;
+ let windowRect;
+
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
- const {availLeft, availTop, availHeight, availWidth} = this.doc.defaultView.screen;
- return {
+ // 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
+ const {
+ availLeft,
+ availTop,
+ availHeight,
+ availWidth,
+ } = this.doc.defaultView.screen;
+ viewportRect = {
top: availTop,
right: availLeft + availWidth,
bottom: availTop + availHeight,
@@ -490,9 +603,27 @@ HTMLTooltip.prototype = {
width: availWidth,
height: availHeight,
};
+
+ const {
+ screenX,
+ screenY,
+ outerWidth,
+ outerHeight,
+ } = this.doc.defaultView;
+ windowRect = {
+ top: screenY,
+ right: screenX + outerWidth,
+ bottom: screenY + outerHeight,
+ left: screenX,
+ width: outerWidth,
+ height: outerHeight,
+ };
+ } else {
+ viewportRect = windowRect =
+ this.doc.documentElement.getBoundingClientRect();
}
- return this.doc.documentElement.getBoundingClientRect();
+ return { viewportRect, windowRect };
},
_measureContainerSize: function() {
@@ -573,7 +704,7 @@ HTMLTooltip.prototype = {
let html = '
';
html += '';
- if (this.type === TYPE.ARROW) {
+ if (this.type === TYPE.ARROW || this.type === TYPE.DOORHANGER) {
html += '';
}
// eslint-disable-next-line no-unsanitized/property
diff --git a/devtools/client/themes/tooltips.css b/devtools/client/themes/tooltips.css
index e9abb27d4a06..7c5596512a84 100644
--- a/devtools/client/themes/tooltips.css
+++ b/devtools/client/themes/tooltips.css
@@ -262,6 +262,212 @@
transform: rotate(45deg);
}
+/* Tooltip : doorhanger style */
+
+:root {
+ --theme-arrowpanel-border-radius: 0px;
+}
+:root[platform="mac"] {
+ --theme-arrowpanel-border-radius: 3.5px;
+}
+
+.tooltip-container[type="doorhanger"] > .tooltip-panel {
+ padding: 4px 0;
+ color: var(--theme-arrowpanel-color);
+ margin: 4px;
+ max-width: 320px;
+}
+
+.tooltip-container[type="doorhanger"] > .tooltip-panel,
+.tooltip-container[type="doorhanger"] > .tooltip-arrow::before {
+ background: var(--theme-arrowpanel-background);
+ border: 1px solid var(--theme-arrowpanel-border-color);
+ border-radius: var(--theme-arrowpanel-border-radius);
+ box-shadow: 0 0 4px hsla(210,4%,10%,.2);
+}
+
+:root[platform="mac"] .tooltip-container[type="doorhanger"] > .tooltip-panel,
+:root[platform="mac"] .tooltip-container[type="doorhanger"] > .tooltip-arrow::before {
+ box-shadow: none;
+ /*
+ * The above should be:
+ *
+ * box-shadow: 0 0 0 1px var(--theme-arrowpanel-border-color);
+ *
+ * but although that gives the right emphasis to the border it makes the
+ * platform shadow much too dark.
+ */
+}
+
+:root[platform="mac"].theme-light .tooltip-container[type="doorhanger"] > .tooltip-panel,
+:root[platform="mac"].theme-light .tooltip-container[type="doorhanger"] > .tooltip-arrow::before {
+ border: none;
+}
+
+.tooltip-container[type="doorhanger"] > .tooltip-arrow {
+ /* Desired width of the arrow */
+ --arrow-width: 20px;
+
+ /* Amount of room to allow for the shadow. Should be about half the radius. */
+ --shadow-radius: 4px;
+ --shadow-margin: calc(var(--shadow-radius) / 2);
+
+ /*
+ * Crop the arrow region to show half the arrow plus allow room for margins.
+ *
+ * The ARROW_WIDTH in HTMLTooltip.js needs to match the following value.
+ */
+ width: calc(var(--arrow-width) + 2 * var(--shadow-margin));
+ height: calc(var(--arrow-width) / 2 + var(--shadow-margin));
+}
+
+.tooltip-container[type="doorhanger"] > .tooltip-arrow::before {
+ /* Make sure the border is included in the size */
+ box-sizing: border-box;
+
+ /* Don't inherit any rounded corners. */
+ border-radius: 0;
+
+ /*
+ * When the box is rotated, it should have width .
+ * That makes the length of one side of the box equal to:
+ *
+ * ( / 2) / sin 45
+ */
+ --sin-45: 0.707106781;
+ --square-side: calc(var(--arrow-width) / 2 / var(--sin-45));
+ width: var(--square-side);
+ height: var(--square-side);
+
+ /*
+ * The rotated square will overshoot the left side
+ * and need to be shifted in by:
+ *
+ * ( - square side) / 2
+ *
+ * But we also want to shift it in so that the box-shadow
+ * is not clipped when we clip the parent so we add
+ * a suitable margin for that.
+ */
+ --overhang: calc((var(--arrow-width) - var(--square-side)) / 2);
+ margin-left: calc(var(--overhang) + var(--shadow-margin));
+}
+
+.tooltip-container[type="doorhanger"].tooltip-top > .tooltip-panel {
+ /* Drop the margin between the doorhanger and the arrow. */
+ margin-bottom: 0;
+}
+
+.tooltip-container[type="doorhanger"].tooltip-bottom > .tooltip-panel {
+ /* Drop the margin between the doorhanger and the arrow. */
+ margin-top: 0;
+}
+
+.tooltip-container[type="doorhanger"].tooltip-top > .tooltip-arrow {
+ /* Overlap the arrow with the 1px border of the doorhanger */
+ margin-top: -1px;
+}
+
+.tooltip-container[type="doorhanger"].tooltip-bottom > .tooltip-arrow {
+ /* Overlap the arrow with the 1px border of the doorhanger */
+ margin-bottom: -1px;
+}
+
+.tooltip-container[type="doorhanger"].tooltip-top > .tooltip-arrow::before {
+ /* Show only the bottom half of the box */
+ margin-top: calc(var(--square-side) / -2);
+}
+
+.tooltip-container[type="doorhanger"].tooltip-bottom > .tooltip-arrow::before {
+ /* Shift the rotated box in so that it is not clipped */
+ margin-top: calc(var(--overhang) + var(--shadow-margin));
+}
+
+.tooltip-container[type="doorhanger"] .tooltip-panel ul {
+ /* Override the display: -moz-box declaration in minimal-xul.css
+ * or else menu items won't stack. */
+ display: block;
+}
+
+.tooltip-container[type="doorhanger"] .menuitem > .command {
+ display: flex;
+ align-items: baseline;
+ margin: 0;
+ padding: 4px 12px;
+ outline: none;
+}
+
+.tooltip-container[type="doorhanger"] .menuitem > button.command:-moz-any([role="menuitem"],[role="menuitemcheckbox"]) {
+ -moz-appearance: none;
+ border: none;
+ color: var(--theme-arrowpanel-color);
+ background-color: transparent;
+ text-align: start;
+ width: 100%;
+}
+
+.tooltip-container[type="doorhanger"] .menuitem > .command:not(:-moz-any([disabled],[open],:active)):-moz-any(:hover,:focus) {
+ background-color: var(--theme-arrowpanel-dimmed);
+}
+
+.tooltip-container[type="doorhanger"] .menuitem > .command:-moz-focusring::-moz-focus-inner {
+ border-color: transparent;
+}
+
+.tooltip-container[type="doorhanger"] .menuitem > .command:not([disabled]):-moz-any([open],:hover:active) {
+ background-color: var(--theme-arrowpanel-dimmed-further);
+ box-shadow: 0 1px 0 hsla(210,4%,10%,.03) inset;
+}
+
+.tooltip-container[type="doorhanger"] .menuitem > .command[aria-checked="true"] {
+ list-style-image: none;
+ -moz-context-properties: fill;
+ fill: currentColor;
+ background: url(chrome://browser/skin/check.svg) no-repeat transparent;
+ background-size: 11px 11px;
+ background-position: center left 7px;
+}
+
+.tooltip-container[type="doorhanger"] .menuitem > .command[aria-checked="true"]:-moz-locale-dir(rtl) {
+ background-position: center right 7px;
+}
+
+.tooltip-container[type="doorhanger"] .menuitem > .command > .label {
+ flex: 1;
+ padding-inline-start: 16px;
+ font: menu;
+}
+
+.tooltip-container[type="doorhanger"] .menuitem > .command.iconic > .label::before {
+ content: " ";
+ display: inline-block;
+ margin-inline-end: 8px;
+ width: 16px;
+ height: 16px;
+ vertical-align: top;
+ -moz-context-properties: fill;
+ fill: currentColor;
+ /*
+ * The icons in the sidebar menu have opacity: 0.8 here, but those in the
+ * hamburger menu don't. For now we match the hamburger menu styling,
+ * especially because the 80% opacity makes the icons look dull in dark mode.
+ */
+}
+
+.tooltip-container[type="doorhanger"] .menuitem > .command > .accelerator {
+ margin-inline-start: 10px;
+ color: var(--theme-arrowpanel-disabled-color);
+ font: message-box;
+}
+
+.tooltip-container[type="doorhanger"] hr {
+ display: block;
+ border: none;
+ border-top: 1px solid var(--theme-arrowpanel-separator);
+ margin: 6px 0;
+ padding: 0;
+}
+
/* Tooltip: Events */
.event-header {
diff --git a/devtools/client/themes/variables.css b/devtools/client/themes/variables.css
index 0a8d621eceb1..bc29f08813f1 100644
--- a/devtools/client/themes/variables.css
+++ b/devtools/client/themes/variables.css
@@ -93,6 +93,17 @@
--theme-tooltip-background: rgba(255, 255, 255, .9);
--theme-tooltip-shadow: rgba(155, 155, 155, 0.26);
+ /* Doorhangers */
+ /* These colors are based on the colors used for doorhangers elsewhere in
+ * Firefox. */
+ --theme-arrowpanel-background: white;
+ --theme-arrowpanel-color: -moz-fieldText;
+ --theme-arrowpanel-border-color: var(--grey-90-a20);
+ --theme-arrowpanel-separator: var(--grey-90-a20);
+ --theme-arrowpanel-dimmed: hsla(0,0%,80%,.3);
+ --theme-arrowpanel-dimmed-further: hsla(0,0%,80%,.45);
+ --theme-arrowpanel-disabled-color: GrayText;
+
/* Command line */
--theme-command-line-image: url(chrome://devtools/skin/images/commandline-icon.svg#light-theme);
--theme-command-line-image-focus: url(chrome://devtools/skin/images/commandline-icon.svg#light-theme-focus);
@@ -101,6 +112,16 @@
--theme-messageCloseButtonFilter: invert(0);
}
+/*
+ * For doorhangers elsewhere in Fireflox, Mac uses a fixed color different to
+ * -moz-fieldText and a slightly lighter border color (presumably since it
+ * combines with the platform shadow).
+ */
+:root[platform="mac"].theme-light {
+ --theme-arrowpanel-color: rgb(26,26,26);
+ --theme-arrowpanel-border-color: hsla(210,4%,10%,.05);
+}
+
:root.theme-dark {
--theme-body-background: var(--grey-80);
--theme-sidebar-background: #1B1B1D;
@@ -180,6 +201,17 @@
--theme-tooltip-background: rgba(19, 28, 38, .9);
--theme-tooltip-shadow: rgba(25, 25, 25, 0.76);
+ /* Doorhangers */
+ /* These colors are based on the colors used for doorhangers elsewhere in
+ * Firefox. */
+ --theme-arrowpanel-background: var(--grey-60);
+ --theme-arrowpanel-color: rgb(249,249,250);
+ --theme-arrowpanel-border-color: #27272b;
+ --theme-arrowpanel-separator: rgba(249,249,250,.1);
+ --theme-arrowpanel-dimmed: rgba(249,249,250,.1);
+ --theme-arrowpanel-dimmed-further: rgba(249,249,250,.15);
+ --theme-arrowpanel-disabled-color: rgba(249,249,250,.5);
+
/* Command line */
--theme-command-line-image: url(chrome://devtools/skin/images/commandline-icon.svg#dark-theme);
--theme-command-line-image-focus: url(chrome://devtools/skin/images/commandline-icon.svg#dark-theme-focus);
@@ -252,5 +284,6 @@
--grey-80: #2a2a2e;
--grey-90: #0c0c0d;
--grey-90-a10: rgba(12, 12, 13, 0.1);
+ --grey-90-a20: rgba(12, 12, 13, 0.2);
--grey-90-a80: rgba(12, 12, 13, 0.8);
}