From 7fc5dc5c6961280861eaa51b14dc5e67f932875f Mon Sep 17 00:00:00 2001 From: shanehandley Date: Mon, 10 Mar 2025 20:44:16 +1100 Subject: [PATCH] script: use passive event listener option on AddEventListenerOptions (#35877) Signed-off-by: Shane Handley --- components/script/dom/event.rs | 47 +++-- components/script/dom/eventtarget.rs | 57 +++++- components/script/dom/mediaquerylist.rs | 1 + .../webidls/EventTarget.webidl | 2 +- ...AddEventListenerOptions-passive.any.js.ini | 26 --- .../dom/events/passive-by-default.html.ini | 171 ------------------ 6 files changed, 87 insertions(+), 217 deletions(-) delete mode 100644 tests/wpt/meta/dom/events/AddEventListenerOptions-passive.any.js.ini delete mode 100644 tests/wpt/meta/dom/events/passive-by-default.html.ini diff --git a/components/script/dom/event.rs b/components/script/dom/event.rs index 896970fec54..00b15f85a82 100644 --- a/components/script/dom/event.rs +++ b/components/script/dom/event.rs @@ -95,6 +95,9 @@ pub(crate) struct Event { /// related_target: MutNullableDom, + + /// + in_passive_listener: Cell, } /// An element on an [event path](https://dom.spec.whatwg.org/#event-path) @@ -140,6 +143,7 @@ impl Event { time_stamp: CrossProcessInstant::now(), path: DomRefCell::default(), related_target: Default::default(), + in_passive_listener: Cell::new(false), } } @@ -217,6 +221,10 @@ impl Event { self.target.set(target_); } + pub(crate) fn set_in_passive_listener(&self, value: bool) { + self.in_passive_listener.set(value); + } + /// #[cfg_attr(crown, allow(crown::unrooted_must_root))] pub(crate) fn append_to_path( @@ -728,6 +736,13 @@ impl Event { &*target_node.GetRootNode(&GetRootNodeOptions::empty()) != shadow_root.upcast::() } + + /// + fn set_the_cancelled_flag(&self) { + if self.cancelable.get() && !self.in_passive_listener.get() { + self.canceled.set(EventDefault::Prevented) + } + } } impl EventMethods for Event { @@ -917,9 +932,7 @@ impl EventMethods for Event { /// fn PreventDefault(&self) { - if self.cancelable.get() { - self.canceled.set(EventDefault::Prevented) - } + self.set_the_cancelled_flag(); } /// @@ -951,7 +964,7 @@ impl EventMethods for Event { /// fn SetReturnValue(&self, val: bool) { if !val { - self.PreventDefault(); + self.set_the_cancelled_flag(); } } @@ -1224,13 +1237,14 @@ fn inner_invoke( // TODO Step 2.3 If phase is "capturing" and listener’s capture is false, then continue. // TODO Step 2.4 If phase is "bubbling" and listener’s capture is true, then continue. + let event_target = event + .GetCurrentTarget() + .expect("event target was initialized as part of \"invoke\""); + // Step 2.5 If listener’s once is true, then remove an event listener given event’s currentTarget // attribute value and listener. if let CompiledEventListener::Listener(event_listener) = listener { - event - .GetCurrentTarget() - .expect("event target was initialized as part of \"invoke\"") - .remove_listener_if_once(&event.type_(), event_listener); + event_target.remove_listener_if_once(&event.type_(), event_listener); } // Step 2.6 Let global be listener callback’s associated realm’s global object. @@ -1249,7 +1263,10 @@ fn inner_invoke( } } - // TODO Step 2.9 If listener’s passive is true, then set event’s in passive listener flag. + // Step 2.9 If listener’s passive is true, then set event's in passive listener flag. + if let CompiledEventListener::Listener(event_listener) = listener { + event.set_in_passive_listener(event_target.is_passive(&event.type_(), event_listener)); + } // Step 2.10 If global is a Window object, then record timing info for event listener given event and listener. // Step 2.11 Call a user object’s operation with listener’s callback, "handleEvent", « event », @@ -1258,19 +1275,13 @@ fn inner_invoke( // associated realm’s global object. // TODO Step 2.10.2 Set legacyOutputDidListenersThrowFlag if given. let marker = TimelineMarker::start("DOMEvent".to_owned()); - listener.call_or_handle_event( - &event - .GetCurrentTarget() - .expect("event target was initialized as part of \"invoke\""), - event, - ExceptionHandling::Report, - can_gc, - ); + listener.call_or_handle_event(&event_target, event, ExceptionHandling::Report, can_gc); if let Some(window) = timeline_window { window.emit_timeline_marker(marker.end()); } - // TODO Step 2.12 Unset event’s in passive listener flag. + // Step 2.12 Unset event’s in passive listener flag. + event.set_in_passive_listener(false); // Step 2.13 If global is a Window object, then set global’s current event to currentEvent. if let Some(window) = global.downcast::() { diff --git a/components/script/dom/eventtarget.rs b/components/script/dom/eventtarget.rs index f8d01f95553..db705a7786e 100644 --- a/components/script/dom/eventtarget.rs +++ b/components/script/dom/eventtarget.rs @@ -23,6 +23,7 @@ use js::rust::{ use libc::c_char; use servo_atoms::Atom; use servo_url::ServoUrl; +use style::str::HTML_SPACE_CHARACTERS; use crate::dom::beforeunloadevent::BeforeUnloadEvent; use crate::dom::bindings::callback::{CallbackContainer, CallbackFunction, ExceptionHandling}; @@ -41,6 +42,7 @@ use crate::dom::bindings::codegen::Bindings::NodeBinding::GetRootNodeOptions; use crate::dom::bindings::codegen::Bindings::NodeBinding::Node_Binding::NodeMethods; use crate::dom::bindings::codegen::Bindings::ShadowRootBinding::ShadowRoot_Binding::ShadowRootMethods; use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods; +use crate::dom::bindings::codegen::GenericBindings::DocumentBinding::Document_Binding::DocumentMethods; use crate::dom::bindings::codegen::UnionTypes::{ AddEventListenerOptionsOrBoolean, EventListenerOptionsOrBoolean, EventOrString, }; @@ -308,6 +310,7 @@ struct EventListenerEntry { phase: ListenerPhase, listener: EventListenerType, once: bool, + passive: Option, } impl std::cmp::PartialEq for EventListenerEntry { @@ -438,6 +441,42 @@ impl EventTarget { *self.handlers.borrow_mut() = Default::default(); } + /// + fn default_passive_value(&self, ty: &Atom) -> bool { + // Return true if all of the following are true: + let event_type = ty.to_ascii_lowercase(); + + // type is one of "touchstart", "touchmove", "wheel", or "mousewheel" + let matches_event_type = matches!( + event_type.trim_matches(HTML_SPACE_CHARACTERS), + "touchstart" | "touchmove" | "wheel" | "mousewheel" + ); + + if !matches_event_type { + return false; + } + + // eventTarget is a Window object + if self.is::() { + return true; + } + + // or ... + if let Some(node) = self.downcast::() { + let node_document = node.owner_document(); + let event_target = self.upcast::(); + + // is a node whose node document is eventTarget + return event_target == node_document.upcast::() + // or is a node whose node document’s document element is eventTarget + || node_document.GetDocumentElement().is_some_and(|n| n.upcast::() == event_target) + // or is a node whose node document’s body element is eventTarget + || node_document.GetBody().is_some_and(|n| n.upcast::() == event_target); + } + + false + } + /// fn set_inline_event_listener(&self, ty: Atom, listener: Option) { let mut handlers = self.handlers.borrow_mut(); @@ -467,6 +506,7 @@ impl EventTarget { phase: ListenerPhase::Bubbling, listener: EventListenerType::Inline(listener.into()), once: false, + passive: None, }); } }, @@ -482,6 +522,17 @@ impl EventTarget { } } + /// Determines the `passive` attribute of an associated event listener + pub(crate) fn is_passive(&self, ty: &Atom, listener: &Rc) -> bool { + let handlers = self.handlers.borrow(); + let listener_instance = EventListenerType::Additive(listener.clone()); + + handlers + .get(ty) + .and_then(|entries| entries.iter().find(|e| e.listener == listener_instance)) + .is_some_and(|entry| entry.passive.unwrap_or(self.default_passive_value(ty))) + } + fn get_inline_event_listener(&self, ty: &Atom, can_gc: CanGc) -> Option { let handlers = self.handlers.borrow(); handlers @@ -745,7 +796,8 @@ impl EventTarget { event.fire(self, can_gc); event } - // https://dom.spec.whatwg.org/#dom-eventtarget-addeventlistener + + /// pub(crate) fn add_event_listener( &self, ty: DOMString, @@ -771,6 +823,7 @@ impl EventTarget { phase, listener: EventListenerType::Additive(listener), once: options.once, + passive: options.passive, }; if !entry.contains(&new_entry) { entry.push(new_entry); @@ -799,6 +852,7 @@ impl EventTarget { phase, listener: EventListenerType::Additive(listener.clone()), once: false, + passive: None, }; if let Some(position) = entry.iter().position(|e| *e == old_entry) { entry.remove(position); @@ -929,6 +983,7 @@ impl From for AddEventListenerOptions { AddEventListenerOptionsOrBoolean::Boolean(capture) => Self { parent: EventListenerOptions { capture }, once: false, + passive: None, }, } } diff --git a/components/script/dom/mediaquerylist.rs b/components/script/dom/mediaquerylist.rs index 0c343f56f33..53b0915c5a5 100644 --- a/components/script/dom/mediaquerylist.rs +++ b/components/script/dom/mediaquerylist.rs @@ -106,6 +106,7 @@ impl MediaQueryListMethods for MediaQueryList { AddEventListenerOptions { parent: EventListenerOptions { capture: false }, once: false, + passive: None, }, ); } diff --git a/components/script_bindings/webidls/EventTarget.webidl b/components/script_bindings/webidls/EventTarget.webidl index 82e8c967483..a8a63cc2b32 100644 --- a/components/script_bindings/webidls/EventTarget.webidl +++ b/components/script_bindings/webidls/EventTarget.webidl @@ -29,6 +29,6 @@ dictionary EventListenerOptions { }; dictionary AddEventListenerOptions : EventListenerOptions { - // boolean passive = false; + boolean passive; boolean once = false; }; diff --git a/tests/wpt/meta/dom/events/AddEventListenerOptions-passive.any.js.ini b/tests/wpt/meta/dom/events/AddEventListenerOptions-passive.any.js.ini deleted file mode 100644 index 58bb9b7d72f..00000000000 --- a/tests/wpt/meta/dom/events/AddEventListenerOptions-passive.any.js.ini +++ /dev/null @@ -1,26 +0,0 @@ -[AddEventListenerOptions-passive.any.worker.html] - [Supports passive option on addEventListener only] - expected: FAIL - - [preventDefault should be ignored if-and-only-if the passive option is true] - expected: FAIL - - [returnValue should be ignored if-and-only-if the passive option is true] - expected: FAIL - - [passive behavior of one listener should be unaffected by the presence of other listeners] - expected: FAIL - - -[AddEventListenerOptions-passive.any.html] - [Supports passive option on addEventListener only] - expected: FAIL - - [preventDefault should be ignored if-and-only-if the passive option is true] - expected: FAIL - - [returnValue should be ignored if-and-only-if the passive option is true] - expected: FAIL - - [passive behavior of one listener should be unaffected by the presence of other listeners] - expected: FAIL diff --git a/tests/wpt/meta/dom/events/passive-by-default.html.ini b/tests/wpt/meta/dom/events/passive-by-default.html.ini deleted file mode 100644 index 00cc677c111..00000000000 --- a/tests/wpt/meta/dom/events/passive-by-default.html.ini +++ /dev/null @@ -1,171 +0,0 @@ -[passive-by-default.html] - [touchstart listener is passive by default for Window] - expected: FAIL - - [touchstart listener is passive with {passive:undefined} for Window] - expected: FAIL - - [touchstart listener is passive with {passive:true} for Window] - expected: FAIL - - [touchstart listener is passive by default for Document] - expected: FAIL - - [touchstart listener is passive with {passive:undefined} for Document] - expected: FAIL - - [touchstart listener is passive with {passive:true} for Document] - expected: FAIL - - [touchstart listener is passive by default for HTMLHtmlElement] - expected: FAIL - - [touchstart listener is passive with {passive:undefined} for HTMLHtmlElement] - expected: FAIL - - [touchstart listener is passive with {passive:true} for HTMLHtmlElement] - expected: FAIL - - [touchstart listener is passive by default for HTMLBodyElement] - expected: FAIL - - [touchstart listener is passive with {passive:undefined} for HTMLBodyElement] - expected: FAIL - - [touchstart listener is passive with {passive:true} for HTMLBodyElement] - expected: FAIL - - [touchstart listener is passive with {passive:true} for HTMLDivElement] - expected: FAIL - - [touchmove listener is passive by default for Window] - expected: FAIL - - [touchmove listener is passive with {passive:undefined} for Window] - expected: FAIL - - [touchmove listener is passive with {passive:true} for Window] - expected: FAIL - - [touchmove listener is passive by default for Document] - expected: FAIL - - [touchmove listener is passive with {passive:undefined} for Document] - expected: FAIL - - [touchmove listener is passive with {passive:true} for Document] - expected: FAIL - - [touchmove listener is passive by default for HTMLHtmlElement] - expected: FAIL - - [touchmove listener is passive with {passive:undefined} for HTMLHtmlElement] - expected: FAIL - - [touchmove listener is passive with {passive:true} for HTMLHtmlElement] - expected: FAIL - - [touchmove listener is passive by default for HTMLBodyElement] - expected: FAIL - - [touchmove listener is passive with {passive:undefined} for HTMLBodyElement] - expected: FAIL - - [touchmove listener is passive with {passive:true} for HTMLBodyElement] - expected: FAIL - - [touchmove listener is passive with {passive:true} for HTMLDivElement] - expected: FAIL - - [wheel listener is passive by default for Window] - expected: FAIL - - [wheel listener is passive with {passive:undefined} for Window] - expected: FAIL - - [wheel listener is passive with {passive:true} for Window] - expected: FAIL - - [wheel listener is passive by default for Document] - expected: FAIL - - [wheel listener is passive with {passive:undefined} for Document] - expected: FAIL - - [wheel listener is passive with {passive:true} for Document] - expected: FAIL - - [wheel listener is passive by default for HTMLHtmlElement] - expected: FAIL - - [wheel listener is passive with {passive:undefined} for HTMLHtmlElement] - expected: FAIL - - [wheel listener is passive with {passive:true} for HTMLHtmlElement] - expected: FAIL - - [wheel listener is passive by default for HTMLBodyElement] - expected: FAIL - - [wheel listener is passive with {passive:undefined} for HTMLBodyElement] - expected: FAIL - - [wheel listener is passive with {passive:true} for HTMLBodyElement] - expected: FAIL - - [wheel listener is passive with {passive:true} for HTMLDivElement] - expected: FAIL - - [mousewheel listener is passive by default for Window] - expected: FAIL - - [mousewheel listener is passive with {passive:undefined} for Window] - expected: FAIL - - [mousewheel listener is passive with {passive:true} for Window] - expected: FAIL - - [mousewheel listener is passive by default for Document] - expected: FAIL - - [mousewheel listener is passive with {passive:undefined} for Document] - expected: FAIL - - [mousewheel listener is passive with {passive:true} for Document] - expected: FAIL - - [mousewheel listener is passive by default for HTMLHtmlElement] - expected: FAIL - - [mousewheel listener is passive with {passive:undefined} for HTMLHtmlElement] - expected: FAIL - - [mousewheel listener is passive with {passive:true} for HTMLHtmlElement] - expected: FAIL - - [mousewheel listener is passive by default for HTMLBodyElement] - expected: FAIL - - [mousewheel listener is passive with {passive:undefined} for HTMLBodyElement] - expected: FAIL - - [mousewheel listener is passive with {passive:true} for HTMLBodyElement] - expected: FAIL - - [mousewheel listener is passive with {passive:true} for HTMLDivElement] - expected: FAIL - - [touchend listener is passive with {passive:true} for Window] - expected: FAIL - - [touchend listener is passive with {passive:true} for Document] - expected: FAIL - - [touchend listener is passive with {passive:true} for HTMLHtmlElement] - expected: FAIL - - [touchend listener is passive with {passive:true} for HTMLBodyElement] - expected: FAIL - - [touchend listener is passive with {passive:true} for HTMLDivElement] - expected: FAIL