diff --git a/packages/react-dom-bindings/src/events/DOMPluginEventSystem.js b/packages/react-dom-bindings/src/events/DOMPluginEventSystem.js
index 6d615160d8be..b951692ffccd 100644
--- a/packages/react-dom-bindings/src/events/DOMPluginEventSystem.js
+++ b/packages/react-dom-bindings/src/events/DOMPluginEventSystem.js
@@ -491,7 +491,11 @@ function addTrappedEventListener(
targetContainer =
enableLegacyFBSupport && isDeferredListenerForLegacyFBSupport
- ? (targetContainer as any).ownerDocument
+ ? // A Document container's ownerDocument is null, so it must be used
+ // as the deferral target itself.
+ (targetContainer as any).nodeType === DOCUMENT_NODE
+ ? targetContainer
+ : (targetContainer as any).ownerDocument
: targetContainer;
let unsubscribeListener;
diff --git a/packages/react-dom/src/__tests__/ReactDOMFragmentRefsDocument-test.js b/packages/react-dom/src/__tests__/ReactDOMFragmentRefsDocument-test.js
index af8bbc86937d..8157b43f3766 100644
--- a/packages/react-dom/src/__tests__/ReactDOMFragmentRefsDocument-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMFragmentRefsDocument-test.js
@@ -30,6 +30,8 @@ describe('FragmentRefs', () => {
document = jsdom.window.document;
global.window = jsdom.window;
global.document = global.window.document;
+ global.navigator = global.window.navigator;
+ global.Event = global.window.Event;
});
describe('focus methods', () => {
@@ -54,7 +56,9 @@ describe('FragmentRefs', () => {
});
await act(() => {
- fragmentRef.current.focus();
+ // focus() would stop at
, which is a child of the fragment
+ // and usually already the activeElement.
+ document.getElementById('child-a').focus();
});
expect(document.activeElement.id).toEqual('child-a');
@@ -65,4 +69,187 @@ describe('FragmentRefs', () => {
});
});
});
+
+ describe('events', () => {
+ describe('dispatchEvent()', () => {
+ // @gate enableFragmentRefs
+ it('fires events when the fragment is a child of a HostSingleton in a document root', async () => {
+ const fragmentRef = React.createRef();
+ const bodyRef = React.createRef();
+ const root = ReactDOMClient.createRoot(document);
+
+ await act(() => {
+ root.render(
+
+
+
+
+ ,
+ );
+ });
+
+ const fragmentListener = jest.fn();
+ fragmentRef.current.addEventListener('custom', fragmentListener);
+ const bodyListener = jest.fn();
+ bodyRef.current.addEventListener('custom', bodyListener);
+
+ // The is the fragment's host parent, so the
+ // temporary event target is appended there.
+ fragmentRef.current.dispatchEvent(new Event('custom', {bubbles: true}));
+
+ expect(fragmentListener).toHaveBeenCalledTimes(1);
+ expect(bodyListener).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('addEventListener()', () => {
+ // @gate enableFragmentRefs
+ it('attaches listeners to the host children inside singletons', async () => {
+ const fragmentRef = React.createRef();
+ const childRef = React.createRef();
+ const root = ReactDOMClient.createRoot(document);
+
+ await act(() => {
+ root.render(
+
+
+
+
+
+
+ ,
+ );
+ });
+
+ const currentTargets = [];
+ fragmentRef.current.addEventListener('click', event => {
+ currentTargets.push(event.currentTarget);
+ });
+
+ childRef.current.dispatchEvent(new Event('click', {bubbles: true}));
+
+ // The singleton is the fragment's child, so the listener is
+ // attached there and receives the bubbling event.
+ expect(currentTargets).toEqual([document.documentElement]);
+ });
+
+ // @gate enableFragmentRefs
+ it('attaches listeners to a singleton mounted into the fragment, but not to its content', async () => {
+ const fragmentRef = React.createRef();
+ const childRef = React.createRef();
+ const root = ReactDOMClient.createRoot(document);
+
+ function Test({showShell}) {
+ return (
+
+ {showShell && (
+
+
+
+
+
+ )}
+
+ );
+ }
+
+ await act(() => {
+ root.render();
+ });
+
+ const currentTargets = [];
+ fragmentRef.current.addEventListener('click', event => {
+ currentTargets.push(event.currentTarget);
+ });
+
+ await act(() => {
+ root.render();
+ });
+
+ childRef.current.dispatchEvent(new Event('click', {bubbles: true}));
+
+ // The placed singleton receives the fragment's listener as a
+ // new child. Its content is not attributed to the fragment, so the
+ // event only fires once when it bubbles to .
+ expect(currentTargets).toEqual([document.documentElement]);
+ });
+
+ // @gate enableFragmentRefs
+ it('attributes new children inside a singleton to fragments below it, not above it', async () => {
+ const outerFragmentRef = React.createRef();
+ const innerFragmentRef = React.createRef();
+ const lateChildRef = React.createRef();
+ const root = ReactDOMClient.createRoot(document);
+
+ function Test({showLateChild}) {
+ return (
+
+
+
+
+
+ {showLateChild && }
+
+
+
+
+ );
+ }
+
+ await act(() => {
+ root.render();
+ });
+
+ const outerCurrentTargets = [];
+ outerFragmentRef.current.addEventListener('click', event => {
+ outerCurrentTargets.push(event.currentTarget);
+ });
+ const innerCurrentTargets = [];
+ innerFragmentRef.current.addEventListener('click', event => {
+ innerCurrentTargets.push(event.currentTarget);
+ });
+
+ await act(() => {
+ root.render();
+ });
+
+ lateChildRef.current.dispatchEvent(new Event('click', {bubbles: true}));
+
+ // The inner fragment owns the new child directly and attaches its
+ // listener on insertion. The outer fragment's child is the
+ // singleton, so the new child inside is not attributed to it
+ // and its listener only fires once via bubbling.
+ expect(innerCurrentTargets).toEqual([lateChildRef.current]);
+ expect(outerCurrentTargets).toEqual([document.documentElement]);
+ });
+ });
+ });
+
+ describe('getClientRects()', () => {
+ // @gate enableFragmentRefs
+ it('measures the host children inside singletons', async () => {
+ const fragmentRef = React.createRef();
+ const childRef = React.createRef();
+ const root = ReactDOMClient.createRoot(document);
+
+ await act(() => {
+ root.render(
+
+
+
+
+
+
+ ,
+ );
+ });
+
+ childRef.current.getClientRects = jest.fn(() => ['child-rect']);
+ document.documentElement.getClientRects = jest.fn(() => ['html-rect']);
+
+ // The singleton is the fragment's child, so it is measured
+ // instead of the elements inside it
+ expect(fragmentRef.current.getClientRects()).toEqual(['html-rect']);
+ });
+ });
});
diff --git a/packages/react-reconciler/src/ReactFiberCommitHostEffects.js b/packages/react-reconciler/src/ReactFiberCommitHostEffects.js
index 75129c1e8c9b..c734882e9c6d 100644
--- a/packages/react-reconciler/src/ReactFiberCommitHostEffects.js
+++ b/packages/react-reconciler/src/ReactFiberCommitHostEffects.js
@@ -262,6 +262,7 @@ export function commitNewChildToFragmentInstances(
): void {
if (
(fiber.tag !== HostComponent &&
+ fiber.tag !== HostSingleton &&
!(enableFragmentRefsTextNodes && fiber.tag === HostText)) ||
// Only run fragment insertion effects for initial insertions
fiber.alternate !== null ||
@@ -283,7 +284,7 @@ export function commitFragmentInstanceInsertionEffects(fiber: Fiber): void {
commitNewChildToFragmentInstance(fiber.stateNode, fragmentInstance);
}
- if (isHostParent(parent)) {
+ if (isFragmentInstanceHostParent(parent)) {
return;
}
@@ -299,7 +300,7 @@ export function commitFragmentInstanceDeletionEffects(fiber: Fiber): void {
deleteChildFromFragmentInstance(fiber.stateNode, fragmentInstance);
}
- if (isHostParent(parent)) {
+ if (isFragmentInstanceHostParent(parent)) {
return;
}
@@ -325,6 +326,19 @@ function isFragmentInstanceParent(fiber: Fiber): boolean {
return fiber && fiber.tag === Fragment && fiber.stateNode !== null;
}
+// Fragments collect HostSingleton children regardless of whether the
+// singleton is a scope for placement, so their host parent boundary is
+// wider than `isHostParent`.
+function isFragmentInstanceHostParent(fiber: Fiber): boolean {
+ return (
+ fiber.tag === HostComponent ||
+ // $FlowFixMe[constant-condition]
+ (supportsSingletons ? fiber.tag === HostSingleton : false) ||
+ fiber.tag === HostRoot ||
+ fiber.tag === HostPortal
+ );
+}
+
function getHostSibling(fiber: Fiber): ?Instance {
// We're going to search forward into the tree until we find a sibling host
// node. Unfortunately, if multiple insertions are done in a row we have to
@@ -411,15 +425,20 @@ function insertOrAppendPlacementNodeIntoContainer(
return;
}
- if (
- // $FlowFixMe[constant-condition]
- (supportsSingletons ? tag === HostSingleton : false) &&
- isSingletonScope(node.type)
- ) {
- // This singleton is the parent of deeper nodes and needs to become
- // the parent for child insertions and appends
- parent = node.stateNode;
- before = null;
+ // $FlowFixMe[constant-condition]
+ if (supportsSingletons ? tag === HostSingleton : false) {
+ if (enableFragmentRefs) {
+ // The singleton is the fragment child. Its own children are not
+ // attributed to the fragment instances above it.
+ commitNewChildToFragmentInstances(node, parentFragmentInstances);
+ parentFragmentInstances = null;
+ }
+ if (isSingletonScope(node.type)) {
+ // This singleton is the parent of deeper nodes and needs to become
+ // the parent for child insertions and appends
+ parent = node.stateNode;
+ before = null;
+ }
}
const child = node.child;
@@ -470,14 +489,19 @@ function insertOrAppendPlacementNode(
return;
}
- if (
- // $FlowFixMe[constant-condition]
- (supportsSingletons ? tag === HostSingleton : false) &&
- isSingletonScope(node.type)
- ) {
- // This singleton is the parent of deeper nodes and needs to become
- // the parent for child insertions and appends
- parent = node.stateNode;
+ // $FlowFixMe[constant-condition]
+ if (supportsSingletons ? tag === HostSingleton : false) {
+ if (enableFragmentRefs) {
+ // The singleton is the fragment child. Its own children are not
+ // attributed to the fragment instances above it.
+ commitNewChildToFragmentInstances(node, parentFragmentInstances);
+ parentFragmentInstances = null;
+ }
+ if (isSingletonScope(node.type)) {
+ // This singleton is the parent of deeper nodes and needs to become
+ // the parent for child insertions and appends
+ parent = node.stateNode;
+ }
}
const child = node.child;
@@ -500,9 +524,10 @@ function commitPlacement(finishedWork: Fiber): void {
// Recursively insert all host nodes into the parent.
let hostParentFiber;
let parentFragmentInstances = null;
+ let collectFragmentInstances = enableFragmentRefs;
let parentFiber = finishedWork.return;
while (parentFiber !== null) {
- if (enableFragmentRefs && isFragmentInstanceParent(parentFiber)) {
+ if (collectFragmentInstances && isFragmentInstanceParent(parentFiber)) {
const fragmentInstance: FragmentInstanceType = parentFiber.stateNode;
if (parentFragmentInstances === null) {
parentFragmentInstances = [fragmentInstance];
@@ -510,6 +535,14 @@ function commitPlacement(finishedWork: Fiber): void {
parentFragmentInstances.push(fragmentInstance);
}
}
+ if (collectFragmentInstances && isFragmentInstanceHostParent(parentFiber)) {
+ // Fragments collect children only down to the nearest host fiber.
+ // The search for the placement parent can continue past host fibers
+ // that are not valid placement parents, like HostSingletons outside
+ // a singleton scope, but fragments above them own that host fiber
+ // as a child, not the placed node.
+ collectFragmentInstances = false;
+ }
if (isHostParent(parentFiber)) {
hostParentFiber = parentFiber;
break;
@@ -600,8 +633,13 @@ function commitImmutablePlacementNodeToFragmentInstances(
if (!enableFragmentRefs) {
return;
}
- const isHost = finishedWork.tag === HostComponent;
+ const isHost =
+ finishedWork.tag === HostComponent ||
+ // $FlowFixMe[constant-condition]
+ (supportsSingletons ? finishedWork.tag === HostSingleton : false);
if (isHost) {
+ // A singleton is the fragment child itself, so its own children are
+ // not attributed to the fragment instances above it.
commitNewChildToFragmentInstances(finishedWork, parentFragmentInstances);
return;
} else if (finishedWork.tag === HostPortal) {
diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js
index d7a15513e69c..2357bc038281 100644
--- a/packages/react-reconciler/src/ReactFiberCommitWork.js
+++ b/packages/react-reconciler/src/ReactFiberCommitWork.js
@@ -1527,6 +1527,9 @@ function commitDeletionEffectsOnFiber(
if (!offscreenSubtreeWasHidden) {
safelyDetachRef(deletedFiber, nearestMountedAncestor);
}
+ if (enableFragmentRefs) {
+ commitFragmentInstanceDeletionEffects(deletedFiber);
+ }
const prevHostParent = hostParent;
const prevHostParentIsContainer = hostParentIsContainer;
@@ -3102,6 +3105,7 @@ function disappearLayoutEffects(
if (
enableFragmentRefs &&
(finishedWork.tag === HostComponent ||
+ finishedWork.tag === HostSingleton ||
(enableFragmentRefsTextNodes && finishedWork.tag === HostText))
) {
commitFragmentInstanceDeletionEffects(finishedWork);
@@ -3287,7 +3291,11 @@ function reappearLayoutEffects(
case HostHoistable:
case HostComponent: {
// TODO: Enable HostText for RN
- if (enableFragmentRefs && finishedWork.tag === HostComponent) {
+ if (
+ enableFragmentRefs &&
+ (finishedWork.tag === HostComponent ||
+ finishedWork.tag === HostSingleton)
+ ) {
commitFragmentInstanceInsertionEffects(finishedWork);
}
recursivelyTraverseReappearLayoutEffects(
diff --git a/packages/react-reconciler/src/ReactFiberTreeReflection.js b/packages/react-reconciler/src/ReactFiberTreeReflection.js
index 9d3d3020268a..a3e8fc67b4ee 100644
--- a/packages/react-reconciler/src/ReactFiberTreeReflection.js
+++ b/packages/react-reconciler/src/ReactFiberTreeReflection.js
@@ -392,6 +392,7 @@ function traverseVisibleInstancesAndTextInstances(
while (child !== null) {
const isHostNode =
child.tag === HostComponent ||
+ child.tag === HostSingleton ||
(enableFragmentRefsTextNodes && child.tag === HostText);
if (isHostNode && fn(child, a, b, c)) {
return true;
@@ -402,7 +403,8 @@ function traverseVisibleInstancesAndTextInstances(
// Skip hidden subtrees
} else {
if (
- (searchWithinHosts || child.tag !== HostComponent) &&
+ (searchWithinHosts ||
+ (child.tag !== HostComponent && child.tag !== HostSingleton)) &&
traverseVisibleInstancesAndTextInstances(
child.child,
searchWithinHosts,
@@ -425,7 +427,11 @@ export function getFragmentParentInstanceOrContainerFiber(
): null | Fiber {
let parent = fiber.return;
while (parent !== null) {
- if (parent.tag === HostRoot || parent.tag === HostComponent) {
+ if (
+ parent.tag === HostRoot ||
+ parent.tag === HostComponent ||
+ parent.tag === HostSingleton
+ ) {
return parent;
}
parent = parent.return;
@@ -441,7 +447,11 @@ export function fiberIsPortaledIntoHost(fiber: Fiber): boolean {
if (parent.tag === HostPortal) {
foundPortalParent = true;
}
- if (parent.tag === HostRoot || parent.tag === HostComponent) {
+ if (
+ parent.tag === HostRoot ||
+ parent.tag === HostComponent ||
+ parent.tag === HostSingleton
+ ) {
break;
}
parent = parent.return;
@@ -486,6 +496,7 @@ function findFragmentInstanceOrTextInstanceSiblings(
}
if (
child.tag === HostComponent ||
+ child.tag === HostSingleton ||
(enableFragmentRefsTextNodes && child.tag === HostText)
) {
if (foundSelf) {
@@ -521,6 +532,7 @@ export function getInstanceFromHostFiber<
>(fiber: Fiber): I {
switch (fiber.tag) {
case HostComponent:
+ case HostSingleton:
case HostText:
return fiber.stateNode;
case HostRoot:
@@ -589,7 +601,9 @@ export function isFragmentContainedByFiber(
getFragmentParentInstanceOrContainerFiber(fragmentFiber);
while (current !== null) {
if (
- (current.tag === HostComponent || current.tag === HostRoot) &&
+ (current.tag === HostComponent ||
+ current.tag === HostRoot ||
+ current.tag === HostSingleton) &&
(current === fiberHostParent || current.alternate === fiberHostParent)
) {
return true;