Complete ShadowRoot child mutation semantics

This commit is contained in:
SGavrl committed 2026-07-31 10:20:22 +00:00
1 parent 2117fa0b8c
commit a4a3ea49ba
2 files changed
+105 -13

No files matched your search

+50 -11
View File
@@ -9004,6 +9004,17 @@ Element.prototype.attachShadow = function attachShadow(opts) {
} }
const host = this; const host = this;
const children = []; const children = [];
const assertInsertable = (node, operation) => {
const createsComposedCycle = node instanceof ShadowRoot
|| node === host
|| !!(node?.contains && node.contains(host));
if (createsComposedCycle) {
throw new DOMException(
`Failed to execute '${operation}' on 'Node': The new child would contain the parent.`,
"HierarchyRequestError"
);
}
};
const shadow = { const shadow = {
mode: opts.mode, mode: opts.mode,
host: host, host: host,
@@ -9022,44 +9033,72 @@ Element.prototype.attachShadow = function attachShadow(opts) {
get firstElementChild() { return children.find(c => c.nodeType === 1) || null; }, get firstElementChild() { return children.find(c => c.nodeType === 1) || null; },
get children() { return children.filter(c => c.nodeType === 1); }, get children() { return children.filter(c => c.nodeType === 1); },
appendChild(c) { appendChild(c) {
if (c) { if (!c) return c;
assertInsertable(c, "appendChild");
if (c instanceof DocumentFragment) {
for (const child of Array.from(c.childNodes)) shadow.appendChild(child);
return c;
}
if (c._shadowParent) c._shadowParent.removeChild(c); if (c._shadowParent) c._shadowParent.removeChild(c);
else if (c.parentNode) c.parentNode.removeChild(c); else if (c.parentNode) c.parentNode.removeChild(c);
children.push(c); children.push(c);
c._shadowParent = shadow; c._shadowParent = shadow;
}
return c; return c;
}, },
insertBefore(n, ref) { insertBefore(n, ref) {
if (!n) return n; if (!n) return n;
if (!ref) { shadow.appendChild(n); return n; } if (!ref) { shadow.appendChild(n); return n; }
const idx = children.indexOf(ref); if (!children.includes(ref)) {
if (idx >= 0) { throw new DOMException(
"Failed to execute 'insertBefore' on 'Node': The reference node is not a child of this node.",
"NotFoundError"
);
}
if (n === ref) return n;
assertInsertable(n, "insertBefore");
if (n instanceof DocumentFragment) {
for (const child of Array.from(n.childNodes)) shadow.insertBefore(child, ref);
return n;
}
if (n._shadowParent) n._shadowParent.removeChild(n); if (n._shadowParent) n._shadowParent.removeChild(n);
else if (n.parentNode) n.parentNode.removeChild(n); else if (n.parentNode) n.parentNode.removeChild(n);
const idx = children.indexOf(ref);
children.splice(idx, 0, n); children.splice(idx, 0, n);
n._shadowParent = shadow; n._shadowParent = shadow;
}
else shadow.appendChild(n);
return n; return n;
}, },
removeChild(c) { removeChild(c) {
const idx = children.indexOf(c); const idx = children.indexOf(c);
if (idx >= 0) { if (idx < 0) {
throw new DOMException(
"Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.",
"NotFoundError"
);
}
children.splice(idx, 1); children.splice(idx, 1);
if (c._shadowParent === shadow) c._shadowParent = null; if (c._shadowParent === shadow) c._shadowParent = null;
}
return c; return c;
}, },
replaceChild(n, o) { replaceChild(n, o) {
const idx = children.indexOf(o); if (!children.includes(o)) {
if (idx >= 0) { throw new DOMException(
"Failed to execute 'replaceChild' on 'Node': The node to be replaced is not a child of this node.",
"NotFoundError"
);
}
if (n === o) return o;
assertInsertable(n, "replaceChild");
if (n instanceof DocumentFragment) {
for (const child of Array.from(n.childNodes)) shadow.insertBefore(child, o);
shadow.removeChild(o);
return o;
}
if (n._shadowParent) n._shadowParent.removeChild(n); if (n._shadowParent) n._shadowParent.removeChild(n);
else if (n.parentNode) n.parentNode.removeChild(n); else if (n.parentNode) n.parentNode.removeChild(n);
const idx = children.indexOf(o);
children[idx] = n; children[idx] = n;
n._shadowParent = shadow; n._shadowParent = shadow;
if (o._shadowParent === shadow) o._shadowParent = null; if (o._shadowParent === shadow) o._shadowParent = null;
}
return o; return o;
}, },
querySelector(s) { querySelector(s) {
+55 -2
View File
@@ -1993,7 +1993,50 @@ mod tests {
inside.getRootNode() === root, inside.getRootNode() === root,
root.textContent root.textContent
]; ];
return [inserted, removed, moved, parsed];
const a = document.createElement("a");
const b = document.createElement("b");
const c = document.createElement("i");
root.replaceChildren(a, b, c);
root.insertBefore(a, c);
const movedWithin = Array.from(root.children, el => el.localName);
const fragment = document.createDocumentFragment();
const x = document.createElement("x-one");
const y = document.createElement("x-two");
fragment.append(x, y);
root.insertBefore(fragment, c);
const flattened = [
Array.from(root.children, el => el.localName),
fragment.childNodes.length,
x.parentNode === root,
y.parentNode === root
];
root.replaceChild(b, c);
const replaced = [
Array.from(root.children, el => el.localName),
c.parentNode === null,
b.parentNode === root
];
const detached = document.createElement("detached-node");
const errors = [];
for (const operation of [
() => root.insertBefore(detached, c),
() => root.removeChild(c),
() => root.replaceChild(detached, c),
() => root.appendChild(root),
() => root.appendChild(host)
]) {
try {
operation();
errors.push("none");
} catch (error) {
errors.push(error.name);
}
}
return [inserted, removed, moved, parsed, movedWithin, flattened, replaced, errors];
"#, "#,
) )
.unwrap(); .unwrap();
@@ -2016,7 +2059,17 @@ mod tests {
], ],
[true, true, true], [true, true, true],
[true, true, true], [true, true, true],
[true, true, "inside"] [true, true, "inside"],
["b", "a", "i"],
[["b", "a", "x-one", "x-two", "i"], 0, true, true],
[["a", "x-one", "x-two", "b"], true, true],
[
"NotFoundError",
"NotFoundError",
"NotFoundError",
"HierarchyRequestError",
"HierarchyRequestError"
]
]) ])
); );
} }