[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 {