├── .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 |
13 |
14 |
15 |
16 |
17 | ## Demo
18 |
19 | 
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 | REACT MOTION MENUver0.3.0
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 |
--------------------------------------------------------------------------------