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 scrollIntoView to fragment instances #32814

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

Conversation

jackpope
Copy link
Contributor

@jackpope jackpope commented Apr 3, 2025

Based off #32813

This adds scrollIntoView(alignToTop). It doesn't yet support scrollIntoView(options).

Cases:

  • No host children: Without host children, we represent the virtual space of the Fragment by attempting to scroll to the nearest edge by using its siblings. If the preferred sibling is not found, we'll try the other side, and then the parent.
  • 1 host child: The simplest case where its the equivalent of calling the method on the child element directly
  • Multiple host children:
    • Single scroll container:
      • Here we find the first child in the list for alignToTop=true|undefined or the last child alignToTop=false. We call scroll on that element.
    • Multiple scroll containers (sticky/fixed positioning or portals):
      • In order to handle the possibility of children being fixed or sticky or portal-ed, where the assumption is that isn't where you want to stop scroll, host children are grouped by the container. Then scroll is attempted on the first (or last) of each container. If the previous scroll was to a sticky/fixed element, or if the target isnt within the viewport and scrolling to it wouldn't put the previous one out of the viewport, scroll again.

I'll continue testing on some additional examples to see if these semantics need adjusting but publishing in the meantime for any initial feedback

@github-actions github-actions bot added the React Core Team Opened by a member of the React Core Team label Apr 3, 2025
fiber => {
const hostNode = getHostNodeFromHostFiber(fiber);
const position = getComputedStyle(hostNode).position;
return position === 'sticky' || position === 'fixed';
Copy link
Collaborator

Choose a reason for hiding this comment

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

If a child is sticky or fixed, we really shouldn't need to call scrollIntoView on them because they're always in the viewport. At least fixed. Where as sticky might need it but not because it is sticky but because its parent might be outside the viewport which makes it no different from other nodes.

What makes a group interesting is really the parent of the hostNode, not the hostNode itself. It's the question of whether calling it would be able to shift a different parent than another node.

Unfortunately, the scrollable parent might not be the immediate DOM node parent. It might be any of the parents. In fact, commonly it's the root document.documentElement.

The other issue is that we'd have to call getComputedStyle(...) (checking overflow and position) on every parent above to figure out if it would. However, we can be smarter than that. To know if two nodes are in different scroll parents we only have to answer the question if there are any scrollable things between the shared common ancestor and each of the nodes.

In the common case the shared common ancestor is the parent node and so there are nothing in between and so no need to make a getComputedStyle call. Worst case something is like a portal in document.body whose child is not position stick/fixed and a deep node sibling. In that case we'd have to check every parent of the deep node to figure out if there's a scroll between. But this would be very unusual.

) {
// Skip hidden subtrees
} else {
// Add children of a portal into their own group
Copy link
Collaborator

Choose a reason for hiding this comment

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

There's not really anything special about portals in that you can have one portal point to the same parent as the regular children and a different portal to document.body. Really what matters is the .parentNode of the DOM node inside the portal.

@sebmarkbage
Copy link
Collaborator

An interesting case is something like:

<div style={{ overflow: 'scroll' }}>
  <div id="a"></div>
  <div style={{ overflow: 'scroll' }}>
     <div id="b"></div>
  </div>
</div>

Then you have a fragment with two portals that render into a and b.

Showing both would require scrolling both the child inside a and the child inside b into view. However, if you call it on a and then b you might scroll a out of view as a result. You could flip it and scroll b into view first and then a since in general we want to show the top edge.

I think technically scrolling b into view is not technically required by the API since we don't guarantee all children all visible since that may be impossible. However, if we didn't then if a is like a portal into a toolbar (for example a breadcrumb) then just showing that isn't sufficient to show the primary content which is in a nested scroll below the toolbar.

So I do think we need to treat this case as first scrolling b into view and then scrolling a into view to attempt to show both.

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.

3 participants