diff --git a/devtools/client/styleeditor/StyleEditorUI.sys.mjs b/devtools/client/styleeditor/StyleEditorUI.sys.mjs index 338a56815cd7..27e3c3d73935 100644 --- a/devtools/client/styleeditor/StyleEditorUI.sys.mjs +++ b/devtools/client/styleeditor/StyleEditorUI.sys.mjs @@ -1329,31 +1329,42 @@ export class StyleEditorUI extends EventEmitter { inSource = true; const div = this.#panelDoc.createElementNS(HTML_NS, "div"); - div.className = "at-rule-label"; + div.classList.add("at-rule-label", rule.type); div.addEventListener( "click", this.#jumpToLocation.bind(this, location) ); - const cond = this.#panelDoc.createElementNS(HTML_NS, "div"); + const ruleTextContainer = this.#panelDoc.createElementNS( + HTML_NS, + "div" + ); + const type = this.#panelDoc.createElementNS(HTML_NS, "span"); + type.className = "at-rule-type"; + type.append(this.#panelDoc.createTextNode(`@${rule.type}\u00A0`)); + if (rule.type == "layer" && rule.layerName) { + type.append(this.#panelDoc.createTextNode(`${rule.layerName}\u00A0`)); + } + + const cond = this.#panelDoc.createElementNS(HTML_NS, "span"); cond.className = "at-rule-condition"; - if (!rule.matches) { + if (rule.type == "media" && !rule.matches) { cond.classList.add("media-condition-unmatched"); } if (this.#commands.descriptorFront.isLocalTab) { - this.#setConditionContents(cond, rule.conditionText); + this.#setConditionContents(cond, rule.conditionText, rule.type); } else { cond.textContent = rule.conditionText; } - div.appendChild(cond); const link = this.#panelDoc.createElementNS(HTML_NS, "div"); link.className = "at-rule-line theme-link"; if (location.line != -1) { link.textContent = ":" + location.line; } - div.appendChild(link); + ruleTextContainer.append(type, cond); + div.append(ruleTextContainer, link); list.appendChild(div); } @@ -1366,38 +1377,55 @@ export class StyleEditorUI extends EventEmitter { }; /** - * Used to safely inject media query links + * Set the condition text for the at-rule element. + * For media queries, it also injects links to open RDM at a specific size. * * @param {HTMLElement} element * The element corresponding to the media sidebar condition - * @param {String} rawText - * The raw condition text to parse + * @param {String} ruleConditionText + * The rule conditionText + * @param {String} type + * The type of the at-rule (e.g. "media", "layer", "supports", …) */ - #setConditionContents(element, rawText) { + #setConditionContents(element, ruleConditionText, type) { + if (!ruleConditionText) { + return; + } + + // For non-media rules, we don't do anything more than displaying the conditionText + // as there are no other condition text that would justify opening RDM at a specific + // size (e.g. `@container` condition is relative to a container size, which varies + // depending the node the rule applies to). + if (type !== "media") { + const node = this.#panelDoc.createTextNode(ruleConditionText); + element.appendChild(node); + return; + } + const minMaxPattern = /(min\-|max\-)(width|height):\s\d+(px)/gi; - let match = minMaxPattern.exec(rawText); + let match = minMaxPattern.exec(ruleConditionText); let lastParsed = 0; while (match && match.index != minMaxPattern.lastIndex) { const matchEnd = match.index + match[0].length; const node = this.#panelDoc.createTextNode( - rawText.substring(lastParsed, match.index) + ruleConditionText.substring(lastParsed, match.index) ); element.appendChild(node); const link = this.#panelDoc.createElementNS(HTML_NS, "a"); link.href = "#"; link.className = "media-responsive-mode-toggle"; - link.textContent = rawText.substring(match.index, matchEnd); + link.textContent = ruleConditionText.substring(match.index, matchEnd); link.addEventListener("click", this.#onMediaConditionClick.bind(this)); element.appendChild(link); - match = minMaxPattern.exec(rawText); + match = minMaxPattern.exec(ruleConditionText); lastParsed = matchEnd; } const node = this.#panelDoc.createTextNode( - rawText.substring(lastParsed, rawText.length) + ruleConditionText.substring(lastParsed, ruleConditionText.length) ); element.appendChild(node); } diff --git a/devtools/client/styleeditor/test/browser.ini b/devtools/client/styleeditor/test/browser.ini index 7a379c3f124e..7afa06ba2173 100644 --- a/devtools/client/styleeditor/test/browser.ini +++ b/devtools/client/styleeditor/test/browser.ini @@ -79,6 +79,7 @@ support-files = !/devtools/client/shared/test/highlighter-test-actor.js [browser_styleeditor_add_stylesheet.js] +[browser_styleeditor_at_rules_sidebar.js] [browser_styleeditor_autocomplete.js] [browser_styleeditor_autocomplete-disabled.js] [browser_styleeditor_bom.js] @@ -101,7 +102,6 @@ skip-if = !debug && (os == "win") || (os == "linux" && os_version == "18.04") #b [browser_styleeditor_inline_friendly_names.js] [browser_styleeditor_loading.js] [browser_styleeditor_loading_with_containers.js] -[browser_styleeditor_media_sidebar.js] [browser_styleeditor_media_sidebar_links.js] [browser_styleeditor_media_sidebar_sourcemaps.js] [browser_styleeditor_missing_stylesheet.js] diff --git a/devtools/client/styleeditor/test/browser_styleeditor_media_sidebar.js b/devtools/client/styleeditor/test/browser_styleeditor_at_rules_sidebar.js similarity index 80% rename from devtools/client/styleeditor/test/browser_styleeditor_media_sidebar.js rename to devtools/client/styleeditor/test/browser_styleeditor_at_rules_sidebar.js index adf7dab2fdbc..2a0e9fe2a4e4 100644 --- a/devtools/client/styleeditor/test/browser_styleeditor_media_sidebar.js +++ b/devtools/client/styleeditor/test/browser_styleeditor_at_rules_sidebar.js @@ -34,6 +34,8 @@ const NEW_RULE = ` waitForExplicitFinish(); add_task(async function() { + await pushPref("layout.css.container-queries.enabled", true); + const { ui } = await openStyleEditorForURL(TESTCASE_URI); is(ui.editors.length, 4, "correct number of editors"); @@ -82,7 +84,7 @@ async function testInlineMediaEditor(ui, editor) { is(sidebar.hidden, false, "sidebar is showing on editor with @media"); const entries = sidebar.querySelectorAll(".at-rule-label"); - is(entries.length, 2, "2 @media rules displayed in sidebar"); + is(entries.length, 5, "5 @media rules displayed in sidebar"); await testRule({ ui, @@ -91,15 +93,45 @@ async function testInlineMediaEditor(ui, editor) { conditionText: "screen", matches: true, line: 2, + type: "media", }); await testRule({ ui, editor, rule: entries[1], + conditionText: "(display: flex)", + line: 7, + type: "support", + }); + + await testRule({ + ui, + editor, + rule: entries[2], conditionText: "(1px < height < 10000px)", matches: true, line: 8, + type: "media", + }); + + await testRule({ + ui, + editor, + rule: entries[3], + conditionText: "", + line: 16, + type: "layer", + layerName: "myLayer", + }); + + await testRule({ + ui, + editor, + rule: entries[4], + conditionText: "(min-width: 1px)", + line: 17, + type: "container", }); } @@ -215,24 +247,45 @@ async function testMediaRuleAdded(ui, editor) { * @param {Element} options.rule: The rule element in the media sidebar * @param {String} options.conditionText: media query condition text * @param {Boolean} options.matches: Whether or not the document matches the rule + * @param {String} options.layerName: Optional name of the @layer * @param {Number} options.line: Line of the rule + * @param {String} options.type: The type of the rule (container, layer, media, support ). + * Defaults to "media". */ -async function testRule({ ui, editor, rule, conditionText, matches, line }) { +async function testRule({ + ui, + editor, + rule, + conditionText, + matches, + layerName, + line, + type = "media", +}) { + const atTypeEl = rule.querySelector(".at-rule-type"); + is( + atTypeEl.textContent, + `@${type}\u00A0${layerName ? `${layerName}\u00A0` : ""}`, + "label for at-rule type is correct" + ); + const cond = rule.querySelector(".at-rule-condition"); is( cond.textContent, conditionText, - "media label is correct for " + conditionText + "condition label is correct for " + conditionText ); - const matched = !cond.classList.contains("media-condition-unmatched"); - ok( - matches ? matched : !matched, - "media rule is " + (matches ? "matched" : "unmatched") - ); + if (type == "media") { + const matched = !cond.classList.contains("media-condition-unmatched"); + ok( + matches ? matched : !matched, + "media rule is " + (matches ? "matched" : "unmatched") + ); + } - const mediaRuleLine = rule.querySelector(".at-rule-line"); - is(mediaRuleLine.textContent, ":" + line, "correct line number shown"); + const ruleLine = rule.querySelector(".at-rule-line"); + is(ruleLine.textContent, ":" + line, "correct line number shown"); info( "Check that clicking on the rule jumps to the expected position in the stylesheet" diff --git a/devtools/client/styleeditor/test/browser_styleeditor_media_sidebar_sourcemaps.js b/devtools/client/styleeditor/test/browser_styleeditor_media_sidebar_sourcemaps.js index f80aa111125c..696bb4107278 100644 --- a/devtools/client/styleeditor/test/browser_styleeditor_media_sidebar_sourcemaps.js +++ b/devtools/client/styleeditor/test/browser_styleeditor_media_sidebar_sourcemaps.js @@ -25,12 +25,12 @@ add_task(async function() { // Test editor with @media rules const mediaEditor = ui.editors[0]; await openEditor(mediaEditor); - testMediaEditor(mediaEditor); + testAtRulesEditor(mediaEditor); Services.prefs.clearUserPref(MAP_PREF); }); -function testMediaEditor(editor) { +function testAtRulesEditor(editor) { const sidebar = editor.details.querySelector(".stylesheet-sidebar"); is(sidebar.hidden, false, "sidebar is showing on editor with @media"); diff --git a/devtools/client/styleeditor/test/media-rules.html b/devtools/client/styleeditor/test/media-rules.html index 7ffe68bf405c..f170e1efad0e 100644 --- a/devtools/client/styleeditor/test/media-rules.html +++ b/devtools/client/styleeditor/test/media-rules.html @@ -24,6 +24,9 @@ } } } + + @layer myLayer {} + @container (min-width: 1px) {} diff --git a/devtools/client/themes/styleeditor.css b/devtools/client/themes/styleeditor.css index 7c3c7a34d1ac..e75fd28f8717 100644 --- a/devtools/client/themes/styleeditor.css +++ b/devtools/client/themes/styleeditor.css @@ -132,10 +132,17 @@ li.error > .stylesheet-info > .stylesheet-more > .stylesheet-error-message { } .at-rule-label { - display: flex; + display: grid; + /* + * +----------------------------------------------+ + * | Rule information | line number | + * +----------------------------------------------+ + */ + grid-template-columns: 1fr max-content; padding: 4px; - cursor: pointer; border-bottom: 1px solid var(--theme-splitter-color); + cursor: pointer; + word-break: break-word; } .media-responsive-mode-toggle { @@ -151,11 +158,6 @@ li.error > .stylesheet-info > .stylesheet-more > .stylesheet-error-message { opacity: 0.4; } -.at-rule-condition { - flex: 1; - overflow: hidden; -} - .stylesheet-toggle { display: -moz-box; cursor: pointer; diff --git a/devtools/server/actors/utils/stylesheets-manager.js b/devtools/server/actors/utils/stylesheets-manager.js index 19547d1ac12e..ca465a32ae63 100644 --- a/devtools/server/actors/utils/stylesheets-manager.js +++ b/devtools/server/actors/utils/stylesheets-manager.js @@ -597,8 +597,55 @@ class StyleSheetsManager extends EventEmitter { continue; } - if (rule.type === CSSRule.MEDIA_RULE) { - rules.push(rule); + const line = InspectorUtils.getRelativeRuleLine(rule); + const column = InspectorUtils.getRuleColumn(rule); + + const className = ChromeUtils.getClassName(rule); + if (className === "CSSMediaRule") { + let matches = false; + + try { + const mql = win.matchMedia(rule.media.mediaText); + matches = mql.matches; + mql.onchange = this._onMatchesChange.bind( + this, + resourceId, + rules.length + ); + this._mqlList.push(mql); + } catch (e) { + // Ignored + } + + rules.push({ + type: "media", + mediaText: rule.media.mediaText, + conditionText: rule.conditionText, + matches, + line, + column, + }); + } else if (className === "CSSContainerRule") { + rules.push({ + type: "container", + conditionText: rule.conditionText, + line, + column, + }); + } else if (className === "CSSSupportsRule") { + rules.push({ + type: "support", + conditionText: rule.conditionText, + line, + column, + }); + } else if (className === "CSSLayerBlockRule") { + rules.push({ + type: "layer", + layerName: rule.name, + line, + column, + }); } if (rule.cssRules) { @@ -607,27 +654,7 @@ class StyleSheetsManager extends EventEmitter { } }; traverseRules(styleSheetRules); - - return rules.map((rule, index) => { - let matches = false; - - try { - const mql = win.matchMedia(rule.media.mediaText); - matches = mql.matches; - mql.onchange = this._onMatchesChange.bind(this, resourceId, index); - this._mqlList.push(mql); - } catch (e) { - // Ignored - } - - return { - mediaText: rule.media.mediaText, - conditionText: rule.conditionText, - matches, - line: InspectorUtils.getRelativeRuleLine(rule), - column: InspectorUtils.getRuleColumn(rule), - }; - }); + return rules; } /** @@ -637,7 +664,7 @@ class StyleSheetsManager extends EventEmitter { * @param {String} resourceId * The id associated with the stylesheet * @param {Number} index - * The index of the media rule relatively to all the other media rules of the stylesheet + * The index of the media rule relatively to all the other at-rules of the stylesheet * @param {MediaQueryList} mql * The result of matchMedia for the given media rule */