Implement Event propagation across shadow roots (#34884)

* Implement Event.composed flag

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

* Allow composed events to pass shadow boundaries

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

* Update WPT expectations

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-22 16:25:16 +01:00 committed by GitHub
parent aed7e8cefd
commit 1b882f2729
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 89 additions and 144 deletions

View file

@ -506,6 +506,7 @@ impl Animations {
let parent = EventInit {
bubbles: true,
cancelable: false,
composed: false,
};
let property_or_animation_name =

View file

@ -73,6 +73,9 @@ pub(crate) struct Event {
/// <https://dom.spec.whatwg.org/#dom-event-bubbles>
bubbles: Cell<bool>,
/// <https://dom.spec.whatwg.org/#dom-event-composed>
composed: Cell<bool>,
/// <https://dom.spec.whatwg.org/#dom-event-istrusted>
is_trusted: Cell<bool>,
@ -129,6 +132,7 @@ impl Event {
stop_immediate_propagation: Cell::new(false),
cancelable: Cell::new(false),
bubbles: Cell::new(false),
composed: Cell::new(false),
is_trusted: Cell::new(false),
dispatch: Cell::new(false),
initialized: Cell::new(false),
@ -169,6 +173,8 @@ impl Event {
can_gc: CanGc,
) -> DomRoot<Event> {
let event = Event::new_uninitialized_with_proto(global, proto, can_gc);
// NOTE: The spec doesn't tell us to call init event here, it just happens to do what we need.
event.init_event(type_, bool::from(bubbles), bool::from(cancelable));
event
}
@ -622,10 +628,68 @@ impl Event {
self.set_trusted(true);
target.dispatch_event(self, can_gc)
}
/// <https://dom.spec.whatwg.org/#inner-event-creation-steps>
fn inner_creation_steps(
global: &GlobalScope,
proto: Option<HandleObject>,
init: &EventBinding::EventInit,
can_gc: CanGc,
) -> DomRoot<Event> {
// Step 1. Let event be the result of creating a new object using eventInterface.
// If realm is non-null, then use that realm; otherwise, use the default behavior defined in Web IDL.
let event = Event::new_uninitialized_with_proto(global, proto, can_gc);
// Step 2. Set events initialized flag.
event.initialized.set(true);
// Step 3. Initialize events timeStamp attribute to the relative high resolution
// coarse time given time and events relevant global object.
// NOTE: This is done inside Event::new_inherited
// Step 3. For each member → value in dictionary, if event has an attribute whose
// identifier is member, then initialize that attribute to value.#
event.bubbles.set(init.bubbles);
event.cancelable.set(init.cancelable);
event.composed.set(init.composed);
// Step 5. Run the event constructing steps with event and dictionary.
// NOTE: Event construction steps may be defined by subclasses
// Step 6. Return event.
event
}
/// Implements the logic behind the [get the parent](https://dom.spec.whatwg.org/#get-the-parent)
/// algorithm for shadow roots.
pub(crate) fn should_pass_shadow_boundary(&self, shadow_root: &ShadowRoot) -> bool {
debug_assert!(self.dispatching());
// > A shadow roots get the parent algorithm, given an event, returns null if events composed flag
// > is unset and shadow root is the root of events paths first structs invocation target;
// > otherwise shadow roots host.
if self.Composed() {
return true;
}
let path = self.path.borrow();
let first_invocation_target = &path
.first()
.expect("Event path is empty despite event currently being dispatched")
.invocation_target
.as_rooted();
// The spec doesn't tell us what should happen if the invocation target is not a node
let Some(target_node) = first_invocation_target.downcast::<Node>() else {
return false;
};
&*target_node.GetRootNode(&GetRootNodeOptions::empty()) != shadow_root.upcast::<Node>()
}
}
impl EventMethods<crate::DomTypeHolder> for Event {
/// <https://dom.spec.whatwg.org/#dom-event-event>
/// <https://dom.spec.whatwg.org/#concept-event-constructor>
fn Constructor(
global: &GlobalScope,
proto: Option<HandleObject>,
@ -633,16 +697,15 @@ impl EventMethods<crate::DomTypeHolder> for Event {
type_: DOMString,
init: &EventBinding::EventInit,
) -> Fallible<DomRoot<Event>> {
let bubbles = EventBubbles::from(init.bubbles);
let cancelable = EventCancelable::from(init.cancelable);
Ok(Event::new_with_proto(
global,
proto,
Atom::from(type_),
bubbles,
cancelable,
can_gc,
))
// Step 1. Let event be the result of running the inner event creation steps with
// this interface, null, now, and eventInitDict.
let event = Event::inner_creation_steps(global, proto, init, can_gc);
// Step 2. Initialize events type attribute to type.
*event.type_.borrow_mut() = Atom::from(type_);
// Step 3. Return event.
Ok(event)
}
/// <https://dom.spec.whatwg.org/#dom-event-eventphase>
@ -805,6 +868,11 @@ impl EventMethods<crate::DomTypeHolder> for Event {
self.canceled.get() == EventDefault::Prevented
}
/// <https://dom.spec.whatwg.org/#dom-event-composed>
fn Composed(&self) -> bool {
self.composed.get()
}
/// <https://dom.spec.whatwg.org/#dom-event-preventdefault>
fn PreventDefault(&self) {
if self.cancelable.get() {

View file

@ -805,11 +805,13 @@ impl EventTarget {
}
}
if self.downcast::<ShadowRoot>().is_some() {
// FIXME: Handle event composed flag here
// We currently assume that events are never composed (so events may never
// cross a shadow boundary)
return None;
if let Some(shadow_root) = self.downcast::<ShadowRoot>() {
if event.should_pass_shadow_boundary(shadow_root) {
let host = shadow_root.Host();
return Some(DomRoot::from_ref(host.upcast::<EventTarget>()));
} else {
return None;
}
}
if let Some(node) = self.downcast::<Node>() {

View file

@ -34,6 +34,7 @@ interface Event {
undefined preventDefault();
[Pure]
readonly attribute boolean defaultPrevented;
readonly attribute boolean composed;
[LegacyUnforgeable]
readonly attribute boolean isTrusted;
@ -46,4 +47,5 @@ interface Event {
dictionary EventInit {
boolean bubbles = false;
boolean cancelable = false;
boolean composed = false;
};

View file

@ -1,3 +0,0 @@
[event-global-extra.window.html]
[window.event should not be affected by nodes moving post-dispatch]
expected: FAIL

View file

@ -1,7 +1,3 @@
[event-global.html]
expected: TIMEOUT
[window.event is undefined if the target is in a shadow tree (event dispatched inside shadow tree)]
expected: TIMEOUT
[window.event is undefined inside window.onerror if the target is in a shadow tree (ErrorEvent dispatched inside shadow tree)]
expected: FAIL

View file

@ -5,15 +5,6 @@
expected: ERROR
[idlharness.any.worker.html]
[Event interface: attribute composed]
expected: FAIL
[Event interface: new Event("foo") must inherit property "composed" with the proper type]
expected: FAIL
[Event interface: new CustomEvent("foo") must inherit property "composed" with the proper type]
expected: FAIL
[AbortController interface: attribute signal]
expected: FAIL

View file

@ -8,9 +8,6 @@
[AbortSignal must be primary interface of new AbortController().signal]
expected: FAIL
[Event interface: attribute composed]
expected: FAIL
[AbortSignal interface: existence and properties of interface prototype object's @@unscopables property]
expected: FAIL
@ -32,9 +29,6 @@
[AbortSignal interface object name]
expected: FAIL
[Event interface: new CustomEvent("foo") must inherit property "composed" with the proper type]
expected: FAIL
[EventTarget interface: new AbortController().signal must inherit property "removeEventListener(DOMString, EventListener?, optional (EventListenerOptions or boolean))" with the proper type]
expected: FAIL
@ -116,9 +110,6 @@
[AbortSignal interface: existence and properties of interface prototype object]
expected: FAIL
[Event interface: new Event("foo") must inherit property "composed" with the proper type]
expected: FAIL
[Element interface: operation remove()]
expected: FAIL
@ -146,9 +137,6 @@
[Document interface: operation append((Node or DOMString)...)]
expected: FAIL
[Event interface: document.createEvent("Event") must inherit property "composed" with the proper type]
expected: FAIL
[DocumentType interface: operation remove()]
expected: FAIL

View file

@ -1,16 +1,4 @@
[Extensions-to-Event-Interface.html]
[composed on EventInit must default to false]
expected: FAIL
[composed on EventInit must set the composed flag]
expected: FAIL
[The event must propagate out of open mode shadow boundaries when the composed flag is set]
expected: FAIL
[The event must propagate out of closed mode shadow boundaries when the composed flag is set]
expected: FAIL
[The event must not propagate out of open mode shadow tree of the target but must propagate out of inner shadow trees when the scoped flag is set]
expected: FAIL

View file

@ -1,10 +1,4 @@
[capturing-and-bubbling-event-listeners-across-shadow-trees.html]
[Capturing event listeners should be invoked before bubbling event listeners when an event is dispatched inside a shadow tree]
expected: FAIL
[Capturing event listeners should be invoked before bubbling event listeners when an event is dispatched inside a doubly nested shadow tree]
expected: FAIL
[Capturing event listeners should be invoked before bubbling event listeners when an event is dispatched via a slot]
expected: FAIL

View file

@ -1,7 +0,0 @@
[event-composed-path-after-dom-mutation.html]
expected: TIMEOUT
[Event.composedPath() should return the same result even if DOM is mutated (1/2)]
expected: TIMEOUT
[Event.composedPath() should return the same result even if DOM is mutated (2/2)]
expected: TIMEOUT

View file

@ -1,22 +1,4 @@
[event-composed-path.html]
[Event Path with an open ShadowRoot.]
expected: FAIL
[Event Path with a closed ShadowRoot.]
expected: FAIL
[Event Path with nested ShadowRoots: open > open.]
expected: FAIL
[Event Path with nested ShadowRoots: open > closed.]
expected: FAIL
[Event Path with nested ShadowRoots: closed > open.]
expected: FAIL
[Event Path with nested ShadowRoots: closed > closed.]
expected: FAIL
[Event Path with a slot in an open Shadow Root.]
expected: FAIL

View file

@ -1,13 +1,4 @@
[event-composed.html]
[A new events composed value should be set to false by default.]
expected: FAIL
[Users should be able to set a composed value.]
expected: FAIL
[An event should not be scoped if composed is specified]
expected: FAIL
[A synthetic MouseEvent with composed=true should not be scoped]
expected: FAIL

View file

@ -1,3 +0,0 @@
[event-dispatch-order.tentative.html]
[Event dispatch order: capture listerns should be called in capturing phase at a shadow host]
expected: FAIL

View file

@ -1,36 +0,0 @@
[event-inside-shadow-tree.html]
[Firing an event inside a grand child of a detached open mode shadow tree]
expected: FAIL
[Firing an event inside a grand child of a detached closed mode shadow tree]
expected: FAIL
[Firing an event inside a grand child of an in-document open mode shadow tree]
expected: FAIL
[Firing an event inside a grand child of an in-document closed mode shadow tree]
expected: FAIL
[Firing an event inside a detached open mode shadow tree inside open mode shadow tree]
expected: FAIL
[Firing an event inside a detached open mode shadow tree inside closed mode shadow tree]
expected: FAIL
[Firing an event inside a detached closed mode shadow tree inside open mode shadow tree]
expected: FAIL
[Firing an event inside a detached closed mode shadow tree inside closed mode shadow tree]
expected: FAIL
[Firing an event inside an in-document open mode shadow tree inside open mode shadow tree]
expected: FAIL
[Firing an event inside an in-document open mode shadow tree inside closed mode shadow tree]
expected: FAIL
[Firing an event inside an in-document closed mode shadow tree inside open mode shadow tree]
expected: FAIL
[Firing an event inside an in-document closed mode shadow tree inside closed mode shadow tree]
expected: FAIL

View file

@ -1,13 +1,4 @@
[event-post-dispatch.html]
[Event properties post dispatch with an open ShadowRoot (composed: true).]
expected: FAIL
[Event properties post dispatch with a closed ShadowRoot (composed: true).]
expected: FAIL
[Event properties post dispatch with nested ShadowRoots (composed: true).]
expected: FAIL
[Event properties post dispatch with relatedTarget in the same shadow tree. (composed: true)]
expected: FAIL