From 509e2032e8bc679c68e590dd6c3d8e5230c66af3 Mon Sep 17 00:00:00 2001 From: Julien Mougenot Date: Thu, 13 Nov 2025 13:58:10 +0100 Subject: [PATCH] [FIX] smarter synthetic events This commit fixes 2 behaviors regarding synthetic events: - "non-bubbling" events (such as 'mouseenter', 'focus', etc.) are not allowed anymore as synthetic events, since this would not work either way; - instead of an internal global variable to keep track of what events have already been registered with synthetic event handlers, special keys are now assigned on the document object. This has been done to avoid duplicate listeners when multiple instances of Owl are spawned. Fixes https://github.com/odoo/owl/issues/1284 --- src/runtime/blockdom/events.ts | 30 +++++++++++++++++++++------ tests/compiler/event_handling.test.ts | 11 ++++++++++ 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/runtime/blockdom/events.ts b/src/runtime/blockdom/events.ts index 9a6c73382..54f94348e 100644 --- a/src/runtime/blockdom/events.ts +++ b/src/runtime/blockdom/events.ts @@ -55,6 +55,11 @@ function createElementHandler(evName: string, capture: boolean = false): EventHa // listener per event type. let nextSyntheticEventId = 1; function createSyntheticHandler(evName: string, capture: boolean = false): EventHandlerCreator { + if (NON_BUBBLING_EVENT_NAMES.has(evName)) { + throw new Error( + `cannot set synthetic event handler for "${evName}" events: these events do not bubble by default` + ); + } let eventKey = `__event__synthetic_${evName}`; if (capture) { eventKey = `${eventKey}_capture`; @@ -88,14 +93,27 @@ function nativeToSyntheticEvent(eventKey: string, event: Event) { } } -const CONFIGURED_SYNTHETIC_EVENTS: { [event: string]: boolean } = {}; +const NON_BUBBLING_EVENT_NAMES = new Set([ + "blur", + "error", + "focus", + "hashchange", + "load", + "mouseenter", + "mouseleave", + "pointercancel", + "pointerenter", + "pointerleave", + "unload", +]); function setupSyntheticEvent(evName: string, eventKey: string, capture: boolean = false) { - if (CONFIGURED_SYNTHETIC_EVENTS[eventKey]) { + const root = document as any; + if (root[eventKey]) { return; } - document.addEventListener(evName, (event) => nativeToSyntheticEvent(eventKey, event), { - capture, - }); - CONFIGURED_SYNTHETIC_EVENTS[eventKey] = true; + const syntheticHandler = nativeToSyntheticEvent.bind(root, eventKey); + const options = { capture }; + root[eventKey] = [evName, syntheticHandler, options]; + root.addEventListener(evName, syntheticHandler, options); } diff --git a/tests/compiler/event_handling.test.ts b/tests/compiler/event_handling.test.ts index 11b98738b..82e9b4cd1 100644 --- a/tests/compiler/event_handling.test.ts +++ b/tests/compiler/event_handling.test.ts @@ -575,5 +575,16 @@ describe("t-on", () => { button.click(); expect(steps).toEqual(["btnClicked", "divClicked"]); }); + + test("non-bubbling events cannot be synthesized", () => { + const template = `
`; + + const owner = { + onMouseEnter() {}, + }; + const node = mountToFixture(template, owner); + const div = node; + div.dispatchEvent(new MouseEvent("mouseenter")); + }); }); });