diff --git a/.changeset/delegated-event-restore.md b/.changeset/delegated-event-restore.md new file mode 100644 index 000000000..d86d7fc24 --- /dev/null +++ b/.changeset/delegated-event-restore.md @@ -0,0 +1,5 @@ +--- +"@solidjs/web": patch +--- + +Delegated dispatch removes its `currentTarget` and `target` overrides from the native event when its walk ends. Native listeners that run later in the same dispatch, such as ones on `document`, `window` or an element above the render root, read their own `currentTarget` again instead of the last node Solid visited. diff --git a/packages/web/src/client.ts b/packages/web/src/client.ts index 9b775a5e0..29dfd81c3 100644 --- a/packages/web/src/client.ts +++ b/packages/web/src/client.ts @@ -2975,7 +2975,6 @@ function eventHandler(e, container, state) { let node = resumeNode || e.target; const key = EVENT_KEY + e.type; - const oriTarget = e.target; const retarget = value => Object.defineProperty(e, "target", { configurable: true, @@ -3040,8 +3039,9 @@ function eventHandler(e, container, state) { } // fallback for browsers that don't support composedPath else walkUpTree(); - // Mixing portals and shadow dom can lead to a nonstandard target, so reset here. - retarget(oriTarget); + // Later native listeners (document, window, outer roots) see the real getters. + delete e.target; + delete e.currentTarget; } function insertExpression(parent, value, current, marker) { diff --git a/packages/web/test/delegated-event-restore.spec.tsx b/packages/web/test/delegated-event-restore.spec.tsx new file mode 100644 index 000000000..ada3773a7 --- /dev/null +++ b/packages/web/test/delegated-event-restore.spec.tsx @@ -0,0 +1,106 @@ +/** + * @jsxImportSource @solidjs/web + * @vitest-environment jsdom + */ +import { describe, expect, test } from "vitest"; +import { render } from "@solidjs/web"; + +describe("delegated dispatch restores the native Event", () => { + test("a native listener above the render root sees its own currentTarget", () => { + const root = document.createElement("div"); + document.body.appendChild(root); + let btn!: HTMLButtonElement; + const seen: any[] = []; + const listener = (e: Event) => seen.push(e.currentTarget); + document.addEventListener("click", listener); + const dispose = render( + () => ( +
+
+ ), + root + ); + btn.click(); + document.removeEventListener("click", listener); + dispose(); + root.remove(); + expect(seen[0]).toBe(document); + }); + + test("a native listener on the root's ancestor sees its own currentTarget", () => { + const outer = document.createElement("div"); + const root = document.createElement("div"); + outer.appendChild(root); + document.body.appendChild(outer); + let btn!: HTMLButtonElement; + const seen: any[] = []; + outer.addEventListener("click", e => seen.push(e.currentTarget)); + const dispose = render(() =>