Skip to content

Commit adbdab3

Browse files
fix: align delegated events with dom-expressions roots
Register Solid portal and custom element render roots with dom-expressions root-owned delegation so events retarget correctly across body portals and shadow DOM. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 4fb7218 commit adbdab3

16 files changed

Lines changed: 330 additions & 42 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@solidjs/web": patch
3+
"babel-preset-solid": patch
4+
---
5+
6+
Bump dom-expressions and babel-plugin-jsx-dom-expressions to 0.50.0-next.12.
7+
8+
This picks up root-owned delegated event targeting: `render()` and `hydrate()` own delegated listeners for their root containers while compiler-emitted `delegateEvents([...])` declares only the delegated event names needed by compiled JSX.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@solidjs/element": patch
3+
---
4+
5+
Register custom element render roots for delegated events so handlers inside shadow DOM render roots fire correctly.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@solidjs/web": patch
3+
---
4+
5+
Portal now participates in root-owned delegated events by registering outside-root mount points as listener containers for the owning render root.

documentation/solid-2.0/07-dom.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,19 @@ DOM behavior in Solid 2.0 follows HTML standards by default: attributes over pro
4848
<some-element enabled="true" />
4949
```
5050

51+
### Root-owned event delegation
52+
53+
CamelCase JSX handlers such as `onClick` still use Solid's delegated event path, but delegation is now owned by render roots rather than by a document-global listener. `render()` / `hydrate()` install delegated listeners on their root container and dispose them when the root is disposed.
54+
55+
This matters for roots that are embedded in a larger page or hosted by web components:
56+
57+
- Rendering into a DOM element scopes delegated events to that root instead of the whole document.
58+
- Rendering into a `ShadowRoot` attaches delegated listeners to that shadow root.
59+
- `Portal` registers outside-root mount points as additional listener containers for the owning render root. Portal mounts already inside the root do not need extra listeners.
60+
- `event.stopPropagation()` inside a nested Solid root can prevent outer roots or host page code from observing the native event, while Solid avoids synthesizing events across unrelated roots.
61+
62+
Compiler-emitted `delegateEvents([...])` now declares which delegated event names are needed; it no longer chooses the physical listener target. The physical listener lifetime is owned by render roots and any framework-provided listener containers.
63+
5164
### Directives via `ref` (and removal of `use:`)
5265

5366
Solid 2.0 removes the `use:` directive namespace and instead treats “directives” as a first-class **ref** pattern. The `ref` prop becomes the single composition point for:

documentation/solid-2.0/MIGRATION.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,15 @@ const on = (type, handler, options) => el => el.addEventListener(type, handler,
549549
<button ref={on("click", handleClick, { capture: true })} />;
550550
```
551551

552+
Delegated events are now owned by render roots. `render()` and `hydrate()` install delegated listeners on their root container and clean them up when that root is disposed; compiler-emitted `delegateEvents([...])` only declares which event names are needed. This replaces the old document-global cleanup model.
553+
554+
For most apps this is automatic. The visible differences are in nested roots, portals, and web component hosts:
555+
556+
- Nested Solid roots no longer synthesize delegated handlers across each other's root boundaries.
557+
- Rendering into a `ShadowRoot` scopes delegated handlers to that shadow root, which is friendlier to web components.
558+
- `Portal` registers outside-root mount points as listener containers for the owning render root, so portal events still bubble through the logical Solid tree. Portal mounts already inside the root do not install extra listeners.
559+
- If you were calling `clearDelegatedEvents()`, remove it. Dispose the render root instead.
560+
552561
### Directives: `use:``ref` directive factories (two-phase pattern)
553562

554563
```jsx

examples/rendering/shared/src/components/Settings.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,37 @@
11
import { createSignal, createUniqueId } from "solid-js";
2+
import { Portal } from "@solidjs/web";
23

34
const Settings = () => {
45
const [text, setText] = createSignal("Hi");
6+
const [modalOpen, setModalOpen] = createSignal(false);
7+
const [modalClicks, setModalClicks] = createSignal(0);
58
const id = createUniqueId();
69

710
return (
8-
<>
11+
<section onClick={() => modalOpen() && setModalClicks(c => c + 1)}>
912
<h1>Settings</h1>
1013
<p>All that configuration you never really ever want to look at.</p>
1114
<label for={id}>Write:</label>
1215
<input type="text" id={id} value={text()} onInput={e => setText(e.currentTarget.value)} />
1316
<p>{text()}</p>
14-
</>
17+
<button type="button" onClick={() => setModalOpen(true)}>
18+
Open body portal
19+
</button>
20+
<p>Portal logical clicks: {modalClicks()}</p>
21+
{modalOpen() && (
22+
<Portal>
23+
<div class="modal-backdrop">
24+
<div class="modal-card" role="dialog" aria-modal="true" aria-label="Settings portal">
25+
<h2>Body Portal</h2>
26+
<p>This modal is portaled to document.body.</p>
27+
<button type="button" onClick={() => setModalOpen(false)}>
28+
Close portal
29+
</button>
30+
</div>
31+
</div>
32+
</Portal>
33+
)}
34+
</section>
1535
);
1636
};
1737

examples/rendering/shared/static/styles.css

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,26 @@ a.link {
7373
display: block;
7474
margin-bottom: 0.5rem;
7575
color: #337ab7;
76-
}
76+
}
77+
.modal-backdrop {
78+
position: fixed;
79+
inset: 0;
80+
display: grid;
81+
place-items: center;
82+
background: rgba(0, 0, 0, 0.45);
83+
z-index: 1000;
84+
}
85+
.modal-card {
86+
width: min(420px, calc(100vw - 2rem));
87+
padding: 1.5rem;
88+
border-radius: 8px;
89+
border: 1px solid #d0d7de;
90+
background: white;
91+
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.25);
92+
color: #444;
93+
font-family: sans-serif;
94+
}
95+
.modal-card h2 {
96+
margin-top: 0;
97+
color: #337ab7;
98+
}

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@
3636
"@rollup/plugin-replace": "^5.0.2",
3737
"@types/node": "^25.0.8",
3838
"@vitest/coverage-v8": "^4.1.6",
39-
"babel-plugin-jsx-dom-expressions": "0.50.0-next.11",
39+
"babel-plugin-jsx-dom-expressions": "0.50.0-next.12",
4040
"babel-plugin-transform-rename-import": "^2.3.0",
4141
"coveralls": "^3.1.1",
4242
"csstype": "^3.1.0",
43-
"dom-expressions": "0.50.0-next.11",
44-
"hyper-dom-expressions": "0.50.0-next.11",
43+
"dom-expressions": "0.50.0-next.12",
44+
"hyper-dom-expressions": "0.50.0-next.12",
4545
"jsdom": "^25.0.1",
4646
"ncp": "^2.0.0",
4747
"npm-run-all": "^4.1.5",
@@ -52,7 +52,7 @@
5252
"rollup-plugin-copy": "^3.4.0",
5353
"seroval": "~1.5.0",
5454
"simple-git-hooks": "^2.8.1",
55-
"sld-dom-expressions": "0.50.0-next.11",
55+
"sld-dom-expressions": "0.50.0-next.12",
5656
"symlink-dir": "^5.0.1",
5757
"tsconfig-replace-paths": "^0.0.11",
5858
"turbo": "^2.0.0",

packages/babel-preset-solid/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"test": "node test.js"
1919
},
2020
"dependencies": {
21-
"babel-plugin-jsx-dom-expressions": "0.50.0-next.11"
21+
"babel-plugin-jsx-dom-expressions": "0.50.0-next.12"
2222
},
2323
"peerDependencies": {
2424
"@babel/core": "^7.0.0",

packages/solid-element/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
"sideEffects": false,
2424
"scripts": {
2525
"clean": "rimraf dist/",
26-
"build": "pnpm run clean && tsc"
26+
"build": "pnpm run clean && tsc",
27+
"test": "vitest run"
2728
},
2829
"dependencies": {
2930
"component-register": "^0.8.7"

0 commit comments

Comments
 (0)