mirror of
https://github.com/servo/servo.git
synced 2025-08-05 13:40:08 +01:00
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:
parent
aed7e8cefd
commit
1b882f2729
16 changed files with 89 additions and 144 deletions
|
@ -506,6 +506,7 @@ impl Animations {
|
||||||
let parent = EventInit {
|
let parent = EventInit {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
cancelable: false,
|
cancelable: false,
|
||||||
|
composed: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let property_or_animation_name =
|
let property_or_animation_name =
|
||||||
|
|
|
@ -73,6 +73,9 @@ pub(crate) struct Event {
|
||||||
/// <https://dom.spec.whatwg.org/#dom-event-bubbles>
|
/// <https://dom.spec.whatwg.org/#dom-event-bubbles>
|
||||||
bubbles: Cell<bool>,
|
bubbles: Cell<bool>,
|
||||||
|
|
||||||
|
/// <https://dom.spec.whatwg.org/#dom-event-composed>
|
||||||
|
composed: Cell<bool>,
|
||||||
|
|
||||||
/// <https://dom.spec.whatwg.org/#dom-event-istrusted>
|
/// <https://dom.spec.whatwg.org/#dom-event-istrusted>
|
||||||
is_trusted: Cell<bool>,
|
is_trusted: Cell<bool>,
|
||||||
|
|
||||||
|
@ -129,6 +132,7 @@ impl Event {
|
||||||
stop_immediate_propagation: Cell::new(false),
|
stop_immediate_propagation: Cell::new(false),
|
||||||
cancelable: Cell::new(false),
|
cancelable: Cell::new(false),
|
||||||
bubbles: Cell::new(false),
|
bubbles: Cell::new(false),
|
||||||
|
composed: Cell::new(false),
|
||||||
is_trusted: Cell::new(false),
|
is_trusted: Cell::new(false),
|
||||||
dispatch: Cell::new(false),
|
dispatch: Cell::new(false),
|
||||||
initialized: Cell::new(false),
|
initialized: Cell::new(false),
|
||||||
|
@ -169,6 +173,8 @@ impl Event {
|
||||||
can_gc: CanGc,
|
can_gc: CanGc,
|
||||||
) -> DomRoot<Event> {
|
) -> DomRoot<Event> {
|
||||||
let event = Event::new_uninitialized_with_proto(global, proto, can_gc);
|
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.init_event(type_, bool::from(bubbles), bool::from(cancelable));
|
||||||
event
|
event
|
||||||
}
|
}
|
||||||
|
@ -622,10 +628,68 @@ impl Event {
|
||||||
self.set_trusted(true);
|
self.set_trusted(true);
|
||||||
target.dispatch_event(self, can_gc)
|
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 event’s initialized flag.
|
||||||
|
event.initialized.set(true);
|
||||||
|
|
||||||
|
// Step 3. Initialize event’s timeStamp attribute to the relative high resolution
|
||||||
|
// coarse time given time and event’s 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 root’s get the parent algorithm, given an event, returns null if event’s composed flag
|
||||||
|
// > is unset and shadow root is the root of event’s path’s first struct’s invocation target;
|
||||||
|
// > otherwise shadow root’s 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 {
|
impl EventMethods<crate::DomTypeHolder> for Event {
|
||||||
/// <https://dom.spec.whatwg.org/#dom-event-event>
|
/// <https://dom.spec.whatwg.org/#concept-event-constructor>
|
||||||
fn Constructor(
|
fn Constructor(
|
||||||
global: &GlobalScope,
|
global: &GlobalScope,
|
||||||
proto: Option<HandleObject>,
|
proto: Option<HandleObject>,
|
||||||
|
@ -633,16 +697,15 @@ impl EventMethods<crate::DomTypeHolder> for Event {
|
||||||
type_: DOMString,
|
type_: DOMString,
|
||||||
init: &EventBinding::EventInit,
|
init: &EventBinding::EventInit,
|
||||||
) -> Fallible<DomRoot<Event>> {
|
) -> Fallible<DomRoot<Event>> {
|
||||||
let bubbles = EventBubbles::from(init.bubbles);
|
// Step 1. Let event be the result of running the inner event creation steps with
|
||||||
let cancelable = EventCancelable::from(init.cancelable);
|
// this interface, null, now, and eventInitDict.
|
||||||
Ok(Event::new_with_proto(
|
let event = Event::inner_creation_steps(global, proto, init, can_gc);
|
||||||
global,
|
|
||||||
proto,
|
// Step 2. Initialize event’s type attribute to type.
|
||||||
Atom::from(type_),
|
*event.type_.borrow_mut() = Atom::from(type_);
|
||||||
bubbles,
|
|
||||||
cancelable,
|
// Step 3. Return event.
|
||||||
can_gc,
|
Ok(event)
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <https://dom.spec.whatwg.org/#dom-event-eventphase>
|
/// <https://dom.spec.whatwg.org/#dom-event-eventphase>
|
||||||
|
@ -805,6 +868,11 @@ impl EventMethods<crate::DomTypeHolder> for Event {
|
||||||
self.canceled.get() == EventDefault::Prevented
|
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>
|
/// <https://dom.spec.whatwg.org/#dom-event-preventdefault>
|
||||||
fn PreventDefault(&self) {
|
fn PreventDefault(&self) {
|
||||||
if self.cancelable.get() {
|
if self.cancelable.get() {
|
||||||
|
|
|
@ -805,12 +805,14 @@ impl EventTarget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.downcast::<ShadowRoot>().is_some() {
|
if let Some(shadow_root) = self.downcast::<ShadowRoot>() {
|
||||||
// FIXME: Handle event composed flag here
|
if event.should_pass_shadow_boundary(shadow_root) {
|
||||||
// We currently assume that events are never composed (so events may never
|
let host = shadow_root.Host();
|
||||||
// cross a shadow boundary)
|
return Some(DomRoot::from_ref(host.upcast::<EventTarget>()));
|
||||||
|
} else {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(node) = self.downcast::<Node>() {
|
if let Some(node) = self.downcast::<Node>() {
|
||||||
// FIXME: Handle slottables here
|
// FIXME: Handle slottables here
|
||||||
|
|
|
@ -34,6 +34,7 @@ interface Event {
|
||||||
undefined preventDefault();
|
undefined preventDefault();
|
||||||
[Pure]
|
[Pure]
|
||||||
readonly attribute boolean defaultPrevented;
|
readonly attribute boolean defaultPrevented;
|
||||||
|
readonly attribute boolean composed;
|
||||||
|
|
||||||
[LegacyUnforgeable]
|
[LegacyUnforgeable]
|
||||||
readonly attribute boolean isTrusted;
|
readonly attribute boolean isTrusted;
|
||||||
|
@ -46,4 +47,5 @@ interface Event {
|
||||||
dictionary EventInit {
|
dictionary EventInit {
|
||||||
boolean bubbles = false;
|
boolean bubbles = false;
|
||||||
boolean cancelable = false;
|
boolean cancelable = false;
|
||||||
|
boolean composed = false;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
[event-global-extra.window.html]
|
|
||||||
[window.event should not be affected by nodes moving post-dispatch]
|
|
||||||
expected: FAIL
|
|
|
@ -1,7 +1,3 @@
|
||||||
[event-global.html]
|
[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)]
|
[window.event is undefined inside window.onerror if the target is in a shadow tree (ErrorEvent dispatched inside shadow tree)]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
9
tests/wpt/meta/dom/idlharness.any.js.ini
vendored
9
tests/wpt/meta/dom/idlharness.any.js.ini
vendored
|
@ -5,15 +5,6 @@
|
||||||
expected: ERROR
|
expected: ERROR
|
||||||
|
|
||||||
[idlharness.any.worker.html]
|
[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]
|
[AbortController interface: attribute signal]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
||||||
|
|
12
tests/wpt/meta/dom/idlharness.window.js.ini
vendored
12
tests/wpt/meta/dom/idlharness.window.js.ini
vendored
|
@ -8,9 +8,6 @@
|
||||||
[AbortSignal must be primary interface of new AbortController().signal]
|
[AbortSignal must be primary interface of new AbortController().signal]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
||||||
[Event interface: attribute composed]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[AbortSignal interface: existence and properties of interface prototype object's @@unscopables property]
|
[AbortSignal interface: existence and properties of interface prototype object's @@unscopables property]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
||||||
|
@ -32,9 +29,6 @@
|
||||||
[AbortSignal interface object name]
|
[AbortSignal interface object name]
|
||||||
expected: FAIL
|
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]
|
[EventTarget interface: new AbortController().signal must inherit property "removeEventListener(DOMString, EventListener?, optional (EventListenerOptions or boolean))" with the proper type]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
||||||
|
@ -116,9 +110,6 @@
|
||||||
[AbortSignal interface: existence and properties of interface prototype object]
|
[AbortSignal interface: existence and properties of interface prototype object]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
||||||
[Event interface: new Event("foo") must inherit property "composed" with the proper type]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[Element interface: operation remove()]
|
[Element interface: operation remove()]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
||||||
|
@ -146,9 +137,6 @@
|
||||||
[Document interface: operation append((Node or DOMString)...)]
|
[Document interface: operation append((Node or DOMString)...)]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
||||||
[Event interface: document.createEvent("Event") must inherit property "composed" with the proper type]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[DocumentType interface: operation remove()]
|
[DocumentType interface: operation remove()]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,4 @@
|
||||||
[Extensions-to-Event-Interface.html]
|
[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]
|
[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
|
expected: FAIL
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,4 @@
|
||||||
[capturing-and-bubbling-event-listeners-across-shadow-trees.html]
|
[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]
|
[Capturing event listeners should be invoked before bubbling event listeners when an event is dispatched via a slot]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
|
@ -1,22 +1,4 @@
|
||||||
[event-composed-path.html]
|
[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.]
|
[Event Path with a slot in an open Shadow Root.]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,4 @@
|
||||||
[event-composed.html]
|
[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]
|
[A synthetic MouseEvent with composed=true should not be scoped]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -1,13 +1,4 @@
|
||||||
[event-post-dispatch.html]
|
[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)]
|
[Event properties post dispatch with relatedTarget in the same shadow tree. (composed: true)]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue