Keep scroll position when adding element to the top - javascript

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>

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.

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.

Related

requestAnimationFrame not pausing my background color transisition

I have a header with a background color of transparent which will change to black when the user scrolls. Since I am using the requestAnimationFrame to accomplish this, the transition should pause when the tab is not active to save resources. I tested this with a function counting to 300 which indeed did pause when the tab was not active and resumed to 300 when active. However, it seems that my header's background color transition does not pause when on a new tab.
I am using mozilla's example which says "This example optimizes the scroll event for requestAnimationFrame." So I think it should work well with my use case (which it does) I would just like some insight as to why my transition doesn't pause when on a different tab to save resources and be as optimal as possible. Thanks!
https://developer.mozilla.org/en-US/docs/Web/API/Document/scroll_event
"use strict";
// Reference: http://www.html5rocks.com/en/tutorials/speed/animations/
const $ = selector => document.querySelector(selector);
let lastKnownScrollPosition = 0;
let ticking = false;
function doSomething(scrollPos) {
// Do something with the scroll position
if ( scrollPos > 0 ) {
$("header").style.backgroundColor = "black";
}
else {
$("header").style.backgroundColor = "transparent";
}
}
document.addEventListener('scroll', function(e) {
lastKnownScrollPosition = window.scrollY;
if (!ticking) {
window.requestAnimationFrame(function() {
doSomething(lastKnownScrollPosition);
ticking = false;
});
ticking = true;
}
});
Here is my JSFiddle https://jsfiddle.net/Boros/zjwyamvq/3/
A different approach would be to use an IntersectionObserver.
If you place a tiny (1px x 1px) element at the top of the page you can set an IntersectionObserver on it which will tell you as soon as it goes out of the viewport or comes back in.
That way you only have to run JS when there is a change from the user having scrolled away from the top - which will happen only once until they scroll back up to the top.
I don't know how much more efficient this is than using the traditional scroll and trying to throttle it, but MDN says:
sites no longer need to do anything on the main thread to watch for this kind of element intersection, and the browser is free to optimize the management of intersections as it sees fit.
The very fact that you aren't coming back to execute some JS every time the user does a scroll must help regardless of whether the browser does additional optimisation.
Here's a trivial example. It adds the 1px div, sets an IntersectionObserver on it and if the div goes out of the viewport that means the user has scrolled and if it is in the viewport the user has scrolled back to the top or the system is at the start position.
Note, if you want to make it slightly less sensitive you can set the check div to have a height of say 20px so if the user scrolls back to pretty near the top the color changes.
const $ = selector => document.querySelector(selector);
const check = (entries) => {
$("header").style.backgroundColor = (entries[0].isIntersecting) ? 'transparent' : 'black';
};
const observer = new IntersectionObserver(check);
observer.observe($("#checker"));
<div id="checker" style="width: 1px; height: 1pc; position: absolute; top: 0; left; 50%;"></div>
<header style="width: 100vw; height: 200vh; transition: 5s;">SCROLL DOWN TO SEE BACKGROUND COLOR CHANGE TO BLACK</header>

scrollBy does not work while user is scrolling (iOS only)

I have a list of items that has an item prepended to it at a regular interval (think a feed of data). The list should update in real-time as new items come in, however if the user scrolls down it should "lock" in place to make the list easier to read. (If a user has scrolled, they want to be able to read the items without them being pushed down every time a new item is added)
It appears that on Chrome (desktop) and Android, the browser implements this automatically. However, on iOS it does not.
As a work around, I figured I could use scrollBy to move the scroll position when a new item is added IF the user has scrolled down. This mostly works, and prevents the list from jumping.
However, if the user is currently in the process of scrolling, scrollBy seems to have no effect and the items jump around.
To reproduce:
Open on iOS: https://codepen.io/Cyral/full/JjbbPaj
Observe that new items are added every second. Scroll down the list a bit and notice how items are no longer moving (great!), however try scrolling slowly (without taking your finger off the screen) and notice how the items start updating again.
It appears that whatever keeps track of the user scroll amount overrides the scrollBy command, resulting in the list jumping around. If you scroll a bit and take your finger off and let it "smooth scroll" you can also see how it jumps a bit. Is there a way to create the desired behavior of the list "pausing" when scrolled away, but updating in realtime once scrolled to the top?
Here is a reference to see the desired behavior on desktop: (It is the same code, except the scrollBy call has been removed as it is not needed) https://codepen.io/Cyral/full/PobboYE
Update: It appears it is only broken on iOS 14. On 12 and 13 it works as it does on desktop, with no hacky scrollBy workaround.
Like this?
const container = document.getElementById("container");
let textNum = 0;
setInterval(() => {
container.innerHTML = `<p>some text ${++textNum}</p><br/>` + container.innerHTML;
//scroll
savedScroll && (container.scrollTop = container.scrollHeight - savedScroll);
}, 700);
let savedScroll = false;
container.addEventListener("scroll", () => {
if (container.scrollTop <= 10 /*if scroll is near top of container*/) {
savedScroll = false;
} else {
savedScroll = container.scrollHeight - container.scrollTop;
}
});
body {
line-height: 5px;
margin: 0px;
}
div {
width: 200px;
height: 190px;
overflow-y: scroll;
border: 1px solid #000000;
}
<div id="container"></div>
It listens for scroll, and if the current scroll is at the top, it disable auto scrolling, else, save current scroll relative to bottom of container. The scroll is set to containerHeight - savedScroll everytime the container is updated.

CSS "position:fixed": mobile zoom

I'm trying to solve an issue with css "position:fixed" property on mobile browsers. I have a fixed div:
<div id="logo">
...other content here...
</div>
with css:
#logo{
position: fixed;
-webkit-backface-visibility: hidden;
bottom: 100px;
right: 0px;
width: 32px;
height: 32px;
}
So, usually the behaviour is exactly the desired one, with the div position always on the bottom right of the window, indipendently of the scroll position.
My issue is that on mobile browsers, when the users zoom the page, after a certain zoom level the div position is wrong (sometimes the div disappear out of the window).
I know that fixed position is not well supported on mobile browsers, but I wonder if there is some workaround. I tried with this js code onScroll event:
window.addEventListener('scroll', function(e){
drag.style['-webkit-transform'] = 'scale(' +window.innerWidth/document.documentElement.clientWidth + ')';\\I want to avoid zoom on this element
var r = logo.getBoundingClientRect();
var w = window.innerWidth;
var h = window.innerHeight;
if(r.right != w){
rOff = r.right - w;
logo.style.right = rOff;
}
if(r.top+132 != h){\
tOff = r.top + 132 - h;
logo.style.bottom = tOff;
}
});
Unfortunately, the code seems to return the wrong position.
Does anyone have any tip?
Ok, that's how I solved the issue...I hope that could help anyone to simulate fixed position on iOS devices.
I switched the position from fixed to absolute;
Attach to window a listener to get the new position when the page is scrolled or zoomed,
setting window.onscroll and window.onresize events with the following function:
function position() {
drag.style.left = window.innerWidth + window.pageXOffset - 32 + 'px';
drag.style.top = window.innerHeight + window.pageYOffset - 132 + 'px';
}
Do you want to catch if zoom is active?
There's no window.onZoom listener, but you can read this thread:
Catch browser's "zoom" event in JavaScript
and this answer: https://stackoverflow.com/a/995967/3616853
There's no way to actively detect if there's a zoom. I found a good entry here on how you can attempt to implement it.
I’ve found two ways of detecting the zoom level. One way to detect zoom level changes relies on the fact that percentage values are not zoomed. A percentage value is relative to the viewport width, and thus unaffected by page zoom. If you insert two elements, one with a position in percentages, and one with the same position in pixels, they’ll move apart when the page is zoomed. Find the ratio between the positions of both elements and you’ve got the zoom level. See test case. http://web.archive.org/web/20080723161031/http://novemberborn.net/javascript/page-zoom-ff3
You could also do it using the tools of the above post. The problem is you're more or less making educated guesses on whether or not the page has zoomed. This will work better in some browsers than other.
There's no way to tell if the page is zoomed if they load your page while zoomed.
Just a theory, but you may want to try setting the bottom/right positions in % rather than px.
I think what you're seeing when using pixel measurements is just the zoom effecting the pixels. Or to put it better, when you zoom-in the pixels appear larger and that throws off the position of the element, even pushing it out of the view-port on smaller screens.
Example using pixel positioning
Notice that even on a desktop as you zoom-in and out the element appears to move up and down?
Example using percent positioning
In this example the element appears to stay in the bottom right corner, because it is always positioned at 10% from the bottom of the view-port.
#logo{
position: fixed;
-webkit-backface-visibility: hidden;
bottom:10%;
right: 0;
width: 32px;
height: 32px;
}
Having two different z-index for the logo and the rest of the page could help. Allowing zooming only to the rest of the page and not to the z-index layer where logo is included. So, this might not affect the stretching on the logo.
We can
Implement a ZOOM listener
Attach it to browser
Make the zoom listener change the zoom level of the element (modify the elements position) using z-index as a factor.

How to keep jQuery function from moving elements all the way to the left?

I was able to implement the solution posted here ("position: fixed and absolute at the same time. HOW?") to get a div element to move with the rest of the page horizontally, but stay fixed vertically. However, this solution causes the selected element to move ALL the way to the left of the page (with what appears to be a 20px margin). I'm still new to javascript and jQuery, but as I understand it, the following:
$(window).scroll(function(){
var $this = $(this);
$('#homeheader').css('left', 20 - $this.scrollLeft());});
takes the selected element and, upon scrolling by the user, affects the CSS of the element so that its position from the left becomes some function of the current scrollbar position adjusted by the 20px margin. If this is correct? And if so, can anyone think of a way that I could change it so that instead of moving the selected element all the way to the left side of the window, we only move it as far left as my default CSS position for the body elements of the HTML document (shown below)?
body {font-size:100%;
width: 800px;
margin-top: 0px;
margin-bottom: 20px;
margin-left: auto;
margin-right: auto;
padding-left: 20px;
padding-right: 20px;}
EDIT: Here is a jsfiddle (code here) that I made to illustrate the issue. My page is designed so that when it is displayed in full-screen or near full-screen mode, the #homeheader element appears centered horizontally due to its width and the left and right margins being set to auto. As the page gets smaller and smaller the margins do as well, until they disappear altogether and are replaced by the padding-left and padding-right settings of 20px. So at this point (when the window is small enough that the margins disappear altogether), which is what the jsfiddle shows, the code appears to work as intended, but when the window is full-sized the JS overrides the CSS and pushes the div element all the way to the left (getting rid of the margin) upon any scrolling action.
There are two events you need to handle to get this to work correctly. First is the scroll event which you are pretty close on. The only thing you might want to do is to use offset to get the current left position value based on the document.
The other event which is not yet handled is the resize event. If you don't handle this then once a left position is defined your element (header) will be there always regardless of whether or not the user resizes the window.
Basically something like this should work:
var headeroffset;
var header = $('#homeheader');
// handle scroll
$(window).scroll(function() {
// auto when not defined
if (headeroffset === undefined) {
// capture offset for current screen size
headeroffset = header.offset();
}
// calculate offset
var leftOffset = headeroffset.left - $(this).scrollLeft();
// set left offset
header.css('left', leftOffset);
});
// handle resize
$(window).resize(function() {
// remove left setting
// (this stops the element from being stuck after a resize event
if (header.css('left') !== 'auto') {
header.css('left', '');
headeroffset = undefined;
}
});
JSFiddle example: http://jsfiddle.net/infiniteloops/ELCq7/6/
http://jsfiddle.net/infiniteloops/ELCq7/6/show
This type of effect can be done purely in css however, i would suggest taking a look at the full page app series Steve Sanderson did recently.
http://blog.stevensanderson.com/2011/10/05/full-height-app-layouts-a-css-trick-to-make-it-easier/
As an example you could do something like this:
http://jsfiddle.net/infiniteloops/ELCq7/18/
Try this
$('#homeheader').css('left', parseInt($('body').css('margin-left')) - $this.scrollLeft());});
What I did here is just replace 20 with body's left-margin value.

Chrome Extension Scrollbar Placement

After dabbling in Chrome Extensions I've noticed that when the data inside the Page Action gets to a certain point the scroll bars automatically attach themselves to the popup, this I expect. However, instead of pushing the content to the left of the scroll bar it overlays the content causing a horizontal scrollbar to become active. I ended up just adding a check on my data and applying a css class to push the content to the left more to run parallel to the scroll bar and beside it not under it. What is the correct way to handle this besides my hackish solution?
I was wondering this myself too. Currently I just don't put anything important closer than 20px to the right side of a popup and disable horizontal scrollbars:
body {overflow-x:hidden;overflow-y:auto;}
So when a vertical scrollbar appears the content at least doesn't jump.
Perhaps you need to specify a width on the scrollbar.
::-webkit-scrollbar {
width: 42px; //Do not know actual width, but I assume you do
}
I haven't found a way to do this that isn't a hack, but here's the simplest hack I could think of:
<script type="text/javascript">
function tweakWidthForScrollbar() {
var db = document.body;
var scrollBarWidth = db.scrollHeight > db.clientHeight ?
db.clientWidth - db.offsetWidth : 0;
db.style.paddingRight = scrollBarWidth + "px";
}
</script>
...
<body onresize="tweakWidthForScrollbar()">
The idea is to detect whether the vertical scrollbar is in use, and if it is, calculate its width and allocate just enough extra padding for it.

Categories