commit 375743d56b88db26ae735f2835c8809380b3178a
parent 054316229cb666e286a8c9746623e2e44509b084
Author: Nicolas Chevobbe <nchevobbe@mozilla.com>
Date: Tue, 16 Dec 2025 18:40:31 +0000
Bug 2005189 - [devtools] Display @custom-media rules in Style Editor at-rules sidebar. r=devtools-reviewers,ochameau
Differential Revision: https://phabricator.services.mozilla.com/D275939
Diffstat:
5 files changed, 184 insertions(+), 7 deletions(-)
diff --git a/devtools/client/styleeditor/StyleEditorUI.sys.mjs b/devtools/client/styleeditor/StyleEditorUI.sys.mjs
@@ -1446,6 +1446,23 @@ export class StyleEditorUI extends EventEmitter {
type.append(
this.#panelDoc.createTextNode(`${rule.positionTryName}\u00A0`)
);
+ } else if (rule.type === "custom-media") {
+ const parts = [];
+ const { customMediaName, customMediaQuery } = rule;
+ for (let i = 0, len = customMediaQuery.length; i < len; i++) {
+ const media = customMediaQuery[i];
+ const queryEl = this.#panelDoc.createElementNS(HTML_NS, "span");
+ queryEl.textContent = media.text;
+ if (!media.matches) {
+ queryEl.classList.add("media-condition-unmatched");
+ }
+ parts.push(queryEl);
+ if (len > 1 && i !== len - 1) {
+ parts.push(", ");
+ }
+ }
+
+ type.append(`${customMediaName} `, ...parts);
}
const cond = this.#panelDoc.createElementNS(HTML_NS, "span");
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_at_rules_sidebar.js b/devtools/client/styleeditor/test/browser_styleeditor_at_rules_sidebar.js
@@ -42,6 +42,8 @@ add_task(async function () {
await pushPref("layout.css.properties-and-values.enabled", true);
// Enable anchor positioning
await pushPref("layout.css.anchor-positioning.enabled", true);
+ // Enable @custom-media
+ await pushPref("layout.css.custom-media.enabled", true);
const { ui } = await openStyleEditorForURL(TESTCASE_URI);
@@ -91,7 +93,7 @@ async function testInlineAtRulesEditor(ui, editor) {
is(sidebar.hidden, false, "sidebar is showing on editor with @media");
const entries = sidebar.querySelectorAll(".at-rule-label");
- is(entries.length, 8, "8 at-rules displayed in sidebar");
+ is(entries.length, 14, "14 at-rules displayed in sidebar");
await testRule({
ui,
@@ -166,6 +168,70 @@ async function testInlineAtRulesEditor(ui, editor) {
type: "position-try",
positionTryName: "--pt-custom-bottom",
});
+
+ await testRule({
+ ui,
+ editor,
+ rule: entries[8],
+ line: 42,
+ type: "custom-media",
+ customMediaName: "--mobile-breakpoint",
+ customMediaQuery: [
+ { text: "(width < 320px) and (height < 1420px)" },
+ { text: ", " },
+ { text: "not print" },
+ ],
+ });
+
+ await testRule({
+ ui,
+ editor,
+ rule: entries[9],
+ line: 43,
+ type: "custom-media",
+ customMediaName: "--enabled",
+ customMediaQuery: [{ text: "true" }],
+ });
+
+ await testRule({
+ ui,
+ editor,
+ rule: entries[10],
+ line: 44,
+ type: "custom-media",
+ customMediaName: "--disabled",
+ customMediaQuery: [{ text: "false", matches: false }],
+ });
+
+ await testRule({
+ ui,
+ editor,
+ rule: entries[11],
+ line: 49,
+ type: "media",
+ conditionText: "(--mobile-breakpoint)",
+ matches: false,
+ });
+
+ await testRule({
+ ui,
+ editor,
+ rule: entries[12],
+ line: 53,
+ type: "media",
+ conditionText: "(--enabled)",
+ matches: false,
+ });
+
+ await testRule({
+ ui,
+ editor,
+ rule: entries[13],
+ line: 57,
+ type: "media",
+ conditionText: "(--disabled)",
+ matches: false,
+ });
}
async function testMediaEditor(ui, editor) {
@@ -298,6 +364,12 @@ async function testMediaRuleAdded(ui, editor) {
* @param {string} options.layerName: Optional name of the @layer
* @param {string} options.positionTryName: Name of the @position-try if type is "position-try"
* @param {string} options.propertyName: Name of the @property if type is "property"
+ * @param {string} options.customMediaName: Name of the @custom-media if type is "custom-media"
+ * @param {Array<object>} options.customMediaQuery: query parts of the @custom-media if type is "custom-media"
+ * @param {string} options.customMediaQuery[].text: the query string of the part of the @custom-media
+ * if type is "custom-media"
+ * @param {boolean} options.customMediaQuery[].matches: whether or not this part is style as matching,
+ * if type is "custom-media". Defaults to true.
* @param {number} options.line: Line of the rule
* @param {string} options.type: The type of the rule (container, layer, media, support, property ).
* Defaults to "media".
@@ -311,6 +383,8 @@ async function testRule({
layerName,
positionTryName,
propertyName,
+ customMediaName,
+ customMediaQuery,
line,
type = "media",
}) {
@@ -323,11 +397,52 @@ async function testRule({
} else if (type === "position-try") {
name = positionTryName;
}
- is(
- atTypeEl.textContent,
- `@${type}\u00A0${name ? `${name}\u00A0` : ""}`,
- "label for at-rule type is correct"
- );
+
+ if (type === "custom-media") {
+ const atTypeChilNodes = Array.from(atTypeEl.childNodes);
+ is(
+ atTypeChilNodes.shift().textContent,
+ `@custom-media\u00A0`,
+ "label for @custom-media is correct"
+ );
+ is(
+ atTypeChilNodes.shift().textContent,
+ `${customMediaName} `,
+ "name for @custom-media is correct"
+ );
+ is(
+ atTypeChilNodes.length,
+ customMediaQuery.length,
+ `Got expected number of children of @custom-media (got ${JSON.stringify(atTypeChilNodes.map(n => n.textContent))})`
+ );
+ for (let i = 0; i < atTypeChilNodes.length; i++) {
+ const node = atTypeChilNodes[i];
+ is(
+ node.textContent,
+ customMediaQuery[i].text,
+ `Got expected text for part #${i} of @custom-media`
+ );
+ if (customMediaQuery[i].matches ?? true) {
+ ok(
+ // handle TextNode
+ !node.classList ||
+ !node.classList.contains("media-condition-unmatched"),
+ `Text for part #${i} of @custom-media ("${node.textContent}") does not have unmatching class`
+ );
+ } else {
+ ok(
+ node.classList.contains("media-condition-unmatched"),
+ `Text for part #${i} of @custom-media ("${node.textContent}") has expected unmatching class`
+ );
+ }
+ }
+ } else {
+ is(
+ atTypeEl.textContent,
+ `@${type}\u00A0${name ? `${name}\u00A0` : ""}`,
+ "label for at-rule type is correct"
+ );
+ }
const cond = rule.querySelector(".at-rule-condition");
is(
diff --git a/devtools/client/styleeditor/test/media-rules.html b/devtools/client/styleeditor/test/media-rules.html
@@ -51,6 +51,26 @@
bottom: unset;
margin-top: 10px;
}
+
+ @custom-media --mobile-breakpoint (width < 320px) and (height < 1420px), not print;
+ @custom-media --enabled true;
+ @custom-media --disabled false;
+
+ div {
+ color: chocolate;
+
+ @media (--mobile-breakpoint) {
+ color: peachpuff;
+ }
+
+ @media (--enabled) {
+ color: green;
+ }
+
+ @media (--disabled) {
+ color: red;
+ }
+ }
</style>
</head>
<body>
diff --git a/devtools/server/actors/utils/stylesheets-manager.js b/devtools/server/actors/utils/stylesheets-manager.js
@@ -728,6 +728,31 @@ class StyleSheetsManager extends EventEmitter {
line: InspectorUtils.getRelativeRuleLine(rule),
column: InspectorUtils.getRuleColumn(rule),
});
+ } else if (className === "CSSCustomMediaRule") {
+ const customMediaQuery = [];
+ if (typeof rule.query === "boolean") {
+ customMediaQuery.push({
+ text: rule.query.toString(),
+ matches: rule.query === true,
+ });
+ } else {
+ // if query is not a boolean, it's a MediaList
+ for (let i = 0, len = rule.query.length; i < len; i++) {
+ customMediaQuery.push({
+ text: rule.query[i],
+ // For now always consider the media query as matching.
+ // This should be changed as part of Bug 2006379
+ matches: true,
+ });
+ }
+ }
+ atRules.push({
+ type: "custom-media",
+ customMediaName: rule.name,
+ customMediaQuery,
+ line: InspectorUtils.getRelativeRuleLine(rule),
+ column: InspectorUtils.getRuleColumn(rule),
+ });
}
}
return {
diff --git a/layout/inspector/InspectorUtils.cpp b/layout/inspector/InspectorUtils.cpp
@@ -568,6 +568,7 @@ static uint32_t CollectAtRules(ServoCSSRuleList& aRuleList,
// so the DevTools team gets notified and can decide if it should be
// displayed.
switch (rule->Type()) {
+ case StyleCssRuleType::CustomMedia:
case StyleCssRuleType::Media:
case StyleCssRuleType::Supports:
case StyleCssRuleType::LayerBlock:
@@ -577,7 +578,6 @@ static uint32_t CollectAtRules(ServoCSSRuleList& aRuleList,
(void)aResult.AppendElement(OwningNonNull(*rule), fallible);
break;
}
- case StyleCssRuleType::CustomMedia:
case StyleCssRuleType::Style:
case StyleCssRuleType::Import:
case StyleCssRuleType::Document: