fix(web): move delegated event handlers off the Solid 1 $$<type> key - #3522
Conversation
🦋 Changeset detectedLatest commit: 6d2bdeb The changes in this PR will be included in the next version bump. This PR includes changesets to release 11 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Merging this PR will not alter performance
Comparing Footnotes
|
Solid 1 delegates from `document` and fires any `$$click`/`$$input`/… it finds while walking up from the target. A 1.x runtime on the same page — an embedded widget, a devtools panel built on 1.x — therefore ran every delegated handler in a 2.x app a second time, and a 2.x root fired 1.x handlers rendered inside it. Both versions used the same key and the same walk; nothing on the event object is consulted by 1.x, so no mark can stop it. The only lever is a key it does not look for. Compiled output (Babel and native) and the runtime (`addEvent`, `eventHandler`) now stamp `_$$<type>` / `_$$<type>Data`. `_$` is the existing Solid-owned expando family (`_$host`, `_$owner`, `_$classes`, `_$styles`, `_$multiple`); the extra `$` gives event keys their own namespace so a custom delegated event can never collide with one of those. The key, the `_$SOLID_EVENT_OWNER` mark, and the walk rules are documented in client.ts as the delegated-event wire contract: shared by every Solid copy on a page and frozen going forward, so two bundles of the same major — or a future major — coordinate through the existing mark rather than needing a new key. Regression coverage carries a faithful copy of the 1.x walker on `document` beside a 2.x root (light DOM, open shadow root, 1.x content nested in a 2.x root) and loads the runtime twice as separate module instances to pin same-major nesting: single dispatch, stopPropagation across instances, outer handlers above the inner root still firing. Co-authored-by: Claude via Cursor <noreply@cursor.com>
b16f13b to
6d2bdeb
Compare
Coverage Report for CI Build 35263342049Warning No base build found for commit Coverage: 71.304%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsRequires a base build to compare against. How to fix this → Coverage Stats
💛 - Coveralls |
Problem
Solid 1 delegates from
documentand fires any$$click/$$input/ … it finds while walking up from the target. Solid 2 delegates from the render root but stamped the same$$<type>key on elements. So on any page with both:documentlistener then fires again.1.x's
eventHandlerconsults nothing on the event object besidescancelBubble(and its own hydration replay buffer while hydrating), so no "already handled" mark can stop it. Scoping our listener to the root only controls what our walker sees. The only lever that works against a runtime we can't change is a key it doesn't look for.Change
Compiled output (
@solidjs/babel-plugin,@solidjs/compiler) and the runtime (addEvent,eventHandler) now stamp_$$<type>/_$$<type>Data. Emit shape is unchanged —_el$._$$click = handler— so there is no per-element cost._$is the existing Solid-owned expando family (_$host,_$owner,_$classes,_$styles,_$multiple). The extra$gives event keys their own namespace so a user-configured custom delegated event (delegatedEvents: ["host"]) can never collide with one of those.Wire contract
The key, the
_$SOLID_EVENT_OWNERmark, and the walk rules are now documented at the top ofclient.tsas the delegated-event wire contract. It is read off the DOM and the event by whichever Solid copy is listening, so it is shared between every copy on a page — two bundles of the same major nested in each other, or a future major nested in this one — and is frozen going forward. Changing any of it means a new key prefix, not a new shape under the old one. (Prior art: Svelte hit exactly this within 5.x when a minor changed the shape of__click— sveltejs/svelte#17057.)Tests
packages/web/test/delegated-event-cross-runtime.spec.tsx:documentbeside a 2.x root — light DOM, open shadow root (the reported custom-element setup), and 1.x content nested inside a 2.x root — asserting single dispatch in each direction. All four fail on the old key (verified by flipping the runtime and compiler back and rebuilding).client.tstwice as separate module instances (?copy=2) and nests a root from one inside the other: single dispatch, inner→outer ordering,stopPropagationacross instances, outer handlers above the inner root still fire. This pins the same-major-twice case and doubles as the freeze test for the contract.Full suites pass: compiler (Rust + 5785 fixture cases), Babel plugin, web under both native and Babel JSX (client/server/hydrate),
test-types,h,html,element.Migration
Anything reading
el.$$clickdirectly must switch toel._$$click.Workarounds no longer needed
Closed shadow roots,
delegateEvents: false, androot.addEventListener(type, e => e.stopPropagation())all worked around this from the app side; after this change none are required for 1.x interop.