commit 0c75044fa77d949dfbcf097a945c7cbf743416c0
parent 770dda45ef43282d4a60863836cbcebd758f951e
Author: Nicolas Chevobbe <nchevobbe@mozilla.com>
Date: Wed, 3 Dec 2025 08:11:56 +0000
Bug 1895196 - [devtools] Add anchor badge in markup view for node with valid anchor-name. r=devtools-reviewers,devtools-backward-compat-reviewers,ochameau.
Differential Revision: https://phabricator.services.mozilla.com/D274552
Diffstat:
8 files changed, 222 insertions(+), 0 deletions(-)
diff --git a/devtools/client/fronts/node.js b/devtools/client/fronts/node.js
@@ -494,6 +494,10 @@ class NodeFront extends FrontClassWithSpec(nodeSpec) {
return this._form.containerType;
}
+ get anchorName() {
+ return this._form.anchorName;
+ }
+
get isTreeDisplayed() {
let parent = this;
while (parent) {
diff --git a/devtools/client/inspector/markup/markup.js b/devtools/client/inspector/markup/markup.js
@@ -379,6 +379,7 @@ class MarkupView extends EventEmitter {
this._initShortcuts();
this._walkerEventListener = new WalkerEventListener(this.inspector, {
+ "anchor-name-change": this._onWalkerNodeStatesChanged,
"container-type-change": this._onWalkerNodeStatesChanged,
"display-change": this._onWalkerNodeStatesChanged,
"scrollable-change": this._onWalkerNodeStatesChanged,
@@ -1058,6 +1059,10 @@ class MarkupView extends EventEmitter {
// TODO: use resource api listeners?
if (nodeFront) {
nodeFront.walkerFront.on(
+ "anchor-name-change",
+ this._onWalkerNodeStatesChanged
+ );
+ nodeFront.walkerFront.on(
"container-type-change",
this._onWalkerNodeStatesChanged
);
diff --git a/devtools/client/inspector/markup/test/browser.toml b/devtools/client/inspector/markup/test/browser.toml
@@ -65,6 +65,8 @@ run-if = [
["browser_markup_accessibility_semantics.js"]
+["browser_markup_anchor_badge.js"]
+
["browser_markup_anonymous_01.js"]
["browser_markup_anonymous_03.js"]
diff --git a/devtools/client/inspector/markup/test/browser_markup_anchor_badge.js b/devtools/client/inspector/markup/test/browser_markup_anchor_badge.js
@@ -0,0 +1,153 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the anchor badge is displayed on element with expected anchor-name values.
+
+const TEST_URI = `
+ <style type="text/css">
+ #not-an-anchor {
+ anchor-name: none;
+ }
+
+ #anchor {
+ anchor-name: --my-anchor;
+ }
+
+ #anchor-with-multiple-names {
+ anchor-name: --my-other-anchor, --anchor-alias;
+ }
+
+ .anchored {
+ position: fixed;
+ left: anchor(right);
+ position-anchor: --my-anchor;
+ width: 20px;
+ height: 20px;
+ background-color: gold;
+ }
+ </style>
+ <span id="anchor">--my-anchor</span>
+ <span id="anchor-with-multiple-names">--my-other-anchor --anchor-alias</span>
+ <span id="not-an-anchor">not an anchor</span>
+ <div class="anchored">A</div>
+ <div class="anchored" style="position-anchor: --my-other-anchor">B</div>
+ <div class="anchored" style="position-anchor: --updated-anchor-name">C</div>
+`;
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+ const { inspector } = await openLayoutView();
+
+ let badge = await getAnchorBadgeForSelector("#anchor", inspector);
+ ok(!!badge, "anchor badge is displayed for element with valid anchor name");
+ is(badge.textContent, "anchor", "badge has expected text");
+ is(badge.title, "anchor-name: --my-anchor", "badge has expected title");
+
+ badge = await getAnchorBadgeForSelector(
+ "#anchor-with-multiple-names",
+ inspector
+ );
+ ok(
+ !!badge,
+ "anchor badge is displayed for element with multiple anchor name"
+ );
+ is(badge.textContent, "anchor", "badge has expected text");
+ is(
+ badge.title,
+ "anchor-name: --my-other-anchor, --anchor-alias",
+ "badge has expected title"
+ );
+
+ info(
+ "Change the element anchorName value to see if the badge title is updated"
+ );
+ const oldTitle = badge.title;
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ content.document.getElementById(
+ "anchor-with-multiple-names"
+ ).style.anchorName = "--updated-anchor-name";
+ });
+ await waitFor(() => badge.title !== oldTitle);
+
+ badge = await getAnchorBadgeForSelector(
+ "#anchor-with-multiple-names",
+ inspector
+ );
+ ok(!!badge, "anchor badge is still displayed after changing the anchor name");
+ is(
+ badge.textContent,
+ "anchor",
+ "badge has expected text after changing the anchor name"
+ );
+ is(
+ badge.title,
+ "anchor-name: --updated-anchor-name",
+ "badge has expected title after changing the anchor name"
+ );
+
+ info("Set the element anchorName to none to see if the badge gets hidden");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ content.document.getElementById(
+ "anchor-with-multiple-names"
+ ).style.anchorName = "none";
+ });
+ await waitFor(
+ async () =>
+ (await getAnchorBadgeForSelector(
+ "#anchor-with-multiple-names",
+ inspector
+ )) === null,
+ "wait for badge to be hidden",
+ // interval
+ 500,
+ // max tries
+ 10
+ );
+ ok(true, "The badge was hidden when setting anchorName to none");
+
+ info(
+ "Change the element anchorName value back to a dashed ident to see if the badge is shown again"
+ );
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ content.document.getElementById(
+ "anchor-with-multiple-names"
+ ).style.anchorName = "--my-other-anchor";
+ });
+ badge = await waitFor(
+ async () =>
+ await getAnchorBadgeForSelector("#anchor-with-multiple-names", inspector),
+ "wait for badge to be visible",
+ // interval
+ 500,
+ // max tries
+ 10
+ );
+
+ ok(
+ !!badge,
+ "anchor badge is displayed again after setting a valid anchor name"
+ );
+ is(
+ badge.textContent,
+ "anchor",
+ "badge has expected text after setting a valid anchor name"
+ );
+ is(
+ badge.title,
+ "anchor-name: --my-other-anchor",
+ "badge has expected title after setting a valid anchor name"
+ );
+
+ badge = await getAnchorBadgeForSelector("#not-an-anchor", inspector);
+ ok(
+ !badge,
+ "anchor badge is not displayed for element with anchor-name: none"
+ );
+});
+
+async function getAnchorBadgeForSelector(selector, inspector) {
+ const container = await getContainerForSelector(selector, inspector);
+ return container.elt.querySelector(".inspector-badge[data-anchor]");
+}
diff --git a/devtools/client/inspector/markup/views/element-editor.js b/devtools/client/inspector/markup/views/element-editor.js
@@ -340,6 +340,7 @@ ElementEditor.prototype = {
this.updateCustomBadge();
this.updateScrollableBadge();
this.updateContainerBadge();
+ this.updateAnchorBadge();
this.updateTextEditor();
this.updateUnavailableChildren();
this.updateOverflowBadge();
@@ -550,6 +551,30 @@ ElementEditor.prototype = {
this.markup.emit("badge-added-event");
},
+ updateAnchorBadge() {
+ const showAnchorBadge = this.node.anchorName?.includes?.("--");
+
+ if (this._anchorBadge && !showAnchorBadge) {
+ this._anchorBadge.remove();
+ this._anchorBadge = null;
+ } else if (showAnchorBadge && !this._anchorBadge) {
+ this._createAnchorBadge();
+ }
+
+ if (this._anchorBadge) {
+ this._anchorBadge.title = `anchor-name: ${this.node.anchorName}`;
+ }
+ },
+
+ _createAnchorBadge() {
+ this._anchorBadge = this.doc.createElement("div");
+ this._anchorBadge.classList.add("inspector-badge");
+ this._anchorBadge.dataset.anchor = "true";
+
+ this._anchorBadge.append(this.doc.createTextNode("anchor"));
+ this.elt.insertBefore(this._anchorBadge, this._containerBadge);
+ },
+
/**
* If node causes overflow, toggle its overflow highlight if its scrollable ancestor's
* scrollable badge is active/inactive.
diff --git a/devtools/server/actors/inspector/node.js b/devtools/server/actors/inspector/node.js
@@ -102,6 +102,7 @@ class NodeActor extends Actor {
this.wasDisplayed = this.isDisplayed;
this.wasScrollable = wasScrollable;
this.currentContainerType = this.containerType;
+ this.currentAnchorName = this.anchorName;
if (wasScrollable) {
this.walker.updateOverflowCausingElements(
@@ -199,6 +200,7 @@ class NodeActor extends Actor {
isTopLevelDocument: this.isTopLevelDocument,
causesOverflow: this.walker.overflowCausingElementsMap.has(this.rawNode),
containerType: this.containerType,
+ anchorName: this.anchorName,
// doctype attributes
name: this.rawNode.name,
@@ -392,6 +394,22 @@ class NodeActor extends Actor {
}
/**
+ * Returns the computed anchorName style property value of the node.
+ */
+ get anchorName() {
+ // non-element nodes can't be anchors
+ if (
+ isNodeDead(this) ||
+ this.rawNode.nodeType !== Node.ELEMENT_NODE ||
+ !this.computedStyle
+ ) {
+ return null;
+ }
+
+ return this.computedStyle.anchorName;
+ }
+
+ /**
* Check whether the node currently has scrollbars and is scrollable.
*/
get isScrollable() {
diff --git a/devtools/server/actors/inspector/walker.js b/devtools/server/actors/inspector/walker.js
@@ -544,6 +544,7 @@ class WalkerActor extends Actor {
const containerTypeChanges = [];
const displayTypeChanges = [];
const scrollableStateChanges = [];
+ const anchorNameChanges = [];
const currentOverflowCausingElementsMap = new Map();
@@ -584,6 +585,12 @@ class WalkerActor extends Actor {
containerTypeChanges.push(actor);
actor.currentContainerType = containerType;
}
+
+ const anchorName = actor.anchorName;
+ if (anchorName !== actor.currentAnchorName) {
+ anchorNameChanges.push(actor);
+ actor.currentAnchorName = anchorName;
+ }
}
// Get the NodeActor for each node in the symmetric difference of
@@ -615,6 +622,10 @@ class WalkerActor extends Actor {
if (containerTypeChanges.length) {
this.emit("container-type-change", containerTypeChanges);
}
+
+ if (anchorNameChanges.length) {
+ this.emit("anchor-name-change", anchorNameChanges);
+ }
}
/**
diff --git a/devtools/shared/specs/walker.js b/devtools/shared/specs/walker.js
@@ -91,6 +91,10 @@ const walkerSpec = generateActorSpec({
type: "container-type-change",
nodes: Arg(0, "array:domnode"),
},
+ "anchor-name-change": {
+ type: "anchor-name-change",
+ nodes: Arg(0, "array:domnode"),
+ },
// The walker actor emits a useful "resize" event to its front to let
// clients know when the browser window gets resized. This may be useful
// for refreshing a DOM node's styles for example, since those may depend on