Trigger scroll event when user scroll up or down react.js - javascript

I'm trying to simulate Tesla's web app landing page scroll behavior using React.js and react-scroll
Assume we have 2 sections, the hero image is section 1, and within a small scroll from the user, an event is triggered it scrolls down to the next section (section 2)
My question is how could I trigger the scroll event to scroll to section 2 when the user is scrolling down while he is on section 1, similarly to the Tesla landing page.
I have achieved it using listener on offsetY and a condition if it is equivalent to 100px if (offsetY === 100), the window will be scrolled to section 2. but that only could be achieved in equivalence to 100. in other words, the window will be scrolled if and only if it meets 100px relative to the document.
any thoughts or recommendations would be useful.
function App() {
const [offsetY, setOffSetY] = useState(0);
const handleScroll = (e) => {
setOffSetY(window.pageYOffset)
};
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, [])
useEffect(() => { // fires when the user scroll up or down, i.e. when the offsetY changes
if (offsetY === 100) { // scroll to section 2 when the offsety is equal to 100px
scroller.scrollTo("section2", {
delay: 100,
duration: 200,
smooth: true
})
}
}, [offsetY]);
return (
<React.Fragment>
<div id="section1" style={{height:"500px", width:"100%", color:"black"}}>
</div>
<div id="section2" style={{height:"500px", width:"100%", color:"red"}}>
</div>
</React.Fragment>
);
}
export default App;

Related

Using Intersection observer to highlight active link isn't consistent

I have a aside section with anchor links on the left and content on the right. What I want to do is to highlight the anchor link when the user scrolls to a particular text that's anchored.
I am using Intersection Observer for this feature. However, the problem is that on scroll it's all working fine, but when you click the anchor, the active class doesn't trigger properly. That's because the paragraphs that I am observing are all different sizes.
Some of them are taller, some smaller. So if I have 2 paragraphs that are small and right close to each other, the class doesn't trigger on a link which I clicked.
I think I'd need some dynamic thershold or margins but I am not sure how to do this.
For example, on this site it works really well even though the paragpraphs are close to each other: https://n26.com/en-eu/blog/bike-tours-europe#france-champagne-region
async highlightAnchorOnScroll() {
if (window.matchMedia('(min-width:768px)')) {
await new Promise((resolve) =>
setTimeout(() => {
let observerOptions = {
threshold: 0.5,
rootMargin: '0px 0px -50% 0px',
}
const list = document.querySelectorAll('.b-anchor')
this.$options.observer = new IntersectionObserver(
observerCallback,
observerOptions
)
const asideLinks = document.querySelectorAll('.aside ul li')
function observerCallback(entries) {
entries.forEach((entry, index) => {
if (entry.isIntersecting) {
asideLinks.forEach((el) => {
if (
entry.target.previousSibling.children[0].innerText.trim() ===
el.innerText.trim()
) {
el.classList.add('activeLink')
} else {
el.classList.remove('activeLink')
}
})
}
})
}
Array.from(list).forEach((element, index) => {
this.$options.observer.observe(element.nextElementSibling)
})
}, 200)
)
}
},
Code when clicking on anchor
scrollToElement(refName) {
const test = document.getElementById(refName)
const y = window.pageYOffset + test.getBoundingClientRect().top - 200
// document.getElementById(refName).getBoundingClientRect().top - 200
window.scroll({
top: y,
behavior: 'smooth',
})
},

React styles update only after wheel event fulfilled

What I want to achieve is smoothly scaled div container while scrolling (using mouse wheel to be strict) so user can zoom in and out.
However, my styles are "applied" by the browser only either when I scroll really slow or scroll normally and then wait about 0.2 seconds (after that time the changes are "bunched up"). I would like for the changes to be visible even during "fast" scrolling, not at the end.
The element with listener:
<div onWheel={(event) => {
console.log("wheeling"); // this console log fires frequently,
// and I want to update styles at the same rate
changeZoom(event);
}}
>
<div ref={scaledItem}> // content div that will be scaled according to event.deltaY
... // contents
</div>
</div>
My React code:
const changeZoom = useCallback((event: React.WheelEvent<HTMLDivElement>) => {
if (!scaledItem.current) return;
const newZoom = parseFloat(scaledItem.current.style.scale) + event.deltaY * 0.001;
console.log(newZoom); // logs as frequently as "wheeling" above
setCurrentZoom(newZoom);
}, []);
useEffect(() => {
if (!scaledItem.current) return;
scaledItem.current.style.scale = currentZoom.toString();
}, [currentZoom]);
useEffect(() => { // this is just for reproduction, needs to set initial scale to 1
if (!scaledItem.current) return;
scaledItem.current.style.scale = "1";
}, [])
What I have tried first was to omit all the React states, and edit scaledItem.current.style.scale directly from useCallback, but the changes took place in a bunch, after the wheeling events stopped coming. Then I moved zoom amount to currentZoom useState hook, but rerenders don't help either.
Edit:
I have also tried adding EventListener inside useEffect directly to the DOM Node:
useEffect(() => {
if (!scaledItemWrapper.current) return; // ref for wrapper of my scaled content
const container = scaledItemWrapper.current;
container.addEventListener("wheel", changeZoom);
return () => {
container.removeEventListener("wheel", changeZoom);
};
}, [changeZoom]);
Instead of setting up multiple states and observing can you try using a single state below is a working example. Try this if this works
https://codesandbox.io/s/wonderful-cerf-69doe?file=/src/App.js:0-727
export default () => {
const [pos, setPos] = useState({ x: 0, y: 0, scale: 1 });
const changeZoom = (e) => {
e.preventDefault();
const delta = e.deltaY * -0.01;
const newScale = pos.scale + delta;
const ratio = 1 - newScale / pos.scale;
setPos({
scale: newScale,
x: pos.x + (e.clientX - pos.x) * ratio,
y: pos.y + (e.clientY - pos.y) * ratio
});
};
return (
<div onWheelCapture={changeZoom}>
<img
src="https://source.unsplash.com/random/300x300?sky"
style={{
transformOrigin: "0 0",
transform: `translate(${pos.x}px, ${pos.y}px) scale(${pos.scale})`
}}
/>
</div>
);
};
Use a CSS transition
What I want to achieve is smoothly scaled div container while scrolling
The question's JavaScript
I didn't have to make changes to the posted JavaScript in my answer's code snippets, besides replacing scale with transform: scale() because it currently has incomplete browser support. Perhaps this can be written better but it does the job here, and is not the cause of the choppy behavior you observe.
Creating fluid motion from choppy input
Scroll events are by nature "bunched" because they arrive as the wheel is being turned "a notch". While that's not as true for all scrollable devices, it is for most people's mouse, so we have to deal with it for the foreseeable future. The browser also does additional bunching, in case of fast motion, but even without that the problem is already there.
So it's best to write code in a way that choppy input still results in a fluid motion, regardless of the step size. Once you have that, it automatically accounts for additional bunching by the browser.
You can add a CSS transition on the transform property to smooth out the scaling movement. It seems to work well with a value of 0.2 seconds, which I assume makes sense as it spreads the motion over the 0.2 seconds the browser is bunching up the changes in.
transition: transform 0.2s ease-out;
Performance implications
As a bonus, your app can keep rendering just 5 times a second.
Conversely, a solution that causes React to capture the maximum amount of fine grained scroll events will likely cause performance issues. A CSS transform is a lot cheaper then achieving the same effect through repeated renders.
Demonstration
You can observe the difference in the following 2 snippets.
It only runs properly if you open it full page. Otherwise it works but it will scroll the whole page too. I didn't want to make the code overly complex just to prevent that on SO.
Without transition (choppy)
const {useCallback, useEffect, useState, useRef} = React;
const minZoom = .01;
function App() {
const [currentZoom, setCurrentZoom] = useState("1");
const scaledItem = useRef();
const changeZoom = useCallback((event) => {
if (!scaledItem.current) return;
const scaleNumber = scaledItem.current.style.transform.replace('scale(','').replace(')','');
const newZoom = Math.max(minZoom, parseFloat(scaleNumber) + event.deltaY * 0.001);
console.log(newZoom); // logs as frequently as "wheeling" above
setCurrentZoom(newZoom);
}, []);
useEffect(() => {
if (!scaledItem.current) return;
scaledItem.current.style.transform = `scale(${currentZoom.toString()})`;
}, [currentZoom]);
useEffect(() => { // this is just for reproduction, needs to set initial scale to 1
if (!scaledItem.current) return;
scaledItem.current.style.transform = "scale(1)";
}, [])
return <div onWheel={(event) => {
console.log("wheeling");
changeZoom(event);
}}
>
<div class="scaled" ref={scaledItem}>
<p>Scale me up and down! (Use "Full page" link of snippet)</p>
</div>
</div>
}
ReactDOM.render(<App/>, document.getElementById('root'));
.scaled {
border: 2px solid lightgreen;
transform: scale(1);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
<div id="root2"></div>
With transition (smooth)
const {useCallback, useEffect, useState, useRef} = React;
const minZoom = .01;
function App() {
const [currentZoom, setCurrentZoom] = useState("1");
const scaledItem = useRef();
const changeZoom = useCallback((event) => {
if (!scaledItem.current) return;
const scaleNumber = scaledItem.current.style.transform.replace('scale(','').replace(')','');
const newZoom = Math.max(minZoom, parseFloat(scaleNumber) + event.deltaY * 0.001);
console.log(newZoom); // logs as frequently as "wheeling" above
setCurrentZoom(newZoom);
}, []);
useEffect(() => {
if (!scaledItem.current) return;
scaledItem.current.style.transform = `scale(${currentZoom.toString()})`;
}, [currentZoom]);
useEffect(() => { // this is just for reproduction, needs to set initial scale to 1
if (!scaledItem.current) return;
scaledItem.current.style.transform = "scale(1)";
}, [])
return <div onWheel={(event) => {
console.log("wheeling");
changeZoom(event);
}}
>
<div class="scaled" ref={scaledItem}>
<p>Scale me up and down! (Use "Full page" link of snippet)</p>
</div>
</div>
}
ReactDOM.render(<App/>, document.getElementById('root'));
.scaled {
border: 2px solid lightgreen;
transform: scale(1);
transition: transform .2s ease-out;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Framer-motion drag not respecting previously updated props

A simple use-case is to allow a user to either click buttons to paginate in a slider, or drag. Both events call the same paginate function with a param to either go forward or back--simple stuff.
However, the trigger from drag seems to cause bizarre behavior where the slider wants to start the animation from several slides back as if it ignores the updated props. This doesn't happen when using the buttons and both use the same simple paginate call.
Any tips appreciated.
Minimal example:
export default function App() {
const [position, setPosition] = useState<number>(0);
const paginate = (direction: Direction) => {
setPosition((prev) => {
return direction === Direction.Forward
? Math.max(-800, prev - 200)
: Math.min(0, prev + 200);
});
};
return (
<div className="App">
<Slider>
<Wrapper
animate={{ x: position }}
transition={{
x: { duration: 1, type: "tween" }
}}
drag="x"
dragConstraints={{
top: 0,
left: 0,
right: 0,
bottom: 0
}}
onDragEnd={(e, { offset, velocity }) => {
const swipe = swipePower(offset.x, velocity.x);
if (swipe < -swipeConfidenceThreshold) {
paginate(Direction.Forward);
} else if (swipe > swipeConfidenceThreshold) {
paginate(Direction.Back);
}
}}
>
<Slide>1</Slide>
<Slide className="alt">2</Slide>
<Slide>3</Slide>
<Slide className="alt">4</Slide>
<Slide>5</Slide>
</Wrapper>
</Slider>
<button onClick={() => paginate(Direction.Back)}>prev</button>
<button onClick={() => paginate(Direction.Forward)}>next</button>
</div>
);
}
Codesandbox Demo
I have to say, this problem is quite interesting. However, I think I figured out a way for you to handle this. One thing I noticed is that if you comment out
onDragEnd={(e, { offset, velocity }) => {
// const swipe = swipePower(offset.x, velocity.x);
// if (swipe < -swipeConfidenceThreshold) {
// paginate(Direction.Forward);
// } else if (swipe > swipeConfidenceThreshold) {
// paginate(Direction.Back);
// }
}}
the entire onDragEnd prop function, this example still doesn't work, since by the looks of things, the draggable component is not respecting your offset.
I realized that at this point, the problem is the internal state of the component is out of sync with your state. And would you look at that, the Framer Motion API actually provides a way to inspect this.
https://www.framer.com/api/motion/motionvalue/#usemotionvalue
It's the hook useMotionValue() which allows us to see what's actually happening. Turns out, our value is being set wrong when the user starts dragging:
useEffect(
() =>
motionX.onChange((latest) => {
console.log("LATEST: ", latest);
}),
[]
);
We can see this, because the state "jumps" to 200 as soon as we start dragging.
So fixing in theory is easy, we just need to make sure to let that value "know" about our offset, and that way it's gonna start with the proper offset in mind!
Anyway, that was my thought process, here's the solution, all you need to do is set the left constraint to make it work:
dragConstraints={{
top: 0,
left: position,
right: 0,
bottom: 0
}}
And tada! This makes it work. Here's my working solution: https://codesandbox.io/s/lingering-waterfall-2tsfi?file=/src/App.tsx

Vue CSS how to move object across the screen when scrolling

I have a project where I try to hover an element from right to left in the window while scrolling. So when the user is scrolling down the element goes from right to left and when the user is scrolling up it goes from left to right.
I have tried to do this by adding a scroll event listener to the window and then check when this component is visible on the browser(This component is one of multiple components which are shown underneath each other in the application)
If some one knows a better way to achieve this please feel free to comment. I just thought of using the absolute positioning to move the object across the screen.
This is the code I am using at this moment but it doens't want to work. The object doesn't move and stays put.
data () {
return {
position: 0
}
},
created () {
window.addEventListener('scroll', (event) => {
this.runOnScroll()
})
},
mounted () {
const navbar = this.$refs.neonComponent
this.navbarOffset = navbar.offsetTop
},
methods: {
runOnScroll () {
if ((this.navbarOffset - 600) < window.pageYOffset) {
this.position = Math.round((window.pageYOffset - (this.navbarOffset - 600)) / 10)
this.$refs.neonText.style.right = this.position
} else {
if (this.position > 0) this.position = 0
}
// console.log(this.position)
}
}
<div ref="neonComponent" class="relative w-full min-h-screen bg-black flex justify-center items-center">
<div ref="neonText" class="absolute max-w-3xl text-center position-text">
<h2 class="text-7xl">This is an example text</h2>
</div>
</div>
Try adding a unit (px, %, vh, vw) to style.right, like so:
this.$refs.neonText.style.right = this.position + 'px';

Not scrolling into view scrollIntoView scrollTo

Why is not my element scrolled into view?
useEffect(() => {
const elements = document.getElementsByClassName('some-class-name');
const [element] = elements;
if (element) {
window.scrollTo(0, 0);
element.scrollIntoView({ inline: 'center' });
}
}, [aProp]);
It did work before, but now it does not work. I am using Chrome. Should not the container with horizontal scroll scroll the element into view? The reason I have the window.scrollTo(0, 0) is because I want the window to scroll to the top as well.

Categories