diff --git a/src/diff/props.js b/src/diff/props.js index db2f1f821d..0ff5b2d26a 100644 --- a/src/diff/props.js +++ b/src/diff/props.js @@ -83,7 +83,8 @@ export function setProperty(dom, name, value, oldValue, isSvg) { } // Benchmark for comparison: https://esbench.com/bench/574c954bdb965b9a00965ac6 else if (name[0] === 'o' && name[1] === 'n') { - useCapture = name !== (name = name.replace(/(PointerCapture)$|Capture$/, '$1')); + useCapture = + name !== (name = name.replace(/(PointerCapture)$|Capture$/, '$1')); // Infer correct casing for DOM built-in events: if (name.toLowerCase() in dom) name = name.toLowerCase().slice(2); @@ -94,8 +95,11 @@ export function setProperty(dom, name, value, oldValue, isSvg) { if (value) { if (!oldValue) { + value._attached = Date.now(); const handler = useCapture ? eventProxyCapture : eventProxy; dom.addEventListener(name, handler, useCapture); + } else { + value._attached = oldValue._attached; } } else { const handler = useCapture ? eventProxyCapture : eventProxy; @@ -151,7 +155,22 @@ export function setProperty(dom, name, value, oldValue, isSvg) { * @private */ function eventProxy(e) { - return this._listeners[e.type + false](options.event ? options.event(e) : e); + const eventHandler = this._listeners[e.type + false]; + /** + * This trick is inspired by Vue https://github.com/vuejs/core/blob/main/packages/runtime-dom/src/modules/events.ts#L90-L101 + * when the dom performs an event it leaves micro-ticks in between bubbling up which means that an event can trigger on a newly + * created DOM-node while the event bubbles up, this can cause quirky behavior as seen in https://github.com/preactjs/preact/issues/3927 + */ + if (!e._dispatched) { + // When an event has no _dispatched we know this is the first event-target in the chain + // so we set the initial dispatched time. + e._dispatched = Date.now(); + // When the _dispatched is smaller than the time when the targetted event handler was attached + // we know we have bubbled up to an element that was added during patching the dom. + } else if (e._dispatched <= eventHandler._attached) { + return; + } + return eventHandler(options.event ? options.event(e) : e); } function eventProxyCapture(e) {