I haven't been able to find an answer to this question but I have seen this exact behaviour in many apps (calendars, agendas etc.). As you can see in the snippet below my container
expands with scrolling to both sides - new div
s are being inserted inside. When you scroll to the right it feels okay and natural, however, when you scroll to the left, it always adds the element and you stay at 0px
needing to scroll a bit back and then to the left again to expand some more. Best if you try below:
import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom/client';
function Test() {
const [span, setSpan] = useState<Array<number>>([-1, 0, 1]);
// Append item to the array - scrolling right
const append = () => {
setSpan([
...span,
span[span.length - 1] + 1,
]);
};
// Prepend item to the array - scrolling left
const prepend = () => {
setSpan([
span[0] - 1,
...span,
]);
};
// Center view on load - to the middle of element '0' - e.i. the center
useEffect(() => {
const element = document.getElementById('element-0');
if (element) {
element.scrollIntoView({ behavior: 'auto', inline: 'center' });
}
}, []);
// Register 'scroll' listener
useEffect(() => {
const element = document.getElementById('container');
const scrolling = () => {
if (element) {
if (element.scrollLeft === 0) {
prepend();
}
if (element.offsetWidth + element.scrollLeft >= (element.scrollWidth - 100)) {
append();
}
}
};
element.addEventListener('scroll', scrolling);
return () => {
element.removeEventListener('scroll', scrolling);
};
}, [span.length]);
return (
<div style={{
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}
>
<div
id="container"
style={{
maxWidth: '50vw', maxHeight: '50vh', overflowX: 'auto', whiteSpace: 'nowrap', backgroundColor: 'red',
}}
>
<div style={{ width: 'fit-content' }}>
<div style={{ width: 'fit-content' }}>
<div style={{ display: 'flex' }}>
{span.map((element) => (
<div key={`element-${element}`} id={`element-${element}`} style={{ minWidth: '40vw', minHeight: '100vh', border: '1px solid black' }}>
{ element }
</div>
))}
</div>
</div>
</div>
</div>
</div>
);
}
const root = ReactDOM.createRoot(
document.getElementById('root')
);
root.render(
<React.StrictMode>
<Test />
</React.StrictMode>
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
I tried programatically scrolling a bit to the right before prepending new item, but it only created more issues. Is there an easy way to solve it?
Prepending an element doesn't make its container's scrollLeft
increase by as much as the element's width.
Instead, the scrollLeft
's value remains the same, and so the new box effectively "pops" into the view:
Since, as you mentioned, the scrollLeft
remains at zero after insertion, further mouse wheel movement doesn't result in the container's scroll, so the scroll
event is not fired, and hence the box insertion logic is not evaluated.
That can be solved, for example, by listening for the wheel
event rather than the scroll
. The problem is, that the scrollLeft
would still stay at zero, so the boxes would just appear in the view one by one rather than letting the user scroll onto them. Demo. Plus, the mouse wheel is not the only way to scroll.
As such, by the very definition of the problem, we need to manually adjust the scroll position so that the view remains at the same element as before the insertion. In this case, this amount is simply the offsetWidth
of the box, so the solution could be as follows:
const boxWidth = document.getElementById("element-0").offsetWidth;
if (element.scrollLeft < 100) {
element.scrollBy(boxWidth, 0);
prepend();
}
else if (/*...*/) {