Repository: nodejs/node
Pull request: lib: implement passive listener behavior per spec

Implement Web API spec-compliant behavior
for passive event listeners
where preventDefault() and returnValue setting are ignored during
passive listener execution.

- Add kInPassiveListener symbol to track passive listener state
- Modify preventDefault() to ignore calls during
passive listener execution
- Add returnValue setter that respects passive listener state
- Set/clear passive flag around listener invocation in dispatchEvent

Fixes WPT test cases that were previously failing in
AddEventListenerOptions-passive.any.js and
satisfies expected behaviors:

- preventDefault should be ignored
if-and-only-if the passive option is true
- returnValue should be ignored
if-and-only-if the passive option is true
- passive behavior of one listener should be unaffected
by other listeners

Refs: https://dom.spec.whatwg.org/#dom-event-preventdefault

Changed files:
filename: lib/internal/event_target.js
patch: @@ -76,6 +76,7 @@ const { now } = require('internal/perf/utils');
 
 const kType = Symbol('type');
 const kDetail = Symbol('detail');
+const kInPassiveListener = Symbol('kInPassiveListener');
 
 const isTrustedSet = new SafeWeakSet();
 const isTrusted = ObjectGetOwnPropertyDescriptor({
@@ -127,6 +128,7 @@ class Event {
 
     this[kTarget] = null;
     this[kIsBeingDispatched] = false;
+    this[kInPassiveListener] = false;
   }
 
   /**
@@ -178,6 +180,7 @@ class Event {
   preventDefault() {
     if (!isEvent(this))
       throw new ERR_INVALID_THIS('Event');
+    if (!this.#cancelable || this[kInPassiveListener]) return;
     this.#defaultPrevented = true;
   }
 
@@ -266,6 +269,19 @@ class Event {
     return !this.#cancelable || !this.#defaultPrevented;
   }
 
+  /**
+   * @type {boolean}
+   */
+  set returnValue(value) {
+    if (!isEvent(this))
+      throw new ERR_INVALID_THIS('Event');
+
+    if (!value) {
+      if (!this.#cancelable || this[kInPassiveListener]) return;
+      this.#defaultPrevented = true;
+    }
+  }
+
   /**
    * @type {boolean}
    */
@@ -760,7 +776,6 @@ class EventTarget {
       throw new ERR_EVENT_RECURSION(event.type);
 
     this[kHybridDispatch](event, event.type, event);
-
     return event.defaultPrevented !== true;
   }
 
@@ -813,8 +828,8 @@ class EventTarget {
         this[kRemoveListener](root.size, type, listener, capture);
       }
 
+      let arg;
       try {
-        let arg;
         if (handler.isNodeStyleListener) {
           arg = nodeValue;
         } else {
@@ -824,6 +839,9 @@ class EventTarget {
           handler.callback.deref() : handler.callback;
         let result;
         if (callback) {
+          if (handler.passive && !handler.isNodeStyleListener) {
+            arg[kInPassiveListener] = true;
+          }
           result = FunctionPrototypeCall(callback, this, arg);
           if (!handler.isNodeStyleListener) {
             arg[kIsBeingDispatched] = false;
@@ -833,6 +851,9 @@ class EventTarget {
           addCatch(result);
       } catch (err) {
         emitUncaughtException(err);
+      } finally {
+        if (arg?.[kInPassiveListener])
+          arg[kInPassiveListener] = false;
       }
 
       handler = next;

filename: test/parallel/test-whatwg-events-add-event-listener-options-passive.js
patch: @@ -1,6 +1,6 @@
 'use strict';
 
-const common = require('../common');
+require('../common');
 
 // Manually converted from https://github.com/web-platform-tests/wpt/blob/master/dom/events/AddEventListenerOptions-passive.html
 // in order to define the `document` ourselves
@@ -58,7 +58,6 @@ const {
   testPassiveValue({}, true);
   testPassiveValue({ passive: false }, true);
 
-  common.skip('TODO: passive listeners is still broken');
   testPassiveValue({ passive: 1 }, false);
   testPassiveValue({ passive: true }, false);
   testPassiveValue({ passive: 0 }, true);

filename: test/wpt/status/dom/events.json
patch: @@ -1,13 +1,4 @@
 {
-  "AddEventListenerOptions-passive.any.js": {
-    "fail": {
-      "expected": [
-        "preventDefault should be ignored if-and-only-if the passive option is true",
-        "returnValue should be ignored if-and-only-if the passive option is true",
-        "passive behavior of one listener should be unaffected by the presence of other listeners"
-      ]
-    }
-  },
   "Event-dispatch-listener-order.window.js": {
     "skip": "document is not defined"
   },