Skip to content

Commit 8e05a6f

Browse files
authored
Ensure useId works in portals (#4752)
1 parent 8aac5fa commit 8e05a6f

File tree

2 files changed

+51
-9
lines changed

2 files changed

+51
-9
lines changed

compat/src/portals.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,20 @@ function Portal(props) {
3232
}
3333

3434
if (!_this._temp) {
35+
// Ensure the element has a mask for useId invocations
36+
let root = _this._vnode;
37+
while (root !== null && !root._mask && root._parent !== null) {
38+
root = root._parent;
39+
}
40+
3541
_this._container = container;
3642

3743
// Create a fake DOM parent node that manages a subset of `container`'s children:
3844
_this._temp = {
3945
nodeType: 1,
4046
parentNode: container,
4147
childNodes: [],
48+
_children: { _mask: root._mask },
4249
contains: () => true,
4350
// Technically this isn't needed
4451
appendChild(child) {

compat/test/browser/portals.test.js

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ import React, {
55
useState,
66
Component,
77
useEffect,
8-
Fragment
8+
Fragment,
9+
useId
910
} from 'preact/compat';
1011
import { setupScratch, teardown } from '../../../test/_util/helpers';
1112
import { setupRerender, act } from 'preact/test-utils';
13+
import { expect } from 'chai';
1214

1315
/* eslint-disable react/jsx-boolean-value, react/display-name, prefer-arrow-callback */
1416

@@ -212,6 +214,38 @@ describe('Portal', () => {
212214
expect(scratch.firstChild.firstChild.childNodes.length).to.equal(0);
213215
});
214216

217+
it('should have unique ids for each portal', () => {
218+
let root = document.createElement('div');
219+
let dialog = document.createElement('div');
220+
dialog.id = 'container';
221+
222+
scratch.appendChild(root);
223+
scratch.appendChild(dialog);
224+
225+
function Id() {
226+
const id = useId();
227+
return id;
228+
}
229+
230+
function Dialog() {
231+
return <Id />;
232+
}
233+
234+
function App() {
235+
return (
236+
<div>
237+
<Id />
238+
{createPortal(<Dialog />, dialog)}
239+
</div>
240+
);
241+
}
242+
243+
render(<App />, root);
244+
expect(scratch.innerHTML).to.equal(
245+
'<div><div>P0-0</div></div><div id="container">P0-1</div>'
246+
);
247+
});
248+
215249
it('should unmount Portal', () => {
216250
let root = document.createElement('div');
217251
let dialog = document.createElement('div');
@@ -237,8 +271,8 @@ describe('Portal', () => {
237271
it('should leave a working root after the portal', () => {
238272
/** @type {() => void} */
239273
let toggle,
240-
/** @type {() => void} */
241-
toggle2;
274+
/** @type {() => void} */
275+
toggle2;
242276

243277
function Foo(props) {
244278
const [mounted, setMounted] = useState(false);
@@ -289,8 +323,8 @@ describe('Portal', () => {
289323
it('should work with stacking portals', () => {
290324
/** @type {() => void} */
291325
let toggle,
292-
/** @type {() => void} */
293-
toggle2;
326+
/** @type {() => void} */
327+
toggle2;
294328

295329
function Foo(props) {
296330
const [mounted, setMounted] = useState(false);
@@ -377,8 +411,8 @@ describe('Portal', () => {
377411
it('should work with replacing placeholder portals', () => {
378412
/** @type {() => void} */
379413
let toggle,
380-
/** @type {() => void} */
381-
toggle2;
414+
/** @type {() => void} */
415+
toggle2;
382416

383417
function Foo(props) {
384418
const [mounted, setMounted] = useState(false);
@@ -463,8 +497,9 @@ describe('Portal', () => {
463497
it('should support nested portals', () => {
464498
/** @type {() => void} */
465499
let toggle,
466-
/** @type {() => void} */
467-
toggle2, inner;
500+
/** @type {() => void} */
501+
toggle2,
502+
inner;
468503

469504
function Bar() {
470505
const [mounted, setMounted] = useState(false);

0 commit comments

Comments
 (0)