├── .babelrc ├── .codeclimate.yml ├── .eslintrc ├── .flowconfig ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── docs ├── bg.png ├── dist │ ├── .keep │ └── bundle.js ├── index.html ├── logo.png ├── screenshot.gif └── src │ ├── example.js │ └── index.js ├── karma.conf.js ├── lib └── .keep ├── logo.png ├── package.json ├── src ├── button.js ├── index.js └── item.js └── test ├── test-button.js ├── test-item.js └── test-menu.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015", "stage-2"], 3 | } -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | languages: 2 | Ruby: true 3 | JavaScript: true 4 | PHP: true 5 | Python: true 6 | exclude_paths: 7 | - example/* 8 | - example/**/* 9 | - karma.conf.js 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "parser": "babel-eslint", 4 | "rules": { 5 | "react/jsx-filename-extension": [0, { "extensions": [".js", ".jsx"] }], 6 | "react/prop-types": [0] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/.* 3 | 4 | [include] 5 | 6 | [libs] 7 | ./decls 8 | 9 | [options] 10 | esproposal.class_static_fields=enable 11 | suppress_comment= \\(.\\|\n\\)*\\flow-disable-line 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | example/dist/bundle.js 3 | lib/ 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - '7' 5 | 6 | addons: 7 | apt: 8 | packages: 9 | - xvfb 10 | 11 | before_install: 12 | - export DISPLAY=':99.0' 13 | - Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & 14 | 15 | install: 16 | - npm i 17 | 18 | script: 19 | - 'npm test' 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 20154 bokuweb 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecated 2 | 3 | Deprecated as the react-motion-menu project is no longer maintained. 4 | 5 | --- 6 | 7 | # react-motion-menu 8 | 9 | Animation menu component for React. 10 | 11 |

12 | Build Status 13 | 14 | Build Status 15 |

16 | 17 | ## Demo 18 | 19 | ![screenshot](https://github.com/bokuweb/react-motion-menu/blob/master/docs/screenshot.gif?raw=true) 20 | 21 | See demo: [http://bokuweb.github.io/react-motion-menu/](http://bokuweb.github.io/react-motion-menu/) 22 | 23 | 24 | ## Installation 25 | 26 | ```sh 27 | npm i react-motion-menu 28 | ``` 29 | 30 | ## Overview 31 | 32 | ### Basic 33 | 34 | ``` javascript 35 | import React from 'react'; 36 | import MotionMenu from '../../src'; 37 | 38 | export default () => ( 39 | 43 |
44 | 45 |
46 |
47 | 48 |
49 |
50 | 51 |
52 |
53 | 54 |
55 |
56 | ); 57 | ``` 58 | 59 | ## Properties 60 | 61 | 62 | #### `x: PropTypes.number` 63 | 64 | The position `x` of the menu button. 65 | If ommited, set 0. 66 | 67 | #### `y: PropTypes.number` 68 | 69 | The position `y` of the menu button. 70 | If ommited, set 0. 71 | 72 | #### `type: PropTypes.oneOf(['vertical', 'horizontal', 'circle'])` 73 | 74 | The Menu opening and closing type. 75 | Please set `horizontal`, `vertical`, `circle`. 76 | 77 | #### `margin: PropTypes.number` 78 | 79 | The `margin` between items or menu button. 80 | 81 | #### `wing: PropTypes.bool` 82 | 83 | If set `true`, menu opened both side, when `vertical` or `horizontal` type selected. 84 | 85 | #### `bumpy: PropTypes.bool` 86 | 87 | This prop controls if the menu items should open in bumpy mode or in smooth mode. 88 | Default mode is set to bumpy effect. 89 | 90 | #### `openSpeed: PropTypes.number` 91 | 92 | This prop controls how fast the menu items should open. Default speed is set to 60 milliseconds. 93 | 94 | #### `reverse: PropTypes.bool` 95 | 96 | This prop controls if the menu should open in reverse direction or not. 97 | 98 | ## Test 99 | 100 | ``` sh 101 | npm t 102 | ``` 103 | 104 | ## License 105 | 106 | The MIT License (MIT) 107 | 108 | Copyright (c) 2016 @bokuweb 109 | 110 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 111 | 112 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 113 | 114 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 115 | -------------------------------------------------------------------------------- /docs/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bokuweb-sandbox/react-motion-menu/4a9723a7c041874531bbe9a95828e70aa913321f/docs/bg.png -------------------------------------------------------------------------------- /docs/dist/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bokuweb-sandbox/react-motion-menu/4a9723a7c041874531bbe9a95828e70aa913321f/docs/dist/.keep -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | example 6 | 7 | 8 | 9 | 10 | 54 | 55 | 56 |
57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bokuweb-sandbox/react-motion-menu/4a9723a7c041874531bbe9a95828e70aa913321f/docs/logo.png -------------------------------------------------------------------------------- /docs/screenshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bokuweb-sandbox/react-motion-menu/4a9723a7c041874531bbe9a95828e70aa913321f/docs/screenshot.gif -------------------------------------------------------------------------------- /docs/src/example.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MotionMenu from '../../src'; 3 | 4 | export default () => ( 5 | console.log('onOpen')} 15 | onClose={() => console.log('onClose')} 16 | > 17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | ); 27 | -------------------------------------------------------------------------------- /docs/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import Example from './example'; 4 | 5 | render(, document.querySelector('.content')); 6 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = (config) => { 2 | config.set({ 3 | 4 | basePath: '', 5 | 6 | frameworks: ['mocha', 'browserify'], 7 | 8 | files: [ 9 | 'test/**/*.js', 10 | ], 11 | 12 | // list of files to exclude 13 | exclude: [ 14 | ], 15 | 16 | browserify: { 17 | debug: true, 18 | extensions: ['.js'], 19 | transform: [ 20 | require('babelify').configure({ 21 | plugins: ['babel-plugin-espower'], 22 | }), 23 | ], 24 | configure: (bundle) => { 25 | bundle.on('prebundle', () => { 26 | bundle.external('react/addons'); 27 | bundle.external('react/lib/ReactContext'); 28 | bundle.external('react/lib/ExecutionEnvironment'); 29 | }); 30 | }, 31 | }, 32 | 33 | // preprocess matching files before serving them to the browser 34 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 35 | preprocessors: { 36 | 'test/**/*.js': ['browserify'], 37 | }, 38 | 39 | 40 | // test results reporter to use 41 | // possible values: 'dots', 'progress' 42 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 43 | reporters: ['progress'], 44 | 45 | 46 | // web server port 47 | port: 9876, 48 | 49 | 50 | // enable / disable colors in the output (reporters and logs) 51 | colors: true, 52 | 53 | // level of logging 54 | logLevel: config.LOG_INFO, 55 | 56 | 57 | // enable / disable watching file and executing tests whenever any file changes 58 | autoWatch: false, 59 | 60 | 61 | // start these browsers 62 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 63 | browsers: ['Nightmare'], 64 | 65 | nightmareOptions: { 66 | width: '800px', 67 | height: '600px', 68 | }, 69 | 70 | // Continuous Integration mode 71 | // if true, Karma captures browsers, runs the tests and exits 72 | singleRun: true, 73 | }); 74 | }; 75 | -------------------------------------------------------------------------------- /lib/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bokuweb-sandbox/react-motion-menu/4a9723a7c041874531bbe9a95828e70aa913321f/lib/.keep -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bokuweb-sandbox/react-motion-menu/4a9723a7c041874531bbe9a95828e70aa913321f/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-motion-menu", 3 | "version": "0.4.4", 4 | "main": "lib/index.js", 5 | "scripts": { 6 | "watch:example": "watchify --extension=js -o docs/dist/bundle.js docs/src/index.js", 7 | "compile": "babel --presets react -d lib/ src/", 8 | "build:example": "browserify --extension=js -o docs/dist/bundle.js docs/src/index.js", 9 | "lint": "eslint ./src", 10 | "styleguide": "styleguidist build", 11 | "test:watch": "karma start --auto-watch --no-single-run", 12 | "test": "npm run lint && karma start" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/bokuweb/react-motion-menu.git" 17 | }, 18 | "keywords": [ 19 | "react", 20 | "menu", 21 | "motion", 22 | "component", 23 | "animation" 24 | ], 25 | "author": "bokuweb", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/bokuweb/react-motion-menu/issues" 29 | }, 30 | "homepage": "https://github.com/bokuweb/react-motion-menu", 31 | "peerDependencies": { 32 | "react": ">=15", 33 | "react-dom": ">=15" 34 | }, 35 | "devDependencies": { 36 | "babel-cli": "^6.18.0", 37 | "babel-eslint": "^7.1.1", 38 | "babel-plugin-espower": "2.3.1", 39 | "babel-polyfill": "^6.20.0", 40 | "babel-preset-es2015": "^6.18.0", 41 | "babel-preset-react": "^6.16.0", 42 | "babel-preset-stage-2": "^6.18.0", 43 | "babelify": "7.3.0", 44 | "browserify": "^13.3.0", 45 | "enzyme": "^2.7.0", 46 | "eslint": "^3.13.0", 47 | "eslint-config-airbnb": "^13.0.0", 48 | "eslint-plugin-import": "^2.1.0", 49 | "eslint-plugin-jsx-a11y": "^2.2.3", 50 | "eslint-plugin-react": "^6.6.0", 51 | "flow-bin": "^0.37.4", 52 | "karma": "^1.3.0", 53 | "karma-browserify": "5.1.0", 54 | "karma-cli": "^1.0.1", 55 | "karma-mocha": "1.3.0", 56 | "karma-nightmare": "^0.2.7", 57 | "mocha": "3.2.0", 58 | "power-assert": "1.4.2", 59 | "react": ">=15.4.1", 60 | "react-addons-test-utils": "^15.4.1", 61 | "react-dom": ">=15.4.1", 62 | "react-test-renderer": "^15.5.4", 63 | "sinon": "^1.17.7", 64 | "watchify": "3.8.0" 65 | }, 66 | "browserify": { 67 | "transform": [ 68 | "babelify" 69 | ] 70 | }, 71 | "dependencies": { 72 | "prop-types": "^15.5.10", 73 | "react-motion": "^0.4.7" 74 | }, 75 | "files": [ 76 | "lib" 77 | ] 78 | } 79 | -------------------------------------------------------------------------------- /src/button.js: -------------------------------------------------------------------------------- 1 | import React, { Component, cloneElement } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Motion, spring } from 'react-motion'; 4 | 5 | export default class MenuButton extends Component { 6 | 7 | static propTypes = { 8 | x: PropTypes.number.isRequired, 9 | y: PropTypes.number.isRequired, 10 | onClick: PropTypes.func, 11 | bumpy: PropTypes.bool, 12 | }; 13 | 14 | constructor(props) { 15 | super(props); 16 | this.state = { 17 | sequence: 0, 18 | }; 19 | this.sequenceParams = this.props.bumpy ? 20 | [ 21 | { 22 | scaleX: spring(1, { stiffness: 1500, damping: 10 }), 23 | scaleY: spring(1, { stiffness: 1500, damping: 10 }), 24 | }, { 25 | scaleX: spring(0.6, { stiffness: 1500, damping: 50 }), 26 | scaleY: spring(0.6, { stiffness: 1500, damping: 50 }), 27 | }, { 28 | scaleX: spring(1, { stiffness: 1500, damping: 10 }), 29 | scaleY: spring(1, { stiffness: 1500, damping: 10 }), 30 | }, 31 | ] : 32 | [ 33 | { 34 | scaleX: spring(1, { stiffness: 1500, damping: 10 }), 35 | scaleY: spring(1, { stiffness: 1500, damping: 10 }), 36 | }, { 37 | scaleX: spring(1, { stiffness: 200, damping: 50 }), 38 | scaleY: spring(1, { stiffness: 200, damping: 50 }), 39 | }, { 40 | scaleX: spring(1, { stiffness: 1500, damping: 10 }), 41 | scaleY: spring(1, { stiffness: 1500, damping: 10 }), 42 | }, 43 | ]; 44 | } 45 | 46 | start() { 47 | setTimeout(() => this.setState({ sequence: 1 }), 100); 48 | setTimeout(() => this.setState({ sequence: 2 }), 150); 49 | } 50 | 51 | reverse() { 52 | this.setState({ sequence: 1 }); 53 | setTimeout(() => this.setState({ sequence: 0 }), 50); 54 | } 55 | 56 | render() { 57 | const { x, y, onClick } = this.props; 58 | if (!this.props.children) return null; 59 | return ( 60 | 61 | {({ scaleX, scaleY }) => ( 62 | cloneElement( 63 | this.props.children, 64 | { 65 | ...(this.props.children.props || {}), 66 | onClick, 67 | style: { 68 | ...((this.props.children.props && this.props.children.props.style) || {}), 69 | transform: `translate3d(${x}px, ${y}px, 0) scaleX(${scaleX}) scaleY(${scaleY})`, 70 | WebkitTransform: `translate3d(${x}px, ${y}px, 0) scaleX(${scaleX}) scaleY(${scaleY})`, 71 | position: 'absolute', 72 | }, 73 | }, 74 | ) 75 | ) 76 | } 77 | 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import MenuItem from './item'; 4 | import MenuButton from './button'; 5 | 6 | export default class MotionMenu extends Component { 7 | 8 | static propTypes = { 9 | margin: PropTypes.number.isRequired, 10 | type: PropTypes.oneOf(['horizontal', 'vertical', 'circle']).isRequired, 11 | wing: PropTypes.bool, 12 | x: PropTypes.number, 13 | y: PropTypes.number, 14 | onClose: PropTypes.func, 15 | onOpen: PropTypes.func, 16 | className: PropTypes.string, 17 | bumpy: PropTypes.bool, 18 | openSpeed: PropTypes.number, 19 | reverse: PropTypes.bool, 20 | } 21 | 22 | static defaultProps = { 23 | x: 0, 24 | y: 0, 25 | style: {}, 26 | onClose: () => {}, 27 | onOpen: () => {}, 28 | bumpy: true, 29 | openSpeed: 60, 30 | reverse: false, 31 | } 32 | 33 | constructor(props) { 34 | super(props); 35 | this.state = { 36 | itemNumber: 1, 37 | status: 'idle', 38 | }; 39 | this.items = []; 40 | this.onOpenEnd = this.onOpenEnd.bind(this); 41 | this.onCloseEnd = this.onCloseEnd.bind(this); 42 | this.onClick = this.onClick.bind(this); 43 | } 44 | 45 | onOpenEnd(name) { 46 | if (this.state.action !== 'open') return; 47 | if (this.state.itemNumber < this.props.children.length) { 48 | this.items[this.state.itemNumber].start(); 49 | this.setState({ 50 | itemNumber: this.state.itemNumber + 1, 51 | }); 52 | return; 53 | } 54 | if (name === `item${this.props.children.length - 1}`) { 55 | this.props.onOpen(); 56 | } 57 | } 58 | 59 | onCloseEnd(name) { 60 | if (this.state.action === 'open') return; 61 | if (name === 'item1') { 62 | this.props.onClose(); 63 | } 64 | if (this.state.itemNumber > 1) { 65 | if (name === 'item1') { 66 | this.props.onClose(); 67 | } 68 | this.setState({ 69 | itemNumber: this.state.itemNumber - 1, 70 | }); 71 | } 72 | } 73 | 74 | onClick() { 75 | if (this.state.action === 'open') { 76 | this.closeItems(); 77 | } else { 78 | this.openItem(); 79 | } 80 | } 81 | 82 | getDistance(i) { 83 | return this.props.wing 84 | ? (parseInt(i / 2, 10) + 1) * this.props.margin * ((i % 2) || -1) 85 | : this.props.margin * (i + 1); 86 | } 87 | 88 | getX(i, x) { 89 | const { type, margin, children } = this.props; 90 | if (type === 'horizontal') { 91 | return this.getDistance(i) + x; 92 | } 93 | if (type === 'circle') { 94 | return x + (margin * Math.cos((Math.PI * 2 * i) / (children.length - 1))); 95 | } 96 | return x; 97 | } 98 | 99 | getY(i, y) { 100 | const { type, margin, children } = this.props; 101 | if (type === 'vertical') { 102 | return this.getDistance(i) + y; 103 | } 104 | if (type === 'circle') { 105 | return y + (margin * Math.sin((Math.PI * 2 * i) / (children.length - 1))); 106 | } 107 | return y; 108 | } 109 | 110 | getItems() { 111 | const { x, y, bumpy } = this.props; 112 | return Array.from(Array(this.state.itemNumber).keys()) 113 | .reverse() 114 | .map(i => ( 115 | { this.items[i + 1] = c; }} 118 | name={`item${i + 1}`} 119 | onOpenAnimationEnd={this.onOpenEnd} 120 | onCloseAnimationEnd={this.onCloseEnd} 121 | x={this.getX(i, x)} 122 | y={this.getY(i, y)} 123 | bumpy={bumpy} 124 | openSpeed={this.props.openSpeed} 125 | reverse={this.props.reverse} 126 | type={this.props.type} 127 | > 128 | {this.props.children[i + 1]} 129 | 130 | ), 131 | ); 132 | } 133 | 134 | get menuButton() { 135 | return ( 136 | { this.button = c; }} 138 | onClick={this.onClick} 139 | x={this.props.x} 140 | y={this.props.y} 141 | bumpy={this.props.bumpy} 142 | > 143 | {this.props.children[0]} 144 | 145 | ); 146 | } 147 | 148 | closeItems() { 149 | this.setState({ action: 'close' }); 150 | this.button.reverse(); 151 | Array.from(Array(this.state.itemNumber).keys()) 152 | .reverse() 153 | .forEach(i => this.items[i + 1].reverse()); 154 | } 155 | 156 | close() { 157 | if (this.state.action !== 'open') return; 158 | this.closeItems(); 159 | } 160 | 161 | open() { 162 | if (this.state.action === 'open') return; 163 | this.openItem(); 164 | } 165 | 166 | openItem() { 167 | this.setState({ action: 'open' }); 168 | this.button.start(); 169 | this.items[this.state.itemNumber].start(); 170 | } 171 | 172 | render() { 173 | return ( 174 |
178 |
179 | {this.menuButton} 180 | {this.getItems()} 181 |
182 |
183 | ); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/item.js: -------------------------------------------------------------------------------- 1 | import React, { Component, cloneElement } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Motion, spring } from 'react-motion'; 4 | 5 | const createSmoothParams = ({ x, y }) => ([ 6 | { 7 | scaleX: spring(0, { stiffness: 1500, damping: 100 }), 8 | scaleY: spring(0, { stiffness: 1500, damping: 100 }), 9 | x: spring(x, { stiffness: 1500, damping: 50 }), 10 | y: spring(y, { stiffness: 1500, damping: 50 }), 11 | }, { 12 | scaleX: spring(0.5, { stiffness: 120, damping: 20 }), 13 | scaleY: spring(0.5, { stiffness: 120, damping: 20 }), 14 | x: spring(x, { stiffness: 120, damping: 20 }), 15 | y: spring(y, { stiffness: 120, damping: 20 }), 16 | }, { 17 | scaleX: spring(1, { stiffness: 120, damping: 20 }), 18 | scaleY: spring(1, { stiffness: 120, damping: 20 }), 19 | x: spring(x, { stiffness: 120, damping: 20 }), 20 | y: spring(y, { stiffness: 120, damping: 20 }), 21 | }, 22 | ]); 23 | 24 | const createBumpyParams = (x, y) => ([ 25 | { 26 | scaleX: spring(0, { stiffness: 1500, damping: 100 }), 27 | scaleY: spring(0, { stiffness: 1500, damping: 100 }), 28 | x: spring(x, { stiffness: 1500, damping: 50 }), 29 | y: spring(y, { stiffness: 1500, damping: 50 }), 30 | }, 31 | { 32 | scaleX: spring(1.6, { stiffness: 1500, damping: 150 }), 33 | scaleY: spring(0.7, { stiffness: 1500, damping: 150 }), 34 | x: spring(x, { stiffness: 1500, damping: 100 }), 35 | y: spring(y, { stiffness: 1500, damping: 100 }), 36 | }, 37 | { 38 | scaleX: spring(1, { stiffness: 1500, damping: 18 }), 39 | scaleY: spring(1, { stiffness: 1500, damping: 18 }), 40 | x: spring(x, { stiffness: 1500, damping: 100 }), 41 | y: spring(y, { stiffness: 1500, damping: 100 }), 42 | }, 43 | ]); 44 | 45 | 46 | export default class MenuItem extends Component { 47 | 48 | static propTypes = { 49 | x: PropTypes.number.isRequired, 50 | y: PropTypes.number.isRequired, 51 | name: PropTypes.string.isRequired, 52 | onOpenAnimationEnd: PropTypes.func, 53 | onCloseAnimationEnd: PropTypes.func, 54 | bumpy: PropTypes.bool.isRequired, 55 | openSpeed: PropTypes.number.isRequired, 56 | reverse: PropTypes.bool.isRequired, 57 | type: PropTypes.oneOf(['horizontal', 'vertical', 'circle']).isRequired, 58 | } 59 | 60 | static defaultProps = { 61 | onOpenAnimationEnd: () => {}, 62 | onCloseAnimationEnd: () => {}, 63 | } 64 | 65 | constructor(props) { 66 | super(props); 67 | this.timerIds = []; 68 | this.state = { 69 | sequence: 0, 70 | }; 71 | 72 | this.sequenceParams = this.props.bumpy ? createBumpyParams(props) : createSmoothParams(props); 73 | } 74 | 75 | start() { 76 | this.timerIds[1] = setTimeout(() => { 77 | this.setState({ sequence: 1 }); 78 | this.timerIds[1] = null; 79 | }, this.props.openSpeed); 80 | 81 | this.timerIds[2] = setTimeout(() => { 82 | this.setState({ sequence: 2 }); 83 | this.timerIds[2] = null; 84 | this.props.onOpenAnimationEnd(this.props.name); 85 | }, this.props.openSpeed); 86 | } 87 | 88 | reverse() { 89 | this.timerIds.forEach((id) => { if (id) clearTimeout(id); }); 90 | this.timerIds[0] = setTimeout(() => { 91 | this.timerIds[0] = null; 92 | this.props.onCloseAnimationEnd(this.props.name); 93 | }, 100); 94 | this.setState({ sequence: 0 }); 95 | } 96 | 97 | render() { 98 | const { x, y, reverse, type } = this.props; 99 | let newX; 100 | let newY; 101 | if (reverse) { 102 | newX = (-1) * (x); 103 | newY = type === 'vertical' ? (-1) * (y) : y; 104 | } else { 105 | newX = x; 106 | newY = y; 107 | } 108 | if (!this.props.children) return null; 109 | return ( 110 | 111 | {({ scaleX, scaleY }) => ( 112 | cloneElement( 113 | this.props.children, 114 | { 115 | ...(this.props.children.props || {}), 116 | style: { 117 | ...((this.props.children.props && this.props.children.props.style) || {}), 118 | transform: `translate3d(${newX}px, ${newY}px, 0) scaleX(${scaleX}) scaleY(${scaleY})`, 119 | WebkitTransform: `translate3d(${newX}px, ${newY}px, 0) scaleX(${scaleX}) scaleY(${scaleY})`, 120 | position: 'absolute', 121 | }, 122 | }, 123 | ) 124 | ) 125 | } 126 | 127 | ); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /test/test-button.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import assert from 'power-assert'; 3 | import { spy } from 'sinon'; 4 | import { mount, shallow } from 'enzyme'; 5 | import MenuButton from '../src/button'; 6 | 7 | describe('Button Component test', () => { 8 | it('should mount button component without error', () => { 9 | mount( 10 | {}} bumpy > 11 |
sample
12 |
, 13 | ); 14 | }); 15 | 16 | it('should x, y position set to button component with bumpy', () => { 17 | const button = mount( 18 | {}} bumpy > 19 |
sample
20 |
, 21 | ); 22 | assert.equal(button.getDOMNode().style.transform, 'translate3d(10px, 20px, 0px) scaleX(1) scaleY(1)'); 23 | assert.equal(button.getDOMNode().style.position, 'absolute'); 24 | }); 25 | 26 | it('should x, y position set to button component with smooth effect', () => { 27 | const button = mount( 28 | {}} bumpy={false} > 29 |
sample
30 |
, 31 | ); 32 | assert.equal(button.getDOMNode().style.transform, 'translate3d(10px, 20px, 0px) scaleX(1) scaleY(1)'); 33 | assert.equal(button.getDOMNode().style.position, 'absolute'); 34 | }); 35 | 36 | it('should call onClick when button clicked', () => { 37 | const onClick = spy(); 38 | const button = mount( 39 | 40 |
sample
41 |
, 42 | ); 43 | button.simulate('click'); 44 | assert.equal(onClick.callCount, 1); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/test-item.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import assert from 'power-assert'; 3 | import { spy } from 'sinon'; 4 | import { mount, shallow } from 'enzyme'; 5 | import MenuItem from '../src/item'; 6 | 7 | describe('Item Component test', function () { 8 | 9 | this.timeout(10000); 10 | 11 | it('should mount item component without error', () => { 12 | mount( 13 | 14 |
sample
15 |
, 16 | ); 17 | }); 18 | 19 | it('should call onOpenAnimationEnd callback, when opend', (done) => { 20 | const onOpenAnimationEnd = spy(); 21 | const wrapper = shallow( 22 | 23 |
sample
24 |
, 25 | ); 26 | wrapper.instance().start(); 27 | setTimeout(() => { 28 | assert.equal(onOpenAnimationEnd.callCount, 1); 29 | assert.equal(onOpenAnimationEnd.getCall(0).args[0], 'sample'); 30 | done(); 31 | }, 200); 32 | }); 33 | 34 | it('should call onOpenAnimationEnd callback, when opend', (done) => { 35 | const wrapper = mount( 36 | 37 |
sample
38 |
, 39 | ); 40 | wrapper.instance().start(); 41 | setTimeout(() => { 42 | assert.equal(wrapper.getDOMNode().style.transform, 'translate3d(10px, 20px, 0px) scaleX(1) scaleY(1)'); 43 | done(); 44 | }, 3000); 45 | }); 46 | 47 | it('should call onCloseAnimationEnd callback, when closed', (done) => { 48 | const onCloseAnimationEnd = spy(); 49 | const wrapper = shallow( 50 | 51 |
sample
52 |
, 53 | ); 54 | wrapper.instance().start(); 55 | wrapper.instance().reverse(); 56 | setTimeout(() => { 57 | assert.equal(onCloseAnimationEnd.callCount, 1); 58 | assert.equal(onCloseAnimationEnd.getCall(0).args[0], 'sample'); 59 | done(); 60 | }, 200); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /test/test-menu.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import assert from 'power-assert'; 3 | import { spy } from 'sinon'; 4 | import { mount, shallow } from 'enzyme'; 5 | import MotionMenu from '../src/'; 6 | 7 | describe('Item Component test', function () { 8 | 9 | this.timeout(10000); 10 | 11 | it('should mount item component without error', () => { 12 | mount( 13 | 14 |
button
15 |
item1
16 |
item2
17 |
, 18 | ); 19 | }); 20 | 21 | it('should render a item and button, when mounted', () => { 22 | const menu = mount( 23 | 24 |
button
25 |
item1
26 |
item2
27 |
, 28 | ); 29 | assert.equal(menu.find('.item1').getDOMNode().style.transform, 'translate3d(10px, 40px, 0px) scaleX(0) scaleY(0)'); 30 | assert.equal(menu.find('.item2').length, 0); 31 | }); 32 | 33 | it('should render a item and button, when opend', (done) => { 34 | const menu = mount( 35 | 36 |
button
37 |
item1
38 |
item2
39 |
, 40 | ); 41 | menu.find('.button').simulate('click'); 42 | setTimeout(() => { 43 | assert.equal(menu.find('.item1').getDOMNode().style.transform, 'translate3d(10px, 40px, 0px) scaleX(1) scaleY(1)'); 44 | assert.equal(menu.find('.item2').getDOMNode().style.transform, 'translate3d(10px, 60px, 0px) scaleX(1) scaleY(1)'); 45 | done(); 46 | }, 3000); 47 | }); 48 | }); 49 | --------------------------------------------------------------------------------