Skip to content
Merged
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 @@ -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;
Expand Down
189 changes: 188 additions & 1 deletion packages/react-dom/src/__tests__/ReactDOMFragmentRefsDocument-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -54,7 +56,9 @@ describe('FragmentRefs', () => {
});

await act(() => {
fragmentRef.current.focus();
// focus() would stop at <body>, which is a child of the fragment
// and usually already the activeElement.
document.getElementById('child-a').focus();
});
expect(document.activeElement.id).toEqual('child-a');

Expand All @@ -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(
<html>
<body ref={bodyRef}>
<Fragment ref={fragmentRef} />
</body>
</html>,
);
});

const fragmentListener = jest.fn();
fragmentRef.current.addEventListener('custom', fragmentListener);
const bodyListener = jest.fn();
bodyRef.current.addEventListener('custom', bodyListener);

// The <body> 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(
<Fragment ref={fragmentRef}>
<html>
<body>
<div ref={childRef} id="child" />
</body>
</html>
</Fragment>,
);
});

const currentTargets = [];
fragmentRef.current.addEventListener('click', event => {
currentTargets.push(event.currentTarget);
});

childRef.current.dispatchEvent(new Event('click', {bubbles: true}));

// The <html> 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 (
<Fragment ref={fragmentRef}>
{showShell && (
<html>
<body>
<div ref={childRef} id="child" />
</body>
</html>
)}
</Fragment>
);
}

await act(() => {
root.render(<Test showShell={false} />);
});

const currentTargets = [];
fragmentRef.current.addEventListener('click', event => {
currentTargets.push(event.currentTarget);
});

await act(() => {
root.render(<Test showShell={true} />);
});

childRef.current.dispatchEvent(new Event('click', {bubbles: true}));

// The placed <html> 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 <html>.
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 (
<Fragment ref={outerFragmentRef}>
<html>
<body>
<Fragment ref={innerFragmentRef}>
<div id="child" />
{showLateChild && <span ref={lateChildRef} id="late" />}
</Fragment>
</body>
</html>
</Fragment>
);
}

await act(() => {
root.render(<Test showLateChild={false} />);
});

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(<Test showLateChild={true} />);
});

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 <html>
// singleton, so the new child inside <body> 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(
<Fragment ref={fragmentRef}>
<html>
<body>
<div ref={childRef} id="child" />
</body>
</html>
</Fragment>,
);
});

childRef.current.getClientRects = jest.fn(() => ['child-rect']);
document.documentElement.getClientRects = jest.fn(() => ['html-rect']);

// The <html> singleton is the fragment's child, so it is measured
// instead of the elements inside it
expect(fragmentRef.current.getClientRects()).toEqual(['html-rect']);
});
});
});
80 changes: 59 additions & 21 deletions packages/react-reconciler/src/ReactFiberCommitHostEffects.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ||
Expand All @@ -283,7 +284,7 @@ export function commitFragmentInstanceInsertionEffects(fiber: Fiber): void {
commitNewChildToFragmentInstance(fiber.stateNode, fragmentInstance);
}

if (isHostParent(parent)) {
if (isFragmentInstanceHostParent(parent)) {
return;
}

Expand All @@ -299,7 +300,7 @@ export function commitFragmentInstanceDeletionEffects(fiber: Fiber): void {
deleteChildFromFragmentInstance(fiber.stateNode, fragmentInstance);
}

if (isHostParent(parent)) {
if (isFragmentInstanceHostParent(parent)) {
return;
}

Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -500,16 +524,25 @@ 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];
} else {
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;
Expand Down Expand Up @@ -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) {
Expand Down
10 changes: 9 additions & 1 deletion packages/react-reconciler/src/ReactFiberCommitWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -1527,6 +1527,9 @@ function commitDeletionEffectsOnFiber(
if (!offscreenSubtreeWasHidden) {
safelyDetachRef(deletedFiber, nearestMountedAncestor);
}
if (enableFragmentRefs) {
commitFragmentInstanceDeletionEffects(deletedFiber);
}

const prevHostParent = hostParent;
const prevHostParentIsContainer = hostParentIsContainer;
Expand Down Expand Up @@ -3102,6 +3105,7 @@ function disappearLayoutEffects(
if (
enableFragmentRefs &&
(finishedWork.tag === HostComponent ||
finishedWork.tag === HostSingleton ||
(enableFragmentRefsTextNodes && finishedWork.tag === HostText))
) {
commitFragmentInstanceDeletionEffects(finishedWork);
Expand Down Expand Up @@ -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(
Expand Down
Loading
Loading