Add and remove css class only during scroll in React? - javascript

I am working on a mockup blog for my portfoilio which has a grid of posts which are 400x400 cards with a hover effect that increases scale and adds a drop shadow (this is rendered in the dashboard route).
I have noticed however that when scrolling my page, the pointer will get hung up on the cards and stop registering scroll when the animation is enabled. I have come across this article (https://www.thecssninja.com/javascript/pointer-events-60fps) discussing the performance benefits of disabling pointer events to the body on scroll however I cannot figure out how to do this in react.
How would you add a class to the document body in React ONLY while scrolling and remove that class as soon as the scroll event had ended? Maybe with a setTimeOut in some way?? I feel like that would be a janky solution...
I included the code I am looking to implement this in, I don't know if that will help. The scroll event I have set up already is to maintain the scroll position of the page while the navbar is extended.
export default class Layout extends Component {
constructor(props) {
super(props);
this.state = {
hasMenuOpen: false,
scroll: {x: 0, y: 0},
};
this.handleScrollY = _.debounce(this.handleScrollY, 250);
this.handleWidthChange = _.debounce(this.handleWidthChange, 250);
}
componentDidMount() {
window.addEventListener('scroll', this.handleScrollY);
window.addEventListener('resize', this.handleWidthChange);
console.log('mount');
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (prevState.hasMenuOpen) {
/* eslint-disable */
let {x, y} = prevState.scroll;
/* eslint-enable */
// correct scroll y position back to 0 for positions <= 100
window.scrollTo(x, (y <= 100 ? y = 0 : y));
}
}
componentWillUnmount() {
window.removeEventListener('scroll', this.handleScrollY);
window.removEventListener('resize', this.handleWidthChange);
}
// if menu is open an y = 0 (i.e position: fixed was added),
// scroll = previous states scroll
// else if you scroll and the menu isn't open, scroll = windows new scroll pos
handleScrollY = () => {
const y = window.scrollY;
const x = window.scrollX;
this.setState((prevState) => {
if (this.state.hasMenuOpen && y === 0) {
return {scroll: Object.assign({}, prevState.scroll)};
}
return {scroll: Object.assign({}, prevState.scroll, {x}, {y})};
});
}
handleWidthChange = () => {
console.log(window.innerWidth);
if (this.state.hasMenuOpen) {
return this.handleBurgerClick();
}
return null;
}
handleBurgerClick = () => {
this.setState((prevState) => ({
hasMenuOpen: !prevState.hasMenuOpen
}));
}
handleLinkClick = () => {
this.setState((prevState) => ({
hasMenuOpen: false
}));
}
render() {
const scrollTop = {
top: `-${this.state.scroll.y}px`,
};
console.log(this.state.scroll);
return (
<React.Fragment>
<Navbar onClick={this.handleBurgerClick} hasMenuOpen={this.state.hasMenuOpen} onLinkClick={this.handleLinkClick} />
<div className={this.state.hasMenuOpen ? styles.scroll_lock : ''} style={this.state.hasMenuOpen ? scrollTop : {}}>
<main className={styles.page_margin} >
<div className={this.state.hasMenuOpen ? styles.margin_extended : styles.margin_top}>
<Route path='/' exact component={Dashboard} />
<Route path='/new-post' component={NewPost} />
</div>
</main>
</div>
</React.Fragment>
);
}
}
For example I have tried setting document.body.style.pointerEvents = 'auto' in componentDidMount() and disabling it in the handleScrollY() however this obviously doesn't work as pointer events are never restored once the scroll event occurs. I have also tried setting it in componentDidUpdate() but that doesn't seem to work either as no component is being updated when the scroll event isn't happening.

One way of toggling a css class is:
componentWillUnmount() {
document.removeEventListener('scroll');
}
componentDidMount() {
document.addEventListener('scroll', (event) => {
const that = this;
if (!that.state.isScrolling) {
that.setState({ isScrolling: true });
setTimeout(() => {
that.setState({ isScrolling: false });
}, 300);
}
});
}
And then use the state or props to toggle the className
in your JSX something like
return (
<div className={`class1 class2 class99 ${this.state.isScrolling ? 'scrolling' : 'not-scrolling'}`} >Content</div>
)

Related

dispatch to redux when screen size is for I.e. large

I have a confusing issue where I want to update a portion of the state of the header when it gets to screen size large. I'm currently using tailwind so I have the following code:
Where when in mobile view, if the template is not hidden, and the screen size gets to large-- the state should automatically get updated with handleHideTemplates...
That's what I'm attempting to do. I understand with this the handle is fired immediately which I don't want...
in mobile view, if the person hits showTemplates, it hides templates immediately.
const handleHideTemplates = () => {
dispatch(setShowTemplates({ hidden: 'hidden' }));
dispatch(setTemplateMargin({ margin: '' }));
};
console.log('hidden', hidden);
return (
<header
className={`text-gray-500 text-semibold text-s fill-accent1
top-0 z-30 sticky lg:grid ${
'grid' ? handleHideTemplates() : null
} bg-templateBg px-8 ${margin}`}
>
figured it out... instead of conditional inside class..
useEffect(() => {
const handleResize = () => {
if (window.innerWidth >= 1024) {
handleHideTemplates();
}
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);

Intersection Observer API not working with hidden elements

I am trying to observe a hidden element. I have an element which has style set to display: none. I want when my element intersect it will perform my action i.e: Play the video. I am sharing my sample code below
var options = {threshold: 0.5 }
var circle = document.getElementById('content_video');
var observer = new IntersectionObserver(entries => {
var [{ isIntersecting }] = entries
if (isIntersecting) {
player.play();
player.ima.getAdsManager().resume();
} else {
player.ima.getAdsManager().pause();
}
}, options);
window.addEventListener('load', () => {
if ('IntersectionObserver' in window) observer.observe(circle);
}, false);
This is normal behaviour, because element with display:none cannot be reached and ignoring by browser
Try set other styles instead display:none. Example, use opacity or width and height 0 with overflow: hidden
Unfortunately if your goal is to use the intersection observer to lazy load the media, the answer will not fulfil the purpose since the media will load before it intersects with the viewport. The solution is to make sure it is the containing element of the media that is being observed, instead the element on which display none is being applied.
https://codesandbox.io/s/react-lazyload-forked-yz93f?file=/src/LazyLoad.js:0-958
import React, { Component, createRef } from "react";
import "./styles.css";
class LazyLoad extends Component {
observer = null;
rootRef = createRef();
state = {
intersected: false
};
componentDidMount() {
this.observer = new IntersectionObserver(
(entries) => {
const entry = entries[0];
if (entry.isIntersecting) {
this.setState({ intersected: true });
this.observer.disconnect();
}
},
{ root: null, threshold: 0.2 }
);
this.observer.observe(this.rootRef.current);
console.log(this.rootRef);
}
render() {
return (
<div className="outer" ref={this.rootRef}>
<span>{JSON.stringify(this.state.intersected)}</span>
<div
className={`container ${
this.state.intersected ? "d-block" : "d-none"
}`}
>
{this.props.children}
</div>
</div>
);
}
}
export default LazyLoad;

Issue with react-sound state management

Having some trouble with state management in react-sound. I have a dynamic grid of buttons - when mousing over a button, audio plays as an example of what that button represents, and when mousing off, it stops. Right now, it's running into problems when you click on a button and the screen reloads another array as a grid of buttons - when you do that and then navigate back to the former screen, the behavior is REVERSED, so that audio plays when you mouse OFF the button and stops when you mouse ON. I've tried several things that I thought should work, including triggering toggleHoverState() on click, but nothing has worked so far. Any advice? Code below:
class Screen extends React.Component {
constructor(props) {
super(props);
this.state = {displayArray: taxonomyArray,
isHovering: false};
this.handleClick = this.handleClick.bind(this);
this.handleMouseHover = this.handleMouseHover.bind(this);
}
handleMouseHover() {
this.setState(this.toggleHoverState);
}
toggleHoverState(state) {
return {
isHovering: !state.isHovering,
};
}
handleClick(updatedArray = taxonomyArray) {
//this.setState(this.toggleHoverState);
this.setState({
displayArray: updatedArray,
});
}
render () {
return (
this.state.displayArray.map(item =>
<Col key={Math.random()} span={4} xs="auto" sm="auto" md="auto" lg="auto">
<a key={Math.random()} data-tip={item.description ? (item.description) : console.log('null description')}>
{ (item.audio && this.state.isHovering) && <div><Sound url={item.audio} playStatus={Sound.status.PLAYING} volume={50}/></div> }
<Button
className='trigger'
key={Math.random()}
variant= {item.child === undefined ? "outline-secondary" : (item.child === null ? "secondary" : "primary") }
onMouseEnter={this.handleMouseHover}
onMouseLeave={this.handleMouseHover}
onClick={ () => ( item.child === null ? console.log('nope') : ( item.child === undefined ? this.handleClick(arrayVisits.pop()) : (arrayVisits.push(this.state.displayArray), this.handleClick(item.child))) ) }
>
{item.name}
</Button>
</a>
<ReactTooltip className='extraClass' delayHide={0} effect='solid' type="info" multiline={true}/>
<div> </div>
</Col>
)
);
}
}
This logic here is very strange:
handleMouseHover() {
this.setState(this.toggleHoverState);
}
toggleHoverState(state) {
return {
isHovering: !state.isHovering,
};
}
Why not just write
handleMouseHover() {
this.setState({ isHovering: !this.state.isHovering })
}
But if you're still having problems, I would write some very explicit functions for onMouseEnter and onMouseLeave to reduce confusion:
onMouseEnter={ () => { this.setState( isHovering: true ) } }
onMouseLeave={ () => { this.setState( isHovering: false) } }
Let me know if that solves it
Is the page actually reloading, or is it a BrowserRouter?
It sounds like, if it's a BrowserRouter, that state isn't being reset. Try adding:
componentDidMount () {
this.setState({isHovering:false})
}

How can I stop "scrollIntoView" when the screen reaches a reference?

How can I stop the scroll Into View when the screen reach the reference (inside the method scroll) ?
(When the screen reaches its reference, I scroll a little bit up and the code atomatcaly come back to the reference)
interface Props {
section: number
}
const Home = ({ section }: Props) => {
const sectionRef1: any = useRef(React.createRef());
const sectionRef2: any = useRef(React.createRef());
const scroll = (ref: any) => {
ref.current.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
};
function scrollToSection(section: number) {
if (section === 1) {
scroll(sectionRef1);
}
else if (section === 2) {
scroll(sectionRef2);
}
else if (section === 3) {
//TODO: active button
}
}
scrollToSection(section);
return (
<div>
<div ref={sectionRef1} />
<Carrousel></Carrousel>
<div ref={sectionRef2} className="margin_top_portrait" />
<Portrait></Portrait>
</div>
);
}
export default Home;
I'm afraid that this funcion will never finish ... and it's asynchronous.
Thanks in advance for your help.
If you want to run the scroll method only when the section changes, you can use the useEffect hook of react:
useEffect(() => {
scrollToSection(section);
}, [section]);
This will execute the scrollToSection method the first time the component is mounted and every time the section prop changes.

How to scroll to an element using react-spring?

I have read through the entire react-spring docs and there doesn't seem to be a clear way to do this.
My attempt:
import React, { useRef, useState } from "react"
import { animated, useSpring } from "react-spring"
const App = () => {
const scrollDestinationRef = useRef()
const [elementScroll, setElementScroll] = useState(false)
const buttonClickHandler = () => setElementScroll(prevState => !prevState)
const scrollAnimation = useSpring({
scroll: elementScroll
? scrollDestinationRef.current.getBoundingClientRect().top
: 0
})
return (
<main>
{/* Click to scroll to destination */}
<animated.button
onClick={buttonClickHandler}
scrollTop={scrollAnimation.scroll}
style={{
height: "1000px",
width: "100%",
backgroundColor: "tomato"
}}
>
Scroll to destination
</animated.button>
{/* Scroll destination */}
<div
ref={scrollDestinationRef}
style={{
height: "200px",
width: "200px",
backgroundColor: "green"
}}
></div>
</main>
)
}
export default App
I'm using a ref and hooks for my attempt.
The useRef is attached the scroll destination in-order to find its offset top from the website's ceiling.
I use useState to toggle between the state on click to trigger the scroll.
I use useSpring to trigger an animation that goes from 0 to the scroll destination's scroll top a.k.a. getBoundingClientRect().top.
Can anyone assist in solving this?
There doesn't to be much explanation online, thanks!
useSpring returns a function to set/update animated values. You can use that function to assign a new value to your animated variable. Then, you can use the onFrame property to update the scroll position.
Define your spring like this:
const [y, setY] = useSpring(() => ({
immediate: false,
y: 0,
onFrame: props => {
window.scroll(0, props.y);
},
config: config.slow,
}));
Then use setY function to start the spring, like this:
<button
onClick={() => {
setY({ y: scrollDestinationRef.current.getBoundingClientRect().top });
}}
>
Click to scroll
</button>
When you click the button it will assign a new value to y variable in your spring, and onFrame function will be called upon every update.
Note that we call window.scroll function from onFrame property in useSpring.
See working demo here.
Finally after getting through https://github.com/pmndrs/react-spring/issues/544, answer from Yannick Schuchmann here worked for me, I just had to change onFrame to onChange
I made a custom hook for the solution: https://github.com/TomasSestak/react-spring-scroll-to-hook
react-sring version: 9.0.0-rc.3
const targetElement = useRef(null)
const [, setY] = useSpring(() => ({ y: 0 }))
let isStopped = false
const onWheel = () => {
isStopped = true
window.removeEventListener('wheel', onWheel)
}
const scrollToTarget = () => {
const element = targetElement.current
const value = window.scrollY + element.getBoundingClientRect().top
window.addEventListener('wheel', onWheel)
setY({
y: value,
reset: true,
from: { y: window.scrollY },
onRest: () => {
isStopped = false
window.removeEventListener('wheel', onWheel)
},
onFrame: props => {
if (!isStopped) {
window.scroll(0, props.y)
}
}
})
}
This version also allows the user to break out of the forced scrolling by using wheel event. (I personally hate forced scrolling :D)
useSpring(fn) does return a stop method besides the set. But I couldn't make it work with that. I posted it to a related github issue. If you read this, maybe there's a good answer for that already :)
For now it uses a little workaround with isStopped flag.
UPDATE:
Seems like there is actually something fishy with stop fn which should be addressed in v9 canary. Haven't tested since v9 is not stable yet.

Categories