├── .babelrc ├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── demo ├── index.js ├── public │ └── index.html └── server.js ├── dist ├── components │ ├── __tests__ │ │ ├── integration-test.js │ │ ├── pulldown-close-test.js │ │ ├── pulldown-goto-test.js │ │ ├── pulldown-stage-test.js │ │ └── pulldown-test.js │ ├── pulldown-close.js │ ├── pulldown-goto.js │ ├── pulldown-stage.js │ └── pulldown.js └── index.js ├── docs └── API.md ├── package.json ├── src ├── components │ ├── __tests__ │ │ ├── integration-test.js │ │ ├── pulldown-close-test.js │ │ ├── pulldown-goto-test.js │ │ ├── pulldown-stage-test.js │ │ └── pulldown-test.js │ ├── pulldown-close.js │ ├── pulldown-goto.js │ ├── pulldown-stage.js │ └── pulldown.js └── index.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "react", 5 | "stage-2" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "plugins": [ 4 | "react" 5 | ], 6 | "env": { 7 | "browser": true, 8 | "node": true 9 | }, 10 | "ecmaFeatures": { 11 | "arrowFunctions": true, 12 | "blockBindings": true, 13 | "classes": true, 14 | "defaultParams": true, 15 | "destructuring": true, 16 | "forOf": true, 17 | "generators": false, 18 | "modules": true, 19 | "objectLiteralComputedProperties": true, 20 | "objectLiteralDuplicateProperties": false, 21 | "objectLiteralShorthandMethods": true, 22 | "objectLiteralShorthandProperties": true, 23 | "spread": true, 24 | "superInFunctions": true, 25 | "templateStrings": true, 26 | "jsx": true 27 | }, 28 | "rules": { 29 | /** 30 | * Strict mode 31 | */ 32 | // babel inserts "use strict"; for us 33 | // http://eslint.org/docs/rules/strict 34 | "strict": [2, "never"], 35 | 36 | /** 37 | * ES6 38 | */ 39 | "no-var": 2, // const and let - no var 40 | "prefer-const": 1, // Warn when let is used but variable is never modified 41 | 42 | /** 43 | * General 44 | */ 45 | "max-len": [2, 200, 4], // Max length of lines is 200 characters 46 | 47 | /** 48 | * Variables 49 | */ 50 | "no-shadow": 2, // http://eslint.org/docs/rules/no-shadow 51 | "no-shadow-restricted-names": 2, // http://eslint.org/docs/rules/no-shadow-restricted-names 52 | "no-unused-vars": [2, { // http://eslint.org/docs/rules/no-unused-vars 53 | "vars": "local", 54 | "args": "after-used" 55 | }], 56 | "no-use-before-define": 2, // http://eslint.org/docs/rules/no-use-before-define 57 | 58 | /** 59 | * Possible errors 60 | */ 61 | "comma-dangle": [2, "always-multiline"], // http://eslint.org/docs/rules/comma-dangle 62 | "no-cond-assign": [2, "always"], // http://eslint.org/docs/rules/no-cond-assign 63 | "no-console": 2, // http://eslint.org/docs/rules/no-console 64 | "no-debugger": 2, // http://eslint.org/docs/rules/no-debugger 65 | "no-alert": 2, // http://eslint.org/docs/rules/no-alert 66 | "no-constant-condition": 2, // http://eslint.org/docs/rules/no-constant-condition 67 | "no-dupe-keys": 2, // http://eslint.org/docs/rules/no-dupe-keys 68 | "no-duplicate-case": 2, // http://eslint.org/docs/rules/no-duplicate-case 69 | "no-empty": 2, // http://eslint.org/docs/rules/no-empty 70 | "no-ex-assign": 2, // http://eslint.org/docs/rules/no-ex-assign 71 | "no-extra-boolean-cast": 2, // http://eslint.org/docs/rules/no-extra-boolean-cast 72 | "no-extra-semi": 2, // http://eslint.org/docs/rules/no-extra-semi 73 | "no-func-assign": 2, // http://eslint.org/docs/rules/no-func-assign 74 | "no-inner-declarations": 2, // http://eslint.org/docs/rules/no-inner-declarations 75 | "no-invalid-regexp": 2, // http://eslint.org/docs/rules/no-invalid-regexp 76 | "no-irregular-whitespace": 2, // http://eslint.org/docs/rules/no-irregular-whitespace 77 | "no-obj-calls": 2, // http://eslint.org/docs/rules/no-obj-calls 78 | "no-sparse-arrays": 2, // http://eslint.org/docs/rules/no-sparse-arrays 79 | "no-unreachable": 2, // http://eslint.org/docs/rules/no-unreachable 80 | "use-isnan": 2, // http://eslint.org/docs/rules/use-isnan 81 | "block-scoped-var": 2, // http://eslint.org/docs/rules/block-scoped-var 82 | 83 | /** 84 | * Best practices 85 | */ 86 | "consistent-return": 2, // http://eslint.org/docs/rules/consistent-return 87 | "curly": 2, // http://eslint.org/docs/rules/curly 88 | "default-case": 2, // http://eslint.org/docs/rules/default-case 89 | "dot-notation": [2, { // http://eslint.org/docs/rules/dot-notation 90 | "allowKeywords": true 91 | }], 92 | "eqeqeq": 2, // http://eslint.org/docs/rules/eqeqeq 93 | "guard-for-in": 2, // http://eslint.org/docs/rules/guard-for-in 94 | "no-caller": 2, // http://eslint.org/docs/rules/no-caller 95 | "no-else-return": 2, // http://eslint.org/docs/rules/no-else-return 96 | "no-eq-null": 2, // http://eslint.org/docs/rules/no-eq-null 97 | "no-eval": 2, // http://eslint.org/docs/rules/no-eval 98 | "no-extend-native": 2, // http://eslint.org/docs/rules/no-extend-native 99 | "no-extra-bind": 2, // http://eslint.org/docs/rules/no-extra-bind 100 | "no-fallthrough": 2, // http://eslint.org/docs/rules/no-fallthrough 101 | "no-floating-decimal": 2, // http://eslint.org/docs/rules/no-floating-decimal 102 | "no-implied-eval": 2, // http://eslint.org/docs/rules/no-implied-eval 103 | "no-lone-blocks": 2, // http://eslint.org/docs/rules/no-lone-blocks 104 | "no-loop-func": 2, // http://eslint.org/docs/rules/no-loop-func 105 | "no-multi-str": 2, // http://eslint.org/docs/rules/no-multi-str 106 | "no-native-reassign": 2, // http://eslint.org/docs/rules/no-native-reassign 107 | "no-new": 2, // http://eslint.org/docs/rules/no-new 108 | "no-new-func": 2, // http://eslint.org/docs/rules/no-new-func 109 | "no-new-wrappers": 2, // http://eslint.org/docs/rules/no-new-wrappers 110 | "no-octal": 2, // http://eslint.org/docs/rules/no-octal 111 | "no-octal-escape": 2, // http://eslint.org/docs/rules/no-octal-escape 112 | "no-param-reassign": 2, // http://eslint.org/docs/rules/no-param-reassign 113 | "no-proto": 2, // http://eslint.org/docs/rules/no-proto 114 | "no-redeclare": 2, // http://eslint.org/docs/rules/no-redeclare 115 | "no-return-assign": 2, // http://eslint.org/docs/rules/no-return-assign 116 | "no-script-url": 2, // http://eslint.org/docs/rules/no-script-url 117 | "no-self-compare": 2, // http://eslint.org/docs/rules/no-self-compare 118 | "no-sequences": 2, // http://eslint.org/docs/rules/no-sequences 119 | "no-throw-literal": 2, // http://eslint.org/docs/rules/no-throw-literal 120 | "no-with": 2, // http://eslint.org/docs/rules/no-with 121 | "radix": 2, // http://eslint.org/docs/rules/radix 122 | "vars-on-top": 2, // http://eslint.org/docs/rules/vars-on-top 123 | "wrap-iife": [2, "any"], // http://eslint.org/docs/rules/wrap-iife 124 | "yoda": 2, // http://eslint.org/docs/rules/yoda 125 | 126 | /** 127 | * Style 128 | */ 129 | "indent": [2, 2, {"SwitchCase": 1}], // http://eslint.org/docs/rules/ 130 | "brace-style": [2, // http://eslint.org/docs/rules/brace-style 131 | "1tbs", { 132 | "allowSingleLine": false 133 | }], 134 | "quotes": [ 135 | 2, "single", "avoid-escape" // http://eslint.org/docs/rules/quotes 136 | ], 137 | "camelcase": [2, { // http://eslint.org/docs/rules/camelcase 138 | "properties": "never" 139 | }], 140 | "comma-spacing": [2, { // http://eslint.org/docs/rules/comma-spacing 141 | "before": false, 142 | "after": true 143 | }], 144 | "comma-style": [2, "last"], // http://eslint.org/docs/rules/comma-style 145 | "eol-last": 2, // http://eslint.org/docs/rules/eol-last 146 | "func-names": 2, // http://eslint.org/docs/rules/func-names 147 | "key-spacing": [2, { // http://eslint.org/docs/rules/key-spacing 148 | "beforeColon": false, 149 | "afterColon": true 150 | }], 151 | "new-cap": [2, { // http://eslint.org/docs/rules/new-cap 152 | "newIsCap": true, 153 | "capIsNewExceptions": ["Map", "Record"] 154 | }], 155 | "no-multiple-empty-lines": [2, { // http://eslint.org/docs/rules/no-multiple-empty-lines 156 | "max": 2 157 | }], 158 | "no-nested-ternary": 2, // http://eslint.org/docs/rules/no-nested-ternary 159 | "no-new-object": 2, // http://eslint.org/docs/rules/no-new-object 160 | "no-spaced-func": 2, // http://eslint.org/docs/rules/no-spaced-func 161 | "no-trailing-spaces": 2, // http://eslint.org/docs/rules/no-trailing-spaces 162 | "no-extra-parens": [2, "functions"], // No extra parentheses around functions 163 | "no-underscore-dangle": 0, // http://eslint.org/docs/rules/no-underscore-dangle 164 | "one-var": [2, "never"], // http://eslint.org/docs/rules/one-var 165 | "padded-blocks": [2, "never"], // http://eslint.org/docs/rules/padded-blocks 166 | "semi": [2, "always"], // http://eslint.org/docs/rules/semi 167 | "semi-spacing": [2, { // http://eslint.org/docs/rules/semi-spacing 168 | "before": false, 169 | "after": true 170 | }], 171 | "space-after-keywords": 2, // http://eslint.org/docs/rules/space-after-keywords 172 | "space-before-blocks": 2, // http://eslint.org/docs/rules/space-before-blocks 173 | "space-before-function-paren": [2, "never"], // http://eslint.org/docs/rules/space-before-function-paren 174 | "space-infix-ops": 2, // http://eslint.org/docs/rules/space-infix-ops 175 | "space-return-throw-case": 2, // http://eslint.org/docs/rules/space-return-throw-case 176 | "spaced-comment": [2, "always"], // Require a space after the beginning of a comment before comment content begins 177 | "jsx-quotes": [2, "prefer-double"], // http://eslint.org/docs/rules/jsx-quotes 178 | /** 179 | * JSX style 180 | */ 181 | "react/display-name": 0, 182 | "react/jsx-boolean-value": [2, "never"], 183 | "react/jsx-closing-bracket-location": 2, 184 | "react/jsx-curly-spacing": [2, "never"], 185 | "react/jsx-indent-props": [2, 2], 186 | "react/jsx-key": 2, 187 | "react/jsx-max-props-per-line": [2, {"maximum": 3}], 188 | "react/jsx-no-duplicate-props": 2, 189 | "react/jsx-no-undef": 2, 190 | "react/jsx-sort-props": 2, 191 | "react/jsx-sort-prop-types": 2, 192 | "react/jsx-uses-react": 2, 193 | "react/jsx-uses-vars": 2, 194 | "react/no-did-mount-set-state": [2, "allow-in-func"], 195 | "react/no-did-update-set-state": 2, 196 | "react/no-direct-mutation-state": 2, 197 | "react/no-multi-comp": 2, 198 | "react/no-unknown-property": 2, 199 | "react/prop-types": 2, 200 | "react/react-in-jsx-scope": 2, 201 | "react/self-closing-comp": 2, 202 | "react/wrap-multilines": 2, 203 | "react/sort-comp": [2, { 204 | "order": [ 205 | "displayName", 206 | "baseClass", 207 | "mixins", 208 | "statics", 209 | "propTypes", 210 | "contextTypes", 211 | "childContextTypes", 212 | "getDefaultProps", 213 | "getInitialState", 214 | "componentWillMount", 215 | "componentDidMount", 216 | "componentWillReceiveProps", 217 | "shouldComponentUpdate", 218 | "componentWillUpdate", 219 | "componentWillUnmount", 220 | "/^component.+$/", 221 | "/^has.+$/", 222 | "/^reset.+$/", 223 | "/^get.+$/", 224 | "/^_handle.+$/", 225 | "/^_.+$/", 226 | "/^_render.+$/", 227 | "render" 228 | ] 229 | }] 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | demo/public/bundle.js 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - 4.2.3 5 | before_deploy: 6 | - npm run demo 7 | deploy: 8 | provider: heroku 9 | skip_cleanup: true 10 | api_key: 11 | secure: O1xa08rqlaa66l193GEblNSBCbjlYhn8pVIf0zHXXW6VlIt0yuhkudfG0deTB9f+qXx9u/wWxwvcgZ+mmwUq54MQujgHV4gKMzj8gVtXF6ZEeIK0JYrbe5EQzg5ZLj1+ioopmoIhvL9gJZCiselgfs5laaD3HE6PKpNLrAnIYbZ+Luc6T0bJLqAZqcog4UMuwvq6QrQg1L7l3AljG+9EkwQXiiSXWZFnOPF+0ok4lWmXZrPE7m6vB90JaIxZW/ffkfpYZOUFhE8UGG/Cl4N8FnOsr5NqdHaXyTMl/NWQ13zIei+vsBsunaIAQNvVg77XSzbvLSpdwwVPcQdyw+qKv938JjWCzWBLIGMXZdJRCPZJdqZt+J47Tvy6q4uKEXW7k4/xHO5oafhUVVpmg6yKjzZJPwH1vKWIYp39SAP1edXdV7bYhOu9lOxba5N22bnfkt96ynQUsLlhNoM6JBF5+ghrWKYHpUF9lOaMxcjE4r8ElyjPheUv+cN6WXSvF2KQeZbvv+PvwHos2Xz4XRhR1BvF380FVqUHlZ41fsL2oL0RbGpylHV3BxjPMNm/W5WXtIeXpkPLC4m8hOYRgqyis5EHZWHc5tmyotcdyI2I+vgKBL4ms0LcXkC0TeWR049gkdK21IQ4kwWkiOtUr1B0A5eTlcIbD7qiyWYhlI32rdY= 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 YPlan 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/YPlan/react-pulldown.svg?branch=master)](https://travis-ci.org/YPlan/react-pulldown) 2 | 3 | React Pulldown 4 | ============== 5 | 6 | A Pulldown component for [React.js](http://facebook.github.io/react/) 7 | 8 | Installation 9 | ------------ 10 | 11 | ```sh 12 | $ npm install react-pulldown --save 13 | ``` 14 | 15 | Demo 16 | ---- 17 | 18 | [https://react-pulldown.herokuapp.com/](https://react-pulldown.herokuapp.com/) 19 | 20 | Usage 21 | ----- 22 | 23 | See the [API](/docs/API.md) section for details. 24 | 25 | ```jsx 26 | import React from 'react'; 27 | import ReactDOM from 'react-dom'; 28 | import {PulldownClose, PulldownGoto, PulldownStage, Pulldown} from 'react-pulldown'; 29 | 30 | const App = React.createClass({ 31 | 32 | render() { 33 | return ( 34 | 39 | 40 | 44 | First stage 45 | 48 | Next 49 | 50 | 51 | 52 | 57 | Second stage 58 | 59 | Close 60 | 61 | 62 | 63 | 64 | ); 65 | }, 66 | 67 | }); 68 | 69 | ReactDOM.render(, document.getElementById('app')); 70 | ``` 71 | 72 | Test 73 | ---- 74 | 75 | ```sh 76 | $ npm test 77 | ``` 78 | -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import {PulldownClose, PulldownGoto, PulldownStage, Pulldown} from '../dist'; 4 | 5 | const App = React.createClass({ 6 | 7 | getInitialState() { 8 | return { 9 | closed: false, 10 | countdown: 3, 11 | }; 12 | }, 13 | 14 | componentDidMount() { 15 | const interval = setInterval(() => { 16 | const {countdown} = this.state; 17 | 18 | if (countdown > 0) { 19 | this.setState({ 20 | countdown: countdown - 1, 21 | }); 22 | } else { 23 | clearTimeout(interval); 24 | } 25 | }, 1000); 26 | }, 27 | 28 | _handleClose() { 29 | this.setState({ 30 | closed: true, 31 | }); 32 | }, 33 | 34 | _renderCountdown() { 35 | const {countdown} = this.state; 36 | 37 | return ( 38 |

39 | The pulldown will appear in 40 | 41 | {countdown}s 42 | 43 |

44 | ); 45 | }, 46 | 47 | _renderCredits() { 48 | return ( 49 |
50 | 51 | Restart 52 | 53 |
54 | 55 | Fork me on Github 56 | 57 |
58 |

59 | Made with ❤ by YPlan 60 |

61 |
62 | ); 63 | }, 64 | 65 | render() { 66 | const {closed, countdown} = this.state; 67 | 68 | return ( 69 |
70 | {countdown > 0 && this._renderCountdown()} 71 | {closed && this._renderCredits()} 72 | 73 | 80 | 81 | 86 |
87 | The 88 | 92 | next 93 | 94 | stage will come from the top 95 |
96 |
97 | 98 | 104 |
105 | The 106 | 110 | next 111 | 112 | one from the bottom 113 |
114 |
115 | 116 | 122 |
123 | The 124 | 128 | next 129 | 130 | one is gonna be bigger 131 |
132 |
133 | 134 | 140 |
141 | The 142 | 146 | last 147 | 148 | one is smaller 149 |
150 |
151 | 152 | 158 |
159 | See ya! 160 | 163 | Close 164 | 165 |
166 |
167 | 168 |
169 |
170 | ); 171 | }, 172 | 173 | }); 174 | 175 | ReactDOM.render(, document.getElementById('app')); 176 | -------------------------------------------------------------------------------- /demo/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React Pulldown 5 | 6 | 86 | 87 |
88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /demo/server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var app = express(); 3 | 4 | app.use(express.static(__dirname + '/public')); 5 | 6 | var host = process.env.IP || '0.0.0.0', 7 | port = process.env.PORT || 3000; 8 | 9 | var server = app.listen(port, host, function() { 10 | console.log('Listening at http://%s:%s', host, port); 11 | }); 12 | -------------------------------------------------------------------------------- /dist/components/__tests__/integration-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _react = require('react'); 4 | 5 | var _react2 = _interopRequireDefault(_react); 6 | 7 | var _reactAddonsTestUtils = require('react-addons-test-utils'); 8 | 9 | var _reactAddonsTestUtils2 = _interopRequireDefault(_reactAddonsTestUtils); 10 | 11 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 12 | 13 | jest.dontMock('shallowequal'); 14 | jest.dontMock('../pulldown'); 15 | jest.dontMock('../pulldown-stage'); 16 | 17 | var Pulldown = require('../pulldown').default; 18 | var PulldownStage = require('../pulldown-stage').default; 19 | 20 | describe('Pulldown', function () { 21 | it('updates its height when the current stage height changes', function () { 22 | var Parent = _react2.default.createClass({ 23 | displayName: 'Parent', 24 | getInitialState: function getInitialState() { 25 | return { 26 | height: 100 27 | }; 28 | }, 29 | render: function render() { 30 | var height = this.state.height; 31 | 32 | return _react2.default.createElement( 33 | Pulldown, 34 | { 35 | className: 'pulldown', 36 | defaultStage: 'a' 37 | }, 38 | _react2.default.createElement(PulldownStage, { 39 | height: height, 40 | name: 'a' 41 | }) 42 | ); 43 | } 44 | }); 45 | var parent = _reactAddonsTestUtils2.default.renderIntoDocument(_react2.default.createElement(Parent, null)); 46 | var pulldown = _reactAddonsTestUtils2.default.findRenderedDOMComponentWithClass(parent, 'pulldown'); 47 | 48 | expect(pulldown.style.height).toBe('100px'); 49 | parent.setState({ 50 | height: 200 51 | }); 52 | expect(pulldown.style.height).toBe('200px'); 53 | }); 54 | }); -------------------------------------------------------------------------------- /dist/components/__tests__/pulldown-close-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _react = require('react'); 4 | 5 | var _react2 = _interopRequireDefault(_react); 6 | 7 | var _reactAddonsTestUtils = require('react-addons-test-utils'); 8 | 9 | var _reactAddonsTestUtils2 = _interopRequireDefault(_reactAddonsTestUtils); 10 | 11 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 12 | 13 | jest.dontMock('../pulldown-close'); 14 | 15 | var PulldownClose = require('../pulldown-close').default; 16 | 17 | describe('PulldownClose', function () { 18 | var className = 'className'; 19 | var close = undefined; 20 | var pulldownClose = undefined; 21 | 22 | beforeEach(function () { 23 | close = jest.genMockFunction(); 24 | 25 | var FakeContext = _react2.default.createClass({ 26 | displayName: 'FakeContext', 27 | 28 | propTypes: { 29 | children: _react2.default.PropTypes.element 30 | }, 31 | childContextTypes: { 32 | close: _react2.default.PropTypes.func 33 | }, 34 | getChildContext: function getChildContext() { 35 | return { 36 | close: close 37 | }; 38 | }, 39 | render: function render() { 40 | return _react2.default.createElement( 41 | 'div', 42 | null, 43 | this.props.children 44 | ); 45 | } 46 | }); 47 | 48 | pulldownClose = _reactAddonsTestUtils2.default.renderIntoDocument(_react2.default.createElement( 49 | FakeContext, 50 | null, 51 | _react2.default.createElement(PulldownClose, { className: className }) 52 | )); 53 | }); 54 | 55 | it('applies the class name', function () { 56 | var element = _reactAddonsTestUtils2.default.findRenderedDOMComponentWithTag(pulldownClose, 'button'); 57 | 58 | expect(element.className).toBe(className); 59 | }); 60 | 61 | it('calls the callback when clicked', function () { 62 | var element = _reactAddonsTestUtils2.default.findRenderedDOMComponentWithTag(pulldownClose, 'button'); 63 | _reactAddonsTestUtils2.default.Simulate.click(element); 64 | 65 | expect(close).toBeCalled(); 66 | }); 67 | }); -------------------------------------------------------------------------------- /dist/components/__tests__/pulldown-goto-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _react = require('react'); 4 | 5 | var _react2 = _interopRequireDefault(_react); 6 | 7 | var _reactAddonsTestUtils = require('react-addons-test-utils'); 8 | 9 | var _reactAddonsTestUtils2 = _interopRequireDefault(_reactAddonsTestUtils); 10 | 11 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 12 | 13 | jest.dontMock('../pulldown-goto'); 14 | 15 | var PulldownGoto = require('../pulldown-goto').default; 16 | 17 | describe('PulldownGoto', function () { 18 | var className = 'className'; 19 | var stage = 'stage'; 20 | var goTo = undefined; 21 | var pulldownGoto = undefined; 22 | 23 | beforeEach(function () { 24 | goTo = jest.genMockFunction(); 25 | 26 | var FakeContext = _react2.default.createClass({ 27 | displayName: 'FakeContext', 28 | 29 | propTypes: { 30 | children: _react2.default.PropTypes.element 31 | }, 32 | childContextTypes: { 33 | goTo: _react2.default.PropTypes.func 34 | }, 35 | getChildContext: function getChildContext() { 36 | return { 37 | goTo: goTo 38 | }; 39 | }, 40 | render: function render() { 41 | return _react2.default.createElement( 42 | 'div', 43 | null, 44 | this.props.children 45 | ); 46 | } 47 | }); 48 | 49 | pulldownGoto = _reactAddonsTestUtils2.default.renderIntoDocument(_react2.default.createElement( 50 | FakeContext, 51 | null, 52 | _react2.default.createElement(PulldownGoto, { className: className, stage: stage }) 53 | )); 54 | }); 55 | 56 | it('applies the class name', function () { 57 | var element = _reactAddonsTestUtils2.default.findRenderedDOMComponentWithTag(pulldownGoto, 'button'); 58 | 59 | expect(element.className).toBe(className); 60 | }); 61 | 62 | it('calls the callback when clicked', function () { 63 | var element = _reactAddonsTestUtils2.default.findRenderedDOMComponentWithTag(pulldownGoto, 'button'); 64 | _reactAddonsTestUtils2.default.Simulate.click(element); 65 | 66 | expect(goTo).toBeCalledWith(stage); 67 | }); 68 | }); -------------------------------------------------------------------------------- /dist/components/__tests__/pulldown-stage-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _react = require('react'); 4 | 5 | var _react2 = _interopRequireDefault(_react); 6 | 7 | var _reactAddonsTestUtils = require('react-addons-test-utils'); 8 | 9 | var _reactAddonsTestUtils2 = _interopRequireDefault(_reactAddonsTestUtils); 10 | 11 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 12 | 13 | /* eslint react/no-multi-comp:0 */ 14 | 15 | jest.dontMock('shallowequal'); 16 | jest.dontMock('../pulldown-stage'); 17 | 18 | var PulldownStage = require('../pulldown-stage').default; 19 | 20 | function createFakeContext(childContext) { 21 | return _react2.default.createClass({ 22 | propTypes: { 23 | children: _react2.default.PropTypes.element 24 | }, 25 | childContextTypes: { 26 | currentStage: _react2.default.PropTypes.object, 27 | previousStage: _react2.default.PropTypes.object, 28 | update: _react2.default.PropTypes.func 29 | }, 30 | getChildContext: function getChildContext() { 31 | return childContext; 32 | }, 33 | render: function render() { 34 | return _react2.default.createElement( 35 | 'div', 36 | null, 37 | this.props.children 38 | ); 39 | } 40 | }); 41 | } 42 | 43 | describe('PulldownStage', function () { 44 | var FakeContext = undefined; 45 | var pulldownStage = undefined; 46 | 47 | describe('current stage', function () { 48 | beforeEach(function () { 49 | FakeContext = createFakeContext({ 50 | currentStage: { 51 | name: 'currentStage' 52 | } 53 | }); 54 | 55 | pulldownStage = _reactAddonsTestUtils2.default.renderIntoDocument(_react2.default.createElement( 56 | FakeContext, 57 | null, 58 | _react2.default.createElement(PulldownStage, { 59 | className: 'className', 60 | height: 100, 61 | name: 'currentStage' 62 | }) 63 | )); 64 | }); 65 | 66 | it('applies the class modifier', function () { 67 | var element = _reactAddonsTestUtils2.default.scryRenderedDOMComponentsWithTag(pulldownStage, 'div')[1]; 68 | 69 | expect(element.className).toContain('className--active'); 70 | }); 71 | 72 | it('sets the top to zero', function () { 73 | var element = _reactAddonsTestUtils2.default.scryRenderedDOMComponentsWithTag(pulldownStage, 'div')[1]; 74 | 75 | expect(element.style.top).toBe('0px'); 76 | }); 77 | }); 78 | 79 | describe('previous stage (top)', function () { 80 | beforeEach(function () { 81 | FakeContext = createFakeContext({ 82 | currentStage: { 83 | direction: 'top', 84 | height: 100, 85 | name: 'currentStage' 86 | }, 87 | previousStage: { 88 | name: 'previousStage' 89 | } 90 | }); 91 | 92 | pulldownStage = _reactAddonsTestUtils2.default.renderIntoDocument(_react2.default.createElement( 93 | FakeContext, 94 | null, 95 | _react2.default.createElement(PulldownStage, { 96 | height: 100, 97 | name: 'previousStage' 98 | }) 99 | )); 100 | }); 101 | 102 | it('sets the top to height', function () { 103 | var element = _reactAddonsTestUtils2.default.scryRenderedDOMComponentsWithTag(pulldownStage, 'div')[1]; 104 | 105 | expect(element.style.top).toBe('100px'); 106 | }); 107 | }); 108 | 109 | describe('previous stage (bottom)', function () { 110 | beforeEach(function () { 111 | FakeContext = createFakeContext({ 112 | currentStage: { 113 | direction: 'bottom', 114 | height: 100, 115 | name: 'currentStage' 116 | }, 117 | previousStage: { 118 | name: 'previousStage' 119 | } 120 | }); 121 | 122 | pulldownStage = _reactAddonsTestUtils2.default.renderIntoDocument(_react2.default.createElement( 123 | FakeContext, 124 | null, 125 | _react2.default.createElement(PulldownStage, { 126 | height: 100, 127 | name: 'previousStage' 128 | }) 129 | )); 130 | }); 131 | 132 | it('sets the top to negative height', function () { 133 | var element = _reactAddonsTestUtils2.default.scryRenderedDOMComponentsWithTag(pulldownStage, 'div')[1]; 134 | 135 | expect(element.style.top).toBe('-100px'); 136 | }); 137 | }); 138 | 139 | describe('stage', function () { 140 | beforeEach(function () { 141 | FakeContext = createFakeContext({ 142 | currentStage: {} 143 | }); 144 | 145 | pulldownStage = _reactAddonsTestUtils2.default.renderIntoDocument(_react2.default.createElement( 146 | FakeContext, 147 | null, 148 | _react2.default.createElement(PulldownStage, { 149 | className: 'className', 150 | height: 100, 151 | name: 'name' 152 | }) 153 | )); 154 | }); 155 | 156 | it('applies the class name', function () { 157 | var element = _reactAddonsTestUtils2.default.scryRenderedDOMComponentsWithTag(pulldownStage, 'div')[1]; 158 | 159 | expect(element.className).toContain('className'); 160 | }); 161 | }); 162 | 163 | describe('stage (height > currentStage height)', function () { 164 | beforeEach(function () { 165 | FakeContext = createFakeContext({ 166 | currentStage: { 167 | height: 100 168 | } 169 | }); 170 | 171 | pulldownStage = _reactAddonsTestUtils2.default.renderIntoDocument(_react2.default.createElement( 172 | FakeContext, 173 | null, 174 | _react2.default.createElement(PulldownStage, { 175 | direction: 'top', 176 | height: 101, 177 | name: 'name' 178 | }) 179 | )); 180 | }); 181 | 182 | it('sets the top to the maximum height', function () { 183 | var element = _reactAddonsTestUtils2.default.scryRenderedDOMComponentsWithTag(pulldownStage, 'div')[1]; 184 | 185 | expect(element.style.top).toBe('-101px'); 186 | }); 187 | }); 188 | 189 | describe('stage (height < currentStage height)', function () { 190 | beforeEach(function () { 191 | FakeContext = createFakeContext({ 192 | currentStage: { 193 | height: 101 194 | } 195 | }); 196 | 197 | pulldownStage = _reactAddonsTestUtils2.default.renderIntoDocument(_react2.default.createElement( 198 | FakeContext, 199 | null, 200 | _react2.default.createElement(PulldownStage, { 201 | direction: 'top', 202 | height: 100, 203 | name: 'name' 204 | }) 205 | )); 206 | }); 207 | 208 | it('sets the top to the maximum height', function () { 209 | var element = _reactAddonsTestUtils2.default.scryRenderedDOMComponentsWithTag(pulldownStage, 'div')[1]; 210 | 211 | expect(element.style.top).toBe('-101px'); 212 | }); 213 | }); 214 | 215 | describe('stage (top)', function () { 216 | beforeEach(function () { 217 | FakeContext = createFakeContext({ 218 | currentStage: { 219 | height: 100 220 | } 221 | }); 222 | 223 | pulldownStage = _reactAddonsTestUtils2.default.renderIntoDocument(_react2.default.createElement( 224 | FakeContext, 225 | null, 226 | _react2.default.createElement(PulldownStage, { 227 | direction: 'top', 228 | height: 100, 229 | name: 'name' 230 | }) 231 | )); 232 | }); 233 | 234 | it('sets the top to the negative height', function () { 235 | var element = _reactAddonsTestUtils2.default.scryRenderedDOMComponentsWithTag(pulldownStage, 'div')[1]; 236 | 237 | expect(element.style.top).toBe('-100px'); 238 | }); 239 | }); 240 | 241 | describe('stage (bottom)', function () { 242 | beforeEach(function () { 243 | FakeContext = createFakeContext({ 244 | currentStage: { 245 | height: 100 246 | } 247 | }); 248 | 249 | pulldownStage = _reactAddonsTestUtils2.default.renderIntoDocument(_react2.default.createElement( 250 | FakeContext, 251 | null, 252 | _react2.default.createElement(PulldownStage, { 253 | direction: 'bottom', 254 | height: 100, 255 | name: 'name' 256 | }) 257 | )); 258 | }); 259 | 260 | it('sets the top to the height', function () { 261 | var element = _reactAddonsTestUtils2.default.scryRenderedDOMComponentsWithTag(pulldownStage, 'div')[1]; 262 | 263 | expect(element.style.top).toBe('100px'); 264 | }); 265 | }); 266 | 267 | describe('update', function () { 268 | function createParent(childContext) { 269 | FakeContext = createFakeContext(childContext); 270 | return _react2.default.createClass({ 271 | getInitialState: function getInitialState() { 272 | return { 273 | height: 100 274 | }; 275 | }, 276 | render: function render() { 277 | var height = this.state.height; 278 | 279 | return _react2.default.createElement( 280 | FakeContext, 281 | null, 282 | _react2.default.createElement(PulldownStage, { 283 | height: height, 284 | name: 'a' 285 | }) 286 | ); 287 | } 288 | }); 289 | } 290 | 291 | it('fires the callback if is current and props change', function () { 292 | var update = jest.genMockFunction(); 293 | var Parent = createParent({ 294 | currentStage: { 295 | name: 'a' 296 | }, 297 | update: update 298 | }); 299 | var parent = _reactAddonsTestUtils2.default.renderIntoDocument(_react2.default.createElement(Parent, null)); 300 | parent.setState({ 301 | height: 200 302 | }); 303 | 304 | expect(update).toBeCalled(); 305 | }); 306 | 307 | it('does not fire the callback if is current and props do not change', function () { 308 | var update = jest.genMockFunction(); 309 | var Parent = createParent({ 310 | currentStage: { 311 | name: 'a' 312 | }, 313 | update: update 314 | }); 315 | var parent = _reactAddonsTestUtils2.default.renderIntoDocument(_react2.default.createElement(Parent, null)); 316 | parent.setState({ 317 | height: 100 318 | }); 319 | 320 | expect(update).not.toBeCalled(); 321 | }); 322 | 323 | it('does not fire the callback if is not current', function () { 324 | var update = jest.genMockFunction(); 325 | var Parent = createParent({ 326 | currentStage: { 327 | name: 'b', 328 | height: 100 329 | }, 330 | update: update 331 | }); 332 | var parent = _reactAddonsTestUtils2.default.renderIntoDocument(_react2.default.createElement(Parent, null)); 333 | parent.setState({ 334 | height: 200 335 | }); 336 | 337 | expect(update).not.toBeCalled(); 338 | }); 339 | }); 340 | }); -------------------------------------------------------------------------------- /dist/components/__tests__/pulldown-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _react = require('react'); 4 | 5 | var _react2 = _interopRequireDefault(_react); 6 | 7 | var _reactAddonsTestUtils = require('react-addons-test-utils'); 8 | 9 | var _reactAddonsTestUtils2 = _interopRequireDefault(_reactAddonsTestUtils); 10 | 11 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 12 | 13 | jest.dontMock('../pulldown'); 14 | 15 | var Pulldown = require('../pulldown').default; 16 | var PulldownStage = require('../pulldown-stage').default; 17 | 18 | describe('Pulldown', function () { 19 | var className = 'className'; 20 | var defaultStageName = 'a'; 21 | var delay = 1000; 22 | var height = 100; 23 | var nextStageName = 'b'; 24 | var onChange = jest.genMockFunction(); 25 | var onClose = jest.genMockFunction(); 26 | var onOpen = jest.genMockFunction(); 27 | var pulldown = undefined; 28 | 29 | describe('default', function () { 30 | beforeEach(function () { 31 | pulldown = _reactAddonsTestUtils2.default.renderIntoDocument(_react2.default.createElement( 32 | Pulldown, 33 | { 34 | className: className, 35 | defaultStage: defaultStageName, 36 | delay: delay, 37 | onChange: onChange, 38 | onClose: onClose, 39 | onOpen: onOpen 40 | }, 41 | _react2.default.createElement(PulldownStage, { height: height, name: defaultStageName }), 42 | _react2.default.createElement(PulldownStage, { name: nextStageName }) 43 | )); 44 | }); 45 | 46 | it('applies the class name', function () { 47 | var element = _reactAddonsTestUtils2.default.findRenderedDOMComponentWithTag(pulldown, 'div'); 48 | 49 | expect(element.className).toBe(className); 50 | }); 51 | 52 | it('sets the current stage object', function () { 53 | expect(pulldown.state.currentStage.name).toBe(defaultStageName); 54 | }); 55 | 56 | it('sets its height according to the current stage height', function () { 57 | var element = _reactAddonsTestUtils2.default.findRenderedDOMComponentWithTag(pulldown, 'div'); 58 | 59 | expect(element.style.height).toBe(height + 'px'); 60 | }); 61 | 62 | it('fires the onChange callback', function () { 63 | pulldown._goTo(nextStageName); 64 | 65 | expect(onChange).toBeCalledWith(nextStageName); 66 | }); 67 | 68 | it('fires the onClose callback', function () { 69 | pulldown._close(); 70 | 71 | expect(onClose).toBeCalled(); 72 | }); 73 | 74 | it('disappears when closed', function () { 75 | pulldown._close(); 76 | var element = _reactAddonsTestUtils2.default.findRenderedDOMComponentWithTag(pulldown, 'div'); 77 | 78 | expect(element.style.marginTop).toBe('-' + height + 'px'); 79 | }); 80 | 81 | it('does not apply the open modifier when closed', function () { 82 | pulldown._close(); 83 | var element = _reactAddonsTestUtils2.default.findRenderedDOMComponentWithTag(pulldown, 'div'); 84 | 85 | expect(element.className).not.toContain(className + '--open'); 86 | }); 87 | 88 | it('fires the onOpen callback', function () { 89 | jest.runAllTimers(); 90 | 91 | expect(onOpen).toBeCalled(); 92 | }); 93 | 94 | it('appears when open', function () { 95 | jest.runAllTimers(); 96 | var element = _reactAddonsTestUtils2.default.findRenderedDOMComponentWithTag(pulldown, 'div'); 97 | 98 | expect(element.style.marginTop).toBe('0'); 99 | }); 100 | 101 | it('applies the open modifier when open', function () { 102 | jest.runAllTimers(); 103 | var element = _reactAddonsTestUtils2.default.findRenderedDOMComponentWithTag(pulldown, 'div'); 104 | 105 | expect(element.className).toContain(className + '--open'); 106 | }); 107 | 108 | it('waits for the delay to appear', function () { 109 | var element = _reactAddonsTestUtils2.default.findRenderedDOMComponentWithTag(pulldown, 'div'); 110 | expect(element.style.marginTop).toBe('-' + height + 'px'); 111 | 112 | jest.runAllTimers(); 113 | expect(element.style.marginTop).toBe('0'); 114 | }); 115 | }); 116 | 117 | describe('fixed', function () { 118 | beforeEach(function () { 119 | pulldown = _reactAddonsTestUtils2.default.renderIntoDocument(_react2.default.createElement( 120 | Pulldown, 121 | { 122 | defaultStage: defaultStageName, 123 | delay: delay, 124 | fixed: true 125 | }, 126 | _react2.default.createElement(PulldownStage, { height: height, name: defaultStageName }) 127 | )); 128 | }); 129 | 130 | it('disappears when closed', function () { 131 | pulldown._close(); 132 | var element = _reactAddonsTestUtils2.default.findRenderedDOMComponentWithTag(pulldown, 'div'); 133 | 134 | expect(element.style.top).toBe('-100%'); 135 | }); 136 | 137 | it('appears when open', function () { 138 | jest.runAllTimers(); 139 | var element = _reactAddonsTestUtils2.default.findRenderedDOMComponentWithTag(pulldown, 'div'); 140 | 141 | expect(element.style.top).toBe('0px'); 142 | }); 143 | 144 | it('waits for the delay to appear', function () { 145 | var element = _reactAddonsTestUtils2.default.findRenderedDOMComponentWithTag(pulldown, 'div'); 146 | expect(element.style.top).toBe('-100%'); 147 | 148 | jest.runAllTimers(); 149 | expect(element.style.top).toBe('0px'); 150 | }); 151 | }); 152 | }); -------------------------------------------------------------------------------- /dist/components/pulldown-close.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _react = require('react'); 8 | 9 | var _react2 = _interopRequireDefault(_react); 10 | 11 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 12 | 13 | var PulldownClose = _react2.default.createClass({ 14 | displayName: 'PulldownClose', 15 | 16 | propTypes: { 17 | children: _react2.default.PropTypes.any, 18 | className: _react2.default.PropTypes.string 19 | }, 20 | 21 | contextTypes: { 22 | close: _react2.default.PropTypes.func 23 | }, 24 | 25 | _handleClick: function _handleClick() { 26 | var close = this.context.close; 27 | 28 | close(); 29 | }, 30 | render: function render() { 31 | var _props = this.props; 32 | var children = _props.children; 33 | var className = _props.className; 34 | 35 | return _react2.default.createElement( 36 | 'button', 37 | { className: className, onClick: this._handleClick }, 38 | children 39 | ); 40 | } 41 | }); 42 | 43 | exports.default = PulldownClose; -------------------------------------------------------------------------------- /dist/components/pulldown-goto.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _react = require('react'); 8 | 9 | var _react2 = _interopRequireDefault(_react); 10 | 11 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 12 | 13 | var PulldownGoto = _react2.default.createClass({ 14 | displayName: 'PulldownGoto', 15 | 16 | propTypes: { 17 | children: _react2.default.PropTypes.any, 18 | className: _react2.default.PropTypes.string, 19 | stage: _react2.default.PropTypes.string.isRequired 20 | }, 21 | 22 | contextTypes: { 23 | goTo: _react2.default.PropTypes.func 24 | }, 25 | 26 | _handleClick: function _handleClick() { 27 | var goTo = this.context.goTo; 28 | var stage = this.props.stage; 29 | 30 | goTo(stage); 31 | }, 32 | render: function render() { 33 | var _props = this.props; 34 | var children = _props.children; 35 | var className = _props.className; 36 | 37 | return _react2.default.createElement( 38 | 'button', 39 | { className: className, onClick: this._handleClick }, 40 | children 41 | ); 42 | } 43 | }); 44 | 45 | exports.default = PulldownGoto; -------------------------------------------------------------------------------- /dist/components/pulldown-stage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _react = require('react'); 8 | 9 | var _react2 = _interopRequireDefault(_react); 10 | 11 | var _shallowequal = require('shallowequal'); 12 | 13 | var _shallowequal2 = _interopRequireDefault(_shallowequal); 14 | 15 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 16 | 17 | var PulldownStage = _react2.default.createClass({ 18 | displayName: 'PulldownStage', 19 | 20 | propTypes: { 21 | children: _react2.default.PropTypes.any, 22 | className: _react2.default.PropTypes.string, 23 | direction: _react2.default.PropTypes.string, 24 | height: _react2.default.PropTypes.number.isRequired, 25 | name: _react2.default.PropTypes.string.isRequired 26 | }, 27 | 28 | contextTypes: { 29 | currentStage: _react2.default.PropTypes.object, 30 | previousStage: _react2.default.PropTypes.object, 31 | update: _react2.default.PropTypes.func 32 | }, 33 | 34 | componentDidUpdate: function componentDidUpdate(prevProps) { 35 | var update = this.context.update; 36 | 37 | if (this._isCurrentStage() && !(0, _shallowequal2.default)(prevProps, this.props)) { 38 | update(); 39 | } 40 | }, 41 | _isCurrentStage: function _isCurrentStage() { 42 | var currentStage = this.context.currentStage; 43 | var name = this.props.name; 44 | 45 | return currentStage.name === name; 46 | }, 47 | _getDirection: function _getDirection() { 48 | var _context = this.context; 49 | var currentStage = _context.currentStage; 50 | var previousStage = _context.previousStage; 51 | var _props = this.props; 52 | var direction = _props.direction; 53 | var name = _props.name; 54 | 55 | if (previousStage && previousStage.name === name) { 56 | this.direction = currentStage.direction === 'top' ? 'bottom' : 'top'; 57 | } 58 | 59 | return this.direction || direction; 60 | }, 61 | _getHeight: function _getHeight() { 62 | var currentStage = this.context.currentStage; 63 | var height = this.props.height; 64 | 65 | return this._isCurrentStage() ? 0 : Math.max(height, currentStage.height); 66 | }, 67 | _getTop: function _getTop() { 68 | var height = this._getHeight(); 69 | 70 | return this._getDirection() === 'top' ? -height : height; 71 | }, 72 | _getStyle: function _getStyle() { 73 | var height = this.props.height; 74 | 75 | return { 76 | height: height, 77 | position: 'absolute', 78 | top: this._getTop(), 79 | transition: 'all 500ms', 80 | width: '100%' 81 | }; 82 | }, 83 | render: function render() { 84 | var children = this.props.children; 85 | var className = this.props.className; 86 | 87 | if (className && this._isCurrentStage()) { 88 | className += ' ' + className + '--active'; 89 | } 90 | 91 | return _react2.default.createElement( 92 | 'div', 93 | { className: className, style: this._getStyle() }, 94 | children 95 | ); 96 | } 97 | }); 98 | 99 | exports.default = PulldownStage; -------------------------------------------------------------------------------- /dist/components/pulldown.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | 9 | var _react = require('react'); 10 | 11 | var _react2 = _interopRequireDefault(_react); 12 | 13 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 14 | 15 | var Pulldown = _react2.default.createClass({ 16 | displayName: 'Pulldown', 17 | 18 | propTypes: { 19 | children: _react2.default.PropTypes.any, 20 | className: _react2.default.PropTypes.string, 21 | defaultStage: _react2.default.PropTypes.string.isRequired, 22 | delay: _react2.default.PropTypes.number, 23 | fixed: _react2.default.PropTypes.bool, 24 | onChange: _react2.default.PropTypes.func, 25 | onClose: _react2.default.PropTypes.func, 26 | onOpen: _react2.default.PropTypes.func 27 | }, 28 | 29 | childContextTypes: { 30 | close: _react2.default.PropTypes.func, 31 | currentStage: _react2.default.PropTypes.object, 32 | goTo: _react2.default.PropTypes.func, 33 | previousStage: _react2.default.PropTypes.object, 34 | update: _react2.default.PropTypes.func 35 | }, 36 | 37 | getInitialState: function getInitialState() { 38 | var _props = this.props; 39 | var defaultStage = _props.defaultStage; 40 | var delay = _props.delay; 41 | 42 | return { 43 | currentStage: this._getStage(defaultStage), 44 | open: !delay 45 | }; 46 | }, 47 | componentDidMount: function componentDidMount() { 48 | var delay = this.props.delay; 49 | 50 | this.timeout = setTimeout(this._open, delay); 51 | }, 52 | componentWillUnmount: function componentWillUnmount() { 53 | if (this.timeout) { 54 | clearTimeout(this.timeout); 55 | } 56 | }, 57 | getChildContext: function getChildContext() { 58 | var _state = this.state; 59 | var currentStage = _state.currentStage; 60 | var previousStage = _state.previousStage; 61 | 62 | return { 63 | close: this._close, 64 | currentStage: currentStage, 65 | goTo: this._goTo, 66 | previousStage: previousStage, 67 | update: this._update 68 | }; 69 | }, 70 | _getStage: function _getStage(stageName) { 71 | var children = this.props.children; 72 | 73 | var currentStage = undefined; 74 | 75 | _react2.default.Children.forEach(children, function (child) { 76 | if (child.props.name === stageName) { 77 | currentStage = child.props; 78 | } 79 | }); 80 | 81 | return currentStage; 82 | }, 83 | _goTo: function _goTo(stageName) { 84 | var onChange = this.props.onChange; 85 | 86 | this.setState({ 87 | currentStage: this._getStage(stageName), 88 | previousStage: this.state.currentStage 89 | }, function () { 90 | if (onChange) { 91 | onChange(stageName); 92 | } 93 | }); 94 | }, 95 | _close: function _close() { 96 | var onClose = this.props.onClose; 97 | 98 | this.setState({ 99 | open: false 100 | }, onClose); 101 | }, 102 | _open: function _open() { 103 | var onOpen = this.props.onOpen; 104 | 105 | this.setState({ 106 | open: true 107 | }, onOpen); 108 | }, 109 | _update: function _update() { 110 | var currentStage = this.state.currentStage; 111 | 112 | this.setState({ 113 | currentStage: this._getStage(currentStage.name) 114 | }); 115 | }, 116 | _getStyle: function _getStyle() { 117 | var _state2 = this.state; 118 | var currentStage = _state2.currentStage; 119 | var open = _state2.open; 120 | var fixed = this.props.fixed; 121 | 122 | var style = undefined; 123 | 124 | if (fixed) { 125 | style = { 126 | height: currentStage.height, 127 | position: 'fixed', 128 | top: open ? 0 : '-100%', 129 | zIndex: 999 130 | }; 131 | } else { 132 | style = { 133 | height: currentStage.height, 134 | marginTop: open ? 0 : -currentStage.height + 'px', 135 | position: 'relative' 136 | }; 137 | } 138 | 139 | return _extends({ 140 | overflow: 'hidden', 141 | transition: 'all 500ms', 142 | width: '100%' 143 | }, style); 144 | }, 145 | render: function render() { 146 | var children = this.props.children; 147 | var className = this.props.className; 148 | var open = this.state.open; 149 | 150 | if (className && open) { 151 | className += ' ' + className + '--open'; 152 | } 153 | 154 | return _react2.default.createElement( 155 | 'div', 156 | { className: className, style: this._getStyle() }, 157 | children 158 | ); 159 | } 160 | }); 161 | 162 | exports.default = Pulldown; -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.Pulldown = exports.PulldownStage = exports.PulldownGoto = exports.PulldownClose = undefined; 7 | 8 | var _pulldownClose = require('./components/pulldown-close'); 9 | 10 | var _pulldownClose2 = _interopRequireDefault(_pulldownClose); 11 | 12 | var _pulldownGoto = require('./components/pulldown-goto'); 13 | 14 | var _pulldownGoto2 = _interopRequireDefault(_pulldownGoto); 15 | 16 | var _pulldownStage = require('./components/pulldown-stage'); 17 | 18 | var _pulldownStage2 = _interopRequireDefault(_pulldownStage); 19 | 20 | var _pulldown = require('./components/pulldown'); 21 | 22 | var _pulldown2 = _interopRequireDefault(_pulldown); 23 | 24 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 25 | 26 | exports.PulldownClose = _pulldownClose2.default; 27 | exports.PulldownGoto = _pulldownGoto2.default; 28 | exports.PulldownStage = _pulldownStage2.default; 29 | exports.Pulldown = _pulldown2.default; -------------------------------------------------------------------------------- /docs/API.md: -------------------------------------------------------------------------------- 1 | # API Reference 2 | 3 | * [Pulldown](#pulldown) 4 | * [PulldownStage](#pulldownstage) 5 | * [PulldownGoto](#pulldowngoto) 6 | * [PulldownClose](#pulldownclose) 7 | 8 | ## Pulldown 9 | 10 | | Name | Type | Required | Description | 11 | | ---- | ---- | :------: | ----------- | 12 | | className | string | | The class applied to the root element. | 13 | | defaultStage | string | x | The name of the first stage. | 14 | | delay | number | | The number of milliseconds to pass before the component appears. | 15 | | fixed | bool | | The style of the root element. | 16 | | onChange | func | | The callback fired everytime the current stage changes. | 17 | | onClose | func | | The callback fired when the pulldown closes. | 18 | | onOpen | func | | The callback fired when the pulldown opens. | 19 | 20 | ## PulldownStage 21 | 22 | | Name | Type | Required | Description | 23 | | ---- | ---- | :------: | ----------- | 24 | | className | string | | The class applied to the root element. | 25 | | direction | string | x | The direction from where the stage will appear. | 26 | | height | number | x | The height in pixels. | 27 | | name | string | | The name of the stage. | 28 | 29 | ## PulldownGoto 30 | 31 | | Name | Type | Required | Description | 32 | | ---- | ---- | :------: | ----------- | 33 | | className | string | | The class applied to the root element. | 34 | | name | string | x | The destination stage. | 35 | 36 | ## PulldownClose 37 | 38 | | Name | Type | Required | Description | 39 | | ---- | ---- | :------: | ----------- | 40 | | className | string | | The class applied to the root element. | 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-pulldown", 3 | "version": "0.1.0", 4 | "description": "A Pulldown component for React.js", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "lint": "eslint ./src", 9 | "build": "babel ./src --out-dir ./dist", 10 | "demo": "webpack ./demo/index.js ./demo/public/bundle.js", 11 | "start": "node ./demo/server" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/YPlan/react-pulldown.git" 16 | }, 17 | "keywords": [ 18 | "react", 19 | "react-component", 20 | "pulldown" 21 | ], 22 | "authors": [ 23 | "Alex Andrews", 24 | "Ben Davis", 25 | "Lucas Motta", 26 | "Michele Bertoli" 27 | ], 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/YPlan/react-pulldown/issues" 31 | }, 32 | "homepage": "https://github.com/YPlan/react-pulldown#readme", 33 | "devDependencies": { 34 | "babel-cli": "^6.3.17", 35 | "babel-core": "^6.3.21", 36 | "babel-eslint": "^4.1.6", 37 | "babel-jest": "^6.0.1", 38 | "babel-loader": "^6.2.0", 39 | "babel-preset-es2015": "^6.3.13", 40 | "babel-preset-react": "^6.3.13", 41 | "babel-preset-stage-2": "^6.3.13", 42 | "eslint": "^1.10.3", 43 | "eslint-plugin-react": "^3.12.0", 44 | "express": "^4.13.3", 45 | "jest-cli": "^0.8.2", 46 | "react-addons-test-utils": "^0.14.3", 47 | "webpack": "^1.12.9" 48 | }, 49 | "jest": { 50 | "testPathDirs": [ 51 | "./src" 52 | ], 53 | "scriptPreprocessor": "/node_modules/babel-jest", 54 | "unmockedModulePathPatterns": [ 55 | "/node_modules/react", 56 | "/node_modules/react-dom", 57 | "/node_modules/react-addons-test-utils", 58 | "/node_modules/fbjs" 59 | ] 60 | }, 61 | "dependencies": { 62 | "react": "^0.14.3", 63 | "react-dom": "^0.14.3", 64 | "shallowequal": "^0.2.2" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/components/__tests__/integration-test.js: -------------------------------------------------------------------------------- 1 | jest.dontMock('shallowequal'); 2 | jest.dontMock('../pulldown'); 3 | jest.dontMock('../pulldown-stage'); 4 | 5 | import React from 'react'; 6 | import TestUtils from 'react-addons-test-utils'; 7 | 8 | const Pulldown = require('../pulldown').default; 9 | const PulldownStage = require('../pulldown-stage').default; 10 | 11 | describe('Pulldown', () => { 12 | it('updates its height when the current stage height changes', () => { 13 | const Parent = React.createClass({ 14 | getInitialState() { 15 | return { 16 | height: 100, 17 | }; 18 | }, 19 | render() { 20 | const {height} = this.state; 21 | return ( 22 | 26 | 30 | 31 | ); 32 | }, 33 | }); 34 | const parent = TestUtils.renderIntoDocument(); 35 | const pulldown = TestUtils.findRenderedDOMComponentWithClass(parent, 'pulldown'); 36 | 37 | expect(pulldown.style.height).toBe('100px'); 38 | parent.setState({ 39 | height: 200, 40 | }); 41 | expect(pulldown.style.height).toBe('200px'); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/components/__tests__/pulldown-close-test.js: -------------------------------------------------------------------------------- 1 | jest.dontMock('../pulldown-close'); 2 | 3 | import React from 'react'; 4 | import TestUtils from 'react-addons-test-utils'; 5 | 6 | const PulldownClose = require('../pulldown-close').default; 7 | 8 | describe('PulldownClose', () => { 9 | const className = 'className'; 10 | let close; 11 | let pulldownClose; 12 | 13 | beforeEach(() => { 14 | close = jest.genMockFunction(); 15 | 16 | const FakeContext = React.createClass({ 17 | propTypes: { 18 | children: React.PropTypes.element, 19 | }, 20 | childContextTypes: { 21 | close: React.PropTypes.func, 22 | }, 23 | getChildContext() { 24 | return { 25 | close, 26 | }; 27 | }, 28 | render() { 29 | return
{this.props.children}
; 30 | }, 31 | }); 32 | 33 | pulldownClose = TestUtils.renderIntoDocument( 34 | 35 | 36 | 37 | ); 38 | }); 39 | 40 | it('applies the class name', () => { 41 | const element = TestUtils.findRenderedDOMComponentWithTag(pulldownClose, 'button'); 42 | 43 | expect(element.className).toBe(className); 44 | }); 45 | 46 | it('calls the callback when clicked', () => { 47 | const element = TestUtils.findRenderedDOMComponentWithTag(pulldownClose, 'button'); 48 | TestUtils.Simulate.click(element); 49 | 50 | expect(close).toBeCalled(); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/components/__tests__/pulldown-goto-test.js: -------------------------------------------------------------------------------- 1 | jest.dontMock('../pulldown-goto'); 2 | 3 | import React from 'react'; 4 | import TestUtils from 'react-addons-test-utils'; 5 | 6 | const PulldownGoto = require('../pulldown-goto').default; 7 | 8 | describe('PulldownGoto', () => { 9 | const className = 'className'; 10 | const stage = 'stage'; 11 | let goTo; 12 | let pulldownGoto; 13 | 14 | beforeEach(() => { 15 | goTo = jest.genMockFunction(); 16 | 17 | const FakeContext = React.createClass({ 18 | propTypes: { 19 | children: React.PropTypes.element, 20 | }, 21 | childContextTypes: { 22 | goTo: React.PropTypes.func, 23 | }, 24 | getChildContext() { 25 | return { 26 | goTo, 27 | }; 28 | }, 29 | render() { 30 | return
{this.props.children}
; 31 | }, 32 | }); 33 | 34 | pulldownGoto = TestUtils.renderIntoDocument( 35 | 36 | 37 | 38 | ); 39 | }); 40 | 41 | it('applies the class name', () => { 42 | const element = TestUtils.findRenderedDOMComponentWithTag(pulldownGoto, 'button'); 43 | 44 | expect(element.className).toBe(className); 45 | }); 46 | 47 | it('calls the callback when clicked', () => { 48 | const element = TestUtils.findRenderedDOMComponentWithTag(pulldownGoto, 'button'); 49 | TestUtils.Simulate.click(element); 50 | 51 | expect(goTo).toBeCalledWith(stage); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /src/components/__tests__/pulldown-stage-test.js: -------------------------------------------------------------------------------- 1 | /* eslint react/no-multi-comp:0 */ 2 | 3 | jest.dontMock('shallowequal'); 4 | jest.dontMock('../pulldown-stage'); 5 | 6 | import React from 'react'; 7 | import TestUtils from 'react-addons-test-utils'; 8 | 9 | const PulldownStage = require('../pulldown-stage').default; 10 | 11 | function createFakeContext(childContext) { 12 | return React.createClass({ 13 | propTypes: { 14 | children: React.PropTypes.element, 15 | }, 16 | childContextTypes: { 17 | currentStage: React.PropTypes.object, 18 | previousStage: React.PropTypes.object, 19 | update: React.PropTypes.func, 20 | }, 21 | getChildContext() { 22 | return childContext; 23 | }, 24 | render() { 25 | return
{this.props.children}
; 26 | }, 27 | }); 28 | } 29 | 30 | describe('PulldownStage', () => { 31 | let FakeContext; 32 | let pulldownStage; 33 | 34 | describe('current stage', () => { 35 | beforeEach(() => { 36 | FakeContext = createFakeContext({ 37 | currentStage: { 38 | name: 'currentStage', 39 | }, 40 | }); 41 | 42 | pulldownStage = TestUtils.renderIntoDocument( 43 | 44 | 49 | 50 | ); 51 | }); 52 | 53 | it('applies the class modifier', () => { 54 | const element = TestUtils.scryRenderedDOMComponentsWithTag(pulldownStage, 'div')[1]; 55 | 56 | expect(element.className).toContain('className--active'); 57 | }); 58 | 59 | it('sets the top to zero', () => { 60 | const element = TestUtils.scryRenderedDOMComponentsWithTag(pulldownStage, 'div')[1]; 61 | 62 | expect(element.style.top).toBe('0px'); 63 | }); 64 | }); 65 | 66 | describe('previous stage (top)', () => { 67 | beforeEach(() => { 68 | FakeContext = createFakeContext({ 69 | currentStage: { 70 | direction: 'top', 71 | height: 100, 72 | name: 'currentStage', 73 | }, 74 | previousStage: { 75 | name: 'previousStage', 76 | }, 77 | }); 78 | 79 | pulldownStage = TestUtils.renderIntoDocument( 80 | 81 | 85 | 86 | ); 87 | }); 88 | 89 | it('sets the top to height', () => { 90 | const element = TestUtils.scryRenderedDOMComponentsWithTag(pulldownStage, 'div')[1]; 91 | 92 | expect(element.style.top).toBe('100px'); 93 | }); 94 | }); 95 | 96 | describe('previous stage (bottom)', () => { 97 | beforeEach(() => { 98 | FakeContext = createFakeContext({ 99 | currentStage: { 100 | direction: 'bottom', 101 | height: 100, 102 | name: 'currentStage', 103 | }, 104 | previousStage: { 105 | name: 'previousStage', 106 | }, 107 | }); 108 | 109 | pulldownStage = TestUtils.renderIntoDocument( 110 | 111 | 115 | 116 | ); 117 | }); 118 | 119 | it('sets the top to negative height', () => { 120 | const element = TestUtils.scryRenderedDOMComponentsWithTag(pulldownStage, 'div')[1]; 121 | 122 | expect(element.style.top).toBe('-100px'); 123 | }); 124 | }); 125 | 126 | describe('stage', () => { 127 | beforeEach(() => { 128 | FakeContext = createFakeContext({ 129 | currentStage: {}, 130 | }); 131 | 132 | pulldownStage = TestUtils.renderIntoDocument( 133 | 134 | 139 | 140 | ); 141 | }); 142 | 143 | it('applies the class name', () => { 144 | const element = TestUtils.scryRenderedDOMComponentsWithTag(pulldownStage, 'div')[1]; 145 | 146 | expect(element.className).toContain('className'); 147 | }); 148 | }); 149 | 150 | describe('stage (height > currentStage height)', () => { 151 | beforeEach(() => { 152 | FakeContext = createFakeContext({ 153 | currentStage: { 154 | height: 100, 155 | }, 156 | }); 157 | 158 | pulldownStage = TestUtils.renderIntoDocument( 159 | 160 | 165 | 166 | ); 167 | }); 168 | 169 | it('sets the top to the maximum height', () => { 170 | const element = TestUtils.scryRenderedDOMComponentsWithTag(pulldownStage, 'div')[1]; 171 | 172 | expect(element.style.top).toBe('-101px'); 173 | }); 174 | }); 175 | 176 | describe('stage (height < currentStage height)', () => { 177 | beforeEach(() => { 178 | FakeContext = createFakeContext({ 179 | currentStage: { 180 | height: 101, 181 | }, 182 | }); 183 | 184 | pulldownStage = TestUtils.renderIntoDocument( 185 | 186 | 191 | 192 | ); 193 | }); 194 | 195 | it('sets the top to the maximum height', () => { 196 | const element = TestUtils.scryRenderedDOMComponentsWithTag(pulldownStage, 'div')[1]; 197 | 198 | expect(element.style.top).toBe('-101px'); 199 | }); 200 | }); 201 | 202 | describe('stage (top)', () => { 203 | beforeEach(() => { 204 | FakeContext = createFakeContext({ 205 | currentStage: { 206 | height: 100, 207 | }, 208 | }); 209 | 210 | pulldownStage = TestUtils.renderIntoDocument( 211 | 212 | 217 | 218 | ); 219 | }); 220 | 221 | it('sets the top to the negative height', () => { 222 | const element = TestUtils.scryRenderedDOMComponentsWithTag(pulldownStage, 'div')[1]; 223 | 224 | expect(element.style.top).toBe('-100px'); 225 | }); 226 | }); 227 | 228 | describe('stage (bottom)', () => { 229 | beforeEach(() => { 230 | FakeContext = createFakeContext({ 231 | currentStage: { 232 | height: 100, 233 | }, 234 | }); 235 | 236 | pulldownStage = TestUtils.renderIntoDocument( 237 | 238 | 243 | 244 | ); 245 | }); 246 | 247 | it('sets the top to the height', () => { 248 | const element = TestUtils.scryRenderedDOMComponentsWithTag(pulldownStage, 'div')[1]; 249 | 250 | expect(element.style.top).toBe('100px'); 251 | }); 252 | }); 253 | 254 | describe('update', () => { 255 | function createParent(childContext) { 256 | FakeContext = createFakeContext(childContext); 257 | return React.createClass({ 258 | getInitialState() { 259 | return { 260 | height: 100, 261 | }; 262 | }, 263 | render() { 264 | const {height} = this.state; 265 | return ( 266 | 267 | 271 | 272 | ); 273 | }, 274 | }); 275 | } 276 | 277 | it('fires the callback if is current and props change', () => { 278 | const update = jest.genMockFunction(); 279 | const Parent = createParent({ 280 | currentStage: { 281 | name: 'a', 282 | }, 283 | update, 284 | }); 285 | const parent = TestUtils.renderIntoDocument(); 286 | parent.setState({ 287 | height: 200, 288 | }); 289 | 290 | expect(update).toBeCalled(); 291 | }); 292 | 293 | it('does not fire the callback if is current and props do not change', () => { 294 | const update = jest.genMockFunction(); 295 | const Parent = createParent({ 296 | currentStage: { 297 | name: 'a', 298 | }, 299 | update, 300 | }); 301 | const parent = TestUtils.renderIntoDocument(); 302 | parent.setState({ 303 | height: 100, 304 | }); 305 | 306 | expect(update).not.toBeCalled(); 307 | }); 308 | 309 | it('does not fire the callback if is not current', () => { 310 | const update = jest.genMockFunction(); 311 | const Parent = createParent({ 312 | currentStage: { 313 | name: 'b', 314 | height: 100, 315 | }, 316 | update, 317 | }); 318 | const parent = TestUtils.renderIntoDocument(); 319 | parent.setState({ 320 | height: 200, 321 | }); 322 | 323 | expect(update).not.toBeCalled(); 324 | }); 325 | }); 326 | }); 327 | -------------------------------------------------------------------------------- /src/components/__tests__/pulldown-test.js: -------------------------------------------------------------------------------- 1 | jest.dontMock('../pulldown'); 2 | 3 | import React from 'react'; 4 | import TestUtils from 'react-addons-test-utils'; 5 | 6 | const Pulldown = require('../pulldown').default; 7 | const PulldownStage = require('../pulldown-stage').default; 8 | 9 | describe('Pulldown', () => { 10 | const className = 'className'; 11 | const defaultStageName = 'a'; 12 | const delay = 1000; 13 | const height = 100; 14 | const nextStageName = 'b'; 15 | const onChange = jest.genMockFunction(); 16 | const onClose = jest.genMockFunction(); 17 | const onOpen = jest.genMockFunction(); 18 | let pulldown; 19 | 20 | describe('default', () => { 21 | beforeEach(() => { 22 | pulldown = TestUtils.renderIntoDocument( 23 | 31 | 32 | 33 | 34 | ); 35 | }); 36 | 37 | it('applies the class name', () => { 38 | const element = TestUtils.findRenderedDOMComponentWithTag(pulldown, 'div'); 39 | 40 | expect(element.className).toBe(className); 41 | }); 42 | 43 | it('sets the current stage object', () => { 44 | expect(pulldown.state.currentStage.name).toBe(defaultStageName); 45 | }); 46 | 47 | it('sets its height according to the current stage height', () => { 48 | const element = TestUtils.findRenderedDOMComponentWithTag(pulldown, 'div'); 49 | 50 | expect(element.style.height).toBe(`${height}px`); 51 | }); 52 | 53 | it('fires the onChange callback', () => { 54 | pulldown._goTo(nextStageName); 55 | 56 | expect(onChange).toBeCalledWith(nextStageName); 57 | }); 58 | 59 | it('fires the onClose callback', () => { 60 | pulldown._close(); 61 | 62 | expect(onClose).toBeCalled(); 63 | }); 64 | 65 | it('disappears when closed', () => { 66 | pulldown._close(); 67 | const element = TestUtils.findRenderedDOMComponentWithTag(pulldown, 'div'); 68 | 69 | expect(element.style.marginTop).toBe(`-${height}px`); 70 | }); 71 | 72 | it('does not apply the open modifier when closed', () => { 73 | pulldown._close(); 74 | const element = TestUtils.findRenderedDOMComponentWithTag(pulldown, 'div'); 75 | 76 | expect(element.className).not.toContain(`${className}--open`); 77 | }); 78 | 79 | it('fires the onOpen callback', () => { 80 | jest.runAllTimers(); 81 | 82 | expect(onOpen).toBeCalled(); 83 | }); 84 | 85 | it('appears when open', () => { 86 | jest.runAllTimers(); 87 | const element = TestUtils.findRenderedDOMComponentWithTag(pulldown, 'div'); 88 | 89 | expect(element.style.marginTop).toBe('0'); 90 | }); 91 | 92 | it('applies the open modifier when open', () => { 93 | jest.runAllTimers(); 94 | const element = TestUtils.findRenderedDOMComponentWithTag(pulldown, 'div'); 95 | 96 | expect(element.className).toContain(`${className}--open`); 97 | }); 98 | 99 | it('waits for the delay to appear', () => { 100 | const element = TestUtils.findRenderedDOMComponentWithTag(pulldown, 'div'); 101 | expect(element.style.marginTop).toBe(`-${height}px`); 102 | 103 | jest.runAllTimers(); 104 | expect(element.style.marginTop).toBe('0'); 105 | }); 106 | }); 107 | 108 | describe('fixed', () => { 109 | beforeEach(() => { 110 | pulldown = TestUtils.renderIntoDocument( 111 | 116 | 117 | 118 | ); 119 | }); 120 | 121 | it('disappears when closed', () => { 122 | pulldown._close(); 123 | const element = TestUtils.findRenderedDOMComponentWithTag(pulldown, 'div'); 124 | 125 | expect(element.style.top).toBe('-100%'); 126 | }); 127 | 128 | it('appears when open', () => { 129 | jest.runAllTimers(); 130 | const element = TestUtils.findRenderedDOMComponentWithTag(pulldown, 'div'); 131 | 132 | expect(element.style.top).toBe('0px'); 133 | }); 134 | 135 | it('waits for the delay to appear', () => { 136 | const element = TestUtils.findRenderedDOMComponentWithTag(pulldown, 'div'); 137 | expect(element.style.top).toBe('-100%'); 138 | 139 | jest.runAllTimers(); 140 | expect(element.style.top).toBe('0px'); 141 | }); 142 | }); 143 | }); 144 | -------------------------------------------------------------------------------- /src/components/pulldown-close.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const PulldownClose = React.createClass({ 4 | 5 | propTypes: { 6 | children: React.PropTypes.any, 7 | className: React.PropTypes.string, 8 | }, 9 | 10 | contextTypes: { 11 | close: React.PropTypes.func, 12 | }, 13 | 14 | _handleClick() { 15 | const {close} = this.context; 16 | 17 | close(); 18 | }, 19 | 20 | render() { 21 | const {children, className} = this.props; 22 | 23 | return ( 24 | 27 | ); 28 | }, 29 | 30 | }); 31 | 32 | export default PulldownClose; 33 | -------------------------------------------------------------------------------- /src/components/pulldown-goto.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const PulldownGoto = React.createClass({ 4 | 5 | propTypes: { 6 | children: React.PropTypes.any, 7 | className: React.PropTypes.string, 8 | stage: React.PropTypes.string.isRequired, 9 | }, 10 | 11 | contextTypes: { 12 | goTo: React.PropTypes.func, 13 | }, 14 | 15 | _handleClick() { 16 | const {goTo} = this.context; 17 | const {stage} = this.props; 18 | 19 | goTo(stage); 20 | }, 21 | 22 | render() { 23 | const {children, className} = this.props; 24 | 25 | return ( 26 | 29 | ); 30 | }, 31 | 32 | }); 33 | 34 | export default PulldownGoto; 35 | -------------------------------------------------------------------------------- /src/components/pulldown-stage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import shallowequal from 'shallowequal'; 3 | 4 | const PulldownStage = React.createClass({ 5 | 6 | propTypes: { 7 | children: React.PropTypes.any, 8 | className: React.PropTypes.string, 9 | direction: React.PropTypes.string, 10 | height: React.PropTypes.number.isRequired, 11 | name: React.PropTypes.string.isRequired, 12 | }, 13 | 14 | contextTypes: { 15 | currentStage: React.PropTypes.object, 16 | previousStage: React.PropTypes.object, 17 | update: React.PropTypes.func, 18 | }, 19 | 20 | componentDidUpdate(prevProps) { 21 | const {update} = this.context; 22 | 23 | if (this._isCurrentStage() && !shallowequal(prevProps, this.props)) { 24 | update(); 25 | } 26 | }, 27 | 28 | _isCurrentStage() { 29 | const {currentStage} = this.context; 30 | const {name} = this.props; 31 | 32 | return currentStage.name === name; 33 | }, 34 | 35 | _getDirection() { 36 | const {currentStage, previousStage} = this.context; 37 | const {direction, name} = this.props; 38 | 39 | if (previousStage && previousStage.name === name) { 40 | this.direction = currentStage.direction === 'top' ? 'bottom' : 'top'; 41 | } 42 | 43 | return this.direction || direction; 44 | }, 45 | 46 | _getHeight() { 47 | const {currentStage} = this.context; 48 | const {height} = this.props; 49 | 50 | return this._isCurrentStage() ? 0 : Math.max(height, currentStage.height); 51 | }, 52 | 53 | _getTop() { 54 | const height = this._getHeight(); 55 | 56 | return this._getDirection() === 'top' ? -height : height; 57 | }, 58 | 59 | _getStyle() { 60 | const {height} = this.props; 61 | 62 | return { 63 | height: height, 64 | position: 'absolute', 65 | top: this._getTop(), 66 | transition: 'all 500ms', 67 | width: '100%', 68 | }; 69 | }, 70 | 71 | render() { 72 | const {children} = this.props; 73 | let {className} = this.props; 74 | 75 | if (className && this._isCurrentStage()) { 76 | className += ` ${className}--active`; 77 | } 78 | 79 | return ( 80 |
81 | {children} 82 |
83 | ); 84 | }, 85 | 86 | }); 87 | 88 | export default PulldownStage; 89 | -------------------------------------------------------------------------------- /src/components/pulldown.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Pulldown = React.createClass({ 4 | 5 | propTypes: { 6 | children: React.PropTypes.any, 7 | className: React.PropTypes.string, 8 | defaultStage: React.PropTypes.string.isRequired, 9 | delay: React.PropTypes.number, 10 | fixed: React.PropTypes.bool, 11 | onChange: React.PropTypes.func, 12 | onClose: React.PropTypes.func, 13 | onOpen: React.PropTypes.func, 14 | }, 15 | 16 | childContextTypes: { 17 | close: React.PropTypes.func, 18 | currentStage: React.PropTypes.object, 19 | goTo: React.PropTypes.func, 20 | previousStage: React.PropTypes.object, 21 | update: React.PropTypes.func, 22 | }, 23 | 24 | getInitialState() { 25 | const {defaultStage, delay} = this.props; 26 | 27 | return { 28 | currentStage: this._getStage(defaultStage), 29 | open: !delay, 30 | }; 31 | }, 32 | 33 | componentDidMount() { 34 | const {delay} = this.props; 35 | 36 | this.timeout = setTimeout(this._open, delay); 37 | }, 38 | 39 | componentWillUnmount() { 40 | if (this.timeout) { 41 | clearTimeout(this.timeout); 42 | } 43 | }, 44 | 45 | getChildContext() { 46 | const {currentStage, previousStage} = this.state; 47 | 48 | return { 49 | close: this._close, 50 | currentStage: currentStage, 51 | goTo: this._goTo, 52 | previousStage: previousStage, 53 | update: this._update, 54 | }; 55 | }, 56 | 57 | _getStage(stageName) { 58 | const {children} = this.props; 59 | let currentStage; 60 | 61 | React.Children.forEach(children, child => { 62 | if (child.props.name === stageName) { 63 | currentStage = child.props; 64 | } 65 | }); 66 | 67 | return currentStage; 68 | }, 69 | 70 | _goTo(stageName) { 71 | const {onChange} = this.props; 72 | 73 | this.setState({ 74 | currentStage: this._getStage(stageName), 75 | previousStage: this.state.currentStage, 76 | }, () => { 77 | if (onChange) { 78 | onChange(stageName); 79 | } 80 | }); 81 | }, 82 | 83 | _close() { 84 | const {onClose} = this.props; 85 | 86 | this.setState({ 87 | open: false, 88 | }, onClose); 89 | }, 90 | 91 | _open() { 92 | const {onOpen} = this.props; 93 | 94 | this.setState({ 95 | open: true, 96 | }, onOpen); 97 | }, 98 | 99 | _update() { 100 | const {currentStage} = this.state; 101 | 102 | this.setState({ 103 | currentStage: this._getStage(currentStage.name), 104 | }); 105 | }, 106 | 107 | _getStyle() { 108 | const {currentStage, open} = this.state; 109 | const {fixed} = this.props; 110 | let style; 111 | 112 | if (fixed) { 113 | style = { 114 | height: currentStage.height, 115 | position: 'fixed', 116 | top: open ? 0 : '-100%', 117 | zIndex: 999, 118 | }; 119 | } else { 120 | style = { 121 | height: currentStage.height, 122 | marginTop: open ? 0 : `${-currentStage.height}px`, 123 | position: 'relative', 124 | }; 125 | } 126 | 127 | return { 128 | overflow: 'hidden', 129 | transition: 'all 500ms', 130 | width: '100%', 131 | ...style, 132 | }; 133 | }, 134 | 135 | render() { 136 | const {children} = this.props; 137 | let {className} = this.props; 138 | const {open} = this.state; 139 | 140 | if (className && open) { 141 | className += ` ${className}--open`; 142 | } 143 | 144 | return ( 145 |
146 | {children} 147 |
148 | ); 149 | }, 150 | 151 | }); 152 | 153 | export default Pulldown; 154 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import PulldownClose from './components/pulldown-close'; 2 | import PulldownGoto from './components/pulldown-goto'; 3 | import PulldownStage from './components/pulldown-stage'; 4 | import Pulldown from './components/pulldown'; 5 | 6 | export {PulldownClose, PulldownGoto, PulldownStage, Pulldown}; 7 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | module: { 3 | loaders: [{ 4 | test: /\.jsx?$/, 5 | exclude: /(node_modules|bower_components)/, 6 | loader: 'babel' 7 | }] 8 | } 9 | } 10 | --------------------------------------------------------------------------------