Add flag enableFragmentRefsScrollIntoView, prefix with experimental_

This commit is contained in:
Jack Pope
2025-08-27 15:26:46 -04:00
parent 716ccf8862
commit 6b66e62148
13 changed files with 70 additions and 58 deletions
@@ -55,11 +55,11 @@ export default function ScrollIntoViewCase() {
const scrollContainerRef = useRef(null);
const scrollVertical = () => {
fragmentRef.current.scrollIntoView(alignToTop);
fragmentRef.current.experimental_scrollIntoView(alignToTop);
};
const scrollVerticalNoChildren = () => {
noChildRef.current.scrollIntoView(alignToTop);
noChildRef.current.experimental_scrollIntoView(alignToTop);
};
useEffect(() => {
+38 -35
View File
@@ -124,6 +124,7 @@ import {
enableSrcObject,
enableViewTransition,
enableHydrationChangeEvent,
enableFragmentRefsScrollIntoView,
} from 'shared/ReactFeatureFlags';
import {
HostComponent,
@@ -3248,46 +3249,48 @@ function validateDocumentPositionWithFiberTree(
return false;
}
// $FlowFixMe[prop-missing]
FragmentInstance.prototype.scrollIntoView = function (
this: FragmentInstanceType,
alignToTop?: boolean,
): void {
if (typeof alignToTop === 'object') {
throw new Error(
'FragmentInstance.scrollIntoView() does not support ' +
'scrollIntoViewOptions. Use the alignToTop boolean instead.',
);
}
// First, get the children nodes
const children: Array<Fiber> = [];
traverseFragmentInstance(this._fragmentFiber, collectChildren, children);
if (enableFragmentRefsScrollIntoView) {
// $FlowFixMe[prop-missing]
FragmentInstance.prototype.experimental_scrollIntoView = function (
this: FragmentInstanceType,
alignToTop?: boolean,
): void {
if (typeof alignToTop === 'object') {
throw new Error(
'FragmentInstance.experimental_scrollIntoView() does not support ' +
'scrollIntoViewOptions. Use the alignToTop boolean instead.',
);
}
// First, get the children nodes
const children: Array<Fiber> = [];
traverseFragmentInstance(this._fragmentFiber, collectChildren, children);
// If there are no children, we can use the parent and siblings to determine a position
if (children.length === 0) {
const hostSiblings = getFragmentInstanceSiblings(this._fragmentFiber);
const targetFiber =
(alignToTop === false
? hostSiblings[0] || hostSiblings[1]
: hostSiblings[1] || hostSiblings[0]) ||
getFragmentParentHostFiber(this._fragmentFiber);
if (targetFiber === null) {
if (__DEV__) {
console.error(
'You are attempting to scroll a FragmentInstance that has no ' +
'children, siblings, or parent. No scroll was performed.',
);
// If there are no children, we can use the parent and siblings to determine a position
if (children.length === 0) {
const hostSiblings = getFragmentInstanceSiblings(this._fragmentFiber);
const targetFiber =
(alignToTop === false
? hostSiblings[0] || hostSiblings[1]
: hostSiblings[1] || hostSiblings[0]) ||
getFragmentParentHostFiber(this._fragmentFiber);
if (targetFiber === null) {
if (__DEV__) {
console.error(
'You are attempting to scroll a FragmentInstance that has no ' +
'children, siblings, or parent. No scroll was performed.',
);
}
return;
}
const target = getInstanceFromHostFiber<Instance>(targetFiber);
target.scrollIntoView(alignToTop);
return;
}
const target = getInstanceFromHostFiber<Instance>(targetFiber);
target.scrollIntoView(alignToTop);
return;
}
// If there are children, handle them per scroll container
scrollIntoViewByScrollContainer(children, alignToTop !== false);
};
// If there are children, handle them per scroll container
scrollIntoViewByScrollContainer(children, alignToTop !== false);
};
}
function isInstanceScrollable(inst: Instance): 0 | 1 | 2 {
const style = getComputedStyle(inst);
+20 -20
View File
@@ -1844,7 +1844,7 @@ describe('FragmentRefs', () => {
});
describe('scrollIntoView', () => {
// @gate enableFragmentRefs
// @gate enableFragmentRefs && enableFragmentRefsScrollIntoView
it('does not yet support options', async () => {
const fragmentRef = React.createRef();
const root = ReactDOMClient.createRoot(container);
@@ -1853,15 +1853,15 @@ describe('FragmentRefs', () => {
});
expect(() => {
fragmentRef.current.scrollIntoView({block: 'start'});
fragmentRef.current.experimental_scrollIntoView({block: 'start'});
}).toThrowError(
'FragmentInstance.scrollIntoView() does not support ' +
'FragmentInstance.experimental_scrollIntoView() does not support ' +
'scrollIntoViewOptions. Use the alignToTop boolean instead.',
);
});
describe('with children', () => {
// @gate enableFragmentRefs
// @gate enableFragmentRefs && enableFragmentRefsScrollIntoView
it('calls scrollIntoView on the first child by default, or if alignToTop=true', async () => {
const fragmentRef = React.createRef();
const childARef = React.createRef();
@@ -1883,19 +1883,19 @@ describe('FragmentRefs', () => {
childBRef.current.scrollIntoView = jest.fn();
// Default call
fragmentRef.current.scrollIntoView();
fragmentRef.current.experimental_scrollIntoView();
expect(childARef.current.scrollIntoView).toHaveBeenCalledTimes(1);
expect(childBRef.current.scrollIntoView).toHaveBeenCalledTimes(0);
childARef.current.scrollIntoView.mockClear();
// alignToTop=true
fragmentRef.current.scrollIntoView(true);
fragmentRef.current.experimental_scrollIntoView(true);
expect(childARef.current.scrollIntoView).toHaveBeenCalledTimes(1);
expect(childBRef.current.scrollIntoView).toHaveBeenCalledTimes(0);
});
// @gate enableFragmentRefs
// @gate enableFragmentRefs && enableFragmentRefsScrollIntoView
it('calls scrollIntoView on the last child if alignToTop is false', async () => {
const fragmentRef = React.createRef();
const childARef = React.createRef();
@@ -1913,12 +1913,12 @@ describe('FragmentRefs', () => {
childARef.current.scrollIntoView = jest.fn();
childBRef.current.scrollIntoView = jest.fn();
fragmentRef.current.scrollIntoView(false);
fragmentRef.current.experimental_scrollIntoView(false);
expect(childARef.current.scrollIntoView).toHaveBeenCalledTimes(0);
expect(childBRef.current.scrollIntoView).toHaveBeenCalledTimes(1);
});
// @gate enableFragmentRefs
// @gate enableFragmentRefs && enableFragmentRefsScrollIntoView
it('handles portaled elements -- same scroll container', async () => {
const fragmentRef = React.createRef();
const childARef = React.createRef();
@@ -1950,12 +1950,12 @@ describe('FragmentRefs', () => {
childBRef.current.scrollIntoView = jest.fn();
// Default call
fragmentRef.current.scrollIntoView();
fragmentRef.current.experimental_scrollIntoView();
expect(childARef.current.scrollIntoView).toHaveBeenCalledTimes(1);
expect(childBRef.current.scrollIntoView).toHaveBeenCalledTimes(0);
});
// @gate enableFragmentRefs
// @gate enableFragmentRefs && enableFragmentRefsScrollIntoView
it('handles portaled elements -- different scroll container', async () => {
const fragmentRef = React.createRef();
const headerChildRef = React.createRef();
@@ -2086,7 +2086,7 @@ describe('FragmentRefs', () => {
});
// Default call
fragmentRef.current.scrollIntoView();
fragmentRef.current.experimental_scrollIntoView();
expect(childCRef.current.scrollIntoView).toHaveBeenCalledTimes(1);
// In the same group as A, we use the first child
expect(childBRef.current.scrollIntoView).toHaveBeenCalledTimes(0);
@@ -2102,7 +2102,7 @@ describe('FragmentRefs', () => {
logs = [];
// // alignToTop=false
fragmentRef.current.scrollIntoView(false);
fragmentRef.current.experimental_scrollIntoView(false);
expect(headerChildRef.current.scrollIntoView).toHaveBeenCalledTimes(1);
// In the same group as B, only attempt B which is the last child
expect(childARef.current.scrollIntoView).toHaveBeenCalledTimes(0);
@@ -2116,7 +2116,7 @@ describe('FragmentRefs', () => {
});
describe('without children', () => {
// @gate enableFragmentRefs
// @gate enableFragmentRefs && enableFragmentRefsScrollIntoView
it('calls scrollIntoView on the next sibling by default, or if alignToTop=true', async () => {
const fragmentRef = React.createRef();
const siblingARef = React.createRef();
@@ -2138,19 +2138,19 @@ describe('FragmentRefs', () => {
siblingBRef.current.scrollIntoView = jest.fn();
// Default call
fragmentRef.current.scrollIntoView();
fragmentRef.current.experimental_scrollIntoView();
expect(siblingARef.current.scrollIntoView).toHaveBeenCalledTimes(0);
expect(siblingBRef.current.scrollIntoView).toHaveBeenCalledTimes(1);
siblingBRef.current.scrollIntoView.mockClear();
// alignToTop=true
fragmentRef.current.scrollIntoView(true);
fragmentRef.current.experimental_scrollIntoView(true);
expect(siblingARef.current.scrollIntoView).toHaveBeenCalledTimes(0);
expect(siblingBRef.current.scrollIntoView).toHaveBeenCalledTimes(1);
});
// @gate enableFragmentRefs
// @gate enableFragmentRefs && enableFragmentRefsScrollIntoView
it('calls scrollIntoView on the prev sibling if alignToTop is false', async () => {
const fragmentRef = React.createRef();
const siblingARef = React.createRef();
@@ -2182,12 +2182,12 @@ describe('FragmentRefs', () => {
siblingBRef.current.scrollIntoView = jest.fn();
// alignToTop=false
fragmentRef.current.scrollIntoView(false);
fragmentRef.current.experimental_scrollIntoView(false);
expect(siblingARef.current.scrollIntoView).toHaveBeenCalledTimes(1);
expect(siblingBRef.current.scrollIntoView).toHaveBeenCalledTimes(0);
});
// @gate enableFragmentRefs
// @gate enableFragmentRefs && enableFragmentRefsScrollIntoView
it('calls scrollIntoView on the parent if there are no siblings', async () => {
const fragmentRef = React.createRef();
const parentRef = React.createRef();
@@ -2203,7 +2203,7 @@ describe('FragmentRefs', () => {
});
parentRef.current.scrollIntoView = jest.fn();
fragmentRef.current.scrollIntoView();
fragmentRef.current.experimental_scrollIntoView();
expect(parentRef.current.scrollIntoView).toHaveBeenCalledTimes(1);
});
});
+1
View File
@@ -152,6 +152,7 @@ export const transitionLaneExpirationMs = 5000;
export const enableInfiniteRenderLoopDetection: boolean = false;
export const enableFragmentRefs = __EXPERIMENTAL__;
export const enableFragmentRefsScrollIntoView = __EXPERIMENTAL__;
// -----------------------------------------------------------------------------
// Ready for next major.
@@ -25,4 +25,5 @@ export const enableEagerAlternateStateNodeCleanup = __VARIANT__;
export const passChildrenWhenCloningPersistedNodes = __VARIANT__;
export const renameElementSymbol = __VARIANT__;
export const enableFragmentRefs = __VARIANT__;
export const enableFragmentRefsScrollIntoView = __VARIANT__;
export const enableComponentPerformanceTrack = __VARIANT__;
@@ -27,6 +27,7 @@ export const {
passChildrenWhenCloningPersistedNodes,
renameElementSymbol,
enableFragmentRefs,
enableFragmentRefsScrollIntoView,
} = dynamicFlags;
// The rest of the flags are static for better dead code elimination.
@@ -73,6 +73,7 @@ export const enableDefaultTransitionIndicator: boolean = false;
export const ownerStackLimit = 1e4;
export const enableFragmentRefs: boolean = false;
export const enableFragmentRefsScrollIntoView: boolean = false;
// Profiling Only
export const enableProfilerTimer: boolean = __PROFILE__;
@@ -75,6 +75,7 @@ export const enableDefaultTransitionIndicator: boolean = false;
export const ownerStackLimit = 1e4;
export const enableFragmentRefs: boolean = false;
export const enableFragmentRefsScrollIntoView: boolean = false;
// TODO: This must be in sync with the main ReactFeatureFlags file because
// the Test Renderer's value must be the same as the one used by the
@@ -68,6 +68,7 @@ export const enableSrcObject = false;
export const enableHydrationChangeEvent = false;
export const enableDefaultTransitionIndicator = false;
export const enableFragmentRefs = false;
export const enableFragmentRefsScrollIntoView = false;
export const ownerStackLimit = 1e4;
// Flow magic to verify the exports of this file match the original version.
@@ -82,6 +82,7 @@ export const enableHydrationChangeEvent: boolean = false;
export const enableDefaultTransitionIndicator: boolean = false;
export const enableFragmentRefs: boolean = false;
export const enableFragmentRefsScrollIntoView: boolean = false;
export const ownerStackLimit = 1e4;
// Flow magic to verify the exports of this file match the original version.
@@ -35,6 +35,7 @@ export const enableViewTransition: boolean = __VARIANT__;
export const enableComponentPerformanceTrack: boolean = __VARIANT__;
export const enableScrollEndPolyfill: boolean = __VARIANT__;
export const enableFragmentRefs: boolean = __VARIANT__;
export const enableFragmentRefsScrollIntoView: boolean = __VARIANT__;
// TODO: These flags are hard-coded to the default values used in open source.
// Update the tests so that they pass in either mode, then set these
@@ -33,6 +33,7 @@ export const {
enableComponentPerformanceTrack,
enableScrollEndPolyfill,
enableFragmentRefs,
enableFragmentRefsScrollIntoView,
} = dynamicFeatureFlags;
// On WWW, __EXPERIMENTAL__ is used for a new modern build.
+1 -1
View File
@@ -551,5 +551,5 @@
"563": "This render completed successfully. All cacheSignals are now aborted to allow clean up of any unused resources.",
"564": "Unknown command. The debugChannel was not wired up properly.",
"565": "resolveDebugMessage/closeDebugChannel should not be called for a Request that wasn't kept alive. This is a bug in React.",
"566": "FragmentInstance.scrollIntoView() does not support scrollIntoViewOptions. Use the alignToTop boolean instead."
"566": "FragmentInstance.experimental_scrollIntoView() does not support scrollIntoViewOptions. Use the alignToTop boolean instead."
}