Skip to content

Commit 8617561

Browse files
committed
Fix ScrollView scroll position issues (#501)
ScrollView would not update if the size of the content changed due to style or window size changes. ScrollView would get stuck in a state where the arrows would not update when the user scrolled fast back and forth.
1 parent 801831f commit 8617561

File tree

2 files changed

+43
-13
lines changed

2 files changed

+43
-13
lines changed

src/components/ScrollView/ScrollView.jsx

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import React, {
55
useLayoutEffect,
66
useRef,
77
useState,
8+
useCallback,
89
} from 'react';
910
import { TranslationsContext } from '../../providers/translations';
1011
import { withGlobalProps } from '../../providers/globalProps';
@@ -58,20 +59,26 @@ export const ScrollView = React.forwardRef((props, ref) => {
5859
const blankRef = useRef(null);
5960
const scrollViewViewportEl = ref ?? blankRef;
6061

61-
const handleScrollViewState = (currentPosition) => {
62+
const handleScrollViewState = useCallback((currentPosition) => {
6263
const isScrolledAtStartActive = currentPosition[scrollPositionStart]
6364
<= -1 * EDGE_DETECTION_INACCURACY_PX;
6465
const isScrolledAtEndActive = currentPosition[scrollPositionEnd]
6566
>= EDGE_DETECTION_INACCURACY_PX;
6667

67-
if (isScrolledAtStartActive !== isScrolledAtStart) {
68-
setIsScrolledAtStart(isScrolledAtStartActive);
69-
}
68+
setIsScrolledAtStart((prevIsScrolledAtStart) => {
69+
if (isScrolledAtStartActive !== prevIsScrolledAtStart) {
70+
return isScrolledAtStartActive;
71+
}
72+
return prevIsScrolledAtStart;
73+
});
7074

71-
if (isScrolledAtEndActive !== isScrolledAtEnd) {
72-
setIsScrolledAtEnd(isScrolledAtEndActive);
73-
}
74-
};
75+
setIsScrolledAtEnd((prevIsScrolledAtEnd) => {
76+
if (isScrolledAtEndActive !== prevIsScrolledAtEnd) {
77+
return isScrolledAtEndActive;
78+
}
79+
return prevIsScrolledAtEnd;
80+
});
81+
}, [scrollPositionStart, scrollPositionEnd]);
7582

7683
/**
7784
* It handles scroll event fired on `scrollViewViewportEl` element. If autoScroll is in progress,
@@ -146,7 +153,6 @@ export const ScrollView = React.forwardRef((props, ref) => {
146153

147154
useScrollPosition(
148155
(currentPosition) => (handleScrollViewState(currentPosition)),
149-
[isScrolledAtStart, isScrolledAtEnd],
150156
scrollViewContentEl,
151157
scrollViewViewportEl,
152158
debounce,
@@ -163,6 +169,30 @@ export const ScrollView = React.forwardRef((props, ref) => {
163169
[autoScroll, autoScrollChildrenKeys, autoScrollChildrenLength],
164170
);
165171

172+
// ResizeObserver to detect when content or viewport dimensions change due to style changes
173+
useLayoutEffect(() => {
174+
const contentElement = scrollViewContentEl.current;
175+
const viewportElement = scrollViewViewportEl.current;
176+
177+
if (!contentElement || !viewportElement) {
178+
return () => {};
179+
}
180+
181+
const resizeObserver = new ResizeObserver(() => {
182+
handleScrollViewState(
183+
getElementsPositionDifference(scrollViewContentEl, scrollViewViewportEl),
184+
);
185+
});
186+
187+
// Observe both content and viewport for dimension changes
188+
resizeObserver.observe(contentElement);
189+
resizeObserver.observe(viewportElement);
190+
191+
return () => {
192+
resizeObserver.disconnect();
193+
};
194+
}, [scrollViewContentEl, scrollViewViewportEl, handleScrollViewState]);
195+
166196
const arrowHandler = (contentEl, viewportEl, scrollViewDirection, shiftDirection, step) => {
167197
const offset = shiftDirection === 'next' ? step : -1 * step;
168198
const differenceX = scrollViewDirection === 'horizontal' ? offset : 0;

src/components/ScrollView/_hooks/useScrollPositionHook.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import {
2-
useLayoutEffect,
32
useRef,
3+
useEffect,
44
} from 'react';
55
import { getElementsPositionDifference } from '../_helpers/getElementsPositionDifference';
66

7-
export const useScrollPosition = (effect, dependencies, contentEl, viewportEl, wait) => {
7+
export const useScrollPosition = (effect, contentEl, viewportEl, wait) => {
88
const throttleTimeout = useRef(null);
99

1010
const callBack = (wasDelayed = false) => {
@@ -15,7 +15,7 @@ export const useScrollPosition = (effect, dependencies, contentEl, viewportEl, w
1515
}
1616
};
1717

18-
useLayoutEffect(() => {
18+
useEffect(() => {
1919
const viewport = viewportEl.current;
2020

2121
const handleScroll = () => {
@@ -34,7 +34,7 @@ export const useScrollPosition = (effect, dependencies, contentEl, viewportEl, w
3434
clearTimeout(throttleTimeout.current);
3535
viewport.removeEventListener('scroll', handleScroll);
3636
};
37-
}, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
37+
}, []); // eslint-disable-line react-hooks/exhaustive-deps
3838
};
3939

4040
export default useScrollPosition;

0 commit comments

Comments
 (0)