React stop/start fade-out on mouseEnter and mouseLeave without Jquery - javascript

I am trying to show an error message as a toast(React Component) that will fade-out after certain seconds. However When the user hover-in the mouse on the toast while fading, the fade-out should stop and the toast should be restored to its initial state and when he hovers-out the mouse on the toast, the fade-out should start again. It can be achieved by using JQuery like this -
//function to start the fade-out after time - t sec
static fadeOutToast(id, t) {
let toast = document.getElementById(id);
if (toast) {
setTimeout(() => {
FadeAndRemove('#' + id);
}, t * 1000);
}
}
/**
* t1 - time for fadeout animation
*/
static FadeAndRemove(id,t1) {
jQuery(id).fadeOut(t1 * 1000, function () {
jQuery(this).hide();
});
handleReAppear(id);
}
static handleReAppear(id) {
jQuery(id).on("mouseover", function (e) {
jQuery(this).stop(true).fadeIn(0);
});
jQuery(id).on("mouseleave", function (e) {
FadeAndRemove(this);
});
}
Its working perfectly fine. However due to projects constraints I am not supposed to mixup Jquery and react.
I tried to achieve it by manipulating the CSS opacity on mouseEnter and mouseLeave events. The problem I face is the toast never goes away from the page using opacity. Is there any way in which we can detect when the opacity of the toast becomes 0 so that I can remove it from the page just when the opacity becomes 0 ?
Can someone help me in achieving the same without using Jquery ?

For the fading animation I would use React-Spring. With a Spring you can delay the start animation so it will fade-out after the delay.
Then you can add onMouseEnter and onMouseLeave event handler to detect the hovering of the toastr.
With this mouse detection you can toggle the to value of the Spring to opacity 1. That way it won't fade-out if the mouse is over the toast.
For the removal of the toastr you can use onRest of Spring and check if opacity is zero. onRest will be called as soon as the animation will end.
The state management is done inside Toastrcomponent which will render all displayed toasts. This component will also handle the removal of the toast with no opacity.
For click event addToast I'm using a higher order component withToastr so I can add the prop of to the containing component.
For event handling I'm using Eventemitter3. If you're using Redux you could also use it to trigger the toasts.
In the next sections I'll give some details to every component that I've created in the following Codesandbox. (Note: The snippets here are not running - for testing the code please have a look at the sandbox)
ToastrItem component
Responsible for rendering a toast and for the animation.
import React, { PureComponent } from "react";
import { Spring } from "react-spring";
import styled from "styled-components";
import PropTypes from "prop-types";
class ToastrItem extends PureComponent {
static propTypes = {
id: PropTypes.string,
timeout: PropTypes.number,
destroy: PropTypes.func
};
static defaultProps = {
timeout: 5000
};
state = {
hovered: false
};
handleRest = ({ opacity }) => {
if (opacity === 0) {
this.props.destroy(this.props.id);
}
};
handleMouseEnter = () => {
this.setState({
hovered: true
});
};
handleMouseLeave = () => {
this.setState({
hovered: false
});
};
render() {
const { message, index, timeout } = this.props;
const { hovered } = this.state;
return (
<Spring
config={{ duration: 600, delay: timeout }}
from={{ opacity: 1.0 }}
to={{ opacity: hovered ? 1.0 : 0 }}
onRest={this.handleRest}
>
{interpolated => (
<Wrapper>
<ToastBox
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
pos={index}
opacity={interpolated.opacity}
>
{message}
{/*- debug info: {JSON.stringify(interpolated)}*/}
</ToastBox>
</Wrapper>
)}
</Spring>
);
}
}
const Wrapper = styled.div`
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
pointer-events: none;
z-index: 100;
`;
const ToastBox = styled.div.attrs(props => ({
style: {
transform: `translateY(${props.pos * 80}px)`,
opacity: props.opacity
}
}))`
width: 60%;
height: 50px;
line-height: 50px;
margin: 0 auto;
color: white;
padding: 10px;
background: rgba(0, 0, 0, 0.8);
text-align: center;
box-shadow: 3px 3px 5px rgba(0, 0, 0, 0.5);
border-radius: 10px;
pointer-events: auto;
`;
export default ToastrItem;
The Spring is doing the animation as mentioned before. The mouse events enter/leave are setting local state hovered so we can change the animation end opacity - this will avoid the animation.
I've also tried reset prop from React-Spring but that wasn't working as expected.
Toastr component
This component is managing the active toasts. Nothing special here. It's rendering the toasts array that are added with addToast.
addToast is creating a relatively unique key with timestamp and array index. It's needed so React is getting a key prop on the component. We could also use a uuid library here but I think the timestamp-id is OK.
destroy will be called if opacity is 0 then it's filter by key and update the state. The map is just there so we're updating the positions of the toasts.
class Toastr extends PureComponent {
state = {
toasts: []
};
addToast = (message, config) => {
const index = this.state.toasts.length;
const id = `toastr-${Date.now()}-${index}`;
const ToastComponent = (
<ToastrItem
key={id}
id={id}
index={index}
message={message}
timeout={config.timeout || 3000}
destroy={this.destroy}
/>
);
this.setState(state => ({
toasts: [...state.toasts, ToastComponent]
}));
};
destroy = id => {
this.setState(state => ({
toasts: [
...state.toasts
.filter(toast => toast.key !== id)
.map((toast, index) => ({
// map for updating index
...toast,
props: {
...toast.props,
index: index
}
}))
]
}));
};
componentDidMount() {
emitter.on("add/toastr", this.addToast);
}
render() {
const { toasts } = this.state;
return toasts;
}
}
export const withToastr = WrappedComponent => {
return class extends PureComponent {
render() {
return <WrappedComponent addToast={actions.add} />;
}
};
};
Usage in the app
We're adding addToast by using withToastr(App). This will add the prop addToastr to the App component.
Then we're rendering the Toastr component that will manage & render our toasts.
Finally we add a button so we can trigger the toasts.
class App extends Component {
toastr;
render() {
const { addToast } = this.props;
return (
<div className="App">
<Toastr />
<button onClick={() => addToast("Hello", { timeout: 4000 })}>
Show toast
</button>
</div>
);
}
}
const rootElement = document.getElementById("root");
const AppWithToasts = withToastr(App);
ReactDOM.render(<AppWithToasts />, rootElement);
Conclusion
The code is working but I would add native prop to the Spring and I would also check if a transition would be a better fit for the use-case. See the example from MessageHub example from React-spring docs. Should be also possible to prevent the fade-out but I haven't checked.

You might want to think about using the Animatable library. It uses a declarative syntax that's quite easy to incorporate.
import * from 'react-native-animatable';
return(
<Animatable.View animation="fadeOut" duration={2000} delay={1000}>
<View>
{/* YOUR CONTENT */}
</View>
</Animatable.View>
);
https://github.com/oblador/react-native-animatable

Related

How to animate child prop changes in react?

I'm trying to create a component that takes a child prop and when this child prop changes I'd like the old component that was the child prop to animate out of screen and the new component to animate in. I attempted to do this using react-spring using the code below, you can run it here https://codesandbox.io/s/react-spring-issue-898-forked-ril7mc?file=/src/App.js.
What am I missing here? Shouldn't the component animate left when it unmounts and animate in from the right when it mounts?
/** #jsx jsx */
import { useState } from "react";
import { useTransition, animated } from "react-spring";
import { jsx } from "#emotion/core";
function FadeTransition({ children, onClick }) {
const [toggle, setToggle] = useState(true);
const transition = useTransition(toggle, {
from: {
opacity: 0,
transform: "translateX(100%)"
},
enter: {
opacity: 1,
transform: "translateX(0)"
},
leave: { transform: "translateX(-100%)" }
});
return (
<div>
{transition(
(props, item) =>
item && <animated.div style={props}>{children}</animated.div>
)}
<button
onClick={() => {
setToggle(!toggle);
onClick();
}}
style={{ marginTop: "2rem" }}
>
Change
</button>
</div>
);
}
export default function App() {
const [currentComponent, setCurrentComponent] = useState(0);
let components = [<div>Thing 1</div>, <div>Thing 2</div>];
const onClick = () => {
setCurrentComponent((currentComponent + 1) % 2);
};
return (
<div style={{ paddingTop: "2rem" }}>
<FadeTransition onClick={onClick}>
{components[currentComponent]}
</FadeTransition>
</div>
);
}
I didn't use rect-sprint as much,There is issue with toggle state so when thing 1 is rendered animation component (FadeTransition) is rendered with setting up toggle state to true.
Now when you click on change button we are setting up the toggle button to opposite of current state which will be false this time and without unmounting animation component (FadeTransition) it will update dom and so the item which is thing 2.
As we are passing toggle in useTransition and if it's values will be false then animation will not be performed.
So I have updated snippet of yours, Here is the working example codesandbox

How can I force React to re-render all components on a page that come from array.map?

I have a parent component (Game) that renders children (Card) from an array. I also have a Menu component that does a callback to Game to change its state. When I change levels (button click from Menu), I want all current cards to fade out, then fade in with the new ones, so they mimic the fade-in CSS applied to newly rendered Cards. I've tried forceUpdate in a couple places, and passing dummy props to check for a change. Basicly, I want all cards currently on the page to re-render while the page renders new ones when needed.
Here is a snippet from Game:
{this._gameArray.map( (item, i) => {
let dummyProp = Math.random();
return <Card key={i}
...
dummyProp={dummyProp}/>
})}
And a snippet from Card:
componentWillReceiveProps(nextProps) {
...
if (nextProps.dummyProp !== this.props.dummyProps) {
this.forceUpdate();
}
}
I'm using componentWillReceiveProps for another purpose, and decided to try it for testing the dummyProp change. The new Cards fade in, but the ones before do not do anything.
React has some algorithms to define which components should be re-rendered, in your case React won't re-render the component because you use indexes as keys and indexes are the same. You need to update key between re-renderings.
As the simplest solution you can use the index + some trail in a key, something like this:
<Card key={index + '-' + Date.now()} card={card}/>;
In this case, the <Card/> component will be re-rendered every time when the state of the <Game/> component has changed.
class Card extends React.Component {
constructor(props) {
super(props)
}
render() {
return (
<div class="card">Card {this.props.card.name}</div>
)
}
}
class Game extends React.Component {
constructor(props) {
super(props)
this.state = {
counter: 0,
cards: [
{ name: 'Card1' },
]
}
}
increaseCounter = () => {
this.setState({ counter: this.state.counter + 1 })
}
render() {
return (
<div>
<h2 onClick={this.increaseCounter}>Click me: {this.state.counter}</h2>
<h3>Will be re-rendered</h3>
{this.state.cards.map((card, index) => {
return <Card key={index + '-' + Date.now()} card={card} />;
})}
<h3>Won't be re-rendered</h3>
{this.state.cards.map((card, index) => {
return <Card key={index} card={card} />;
})}
</div>
)
}
}
ReactDOM.render(<Game />, document.querySelector("#app"))
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
transition: all 0.2s;
}
h2 {
font-weight: bold;
margin-bottom: 15px;
}
.card{
animation: fade-in-out 3000ms ease-in-out forwards;
}
#keyframes fade-in-out {
0% {
background-color: transparent;
}
50% {
background-color: red;
}
100%{
background-color: transparent;
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="app"></div>
BUT, for real-life application, I would recommend you to consider the using of some React animation library like react-transition-group, because if you animate cards only with CSS, there is can be some flashing if re-rendering will happen faster than animation timeout.
You can simply try a different trick.
If you want all the components to re-render each time a level changes, you can use a boolean, set it to false when you start the change, and set it to true when you finish the change / after some time.
Then add a condition to the rendering of the elements.
{shouldRender && this._gameArray.map( (item, i) => {
let dummyProp = Math.random();
return <Card key={i}
...
dummyProp={dummyProp}/>
})}
when shouldRender is true the cards will be rendered, and when it's false they won't. That way each time it will be true they will all re-rendered and you'll have you effect.

Trigger CSS Transition on component prop changes

I want to fade in/out a component whenever its prop changes. Some basic examples I've found achieve this via a boolean condition that toggles the element's styles, but I want to update the toggle state via a prop change.
For example, in my code sandbox, I have a few buttons that when clicked, will update the text on screen. I want to be able to fade in/out this text whenever the user clicks on a different option.
class Example extends Component {
constructor(props) {
super(props);
this.state = {
current: "dog",
isVisible: false
};
}
componentDidMount() {
this.setState({ isVisible: true });
}
handleOnClick = option => {
this.setState({ current: option });
};
// only used to test css transition
forceToggleState = () => {
this.setState(prevState => ({ isVisible: !prevState.isVisible }));
};
render() {
const options = ["dog", "cat", "rabbit"];
return (
<div>
{options.map((option, index) => (
<button onClick={() => this.handleOnClick(option)} key={index}>
{option}
</button>
))}
<div style={this.state.isVisible ? styles.toggleIn : styles.toggleOut}>
{this.state.current}
</div>
<button onClick={this.forceToggleState}>toggle me</button>
</div>
);
}
}
const styles = {
toggleIn: {
opacity: 1,
transition: "all 0.5s ease",
border: "1px solid red"
},
toggleOut: {
opacity: 0,
transition: "all 0.5s ease-in"
}
};
current sandbox
Solution: https://codesandbox.io/s/react-example-lyf3f
I made a few edits to your sandbox to achieve the desired functionality.
Move options array into state
Pass this.state.current as a prop to an Option component
Create a toggleIn animation using an #keyframes to create the desired animation
Set the key prop equal to this.props.current in the Option component to make sure the DOM element remounts any time the this.props.current changes. This will ensure our animation runs each time the this.props.current changes.

React w Gatsby: implemented sequential fade-in animation for gallery but it doesn't work right after being deployed

The source code is here: https://codesandbox.io/s/gatsby-starter-default-nvhl7
And the deployed site is here: https://csb-nvhl7-24q4bchuz.now.sh/
The effect I am trying to achieve is simple and straightforward.
First I used this query to load all the image files from the images folder
const data = useStaticQuery(graphql`
query {
allFile(
filter: {
extension: { regex: "/(jpg)|(jpeg)|(png)/" }
sourceInstanceName: { eq: "images" }
}
) {
edges {
node {
childImageSharp {
fluid(maxWidth: 800, quality: 95) {
aspectRatio
src
srcSet
originalName
srcWebp
srcSetWebp
sizes
}
}
}
}
}
}
Then I have a gallery component to display them by dividing these images into three groups, and we can use project1, project2 and project3 to navigate between them.
const Gallery = ({ minWidth }) => {
let refs = {}
const allPics = Image().map(({ childImageSharp }, i) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
refs[i] = useRef(null)
childImageSharp.index = i
return childImageSharp
})
const firsload = allPics.slice(0, 5)
const secload = allPics.slice(5, 10)
const third = allPics.slice(10)
const [imgs, setImgs] = useState(firsload)
const thumbnails = imgs.map(img => img.fluid.srcSet.split(" ")[0])
return (
<>
<ProjectsContainer>
<Project
onClick={() => {
setImgs(firsload)
}}
>
Project 1.
</Project>
<Project
onClick={() => {
setImgs(secload)
}}
>
Project 2.
</Project>
<Project
onClick={() => {
setImgs(third)
}}
>
Project 3.
</Project>
</ProjectsContainer>
<Mansory gap={"0em"} minWidth={minWidth}>
{imgs.map((img, i) => {
return (
<PicContainer key={img.index}>
<Enlarger
src={thumbnails[i]}
enlargedSrc={img.fluid.src}
index={img.index}
orderIndex={i}
onLoad={() => {
refs[img.index].current.toggleOpacity(1) <-- use ref to keep track of every Enlarger
}}
ref={refs[img.index]}
/>
</PicContainer>
)
})}
</Mansory>
</>
)
}
For every Enlarger that gets rendered by the Gallery, they are a zoom image component
import Img from "react-image-enlarger"
class Enlarger extends React.Component {
state = { zoomed: false, opacity: 0 } <--- initially every image's opacity is 0, then it shows up by being toggled opacity 1
toggleOpacity = o => {
this.setState({ opacity: o })
}
render() {
const { index, orderIndex, src, enlargedSrc, onLoad } = this.props
return (
<div style={{ margin: "0.25rem" }} onLoad={onLoad}> <-- where we toggle the opacity when the element is onloaded
<Img
style={{
opacity: this.state.opacity,
transition: "opacity 0.5s cubic-bezier(0.25,0.46,0.45,0.94)",
transitionDelay: `${orderIndex * 0.07}s`,
}}
zoomed={this.state.zoomed}
src={src}
enlargedSrc={enlargedSrc}
onClick={() => {
this.setState({ zoomed: true })
}}
onRequestClose={() => {
this.setState({ zoomed: false })
}}
/>
</div>
)
}
}
And I have made it clear in the code snippet where I implemented the sequential fade-in animation by using ref to control every Enlarger's toggleOpacity method.
This code works great on localhost, i.e. during development. You can try the codesandbox link above to see it. However the bug appears only when the page is deployed.. Click on the deployed version (https://csb-nvhl7-24q4bchuz.now.sh/) you can see that when browser first loading the page, some pictures are missing, because their opacity are still 0, which I suspect is because refs[img.index].current.toggleOpacity(1) somehow didn't get called on the image when they are onloaded. However the weird thing is, when you navigate between the projects, there's no problems at all with this animation and opacity change, they are normal. And when you refresh the page, the problem shows up again.
I been struggling with this problem for days and couldn't figure out why. Also although here I used ZEIT Now to deploy the site, the problem didn't go away when I used Netlify to deploy.
Its not because of deployment, in Production Your images taking more time to load. See the image network call in the browser
In addition to Kishore's answer, you may want make your query after the page loaded (maybe in componentDidMount) and use setState to trigger react to re-render once that content has been fully loaded.

create "TouchableOpacity" for ReactJS without using react-native-web

I'd like to create a wrapper component that will behave like react- natives <TouchableOpacity/> without using react-native-web.
I'm thinking something that manages state and using inline styles that transitions from .5 to 1 for opacity with a setInterval or should it be a css transition.
Is there anything already out there?
What's the best approach for this?
You can achieve this affect with css transitions and using state to track when the user is clicking the button:
class App extends React.Component {
state = {
touched: false
}
toggleTouched = () => {
this.setState( prevState => ({
touched: !prevState.touched
}));
}
handleMouseUp = () => {
// Handle smooth animation when clicking without holding
setTimeout( () => {
this.setState({ touched: false });
}, 150);
}
render () {
const { touched } = this.state;
const className = touched ? 'btn touched' : 'btn';
return (
<button
className={className}
onMouseDown={this.toggleTouched}
onMouseUp={this.handleMouseUp}
>
Touch Here
</button>
)
}
}
ReactDOM.render(<App/>, document.getElementById('root'));
.btn {
background: #ededed;
padding: 10px 15px;
border: none;
outline: none;
cursor: pointer;
font-size: 15px;
opacity: 1;
transition: opacity 300ms ease;
}
.touched {
opacity: 0.5;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
The answer from #ChaseDeAnda is really great. I have made my typescript component version of it
TouchableOpacity.tsx
import React from 'react'
import classNames from 'classnames'
import styles from './index.module.scss'
export default function TouchableOpacity({ className, children, ...rest }: TTouchableOpacityProps) {
const [touched, touchedSet] = React.useState(false)
return (
<button
className={classNames(styles.container, className)}
style={{ opacity: touched ? 0.5 : 1, transition: 'opacity 300ms ease' }}
onMouseDown={() => touchedSet(true)}
onMouseUp={() => touchedSet(false)}
{...rest}>
{children}
</button>
)
}
types.d.ts
type TTouchableOpacityProps = React.ButtonHTMLAttributes<HTMLButtonElement>
(you don't have to use the 'classnames` package - string interpolation is just fine)

Categories

Resources