nashvail/ReactPathMenu

Update to React-motion 0.4.X

Closed this issue · 0 comments

Recently I've been trying to update your code for react-motion 0.4.3.

Everything works fine except when I try to close the menu. Here a gif to show you what I have

I've managed to see that the reverse function made my problem.

Here is my attemp to make the upgrade.

'use strict';
import React from 'react';
import ReactDOM from 'react-dom';
import {Motion, StaggeredMotion, spring} from 'react-motion';
import range from 'lodash.range';

// Components

//Constants

// Diameter of the main button in pixels
const MAIN_BUTTON_DIAM = 90;
const CHILD_BUTTON_DIAM = 48;
// The number of child buttons that fly out from the main button
const NUM_CHILDREN = 5;
// Hard code the position values of the mainButton
const M_X = 490;
const M_Y = 450;

//should be between 0 and 0.5 (its maximum value is difference between scale in finalChildButtonStyles a
// nd initialChildButtonStyles)
// const OFFSET = 0.4;
const OFFSET = 0.05; //0.05 for a better view of the reverse bug

//Version 3.1 with array
//const SPRING_CONFIG = [400, 28];
// version 4.X uses object instead
const SPRING_CONFIG = {stiffness : 400, damping : 28};

// How far away from the main button does the child buttons go
const FLY_OUT_RADIUS = 130,
    SEPARATION_ANGLE = 40, //degrees
    FAN_ANGLE = (NUM_CHILDREN - 1) * SEPARATION_ANGLE, //degrees
    BASE_ANGLE = ((180 - FAN_ANGLE)/2); // degrees

// Names of icons for each button retreived from fontAwesome, we'll add a little extra just in case
// the NUM_CHILDREN is changed to a bigger value
let childButtonIcons = ['pencil', 'at', 'camera', 'bell', 'comment', 'bolt', 'ban', 'code'];


// Utility functions

function toRadians(degrees) {
    return degrees * 0.0174533;
}

function finalChildDeltaPositions(index) {
    let angle = BASE_ANGLE + (index* SEPARATION_ANGLE);
    return {
        deltaX: FLY_OUT_RADIUS * Math.cos(toRadians(angle)) - (CHILD_BUTTON_DIAM/2),
        deltaY: FLY_OUT_RADIUS * Math.sin(toRadians(angle)) + (CHILD_BUTTON_DIAM/2)
    };
}


class APP extends React.Component {
    constructor(props) {
        super(props);

        this.state = {
            isOpen: false,
            childButtons: []
        };

        // Bind this to the functions
        this.toggleMenu = this.toggleMenu.bind(this);
        this.closeMenu = this.closeMenu.bind(this);
    }

    componentDidMount() {
        window.addEventListener('click', this.closeMenu);
        let childButtons = [];

        this.setState({childButtons: childButtons.slice(0)});
    }

    mainButtonStyles() {
        return {
            width: MAIN_BUTTON_DIAM,
            height: MAIN_BUTTON_DIAM,
            top: M_Y - (MAIN_BUTTON_DIAM/2),
            left: M_X - (MAIN_BUTTON_DIAM/2)
        };
    }

    initialChildButtonStyles() {
        return {
            width: CHILD_BUTTON_DIAM,
            height: CHILD_BUTTON_DIAM,
            top: spring(M_Y - (CHILD_BUTTON_DIAM/2), SPRING_CONFIG),
            left: spring(M_X - (CHILD_BUTTON_DIAM/2), SPRING_CONFIG),
            rotate: spring(-180, SPRING_CONFIG),
            scale: spring(0.5, SPRING_CONFIG)
        };
    }

    initialChildButtonStylesInit() {
        return {
            width: CHILD_BUTTON_DIAM,
            height: CHILD_BUTTON_DIAM,
            top: M_Y - (CHILD_BUTTON_DIAM/2),
            left: M_X - (CHILD_BUTTON_DIAM/2),
            rotate: -180,
            scale: 0.5
        };
    }

    finalChildButtonStylesInit(childIndex) {
        let {deltaX, deltaY} = finalChildDeltaPositions(childIndex);
        return {
            width: CHILD_BUTTON_DIAM,
            height: CHILD_BUTTON_DIAM,
            top: M_Y - deltaY,
            left: M_X + deltaX,
            rotate: 0,
            scale: 1
        };
    }

    finalChildButtonStyles(childIndex) {
        let {deltaX, deltaY} = finalChildDeltaPositions(childIndex);
        return {
            width: CHILD_BUTTON_DIAM,
            height: CHILD_BUTTON_DIAM,
            top: spring(M_Y - deltaY, SPRING_CONFIG),
            left: spring(M_X + deltaX, SPRING_CONFIG),
            rotate: spring(0, SPRING_CONFIG),
            scale: spring(1, SPRING_CONFIG)
        };
    }

    toggleMenu(e) {
        e.stopPropagation();
        let{isOpen} = this.state;
        this.setState({
            isOpen: !isOpen
        });
    }

    closeMenu() {
        this.setState({ isOpen: false});
    }

    renderChildButtons() {
        const {isOpen} = this.state;
        const targetButtonStylesInitObject = range(NUM_CHILDREN).map(i => {
            return isOpen ? this.finalChildButtonStylesInit(i) : this.initialChildButtonStylesInit();
        });

        //StaggeredMotion now takes an Array of object
        const targetButtonStylesInit = Object.keys(targetButtonStylesInitObject).map(key => targetButtonStylesInitObject[key]);

        const targetButtonStyles = range(NUM_CHILDREN).map(i => {
            return isOpen ? this.finalChildButtonStyles(i) : this.initialChildButtonStyles();
        });

        const scaleMin = this.initialChildButtonStyles().scale.val;
        const scaleMax = this.finalChildButtonStyles(0).scale.val;

        let calculateStylesForNextFrame = prevFrameStyles => {
             prevFrameStyles = isOpen ? prevFrameStyles : prevFrameStyles.reverse();
            //prevFrameStyles = isOpen ? prevFrameStyles : prevFrameStyles;

            let nextFrameTargetStyles =  prevFrameStyles.map((buttonStyleInPreviousFrame, i) => {
                //animation always starts from first button
                if (i === 0) {
                    return targetButtonStyles[i];
                }

                const prevButtonScale = prevFrameStyles[i - 1].scale;
                const shouldApplyTargetStyle = () => {
                    if (isOpen) {
                        return prevButtonScale >= scaleMin + OFFSET;
                    } else {
                        return prevButtonScale <= scaleMax - OFFSET;
                    }
                };

                return shouldApplyTargetStyle() ? targetButtonStyles[i] : buttonStyleInPreviousFrame;
            });

            return isOpen ? nextFrameTargetStyles : nextFrameTargetStyles.reverse();
            // return isOpen ? nextFrameTargetStyles : nextFrameTargetStyles;
        };

        return (
            <StaggeredMotion
                defaultStyles={targetButtonStylesInit}
                styles={calculateStylesForNextFrame}>
                {interpolatedStyles =>
                    <div>
                        {interpolatedStyles.map(({height, left, rotate, scale, top, width}, index) =>
                            <div
                                className="child-button"
                                key={index}
                                style={{
                                    left,
                                    height,
                                    top,
                                    transform: `rotate(${rotate}deg) scale(${scale})`,
                                    width
                                }}
                            >
                                <i className={"fa fa-" + childButtonIcons[index] + " fa-lg"}></i>
                            </div>
                        )}
                    </div>
                }
            </StaggeredMotion>
        );
    }

    render() {
        let {isOpen} = this.state;
        let mainButtonRotation = isOpen ? {rotate: spring(0, {stiffness : 500, damping : 30})} : {rotate: spring(-135, {stiffness : 500, damping : 30})};
        return (
            <div>
                {this.renderChildButtons()}
                <Motion style={mainButtonRotation}>
                    {({rotate}) =>
                        <div
                            className="main-button"
                            style={{...this.mainButtonStyles(), transform: `rotate(${rotate}deg)`}}
                            onClick={this.toggleMenu}>
                            {/*Using fa-close instead of fa-plus because fa-plus doesn't center properly*/}
                            <i className="fa fa-close fa-3x"/>
                        </div>
                    }
                </Motion>
            </div>
        );
    }
};

module.exports = APP;