Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Keep scroll position when adding element to the top

I'm trying to create a calendar that can be infinitely scrolled to the left and to the right. It has to load new content dynamically as the user scrolls forward (easy) or backward (the problem is here).

When I add content to the end of the page, it works fine - the scrollbar adjusts to the new container.scrollWidth. But when I have to add content to the start, the whole calendar moves to the right in a huge 400px jump, because the container.scrollLeft property hasn't changed and there's now a new element at the start.

I'm trying to mitigate this by increasing the container.scrollLeft by 400px - width of the newly created element. This approach works, but only when scrolling with mousewheel (shift+mousewheel to scroll sideways) or mobile touchscreen.

If I use my mouse to drag the scrollbar, it kind of glitches out - my guess is it keeps trying to scroll to the old scrollLeft position and disregards that I increased it.

Could you please suggest a way to achieve this behavior for all ways of scrolling?

It would also be great if you could just point me to a site that uses this technique so I could investigate myself.

Here's my semi-working example:

function Container() {
  const rectangleWidth = 400;

  const container = React.useRef(null);

  const [leftRectangles, setLeftRectangles] = React.useState([0]);
  const [rightRectangles, setRightRectangles] = React.useState([1, 2, 3, 4]);


    // When we just rendered a new rectangle in the left of our container,
  // move the scroll position back
  React.useEffect(() => {
    container.current.scrollLeft += rectangleWidth;
  }, [leftRectangles]);

  const loadLeftRectangle = () => {
    const newAddress = leftRectangles[0] - 1;
    setLeftRectangles([newAddress, ...leftRectangles]);
  };

  const loadRightRectangle = () => {
    const newAddress = rightRectangles[rightRectangles.length - 1] + 1;
    setRightRectangles([...rightRectangles, newAddress]);
  };

  const handleScroll = (e) => {
    // When there is only 100px of content left, load new content
    const loadingOffset = 100;
    
    if (e.target.scrollLeft < loadingOffset) {
      loadLeftRectangle(e.target);
    } else if (e.target.scrollLeft > e.target.scrollWidth - e.target.clientWidth - loadingOffset) {
      loadRightRectangle(e.target);
    }
  };

  return (
    <div
      className="container"
      onScroll={handleScroll}
      ref={container}
    >
      {leftRectangles.map((address) => (
        <div className="rectangle" key={address}>
          {address}
        </div>
      ))}
      {rightRectangles.map((address) => (
        <div className="rectangle" key={address}>
          {address}
        </div>
      ))}
    </div>
  );
}

ReactDOM.render(<Container />, document.querySelector("#app"))
.container {
  display: flex;
  overflow-x: scroll;
}

.rectangle {
  border: 1px solid #000;
  box-sizing: border-box;
  flex-shrink: 0;
  height: 165px;
  width: 400px;
  font-size: 50px;
  line-height: 165px;
  text-align: center;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.12.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>
like image 936
Ilya Peterov Avatar asked Jul 10 '20 15:07

Ilya Peterov


People also ask

Which position will keep the element in the same place regardless of scrolling?

An element with position:fixed is fixed with respect to the viewport. It stays where it is, even if the document is scrolled.

How do I change the scroll position to top in CSS?

If you want to set the scroll position of document. body , you can scroll the entire window altogether using window. scrollTo() ; it takes either a pair of coordinates (x,y) or an options object – if you just want to scroll nicely to the top, try window. scrollTo({top:0,behavior:'smooth'}); .

What is scroll offset?

forward, then scroll offset is the amount the top of the sliver has been scrolled past the top of the viewport. This value is typically used to compute whether this sliver should still protrude into the viewport via SliverGeometry.

How do I display the scroll position of a paragraph?

A paragraph element is defined within this division element with a height and width greater than that of the div element for the scroll bars to be visible. Another div element is created with the “output” class to show the scroll position of the first division element.

How do I change the scroll position of a postback page?

this is typically done by JavaScript adding the current scroll position to the postback data, then the new page renders JavaScript to adjust the scroll position. If it’s a simple page, you just need the scroll position of the body.

How do I scroll the First Div in a Div?

Another div element is created with the “output” class to show the scroll position of the first division element. Use the addEventListener () method to attach an event listener to the div element with an id of “scroll-element”. The event to be attached is the scroll event.

What is the difference between scrolltop and scrollleft in HTML?

The scrollTop property gets or sets the number of pixels that an element’s content is scrolled vertically whereas the scrollLeft property gets or sets the number of pixels that an element’s content is scrolled from its left edge. The innerHTML is repeatedly updated on scrolling through the HTML document.


2 Answers

I think this is a case where one should not use native browser scroll areas. If you think about it, scrollbars have no meaning if they continue to get longer infinitely in both directions. Or their only meaning is "area which has been viewed". A scrollbar is not a good metaphor for an infinite area, and therefore a scrollbox is a poor way to do what you want to do here.

If you think of it another way, you are only showing a set of months which fit within a known screen width. The way I would approach this would be to absolute position each calendar inside a container and calculate their positions in a render loop based on where in a virtual view the user is shuttling. This also would allow you to remove calendars once they go too far off screen and create/buffer them offscreen for display before the user scrolls to their position. It would prevent you from having an arbitrarily wide object which would eventually slow down rendering.

One approach to this would be to simply number each month since 1970 and treat your view as having a fractional position viewX along this line. The month for x should have a position of viewX - x. When the user is dragging you move viewX inversely and reposition the buffered elements. When the user stops dragging, you take the last velocity and slow it until viewX - x is an integer.

This would avoid most cross-browser issues and offset problems. It only requires a render loop while the display is in motion.

like image 138
joshstrike Avatar answered Sep 19 '22 01:09

joshstrike


Use the read-write scrollLeft to get the scroll position prior to dynamically adding content then use the .scrollLeft method to reposition the scroll position back to where you want it. You might need fire off a dialog displaying an indeterminant progress indicator ( or simply display text "working..." ) which displays during the process to prevent janking.

The trick for cross browser functionality is that dialog element which is well known to be challenging regarding consistency across device types so I would recommend using a UI library for your dialog or build your own but keep it super simple. That progress indicator will be the fix for screen jank.

Another feature to consider regarding janking would be CSS transitions where the addition of content (e.g. a block element) would gradually fade/animate in to the viewport.

like image 22
Ronnie Royston Avatar answered Sep 18 '22 01:09

Ronnie Royston