简体   繁体   中英

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

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 Toastr component 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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM