Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add compareDocumentPosition to fragment instances #32722

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

jackpope
Copy link
Contributor

This adds compareDocumentPosition(otherNode) to fragment instances.

The semantics implemented are meant to match typical element positioning, with some fragment specifics. See the unit tests for all expectations.

  • An element preceding a fragment is Node.DOCUMENT_POSITION_PRECEDING
  • An element after a fragment is Node.DOCUMENT_POSITION_FOLLOWING
  • An element containing the fragment is Node.DOCUMENT_POSITION_PRECEDING and Node.DOCUMENT_POSITION_CONTAINING
  • An element within the fragment is Node.DOCUMENT_POSITION_CONTAINED_BY
  • An element compared against an empty fragment will result in Node.DOCUMENT_POSITION_DISCONNECTED and Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC

Since we assume a fragment instances target children are DOM siblings and we want to compare the full fragment as a pseudo container, we can compare against the first target child outside of handling the special cases (empty fragments and contained elements).

@react-sizebot
Copy link

react-sizebot commented Mar 24, 2025

Comparing: 540cd65...f53dc02

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable/react-dom/cjs/react-dom.production.js = 6.68 kB 6.68 kB = 1.83 kB 1.83 kB
oss-stable/react-dom/cjs/react-dom-client.production.js = 515.56 kB 515.56 kB = 91.84 kB 91.84 kB
oss-experimental/react-dom/cjs/react-dom.production.js = 6.69 kB 6.69 kB = 1.83 kB 1.83 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js +0.27% 616.30 kB 617.97 kB +0.33% 109.03 kB 109.39 kB
facebook-www/ReactDOM-prod.classic.js +0.26% 650.16 kB 651.83 kB +0.30% 114.89 kB 115.23 kB
facebook-www/ReactDOM-prod.modern.js +0.26% 640.44 kB 642.11 kB +0.30% 113.33 kB 113.67 kB
oss-experimental/react-reconciler/cjs/react-reconciler-reflection.development.js +8.01% 6.84 kB 7.39 kB +6.99% 1.65 kB 1.76 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler-reflection.development.js +8.01% 6.84 kB 7.39 kB +6.99% 1.65 kB 1.76 kB
oss-stable/react-reconciler/cjs/react-reconciler-reflection.development.js +8.01% 6.84 kB 7.39 kB +6.99% 1.65 kB 1.76 kB
oss-experimental/react-reconciler/cjs/react-reconciler-reflection.production.js +5.85% 6.19 kB 6.55 kB +5.66% 1.64 kB 1.74 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler-reflection.production.js +5.85% 6.19 kB 6.55 kB +5.66% 1.64 kB 1.74 kB
oss-stable/react-reconciler/cjs/react-reconciler-reflection.production.js +5.85% 6.19 kB 6.55 kB +5.66% 1.64 kB 1.74 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name +/- Base Current +/- gzip Base gzip Current gzip
oss-experimental/react-reconciler/cjs/react-reconciler-reflection.development.js +8.01% 6.84 kB 7.39 kB +6.99% 1.65 kB 1.76 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler-reflection.development.js +8.01% 6.84 kB 7.39 kB +6.99% 1.65 kB 1.76 kB
oss-stable/react-reconciler/cjs/react-reconciler-reflection.development.js +8.01% 6.84 kB 7.39 kB +6.99% 1.65 kB 1.76 kB
oss-experimental/react-reconciler/cjs/react-reconciler-reflection.production.js +5.85% 6.19 kB 6.55 kB +5.66% 1.64 kB 1.74 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler-reflection.production.js +5.85% 6.19 kB 6.55 kB +5.66% 1.64 kB 1.74 kB
oss-stable/react-reconciler/cjs/react-reconciler-reflection.production.js +5.85% 6.19 kB 6.55 kB +5.66% 1.64 kB 1.74 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js +0.27% 616.30 kB 617.97 kB +0.33% 109.03 kB 109.39 kB
oss-experimental/react-dom/cjs/react-dom-unstable_testing.production.js +0.26% 630.71 kB 632.38 kB +0.32% 112.59 kB 112.95 kB
facebook-www/ReactDOM-prod.modern.js +0.26% 640.44 kB 642.11 kB +0.30% 113.33 kB 113.67 kB
facebook-www/ReactDOM-prod.classic.js +0.26% 650.16 kB 651.83 kB +0.30% 114.89 kB 115.23 kB
facebook-www/ReactDOMTesting-prod.modern.js +0.25% 654.84 kB 656.51 kB +0.30% 116.93 kB 117.29 kB
facebook-www/ReactDOMTesting-prod.classic.js +0.25% 664.56 kB 666.23 kB +0.30% 118.57 kB 118.93 kB
oss-experimental/react-dom/cjs/react-dom-profiling.profiling.js +0.24% 681.00 kB 682.66 kB +0.30% 118.83 kB 119.18 kB
facebook-www/ReactDOM-profiling.modern.js +0.23% 714.13 kB 715.80 kB +0.28% 123.61 kB 123.96 kB
facebook-www/ReactDOM-profiling.classic.js +0.23% 722.17 kB 723.84 kB +0.27% 124.95 kB 125.29 kB

Generated by 🚫 dangerJS against f53dc02

@sebmarkbage
Copy link
Collaborator

sebmarkbage commented Apr 3, 2025

Since we assume a fragment instances target children are DOM siblings

While that's generally the case I think we need to gracefully handle the case when they're not. Like if they're inside Portals and have different parents or non-consecutive siblings. If we cannot give a definite answer we could return only DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC, if we get results that are conflicting.

I believe another edge case to look at is <SuspenseList revealOrder="backwards">. I believe those children end up in reverse order. However, the fix there is probably that traverseFragmentInstanceChildren should special case those and call them in reverse. We might want to change that implementation of SuspenseList later anyway. This can be done in a separate PR. It's also interesting because in that cause maybe you do want focus() start from the end?

traverseFragmentInstance(this._fragmentFiber, collectChildren, children);
if (children.length === 0) {
return (
Node.DOCUMENT_POSITION_DISCONNECTED |
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure it makes sense to treat this as disconnected. Since the Fragment is still "in the document".

This is similar to the getRootNode() issue.

#32682 (review)

Where it should really return DISCONNECTED after it gets unmounted.

What you could do here is call it compareDocumentPosition() on the parent HostComponent or HostRoot by traversing the return path.

That should give you the right answer for anything except if you get Node.DOCUMENT_POSITION_CONTAINS since obviously that answer must be wrong. That implies that the real answer should be either Node.DOCUMENT_POSITION_PRECEDING or Node.DOCUMENT_POSITION_FOLLOWING. To answer that you could walk to the next .sibling tree to find the next HostComponent. Then compare to it to know whether the fragment is conceptually before or after.

@jackpope
Copy link
Contributor Author

jackpope commented Apr 4, 2025

Can you say more about the non-consecutive siblings case? I want to make sure I understand and can cover it. One thing we can try with portals is to skip them when traversing a fragments children for layout based methods but continue traversing through them for event based methods. For comparing the document position of a portaled element, we can return its comparison to one of the non-portaled children. In the case that you're portaling higher up in the tree it would then return as FOLLOWING.

@jackpope jackpope force-pushed the fragment-refs-comparedocposition branch from 1fd3b75 to 1a5d530 Compare April 4, 2025 17:17
@github-actions github-actions bot added the React Core Team Opened by a member of the React Core Team label Apr 4, 2025
Comment on lines +2533 to +2535
if (parentHostInstance === null) {
return Node.DOCUMENT_POSITION_DISCONNECTED;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added check for unmounted to return DISCONNECTED

children,
);

if (children.length === 0) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated support for empty fragments to compare against parent/siblings

Comment on lines +358 to +359
} else if (skipPortals && child.tag === HostPortal) {
// Skip portals
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optionally skip portal components during child traversal.
Idea being that for layout related instance methods, we don't want to special case their children as child host targets for the fragment. But for event registration we would.

@sebmarkbage
Copy link
Collaborator

Can you say more about the non-consecutive siblings case?

It's possible to insert DOM nodes between React children. E.g. by manually insertBefore a React child. You shouldn't really do this but people could. Extensions too.

Another case is if you have multiple Portals targeting the same container they can end up interleaved.

This can end up with the <p> in the middle because the div is added to the end of body (appendChild) later. So if you have a ref on p and end up comparing it to the fragment it can actually be kind of inside of it.

let [c, setC] = useState(false);
useEffect(() => {
  setC(true);
});
<>
  {createPortal(<Fragment ref={...}><div />{c ? <div /> : null}</Fragment>, document.body)}
  {createPortal(<p />, document.body)}
</>

Note that in this case the Portal isn't even inside the Fragment but it could also wrap it.

@sebmarkbage
Copy link
Collaborator

While I'd love to skip Portals (I'm dealing with a gesture cloning thing with portals that I don't know how to workaround) but in practice, it's just too easy to end up with these. Especially in Fragments. The Portal can be used in many different ways like it can just be a way to render a child into a component, or it can be part of a Page component showing a model, where a Layout wrapping it has no idea. So I think we need the Portal to do something useful. Like if I'm just wrapping a <Fragment ref={...}><Modal /></Fragment> and it's just a simple child it should just work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed React Core Team Opened by a member of the React Core Team
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants