[DOM] Handle scrolling of empty Fragments below containers (#37061)
e.g. `render(<Fragment ref={...}>{children}</Fragment>)` where `children` contain no Host Components. For `Document` we just noop because some part of the document is always in view. For `ShadowRoot` we use its `host`. Any other `DocumentFragment` without a `host` issues a warning because there's nothing to scroll. This matches the existing warning when you call `scrollIntoView(false)` on an empty Fragment without siblings. Notably this also applies to `<html><body /></html>` in `children` because we skip `HostSingleton` at the moment. --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sebastian "Sebbie" Silbermann committed
Jul 20, 2026 at 22:40 UTC
fa5212c188a1598cbc3a29b9abd753c83373a565
2 files changed
+69
-6
packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+28
-6
@@ -3614,12 +3614,34 @@ if (enableFragmentRefsScrollIntoView) {
3614
const target = getInstanceFromHostFiber<Instance | Container>(
3615
targetFiber,
3616
);
3617
- // TODO: If the parent host fiber is a HostRoot, the target is a
3618
- // Container which can be a Document or DocumentFragment. Those have no
3619
- // scrollIntoView method, so this crashes at runtime.
3620
- // $FlowFixMe[prop-missing]
3621
- target.scrollIntoView(alignToTop);
3622
- return;
3617
+ // If the parent host fiber is a HostRoot, the target is a Container
3618
+ // which is not necessarily an Element with a scrollIntoView method.
3619
+ if (target.nodeType === DOCUMENT_NODE) {
3620
+ // A Document is always in view.
3621
+ } else if (target.nodeType === DOCUMENT_FRAGMENT_NODE) {
3622
+ const fragment = target as any as DocumentFragment;
3623
+ // ShadowRoot always has a host: https://dom.spec.whatwg.org/#ref-for-concept-documentfragment-host%E2%91%A5
3624
+ // A generic DocumentFragment doesn't implement this property but conceptually
3625
+ // host is a nullable Element: https://dom.spec.whatwg.org/#concept-documentfragment-host
3626
+ const host =
3627
+ 'host' in fragment ? (fragment as any as ShadowRoot).host : null;
3628
+ if (host !== null) {
3629
+ // The ShadowRoot's host element marks the position where the
3630
+ // fragment's content would appear.
3631
+ host.scrollIntoView(alignToTop);
3632
+ } else if (__DEV__) {
3633
+ console.warn(
3634
+ 'You are attempting to scroll a FragmentInstance that is only ' +
3635
+ 'mounted inside a detached DocumentFragment. No scroll was ' +
3636
+ 'performed.',
3637
+ );
3638
+ }
3639
+ return;
3640
+ } else {
3641
+ // Narrowed down to Element by nodeType check above, but Flow doesn't know that.
3642
+ const element = target as any as Element;
3643
+ element.scrollIntoView(alignToTop);
3644
+ }
3645
}
3646
3647
let i = resolvedAlignToTop ? children.length - 1 : 0;
packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js
+41
@@ -22,6 +22,7 @@ let simulateIntersection;
22
let setClientRects;
23
let mockRangeClientRects;
24
let assertConsoleErrorDev;
25
+let assertConsoleWarnDev;
26
27
function Wrapper({children}) {
28
return children;
@@ -44,6 +45,7 @@ describe('FragmentRefs', () => {
45
mockRangeClientRects = IntersectionMocks.mockRangeClientRects;
46
assertConsoleErrorDev =
47
require('internal-test-utils').assertConsoleErrorDev;
48
+ assertConsoleWarnDev = require('internal-test-utils').assertConsoleWarnDev;
49
50
container = document.createElement('div');
51
document.body.innerHTML = '';
@@ -2588,6 +2590,45 @@ describe('FragmentRefs', () => {
2590
fragmentRef.current.scrollIntoView();
2591
expect(parentRef.current.scrollIntoView).toHaveBeenCalledTimes(1);
2592
});
2593
+
2594
+ // @gate enableFragmentRefs && enableFragmentRefsScrollIntoView
2595
+ it('scrolls the host element when the fallback target is a ShadowRoot container', async () => {
2596
+ const fragmentRef = React.createRef();
2597
+ const host = document.createElement('div');
2598
+ container.appendChild(host);
2599
+ const shadowRoot = host.attachShadow({mode: 'open'});
2600
+ const root = ReactDOMClient.createRoot(shadowRoot);
2601
+ await act(() => {
2602
+ root.render(<Fragment ref={fragmentRef} />);
2603
+ });
2604
+
2605
+ // The ShadowRoot's host element marks where the fragment's content
2606
+ // would appear
2607
+ host.scrollIntoView = jest.fn();
2608
+ fragmentRef.current.scrollIntoView();
2609
+ expect(host.scrollIntoView).toHaveBeenCalledTimes(1);
2610
+ });
2611
+
2612
+ // @gate enableFragmentRefs && enableFragmentRefsScrollIntoView
2613
+ it('warns without scrolling when the fallback target is a detached DocumentFragment container', async () => {
2614
+ const fragmentRef = React.createRef();
2615
+ const root = ReactDOMClient.createRoot(
2616
+ document.createDocumentFragment(),
2617
+ );
2618
+ await act(() => {
2619
+ root.render(<Fragment ref={fragmentRef} />);
2620
+ });
2621
+
2622
+ expect(() => fragmentRef.current.scrollIntoView()).not.toThrow();
2623
+ assertConsoleWarnDev(
2624
+ [
2625
+ 'You are attempting to scroll a FragmentInstance that is only ' +
2626
+ 'mounted inside a detached DocumentFragment. No scroll was ' +
2627
+ 'performed.',
2628
+ ],
2629
+ {withoutStack: true},
2630
+ );
2631
+ });
2632
});
2633
});
2634