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

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.

Related

bidirectional intersection observer on firefox

let wheelObserverOptions = {
root: dateWheelNode,
rootMargin: '100px',
}
const dateWheelObserver = new IntersectionObserver(wheelObserverCallback, wheelObserverOptions);
// const dateWheelPastDateObserver = new IntersectionObserver(wheelObserverCallback, wheelObserverOptions);
function wheelObserverCallback(entries, observer) {
entries.forEach(entry => {
if(entry.isIntersecting) {
debugMsg('triggered')
if (entry.target === dateWheelNode.lastChild) loadFutureDates()
if (entry.target === dateWheelNode.firstChild) loadPastDates()
// observer.unobserve(entry.target)
}
});
};
prepending new elements when scrolling the dateWheelNode - which is the parent container
function loadPastDates() {
dateWheelDateFuture.subtract(1,'day')
dateWheelDatePast.subtract(1,'day')
wheelCheckDays(dateWheelDatePast)
}
appending
function loadFutureDates() {
dateWheelDateFuture.add(1,'day')
dateWheelDatePast.add(1,'day')
wheelCheckDays(dateWheelDateFuture)
}
now regardless if prepending or appending this is in the wheelCheckDays() function (which works as intended)
dateWheelNode.scroll(0, dateWheelNode.scrollTop);
dateWheelObserver.observe(dateWheelElementDOM);
this also makes sure regardless of the scrolling direction the elements do not jump and the scrolling remains smooth (works on chrome & edge) and is on the bottom of wheelCheckDays() after
also dateWheelNode gets initial 30 elements when opening it with (and which are not being removed - only if appending/prepending additional ones by scrolling)
for (let i = 0; i <= 30; i++ ) {
if (i === 0) {
wheelCheckDays(dateWheelDateFuture, true)
wheelYearObserver.observe(document.querySelector(`.dateWheelDay${dateWheelDateFuture.format("YYYYMMDD")}`));
wheelMonthObserver.observe(document.querySelector(`.dateWheelDay${dateWheelDateFuture.format("YYYYMMDD")}`));
} else if (i > 0 && i <= 15) {
dateWheelDatePast.subtract(1,'day')
wheelCheckDays(dateWheelDatePast, true)
} else if (i > 15) {
dateWheelDateFuture.add(1,'day')
wheelCheckDays(dateWheelDateFuture, true)
}
}
besides that dateWheelNode has the height set to min(350px, 80%)
other than that wheelCheckDays() function does modify the new elements before prepending/appending it and removes the last or first child of dateWheelNode every time depending on if a element gets appended a prepended
now my problem is that on chrome and edge kinda works as it tends occasionally to stop the scrolling abruptly and sometimes (rarely) scroll infinitely upwards, but on firefox it happens 100% of times and additionally when scrolling down (loadFutureDates()) the scrolling is not smooth at all and does jump dates very fast at once too and when scrolling upwards loadPastDates() it scrolls infinitely and won't stop until scrolling down
what I'm missing here and how to fix the infinite automatic scroll and smooth scrolling (still though not throttling the scrolling in order to be able to switch the dates fast too - just like normal scrolling behaviour)
changing dateWheelNode.scroll(0, dateWheelNode.scrollTop) into dateWheelNode.scroll({top: dateWheelNode.scrollTop, behaviour: 'smoooth'}) does not help with that scrolling either as the infinite scroll upwards happens then on all browsers
PS: it is intentioned being viewed mostly on mobile, but the issues described happens also on desktop
PS2: couldn't really post a working example as the code is a mess and I've cut out the (seemingly) important parts of it and and that's also why the solution/answer shouldn't be a totally different approach (unless the rewriting wouldn't take too much time) and also not over complicated as I'm still a junior dev
UPDATE: I managed to make a a example on jsfiddle
https://jsfiddle.net/DadoIrie/a8cge9o6/34/
best way to test that scrolling issue is just move the scrollbar on top with firefox - it just starts scrolling infinitely - while chrome doesn't even allow to bring the scrollbar on top - bottom works perfectly
UPDATE (temporary ugly solution) well, temporary solution is if (dateWheelNode.scrollTop < 1) document.querySelector('#dateWheelDays').scroll(0, 100); ... which is quite ugly, but at least stops firefox from infinitely auto-scroll upwards
still open for sugestions regarding this

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>

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>
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.

How to create forced scrolling to anchors on a website on scroll

I have a site where I have each section as 100vh so it fills the height of the screen perfectly. The next step I wanted to implement was disabling the regular scrolling, and on scroll force the screen to jump smoothly to the top of the next 100vh section. Here is the example of this animation / feature:
https://www.quay.com.au/
I was having a hard time finding any answers for this as most things just deal with smooth scrolling when clicking on anchors, not actually forcing div relocation when the user scrolls up / down.
I just wanted to know what code I would need do this...
Thanks, been using stack overflow for a while but first post, let me know if there is anything I can do to make this more clear.
disclaimer: this solution needs some testing and probably a bit of improvements, but works for me
if you don't want to use a plugin and prefer a vanilla JavaScript solution I hacked together a small example how this can be achieved with JS features in the following codepen:
https://codepen.io/lehnerchristian/pen/QYPBbX
but the main part is:
function(e) {
console.log(e);
const delta = e.deltaY;
// check which direction we should scroll
if (delta > 0 && currentlyVisible.nextElementSibling) {
// scroll downwards
currentlyVisible = currentlyVisible.nextElementSibling;
} else if (delta < 0 && currentlyVisible.previousElementSibling) {
// scroll upwards
currentlyVisible = currentlyVisible.previousElementSibling;
} else {
return false;
}
// perform scroll
currentlyVisible.scrollIntoView({ behavior: 'smooth' });
e.preventDefault();
e.stopPropagation();
}
what it does is that it listens for the wheel event and then calls the callback, which intercepts the scroll event. inside the callback the direction is determined and then Element.scrollIntoView() is called to let the browser do the actual scrolling
check https://caniuse.com/#search=scrollintoview for browser support, if you're going for this solution

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.

Categories