tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 98b486020c1d538e194dccade88916a85e56f201
parent d1c13e06bdda52657db296d75ef9230fc8ec22be
Author: Nicolas Chevobbe <nchevobbe@mozilla.com>
Date:   Wed, 10 Dec 2025 16:17:04 +0000

Bug 1895176 - [devtools] Show @position-try rules. r=devtools-reviewers,ochameau.

In a similar fashion to what we do for `@keyframes` rules, we collect the different
`@position-try` rules from the stylesheets of the page, and only return them
when they're actually being used in a `position-try-fallback` declaration.

For now, we show them as read-only, as updating the declarations of `CSSPositionTryRule`
is still brittle (see Bug 2004046).

A test is added to cover this.

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

Diffstat:
Mdevtools/client/fronts/style-rule.js | 3+++
Mdevtools/client/inspector/rules/models/rule.js | 7++++++-
Mdevtools/client/inspector/rules/rules.js | 17++++++++++++++++-
Mdevtools/client/inspector/rules/test/browser_part2.toml | 2++
Adevtools/client/inspector/rules/test/browser_rules_position-try.js | 226+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdevtools/client/inspector/rules/test/head.js | 7+++++++
Mdevtools/client/inspector/rules/views/rule-editor.js | 38+++++++++++++++++++++++++-------------
Mdevtools/server/actors/inspector/css-logic.js | 24+++++++++++++++++++++---
Mdevtools/server/actors/page-style.js | 21++++++++++++++++++++-
Mdevtools/server/actors/style-rule.js | 4++++
10 files changed, 330 insertions(+), 19 deletions(-)

diff --git a/devtools/client/fronts/style-rule.js b/devtools/client/fronts/style-rule.js @@ -69,6 +69,9 @@ class StyleRuleFront extends FrontClassWithSpec(styleRuleSpec) { get type() { return this._form.type; } + get className() { + return this._form.className; + } get line() { return this._form.line || -1; } diff --git a/devtools/client/inspector/rules/models/rule.js b/devtools/client/inspector/rules/models/rule.js @@ -904,7 +904,12 @@ class Rule { * @returns {boolean} Whether or not the rule can be edited */ isEditable() { - return !this.isSystem && this.domRule.type !== PRES_HINTS; + return ( + !this.isSystem && + this.domRule.type !== PRES_HINTS && + // FIXME: Should be removed as part of Bug 2004046 + this.domRule.className !== "CSSPositionTryRule" + ); } /** diff --git a/devtools/client/inspector/rules/rules.js b/devtools/client/inspector/rules/rules.js @@ -94,6 +94,7 @@ const FILTER_STRICT_RE = /\s*`(.*?)`\s*$/; const RULE_VIEW_HEADER_CLASSNAME = "ruleview-header"; const PSEUDO_ELEMENTS_CONTAINER_ID = "pseudo-elements-container"; const REGISTERED_PROPERTIES_CONTAINER_ID = "registered-properties-container"; +const POSITION_TRY_CONTAINER_ID = "position-try-container"; /** * Our model looks like this: @@ -1606,6 +1607,17 @@ CssRuleView.prototype = { ) ); } + } else if (rule.domRule.className === "CSSPositionTryRule") { + containerKey = POSITION_TRY_CONTAINER_ID; + if (!containers.has(containerKey)) { + containers.set( + containerKey, + this.createExpandableContainer( + `@position-try`, + `position-try-container` + ) + ); + } } rule.editor.element.setAttribute("role", "article"); @@ -1696,7 +1708,10 @@ CssRuleView.prototype = { let isSelectorHighlighted = false; let selectorNodes = [...rule.editor.selectorText.childNodes]; - if (rule.domRule.type === CSSRule.KEYFRAME_RULE) { + if ( + rule.domRule.type === CSSRule.KEYFRAME_RULE || + rule.domRule.className === "CSSPositionTryRule" + ) { selectorNodes = [rule.editor.selectorText]; } else if (rule.domRule.type === ELEMENT_STYLE) { selectorNodes = []; diff --git a/devtools/client/inspector/rules/test/browser_part2.toml b/devtools/client/inspector/rules/test/browser_part2.toml @@ -246,6 +246,8 @@ fail-if = [ "a11y_checks", # Bug 1849028 clicked element may not be focusable and/or labeled ] +["browser_rules_position-try.js"] + ["browser_rules_preview-tooltips-sizes.js"] ["browser_rules_print_media_simulation.js"] diff --git a/devtools/client/inspector/rules/test/browser_rules_position-try.js b/devtools/client/inspector/rules/test/browser_rules_position-try.js @@ -0,0 +1,226 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that @position-try rules are displayed in a dedicated section. + +const TEST_URI = `https://example.org/document-builder.sjs?html=${encodeURIComponent(` + <style> + .anchor { + anchor-name: --test-anchor; + } + + .anchored { + position: fixed; + position-anchor: --test-anchor; + color: peachpuff; + } + + .no-at-position-try { + position-try: top; + } + + .unknown-at-position-try { + position-try: top, --unknown; + } + + .single-at-position-try { + position-try: bottom, --custom-bottom; + } + + .multiple-at-position-try { + position-try: left, --custom-right,--custom-bottom; + } + + @position-try --custom-bottom { + top: anchor(bottom); + color: gold; + } + + @position-try --custom-right { + top: anchor(bottom); + left: anchor(right); + color: tomato; + } + </style> + <main> + <div class=anchor>⚓️</div> + <span class="anchored no-at-position-try"></span> + <span class="anchored unknown-at-position-try"></span> + <span class="anchored single-at-position-try"></span> + <span class="anchored multiple-at-position-try"></span> + </main> +`)}`; + +add_task(async function () { + await pushPref("layout.css.anchor-positioning.enabled", true); + await addTab(TEST_URI); + const { inspector, view } = await openRuleView(); + + info( + "Check that the @property-try section isn't displayed if the at-rules are not used in the position-try declaration" + ); + await selectNode(".anchored.no-at-position-try", inspector); + + const anchoredClassRuleItem = { + selector: ".anchored", + declarations: [ + { name: "position", value: "fixed" }, + { name: "position-anchor", value: "--test-anchor" }, + { name: "color", value: "peachpuff" }, + ], + }; + + await checkRuleViewContent(view, [ + { + selector: "element", + selectorEditable: false, + declarations: [], + }, + { + selector: ".no-at-position-try", + declarations: [{ name: "position-try", value: "top" }], + }, + anchoredClassRuleItem, + ]); + + info( + "Check that the @property-try section isn't displayed if the the position-try value " + + "refers to an unknown dashed ident" + ); + await selectNode(".anchored.unknown-at-position-try", inspector); + await checkRuleViewContent(view, [ + { + selector: "element", + selectorEditable: false, + declarations: [], + }, + { + selector: ".unknown-at-position-try", + declarations: [{ name: "position-try", value: "top, --unknown" }], + }, + anchoredClassRuleItem, + ]); + + info( + "Check that the @property-try section is displayed and has expected content if a" + + "dashed ident is used in the position-try declaration" + ); + await selectNode(".anchored.single-at-position-try", inspector); + await checkRuleViewContent(view, [ + { + selector: "element", + selectorEditable: false, + declarations: [], + }, + { + selector: ".single-at-position-try", + declarations: [ + { name: "position-try", value: "bottom, --custom-bottom" }, + ], + }, + anchoredClassRuleItem, + { + header: `@position-try`, + }, + { + selector: "--custom-bottom", + selectorEditable: false, + hasSelectorHighlighterButton: false, + declarations: [ + { name: "top", value: "anchor(bottom)" }, + // we have this here to make sure it's not marked as overridden / does not override + // color declaration for regular rules. + // Ultimately this should be marked as inactive (see Bug 1895178) + { name: "color", value: "gold" }, + ], + }, + ]); + + info( + "Check that the @property-try section is displayed and has expected content if multiple " + + "dashed-ident are used in the position-try declaration" + ); + await selectNode(".anchored.multiple-at-position-try", inspector); + await checkRuleViewContent(view, [ + { + selector: "element", + selectorEditable: false, + declarations: [], + }, + { + selector: ".multiple-at-position-try", + declarations: [ + { name: "position-try", value: "left, --custom-right,--custom-bottom" }, + ], + }, + anchoredClassRuleItem, + { + header: `@position-try`, + }, + { + selector: "--custom-bottom", + selectorEditable: false, + hasSelectorHighlighterButton: false, + declarations: [ + { name: "top", value: "anchor(bottom)" }, + // we have this here to make sure it's not marked as overridden / does not override + // color declaration for regular rules. + // Ultimately this should be marked as inactive (see Bug 1895178) + { name: "color", value: "gold" }, + ], + }, + { + selector: "--custom-right", + selectorEditable: false, + hasSelectorHighlighterButton: false, + declarations: [ + { name: "top", value: "anchor(bottom)" }, + { name: "left", value: "anchor(right)" }, + // we have this here to make sure it's not marked as overridden / does not override + // color declaration for regular rules. + // Ultimately this should be marked as inactive (see Bug 1895178) + { name: "color", value: "tomato" }, + ], + }, + ]); + + info("Check that we can filter on the @position-try name"); + await setSearchFilter(view, "--custom-r"); + + await checkRuleViewContent(view, [ + { + selector: "element", + selectorEditable: false, + declarations: [], + }, + { + selector: ".multiple-at-position-try", + declarations: [ + { + name: "position-try", + value: "left, --custom-right,--custom-bottom", + highlighted: true, + }, + ], + }, + { + header: `@position-try`, + }, + { + selector: "--custom-right", + selectorEditable: false, + hasSelectorHighlighterButton: false, + declarations: [ + { name: "top", value: "anchor(bottom)" }, + { name: "left", value: "anchor(right)" }, + { name: "color", value: "tomato" }, + ], + }, + ]); + + // TODO: At the moment we display @position-try rules as read-only, but as part of + // Bug 2004046, we should assert that adding modifying/adding declaration propagates the change + // stylesheet as expected, and that the declarations of the rules are properly updated. +}); diff --git a/devtools/client/inspector/rules/test/head.js b/devtools/client/inspector/rules/test/head.js @@ -1373,6 +1373,8 @@ function getSmallIncrementKey() { * unmatched selector with `~~` characters (e.g. "div, ~~unmatched~~") * @param {boolean} expectedElements[].selectorEditable - Whether or not the selector can * be edited. Defaults to true. + * @param {boolean} expectedElements[].hasSelectorHighlighterButton - Whether or not a + * selector highlighter button is visible. Defaults to true. * @param {string[]|null} expectedElements[].ancestorRulesData - An array of the parent * selectors of the rule, with their indentations and the opening brace. * e.g. for the following rule `html { body { span {} } }`, for the `span` rule, @@ -1458,6 +1460,11 @@ function checkRuleViewContent(view, expectedElements) { expectedElement.selectorEditable ?? true, `Selector for element #${i} (${selector}) ${(expectedElement.selectorEditable ?? true) ? "is" : "isn't"} editable` ); + is( + elementInView.querySelector(`.ruleview-selectorhighlighter`) !== null, + expectedElement.hasSelectorHighlighterButton ?? true, + `Element #${i} (${selector}) ${(expectedElement.hasSelectorHighlighterButton ?? true) ? "has" : "does not have"} a selector highlighter button` + ); const ancestorData = elementInView.querySelector( `.ruleview-rule-ancestor-data` diff --git a/devtools/client/inspector/rules/views/rule-editor.js b/devtools/client/inspector/rules/views/rule-editor.js @@ -144,7 +144,15 @@ RuleEditor.prototype = { return ( this.isEditable && this.rule.domRule.type !== ELEMENT_STYLE && - this.rule.domRule.type !== CSSRule.KEYFRAME_RULE + this.rule.domRule.type !== CSSRule.KEYFRAME_RULE && + this.rule.domRule.className !== "CSSPositionTryRule" + ); + }, + + get showSelectorHighlighterButton() { + return ( + this.rule.domRule.type !== CSSRule.KEYFRAME_RULE && + this.rule.domRule.className !== "CSSPositionTryRule" ); }, @@ -394,18 +402,20 @@ RuleEditor.prototype = { // be computed on demand when the highlighter is requested. } - const isHighlighted = - this.ruleView.isSelectorHighlighted(computedSelector); - // Handling of click events is delegated to CssRuleView.handleEvent() - createChild(header, "button", { - class: - "ruleview-selectorhighlighter js-toggle-selector-highlighter" + - (isHighlighted ? " highlighted" : ""), - "aria-pressed": isHighlighted, - // This is used in rules.js for the selector highlighter - "data-computed-selector": computedSelector, - title: l10n("rule.selectorHighlighter.tooltip"), - }); + if (this.showSelectorHighlighterButton) { + const isHighlighted = + this.ruleView.isSelectorHighlighted(computedSelector); + // Handling of click events is delegated to CssRuleView.handleEvent() + createChild(header, "button", { + class: + "ruleview-selectorhighlighter js-toggle-selector-highlighter" + + (isHighlighted ? " highlighted" : ""), + "aria-pressed": isHighlighted, + // This is used in rules.js for the selector highlighter + "data-computed-selector": computedSelector, + title: l10n("rule.selectorHighlighter.tooltip"), + }); + } } this.openBrace = createChild(header, "span", { @@ -662,6 +672,8 @@ RuleEditor.prototype = { this.selectorText.textContent = this.rule.selectorText; } else if (this.rule.domRule.type === CSSRule.KEYFRAME_RULE) { this.selectorText.textContent = this.rule.domRule.keyText; + } else if (this.rule.domRule.className === "CSSPositionTryRule") { + this.selectorText.textContent = this.rule.domRule.name; } else { this.rule.domRule.selectors.forEach((selector, i) => { this._populateSelector(selector, i); diff --git a/devtools/server/actors/inspector/css-logic.js b/devtools/server/actors/inspector/css-logic.js @@ -195,6 +195,9 @@ class CssLogic { // Cached keyframes rules in all stylesheets #keyframesRules = null; + // Cached @position-try rules in all stylesheets + #positionTryRules = null; + /** * Reset various properties */ @@ -207,6 +210,7 @@ class CssLogic { this.#matchedRules = null; this.#matchedSelectors = null; this.#keyframesRules = []; + this.#positionTryRules = []; } /** @@ -350,7 +354,7 @@ class CssLogic { * Cache a stylesheet if it falls within the requirements: if it's enabled, * and if the @media is allowed. This method also walks through the stylesheet * cssRules to find @imported rules, to cache the stylesheets of those rules - * as well. In addition, the @keyframes rules in the stylesheet are cached. + * as well. In addition, @keyframes and @position-try rules in the stylesheet are cached. * * @private * @param {CSSStyleSheet} domSheet the CSSStyleSheet object to cache. @@ -370,8 +374,8 @@ class CssLogic { if (cssSheet.passId != this.passId) { cssSheet.passId = this.passId; - // Find import and keyframes rules. We loop through all the stylesheet recursively, - // so we can go through nested rules. + // Find import, keyframes and position-try rules. We loop through all the stylesheet + // recursively, so we can go through nested rules. const traverseRules = ruleList => { for (const aDomRule of ruleList) { const ruleClassName = ChromeUtils.getClassName(aDomRule); @@ -383,6 +387,8 @@ class CssLogic { this.#cacheSheet(aDomRule.styleSheet); } else if (ruleClassName === "CSSKeyframesRule") { this.#keyframesRules.push(aDomRule); + } else if (ruleClassName === "CSSPositionTryRule") { + this.#positionTryRules.push(aDomRule); } if (aDomRule.cssRules) { @@ -428,6 +434,18 @@ class CssLogic { } /** + * Retrieve the list of @position-try rules in the document. + * + * @returns {CSSPositionTryRule[]} the list of @position-try rules in the document. + */ + get positionTryRules() { + if (!this.#sheetsCached) { + this.#cacheSheets(); + } + return this.#positionTryRules; + } + + /** * Retrieve a CssSheet object for a given a CSSStyleSheet object. If the * stylesheet is already cached, you get the existing CssSheet object, * otherwise the new CSSStyleSheet object is cached. diff --git a/devtools/server/actors/page-style.js b/devtools/server/actors/page-style.js @@ -1129,9 +1129,9 @@ class PageStyleActor extends Actor { } } - // Add all the keyframes rule associated with the element const computedStyle = this.cssLogic.computedStyle; if (computedStyle) { + // Add all the keyframes rule associated with the element let animationNames = computedStyle.animationName.split(","); animationNames = animationNames.map(name => name.trim()); @@ -1151,6 +1151,25 @@ class PageStyleActor extends Actor { } } } + + // Add all the @position-try associated with the element + const positionTryIdents = new Set(); + for (const part of computedStyle.positionTryFallbacks.split(",")) { + const name = part.trim(); + if (name.startsWith("--")) { + positionTryIdents.add(name); + } + } + + for (const positionTryRule of this.cssLogic.positionTryRules) { + if (!positionTryIdents.has(positionTryRule.name)) { + continue; + } + + entries.push({ + rule: this.styleRef(positionTryRule), + }); + } } return entries; diff --git a/devtools/server/actors/style-rule.js b/devtools/server/actors/style-rule.js @@ -406,6 +406,7 @@ class StyleRuleActor extends Actor { const form = { actor: this.actorID, type: this.type, + className: this.ruleClassName, line: this.line || undefined, column: this.column, traits: { @@ -483,6 +484,7 @@ class StyleRuleActor extends Actor { form.href = this.rawRule.href; break; case "CSSKeyframesRule": + case "CSSPositionTryRule": form.name = this.rawRule.name; break; case "CSSKeyframeRule": @@ -641,6 +643,7 @@ class StyleRuleActor extends Actor { _getCssText() { switch (this.ruleClassName) { case "CSSNestedDeclarations": + case "CSSPositionTryRule": case "CSSStyleRule": case ELEMENT_STYLE: case PRES_HINTS: @@ -870,6 +873,7 @@ class StyleRuleActor extends Actor { "CSSLayerBlockRule", "CSSMediaRule", "CSSNestedDeclarations", + "CSSPositionTryRule", "CSSStyleRule", "CSSSupportsRule", ]);