lib: implement passive listener behavior per spec

Implements the WHATWG DOM specification for passive event listeners,
ensuring that calls to `preventDefault()` are correctly ignored within
a passive listener context.

An internal `kInPassiveListener` state is added to the Event object
to track when a passive listener is executing. The `preventDefault()`
method and the `returnValue` setter are modified to check this state,
as well as the event's `cancelable` property. This state is reliably
cleaned up within a `finally` block to prevent state pollution in
case a listener throws an error.

This resolves previously failing Web Platform Tests (WPT) in
`AddEventListenerOptions-passive.any.js`.

Refs: https://dom.spec.whatwg.org/#dom-event-preventdefault
PR-URL: https://github.com/nodejs/node/pull/59995
Reviewed-By: Daeyeon Jeong <daeyeon.dev@gmail.com>
Reviewed-By: Mattias Buelens <mattias@buelens.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Jason Zhang <xzha4350@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
This commit is contained in:
BCD1me 2025-10-04 12:17:35 +09:00 committed by GitHub
parent 60f1a5d077
commit 6f941fcfba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 24 additions and 13 deletions

View File

@ -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;

View File

@ -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);

View File

@ -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"
},