Skip to content

Commit 305feb9

Browse files
authored
[DOM] Fix Fragment dispatchEvent when the container is a Document (#37165)
dispatchEvent appends a temporary Text node to the fragment's nearest host parent, but a Document can't contain Text, so createRoot(document) threw HierarchyRequestError whenever the fragment had a listener or the event didn't bubble. Use a Comment node for Document containers: it is a legal document child and sits at the fragment's own position, unlike documentElement, which would put the target inside the fragment and fire its listeners twice.
1 parent 18c30e7 commit 305feb9

2 files changed

Lines changed: 109 additions & 1 deletion

File tree

‎packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3156,7 +3156,14 @@ FragmentInstance.prototype.dispatchEvent = function (
31563156
(eventListeners !== null && eventListeners.length > 0) ||
31573157
!event.bubbles
31583158
) {
3159-
const temp = document.createTextNode('');
3159+
// The temporary node stands in for the fragment's position so that its own
3160+
// listeners fire before the event propagates to the parent. A Document can
3161+
// only hold comments and processing instructions alongside its
3162+
// documentElement, so a Text node would be an invalid child there.
3163+
const temp =
3164+
parentHostInstance.nodeType === DOCUMENT_NODE
3165+
? (parentHostInstance as any as Document).createComment('')
3166+
: document.createTextNode('');
31603167
if (eventListeners) {
31613168
for (let i = 0; i < eventListeners.length; i++) {
31623169
const {type, listener, optionsOrUseCapture} = eventListeners[i];

‎packages/react-dom/src/__tests__/ReactDOMFragmentRefsDocument-test.js‎

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ describe('FragmentRefs', () => {
3434
global.document = global.window.document;
3535
global.navigator = global.window.navigator;
3636
global.Event = global.window.Event;
37+
global.MouseEvent = global.window.MouseEvent;
3738
global.Node = Node;
3839
});
3940

@@ -103,6 +104,106 @@ describe('FragmentRefs', () => {
103104
expect(fragmentListener).toHaveBeenCalledTimes(1);
104105
expect(bodyListener).toHaveBeenCalledTimes(1);
105106
});
107+
108+
// @gate enableFragmentRefs
109+
it('dispatches to its own listeners when the container is a Document', async () => {
110+
const fragmentRef = React.createRef();
111+
const root = ReactDOMClient.createRoot(document);
112+
const logs = [];
113+
114+
await act(() => {
115+
root.render(
116+
<>
117+
<Fragment ref={fragmentRef} />
118+
<html>
119+
<body>
120+
<div id="child" />
121+
</body>
122+
</html>
123+
</>,
124+
);
125+
});
126+
127+
fragmentRef.current.addEventListener('click', () => {
128+
logs.push('fragment');
129+
});
130+
document.addEventListener('click', () => {
131+
logs.push('document');
132+
});
133+
134+
const isCancelable = !fragmentRef.current.dispatchEvent(
135+
new MouseEvent('click', {bubbles: true}),
136+
);
137+
138+
expect(logs).toEqual(['fragment', 'document']);
139+
expect(isCancelable).toBe(false);
140+
});
141+
142+
// @gate enableFragmentRefs
143+
it('does not propagate through its own children when wrapping documentElement', async () => {
144+
const fragmentRef = React.createRef();
145+
const root = ReactDOMClient.createRoot(document);
146+
const logs = [];
147+
148+
await act(() => {
149+
root.render(
150+
<Fragment ref={fragmentRef}>
151+
<html>
152+
<body>
153+
<div id="child" />
154+
</body>
155+
</html>
156+
</Fragment>,
157+
);
158+
});
159+
160+
// This also registers the listener on the <html> child. Because the
161+
// fragment's position is a sibling of <html>, the event must not
162+
// propagate through it and fire the listener a second time.
163+
fragmentRef.current.addEventListener('click', () => {
164+
logs.push('fragment');
165+
});
166+
document.addEventListener('click', () => {
167+
logs.push('document');
168+
});
169+
170+
fragmentRef.current.dispatchEvent(
171+
new MouseEvent('click', {bubbles: true}),
172+
);
173+
174+
expect(logs).toEqual(['fragment', 'document']);
175+
});
176+
177+
// @gate enableFragmentRefs
178+
it('dispatches non-bubbling events when the container is a Document', async () => {
179+
const fragmentRef = React.createRef();
180+
const root = ReactDOMClient.createRoot(document);
181+
const logs = [];
182+
183+
await act(() => {
184+
root.render(
185+
<>
186+
<Fragment ref={fragmentRef} />
187+
<html>
188+
<body>
189+
<div id="child" />
190+
</body>
191+
</html>
192+
</>,
193+
);
194+
});
195+
196+
document.addEventListener('click', () => {
197+
logs.push('document');
198+
});
199+
200+
const isCancelable = !fragmentRef.current.dispatchEvent(
201+
new MouseEvent('click', {bubbles: false}),
202+
);
203+
204+
expect(logs).toEqual([]);
205+
expect(isCancelable).toBe(false);
206+
});
106207
});
107208

108209
describe('addEventListener()', () => {

0 commit comments

Comments
 (0)