Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -198,17 +198,23 @@ jsi::Value NativeDOM::getParentNode(
}

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

auto currentRevision =
getCurrentShadowTreeRevision(rt, shadowNode->getSurfaceId());
if (currentRevision == nullptr) {
return jsi::Value::undefined();
}

// The parent of the surface's root node is the document. Only the actual
// root node qualifies: nested nodes that carry the `RootNodeKind` trait
// (e.g. <Modal>, portals/overlays) still have a real parent in the shadow
// tree and must report it. Otherwise capture/bubble event propagation is
// silently severed at that boundary (a listener on an ancestor rendered
// above the modal would never receive descendant focus/blur, etc.).
if (ShadowNode::sameFamily(*currentRevision, *shadowNode)) {
return jsi::Value{shadowNode->getSurfaceId()};
}

auto parentShadowNode = dom::getParentNode(currentRevision, *shadowNode);
if (parentShadowNode == nullptr) {
return jsi::Value::undefined();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ import TextInputState from '../../../../../../Libraries/Components/TextInput/Tex
import * as Fantom from '@react-native/fantom';
import * as React from 'react';
import {createRef} from 'react';
import {ScrollView, Text, TextInput, View} from 'react-native';
import {Modal, ScrollView, Text, TextInput, View} from 'react-native';
import {
NativeText,
NativeVirtualText,
} from 'react-native/Libraries/Text/TextNativeComponent';
import * as ReactNativeFeatureFlags from 'react-native/src/private/featureflags/ReactNativeFeatureFlags';
import Event from 'react-native/src/private/webapis/dom/events/Event';
import ReactNativeDocument from 'react-native/src/private/webapis/dom/nodes/ReactNativeDocument';
import ReactNativeElement from 'react-native/src/private/webapis/dom/nodes/ReactNativeElement';
import ReadOnlyElement from 'react-native/src/private/webapis/dom/nodes/ReadOnlyElement';
import ReadOnlyNode from 'react-native/src/private/webapis/dom/nodes/ReadOnlyNode';
Expand Down Expand Up @@ -398,6 +399,47 @@ describe('ReactNativeElement', () => {
expect(childNodeC.parentNode).toBe(null);
expect(childNodeC.parentElement).toBe(null);
});

it('returns the containing element as the parent of a modal host view, not the document', () => {
const parentRef = createRef<HostInstance>();
const modalRef = createRef<HostInstance>();

const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(
<View ref={parentRef}>
<Modal ref={modalRef} />
</View>,
);
});

const parentNode = ensureReactNativeElement(parentRef.current);
const modalNode = ensureReactNativeElement(modalRef.current);
const document = ensureInstance(
parentNode.ownerDocument,
ReactNativeDocument,
);

// Capture the relations before tearing down, so cleanup runs even if
// the assertions below fail.
const modalParentNode = modalNode.parentNode;
const modalParentElement = modalNode.parentElement;

// Unmount and drain the queue so the modal's AppContainer passive
// effects (in __DEV__) don't trip the global "MessageQueue is not
// empty" validation hook.
root.destroy();
Fantom.runWorkLoop();

// The <Modal> host view is a root-kind shadow node, but its parent
// must still be its actual containing element, NOT the document.
// Two-phase event propagation (e.g. focus/blur bubbling to ancestors
// rendered above the modal) walks this parent chain, so returning the
// document here silently severs bubbling at the modal boundary.
expect(modalParentNode).toBe(parentNode);
expect(modalParentElement).toBe(parentNode);
expect(modalParentNode).not.toBe(document);
});
});

describe('compareDocumentPosition / contains', () => {
Expand Down
Loading