Fire slot change events when the slot content changes (#35137)

* Add the onslotchange attribute to ShadowRoot

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>

* Add spec comments to MutationObserver::queue_mutation_observer_microtask

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>

* Add DomRefCell::take

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>

* Add spec comments to notify_mutation_observers

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>

* Fire slotchange events when a slot changes

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>

* ./mach fmt

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>

* Fix check for when to dispatch slot events

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>

* Potentially fire slot change events in Node::remove

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>

* Update WPT expectations

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>

* ./mach fmt

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>

* Bump stylo

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>

* Move "signal a slot change" into ScriptThread impl

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>

---------

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>
This commit is contained in:
Simon Wülker 2025-01-27 15:13:22 +01:00 committed by GitHub
parent cd93841ba1
commit 859cc6ab9b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 152 additions and 119 deletions

30
Cargo.lock generated
View file

@ -949,7 +949,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys 0.48.0",
"windows-sys 0.52.0",
]
[[package]]
@ -1600,7 +1600,7 @@ dependencies = [
[[package]]
name = "dom"
version = "0.0.1"
source = "git+https://github.com/servo/stylo?branch=2025-01-02#dfed17bd04a713f5dce775176c3a28c39c934970"
source = "git+https://github.com/servo/stylo?branch=2025-01-02#e5643425c05e27fe4aeeec1b8e5e95d6c4550d0f"
dependencies = [
"bitflags 2.8.0",
"malloc_size_of",
@ -4162,7 +4162,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [
"cfg-if",
"windows-targets 0.48.5",
"windows-targets 0.52.6",
]
[[package]]
@ -4338,7 +4338,7 @@ dependencies = [
[[package]]
name = "malloc_size_of"
version = "0.0.1"
source = "git+https://github.com/servo/stylo?branch=2025-01-02#dfed17bd04a713f5dce775176c3a28c39c934970"
source = "git+https://github.com/servo/stylo?branch=2025-01-02#e5643425c05e27fe4aeeec1b8e5e95d6c4550d0f"
dependencies = [
"app_units",
"cssparser",
@ -6328,7 +6328,7 @@ dependencies = [
[[package]]
name = "selectors"
version = "0.26.0"
source = "git+https://github.com/servo/stylo?branch=2025-01-02#dfed17bd04a713f5dce775176c3a28c39c934970"
source = "git+https://github.com/servo/stylo?branch=2025-01-02#e5643425c05e27fe4aeeec1b8e5e95d6c4550d0f"
dependencies = [
"bitflags 2.8.0",
"cssparser",
@ -6613,7 +6613,7 @@ dependencies = [
[[package]]
name = "servo_arc"
version = "0.4.0"
source = "git+https://github.com/servo/stylo?branch=2025-01-02#dfed17bd04a713f5dce775176c3a28c39c934970"
source = "git+https://github.com/servo/stylo?branch=2025-01-02#e5643425c05e27fe4aeeec1b8e5e95d6c4550d0f"
dependencies = [
"serde",
"stable_deref_trait",
@ -6622,7 +6622,7 @@ dependencies = [
[[package]]
name = "servo_atoms"
version = "0.0.1"
source = "git+https://github.com/servo/stylo?branch=2025-01-02#dfed17bd04a713f5dce775176c3a28c39c934970"
source = "git+https://github.com/servo/stylo?branch=2025-01-02#e5643425c05e27fe4aeeec1b8e5e95d6c4550d0f"
dependencies = [
"string_cache",
"string_cache_codegen",
@ -6991,7 +6991,7 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "static_prefs"
version = "0.1.0"
source = "git+https://github.com/servo/stylo?branch=2025-01-02#dfed17bd04a713f5dce775176c3a28c39c934970"
source = "git+https://github.com/servo/stylo?branch=2025-01-02#e5643425c05e27fe4aeeec1b8e5e95d6c4550d0f"
[[package]]
name = "strck"
@ -7072,7 +7072,7 @@ dependencies = [
[[package]]
name = "style"
version = "0.0.1"
source = "git+https://github.com/servo/stylo?branch=2025-01-02#dfed17bd04a713f5dce775176c3a28c39c934970"
source = "git+https://github.com/servo/stylo?branch=2025-01-02#e5643425c05e27fe4aeeec1b8e5e95d6c4550d0f"
dependencies = [
"app_units",
"arrayvec",
@ -7130,7 +7130,7 @@ dependencies = [
[[package]]
name = "style_config"
version = "0.0.1"
source = "git+https://github.com/servo/stylo?branch=2025-01-02#dfed17bd04a713f5dce775176c3a28c39c934970"
source = "git+https://github.com/servo/stylo?branch=2025-01-02#e5643425c05e27fe4aeeec1b8e5e95d6c4550d0f"
dependencies = [
"lazy_static",
]
@ -7138,7 +7138,7 @@ dependencies = [
[[package]]
name = "style_derive"
version = "0.0.1"
source = "git+https://github.com/servo/stylo?branch=2025-01-02#dfed17bd04a713f5dce775176c3a28c39c934970"
source = "git+https://github.com/servo/stylo?branch=2025-01-02#e5643425c05e27fe4aeeec1b8e5e95d6c4550d0f"
dependencies = [
"darling",
"proc-macro2",
@ -7168,7 +7168,7 @@ dependencies = [
[[package]]
name = "style_traits"
version = "0.0.1"
source = "git+https://github.com/servo/stylo?branch=2025-01-02#dfed17bd04a713f5dce775176c3a28c39c934970"
source = "git+https://github.com/servo/stylo?branch=2025-01-02#e5643425c05e27fe4aeeec1b8e5e95d6c4550d0f"
dependencies = [
"app_units",
"bitflags 2.8.0",
@ -7563,7 +7563,7 @@ dependencies = [
[[package]]
name = "to_shmem"
version = "0.1.0"
source = "git+https://github.com/servo/stylo?branch=2025-01-02#dfed17bd04a713f5dce775176c3a28c39c934970"
source = "git+https://github.com/servo/stylo?branch=2025-01-02#e5643425c05e27fe4aeeec1b8e5e95d6c4550d0f"
dependencies = [
"cssparser",
"servo_arc",
@ -7576,7 +7576,7 @@ dependencies = [
[[package]]
name = "to_shmem_derive"
version = "0.1.0"
source = "git+https://github.com/servo/stylo?branch=2025-01-02#dfed17bd04a713f5dce775176c3a28c39c934970"
source = "git+https://github.com/servo/stylo?branch=2025-01-02#e5643425c05e27fe4aeeec1b8e5e95d6c4550d0f"
dependencies = [
"darling",
"proc-macro2",
@ -8609,7 +8609,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.48.0",
"windows-sys 0.52.0",
]
[[package]]

View file

@ -151,3 +151,14 @@ impl<T> DomRefCell<T> {
self.value.try_borrow_mut()
}
}
impl<T: Default> DomRefCell<T> {
/// Takes the wrapped value, leaving `Default::default()` in its place.
///
/// # Panics
///
/// Panics if the value is currently borrowed.
pub(crate) fn take(&self) -> T {
self.value.take()
}
}

View file

@ -32,6 +32,7 @@ use crate::dom::node::{Node, ShadowIncluding};
use crate::dom::text::Text;
use crate::dom::virtualmethods::VirtualMethods;
use crate::script_runtime::CanGc;
use crate::ScriptThread;
/// <https://html.spec.whatwg.org/multipage/#the-slot-element>
#[dom_struct]
@ -185,6 +186,10 @@ impl HTMLSlotElement {
)
}
pub(crate) fn has_assigned_nodes(&self) -> bool {
!self.assigned_nodes.borrow().is_empty()
}
/// <https://dom.spec.whatwg.org/#find-flattened-slotables>
fn find_flattened_slottables(&self, result: &mut RootedVec<Slottable>) {
// Step 1. Let result be an empty list.
@ -312,8 +317,12 @@ impl HTMLSlotElement {
rooted_vec!(let mut slottables);
self.find_slottables(&mut slottables);
// Step 2. TODO If slottables and slots assigned nodes are not identical,
// Step 2. If slottables and slots assigned nodes are not identical,
// then run signal a slot change for slot.
let slots_are_identical = self.assigned_nodes.borrow().iter().eq(slottables.iter());
if !slots_are_identical {
ScriptThread::signal_a_slot_change(self);
}
// Step 3. Set slots assigned nodes to slottables.
*self.assigned_nodes.borrow_mut() = slottables.iter().cloned().collect();

View file

@ -15,9 +15,11 @@ use crate::dom::bindings::codegen::Bindings::MutationObserverBinding::{
MutationCallback, MutationObserverInit,
};
use crate::dom::bindings::error::{Error, Fallible};
use crate::dom::bindings::inheritance::Castable;
use crate::dom::bindings::reflector::{reflect_dom_object_with_proto, DomObject, Reflector};
use crate::dom::bindings::root::DomRoot;
use crate::dom::bindings::str::DOMString;
use crate::dom::eventtarget::EventTarget;
use crate::dom::mutationrecord::MutationRecord;
use crate::dom::node::{Node, ShadowIncluding};
use crate::dom::window::Window;
@ -90,35 +92,57 @@ impl MutationObserver {
/// <https://dom.spec.whatwg.org/#queue-a-mutation-observer-compound-microtask>
pub(crate) fn queue_mutation_observer_microtask() {
// Step 1
// Step 1. If the surrounding agents mutation observer microtask queued is true, then return.
if ScriptThread::is_mutation_observer_microtask_queued() {
return;
}
// Step 2
// Step 2. Set the surrounding agents mutation observer microtask queued to true.
ScriptThread::set_mutation_observer_microtask_queued(true);
// Step 3
// Step 3. Queue a microtask to notify mutation observers.
ScriptThread::enqueue_microtask(Microtask::NotifyMutationObservers);
}
/// <https://dom.spec.whatwg.org/#notify-mutation-observers>
pub(crate) fn notify_mutation_observers() {
// Step 1
pub(crate) fn notify_mutation_observers(can_gc: CanGc) {
// Step 1. Set the surrounding agents mutation observer microtask queued to false.
ScriptThread::set_mutation_observer_microtask_queued(false);
// Step 2
// Step 2. Let notifySet be a clone of the surrounding agents pending mutation observers.
// TODO Step 3. Empty the surrounding agents pending mutation observers.
let notify_list = ScriptThread::get_mutation_observers();
// TODO: steps 3-4 (slots)
// Step 5
// Step 4. Let signalSet be a clone of the surrounding agents signal slots.
// Step 5. Empty the surrounding agents signal slots.
let signal_set = ScriptThread::take_signal_slots();
// Step 6. For each mo of notifySet:
for mo in &notify_list {
// Step 6.1 Let records be a clone of mos record queue.
let queue: Vec<DomRoot<MutationRecord>> = mo.record_queue.borrow().clone();
// Step 6.2 Empty mos record queue.
mo.record_queue.borrow_mut().clear();
// TODO: Step 5.3 Remove all transient registered observers whose observer is mo.
// TODO Step 6.3 For each node of mos node list, remove all transient registered observers
// whose observer is mo from nodes registered observer list.
// Step 6.4 If records is not empty, then invoke mos callback with « records,
// mo » and "report", and with callback this value mo.
if !queue.is_empty() {
let _ = mo
.callback
.Call_(&**mo, queue, mo, ExceptionHandling::Report);
}
}
// TODO: Step 6 (slot signals)
// Step 6. For each slot of signalSet, fire an event named slotchange,
// with its bubbles attribute set to true, at slot.
for slot in signal_set {
slot.upcast::<EventTarget>()
.fire_event(atom!("slotchange"), can_gc);
}
}
/// <https://dom.spec.whatwg.org/#queueing-a-mutation-record>

View file

@ -2323,6 +2323,48 @@ impl Node {
let old_next_sibling = node.GetNextSibling();
// Steps 9-10 are handled in unbind_from_tree.
parent.remove_child(node, cached_index);
// Step 12. If node is assigned, then run assign slottables for nodes assigned slot.
let assigned_slot = node
.downcast::<Element>()
.and_then(|e| e.assigned_slot())
.or_else(|| {
node.downcast::<Text>().and_then(|text| {
text.slottable_data()
.borrow()
.assigned_slot
.as_ref()
.map(Dom::as_rooted)
})
});
if let Some(slot) = assigned_slot {
slot.assign_slottables();
}
// Step 13. If parents root is a shadow root, and parent is a slot whose assigned nodes is the empty list,
// then run signal a slot change for parent.
if parent.is_in_a_shadow_tree() {
if let Some(slot_element) = parent.downcast::<HTMLSlotElement>() {
if !slot_element.has_assigned_nodes() {
ScriptThread::signal_a_slot_change(slot_element);
}
}
}
// Step 14. If node has an inclusive descendant that is a slot:
let has_slot_descendant = node
.traverse_preorder(ShadowIncluding::No)
.any(|elem| elem.is::<HTMLSlotElement>());
if has_slot_descendant {
// Step 14.1 Run assign slottables for a tree with parents root.
parent
.GetRootNode(&GetRootNodeOptions::empty())
.assign_slottables_for_a_tree();
// Step 14.2 Run assign slottables for a tree with node.
node.assign_slottables_for_a_tree();
}
// Step 11. transient registered observers
// Step 12.
if let SuppressObserver::Unsuppressed = suppress_observers {

View file

@ -325,6 +325,9 @@ impl ShadowRootMethods<crate::DomTypeHolder> for ShadowRoot {
fn SlotAssignment(&self) -> SlotAssignmentMode {
self.slot_assignment_mode
}
// https://dom.spec.whatwg.org/#dom-shadowroot-onslotchange
event_handler!(onslotchange, GetOnslotchange, SetOnslotchange);
}
impl VirtualMethods for ShadowRoot {

View file

@ -140,7 +140,7 @@ impl MicrotaskQueue {
ScriptThread::invoke_backup_element_queue(can_gc);
},
Microtask::NotifyMutationObservers => {
MutationObserver::notify_mutation_observers();
MutationObserver::notify_mutation_observers(can_gc);
},
Microtask::ReadableStreamTeeReadRequest(ref task) => {
task.microtask_chunk_steps(can_gc)

View file

@ -128,6 +128,7 @@ use crate::dom::element::Element;
use crate::dom::globalscope::GlobalScope;
use crate::dom::htmlanchorelement::HTMLAnchorElement;
use crate::dom::htmliframeelement::HTMLIFrameElement;
use crate::dom::htmlslotelement::HTMLSlotElement;
use crate::dom::mutationobserver::MutationObserver;
use crate::dom::node::{Node, NodeTraits, ShadowIncluding};
use crate::dom::performanceentry::PerformanceEntry;
@ -259,6 +260,9 @@ pub struct ScriptThread {
/// The unit of related similar-origin browsing contexts' list of MutationObserver objects
mutation_observers: DomRefCell<Vec<Dom<MutationObserver>>>,
/// <https://dom.spec.whatwg.org/#signal-slot-list>
signal_slots: DomRefCell<Vec<Dom<HTMLSlotElement>>>,
/// A handle to the WebGL thread
#[no_trace]
webgl_chan: Option<WebGLPipeline>,
@ -508,6 +512,26 @@ impl ScriptThread {
})
}
pub(crate) fn add_signal_slot(observer: &HTMLSlotElement) {
with_script_thread(|script_thread| {
script_thread
.signal_slots
.borrow_mut()
.push(Dom::from_ref(observer));
})
}
pub(crate) fn take_signal_slots() -> Vec<DomRoot<HTMLSlotElement>> {
with_script_thread(|script_thread| {
script_thread
.signal_slots
.take()
.into_iter()
.map(|slot| slot.as_rooted())
.collect()
})
}
pub(crate) fn mark_document_with_no_blocked_loads(doc: &Document) {
with_script_thread(|script_thread| {
script_thread
@ -914,6 +938,7 @@ impl ScriptThread {
closed_pipelines: DomRefCell::new(HashSet::new()),
mutation_observer_microtask_queued: Default::default(),
mutation_observers: Default::default(),
signal_slots: Default::default(),
system_font_service,
webgl_chan: state.webgl_chan,
#[cfg(feature = "webxr")]
@ -3718,6 +3743,15 @@ impl ScriptThread {
)
}
}
/// <https://dom.spec.whatwg.org/#signal-a-slot-change>
pub(crate) fn signal_a_slot_change(slot: &HTMLSlotElement) {
// Step 1. Append slot to slots relevant agents signal slots.
ScriptThread::add_signal_slot(slot);
// Step 2. Queue a mutation observer microtask.
MutationObserver::queue_mutation_observer_microtask();
}
}
impl Drop for ScriptThread {

View file

@ -14,7 +14,7 @@ interface ShadowRoot : DocumentFragment {
readonly attribute boolean clonable;
// readonly attribute boolean serializable;
readonly attribute Element host;
// attribute EventHandler onslotchange;
attribute EventHandler onslotchange;
};

View file

@ -62,9 +62,6 @@
[AbortController interface: new AbortController() must inherit property "signal" with the proper type]
expected: FAIL
[ShadowRoot interface: attribute onslotchange]
expected: FAIL
[AbortController interface: new AbortController() must inherit property "abort()" with the proper type]
expected: FAIL

View file

@ -1,28 +1,7 @@
[imperative-slot-api-slotchange.html]
expected: TIMEOUT
[slotchange event must not fire synchronously.]
expected: TIMEOUT
[slotchange event should not fire when same node is assigned.]
expected: TIMEOUT
[Fire slotchange event when slot's assigned nodes changes.]
expected: TIMEOUT
[Fire slotchange event on previous slot and new slot when node is reassigned.]
expected: TIMEOUT
[Fire slotchange event on node assignment and when assigned node is removed.]
expected: TIMEOUT
[Fire slotchange event when order of assigned nodes changes.]
expected: TIMEOUT
[Fire slotchange event when assigned node is removed.]
expected: TIMEOUT
[Fire slotchange event when removing a slot from Shadows Root that changes its assigned nodes.]
expected: NOTRUN
expected: TIMEOUT
[Fire slotchange event when assign node to nested slot, ensure event bubbles ups.]
expected: TIMEOUT

View file

@ -8,9 +8,6 @@
[Moving a slot's tree order position within a shadow host has no impact on its assigned slottables.]
expected: FAIL
[Appending slottable to different host, it loses slot assignment. It can be re-assigned within a new host.]
expected: FAIL
[Nodes can be assigned even if slots or nodes aren't in the same tree.]
expected: FAIL

View file

@ -35,18 +35,6 @@
[slotchange event must not fire on a slot element inside a closed shadow root not in a document when another slot's assigned nodes change]
expected: FAIL
[slotchange event must fire on a slot element when a shadow host has a slottable and the slot was inserted and must not fire when the shadow host was mutated after the slot was removed inside an open shadow root in a document]
expected: FAIL
[slotchange event must fire on a slot element when a shadow host has a slottable and the slot was inserted and must not fire when the shadow host was mutated after the slot was removed inside a closed shadow root in a document]
expected: FAIL
[slotchange event must fire on a slot element when a shadow host has a slottable and the slot was inserted and must not fire when the shadow host was mutated after the slot was removed inside an open shadow root not in a document]
expected: FAIL
[slotchange event must fire on a slot element when a shadow host has a slottable and the slot was inserted and must not fire when the shadow host was mutated after the slot was removed inside a closed shadow root not in a document]
expected: FAIL
[slotchange event must fire on a slot element inside an open shadow root in a document even if the slot was removed immediately after the assigned nodes were mutated]
expected: FAIL

View file

@ -3,47 +3,8 @@
[slotchange event: Append a child to a host.]
expected: TIMEOUT
[slotchange event: Remove a child from a host.]
expected: TIMEOUT
[slotchange event: Remove a child before adding an event listener.]
expected: TIMEOUT
[slotchange event: Change slot= attribute to make it un-assigned.]
expected: TIMEOUT
[slotchange event: Change slot's name= attribute so that none is assigned.]
expected: TIMEOUT
[slotchange event: Change slot= attribute to make it assigned.]
expected: TIMEOUT
[slotchange event: Change slot's name= attribute so that a node is assigned to the slot.]
expected: TIMEOUT
[slotchange event: Change fallback content - assignedNodes still empty.]
expected: TIMEOUT
[slotchange event: Remove a fallback content - assignedNodes still empty.]
expected: TIMEOUT
[slotchange event: Add a fallback content to nested slots - assignedNodes still empty.]
expected: TIMEOUT
[slotchange event: Remove a fallback content from nested slots - assignedNodes still empty.]
expected: TIMEOUT
[slotchange event: Insert a slot before an existing slot.]
expected: TIMEOUT
[slotchange event: Remove a preceding slot.]
expected: TIMEOUT
[slotchange event: A slot is assigned to another slot.]
expected: TIMEOUT
[slotchange event: Slotchange should be fired if assigned nodes are changed.]
expected: TIMEOUT
[slotchange event: Child content is added to nested slots.]
expected: TIMEOUT

View file

@ -1,6 +0,0 @@
[slots-fallback-in-document.html]
[Children of a slot in a document tree should not be counted in flattened assigned nodes.]
expected: FAIL
[Slot fallback content in shadow tree should be counted in flattened assigned nodes.]
expected: FAIL

View file

@ -14,8 +14,5 @@
[Slots fallback: Mutation. Assign a node to a slot so that fallback contens are no longer used.]
expected: FAIL
[Slots fallback: Mutation. Remove an assigned node from a slot so that fallback contens will be used.]
expected: FAIL
[Slots fallback: Mutation. Remove a slot which is a fallback content of another slot.]
expected: FAIL

View file

@ -26,9 +26,6 @@
[Slots: Mutation: Add a slot: before.]
expected: FAIL
[Slots: Mutation: Remove a slot.]
expected: FAIL
[Slots: Mutation: Change slot name= attribute.]
expected: FAIL