Skip to content

Commit e410ff5

Browse files
rubennortemeta-codesync[bot]
authored andcommitted
Fix parentNode for nested root host views like <Modal> (#57623)
Summary: Pull Request resolved: #57623 `NativeDOM::getParentNode` returned the document for any shadow node carrying the `RootNodeKind` trait. That trait is set not only on a surface's actual root node but also on nested host views such as `<Modal>` (and portal/overlay-style components), so their `parentNode`/`parentElement` incorrectly resolved to the document instead of their real containing element. Because the EventTarget-based event dispatch builds its capture/bubble ancestor path by walking `getParentNode` up the shadow tree, this truncated the path at the modal boundary: a listener registered on an ancestor rendered above the modal never received events (e.g. focus/blur) originating inside it. Fix by returning the document only for the surface's actual root node, identified via `ShadowNode::sameFamily(*currentRevision, *shadowNode)`. Every other node — including nested root-kind nodes — now reports its real structural parent, while disconnected nodes still report none. Changelog: [General][Fixed] - Fix `parentNode`/`parentElement` returning the document instead of the containing element for `<Modal>` and other nested root host views, which severed capture/bubble event propagation to ancestors rendered above them Reviewed By: javache Differential Revision: D113012362 fbshipit-source-id: 88043a9b9de128e7c3c96d8516b0950cbd6c3c68
1 parent 95df500 commit e410ff5

2 files changed

Lines changed: 53 additions & 5 deletions

File tree

packages/react-native/ReactCommon/react/nativemodule/dom/NativeDOM.cpp

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -198,17 +198,23 @@ jsi::Value NativeDOM::getParentNode(
198198
}
199199

200200
auto shadowNode = getShadowNode(rt, nativeNodeReference);
201-
if (isRootShadowNode(*shadowNode)) {
202-
// The parent of the root node is the document.
203-
return jsi::Value{shadowNode->getSurfaceId()};
204-
}
205201

206202
auto currentRevision =
207203
getCurrentShadowTreeRevision(rt, shadowNode->getSurfaceId());
208204
if (currentRevision == nullptr) {
209205
return jsi::Value::undefined();
210206
}
211207

208+
// The parent of the surface's root node is the document. Only the actual
209+
// root node qualifies: nested nodes that carry the `RootNodeKind` trait
210+
// (e.g. <Modal>, portals/overlays) still have a real parent in the shadow
211+
// tree and must report it. Otherwise capture/bubble event propagation is
212+
// silently severed at that boundary (a listener on an ancestor rendered
213+
// above the modal would never receive descendant focus/blur, etc.).
214+
if (ShadowNode::sameFamily(*currentRevision, *shadowNode)) {
215+
return jsi::Value{shadowNode->getSurfaceId()};
216+
}
217+
212218
auto parentShadowNode = dom::getParentNode(currentRevision, *shadowNode);
213219
if (parentShadowNode == nullptr) {
214220
return jsi::Value::undefined();

packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeElement-itest.js

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,14 @@ import TextInputState from '../../../../../../Libraries/Components/TextInput/Tex
2020
import * as Fantom from '@react-native/fantom';
2121
import * as React from 'react';
2222
import {createRef} from 'react';
23-
import {ScrollView, Text, TextInput, View} from 'react-native';
23+
import {Modal, ScrollView, Text, TextInput, View} from 'react-native';
2424
import {
2525
NativeText,
2626
NativeVirtualText,
2727
} from 'react-native/Libraries/Text/TextNativeComponent';
2828
import * as ReactNativeFeatureFlags from 'react-native/src/private/featureflags/ReactNativeFeatureFlags';
2929
import Event from 'react-native/src/private/webapis/dom/events/Event';
30+
import ReactNativeDocument from 'react-native/src/private/webapis/dom/nodes/ReactNativeDocument';
3031
import ReactNativeElement from 'react-native/src/private/webapis/dom/nodes/ReactNativeElement';
3132
import ReadOnlyElement from 'react-native/src/private/webapis/dom/nodes/ReadOnlyElement';
3233
import ReadOnlyNode from 'react-native/src/private/webapis/dom/nodes/ReadOnlyNode';
@@ -398,6 +399,47 @@ describe('ReactNativeElement', () => {
398399
expect(childNodeC.parentNode).toBe(null);
399400
expect(childNodeC.parentElement).toBe(null);
400401
});
402+
403+
it('returns the containing element as the parent of a modal host view, not the document', () => {
404+
const parentRef = createRef<HostInstance>();
405+
const modalRef = createRef<HostInstance>();
406+
407+
const root = Fantom.createRoot();
408+
Fantom.runTask(() => {
409+
root.render(
410+
<View ref={parentRef}>
411+
<Modal ref={modalRef} />
412+
</View>,
413+
);
414+
});
415+
416+
const parentNode = ensureReactNativeElement(parentRef.current);
417+
const modalNode = ensureReactNativeElement(modalRef.current);
418+
const document = ensureInstance(
419+
parentNode.ownerDocument,
420+
ReactNativeDocument,
421+
);
422+
423+
// Capture the relations before tearing down, so cleanup runs even if
424+
// the assertions below fail.
425+
const modalParentNode = modalNode.parentNode;
426+
const modalParentElement = modalNode.parentElement;
427+
428+
// Unmount and drain the queue so the modal's AppContainer passive
429+
// effects (in __DEV__) don't trip the global "MessageQueue is not
430+
// empty" validation hook.
431+
root.destroy();
432+
Fantom.runWorkLoop();
433+
434+
// The <Modal> host view is a root-kind shadow node, but its parent
435+
// must still be its actual containing element, NOT the document.
436+
// Two-phase event propagation (e.g. focus/blur bubbling to ancestors
437+
// rendered above the modal) walks this parent chain, so returning the
438+
// document here silently severs bubbling at the modal boundary.
439+
expect(modalParentNode).toBe(parentNode);
440+
expect(modalParentElement).toBe(parentNode);
441+
expect(modalParentNode).not.toBe(document);
442+
});
401443
});
402444

403445
describe('compareDocumentPosition / contains', () => {

0 commit comments

Comments
 (0)