Skip to content

Commit 2111a12

Browse files
authored
script: Don't unregister slots from their shadow root when the whole shadow tree is being unbound (servo#40278)
Each shadow root holds a map of slot names to slot elements. Slot elements dynamically register and unregister themselves at their respective shadow root when they are bound and unbound from the tree. Previously, they were doing this too often. When a slot element is unbound from the tree then it might still be connected to its shadow root, in case the entire shadow tree is being unbound. Fixing this requires the slot element to know whether it was in a shadow tree prior to `bind_to_tree` being called (then it's already registered and doesn't need to do so again), and whether it will be in a shadow tree after `unbind_from_tree` is called (then there's no need to unregister). Testing: This change adds a new web platform test Fixes: servo#40242 --------- Signed-off-by: Simon Wülker <[email protected]>
1 parent 8494b56 commit 2111a12

File tree

6 files changed

+75
-17
lines changed

6 files changed

+75
-17
lines changed

components/script/dom/element.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,8 @@ use crate::dom::intersectionobserver::{IntersectionObserver, IntersectionObserve
157157
use crate::dom::mutationobserver::{Mutation, MutationObserver};
158158
use crate::dom::namednodemap::NamedNodeMap;
159159
use crate::dom::node::{
160-
BindContext, ChildrenMutation, CloneChildrenFlag, LayoutNodeHelpers, Node, NodeDamage,
161-
NodeFlags, NodeTraits, ShadowIncluding, UnbindContext,
160+
BindContext, ChildrenMutation, CloneChildrenFlag, IsShadowTree, LayoutNodeHelpers, Node,
161+
NodeDamage, NodeFlags, NodeTraits, ShadowIncluding, UnbindContext,
162162
};
163163
use crate::dom::nodelist::NodeList;
164164
use crate::dom::promise::Promise;
@@ -695,7 +695,7 @@ impl Element {
695695
.upcast::<Node>()
696696
.set_containing_shadow_root(Some(&shadow_root));
697697

698-
let bind_context = BindContext::new(&self.node);
698+
let bind_context = BindContext::new(self.upcast(), IsShadowTree::Yes);
699699
shadow_root.bind_to_tree(&bind_context, can_gc);
700700

701701
let node = self.upcast::<Node>();

components/script/dom/html/htmlslotelement.rs

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ use crate::dom::document::Document;
2828
use crate::dom::element::{AttributeMutation, Element};
2929
use crate::dom::globalscope::GlobalScope;
3030
use crate::dom::html::htmlelement::HTMLElement;
31-
use crate::dom::node::{BindContext, Node, NodeDamage, NodeTraits, ShadowIncluding, UnbindContext};
31+
use crate::dom::node::{
32+
BindContext, IsShadowTree, Node, NodeDamage, NodeTraits, ShadowIncluding, UnbindContext,
33+
};
3234
use crate::dom::virtualmethods::VirtualMethods;
3335
use crate::script_runtime::CanGc;
3436

@@ -495,22 +497,24 @@ impl VirtualMethods for HTMLSlotElement {
495497
s.bind_to_tree(context, can_gc);
496498
}
497499

498-
if !context.tree_is_in_a_shadow_tree {
499-
return;
500+
let was_already_in_shadow_tree = context.is_shadow_tree == IsShadowTree::Yes;
501+
if !was_already_in_shadow_tree {
502+
if let Some(shadow_root) = self.containing_shadow_root() {
503+
shadow_root.register_slot(self);
504+
}
500505
}
501-
502-
self.containing_shadow_root()
503-
.expect("not in a shadow tree")
504-
.register_slot(self);
505506
}
506507

507508
fn unbind_from_tree(&self, context: &UnbindContext, can_gc: CanGc) {
508509
if let Some(s) = self.super_type() {
509510
s.unbind_from_tree(context, can_gc);
510511
}
511512

512-
if let Some(shadow_root) = self.containing_shadow_root() {
513-
shadow_root.unregister_slot(self.Name(), self);
513+
if !self.upcast::<Node>().is_in_a_shadow_tree() {
514+
if let Some(old_shadow_root) = self.containing_shadow_root() {
515+
// If we used to be in a shadow root, but aren't anymore, then unregister this slot
516+
old_shadow_root.unregister_slot(self.Name(), self);
517+
}
514518
}
515519
}
516520
}

components/script/dom/node.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ impl Node {
301301
let parent_is_connected = self.is_connected();
302302
let parent_is_in_ua_widget = self.is_in_ua_widget();
303303

304-
let context = BindContext::new(self);
304+
let context = BindContext::new(self, IsShadowTree::No);
305305

306306
for node in new_child.traverse_preorder(ShadowIncluding::No) {
307307
if parent_in_shadow_tree {
@@ -4305,16 +4305,29 @@ pub(crate) struct BindContext<'a> {
43054305

43064306
/// Whether the tree's root is a shadow root
43074307
pub(crate) tree_is_in_a_shadow_tree: bool,
4308+
4309+
/// Whether the root of the subtree that is being bound to the parent is a shadow root.
4310+
///
4311+
/// This implies that all elements whose "bind_to_tree" method are called were already
4312+
/// in a shadow tree beforehand.
4313+
pub(crate) is_shadow_tree: IsShadowTree,
4314+
}
4315+
4316+
#[derive(Debug, Eq, PartialEq)]
4317+
pub(crate) enum IsShadowTree {
4318+
Yes,
4319+
No,
43084320
}
43094321

43104322
impl<'a> BindContext<'a> {
43114323
/// Create a new `BindContext` value.
4312-
pub(crate) fn new(parent: &'a Node) -> Self {
4324+
pub(crate) fn new(parent: &'a Node, is_shadow_tree: IsShadowTree) -> Self {
43134325
BindContext {
43144326
parent,
43154327
tree_connected: parent.is_connected(),
43164328
tree_is_in_a_document_tree: parent.is_in_a_document_tree(),
43174329
tree_is_in_a_shadow_tree: parent.is_in_a_shadow_tree(),
4330+
is_shadow_tree,
43184331
}
43194332
}
43204333

components/script/dom/shadowroot.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ use crate::dom::documentorshadowroot::{
4747
use crate::dom::element::Element;
4848
use crate::dom::html::htmlslotelement::HTMLSlotElement;
4949
use crate::dom::node::{
50-
BindContext, Node, NodeDamage, NodeFlags, NodeTraits, ShadowIncluding, UnbindContext,
51-
VecPreOrderInsertionHelper,
50+
BindContext, IsShadowTree, Node, NodeDamage, NodeFlags, NodeTraits, ShadowIncluding,
51+
UnbindContext, VecPreOrderInsertionHelper,
5252
};
5353
use crate::dom::trustedhtml::TrustedHTML;
5454
use crate::dom::types::EventTarget;
@@ -575,7 +575,7 @@ impl VirtualMethods for ShadowRoot {
575575

576576
shadow_root.set_flag(NodeFlags::IS_CONNECTED, context.tree_connected);
577577

578-
let context = BindContext::new(shadow_root);
578+
let context = BindContext::new(shadow_root, IsShadowTree::Yes);
579579

580580
// avoid iterate over the shadow root itself
581581
for node in shadow_root.traverse_preorder(ShadowIncluding::Yes).skip(1) {

tests/wpt/meta/MANIFEST.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -822432,6 +822432,13 @@
822432822432
}
822433822433
]
822434822434
],
822435+
"assign-slottables-after-removing-shadow-tree-from-document.html": [
822436+
"216c5747625695b58666d32a04d0f3037b6b690b",
822437+
[
822438+
null,
822439+
{}
822440+
]
822441+
],
822435822442
"attachShadow-with-ShadowRoot.html": [
822436822443
"994053822909ff9b1adc077cb7d912ecb54c430b",
822437822444
[
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<!DOCTYPE html>
2+
<head>
3+
<title>Slottables should match slots when the shadow host is removed from the document</title>
4+
<meta name="author" title="Simon Wülker" href="mailto:[email protected]">
5+
<meta name="assert" content="Slottables should be assigned to slots even after the shadow host has been removed from the document">
6+
<link rel="help" href="https://github.com/servo/servo/issues/40242">
7+
8+
<script src="/resources/testharness.js"></script>
9+
<script src="/resources/testharnessreport.js"></script>
10+
</head>
11+
12+
<body>
13+
<script defer>
14+
test((t) => {
15+
// Attach a shadow root with a slot named "foo" inside it
16+
let host = document.createElement("div");
17+
document.body.appendChild(host);
18+
let root = host.attachShadow({mode: "closed"});
19+
let slot = document.createElement("slot");
20+
slot.name = "foo";
21+
root.appendChild(slot);
22+
23+
// Remove the host from the document tree
24+
host.remove();
25+
assert_array_equals(slot.assignedNodes(), []);
26+
27+
// Create a slottable that should be slotted into "foo"
28+
let slottable = document.createElement("div");
29+
slottable.slot = "foo"
30+
host.appendChild(slottable);
31+
assert_array_equals(slot.assignedNodes(), [slottable]);
32+
}, "Slottables should be assigned to slots even after the shadow host has been removed from the document");
33+
</script>
34+
</body>

0 commit comments

Comments
 (0)