Skip to content

Commit 01f31d4

Browse files
committed
Simplify scrollIntoView algorithm for initial version
1 parent e9e5f13 commit 01f31d4

File tree

4 files changed

+50
-313
lines changed

4 files changed

+50
-313
lines changed

fixtures/dom/src/components/fixtures/fragment-refs/ScrollIntoViewCase.js

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -90,17 +90,9 @@ export default function ScrollIntoViewCase() {
9090
<TestCase.ExpectedResult>
9191
<p>When the Fragment has children:</p>
9292
<p>
93-
The simple path is that all children are in the same scroll
94-
container. If alignToTop=true|undefined, we will select the first
95-
Fragment host child to call scrollIntoView on. Otherwise we'll call
96-
on the last host child.
97-
</p>
98-
<p>
99-
In the case of fixed elements and inserted elements or portals
100-
causing fragment siblings to be in different scroll containers, we
101-
split up the host children into groups of scroll containers. If we
102-
hit a fixed element, we'll always attempt to scroll on the first or
103-
last element of the next group, depending on alignToTop value.
93+
In order to handle the case where children are split between
94+
multiple scroll containers, we call scrollIntoView on each child in
95+
reverse order.
10496
</p>
10597
<p>When the Fragment does not have children:</p>
10698
<p>

packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js

Lines changed: 13 additions & 193 deletions
Original file line numberDiff line numberDiff line change
@@ -3265,14 +3265,17 @@ if (enableFragmentRefsScrollIntoView) {
32653265
const children: Array<Fiber> = [];
32663266
traverseFragmentInstance(this._fragmentFiber, collectChildren, children);
32673267

3268+
const resolvedAlignToTop = alignToTop !== false;
3269+
32683270
// If there are no children, we can use the parent and siblings to determine a position
32693271
if (children.length === 0) {
32703272
const hostSiblings = getFragmentInstanceSiblings(this._fragmentFiber);
3271-
const targetFiber =
3272-
(alignToTop === false
3273-
? hostSiblings[0] || hostSiblings[1]
3274-
: hostSiblings[1] || hostSiblings[0]) ||
3275-
getFragmentParentHostFiber(this._fragmentFiber);
3273+
const targetFiber = resolvedAlignToTop
3274+
? hostSiblings[1] ||
3275+
hostSiblings[0] ||
3276+
getFragmentParentHostFiber(this._fragmentFiber)
3277+
: hostSiblings[0] || hostSiblings[1];
3278+
32763279
if (targetFiber === null) {
32773280
if (__DEV__) {
32783281
console.warn(
@@ -3287,197 +3290,14 @@ if (enableFragmentRefsScrollIntoView) {
32873290
return;
32883291
}
32893292

3290-
// If there are children, handle them per scroll container
3291-
scrollIntoViewByScrollContainer(children, alignToTop !== false);
3292-
};
3293-
}
3294-
3295-
const CONTAINER_STD = 0;
3296-
const CONTAINER_FIXED = 1;
3297-
const CONTAINER_SCROLL = 2;
3298-
type ScrollableContainerType = 0 | 1 | 2;
3299-
function isInstanceScrollable(inst: Instance): ScrollableContainerType {
3300-
const style = getComputedStyle(inst);
3301-
3302-
if (style.position === 'fixed') {
3303-
return CONTAINER_FIXED;
3304-
}
3305-
3306-
if (
3307-
style.overflow === 'auto' ||
3308-
style.overflow === 'scroll' ||
3309-
style.overflowY === 'auto' ||
3310-
style.overflowY === 'scroll' ||
3311-
style.overflowX === 'auto' ||
3312-
style.overflowX === 'scroll'
3313-
) {
3314-
return CONTAINER_SCROLL;
3315-
}
3316-
3317-
return CONTAINER_STD;
3318-
}
3319-
3320-
function searchDOMUntilCommonAncestor<T>(
3321-
instA: Instance,
3322-
instB: Instance,
3323-
testFn: (instA: Instance) => T,
3324-
): T | null {
3325-
// Walk up from instA and count depth
3326-
let currentNode: ?Instance = instA;
3327-
let depthA = 0;
3328-
while (currentNode) {
3329-
const result = testFn(currentNode);
3330-
if (result) {
3331-
return result;
3332-
}
3333-
depthA++;
3334-
currentNode = currentNode.parentElement;
3335-
}
3336-
3337-
// Walk up from instB and count depth
3338-
currentNode = instB;
3339-
let depthB = 0;
3340-
while (currentNode) {
3341-
const result = testFn(currentNode);
3342-
if (result) {
3343-
return result;
3344-
}
3345-
3346-
depthB++;
3347-
currentNode = currentNode.parentElement;
3348-
}
3349-
3350-
// Reset currentNode to instA and instB
3351-
let nodeA: ?Instance = instA;
3352-
let nodeB: ?Instance = instB;
3353-
3354-
// Align depths
3355-
while (depthA > depthB && nodeA) {
3356-
nodeA = nodeA.parentElement;
3357-
depthA--;
3358-
}
3359-
while (depthB > depthA && nodeB) {
3360-
nodeB = nodeB.parentElement;
3361-
depthB--;
3362-
}
3363-
3364-
// Walk up both nodes to find common ancestor
3365-
while (nodeA && nodeB) {
3366-
if (nodeA === nodeB) {
3367-
return testFn(nodeA);
3368-
}
3369-
nodeA = nodeA.parentElement;
3370-
nodeB = nodeB.parentElement;
3371-
}
3372-
3373-
return null;
3374-
}
3375-
3376-
function maybeScrollContainerIntoView(
3377-
currentInstance: Instance,
3378-
prevInstance: Instance | null,
3379-
alignToTop: boolean,
3380-
prevContainerIsFixed: boolean,
3381-
): boolean {
3382-
if (prevInstance === null || prevContainerIsFixed) {
3383-
currentInstance.scrollIntoView(alignToTop);
3384-
return true;
3385-
}
3386-
3387-
const currentRect = currentInstance.getBoundingClientRect();
3388-
const prevRect = prevInstance.getBoundingClientRect();
3389-
3390-
// Check if scrolling to current element would push previous element out of viewport
3391-
// alignToTop=true: current goes to top, check if prev would still be visible below
3392-
// alignToTop=false: current goes to bottom, check if prev would still be visible above
3393-
const canScrollVertical = alignToTop
3394-
? currentRect.top + window.innerHeight > prevRect.top
3395-
: currentRect.bottom - window.innerHeight < prevRect.bottom;
3396-
const canScrollHorizontal = alignToTop
3397-
? currentRect.left + window.innerWidth > prevRect.left
3398-
: currentRect.right - window.innerWidth < prevRect.right;
3399-
3400-
if (canScrollVertical && canScrollHorizontal) {
3401-
currentInstance.scrollIntoView(alignToTop);
3402-
return true;
3403-
}
3404-
3405-
return false;
3406-
}
3407-
3408-
function scrollIntoViewByScrollContainer(
3409-
children: Array<Fiber>,
3410-
alignToTop: boolean,
3411-
): void {
3412-
// Loop through the children, order dependent on alignToTop
3413-
// Each time we reach a new scroll container, we look back at the last one
3414-
// and scroll the first or last child in that container, depending on alignToTop
3415-
// alignToTop=true means iterate in reverse, scrolling the first child of each container
3416-
// alignToTop=false means iterate in normal order, scrolling the last child of each container
3417-
let prevScrolledInstance = null;
3418-
let prevContainerIsFixed = false;
3419-
let i = alignToTop ? children.length - 1 : 0;
3420-
// We extend the loop one iteration beyond the actual children to handle the last group
3421-
while (i !== (alignToTop ? -2 : children.length + 1)) {
3422-
const isLastGroup = i < 0 || i >= children.length;
3423-
let isNewScrollContainer: null | ScrollableContainerType = null;
3424-
3425-
if (isLastGroup) {
3426-
// We're past the end, treat as new scroll container to complete the last group
3427-
isNewScrollContainer = CONTAINER_SCROLL;
3428-
} else {
3293+
let i = resolvedAlignToTop ? children.length - 1 : 0;
3294+
while (i !== (resolvedAlignToTop ? -1 : children.length)) {
34293295
const child = children[i];
34303296
const instance = getInstanceFromHostFiber<Instance>(child);
3431-
const prevChild = children[alignToTop ? i + 1 : i - 1];
3432-
3433-
if (prevChild) {
3434-
const prevInstance = getInstanceFromHostFiber<Instance>(prevChild);
3435-
if (prevInstance.parentNode === instance.parentNode) {
3436-
// If these are DOM siblings, check if either is fixed
3437-
isNewScrollContainer =
3438-
isInstanceScrollable(prevInstance) === CONTAINER_FIXED ||
3439-
isInstanceScrollable(instance) === CONTAINER_FIXED
3440-
? CONTAINER_FIXED
3441-
: CONTAINER_STD;
3442-
} else {
3443-
isNewScrollContainer = searchDOMUntilCommonAncestor(
3444-
instance,
3445-
prevInstance,
3446-
isInstanceScrollable,
3447-
);
3448-
}
3449-
}
3297+
instance.scrollIntoView(alignToTop);
3298+
i += resolvedAlignToTop ? -1 : 1;
34503299
}
3451-
3452-
if (isNewScrollContainer) {
3453-
// We found a new scroll container, so scroll the appropriate child from the previous group
3454-
let childToScrollIndex;
3455-
if (alignToTop) {
3456-
childToScrollIndex = isLastGroup ? 0 : alignToTop ? i + 1 : i - 1;
3457-
} else {
3458-
childToScrollIndex = alignToTop ? i + 1 : i - 1;
3459-
}
3460-
3461-
if (childToScrollIndex >= 0 && childToScrollIndex < children.length) {
3462-
const childToScroll = children[childToScrollIndex];
3463-
const instanceToScroll =
3464-
getInstanceFromHostFiber<Instance>(childToScroll);
3465-
3466-
const didScroll = maybeScrollContainerIntoView(
3467-
instanceToScroll,
3468-
prevScrolledInstance,
3469-
alignToTop,
3470-
prevContainerIsFixed,
3471-
);
3472-
if (didScroll) {
3473-
prevScrolledInstance = instanceToScroll;
3474-
prevContainerIsFixed = isNewScrollContainer === CONTAINER_FIXED;
3475-
}
3476-
}
3477-
}
3478-
3479-
i += alignToTop ? -1 : 1;
3480-
}
3300+
};
34813301
}
34823302

34833303
export function createFragmentInstance(

0 commit comments

Comments
 (0)