@samitouri / QOS-React / commits / 2ba07c6d52

[DOM] Scroll to text siblings of empty Fragments instead of the parent (#37060)

When a Fragment has no children, React would consider scrolling to siblings first and then to parents. However, React only considered `HostComponent` for the siblings. Since we already have a heuristic for scrolling to `HostText`, we can reuse that same heuristic. --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

Sebastian "Sebbie" Silbermann committed Jul 19, 2026 at 21:41 UTC 2ba07c6d52ba851ed896a50477d99aa7b70c7a08
5 files changed +267 -90
packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+124 -60
@@ -62,13 +62,13 @@ import {
62 isOwnedInstance,
63 } from './ReactDOMComponentTree';
64 import {
65 - traverseFragmentInstance,
66 - getFragmentParentHostFiber,
65 + traverseFragmentInstancesAndTextInstances,
66 + getFragmentParentInstanceOrContainerFiber,
67 getInstanceFromHostFiber,
68 isFiberFollowing,
69 isFiberPreceding,
70 - getFragmentInstanceSiblings,
71 - traverseFragmentInstanceDeeply,
70 + getFragmentInstanceOrTextInstanceSiblings,
71 + traverseFragmentInstancesAndTextInstancesDeeply,
72 fiberIsPortaledIntoHost,
73 isFiberContainedByFragment,
74 isFragmentContainedByFiber,
@@ -223,6 +223,9 @@ export type TextInstance = Text;
223 type InstanceWithFragmentHandles = Instance & {
224 reactFragments?: Set<FragmentInstanceType>,
225 };
226 +type HostNodeWithFragmentHandles = (Instance | TextInstance) & {
227 + reactFragments?: Set<FragmentInstanceType>,
228 +};
229
230 declare class ActivityInterface extends Comment {}
231 declare class SuspenseInterface extends Comment {
@@ -3036,7 +3039,7 @@ FragmentInstance.prototype.addEventListener = function (
3039 indexOfEventListener(listeners, type, listener, optionsOrUseCapture) === -1;
3040 if (isNewEventListener) {
3041 listeners.push({type, listener, optionsOrUseCapture});
3039 - traverseFragmentInstance(
3042 + traverseFragmentInstancesAndTextInstances(
3043 this._fragmentFiber,
3044 addEventListenerToChild,
3045 type,
@@ -3052,7 +3055,7 @@ function addEventListenerToChild(
3055 listener: EventListener,
3056 optionsOrUseCapture?: EventListenerOptionsOrUseCapture,
3057 ): boolean {
3055 - const instance = getInstanceFromHostFiber<Instance>(child);
3058 + const instance = getInstanceFromHostFiber<Instance | TextInstance>(child);
3059 instance.addEventListener(type, listener, optionsOrUseCapture);
3060 return false;
3061 }
@@ -3068,7 +3071,7 @@ FragmentInstance.prototype.removeEventListener = function (
3071 return;
3072 }
3073 if (typeof listeners !== 'undefined' && listeners.length > 0) {
3071 - traverseFragmentInstance(
3074 + traverseFragmentInstancesAndTextInstances(
3075 this._fragmentFiber,
3076 removeEventListenerFromChild,
3077 type,
@@ -3092,7 +3095,7 @@ function removeEventListenerFromChild(
3095 listener: EventListener,
3096 optionsOrUseCapture?: EventListenerOptionsOrUseCapture,
3097 ): boolean {
3095 - const instance = getInstanceFromHostFiber<Instance>(child);
3098 + const instance = getInstanceFromHostFiber<Instance | TextInstance>(child);
3099 instance.removeEventListener(type, listener, optionsOrUseCapture);
3100 return false;
3101 }
@@ -3136,12 +3139,15 @@ FragmentInstance.prototype.dispatchEvent = function (
3139 this: FragmentInstanceType,
3140 event: Event,
3141 ): boolean {
3139 - const parentHostFiber = getFragmentParentHostFiber(this._fragmentFiber);
3142 + const parentHostFiber = getFragmentParentInstanceOrContainerFiber(
3143 + this._fragmentFiber,
3144 + );
3145 if (parentHostFiber === null) {
3146 return true;
3147 }
3143 - const parentHostInstance =
3144 - getInstanceFromHostFiber<Instance>(parentHostFiber);
3148 + const parentHostInstance = getInstanceFromHostFiber<Instance | Container>(
3149 + parentHostFiber,
3150 + );
3151 const eventListeners = this._eventListeners;
3152 if (
3153 (eventListeners !== null && eventListeners.length > 0) ||
@@ -3173,7 +3179,7 @@ FragmentInstance.prototype.focus = function (
3179 this: FragmentInstanceType,
3180 focusOptions?: FocusOptions,
3181 ): void {
3176 - traverseFragmentInstanceDeeply(
3182 + traverseFragmentInstancesAndTextInstancesDeeply(
3183 this._fragmentFiber,
3184 setFocusOnFiberIfFocusable,
3185 focusOptions,
@@ -3198,7 +3204,7 @@ FragmentInstance.prototype.focusLast = function (
3204 focusOptions?: FocusOptions,
3205 ): void {
3206 const children: Array<Fiber> = [];
3201 - traverseFragmentInstanceDeeply(
3207 + traverseFragmentInstancesAndTextInstancesDeeply(
3208 this._fragmentFiber,
3209 collectChildren,
3210 children,
@@ -3217,18 +3223,25 @@ function collectChildren(child: Fiber, collection: Array<Fiber>): boolean {
3223 // $FlowFixMe[prop-missing]
3224 FragmentInstance.prototype.blur = function (this: FragmentInstanceType): void {
3225 // Early exit if activeElement is not within the fragment's parent
3220 - const parentHostFiber = getFragmentParentHostFiber(this._fragmentFiber);
3226 + const parentHostFiber = getFragmentParentInstanceOrContainerFiber(
3227 + this._fragmentFiber,
3228 + );
3229 if (parentHostFiber === null) {
3230 return;
3231 }
3224 - const parentHostInstance =
3225 - getInstanceFromHostFiber<Instance>(parentHostFiber);
3226 - const activeElement = parentHostInstance.ownerDocument.activeElement;
3227 - if (activeElement === null || !parentHostInstance.contains(activeElement)) {
3232 + const parentInstanceOrContainer = getInstanceFromHostFiber<
3233 + Instance | Container,
3234 + >(parentHostFiber);
3235 + // TODO: Handle parentInstanceOrContainer being a document
3236 + const activeElement = parentInstanceOrContainer.ownerDocument.activeElement;
3237 + if (
3238 + activeElement === null ||
3239 + !parentInstanceOrContainer.contains(activeElement)
3240 + ) {
3241 return;
3242 }
3243
3231 - traverseFragmentInstance(
3244 + traverseFragmentInstancesAndTextInstances(
3245 this._fragmentFiber,
3246 blurActiveElementWithinFragment,
3247 activeElement,
@@ -3259,16 +3272,19 @@ FragmentInstance.prototype.observeUsing = function (
3272 if (enableFragmentRefsTextNodes) {
3273 let hasText = false;
3274 let hasElement = false;
3262 - traverseFragmentInstance(this._fragmentFiber, (child: Fiber) => {
3263 - if (child.tag === HostText) {
3264 - hasText = true;
3265 - } else {
3266 - // Stop traversal, found element
3267 - hasElement = true;
3268 - return true;
3269 - }
3270 - return false;
3271 - });
3275 + traverseFragmentInstancesAndTextInstances(
3276 + this._fragmentFiber,
3277 + (child: Fiber) => {
3278 + if (child.tag === HostText) {
3279 + hasText = true;
3280 + } else {
3281 + // Stop traversal, found element
3282 + hasElement = true;
3283 + return true;
3284 + }
3285 + return false;
3286 + },
3287 + );
3288 if (hasText && !hasElement) {
3289 console.error(
3290 'observeUsing() was called on a FragmentInstance with only text children. ' +
@@ -3281,7 +3297,11 @@ FragmentInstance.prototype.observeUsing = function (
3297 this._observers = new Set();
3298 }
3299 this._observers.add(observer);
3284 - traverseFragmentInstance(this._fragmentFiber, observeChild, observer);
3300 + traverseFragmentInstancesAndTextInstances(
3301 + this._fragmentFiber,
3302 + observeChild,
3303 + observer,
3304 + );
3305 };
3306 function observeChild(
3307 child: Fiber,
@@ -3312,7 +3332,11 @@ FragmentInstance.prototype.unobserveUsing = function (
3332 }
3333 } else {
3334 observers.delete(observer);
3315 - traverseFragmentInstance(this._fragmentFiber, unobserveChild, observer);
3335 + traverseFragmentInstancesAndTextInstances(
3336 + this._fragmentFiber,
3337 + unobserveChild,
3338 + observer,
3339 + );
3340 }
3341 };
3342 function unobserveChild(
@@ -3334,7 +3358,11 @@ FragmentInstance.prototype.getClientRects = function (
3358 this: FragmentInstanceType,
3359 ): Array<DOMRect> {
3360 const rects: Array<DOMRect> = [];
3337 - traverseFragmentInstance(this._fragmentFiber, collectClientRects, rects);
3361 + traverseFragmentInstancesAndTextInstances(
3362 + this._fragmentFiber,
3363 + collectClientRects,
3364 + rects,
3365 + );
3366 return rects;
3367 };
3368 function collectClientRects(child: Fiber, rects: Array<DOMRect>): boolean {
@@ -3356,12 +3384,15 @@ FragmentInstance.prototype.getRootNode = function (
3384 this: FragmentInstanceType,
3385 getRootNodeOptions?: {composed: boolean},
3386 ): Document | ShadowRoot | FragmentInstanceType {
3359 - const parentHostFiber = getFragmentParentHostFiber(this._fragmentFiber);
3387 + const parentHostFiber = getFragmentParentInstanceOrContainerFiber(
3388 + this._fragmentFiber,
3389 + );
3390 if (parentHostFiber === null) {
3391 return this;
3392 }
3363 - const parentHostInstance =
3364 - getInstanceFromHostFiber<Instance>(parentHostFiber);
3393 + const parentHostInstance = getInstanceFromHostFiber<Instance | Container>(
3394 + parentHostFiber,
3395 + );
3396 const rootNode =
3397 // $FlowFixMe[incompatible-type] Flow expects Node
3398 parentHostInstance.getRootNode(getRootNodeOptions) as Document | ShadowRoot;
@@ -3372,14 +3403,21 @@ FragmentInstance.prototype.compareDocumentPosition = function (
3403 this: FragmentInstanceType,
3404 otherNode: Instance,
3405 ): number {
3375 - const parentHostFiber = getFragmentParentHostFiber(this._fragmentFiber);
3406 + const parentHostFiber = getFragmentParentInstanceOrContainerFiber(
3407 + this._fragmentFiber,
3408 + );
3409 if (parentHostFiber === null) {
3410 return Node.DOCUMENT_POSITION_DISCONNECTED;
3411 }
3412 const children: Array<Fiber> = [];
3380 - traverseFragmentInstance(this._fragmentFiber, collectChildren, children);
3381 - const parentHostInstance =
3382 - getInstanceFromHostFiber<Instance>(parentHostFiber);
3413 + traverseFragmentInstancesAndTextInstances(
3414 + this._fragmentFiber,
3415 + collectChildren,
3416 + children,
3417 + );
3418 + const parentHostInstance = getInstanceFromHostFiber<Instance | Container>(
3419 + parentHostFiber,
3420 + );
3421
3422 if (children.length === 0) {
3423 return compareDocumentPositionForEmptyFragment(
@@ -3390,8 +3428,10 @@ FragmentInstance.prototype.compareDocumentPosition = function (
3428 );
3429 }
3430
3393 - const firstNode = getInstanceFromHostFiber<Instance>(children[0]);
3394 - const lastNode = getInstanceFromHostFiber<Instance>(
3431 + const firstNode = getInstanceFromHostFiber<Instance | TextInstance>(
3432 + children[0],
3433 + );
3434 + const lastNode = getInstanceFromHostFiber<Instance | TextInstance>(
3435 children[children.length - 1],
3436 );
3437
@@ -3510,6 +3550,19 @@ function validateDocumentPositionWithFiberTree(
3550 return false;
3551 }
3552
3553 +function scrollTextNodeIntoView(
3554 + textNode: TextInstance,
3555 + resolvedAlignToTop: boolean,
3556 +): void {
3557 + const range = textNode.ownerDocument.createRange();
3558 + range.selectNodeContents(textNode);
3559 + const rect = range.getBoundingClientRect();
3560 + const scrollY = resolvedAlignToTop
3561 + ? window.scrollY + rect.top
3562 + : window.scrollY + rect.bottom - window.innerHeight;
3563 + window.scrollTo(window.scrollX + rect.left, scrollY);
3564 +}
3565 +
3566 if (enableFragmentRefsScrollIntoView) {
3567 // $FlowFixMe[prop-missing]
3568 FragmentInstance.prototype.scrollIntoView = function (
@@ -3524,17 +3577,23 @@ if (enableFragmentRefsScrollIntoView) {
3577 }
3578 // First, get the children nodes
3579 const children: Array<Fiber> = [];
3527 - traverseFragmentInstance(this._fragmentFiber, collectChildren, children);
3580 + traverseFragmentInstancesAndTextInstances(
3581 + this._fragmentFiber,
3582 + collectChildren,
3583 + children,
3584 + );
3585
3586 const resolvedAlignToTop = alignToTop !== false;
3587
3588 // If there are no children, we can use the parent and siblings to determine a position
3589 if (children.length === 0) {
3533 - const hostSiblings = getFragmentInstanceSiblings(this._fragmentFiber);
3590 + const hostSiblings = getFragmentInstanceOrTextInstanceSiblings(
3591 + this._fragmentFiber,
3592 + );
3593 const targetFiber = resolvedAlignToTop
3594 ? hostSiblings[1] ||
3595 hostSiblings[0] ||
3537 - getFragmentParentHostFiber(this._fragmentFiber)
3596 + getFragmentParentInstanceOrContainerFiber(this._fragmentFiber)
3597 : hostSiblings[0] || hostSiblings[1];
3598
3599 if (targetFiber === null) {
@@ -3546,7 +3605,19 @@ if (enableFragmentRefsScrollIntoView) {
3605 }
3606 return;
3607 }
3549 - const target = getInstanceFromHostFiber<Instance>(targetFiber);
3608 + // For text node siblings, use Range API to scroll to their position
3609 + if (enableFragmentRefsTextNodes && targetFiber.tag === HostText) {
3610 + const textNode = getInstanceFromHostFiber<TextInstance>(targetFiber);
3611 + scrollTextNodeIntoView(textNode, resolvedAlignToTop);
3612 + return;
3613 + }
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;
3623 }
@@ -3556,14 +3627,8 @@ if (enableFragmentRefsScrollIntoView) {
3627 const child = children[i];
3628 // For text nodes, use Range API to scroll to their position
3629 if (enableFragmentRefsTextNodes && child.tag === HostText) {
3559 - const textNode: Text = child.stateNode;
3560 - const range = textNode.ownerDocument.createRange();
3561 - range.selectNodeContents(textNode);
3562 - const rect = range.getBoundingClientRect();
3563 - const scrollY = resolvedAlignToTop
3564 - ? window.scrollY + rect.top
3565 - : window.scrollY + rect.bottom - window.innerHeight;
3566 - window.scrollTo(window.scrollX + rect.left, scrollY);
3630 + const textNode = getInstanceFromHostFiber<TextInstance>(child);
3631 + scrollTextNodeIntoView(textNode, resolvedAlignToTop);
3632 i += resolvedAlignToTop ? -1 : 1;
3633 continue;
3634 }
@@ -3579,17 +3644,16 @@ function addFragmentHandleToFiber(
3644 fragmentInstance: FragmentInstanceType,
3645 ): boolean {
3646 if (enableFragmentRefsInstanceHandles) {
3582 - const instance =
3583 - getInstanceFromHostFiber<InstanceWithFragmentHandles>(child);
3584 - if (instance != null) {
3585 - addFragmentHandleToInstance(instance, fragmentInstance);
3586 - }
3647 + const instance = getInstanceFromHostFiber<Instance | TextInstance>(
3648 + child,
3649 + ) as any as HostNodeWithFragmentHandles;
3650 + addFragmentHandleToInstance(instance, fragmentInstance);
3651 }
3652 return false;
3653 }
3654
3655 function addFragmentHandleToInstance(
3592 - instance: InstanceWithFragmentHandles,
3656 + instance: HostNodeWithFragmentHandles,
3657 fragmentInstance: FragmentInstanceType,
3658 ): void {
3659 if (enableFragmentRefsInstanceHandles) {
@@ -3605,7 +3669,7 @@ export function createFragmentInstance(
3669 ): FragmentInstanceType {
3670 const fragmentInstance = new (FragmentInstance as any)(fragmentFiber);
3671 if (enableFragmentRefsInstanceHandles) {
3608 - traverseFragmentInstance(
3672 + traverseFragmentInstancesAndTextInstances(
3673 fragmentFiber,
3674 addFragmentHandleToFiber,
3675 fragmentInstance,
packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js
+42
@@ -2755,6 +2755,48 @@ describe('FragmentRefs', () => {
2755 restoreRange();
2756 });
2757
2758 + // @gate enableFragmentRefs && enableFragmentRefsTextNodes && enableFragmentRefsScrollIntoView
2759 + it('scrollIntoView scrolls to text siblings of an empty fragment using the Range API', async () => {
2760 + const restoreRange = mockRangeClientRects([
2761 + {x: 100, y: 200, width: 80, height: 16},
2762 + ]);
2763 + const fragmentRef = React.createRef();
2764 + const parentRef = React.createRef();
2765 + const root = ReactDOMClient.createRoot(container);
2766 +
2767 + await act(() =>
2768 + root.render(
2769 + <div ref={parentRef}>
2770 + Text before
2771 + <Fragment ref={fragmentRef} />
2772 + Text after
2773 + </div>,
2774 + ),
2775 + );
2776 +
2777 + const parentScrollMock = jest.fn();
2778 + parentRef.current.scrollIntoView = parentScrollMock;
2779 + // Mock window.scrollTo to verify Range-based text scrolling
2780 + const originalScrollTo = window.scrollTo;
2781 + const scrollToMock = jest.fn();
2782 + window.scrollTo = scrollToMock;
2783 +
2784 + // Default call scrolls to the following text sibling
2785 + fragmentRef.current.scrollIntoView();
2786 + expect(scrollToMock).toHaveBeenCalledTimes(1);
2787 + expect(parentScrollMock).toHaveBeenCalledTimes(0);
2788 +
2789 + scrollToMock.mockClear();
2790 +
2791 + // alignToTop=false scrolls to the preceding text sibling
2792 + fragmentRef.current.scrollIntoView(false);
2793 + expect(scrollToMock).toHaveBeenCalledTimes(1);
2794 + expect(parentScrollMock).toHaveBeenCalledTimes(0);
2795 +
2796 + window.scrollTo = originalScrollTo;
2797 + restoreRange();
2798 + });
2799 +
2800 // @gate enableFragmentRefs
2801 it('treats passive:true and passive:false as same listener per DOM spec', async () => {
2802 const fragmentRef = React.createRef();
packages/react-native-renderer/src/ReactFiberConfigFabric.js
+29 -9
@@ -24,8 +24,8 @@ import {
24 import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
25 import {HostText} from 'react-reconciler/src/ReactWorkTags';
26 import {
27 - getFragmentParentHostFiber,
28 - traverseFragmentInstance,
27 + getFragmentParentInstanceOrContainerFiber,
28 + traverseFragmentInstancesAndTextInstances,
29 } from 'react-reconciler/src/ReactFiberTreeReflection';
30
31 // Modules provided by RN:
@@ -704,7 +704,11 @@ FragmentInstance.prototype.observeUsing = function (
704 this._observers = new Set();
705 }
706 this._observers.add(observer);
707 - traverseFragmentInstance(this._fragmentFiber, observeChild, observer);
707 + traverseFragmentInstancesAndTextInstances(
708 + this._fragmentFiber,
709 + observeChild,
710 + observer,
711 + );
712 };
713 function observeChild(child: Fiber, observer: IntersectionObserver) {
714 // $FlowFixMe[incompatible-type]
@@ -728,7 +732,11 @@ FragmentInstance.prototype.unobserveUsing = function (
732 }
733 } else {
734 observers.delete(observer);
731 - traverseFragmentInstance(this._fragmentFiber, unobserveChild, observer);
735 + traverseFragmentInstancesAndTextInstances(
736 + this._fragmentFiber,
737 + unobserveChild,
738 + observer,
739 + );
740 }
741 };
742 function unobserveChild(child: Fiber, observer: IntersectionObserver) {
@@ -744,12 +752,18 @@ FragmentInstance.prototype.compareDocumentPosition = function (
752 this: FragmentInstanceType,
753 otherNode: PublicInstance,
754 ): number {
747 - const parentHostFiber = getFragmentParentHostFiber(this._fragmentFiber);
755 + const parentHostFiber = getFragmentParentInstanceOrContainerFiber(
756 + this._fragmentFiber,
757 + );
758 if (parentHostFiber === null) {
759 return Node.DOCUMENT_POSITION_DISCONNECTED;
760 }
761 const children: Array<Fiber> = [];
752 - traverseFragmentInstance(this._fragmentFiber, collectChildren, children);
762 + traverseFragmentInstancesAndTextInstances(
763 + this._fragmentFiber,
764 + collectChildren,
765 + children,
766 + );
767 if (children.length === 0) {
768 const parentHostInstance = getPublicInstanceFromHostFiber(parentHostFiber);
769 return compareDocumentPositionForEmptyFragment<PublicInstance>(
@@ -804,7 +818,9 @@ FragmentInstance.prototype.getRootNode = function (
818 this: FragmentInstanceType,
819 getRootNodeOptions?: {composed: boolean},
820 ): Node | FragmentInstanceType {
807 - const parentHostFiber = getFragmentParentHostFiber(this._fragmentFiber);
821 + const parentHostFiber = getFragmentParentInstanceOrContainerFiber(
822 + this._fragmentFiber,
823 + );
824 if (parentHostFiber === null) {
825 return this;
826 }
@@ -819,7 +835,11 @@ FragmentInstance.prototype.getClientRects = function (
835 this: FragmentInstanceType,
836 ): Array<DOMRect> {
837 const rects: Array<DOMRect> = [];
822 - traverseFragmentInstance(this._fragmentFiber, collectClientRects, rects);
838 + traverseFragmentInstancesAndTextInstances(
839 + this._fragmentFiber,
840 + collectClientRects,
841 + rects,
842 + );
843 return rects;
844 };
845 function collectClientRects(child: Fiber, rects: Array<DOMRect>): boolean {
@@ -867,7 +887,7 @@ export function createFragmentInstance(
887 ): FragmentInstanceType {
888 const fragmentInstance = new (FragmentInstance as any)(fragmentFiber);
889 if (enableFragmentRefsInstanceHandles) {
870 - traverseFragmentInstance(
890 + traverseFragmentInstancesAndTextInstances(
891 fragmentFiber,
892 addFragmentHandleToFiber,
893 fragmentInstance,
packages/react-reconciler/src/ReactFiberTreeReflection.js
+69 -19
@@ -10,6 +10,8 @@
10 import type {Fiber} from './ReactInternalTypes';
11 import type {
12 Container,
13 + Instance,
14 + TextInstance,
15 ActivityInstance,
16 SuspenseInstance,
17 } from './ReactFiberConfig';
@@ -345,27 +347,41 @@ export function doesFiberContain(
347 return false;
348 }
349
348 -export function traverseFragmentInstance<A, B, C>(
350 +export function traverseFragmentInstancesAndTextInstances<A, B, C>(
351 fragmentFiber: Fiber,
352 fn: (Fiber, A, B, C) => boolean,
353 a: A,
354 b: B,
355 c: C,
356 ): void {
355 - traverseVisibleHostChildren(fragmentFiber.child, false, fn, a, b, c);
357 + traverseVisibleInstancesAndTextInstances(
358 + fragmentFiber.child,
359 + false,
360 + fn,
361 + a,
362 + b,
363 + c,
364 + );
365 }
366
358 -export function traverseFragmentInstanceDeeply<A, B, C>(
367 +export function traverseFragmentInstancesAndTextInstancesDeeply<A, B, C>(
368 fragmentFiber: Fiber,
369 fn: (Fiber, A, B, C) => boolean,
370 a: A,
371 b: B,
372 c: C,
373 ): void {
365 - traverseVisibleHostChildren(fragmentFiber.child, true, fn, a, b, c);
374 + traverseVisibleInstancesAndTextInstances(
375 + fragmentFiber.child,
376 + true,
377 + fn,
378 + a,
379 + b,
380 + c,
381 + );
382 }
383
368 -function traverseVisibleHostChildren<A, B, C>(
384 +function traverseVisibleInstancesAndTextInstances<A, B, C>(
385 child: Fiber | null,
386 searchWithinHosts: boolean,
387 fn: (Fiber, A, B, C) => boolean,
@@ -387,7 +403,14 @@ function traverseVisibleHostChildren<A, B, C>(
403 } else {
404 if (
405 (searchWithinHosts || child.tag !== HostComponent) &&
390 - traverseVisibleHostChildren(child.child, searchWithinHosts, fn, a, b, c)
406 + traverseVisibleInstancesAndTextInstances(
407 + child.child,
408 + searchWithinHosts,
409 + fn,
410 + a,
411 + b,
412 + c,
413 + )
414 ) {
415 return true;
416 }
@@ -397,7 +420,9 @@ function traverseVisibleHostChildren<A, B, C>(
420 return false;
421 }
422
400 -export function getFragmentParentHostFiber(fiber: Fiber): null | Fiber {
423 +export function getFragmentParentInstanceOrContainerFiber(
424 + fiber: Fiber,
425 +): null | Fiber {
426 let parent = fiber.return;
427 while (parent !== null) {
428 if (parent.tag === HostRoot || parent.tag === HostComponent) {
@@ -424,20 +449,27 @@ export function fiberIsPortaledIntoHost(fiber: Fiber): boolean {
449 return foundPortalParent;
450 }
451
427 -export function getFragmentInstanceSiblings(
452 +export function getFragmentInstanceOrTextInstanceSiblings(
453 fiber: Fiber,
454 ): [Fiber | null, Fiber | null] {
455 const result: [Fiber | null, Fiber | null] = [null, null];
431 - const parentHostFiber = getFragmentParentHostFiber(fiber);
456 + const parentHostFiber = getFragmentParentInstanceOrContainerFiber(fiber);
457 if (parentHostFiber === null) {
458 return result;
459 }
460
436 - findFragmentInstanceSiblings(result, fiber, parentHostFiber.child);
461 + findFragmentInstanceOrTextInstanceSiblings(
462 + result,
463 + fiber,
464 + parentHostFiber.child,
465 + );
466 return result;
467 }
468
440 -function findFragmentInstanceSiblings(
469 +/**
470 + * Only collects HostText with enableFragmentRefsTextNodes enabled. Otherwise, only collects HostComponent.
471 + */
472 +function findFragmentInstanceOrTextInstanceSiblings(
473 result: [Fiber | null, Fiber | null],
474 self: Fiber,
475 child: null | Fiber,
@@ -452,7 +484,10 @@ function findFragmentInstanceSiblings(
484 return true;
485 }
486 }
455 - if (child.tag === HostComponent) {
487 + if (
488 + child.tag === HostComponent ||
489 + (enableFragmentRefsTextNodes && child.tag === HostText)
490 + ) {
491 if (foundSelf) {
492 result[1] = child;
493 return true;
@@ -465,7 +500,14 @@ function findFragmentInstanceSiblings(
500 ) {
501 // Skip hidden subtrees
502 } else {
468 - if (findFragmentInstanceSiblings(result, self, child.child, foundSelf)) {
503 + if (
504 + findFragmentInstanceOrTextInstanceSiblings(
505 + result,
506 + self,
507 + child.child,
508 + foundSelf,
509 + )
510 + ) {
511 return true;
512 }
513 }
@@ -474,7 +516,9 @@ function findFragmentInstanceSiblings(
516 return false;
517 }
518
477 -export function getInstanceFromHostFiber<I>(fiber: Fiber): I {
519 +export function getInstanceFromHostFiber<
520 + I: Instance | TextInstance | Container,
521 +>(fiber: Fiber): I {
522 switch (fiber.tag) {
523 case HostComponent:
524 case HostText:
@@ -501,8 +545,14 @@ function popSearchBoundary(): null | Fiber {
545 return searchBoundary;
546 }
547
504 -export function getNextSiblingHostFiber(fiber: Fiber): null | Fiber {
505 - traverseVisibleHostChildren(fiber.sibling, false, findNextSibling);
548 +export function getNextSiblingInstanceOrTextInstanceFiber(
549 + fiber: Fiber,
550 +): null | Fiber {
551 + traverseVisibleInstancesAndTextInstances(
552 + fiber.sibling,
553 + false,
554 + findNextSibling,
555 + );
556 const sibling = popSearchTarget();
557 pushSearchTarget(null);
558 return sibling;
@@ -536,7 +586,7 @@ export function isFragmentContainedByFiber(
586 ): boolean {
587 let current: Fiber | null = fragmentFiber;
588 const fiberHostParent: Fiber | null =
539 - getFragmentParentHostFiber(fragmentFiber);
589 + getFragmentParentInstanceOrContainerFiber(fragmentFiber);
590 while (current !== null) {
591 if (
592 (current.tag === HostComponent || current.tag === HostRoot) &&
@@ -558,7 +608,7 @@ export function isFiberPreceding(fiber: Fiber, otherFiber: Fiber): boolean {
608 if (commonAncestor === null) {
609 return false;
610 }
561 - traverseVisibleHostChildren(
611 + traverseVisibleInstancesAndTextInstances(
612 commonAncestor,
613 true,
614 isFiberPrecedingCheck,
@@ -594,7 +644,7 @@ export function isFiberFollowing(fiber: Fiber, otherFiber: Fiber): boolean {
644 if (commonAncestor === null) {
645 return false;
646 }
597 - traverseVisibleHostChildren(
647 + traverseVisibleInstancesAndTextInstances(
648 commonAncestor,
649 true,
650 isFiberFollowingCheck,
packages/shared/ReactDOMFragmentRefShared.js
+3 -2
@@ -11,7 +11,7 @@
11
12 import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
13
14 -import {getNextSiblingHostFiber} from 'react-reconciler/src/ReactFiberTreeReflection';
14 +import {getNextSiblingInstanceOrTextInstanceFiber} from 'react-reconciler/src/ReactFiberTreeReflection';
15
16 export function compareDocumentPositionForEmptyFragment<TPublicInstance>(
17 fragmentFiber: Fiber,
@@ -32,7 +32,8 @@ export function compareDocumentPositionForEmptyFragment<TPublicInstance>(
32 if (parentResult & Node.DOCUMENT_POSITION_CONTAINED_BY) {
33 // otherNode is one of the fragment's siblings. Use the next
34 // sibling to determine if its preceding or following.
35 - const nextSiblingFiber = getNextSiblingHostFiber(fragmentFiber);
35 + const nextSiblingFiber =
36 + getNextSiblingInstanceOrTextInstanceFiber(fragmentFiber);
37 if (nextSiblingFiber === null) {
38 result = Node.DOCUMENT_POSITION_PRECEDING;
39 } else {