Bug 1306054 - Use custom tooltip for inactive properties r=jdescottes,flod,rcaliman

### Changes

Probably the most important change apart from the tooltips is that we now only support one property at a time. This allows us to short circuit at the first invalid property and improve performance. This was previously agreed with Razvan but there were some relics left in the code.

`toolbox.xul`
- Added tooltips.ftl

`devtools/client/inspector/markup/test/helper_events_test_runner.js`:
- Had to change to synthesizeMouseAtCenter because CSS changes caused the original to fail.

`devtools/client/inspector/rules/rules.js`:
- Added `VIEW_NODE_INACTIVE_CSS` to node types and sorted alphabetically.
- Added new nodeInfo data for Inactive CSS icons.

`devtools/client/inspector/rules/test/browser_rules_inactive_css_flexbox.js` &
`devtools/client/inspector/rules/test/browser_rules_inactive_css_grid.js`:
- removed some listeners that are no longer needed

`devtools/client/inspector/rules/test/head.js`:
- Refactored `getPropertiesForRuleIndex()` in order to pass along information needed for testing our Fluent strings.
- Refactored `checkDeclarationIsInactive()` to check tooltip contnts using a new method.
- Added `checkInteractiveTooltip()` for checking the tooltip contents themselves.
- Simple changes to `runInactiveCSSTests()`.

`devtools/client/inspector/rules/views/text-property-editor.js`:
- We no longer create the tooltip by adding the title attribute.

`devtools/client/inspector/shared/node-types.js`:
- Changed the enum to use strings to simplify debugging.
- Added `VIEW_NODE_INACTIVE_CSS`.
- Sorted alphabetically.

`devtools/client/inspector/shared/tooltips-overlay.js`:
- Introduced a new tooltip type called `interactiveTooltip`.

`devtools/client/locales/en-US/inspector.properties`:
- Removed strings.

`devtools/client/locales/en-US/tooltips.ftl`:
- Added structured versions of the properties from `inspector.properties`.

`devtools/client/shared/widgets/tooltip/HTMLTooltip.js`:
- Made the tooltips obey the "prevent popup autohide" option in the browser debugger.

`devtools/client/shared/widgets/tooltip/InactiveCSSTooltipHelper.js`:
- Main file for handling InactiveCSS Tooltips.

`devtools/client/themes/tooltips.css`:
- Made arrow tooltips follow the Proton theme.

`devtools/server/actors/utils/inactive-property-helper.js`:
- General changes to support Fluent.
- Bail on first inactive property found.

### Latest Try (expecting green)

https://treeherder.mozilla.org/#/jobs?repo=try&revision=de28939206d444dc4b534a3e5cc7a84b8797bec3

Differential Revision: https://phabricator.services.mozilla.com/D29372

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Michael Ratcliffe 2019-05-10 17:03:27 +00:00
Родитель 31429a113a
Коммит a58a67412c
17 изменённых файлов: 444 добавлений и 145 удалений

Просмотреть файл

@ -21,6 +21,8 @@ add_task(async function() {
// available in the toolbox top window.
toolbox.topWindow.MozXULElement.insertFTLIfNeeded("toolkit/main-window/editmenu.ftl");
loadFTL(toolbox, "toolkit/main-window/editmenu.ftl");
await testMenuItems();
await testMenuPopup(toolbox);
await testSubmenu(toolbox);

Просмотреть файл

@ -427,3 +427,19 @@ function setupPreferencesForBrowserToolbox() {
return SpecialPowers.pushPrefEnv(options);
}
/**
* Load FTL.
*
* @param {Toolbox} toolbox
* Toolbox instance.
* @param {String} path
* Path to the FTL file.
*/
function loadFTL(toolbox, path) {
const win = toolbox.doc.ownerGlobal;
if (win.MozXULElement) {
win.MozXULElement.insertFTLIfNeeded(path);
}
}

Просмотреть файл

@ -16,6 +16,9 @@
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:html="http://www.w3.org/1999/xhtml">
<linkset>
<html:link rel="localization" href="devtools/tooltips.ftl"/>
</linkset>
<html:link href="chrome://browser/skin/window.svg" rel="shortcut icon"/>
<script src="chrome://devtools/content/shared/theme-switching.js"/>

Просмотреть файл

@ -16,15 +16,16 @@ const {PrefObserver} = require("devtools/client/shared/prefs");
const ElementStyle = require("devtools/client/inspector/rules/models/element-style");
const RuleEditor = require("devtools/client/inspector/rules/views/rule-editor");
const {
VIEW_NODE_SELECTOR_TYPE,
VIEW_NODE_PROPERTY_TYPE,
VIEW_NODE_VALUE_TYPE,
VIEW_NODE_FONT_TYPE,
VIEW_NODE_IMAGE_URL_TYPE,
VIEW_NODE_INACTIVE_CSS,
VIEW_NODE_LOCATION_TYPE,
VIEW_NODE_PROPERTY_TYPE,
VIEW_NODE_SELECTOR_TYPE,
VIEW_NODE_SHAPE_POINT_TYPE,
VIEW_NODE_SHAPE_SWATCH,
VIEW_NODE_VALUE_TYPE,
VIEW_NODE_VARIABLE_TYPE,
VIEW_NODE_FONT_TYPE,
} = require("devtools/client/inspector/shared/node-types");
const TooltipsOverlay = require("devtools/client/inspector/shared/tooltips-overlay");
const {createChild, promiseWarn} = require("devtools/client/inspector/shared/utils");
@ -425,6 +426,9 @@ CssRuleView.prototype = {
toggleActive: getShapeToggleActive(node),
point: getShapePoint(node),
};
} else if (classes.contains("ruleview-unused-warning") && prop) {
type = VIEW_NODE_INACTIVE_CSS;
value = prop.isUsed();
} else if (classes.contains("ruleview-shapeswatch") && prop) {
type = VIEW_NODE_SHAPE_SWATCH;
value = {

Просмотреть файл

@ -44,7 +44,6 @@ const BEFORE = [
selector: "#self-aligned",
inactiveDeclarations: [
{
l10n: "rule.inactive.css.not.grid.or.flex.item",
declaration: {
"align-self": "stretch",
},
@ -72,7 +71,6 @@ const BEFORE = [
],
inactiveDeclarations: [
{
l10n: "rule.inactive.css.not.flex.container",
declaration: {
"flex-direction": "row",
},
@ -101,14 +99,12 @@ const BEFORE = [
],
inactiveDeclarations: [
{
l10n: "rule.inactive.css.not.flex.item",
declaration: {
"order": "1",
},
ruleIndex: 1,
},
],
waitFor: "inspector-updated",
},
];
@ -117,35 +113,30 @@ const AFTER = [
selector: ".item-2",
inactiveDeclarations: [
{
l10n: "rule.inactive.css.not.flex.item",
declaration: {
"order": "2",
},
ruleIndex: 0,
},
{
l10n: "rule.inactive.css.not.flex.item",
declaration: {
"flex-basis": "auto",
},
ruleIndex: 1,
},
{
l10n: "rule.inactive.css.not.flex.item",
declaration: {
"flex-grow": "1",
},
ruleIndex: 1,
},
{
l10n: "rule.inactive.css.not.flex.item",
declaration: {
"flex-shrink": "1",
},
ruleIndex: 1,
},
{
l10n: "rule.inactive.css.not.flex.container",
declaration: {
"flex-direction": "row",
},
@ -165,6 +156,5 @@ add_task(async function() {
await toggleDeclaration(inspector, view, 0, {
display: "flex",
});
await view.once("ruleview-refreshed");
await runInactiveCSSTests(view, inspector, AFTER);
});

Просмотреть файл

@ -47,7 +47,6 @@ const BEFORE = [
selector: "#self-aligned",
inactiveDeclarations: [
{
l10n: "rule.inactive.css.not.grid.or.flex.item",
declaration: {
"align-self": "stretch",
},
@ -70,7 +69,6 @@ const BEFORE = [
],
inactiveDeclarations: [
{
l10n: "rule.inactive.css.not.flex.container",
declaration: {
"flex-direction": "row",
},
@ -100,14 +98,12 @@ const BEFORE = [
],
inactiveDeclarations: [
{
l10n: "rule.inactive.css.not.grid.or.flex.item",
declaration: {
"align-self": "start",
},
ruleIndex: 1,
},
],
waitFor: "inspector-updated",
},
];
@ -131,21 +127,18 @@ const AFTER = [
],
inactiveDeclarations: [
{
l10n: "rule.inactive.css.not.grid.container",
declaration: {
"column-gap": "10px",
},
ruleIndex: 1,
},
{
l10n: "rule.inactive.css.not.grid.container",
declaration: {
"row-gap": "10px",
},
ruleIndex: 1,
},
{
l10n: "rule.inactive.css.not.grid.or.flex.item",
declaration: {
"align-self": "start",
},

Просмотреть файл

@ -622,7 +622,7 @@ async function openEyedropper(view, swatch) {
* {
* propertyName: "color",
* propertyValue: "red",
* warnings: "This won't work",
* warning: "This won't work",
* used: true,
* }
* },
@ -635,12 +635,15 @@ function getPropertiesForRuleIndex(view, ruleIndex) {
for (const currProp of ruleEditor.rule.textProps) {
const icon = currProp.editor.unusedState;
const unused = currProp.editor.element.classList.contains("unused");
declaration.set(`${currProp.name}:${currProp.value}`, {
propertyName: currProp.name,
propertyValue: currProp.value,
warnings: icon.title ? icon.title.split("\n") : [],
used: !currProp.editor.element.classList.contains("unused"),
icon: icon,
data: currProp.isUsed(),
warning: unused,
used: !unused,
});
}
@ -679,6 +682,7 @@ async function toggleDeclaration(inspector, view, ruleIndex, declaration) {
info(`Toggling declaration "${dec}" of rule ${ruleIndex} to ${newStatus}`);
await togglePropStatus(view, textProp);
info("Toggled successfully.");
}
/**
@ -691,20 +695,17 @@ async function toggleDeclaration(inspector, view, ruleIndex, declaration) {
* The index we expect the rule to have in the rule-view.
* @param {Object} declaration
* An object representing the declaration e.g. { color: "red" }.
* @param {String} warningL10nString
* l10n string representing an expected warning.
*/
function checkDeclarationIsInactive(view, ruleIndex, declaration, warningL10nString) {
async function checkDeclarationIsInactive(view, ruleIndex, declaration) {
const declarations = getPropertiesForRuleIndex(view, ruleIndex);
const [[ name, value ]] = Object.entries(declaration);
const dec = `${name}:${value}`;
const { used, warnings } = declarations.get(dec);
const { used, warning } = declarations.get(dec);
ok(!used, `"${dec}" is inactive`);
is(warnings.length, 1, `"${dec}" has a warning`);
ok(warning, `"${dec}" has a warning`);
const warning = INSPECTOR_L10N.getFormatStr(warningL10nString, name);
is(warnings[0], warning, `The warning on "${dec}" is correct`);
await checkInteractiveTooltip(view, ruleIndex, declaration);
}
/**
@ -721,10 +722,59 @@ function checkDeclarationIsActive(view, ruleIndex, declaration) {
const declarations = getPropertiesForRuleIndex(view, ruleIndex);
const [[ name, value ]] = Object.entries(declaration);
const dec = `${name}:${value}`;
const { used, warnings } = declarations.get(dec);
const { used, warning } = declarations.get(dec);
ok(used, `${dec} is active`);
is(warnings.length, 0, `${dec} has no warnings`);
ok(!warning, `${dec} has no warning`);
}
/**
* Check that a tooltip contains the correct value.
*
* @param {ruleView} view
* The rule-view instance.
* @param {Number} ruleIndex
* The index we expect the rule to have in the rule-view.
* @param {Object} declaration
* An object representing the declaration e.g. { color: "red" }.
*/
async function checkInteractiveTooltip(view, ruleIndex, declaration) {
// Get the declaration
const declarations = getPropertiesForRuleIndex(view, ruleIndex);
const [[ name, value ]] = Object.entries(declaration);
const dec = `${name}:${value}`;
const { icon, data } = declarations.get(dec);
// Get the tooltip.
const tooltip = view.tooltips.getTooltip("interactiveTooltip");
// Get the HTML template.
const inactiveCssTooltipHelper = view.tooltips.inactiveCssTooltipHelper;
const template = inactiveCssTooltipHelper.getTemplate(data, tooltip);
// Translate the template using Fluent.
const { doc } = tooltip;
await doc.l10n.translateFragment(template);
// Get the expected HTML content of the now translated template.
const expected = template.firstElementChild.outerHTML;
// Show the tooltip for the correct icon.
const onTooltipReady = tooltip.once("shown");
await view.tooltips.onInteractiveTooltipTargetHover(icon);
tooltip.show(icon);
await onTooltipReady;
// Get the tooltip's actual HTML content.
const actual = tooltip.panel.firstElementChild.outerHTML;
// Hide the tooltip.
const onTooltipHidden = tooltip.once("hidden");
tooltip.hide();
await onTooltipHidden;
// Finally, check the values.
is(actual, expected, "Tooltip contains the correct value.");
}
/**
@ -757,36 +807,25 @@ function checkDeclarationIsActive(view, ruleIndex, declaration) {
* ],
* inactiveDeclarations: [
* {
* l10n: "rule.inactive.css.not.flex.container",
* declaration: {
* "flex-direction": "row",
* },
* ruleIndex: 1,
* },
* ],
* waitFor: "markupmutation",
* },
* ...
* ]
*/
async function runInactiveCSSTests(view, inspector, tests) {
for (const test of tests) {
let event = null;
if (test.waitFor) {
event = inspector.once(test.waitFor);
}
if (test.selector) {
await selectNode(test.selector, inspector);
}
if (test.waitFor) {
await event;
}
if (test.activeDeclarations) {
// Check whether declarations are marked as used.
info("Checking whether declarations are marked as used.");
for (const activeDeclarations of test.activeDeclarations) {
for (const [name, value] of Object.entries(activeDeclarations.declarations)) {
checkDeclarationIsActive(view, activeDeclarations.ruleIndex, {
@ -797,12 +836,12 @@ async function runInactiveCSSTests(view, inspector, tests) {
}
if (test.inactiveDeclarations) {
info("Checking that declarations are unused and have a warning.");
for (const inactiveDeclaration of test.inactiveDeclarations) {
// Check that declaration is unused and has a warning.
checkDeclarationIsInactive(view,
await checkDeclarationIsInactive(view,
inactiveDeclaration.ruleIndex,
inactiveDeclaration.declaration,
inactiveDeclaration.l10n);
inactiveDeclaration.declaration);
}
}
}

Просмотреть файл

@ -634,14 +634,13 @@ TextPropertyEditor.prototype = {
this.element.classList.remove("ruleview-overridden");
}
const { used, reasons } = this.prop.isUsed();
const { used } = this.prop.isUsed();
if (this.editing || this.prop.overridden || !this.prop.enabled || used) {
this.element.classList.remove("unused");
this.unusedState.hidden = true;
} else {
this.element.classList.add("unused");
this.unusedState.title = reasons.join("\n");
this.unusedState.hidden = false;
}
},

Просмотреть файл

@ -10,12 +10,13 @@
* Types of nodes used in the rule and computed view.
*/
exports.VIEW_NODE_SELECTOR_TYPE = 1;
exports.VIEW_NODE_PROPERTY_TYPE = 2;
exports.VIEW_NODE_VALUE_TYPE = 3;
exports.VIEW_NODE_IMAGE_URL_TYPE = 4;
exports.VIEW_NODE_LOCATION_TYPE = 5;
exports.VIEW_NODE_SHAPE_POINT_TYPE = 6;
exports.VIEW_NODE_VARIABLE_TYPE = 7;
exports.VIEW_NODE_FONT_TYPE = 8;
exports.VIEW_NODE_SHAPE_SWATCH = 9;
exports.VIEW_NODE_FONT_TYPE = "font-type";
exports.VIEW_NODE_IMAGE_URL_TYPE = "image-url-type";
exports.VIEW_NODE_INACTIVE_CSS = "inactive-css";
exports.VIEW_NODE_LOCATION_TYPE = "location-type";
exports.VIEW_NODE_PROPERTY_TYPE = "property-type";
exports.VIEW_NODE_SELECTOR_TYPE = "selector-type";
exports.VIEW_NODE_SHAPE_POINT_TYPE = "shape-point-type";
exports.VIEW_NODE_SHAPE_SWATCH = "shape-swatch";
exports.VIEW_NODE_VALUE_TYPE = "value-type";
exports.VIEW_NODE_VARIABLE_TYPE = "variable-type";

Просмотреть файл

@ -17,6 +17,7 @@ const {
VIEW_NODE_VALUE_TYPE,
VIEW_NODE_FONT_TYPE,
VIEW_NODE_IMAGE_URL_TYPE,
VIEW_NODE_INACTIVE_CSS,
VIEW_NODE_VARIABLE_TYPE,
} = require("devtools/client/inspector/shared/node-types");
@ -32,12 +33,15 @@ loader.lazyRequireGetter(this, "setBrokenImageTooltip",
"devtools/client/shared/widgets/tooltip/ImageTooltipHelper", true);
loader.lazyRequireGetter(this, "setVariableTooltip",
"devtools/client/shared/widgets/tooltip/VariableTooltipHelper", true);
loader.lazyRequireGetter(this, "InactiveCssTooltipHelper",
"devtools/client/shared/widgets/tooltip/inactive-css-tooltip-helper", false);
const PREF_IMAGE_TOOLTIP_SIZE = "devtools.inspector.imagePreviewTooltipSize";
// Types of existing tooltips
const TOOLTIP_IMAGE_TYPE = "image";
const TOOLTIP_FONTFAMILY_TYPE = "font-family";
const TOOLTIP_INACTIVE_CSS = "inactive-css";
const TOOLTIP_VARIABLE_TYPE = "variable";
/**
@ -81,17 +85,22 @@ TooltipsOverlay.prototype = {
this._isStarted = true;
// Instantiate the preview tooltip when the rule/computed view is hovered over in
// order to call tooltip.starTogglingOnHover. This will allow the preview tooltip
// to be shown when an appropriate element is hovered over.
this.inactiveCssTooltipHelper = new InactiveCssTooltipHelper();
// Instantiate the interactiveTooltip and preview tooltip when the
// rule/computed view is hovered over in order to call
// `tooltip.startTogglingOnHover`. This will allow the tooltip to be shown
// when an appropriate element is hovered over.
for (const type of ["interactiveTooltip", "previewTooltip"]) {
if (flags.testing) {
this.getTooltip("previewTooltip");
this.getTooltip(type);
} else {
// Lazily get the preview tooltip to avoid loading HTMLTooltip.
this.view.element.addEventListener("mousemove", () => {
this.getTooltip("previewTooltip");
this.getTooltip(type);
}, { once: true });
}
}
},
/**
@ -126,6 +135,16 @@ TooltipsOverlay.prototype = {
tooltip = new SwatchFilterTooltip(doc,
this._cssProperties.getValidityChecker(this.view.inspector.panelDoc));
break;
case "interactiveTooltip":
tooltip = new HTMLTooltip(doc, {
type: "doorhanger",
useXulWrapper: true,
});
tooltip.startTogglingOnHover(this.view.element,
this.onInteractiveTooltipTargetHover.bind(this), {
interactive: true,
});
break;
case "previewTooltip":
tooltip = new HTMLTooltip(doc, {
type: "arrow",
@ -154,6 +173,8 @@ TooltipsOverlay.prototype = {
tooltip.destroy();
}
this.inactiveCssTooltipHelper.destroy();
this._isStarted = false;
},
@ -181,6 +202,11 @@ TooltipsOverlay.prototype = {
}
}
// Inactive CSS tooltip
if (type === VIEW_NODE_INACTIVE_CSS) {
tooltipType = TOOLTIP_INACTIVE_CSS;
}
// Variable preview tooltip
if (type === VIEW_NODE_VARIABLE_TYPE) {
tooltipType = TOOLTIP_VARIABLE_TYPE;
@ -252,6 +278,54 @@ TooltipsOverlay.prototype = {
return false;
},
/**
* Executed by the tooltip when the pointer hovers over an element of the
* view. Used to decide whether the tooltip should be shown or not and to
* actually put content in it.
* Checks if the hovered target is a css value we support tooltips for.
*
* @param {DOMNode} target
* The currently hovered node
* @return {Boolean}
* true if shown, false otherwise.
*/
async onInteractiveTooltipTargetHover(target) {
const nodeInfo = this.view.getNodeInfo(target);
if (!nodeInfo) {
// The hovered node isn't something we care about.
return false;
}
const type = this._getTooltipType(nodeInfo);
if (!type) {
// There is no tooltip type defined for the hovered node.
return false;
}
// Remove previous tooltip instances.
for (const [, tooltip] of this._instances) {
if (tooltip.isVisible()) {
if (tooltip.revert) {
tooltip.revert();
}
tooltip.hide();
}
}
if (type === TOOLTIP_INACTIVE_CSS) {
// Ensure this is the correct node and not a parent.
if (!target.classList.contains("ruleview-unused-warning")) {
return false;
}
await this.inactiveCssTooltipHelper.setContent(
nodeInfo.value, this.getTooltip("interactiveTooltip"));
return true;
}
return false;
},
/**
* Set the content of the preview tooltip to display an image preview. The image URL can
* be relative, a call will be made to the debuggee to retrieve the image content as an

Просмотреть файл

@ -488,33 +488,3 @@ markupView.scrollableBadge.tooltip=This element has scrollable overflow.
# LOCALIZATION NOTE (rulePreviewTooltip.noAssociatedRule): This is the text displayed inside
# the RulePreviewTooltip when a rule cannot be found for a CSS property declaration.
rulePreviewTooltip.noAssociatedRule=No associated rule
# LOCALIZATION NOTE (rule.inactive.css.not.flex.container): These properties
# contain the text displayed inside the Rule Views Inactive CSS Tooltip when a
# property is not active. %S will be replaced with a property name.
rule.inactive.css.not.flex.container=“%S” has no effect on this element since its not a flex container (try adding “display:flex” or “display:inline-flex”)
# LOCALIZATION NOTE (rule.inactive.css.not.flex.item): These properties
# contain the text displayed inside the Rule Views Inactive CSS Tooltip when a
# property is not active. %S will be replaced with a property name.
rule.inactive.css.not.flex.item=“%S” has no effect on this element since its not a flex item (try adding “display:flex” or “display:inline-flex” to the items parent)
# LOCALIZATION NOTE (rule.inactive.css.not.grid.container): These properties
# contain the text displayed inside the Rule Views Inactive CSS Tooltip when a
# property is not active. %S will be replaced with a property name.
rule.inactive.css.not.grid.container=“%S” has no effect on this element since its not a grid container (try adding “display:grid” or “display:inline-grid”)
# LOCALIZATION NOTE (rule.inactive.css.not.grid.item): These properties
# contain the text displayed inside the Rule Views Inactive CSS Tooltip when a
# property is not active. %S will be replaced with a property name.
rule.inactive.css.not.grid.item=“%S” has no effect on this element since its not a grid item (try adding “display:grid” or “display:inline-grid” to the items parent)
# LOCALIZATION NOTE (rule.inactive.css.not.grid.or.flex.item): These properties
# contain the text displayed inside the Rule Views Inactive CSS Tooltip when a
# property is not active. %S will be replaced with a property name.
rule.inactive.css.not.grid.or.flex.item=“%S” has no effect on this element since its not a grid or flex item (try adding “display:grid”, “display:flex”, “display:inline-grid” or “display:inline-flex” to the items parent)
# LOCALIZATION NOTE (rule.inactive.css.not.grid.or.flex.container): These properties
# contain the text displayed inside the Rule Views Inactive CSS Tooltip when a
# property is not active. %S will be replaced with a property name.
rule.inactive.css.not.grid.or.flex.container=“%S” has no effect on this element since its neither a flex container nor a grid container (try adding “display:grid” or “display:flex”)

Просмотреть файл

@ -0,0 +1,41 @@
# 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/.
### Localization for Developer Tools tooltips.
learn-more = <span data-l10n-name="link">Learn more</span>
## In the Rule View when a CSS property cannot be successfully applied we display
## an icon. When this icon is hovered this message is displayed to explain why
## the property is not applied.
## Variables:
## $property (string) - A CSS property name e.g. "color".
inactive-css-not-grid-or-flex-container = <strong>{ $property }</strong> has no effect on this element since its neither a flex container nor a grid container.
inactive-css-not-grid-or-flex-item = <strong>{ $property }</strong> has no effect on this element since its not a grid or flex item.
inactive-css-not-grid-item = <strong>{ $property }</strong> has no effect on this element since its not a grid item.
inactive-css-not-grid-container = <strong>{ $property }</strong> has no effect on this element since its not a grid container.
inactive-css-not-flex-item = <strong>{ $property }</strong> has no effect on this element since its not a flex item.
inactive-css-not-flex-container = <strong>{ $property }</strong> has no effect on this element since its not a flex container.
## In the Rule View when a CSS property cannot be successfully applied we display
## an icon. When this icon is hovered this message is displayed to explain how
## the problem can be solved.
inactive-css-not-grid-or-flex-container-fix = Try adding <strong>display:grid</strong> or <strong>display:flex</strong>. { learn-more }
inactive-css-not-grid-or-flex-item-fix = Try adding <strong>display:grid</strong>, <strong>display:flex</strong>, <strong>display:inline-grid</strong> or <strong>display:inline-flex</strong>. { learn-more }
inactive-css-not-grid-item-fix =Try adding <strong>display:grid</strong> or <strong>display:inline-grid</strong> to the items parent. { learn-more }
inactive-css-not-grid-container-fix = Try adding <strong>display:grid</strong> or <strong>display:inline-grid</strong>. { learn-more }
inactive-css-not-flex-item-fix = Try adding <strong>display:flex</strong> or <strong>display:inline-flex</strong> to the items parent. { learn-more }
inactive-css-not-flex-container-fix = Try adding <strong>display:flex</strong> or <strong>display:inline-flex</strong>. { learn-more }

Просмотреть файл

@ -721,6 +721,11 @@ HTMLTooltip.prototype = {
* is hidden.
*/
async hide({ fromMouseup = false } = {}) {
// Exit if the disable autohide setting is in effect.
if (Services.prefs.getBoolPref("ui.popup.disable_autohide", false)) {
return;
}
this.doc.defaultView.clearTimeout(this.attachEventsTimer);
if (!this.isVisible()) {
this.emit("hidden");
@ -826,11 +831,6 @@ HTMLTooltip.prototype = {
return;
}
// If the disable autohide setting is in effect, ignore.
if (Services.prefs.getBoolPref("ui.popup.disable_autohide", false)) {
return;
}
this.hide({ fromMouseup: true });
},

Просмотреть файл

@ -0,0 +1,128 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* 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";
loader.lazyRequireGetter(this, "openDocLink", "devtools/client/shared/link", true);
class InactiveCssTooltipHelper {
constructor() {
this.addTab = this.addTab.bind(this);
}
/**
* Fill the tooltip with inactive CSS information.
*
* @param {String} propertyName
* The property name to be displayed in bold.
* @param {String} text
* The main text, which follows property name.
*/
async setContent(data, tooltip) {
const fragment = this.getTemplate(data, tooltip);
const { doc } = tooltip;
tooltip.panel.innerHTML = "";
tooltip.panel.addEventListener("click", this.addTab);
tooltip.once("hidden", () => {
tooltip.panel.removeEventListener("click", this.addTab);
});
// Because Fluent is async we need to manually translate the fragment and
// then insert it into the tooltip. This is needed in order for the tooltip
// to size to the contents properly and for tests.
await doc.l10n.translateFragment(fragment);
doc.l10n.pauseObserving();
tooltip.panel.appendChild(fragment);
doc.l10n.resumeObserving();
// Size the content.
tooltip.setContentSize({width: 275, height: Infinity});
}
/**
* Get the template that the Fluent string will be merged with. This template
* looks something like this but there is a variable amount of properties in the
* fix section:
*
* <div class="devtools-tooltip-inactive-css">
* <p data-l10n-id="inactive-css-not-grid-or-flex-container"
* data-l10n-args="{&quot;property&quot;:&quot;align-content&quot;}">
* <strong></strong>
* </p>
* <p data-l10n-id="inactive-css-not-grid-or-flex-container-fix">
* <strong></strong>
* <strong></strong>
* <span data-l10n-name="link" class="link"></span>
* </p>
* </div>
*
* @param {Object} data
* An object in the following format: {
* fixId: "inactive-css-not-grid-item-fix", // Fluent id containing the
* // Inactive CSS fix.
* msgId: "inactive-css-not-grid-item", // Fluent id containing the
* // Inactive CSS message.
* numFixProps: 2, // Number of properties in the fix section of the
* // tooltip.
* property: "color", // Property name
* }
* @param {HTMLTooltip} tooltip
* The tooltip we are targetting.
*/
getTemplate(data, tooltip) {
const XHTML_NS = "http://www.w3.org/1999/xhtml";
const { fixId, msgId, numFixProps, property } = data;
const { doc } = tooltip;
this._currentTooltip = tooltip;
this._currentUrl = `https://developer.mozilla.org/docs/Web/CSS/${property}`;
const templateNode = doc.createElementNS(XHTML_NS, "template");
// eslint-disable-next-line
templateNode.innerHTML = `
<div class="devtools-tooltip-inactive-css">
<p data-l10n-id="${msgId}"
data-l10n-args='${JSON.stringify({property})}'>
<strong></strong>
</p>
<p data-l10n-id="${fixId}">
${"<strong></strong>".repeat(numFixProps)}
<span data-l10n-name="link" class="link"></span>
</p>
</div>`;
return doc.importNode(templateNode.content, true);
}
/**
* Hide the tooltip, open `this._currentUrl` in a new tab and focus it.
*
* @param {DOMEvent} event
* The click event originating from the tooltip.
*
*/
addTab(event) {
// The XUL panel swallows click events so handlers can't be added directly
// to the link span. As a workaround we listen to all click events in the
// panel and if a link span is clicked we proceed.
if (event.target.className !== "link") {
return;
}
const tooltip = this._currentTooltip;
tooltip.hide();
openDocLink(this._currentUrl);
}
destroy() {
this._currentTooltip = null;
this._currentUrl = null;
}
}
module.exports = InactiveCssTooltipHelper;

Просмотреть файл

@ -8,6 +8,7 @@ DevToolsModules(
'EventTooltipHelper.js',
'HTMLTooltip.js',
'ImageTooltipHelper.js',
'inactive-css-tooltip-helper.js',
'InlineTooltip.js',
'RulePreviewTooltip.js',
'SwatchBasedEditorTooltip.js',

Просмотреть файл

@ -67,6 +67,10 @@
--theme-arrowpanel-separator: hsla(210,4%,10%,.14);
}
strong {
font-weight: bold;
}
/* Tooltip: CSS variables tooltip */
.devtools-tooltip-css-variable {
@ -74,6 +78,24 @@
padding: 2px;
}
/* Tooltip: Inactive CSS tooltip */
.devtools-tooltip-inactive-css {
color: var(--theme-body-color);
padding: 7px 10px;
margin: 0;
}
.devtools-tooltip-inactive-css p {
margin-block-start: 0;
margin-block-end: 0;
}
.devtools-tooltip-inactive-css .link {
color: var(--theme-highlight-blue);
cursor: pointer;
}
/* Tooltip: Tiles */
.devtools-tooltip-tiles {

Просмотреть файл

@ -7,11 +7,8 @@
"use strict";
const Services = require("Services");
const { LocalizationHelper } = require("devtools/shared/l10n");
const PREF_UNUSED_CSS_ENABLED = "devtools.inspector.inactive.css.enabled";
const INSPECTOR_L10N =
new LocalizationHelper("devtools/client/locales/inspector.properties");
class InactivePropertyHelper {
/**
@ -36,11 +33,14 @@ class InactivePropertyHelper {
* when:
* The rule itself, a JS function used to identify the conditions
* indicating whether a property is valid or not.
*
* error:
* A JS function that returns a custom error message explaining why the
* property is inactive in this situation. This function takes a single
* argument: the property name.
* fixId:
* A Fluent id containing a suggested solution to the problem that is
* causing a property to be inactive.
* msgId:
* A Fluent id containing an error message explaining why a property is
* inactive in this situation.
* numFixProps:
* The number of properties we suggest in the fixId string.
* }
*
* NOTE: validProperties and invalidProperties are mutually exclusive.
@ -58,7 +58,9 @@ class InactivePropertyHelper {
"flex-wrap",
],
when: () => !this.flexContainer,
error: property => msg("rule.inactive.css.not.flex.container", property),
fixId: "inactive-css-not-flex-container-fix",
msgId: "inactive-css-not-flex-container",
numFixProps: 2,
},
// Flex item property used on non-flex item.
{
@ -70,7 +72,9 @@ class InactivePropertyHelper {
"order",
],
when: () => !this.flexItem,
error: property => msg("rule.inactive.css.not.flex.item", property),
fixId: "inactive-css-not-flex-item-fix",
msgId: "inactive-css-not-flex-item",
numFixProps: 2,
},
// Grid container property used on non-grid container.
{
@ -85,7 +89,9 @@ class InactivePropertyHelper {
"justify-items",
],
when: () => !this.gridContainer,
error: property => msg("rule.inactive.css.not.grid.container", property),
fixId: "inactive-css-not-grid-container-fix",
msgId: "inactive-css-not-grid-container",
numFixProps: 2,
},
// Grid item property used on non-grid item.
{
@ -100,7 +106,9 @@ class InactivePropertyHelper {
"justify-self",
],
when: () => !this.gridItem,
error: property => msg("rule.inactive.css.not.grid.item", property),
fixId: "inactive-css-not-grid-item-fix",
msgId: "inactive-css-not-grid-item",
numFixProps: 2,
},
// Grid and flex item properties used on non-grid or non-flex item.
{
@ -108,7 +116,9 @@ class InactivePropertyHelper {
"align-self",
],
when: () => !this.gridItem && !this.flexItem,
error: property => msg("rule.inactive.css.not.grid.or.flex.item", property),
fixId: "inactive-css-not-grid-or-flex-item-fix",
msgId: "inactive-css-not-grid-or-flex-item",
numFixProps: 4,
},
// Grid and flex container properties used on non-grid or non-flex container.
{
@ -118,7 +128,9 @@ class InactivePropertyHelper {
"justify-content",
],
when: () => !this.gridContainer && !this.flexContainer,
error: property => msg("rule.inactive.css.not.grid.or.flex.container", property),
fixId: "inactive-css-not-grid-or-flex-container-fix",
msgId: "inactive-css-not-grid-or-flex-container",
numFixProps: 2,
},
];
}
@ -143,19 +155,30 @@ class InactivePropertyHelper {
* The CSS property name.
*
* @return {Object} object
* @return {Boolean} object.fixId
* A Fluent id containing a suggested solution to the problem that is
* causing a property to be inactive.
* @return {Boolean} object.msgId
* A Fluent id containing an error message explaining why a property
* is inactive in this situation.
* @return {Boolean} object.numFixProps
* The number of properties we suggest in the fixId string.
* @return {Boolean} object.property
* The inactive property name.
* @return {Boolean} object.used
* true if the property is used.
* @return {Array} object.reasons
* A string array listing the reasons a property isn't used.
*/
isPropertyUsed(el, elStyle, cssRule, property) {
if (!this.unusedCssEnabled) {
return {used: true};
}
const errors = [];
let fixId = "";
let msgId = "";
let numFixProps = 0;
let used = true;
this.VALIDATORS.forEach(validator => {
this.VALIDATORS.some(validator => {
// First check if this rule cares about this property.
let isRuleConcerned = false;
@ -167,7 +190,7 @@ class InactivePropertyHelper {
}
if (!isRuleConcerned) {
return;
return false;
}
this.select(el, elStyle, cssRule, property);
@ -175,17 +198,23 @@ class InactivePropertyHelper {
// And then run the validator, gathering the error message if the
// validator passes.
if (validator.when()) {
const error = validator.error(property);
fixId = validator.fixId;
msgId = validator.msgId;
numFixProps = validator.numFixProps;
used = false;
if (typeof error === "string") {
errors.push(validator.error(property));
}
return true;
}
return false;
});
return {
used: !errors.length,
reasons: errors,
fixId,
msgId,
numFixProps,
property,
used,
};
}
@ -350,17 +379,4 @@ class InactivePropertyHelper {
}
}
/**
* Helper function that gets localized strings.
*
* @param {String} propName
* The property name to use. This property name must exist in the
* `inspector.properties` file).
* @param {*} values
* Values to be used as replacement strings.
*/
function msg(...args) {
return INSPECTOR_L10N.getFormatStr(...args);
}
exports.inactivePropertyHelper = new InactivePropertyHelper();