Bug 1289553 - Move the eyedropper label so it's always visible; r=jdescottes
This adds a few new CSS selectors that are used to move the label to the
top and/or left/right of the eyedropper canvas.
The CSS rules use transform and a quick transition.
The eyedropper highlighter then just makes use of this by adding top, left,
right attributes to the DOM depending on its position.
This also adds a test for this, and while testing, I discovered a bug in
shared/layout/utils.js that I fixed here too. Sometimes, the node passed is
actually a DOCUMENT_NODE and so we must account for this in a couple of places
in this file to avoid JS errors.
MozReview-Commit-ID: H969k3mEDJE
--- a/devtools/client/inspector/test/browser.ini
+++ b/devtools/client/inspector/test/browser.ini
@@ -64,16 +64,17 @@ skip-if = os == "mac" # Full keyboard na
[browser_inspector_highlighter-by-type.js]
[browser_inspector_highlighter-comments.js]
[browser_inspector_highlighter-csstransform_01.js]
[browser_inspector_highlighter-csstransform_02.js]
[browser_inspector_highlighter-embed.js]
[browser_inspector_highlighter-eyedropper-clipboard.js]
subsuite = clipboard
[browser_inspector_highlighter-eyedropper-events.js]
+[browser_inspector_highlighter-eyedropper-label.js]
[browser_inspector_highlighter-eyedropper-show-hide.js]
[browser_inspector_highlighter-geometry_01.js]
[browser_inspector_highlighter-geometry_02.js]
[browser_inspector_highlighter-geometry_03.js]
[browser_inspector_highlighter-geometry_04.js]
[browser_inspector_highlighter-geometry_05.js]
[browser_inspector_highlighter-geometry_06.js]
[browser_inspector_highlighter-hover_01.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-label.js
@@ -0,0 +1,108 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// Test the position of the eyedropper label.
+// It should move around when the eyedropper is close to the viewport height so as to
+// always stay visible.
+
+const HIGHLIGHTER_TYPE = "EyeDropper";
+const ID = "eye-dropper-";
+
+const HTML = `
+<style>
+html, body {height: 100%; margin: 0;}
+body {background: linear-gradient(red, gold); display: flex; justify-content: center;
+ align-items: center;}
+</style>
+Eyedropper label position test
+`;
+const TEST_PAGE = "data:text/html;charset=utf-8," + encodeURI(HTML);
+
+const TEST_DATA = [{
+ desc: "Move the mouse to the center of the screen",
+ getCoordinates: (width, height) => {
+ return {x: width / 2, y: height / 2};
+ },
+ expectedPositions: {top: false, right: false, left: false}
+}, {
+ desc: "Move the mouse to the center left",
+ getCoordinates: (width, height) => {
+ return {x: 0, y: height / 2};
+ },
+ expectedPositions: {top: false, right: true, left: false}
+}, {
+ desc: "Move the mouse to the center right",
+ getCoordinates: (width, height) => {
+ return {x: width, y: height / 2};
+ },
+ expectedPositions: {top: false, right: false, left: true}
+}, {
+ desc: "Move the mouse to the bottom center",
+ getCoordinates: (width, height) => {
+ return {x: width / 2, y: height};
+ },
+ expectedPositions: {top: true, right: false, left: false}
+}, {
+ desc: "Move the mouse to the bottom left",
+ getCoordinates: (width, height) => {
+ return {x: 0, y: height};
+ },
+ expectedPositions: {top: true, right: true, left: false}
+}, {
+ desc: "Move the mouse to the bottom right",
+ getCoordinates: (width, height) => {
+ return {x: width, y: height};
+ },
+ expectedPositions: {top: true, right: false, left: true}
+}, {
+ desc: "Move the mouse to the top left",
+ getCoordinates: (width, height) => {
+ return {x: 0, y: 0};
+ },
+ expectedPositions: {top: false, right: true, left: false}
+}, {
+ desc: "Move the mouse to the top right",
+ getCoordinates: (width, height) => {
+ return {x: width, y: 0};
+ },
+ expectedPositions: {top: false, right: false, left: true}
+}];
+
+add_task(function* () {
+ let {inspector, testActor} = yield openInspectorForURL(TEST_PAGE);
+ let helper = yield getHighlighterHelperFor(HIGHLIGHTER_TYPE)({inspector, testActor});
+ helper.prefix = ID;
+
+ let {mouse, show, hide, finalize} = helper;
+ let {width, height} = yield testActor.getBoundingClientRect("html");
+
+ info("Show the eyedropper on the page");
+ yield show("html");
+
+ info("Move the eyedropper around and check that the label appears at the right place");
+ for (let {desc, getCoordinates, expectedPositions} of TEST_DATA) {
+ info(desc);
+ let {x, y} = getCoordinates(width, height);
+ info(`Moving the mouse to ${x} ${y}`);
+ yield mouse.move(x, y);
+ yield checkLabelPositionAttributes(helper, expectedPositions);
+ }
+
+ info("Hide the eyedropper");
+ yield hide();
+ finalize();
+});
+
+function* checkLabelPositionAttributes(helper, positions) {
+ for (let position in positions) {
+ is((yield hasAttribute(helper, position)), positions[position],
+ `The label was ${positions[position] ? "" : "not "}moved to the ${position}`);
+ }
+}
+
+function* hasAttribute({getElementAttribute}, name) {
+ let value = yield getElementAttribute("root", name);
+ return value !== null;
+}
--- a/devtools/client/shared/test/test-actor.js
+++ b/devtools/client/shared/test/test-actor.js
@@ -538,17 +538,28 @@ var TestActor = exports.TestActor = prot
/**
* Get the bounding rect for a given DOM node once.
* @param {String} selector selector identifier to select the DOM node
* @return {json} the bounding rect info
*/
getBoundingClientRect: function (selector) {
let node = this._querySelector(selector);
- return node.getBoundingClientRect();
+ let rect = node.getBoundingClientRect();
+ // DOMRect can't be stringified directly, so return a simple object instead.
+ return {
+ x: rect.x,
+ y: rect.y,
+ width: rect.width,
+ height: rect.height,
+ top: rect.top,
+ right: rect.right,
+ bottom: rect.bottom,
+ left: rect.left
+ };
},
/**
* Set a JS property on a DOM Node.
* @param {String} selector The node selector
* @param {String} property The property name
* @param {String} value The attribute value
*/
--- a/devtools/server/actors/highlighters.css
+++ b/devtools/server/actors/highlighters.css
@@ -406,16 +406,17 @@
/* Eye dropper */
:-moz-native-anonymous .eye-dropper-root {
--magnifier-width: 96px;
--magnifier-height: 96px;
/* Width accounts for all color formats (hsl being the longest) */
--label-width: 160px;
+ --label-height: 23px;
--color: #e0e0e0;
position: absolute;
/* Tool start position. This should match the X/Y defines in JS */
top: 100px;
left: 100px;
/* Prevent interacting with the page when hovering and clicking */
@@ -441,18 +442,54 @@
box-shadow: 0 0 0 3px var(--color);
display: block;
}
:-moz-native-anonymous .eye-dropper-color-container {
background-color: var(--color);
border-radius: 2px;
width: var(--label-width);
- transform: translateX(calc((var(--magnifier-width) - var(--label-width)) / 2));
+ height: var(--label-height);
position: relative;
+
+ --label-horizontal-center:
+ translateX(calc((var(--magnifier-width) - var(--label-width)) / 2));
+ --label-horizontal-left:
+ translateX(calc((-1 * var(--label-width) + var(--magnifier-width) / 2)));
+ --label-horizontal-right:
+ translateX(calc(var(--magnifier-width) / 2));
+ --label-vertical-top:
+ translateY(calc((-1 * var(--magnifier-height)) - var(--label-height)));
+
+ /* By default the color label container sits below the canvas.
+ Here we just center it horizontally */
+ transform: var(--label-horizontal-center);
+ transition: transform .1s ease-in-out;
+}
+
+/* If there isn't enough space below the canvas, we move the label container to the top */
+:-moz-native-anonymous .eye-dropper-root[top] .eye-dropper-color-container {
+ transform: var(--label-horizontal-center) var(--label-vertical-top);
+}
+
+/* If there isn't enough space right of the canvas to horizontally center the label
+ container, offset it to the left */
+:-moz-native-anonymous .eye-dropper-root[left] .eye-dropper-color-container {
+ transform: var(--label-horizontal-left);
+}
+:-moz-native-anonymous .eye-dropper-root[left][top] .eye-dropper-color-container {
+ transform: var(--label-horizontal-left) var(--label-vertical-top);
+}
+
+/* Same on the other side */
+:-moz-native-anonymous .eye-dropper-root[right] .eye-dropper-color-container {
+ transform: var(--label-horizontal-right);
+}
+:-moz-native-anonymous .eye-dropper-root[right][top] .eye-dropper-color-container {
+ transform: var(--label-horizontal-right) var(--label-vertical-top);
}
:-moz-native-anonymous .eye-dropper-color-preview {
width: 16px;
height: 16px;
position: absolute;
offset-inline-start: 3px;
offset-block-start: 3px;
--- a/devtools/server/actors/highlighters/eye-dropper.js
+++ b/devtools/server/actors/highlighters/eye-dropper.js
@@ -333,17 +333,35 @@ EyeDropper.prototype = {
case "FullZoomChange":
this.hide();
this.show();
break;
}
},
moveTo(x, y) {
- this.getElement("root").setAttribute("style", `top:${y}px;left:${x}px;`);
+ let root = this.getElement("root");
+ root.setAttribute("style", `top:${y}px;left:${x}px;`);
+
+ // Move the label container to the top if the magnifier is close to the bottom edge.
+ if (y >= this.win.innerHeight - MAGNIFIER_HEIGHT) {
+ root.setAttribute("top", "");
+ } else {
+ root.removeAttribute("top");
+ }
+
+ // Also offset the label container to the right or left if the magnifier is close to
+ // the edge.
+ root.removeAttribute("left");
+ root.removeAttribute("right");
+ if (x <= MAGNIFIER_WIDTH) {
+ root.setAttribute("right", "");
+ } else if (x >= this.win.innerWidth - MAGNIFIER_WIDTH) {
+ root.setAttribute("left", "");
+ }
},
/**
* Select the current color that's being previewed. Depending on the current options,
* selecting might mean copying to the clipboard and closing the
*/
selectColor() {
let onColorSelected = Promise.resolve();
--- a/devtools/shared/layout/utils.js
+++ b/devtools/shared/layout/utils.js
@@ -131,17 +131,18 @@ exports.getFrameElement = getFrameElemen
* @param {DOMNode} node
* The node for which we are to get the offset
* @return {Array}
* The frame offset [x, y]
*/
function getFrameOffsets(boundaryWindow, node) {
let xOffset = 0;
let yOffset = 0;
- let frameWin = node.ownerDocument.defaultView;
+
+ let frameWin = getWindowFor(node);
let scale = getCurrentZoom(node);
if (boundaryWindow === null) {
boundaryWindow = getTopWindow(frameWin);
} else if (typeof boundaryWindow === "undefined") {
throw new Error("No boundaryWindow given. Use null for the default one.");
}
@@ -656,23 +657,35 @@ exports.isShadowAnonymous = isShadowAnon
* nsIDOMWindowUtils instance to avoid querying it every time.
*
* @param {DOMNode|DOMWindow}
* The node for which the zoom factor should be calculated, or its
* owner window.
* @return {Number}
*/
function getCurrentZoom(node) {
- let win = null;
-
- if (node instanceof Ci.nsIDOMNode) {
- win = node.ownerDocument.defaultView;
- } else if (node instanceof Ci.nsIDOMWindow) {
- win = node;
- }
+ let win = getWindowFor(node);
if (!win) {
throw new Error("Unable to get the zoom from the given argument.");
}
return utilsFor(win).fullZoom;
}
exports.getCurrentZoom = getCurrentZoom;
+
+/**
+ * Return the default view for a given node, where node can be:
+ * - a DOM node
+ * - the document node
+ * - the window itself
+ */
+function getWindowFor(node) {
+ if (node instanceof Ci.nsIDOMNode) {
+ if (node.nodeType === node.DOCUMENT_NODE) {
+ return node.defaultView;
+ }
+ return node.ownerDocument.defaultView;
+ } else if (node instanceof Ci.nsIDOMWindow) {
+ return node;
+ }
+ return null;
+}