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
+116 -24

No files matched your search

+61 -22
View File
@@ -9004,6 +9004,17 @@ Element.prototype.attachShadow = function attachShadow(opts) {
}
const host = this;
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 = {
mode: opts.mode,
host: host,
@@ -9022,44 +9033,72 @@ Element.prototype.attachShadow = function attachShadow(opts) {
get firstElementChild() { return children.find(c => c.nodeType === 1) || null; },
get children() { return children.filter(c => c.nodeType === 1); },
appendChild(c) {
if (c) {
if (c._shadowParent) c._shadowParent.removeChild(c);
else if (c.parentNode) c.parentNode.removeChild(c);
children.push(c);
c._shadowParent = shadow;
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);
else if (c.parentNode) c.parentNode.removeChild(c);
children.push(c);
c._shadowParent = shadow;
return c;
},
insertBefore(n, ref) {
if (!n) return n;
if (!ref) { shadow.appendChild(n); return n; }
const idx = children.indexOf(ref);
if (idx >= 0) {
if (n._shadowParent) n._shadowParent.removeChild(n);
else if (n.parentNode) n.parentNode.removeChild(n);
children.splice(idx, 0, n);
n._shadowParent = shadow;
if (!children.includes(ref)) {
throw new DOMException(
"Failed to execute 'insertBefore' on 'Node': The reference node is not a child of this node.",
"NotFoundError"
);
}
else shadow.appendChild(n);
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);
else if (n.parentNode) n.parentNode.removeChild(n);
const idx = children.indexOf(ref);
children.splice(idx, 0, n);
n._shadowParent = shadow;
return n;
},
removeChild(c) {
const idx = children.indexOf(c);
if (idx >= 0) {
children.splice(idx, 1);
if (c._shadowParent === shadow) c._shadowParent = null;
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);
if (c._shadowParent === shadow) c._shadowParent = null;
return c;
},
replaceChild(n, o) {
const idx = children.indexOf(o);
if (idx >= 0) {
if (n._shadowParent) n._shadowParent.removeChild(n);
else if (n.parentNode) n.parentNode.removeChild(n);
children[idx] = n;
n._shadowParent = shadow;
if (o._shadowParent === shadow) o._shadowParent = null;
if (!children.includes(o)) {
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);
else if (n.parentNode) n.parentNode.removeChild(n);
const idx = children.indexOf(o);
children[idx] = n;
n._shadowParent = shadow;
if (o._shadowParent === shadow) o._shadowParent = null;
return o;
},
querySelector(s) {
+55 -2
View File
@@ -1993,7 +1993,50 @@ mod tests {
inside.getRootNode() === root,
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();
@@ -2016,7 +2059,17 @@ mod tests {
],
[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"
]
])
);
}