Merge pull request #2366 from lightpanda-io/custom_element_callbacks_again

Protect DOM mutations against custom element callbacks.
This commit is contained in:
Karl Seguin
2026-05-06 08:02:16 +08:00
committed by GitHub
3 changed files with 81 additions and 0 deletions

View File

@@ -65,3 +65,32 @@
assertChildren(['a'], d3);
testing.expectEqual(null, b.parentNode);
</script>
<div id=d4></div>
<div id=d4_stash></div>
<script id=appendChild_disconnect_callback_reparents>
// Moving a connected child into a disconnected target makes
// will_be_reconnected=false, so disconnectedCallback fires synchronously
// inside removeNode. The callback re-parents the child; appendChild
// respects that placement instead of overriding it.
const d4 = $('#d4');
const stash = $('#d4_stash');
class ReparentOnDisconnect extends HTMLElement {
disconnectedCallback() {
if (this.parentNode === null) {
stash.appendChild(this);
}
}
}
customElements.define('reparent-on-disconnect', ReparentOnDisconnect);
const rpd = document.createElement('reparent-on-disconnect');
rpd.id = 'rpd';
d4.appendChild(rpd);
const detached = document.createElement('div');
detached.appendChild(rpd);
testing.expectEqual(stash, rpd.parentNode);
</script>

View File

@@ -39,3 +39,34 @@
assertChildren([], d1);
assertChildren([c1, c2], d2);
</script>
<div id=d3></div>
<div id=d3_stash></div>
<script id=insertBefore_disconnect_callback_reparents>
// Same disconnectedCallback re-parenting pattern as in append_child.html,
// exercised through insertBefore. insertBefore respects the callback's
// placement instead of overriding it.
const d3 = $('#d3');
const stash = $('#d3_stash');
class IBReparentOnDisconnect extends HTMLElement {
disconnectedCallback() {
if (this.parentNode === null) {
stash.appendChild(this);
}
}
}
customElements.define('ib-reparent-on-disconnect', IBReparentOnDisconnect);
const moving = document.createElement('ib-reparent-on-disconnect');
moving.id = 'ib_moving';
d3.appendChild(moving);
const detached = document.createElement('div');
const ref = document.createElement('span');
detached.appendChild(ref);
detached.insertBefore(moving, ref);
testing.expectEqual(stash, moving.parentNode);
</script>

View File

@@ -253,6 +253,11 @@ pub fn appendChild(self: *Node, child: *Node, frame: *Frame) !*Node {
try frame.adoptNodeTree(child, child_owner.?, parent_owner);
}
// A custom element callback can re-parent the node. If it does, we're done
if (child._parent != null) {
return child;
}
try frame.appendNode(self, child, .{
.child_already_connected = child_connected,
.adopting_to_new_document = adopting_to_new_document,
@@ -624,6 +629,22 @@ pub fn insertBefore(self: *Node, new_node: *Node, ref_node_: ?*Node, frame: *Fra
try frame.adoptNodeTree(new_node, child_owner.?, parent_owner);
}
// See Node.appendChild: a callback above (disconnectedCallback or
// adoptedCallback) can re-parent new_node. Let that placement stand.
if (new_node._parent != null) {
return new_node;
}
// The same callback could also have detached ref_node from self. Fall
// back to append so new_node still lands in self.
if (ref_node._parent != self) {
try frame.appendNode(self, new_node, .{
.child_already_connected = child_already_connected,
.adopting_to_new_document = adopting_to_new_document,
});
return new_node;
}
try frame.insertNodeRelative(
self,
new_node,