Horizontal Scrolling on React Component Using Vertical Mouse Wheel - javascript

I have a component that resizes into a horizontal row of bootstrap cards when in a smaller desktop window. For users without a horizontal mouse wheel and not using a touchpad, I would like to allow users to scroll horizontally using their vertical mouse wheel movements when hovering over this particular component.
Here is the original StackOverflow issue I based my code off of:
https://stackoverflow.com/a/15343916/8387497
Horizontal Scroll helper component:
function horizontalScroll (event) {
const delta = Math.max(-1, Math.min(1, (event.nativeEvent.wheelDelta || -event.nativeEvent.detail)))
event.currentTarget.scrollLeft -= (delta * 10)
event.preventDefault
}
How I've implemented it on component requiring horizontal scrolling:
<Row className='announcements-home' onWheel={horizontalScroll} >
When I've placed this horizontalScroll helper function within the React onWheel event, it scrolls horizontally AND vertically. My desired outcome is just horizontal scrolling. Also, Firefox does not appear to respond at all to horizontal scrolling with these changes.

Okay, so the issue seems to be that you only refer to the function event.preventDefault rather than invoking it.
Adding some brackets at the end to invoke it should do the trick:
event.preventDefault().
I however found this issue while looking for some simple code to use, so I will also leave the hook I made for this if others in the same situation:
import { useRef, useEffect } from "react";
export function useHorizontalScroll() {
const elRef = useRef();
useEffect(() => {
const el = elRef.current;
if (el) {
const onWheel = e => {
if (e.deltaY == 0) return;
e.preventDefault();
el.scrollTo({
left: el.scrollLeft + e.deltaY,
behavior: "smooth"
});
};
el.addEventListener("wheel", onWheel);
return () => el.removeEventListener("wheel", onWheel);
}
}, []);
return elRef;
}
Usage:
import React from "react";
import { useSideScroll } from "./useSideScroll";
export const SideScrollTest = () => {
const scrollRef = useHorizontalScroll();
return (
<div ref={scrollRef} style={{ width: 300, overflow: "auto" }}>
<div style={{ whiteSpace: "nowrap" }}>
I will definitely overflow due to the small width of my parent container
</div>
</div>
);
};
Note:
The scroll behavior "smooth" seems to be giving some trouble when trying to do continuous scrolling. This behavior can be omitted to have proper continuous scrolling, but it will look jerky.
As far as I know, there is no easy solution for this. I have however created a rather involved solution in my own project, so thought some people may appreciate that also: https://gist.github.com/TarVK/4cc89772e606e57f268d479605d7aded

onWheel = (e) => {
e.preventDefault()
var container = document.getElementById('container')
var containerScrollPosition = document.getElementById('container').scrollLeft
container.scrollTo({
top: 0,
left: largeContainerScrollPosition + e.deltaY
behaviour: 'smooth' //if you want smooth scrolling
})
}

There is another small problem with TarVK's proposed hook. Once you scroll to the end and continue scrolling nothing happens, when we are used to containing elements starting to scroll as well. So I made a fix for that:
export function useHorizontalScroll () {
const elRef = useRef();
useEffect(() => {
const el = elRef.current;
if (el) {
const onWheel = (e) => {
if (e.deltaY === 0) return;
if (
!(el.scrollLeft === 0 && e.deltaY < 0) &&
!(el.scrollWidth - el.clientWidth - Math.round(el.scrollLeft) === 0 &&
e.deltaY > 0)
) {
e.preventDefault();
}
el.scrollTo({
left: el.scrollLeft + e.deltaY,
behavior: 'smooth'
});
};
el.addEventListener('wheel', onWheel);
return () => el.removeEventListener('wheel', onWheel);
}
}, []);
return elRef;
}
It's conditionally preventing default behavior only when there is space to scroll in that direction, so when there is no space to scroll, for example the whole page will start to scroll. The change is here:
if (
!(el.scrollLeft === 0 && e.deltaY < 0) &&
!(el.scrollWidth - el.clientWidth - Math.round(el.scrollLeft) === 0 &&
e.deltaY > 0)
) {
e.preventDefault();
}

I can not comment, because my reputation is not enough.
#arVK's answer works, but using 'scrollBy' instead of 'scrollTo' can get smooth wheel.
import { useRef, useEffect } from "react";
export function useHorizontalScroll() {
const elRef = useRef();
useEffect(() => {
const el = elRef.current;
if (el) {
const onWheel = e => {
if (e.deltaY == 0) return;
e.preventDefault();
el.scrollBy(e.deltaY, 0);
};
el.addEventListener("wheel", onWheel);
return () => el.removeEventListener("wheel", onWheel);
}
}, []);
return elRef;
}

You can use onWheel event directly:
import React, { useRef } from "react";
export const Content = () => {
const ref = useRef<HTMLDivElement>(null);
const onWheel = (e: UIEvent) => {
const elelemnt = ref.current;
if (elelemnt) {
if (e.deltaY == 0) return;
elelemnt.scrollTo({
left: elelemnt.scrollLeft + e.deltaY,
});
}
};
return (
<div ref={ref} onWheel={onWheel}>
TEST CONTENT
</div>
);
};

Related

Recat.js Vanilla JavaScript Fade in animation effect

I want to make a fade-in animation effect using javaScript scroll event. What should I do?
The current style is working on style components. Please advise
const handleAnimation = ( ) => {
const windowHeight = window.innerHeight;
const scrollY = window.scrollY;
const bannerTop = ref.current && ref.current.getBoundingClientRect().top;
const bannerAbsolute = scrollY + bannerTop;
return { windowHeight: windowHeight, scrollY: scrollY };
}
useEffect(() => {
window.addEventListener('scroll', handleAnimationScroll);
return () => {
window.removeEventListener('scroll', handleAnimationScroll);
};
}, []);
handleAnimationScroll();

Maintain scroll position on expanding page with JavaScript

I am trying to maintain scroll position despite content being added to the top of the page, when you scroll to the top in a React app I am working on.
I have tried:
Getting the position of the top element on the page before the content is added and then using something like window.scrollTo(0, elementPosition)
Calculating the difference between the page height before and after the added content and scrolling back to the original position based on the difference.
Determining where to scroll based on how far the Viewport was from the bottom before the added content was displayed as that position should not be changing.
None of it worked and I am at a loss. This is the code that is responsible for creating the content, rendering a portion of the content and then adding content once you scroll to the top:
//Creating the content:
useEffect(() => {
fetch('/api/endpoint', {
method: 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
})
.then(res => res.json())
.then(res => {
setLogUsers(res.users);
setTotalAmount(res.log.length);
setMessages(limitedRender(res.log));
})
.catch(err => console.log(err))
}, [renderAmount])
return(
messages.map(e => {
return(
<Msg message = {e}/>
);
})
);
//Selecting a portion of the content and detecting when you scroll to the top:
const [totalAmount, setTotalAmount] = useState();
const [renderAmount, setRenderAmount] = useState(10);
const limitedRender = (arr) => {
if (arr.length >= renderAmount) {
return arr.slice(arr.length - (renderAmount), arr.length)
}
else {
return arr
}
}
const posTop = () => {
return (
typeof window.scrollY != 'undefined' ? window.scrollY :
document.documentElement && document.documentElement.scrollTop ?
document.documentElement.scrollTop :
document.body.scrollTop ? document.body.scrollTop : 0
);
}
document.addEventListener('scroll', () => {
if (posTop() === 0) {
if (totalAmount - renderAmount >= 10) {
setRenderAmount(renderAmount + 10);
}
else if (totalAmount - renderAmount < 10) {
setRenderAmount(renderAmount + (totalAmount - renderAmount));
}
}
})
//Also may be important. I use this bit of code to ensure that the component stays scrolled to the bottom on initial render.
const btmUpView = () => {
window.scrollTo(0, document.body.scrollHeight);
}
btmUpView();
return(
<div>
<div>
<MsgFeed/>
</div>
<div>
<input type="text"></input>
<button onClick={() => sendMsg()}>Send</button>
</div>
</div>
);

Make navbar link active on wheel event rather than scroll?

I want to make the links in my navbar have an active class when you scroll into the corresponding section.
The code below was working just fine until I implemented a smooth scroll/parallax library which removes the scroll event.
I tried making the code work using the wheel event, tried seeing if there was anything similar to scrollY for wheel events but I couldn't find anything.
Any ideas on how I could implement this feature? It can be different from the implementation I had before
EDIT: Here's a codepen. If you uncomment the locomotive portion, the code no longer works. How can I make it work?
const sections = document.querySelectorAll("section");
const navLinks = document.querySelectorAll("nav a");
window.addEventListener("scroll", function () {
let navbar = document.querySelector("nav");
let current = "";
sections.forEach(function (section) {
const sectionTop = section.offsetTop;
const sectionHeight = section.clientHeight;
if (scrollY >= sectionTop - sectionHeight / 3) {
current = `#${section.getAttribute("id")}`;
}
navLinks.forEach(function (each) {
// add/remove active class
each.classList.remove("nav-active");
if (each.getAttribute("href") == current) {
each.classList.add("nav-active");
}
});
});
});
Try using an IntersectionObserver instead:
const navLinks = document.querySelectorAll("nav a");
const updateNav = (entries, observer) => {
const matchingIds = entries.filter(e => e.isIntersecting).map(e => `#${e.target.id}`);
if (matchingIds.length !== 0) {
const current = matchingIds[0];
navLinks.forEach(function(link) {
link.classList.remove("nav-active");
if (link.getAttribute("href") == current) {
link.classList.add("nav-active");
}
});
}
};
const options = {
root: null,
rootMargin: "0px",
threshold: 0.66
}
const observer = new IntersectionObserver(updateNav, options);
document.querySelectorAll("*[data-scroll-section]").forEach(el => observer.observe(el));
Intersection Observer API
Demo

Having conditional rendering on an id of div tag

Currently I'm trying to move my image from center to left and left to right as the user scrolls down. For achieving this, I'm using useEffect to manipulate my DOM events. I want the layout in such a way that after the user has scrolled 600 pixels in height the image starts moving to the right. For this I tried conditionally rendering a div tag but I get an error in my useEffect since it doesnt recognize the other element. So how can I move my image when it reaches a certain height?
CodeSandbox: https://codesandbox.io/s/misty-sun-e6odq?file=/src/App.js
Code:
const [display, setDisplay] = useState(false);
useEffect(function onFirstMount() {
const changeBackground = () => {
let value = window.scrollY;
console.log(value);
let img = document.getElementById("moveLeft");
let img2 = document.getElementById("moveRight");
img.style.transform = `translateX(-${value * 0.5}px)`;
img2.style.transform = `translateX(${value * 0.5}px)`;
if (value > 600) {
setDisplay(true);
} else {
setDisplay(false);
}
};
window.addEventListener("scroll", changeBackground);
return () => window.removeEventListener("scroll", changeBackground);
}, []);
return (
<>
<div className="App">
<div class="inflow">
<div class="positioner">
<div class="fixed">
<div id={display?"moveRight":"moveLeft"}>
<img
alt="passport"
src="https://cdn.britannica.com/87/122087-050-1C269E8D/Cover-passport.jpg"
/>
</div>
</div>
</div>
</div>
</div>
<div className="App2">
</div>
<div className="App2"></div>
</>
As you have two calls to getElementById and you dynamically change the id based on state, you will always have an undefined element.
You could fix it like so
let img =
document.getElementById("moveLeft") ||
document.getElementById("moveRight");
const val = img.id === "moveLeft" ? -(value * 0.5) : value * 0.5;
img.style.transform = `translateX(${val}px)`;
And remove the img2 call, as there is always one of the above in the document. Example of this here: https://codesandbox.io/s/stoic-babycat-6bl5i?file=/src/App.js
You could also try to achieve it with only one image:
useEffect(
function onFirstMount() {
const changeBackground = () => {
if (!imgRef || !imgRef.current) return;
const value = window.scrollY;
if (value > 600) {
imgRef.current.style.display = `block`;
} else {
imgRef.current.style.display = `none`;
return;
}
const progress = (window.innerWidth / 100) * moveRatio;
if (
animData.current.isGoingRight &&
animData.current.currentPos >= window.innerWidth - 200
) {
animData.current.isGoingRight = false;
} else if (
!animData.current.isGoingRight &&
animData.current.currentPos <= startPosition
) {
animData.current.isGoingRight = true;
}
if (animData.current.isGoingRight)
animData.current.currentPos += progress;
else animData.current.currentPos -= progress;
imgRef.current.style.transform = `translateX(${animData.current.currentPos}px)`;
};
window.addEventListener("scroll", changeBackground);
return () => window.removeEventListener("scroll", changeBackground);
},
[imgRef]
);
This one moves in relation to the window inner width, you can see it here https://codesandbox.io/s/hidden-microservice-xb2ku?file=/src/App.js, that's just an example, I'm sure there are another approaches

Is there a callback for window.scrollTo?

I want to call focus() on an input after the widow scrolled. I'm using the smooth behavior for the scrollTo() method. The problem is the focus method cut the smooth behavior. The solution is to call the focus function just after the scroll end.
But I can't find any doc or threads speaking about how to detect the end of scrollTo method.
let el = document.getElementById('input')
let elScrollOffset = el.getBoundingClientRect().top
let scrollOffset = window.pageYOffset || document.documentElement.scrollTop
let padding = 12
window.scrollTo({
top: elScrollOffset + scrollOffset - padding,
behavior: 'smooth'
})
// wait for the end of scrolling and then
el.focus()
Any ideas?
I wrote a generic function based on the solution of George Abitbol, without overwriting window.onscroll:
/**
* Native scrollTo with callback
* #param offset - offset to scroll to
* #param callback - callback function
*/
function scrollTo(offset, callback) {
const fixedOffset = offset.toFixed();
const onScroll = function () {
if (window.pageYOffset.toFixed() === fixedOffset) {
window.removeEventListener('scroll', onScroll)
callback()
}
}
window.addEventListener('scroll', onScroll)
onScroll()
window.scrollTo({
top: offset,
behavior: 'smooth'
})
}
I found a way to achieve what I want but I think it's a bit hacky, isn't it?
let el = document.getElementById('input')
let elScrollOffset = el.getBoundingClientRect().top
let scrollOffset = window.pageYOffset || document.documentElement.scrollTop
let padding = 12
let target = elScrollOffset + scrollOffset - padding
window.scrollTo({
top: target,
behavior: 'smooth'
})
window.onscroll = e => {
let currentScrollOffset = window.pageYOffset || document.documentElement.scrollTop
// Scroll reach the target
if (currentScrollOffset === target) {
el.focus()
window.onscroll = null // remove listener
}
}
Other answers didn't fully work for me, therefore based on #Fabian von Ellerts answer, I wrote my own solution.
My problems were that :
The element I was scrolling (and all its parents along the hierarchy) always had a offsetTop of 0, so it was not working.
I needed to scroll a nested element.
Using getBoundingClientRect and a container element as reference works :
const smoothScrollTo = (
scrollContainer,
scrolledContent,
offset,
callback
) => {
const fixedOffset = (
scrollContainer.getBoundingClientRect().top + offset
).toFixed()
const onScroll = () => {
if (
scrolledContent.getBoundingClientRect().top.toFixed() ===
fixedOffset
) {
scrollContainer.removeEventListener('scroll', onScroll)
callback()
}
}
scrollContainer.addEventListener('scroll', onScroll)
onScroll()
scrollContainer.scrollTo({
top: offset,
behavior: 'smooth',
})
}

Categories

Resources