Last week, inspired by a wonderful GitHub user experience (UX), I took a look at maintaining scroll offsets when collapsing sticky elements in Angular. This week, I wanted to follow-up with the somewhat-related concept of maintaining the scroll offset of the user's browser when new content is added above the user's viewport in Angular 9.0.0-rc.2. This way, when new content is added, the user's viewing experience isn't unexpectedly "scrolled up". Instead, the viewport is programmatically scrolled down in order to keep the previous content "in view" for the user.
To understand the problematic user experience (UX) that is created when new content is added above the user's viewport, consider the following illustration that shows where the user's current reading experience (red outline) ends-up:
As you can see, when new content is added to the top of the content container - above the user's viewport - the user's current reading experience is actually pushed down below the fold of the user's browser. Or, to put it another way, the user's viewport was implicitly "scrolled up" by the change in the content height.
To create a more pleasant user experience (UX), what we'd like to do is add the new content while simultaneously scrolling the user's viewport down so that the user maintains the same reading experience. To do this, we can follow the general algorithm:
Step 1: Before adding the new content, get the current scroll height and offset of the user's viewport.
Step 2: Add the new content to the content container and then force Angular to reconcile the updated View Model with the Document Object Model (DOM).
Step 3: Get the new scroll height and offset of the user's viewport.
Step 4: If necessary, scroll the user down by an offset that adjusts for the height of the new content (ie, the delta in pre-and-post scroll height).
Now, one thing that is really cool is that, while working on this demo, I discovered that Chrome and Firefox actually do this for you (at least on my Mac)! Meaning, when you add content above the user's viewport, Chrome and Firefox automatically scroll the viewport down in order to maintain the same user experience.
Of course, Safari and Microsoft Edge do not do this. As such, we still have an opportunity to step in and programmatically create a better user experience.
To explore this concept, I've created a demo News Feed. Every 500ms, a new news item is added to the top of the content container, forcing all the content below it to shift down. The demo can be run unconstrained, using the Window as the "viewport"; or, it can be run within an
overflow:auto container, which uses the overflow container as the "viewport".
Depending on which container we are using, methods for getting and setting the various scroll properties are slightly different. As such, I've encapsulated the logic in some private methods like
.setScrollTop(). This way, we can keep the same algorithm, but change the data-access based on the type of demo.
In the following App component, the meat of the logic takes place in
.addNewsItem(), which walks through the algorithm steps as outlined above:
As I mentioned earlier, Chrome and Firefox automatically create a good user experience. We can tell when this happens if the "scroll offset" automatically changes after the new content is added. In browsers like Safari, the "scroll offset" remains the same because the browser doesn't do anything. However, in Chrome and Firefox, the "scroll offset" is different after the new content is injected because these browsers are scrolling the user down in order to account of the new content.
Because of this, the demo is really only meaningful in other browsers like Safari and Edge. And, when it comes to both of these browser, I couldn't quite get the demo to be seamless. When we use the "window" as the constrained container, Safari shows a "flash" of the new content before we have time to scroll the user down. However, if we use the overflow container as the constraint, Safari works well:
As you can see (it is easier to see in the video), when we use the "window" as the content container, the content flash is a bit janky. However, when we use the overflow container as the constraint, the demo runs smooth like butter.
When I started this demo, I expected to have to do something programmatically in all browser. However, I was delighted to find out that Chrome and Firefox inherently create a great user experience (UX). As such, this demo only applies to a subset of browsers like Safari and Edge. That said, even in those browsers, we can still create a better user experience by programmatically scrolling the user down in order to account for new content in Angular 9.0.0-rc.2.