Initial animation on react-spring not working - reactjs

I'm using react-spring to animate the transition of opening and closing an accordion component that reveals some text. Using this example on the documentation I was able to come up with a simpler version that creates a transition for the height and opacity:
function CollapseListItem({ title, text }: CollapseItemType) {
const [isOpen, setIsOpen] = useState(false);
const [ref, { height: viewHeight }] = useMeasure();
const { height, opacity } = useSpring({
from: { height: 0, opacity: 0 },
to: {
height: isOpen ? viewHeight : 0,
opacity: isOpen ? 1 : 0
}
});
const toggleOpen = () => {
setIsOpen(!isOpen);
};
return (
<>
<button onClick={toggleOpen}>
{title} click to {isOpen ? "close" : "open"}
</button>
<animated.div
ref={ref}
style={{
opacity,
height: isOpen ? "auto" : height,
overflow: "hidden"
}}
>
{text}
</animated.div>
</>
);
}
The issue is that the height transition is only being shown when you close the accordion, when you open the accordion the text suddenly appears, but on the code I can't seem to find why it only works on close, I've tried to hardcode some viewHeight values but I've had no luck.
Here's a code sandbox of what I have

After checking more examples, I realized that I was putting the ref on the wrong component, this change solve the issue:
function CollapseListItem({ title, text }: CollapseItemType) {
const [isOpen, setIsOpen] = useState(false);
const [ref, { height: viewHeight }] = useMeasure();
const props = useSpring({
height: isOpen ? viewHeight : 0,
opacity: isOpen ? 1 : 0
});
const toggleOpen = () => {
setIsOpen(!isOpen);
};
return (
<>
<button onClick={toggleOpen}>
{title} click to {isOpen ? "close" : "open"}
</button>
<animated.div
style={{
...props,
overflow: "hidden"
}}
>
<div ref={ref}>{text}</div>
</animated.div>
</>
);
}
Here's the full solution in case anyone is also trying to build an animated accordion / collapse using spring.

Related

Change background color of div by clicking in react

I have three div in the component(which can be more than three as well). I want to change their color when they will be clicked. If again I will click, they will get back their old color. In my code if I am clicking any one div, all div s are changing, Can you help me to do it for particular div?
The code is:
import React,{useState} from 'react'
export default function ChangeColor() {
let [colorState,changeState]=useState(['red','green','blue']);
let [isActive,setIsActive]=useState(true);
return (
<>
{colorState.map((color,index)=>{
return(
<React.Fragment key={index}>
<div style={{width:'100px',height:'100px',backgroundColor:isActive?`${color}`:'yellow' }}
onClick={()=>{isActive?setIsActive(false) :setIsActive(true)}}>
<p>{color}</p>
</div>
</React.Fragment>
)})
}
</>
)
}
All 3 div are changing together because only a single value is used to control the state.
To solve this, you could make isActive an object that contain a Boolean value for each color, so its structure could be something like this:
{red: true, green: false, blue: false}
This way, each of the div can set the styles based on condition like:
backgroundColor: isActive[color] ? 'yellow' : color
Full example: (live demo on stackblitz)
import React, { useState } from 'react';
export default function ChangeColor() {
const [colorState, changeState] = useState(['red', 'green', 'blue']);
const [isActive, setIsActive] = useState({});
const toggleActive = (color) =>
setIsActive((prev) => {
if (prev[color]) return { ...prev, [color]: false };
return { ...prev, [color]: true };
});
return (
<>
{colorState.map((color, index) => {
return (
<React.Fragment key={index}>
<div
style={{
width: '100px',
height: '100px',
backgroundColor: isActive[color] ? 'yellow' : color,
cursor: 'pointer',
}}
onClick={() => toggleActive(color)}
>
<p>{isActive[color] ? 'yellow' : color}</p>
</div>
</React.Fragment>
);
})}
</>
);
}

Why doesn't my Fade-out effect work with Transition component from 'react-transition-group'?

I don't know why my fade-out effect doesn't work, but at least fade-in works.
When you press the Open Modal button, the modal window is opened with the fade-in effect, but when you close the modal, it is closed without any fade-out effect.
Why does this happen?
How to fix it?
CodeSandbox: https://codesandbox.io/s/modal-window-5g2q8s?file=/src/components/Modal.js:157-937
const transitionStyles = {
entering: { opacity: 0, visibility: "hidden" },
entered: { opacity: 1, visibility: "visible" },
exiting: { opacity: 1, visibility: "visible" },
exited: { opacity: 0, visibility: "hidden" }
};
export const Modal = ({ children, isOpen, onClose }) => {
return ReactDOM.createPortal(
<Transition in={isOpen} timeout={500}>
{(state) => (
<>
<div
className={styles.modalBackground}
style={transitionStyles[state]}
onClick={onClose}
>
{null}
</div>
<div className={styles.modalWindow} style={transitionStyles[state]}>
{children}
</div>
</>
)}
</Transition>,
document.getElementById("portal")
);
};

react-spring transition is not showing when first time rendering

I am using React-spring for first time. I am trying to use transition hook on a side drawer on my page by toggling a button.
But when I am clicking on that button there is no animation as that side drawer opens instantly, but if I click second time then side drawer is closing with animation.
And also if I click that button before that drawer removed from DOM then slide from left animation is there. I can't figure it out where is the problem. Help me please. Thanks.
Here is my code:
import React, { useState } from "react";
import { useTransition, animated, config } from "react-spring";
const Transform = (props) => {
const myStyle = {
position: "fixed",
left: 0,
top: 0,
zIndex: 100,
backgroundColor: "black",
};
const [drawerIsOpen, setDrawerState] = useState(false);
const closeDrawerHandler = () => {
setDrawerState((v) => !v);
};
const transition = useTransition(drawerIsOpen, {
form: { transform: "translateX(-100%)", opacity: 0 },
enter: { transform: "translateX(0%)", opacity: 1 },
leave: { transform: "translateX(-100%)", opacity: 0 },
config: { duration: 2000 },
// config: config.molasses,
// openDrawerHandler: () => setDrawerState(true),
});
return (
<>
{transition((style, item) =>
item ? (
<animated.aside
className='bg-white h-100 w-70 shadow'
style={{ ...style, ...myStyle }}
onClick={closeDrawerHandler}
>
<nav className='h-100'>
<h2>It's a Side Drawer</h2>
</nav>
</animated.aside>
) : (
""
)
)}
<div className='d-flex justify-content-end'>
<button className='btn btn-primary ' onClick={closeDrawerHandler}>
Toggle Btn
</button>
</div>
</>
);
};
export default Transform;
Image of transition problem
I made spelling mistake. Silly one, I spelt from as form . That's why it was happening.

Avoiding framer-motion initial animations on mount

Please see this codesandbox.
I have a basic framer-motion animation where the height of a box is animated when toggled. However, I want the box to be shown by default, but when the page loads the initial animation is presented.
My question is, how do I avoid having an initial animation for a component if it should be shown on mount, but still maintain future enter and exit animations? Thanks!
I came up with this kind of solution;
1- I took variants to inside of the component
2 - I created two states for opacity and height
3 - States are initially same as where you animate to. So basically nothing happens when you first render.
4 - With useEffect, you can swap the values with the actual initial values, so after first render, the animation works.
export const AnimatedFallback = ({ isVisible }) => {
const [opacity, setOpacity] = useState(1);
const [height, setHeight] = useState("200px");
const variants = {
initial: {
opacity: opacity,
height: height
},
enter: {
opacity: 1,
height: "200px",
transition: { duration: 0.5 }
},
exit: {
opacity: 0,
height: 0,
transition: { duration: 0.5 }
}
};
useEffect(()=> {
setHeight(0)
setOpacity(0)
}, [])
return (
<AnimatePresence>
{isVisible && (
<motion.div
animate="enter"
className="fallback"
exit="exit"
initial="initial"
variants={variants}
>
Suspense Fallback Component
</motion.div>
)}
</AnimatePresence>
);
};
You can check if it is the components first render by:
const firstRender = useRef(true);
useEffect(() => {
if (firstRender.current) {
firstRender.current = false;
return;
}
});
If it is the first render of the component, don't pass the variants to the motion.div. In your example this would look like this:
const variants = {
initial: {
opacity: 0,
height: 0
},
enter: {
opacity: 1,
height: "200px",
transition: { duration: 0.5 }
},
exit: {
opacity: 0,
height: 0,
transition: { duration: 0.5 }
}
};
export const AnimatedFallback = ({ isVisible }) => {
const firstRender = useRef(true);
useEffect(() => {
if (firstRender.current) {
firstRender.current = false;
return;
}
});
return (
<AnimatePresence>
{isVisible && (
<motion.div
animate="enter"
className="fallback"
exit="exit"
initial="initial"
variants={firstRender.current ? {} : variants}
>
Suspense Fallback Component
</motion.div>
)}
</AnimatePresence>
);
};
Bit old of a question, but just in case some people stumble upon this. if you are using an AnimatePresence you can use initial={false} on the AnimatePresence component. Like so,
<AnimatePresence initial={false}>
{someCondition ? (
<motion.h1 {...yourProps}>
yooo
</motion.h1>
) : null}
</AnimatePresence>
More info here:
https://www.framer.com/docs/animate-presence/##suppressing-initial-animations
The simplest method is provided in the framer motion official docs:
specifiy the initial prop with the value as false
<motion.div
...
initial={false}
>
</motion.div>
https://www.framer.com/docs/animation/##enter-animations
For me specifying initial property in motion.div helped
<motion.div initial={{ opacity: 0, height: 0}}>
...
</motion.div>

Is it possible to animate to 100% in react-spring?

I am using react-spring to try and animate in AJAX content as it is loaded.
I have a container component that I sometimes want to animate to 'auto' from 0 and sometimes I want to animate to 100% depending on a prop that is passed in.
I have a const that I set that is then passed into a calculatedHeight property in the Transition component. I then use this to set the height property in the mounted child component's style property.
const Container = ({ data, children, stretchHeight }) => {
const loaded = data.loadStatus === 'LOADED';
const loading = data.loadStatus === 'LOADING';
const animationHeight = stretchHeight ? '100%' : 'auto';
return (
<div
className={classnames({
'data-container': true,
'is-loading': loading,
'is-loaded': loaded,
'stretch-height': stretchHeight
})}
aria-live="polite"
>
{loading &&
<div style={styles} className='data-container__spinner-wrapper'>
<LoadingSpinner />
</div>
}
<Transition
from={{ opacity: 0, calculatedHeight: 0 }}
enter={{ opacity: 1, calculatedHeight: animationHeight }}
leave={{ opacity: 0, calculatedHeight: 0 }}
config={config.slow}
>
{loaded && (styles => {
return (
<div style={{ opacity: styles.opacity, height: styles.calculatedHeight }}>
{children}
</div>
)
}
)}
</Transition>
</div>
)
}
The problem is that this causes a max callstack exceeded error as I don't think react-spring can understand the '100%' string value, only 'auto'.
Is there a work around for this?
The problem is that you switch types, you go from 0 to auto to 0%. It can interpolate auto, but that gets interpolated as a number, you're going to confuse it by mixing that number with a percentage.
PS. Maybe you can trick a little using css: https://codesandbox.io/embed/xolnko178q
Thanks to #hpalu for helping me realise what the issue was:
The problem is that you switch types, you go from 0 to auto to 0%. It
can interpolate auto, but that gets interpolated as a number, you're
going to confuse it by mixing that number with a percentage.
To resolve this I created consts for both my start and end points.
const containerHeightAnimationStart = stretchHeight ? '0%' : 0;
const containerHeightAnimationEnd = stretchHeight ? '100%' : 'auto';
I then used these in the animation:
<Transition
native
from={{ opacity: 0, height: containerHeightAnimationStart }}
enter={{ opacity: 1, height: containerHeightAnimationEnd }}
leave={{ opacity: 0, height: containerHeightAnimationStart }}
>
{loaded && (styles => {
return (
<animated.div style={styles}>
{children}
</animated.div>
)
}
)}
</Transition>
from & to need same unit (number or string)
const [percentage, setPercentage] = useState(100);
// wrong
const animationState2 = useSpring({
from:{width: 0},
to: {width: `${percentage}%`}
});
// right
const animationState2 = useSpring({
from:{width: '0%'},
to: {width: `${percentage}%`}
});

Resources