I'm attempting to create an animation in which one element fades based upon the scroll position of another. I was able to get the scrolling element to work using React Spring, but I'm struggling to wrap my head around how to leverage state hooks without conditionals and only being able to set state at a component top level.
SandBox
const HomeView = () => {
const ref = useRef();
const [isVisible, setVisible] = useState(true);
const [{ offset }, set] = useSpring(() => ({ offset: 0 }));
const calc = (o) => {
if (o < 1004) {
return `translateY(${o * 0.08}vh)`;
} else {
// this won't work b/c im trying to useState in a Fn
setVisible(false);
return `translateY(${1012 * 0.08}vh)`;
}
};
const handleScroll = () => {
const posY = ref.current.getBoundingClientRect().top;
const offset = window.pageYOffset - posY;
set({ offset });
};
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
});
return (
<div ref={ref} className="home-view" style={homeViewStyles}>
<div style={topSectionStyles} className="top-content-container"></div>
<animated.div
className="well-anim"
style={{
width: "100vw",
height: "500px",
transform: offset.interpolate(calc),
zIndex: 300,
top: "340px",
position: "absolute"
}}
>
<h1>Well,</h1>
</animated.div>
{/* Trying to fade this component when above animated.div is right above it */}
<h2 style={{ paddingTop: "90px" }} fade={isVisible}>
Hello There!
</h2>
{/***************************************/}
</div>
);
};
You are almost there. I think the problem here is with the fade attribute. The setVisible function is invoked all right. I would introduce a second spring to deal with the opacity with the h2 element, based on the state of the isVisible variable.
const {opacity} = useSpring({opacity: isVisible ? 1 : 0});
<animated.h2 style={{ paddingTop: "90px", opacity }} >
Hello There!
</animated.h2>
https://codesandbox.io/s/wild-dew-tyynk?file=/src/App.js
Related
I am implementing a chat view in React,and the desired behavior is that whenever new data is pushed into the dataSource, the chat view (an infinite list) scrolls to the bottom, there are many implementations, some found here: How to scroll to bottom in react?. However when I try to implement it, I am getting this strange behavior where all the views in the window are "pushed upward" out of sight by some 300px, as if to accomadate this new <div> that is at the bottom of the list view. My implementation below:
import React, {useEffect, useRef} from "react";
import { createStyles, makeStyles, Theme } from "#material-ui/core/styles";
import InfiniteScroll from 'react-infinite-scroll-component';
const row_1 = 2.5;
const row_chat = 4
const useStyles = makeStyles((theme: Theme) => createStyles({
container: {
width: '40vw',
height: `calc(100vh - 240px)`,
position: 'relative',
padding: theme.spacing(3),
},
}));
const chat_container_style = {
width: '40vw',
height: `calc(100vh - 240px - ${row_chat}vh - ${row_1}vh)`,
}
function ChatView(props) {
const classes = useStyles();
const { _dataSource } = props;
// scroll to bottom
const messagesEndRef = useRef(null)
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
}
useEffect(() => {
scrollToBottom()
}, [_dataSource]);
return (
<div className={classes.container}>
{/* chat window */}
<InfiniteScroll
dataLength={_dataSource.length}
next={() => { return }}
hasMore={true}
loader={<></>}
style={chat_container_style}
>
{_dataSource.map((item, index) => {
return (
<div {...props} key={index} item={item}>
{`item: ${index}`}
</div>
)
})}
{/* putting an item here push all divs upward */}
<div ref={messagesEndRef} />
</InfiniteScroll>
</div>
)
}
Note the use of <InfiniteScroll/> is not the cause of the behavior, Really if I put the ref={messagesEndRef} into any view, it pushes all the views up in the viewport.
The issue has been resolved. The source of the issue is the scrollIntoView function, it's scrolling the entire page instead of just the listView, here's the correct scrollIntoView function with the correct parameters:
const scrollDivRef = createRef();
useEffect(() => {
scrollDivRef.current?.scrollIntoView({
block : 'nearest',
inline : 'start',
behavior: 'smooth',
})
}, [_dataSource.length]);
Here's how the ref is nested inside the DOM:
<InfiniteScroll
next={() => { return }}
hasMore={true}
loader={<></>}
style={chat_container_style}
dataLength={_dataSource.length}
>
{_dataSource.map((item, index) => (
<BubbleView {...props} key={index} item={item}/>
))}
<div style={refDivStyle} ref={scrollDivRef}/>
</InfiniteScroll>
This problem has nothing to do w/ how I layout the style sheet.
I am working on a nextjs project with Material UI. I am to create a page with an app bar, essentially unlimited contents in the middle, and a footer.
I am trying to create a partially sticky footer, where the Top part is just a small orange bar, which is already implemented using css
export default function Footer() {
return ( <div style={{ position: "fixed", width: "100%", backgroundColor: Colors.orange6, bottom: "0", left: "0", height: 16, }} /> );
}
I need to create a bottom part of the footer, which is partially sticky. It will only be visible when scrolled to the bottom.
What would be an elegant way to implement this?
try checking if you are at the bottom of the page and conditionally hide and show your footer.
const App = () => {
const [isBottom, setIsBottom] = React.useState(false);
const handleScroll = () => {
const bottom = Math.ceil(window.innerHeight + window.scrollY) >= document.documentElement.scrollHeight
if (bottom) {
setIsBottom(true)
} else {
setIsBottom(false)
}
};
React.useEffect(() => {
window.addEventListener('scroll', handleScroll, {
passive: true
});
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
return (
<div>content.....</div>
<footer className={isBottom ? "showFooter" : "hideFooter"}>M</footer>
)
};
I am using react-spring for animation and all the animations start once the page is loaded. I want to control the start of the animation. The desired outcome is to let the components down in the screen start the animation once they are in view (i.e the user scrolled down). The code follows something like this :
const cols = [
/*Components here that will be animated ..*/
{component: <div><p>A<p></div> , key:1},
{component: <div><p>B<p></div> , key:2},
{component: <div><p>C<p></div> , key:3},
]
export default function foocomponent(){
const [items, setItems] = React.useState(cols);
const [appear, setAppear] = React.useState(false); // Should trigger when the component is in view
const transitions = useTransition(items, (item) => item.key, {
from: { opacity: 0, transform: 'translateY(70px) scale(0.5)', borderRadius: '0px' },
enter: { opacity: 1, transform: 'translateY(0px) scale(1)', borderRadius: '20px', border: '1px solid #00b8d8' },
// leave: { opacity: 1, },
delay: 200,
config: config.molasses,
})
React.useEffect(() => {
if (items.length === 0) {
setTimeout(() => {
setItems(cols)
}, 2000)
}
}, [cols]);
return (
<Container>
<Row>
{appear && transitions.map(({ item, props, key }) => (
<Col className="feature-item">
<animated.div key={key} style={props} >
{item.component}
</animated.div>
</Col>
))}
</Row>
</Container>
);
}
I tried using appear && transitions.map(...) but unfortunately that doesn't work. Any idea how should I control the start of the animation based on a condition?
I use https://github.com/civiccc/react-waypoint for this type of problems.
If you place this hidden component just before your animation. You can switch the appear state with it. Something like this:
<Waypoint
onEnter={() => setAppear(true) }
/>
You can even specify an offset with it. To finetune the experience.
If you wish to have various sections fade in, scroll in, whatever on enter, it's actually very simple to create a custom wrapper. Since this question is regarding React Spring, here's an example but you could also refactor this a little to use pure CSS.
// React
import { useState } from "react";
// Libs
import { Waypoint } from "react-waypoint";
import { useSpring, animated } from "react-spring";
const FadeIn = ({ children }) => {
const [inView, setInview] = useState(false);
const transition = useSpring({
delay: 500,
to: {
y: !inView ? 24 : 0,
opacity: !inView ? 0 : 1,
},
});
return (
<Waypoint onEnter={() => setInview(true)}>
<animated.div style={transition}>
{children}
</animated.div>
</Waypoint>
);
};
export default FadeIn;
You can then wrap any component you want to fade in on view in this FadeIn component as such:
<FadeIn>
<Clients />
</FadeIn>
Or write your own html:
<FadeIn>
<div>
<h1>I will fade in on enter</h1>
</div>
</FadeIn>
I have an Accordion component which his children can change his height dynamically (by API response), I try this code but not working because the height changes only if I close and re-open the accordion. The useEffect not triggering when children DOM change. Can anyone help me? Thanks
export const VerticalAccordion = (props) => {
const accordionContainerRef = useRef<HTMLDivElement>(null);
const [contentHeight, setContentHeight] = useState<number | undefined>(0);
const [animationClass, setAnimationClass] = useState<'animated'>();
const [overflow, setOverflow] = useState<'visible' | 'hidden'>('visible');
const [isOpen, setIsOpen] = useState<boolean>(true);
const {title, children} = props;
useEffect(() =>{
if(accordionContainerRef.current){
const height = isOpen ? accordionContainerRef.current.scrollHeight: 0;
setContentHeight(height);
if(isOpen){
// delay need for animation
setTimeout(() => setOverflow('visible'),700);
return;
}
return setOverflow('hidden')
}
}, [isOpen, accordionContainerRef, children]);
const onAccordionClick = () => {
setAnimationClass('animated');
setIsOpen(prevState => !prevState)
};
return (
<div className={'accordion'}>
<div className={`header`}>
<div className={`header-title`}>{title}</div>
<MyIcon onClick={() => onAccordionClick()}
customClass={`header-arrow`}
path={menuDown}
size={20}/>
</div>
<div ref={accordionContainerRef}
style={{ height: `${contentHeight}px`, overflow}}
className={`data` + (animationClass ? ` data--${animationClass}` : '')}>
{children}
</div>
</div>
)
}
Even though the working around what I have found is not a robust one, but it is working completely fine for me. If someone stumbles upon this same issue might find this useful.
const [activeState, setActiveState] = useState("");
const [activeHeight, setActiveHeight] = useState("0px");
const contentRef = useRef(null)
const toogleActive = () => {
setActiveState(activeState === "" ? "active" : "");
setActiveHeight(activeState === "active" ? "0px" :`${contentRef.current.scrollHeight + 100}px`)
}
return (
<div className={styles.accordion_section}>
<button className={styles.accordion} onClick={toogleActive}>
<p className={styles.accordion_title}>{title}</p>
</button>
<div ref={contentRef}
style={{ maxHeight: `${activeHeight}` }}
className={styles.accordion_content}>
<div>
{content}
</div>
</div>
</div>
)
I have hardcoded some extra space so that while the dynamic response is accepted the Child DOM is shown. In the CSS module file, I have kept the overflow as auto, earlier it was hidden.
.accordion_content {
background-color: white;
overflow: auto;
max-height: max-content;
}
As a result, the Child DOM is appearing dynamically and the user can scroll inside the Accordion if the Child DOM needs larger space.
If you want to use state hooks it is better to use context, so that the hook will be shared between the child and parent. This link might give you an idea of how does context works.
Here is how I am solving a similar problem. I needed an accordion that
Could be opened/closed depending on external variable
Could be opened/closed by clicking on the accordion title
Would resize when the contents of the accordion changes height
Here's the accordion in its parent component. Also, the useState hook for opening/closing the accordion from outside the accordion (as necessary).
const [isOpen, setOpen] = useState(false)
// some code...
<Accordion title={"Settings"} setOpen={setOpen} open={isOpen}>
{children}
</Accordion>
Here's the accordion component. I pass the setOpen hook from the parent to the accordion. I use useEffect to update the accordion's properties when either open or the children change.
import React, { useEffect, useState, useRef } from "react"
import styled from "styled-components"
const AccordionTitle = styled.span`
cursor: pointer;
`
const AccordionContent = styled.div`
height: ${({ height }) => height}px;
opacity: ${({ height }) => (height > 0 ? 1 : 0)};
overflow: hidden;
transition: 0.5s;
`
export const Accordion = ({ title, setOpen, open, children }) => {
const content = useRef(null)
const [height, setHeight] = useState(0)
const [direction, setDirection] = useState("right")
useEffect(() => {
if (open) {
setHeight(content.current.scrollHeight)
setDirection("down")
} else {
setHeight(0)
setDirection("right")
}
}, [open, children])
return (
<>
<h3>
<AccordionTitle onClick={(e) => setOpen((prev) => !prev)}>
{title}
<i className={`arrow-accordion ${direction}`}></i>
</AccordionTitle>
</h3>
<AccordionContent height={height} ref={content}>
{children}
</AccordionContent>
</>
)
}
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.